{"checkedOn":"2026-10-28","npm":{"latest":"21.0.4","published":"2026-10-20T18:05:53.295Z","gitHead":"fd163520b74d9fa146be91f7b4e4f46d6cccbe13"},"issues":[{"url":"https://github.com/capricorn86/happy-dom/issues/990","title":"`querySelectorAll` ignores `:not()` pseudo-class with multiple selectors","state":"closed","state_reason":"completed","created_at":"2023-07-14T07:05:23Z","updated_at":"2025-03-04T22:28:26Z","closed_at":"2025-03-04T22:23:19Z","body":"Enter the following code into https://npm.runkit.com/happy-dom\r\n\r\n```js\r\nvar happyDom = require(\"happy-dom\")\r\n\r\nconst window = new happyDom.Window();\r\nconst document = window.document;\r\ndocument.body.innerHTML = `<ul class=\"list\">\r\n  <li class=\"list-item\"></li>\r\n  <li class=\"list-item\"></li>\r\n  <li class=\"list-item\"></li>\r\n  <li class=\"other-item\"></li>\r\n  <li></li>\r\n</ul>`;\r\n\r\nconst lastItem = document.querySelectorAll('ul > li:not(.list-item, .other-item)');\r\n\r\nconsole.log('lastItem', lastItem.length); // prints 2 but should be 1\r\n```\r\n\r\n`querySelectorAll` will only respect the first selector inside of the `:not` pseudo-class (`.list-item`) and ignores all subsequent selectors (`.other-item`).\r\n\r\n**Expected behavior**\r\nIt respects the complete behaviour, especially comma-separated selector lists, of the [not()](https://developer.mozilla.org/en-US/docs/Web/CSS/:not) pseudo-class.\r\n"}],"pullRequests":[{"url":"https://github.com/capricorn86/happy-dom/pull/1751","title":"feat: [#990] Add support for multiple selectors in :not pseudo-class","state":"closed","merged":true,"created_at":"2025-02-27T19:12:41Z","updated_at":"2025-03-04T22:23:18Z","closed_at":"2025-03-04T22:23:18Z","merged_at":"2025-03-04T22:23:18Z","base_sha":"6dae37905ad750cb529c44e57beb4320ca06a723","head_sha":"36c2bcbf755f10dd50d6db5a9c49ee042791759a","merge_commit_sha":"6f7057d98db373f22af70045d4551cb8cecd5ad9","diff":"@@ -248,7 +248,12 @@ export default class SelectorItem {\n \t\t\tcase 'root':\n \t\t\t\treturn element[PropertySymbol.tagName] === 'HTML' ? { priorityWeight: 10 } : null;\n \t\t\tcase 'not':\n-\t\t\t\treturn !pseudo.selectorItems[0].match(element) ? { priorityWeight: 10 } : null;\n+\t\t\t\tfor (const selectorItem of pseudo.selectorItems) {\n+\t\t\t\t\tif (selectorItem.match(element)) {\n+\t\t\t\t\t\treturn null;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t\treturn { priorityWeight: 10 };\n \t\t\tcase 'nth-child':\n \t\t\t\tlet nthChildIndex = -1;\n \t\t\t\tif (pseudo.selectorItems[0] && !pseudo.selectorItems[0].match(element)) {\n@@ -316,10 +316,14 @@ export default class SelectorParser {\n \t\t\t\t\tnthFunction: this.getPseudoNthFunction(args)\n \t\t\t\t};\n \t\t\tcase 'not':\n+\t\t\t\tconst notSelectorItems = [];\n+\t\t\t\tfor (const group of this.getSelectorGroups(args, options)) {\n+\t\t\t\t\tnotSelectorItems.push(group[0]);\n+\t\t\t\t}\n \t\t\t\treturn {\n \t\t\t\t\tname: lowerName,\n \t\t\t\t\targuments: args,\n-\t\t\t\t\tselectorItems: [this.getSelectorItem(args, options)],\n+\t\t\t\t\tselectorItems: notSelectorItems,\n \t\t\t\t\tnthFunction: null\n \t\t\t\t};\n \t\t\tcase 'is':\n@@ -901,6 +901,20 @@ describe('QuerySelector', () => {\n \t\t\texpect(elements[0] === container.children[1]).toBe(true);\n \t\t});\n \n+\t\tit('Supports :not with multiple selectors within', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\tcontainer.innerHTML = `<ul class=\"list\">\n+\t\t\t\t<li class=\"list-item\"></li>\n+\t\t\t\t<li class=\"list-item\"></li>\n+\t\t\t\t<li class=\"list-item\"></li>\n+\t\t\t\t<li class=\"other-item\"></li>\n+\t\t\t\t<li></li>\n+\t\t\t</ul>`;\n+\n+\t\t\tconst lastItem = container.querySelectorAll('ul > li:not(.list-item, .other-item)');\n+\t\t\texpect(lastItem.length).toBe(1);\n+\t\t});\n+\n \t\tit('Returns all elements matching \".bar:not(.foo)\".', () => {\n \t\t\tconst container = document.createElement('div');\n \t\t\tcontainer.innerHTML = `"},{"url":"https://github.com/capricorn86/happy-dom/pull/2068","title":"fix: [#1910] Fixes issue when parsing complex query selector with has expression","state":"closed","merged":true,"created_at":"2026-02-19T00:26:46Z","updated_at":"2026-02-19T01:02:16Z","closed_at":"2026-02-19T00:45:21Z","merged_at":"2026-02-19T00:45:21Z","base_sha":"b869287d6696147dd8016aa1fd982d725619f876","head_sha":"e2180d59e9baf5af2d8747c0fad259725971f0bb","merge_commit_sha":"e6a64da42384e3dbc74607e4bb1e71b9c3477e74","diff":"@@ -301,11 +301,11 @@ export default class CSSParser {\n \t * @returns True if valid, false otherwise.\n \t */\n \tprivate validateSelectorText(selectorText: string): boolean {\n-\t\ttry {\n-\t\t\tSelectorParser.getSelectorGroups(selectorText);\n-\t\t} catch (e) {\n-\t\t\treturn false;\n-\t\t}\n-\t\treturn true;\n+\t\tconst window = this.#parentStyleSheet[PropertySymbol.window];\n+\t\treturn (\n+\t\t\tnew SelectorParser({ window, scope: window.document, ignoreErrors: true }).getSelectorGroups(\n+\t\t\t\tselectorText\n+\t\t\t).length > 0\n+\t\t);\n \t}\n }\n@@ -122,7 +122,7 @@ export default class QuerySelector {\n \t\t\tnode[PropertySymbol.nodeType] === NodeTypeEnum.documentNode\n \t\t\t\t? (<Document>node).documentElement\n \t\t\t\t: node;\n-\t\tconst groups = SelectorParser.getSelectorGroups(selector, { scope });\n+\t\tconst groups = new SelectorParser({ window, scope }).getSelectorGroups(selector);\n \t\tconst items: Element[] = [];\n \t\tconst nodeList = new NodeList<Element>(PropertySymbol.illegalConstructor, items);\n \t\tconst matchesMap: Map<string, Element> = new Map();\n@@ -267,7 +267,7 @@ export default class QuerySelector {\n \t\t\t\t? (<Document>node).documentElement\n \t\t\t\t: node;\n \n-\t\tfor (const items of SelectorParser.getSelectorGroups(selector, { scope })) {\n+\t\tfor (const items of new SelectorParser({ window, scope }).getSelectorGroups(selector)) {\n \t\t\tconst match =\n \t\t\t\tnode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode\n \t\t\t\t\t? this.findFirst(<Element>node, [<Element>node], items, cachedItem)\n@@ -375,10 +375,11 @@ export default class QuerySelector {\n \t\t\tscopeOrElement[PropertySymbol.nodeType] === NodeTypeEnum.documentNode\n \t\t\t\t? (<Document>scopeOrElement).documentElement\n \t\t\t\t: scopeOrElement;\n-\t\tfor (const items of SelectorParser.getSelectorGroups(selector, {\n-\t\t\t...options,\n+\t\tfor (const items of new SelectorParser({\n+\t\t\tignoreErrors: options?.ignoreErrors,\n+\t\t\twindow,\n \t\t\tscope\n-\t\t})) {\n+\t\t}).getSelectorGroups(selector)) {\n \t\t\tconst result = this.matchSelector(element, items.reverse(), cachedItem);\n \n \t\t\tif (result) {\n@@ -409,6 +410,11 @@ export default class QuerySelector {\n \t\tpriorityWeight = 0\n \t): ISelectorMatch | null {\n \t\tconst selectorItem = selectorItems[0];\n+\n+\t\tif (!selectorItem) {\n+\t\t\treturn null;\n+\t\t}\n+\n \t\tconst result = selectorItem.match(element);\n \n \t\tif (result) {\n@@ -437,6 +443,7 @@ export default class QuerySelector {\n \t\t\t\t\t\t}\n \t\t\t\t\t}\n \t\t\t\t\tbreak;\n+\t\t\t\tcase SelectorCombinatorEnum.none:\n \t\t\t\tcase SelectorCombinatorEnum.child:\n \t\t\t\tcase SelectorCombinatorEnum.descendant:\n \t\t\t\t\tconst parentElement = element.parentElement;\n@@ -486,7 +493,10 @@ export default class QuerySelector {\n \t\t\t}\n \t\t}\n \n-\t\tif (previousSelectorItem?.combinator === SelectorCombinatorEnum.descendant) {\n+\t\tif (\n+\t\t\tpreviousSelectorItem?.combinator === SelectorCombinatorEnum.none ||\n+\t\t\tpreviousSelectorItem?.combinator === SelectorCombinatorEnum.descendant\n+\t\t) {\n \t\t\tconst parentElement = element.parentElement;\n \t\t\tif (parentElement) {\n \t\t\t\treturn this.matchSelector(\n@@ -523,6 +533,10 @@ export default class QuerySelector {\n \t\tconst nextSelectorItem = selectorItems[1];\n \t\tlet matched: DocumentPositionAndElement[] = [];\n \n+\t\tif (!selectorItem) {\n+\t\t\treturn [];\n+\t\t}\n+\n \t\tfor (let i = 0, max = children.length; i < max; i++) {\n \t\t\tconst child = children[i];\n \t\t\tconst childrenOfChild = (<Element>child)[PropertySymbol.elementArray];\n@@ -554,6 +568,7 @@ export default class QuerySelector {\n \t\t\t\t\t\t\t\t);\n \t\t\t\t\t\t\t}\n \t\t\t\t\t\t\tbreak;\n+\t\t\t\t\t\tcase SelectorCombinatorEnum.none:\n \t\t\t\t\t\tcase SelectorCombinatorEnum.descendant:\n \t\t\t\t\t\tcase SelectorCombinatorEnum.child:\n \t\t\t\t\t\t\tmatched = matched.concat(\n@@ -579,7 +594,11 @@ export default class QuerySelector {\n \t\t\t\t}\n \t\t\t}\n \n-\t\t\tif (selectorItem.combinator === SelectorCombinatorEnum.descendant && childrenOfChild.length) {\n+\t\t\tif (\n+\t\t\t\t(selectorItem.combinator === SelectorCombinatorEnum.none ||\n+\t\t\t\t\tselectorItem.combinator === SelectorCombinatorEnum.descendant) &&\n+\t\t\t\tchildrenOfChild.length\n+\t\t\t) {\n \t\t\t\tmatched = matched.concat(\n \t\t\t\t\tthis.findAll(rootElement, childrenOfChild, selectorItems, cachedItem, position)\n \t\t\t\t);\n@@ -609,6 +628,10 @@ export default class QuerySelector {\n \t\tconst selectorItem = selectorItems[0];\n \t\tconst nextSelectorItem = selectorItems[1];\n \n+\t\tif (!selectorItem) {\n+\t\t\treturn null;\n+\t\t}\n+\n \t\tfor (let i = 0, max = children.length; i < max; i++) {\n \t\t\tconst child = children[i];\n \t\t\tconst childrenOfChild = (<Element>child)[PropertySymbol.elementArray];\n@@ -638,6 +661,7 @@ export default class QuerySelector {\n \t\t\t\t\t\t\t\t}\n \t\t\t\t\t\t\t}\n \t\t\t\t\t\t\tbreak;\n+\t\t\t\t\t\tcase SelectorCombinatorEnum.none:\n \t\t\t\t\t\tcase SelectorCombinatorEnum.descendant:\n \t\t\t\t\t\tcase SelectorCombinatorEnum.child:\n \t\t\t\t\t\t\tconst match = this.findFirst(\n@@ -671,7 +695,11 @@ export default class QuerySelector {\n \t\t\t\t}\n \t\t\t}\n \n-\t\t\tif (selectorItem.combinator === SelectorCombinatorEnum.descendant && childrenOfChild.length) {\n+\t\t\tif (\n+\t\t\t\t(selectorItem.combinator === SelectorCombinatorEnum.none ||\n+\t\t\t\t\tselectorItem.combinator === SelectorCombinatorEnum.descendant) &&\n+\t\t\t\tchildrenOfChild.length\n+\t\t\t) {\n \t\t\t\tconst match = this.findFirst(\n \t\t\t\t\trootElement,\n \t\t\t\t\tchildrenOfChild,\n@@ -1,4 +1,5 @@\n enum SelectorCombinatorEnum {\n+\tnone = 'none',\n \tdescendant = 'descendant',\n \tchild = 'child',\n \tadjacentSibling = 'adjacentSibling',\n@@ -47,15 +47,18 @@ export default class SelectorItem {\n \t\tcombinator?: SelectorCombinatorEnum;\n \t\tignoreErrors?: boolean;\n \t}) {\n-\t\tthis.root = options?.scope ? options.scope[PropertySymbol.ownerDocument].documentElement : null;\n+\t\tthis.root =\n+\t\t\toptions?.scope?.[PropertySymbol.ownerDocument]?.documentElement ||\n+\t\t\toptions?.scope?.[PropertySymbol.window].document?.documentElement ||\n+\t\t\tnull;\n \t\tthis.scope = options?.scope || null;\n \t\tthis.tagName = options?.tagName || null;\n \t\tthis.id = options?.id || null;\n \t\tthis.classNames = options?.classNames || null;\n \t\tthis.attributes = options?.attributes || null;\n \t\tthis.pseudos = options?.pseudos || null;\n \t\tthis.isPseudoElement = options?.isPseudoElement || false;\n-\t\tthis.combinator = options?.combinator || SelectorCombinatorEnum.descendant;\n+\t\tthis.combinator = options?.combinator || SelectorCombinatorEnum.none;\n \t\tthis.ignoreErrors = options?.ignoreErrors || false;\n \t}\n \n@@ -1,33 +1,56 @@\n import SelectorItem from './SelectorItem.js';\n import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';\n-import DOMException from '../exception/DOMException.js';\n import type ISelectorPseudo from './ISelectorPseudo.js';\n import type Element from '../nodes/element/Element.js';\n import type DocumentFragment from '../nodes/document-fragment/DocumentFragment.js';\n+import type BrowserWindow from '../window/BrowserWindow.js';\n+import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js';\n+\n+/**\n+ * Selector group RegExp.\n+ *\n+ * Group 1: Combinator (\" \", \",\", \"+\", \">\", \"̣~\")\n+ * Group 2: Parentheses or brackets.\n+ */\n+const SELECTOR_GROUP_REGEXP = /(\\s*[\\s,+>~]\\s*)|([\\[\\]\\(\\)\"'])/gm;\n \n /**\n  * Selector RegExp.\n  *\n  * Group 1: All (e.g. \"*\")\n  * Group 2: Tag name (e.g. \"div\")\n  * Group 3: ID (e.g. \"#id\")\n- * Group 4: Class (e.g. \".class\")\n- * Group 5: Attribute name when no value (e.g. \"attr1\")\n- * Group 6: Attribute name when there is a value using apostrophe (e.g. \"attr1\")\n- * Group 7: Attribute operator when using apostrophe (e.g. \"~\")\n- * Group 8: Attribute value when using apostrophe (e.g. \"value1\")\n- * Group 9: Attribute modifier when using apostrophe (e.g. \"i\" or \"s\")\n- * Group 10: Attribute name when threre is a value not using apostrophe (e.g. \"attr1\")\n- * Group 11: Attribute operator when not using apostrophe (e.g. \"~\")\n- * Group 12: Attribute value when notusing apostrophe (e.g. \"value1\")\n- * Group 13: Pseudo name when arguments (e.g. \"nth-child\")\n- * Group 14: Arguments of pseudo (e.g. \"2n + 1\")\n- * Group 15: Pseudo name when no arguments (e.g. \"empty\")\n- * Group 16: Pseudo element (e.g. \"::after\", \"::-webkit-inner-spin-button\").\n- * Group 17: Combinator.\n+ * Group 4: ID capture characters (e.g. \"\\:\") (should be ignored)\n+ * Group 5: Class (e.g. \".class\")\n+ * Group 6: Class capture characters (e.g. \"r\") (should be ignored)\n+ * Group 7: Attribute name when no value (e.g. \"attr1\")\n+ * Group 8: Attribute name capture characters (e.g. \"t\") (should be ignored)\n+ * Group 9: Attribute name when there is a value using apostrophe (e.g. \"attr1\")\n+ * Group 10: Attribute name capture characters (e.g. \"t\") (should be ignored)\n+ * Group 11: Attribute operator when using apostrophe (e.g. \"~\")\n+ * Group 12: Attribute value including apostrophes (e.g. \"'value1'\")\n+ * Group 13: Attribute value when using double apostrophes (e.g. \"value1\")\n+ * Group 14: Attribute value when using single apostrophes (e.g. \"value1\")\n+ * Group 15: Attribute modifier when using apostrophe (e.g. \"i\" or \"s\")\n+ * Group 16: Attribute name when there is a value not using apostrophe (e.g. \"attr1\")\n+ * Group 17: Attribute name capture characters (e.g. \"t\") (should be ignored)\n+ * Group 18: Attribute operator when not using apostrophe (e.g. \"~\")\n+ * Group 19: Attribute value when not using apostrophe (e.g. \"value1\")\n+ * Group 20: Attribute value capture characters (e.g. \"s\") (should be ignored)\n+ * Group 21: Pseudo name when arguments (e.g. \"nth-child\")\n+ * Group 22: Pseudo name when no arguments (e.g. \"empty\")\n+ * Group 23: Pseudo element (e.g. \"::after\", \"::-webkit-inner-spin-button\").\n  */\n const SELECTOR_REGEXP =\n-\t/(\\*)|([a-zA-Z0-9\\u00A0-\\uFFFF-]+)|#((?:[a-zA-Z0-9\\u00A0-\\uFFFF_-]|\\\\.)+)|\\.((?:[a-zA-Z0-9\\u00A0-\\uFFFF_-]|\\\\.)+)|\\[([a-zA-Z0-9-_\\\\:]+)\\]|\\[([a-zA-Z0-9-_\\\\:]+)\\s*([~|^$*]{0,1})\\s*=\\s*[\"']{1}([^\"']*)[\"']{1}\\s*(s|i){0,1}\\]|\\[([a-zA-Z0-9-_]+)\\s*([~|^$*]{0,1})\\s*=\\s*([^\\]]*)\\]|:([a-zA-Z-]+)\\s*\\(((?:[^()]|\\[[^\\]]*\\]|\\([^()]*\\))*)\\){0,1}|:([a-zA-Z-]+)|::([a-zA-Z-]+)|([\\s,+>~]*)/gm;\n+\t/(\\*)|([a-zA-Z0-9\\u00A0-\\uFFFF-]+)|#(([a-zA-Z0-9\\u00A0-\\uFFFF_-]|\\\\.)+)|\\.(([a-zA-Z0-9\\u00A0-\\uFFFF_-]|\\\\.)+)|\\[(([a-zA-Z0-9-_]|\\\\.)+)\\]|\\[(([a-zA-Z0-9-_]|\\\\.)+)\\s*([~|^$*]{0,1})\\s*=\\s*(\"([^\"]*)\"|'([^']*)')\\s*(s|i){0,1}\\]|\\[(([a-zA-Z0-9-_]|\\\\.)+)\\s*([~|^$*]{0,1})\\s*=\\s*(([a-zA-Z0-9\\u00A0-\\uFFFF_¤£-]|\\\\.)+)\\]|:([a-zA-Z-]+)\\s*\\(.+\\)|:([a-zA-Z-]+)|::([a-zA-Z-]+)/gm;\n+\n+/**\n+ * Selector pseudo RegExp.\n+ *\n+ * Group 1: Pseudo name (e.g. \"nth-child\")\n+ * Group 2: Parentheses or brackets.\n+ */\n+const SELECTOR_PSEUDO_REGEXP = /:([a-zA-Z-]+)|([()])/gm;\n \n /**\n  * Escaped Character RegExp.\n@@ -53,206 +76,368 @@ const NTH_FUNCTION = {\n  */\n const SPACE_REGEXP = / /g;\n \n-/**\n- * Simple Selector RegExp.\n- *\n- * Group 1: Tag name (e.g. \"div\")\n- * Group 2: Class (e.g. \".classA.classB\")\n- * Group 3: ID (e.g. \"#id\")\n- */\n-const SIMPLE_SELECTOR_REGEXP =\n-\t/(^[a-zA-Z0-9\\u00A0-\\uFFFF-]+$)|(^\\.[a-zA-Z0-9\\u00A0-\\uFFFF_.]+$)|(^#[a-zA-Z0-9\\u00A0-\\uFFFF_-]+$)/;\n-\n /**\n  * Utility for parsing a selection string.\n  */\n export default class SelectorParser {\n+\tprivate window: BrowserWindow;\n+\tprivate scope: Element | DocumentFragment;\n+\tprivate ignoreErrors: boolean;\n+\n+\t/**\n+\t *\n+\t * @param options\n+\t * @param options.window\n+\t * @param options.scope\n+\t * @param options.ignoreErrors\n+\t */\n+\tconstructor(options: {\n+\t\twindow: BrowserWindow;\n+\t\tscope: Element | DocumentFragment;\n+\t\tignoreErrors?: boolean;\n+\t}) {\n+\t\tthis.window = options.window;\n+\t\tthis.scope = options.scope;\n+\t\tthis.ignoreErrors = options.ignoreErrors ?? false;\n+\t}\n \t/**\n \t * Parses a selector string and returns an instance of SelectorItem.\n \t *\n+\t * @param window Window.\n \t * @param selector Selector.\n \t * @param options Options.\n \t * @param [options.scope] Scope.\n \t * @param [options.ignoreErrors] Ignores errors.\n \t * @returns Selector item.\n \t */\n-\tpublic static getSelectorItem(\n-\t\tselector: string,\n-\t\toptions?: {\n-\t\t\tscope?: Element | DocumentFragment;\n-\t\t\tignoreErrors?: boolean;\n-\t\t}\n-\t): SelectorItem {\n-\t\treturn this.getSelectorGroups(selector, options)[0][0];\n+\tpublic getSelectorItem(selector: string): SelectorItem {\n+\t\treturn this.getSelectorGroups(selector)[0][0];\n \t}\n \n \t/**\n-\t * Parses a selector string and returns groups with SelectorItem instances.\n+\t * Parses a selector string and returns instances of SelectorItem.\n \t *\n+\t * @param window Window.\n \t * @param selector Selector.\n \t * @param options Options.\n \t * @param [options.scope] Scope.\n \t * @param [options.ignoreErrors] Ignores errors.\n \t * @returns Selector groups.\n \t */\n-\tpublic static getSelectorGroups(\n-\t\tselector: string,\n-\t\toptions?: {\n-\t\t\tscope?: Element | DocumentFragment;\n-\t\t\tignoreErrors?: boolean;\n-\t\t}\n-\t): Array<Array<SelectorItem>> {\n+\tpublic getSelectorGroups(selector: string): Array<Array<SelectorItem>> {\n \t\tselector = selector.trim();\n-\t\tconst ignoreErrors = options?.ignoreErrors;\n-\t\tconst scope = options?.scope;\n-\n-\t\tif (selector === '*') {\n-\t\t\treturn [[new SelectorItem({ scope, tagName: '*', ignoreErrors })]];\n-\t\t}\n-\n-\t\tconst simpleMatch = selector.match(SIMPLE_SELECTOR_REGEXP);\n-\n-\t\tif (simpleMatch) {\n-\t\t\tif (simpleMatch[1]) {\n-\t\t\t\treturn [[new SelectorItem({ scope, tagName: selector.toUpperCase(), ignoreErrors })]];\n-\t\t\t} else if (simpleMatch[2]) {\n-\t\t\t\treturn [\n-\t\t\t\t\t[\n-\t\t\t\t\t\tnew SelectorItem({\n-\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\tclassNames: selector.replace('.', '').split('.'),\n-\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t})\n-\t\t\t\t\t]\n-\t\t\t\t];\n-\t\t\t} else if (simpleMatch[3]) {\n-\t\t\t\treturn [[new SelectorItem({ scope, id: selector.replace('#', ''), ignoreErrors })]];\n-\t\t\t}\n-\t\t}\n-\n-\t\tconst regexp = new RegExp(SELECTOR_REGEXP);\n-\t\tlet currentSelectorItem: SelectorItem = new SelectorItem({\n-\t\t\tscope,\n-\t\t\tcombinator: SelectorCombinatorEnum.descendant,\n-\t\t\tignoreErrors\n-\t\t});\n-\t\tlet currentGroup: SelectorItem[] = [currentSelectorItem];\n+\t\tlet currentGroup: Array<SelectorItem> = [];\n \t\tconst groups: Array<Array<SelectorItem>> = [currentGroup];\n-\t\tlet isValid = false;\n-\t\tlet match;\n-\n-\t\twhile ((match = regexp.exec(selector))) {\n-\t\t\tif (match[0]) {\n-\t\t\t\tisValid = true;\n-\n-\t\t\t\tif (match[1]) {\n-\t\t\t\t\tcurrentSelectorItem.tagName = '*';\n-\t\t\t\t} else if (match[2]) {\n-\t\t\t\t\tcurrentSelectorItem.tagName = match[2].toUpperCase();\n-\t\t\t\t} else if (match[3]) {\n-\t\t\t\t\tcurrentSelectorItem.id = match[3].replace(ESCAPED_CHARACTER_REGEXP, '');\n-\t\t\t\t} else if (match[4]) {\n-\t\t\t\t\tcurrentSelectorItem.classNames = currentSelectorItem.classNames || [];\n-\t\t\t\t\tcurrentSelectorItem.classNames.push(match[4].replace(ESCAPED_CHARACTER_REGEXP, ''));\n-\t\t\t\t} else if (match[5]) {\n-\t\t\t\t\tcurrentSelectorItem.attributes = currentSelectorItem.attributes || [];\n-\t\t\t\t\tcurrentSelectorItem.attributes.push({\n-\t\t\t\t\t\tname: match[5],\n-\t\t\t\t\t\toperator: null,\n-\t\t\t\t\t\tvalue: null,\n-\t\t\t\t\t\tmodifier: null,\n-\t\t\t\t\t\tregExp: null\n-\t\t\t\t\t});\n-\t\t\t\t} else if (match[6] && match[8] !== undefined) {\n-\t\t\t\t\tcurrentSelectorItem.attributes = currentSelectorItem.attributes || [];\n-\t\t\t\t\tcurrentSelectorItem.attributes.push({\n-\t\t\t\t\t\tname: match[6],\n-\t\t\t\t\t\toperator: match[7] || null,\n-\t\t\t\t\t\tvalue: match[8].replace(ESCAPED_CHARACTER_REGEXP, ''),\n-\t\t\t\t\t\tmodifier: <'s'>match[9] || null,\n-\t\t\t\t\t\tregExp: this.getAttributeRegExp({\n-\t\t\t\t\t\t\toperator: match[7],\n-\t\t\t\t\t\t\tvalue: match[8],\n-\t\t\t\t\t\t\tmodifier: match[9]\n-\t\t\t\t\t\t})\n-\t\t\t\t\t});\n-\t\t\t\t} else if (match[10] && match[12] !== undefined) {\n-\t\t\t\t\tcurrentSelectorItem.attributes = currentSelectorItem.attributes || [];\n-\t\t\t\t\tcurrentSelectorItem.attributes.push({\n-\t\t\t\t\t\tname: match[10],\n-\t\t\t\t\t\toperator: match[11] || null,\n-\t\t\t\t\t\tvalue: match[12].replace(ESCAPED_CHARACTER_REGEXP, ''),\n-\t\t\t\t\t\tmodifier: null,\n-\t\t\t\t\t\tregExp: this.getAttributeRegExp({ operator: match[11], value: match[12] })\n-\t\t\t\t\t});\n-\t\t\t\t} else if (match[13] && match[14]) {\n-\t\t\t\t\tcurrentSelectorItem.pseudos = currentSelectorItem.pseudos || [];\n-\t\t\t\t\tcurrentSelectorItem.pseudos.push(this.getPseudo(match[13], match[14], options));\n-\t\t\t\t} else if (match[15]) {\n-\t\t\t\t\tcurrentSelectorItem.pseudos = currentSelectorItem.pseudos || [];\n-\t\t\t\t\tcurrentSelectorItem.pseudos.push(this.getPseudo(match[15], null, options));\n-\t\t\t\t} else if (match[16]) {\n-\t\t\t\t\tcurrentSelectorItem.isPseudoElement = true;\n-\t\t\t\t} else if (match[17]) {\n-\t\t\t\t\tswitch (match[17].trim()) {\n+\t\tconst regExp = new RegExp(SELECTOR_GROUP_REGEXP);\n+\t\tconst depth = {\n+\t\t\tparentheses: 0,\n+\t\t\tbrackets: 0,\n+\t\t\tdoubleApostrophe: 0,\n+\t\t\tsingleApostrophe: 0\n+\t\t};\n+\t\tconst name = this.scope.nodeType === NodeTypeEnum.documentNode ? 'Document' : 'Element';\n+\t\tconst error = new this.window.SyntaxError(\n+\t\t\t`Failed to execute 'querySelectorAll' on '${name}': '${selector}' is not a valid selector.`\n+\t\t);\n+\t\tlet match: null | RegExpExecArray = null;\n+\t\tlet lastIndex = 0;\n+\t\tlet selectorItem: SelectorItem | null = null;\n+\t\tlet combinator: SelectorCombinatorEnum = SelectorCombinatorEnum.none;\n+\n+\t\twhile ((match = regExp.exec(selector))) {\n+\t\t\tif (match[1]) {\n+\t\t\t\t// Matches combinator (\" \", \",\", \"+\", \">\", \"̣~\")\n+\t\t\t\t// We should ignore combinators that are inside parentheses, brackets or apostrophes\n+\n+\t\t\t\tif (\n+\t\t\t\t\tdepth.parentheses === 0 &&\n+\t\t\t\t\tdepth.brackets === 0 &&\n+\t\t\t\t\tdepth.singleApostrophe === 0 &&\n+\t\t\t\t\tdepth.doubleApostrophe === 0\n+\t\t\t\t) {\n+\t\t\t\t\tconst childSelector = selector.substring(lastIndex, match.index).trim();\n+\n+\t\t\t\t\tswitch (match[1].trim()) {\n \t\t\t\t\t\tcase ',':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.descendant,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup = [currentSelectorItem];\n+\t\t\t\t\t\t\tselectorItem = this.getSelectorGroupItem(childSelector, combinator);\n+\t\t\t\t\t\t\tif (!selectorItem) {\n+\t\t\t\t\t\t\t\tif (this.ignoreErrors) {\n+\t\t\t\t\t\t\t\t\treturn [];\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\tthrow error;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tcurrentGroup.push(selectorItem);\n+\t\t\t\t\t\t\tcurrentGroup = [];\n \t\t\t\t\t\t\tgroups.push(currentGroup);\n+\t\t\t\t\t\t\tcombinator = SelectorCombinatorEnum.none;\n \t\t\t\t\t\t\tbreak;\n \t\t\t\t\t\tcase '>':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.child,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup.push(currentSelectorItem);\n+\t\t\t\t\t\t\tselectorItem = this.getSelectorGroupItem(childSelector, combinator);\n+\t\t\t\t\t\t\tif (!selectorItem) {\n+\t\t\t\t\t\t\t\tif (this.ignoreErrors) {\n+\t\t\t\t\t\t\t\t\treturn [];\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\tthrow error;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tcurrentGroup.push(selectorItem);\n+\t\t\t\t\t\t\tcombinator = SelectorCombinatorEnum.child;\n \t\t\t\t\t\t\tbreak;\n \t\t\t\t\t\tcase '+':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.adjacentSibling,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup.push(currentSelectorItem);\n+\t\t\t\t\t\t\tselectorItem = this.getSelectorGroupItem(childSelector, combinator);\n+\t\t\t\t\t\t\tif (!selectorItem) {\n+\t\t\t\t\t\t\t\tif (this.ignoreErrors) {\n+\t\t\t\t\t\t\t\t\treturn [];\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\tthrow error;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tcurrentGroup.push(selectorItem);\n+\t\t\t\t\t\t\tcombinator = SelectorCombinatorEnum.adjacentSibling;\n \t\t\t\t\t\t\tbreak;\n \t\t\t\t\t\tcase '~':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.subsequentSibling,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup.push(currentSelectorItem);\n+\t\t\t\t\t\t\tselectorItem = this.getSelectorGroupItem(childSelector, combinator);\n+\t\t\t\t\t\t\tif (!selectorItem) {\n+\t\t\t\t\t\t\t\tif (this.ignoreErrors) {\n+\t\t\t\t\t\t\t\t\treturn [];\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\tthrow error;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tcurrentGroup.push(selectorItem);\n+\t\t\t\t\t\t\tcombinator = SelectorCombinatorEnum.subsequentSibling;\n \t\t\t\t\t\t\tbreak;\n \t\t\t\t\t\tcase '':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.descendant,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup.push(currentSelectorItem);\n+\t\t\t\t\t\t\tselectorItem = this.getSelectorGroupItem(childSelector, combinator);\n+\t\t\t\t\t\t\tif (!selectorItem) {\n+\t\t\t\t\t\t\t\tif (this.ignoreErrors) {\n+\t\t\t\t\t\t\t\t\treturn [];\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\tthrow error;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tcurrentGroup.push(selectorItem);\n+\t\t\t\t\t\t\tcombinator = SelectorCombinatorEnum.descendant;\n \t\t\t\t\t\t\tbreak;\n \t\t\t\t\t}\n+\t\t\t\t\tlastIndex = regExp.lastIndex;\n \t\t\t\t}\n \t\t\t} else {\n-\t\t\t\tbreak;\n+\t\t\t\t// Matches parentheses or brackets.\n+\n+\t\t\t\tswitch (match[2]) {\n+\t\t\t\t\tcase '(':\n+\t\t\t\t\t\tif (depth.singleApostrophe === 0 && depth.doubleApostrophe === 0) {\n+\t\t\t\t\t\t\tdepth.parentheses++;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\tcase ')':\n+\t\t\t\t\t\tif (depth.singleApostrophe === 0 && depth.doubleApostrophe === 0) {\n+\t\t\t\t\t\t\tdepth.parentheses--;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\tcase '[':\n+\t\t\t\t\t\tif (depth.singleApostrophe === 0 && depth.doubleApostrophe === 0) {\n+\t\t\t\t\t\t\tdepth.brackets++;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\tcase ']':\n+\t\t\t\t\t\tif (depth.singleApostrophe === 0 && depth.doubleApostrophe === 0) {\n+\t\t\t\t\t\t\tdepth.brackets--;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\tcase '\"':\n+\t\t\t\t\t\tif (depth.singleApostrophe === 0) {\n+\t\t\t\t\t\t\tdepth.doubleApostrophe = depth.doubleApostrophe === 1 ? 0 : 1;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\tcase \"'\":\n+\t\t\t\t\t\tif (depth.doubleApostrophe === 0) {\n+\t\t\t\t\t\t\tdepth.singleApostrophe = depth.singleApostrophe === 1 ? 0 : 1;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \n-\t\tif (!isValid) {\n-\t\t\tif (options?.ignoreErrors) {\n+\t\tselectorItem = this.getSelectorGroupItem(selector.substring(lastIndex), combinator);\n+\n+\t\tif (\n+\t\t\t!selectorItem ||\n+\t\t\tdepth.parentheses !== 0 ||\n+\t\t\tdepth.brackets !== 0 ||\n+\t\t\tdepth.singleApostrophe !== 0 ||\n+\t\t\tdepth.doubleApostrophe !== 0\n+\t\t) {\n+\t\t\tif (this.ignoreErrors) {\n \t\t\t\treturn [];\n \t\t\t}\n-\t\t\tthrow new DOMException(`Invalid selector: \"${selector}\"`);\n+\t\t\tthrow error;\n+\t\t}\n+\n+\t\tif (combinator === SelectorCombinatorEnum.none && currentGroup.length > 0) {\n+\t\t\tgroups.push([selectorItem]);\n+\t\t} else {\n+\t\t\tcurrentGroup.push(selectorItem);\n \t\t}\n \n \t\treturn groups;\n \t}\n \n+\t/**\n+\t * Parses a selector string and returns an SelectorItem.\n+\t *\n+\t * @param selector Selector.\n+\t * @param combinator Combinator.\n+\t * @returns Selector item.\n+\t */\n+\tprivate getSelectorGroupItem(\n+\t\tselector: string,\n+\t\tcombinator: SelectorCombinatorEnum\n+\t): SelectorItem | null {\n+\t\tselector = selector.trim();\n+\t\tconst ignoreErrors = this.ignoreErrors;\n+\t\tconst scope = this.scope;\n+\n+\t\tif (!selector) {\n+\t\t\treturn null;\n+\t\t}\n+\n+\t\tif (selector === '*') {\n+\t\t\treturn new SelectorItem({ scope, tagName: '*', ignoreErrors });\n+\t\t}\n+\n+\t\tconst regexp = new RegExp(SELECTOR_REGEXP);\n+\t\tconst selectorItem: SelectorItem = new SelectorItem({\n+\t\t\tscope,\n+\t\t\tcombinator,\n+\t\t\tignoreErrors\n+\t\t});\n+\t\tlet match;\n+\t\tlet lastIndex = 0;\n+\n+\t\twhile ((match = regexp.exec(selector))) {\n+\t\t\tif (selector.substring(lastIndex, match.index).trim() !== '') {\n+\t\t\t\treturn null;\n+\t\t\t}\n+\n+\t\t\tif (match[1]) {\n+\t\t\t\t// Matches all, e.g. \"*\"\n+\n+\t\t\t\tselectorItem.tagName = '*';\n+\t\t\t} else if (match[2]) {\n+\t\t\t\t// Matches tag name, e.g. \"div\"\n+\n+\t\t\t\tselectorItem.tagName = match[2].toUpperCase();\n+\t\t\t} else if (match[3]) {\n+\t\t\t\t// Matches ID, e.g. \"#id\"\n+\n+\t\t\t\tselectorItem.id = match[3].replace(ESCAPED_CHARACTER_REGEXP, '');\n+\t\t\t} else if (match[5]) {\n+\t\t\t\t// Matches class names, e.g. \".class1\"\n+\n+\t\t\t\tselectorItem.classNames = selectorItem.classNames || [];\n+\t\t\t\tselectorItem.classNames.push(match[5].replace(ESCAPED_CHARACTER_REGEXP, ''));\n+\t\t\t} else if (match[7]) {\n+\t\t\t\t// Matches attributes without value, e.g. [attr]\n+\n+\t\t\t\tselectorItem.attributes = selectorItem.attributes || [];\n+\t\t\t\tselectorItem.attributes.push({\n+\t\t\t\t\tname: match[7].replace(ESCAPED_CHARACTER_REGEXP, ''),\n+\t\t\t\t\toperator: null,\n+\t\t\t\t\tvalue: null,\n+\t\t\t\t\tmodifier: null,\n+\t\t\t\t\tregExp: null\n+\t\t\t\t});\n+\t\t\t} else if (match[9] && (match[13] !== undefined || match[14] !== undefined)) {\n+\t\t\t\t// Matches attributes with apostrophes, e.g. [attr='value'] or [attr=\"value\"] or [attr='value' i]\n+\n+\t\t\t\tconst value = match[13] ?? match[14];\n+\t\t\t\tselectorItem.attributes = selectorItem.attributes || [];\n+\t\t\t\tselectorItem.attributes.push({\n+\t\t\t\t\tname: match[9].replace(ESCAPED_CHARACTER_REGEXP, ''),\n+\t\t\t\t\toperator: match[11] || null,\n+\t\t\t\t\tvalue: value.replace(ESCAPED_CHARACTER_REGEXP, ''),\n+\t\t\t\t\tmodifier: <'s'>match[15] || null,\n+\t\t\t\t\tregExp: this.getAttributeRegExp({\n+\t\t\t\t\t\toperator: match[11],\n+\t\t\t\t\t\tvalue,\n+\t\t\t\t\t\tmodifier: match[15]\n+\t\t\t\t\t})\n+\t\t\t\t});\n+\t\t\t} else if (match[16] && match[19] !== undefined) {\n+\t\t\t\t// Matches attributes without apostrophes, e.g. [attr=value] or [attr=value i]\n+\n+\t\t\t\tselectorItem.attributes = selectorItem.attributes || [];\n+\t\t\t\tselectorItem.attributes.push({\n+\t\t\t\t\tname: match[16].replace(ESCAPED_CHARACTER_REGEXP, ''),\n+\t\t\t\t\toperator: match[18] || null,\n+\t\t\t\t\tvalue: match[19].replace(ESCAPED_CHARACTER_REGEXP, ''),\n+\t\t\t\t\tmodifier: null,\n+\t\t\t\t\tregExp: this.getAttributeRegExp({ operator: match[18], value: match[19] })\n+\t\t\t\t});\n+\t\t\t} else if (match[21]) {\n+\t\t\t\t// Matches pseudo selectors with arguments, e.g. \":nth-child(2n+1)\" or \":not(.class)\"\n+\n+\t\t\t\tconst pseudoRegExp = new RegExp(SELECTOR_PSEUDO_REGEXP);\n+\t\t\t\tlet pseudoMatch: null | RegExpExecArray = null;\n+\t\t\t\tlet name: string | null = null;\n+\t\t\t\tlet depth = 0;\n+\t\t\t\tlet pseudoStartIndex = -1;\n+\n+\t\t\t\twhile ((pseudoMatch = pseudoRegExp.exec(match[0]))) {\n+\t\t\t\t\tif (pseudoMatch[1]) {\n+\t\t\t\t\t\tif (depth === 0) {\n+\t\t\t\t\t\t\tname = pseudoMatch[1];\n+\t\t\t\t\t\t}\n+\t\t\t\t\t} else if (pseudoMatch[2]) {\n+\t\t\t\t\t\tif (pseudoMatch[2] === '(') {\n+\t\t\t\t\t\t\tif (depth === 0) {\n+\t\t\t\t\t\t\t\tpseudoStartIndex = pseudoRegExp.lastIndex;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tdepth++;\n+\t\t\t\t\t\t} else if (pseudoMatch[2] === ')') {\n+\t\t\t\t\t\t\tdepth--;\n+\n+\t\t\t\t\t\t\tif (depth < 0) {\n+\t\t\t\t\t\t\t\t// More closing parentheses than opening parentheses, invalid selector\n+\t\t\t\t\t\t\t\treturn null;\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\tif (depth === 0) {\n+\t\t\t\t\t\t\t\t// Missing start parenthesis or name for pseudo selector, invalid selector\n+\t\t\t\t\t\t\t\tif (pseudoStartIndex === -1 || !name) {\n+\t\t\t\t\t\t\t\t\treturn null;\n+\t\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t\tselectorItem.pseudos = selectorItem.pseudos || [];\n+\t\t\t\t\t\t\t\tselectorItem.pseudos.push(\n+\t\t\t\t\t\t\t\t\tthis.getPseudo(name, match[0].substring(pseudoStartIndex, pseudoMatch.index))\n+\t\t\t\t\t\t\t\t);\n+\t\t\t\t\t\t\t\tname = null;\n+\t\t\t\t\t\t\t\tpseudoStartIndex = -1;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t} else if (match[22]) {\n+\t\t\t\t// Matches pseudo selectors without arguments, e.g. \":empty\" or \":checked\"\n+\n+\t\t\t\tselectorItem.pseudos = selectorItem.pseudos || [];\n+\t\t\t\tselectorItem.pseudos.push(this.getPseudo(match[22], null));\n+\t\t\t} else if (match[23]) {\n+\t\t\t\t// Matches pseudo elements, e.g. \"::after\" or \"::-webkit-inner-spin-button\"\n+\n+\t\t\t\tselectorItem.isPseudoElement = true;\n+\t\t\t}\n+\n+\t\t\tlastIndex = regexp.lastIndex;\n+\t\t}\n+\n+\t\t// If there are any characters left in the selector that were not matched, the selector is invalid.\n+\t\tif (lastIndex < selector.length) {\n+\t\t\treturn null;\n+\t\t}\n+\n+\t\treturn selectorItem;\n+\t}\n+\n \t/**\n \t * Returns attribute RegExp.\n \t *\n@@ -262,7 +447,7 @@ export default class SelectorParser {\n \t * @param attribute.modifier Attribute modifier.\n \t * @returns Attribute RegExp.\n \t */\n-\tprivate static getAttributeRegExp(attribute: {\n+\tprivate getAttributeRegExp(attribute: {\n \t\tvalue?: string;\n \t\toperator?: string;\n \t\tmodifier?: string;\n@@ -302,19 +487,9 @@ export default class SelectorParser {\n \t *\n \t * @param name Pseudo name.\n \t * @param args Pseudo arguments.\n-\t * @param [options] Options.\n-\t * @param [options.scope] Scope.\n-\t * @param [options.ignoreErrors] Ignores errors.\n \t * @returns Pseudo.\n \t */\n-\tprivate static getPseudo(\n-\t\tname: string,\n-\t\targs: string | null | undefined,\n-\t\toptions?: {\n-\t\t\tscope?: Element | DocumentFragment;\n-\t\t\tignoreErrors?: boolean;\n-\t\t}\n-\t): ISelectorPseudo {\n+\tprivate getPseudo(name: string, args: string | null | undefined): ISelectorPseudo {\n \t\tconst lowerName = name.toLowerCase();\n \n \t\tif (args) {\n@@ -331,9 +506,7 @@ export default class SelectorParser {\n \t\t\t\tconst nthOfIndex = args.indexOf(' of ');\n \t\t\t\tconst nthFunction = nthOfIndex !== -1 ? args.substring(0, nthOfIndex) : args;\n \t\t\t\tconst selectorItem =\n-\t\t\t\t\tnthOfIndex !== -1\n-\t\t\t\t\t\t? this.getSelectorItem(args.substring(nthOfIndex + 4).trim(), options)\n-\t\t\t\t\t\t: null;\n+\t\t\t\t\tnthOfIndex !== -1 ? this.getSelectorItem(args.substring(nthOfIndex + 4).trim()) : null;\n \t\t\t\treturn {\n \t\t\t\t\tname: lowerName,\n \t\t\t\t\targuments: args,\n@@ -350,8 +523,10 @@ export default class SelectorParser {\n \t\t\t\t};\n \t\t\tcase 'not':\n \t\t\t\tconst notSelectorItems = [];\n-\t\t\t\tfor (const group of this.getSelectorGroups(args, options)) {\n-\t\t\t\t\tnotSelectorItems.push(group[0]);\n+\t\t\t\tfor (const group of this.getSelectorGroups(args)) {\n+\t\t\t\t\tif (group[0]) {\n+\t\t\t\t\t\tnotSelectorItems.push(group[0]);\n+\t\t\t\t\t}\n \t\t\t\t}\n \t\t\t\treturn {\n \t\t\t\t\tname: lowerName,\n@@ -362,8 +537,10 @@ export default class SelectorParser {\n \t\t\tcase 'is':\n \t\t\tcase 'where':\n \t\t\t\tconst selectorItems = [];\n-\t\t\t\tfor (const group of this.getSelectorGroups(args, options)) {\n-\t\t\t\t\tselectorItems.push(group[0]);\n+\t\t\t\tfor (const group of this.getSelectorGroups(args)) {\n+\t\t\t\t\tif (group[0]) {\n+\t\t\t\t\t\tselectorItems.push(group[0]);\n+\t\t\t\t\t}\n \t\t\t\t}\n \t\t\t\treturn {\n \t\t\t\t\tname: lowerName,\n@@ -382,8 +559,10 @@ export default class SelectorParser {\n \t\t\t\t\t} else if (args[0] === '>') {\n \t\t\t\t\t\tnewArgs = args.replace('>', '');\n \t\t\t\t\t}\n-\t\t\t\t\tfor (const group of this.getSelectorGroups(newArgs, options)) {\n-\t\t\t\t\t\thasSelectorItems.push(group[0]);\n+\t\t\t\t\tfor (const group of this.getSelectorGroups(newArgs)) {\n+\t\t\t\t\t\tif (group[0]) {\n+\t\t\t\t\t\t\thasSelectorItems.push(group[0]);\n+\t\t\t\t\t\t}\n \t\t\t\t\t}\n \t\t\t\t}\n \n@@ -409,7 +588,7 @@ export default class SelectorParser {\n \t * @param args Pseudo arguments.\n \t * @returns Pseudo nth function.\n \t */\n-\tprivate static getPseudoNthFunction(args?: string): ((n: number) => boolean) | null {\n+\tprivate getPseudoNthFunction(args?: string): ((n: number) => boolean) | null {\n \t\tif (!args) {\n \t\t\treturn null;\n \t\t}\n@@ -1,6 +1,5 @@\n import type ShadowRoot from '../src/nodes/shadow-root/ShadowRoot.js';\n import HTMLElement from '../src/nodes/html-element/HTMLElement.js';\n-import CSSStyleSheet from '../src/css/CSSStyleSheet.js';\n \n /**\n  * CustomElement test class.\n@@ -23,7 +22,7 @@ export default class AdoptedStyleSheetCustomElement extends HTMLElement {\n \t\tthis.internalShadowRoot = this.attachShadow({\n \t\t\tmode: AdoptedStyleSheetCustomElement.shadowRootMode\n \t\t});\n-\t\tconst styleSheet = new CSSStyleSheet();\n+\t\tconst styleSheet = new this.ownerDocument.defaultView!.CSSStyleSheet();\n \t\tstyleSheet.replaceSync(`\n             :host {\n                 display: block;\n@@ -72,7 +71,7 @@ export default class AdoptedStyleSheetCustomElement extends HTMLElement {\n                 </span>\n                 <span class=\"children\">${Array.from(this.childNodes)\n \t\t\t\t\t\t\t\t\t.map(\n-\t\t\t\t\t\t\t\t\t\t(child) =>\n+\t\t\t\t\t\t\t\t\t\t(child: any) =>\n \t\t\t\t\t\t\t\t\t\t\t'#' + child['nodeType'] + (child['tagName'] || '') + child.textContent\n \t\t\t\t\t\t\t\t\t)\n \t\t\t\t\t\t\t\t\t.join(', ')}</span>\n@@ -7,12 +7,19 @@ import type CSSKeyframeRule from '../../src/css/rules/CSSKeyframeRule.js';\n import type CSSKeyframesRule from '../../src/css/rules/CSSKeyframesRule.js';\n import type CSSContainerRule from '../../src/css/rules/CSSContainerRule.js';\n import type CSSSupportsRule from '../../src/css/rules/CSSSupportsRule.js';\n-import { describe, it, expect } from 'vitest';\n+import { describe, it, expect, beforeEach } from 'vitest';\n+import type BrowserWindow from '../../src/window/BrowserWindow.js';\n+import Window from '../../src/window/Window.js';\n \n describe('CSSParser', () => {\n+\tlet window: BrowserWindow;\n+\tbeforeEach(() => {\n+\t\twindow = new Window();\n+\t});\n+\n \tdescribe('parseFromString()', () => {\n \t\tit('Parses CSS into an Array of CSSRule.', () => {\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(CSSParserInput);\n \n \t\t\texpect(cssRules.length).toBe(11);\n@@ -227,7 +234,7 @@ describe('CSSParser', () => {\n                     src: url(\"~react-native-vector-icons/Fonts/Ionicons.ttf\");\n                 }\n             `;\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -247,7 +254,7 @@ describe('CSSParser', () => {\n                 }\n             `;\n \n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -265,7 +272,7 @@ describe('CSSParser', () => {\n \t\t\tconst css =\n \t\t\t\t'@media (forced-colors: active) { @media screen and (max-width: 36rem) { .foo { height: 0.5rem; } } }';\n \n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -277,7 +284,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @media rule inside a @container rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@container (min-width: 36rem) { @media screen and (max-width: 36rem) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -289,7 +296,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @media rule inside a @supports rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@supports (display: flex) { @media screen and (max-width: 36rem) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -301,7 +308,7 @@ describe('CSSParser', () => {\n \t\tit('Ignores @media rule inside a @keyframes rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@keyframes keyframes1 { @media screen and (max-width: 36rem) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -311,7 +318,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @container rule inside a @container rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@container containerName (min-width: 36rem) { @container containerName (min-width: 36rem) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -323,7 +330,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @container rule inside a @supports rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@supports (display: flex) { @container (min-width: 36rem) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -335,7 +342,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @container rule inside a @media rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@media screen and (max-width: 36rem) { @container (min-width: 36rem) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -347,7 +354,7 @@ describe('CSSParser', () => {\n \t\tit('Ignores @container rule inside a @keyframes rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@keyframes keyframes1 { @container (min-width: 36rem) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -357,7 +364,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @supports rule inside a @supports rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@supports (display: flex) { @supports (display: grid) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -369,7 +376,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @supports rule inside a @media rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@media screen and (max-width: 36rem) { @supports (display: grid) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -381,7 +388,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @supports rule inside a @container rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@container (min-width: 36rem) { @supports (display: grid) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -393,7 +400,7 @@ describe('CSSParser', () => {\n \t\tit('Ignores @supports rule inside a @keyframes rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@keyframes keyframes1 { @supports (display: grid) { .foo { height: 0.5rem; } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -403,7 +410,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @keyframes rule inside a @supports rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@supports (display: flex) { @keyframes keyframes1 { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -415,7 +422,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @keyframes rule inside a @media rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@media screen and (max-width: 36rem) { @keyframes keyframes1 { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -427,7 +434,7 @@ describe('CSSParser', () => {\n \t\tit('Supports @keyframes rule inside a @container rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@container (min-width: 36rem) { @keyframes keyframes1 { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n@@ -439,15 +446,15 @@ describe('CSSParser', () => {\n \t\tit('Ignores @keyframes rule inside a @keyframes rule', () => {\n \t\t\tconst css =\n \t\t\t\t'@keyframes keyframes1 { @keyframes keyframes2 { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } }';\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(css);\n \n \t\t\texpect(cssRules.length).toBe(1);\n \t\t\texpect((<CSSKeyframesRule>cssRules[0]).cssText).toBe('@keyframes keyframes1 { \\n}');\n \t\t});\n \n \t\tit('Supports @scope rule', () => {\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(`\n                 @scope {\n                     .foo { color: red; }\n@@ -471,7 +478,7 @@ describe('CSSParser', () => {\n \t\t});\n \n \t\tit('Supports @scope rule inside a @scope rule', () => {\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(`\n                 @scope {\n                     @scope {\n@@ -487,7 +494,7 @@ describe('CSSParser', () => {\n \t\t});\n \n \t\tit('Supports @scope rule inside a @container rule', () => {\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(`\n                 @container (min-width: 36rem) {\n                     @scope {\n@@ -503,7 +510,7 @@ describe('CSSParser', () => {\n \t\t});\n \n \t\tit('Supports @scope rule inside a @media rule', () => {\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(`\n                 @media screen and (max-width: 36rem) {\n                     @scope {\n@@ -519,7 +526,7 @@ describe('CSSParser', () => {\n \t\t});\n \n \t\tit('Supports @scope rule inside a @supports rule', () => {\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(`\n                 @supports (display: flex) {\n                     @scope {\n@@ -535,7 +542,7 @@ describe('CSSParser', () => {\n \t\t});\n \n \t\tit('Ignores @scope rule inside a @keyframes rule', () => {\n-\t\t\tconst cssStyleSheet = new CSSStyleSheet();\n+\t\t\tconst cssStyleSheet = new window.CSSStyleSheet();\n \t\t\tconst cssRules = new CSSParser(cssStyleSheet).parseFromString(`\n                 @keyframes keyframes1 {\n                     @scope {\n@@ -151,7 +151,7 @@ describe('ShadowRoot', () => {\n \tdescribe('get adoptedStyleSheets()', () => {\n \t\tit('Returns set adopted style sheets.', () => {\n \t\t\tconst shadowRoot = <ShadowRoot>document.createElement('custom-element').shadowRoot;\n-\t\t\tconst styleSheet = new CSSStyleSheet();\n+\t\t\tconst styleSheet = new window.CSSStyleSheet();\n \t\t\tshadowRoot.adoptedStyleSheets = [styleSheet];\n \t\t\texpect(shadowRoot.adoptedStyleSheets).toEqual([styleSheet]);\n \t\t});\n@@ -160,7 +160,7 @@ describe('ShadowRoot', () => {\n \tdescribe('set adoptedStyleSheets()', () => {\n \t\tit('Sets adopted style sheets.', () => {\n \t\t\tconst shadowRoot = <ShadowRoot>document.createElement('custom-element').shadowRoot;\n-\t\t\tconst styleSheet = new CSSStyleSheet();\n+\t\t\tconst styleSheet = new window.CSSStyleSheet();\n \t\t\tshadowRoot.adoptedStyleSheets = [styleSheet];\n \t\t\texpect(shadowRoot.adoptedStyleSheets).toEqual([styleSheet]);\n \t\t});\n@@ -6,7 +6,6 @@ import QuerySelectorNthChildHTML from './data/QuerySelectorNthChildHTML.js';\n import type HTMLInputElement from '../../src/nodes/html-input-element/HTMLInputElement.js';\n import { beforeEach, describe, it, expect } from 'vitest';\n import QuerySelector from '../../src/query-selector/QuerySelector.js';\n-import DOMException from '../../src/exception/DOMException.js';\n \n describe('QuerySelector', () => {\n \tlet window: Window;\n@@ -295,9 +294,14 @@ describe('QuerySelector', () => {\n \t\t\tcontainer.appendChild(element1);\n \t\t\tcontainer.appendChild(element2);\n \n-\t\t\tconst invalidSelectorElements = container.querySelectorAll('.before:');\n+\t\t\t// Invalid selector \".before:\" should throw (colon without pseudo-class name)\n+\t\t\texpect(() => container.querySelectorAll('.before:')).toThrowError(\n+\t\t\t\tnew SyntaxError(\n+\t\t\t\t\t`Failed to execute 'querySelectorAll' on 'Document': '.before:' is not a valid selector.`\n+\t\t\t\t)\n+\t\t\t);\n+\t\t\t// Valid selector with escaped colon should match\n \t\t\tconst validSelectorElements = container.querySelectorAll('.before\\\\:after');\n-\t\t\texpect(invalidSelectorElements.length).toBe(0);\n \t\t\texpect(validSelectorElements.length).toBe(2);\n \t\t\texpect(validSelectorElements[0] === element1).toBe(true);\n \t\t\texpect(validSelectorElements[1] === element2).toBe(true);\n@@ -329,9 +333,10 @@ describe('QuerySelector', () => {\n \t\t\tcontainer.appendChild(element1);\n \t\t\tcontainer.appendChild(element2);\n \n-\t\t\tconst invalidSelectorElements = container.querySelectorAll('.before&after');\n+\t\t\t// Invalid selector \".before&after\" should throw (& is not valid in CSS selectors)\n+\t\t\texpect(() => container.querySelectorAll('.before&after')).toThrow();\n+\t\t\t// Valid selector with escaped ampersand should match\n \t\t\tconst validSelectorElements = container.querySelectorAll('.before\\\\&after');\n-\t\t\texpect(invalidSelectorElements.length).toBe(0);\n \t\t\texpect(validSelectorElements.length).toBe(2);\n \t\t\texpect(validSelectorElements[0] === element1).toBe(true);\n \t\t\texpect(validSelectorElements[1] === element2).toBe(true);\n@@ -502,6 +507,24 @@ describe('QuerySelector', () => {\n \t\t\texpect(elements[1] === container.children[0].children[1].children[1]).toBe(true);\n \t\t});\n \n+\t\tit('Returns elements with attribute values containing apostrophes using double quotes as delimiter.', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\tcontainer.innerHTML = `<div data-value=\"it's a test\">Content</div>`;\n+\n+\t\t\tconst elements = container.querySelectorAll('[data-value=\"it\\'s a test\"]');\n+\t\t\texpect(elements.length).toBe(1);\n+\t\t\texpect(elements[0] === container.children[0]).toBe(true);\n+\t\t});\n+\n+\t\tit('Returns elements with attribute values containing double quotes using apostrophes as delimiter.', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\tcontainer.innerHTML = `<div data-value='say \"hello\"'>Content</div>`;\n+\n+\t\t\tconst elements = container.querySelectorAll('[data-value=\\'say \"hello\"\\']');\n+\t\t\texpect(elements.length).toBe(1);\n+\t\t\texpect(elements[0] === container.children[0]).toBe(true);\n+\t\t});\n+\n \t\tit('Returns all elements with tag name and matching attributes using \"span[_attr1]\".', () => {\n \t\t\tconst container = document.createElement('div');\n \t\t\tcontainer.innerHTML = QuerySelectorHTML.replace(/ attr1/gm, '_attr1');\n@@ -912,26 +935,32 @@ describe('QuerySelector', () => {\n \t\t\texpect(elements3[1] === container.children[0].children[1].children[2]).toBe(true);\n \t\t});\n \n-\t\tit('Returns all elements matching the selector without ending parenthese \"button:not([type]\"', () => {\n+\t\tit('Throws error for selector without ending parenthesis \"button:not([type]\"', () => {\n \t\t\tconst container = document.createElement('div');\n \t\t\tcontainer.innerHTML = `\n \t\t\t\t<button></button>\n                 <button type=\"submit\"></button>\n                 <button></button>\n \t\t\t`;\n-\t\t\tconst elements = container.querySelectorAll('button:not([type]');\n+\t\t\t// Invalid selector (missing closing parenthesis) should throw\n+\t\t\texpect(() => container.querySelectorAll('button:not([type]')).toThrow();\n+\t\t\t// Valid selector should work\n+\t\t\tconst elements = container.querySelectorAll('button:not([type])');\n \t\t\texpect(elements.length).toBe(2);\n \t\t\texpect(elements[0]).toBe(container.children[0]);\n \t\t\texpect(elements[1]).toBe(container.children[2]);\n \t\t});\n \n-\t\tit('Returns all elements matching the invalid selector \"[q\\\\:shadowroot]\"', () => {\n+\t\tit('Returns all elements matching the selector \"[q\\\\:shadowroot]\" with escaped colon', () => {\n \t\t\tconst container = document.createElement('div');\n-\t\t\tcontainer.innerHTML = `\n-\t\t\t\t<span q\\\\:shadowroot></span>\n-                <button></button>\n-                <article></article>\n-\t\t\t`;\n+\t\t\t// Create element with attribute name containing colon\n+\t\t\tconst span = document.createElement('span');\n+\t\t\tspan.setAttribute('q:shadowroot', '');\n+\t\t\tcontainer.appendChild(span);\n+\t\t\tcontainer.appendChild(document.createElement('button'));\n+\t\t\tcontainer.appendChild(document.createElement('article'));\n+\n+\t\t\t// Selector [q\\:shadowroot] looks for attribute \"q:shadowroot\" (colon is escaped)\n \t\t\tconst elements = container.querySelectorAll('[q\\\\:shadowroot]');\n \t\t\texpect(elements.length).toBe(1);\n \t\t\texpect(elements[0]).toBe(container.children[0]);\n@@ -1290,6 +1319,71 @@ describe('QuerySelector', () => {\n \t\t\t]);\n \t\t});\n \n+\t\tit('Correctly parses complex CSS :has() expression with nested :not() (issue #1910)', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\tcontainer.innerHTML = `\n+\t\t\t\t<table class=\"emXRrt\">\n+\t\t\t\t\t<thead>\n+\t\t\t\t\t\t<tr>\n+\t\t\t\t\t\t\t<th>Header 1</th>\n+\t\t\t\t\t\t\t<th>Header 2</th>\n+\t\t\t\t\t\t</tr>\n+\t\t\t\t\t</thead>\n+\t\t\t\t\t<tbody>\n+\t\t\t\t\t\t<tr>\n+\t\t\t\t\t\t\t<td>Cell 1</td>\n+\t\t\t\t\t\t\t<td>Cell 2</td>\n+\t\t\t\t\t\t</tr>\n+\t\t\t\t\t\t<tr data-ignore-row>\n+\t\t\t\t\t\t\t<td>Ignored 1</td>\n+\t\t\t\t\t\t\t<td>Ignored 2</td>\n+\t\t\t\t\t\t</tr>\n+\t\t\t\t\t</tbody>\n+\t\t\t\t</table>\n+\t\t\t`;\n+\n+\t\t\tconst elements = container.querySelectorAll(\n+\t\t\t\t'.emXRrt>tbody>tr>td:nth-child(2),.emXRrt:not(:has(> tbody > tr:not([data-ignore-row])))>thead>tr>th:nth-child(2)'\n+\t\t\t);\n+\n+\t\t\t// Should only match the 2 td elements from the first selector part\n+\t\t\texpect(elements.length).toBe(2);\n+\t\t\texpect(elements[0].textContent).toBe('Cell 2');\n+\t\t\texpect(elements[1].textContent).toBe('Ignored 2');\n+\t\t});\n+\n+\t\tit('Should throw error when ending with \",\"', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\texpect(() => container.querySelectorAll('.test,.test-2,')).toThrowError(\n+\t\t\t\tnew SyntaxError(\n+\t\t\t\t\t\"Failed to execute 'querySelectorAll' on 'Element': '.test,.test-2,' is not a valid selector.\"\n+\t\t\t\t)\n+\t\t\t);\n+\t\t\texpect(() => container.querySelectorAll('.test.,,test-2')).toThrowError(\n+\t\t\t\tnew SyntaxError(\n+\t\t\t\t\t\"Failed to execute 'querySelectorAll' on 'Element': '.test,,.test-2' is not a valid selector.\"\n+\t\t\t\t)\n+\t\t\t);\n+\t\t});\n+\n+\t\tit('Correctly parses :not() with nested :has() containing :not() (issue #1910)', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\tcontainer.innerHTML = `\n+\t\t\t\t<div class=\"parent\">\n+\t\t\t\t\t<div class=\"child\" data-keep></div>\n+\t\t\t\t\t<div class=\"child\"></div>\n+\t\t\t\t</div>\n+\t\t\t\t<div class=\"parent no-match\">\n+\t\t\t\t\t<div class=\"child\" data-keep></div>\n+\t\t\t\t</div>\n+\t\t\t`;\n+\n+\t\t\tconst elements = container.querySelectorAll('.parent:not(:has(> .child:not([data-keep])))');\n+\n+\t\t\texpect(elements.length).toBe(1);\n+\t\t\texpect(elements[0].classList.contains('no-match')).toBe(true);\n+\t\t});\n+\n \t\tit('Returns all elements for subsequent sibling selector using \".a ~ .a\"', () => {\n \t\t\tconst div = document.createElement('div');\n \n@@ -1382,6 +1476,14 @@ describe('QuerySelector', () => {\n \t\t\texpect(root.length).toBe(1);\n \t\t\texpect(root[0]).toBe(document.documentElement);\n \t\t});\n+\n+\t\tit('Matches XML query selector for #1912', () => {\n+\t\t\tconst xml = '<root><child Name=\"John\">Content</child></root>';\n+\t\t\tconst parser = new window.DOMParser();\n+\t\t\tconst xmlDoc = parser.parseFromString(xml, 'application/xml');\n+\n+\t\t\texpect(xmlDoc.querySelectorAll('[Name]').length).toBe(1);\n+\t\t});\n \t});\n \n \tdescribe('querySelector()', () => {\n@@ -2154,5 +2256,22 @@ describe('QuerySelector', () => {\n \n \t\t\texpect(div.matches(':root')).toBe(false);\n \t\t});\n+\n+\t\tit('Matches unicode characters for #2034', () => {\n+\t\t\t// Apostrophe in double-quoted attribute value\n+\t\t\tdocument.body.innerHTML = `\n+            <label id=\"type d'activité-label\">Type d'activité</label>\n+            <input aria-labelledby=\"type d'activité-label\" />\n+            `;\n+\t\t\texpect(!!document.querySelector('[id=\"type d\\'activité-label\"]')).toBe(true);\n+\n+\t\t\t// Test 2: Double quote in single-quoted attribute value\n+\t\t\tdocument.body.innerHTML = `<div id='say \"hello\"'></div>`;\n+\t\t\texpect(!!document.querySelector('[id=\\'say \"hello\"\\']')).toBe(true);\n+\n+\t\t\t// Test 3: Simple ID (control test)\n+\t\t\tdocument.body.innerHTML = `<div id=\"simple\"></div>`;\n+\t\t\texpect(!!document.querySelector('[id=\"simple\"]')).toBe(true);\n+\t\t});\n \t});\n });\n@@ -652,7 +652,7 @@ describe('BrowserWindow', () => {\n \t\t\t);\n \t\t\tconst elementComputedStyle = window.getComputedStyle(element);\n \n-\t\t\tconst styleSheet = new CSSStyleSheet();\n+\t\t\tconst styleSheet = new window.CSSStyleSheet();\n \t\t\tstyleSheet.replaceSync(`\n                 span {\n \t\t\t\t\tcolor: green;"},{"url":"https://github.com/capricorn86/happy-dom/pull/2010","title":"fix: [#1910] Replace custom CSS selector parser with parsel-js","state":"closed","merged":false,"created_at":"2026-01-20T14:22:07Z","updated_at":"2026-02-19T00:50:50Z","closed_at":"2026-02-19T00:50:49Z","merged_at":null,"base_sha":"aa2dbb813416e54c4b34c77e3834952cdcc8dd6d","head_sha":"8c749acad8c03c398d67cb1696a037c021e1fcd5","merge_commit_sha":null,"diff":"@@ -9963,6 +9963,22 @@\n \t\t\t\t\"url\": \"https://github.com/sponsors/sindresorhus\"\n \t\t\t}\n \t\t},\n+\t\t\"node_modules/parsel-js\": {\n+\t\t\t\"version\": \"1.2.2\",\n+\t\t\t\"resolved\": \"https://registry.npmjs.org/parsel-js/-/parsel-js-1.2.2.tgz\",\n+\t\t\t\"integrity\": \"sha512-AVJMlwQ4bL2Y0VvYJGk+Fp7eX4SCH2uFoNApmn4yKWACUewZ+alwW3tyoe1r5Z3aLYQTuAuPZIyGghMfO/Tlxw==\",\n+\t\t\t\"funding\": [\n+\t\t\t\t{\n+\t\t\t\t\t\"type\": \"individual\",\n+\t\t\t\t\t\"url\": \"https://github.com/sponsors/LeaVerou\"\n+\t\t\t\t},\n+\t\t\t\t{\n+\t\t\t\t\t\"type\": \"opencollective\",\n+\t\t\t\t\t\"url\": \"https://opencollective.com/leaverou\"\n+\t\t\t\t}\n+\t\t\t],\n+\t\t\t\"license\": \"MIT\"\n+\t\t},\n \t\t\"node_modules/parseurl\": {\n \t\t\t\"version\": \"1.3.3\",\n \t\t\t\"resolved\": \"https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz\",\n@@ -13855,6 +13871,7 @@\n \t\t\t\t\"@types/whatwg-mimetype\": \"^3.0.2\",\n \t\t\t\t\"@types/ws\": \"^8.18.1\",\n \t\t\t\t\"entities\": \"^4.5.0\",\n+\t\t\t\t\"parsel-js\": \"^1.2.2\",\n \t\t\t\t\"whatwg-mimetype\": \"^3.0.0\",\n \t\t\t\t\"ws\": \"^8.18.3\"\n \t\t\t},\n@@ -33,10 +33,11 @@\n \t\t\"test:debug\": \"vitest run --inspect-brk --no-file-parallelism\"\n \t},\n \t\"dependencies\": {\n-\t\t\"@types/whatwg-mimetype\": \"^3.0.2\",\n \t\t\"@types/node\": \">=20.0.0\",\n+\t\t\"@types/whatwg-mimetype\": \"^3.0.2\",\n \t\t\"@types/ws\": \"^8.18.1\",\n \t\t\"entities\": \"^4.5.0\",\n+\t\t\"parsel-js\": \"^1.2.2\",\n \t\t\"whatwg-mimetype\": \"^3.0.0\",\n \t\t\"ws\": \"^8.18.3\"\n \t},\n@@ -2,37 +2,15 @@ import SelectorItem from './SelectorItem.js';\n import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';\n import DOMException from '../exception/DOMException.js';\n import ISelectorPseudo from './ISelectorPseudo.js';\n+import ISelectorAttribute from './ISelectorAttribute.js';\n import Element from '../nodes/element/Element.js';\n import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js';\n+import { parse, type AST } from 'parsel-js';\n \n /**\n- * Selector RegExp.\n- *\n- * Group 1: All (e.g. \"*\")\n- * Group 2: Tag name (e.g. \"div\")\n- * Group 3: ID (e.g. \"#id\")\n- * Group 4: Class (e.g. \".class\")\n- * Group 5: Attribute name when no value (e.g. \"attr1\")\n- * Group 6: Attribute name when there is a value using apostrophe (e.g. \"attr1\")\n- * Group 7: Attribute operator when using apostrophe (e.g. \"~\")\n- * Group 8: Attribute value when using apostrophe (e.g. \"value1\")\n- * Group 9: Attribute modifier when using apostrophe (e.g. \"i\" or \"s\")\n- * Group 10: Attribute name when threre is a value not using apostrophe (e.g. \"attr1\")\n- * Group 11: Attribute operator when not using apostrophe (e.g. \"~\")\n- * Group 12: Attribute value when notusing apostrophe (e.g. \"value1\")\n- * Group 13: Pseudo name when arguments (e.g. \"nth-child\")\n- * Group 14: Arguments of pseudo (e.g. \"2n + 1\")\n- * Group 15: Pseudo name when no arguments (e.g. \"empty\")\n- * Group 16: Pseudo element (e.g. \"::after\", \"::-webkit-inner-spin-button\").\n- * Group 17: Combinator.\n+ * Escaped Character RegExp - matches backslash followed by any character.\n  */\n-const SELECTOR_REGEXP =\n-\t/(\\*)|([a-zA-Z0-9-]+)|#((?:[a-zA-Z0-9-_«»]|\\\\.)+)|\\.((?:[a-zA-Z0-9-_«»]|\\\\.)+)|\\[([a-zA-Z0-9-_\\\\:]+)\\]|\\[([a-zA-Z0-9-_\\\\:]+)\\s*([~|^$*]{0,1})\\s*=\\s*[\"']{1}([^\"']*)[\"']{1}\\s*(s|i){0,1}\\]|\\[([a-zA-Z0-9-_]+)\\s*([~|^$*]{0,1})\\s*=\\s*([^\\]]*)\\]|:([a-zA-Z-]+)\\s*\\(((?:[^()]|\\[[^\\]]*\\]|\\([^()]*\\))*)\\){0,1}|:([a-zA-Z-]+)|::([a-zA-Z-]+)|([\\s,+>~]*)/gm;\n-\n-/**\n- * Escaped Character RegExp.\n- */\n-const ESCAPED_CHARACTER_REGEXP = /\\\\/g;\n+const ESCAPED_CHARACTER_REGEXP = /\\\\(.)/g;\n \n /**\n  * Attribute Escape RegExp.\n@@ -54,26 +32,28 @@ const NTH_FUNCTION = {\n const SPACE_REGEXP = / /g;\n \n /**\n- * Simple Selector RegExp.\n- *\n- * Group 1: Tag name (e.g. \"div\")\n- * Group 2: Class (e.g. \".classA.classB\")\n- * Group 3: ID (e.g. \"#id\")\n+ * Placeholder for escaped characters that parsel-js can't handle.\n+ * Using a unique string that won't appear in normal selectors.\n  */\n-const SIMPLE_SELECTOR_REGEXP = /(^[a-zA-Z0-9-]+$)|(^\\.[a-zA-Z0-9-_.]+$)|(^#[a-zA-Z0-9-_]+$)/;\n+const ESCAPE_PLACEHOLDER_PREFIX = '__HDOM_ESC_';\n+const ESCAPE_PLACEHOLDER_SUFFIX = '__';\n \n /**\n- * Utility for parsing a selection string.\n+ * RegExp to find escape sequences that need pre-processing.\n+ * Matches backslash followed by special characters that parsel-js can't handle.\n+ */\n+const PROBLEMATIC_ESCAPE_REGEXP = /\\\\([:#&\\[\\]])/g;\n+\n+/**\n+ * Utility for parsing a selection string using parsel-js.\n  */\n export default class SelectorParser {\n \t/**\n \t * Parses a selector string and returns an instance of SelectorItem.\n-\t *\n-\t * @param selector Selector.\n-\t * @param options Options.\n-\t * @param [options.scope] Scope.\n-\t * @param [options.ignoreErrors] Ignores errors.\n-\t * @returns Selector item.\n+\t * @param selector\n+\t * @param options\n+\t * @param options.scope\n+\t * @param options.ignoreErrors\n \t */\n \tpublic static getSelectorItem(\n \t\tselector: string,\n@@ -87,12 +67,10 @@ export default class SelectorParser {\n \n \t/**\n \t * Parses a selector string and returns groups with SelectorItem instances.\n-\t *\n-\t * @param selector Selector.\n-\t * @param options Options.\n-\t * @param [options.scope] Scope.\n-\t * @param [options.ignoreErrors] Ignores errors.\n-\t * @returns Selector groups.\n+\t * @param selector\n+\t * @param options\n+\t * @param options.scope\n+\t * @param options.ignoreErrors\n \t */\n \tpublic static getSelectorGroups(\n \t\tselector: string,\n@@ -105,308 +83,488 @@ export default class SelectorParser {\n \t\tconst ignoreErrors = options?.ignoreErrors;\n \t\tconst scope = options?.scope;\n \n-\t\tif (selector === '*') {\n-\t\t\treturn [[new SelectorItem({ scope, tagName: '*', ignoreErrors })]];\n-\t\t}\n-\n-\t\tconst simpleMatch = selector.match(SIMPLE_SELECTOR_REGEXP);\n-\n-\t\tif (simpleMatch) {\n-\t\t\tif (simpleMatch[1]) {\n-\t\t\t\treturn [[new SelectorItem({ scope, tagName: selector.toUpperCase(), ignoreErrors })]];\n-\t\t\t} else if (simpleMatch[2]) {\n-\t\t\t\treturn [\n-\t\t\t\t\t[\n-\t\t\t\t\t\tnew SelectorItem({\n-\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\tclassNames: selector.replace('.', '').split('.'),\n-\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t})\n-\t\t\t\t\t]\n-\t\t\t\t];\n-\t\t\t} else if (simpleMatch[3]) {\n-\t\t\t\treturn [[new SelectorItem({ scope, id: selector.replace('#', ''), ignoreErrors })]];\n+\t\tif (!selector) {\n+\t\t\tif (ignoreErrors) {\n+\t\t\t\treturn [];\n \t\t\t}\n+\t\t\tthrow new DOMException(`Invalid selector: \"${selector}\"`);\n \t\t}\n \n-\t\tconst regexp = new RegExp(SELECTOR_REGEXP);\n-\t\tlet currentSelectorItem: SelectorItem = new SelectorItem({\n-\t\t\tscope,\n-\t\t\tcombinator: SelectorCombinatorEnum.descendant,\n-\t\t\tignoreErrors\n-\t\t});\n-\t\tlet currentGroup: SelectorItem[] = [currentSelectorItem];\n-\t\tconst groups: Array<Array<SelectorItem>> = [currentGroup];\n-\t\tlet isValid = false;\n-\t\tlet match;\n-\n-\t\twhile ((match = regexp.exec(selector))) {\n-\t\t\tif (match[0]) {\n-\t\t\t\tisValid = true;\n-\n-\t\t\t\tif (match[1]) {\n-\t\t\t\t\tcurrentSelectorItem.tagName = '*';\n-\t\t\t\t} else if (match[2]) {\n-\t\t\t\t\tcurrentSelectorItem.tagName = match[2].toUpperCase();\n-\t\t\t\t} else if (match[3]) {\n-\t\t\t\t\tcurrentSelectorItem.id = match[3].replace(ESCAPED_CHARACTER_REGEXP, '');\n-\t\t\t\t} else if (match[4]) {\n-\t\t\t\t\tcurrentSelectorItem.classNames = currentSelectorItem.classNames || [];\n-\t\t\t\t\tcurrentSelectorItem.classNames.push(match[4].replace(ESCAPED_CHARACTER_REGEXP, ''));\n-\t\t\t\t} else if (match[5]) {\n-\t\t\t\t\tcurrentSelectorItem.attributes = currentSelectorItem.attributes || [];\n-\t\t\t\t\tcurrentSelectorItem.attributes.push({\n-\t\t\t\t\t\tname: match[5],\n-\t\t\t\t\t\toperator: null,\n-\t\t\t\t\t\tvalue: null,\n-\t\t\t\t\t\tmodifier: null,\n-\t\t\t\t\t\tregExp: null\n-\t\t\t\t\t});\n-\t\t\t\t} else if (match[6] && match[8] !== undefined) {\n-\t\t\t\t\tcurrentSelectorItem.attributes = currentSelectorItem.attributes || [];\n-\t\t\t\t\tcurrentSelectorItem.attributes.push({\n-\t\t\t\t\t\tname: match[6],\n-\t\t\t\t\t\toperator: match[7] || null,\n-\t\t\t\t\t\tvalue: match[8].replace(ESCAPED_CHARACTER_REGEXP, ''),\n-\t\t\t\t\t\tmodifier: <'s'>match[9] || null,\n-\t\t\t\t\t\tregExp: this.getAttributeRegExp({\n-\t\t\t\t\t\t\toperator: match[7],\n-\t\t\t\t\t\t\tvalue: match[8],\n-\t\t\t\t\t\t\tmodifier: match[9]\n-\t\t\t\t\t\t})\n-\t\t\t\t\t});\n-\t\t\t\t} else if (match[10] && match[12] !== undefined) {\n-\t\t\t\t\tcurrentSelectorItem.attributes = currentSelectorItem.attributes || [];\n-\t\t\t\t\tcurrentSelectorItem.attributes.push({\n-\t\t\t\t\t\tname: match[10],\n-\t\t\t\t\t\toperator: match[11] || null,\n-\t\t\t\t\t\tvalue: match[12].replace(ESCAPED_CHARACTER_REGEXP, ''),\n-\t\t\t\t\t\tmodifier: null,\n-\t\t\t\t\t\tregExp: this.getAttributeRegExp({ operator: match[11], value: match[12] })\n-\t\t\t\t\t});\n-\t\t\t\t} else if (match[13] && match[14]) {\n-\t\t\t\t\tcurrentSelectorItem.pseudos = currentSelectorItem.pseudos || [];\n-\t\t\t\t\tcurrentSelectorItem.pseudos.push(this.getPseudo(match[13], match[14], options));\n-\t\t\t\t} else if (match[15]) {\n-\t\t\t\t\tcurrentSelectorItem.pseudos = currentSelectorItem.pseudos || [];\n-\t\t\t\t\tcurrentSelectorItem.pseudos.push(this.getPseudo(match[15], null, options));\n-\t\t\t\t} else if (match[16]) {\n-\t\t\t\t\tcurrentSelectorItem.isPseudoElement = true;\n-\t\t\t\t} else if (match[17]) {\n-\t\t\t\t\tswitch (match[17].trim()) {\n-\t\t\t\t\t\tcase ',':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.descendant,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup = [currentSelectorItem];\n-\t\t\t\t\t\t\tgroups.push(currentGroup);\n-\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\tcase '>':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.child,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup.push(currentSelectorItem);\n-\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\tcase '+':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.adjacentSibling,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup.push(currentSelectorItem);\n-\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\tcase '~':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.subsequentSibling,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup.push(currentSelectorItem);\n-\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\tcase '':\n-\t\t\t\t\t\t\tcurrentSelectorItem = new SelectorItem({\n-\t\t\t\t\t\t\t\tscope,\n-\t\t\t\t\t\t\t\tcombinator: SelectorCombinatorEnum.descendant,\n-\t\t\t\t\t\t\t\tignoreErrors\n-\t\t\t\t\t\t\t});\n-\t\t\t\t\t\t\tcurrentGroup.push(currentSelectorItem);\n-\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t} else {\n-\t\t\t\tbreak;\n+\t\t// Pre-process selector to handle escaped characters that parsel-js can't handle\n+\t\tconst { processedSelector, escapeMap } = this.preprocessSelector(selector);\n+\n+\t\tlet ast: AST | undefined;\n+\t\ttry {\n+\t\t\tast = parse(processedSelector, { recursive: true, list: true });\n+\t\t} catch {\n+\t\t\tif (ignoreErrors) {\n+\t\t\t\treturn [];\n \t\t\t}\n+\t\t\tthrow new DOMException(`Invalid selector: \"${selector}\"`);\n \t\t}\n \n-\t\tif (!isValid) {\n-\t\t\tif (options?.ignoreErrors) {\n+\t\tif (!ast) {\n+\t\t\tif (ignoreErrors) {\n \t\t\t\treturn [];\n \t\t\t}\n \t\t\tthrow new DOMException(`Invalid selector: \"${selector}\"`);\n \t\t}\n \n-\t\treturn groups;\n+\t\tif (ast.type === 'list') {\n+\t\t\tconst groups: Array<Array<SelectorItem>> = [];\n+\t\t\tfor (const item of ast.list) {\n+\t\t\t\tgroups.push(this.astToSelectorItems(item, scope, ignoreErrors, options, escapeMap));\n+\t\t\t}\n+\t\t\treturn groups;\n+\t\t}\n+\n+\t\treturn [this.astToSelectorItems(ast, scope, ignoreErrors, options, escapeMap)];\n \t}\n \n \t/**\n-\t * Returns attribute RegExp.\n-\t *\n-\t * @param attribute Attribute.\n-\t * @param attribute.value Attribute value.\n-\t * @param attribute.operator Attribute operator.\n-\t * @param attribute.modifier Attribute modifier.\n-\t * @returns Attribute RegExp.\n+\t * Pre-processes a selector to handle escaped characters that parsel-js can't handle.\n+\t * @param selector\n \t */\n-\tprivate static getAttributeRegExp(attribute: {\n-\t\tvalue?: string;\n-\t\toperator?: string;\n-\t\tmodifier?: string;\n-\t}): RegExp | null {\n-\t\tconst modifier = attribute.modifier === 'i' ? 'i' : '';\n+\tprivate static preprocessSelector(selector: string): {\n+\t\tprocessedSelector: string;\n+\t\tescapeMap: Map<string, string>;\n+\t} {\n+\t\tconst escapeMap = new Map<string, string>();\n+\t\tlet counter = 0;\n+\n+\t\tconst processedSelector = selector.replace(PROBLEMATIC_ESCAPE_REGEXP, (_match, char) => {\n+\t\t\tconst placeholder = `${ESCAPE_PLACEHOLDER_PREFIX}${counter}${ESCAPE_PLACEHOLDER_SUFFIX}`;\n+\t\t\tescapeMap.set(placeholder, char);\n+\t\t\tcounter++;\n+\t\t\treturn placeholder;\n+\t\t});\n \n-\t\tif (!attribute.operator || !attribute.value) {\n-\t\t\treturn null;\n+\t\treturn { processedSelector, escapeMap };\n+\t}\n+\n+\t/**\n+\t * Restores escaped characters from placeholders.\n+\t * @param value\n+\t * @param escapeMap\n+\t */\n+\tprivate static restoreEscapedChars(value: string, escapeMap: Map<string, string>): string {\n+\t\tlet result = value;\n+\t\tfor (const [placeholder, char] of escapeMap) {\n+\t\t\tresult = result.split(placeholder).join(char);\n \t\t}\n+\t\treturn result;\n+\t}\n \n-\t\t// Escape special regex characters in the value\n-\t\tconst escapedValue = attribute.value.replace(ATTRIBUTE_ESCAPE_REGEXP, '\\\\$&');\n+\t/**\n+\t * Converts a parsel AST node to an array of SelectorItems.\n+\t * @param ast\n+\t * @param scope\n+\t * @param ignoreErrors\n+\t * @param options\n+\t * @param options.scope\n+\t * @param options.ignoreErrors\n+\t * @param escapeMap\n+\t */\n+\tprivate static astToSelectorItems(\n+\t\tast: AST,\n+\t\tscope: Element | DocumentFragment | undefined,\n+\t\tignoreErrors: boolean | undefined,\n+\t\toptions: { scope?: Element | DocumentFragment; ignoreErrors?: boolean } | undefined,\n+\t\tescapeMap: Map<string, string>\n+\t): SelectorItem[] {\n+\t\tconst items: SelectorItem[] = [];\n+\t\tthis.flattenComplexSelector(\n+\t\t\tast,\n+\t\t\titems,\n+\t\t\tSelectorCombinatorEnum.descendant,\n+\t\t\tscope,\n+\t\t\tignoreErrors,\n+\t\t\toptions,\n+\t\t\tescapeMap\n+\t\t);\n+\t\treturn items;\n+\t}\n \n-\t\tswitch (attribute.operator) {\n-\t\t\t// [attribute~=\"value\"] - Contains a specified word.\n+\t/**\n+\t * Flattens a complex selector AST into an array of SelectorItems.\n+\t * @param ast\n+\t * @param items\n+\t * @param combinator\n+\t * @param scope\n+\t * @param ignoreErrors\n+\t * @param options\n+\t * @param options.scope\n+\t * @param options.ignoreErrors\n+\t * @param escapeMap\n+\t */\n+\tprivate static flattenComplexSelector(\n+\t\tast: AST,\n+\t\titems: SelectorItem[],\n+\t\tcombinator: SelectorCombinatorEnum,\n+\t\tscope: Element | DocumentFragment | undefined,\n+\t\tignoreErrors: boolean | undefined,\n+\t\toptions: { scope?: Element | DocumentFragment; ignoreErrors?: boolean } | undefined,\n+\t\tescapeMap: Map<string, string>\n+\t): void {\n+\t\tif (ast.type === 'complex') {\n+\t\t\tthis.flattenComplexSelector(\n+\t\t\t\tast.left,\n+\t\t\t\titems,\n+\t\t\t\tcombinator,\n+\t\t\t\tscope,\n+\t\t\t\tignoreErrors,\n+\t\t\t\toptions,\n+\t\t\t\tescapeMap\n+\t\t\t);\n+\t\t\tconst rightCombinator = this.parselCombinatorToEnum(ast.combinator);\n+\t\t\tthis.flattenComplexSelector(\n+\t\t\t\tast.right,\n+\t\t\t\titems,\n+\t\t\t\trightCombinator,\n+\t\t\t\tscope,\n+\t\t\t\tignoreErrors,\n+\t\t\t\toptions,\n+\t\t\t\tescapeMap\n+\t\t\t);\n+\t\t} else {\n+\t\t\titems.push(\n+\t\t\t\tthis.astNodeToSelectorItem(ast, combinator, scope, ignoreErrors, options, escapeMap)\n+\t\t\t);\n+\t\t}\n+\t}\n+\n+\t/**\n+\t * Converts a parsel combinator string to SelectorCombinatorEnum.\n+\t * @param combinator\n+\t */\n+\tprivate static parselCombinatorToEnum(combinator: string): SelectorCombinatorEnum {\n+\t\tswitch (combinator.trim()) {\n+\t\t\tcase '>':\n+\t\t\t\treturn SelectorCombinatorEnum.child;\n+\t\t\tcase '+':\n+\t\t\t\treturn SelectorCombinatorEnum.adjacentSibling;\n \t\t\tcase '~':\n-\t\t\t\treturn new RegExp(`[- ]${escapedValue}|${escapedValue}[- ]|^${escapedValue}$`, modifier);\n-\t\t\t// [attribute|=\"value\"] - Starts with the specified word.\n-\t\t\tcase '|':\n-\t\t\t\treturn new RegExp(`^${escapedValue}[- ]|^${escapedValue}$`, modifier);\n-\t\t\t// [attribute^=\"value\"] - Begins with a specified value.\n-\t\t\tcase '^':\n-\t\t\t\treturn new RegExp(`^${escapedValue}`, modifier);\n-\t\t\t// [attribute$=\"value\"] - Ends with a specified value.\n-\t\t\tcase '$':\n-\t\t\t\treturn new RegExp(`${escapedValue}$`, modifier);\n-\t\t\t// [attribute*=\"value\"] - Contains a specified value.\n-\t\t\tcase '*':\n-\t\t\t\treturn new RegExp(`${escapedValue}`, modifier);\n+\t\t\t\treturn SelectorCombinatorEnum.subsequentSibling;\n \t\t\tdefault:\n-\t\t\t\treturn null;\n+\t\t\t\treturn SelectorCombinatorEnum.descendant;\n \t\t}\n \t}\n \n \t/**\n-\t * Returns pseudo.\n-\t *\n-\t * @param name Pseudo name.\n-\t * @param args Pseudo arguments.\n-\t * @param [options] Options.\n-\t * @param [options.scope] Scope.\n-\t * @param [options.ignoreErrors] Ignores errors.\n-\t * @returns Pseudo.\n+\t * Converts a single AST node to a SelectorItem.\n+\t * @param ast\n+\t * @param combinator\n+\t * @param scope\n+\t * @param ignoreErrors\n+\t * @param options\n+\t * @param options.scope\n+\t * @param options.ignoreErrors\n+\t * @param escapeMap\n \t */\n-\tprivate static getPseudo(\n-\t\tname: string,\n-\t\targs: string | null | undefined,\n-\t\toptions?: {\n-\t\t\tscope?: Element | DocumentFragment;\n-\t\t\tignoreErrors?: boolean;\n+\tprivate static astNodeToSelectorItem(\n+\t\tast: AST,\n+\t\tcombinator: SelectorCombinatorEnum,\n+\t\tscope: Element | DocumentFragment | undefined,\n+\t\tignoreErrors: boolean | undefined,\n+\t\toptions: { scope?: Element | DocumentFragment; ignoreErrors?: boolean } | undefined,\n+\t\tescapeMap: Map<string, string>\n+\t): SelectorItem {\n+\t\tconst item = new SelectorItem({ scope, combinator, ignoreErrors });\n+\n+\t\tif (ast.type === 'compound') {\n+\t\t\tfor (const token of ast.list) {\n+\t\t\t\tthis.applyTokenToSelectorItem(token, item, options, escapeMap);\n+\t\t\t}\n+\t\t} else {\n+\t\t\tthis.applyTokenToSelectorItem(ast, item, options, escapeMap);\n+\t\t}\n+\n+\t\treturn item;\n+\t}\n+\n+\t/**\n+\t * Extracts the name from a token, handling escaped characters.\n+\t * parsel-js returns only the part before escaped characters in `name`,\n+\t * but the full content is in `content`. We need to extract the full name.\n+\t * @param token\n+\t * @param prefix The prefix to remove (e.g., '#' for id, '.' for class)\n+\t * @param escapeMap\n+\t */\n+\tprivate static extractNameFromToken(\n+\t\ttoken: AST,\n+\t\tprefix: string,\n+\t\tescapeMap: Map<string, string>\n+\t): string {\n+\t\t// Cast to access content and name properties\n+\t\tconst tokenWithContent = <{ content: string; name: string }>token;\n+\n+\t\t// Get the content and remove the prefix\n+\t\tlet name = tokenWithContent.content.startsWith(prefix)\n+\t\t\t? tokenWithContent.content.slice(prefix.length)\n+\t\t\t: tokenWithContent.name;\n+\n+\t\t// Restore any pre-processed escape placeholders\n+\t\tname = this.restoreEscapedChars(name, escapeMap);\n+\n+\t\t// Remove remaining escape characters (backslash followed by any char becomes just the char)\n+\t\tname = name.replace(ESCAPED_CHARACTER_REGEXP, '$1');\n+\n+\t\treturn name;\n+\t}\n+\n+\t/**\n+\t * Applies a token to a SelectorItem.\n+\t * @param token\n+\t * @param item\n+\t * @param options\n+\t * @param options.scope\n+\t * @param options.ignoreErrors\n+\t * @param escapeMap\n+\t */\n+\tprivate static applyTokenToSelectorItem(\n+\t\ttoken: AST,\n+\t\titem: SelectorItem,\n+\t\toptions: { scope?: Element | DocumentFragment; ignoreErrors?: boolean } | undefined,\n+\t\tescapeMap: Map<string, string>\n+\t): void {\n+\t\tswitch (token.type) {\n+\t\t\tcase 'universal':\n+\t\t\t\titem.tagName = '*';\n+\t\t\t\tbreak;\n+\t\t\tcase 'type':\n+\t\t\t\titem.tagName = token.name.toUpperCase();\n+\t\t\t\tbreak;\n+\t\t\tcase 'id':\n+\t\t\t\titem.id = this.extractNameFromToken(token, '#', escapeMap);\n+\t\t\t\tbreak;\n+\t\t\tcase 'class':\n+\t\t\t\titem.classNames = item.classNames || [];\n+\t\t\t\titem.classNames.push(this.extractNameFromToken(token, '.', escapeMap));\n+\t\t\t\tbreak;\n+\t\t\tcase 'attribute':\n+\t\t\t\titem.attributes = item.attributes || [];\n+\t\t\t\titem.attributes.push(this.parseAttribute(token, escapeMap));\n+\t\t\t\tbreak;\n+\t\t\tcase 'pseudo-class':\n+\t\t\t\titem.pseudos = item.pseudos || [];\n+\t\t\t\titem.pseudos.push(this.parsePseudoClass(token, options, escapeMap));\n+\t\t\t\tbreak;\n+\t\t\tcase 'pseudo-element':\n+\t\t\t\titem.isPseudoElement = true;\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\t/**\n+\t * Parses an attribute token into ISelectorAttribute.\n+\t * @param token\n+\t * @param escapeMap\n+\t */\n+\tprivate static parseAttribute(token: AST, escapeMap: Map<string, string>): ISelectorAttribute {\n+\t\tconst attrToken = <\n+\t\t\t{\n+\t\t\t\tname: string;\n+\t\t\t\tvalue?: string;\n+\t\t\t\toperator?: string;\n+\t\t\t\tcaseSensitive?: string;\n+\t\t\t\tcontent: string;\n+\t\t\t}\n+\t\t>token;\n+\n+\t\t// Extract attribute name, handling escaped characters\n+\t\tlet attrName = this.restoreEscapedChars(attrToken.name, escapeMap);\n+\t\tattrName = attrName.replace(ESCAPED_CHARACTER_REGEXP, '$1');\n+\n+\t\t// Extract attribute value from content if parsel-js parsed it incorrectly\n+\t\tlet value: string | null = null;\n+\t\tif (attrToken.value !== undefined) {\n+\t\t\t// Try to extract value from content for cases where parsel-js fails (e.g., brackets in value)\n+\t\t\tvalue = this.extractAttributeValue(attrToken.content, escapeMap);\n+\t\t\tif (value === null) {\n+\t\t\t\tvalue = attrToken.value;\n+\t\t\t\t// Restore escape placeholders\n+\t\t\t\tvalue = this.restoreEscapedChars(value, escapeMap);\n+\t\t\t\t// Strip quotes if present\n+\t\t\t\tif (\n+\t\t\t\t\t(value.startsWith('\"') && value.endsWith('\"')) ||\n+\t\t\t\t\t(value.startsWith(\"'\") && value.endsWith(\"'\"))\n+\t\t\t\t) {\n+\t\t\t\t\tvalue = value.slice(1, -1);\n+\t\t\t\t}\n+\t\t\t\t// Remove escape characters\n+\t\t\t\tvalue = value.replace(ESCAPED_CHARACTER_REGEXP, '$1');\n+\t\t\t}\n+\t\t}\n+\n+\t\tlet operator: string | null = null;\n+\t\tif (attrToken.operator) {\n+\t\t\toperator = attrToken.operator.replace('=', '');\n+\t\t\tif (operator === '') {\n+\t\t\t\toperator = null;\n+\t\t\t}\n \t\t}\n+\n+\t\tconst modifier = <'s' | 'i'>attrToken.caseSensitive?.toLowerCase() ?? null;\n+\n+\t\treturn {\n+\t\t\tname: attrName,\n+\t\t\toperator,\n+\t\t\tvalue,\n+\t\t\tmodifier,\n+\t\t\tregExp: this.getAttributeRegExp({ operator, value, modifier })\n+\t\t};\n+\t}\n+\n+\t/**\n+\t * Extracts attribute value from the full content string.\n+\t * This handles cases where parsel-js fails to parse values correctly (e.g., brackets).\n+\t * @param content\n+\t * @param escapeMap\n+\t */\n+\tprivate static extractAttributeValue(\n+\t\tcontent: string,\n+\t\tescapeMap: Map<string, string>\n+\t): string | null {\n+\t\t// Match [name=\"value\"] or [name='value'] or [name=value]\n+\t\t// Also handles empty values like [name=\"\"]\n+\t\tconst match = content.match(/\\[[^\\]]*?=\\s*(['\"])?(.*?)\\1\\s*(?:[iIsS]\\s*)?\\]$/);\n+\t\tif (match) {\n+\t\t\tlet value = match[2];\n+\t\t\tvalue = this.restoreEscapedChars(value, escapeMap);\n+\t\t\tvalue = value.replace(ESCAPED_CHARACTER_REGEXP, '$1');\n+\t\t\treturn value;\n+\t\t}\n+\t\treturn null;\n+\t}\n+\n+\t/**\n+\t * Parses a pseudo-class token into ISelectorPseudo.\n+\t * @param token\n+\t * @param options\n+\t * @param options.scope\n+\t * @param options.ignoreErrors\n+\t * @param escapeMap\n+\t */\n+\tprivate static parsePseudoClass(\n+\t\ttoken: AST,\n+\t\toptions: { scope?: Element | DocumentFragment; ignoreErrors?: boolean } | undefined,\n+\t\tescapeMap: Map<string, string>\n \t): ISelectorPseudo {\n-\t\tconst lowerName = name.toLowerCase();\n+\t\tconst pseudoToken = <{ name: string; argument?: string }>token;\n+\t\tconst name = pseudoToken.name.toLowerCase();\n+\t\tlet args = pseudoToken.argument ?? null;\n \n+\t\t// Restore escape placeholders in arguments\n \t\tif (args) {\n-\t\t\targs = args.trim();\n+\t\t\targs = this.restoreEscapedChars(args, escapeMap);\n \t\t}\n \n \t\tif (!args) {\n-\t\t\treturn { name: lowerName, arguments: null, selectorItems: null, nthFunction: null };\n+\t\t\treturn { name, arguments: null, selectorItems: null, nthFunction: null };\n \t\t}\n \n-\t\tswitch (lowerName) {\n+\t\tswitch (name) {\n \t\t\tcase 'nth-last-child':\n-\t\t\tcase 'nth-child':\n+\t\t\tcase 'nth-child': {\n \t\t\t\tconst nthOfIndex = args.indexOf(' of ');\n \t\t\t\tconst nthFunction = nthOfIndex !== -1 ? args.substring(0, nthOfIndex) : args;\n \t\t\t\tconst selectorItem =\n \t\t\t\t\tnthOfIndex !== -1\n \t\t\t\t\t\t? this.getSelectorItem(args.substring(nthOfIndex + 4).trim(), options)\n \t\t\t\t\t\t: null;\n \t\t\t\treturn {\n-\t\t\t\t\tname: lowerName,\n+\t\t\t\t\tname,\n \t\t\t\t\targuments: args,\n \t\t\t\t\tselectorItems: selectorItem ? [selectorItem] : null,\n \t\t\t\t\tnthFunction: this.getPseudoNthFunction(nthFunction)\n \t\t\t\t};\n+\t\t\t}\n \t\t\tcase 'nth-of-type':\n \t\t\tcase 'nth-last-of-type':\n \t\t\t\treturn {\n-\t\t\t\t\tname: lowerName,\n+\t\t\t\t\tname,\n \t\t\t\t\targuments: args,\n \t\t\t\t\tselectorItems: null,\n \t\t\t\t\tnthFunction: this.getPseudoNthFunction(args)\n \t\t\t\t};\n-\t\t\tcase 'not':\n-\t\t\t\tconst notSelectorItems = [];\n+\t\t\tcase 'not': {\n+\t\t\t\tconst notSelectorItems: SelectorItem[] = [];\n \t\t\t\tfor (const group of this.getSelectorGroups(args, options)) {\n \t\t\t\t\tnotSelectorItems.push(group[0]);\n \t\t\t\t}\n-\t\t\t\treturn {\n-\t\t\t\t\tname: lowerName,\n-\t\t\t\t\targuments: args,\n-\t\t\t\t\tselectorItems: notSelectorItems,\n-\t\t\t\t\tnthFunction: null\n-\t\t\t\t};\n+\t\t\t\treturn { name, arguments: args, selectorItems: notSelectorItems, nthFunction: null };\n+\t\t\t}\n \t\t\tcase 'is':\n-\t\t\tcase 'where':\n-\t\t\t\tconst selectorItems = [];\n+\t\t\tcase 'where': {\n+\t\t\t\tconst selectorItems: SelectorItem[] = [];\n \t\t\t\tfor (const group of this.getSelectorGroups(args, options)) {\n \t\t\t\t\tselectorItems.push(group[0]);\n \t\t\t\t}\n-\t\t\t\treturn {\n-\t\t\t\t\tname: lowerName,\n-\t\t\t\t\targuments: args,\n-\t\t\t\t\tselectorItems,\n-\t\t\t\t\tnthFunction: null\n-\t\t\t\t};\n-\t\t\tcase 'has':\n-\t\t\t\tconst hasSelectorItems = [];\n-\n-\t\t\t\t// The \":has()\" pseudo selector doesn't allow for it to be nested inside another \":has()\" pseudo selector, as it can lead to cyclic querying.\n+\t\t\t\treturn { name, arguments: args, selectorItems, nthFunction: null };\n+\t\t\t}\n+\t\t\tcase 'has': {\n+\t\t\t\tconst hasSelectorItems: SelectorItem[] = [];\n+\t\t\t\t// Trim the arguments to handle cases like \":has( +h2)\"\n+\t\t\t\tconst trimmedArgs = args.trim();\n \t\t\t\tif (!args.includes(':has(')) {\n-\t\t\t\t\tlet newArgs = args;\n-\t\t\t\t\tif (args[0] === '+') {\n-\t\t\t\t\t\tnewArgs = args.replace('+', '');\n-\t\t\t\t\t} else if (args[0] === '>') {\n-\t\t\t\t\t\tnewArgs = args.replace('>', '');\n+\t\t\t\t\tlet newArgs = trimmedArgs;\n+\t\t\t\t\t// Check for combinator at the start (after trimming whitespace)\n+\t\t\t\t\tif (newArgs.startsWith('+')) {\n+\t\t\t\t\t\tnewArgs = newArgs.slice(1).trim();\n+\t\t\t\t\t} else if (newArgs.startsWith('>')) {\n+\t\t\t\t\t\tnewArgs = newArgs.slice(1).trim();\n \t\t\t\t\t}\n \t\t\t\t\tfor (const group of this.getSelectorGroups(newArgs, options)) {\n \t\t\t\t\t\thasSelectorItems.push(group[0]);\n \t\t\t\t\t}\n \t\t\t\t}\n+\t\t\t\t// Store trimmed arguments so SelectorItem can check combinator correctly\n+\t\t\t\treturn { name, arguments: trimmedArgs, selectorItems: hasSelectorItems, nthFunction: null };\n+\t\t\t}\n+\t\t\tdefault:\n+\t\t\t\treturn { name, arguments: args, selectorItems: null, nthFunction: null };\n+\t\t}\n+\t}\n \n-\t\t\t\treturn {\n-\t\t\t\t\tname: lowerName,\n-\t\t\t\t\targuments: args || '',\n-\t\t\t\t\tselectorItems: hasSelectorItems,\n-\t\t\t\t\tnthFunction: null\n-\t\t\t\t};\n-\t\t\tcase 'scope':\n-\t\t\tcase 'root':\n+\t/**\n+\t * Returns attribute RegExp.\n+\t * @param attribute\n+\t * @param attribute.value\n+\t * @param attribute.operator\n+\t * @param attribute.modifier\n+\t */\n+\tprivate static getAttributeRegExp(attribute: {\n+\t\tvalue?: string | null;\n+\t\toperator?: string | null;\n+\t\tmodifier?: string | null;\n+\t}): RegExp | null {\n+\t\tconst modifier = attribute.modifier === 'i' ? 'i' : '';\n+\n+\t\tif (!attribute.operator || !attribute.value) {\n+\t\t\treturn null;\n+\t\t}\n+\n+\t\tconst escapedValue = attribute.value.replace(ATTRIBUTE_ESCAPE_REGEXP, '\\\\$&');\n+\n+\t\tswitch (attribute.operator) {\n+\t\t\tcase '~':\n+\t\t\t\treturn new RegExp(`[- ]${escapedValue}|${escapedValue}[- ]|^${escapedValue}$`, modifier);\n+\t\t\tcase '|':\n+\t\t\t\treturn new RegExp(`^${escapedValue}[- ]|^${escapedValue}$`, modifier);\n+\t\t\tcase '^':\n+\t\t\t\treturn new RegExp(`^${escapedValue}`, modifier);\n+\t\t\tcase '$':\n+\t\t\t\treturn new RegExp(`${escapedValue}$`, modifier);\n+\t\t\tcase '*':\n+\t\t\t\treturn new RegExp(`${escapedValue}`, modifier);\n \t\t\tdefault:\n-\t\t\t\treturn { name: lowerName, arguments: args, selectorItems: null, nthFunction: null };\n+\t\t\t\treturn null;\n \t\t}\n \t}\n \n \t/**\n \t * Returns pseudo nth function.\n-\t *\n-\t * Based on:\n-\t * https://github.com/dperini/nwsapi/blob/master/src/nwsapi.js\n-\t *\n-\t * @param args Pseudo arguments.\n-\t * @returns Pseudo nth function.\n+\t * @param args\n \t */\n \tprivate static getPseudoNthFunction(args?: string): ((n: number) => boolean) | null {\n \t\tif (!args) {\n@@ -295,9 +295,10 @@ describe('QuerySelector', () => {\n \t\t\tcontainer.appendChild(element1);\n \t\t\tcontainer.appendChild(element2);\n \n-\t\t\tconst invalidSelectorElements = container.querySelectorAll('.before:');\n+\t\t\t// Invalid selector \".before:\" should throw (colon without pseudo-class name)\n+\t\t\texpect(() => container.querySelectorAll('.before:')).toThrow();\n+\t\t\t// Valid selector with escaped colon should match\n \t\t\tconst validSelectorElements = container.querySelectorAll('.before\\\\:after');\n-\t\t\texpect(invalidSelectorElements.length).toBe(0);\n \t\t\texpect(validSelectorElements.length).toBe(2);\n \t\t\texpect(validSelectorElements[0] === element1).toBe(true);\n \t\t\texpect(validSelectorElements[1] === element2).toBe(true);\n@@ -329,9 +330,10 @@ describe('QuerySelector', () => {\n \t\t\tcontainer.appendChild(element1);\n \t\t\tcontainer.appendChild(element2);\n \n-\t\t\tconst invalidSelectorElements = container.querySelectorAll('.before&after');\n+\t\t\t// Invalid selector \".before&after\" should throw (& is not valid in CSS selectors)\n+\t\t\texpect(() => container.querySelectorAll('.before&after')).toThrow();\n+\t\t\t// Valid selector with escaped ampersand should match\n \t\t\tconst validSelectorElements = container.querySelectorAll('.before\\\\&after');\n-\t\t\texpect(invalidSelectorElements.length).toBe(0);\n \t\t\texpect(validSelectorElements.length).toBe(2);\n \t\t\texpect(validSelectorElements[0] === element1).toBe(true);\n \t\t\texpect(validSelectorElements[1] === element2).toBe(true);\n@@ -502,6 +504,24 @@ describe('QuerySelector', () => {\n \t\t\texpect(elements[1] === container.children[0].children[1].children[1]).toBe(true);\n \t\t});\n \n+\t\tit('Returns elements with attribute values containing apostrophes using double quotes as delimiter.', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\tcontainer.innerHTML = `<div data-value=\"it's a test\">Content</div>`;\n+\n+\t\t\tconst elements = container.querySelectorAll('[data-value=\"it\\'s a test\"]');\n+\t\t\texpect(elements.length).toBe(1);\n+\t\t\texpect(elements[0] === container.children[0]).toBe(true);\n+\t\t});\n+\n+\t\tit('Returns elements with attribute values containing double quotes using apostrophes as delimiter.', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\tcontainer.innerHTML = `<div data-value='say \"hello\"'>Content</div>`;\n+\n+\t\t\tconst elements = container.querySelectorAll('[data-value=\\'say \"hello\"\\']');\n+\t\t\texpect(elements.length).toBe(1);\n+\t\t\texpect(elements[0] === container.children[0]).toBe(true);\n+\t\t});\n+\n \t\tit('Returns all elements with tag name and matching attributes using \"span[_attr1]\".', () => {\n \t\t\tconst container = document.createElement('div');\n \t\t\tcontainer.innerHTML = QuerySelectorHTML.replace(/ attr1/gm, '_attr1');\n@@ -912,26 +932,32 @@ describe('QuerySelector', () => {\n \t\t\texpect(elements3[1] === container.children[0].children[1].children[2]).toBe(true);\n \t\t});\n \n-\t\tit('Returns all elements matching the selector without ending parenthese \"button:not([type]\"', () => {\n+\t\tit('Throws error for selector without ending parenthesis \"button:not([type]\"', () => {\n \t\t\tconst container = document.createElement('div');\n \t\t\tcontainer.innerHTML = `\n \t\t\t\t<button></button>\n                 <button type=\"submit\"></button>\n                 <button></button>\n \t\t\t`;\n-\t\t\tconst elements = container.querySelectorAll('button:not([type]');\n+\t\t\t// Invalid selector (missing closing parenthesis) should throw\n+\t\t\texpect(() => container.querySelectorAll('button:not([type]')).toThrow();\n+\t\t\t// Valid selector should work\n+\t\t\tconst elements = container.querySelectorAll('button:not([type])');\n \t\t\texpect(elements.length).toBe(2);\n \t\t\texpect(elements[0]).toBe(container.children[0]);\n \t\t\texpect(elements[1]).toBe(container.children[2]);\n \t\t});\n \n-\t\tit('Returns all elements matching the invalid selector \"[q\\\\:shadowroot]\"', () => {\n+\t\tit('Returns all elements matching the selector \"[q\\\\:shadowroot]\" with escaped colon', () => {\n \t\t\tconst container = document.createElement('div');\n-\t\t\tcontainer.innerHTML = `\n-\t\t\t\t<span q\\\\:shadowroot></span>\n-                <button></button>\n-                <article></article>\n-\t\t\t`;\n+\t\t\t// Create element with attribute name containing colon\n+\t\t\tconst span = document.createElement('span');\n+\t\t\tspan.setAttribute('q:shadowroot', '');\n+\t\t\tcontainer.appendChild(span);\n+\t\t\tcontainer.appendChild(document.createElement('button'));\n+\t\t\tcontainer.appendChild(document.createElement('article'));\n+\n+\t\t\t// Selector [q\\:shadowroot] looks for attribute \"q:shadowroot\" (colon is escaped)\n \t\t\tconst elements = container.querySelectorAll('[q\\\\:shadowroot]');\n \t\t\texpect(elements.length).toBe(1);\n \t\t\texpect(elements[0]).toBe(container.children[0]);\n@@ -1290,6 +1316,75 @@ describe('QuerySelector', () => {\n \t\t\t]);\n \t\t});\n \n+\t\tit('Does not throw error when parsing complex CSS :has() expression with nested :not() (issue #1910)', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\tcontainer.innerHTML = `\n+\t\t\t\t<table class=\"emXRrt\">\n+\t\t\t\t\t<thead>\n+\t\t\t\t\t\t<tr>\n+\t\t\t\t\t\t\t<th>Header 1</th>\n+\t\t\t\t\t\t\t<th>Header 2</th>\n+\t\t\t\t\t\t</tr>\n+\t\t\t\t\t</thead>\n+\t\t\t\t\t<tbody>\n+\t\t\t\t\t\t<tr>\n+\t\t\t\t\t\t\t<td>Cell 1</td>\n+\t\t\t\t\t\t\t<td>Cell 2</td>\n+\t\t\t\t\t\t</tr>\n+\t\t\t\t\t\t<tr data-ignore-row>\n+\t\t\t\t\t\t\t<td>Ignored 1</td>\n+\t\t\t\t\t\t\t<td>Ignored 2</td>\n+\t\t\t\t\t\t</tr>\n+\t\t\t\t\t</tbody>\n+\t\t\t\t</table>\n+\t\t\t`;\n+\n+\t\t\t// This selector should not throw an error\n+\t\t\t// The bug was that :not(:has(...)) with nested selectors caused a null reference error\n+\t\t\texpect(() => {\n+\t\t\t\tcontainer.querySelectorAll(\n+\t\t\t\t\t'.emXRrt>tbody>tr>td:nth-child(2),.emXRrt:not(:has(> tbody > tr:not([data-ignore-row])))>thead>tr>th:nth-child(2)'\n+\t\t\t\t);\n+\t\t\t}).not.toThrow();\n+\n+\t\t\t// Verify the selector actually works correctly\n+\t\t\t// First part: .emXRrt>tbody>tr>td:nth-child(2) - matches 2nd td in each tbody row (2 elements)\n+\t\t\t// Second part: .emXRrt:not(:has(> tbody > tr:not([data-ignore-row])))>thead>tr>th:nth-child(2)\n+\t\t\t//   - The table HAS a tbody > tr without data-ignore-row, so :not(:has(...)) should NOT match\n+\t\t\t//   - Therefore the second part should match 0 elements\n+\t\t\tconst elements = container.querySelectorAll(\n+\t\t\t\t'.emXRrt>tbody>tr>td:nth-child(2),.emXRrt:not(:has(> tbody > tr:not([data-ignore-row])))>thead>tr>th:nth-child(2)'\n+\t\t\t);\n+\n+\t\t\t// Should only match the 2 td elements from the first selector part\n+\t\t\texpect(elements.length).toBe(2);\n+\t\t\texpect(elements[0].textContent).toBe('Cell 2');\n+\t\t\texpect(elements[1].textContent).toBe('Ignored 2');\n+\t\t});\n+\n+\t\tit('Correctly parses :not() with nested :has() containing :not() (issue #1910)', () => {\n+\t\t\tconst container = document.createElement('div');\n+\t\t\tcontainer.innerHTML = `\n+\t\t\t\t<div class=\"parent\">\n+\t\t\t\t\t<div class=\"child\" data-keep></div>\n+\t\t\t\t\t<div class=\"child\"></div>\n+\t\t\t\t</div>\n+\t\t\t\t<div class=\"parent no-match\">\n+\t\t\t\t\t<div class=\"child\" data-keep></div>\n+\t\t\t\t</div>\n+\t\t\t`;\n+\n+\t\t\t// Test :not(:has(...)) with nested :not() - the regex must handle nested parentheses\n+\t\t\t// .parent:not(:has(> .child:not([data-keep]))) should match parents that do NOT have\n+\t\t\t// a direct child .child without data-keep attribute\n+\t\t\t// First parent has a .child without data-keep, so it should NOT match\n+\t\t\t// Second parent only has .child with data-keep, so it SHOULD match\n+\t\t\tconst elements = container.querySelectorAll('.parent:not(:has(> .child:not([data-keep])))');\n+\n+\t\t\texpect(elements.length).toBe(1);\n+\t\t\texpect(elements[0].classList.contains('no-match')).toBe(true);\n+\t\t});\n+\n \t\tit('Returns all elements for subsequent sibling selector using \".a ~ .a\"', () => {\n \t\t\tconst div = document.createElement('div');\n "}],"searches":[{"query":"\":not\" \"descendant\"","result":[{"number":2499,"title":"matches() and getComputedStyle() keep a stale :has() result after a descendant changes","url":"https://github.com/capricorn86/happy-dom/issues/2499"},{"number":2488,"title":"getComputedStyle() returns '' for properties nothing sets and for inherited custom properties","url":"https://github.com/capricorn86/happy-dom/issues/2488"},{"number":2489,"title":"matches() and getComputedStyle() still keep a stale result after an ancestor above the parent changes (follow-up to #2442)","url":"https://github.com/capricorn86/happy-dom/issues/2489"},{"number":2170,"title":"Regression (20.3.3): Node.contains() returns false for a genuine descendant of a proxied <form>/<select> when the subtree is assembled detached then attached","url":"https://github.com/capricorn86/happy-dom/issues/2170"},{"number":2230,"title":"`<textarea>` `value`/`defaultValue` leak text from nested element children","url":"https://github.com/capricorn86/happy-dom/issues/2230"},{"number":2156,"title":"Bug: `innerHTML` incorrectly parses `<` followed by a digit as an HTML tag","url":"https://github.com/capricorn86/happy-dom/issues/2156"},{"number":2184,"title":"FormData constructor does not exclude disabled <select> and <textarea> elements","url":"https://github.com/capricorn86/happy-dom/issues/2184"},{"number":2538,"title":"Performance: getComputedStyle() runs QuerySelector.matches() for every style rule, even when the element lacks the rule's subject class","url":"https://github.com/capricorn86/happy-dom/issues/2538"},{"number":2088,"title":"querySelector throws SyntaxError on selectors containing CSS hex escapes (e.g. #\\39 7e...)","url":"https://github.com/capricorn86/happy-dom/issues/2088"},{"number":2081,"title":"[Bug]: Click events on nested <span> inside <button type=\"submit\"> fail to trigger form submission (Activation Behavior regression since v15.0.0)","url":"https://github.com/capricorn86/happy-dom/issues/2081"},{"number":2442,"title":"matches() and getComputedStyle() keep a stale result after an attribute changes on an ancestor other than the parent (e.g. html[data-theme=\"dark\"] span)","url":"https://github.com/capricorn86/happy-dom/issues/2442"},{"number":2253,"title":"Inserting a <form>/<select> that already has children re-points the children's parentNode at the raw target instead of the Proxy","url":"https://github.com/capricorn86/happy-dom/issues/2253"}]},{"query":"\":not\" \"combinator\"","result":[{"number":2489,"title":"matches() and getComputedStyle() still keep a stale result after an ancestor above the parent changes (follow-up to #2442)","url":"https://github.com/capricorn86/happy-dom/issues/2489"},{"number":2518,"title":"getComputedStyle() keeps a stale result for siblings matched with + or ~ after a class change (since 20.14.0)","url":"https://github.com/capricorn86/happy-dom/issues/2518"},{"number":2088,"title":"querySelector throws SyntaxError on selectors containing CSS hex escapes (e.g. #\\39 7e...)","url":"https://github.com/capricorn86/happy-dom/issues/2088"},{"number":2538,"title":"Performance: getComputedStyle() runs QuerySelector.matches() for every style rule, even when the element lacks the rule's subject class","url":"https://github.com/capricorn86/happy-dom/issues/2538"},{"number":2222,"title":"Styled width and height not retrieved by getBoundingClientRect and getClientRects","url":"https://github.com/capricorn86/happy-dom/issues/2222"},{"number":2442,"title":"matches() and getComputedStyle() keep a stale result after an attribute changes on an ancestor other than the parent (e.g. html[data-theme=\"dark\"] span)","url":"https://github.com/capricorn86/happy-dom/issues/2442"},{"number":2078,"title":"`querySelectorAll` with child combinator `>` and universal selector `*` matches descendants instead of direct children","url":"https://github.com/capricorn86/happy-dom/issues/2078"},{"number":1785,"title":"React 19.1's new useId with unicode characters `«` and `»` break selectors","url":"https://github.com/capricorn86/happy-dom/issues/1785"},{"number":1683,"title":"getByRole doesn't work - CSS subsequent sibling combinator (~) ","url":"https://github.com/capricorn86/happy-dom/issues/1683"},{"number":792,"title":"Implementing Adjacent Sibling Selectors","url":"https://github.com/capricorn86/happy-dom/issues/792"}]},{"query":"\"group[0]\" \"not\"","result":[{"number":1927,"title":"Jest 30, clearImmediate missing","url":"https://github.com/capricorn86/happy-dom/issues/1927"}]},{"query":"\"complex\" in:title","result":[{"number":1910,"title":"Error when parsing complex CSS has expression","url":"https://github.com/capricorn86/happy-dom/issues/1910"}]}]}