From c0631e127a13c3f5ac4d0427dacd801aeffb73dd Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 12 May 2023 11:32:01 +0200 Subject: [PATCH] #911@minor: Adds support for "i" and "s" modifiers to attribute query selectors. --- .../src/query-selector/ISelectorAttribute.ts | 2 + .../src/query-selector/SelectorItem.ts | 50 ++-------- .../src/query-selector/SelectorParser.ts | 92 +++++++++++++++---- .../test/query-selector/QuerySelector.test.ts | 9 ++ 4 files changed, 90 insertions(+), 63 deletions(-) diff --git a/packages/happy-dom/src/query-selector/ISelectorAttribute.ts b/packages/happy-dom/src/query-selector/ISelectorAttribute.ts index 403a51376..f0bd484d7 100644 --- a/packages/happy-dom/src/query-selector/ISelectorAttribute.ts +++ b/packages/happy-dom/src/query-selector/ISelectorAttribute.ts @@ -2,4 +2,6 @@ export default interface ISelectorAttribute { name: string; operator: string | null; value: string | null; + modifier: 's' | 'i' | null; + regExp: RegExp | null; } diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index 361295d31..5fdcb6e11 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -236,49 +236,13 @@ export default class SelectorItem { priorityWeight += 10; - if (attribute.value !== null) { - if (elementAttribute.value === null) { - return null; - } - - switch (attribute.operator) { - // [attribute~="value"] - Contains a specified word. - case null: - if (attribute.value !== elementAttribute.value) { - return null; - } - break; - // [attribute~="value"] - Contains a specified word. - case '~': - if (!elementAttribute.value.split(' ').includes(attribute.value)) { - return null; - } - break; - // [attribute|="value"] - Starts with the specified word. - case '|': - if (!new RegExp(`^${attribute.value}[- ]`).test(elementAttribute.value)) { - return null; - } - break; - // [attribute^="value"] - Begins with a specified value. - case '^': - if (!elementAttribute.value.startsWith(attribute.value)) { - return null; - } - break; - // [attribute$="value"] - Ends with a specified value. - case '$': - if (!elementAttribute.value.endsWith(attribute.value)) { - return null; - } - break; - // [attribute*="value"] - Contains a specified value. - case '*': - if (!elementAttribute.value.includes(attribute.value)) { - return null; - } - break; - } + if ( + attribute.value !== null && + (elementAttribute.value === null || + (attribute.regExp && !attribute.regExp.test(elementAttribute.value)) || + (!attribute.regExp && attribute.value !== elementAttribute.value)) + ) { + return null; } } diff --git a/packages/happy-dom/src/query-selector/SelectorParser.ts b/packages/happy-dom/src/query-selector/SelectorParser.ts index 56c29e256..5195c0d7d 100644 --- a/packages/happy-dom/src/query-selector/SelectorParser.ts +++ b/packages/happy-dom/src/query-selector/SelectorParser.ts @@ -14,16 +14,17 @@ import ISelectorPseudo from './ISelectorPseudo'; * 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 name when threre is a value not using apostrophe (e.g. "attr1") - * Group 10: Attribute operator when not using apostrophe (e.g. "~") - * Group 11: Attribute value when notusing apostrophe (e.g. "value1") - * Group 12: Pseudo name when arguments (e.g. "nth-child") - * Group 13: Arguments of pseudo (e.g. "2n + 1") - * Group 14: Pseudo name when no arguments (e.g. "empty") - * Group 15: Combinator. + * 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: 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}\]|\[([a-zA-Z0-9-_]+)([~|^$*]{0,1}) *= *([^\]]*)\]|:([a-zA-Z-]+) *\(([^)]+)\)|:([a-zA-Z-]+)|([ ,+>]*)/g; + /(\*)|([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-]+)|([ ,+>]*)/g; /** * Escaped Character RegExp. @@ -117,30 +118,40 @@ export default class SelectorParser { currentSelectorItem.attributes.push({ name: match[5].toLowerCase(), operator: null, - value: 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] + value: match[8], + modifier: match[9] || null, + regExp: this.getAttributeRegExp({ + operator: match[7], + value: match[8], + modifier: match[9] + }) }); - } else if (match[9] && match[11] !== undefined) { + } else if (match[10] && match[12] !== undefined) { currentSelectorItem.attributes = currentSelectorItem.attributes || []; currentSelectorItem.attributes.push({ - name: match[9].toLowerCase(), - operator: match[10] || null, - value: match[11] + name: match[10].toLowerCase(), + operator: match[11] || null, + value: match[12], + modifier: null, + regExp: this.getAttributeRegExp({ operator: match[7], value: match[8] }) }); - } else if (match[12] && match[13]) { + } else if (match[13] && match[14]) { currentSelectorItem.pseudos = currentSelectorItem.pseudos || []; - currentSelectorItem.pseudos.push(this.getPseudo(match[12], match[13])); - } else if (match[14]) { - currentSelectorItem.pseudos = currentSelectorItem.pseudos || []; - currentSelectorItem.pseudos.push(this.getPseudo(match[14])); + currentSelectorItem.pseudos.push(this.getPseudo(match[13], match[14])); } else if (match[15]) { - switch (match[15].trim()) { + currentSelectorItem.pseudos = currentSelectorItem.pseudos || []; + currentSelectorItem.pseudos.push(this.getPseudo(match[15])); + } else if (match[16]) { + switch (match[16].trim()) { case ',': currentSelectorItem = new SelectorItem({ combinator: SelectorCombinatorEnum.descendant @@ -178,6 +189,47 @@ export default class SelectorParser { 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. + */ + private static getAttributeRegExp(attribute: { + value?: string; + operator?: string; + modifier?: string; + }): RegExp | null { + 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}[- ]`, modifier); + // [attribute|="value"] - Starts with the specified word. + case '|': + return new RegExp(`^${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. * diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index 042743a79..59b992727 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -814,6 +814,15 @@ describe('QuerySelector', () => { ).toEqual(['div.n3', 'div.n6', 'div.n9']); }); + it('Returns all elements matching "a[href]:not([href *= "javascript:" i])".', () => { + const container = document.createElement('div'); + container.innerHTML = `LinkLink`; + const elements = container.querySelectorAll('a[href]:not([href *= "javascript:" i])'); + + expect(elements.length).toBe(1); + expect(elements[0] === container.children[1]).toBe(true); + }); + it('Returns all elements matching ":nth-child(odd)".', () => { const container = document.createElement('div'); container.innerHTML = QuerySelectorNthChildHTML;