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