happy-dom
Version:
Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.
354 lines • 14.5 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const SelectorItem_js_1 = __importDefault(require("./SelectorItem.cjs"));
const SelectorCombinatorEnum_js_1 = __importDefault(require("./SelectorCombinatorEnum.cjs"));
const DOMException_js_1 = __importDefault(require("../exception/DOMException.cjs"));
/**
* Selector RegExp.
*
* Group 1: All (e.g. "*")
* Group 2: Tag name (e.g. "div")
* Group 3: ID (e.g. "#id")
* Group 4: Class (e.g. ".class")
* Group 5: Attribute name when no value (e.g. "attr1")
* Group 6: Attribute name when there is a value using apostrophe (e.g. "attr1")
* Group 7: Attribute operator when using apostrophe (e.g. "~")
* Group 8: Attribute value when using apostrophe (e.g. "value1")
* Group 9: Attribute modifier when using apostrophe (e.g. "i" or "s")
* Group 10: Attribute name when threre is a value not using apostrophe (e.g. "attr1")
* Group 11: Attribute operator when not using apostrophe (e.g. "~")
* Group 12: Attribute value when notusing apostrophe (e.g. "value1")
* Group 13: Pseudo name when arguments (e.g. "nth-child")
* Group 14: Arguments of pseudo (e.g. "2n + 1")
* Group 15: Pseudo name when no arguments (e.g. "empty")
* Group 16: Pseudo element (e.g. "::after", "::-webkit-inner-spin-button").
* Group 17: Combinator.
*/
const SELECTOR_REGEXP = /(\*)|([a-zA-Z0-9-]+)|#((?:[a-zA-Z0-9-_]|\\.)+)|\.((?:[a-zA-Z0-9-_]|\\.)+)|\[([a-zA-Z0-9-_]+)\]|\[([a-zA-Z0-9-_]+) *([~|^$*]{0,1}) *= *["']{1}([^"']*)["']{1} *(s|i){0,1}\]|\[([a-zA-Z0-9-_]+) *([~|^$*]{0,1}) *= *([^\]]*)\]|:([a-zA-Z-]+) *\(([^)]+\)?)\)|:([a-zA-Z-]+)|::([a-zA-Z-]+)|([ ,+>]*)/g;
/**
* Escaped Character RegExp.
*/
const ESCAPED_CHARACTER_REGEXP = /\\/g;
/**
* Nth Function.
*/
const NTH_FUNCTION = {
odd: (n) => (n + 1) % 2 === 0,
even: (n) => (n + 1) % 2 !== 0,
alwaysFalse: () => false
};
/**
* Space RegExp.
*/
const SPACE_REGEXP = / /g;
/**
* Simple Selector RegExp.
*
* Group 1: Tag name (e.g. "div")
* Group 2: Class (e.g. ".classA.classB")
* Group 3: ID (e.g. "#id")
*/
const SIMPLE_SELECTOR_REGEXP = /(^[a-zA-Z0-9-]+$)|(^\.[a-zA-Z0-9-_.]+$)|(^#[a-zA-Z0-9-_]+$)/;
/**
* Utility for parsing a selection string.
*/
class SelectorParser {
/**
* Parses a selector string and returns an instance of SelectorItem.
*
* @param selector Selector.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Selector item.
*/
static getSelectorItem(selector, options) {
return this.getSelectorGroups(selector, options)[0][0];
}
/**
* Parses a selector string and returns groups with SelectorItem instances.
*
* @param selector Selector.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Selector groups.
*/
static getSelectorGroups(selector, options) {
const ignoreErrors = options?.ignoreErrors;
if (selector === '*') {
return [[new SelectorItem_js_1.default({ tagName: '*', ignoreErrors })]];
}
const simpleMatch = selector.match(SIMPLE_SELECTOR_REGEXP);
if (simpleMatch) {
if (simpleMatch[1]) {
return [[new SelectorItem_js_1.default({ tagName: selector.toUpperCase(), ignoreErrors })]];
}
else if (simpleMatch[2]) {
return [
[new SelectorItem_js_1.default({ classNames: selector.replace('.', '').split('.'), ignoreErrors })]
];
}
else if (simpleMatch[3]) {
return [[new SelectorItem_js_1.default({ id: selector.replace('#', ''), ignoreErrors })]];
}
}
const regexp = new RegExp(SELECTOR_REGEXP);
let currentSelectorItem = new SelectorItem_js_1.default({
combinator: SelectorCombinatorEnum_js_1.default.descendant,
ignoreErrors
});
let currentGroup = [currentSelectorItem];
const groups = [currentGroup];
let isValid = false;
let match;
while ((match = regexp.exec(selector))) {
if (match[0]) {
isValid = true;
if (match[1]) {
currentSelectorItem.tagName = '*';
}
else if (match[2]) {
currentSelectorItem.tagName = match[2].toUpperCase();
}
else if (match[3]) {
currentSelectorItem.id = match[3].replace(ESCAPED_CHARACTER_REGEXP, '');
}
else if (match[4]) {
currentSelectorItem.classNames = currentSelectorItem.classNames || [];
currentSelectorItem.classNames.push(match[4].replace(ESCAPED_CHARACTER_REGEXP, ''));
}
else if (match[5]) {
currentSelectorItem.attributes = currentSelectorItem.attributes || [];
currentSelectorItem.attributes.push({
name: match[5].toLowerCase(),
operator: null,
value: null,
modifier: null,
regExp: null
});
}
else if (match[6] && match[8] !== undefined) {
currentSelectorItem.attributes = currentSelectorItem.attributes || [];
currentSelectorItem.attributes.push({
name: match[6].toLowerCase(),
operator: match[7] || null,
value: match[8].replace(ESCAPED_CHARACTER_REGEXP, ''),
modifier: match[9] || null,
regExp: this.getAttributeRegExp({
operator: match[7],
value: match[8],
modifier: match[9]
})
});
}
else if (match[10] && match[12] !== undefined) {
currentSelectorItem.attributes = currentSelectorItem.attributes || [];
currentSelectorItem.attributes.push({
name: match[10].toLowerCase(),
operator: match[11] || null,
value: match[12].replace(ESCAPED_CHARACTER_REGEXP, ''),
modifier: null,
regExp: this.getAttributeRegExp({ operator: match[11], value: match[12] })
});
}
else if (match[13] && match[14]) {
currentSelectorItem.pseudos = currentSelectorItem.pseudos || [];
currentSelectorItem.pseudos.push(this.getPseudo(match[13], match[14], options));
}
else if (match[15]) {
currentSelectorItem.pseudos = currentSelectorItem.pseudos || [];
currentSelectorItem.pseudos.push(this.getPseudo(match[15], null, options));
}
else if (match[16]) {
currentSelectorItem.isPseudoElement = true;
}
else if (match[17]) {
switch (match[17].trim()) {
case ',':
currentSelectorItem = new SelectorItem_js_1.default({
combinator: SelectorCombinatorEnum_js_1.default.descendant,
ignoreErrors
});
currentGroup = [currentSelectorItem];
groups.push(currentGroup);
break;
case '>':
currentSelectorItem = new SelectorItem_js_1.default({
combinator: SelectorCombinatorEnum_js_1.default.child,
ignoreErrors
});
currentGroup.push(currentSelectorItem);
break;
case '+':
currentSelectorItem = new SelectorItem_js_1.default({
combinator: SelectorCombinatorEnum_js_1.default.adjacentSibling,
ignoreErrors
});
currentGroup.push(currentSelectorItem);
break;
case '':
currentSelectorItem = new SelectorItem_js_1.default({
combinator: SelectorCombinatorEnum_js_1.default.descendant,
ignoreErrors
});
currentGroup.push(currentSelectorItem);
break;
}
}
}
else {
break;
}
}
if (!isValid) {
if (options?.ignoreErrors) {
return [];
}
throw new DOMException_js_1.default(`Invalid selector: "${selector}"`);
}
return groups;
}
/**
* Returns attribute RegExp.
*
* @param attribute Attribute.
* @param attribute.value Attribute value.
* @param attribute.operator Attribute operator.
* @param attribute.modifier Attribute modifier.
* @returns Attribute RegExp.
*/
static getAttributeRegExp(attribute) {
const modifier = attribute.modifier === 'i' ? 'i' : '';
if (!attribute.operator || !attribute.value) {
return null;
}
switch (attribute.operator) {
// [attribute~="value"] - Contains a specified word.
case '~':
return new RegExp(`[- ]${attribute.value}|${attribute.value}[- ]|^${attribute.value}$`, modifier);
// [attribute|="value"] - Starts with the specified word.
case '|':
return new RegExp(`^${attribute.value}[- ]|^${attribute.value}$`, modifier);
// [attribute^="value"] - Begins with a specified value.
case '^':
return new RegExp(`^${attribute.value}`, modifier);
// [attribute$="value"] - Ends with a specified value.
case '$':
return new RegExp(`${attribute.value}$`, modifier);
// [attribute*="value"] - Contains a specified value.
case '*':
return new RegExp(`${attribute.value}`, modifier);
default:
return null;
}
}
/**
* Returns pseudo.
*
* @param name Pseudo name.
* @param args Pseudo arguments.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Pseudo.
*/
static getPseudo(name, args, options) {
const lowerName = name.toLowerCase();
if (!args) {
return { name: lowerName, arguments: null, selectorItems: null, nthFunction: null };
}
switch (lowerName) {
case 'nth-last-child':
case 'nth-child':
const nthOfIndex = args.indexOf(' of ');
const nthFunction = nthOfIndex !== -1 ? args.substring(0, nthOfIndex) : args;
const selectorItem = nthOfIndex !== -1
? this.getSelectorItem(args.substring(nthOfIndex + 4).trim(), options)
: null;
return {
name: lowerName,
arguments: args,
selectorItems: [selectorItem],
nthFunction: this.getPseudoNthFunction(nthFunction)
};
case 'nth-of-type':
case 'nth-last-of-type':
return {
name: lowerName,
arguments: args,
selectorItems: null,
nthFunction: this.getPseudoNthFunction(args)
};
case 'not':
return {
name: lowerName,
arguments: args,
selectorItems: [this.getSelectorItem(args, options)],
nthFunction: null
};
case 'is':
case 'where':
const selectorGroups = this.getSelectorGroups(args, options);
const selectorItems = [];
for (const group of selectorGroups) {
selectorItems.push(group[0]);
}
return {
name: lowerName,
arguments: args,
selectorItems,
nthFunction: null
};
default:
return { name: lowerName, arguments: args, selectorItems: null, nthFunction: null };
}
}
/**
* Returns pseudo nth function.
*
* Based on:
* https://github.com/dperini/nwsapi/blob/master/src/nwsapi.js
*
* @param args Pseudo arguments.
* @returns Pseudo nth function.
*/
static getPseudoNthFunction(args) {
if (args === 'odd') {
return NTH_FUNCTION.odd;
}
else if (args === 'even') {
return NTH_FUNCTION.even;
}
const parts = args.replace(SPACE_REGEXP, '').split('n');
let partA = parseInt(parts[0], 10) || 0;
if (parts[0] == '-') {
partA = -1;
}
if (parts.length === 1) {
return (n) => n == partA;
}
let partB = parseInt(parts[1], 10) || 0;
if (parts[0] == '+') {
partB = 1;
}
if (partA >= 1 || partA <= -1) {
if (partA >= 1) {
if (Math.abs(partA) === 1) {
return (n) => n > partB - 1;
}
return (n) => n > partB - 1 && (n + -1 * partB) % partA === 0;
}
if (Math.abs(partA) === 1) {
return (n) => n < partB + 1;
}
return (n) => n < partB + 1 && (n + -1 * partB) % partA === 0;
}
if (parts[0]) {
return (n) => n === partB;
}
return (n) => n > partB - 1;
}
}
exports.default = SelectorParser;
//# sourceMappingURL=SelectorParser.cjs.map
;