@asamuzakjp/dom-selector 8.0.2 → 8.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/js/finder.js CHANGED
@@ -22,11 +22,13 @@ import {
22
22
  import { createHasValidator, isInvalidCombinator } from './selector.js';
23
23
  import {
24
24
  filterNodesByAnB,
25
+ findBestSeed,
25
26
  generateException,
26
27
  isCustomElement,
27
28
  isFocusVisible,
28
29
  isFocusableArea,
29
30
  isVisible,
31
+ populateHasAllowlist,
30
32
  resolveContent,
31
33
  sortNodes,
32
34
  traverseNode
@@ -40,6 +42,7 @@ import {
40
42
  DOCUMENT_FRAGMENT_NODE,
41
43
  ELEMENT_NODE,
42
44
  FORM_PARTS,
45
+ HEX,
43
46
  ID_SELECTOR,
44
47
  INPUT_CHECK,
45
48
  INPUT_DATE,
@@ -59,6 +62,8 @@ import {
59
62
  TEXT_NODE,
60
63
  TYPE_SELECTOR
61
64
  } from './constant.js';
65
+ const ANB_FIRST = { a: 0, b: 1 };
66
+ const ANB_LAST = { a: 0, b: 1, reverse: true };
62
67
  const DIR_NEXT = 'next';
63
68
  const DIR_PREV = 'prev';
64
69
  const KEYS_FORM = new Set([...FORM_PARTS, 'fieldset', 'form']);
@@ -104,17 +109,20 @@ const KEYS_PS_NTH_OF_TYPE = new Set([
104
109
  */
105
110
  export class Finder {
106
111
  /* private fields */
112
+ #anbCache;
107
113
  #ast;
108
- #astCache;
114
+ #astCache = new WeakMap();
109
115
  #check;
116
+ #combinatorCache;
110
117
  #descendant;
111
118
  #document;
112
- #documentCache;
119
+ #documentCache = new WeakMap();
113
120
  #documentURL;
114
121
  #event;
115
122
  #eventHandlers;
116
123
  #filterLeavesCache;
117
124
  #focus;
125
+ #focusWithinCache;
118
126
  #invalidate;
119
127
  #invalidateResults;
120
128
  #lastFocusVisible;
@@ -122,6 +130,17 @@ export class Finder {
122
130
  #nodeWalker;
123
131
  #nodes;
124
132
  #noexcept;
133
+ #nthChildCache;
134
+ #nthChildOfCache;
135
+ #nthChildResultCache;
136
+ #nthOfTypeCache;
137
+ #nthOfTypeResultCache;
138
+ #psDefaultCache;
139
+ #psDirCache;
140
+ #psHasFilterCache;
141
+ #psIndeterminateCache;
142
+ #psLangCache;
143
+ #psValidCache;
125
144
  #pseudoElement;
126
145
  #results;
127
146
  #root;
@@ -130,6 +149,7 @@ export class Finder {
130
149
  #selector;
131
150
  #selectorAST;
132
151
  #shadow;
152
+ #targetWithinCache;
133
153
  #verifyShadowHost;
134
154
  #walkers;
135
155
  #warn;
@@ -141,11 +161,6 @@ export class Finder {
141
161
  */
142
162
  constructor(window) {
143
163
  this.#window = window;
144
- this.#astCache = new WeakMap();
145
- this.#documentCache = new WeakMap();
146
- this.#event = null;
147
- this.#focus = null;
148
- this.#lastFocusVisible = null;
149
164
  this.#eventHandlers = new Set([
150
165
  {
151
166
  keys: ['focus', 'focusin'],
@@ -160,7 +175,6 @@ export class Finder {
160
175
  handler: this._handleMouseEvent
161
176
  }
162
177
  ]);
163
- this.#filterLeavesCache = new WeakMap();
164
178
  this._registerEventListeners();
165
179
  this.clearResults(true);
166
180
  }
@@ -217,7 +231,7 @@ export class Finder {
217
231
  this.#node !== this.#root && this.#node.nodeType === ELEMENT_NODE;
218
232
  this.#selector = selector;
219
233
  this.#pseudoElement = [];
220
- this.#walkers = new WeakMap();
234
+ this.#walkers = null;
221
235
  this.#nodeWalker = null;
222
236
  this.#rootWalker = null;
223
237
  this.#verifyShadowHost = null;
@@ -227,14 +241,29 @@ export class Finder {
227
241
 
228
242
  /**
229
243
  * Clear cached results.
230
- * @param {boolean} all - clear all results
244
+ * @param {boolean} all - Clear all results.
231
245
  * @returns {void}
232
246
  */
233
247
  clearResults = (all = false) => {
234
- this.#invalidateResults = new WeakMap();
248
+ this.#anbCache = null;
249
+ this.#combinatorCache = null;
250
+ this.#focusWithinCache = null;
251
+ this.#invalidateResults = null;
252
+ this.#nthChildCache = null;
253
+ this.#nthChildOfCache = null;
254
+ this.#nthChildResultCache = null;
255
+ this.#nthOfTypeCache = null;
256
+ this.#nthOfTypeResultCache = null;
257
+ this.#psDefaultCache = null;
258
+ this.#psDirCache = null;
259
+ this.#psHasFilterCache = null;
260
+ this.#psIndeterminateCache = null;
261
+ this.#psLangCache = null;
262
+ this.#psValidCache = null;
263
+ this.#targetWithinCache = null;
235
264
  if (all) {
265
+ this.#filterLeavesCache = null;
236
266
  this.#results = new WeakMap();
237
- this.#filterLeavesCache = new WeakMap();
238
267
  }
239
268
  };
240
269
 
@@ -299,22 +328,20 @@ export class Finder {
299
328
  * @private
300
329
  * @param {Array.<Array.<object>>} branches - The branches from walkAST.
301
330
  * @param {string} selector - The original selector for error reporting.
302
- * @returns {{ast: Array, descendant: boolean}}
303
- * An object with the AST, descendant flag.
331
+ * @returns {{ast: Array, descendant: boolean}} An object with the AST, descendant flag.
304
332
  */
305
333
  _processSelectorBranches = (branches, selector) => {
306
334
  let descendant = false;
307
335
  const ast = [];
308
- const l = branches.length;
309
- for (let i = 0; i < l; i++) {
310
- const items = [...branches[i]];
336
+ for (const items of branches) {
311
337
  const branch = [];
312
338
  let prevType = null;
313
- let item = items.shift();
314
- if (item) {
339
+ const itemsLen = items.length;
340
+ if (itemsLen) {
315
341
  const leaves = new Set();
316
- while (item) {
317
- const isLast = items.length === 0;
342
+ for (let j = 0; j < itemsLen; j++) {
343
+ const item = items[j];
344
+ const isLast = j === itemsLen - 1;
318
345
  if (isInvalidCombinator(item.type, prevType, isLast)) {
319
346
  const msg = `Invalid selector ${selector}`;
320
347
  this.onError(generateException(msg, SYNTAX_ERR, this.#window));
@@ -339,12 +366,9 @@ export class Finder {
339
366
  leaves.add(item);
340
367
  }
341
368
  prevType = item.type;
342
- if (!isLast) {
343
- item = items.shift();
344
- } else {
369
+ if (isLast) {
345
370
  branch.push({ combo: null, leaves: sortAST(leaves) });
346
371
  leaves.clear();
347
- break;
348
372
  }
349
373
  }
350
374
  }
@@ -438,7 +462,11 @@ export class Finder {
438
462
  const { force = false, whatToShow = SHOW_CONTAINER } = opt;
439
463
  if (force) {
440
464
  return this.#document.createTreeWalker(node, whatToShow);
441
- } else if (this.#walkers.has(node)) {
465
+ }
466
+ if (!this.#walkers) {
467
+ this.#walkers = new WeakMap();
468
+ }
469
+ if (this.#walkers.has(node)) {
442
470
  return this.#walkers.get(node);
443
471
  }
444
472
  const walker = this.#document.createTreeWalker(node, whatToShow);
@@ -505,7 +533,7 @@ export class Finder {
505
533
  * @param {number} anb.a - The 'a' value.
506
534
  * @param {number} anb.b - The 'b' value.
507
535
  * @param {boolean} [anb.reverse] - If true, reverses the order.
508
- * @param {object} [anb.selector] - The AST.
536
+ * @param {object} [anb.selector] - The selector for 'of S'.
509
537
  * @param {object} node - The Element node.
510
538
  * @param {object} opt - Options.
511
539
  * @returns {Set.<object>} A collection of matched nodes.
@@ -532,16 +560,48 @@ export class Finder {
532
560
  }
533
561
  return matchedNode;
534
562
  }
535
- const selectorBranches = selector
536
- ? this._getSelectorBranches(selector)
537
- : null;
538
- const children = this._getFilteredChildren(
539
- parentNode,
540
- selectorBranches,
541
- opt
542
- );
543
- const matchedNodes = filterNodesByAnB(children, anb);
544
- return new Set(matchedNodes);
563
+ if (!this.#nthChildResultCache) {
564
+ this.#nthChildResultCache = new WeakMap();
565
+ }
566
+ let parentResultCache = this.#nthChildResultCache.get(parentNode);
567
+ if (!parentResultCache) {
568
+ parentResultCache = new WeakMap();
569
+ this.#nthChildResultCache.set(parentNode, parentResultCache);
570
+ }
571
+ const cachedSet = parentResultCache.get(anb);
572
+ if (cachedSet) {
573
+ return cachedSet;
574
+ }
575
+ let siblings;
576
+ if (selector) {
577
+ if (!this.#nthChildOfCache) {
578
+ this.#nthChildOfCache = new WeakMap();
579
+ }
580
+ let parentOfCacheMap = this.#nthChildOfCache.get(parentNode);
581
+ if (!parentOfCacheMap) {
582
+ parentOfCacheMap = new Map();
583
+ this.#nthChildOfCache.set(parentNode, parentOfCacheMap);
584
+ }
585
+ siblings = parentOfCacheMap.get(selector);
586
+ if (!siblings) {
587
+ const selectorBranches = this._getSelectorBranches(selector);
588
+ siblings = this._getFilteredChildren(parentNode, selectorBranches, opt);
589
+ parentOfCacheMap.set(selector, siblings);
590
+ }
591
+ } else {
592
+ if (!this.#nthChildCache) {
593
+ this.#nthChildCache = new WeakMap();
594
+ }
595
+ siblings = this.#nthChildCache.get(parentNode);
596
+ if (!siblings) {
597
+ siblings = this._getFilteredChildren(parentNode, null, opt);
598
+ this.#nthChildCache.set(parentNode, siblings);
599
+ }
600
+ }
601
+ const matchedNodes = filterNodesByAnB(siblings, anb);
602
+ const resultSet = new Set(matchedNodes);
603
+ parentResultCache.set(anb, resultSet);
604
+ return resultSet;
545
605
  };
546
606
 
547
607
  /**
@@ -555,27 +615,59 @@ export class Finder {
555
615
  * @returns {Set.<object>} A collection of matched nodes.
556
616
  */
557
617
  _collectNthOfType = (anb, node) => {
558
- const { parentNode } = node;
618
+ const { localName, namespaceURI, parentNode, prefix } = node;
559
619
  if (!parentNode) {
560
620
  if (node === this.#root && anb.a * 1 + anb.b * 1 === 1) {
561
621
  return new Set([node]);
562
622
  }
563
623
  return new Set();
564
624
  }
565
- const typedSiblings = [];
566
- let sibling = parentNode.firstElementChild;
567
- while (sibling) {
568
- if (
569
- sibling.localName === node.localName &&
570
- sibling.namespaceURI === node.namespaceURI &&
571
- sibling.prefix === node.prefix
572
- ) {
573
- typedSiblings.push(sibling);
625
+ if (!this.#nthOfTypeResultCache) {
626
+ this.#nthOfTypeResultCache = new WeakMap();
627
+ }
628
+ let parentResultCache = this.#nthOfTypeResultCache.get(parentNode);
629
+ if (!parentResultCache) {
630
+ parentResultCache = new WeakMap();
631
+ this.#nthOfTypeResultCache.set(parentNode, parentResultCache);
632
+ }
633
+ let typeResultMap = parentResultCache.get(anb);
634
+ if (!typeResultMap) {
635
+ typeResultMap = new Map();
636
+ parentResultCache.set(anb, typeResultMap);
637
+ }
638
+ const typeKey = `${namespaceURI || ''}|${prefix || ''}|${localName}`;
639
+ const cachedSet = typeResultMap.get(typeKey);
640
+ if (cachedSet) {
641
+ return cachedSet;
642
+ }
643
+ if (!this.#nthOfTypeCache) {
644
+ this.#nthOfTypeCache = new WeakMap();
645
+ }
646
+ let typeMap = this.#nthOfTypeCache.get(parentNode);
647
+ if (!typeMap) {
648
+ typeMap = new Map();
649
+ this.#nthOfTypeCache.set(parentNode, typeMap);
650
+ }
651
+ let typedSiblings = typeMap.get(typeKey);
652
+ if (!typedSiblings) {
653
+ typedSiblings = [];
654
+ let sibling = parentNode.firstElementChild;
655
+ while (sibling) {
656
+ if (
657
+ sibling.localName === localName &&
658
+ sibling.namespaceURI === namespaceURI &&
659
+ sibling.prefix === prefix
660
+ ) {
661
+ typedSiblings.push(sibling);
662
+ }
663
+ sibling = sibling.nextElementSibling;
574
664
  }
575
- sibling = sibling.nextElementSibling;
665
+ typeMap.set(typeKey, typedSiblings);
576
666
  }
577
667
  const matchedNodes = filterNodesByAnB(typedSiblings, anb);
578
- return new Set(matchedNodes);
668
+ const resultSet = new Set(matchedNodes);
669
+ typeResultMap.set(typeKey, resultSet);
670
+ return resultSet;
579
671
  };
580
672
 
581
673
  /**
@@ -585,53 +677,61 @@ export class Finder {
585
677
  * @param {object} node - The Element node.
586
678
  * @param {string} nthName - The name of the nth pseudo-class.
587
679
  * @param {object} opt - Options.
588
- * @returns {Set.<object>} A collection of matched nodes.
680
+ * @returns {boolean} True if matches, otherwise false.
589
681
  */
590
682
  _matchAnPlusB = (ast, node, nthName, opt) => {
591
- const {
592
- nth: { a, b, name: nthIdentName },
593
- selector
594
- } = ast;
595
- const anbMap = new Map();
596
- if (nthIdentName) {
597
- if (nthIdentName === 'even') {
598
- anbMap.set('a', 2);
599
- anbMap.set('b', 0);
600
- } else if (nthIdentName === 'odd') {
601
- anbMap.set('a', 2);
602
- anbMap.set('b', 1);
603
- }
604
- if (nthName.indexOf('last') > -1) {
605
- anbMap.set('reverse', true);
606
- }
607
- } else {
608
- if (typeof a === 'string' && /-?\d+/.test(a)) {
609
- anbMap.set('a', a * 1);
610
- } else {
611
- anbMap.set('a', 0);
612
- }
613
- if (typeof b === 'string' && /-?\d+/.test(b)) {
614
- anbMap.set('b', b * 1);
683
+ if (!this.#anbCache) {
684
+ this.#anbCache = new WeakMap();
685
+ }
686
+ let anb = this.#anbCache.get(ast);
687
+ if (!anb) {
688
+ const {
689
+ nth: { a, b, name: nthIdentName },
690
+ selector
691
+ } = ast;
692
+ const anbMap = new Map();
693
+ if (nthIdentName) {
694
+ if (nthIdentName === 'even') {
695
+ anbMap.set('a', 2);
696
+ anbMap.set('b', 0);
697
+ } else if (nthIdentName === 'odd') {
698
+ anbMap.set('a', 2);
699
+ anbMap.set('b', 1);
700
+ }
701
+ if (nthName.indexOf('last') > -1) {
702
+ anbMap.set('reverse', true);
703
+ }
615
704
  } else {
616
- anbMap.set('b', 0);
705
+ if (typeof a === 'string' && /-?\d+/.test(a)) {
706
+ anbMap.set('a', a * 1);
707
+ } else {
708
+ anbMap.set('a', 0);
709
+ }
710
+ if (typeof b === 'string' && /-?\d+/.test(b)) {
711
+ anbMap.set('b', b * 1);
712
+ } else {
713
+ anbMap.set('b', 0);
714
+ }
715
+ if (nthName.indexOf('last') > -1) {
716
+ anbMap.set('reverse', true);
717
+ }
617
718
  }
618
- if (nthName.indexOf('last') > -1) {
619
- anbMap.set('reverse', true);
719
+ if (nthName === 'nth-child' || nthName === 'nth-last-child') {
720
+ if (selector) {
721
+ anbMap.set('selector', selector);
722
+ }
620
723
  }
724
+ anb = Object.fromEntries(anbMap);
725
+ this.#anbCache.set(ast, anb);
621
726
  }
622
727
  if (nthName === 'nth-child' || nthName === 'nth-last-child') {
623
- if (selector) {
624
- anbMap.set('selector', selector);
625
- }
626
- const anb = Object.fromEntries(anbMap);
627
728
  const nodes = this._collectNthChild(anb, node, opt);
628
- return nodes;
729
+ return nodes.has(node);
629
730
  } else if (nthName === 'nth-of-type' || nthName === 'nth-last-of-type') {
630
- const anb = Object.fromEntries(anbMap);
631
731
  const nodes = this._collectNthOfType(anb, node);
632
- return nodes;
732
+ return nodes.has(node);
633
733
  }
634
- return new Set();
734
+ return false;
635
735
  };
636
736
 
637
737
  /**
@@ -643,55 +743,113 @@ export class Finder {
643
743
  * @returns {boolean} The result.
644
744
  */
645
745
  _matchHasPseudoFunc = (astLeaves, node, opt = {}) => {
646
- if (Array.isArray(astLeaves) && astLeaves.length) {
647
- // Prepare a copy to avoid astLeaves being consumed.
648
- const leaves = [...astLeaves];
649
- const [leaf] = leaves;
650
- const { type: leafType } = leaf;
651
- let combo;
652
- if (leafType === COMBINATOR) {
653
- combo = leaves.shift();
654
- } else {
655
- combo = {
656
- name: ' ',
657
- type: COMBINATOR
658
- };
659
- }
660
- const twigLeaves = [];
661
- while (leaves.length) {
662
- const [item] = leaves;
663
- const { type: itemType } = item;
664
- if (itemType === COMBINATOR) {
665
- break;
666
- } else {
667
- twigLeaves.push(leaves.shift());
668
- }
669
- }
670
- const twig = {
671
- combo,
672
- leaves: twigLeaves
746
+ const l = astLeaves.length;
747
+ if (!l) {
748
+ return false;
749
+ }
750
+ let startIndex = 0;
751
+ let combo;
752
+ if (astLeaves[0].type === COMBINATOR) {
753
+ combo = astLeaves[0];
754
+ startIndex = 1;
755
+ } else {
756
+ combo = {
757
+ name: ' ',
758
+ type: COMBINATOR
673
759
  };
674
- opt.dir = DIR_NEXT;
675
- const nodes = this._collectCombinatorMatches(twig, node, opt, []);
676
- if (nodes.length) {
677
- if (leaves.length) {
678
- let bool = false;
679
- for (const nextNode of nodes) {
680
- bool = this._matchHasPseudoFunc(leaves, nextNode, opt);
681
- if (bool) {
682
- break;
683
- }
760
+ startIndex = 0;
761
+ }
762
+ const twigLeaves = [];
763
+ let nextComboIndex = startIndex;
764
+ for (; nextComboIndex < l; nextComboIndex++) {
765
+ if (astLeaves[nextComboIndex].type === COMBINATOR) {
766
+ break;
767
+ }
768
+ twigLeaves.push(astLeaves[nextComboIndex]);
769
+ }
770
+ const twig = {
771
+ combo,
772
+ leaves: twigLeaves
773
+ };
774
+ opt.dir = DIR_NEXT;
775
+ const nodes = this._collectCombinatorMatches(twig, node, opt, []);
776
+ if (nodes.length) {
777
+ if (nextComboIndex < l) {
778
+ let bool = false;
779
+ const remainingLeaves = astLeaves.slice(nextComboIndex);
780
+ for (const nextNode of nodes) {
781
+ bool = this._matchHasPseudoFunc(remainingLeaves, nextNode, opt);
782
+ if (bool) {
783
+ break;
684
784
  }
685
- return bool;
686
785
  }
687
- return true;
786
+ return bool;
688
787
  }
788
+ return true;
689
789
  }
690
790
  return false;
691
791
  };
692
792
 
693
793
  /**
694
- * Evaluates the :has() pseudo-class.
794
+ * Builds an Allowlist for the :has() branch using a sparse seed element.
795
+ * @private
796
+ * @param {Array} leaves - The AST leaves of the selector branch.
797
+ * @returns {object|null} The wrapper object containing the WeakSet, or null.
798
+ */
799
+ _buildHasAllowlist = leaves => {
800
+ const { seed } = findBestSeed(leaves);
801
+ if (!seed) {
802
+ return null;
803
+ }
804
+ if (this.#shadow || this.#node.nodeType === DOCUMENT_FRAGMENT_NODE) {
805
+ return null;
806
+ }
807
+ let seedElements = null;
808
+ let isSingleNode = false;
809
+ if (seed.type === 'id') {
810
+ if (typeof this.#root.getElementById === 'function') {
811
+ const node = this.#root.getElementById(seed.value);
812
+ if (node) {
813
+ seedElements = node;
814
+ isSingleNode = true;
815
+ }
816
+ }
817
+ } else if (seed.type === 'class') {
818
+ if (typeof this.#root.getElementsByClassName === 'function') {
819
+ seedElements = this.#root.getElementsByClassName(seed.value);
820
+ }
821
+ } else if (seed.type === 'tag') {
822
+ if (typeof this.#root.getElementsByTagName === 'function') {
823
+ seedElements = this.#root.getElementsByTagName(seed.value);
824
+ }
825
+ }
826
+ if (!seedElements) {
827
+ return null;
828
+ }
829
+ const len = isSingleNode ? 1 : seedElements.length;
830
+ if (len === 0 || len > HEX * HEX) {
831
+ return null;
832
+ }
833
+ const filterResult = {
834
+ seeded: true,
835
+ set: new WeakSet()
836
+ };
837
+ const list = filterResult.set;
838
+ const visitedAncestors = new Set();
839
+ if (this.#node) {
840
+ list.add(this.#node);
841
+ }
842
+ for (let i = 0; i < len; i++) {
843
+ const current = isSingleNode ? seedElements : seedElements[i];
844
+ if (current) {
845
+ populateHasAllowlist(current, list, visitedAncestors);
846
+ }
847
+ }
848
+ return filterResult;
849
+ };
850
+
851
+ /**
852
+ * Evaluates :has() pseudo-class.
695
853
  * @private
696
854
  * @param {object} astData - The AST data.
697
855
  * @param {object} node - The Element node.
@@ -701,9 +859,28 @@ export class Finder {
701
859
  _evaluateHasPseudo = (astData, node, opt = {}) => {
702
860
  const { branches } = astData;
703
861
  let bool = false;
704
- const l = branches.length;
705
- for (let i = 0; i < l; i++) {
706
- const leaves = branches[i];
862
+ if (!this.#psHasFilterCache) {
863
+ this.#psHasFilterCache = new WeakMap();
864
+ }
865
+ let rootCache = this.#psHasFilterCache.get(this.#root);
866
+ if (!rootCache) {
867
+ rootCache = new WeakMap();
868
+ this.#psHasFilterCache.set(this.#root, rootCache);
869
+ }
870
+ for (const leaves of branches) {
871
+ if (!rootCache.has(leaves)) {
872
+ const filterResult = this._buildHasAllowlist(leaves);
873
+ rootCache.set(leaves, filterResult);
874
+ }
875
+ const allowlist = rootCache.get(leaves);
876
+ if (
877
+ allowlist &&
878
+ allowlist.seeded &&
879
+ node.nodeType !== DOCUMENT_FRAGMENT_NODE &&
880
+ !allowlist.set.has(node)
881
+ ) {
882
+ continue;
883
+ }
707
884
  bool = this._matchHasPseudoFunc(leaves, node, opt);
708
885
  if (bool) {
709
886
  break;
@@ -727,13 +904,13 @@ export class Finder {
727
904
  * @param {object} astData - The AST data.
728
905
  * @param {object} node - The Element node.
729
906
  * @param {object} [opt] - Options.
730
- * @returns {?object} The matched node.
907
+ * @returns {boolean} Tru if matches, otherwise false.
731
908
  */
732
909
  _matchLogicalPseudoFunc = (astData, node, opt = {}) => {
733
910
  const { astName, branches, twigBranches } = astData;
734
911
  // Handle :has().
735
912
  if (astName === 'has') {
736
- return this._evaluateHasPseudo(astData, node, opt);
913
+ return this._evaluateHasPseudo(astData, node, opt) === node;
737
914
  }
738
915
  // Handle :is(), :not(), :where().
739
916
  const isShadowRoot =
@@ -755,7 +932,7 @@ export class Finder {
755
932
  }
756
933
  }
757
934
  if (invalid) {
758
- return null;
935
+ return false;
759
936
  }
760
937
  }
761
938
  opt.forgive = astName === 'is' || astName === 'where';
@@ -792,635 +969,376 @@ export class Finder {
792
969
  }
793
970
  }
794
971
  if (astName === 'not') {
795
- if (bool) {
796
- return null;
797
- }
798
- return node;
799
- } else if (bool) {
800
- return node;
972
+ return !bool;
801
973
  }
802
- return null;
974
+ return bool;
803
975
  };
804
976
 
805
977
  /**
806
- * Matches pseudo-class selector.
978
+ * Evaluates logical pseudo-class selector.
807
979
  * @private
808
- * @see https://html.spec.whatwg.org/#pseudo-classes
809
980
  * @param {object} ast - The AST.
810
981
  * @param {object} node - The Element node.
811
982
  * @param {object} [opt] - Options.
812
983
  * @param {boolean} [opt.forgive] - Ignores unknown or invalid selectors.
813
984
  * @param {boolean} [opt.warn] - If true, console warnings are enabled.
814
- * @returns {Set.<object>} A collection of matched nodes.
985
+ * @returns {boolean} True if matches, otherwise false.
815
986
  */
816
- _matchPseudoClassSelector(ast, node, opt = {}) {
987
+ _evaluateLogicalPseudo(ast, node, opt = {}) {
817
988
  const { children: astChildren, name: astName } = ast;
818
- const { localName, parentNode } = node;
819
- const { forgive, warn = this.#warn } = opt;
820
- const matched = new Set();
821
- // :has(), :is(), :not(), :where()
822
- if (Array.isArray(astChildren) && KEYS_LOGICAL.has(astName)) {
823
- if (!astChildren.length && astName !== 'is' && astName !== 'where') {
824
- const css = generateCSS(ast);
825
- const msg = `Invalid selector ${css}`;
826
- return this.onError(generateException(msg, SYNTAX_ERR, this.#window));
827
- }
828
- let astData;
829
- if (this.#astCache.has(ast)) {
830
- astData = this.#astCache.get(ast);
989
+ if (!astChildren.length && astName !== 'is' && astName !== 'where') {
990
+ const css = generateCSS(ast);
991
+ const msg = `Invalid selector ${css}`;
992
+ this.onError(generateException(msg, SYNTAX_ERR, this.#window));
993
+ return false;
994
+ }
995
+ let astData;
996
+ if (this.#astCache.has(ast)) {
997
+ astData = this.#astCache.get(ast);
998
+ } else {
999
+ const { branches } = walkAST(ast);
1000
+ if (astName === 'has') {
1001
+ astData = {
1002
+ astName,
1003
+ branches
1004
+ };
831
1005
  } else {
832
- const { branches } = walkAST(ast);
833
- if (astName === 'has') {
834
- astData = {
835
- astName,
836
- branches
837
- };
838
- } else {
839
- const twigBranches = [];
840
- const l = branches.length;
841
- for (let i = 0; i < l; i++) {
842
- const [...leaves] = branches[i];
843
- const branch = [];
844
- const leavesSet = new Set();
845
- let item = leaves.shift();
846
- while (item) {
847
- if (item.type === COMBINATOR) {
848
- branch.push({
849
- combo: item,
850
- leaves: [...leavesSet]
851
- });
852
- leavesSet.clear();
853
- } else if (item) {
854
- leavesSet.add(item);
855
- }
856
- if (leaves.length) {
857
- item = leaves.shift();
858
- } else {
859
- branch.push({
860
- combo: null,
861
- leaves: [...leavesSet]
862
- });
863
- leavesSet.clear();
864
- break;
865
- }
1006
+ const twigBranches = [];
1007
+ const l = branches.length;
1008
+ for (let i = 0; i < l; i++) {
1009
+ const leaves = branches[i];
1010
+ const branch = [];
1011
+ const leavesSet = new Set();
1012
+ const leavesLen = leaves.length;
1013
+ for (let j = 0; j < leavesLen; j++) {
1014
+ const item = leaves[j];
1015
+ if (item.type === COMBINATOR) {
1016
+ branch.push({
1017
+ combo: item,
1018
+ leaves: [...leavesSet]
1019
+ });
1020
+ leavesSet.clear();
1021
+ } else {
1022
+ leavesSet.add(item);
1023
+ }
1024
+ if (j === leavesLen - 1) {
1025
+ branch.push({
1026
+ combo: null,
1027
+ leaves: [...leavesSet]
1028
+ });
1029
+ leavesSet.clear();
866
1030
  }
867
- twigBranches.push(branch);
868
1031
  }
869
- astData = {
870
- astName,
871
- branches,
872
- twigBranches
873
- };
874
- this.#astCache.set(ast, astData);
1032
+ twigBranches.push(branch);
875
1033
  }
1034
+ astData = {
1035
+ astName,
1036
+ branches,
1037
+ twigBranches
1038
+ };
876
1039
  }
877
- const res = this._matchLogicalPseudoFunc(astData, node, opt);
878
- if (res) {
879
- matched.add(res);
1040
+ this.#astCache.set(ast, astData);
1041
+ }
1042
+ return this._matchLogicalPseudoFunc(astData, node, opt);
1043
+ }
1044
+
1045
+ /**
1046
+ * Evaluates pseudo-class function.
1047
+ * @private
1048
+ * @see https://html.spec.whatwg.org/#pseudo-classes
1049
+ * @param {object} ast - The AST.
1050
+ * @param {object} node - The Element node.
1051
+ * @param {object} [opt] - Options.
1052
+ * @param {boolean} [opt.forgive] - Ignores unknown or invalid selectors.
1053
+ * @param {boolean} [opt.warn] - If true, console warnings are enabled.
1054
+ * @returns {boolean} True if matches, otherwise false.
1055
+ */
1056
+ _evaluatePseudoClassFunc(ast, node, opt = {}) {
1057
+ const { children: astChildren, name: astName } = ast;
1058
+ const { forgive, warn = this.#warn } = opt;
1059
+ // :nth-child(), :nth-last-child(), nth-of-type(), :nth-last-of-type()
1060
+ if (/^nth-(?:last-)?(?:child|of-type)$/.test(astName)) {
1061
+ if (astChildren.length !== 1) {
1062
+ const css = generateCSS(ast);
1063
+ this.onError(
1064
+ generateException(`Invalid selector ${css}`, SYNTAX_ERR, this.#window)
1065
+ );
1066
+ return false;
880
1067
  }
881
- } else if (Array.isArray(astChildren)) {
882
- // :nth-child(), :nth-last-child(), nth-of-type(), :nth-last-of-type()
883
- if (/^nth-(?:last-)?(?:child|of-type)$/.test(astName)) {
1068
+ const [branch] = astChildren;
1069
+ return this._matchAnPlusB(branch, node, astName, opt);
1070
+ }
1071
+ switch (astName) {
1072
+ // :dir()
1073
+ case 'dir': {
884
1074
  if (astChildren.length !== 1) {
885
1075
  const css = generateCSS(ast);
886
- return this.onError(
1076
+ this.onError(
887
1077
  generateException(
888
1078
  `Invalid selector ${css}`,
889
1079
  SYNTAX_ERR,
890
1080
  this.#window
891
1081
  )
892
1082
  );
1083
+ return false;
893
1084
  }
894
- const [branch] = astChildren;
895
- const nodes = this._matchAnPlusB(branch, node, astName, opt);
896
- return nodes;
897
- } else {
898
- switch (astName) {
899
- // :dir()
900
- case 'dir': {
901
- if (astChildren.length !== 1) {
902
- const css = generateCSS(ast);
903
- return this.onError(
904
- generateException(
905
- `Invalid selector ${css}`,
906
- SYNTAX_ERR,
907
- this.#window
908
- )
909
- );
910
- }
911
- const [astChild] = astChildren;
912
- const res = matchDirectionPseudoClass(astChild, node);
913
- if (res) {
914
- matched.add(node);
915
- }
916
- break;
917
- }
918
- // :lang()
919
- case 'lang': {
920
- if (!astChildren.length) {
921
- const css = generateCSS(ast);
922
- return this.onError(
923
- generateException(
924
- `Invalid selector ${css}`,
925
- SYNTAX_ERR,
926
- this.#window
927
- )
928
- );
929
- }
930
- let bool;
931
- for (const astChild of astChildren) {
932
- bool = matchLanguagePseudoClass(astChild, node);
933
- if (bool) {
934
- break;
935
- }
936
- }
937
- if (bool) {
938
- matched.add(node);
939
- }
940
- break;
941
- }
942
- // :state()
943
- case 'state': {
944
- if (isCustomElement(node)) {
945
- const [{ value: stateValue }] = astChildren;
946
- if (stateValue) {
947
- if (node[stateValue]) {
948
- matched.add(node);
949
- } else {
950
- for (const i in node) {
951
- const prop = node[i];
952
- if (prop instanceof this.#window.ElementInternals) {
953
- if (prop?.states?.has(stateValue)) {
954
- matched.add(node);
955
- }
956
- break;
957
- }
958
- }
959
- }
960
- }
961
- }
962
- break;
963
- }
964
- case 'current':
965
- case 'heading':
966
- case 'nth-col':
967
- case 'nth-last-col': {
968
- if (warn) {
969
- this.onError(
970
- generateException(
971
- `Unsupported pseudo-class :${astName}()`,
972
- NOT_SUPPORTED_ERR,
973
- this.#window
974
- )
975
- );
976
- }
977
- break;
978
- }
979
- // Ignore :host() and :host-context().
980
- case 'host':
981
- case 'host-context': {
982
- break;
983
- }
984
- // Deprecated in CSS Selectors 3.
985
- case 'contains': {
986
- if (warn) {
987
- this.onError(
988
- generateException(
989
- `Unknown pseudo-class :${astName}()`,
990
- NOT_SUPPORTED_ERR,
991
- this.#window
992
- )
993
- );
994
- }
995
- break;
996
- }
997
- default: {
998
- if (!forgive) {
999
- this.onError(
1000
- generateException(
1001
- `Unknown pseudo-class :${astName}()`,
1002
- SYNTAX_ERR,
1003
- this.#window
1004
- )
1005
- );
1006
- }
1007
- }
1085
+ const [astChild] = astChildren;
1086
+ if (!this.#psDirCache) {
1087
+ this.#psDirCache = new WeakMap();
1008
1088
  }
1009
- }
1010
- } else if (KEYS_PS_NTH_OF_TYPE.has(astName)) {
1011
- if (node === this.#root) {
1012
- matched.add(node);
1013
- } else if (parentNode) {
1014
- switch (astName) {
1015
- case 'first-of-type': {
1016
- const [node1] = this._collectNthOfType(
1017
- {
1018
- a: 0,
1019
- b: 1
1020
- },
1021
- node
1022
- );
1023
- if (node1) {
1024
- matched.add(node1);
1025
- }
1026
- break;
1027
- }
1028
- case 'last-of-type': {
1029
- const [node1] = this._collectNthOfType(
1030
- {
1031
- a: 0,
1032
- b: 1,
1033
- reverse: true
1034
- },
1035
- node
1036
- );
1037
- if (node1) {
1038
- matched.add(node1);
1039
- }
1040
- break;
1041
- }
1042
- // 'only-of-type' is handled by default.
1043
- default: {
1044
- const [node1] = this._collectNthOfType(
1045
- {
1046
- a: 0,
1047
- b: 1
1048
- },
1049
- node
1050
- );
1051
- if (node1 === node) {
1052
- const [node2] = this._collectNthOfType(
1053
- {
1054
- a: 0,
1055
- b: 1,
1056
- reverse: true
1057
- },
1058
- node
1059
- );
1060
- if (node2 === node) {
1061
- matched.add(node);
1062
- }
1063
- }
1064
- }
1089
+ const res = matchDirectionPseudoClass(astChild, node, this.#psDirCache);
1090
+ if (res) {
1091
+ return true;
1065
1092
  }
1093
+ break;
1066
1094
  }
1067
- } else {
1068
- switch (astName) {
1069
- case 'disabled':
1070
- case 'enabled': {
1071
- const isMatch = matchDisabledPseudoClass(astName, node);
1072
- if (isMatch) {
1073
- matched.add(node);
1074
- }
1075
- break;
1095
+ // :lang()
1096
+ case 'lang': {
1097
+ if (!astChildren.length) {
1098
+ const css = generateCSS(ast);
1099
+ this.onError(
1100
+ generateException(
1101
+ `Invalid selector ${css}`,
1102
+ SYNTAX_ERR,
1103
+ this.#window
1104
+ )
1105
+ );
1106
+ return false;
1076
1107
  }
1077
- case 'read-only':
1078
- case 'read-write': {
1079
- const isMatch = matchReadOnlyPseudoClass(astName, node);
1080
- if (isMatch) {
1081
- matched.add(node);
1082
- }
1083
- break;
1108
+ if (!this.#psLangCache) {
1109
+ this.#psLangCache = new WeakMap();
1084
1110
  }
1085
- case 'any-link':
1086
- case 'link': {
1087
- if (
1088
- (localName === 'a' || localName === 'area') &&
1089
- node.hasAttribute('href')
1090
- ) {
1091
- matched.add(node);
1111
+ let bool;
1112
+ for (const astChild of astChildren) {
1113
+ bool = matchLanguagePseudoClass(astChild, node, this.#psLangCache);
1114
+ if (bool) {
1115
+ break;
1092
1116
  }
1093
- break;
1094
1117
  }
1095
- case 'local-link': {
1096
- if (
1097
- (localName === 'a' || localName === 'area') &&
1098
- node.hasAttribute('href')
1099
- ) {
1100
- if (!this.#documentURL) {
1101
- this.#documentURL = new URL(this.#document.URL);
1118
+ if (bool) {
1119
+ return true;
1120
+ }
1121
+ break;
1122
+ }
1123
+ // :state()
1124
+ case 'state': {
1125
+ if (isCustomElement(node)) {
1126
+ const [{ value: stateValue }] = astChildren;
1127
+ if (stateValue) {
1128
+ if (node[stateValue]) {
1129
+ return true;
1102
1130
  }
1103
- const { href, origin, pathname } = this.#documentURL;
1104
- const attrURL = new URL(node.getAttribute('href'), href);
1105
- if (attrURL.origin === origin && attrURL.pathname === pathname) {
1106
- matched.add(node);
1131
+ for (const i in node) {
1132
+ const prop = node[i];
1133
+ if (prop instanceof this.#window.ElementInternals) {
1134
+ if (prop?.states?.has(stateValue)) {
1135
+ return true;
1136
+ }
1137
+ break;
1138
+ }
1107
1139
  }
1108
1140
  }
1109
- break;
1110
1141
  }
1111
- case 'visited': {
1112
- // prevent fingerprinting
1113
- break;
1142
+ break;
1143
+ }
1144
+ case 'current':
1145
+ case 'heading':
1146
+ case 'nth-col':
1147
+ case 'nth-last-col': {
1148
+ if (warn) {
1149
+ this.onError(
1150
+ generateException(
1151
+ `Unsupported pseudo-class :${astName}()`,
1152
+ NOT_SUPPORTED_ERR,
1153
+ this.#window
1154
+ )
1155
+ );
1114
1156
  }
1115
- case 'hover': {
1116
- const { target, type } = this.#event ?? {};
1117
- if (
1118
- /^(?:click|mouse(?:down|over|up))$/.test(type) &&
1119
- target?.nodeType === ELEMENT_NODE &&
1120
- node.contains(target)
1121
- ) {
1122
- matched.add(node);
1123
- }
1124
- break;
1157
+ break;
1158
+ }
1159
+ // Ignore :host() and :host-context().
1160
+ case 'host':
1161
+ case 'host-context': {
1162
+ break;
1163
+ }
1164
+ // Deprecated in CSS Selectors 3.
1165
+ case 'contains': {
1166
+ if (warn) {
1167
+ this.onError(
1168
+ generateException(
1169
+ `Unknown pseudo-class :${astName}()`,
1170
+ NOT_SUPPORTED_ERR,
1171
+ this.#window
1172
+ )
1173
+ );
1125
1174
  }
1126
- case 'active': {
1127
- const { buttons, target, type } = this.#event ?? {};
1128
- if (
1129
- type === 'mousedown' &&
1130
- buttons & 1 &&
1131
- target?.nodeType === ELEMENT_NODE &&
1132
- node.contains(target)
1133
- ) {
1134
- matched.add(node);
1135
- }
1136
- break;
1175
+ break;
1176
+ }
1177
+ default: {
1178
+ if (!forgive) {
1179
+ this.onError(
1180
+ generateException(
1181
+ `Unknown pseudo-class :${astName}()`,
1182
+ SYNTAX_ERR,
1183
+ this.#window
1184
+ )
1185
+ );
1137
1186
  }
1138
- case 'target': {
1139
- if (!this.#documentURL) {
1140
- this.#documentURL = new URL(this.#document.URL);
1141
- }
1142
- const { hash } = this.#documentURL;
1143
- if (
1144
- node.id &&
1145
- hash === `#${node.id}` &&
1146
- this.#document.contains(node)
1147
- ) {
1148
- matched.add(node);
1149
- }
1150
- break;
1187
+ }
1188
+ }
1189
+ return false;
1190
+ }
1191
+
1192
+ /**
1193
+ * Evaluates *-of-type pseudo-class selector.
1194
+ * @private
1195
+ * @param {string} astName - The AST name.
1196
+ * @param {object} node - The Element node.
1197
+ * @returns {boolean} True if matches, otherwise false.
1198
+ */
1199
+
1200
+ /**
1201
+ * Matches pseudo-class selector.
1202
+ * @private
1203
+ * @see https://html.spec.whatwg.org/#pseudo-classes
1204
+ * @param {object} ast - The AST.
1205
+ * @param {object} node - The Element node.
1206
+ * @param {object} [opt] - Options.
1207
+ * @param {boolean} [opt.forgive] - Ignores unknown or invalid selectors.
1208
+ * @param {boolean} [opt.warn] - If true, console warnings are enabled.
1209
+ * @returns {Set.<object>|boolean} A collection of matched nodes.
1210
+ */
1211
+ _matchPseudoClassSelector(ast, node, opt = {}) {
1212
+ const { children: astChildren, name: astName } = ast;
1213
+ const { localName, parentNode } = node;
1214
+ const { forgive, warn = this.#warn } = opt;
1215
+ if (Array.isArray(astChildren)) {
1216
+ // :has(), :is(), :not(), :where()
1217
+ if (KEYS_LOGICAL.has(astName)) {
1218
+ return this._evaluateLogicalPseudo(ast, node, opt);
1219
+ }
1220
+ return this._evaluatePseudoClassFunc(ast, node, opt);
1221
+ }
1222
+ if (KEYS_PS_NTH_OF_TYPE.has(astName)) {
1223
+ if (!parentNode) {
1224
+ return node === this.#root;
1225
+ }
1226
+ switch (astName) {
1227
+ case 'first-of-type': {
1228
+ const [node1] = this._collectNthOfType(ANB_FIRST, node);
1229
+ return node1 === node;
1151
1230
  }
1152
- case 'target-within': {
1153
- if (!this.#documentURL) {
1154
- this.#documentURL = new URL(this.#document.URL);
1155
- }
1156
- const { hash } = this.#documentURL;
1157
- if (hash) {
1158
- const id = hash.replace(/^#/, '');
1159
- let current = this.#document.getElementById(id);
1160
- while (current) {
1161
- if (current === node) {
1162
- matched.add(node);
1163
- break;
1164
- }
1165
- current = current.parentNode;
1166
- }
1167
- }
1168
- break;
1231
+ case 'last-of-type': {
1232
+ const [node1] = this._collectNthOfType(ANB_LAST, node);
1233
+ return node1 === node;
1169
1234
  }
1170
- case 'scope': {
1171
- if (this.#node.nodeType === ELEMENT_NODE) {
1172
- if (!this.#shadow && node === this.#node) {
1173
- matched.add(node);
1174
- }
1175
- } else if (node === this.#document.documentElement) {
1176
- matched.add(node);
1235
+ // 'only-of-type' is handled by default.
1236
+ default: {
1237
+ const [node1] = this._collectNthOfType(ANB_FIRST, node);
1238
+ if (node1 === node) {
1239
+ const [node2] = this._collectNthOfType(ANB_LAST, node);
1240
+ return node2 === node;
1177
1241
  }
1178
- break;
1179
1242
  }
1180
- case 'focus': {
1181
- const activeElement = this.#document.activeElement;
1182
- if (node === activeElement && isFocusableArea(node)) {
1183
- matched.add(node);
1184
- } else if (activeElement.shadowRoot) {
1185
- const activeShadowElement = activeElement.shadowRoot.activeElement;
1186
- let current = activeShadowElement;
1187
- while (current) {
1188
- if (current.nodeType === DOCUMENT_FRAGMENT_NODE) {
1189
- const { host } = current;
1190
- if (host === activeElement) {
1191
- if (isFocusableArea(node)) {
1192
- matched.add(node);
1193
- } else {
1194
- matched.add(host);
1195
- }
1196
- }
1197
- break;
1198
- } else {
1199
- current = current.parentNode;
1200
- }
1201
- }
1202
- }
1203
- break;
1243
+ }
1244
+ return false;
1245
+ }
1246
+ switch (astName) {
1247
+ /* Elemental pseudo-classes */
1248
+ case 'defined': {
1249
+ if (node.hasAttribute('is') || localName.includes('-')) {
1250
+ return isCustomElement(node);
1204
1251
  }
1205
- case 'focus-visible': {
1206
- if (node === this.#document.activeElement && isFocusableArea(node)) {
1207
- let bool;
1208
- if (isFocusVisible(node)) {
1209
- bool = true;
1210
- } else if (this.#focus) {
1211
- const { relatedTarget, target: focusTarget } = this.#focus;
1212
- if (focusTarget === node) {
1213
- if (isFocusVisible(relatedTarget)) {
1214
- bool = true;
1215
- } else if (this.#event) {
1216
- const {
1217
- altKey: eventAltKey,
1218
- ctrlKey: eventCtrlKey,
1219
- key: eventKey,
1220
- metaKey: eventMetaKey,
1221
- target: eventTarget,
1222
- type: eventType
1223
- } = this.#event;
1224
- // this.#event is irrelevant if eventTarget === relatedTarget
1225
- if (eventTarget === relatedTarget) {
1226
- if (this.#lastFocusVisible === null) {
1227
- bool = true;
1228
- } else if (focusTarget === this.#lastFocusVisible) {
1229
- bool = true;
1230
- }
1231
- } else if (eventKey === 'Tab') {
1232
- if (
1233
- (eventType === 'keydown' && eventTarget !== node) ||
1234
- (eventType === 'keyup' && eventTarget === node)
1235
- ) {
1236
- if (eventTarget === focusTarget) {
1237
- if (this.#lastFocusVisible === null) {
1238
- bool = true;
1239
- } else if (
1240
- eventTarget === this.#lastFocusVisible &&
1241
- relatedTarget === null
1242
- ) {
1243
- bool = true;
1244
- }
1245
- } else {
1246
- bool = true;
1247
- }
1248
- }
1249
- } else if (eventKey) {
1250
- if (
1251
- (eventType === 'keydown' || eventType === 'keyup') &&
1252
- !eventAltKey &&
1253
- !eventCtrlKey &&
1254
- !eventMetaKey &&
1255
- eventTarget === node
1256
- ) {
1257
- bool = true;
1258
- }
1259
- }
1260
- } else if (
1261
- relatedTarget === null ||
1262
- relatedTarget === this.#lastFocusVisible
1263
- ) {
1264
- bool = true;
1265
- }
1252
+ return (
1253
+ node instanceof this.#window.HTMLElement ||
1254
+ node instanceof this.#window.SVGElement
1255
+ );
1256
+ }
1257
+ /* Element display state pseudo-classes */
1258
+ case 'open': {
1259
+ // <select> and <input type="color"> are not supported.
1260
+ return (
1261
+ (localName === 'details' || localName === 'dialog') &&
1262
+ node.hasAttribute('open')
1263
+ );
1264
+ }
1265
+ case 'popover-open': {
1266
+ // FIXME: Not implemented in jsdom
1267
+ // @see https://github.com/jsdom/jsdom/issues/3721
1268
+ // return node.popover && isVisible(node);
1269
+ break;
1270
+ }
1271
+ /* Input pseudo-classes */
1272
+ case 'disabled':
1273
+ case 'enabled': {
1274
+ return matchDisabledPseudoClass(astName, node);
1275
+ }
1276
+ case 'read-only':
1277
+ case 'read-write': {
1278
+ return matchReadOnlyPseudoClass(astName, node);
1279
+ }
1280
+ case 'placeholder-shown': {
1281
+ let placeholder;
1282
+ if (node.placeholder) {
1283
+ placeholder = node.placeholder;
1284
+ } else if (node.hasAttribute('placeholder')) {
1285
+ placeholder = node.getAttribute('placeholder');
1286
+ }
1287
+ if (typeof placeholder === 'string' && !/[\r\n]/.test(placeholder)) {
1288
+ let targetNode;
1289
+ if (localName === 'textarea') {
1290
+ targetNode = node;
1291
+ } else if (localName === 'input') {
1292
+ if (node.hasAttribute('type')) {
1293
+ if (KEYS_INPUT_PLACEHOLDER.has(node.getAttribute('type'))) {
1294
+ targetNode = node;
1266
1295
  }
1267
- }
1268
- if (bool) {
1269
- this.#lastFocusVisible = node;
1270
- matched.add(node);
1271
- } else if (this.#lastFocusVisible === node) {
1272
- this.#lastFocusVisible = null;
1273
- }
1274
- }
1275
- break;
1276
- }
1277
- case 'focus-within': {
1278
- const activeElement = this.#document.activeElement;
1279
- if (node.contains(activeElement) && isFocusableArea(activeElement)) {
1280
- matched.add(node);
1281
- } else if (activeElement.shadowRoot) {
1282
- const activeShadowElement = activeElement.shadowRoot.activeElement;
1283
- if (node.contains(activeShadowElement)) {
1284
- matched.add(node);
1285
1296
  } else {
1286
- let current = activeShadowElement;
1287
- while (current) {
1288
- if (current.nodeType === DOCUMENT_FRAGMENT_NODE) {
1289
- const { host } = current;
1290
- if (host === activeElement && node.contains(host)) {
1291
- matched.add(node);
1292
- }
1293
- break;
1294
- } else {
1295
- current = current.parentNode;
1296
- }
1297
- }
1298
- }
1299
- }
1300
- break;
1301
- }
1302
- case 'open':
1303
- case 'closed': {
1304
- if (localName === 'details' || localName === 'dialog') {
1305
- if (node.hasAttribute('open')) {
1306
- if (astName === 'open') {
1307
- matched.add(node);
1308
- }
1309
- } else if (astName === 'closed') {
1310
- matched.add(node);
1311
- }
1312
- }
1313
- break;
1314
- }
1315
- case 'placeholder-shown': {
1316
- let placeholder;
1317
- if (node.placeholder) {
1318
- placeholder = node.placeholder;
1319
- } else if (node.hasAttribute('placeholder')) {
1320
- placeholder = node.getAttribute('placeholder');
1321
- }
1322
- if (typeof placeholder === 'string' && !/[\r\n]/.test(placeholder)) {
1323
- let targetNode;
1324
- if (localName === 'textarea') {
1325
1297
  targetNode = node;
1326
- } else if (localName === 'input') {
1327
- if (node.hasAttribute('type')) {
1328
- if (KEYS_INPUT_PLACEHOLDER.has(node.getAttribute('type'))) {
1329
- targetNode = node;
1330
- }
1331
- } else {
1332
- targetNode = node;
1333
- }
1334
- }
1335
- if (targetNode && node.value === '') {
1336
- matched.add(node);
1337
1298
  }
1338
1299
  }
1339
- break;
1340
- }
1341
- case 'checked': {
1342
- const attrType = node.getAttribute('type');
1343
- if (
1344
- (node.checked &&
1345
- localName === 'input' &&
1346
- (attrType === 'checkbox' || attrType === 'radio')) ||
1347
- (node.selected && localName === 'option')
1348
- ) {
1349
- matched.add(node);
1300
+ if (targetNode) {
1301
+ return node.value === '';
1350
1302
  }
1351
- break;
1352
1303
  }
1353
- case 'indeterminate': {
1354
- if (
1355
- (node.indeterminate &&
1356
- localName === 'input' &&
1357
- node.type === 'checkbox') ||
1358
- (localName === 'progress' && !node.hasAttribute('value'))
1359
- ) {
1360
- matched.add(node);
1361
- } else if (
1362
- localName === 'input' &&
1363
- node.type === 'radio' &&
1364
- !node.hasAttribute('checked')
1365
- ) {
1366
- const nodeName = node.name;
1367
- let parent = node.parentNode;
1368
- while (parent) {
1369
- if (parent.localName === 'form') {
1370
- break;
1371
- }
1372
- parent = parent.parentNode;
1373
- }
1374
- if (!parent) {
1375
- parent = this.#document.documentElement;
1376
- }
1377
- const walker = this._createTreeWalker(parent);
1378
- let refNode = traverseNode(parent, walker);
1379
- refNode = walker.firstChild();
1380
- let checked;
1381
- while (refNode) {
1382
- if (
1383
- refNode.localName === 'input' &&
1384
- refNode.getAttribute('type') === 'radio'
1385
- ) {
1386
- if (refNode.hasAttribute('name')) {
1387
- if (refNode.getAttribute('name') === nodeName) {
1388
- checked = !!refNode.checked;
1389
- }
1390
- } else {
1391
- checked = !!refNode.checked;
1392
- }
1393
- if (checked) {
1394
- break;
1395
- }
1396
- }
1397
- refNode = walker.nextNode();
1398
- }
1399
- if (!checked) {
1400
- matched.add(node);
1401
- }
1402
- }
1403
- break;
1304
+ break;
1305
+ }
1306
+ case 'default': {
1307
+ // option
1308
+ if (localName === 'option') {
1309
+ return node.hasAttribute('selected');
1404
1310
  }
1405
- case 'default': {
1406
- // button[type="submit"], input[type="submit"], input[type="image"]
1407
- const attrType = node.getAttribute('type');
1408
- if (
1409
- (localName === 'button' &&
1410
- !(node.hasAttribute('type') && KEYS_INPUT_RESET.has(attrType))) ||
1411
- (localName === 'input' &&
1412
- node.hasAttribute('type') &&
1413
- KEYS_INPUT_SUBMIT.has(attrType))
1414
- ) {
1415
- let form = node.parentNode;
1416
- while (form) {
1417
- if (form.localName === 'form') {
1418
- break;
1419
- }
1420
- form = form.parentNode;
1311
+ const attrType = node.getAttribute('type');
1312
+ // input[type="checkbox"], input[type="radio"]
1313
+ if (
1314
+ localName === 'input' &&
1315
+ node.hasAttribute('type') &&
1316
+ node.hasAttribute('checked')
1317
+ ) {
1318
+ return KEYS_INPUT_CHECK.has(attrType);
1319
+ }
1320
+ // button[type="submit"], input[type="submit"], input[type="image"]
1321
+ if (
1322
+ (localName === 'button' &&
1323
+ !(node.hasAttribute('type') && KEYS_INPUT_RESET.has(attrType))) ||
1324
+ (localName === 'input' &&
1325
+ node.hasAttribute('type') &&
1326
+ KEYS_INPUT_SUBMIT.has(attrType))
1327
+ ) {
1328
+ let form = node.parentNode;
1329
+ while (form) {
1330
+ if (form.localName === 'form') {
1331
+ break;
1332
+ }
1333
+ form = form.parentNode;
1334
+ }
1335
+ if (form) {
1336
+ if (!this.#psDefaultCache) {
1337
+ this.#psDefaultCache = new WeakMap();
1421
1338
  }
1422
- if (form) {
1423
- const walker = this._createTreeWalker(form);
1339
+ let defaultSubmit = this.#psDefaultCache.get(form);
1340
+ if (!defaultSubmit) {
1341
+ const walker = this._createTreeWalker(form, { force: true });
1424
1342
  let refNode = traverseNode(form, walker);
1425
1343
  refNode = walker.firstChild();
1426
1344
  while (refNode) {
@@ -1438,53 +1356,117 @@ export class Finder {
1438
1356
  KEYS_INPUT_SUBMIT.has(nodeAttrType);
1439
1357
  }
1440
1358
  if (m) {
1441
- if (refNode === node) {
1442
- matched.add(node);
1443
- }
1359
+ defaultSubmit = refNode;
1444
1360
  break;
1445
1361
  }
1446
1362
  refNode = walker.nextNode();
1447
1363
  }
1364
+ this.#psDefaultCache.set(form, defaultSubmit);
1448
1365
  }
1449
- // input[type="checkbox"], input[type="radio"]
1450
- } else if (
1451
- localName === 'input' &&
1452
- node.hasAttribute('type') &&
1453
- node.hasAttribute('checked') &&
1454
- KEYS_INPUT_CHECK.has(attrType)
1455
- ) {
1456
- matched.add(node);
1457
- // option
1458
- } else if (localName === 'option' && node.hasAttribute('selected')) {
1459
- matched.add(node);
1366
+ return defaultSubmit === node;
1460
1367
  }
1461
- break;
1462
1368
  }
1463
- case 'valid':
1464
- case 'invalid': {
1465
- if (KEYS_FORM_PS_VALID.has(localName)) {
1466
- let valid;
1467
- if (node.checkValidity()) {
1468
- if (node.maxLength >= 0) {
1469
- if (node.maxLength >= node.value.length) {
1470
- valid = true;
1369
+ break;
1370
+ }
1371
+ case 'checked': {
1372
+ if (localName === 'option') {
1373
+ return node.selected;
1374
+ }
1375
+ if (localName === 'input') {
1376
+ const attrType = node.getAttribute('type');
1377
+ return (
1378
+ node.checked && (attrType === 'checkbox' || attrType === 'radio')
1379
+ );
1380
+ }
1381
+ break;
1382
+ }
1383
+ case 'indeterminate': {
1384
+ if (localName === 'progress') {
1385
+ return !node.hasAttribute('value');
1386
+ }
1387
+ if (localName === 'input' && node.type === 'checkbox') {
1388
+ return node.indeterminate;
1389
+ }
1390
+ if (localName === 'input' && node.type === 'radio') {
1391
+ if (node.checked || node.hasAttribute('checked')) {
1392
+ return false;
1393
+ }
1394
+ const nodeName = node.name;
1395
+ let parent = node.parentNode;
1396
+ while (parent) {
1397
+ if (parent.localName === 'form') {
1398
+ break;
1399
+ }
1400
+ parent = parent.parentNode;
1401
+ }
1402
+ if (!parent) {
1403
+ parent = this.#document.documentElement;
1404
+ }
1405
+ if (!this.#psIndeterminateCache) {
1406
+ this.#psIndeterminateCache = new WeakMap();
1407
+ }
1408
+ let parentCache = this.#psIndeterminateCache.get(parent);
1409
+ if (!parentCache) {
1410
+ parentCache = new Map();
1411
+ this.#psIndeterminateCache.set(parent, parentCache);
1412
+ }
1413
+ let checked = parentCache.get(nodeName);
1414
+ if (checked === undefined) {
1415
+ const walker = this._createTreeWalker(parent, { force: true });
1416
+ let refNode = traverseNode(parent, walker);
1417
+ refNode = walker.firstChild();
1418
+ while (refNode) {
1419
+ if (
1420
+ refNode.localName === 'input' &&
1421
+ refNode.getAttribute('type') === 'radio'
1422
+ ) {
1423
+ if (refNode.hasAttribute('name')) {
1424
+ if (refNode.getAttribute('name') === nodeName) {
1425
+ checked = !!refNode.checked;
1426
+ }
1427
+ } else {
1428
+ checked = !!refNode.checked;
1429
+ }
1430
+ if (checked) {
1431
+ break;
1471
1432
  }
1472
- } else {
1473
- valid = true;
1474
1433
  }
1434
+ refNode = walker.nextNode();
1475
1435
  }
1476
- if (valid) {
1477
- if (astName === 'valid') {
1478
- matched.add(node);
1436
+ checked = !!checked;
1437
+ parentCache.set(nodeName, checked);
1438
+ }
1439
+ return !checked;
1440
+ }
1441
+ break;
1442
+ }
1443
+ case 'valid':
1444
+ case 'invalid': {
1445
+ if (KEYS_FORM_PS_VALID.has(localName)) {
1446
+ let valid = false;
1447
+ if (node.checkValidity()) {
1448
+ if (node.maxLength >= 0) {
1449
+ if (node.maxLength >= node.value.length) {
1450
+ valid = true;
1479
1451
  }
1480
- } else if (astName === 'invalid') {
1481
- matched.add(node);
1452
+ } else {
1453
+ valid = true;
1482
1454
  }
1483
- } else if (localName === 'fieldset') {
1484
- const walker = this._createTreeWalker(node);
1455
+ }
1456
+ if (astName === 'invalid') {
1457
+ return !valid;
1458
+ }
1459
+ return valid;
1460
+ }
1461
+ if (localName === 'fieldset') {
1462
+ if (!this.#psValidCache) {
1463
+ this.#psValidCache = new WeakMap();
1464
+ }
1465
+ let valid = this.#psValidCache.get(node);
1466
+ if (valid === undefined && !this.#psValidCache.has(node)) {
1467
+ const walker = this._createTreeWalker(node, { force: true });
1485
1468
  let refNode = traverseNode(node, walker);
1486
1469
  refNode = walker.firstChild();
1487
- let valid;
1488
1470
  if (!refNode) {
1489
1471
  valid = true;
1490
1472
  } else {
@@ -1506,201 +1488,345 @@ export class Finder {
1506
1488
  refNode = walker.nextNode();
1507
1489
  }
1508
1490
  }
1509
- if (valid) {
1510
- if (astName === 'valid') {
1511
- matched.add(node);
1512
- }
1513
- } else if (astName === 'invalid') {
1514
- matched.add(node);
1515
- }
1491
+ this.#psValidCache.set(node, valid);
1516
1492
  }
1517
- break;
1493
+ if (astName === 'invalid') {
1494
+ return !valid;
1495
+ }
1496
+ return valid;
1518
1497
  }
1519
- case 'in-range':
1520
- case 'out-of-range': {
1521
- const attrType = node.getAttribute('type');
1522
- if (
1523
- localName === 'input' &&
1524
- !(node.readOnly || node.hasAttribute('readonly')) &&
1525
- !(node.disabled || node.hasAttribute('disabled')) &&
1526
- KEYS_INPUT_RANGE.has(attrType)
1527
- ) {
1528
- const flowed =
1529
- node.validity.rangeUnderflow || node.validity.rangeOverflow;
1530
- if (astName === 'out-of-range' && flowed) {
1531
- matched.add(node);
1532
- } else if (
1533
- astName === 'in-range' &&
1534
- !flowed &&
1535
- (node.hasAttribute('min') ||
1498
+ break;
1499
+ }
1500
+ case 'in-range':
1501
+ case 'out-of-range': {
1502
+ const attrType = node.getAttribute('type');
1503
+ if (
1504
+ localName === 'input' &&
1505
+ !(node.readOnly || node.hasAttribute('readonly')) &&
1506
+ !(node.disabled || node.hasAttribute('disabled')) &&
1507
+ KEYS_INPUT_RANGE.has(attrType)
1508
+ ) {
1509
+ const flowed =
1510
+ node.validity.rangeUnderflow || node.validity.rangeOverflow;
1511
+ if (astName === 'out-of-range') {
1512
+ return flowed;
1513
+ }
1514
+ return flowed
1515
+ ? false
1516
+ : node.hasAttribute('min') ||
1536
1517
  node.hasAttribute('max') ||
1537
- attrType === 'range')
1538
- ) {
1539
- matched.add(node);
1540
- }
1541
- }
1542
- break;
1518
+ attrType === 'range';
1543
1519
  }
1544
- case 'required':
1545
- case 'optional': {
1546
- let required;
1547
- let optional;
1548
- if (localName === 'select' || localName === 'textarea') {
1549
- if (node.required || node.hasAttribute('required')) {
1550
- required = true;
1551
- } else {
1552
- optional = true;
1553
- }
1554
- } else if (localName === 'input') {
1555
- if (node.hasAttribute('type')) {
1556
- const attrType = node.getAttribute('type');
1557
- if (KEYS_INPUT_REQUIRED.has(attrType)) {
1558
- if (node.required || node.hasAttribute('required')) {
1559
- required = true;
1560
- } else {
1561
- optional = true;
1562
- }
1563
- } else {
1564
- optional = true;
1520
+ break;
1521
+ }
1522
+ case 'required':
1523
+ case 'optional': {
1524
+ let required = false;
1525
+ if (localName === 'select' || localName === 'textarea') {
1526
+ if (node.required || node.hasAttribute('required')) {
1527
+ required = true;
1528
+ }
1529
+ } else if (localName === 'input') {
1530
+ if (node.hasAttribute('type')) {
1531
+ const attrType = node.getAttribute('type');
1532
+ if (KEYS_INPUT_REQUIRED.has(attrType)) {
1533
+ if (node.required || node.hasAttribute('required')) {
1534
+ required = true;
1565
1535
  }
1566
- } else if (node.required || node.hasAttribute('required')) {
1567
- required = true;
1568
- } else {
1569
- optional = true;
1570
1536
  }
1537
+ } else if (node.required || node.hasAttribute('required')) {
1538
+ required = true;
1571
1539
  }
1572
- if (astName === 'required' && required) {
1573
- matched.add(node);
1574
- } else if (astName === 'optional' && optional) {
1575
- matched.add(node);
1540
+ }
1541
+ if (astName === 'optional') {
1542
+ return !required;
1543
+ }
1544
+ return required;
1545
+ }
1546
+ /* Location pseudo-classes */
1547
+ case 'any-link':
1548
+ case 'link': {
1549
+ return (
1550
+ (localName === 'a' || localName === 'area') &&
1551
+ node.hasAttribute('href')
1552
+ );
1553
+ }
1554
+ case 'local-link': {
1555
+ if (
1556
+ (localName === 'a' || localName === 'area') &&
1557
+ node.hasAttribute('href')
1558
+ ) {
1559
+ if (!this.#documentURL) {
1560
+ this.#documentURL = new URL(this.#document.URL);
1576
1561
  }
1577
- break;
1562
+ const { href, origin, pathname } = this.#documentURL;
1563
+ const attrURL = new URL(node.getAttribute('href'), href);
1564
+ return attrURL.origin === origin && attrURL.pathname === pathname;
1565
+ }
1566
+ break;
1567
+ }
1568
+ case 'visited': {
1569
+ // prevent fingerprinting
1570
+ break;
1571
+ }
1572
+ case 'target': {
1573
+ if (!this.#documentURL) {
1574
+ this.#documentURL = new URL(this.#document.URL);
1575
+ }
1576
+ const { hash } = this.#documentURL;
1577
+ return hash && hash === `#${node.id}` && this.#document.contains(node);
1578
+ }
1579
+ case 'scope': {
1580
+ if (this.#node.nodeType === ELEMENT_NODE) {
1581
+ return !this.#shadow && node === this.#node;
1582
+ }
1583
+ return node === this.#document.documentElement;
1584
+ }
1585
+ /* Tree-structural pseudo-classes */
1586
+ case 'root': {
1587
+ return node === this.#document.documentElement;
1588
+ }
1589
+ case 'empty': {
1590
+ if (!node.hasChildNodes()) {
1591
+ return true;
1578
1592
  }
1579
- case 'root': {
1580
- if (node === this.#document.documentElement) {
1581
- matched.add(node);
1593
+ const walker = this._createTreeWalker(node, {
1594
+ force: true,
1595
+ whatToShow: SHOW_ALL
1596
+ });
1597
+ let refNode = walker.firstChild();
1598
+ let bool;
1599
+ while (refNode) {
1600
+ bool =
1601
+ refNode.nodeType !== ELEMENT_NODE && refNode.nodeType !== TEXT_NODE;
1602
+ if (!bool) {
1603
+ break;
1582
1604
  }
1583
- break;
1605
+ refNode = walker.nextSibling();
1584
1606
  }
1585
- case 'empty': {
1586
- if (node.hasChildNodes()) {
1587
- const walker = this._createTreeWalker(node, {
1588
- force: true,
1589
- whatToShow: SHOW_ALL
1590
- });
1591
- let refNode = walker.firstChild();
1592
- let bool;
1593
- while (refNode) {
1594
- bool =
1595
- refNode.nodeType !== ELEMENT_NODE &&
1596
- refNode.nodeType !== TEXT_NODE;
1597
- if (!bool) {
1598
- break;
1607
+ return bool;
1608
+ }
1609
+ case 'first-child':
1610
+ case 'last-child':
1611
+ case 'only-child': {
1612
+ if (!parentNode) {
1613
+ return node === this.#root;
1614
+ }
1615
+ if (astName === 'first-child') {
1616
+ return node === parentNode.firstElementChild;
1617
+ }
1618
+ if (astName === 'last-child') {
1619
+ return node === parentNode.lastElementChild;
1620
+ }
1621
+ return (
1622
+ node === parentNode.firstElementChild &&
1623
+ node === parentNode.lastElementChild
1624
+ );
1625
+ }
1626
+ /* User action pseudo-classes */
1627
+ case 'hover': {
1628
+ const { target, type } = this.#event ?? {};
1629
+ return (
1630
+ /^(?:click|mouse(?:down|over|up))$/.test(type) &&
1631
+ target?.nodeType === ELEMENT_NODE &&
1632
+ node.contains(target)
1633
+ );
1634
+ }
1635
+ case 'active': {
1636
+ const { buttons, target, type } = this.#event ?? {};
1637
+ return (
1638
+ type === 'mousedown' &&
1639
+ buttons & 1 &&
1640
+ target?.nodeType === ELEMENT_NODE &&
1641
+ node.contains(target)
1642
+ );
1643
+ }
1644
+ case 'focus': {
1645
+ const activeElement = this.#document.activeElement;
1646
+ if (activeElement.shadowRoot) {
1647
+ const activeShadowElement = activeElement.shadowRoot.activeElement;
1648
+ let current = activeShadowElement;
1649
+ while (current) {
1650
+ if (current.nodeType === DOCUMENT_FRAGMENT_NODE) {
1651
+ const { host } = current;
1652
+ if (host === activeElement) {
1653
+ if (isFocusableArea(node)) {
1654
+ return true;
1655
+ }
1656
+ return host === node;
1599
1657
  }
1600
- refNode = walker.nextSibling();
1601
1658
  }
1602
- if (bool) {
1603
- matched.add(node);
1659
+ current = current.parentNode;
1660
+ }
1661
+ }
1662
+ return node === activeElement && isFocusableArea(node);
1663
+ }
1664
+ case 'focus-visible': {
1665
+ if (node === this.#document.activeElement && isFocusableArea(node)) {
1666
+ let bool;
1667
+ if (isFocusVisible(node)) {
1668
+ bool = true;
1669
+ } else if (this.#focus) {
1670
+ const { relatedTarget, target: focusTarget } = this.#focus;
1671
+ if (focusTarget === node) {
1672
+ if (isFocusVisible(relatedTarget)) {
1673
+ bool = true;
1674
+ } else if (this.#event) {
1675
+ const {
1676
+ altKey: eventAltKey,
1677
+ ctrlKey: eventCtrlKey,
1678
+ key: eventKey,
1679
+ metaKey: eventMetaKey,
1680
+ target: eventTarget,
1681
+ type: eventType
1682
+ } = this.#event;
1683
+ // this.#event is irrelevant if eventTarget === relatedTarget
1684
+ if (eventTarget === relatedTarget) {
1685
+ if (!this.#lastFocusVisible) {
1686
+ bool = true;
1687
+ } else if (focusTarget === this.#lastFocusVisible) {
1688
+ bool = true;
1689
+ }
1690
+ } else if (eventKey === 'Tab') {
1691
+ if (
1692
+ (eventType === 'keydown' && eventTarget !== node) ||
1693
+ (eventType === 'keyup' && eventTarget === node)
1694
+ ) {
1695
+ if (eventTarget === focusTarget) {
1696
+ if (!this.#lastFocusVisible) {
1697
+ bool = true;
1698
+ } else if (
1699
+ eventTarget === this.#lastFocusVisible &&
1700
+ relatedTarget === null
1701
+ ) {
1702
+ bool = true;
1703
+ }
1704
+ } else {
1705
+ bool = true;
1706
+ }
1707
+ }
1708
+ } else if (eventKey) {
1709
+ if (
1710
+ (eventType === 'keydown' || eventType === 'keyup') &&
1711
+ !eventAltKey &&
1712
+ !eventCtrlKey &&
1713
+ !eventMetaKey &&
1714
+ eventTarget === node
1715
+ ) {
1716
+ bool = true;
1717
+ }
1718
+ }
1719
+ } else if (
1720
+ relatedTarget === null ||
1721
+ relatedTarget === this.#lastFocusVisible
1722
+ ) {
1723
+ bool = true;
1724
+ }
1604
1725
  }
1605
- } else {
1606
- matched.add(node);
1607
- }
1608
- break;
1609
- }
1610
- case 'first-child': {
1611
- if (
1612
- (parentNode && node === parentNode.firstElementChild) ||
1613
- node === this.#root
1614
- ) {
1615
- matched.add(node);
1616
1726
  }
1617
- break;
1618
- }
1619
- case 'last-child': {
1620
- if (
1621
- (parentNode && node === parentNode.lastElementChild) ||
1622
- node === this.#root
1623
- ) {
1624
- matched.add(node);
1727
+ if (bool) {
1728
+ this.#lastFocusVisible = node;
1729
+ return bool;
1625
1730
  }
1626
- break;
1627
- }
1628
- case 'only-child': {
1629
- if (
1630
- (parentNode &&
1631
- node === parentNode.firstElementChild &&
1632
- node === parentNode.lastElementChild) ||
1633
- node === this.#root
1634
- ) {
1635
- matched.add(node);
1731
+ if (this.#lastFocusVisible === node) {
1732
+ this.#lastFocusVisible = null;
1636
1733
  }
1637
- break;
1638
1734
  }
1639
- case 'defined': {
1640
- if (node.hasAttribute('is') || localName.includes('-')) {
1641
- if (isCustomElement(node)) {
1642
- matched.add(node);
1735
+ break;
1736
+ }
1737
+ case 'focus-within': {
1738
+ if (!this.#focusWithinCache) {
1739
+ this.#focusWithinCache = new Set();
1740
+ let currentFocus = this.#document.activeElement;
1741
+ if (currentFocus && isFocusableArea(currentFocus)) {
1742
+ while (currentFocus) {
1743
+ this.#focusWithinCache.add(currentFocus);
1744
+ if (currentFocus.parentNode) {
1745
+ currentFocus = currentFocus.parentNode;
1746
+ } else if (
1747
+ currentFocus.nodeType === DOCUMENT_FRAGMENT_NODE &&
1748
+ currentFocus.host
1749
+ ) {
1750
+ currentFocus = currentFocus.host;
1751
+ } else {
1752
+ break;
1753
+ }
1754
+ }
1755
+ } else if (currentFocus && currentFocus.shadowRoot) {
1756
+ let shadowFocus = currentFocus.shadowRoot.activeElement;
1757
+ if (shadowFocus) {
1758
+ while (shadowFocus) {
1759
+ this.#focusWithinCache.add(shadowFocus);
1760
+ if (shadowFocus.parentNode) {
1761
+ shadowFocus = shadowFocus.parentNode;
1762
+ } else if (
1763
+ shadowFocus.nodeType === DOCUMENT_FRAGMENT_NODE &&
1764
+ shadowFocus.host
1765
+ ) {
1766
+ shadowFocus = shadowFocus.host;
1767
+ } else {
1768
+ break;
1769
+ }
1770
+ }
1643
1771
  }
1644
- // NOTE: MathMLElement is not implemented in jsdom.
1645
- } else if (
1646
- node instanceof this.#window.HTMLElement ||
1647
- node instanceof this.#window.SVGElement
1648
- ) {
1649
- matched.add(node);
1650
- }
1651
- break;
1652
- }
1653
- case 'popover-open': {
1654
- // FIXME: not implemented in jsdom
1655
- // @see https://github.com/jsdom/jsdom/issues/3721
1656
- /*
1657
- if (node.popover && isVisible(node)) {
1658
- matched.add(node);
1659
1772
  }
1660
- */
1661
- break;
1662
1773
  }
1663
- // Ignore :host.
1664
- case 'host': {
1665
- break;
1774
+ return this.#focusWithinCache.has(node);
1775
+ }
1776
+ // Ignore :host.
1777
+ case 'host': {
1778
+ break;
1779
+ }
1780
+ // Legacy pseudo-elements.
1781
+ case 'after':
1782
+ case 'before':
1783
+ case 'first-letter':
1784
+ case 'first-line': {
1785
+ if (warn) {
1786
+ this.onError(
1787
+ generateException(
1788
+ `Unsupported pseudo-element ::${astName}`,
1789
+ NOT_SUPPORTED_ERR,
1790
+ this.#window
1791
+ )
1792
+ );
1666
1793
  }
1667
- // Legacy pseudo-elements.
1668
- case 'after':
1669
- case 'before':
1670
- case 'first-letter':
1671
- case 'first-line': {
1672
- if (warn) {
1673
- this.onError(
1674
- generateException(
1675
- `Unsupported pseudo-element ::${astName}`,
1676
- NOT_SUPPORTED_ERR,
1677
- this.#window
1678
- )
1679
- );
1680
- }
1681
- break;
1794
+ break;
1795
+ }
1796
+ // Not supported.
1797
+ case 'autofill':
1798
+ case 'blank':
1799
+ case 'buffering':
1800
+ case 'current':
1801
+ case 'fullscreen':
1802
+ case 'future':
1803
+ case 'has-slotted':
1804
+ case 'heading':
1805
+ case 'modal':
1806
+ case 'muted':
1807
+ case 'past':
1808
+ case 'paused':
1809
+ case 'picture-in-picture':
1810
+ case 'playing':
1811
+ case 'seeking':
1812
+ case 'stalled':
1813
+ case 'user-invalid':
1814
+ case 'user-valid':
1815
+ case 'volume-locked':
1816
+ case '-webkit-autofill': {
1817
+ if (warn) {
1818
+ this.onError(
1819
+ generateException(
1820
+ `Unsupported pseudo-class :${astName}`,
1821
+ NOT_SUPPORTED_ERR,
1822
+ this.#window
1823
+ )
1824
+ );
1682
1825
  }
1683
- // Not supported.
1684
- case 'autofill':
1685
- case 'blank':
1686
- case 'buffering':
1687
- case 'current':
1688
- case 'fullscreen':
1689
- case 'future':
1690
- case 'has-slotted':
1691
- case 'heading':
1692
- case 'modal':
1693
- case 'muted':
1694
- case 'past':
1695
- case 'paused':
1696
- case 'picture-in-picture':
1697
- case 'playing':
1698
- case 'seeking':
1699
- case 'stalled':
1700
- case 'user-invalid':
1701
- case 'user-valid':
1702
- case 'volume-locked':
1703
- case '-webkit-autofill': {
1826
+ break;
1827
+ }
1828
+ default: {
1829
+ if (astName.startsWith('-webkit-')) {
1704
1830
  if (warn) {
1705
1831
  this.onError(
1706
1832
  generateException(
@@ -1710,32 +1836,18 @@ export class Finder {
1710
1836
  )
1711
1837
  );
1712
1838
  }
1713
- break;
1714
- }
1715
- default: {
1716
- if (astName.startsWith('-webkit-')) {
1717
- if (warn) {
1718
- this.onError(
1719
- generateException(
1720
- `Unsupported pseudo-class :${astName}`,
1721
- NOT_SUPPORTED_ERR,
1722
- this.#window
1723
- )
1724
- );
1725
- }
1726
- } else if (!forgive) {
1727
- this.onError(
1728
- generateException(
1729
- `Unknown pseudo-class :${astName}`,
1730
- SYNTAX_ERR,
1731
- this.#window
1732
- )
1733
- );
1734
- }
1839
+ } else if (!forgive) {
1840
+ this.onError(
1841
+ generateException(
1842
+ `Unknown pseudo-class :${astName}`,
1843
+ SYNTAX_ERR,
1844
+ this.#window
1845
+ )
1846
+ );
1735
1847
  }
1736
1848
  }
1737
1849
  }
1738
- return matched;
1850
+ return false;
1739
1851
  }
1740
1852
 
1741
1853
  /**
@@ -1744,7 +1856,7 @@ export class Finder {
1744
1856
  * @param {Array.<object>} leaves - The AST leaves.
1745
1857
  * @param {object} host - The host element.
1746
1858
  * @param {object} ast - The original AST for error reporting.
1747
- * @returns {boolean} True if matched.
1859
+ * @returns {boolean} True if matches, otherwise false.
1748
1860
  */
1749
1861
  _evaluateHostPseudo = (leaves, host, ast) => {
1750
1862
  const l = leaves.length;
@@ -1756,7 +1868,7 @@ export class Finder {
1756
1868
  this.onError(generateException(msg, SYNTAX_ERR, this.#window));
1757
1869
  return false;
1758
1870
  }
1759
- if (!this._matchSelector(leaf, host).has(host)) {
1871
+ if (!this._matchSelector(leaf, host)) {
1760
1872
  return false;
1761
1873
  }
1762
1874
  }
@@ -1784,7 +1896,7 @@ export class Finder {
1784
1896
  this.onError(generateException(msg, SYNTAX_ERR, this.#window));
1785
1897
  return false;
1786
1898
  }
1787
- bool = this._matchSelector(leaf, parent).has(parent);
1899
+ bool = this._matchSelector(leaf, parent);
1788
1900
  if (!bool) {
1789
1901
  break;
1790
1902
  }
@@ -1798,44 +1910,48 @@ export class Finder {
1798
1910
  };
1799
1911
 
1800
1912
  /**
1801
- * Matches shadow host pseudo-classes.
1913
+ * Evaluates shadow host pseudo-classes.
1802
1914
  * @private
1803
1915
  * @param {object} ast - The AST.
1804
1916
  * @param {object} node - The DocumentFragment node.
1805
- * @returns {?object} The matched node.
1917
+ * @returns {boolean} True if matches, otherwise false.
1806
1918
  */
1807
- _matchShadowHostPseudoClass = (ast, node) => {
1919
+ _evaluateShadowHost = (ast, node) => {
1808
1920
  const { children: astChildren, name: astName } = ast;
1809
1921
  // Handle simple pseudo-class (no arguments).
1810
1922
  if (!Array.isArray(astChildren)) {
1811
1923
  if (astName === 'host') {
1812
- return node;
1924
+ return true;
1813
1925
  }
1814
1926
  const msg = `Invalid selector :${astName}`;
1815
- return this.onError(generateException(msg, SYNTAX_ERR, this.#window));
1927
+ this.onError(generateException(msg, SYNTAX_ERR, this.#window));
1928
+ return false;
1816
1929
  }
1817
1930
  // Handle functional pseudo-class like :host(...).
1818
1931
  if (astName !== 'host' && astName !== 'host-context') {
1819
1932
  const msg = `Invalid selector :${astName}()`;
1820
- return this.onError(generateException(msg, SYNTAX_ERR, this.#window));
1933
+ this.onError(generateException(msg, SYNTAX_ERR, this.#window));
1934
+ return false;
1821
1935
  }
1822
1936
  if (astChildren.length !== 1) {
1823
1937
  const css = generateCSS(ast);
1824
1938
  const msg = `Invalid selector ${css}`;
1825
- return this.onError(generateException(msg, SYNTAX_ERR, this.#window));
1939
+ this.onError(generateException(msg, SYNTAX_ERR, this.#window));
1940
+ return false;
1826
1941
  }
1827
1942
  const { host } = node;
1828
1943
  const { branches } = walkAST(astChildren[0]);
1829
1944
  const [branch] = branches;
1830
1945
  const [...leaves] = branch;
1831
- let isMatch = false;
1832
- if (astName === 'host') {
1833
- isMatch = this._evaluateHostPseudo(leaves, host, ast);
1834
- // astName === 'host-context'.
1835
- } else {
1836
- isMatch = this._evaluateHostContextPseudo(leaves, host, ast);
1946
+ if (astName === 'host' && this._evaluateHostPseudo(leaves, host, ast)) {
1947
+ return true;
1948
+ } else if (
1949
+ astName === 'host-context' &&
1950
+ this._evaluateHostContextPseudo(leaves, host, ast)
1951
+ ) {
1952
+ return true;
1837
1953
  }
1838
- return isMatch ? node : null;
1954
+ return false;
1839
1955
  };
1840
1956
 
1841
1957
  /**
@@ -1844,39 +1960,26 @@ export class Finder {
1844
1960
  * @param {object} ast - The AST.
1845
1961
  * @param {object} node - The Element node.
1846
1962
  * @param {object} opt - Options.
1847
- * @returns {Set.<object>} A collection of matched nodes.
1963
+ * @returns {boolean} True if matches, otherwise false.
1848
1964
  */
1849
1965
  _matchSelectorForElement = (ast, node, opt) => {
1850
1966
  const { type: astType } = ast;
1851
1967
  const astName = unescapeSelector(ast.name);
1852
- const matched = new Set();
1853
1968
  switch (astType) {
1854
1969
  case ATTR_SELECTOR: {
1855
- if (matchAttributeSelector(ast, node, opt)) {
1856
- matched.add(node);
1857
- }
1858
- break;
1970
+ return matchAttributeSelector(ast, node, opt);
1859
1971
  }
1860
1972
  case ID_SELECTOR: {
1861
- if (node.id === astName) {
1862
- matched.add(node);
1863
- }
1864
- break;
1973
+ return node.id === astName;
1865
1974
  }
1866
1975
  case CLASS_SELECTOR: {
1867
- if (node.classList.contains(astName)) {
1868
- matched.add(node);
1869
- }
1870
- break;
1976
+ return node.classList.contains(astName);
1871
1977
  }
1872
1978
  case PS_CLASS_SELECTOR: {
1873
1979
  return this._matchPseudoClassSelector(ast, node, opt);
1874
1980
  }
1875
1981
  case TYPE_SELECTOR: {
1876
- if (matchTypeSelector(ast, node, opt)) {
1877
- matched.add(node);
1878
- }
1879
- break;
1982
+ return matchTypeSelector(ast, node, opt);
1880
1983
  }
1881
1984
  // PS_ELEMENT_SELECTOR is handled by default.
1882
1985
  default: {
@@ -1884,7 +1987,7 @@ export class Finder {
1884
1987
  if (this.#check) {
1885
1988
  const css = generateCSS(ast);
1886
1989
  this.#pseudoElement.push(css);
1887
- matched.add(node);
1990
+ return true;
1888
1991
  } else {
1889
1992
  matchPseudoElementSelector(astName, astType, opt);
1890
1993
  }
@@ -1893,7 +1996,7 @@ export class Finder {
1893
1996
  }
1894
1997
  }
1895
1998
  }
1896
- return matched;
1999
+ return false;
1897
2000
  };
1898
2001
 
1899
2002
  /**
@@ -1902,7 +2005,7 @@ export class Finder {
1902
2005
  * @param {object} ast - The AST.
1903
2006
  * @param {object} node - The DocumentFragment node.
1904
2007
  * @param {object} [opt] - Options.
1905
- * @returns {Set.<object>} A collection of matched nodes.
2008
+ * @returns {boolean} True if matches, otherwise false.
1906
2009
  */
1907
2010
  _matchSelectorForShadowRoot = (ast, node, opt = {}) => {
1908
2011
  const { name: astName } = ast;
@@ -1910,15 +2013,14 @@ export class Finder {
1910
2013
  opt.isShadowRoot = true;
1911
2014
  return this._matchPseudoClassSelector(ast, node, opt);
1912
2015
  }
1913
- const matched = new Set();
1914
2016
  if (astName === 'host' || astName === 'host-context') {
1915
- const res = this._matchShadowHostPseudoClass(ast, node, opt);
1916
- if (res) {
2017
+ const matches = this._evaluateShadowHost(ast, node, opt);
2018
+ if (matches) {
1917
2019
  this.#verifyShadowHost = true;
1918
- matched.add(res);
2020
+ return true;
1919
2021
  }
1920
2022
  }
1921
- return matched;
2023
+ return false;
1922
2024
  };
1923
2025
 
1924
2026
  /**
@@ -1927,7 +2029,7 @@ export class Finder {
1927
2029
  * @param {object} ast - The AST.
1928
2030
  * @param {object} node - The Document, DocumentFragment, or Element node.
1929
2031
  * @param {object} opt - Options.
1930
- * @returns {Set.<object>} A collection of matched nodes.
2032
+ * @returns {boolean} True if matches, otherwise false.
1931
2033
  */
1932
2034
  _matchSelector = (ast, node, opt) => {
1933
2035
  if (node.nodeType === ELEMENT_NODE) {
@@ -1940,7 +2042,7 @@ export class Finder {
1940
2042
  ) {
1941
2043
  return this._matchSelectorForShadowRoot(ast, node, opt);
1942
2044
  }
1943
- return new Set();
2045
+ return false;
1944
2046
  };
1945
2047
 
1946
2048
  /**
@@ -1952,6 +2054,9 @@ export class Finder {
1952
2054
  * @returns {boolean} The result.
1953
2055
  */
1954
2056
  _matchLeaves = (leaves, node, opt) => {
2057
+ if (!this.#invalidateResults) {
2058
+ this.#invalidateResults = new WeakMap();
2059
+ }
1955
2060
  const results = this.#invalidate ? this.#invalidateResults : this.#results;
1956
2061
  let result = results.get(leaves);
1957
2062
  if (result && result.has(node)) {
@@ -1982,7 +2087,7 @@ export class Finder {
1982
2087
  // No action needed for other types.
1983
2088
  }
1984
2089
  }
1985
- bool = this._matchSelector(leaf, node, opt).has(node);
2090
+ bool = this._matchSelector(leaf, node, opt);
1986
2091
  if (!bool) {
1987
2092
  break;
1988
2093
  }
@@ -2006,6 +2111,9 @@ export class Finder {
2006
2111
  * @returns {Array.<object>} The filtered leaves.
2007
2112
  */
2008
2113
  _getFilterLeaves = leaves => {
2114
+ if (!this.#filterLeavesCache) {
2115
+ this.#filterLeavesCache = new WeakMap();
2116
+ }
2009
2117
  if (this.#filterLeavesCache.has(leaves)) {
2010
2118
  return this.#filterLeavesCache.get(leaves);
2011
2119
  }
@@ -2088,7 +2196,7 @@ export class Finder {
2088
2196
  };
2089
2197
 
2090
2198
  /**
2091
- * Collects combinator matches into an array without creating intermediate sets.
2199
+ * Collects combinator matches into an array.
2092
2200
  * @private
2093
2201
  * @param {object} twig - The twig object.
2094
2202
  * @param {object} node - The Element node.
@@ -2115,12 +2223,36 @@ export class Finder {
2115
2223
  break;
2116
2224
  }
2117
2225
  case '~': {
2226
+ const parentNode = node.parentNode;
2227
+ if (!parentNode) {
2228
+ break;
2229
+ }
2230
+ if (!this.#combinatorCache) {
2231
+ this.#combinatorCache = new WeakMap();
2232
+ }
2233
+ let cacheMap = this.#combinatorCache.get(parentNode);
2234
+ if (!cacheMap) {
2235
+ cacheMap = new Map();
2236
+ this.#combinatorCache.set(parentNode, cacheMap);
2237
+ }
2238
+ let matchedSet = cacheMap.get(leaves);
2239
+ if (!matchedSet) {
2240
+ matchedSet = new Set();
2241
+ let child = parentNode.firstElementChild;
2242
+ while (child) {
2243
+ if (this._matchLeaves(leaves, child, opt)) {
2244
+ matchedSet.add(child);
2245
+ }
2246
+ child = child.nextElementSibling;
2247
+ }
2248
+ cacheMap.set(leaves, matchedSet);
2249
+ }
2118
2250
  let refNode =
2119
2251
  dir === DIR_NEXT
2120
2252
  ? node.nextElementSibling
2121
2253
  : node.previousElementSibling;
2122
2254
  while (refNode) {
2123
- if (this._matchLeaves(leaves, refNode, opt)) {
2255
+ if (matchedSet.has(refNode)) {
2124
2256
  matched.push(refNode);
2125
2257
  }
2126
2258
  refNode =
@@ -2281,8 +2413,8 @@ export class Finder {
2281
2413
  this.#nodeWalker = this._createTreeWalker(this.#node);
2282
2414
  }
2283
2415
  return this._traverseAndCollectNodes(this.#nodeWalker, leaves, {
2284
- startNode: node,
2285
- ...traversalOpts
2416
+ ...traversalOpts,
2417
+ startNode: node
2286
2418
  });
2287
2419
  };
2288
2420
 
@@ -2470,13 +2602,18 @@ export class Finder {
2470
2602
  let pending = false;
2471
2603
  if (targetType !== TARGET_LINEAL && /host(?:-context)?/.test(leaf.name)) {
2472
2604
  let shadowRoot = null;
2473
- if (this.#shadow && this.#node.nodeType === DOCUMENT_FRAGMENT_NODE) {
2474
- shadowRoot = this._matchShadowHostPseudoClass(leaf, this.#node);
2475
- } else if (filterLeaves.length && this.#node.nodeType === ELEMENT_NODE) {
2476
- shadowRoot = this._matchShadowHostPseudoClass(
2477
- leaf,
2478
- this.#node.shadowRoot
2479
- );
2605
+ if (
2606
+ this.#shadow &&
2607
+ this.#node.nodeType === DOCUMENT_FRAGMENT_NODE &&
2608
+ this._evaluateShadowHost(leaf, this.#node)
2609
+ ) {
2610
+ shadowRoot = this.#node;
2611
+ } else if (
2612
+ filterLeaves.length &&
2613
+ this.#node.nodeType === ELEMENT_NODE &&
2614
+ this._evaluateShadowHost(leaf, this.#node.shadowRoot)
2615
+ ) {
2616
+ shadowRoot = this.#node.shadowRoot;
2480
2617
  }
2481
2618
  if (shadowRoot) {
2482
2619
  let bool = true;
@@ -2486,19 +2623,11 @@ export class Finder {
2486
2623
  switch (filterLeaf.name) {
2487
2624
  case 'host':
2488
2625
  case 'host-context': {
2489
- const matchedNode = this._matchShadowHostPseudoClass(
2490
- filterLeaf,
2491
- shadowRoot
2492
- );
2493
- bool = matchedNode === shadowRoot;
2626
+ bool = this._evaluateShadowHost(filterLeaf, shadowRoot);
2494
2627
  break;
2495
2628
  }
2496
2629
  case 'has': {
2497
- bool = this._matchPseudoClassSelector(
2498
- filterLeaf,
2499
- shadowRoot,
2500
- {}
2501
- ).has(shadowRoot);
2630
+ bool = this._matchPseudoClassSelector(filterLeaf, shadowRoot, {});
2502
2631
  break;
2503
2632
  }
2504
2633
  default: {
@@ -2613,31 +2742,20 @@ export class Finder {
2613
2742
  const {
2614
2743
  leaves: [{ name: lastName, type: lastType }]
2615
2744
  } = lastTwig;
2616
- const { combo: firstCombo } = firstTwig;
2617
2745
  if (
2618
2746
  this.#selector.includes(':scope') ||
2619
2747
  lastType === PS_ELEMENT_SELECTOR ||
2620
2748
  lastType === ID_SELECTOR
2621
2749
  ) {
2622
2750
  return { dir: DIR_PREV, twig: lastTwig };
2623
- }
2624
- if (firstType === ID_SELECTOR) {
2751
+ } else if (firstType === ID_SELECTOR) {
2625
2752
  return { dir: DIR_NEXT, twig: firstTwig };
2626
- }
2627
- if (firstName === '*' && firstType === TYPE_SELECTOR) {
2753
+ } else if (firstName === '*' && firstType === TYPE_SELECTOR) {
2628
2754
  return { dir: DIR_PREV, twig: lastTwig };
2629
- }
2630
- if (lastName === '*' && lastType === TYPE_SELECTOR) {
2755
+ } else if (lastName === '*' && lastType === TYPE_SELECTOR) {
2631
2756
  return { dir: DIR_NEXT, twig: firstTwig };
2632
- }
2633
- if (branchLen === 2) {
2634
- if (targetType === TARGET_FIRST) {
2635
- return { dir: DIR_PREV, twig: lastTwig };
2636
- }
2637
- const { name: comboName } = firstCombo;
2638
- if (comboName === '+' || comboName === '~') {
2639
- return { dir: DIR_PREV, twig: lastTwig };
2640
- }
2757
+ } else if (branchLen === 1 || branchLen === 2) {
2758
+ return { dir: DIR_PREV, twig: lastTwig };
2641
2759
  } else if (branchLen > 2 && this.#scoped && targetType === TARGET_FIRST) {
2642
2760
  if (lastType === TYPE_SELECTOR) {
2643
2761
  return { dir: DIR_PREV, twig: lastTwig };
@@ -2862,7 +2980,6 @@ export class Finder {
2862
2980
  const matchedNodes = new Set();
2863
2981
  const branchLen = branch.length;
2864
2982
  const lastIndex = branchLen - 1;
2865
-
2866
2983
  if (dir === DIR_NEXT) {
2867
2984
  const { combo: firstCombo } = branch[0];
2868
2985
  for (const node of entryNodes) {