@asamuzakjp/dom-selector 8.1.5 → 8.2.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.
@@ -0,0 +1,2142 @@
1
+ /**
2
+ * evaluator.js
3
+ */
4
+
5
+ /* import */
6
+ import {
7
+ matchAttributeSelector,
8
+ matchDirectionPseudoClass,
9
+ matchDisabledPseudoClass,
10
+ matchLanguagePseudoClass,
11
+ matchPseudoElementSelector,
12
+ matchReadOnlyPseudoClass,
13
+ matchTypeSelector
14
+ } from './matcher.js';
15
+ import { generateCSS, unescapeSelector, walkAST } from './parser.js';
16
+ import {
17
+ findBestSeed,
18
+ generateException,
19
+ isCustomElement,
20
+ isFocusVisible,
21
+ isFocusableArea,
22
+ populateHasAllowlist,
23
+ resolveContent,
24
+ traverseNode
25
+ } from './utility.js';
26
+
27
+ /* constants */
28
+ import {
29
+ ATTR_SELECTOR,
30
+ CLASS_SELECTOR,
31
+ COMBINATOR,
32
+ DIR_NEXT,
33
+ DIR_PREV,
34
+ DOCUMENT_FRAGMENT_NODE,
35
+ ELEMENT_NODE,
36
+ FORM_PARTS,
37
+ HEX,
38
+ ID_SELECTOR,
39
+ INPUT_CHECK,
40
+ INPUT_DATE,
41
+ INPUT_EDIT,
42
+ INPUT_TEXT,
43
+ KEYS_LOGICAL,
44
+ NOT_SUPPORTED_ERR,
45
+ PS_CLASS_SELECTOR,
46
+ PS_ELEMENT_SELECTOR,
47
+ SHOW_ALL,
48
+ SHOW_CONTAINER,
49
+ SYNTAX_ERR,
50
+ TEXT_NODE,
51
+ TYPE_SELECTOR
52
+ } from './constant.js';
53
+ const KEYS_FORM = new Set([...FORM_PARTS, 'fieldset', 'form']);
54
+ const KEYS_FORM_PS_VALID = new Set([...FORM_PARTS, 'form']);
55
+ const KEYS_INPUT_CHECK = new Set(INPUT_CHECK);
56
+ const KEYS_INPUT_PLACEHOLDER = new Set([...INPUT_TEXT, 'number']);
57
+ const KEYS_INPUT_RANGE = new Set([...INPUT_DATE, 'number', 'range']);
58
+ const KEYS_INPUT_REQUIRED = new Set([...INPUT_CHECK, ...INPUT_EDIT, 'file']);
59
+ const KEYS_INPUT_RESET = new Set(['button', 'reset']);
60
+ const KEYS_INPUT_SUBMIT = new Set(['image', 'submit']);
61
+ const KEYS_MODIFIER = new Set([
62
+ 'Alt',
63
+ 'AltGraph',
64
+ 'CapsLock',
65
+ 'Control',
66
+ 'Fn',
67
+ 'FnLock',
68
+ 'Hyper',
69
+ 'Meta',
70
+ 'NumLock',
71
+ 'ScrollLock',
72
+ 'Shift',
73
+ 'Super',
74
+ 'Symbol',
75
+ 'SymbolLock'
76
+ ]);
77
+ const KEYS_PS_UNCACHE = new Set([
78
+ 'any-link',
79
+ 'defined',
80
+ 'dir',
81
+ 'link',
82
+ 'scope'
83
+ ]);
84
+ const KEYS_PS_NTH_OF_TYPE = new Set([
85
+ 'first-of-type',
86
+ 'last-of-type',
87
+ 'only-of-type'
88
+ ]);
89
+
90
+ /**
91
+ * Evaluator
92
+ */
93
+ export class Evaluator {
94
+ /* private fields */
95
+ #anbCache;
96
+ #astCache = new WeakMap();
97
+ #documentURL;
98
+ #event;
99
+ #eventHandlers;
100
+ #filterLeavesCache;
101
+ #focus;
102
+ #focusWithinCache;
103
+ #invalidateResults;
104
+ #lastFocusVisible;
105
+ #psDefaultCache;
106
+ #psDirCache;
107
+ #psHasFilterCache;
108
+ #psIndeterminateCache;
109
+ #psLangCache;
110
+ #psValidCache;
111
+ #results;
112
+ #verifyShadowHost;
113
+ #walkers;
114
+
115
+ /**
116
+ * constructor
117
+ * @param {object} window - The window object.
118
+ */
119
+ constructor(window) {
120
+ this.window = window;
121
+ this.documentCache = new WeakMap();
122
+ this.clearResults(true);
123
+ this.#event = null;
124
+ this.#focus = null;
125
+ this.#lastFocusVisible = null;
126
+ this.#eventHandlers = new Set([
127
+ {
128
+ keys: ['focus', 'focusin'],
129
+ handler: this._handleFocusEvent
130
+ },
131
+ {
132
+ keys: ['keydown', 'keyup'],
133
+ handler: this._handleKeyboardEvent
134
+ },
135
+ {
136
+ keys: ['mouseover', 'mousedown', 'mouseup', 'click', 'mouseout'],
137
+ handler: this._handleMouseEvent
138
+ }
139
+ ]);
140
+ this._registerEventListeners();
141
+ }
142
+
143
+ /**
144
+ * Sets up the evaluator.
145
+ * @param {string} selector - The CSS selector.
146
+ * @param {object} node - Document, DocumentFragment, or Element.
147
+ * @param {object} [opt] - Options.
148
+ * @param {boolean} [opt.check] - Indicates if running in internal check().
149
+ * @param {boolean} [opt.noexcept] - If true, exceptions are not thrown.
150
+ * @param {boolean} [opt.warn] - If true, console warnings are enabled.
151
+ * @returns {object} The evaluator instance.
152
+ */
153
+ setup(selector, node, opt = {}) {
154
+ const { check, noexcept, warn } = opt;
155
+ this.check = !!check;
156
+ this.noexcept = !!noexcept;
157
+ this.warn = !!warn;
158
+ this.matchOpts = { warn: this.warn };
159
+ [this.document, this.root, this.shadow] = resolveContent(node);
160
+ this.node = node;
161
+ this.pseudoElements = [];
162
+ this.invalidate = false;
163
+ this.clearResults();
164
+ this.#documentURL = null;
165
+ this.#verifyShadowHost = false;
166
+ this.#walkers = null;
167
+ return this;
168
+ }
169
+
170
+ /**
171
+ * Handles errors.
172
+ * @param {Error} e - The error object.
173
+ * @param {object} [opt] - Options.
174
+ * @param {boolean} [opt.noexcept] - If true, exceptions are not thrown.
175
+ * @throws {Error} Throws an error.
176
+ * @returns {void}
177
+ */
178
+ onError = (e, opt = {}) => {
179
+ const noexcept = opt.noexcept ?? this.noexcept;
180
+ if (noexcept) {
181
+ return;
182
+ }
183
+ const isDOMException =
184
+ e instanceof DOMException || e instanceof this.window.DOMException;
185
+ if (isDOMException) {
186
+ if (e.name === NOT_SUPPORTED_ERR) {
187
+ if (this.warn) {
188
+ console.warn(e.message);
189
+ }
190
+ return;
191
+ }
192
+ throw new this.window.DOMException(e.message, e.name);
193
+ }
194
+ if (e.name in this.window) {
195
+ throw new this.window[e.name](e.message, { cause: e });
196
+ }
197
+ throw e;
198
+ };
199
+
200
+ /**
201
+ * Clear cached results.
202
+ * @param {boolean} all - Clear all results.
203
+ * @returns {void}
204
+ */
205
+ clearResults = (all = false) => {
206
+ this.#anbCache = null;
207
+ this.#focusWithinCache = null;
208
+ this.#invalidateResults = null;
209
+ this.#psDefaultCache = null;
210
+ this.#psDirCache = null;
211
+ this.#psHasFilterCache = null;
212
+ this.#psIndeterminateCache = null;
213
+ this.#psLangCache = null;
214
+ this.#psValidCache = null;
215
+ if (all) {
216
+ this.#filterLeavesCache = null;
217
+ this.#results = new WeakMap();
218
+ }
219
+ };
220
+
221
+ /**
222
+ * Matches a selector.
223
+ * @param {object} ast - The AST.
224
+ * @param {object} node - The Document, DocumentFragment, or Element node.
225
+ * @param {object} opt - Options.
226
+ * @returns {boolean} True if matches, otherwise false.
227
+ */
228
+ matchSelector = (ast, node, opt) => {
229
+ if (node.nodeType === ELEMENT_NODE) {
230
+ return this._matchSelectorForElement(ast, node, opt);
231
+ }
232
+ if (
233
+ this.shadow &&
234
+ node.nodeType === DOCUMENT_FRAGMENT_NODE &&
235
+ ast.type === PS_CLASS_SELECTOR
236
+ ) {
237
+ return this._matchSelectorForShadowRoot(ast, node, opt);
238
+ }
239
+ return false;
240
+ };
241
+
242
+ /**
243
+ * Matches leaves against a node with cache check.
244
+ * @param {Array.<object>} leaves - The AST leaves to match.
245
+ * @param {object} node - The DOM node.
246
+ * @param {object} opt - The match options.
247
+ * @returns {boolean} True if matched, otherwise false.
248
+ */
249
+ matchLeaves = (leaves, node, opt) => {
250
+ if (!this.#invalidateResults) {
251
+ this.#invalidateResults = new WeakMap();
252
+ }
253
+ const results = this.invalidate ? this.#invalidateResults : this.#results;
254
+ let result = results.get(leaves);
255
+ if (result) {
256
+ const nodeResult = result.get(node);
257
+ if (nodeResult) {
258
+ return nodeResult.matched;
259
+ }
260
+ }
261
+ let cacheable = true;
262
+ if (node.nodeType === ELEMENT_NODE && KEYS_FORM.has(node.localName)) {
263
+ cacheable = false;
264
+ }
265
+ let bool;
266
+ const l = leaves.length;
267
+ for (let i = 0; i < l; i++) {
268
+ const leaf = leaves[i];
269
+ switch (leaf.type) {
270
+ case ATTR_SELECTOR:
271
+ case ID_SELECTOR: {
272
+ cacheable = false;
273
+ break;
274
+ }
275
+ case PS_CLASS_SELECTOR: {
276
+ if (KEYS_PS_UNCACHE.has(leaf.name)) {
277
+ cacheable = false;
278
+ }
279
+ break;
280
+ }
281
+ default: {
282
+ // No action needed for other types.
283
+ }
284
+ }
285
+ bool = this.matchSelector(leaf, node, opt);
286
+ if (!bool) {
287
+ break;
288
+ }
289
+ }
290
+ if (cacheable) {
291
+ if (!result) {
292
+ result = new WeakMap();
293
+ }
294
+ result.set(node, {
295
+ matched: bool
296
+ });
297
+ results.set(leaves, result);
298
+ }
299
+ return bool;
300
+ };
301
+
302
+ /**
303
+ * Returns a cached slice of the leaves array (excluding the first item).
304
+ * @param {Array.<object>} leaves - The original AST leaves array.
305
+ * @returns {Array.<object>} The filtered leaves.
306
+ */
307
+ getFilterLeaves = leaves => {
308
+ if (!this.#filterLeavesCache) {
309
+ this.#filterLeavesCache = new WeakMap();
310
+ }
311
+ let filterLeaves = this.#filterLeavesCache.get(leaves);
312
+ if (filterLeaves) {
313
+ return filterLeaves;
314
+ }
315
+ filterLeaves = leaves.slice(1);
316
+ this.#filterLeavesCache.set(leaves, filterLeaves);
317
+ return filterLeaves;
318
+ };
319
+
320
+ /**
321
+ * Evaluates shadow host pseudo-classes.
322
+ * @param {object} ast - The AST.
323
+ * @param {object} node - The DocumentFragment node.
324
+ * @returns {boolean} True if matches, otherwise false.
325
+ */
326
+ evaluateShadowHost = (ast, node) => {
327
+ const { children: astChildren, name: astName } = ast;
328
+ // Handle simple pseudo-class (no arguments).
329
+ if (!Array.isArray(astChildren)) {
330
+ if (astName === 'host') {
331
+ return true;
332
+ }
333
+ const msg = `Invalid selector :${astName}`;
334
+ this.onError(generateException(msg, SYNTAX_ERR, this.window));
335
+ return false;
336
+ }
337
+ // Handle functional pseudo-class like :host(...).
338
+ if (astName !== 'host' && astName !== 'host-context') {
339
+ const msg = `Invalid selector :${astName}()`;
340
+ this.onError(generateException(msg, SYNTAX_ERR, this.window));
341
+ return false;
342
+ }
343
+ if (astChildren.length !== 1) {
344
+ const css = generateCSS(ast);
345
+ const msg = `Invalid selector ${css}`;
346
+ this.onError(generateException(msg, SYNTAX_ERR, this.window));
347
+ return false;
348
+ }
349
+ const { host } = node;
350
+ const { branches } = walkAST(astChildren[0]);
351
+ const [branch] = branches;
352
+ const [...leaves] = branch;
353
+ if (astName === 'host' && this._evaluateHostPseudo(leaves, host, ast)) {
354
+ return true;
355
+ } else if (
356
+ astName === 'host-context' &&
357
+ this._evaluateHostContextPseudo(leaves, host, ast)
358
+ ) {
359
+ return true;
360
+ }
361
+ return false;
362
+ };
363
+
364
+ /**
365
+ * Matches pseudo-class selector.
366
+ * @see https://html.spec.whatwg.org/_pseudo-classes
367
+ * @param {object} ast - The AST.
368
+ * @param {object} node - The Element node.
369
+ * @param {object} [opt] - Options.
370
+ * @param {boolean} [opt.forgive] - Ignores unknown or invalid selectors.
371
+ * @param {boolean} [opt.warn] - If true, console warnings are enabled.
372
+ * @returns {Set.<object>|boolean} A collection of matched nodes.
373
+ */
374
+ matchPseudoClassSelector = (ast, node, opt = {}) => {
375
+ const { children: astChildren, name: astName } = ast;
376
+ const { localName, parentNode } = node;
377
+ const { forgive, warn = this.warn } = opt;
378
+ if (Array.isArray(astChildren)) {
379
+ // :has(), :is(), :not(), :where()
380
+ if (KEYS_LOGICAL.has(astName)) {
381
+ return this._evaluateLogicalPseudo(ast, node, opt);
382
+ }
383
+ return this._evaluatePseudoClassFunc(ast, node, opt);
384
+ }
385
+ if (KEYS_PS_NTH_OF_TYPE.has(astName)) {
386
+ if (!parentNode) {
387
+ return node === this.root;
388
+ }
389
+ const { localName, namespaceURI } = node;
390
+ let hasPrev = false;
391
+ let hasNext = false;
392
+ let current = node.previousElementSibling;
393
+ while (current) {
394
+ if (
395
+ current.localName === localName &&
396
+ current.namespaceURI === namespaceURI
397
+ ) {
398
+ hasPrev = true;
399
+ break;
400
+ }
401
+ current = current.previousElementSibling;
402
+ }
403
+ if (astName !== 'first-of-type') {
404
+ current = node.nextElementSibling;
405
+ while (current) {
406
+ if (
407
+ current.localName === localName &&
408
+ current.namespaceURI === namespaceURI
409
+ ) {
410
+ hasNext = true;
411
+ break;
412
+ }
413
+ current = current.nextElementSibling;
414
+ }
415
+ }
416
+ switch (astName) {
417
+ case 'first-of-type': {
418
+ return !hasPrev;
419
+ }
420
+ case 'last-of-type': {
421
+ return !hasNext;
422
+ }
423
+ case 'only-of-type':
424
+ default: {
425
+ return !hasPrev && !hasNext;
426
+ }
427
+ }
428
+ }
429
+ switch (astName) {
430
+ /* Elemental pseudo-classes */
431
+ case 'defined': {
432
+ if (node.hasAttribute('is') || localName.includes('-')) {
433
+ return isCustomElement(node);
434
+ }
435
+ return (
436
+ node instanceof this.window.HTMLElement ||
437
+ node instanceof this.window.SVGElement
438
+ );
439
+ }
440
+ /* Element display state pseudo-classes */
441
+ case 'open': {
442
+ // <select> and <input type="color"> are not supported.
443
+ return (
444
+ (localName === 'details' || localName === 'dialog') &&
445
+ node.hasAttribute('open')
446
+ );
447
+ }
448
+ case 'popover-open': {
449
+ // FIXME: Not implemented in jsdom
450
+ // @see https://github.com/jsdom/jsdom/issues/3721
451
+ // return node.popover && isVisible(node);
452
+ break;
453
+ }
454
+ /* Input pseudo-classes */
455
+ case 'disabled':
456
+ case 'enabled': {
457
+ return matchDisabledPseudoClass(astName, node);
458
+ }
459
+ case 'read-only':
460
+ case 'read-write': {
461
+ return matchReadOnlyPseudoClass(astName, node);
462
+ }
463
+ case 'placeholder-shown': {
464
+ let placeholder;
465
+ if (node.placeholder) {
466
+ placeholder = node.placeholder;
467
+ } else if (node.hasAttribute('placeholder')) {
468
+ placeholder = node.getAttribute('placeholder');
469
+ }
470
+ if (typeof placeholder === 'string' && !/[\r\n]/.test(placeholder)) {
471
+ let targetNode;
472
+ if (localName === 'textarea') {
473
+ targetNode = node;
474
+ } else if (localName === 'input') {
475
+ if (node.hasAttribute('type')) {
476
+ if (KEYS_INPUT_PLACEHOLDER.has(node.getAttribute('type'))) {
477
+ targetNode = node;
478
+ }
479
+ } else {
480
+ targetNode = node;
481
+ }
482
+ }
483
+ if (targetNode) {
484
+ return node.value === '';
485
+ }
486
+ }
487
+ break;
488
+ }
489
+ case 'default': {
490
+ // option
491
+ if (localName === 'option') {
492
+ return node.hasAttribute('selected');
493
+ }
494
+ const attrType = node.getAttribute('type');
495
+ // input[type="checkbox"], input[type="radio"]
496
+ if (
497
+ localName === 'input' &&
498
+ node.hasAttribute('type') &&
499
+ node.hasAttribute('checked')
500
+ ) {
501
+ return KEYS_INPUT_CHECK.has(attrType);
502
+ }
503
+ // button[type="submit"], input[type="submit"], input[type="image"]
504
+ if (
505
+ (localName === 'button' &&
506
+ !(node.hasAttribute('type') && KEYS_INPUT_RESET.has(attrType))) ||
507
+ (localName === 'input' &&
508
+ node.hasAttribute('type') &&
509
+ KEYS_INPUT_SUBMIT.has(attrType))
510
+ ) {
511
+ let form = node.parentNode;
512
+ while (form) {
513
+ if (form.localName === 'form') {
514
+ break;
515
+ }
516
+ form = form.parentNode;
517
+ }
518
+ if (form) {
519
+ if (!this.#psDefaultCache) {
520
+ this.#psDefaultCache = new WeakMap();
521
+ }
522
+ let defaultSubmit = this.#psDefaultCache.get(form);
523
+ if (defaultSubmit === undefined) {
524
+ const walker = this.createTreeWalker(form, { force: true });
525
+ let refNode = traverseNode(form, walker);
526
+ refNode = walker.firstChild();
527
+ while (refNode) {
528
+ const nodeName = refNode.localName;
529
+ const nodeAttrType = refNode.getAttribute('type');
530
+ let m;
531
+ if (nodeName === 'button') {
532
+ m = !(
533
+ refNode.hasAttribute('type') &&
534
+ KEYS_INPUT_RESET.has(nodeAttrType)
535
+ );
536
+ } else if (nodeName === 'input') {
537
+ m =
538
+ refNode.hasAttribute('type') &&
539
+ KEYS_INPUT_SUBMIT.has(nodeAttrType);
540
+ }
541
+ if (m) {
542
+ defaultSubmit = refNode;
543
+ break;
544
+ }
545
+ refNode = walker.nextNode();
546
+ }
547
+ this.#psDefaultCache.set(form, defaultSubmit);
548
+ }
549
+ return defaultSubmit === node;
550
+ }
551
+ }
552
+ break;
553
+ }
554
+ case 'checked': {
555
+ if (localName === 'option') {
556
+ return node.selected;
557
+ }
558
+ if (localName === 'input') {
559
+ const attrType = node.getAttribute('type');
560
+ return (
561
+ node.checked && (attrType === 'checkbox' || attrType === 'radio')
562
+ );
563
+ }
564
+ break;
565
+ }
566
+ case 'indeterminate': {
567
+ if (localName === 'progress') {
568
+ return !node.hasAttribute('value');
569
+ }
570
+ if (localName === 'input' && node.type === 'checkbox') {
571
+ return node.indeterminate;
572
+ }
573
+ if (localName === 'input' && node.type === 'radio') {
574
+ if (node.checked || node.hasAttribute('checked')) {
575
+ return false;
576
+ }
577
+ const nodeName = node.name;
578
+ let parent = node.parentNode;
579
+ while (parent) {
580
+ if (parent.localName === 'form') {
581
+ break;
582
+ }
583
+ parent = parent.parentNode;
584
+ }
585
+ if (!parent) {
586
+ parent = this.document.documentElement;
587
+ }
588
+ if (!this.#psIndeterminateCache) {
589
+ this.#psIndeterminateCache = new WeakMap();
590
+ }
591
+ let parentCache = this.#psIndeterminateCache.get(parent);
592
+ if (parentCache === undefined) {
593
+ parentCache = new Map();
594
+ this.#psIndeterminateCache.set(parent, parentCache);
595
+ }
596
+ let checked = parentCache.get(nodeName);
597
+ if (checked === undefined) {
598
+ const walker = this.createTreeWalker(parent, { force: true });
599
+ let refNode = traverseNode(parent, walker);
600
+ refNode = walker.firstChild();
601
+ while (refNode) {
602
+ if (
603
+ refNode.localName === 'input' &&
604
+ refNode.getAttribute('type') === 'radio'
605
+ ) {
606
+ if (refNode.hasAttribute('name')) {
607
+ if (refNode.getAttribute('name') === nodeName) {
608
+ checked = !!refNode.checked;
609
+ }
610
+ } else {
611
+ checked = !!refNode.checked;
612
+ }
613
+ if (checked) {
614
+ break;
615
+ }
616
+ }
617
+ refNode = walker.nextNode();
618
+ }
619
+ checked = !!checked;
620
+ parentCache.set(nodeName, checked);
621
+ }
622
+ return !checked;
623
+ }
624
+ break;
625
+ }
626
+ case 'valid':
627
+ case 'invalid': {
628
+ if (KEYS_FORM_PS_VALID.has(localName)) {
629
+ let valid = false;
630
+ if (node.checkValidity()) {
631
+ if (node.maxLength >= 0) {
632
+ if (node.maxLength >= node.value.length) {
633
+ valid = true;
634
+ }
635
+ } else {
636
+ valid = true;
637
+ }
638
+ }
639
+ if (astName === 'invalid') {
640
+ return !valid;
641
+ }
642
+ return valid;
643
+ }
644
+ if (localName === 'fieldset') {
645
+ if (!this.#psValidCache) {
646
+ this.#psValidCache = new WeakMap();
647
+ }
648
+ let valid = this.#psValidCache.get(node);
649
+ if (valid === undefined) {
650
+ const walker = this.createTreeWalker(node, { force: true });
651
+ let refNode = traverseNode(node, walker);
652
+ refNode = walker.firstChild();
653
+ if (!refNode) {
654
+ valid = true;
655
+ } else {
656
+ while (refNode) {
657
+ if (KEYS_FORM_PS_VALID.has(refNode.localName)) {
658
+ if (refNode.checkValidity()) {
659
+ if (refNode.maxLength >= 0) {
660
+ valid = refNode.maxLength >= refNode.value.length;
661
+ } else {
662
+ valid = true;
663
+ }
664
+ } else {
665
+ valid = false;
666
+ }
667
+ if (!valid) {
668
+ break;
669
+ }
670
+ }
671
+ refNode = walker.nextNode();
672
+ }
673
+ }
674
+ this.#psValidCache.set(node, valid);
675
+ }
676
+ if (astName === 'invalid') {
677
+ return !valid;
678
+ }
679
+ return valid;
680
+ }
681
+ break;
682
+ }
683
+ case 'in-range':
684
+ case 'out-of-range': {
685
+ const attrType = node.getAttribute('type');
686
+ if (
687
+ localName === 'input' &&
688
+ !(node.readOnly || node.hasAttribute('readonly')) &&
689
+ !(node.disabled || node.hasAttribute('disabled')) &&
690
+ KEYS_INPUT_RANGE.has(attrType)
691
+ ) {
692
+ const flowed =
693
+ node.validity.rangeUnderflow || node.validity.rangeOverflow;
694
+ if (astName === 'out-of-range') {
695
+ return flowed;
696
+ }
697
+ return flowed
698
+ ? false
699
+ : node.hasAttribute('min') ||
700
+ node.hasAttribute('max') ||
701
+ attrType === 'range';
702
+ }
703
+ break;
704
+ }
705
+ case 'required':
706
+ case 'optional': {
707
+ let required = false;
708
+ if (localName === 'select' || localName === 'textarea') {
709
+ if (node.required || node.hasAttribute('required')) {
710
+ required = true;
711
+ }
712
+ } else if (localName === 'input') {
713
+ if (node.hasAttribute('type')) {
714
+ const attrType = node.getAttribute('type');
715
+ if (KEYS_INPUT_REQUIRED.has(attrType)) {
716
+ if (node.required || node.hasAttribute('required')) {
717
+ required = true;
718
+ }
719
+ }
720
+ } else if (node.required || node.hasAttribute('required')) {
721
+ required = true;
722
+ }
723
+ }
724
+ if (astName === 'optional') {
725
+ return !required;
726
+ }
727
+ return required;
728
+ }
729
+ /* Location pseudo-classes */
730
+ case 'any-link':
731
+ case 'link': {
732
+ return (
733
+ (localName === 'a' || localName === 'area') &&
734
+ node.hasAttribute('href')
735
+ );
736
+ }
737
+ case 'local-link': {
738
+ if (
739
+ (localName === 'a' || localName === 'area') &&
740
+ node.hasAttribute('href')
741
+ ) {
742
+ if (!this.#documentURL) {
743
+ this.#documentURL = new URL(this.document.URL);
744
+ }
745
+ const { href, origin, pathname } = this.#documentURL;
746
+ const attrURL = new URL(node.getAttribute('href'), href);
747
+ return attrURL.origin === origin && attrURL.pathname === pathname;
748
+ }
749
+ break;
750
+ }
751
+ case 'visited': {
752
+ // prevent fingerprinting
753
+ break;
754
+ }
755
+ case 'target': {
756
+ if (!this.#documentURL) {
757
+ this.#documentURL = new URL(this.document.URL);
758
+ }
759
+ const { hash } = this.#documentURL;
760
+ return hash && hash === `#${node.id}` && this.document.contains(node);
761
+ }
762
+ case 'scope': {
763
+ if (this.node.nodeType === ELEMENT_NODE) {
764
+ return !this.shadow && node === this.node;
765
+ }
766
+ return node === this.document.documentElement;
767
+ }
768
+ /* Tree-structural pseudo-classes */
769
+ case 'root': {
770
+ return node === this.document.documentElement;
771
+ }
772
+ case 'empty': {
773
+ if (!node.hasChildNodes()) {
774
+ return true;
775
+ }
776
+ const walker = this.createTreeWalker(node, {
777
+ force: true,
778
+ whatToShow: SHOW_ALL
779
+ });
780
+ let refNode = walker.firstChild();
781
+ let bool;
782
+ while (refNode) {
783
+ bool =
784
+ refNode.nodeType !== ELEMENT_NODE && refNode.nodeType !== TEXT_NODE;
785
+ if (!bool) {
786
+ break;
787
+ }
788
+ refNode = walker.nextSibling();
789
+ }
790
+ return bool;
791
+ }
792
+ case 'first-child':
793
+ case 'last-child':
794
+ case 'only-child': {
795
+ if (!parentNode) {
796
+ return node === this.root;
797
+ }
798
+ if (astName === 'first-child') {
799
+ return node === parentNode.firstElementChild;
800
+ }
801
+ if (astName === 'last-child') {
802
+ return node === parentNode.lastElementChild;
803
+ }
804
+ return (
805
+ node === parentNode.firstElementChild &&
806
+ node === parentNode.lastElementChild
807
+ );
808
+ }
809
+ /* User action pseudo-classes */
810
+ case 'hover': {
811
+ const { target, type } = this.#event ?? {};
812
+ return (
813
+ /^(?:click|mouse(?:down|over|up))$/.test(type) &&
814
+ target?.nodeType === ELEMENT_NODE &&
815
+ node.contains(target)
816
+ );
817
+ }
818
+ case 'active': {
819
+ const { buttons, target, type } = this.#event ?? {};
820
+ return (
821
+ type === 'mousedown' &&
822
+ buttons & 1 &&
823
+ target?.nodeType === ELEMENT_NODE &&
824
+ node.contains(target)
825
+ );
826
+ }
827
+ case 'focus': {
828
+ const activeElement = this.document.activeElement;
829
+ if (activeElement.shadowRoot) {
830
+ const activeShadowElement = activeElement.shadowRoot.activeElement;
831
+ let current = activeShadowElement;
832
+ while (current) {
833
+ if (current.nodeType === DOCUMENT_FRAGMENT_NODE) {
834
+ const { host } = current;
835
+ if (host === activeElement) {
836
+ if (isFocusableArea(node)) {
837
+ return true;
838
+ }
839
+ return host === node;
840
+ }
841
+ }
842
+ current = current.parentNode;
843
+ }
844
+ }
845
+ return node === activeElement && isFocusableArea(node);
846
+ }
847
+ case 'focus-visible': {
848
+ if (node === this.document.activeElement && isFocusableArea(node)) {
849
+ let bool;
850
+ if (isFocusVisible(node)) {
851
+ bool = true;
852
+ } else if (this.#focus) {
853
+ const { relatedTarget, target: focusTarget } = this.#focus;
854
+ if (focusTarget === node) {
855
+ if (isFocusVisible(relatedTarget)) {
856
+ bool = true;
857
+ } else if (this.#event) {
858
+ const {
859
+ altKey: eventAltKey,
860
+ ctrlKey: eventCtrlKey,
861
+ key: eventKey,
862
+ metaKey: eventMetaKey,
863
+ target: eventTarget,
864
+ type: eventType
865
+ } = this.#event;
866
+ // this.#event is irrelevant if eventTarget === relatedTarget
867
+ if (eventTarget === relatedTarget) {
868
+ if (!this.#lastFocusVisible) {
869
+ bool = true;
870
+ } else if (focusTarget === this.#lastFocusVisible) {
871
+ bool = true;
872
+ }
873
+ } else if (eventKey === 'Tab') {
874
+ if (
875
+ (eventType === 'keydown' && eventTarget !== node) ||
876
+ (eventType === 'keyup' && eventTarget === node)
877
+ ) {
878
+ if (eventTarget === focusTarget) {
879
+ if (!this.#lastFocusVisible) {
880
+ bool = true;
881
+ } else if (
882
+ eventTarget === this.#lastFocusVisible &&
883
+ relatedTarget === null
884
+ ) {
885
+ bool = true;
886
+ }
887
+ } else {
888
+ bool = true;
889
+ }
890
+ }
891
+ } else if (eventKey) {
892
+ if (
893
+ (eventType === 'keydown' || eventType === 'keyup') &&
894
+ !eventAltKey &&
895
+ !eventCtrlKey &&
896
+ !eventMetaKey &&
897
+ eventTarget === node
898
+ ) {
899
+ bool = true;
900
+ }
901
+ }
902
+ } else if (
903
+ relatedTarget === null ||
904
+ relatedTarget === this.#lastFocusVisible
905
+ ) {
906
+ bool = true;
907
+ }
908
+ }
909
+ }
910
+ if (bool) {
911
+ this.#lastFocusVisible = node;
912
+ return bool;
913
+ }
914
+ if (this.#lastFocusVisible === node) {
915
+ this.#lastFocusVisible = null;
916
+ }
917
+ }
918
+ break;
919
+ }
920
+ case 'focus-within': {
921
+ if (!this.#focusWithinCache) {
922
+ this.#focusWithinCache = new Set();
923
+ let currentFocus = this.document.activeElement;
924
+ while (currentFocus?.shadowRoot?.activeElement) {
925
+ currentFocus = currentFocus.shadowRoot.activeElement;
926
+ }
927
+ if (currentFocus && isFocusableArea(currentFocus)) {
928
+ while (currentFocus) {
929
+ this.#focusWithinCache.add(currentFocus);
930
+ if (currentFocus.parentNode) {
931
+ currentFocus = currentFocus.parentNode;
932
+ } else if (
933
+ currentFocus.nodeType === DOCUMENT_FRAGMENT_NODE &&
934
+ currentFocus.host
935
+ ) {
936
+ currentFocus = currentFocus.host;
937
+ } else {
938
+ break;
939
+ }
940
+ }
941
+ }
942
+ }
943
+ return this.#focusWithinCache.has(node);
944
+ }
945
+ // Ignore :host.
946
+ case 'host': {
947
+ break;
948
+ }
949
+ // Legacy pseudo-elements.
950
+ case 'after':
951
+ case 'before':
952
+ case 'first-letter':
953
+ case 'first-line': {
954
+ if (warn) {
955
+ this.onError(
956
+ generateException(
957
+ `Unsupported pseudo-element ::${astName}`,
958
+ NOT_SUPPORTED_ERR,
959
+ this.window
960
+ )
961
+ );
962
+ }
963
+ break;
964
+ }
965
+ // Not supported.
966
+ case 'autofill':
967
+ case 'blank':
968
+ case 'buffering':
969
+ case 'current':
970
+ case 'fullscreen':
971
+ case 'future':
972
+ case 'has-slotted':
973
+ case 'heading':
974
+ case 'modal':
975
+ case 'muted':
976
+ case 'past':
977
+ case 'paused':
978
+ case 'picture-in-picture':
979
+ case 'playing':
980
+ case 'seeking':
981
+ case 'stalled':
982
+ case 'user-invalid':
983
+ case 'user-valid':
984
+ case 'volume-locked':
985
+ case '-webkit-autofill': {
986
+ if (warn) {
987
+ this.onError(
988
+ generateException(
989
+ `Unsupported pseudo-class :${astName}`,
990
+ NOT_SUPPORTED_ERR,
991
+ this.window
992
+ )
993
+ );
994
+ }
995
+ break;
996
+ }
997
+ default: {
998
+ if (astName.startsWith('-webkit-')) {
999
+ if (warn) {
1000
+ this.onError(
1001
+ generateException(
1002
+ `Unsupported pseudo-class :${astName}`,
1003
+ NOT_SUPPORTED_ERR,
1004
+ this.window
1005
+ )
1006
+ );
1007
+ }
1008
+ } else if (!forgive) {
1009
+ this.onError(
1010
+ generateException(
1011
+ `Unknown pseudo-class :${astName}`,
1012
+ SYNTAX_ERR,
1013
+ this.window
1014
+ )
1015
+ );
1016
+ }
1017
+ }
1018
+ }
1019
+ return false;
1020
+ };
1021
+
1022
+ /**
1023
+ * Creates a TreeWalker.
1024
+ * @param {object} node - The Document, DocumentFragment, or Element node.
1025
+ * @param {object} [opt] - Options.
1026
+ * @param {boolean} [opt.force] - Force creation of a new TreeWalker.
1027
+ * @param {number} [opt.whatToShow] - The NodeFilter whatToShow value.
1028
+ * @returns {object} The TreeWalker object.
1029
+ */
1030
+ createTreeWalker = (node, opt = {}) => {
1031
+ const { force = false, whatToShow = SHOW_CONTAINER } = opt;
1032
+ if (force) {
1033
+ return this.document.createTreeWalker(node, whatToShow);
1034
+ }
1035
+ if (!this.#walkers) {
1036
+ this.#walkers = new WeakMap();
1037
+ }
1038
+ let walker = this.#walkers.get(node);
1039
+ if (walker) {
1040
+ return walker;
1041
+ }
1042
+ walker = this.document.createTreeWalker(node, whatToShow);
1043
+ this.#walkers.set(node, walker);
1044
+ return walker;
1045
+ };
1046
+
1047
+ /**
1048
+ * Yields combinator matches (Lazy evaluation, O(1) memory).
1049
+ * @param {object} twig - The twig object.
1050
+ * @param {object} node - The Element node.
1051
+ * @param {object} [opt] - Options.
1052
+ * @param {string} [opt.dir] - The find direction.
1053
+ * @yields {object} The matched node.
1054
+ */
1055
+ *yieldCombinatorMatches(twig, node, opt = {}) {
1056
+ const {
1057
+ combo: { name: comboName },
1058
+ leaves
1059
+ } = twig;
1060
+ const { dir } = opt;
1061
+ switch (comboName) {
1062
+ case '+': {
1063
+ const refNode =
1064
+ dir === DIR_NEXT
1065
+ ? node.nextElementSibling
1066
+ : node.previousElementSibling;
1067
+ if (refNode && this.matchLeaves(leaves, refNode, opt)) {
1068
+ yield refNode;
1069
+ }
1070
+ break;
1071
+ }
1072
+ case '~': {
1073
+ let refNode =
1074
+ dir === DIR_NEXT
1075
+ ? node.nextElementSibling
1076
+ : node.previousElementSibling;
1077
+ while (refNode) {
1078
+ if (this.matchLeaves(leaves, refNode, opt)) {
1079
+ yield refNode;
1080
+ }
1081
+ refNode =
1082
+ dir === DIR_NEXT
1083
+ ? refNode.nextElementSibling
1084
+ : refNode.previousElementSibling;
1085
+ }
1086
+ break;
1087
+ }
1088
+ case '>': {
1089
+ if (dir === DIR_NEXT) {
1090
+ let refNode = node.firstElementChild;
1091
+ while (refNode) {
1092
+ if (this.matchLeaves(leaves, refNode, opt)) {
1093
+ yield refNode;
1094
+ }
1095
+ refNode = refNode.nextElementSibling;
1096
+ }
1097
+ } else {
1098
+ const { parentNode } = node;
1099
+ if (parentNode && this.matchLeaves(leaves, parentNode, opt)) {
1100
+ yield parentNode;
1101
+ }
1102
+ }
1103
+ break;
1104
+ }
1105
+ case ' ':
1106
+ default: {
1107
+ if (dir === DIR_NEXT) {
1108
+ for (const refNode of this.yieldFindDescendantNodes(
1109
+ leaves,
1110
+ node,
1111
+ opt
1112
+ )) {
1113
+ yield refNode;
1114
+ }
1115
+ } else {
1116
+ const ancestors = [];
1117
+ let refNode = node.parentNode;
1118
+ while (refNode) {
1119
+ if (this.matchLeaves(leaves, refNode, opt)) {
1120
+ ancestors.push(refNode);
1121
+ }
1122
+ refNode = refNode.parentNode;
1123
+ }
1124
+ if (ancestors.length) {
1125
+ for (let i = ancestors.length - 1; i >= 0; i--) {
1126
+ yield ancestors[i];
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+ }
1133
+
1134
+ /**
1135
+ * Traverses all descendant nodes and yields matches.
1136
+ * @param {object} baseNode - The base Element node or Element.shadowRoot.
1137
+ * @param {Array.<object>} leaves - The AST leaves.
1138
+ * @param {object} opt - Options.
1139
+ * @yields {object} The matched node.
1140
+ */
1141
+ *yieldTraverseAllDescendants(baseNode, leaves, opt) {
1142
+ const walker = this.createTreeWalker(baseNode);
1143
+ traverseNode(baseNode, walker);
1144
+ let currentNode = walker.firstChild();
1145
+ while (currentNode) {
1146
+ if (this.matchLeaves(leaves, currentNode, opt)) {
1147
+ yield currentNode;
1148
+ }
1149
+ currentNode = walker.nextNode();
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Finds descendant nodes and yields matches.
1155
+ * @param {Array.<object>} leaves - The AST leaves.
1156
+ * @param {object} baseNode - The base Element node or Element.shadowRoot.
1157
+ * @param {object} opt - Options.
1158
+ * @yields {object} The matched node.
1159
+ */
1160
+ *yieldFindDescendantNodes(leaves, baseNode, opt) {
1161
+ const [{ name, type: leafType }] = leaves;
1162
+ const leafName = unescapeSelector(name);
1163
+ const filterLeaves = this.getFilterLeaves(leaves);
1164
+ const isSimple = filterLeaves.length === 0;
1165
+
1166
+ switch (leafType) {
1167
+ case ID_SELECTOR: {
1168
+ if (
1169
+ !this.shadow &&
1170
+ baseNode.nodeType === ELEMENT_NODE &&
1171
+ this.root.nodeType !== ELEMENT_NODE
1172
+ ) {
1173
+ const foundNode = this.root.getElementById(leafName);
1174
+ if (
1175
+ foundNode &&
1176
+ foundNode !== baseNode &&
1177
+ baseNode.contains(foundNode)
1178
+ ) {
1179
+ if (isSimple || this.matchLeaves(filterLeaves, foundNode, opt)) {
1180
+ yield foundNode;
1181
+ }
1182
+ }
1183
+ return;
1184
+ }
1185
+ break;
1186
+ }
1187
+ case CLASS_SELECTOR: {
1188
+ if (typeof baseNode.getElementsByClassName === 'function') {
1189
+ const collection = baseNode.getElementsByClassName(leafName);
1190
+ for (let i = 0, len = collection.length; i < len; i++) {
1191
+ const foundNode = collection[i];
1192
+ if (isSimple || this.matchLeaves(filterLeaves, foundNode, opt)) {
1193
+ yield foundNode;
1194
+ }
1195
+ }
1196
+ return;
1197
+ }
1198
+ break;
1199
+ }
1200
+ case TYPE_SELECTOR: {
1201
+ if (
1202
+ typeof baseNode.getElementsByTagName === 'function' &&
1203
+ !leafName.includes('|')
1204
+ ) {
1205
+ const collection = baseNode.getElementsByTagName(leafName);
1206
+ for (let i = 0, len = collection.length; i < len; i++) {
1207
+ const foundNode = collection[i];
1208
+ if (isSimple || this.matchLeaves(filterLeaves, foundNode, opt)) {
1209
+ yield foundNode;
1210
+ }
1211
+ }
1212
+ return;
1213
+ }
1214
+ break;
1215
+ }
1216
+ case PS_ELEMENT_SELECTOR: {
1217
+ matchPseudoElementSelector(leafName, leafType, opt);
1218
+ return;
1219
+ }
1220
+ default: {
1221
+ // no-op
1222
+ }
1223
+ }
1224
+ yield* this.yieldTraverseAllDescendants(baseNode, leaves, opt);
1225
+ }
1226
+
1227
+ /**
1228
+ * Handles focus events.
1229
+ * @private
1230
+ * @param {Event} evt - The event object.
1231
+ * @returns {void}
1232
+ */
1233
+ _handleFocusEvent = evt => {
1234
+ this.#focus = evt;
1235
+ };
1236
+
1237
+ /**
1238
+ * Handles keyboard events.
1239
+ * @private
1240
+ * @param {Event} evt - The event object.
1241
+ * @returns {void}
1242
+ */
1243
+ _handleKeyboardEvent = evt => {
1244
+ const { key } = evt;
1245
+ if (!KEYS_MODIFIER.has(key)) {
1246
+ this.#event = evt;
1247
+ }
1248
+ };
1249
+
1250
+ /**
1251
+ * Handles mouse events.
1252
+ * @private
1253
+ * @param {Event} evt - The event object.
1254
+ * @returns {void}
1255
+ */
1256
+ _handleMouseEvent = evt => {
1257
+ this.#event = evt;
1258
+ };
1259
+
1260
+ /**
1261
+ * Registers event listeners.
1262
+ * @private
1263
+ * @returns {Array.<void>} An array of return values from addEventListener.
1264
+ */
1265
+ _registerEventListeners = () => {
1266
+ const func = [];
1267
+ for (const eventHandler of this.#eventHandlers) {
1268
+ const { keys, handler } = eventHandler;
1269
+ const l = keys.length;
1270
+ for (let i = 0; i < l; i++) {
1271
+ const key = keys[i];
1272
+ func.push(
1273
+ this.window.addEventListener(key, handler, {
1274
+ capture: true,
1275
+ passive: true
1276
+ })
1277
+ );
1278
+ }
1279
+ }
1280
+ return func;
1281
+ };
1282
+
1283
+ /**
1284
+ * Gets selector branches from cache or parses them.
1285
+ * @private
1286
+ * @param {object} selector - The AST.
1287
+ * @returns {Array.<Array.<object>>} The selector branches.
1288
+ */
1289
+ _getSelectorBranches = selector => {
1290
+ let branches = this.#astCache.get(selector);
1291
+ if (branches) {
1292
+ return branches;
1293
+ }
1294
+ const walkedResult = walkAST(selector);
1295
+ branches = walkedResult.branches;
1296
+ this.#astCache.set(selector, branches);
1297
+ return branches;
1298
+ };
1299
+
1300
+ /**
1301
+ * Checks if a node matches any of the given selector branches.
1302
+ * @private
1303
+ * @param {Array.<Array.<object>>} branches - The selector branches to test.
1304
+ * @param {object} node - The element node to match against.
1305
+ * @param {object} [opt] - Optional parameters.
1306
+ * @returns {boolean} True if any branch matches, otherwise false.
1307
+ */
1308
+ _filterNthChildOfSelectorBranches = (branches, node, opt) => {
1309
+ let filterMatch = false;
1310
+ for (const branch of branches) {
1311
+ if (this.matchLeaves(branch, node, opt)) {
1312
+ filterMatch = true;
1313
+ break;
1314
+ }
1315
+ }
1316
+ return filterMatch;
1317
+ };
1318
+
1319
+ /**
1320
+ * Evaluates An+B mathematically.
1321
+ * @private
1322
+ * @param {object} ast - The AST.
1323
+ * @param {object} node - The Element node.
1324
+ * @param {string} nthName - The name of the nth pseudo-class.
1325
+ * @param {object} opt - Options.
1326
+ * @returns {boolean} True if matches, otherwise false.
1327
+ */
1328
+ _matchAnPlusB = (ast, node, nthName, opt) => {
1329
+ const {
1330
+ localName,
1331
+ namespaceURI,
1332
+ nextElementSibling,
1333
+ parentNode,
1334
+ previousElementSibling
1335
+ } = node;
1336
+ if (!parentNode && node !== this.root) {
1337
+ return false;
1338
+ }
1339
+ if (!this.#anbCache) {
1340
+ this.#anbCache = new WeakMap();
1341
+ }
1342
+ let anb = this.#anbCache.get(ast);
1343
+ if (anb === undefined) {
1344
+ const {
1345
+ nth: { a, b, name: nthIdentName },
1346
+ selector
1347
+ } = ast;
1348
+ anb = {
1349
+ a: 0,
1350
+ b: 0,
1351
+ isLast: nthName.includes('last'),
1352
+ isOfType: nthName.includes('of-type'),
1353
+ selector: null
1354
+ };
1355
+ if (nthIdentName) {
1356
+ if (nthIdentName === 'even') {
1357
+ anb.a = 2;
1358
+ anb.b = 0;
1359
+ } else if (nthIdentName === 'odd') {
1360
+ anb.a = 2;
1361
+ anb.b = 1;
1362
+ }
1363
+ } else {
1364
+ const intA = parseInt(a);
1365
+ if (Number.isInteger(intA)) {
1366
+ anb.a = intA;
1367
+ }
1368
+ const intB = parseInt(b);
1369
+ if (Number.isInteger(intB)) {
1370
+ anb.b = intB;
1371
+ }
1372
+ }
1373
+ if (selector && /^nth-(?:last-)?child$/.test(nthName)) {
1374
+ anb.selector = selector;
1375
+ }
1376
+ this.#anbCache.set(ast, anb);
1377
+ }
1378
+ const { a, b, isLast, isOfType, selector: anbSelector } = anb;
1379
+ const startNode = isLast ? nextElementSibling : previousElementSibling;
1380
+ let pos = 1;
1381
+ if (anbSelector) {
1382
+ const selectorBranches = this._getSelectorBranches(anbSelector);
1383
+ const filterMatch = this._filterNthChildOfSelectorBranches(
1384
+ selectorBranches,
1385
+ node,
1386
+ opt
1387
+ );
1388
+ if (!filterMatch) {
1389
+ return false;
1390
+ }
1391
+ let current = startNode;
1392
+ while (current) {
1393
+ if (
1394
+ this._filterNthChildOfSelectorBranches(selectorBranches, current, opt)
1395
+ ) {
1396
+ pos++;
1397
+ }
1398
+ current = isLast
1399
+ ? current.nextElementSibling
1400
+ : current.previousElementSibling;
1401
+ }
1402
+ } else {
1403
+ let current = startNode;
1404
+ while (current) {
1405
+ if (isOfType) {
1406
+ if (
1407
+ current.localName === localName &&
1408
+ current.namespaceURI === namespaceURI
1409
+ ) {
1410
+ pos++;
1411
+ }
1412
+ } else {
1413
+ pos++;
1414
+ }
1415
+ current = isLast
1416
+ ? current.nextElementSibling
1417
+ : current.previousElementSibling;
1418
+ }
1419
+ }
1420
+ if (a === 0) {
1421
+ return pos === b;
1422
+ }
1423
+ const diff = pos - b;
1424
+ if (diff % a !== 0) {
1425
+ return false;
1426
+ }
1427
+ // Equation: diff / a >= 0
1428
+ return a > 0 ? diff >= 0 : diff <= 0;
1429
+ };
1430
+
1431
+ /**
1432
+ * Evaluates if any combinator match satisfies the condition to short-circuit.
1433
+ * @private
1434
+ * @param {object} twig - The AST twig object.
1435
+ * @param {object} node - The element node.
1436
+ * @param {Array.<object>} remainingLeaves - The remaining AST leaves.
1437
+ * @param {object} opt - The match options.
1438
+ * @returns {boolean} True if matched, otherwise false.
1439
+ */
1440
+ _hasCombinatorMatch = (twig, node, remainingLeaves, opt) => {
1441
+ const {
1442
+ combo: { name: comboName },
1443
+ leaves
1444
+ } = twig;
1445
+ const isLast = remainingLeaves.length === 0;
1446
+ // Check if the target node satisfies the leaves and remaining conditions.
1447
+ const checkNode = refNode => {
1448
+ if (this.matchLeaves(leaves, refNode, opt)) {
1449
+ if (isLast) {
1450
+ return true;
1451
+ }
1452
+ if (this._matchHasPseudoFunc(remainingLeaves, refNode, opt)) {
1453
+ return true;
1454
+ }
1455
+ }
1456
+ return false;
1457
+ };
1458
+ switch (comboName) {
1459
+ case '+': {
1460
+ const refNode = node.nextElementSibling;
1461
+ return refNode ? checkNode(refNode) : false;
1462
+ }
1463
+ case '~': {
1464
+ let refNode = node.nextElementSibling;
1465
+ while (refNode) {
1466
+ if (checkNode(refNode)) {
1467
+ return true;
1468
+ }
1469
+ refNode = refNode.nextElementSibling;
1470
+ }
1471
+ return false;
1472
+ }
1473
+ case '>': {
1474
+ // Direct children only
1475
+ let refNode = node.firstElementChild;
1476
+ while (refNode) {
1477
+ if (checkNode(refNode)) {
1478
+ return true;
1479
+ }
1480
+ refNode = refNode.nextElementSibling;
1481
+ }
1482
+ return false;
1483
+ }
1484
+ case ' ':
1485
+ default: {
1486
+ const [leaf] = leaves;
1487
+ const filterLeaves = this.getFilterLeaves(leaves);
1488
+ // Fast path 1: ID
1489
+ if (
1490
+ leaf.type === ID_SELECTOR &&
1491
+ !this.shadow &&
1492
+ node.nodeType === ELEMENT_NODE &&
1493
+ this.root.nodeType !== ELEMENT_NODE
1494
+ ) {
1495
+ const leafName = unescapeSelector(leaf.name);
1496
+ const foundNode = this.root.getElementById(leafName);
1497
+ if (foundNode && foundNode !== node && node.contains(foundNode)) {
1498
+ // Only check filter leaves if it's a compound selector
1499
+ if (
1500
+ filterLeaves.length === 0 ||
1501
+ this.matchLeaves(filterLeaves, foundNode, opt)
1502
+ ) {
1503
+ if (isLast) {
1504
+ return true;
1505
+ }
1506
+ if (this._matchHasPseudoFunc(remainingLeaves, foundNode, opt)) {
1507
+ return true;
1508
+ }
1509
+ }
1510
+ }
1511
+ return false;
1512
+ }
1513
+ // Fast path 2: Class
1514
+ if (
1515
+ leaf.type === CLASS_SELECTOR &&
1516
+ typeof node.getElementsByClassName === 'function'
1517
+ ) {
1518
+ const leafName = unescapeSelector(leaf.name);
1519
+ const collection = node.getElementsByClassName(leafName);
1520
+ for (let i = 0, len = collection.length; i < len; i++) {
1521
+ const refNode = collection[i];
1522
+ // Apply filter before calling the expensive checkNode
1523
+ if (
1524
+ filterLeaves.length === 0 ||
1525
+ this.matchLeaves(filterLeaves, refNode, opt)
1526
+ ) {
1527
+ if (isLast) {
1528
+ return true;
1529
+ }
1530
+ if (this._matchHasPseudoFunc(remainingLeaves, refNode, opt)) {
1531
+ return true;
1532
+ }
1533
+ }
1534
+ }
1535
+ return false;
1536
+ }
1537
+ // Fast path 3: Type
1538
+ if (
1539
+ leaf.type === TYPE_SELECTOR &&
1540
+ typeof node.getElementsByTagName === 'function' &&
1541
+ !leaf.name.includes('|')
1542
+ ) {
1543
+ const leafName = unescapeSelector(leaf.name);
1544
+ const collection = node.getElementsByTagName(leafName);
1545
+ for (let i = 0, len = collection.length; i < len; i++) {
1546
+ const refNode = collection[i];
1547
+ // Apply filter before calling the expensive checkNode
1548
+ if (
1549
+ filterLeaves.length === 0 ||
1550
+ this.matchLeaves(filterLeaves, refNode, opt)
1551
+ ) {
1552
+ if (isLast) {
1553
+ return true;
1554
+ }
1555
+ if (this._matchHasPseudoFunc(remainingLeaves, refNode, opt)) {
1556
+ return true;
1557
+ }
1558
+ }
1559
+ }
1560
+ return false;
1561
+ }
1562
+ // Fallback: TreeWalker (for pseudo-elements, attributes, etc.)
1563
+ const walker = this.createTreeWalker(node);
1564
+ traverseNode(node, walker);
1565
+ let currentNode = walker.firstChild();
1566
+ while (currentNode) {
1567
+ if (checkNode(currentNode)) {
1568
+ return true;
1569
+ }
1570
+ currentNode = walker.nextNode();
1571
+ }
1572
+ return false;
1573
+ }
1574
+ }
1575
+ };
1576
+
1577
+ /**
1578
+ * Matches the :has() pseudo-class function.
1579
+ * @private
1580
+ * @param {Array.<object>} astLeaves - The AST leaves.
1581
+ * @param {object} node - The Element node.
1582
+ * @param {object} [opt] - Options.
1583
+ * @returns {boolean} True if matched, otherwise false.
1584
+ */
1585
+ _matchHasPseudoFunc = (astLeaves, node, opt = {}) => {
1586
+ const l = astLeaves.length;
1587
+ if (!l) {
1588
+ return false;
1589
+ }
1590
+ let combo;
1591
+ let startIndex = 0;
1592
+ if (astLeaves[0].type === COMBINATOR) {
1593
+ combo = astLeaves[0];
1594
+ startIndex = 1;
1595
+ } else {
1596
+ combo = { name: ' ', type: COMBINATOR };
1597
+ startIndex = 0;
1598
+ }
1599
+ const twigLeaves = [];
1600
+ let nextComboIndex = startIndex;
1601
+ for (; nextComboIndex < l; nextComboIndex++) {
1602
+ if (astLeaves[nextComboIndex].type === COMBINATOR) {
1603
+ break;
1604
+ }
1605
+ twigLeaves.push(astLeaves[nextComboIndex]);
1606
+ }
1607
+ const twig = { combo, leaves: twigLeaves };
1608
+ opt.dir = DIR_NEXT;
1609
+ const remainingLeaves = astLeaves.slice(nextComboIndex);
1610
+ return this._hasCombinatorMatch(twig, node, remainingLeaves, opt);
1611
+ };
1612
+
1613
+ /**
1614
+ * Builds an Allowlist for the :has() branch using a sparse seed element.
1615
+ * @private
1616
+ * @param {Array} leaves - The AST leaves of the selector branch.
1617
+ * @returns {object|null} The wrapper object containing the WeakSet, or null.
1618
+ */
1619
+ _buildHasAllowlist = leaves => {
1620
+ const { seed } = findBestSeed(leaves);
1621
+ if (!seed) {
1622
+ return null;
1623
+ }
1624
+ if (this.shadow || this.node.nodeType === DOCUMENT_FRAGMENT_NODE) {
1625
+ return null;
1626
+ }
1627
+ let seedElements = null;
1628
+ let isSingleNode = false;
1629
+ if (seed.type === 'id') {
1630
+ if (typeof this.root.getElementById === 'function') {
1631
+ const node = this.root.getElementById(seed.value);
1632
+ if (node) {
1633
+ seedElements = node;
1634
+ isSingleNode = true;
1635
+ }
1636
+ }
1637
+ } else if (seed.type === 'class') {
1638
+ if (typeof this.root.getElementsByClassName === 'function') {
1639
+ seedElements = this.root.getElementsByClassName(seed.value);
1640
+ }
1641
+ } else if (seed.type === 'tag') {
1642
+ if (typeof this.root.getElementsByTagName === 'function') {
1643
+ seedElements = this.root.getElementsByTagName(seed.value);
1644
+ }
1645
+ }
1646
+ if (!seedElements) {
1647
+ return null;
1648
+ }
1649
+ const len = isSingleNode ? 1 : seedElements.length;
1650
+ if (len === 0 || len > HEX * HEX) {
1651
+ return null;
1652
+ }
1653
+ const filterResult = {
1654
+ seeded: true,
1655
+ set: new WeakSet()
1656
+ };
1657
+ const list = filterResult.set;
1658
+ const visitedAncestors = new Set();
1659
+ if (this.node) {
1660
+ list.add(this.node);
1661
+ }
1662
+ for (let i = 0; i < len; i++) {
1663
+ const current = isSingleNode ? seedElements : seedElements[i];
1664
+ if (current) {
1665
+ populateHasAllowlist(current, list, visitedAncestors);
1666
+ }
1667
+ }
1668
+ return filterResult;
1669
+ };
1670
+
1671
+ /**
1672
+ * Evaluates :has() pseudo-class.
1673
+ * @private
1674
+ * @param {object} astData - The AST data.
1675
+ * @param {object} node - The Element node.
1676
+ * @param {object} [opt] - Options.
1677
+ * @returns {?object} The matched node.
1678
+ */
1679
+ _evaluateHasPseudo = (astData, node, opt = {}) => {
1680
+ const { branches } = astData;
1681
+ let bool = false;
1682
+ if (!this.#psHasFilterCache) {
1683
+ this.#psHasFilterCache = new WeakMap();
1684
+ }
1685
+ let rootCache = this.#psHasFilterCache.get(this.root);
1686
+ if (rootCache === undefined) {
1687
+ rootCache = new WeakMap();
1688
+ this.#psHasFilterCache.set(this.root, rootCache);
1689
+ }
1690
+ for (const leaves of branches) {
1691
+ if (!rootCache.has(leaves)) {
1692
+ const filterResult = this._buildHasAllowlist(leaves);
1693
+ rootCache.set(leaves, filterResult);
1694
+ }
1695
+ const allowlist = rootCache.get(leaves);
1696
+ if (
1697
+ allowlist &&
1698
+ allowlist.seeded &&
1699
+ node.nodeType !== DOCUMENT_FRAGMENT_NODE &&
1700
+ !allowlist.set.has(node)
1701
+ ) {
1702
+ continue;
1703
+ }
1704
+ bool = this._matchHasPseudoFunc(leaves, node, opt);
1705
+ if (bool) {
1706
+ break;
1707
+ }
1708
+ }
1709
+ if (!bool) {
1710
+ return null;
1711
+ }
1712
+ if (
1713
+ (opt.isShadowRoot || this.shadow) &&
1714
+ node.nodeType === DOCUMENT_FRAGMENT_NODE
1715
+ ) {
1716
+ return this.#verifyShadowHost ? node : null;
1717
+ }
1718
+ return node;
1719
+ };
1720
+
1721
+ /**
1722
+ * Matches logical pseudo-class functions.
1723
+ * @private
1724
+ * @param {object} astData - The AST data.
1725
+ * @param {object} node - The Element node.
1726
+ * @param {object} [opt] - Options.
1727
+ * @returns {boolean} Tru if matches, otherwise false.
1728
+ */
1729
+ _matchLogicalPseudoFunc = (astData, node, opt = {}) => {
1730
+ const { astName, branches, twigBranches } = astData;
1731
+ // Handle :has().
1732
+ if (astName === 'has') {
1733
+ return this._evaluateHasPseudo(astData, node, opt) === node;
1734
+ }
1735
+ // Handle :is(), :not(), :where().
1736
+ const isShadowRoot =
1737
+ (opt.isShadowRoot || this.shadow) &&
1738
+ node.nodeType === DOCUMENT_FRAGMENT_NODE;
1739
+ // Check for invalid shadow root.
1740
+ if (isShadowRoot) {
1741
+ let invalid = false;
1742
+ for (const branch of branches) {
1743
+ if (branch.length > 1) {
1744
+ invalid = true;
1745
+ break;
1746
+ } else if (astName === 'not') {
1747
+ const [{ type: childAstType }] = branch;
1748
+ if (childAstType !== PS_CLASS_SELECTOR) {
1749
+ invalid = true;
1750
+ break;
1751
+ }
1752
+ }
1753
+ }
1754
+ if (invalid) {
1755
+ return false;
1756
+ }
1757
+ }
1758
+ opt.forgive = astName === 'is' || astName === 'where';
1759
+ const l = twigBranches.length;
1760
+ let bool;
1761
+ for (let i = 0; i < l; i++) {
1762
+ const branch = twigBranches[i];
1763
+ const lastIndex = branch.length - 1;
1764
+ const { leaves } = branch[lastIndex];
1765
+ bool = this.matchLeaves(leaves, node, opt);
1766
+ if (bool && lastIndex > 0) {
1767
+ let nextNodes = new Set([node]);
1768
+ for (let j = lastIndex - 1; j >= 0; j--) {
1769
+ const twig = branch[j];
1770
+ const arr = [];
1771
+ opt.dir = DIR_PREV;
1772
+ for (const nextNode of nextNodes) {
1773
+ for (const matchedNode of this.yieldCombinatorMatches(
1774
+ twig,
1775
+ nextNode,
1776
+ opt
1777
+ )) {
1778
+ arr.push(matchedNode);
1779
+ }
1780
+ }
1781
+ if (arr.length) {
1782
+ if (j === 0) {
1783
+ bool = true;
1784
+ } else {
1785
+ nextNodes = new Set(arr);
1786
+ }
1787
+ } else {
1788
+ bool = false;
1789
+ break;
1790
+ }
1791
+ }
1792
+ }
1793
+ if (bool) {
1794
+ break;
1795
+ }
1796
+ }
1797
+ if (astName === 'not') {
1798
+ return !bool;
1799
+ }
1800
+ return bool;
1801
+ };
1802
+
1803
+ /**
1804
+ * Evaluates logical pseudo-class selector.
1805
+ * @private
1806
+ * @param {object} ast - The AST.
1807
+ * @param {object} node - The Element node.
1808
+ * @param {object} [opt] - Options.
1809
+ * @param {boolean} [opt.forgive] - Ignores unknown or invalid selectors.
1810
+ * @param {boolean} [opt.warn] - If true, console warnings are enabled.
1811
+ * @returns {boolean} True if matches, otherwise false.
1812
+ */
1813
+ _evaluateLogicalPseudo = (ast, node, opt = {}) => {
1814
+ const { children: astChildren, name: astName } = ast;
1815
+ if (!astChildren.length && astName !== 'is' && astName !== 'where') {
1816
+ const css = generateCSS(ast);
1817
+ const msg = `Invalid selector ${css}`;
1818
+ this.onError(generateException(msg, SYNTAX_ERR, this.window));
1819
+ return false;
1820
+ }
1821
+ const cachedAstData = this.#astCache.get(ast);
1822
+ if (cachedAstData) {
1823
+ return this._matchLogicalPseudoFunc(cachedAstData, node, opt);
1824
+ }
1825
+ const { branches } = walkAST(ast);
1826
+ if (astName === 'has') {
1827
+ const astData = { astName, branches };
1828
+ this.#astCache.set(ast, astData);
1829
+ return this._matchLogicalPseudoFunc(astData, node, opt);
1830
+ }
1831
+ const twigBranches = [];
1832
+ const l = branches.length;
1833
+ for (let i = 0; i < l; i++) {
1834
+ const leaves = branches[i];
1835
+ const branch = [];
1836
+ const leavesSet = new Set();
1837
+ const leavesLen = leaves.length;
1838
+ for (let j = 0; j < leavesLen; j++) {
1839
+ const item = leaves[j];
1840
+ if (item.type === COMBINATOR) {
1841
+ branch.push({
1842
+ combo: item,
1843
+ leaves: [...leavesSet]
1844
+ });
1845
+ leavesSet.clear();
1846
+ } else {
1847
+ leavesSet.add(item);
1848
+ }
1849
+ if (j === leavesLen - 1) {
1850
+ branch.push({
1851
+ combo: null,
1852
+ leaves: [...leavesSet]
1853
+ });
1854
+ leavesSet.clear();
1855
+ }
1856
+ }
1857
+ twigBranches.push(branch);
1858
+ }
1859
+ const astData = {
1860
+ astName,
1861
+ branches,
1862
+ twigBranches
1863
+ };
1864
+ this.#astCache.set(ast, astData);
1865
+ return this._matchLogicalPseudoFunc(astData, node, opt);
1866
+ };
1867
+
1868
+ /**
1869
+ * Evaluates pseudo-class function.
1870
+ * @private
1871
+ * @see https://html.spec.whatwg.org/_pseudo-classes
1872
+ * @param {object} ast - The AST.
1873
+ * @param {object} node - The Element node.
1874
+ * @param {object} [opt] - Options.
1875
+ * @param {boolean} [opt.forgive] - Ignores unknown or invalid selectors.
1876
+ * @param {boolean} [opt.warn] - If true, console warnings are enabled.
1877
+ * @returns {boolean} True if matches, otherwise false.
1878
+ */
1879
+ _evaluatePseudoClassFunc = (ast, node, opt = {}) => {
1880
+ const { children: astChildren, name: astName } = ast;
1881
+ const { forgive, warn = this.warn } = opt;
1882
+ // :nth-child(), :nth-last-child(), nth-of-type(), :nth-last-of-type()
1883
+ if (/^nth-(?:last-)?(?:child|of-type)$/.test(astName)) {
1884
+ if (astChildren.length !== 1) {
1885
+ const css = generateCSS(ast);
1886
+ this.onError(
1887
+ generateException(`Invalid selector ${css}`, SYNTAX_ERR, this.window)
1888
+ );
1889
+ return false;
1890
+ }
1891
+ const [branch] = astChildren;
1892
+ return this._matchAnPlusB(branch, node, astName, opt);
1893
+ }
1894
+ switch (astName) {
1895
+ // :dir()
1896
+ case 'dir': {
1897
+ if (astChildren.length !== 1) {
1898
+ const css = generateCSS(ast);
1899
+ this.onError(
1900
+ generateException(
1901
+ `Invalid selector ${css}`,
1902
+ SYNTAX_ERR,
1903
+ this.window
1904
+ )
1905
+ );
1906
+ return false;
1907
+ }
1908
+ const [astChild] = astChildren;
1909
+ if (!this.#psDirCache) {
1910
+ this.#psDirCache = new WeakMap();
1911
+ }
1912
+ const res = matchDirectionPseudoClass(astChild, node, this.#psDirCache);
1913
+ if (res) {
1914
+ return true;
1915
+ }
1916
+ break;
1917
+ }
1918
+ // :lang()
1919
+ case 'lang': {
1920
+ if (!astChildren.length) {
1921
+ const css = generateCSS(ast);
1922
+ this.onError(
1923
+ generateException(
1924
+ `Invalid selector ${css}`,
1925
+ SYNTAX_ERR,
1926
+ this.window
1927
+ )
1928
+ );
1929
+ return false;
1930
+ }
1931
+ if (!this.#psLangCache) {
1932
+ this.#psLangCache = new WeakMap();
1933
+ }
1934
+ let bool;
1935
+ for (const astChild of astChildren) {
1936
+ bool = matchLanguagePseudoClass(astChild, node, this.#psLangCache);
1937
+ if (bool) {
1938
+ break;
1939
+ }
1940
+ }
1941
+ if (bool) {
1942
+ return true;
1943
+ }
1944
+ break;
1945
+ }
1946
+ // :state()
1947
+ case 'state': {
1948
+ if (isCustomElement(node)) {
1949
+ const [{ value: stateValue }] = astChildren;
1950
+ if (stateValue) {
1951
+ if (node[stateValue]) {
1952
+ return true;
1953
+ }
1954
+ for (const i in node) {
1955
+ const prop = node[i];
1956
+ if (prop instanceof this.window.ElementInternals) {
1957
+ if (prop?.states?.has(stateValue)) {
1958
+ return true;
1959
+ }
1960
+ break;
1961
+ }
1962
+ }
1963
+ }
1964
+ }
1965
+ break;
1966
+ }
1967
+ case 'current':
1968
+ case 'heading':
1969
+ case 'nth-col':
1970
+ case 'nth-last-col': {
1971
+ if (warn) {
1972
+ this.onError(
1973
+ generateException(
1974
+ `Unsupported pseudo-class :${astName}()`,
1975
+ NOT_SUPPORTED_ERR,
1976
+ this.window
1977
+ )
1978
+ );
1979
+ }
1980
+ break;
1981
+ }
1982
+ // Ignore :host() and :host-context().
1983
+ case 'host':
1984
+ case 'host-context': {
1985
+ break;
1986
+ }
1987
+ // Deprecated in CSS Selectors 3.
1988
+ case 'contains': {
1989
+ if (warn) {
1990
+ this.onError(
1991
+ generateException(
1992
+ `Unknown pseudo-class :${astName}()`,
1993
+ NOT_SUPPORTED_ERR,
1994
+ this.window
1995
+ )
1996
+ );
1997
+ }
1998
+ break;
1999
+ }
2000
+ default: {
2001
+ if (!forgive) {
2002
+ this.onError(
2003
+ generateException(
2004
+ `Unknown pseudo-class :${astName}()`,
2005
+ SYNTAX_ERR,
2006
+ this.window
2007
+ )
2008
+ );
2009
+ }
2010
+ }
2011
+ }
2012
+ return false;
2013
+ };
2014
+
2015
+ /**
2016
+ * Evaluates the :host() pseudo-class.
2017
+ * @private
2018
+ * @param {Array.<object>} leaves - The AST leaves.
2019
+ * @param {object} host - The host element.
2020
+ * @param {object} ast - The original AST for error reporting.
2021
+ * @returns {boolean} True if matches, otherwise false.
2022
+ */
2023
+ _evaluateHostPseudo = (leaves, host, ast) => {
2024
+ const l = leaves.length;
2025
+ for (let i = 0; i < l; i++) {
2026
+ const leaf = leaves[i];
2027
+ if (leaf.type === COMBINATOR) {
2028
+ const css = generateCSS(ast);
2029
+ const msg = `Invalid selector ${css}`;
2030
+ this.onError(generateException(msg, SYNTAX_ERR, this.window));
2031
+ return false;
2032
+ }
2033
+ if (!this.matchSelector(leaf, host)) {
2034
+ return false;
2035
+ }
2036
+ }
2037
+ return true;
2038
+ };
2039
+
2040
+ /**
2041
+ * Evaluates the :host-context() pseudo-class.
2042
+ * @private
2043
+ * @param {Array.<object>} leaves - The AST leaves.
2044
+ * @param {object} host - The host element.
2045
+ * @param {object} ast - The original AST for error reporting.
2046
+ * @returns {boolean} True if matched.
2047
+ */
2048
+ _evaluateHostContextPseudo = (leaves, host, ast) => {
2049
+ let parent = host;
2050
+ while (parent) {
2051
+ let bool;
2052
+ const l = leaves.length;
2053
+ for (let i = 0; i < l; i++) {
2054
+ const leaf = leaves[i];
2055
+ if (leaf.type === COMBINATOR) {
2056
+ const css = generateCSS(ast);
2057
+ const msg = `Invalid selector ${css}`;
2058
+ this.onError(generateException(msg, SYNTAX_ERR, this.window));
2059
+ return false;
2060
+ }
2061
+ bool = this.matchSelector(leaf, parent);
2062
+ if (!bool) {
2063
+ break;
2064
+ }
2065
+ }
2066
+ if (bool) {
2067
+ return true;
2068
+ }
2069
+ parent = parent.parentNode;
2070
+ }
2071
+ return false;
2072
+ };
2073
+
2074
+ /**
2075
+ * Matches a selector for element nodes.
2076
+ * @private
2077
+ * @param {object} ast - The AST.
2078
+ * @param {object} node - The Element node.
2079
+ * @param {object} opt - Options.
2080
+ * @returns {boolean} True if matches, otherwise false.
2081
+ */
2082
+ _matchSelectorForElement = (ast, node, opt) => {
2083
+ const { type: astType } = ast;
2084
+ const astName = unescapeSelector(ast.name);
2085
+ switch (astType) {
2086
+ case ATTR_SELECTOR: {
2087
+ return matchAttributeSelector(ast, node, opt);
2088
+ }
2089
+ case ID_SELECTOR: {
2090
+ return node.id === astName;
2091
+ }
2092
+ case CLASS_SELECTOR: {
2093
+ return node.classList.contains(astName);
2094
+ }
2095
+ case PS_CLASS_SELECTOR: {
2096
+ return this.matchPseudoClassSelector(ast, node, opt);
2097
+ }
2098
+ case TYPE_SELECTOR: {
2099
+ return matchTypeSelector(ast, node, opt);
2100
+ }
2101
+ // PS_ELEMENT_SELECTOR is handled by default.
2102
+ default: {
2103
+ try {
2104
+ if (this.check) {
2105
+ const css = generateCSS(ast);
2106
+ this.pseudoElements.push(css);
2107
+ return true;
2108
+ } else {
2109
+ matchPseudoElementSelector(astName, astType, opt);
2110
+ }
2111
+ } catch (e) {
2112
+ this.onError(e);
2113
+ }
2114
+ }
2115
+ }
2116
+ return false;
2117
+ };
2118
+
2119
+ /**
2120
+ * Matches a selector for a shadow root.
2121
+ * @private
2122
+ * @param {object} ast - The AST.
2123
+ * @param {object} node - The DocumentFragment node.
2124
+ * @param {object} [opt] - Options.
2125
+ * @returns {boolean} True if matches, otherwise false.
2126
+ */
2127
+ _matchSelectorForShadowRoot = (ast, node, opt = {}) => {
2128
+ const { name: astName } = ast;
2129
+ if (KEYS_LOGICAL.has(astName)) {
2130
+ opt.isShadowRoot = true;
2131
+ return this.matchPseudoClassSelector(ast, node, opt);
2132
+ }
2133
+ if (astName === 'host' || astName === 'host-context') {
2134
+ const matches = this.evaluateShadowHost(ast, node, opt);
2135
+ if (matches) {
2136
+ this.#verifyShadowHost = true;
2137
+ return true;
2138
+ }
2139
+ }
2140
+ return false;
2141
+ };
2142
+ }