@asamuzakjp/dom-selector 8.0.1 → 8.1.1

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