@asamuzakjp/dom-selector 8.0.1 → 8.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -155
- package/package.json +18 -11
- package/src/index.js +132 -66
- package/src/js/constant.js +106 -4
- package/src/js/finder.js +1149 -1049
- package/src/js/matcher.js +59 -24
- package/src/js/nwsapi.js +29 -11
- package/src/js/parser.js +16 -7
- package/src/js/selector.js +325 -0
- package/src/js/utility.js +155 -349
- package/types/index.d.ts +2 -1
- package/types/js/constant.d.ts +6 -0
- package/types/js/finder.d.ts +4 -1
- package/types/js/matcher.d.ts +2 -2
- package/types/js/parser.d.ts +1 -1
- package/types/js/selector.d.ts +12 -0
- package/types/js/utility.d.ts +5 -12
package/src/js/utility.js
CHANGED
|
@@ -4,38 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
/* import */
|
|
6
6
|
import bidiFactory from 'bidi-js';
|
|
7
|
-
import * as cssTree from 'css-tree';
|
|
8
7
|
import isCustomElementName from 'is-potential-custom-element-name';
|
|
9
8
|
|
|
10
9
|
/* constants */
|
|
11
10
|
import {
|
|
12
|
-
|
|
13
|
-
COMBO,
|
|
14
|
-
COMPOUND_I,
|
|
15
|
-
DESCEND,
|
|
11
|
+
CLASS_SELECTOR,
|
|
16
12
|
DOCUMENT_FRAGMENT_NODE,
|
|
17
13
|
DOCUMENT_NODE,
|
|
18
14
|
DOCUMENT_POSITION_CONTAINS,
|
|
19
15
|
DOCUMENT_POSITION_PRECEDING,
|
|
20
16
|
ELEMENT_NODE,
|
|
21
|
-
|
|
17
|
+
ID_SELECTOR,
|
|
22
18
|
INPUT_BUTTON,
|
|
23
19
|
INPUT_EDIT,
|
|
24
20
|
INPUT_LTR,
|
|
25
21
|
INPUT_TEXT,
|
|
26
|
-
|
|
27
|
-
LOGIC_COMPLEX,
|
|
28
|
-
LOGIC_COMPOUND,
|
|
29
|
-
N_TH,
|
|
30
|
-
PSEUDO_CLASS,
|
|
31
|
-
RULE,
|
|
32
|
-
SCOPE,
|
|
33
|
-
SELECTOR_LIST,
|
|
34
|
-
TAG_TYPE,
|
|
35
|
-
TARGET_ALL,
|
|
36
|
-
TARGET_FIRST,
|
|
22
|
+
SHOW_ELEMENT,
|
|
37
23
|
TEXT_NODE,
|
|
38
24
|
TYPE_FROM,
|
|
25
|
+
TYPE_SELECTOR,
|
|
39
26
|
TYPE_TO
|
|
40
27
|
} from './constant.js';
|
|
41
28
|
const KEYS_DIR_AUTO = new Set([...INPUT_BUTTON, ...INPUT_TEXT, 'hidden']);
|
|
@@ -58,105 +45,9 @@ const KEYS_NODE_FOCUSABLE_SVG = new Set([
|
|
|
58
45
|
'symbol',
|
|
59
46
|
'title'
|
|
60
47
|
]);
|
|
61
|
-
const REG_ATTR_SIMPLE = /^\[[A-Z\d-]{1,255}(?:="?[A-Z\d\s-]{1,255}"?)?\]$/i;
|
|
62
|
-
const REG_TAG_SIMPLE = new RegExp(`^(?:${TAG_TYPE})$`);
|
|
63
|
-
const REG_EXCLUDE_BASIC =
|
|
64
|
-
/[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
|
|
65
|
-
const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
|
|
66
|
-
const REG_DESCEND = new RegExp(`${COMPOUND_I}${DESCEND}${COMPOUND_I}`, 'i');
|
|
67
|
-
const REG_LOGIC_COMPLEX = new RegExp(
|
|
68
|
-
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
|
|
69
|
-
);
|
|
70
|
-
const REG_LOGIC_COMPOUND = new RegExp(
|
|
71
|
-
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
|
|
72
|
-
);
|
|
73
|
-
const REG_LOGIC_HAS_COMPOUND = new RegExp(
|
|
74
|
-
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
|
|
75
|
-
);
|
|
76
|
-
const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
|
|
77
|
-
const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
|
|
78
48
|
const REG_IS_HTML = /^(?:application\/xhtml\+x|text\/ht)ml$/;
|
|
79
49
|
const REG_IS_XML =
|
|
80
50
|
/^(?:application\/(?:[\w\-.]+\+)?|image\/[\w\-.]+\+|text\/)xml$/;
|
|
81
|
-
const REG_COMBO = new RegExp(COMBO);
|
|
82
|
-
const REG_ID = /#(\D[^#.*]+)/g;
|
|
83
|
-
const REG_CLASS = /\.(\D[^#.*]+)/g;
|
|
84
|
-
const REG_TAG = /^([^#.]+)/;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Manages state for extracting nested selectors from a CSS AST.
|
|
88
|
-
*/
|
|
89
|
-
class SelectorExtractor {
|
|
90
|
-
constructor() {
|
|
91
|
-
this.selectors = [];
|
|
92
|
-
this.isScoped = false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Walker enter function.
|
|
97
|
-
* @param {object} node - The AST node.
|
|
98
|
-
*/
|
|
99
|
-
enter(node) {
|
|
100
|
-
switch (node.type) {
|
|
101
|
-
case ATRULE: {
|
|
102
|
-
if (node.name === 'scope') {
|
|
103
|
-
this.isScoped = true;
|
|
104
|
-
}
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
case SCOPE: {
|
|
108
|
-
const { children, type } = node.root;
|
|
109
|
-
const arr = [];
|
|
110
|
-
if (type === SELECTOR_LIST) {
|
|
111
|
-
for (const child of children) {
|
|
112
|
-
const selector = cssTree.generate(child);
|
|
113
|
-
arr.push(selector);
|
|
114
|
-
}
|
|
115
|
-
this.selectors.push(arr);
|
|
116
|
-
}
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
case RULE: {
|
|
120
|
-
const { children, type } = node.prelude;
|
|
121
|
-
const arr = [];
|
|
122
|
-
if (type === SELECTOR_LIST) {
|
|
123
|
-
let hasAmp = false;
|
|
124
|
-
for (const child of children) {
|
|
125
|
-
const selector = cssTree.generate(child);
|
|
126
|
-
if (this.isScoped && !hasAmp) {
|
|
127
|
-
hasAmp = /\x26/.test(selector);
|
|
128
|
-
}
|
|
129
|
-
arr.push(selector);
|
|
130
|
-
}
|
|
131
|
-
if (this.isScoped) {
|
|
132
|
-
if (hasAmp) {
|
|
133
|
-
this.selectors.push(arr);
|
|
134
|
-
/* FIXME:
|
|
135
|
-
} else {
|
|
136
|
-
this.selectors = arr;
|
|
137
|
-
this.isScoped = false;
|
|
138
|
-
*/
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
this.selectors.push(arr);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Walker leave function.
|
|
150
|
-
* @param {object} node - The AST node.
|
|
151
|
-
*/
|
|
152
|
-
leave(node) {
|
|
153
|
-
if (node.type === ATRULE) {
|
|
154
|
-
if (node.name === 'scope') {
|
|
155
|
-
this.isScoped = false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
51
|
|
|
161
52
|
/**
|
|
162
53
|
* Get type of an object.
|
|
@@ -199,27 +90,6 @@ export const generateException = (msg, name, globalObject = globalThis) => {
|
|
|
199
90
|
return new globalObject.DOMException(msg, name);
|
|
200
91
|
};
|
|
201
92
|
|
|
202
|
-
/**
|
|
203
|
-
* Find a nested :has() pseudo-class.
|
|
204
|
-
* @param {object} leaf - The AST leaf to check.
|
|
205
|
-
* @returns {?object} The leaf if it's :has, otherwise null.
|
|
206
|
-
*/
|
|
207
|
-
export const findNestedHas = leaf => {
|
|
208
|
-
return leaf.name === 'has';
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Find a logical pseudo-class that contains a nested :has().
|
|
213
|
-
* @param {object} leaf - The AST leaf to check.
|
|
214
|
-
* @returns {?object} The leaf if it matches, otherwise null.
|
|
215
|
-
*/
|
|
216
|
-
export const findLogicalWithNestedHas = leaf => {
|
|
217
|
-
if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
|
|
218
|
-
return leaf;
|
|
219
|
-
}
|
|
220
|
-
return null;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
93
|
/**
|
|
224
94
|
* Filter a list of nodes based on An+B logic
|
|
225
95
|
* @param {Array.<object>} nodes - array of nodes to filter
|
|
@@ -431,19 +301,24 @@ export const getSlottedTextContent = node => {
|
|
|
431
301
|
* Get directionality of a node.
|
|
432
302
|
* @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
|
|
433
303
|
* @param {object} node - The Element node.
|
|
304
|
+
* @param {WeakMap} [dirCache] - Cache for directionality.
|
|
434
305
|
* @returns {?string} - 'ltr' or 'rtl'.
|
|
435
306
|
*/
|
|
436
|
-
export const getDirectionality = node => {
|
|
307
|
+
export const getDirectionality = (node, dirCache = new WeakMap()) => {
|
|
437
308
|
if (!node?.nodeType) {
|
|
438
309
|
throw new TypeError(`Unexpected type ${getType(node)}`);
|
|
439
310
|
}
|
|
440
311
|
if (node.nodeType !== ELEMENT_NODE) {
|
|
441
312
|
return null;
|
|
442
313
|
}
|
|
314
|
+
if (dirCache.has(node)) {
|
|
315
|
+
return dirCache.get(node);
|
|
316
|
+
}
|
|
443
317
|
const { dir: dirAttr, localName, parentNode } = node;
|
|
444
318
|
const { getEmbeddingLevels } = bidiFactory();
|
|
319
|
+
let result = 'ltr';
|
|
445
320
|
if (dirAttr === 'ltr' || dirAttr === 'rtl') {
|
|
446
|
-
|
|
321
|
+
result = dirAttr;
|
|
447
322
|
} else if (dirAttr === 'auto') {
|
|
448
323
|
let text = '';
|
|
449
324
|
switch (localName) {
|
|
@@ -451,7 +326,8 @@ export const getDirectionality = node => {
|
|
|
451
326
|
if (!node.type || KEYS_DIR_AUTO.has(node.type)) {
|
|
452
327
|
text = node.value;
|
|
453
328
|
} else if (KEYS_DIR_LTR.has(node.type)) {
|
|
454
|
-
|
|
329
|
+
result = 'ltr';
|
|
330
|
+
text = null; // Flag to skip text evaluation
|
|
455
331
|
}
|
|
456
332
|
break;
|
|
457
333
|
}
|
|
@@ -491,21 +367,23 @@ export const getDirectionality = node => {
|
|
|
491
367
|
}
|
|
492
368
|
}
|
|
493
369
|
}
|
|
494
|
-
if (text) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
370
|
+
if (text !== null) {
|
|
371
|
+
if (text) {
|
|
372
|
+
const {
|
|
373
|
+
paragraphs: [{ level }]
|
|
374
|
+
} = getEmbeddingLevels(text);
|
|
375
|
+
if (level % 2 === 1) {
|
|
376
|
+
result = 'rtl';
|
|
377
|
+
}
|
|
378
|
+
} else if (parentNode) {
|
|
379
|
+
const { nodeType: parentNodeType } = parentNode;
|
|
380
|
+
if (parentNodeType === ELEMENT_NODE) {
|
|
381
|
+
result = getDirectionality(parentNode, dirCache);
|
|
382
|
+
}
|
|
505
383
|
}
|
|
506
384
|
}
|
|
507
385
|
} else if (localName === 'input' && node.type === 'tel') {
|
|
508
|
-
|
|
386
|
+
result = 'ltr';
|
|
509
387
|
} else if (localName === 'bdi') {
|
|
510
388
|
const text = node.textContent.trim();
|
|
511
389
|
if (text) {
|
|
@@ -513,7 +391,7 @@ export const getDirectionality = node => {
|
|
|
513
391
|
paragraphs: [{ level }]
|
|
514
392
|
} = getEmbeddingLevels(text);
|
|
515
393
|
if (level % 2 === 1) {
|
|
516
|
-
|
|
394
|
+
result = 'rtl';
|
|
517
395
|
}
|
|
518
396
|
}
|
|
519
397
|
} else if (parentNode) {
|
|
@@ -524,47 +402,67 @@ export const getDirectionality = node => {
|
|
|
524
402
|
paragraphs: [{ level }]
|
|
525
403
|
} = getEmbeddingLevels(text);
|
|
526
404
|
if (level % 2 === 1) {
|
|
527
|
-
|
|
405
|
+
result = 'rtl';
|
|
406
|
+
} else {
|
|
407
|
+
result = 'ltr';
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
const { nodeType: parentNodeType } = parentNode;
|
|
411
|
+
if (parentNodeType === ELEMENT_NODE) {
|
|
412
|
+
result = getDirectionality(parentNode, dirCache);
|
|
528
413
|
}
|
|
529
|
-
return 'ltr';
|
|
530
414
|
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
415
|
+
} else {
|
|
416
|
+
const { nodeType: parentNodeType } = parentNode;
|
|
417
|
+
if (parentNodeType === ELEMENT_NODE) {
|
|
418
|
+
result = getDirectionality(parentNode, dirCache);
|
|
419
|
+
}
|
|
535
420
|
}
|
|
536
421
|
}
|
|
537
|
-
|
|
422
|
+
dirCache.set(node, result);
|
|
423
|
+
return result;
|
|
538
424
|
};
|
|
539
425
|
|
|
540
426
|
/**
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
* @param {
|
|
544
|
-
* @returns {string
|
|
427
|
+
* Get language attribute of a node.
|
|
428
|
+
* @param {object} node - The Element node.
|
|
429
|
+
* @param {WeakMap} [langCache] - Cache for language attributes.
|
|
430
|
+
* @returns {?string} - Language attribute value.
|
|
545
431
|
*/
|
|
546
|
-
export const getLanguageAttribute = node => {
|
|
432
|
+
export const getLanguageAttribute = (node, langCache = new WeakMap()) => {
|
|
547
433
|
if (!node?.nodeType) {
|
|
548
434
|
throw new TypeError(`Unexpected type ${getType(node)}`);
|
|
549
435
|
}
|
|
550
436
|
if (node.nodeType !== ELEMENT_NODE) {
|
|
551
437
|
return null;
|
|
552
438
|
}
|
|
439
|
+
if (langCache.has(node)) {
|
|
440
|
+
return langCache.get(node);
|
|
441
|
+
}
|
|
553
442
|
const { contentType } = node.ownerDocument;
|
|
554
443
|
const isHtml = REG_IS_HTML.test(contentType);
|
|
555
444
|
const isXml = REG_IS_XML.test(contentType);
|
|
556
445
|
let isShadow = false;
|
|
446
|
+
let result;
|
|
447
|
+
const visited = [];
|
|
557
448
|
// Traverse up from the current node to the root.
|
|
558
449
|
let current = node;
|
|
559
450
|
while (current) {
|
|
451
|
+
if (current.nodeType === ELEMENT_NODE && langCache.has(current)) {
|
|
452
|
+
result = langCache.get(current);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
if (current.nodeType === ELEMENT_NODE) {
|
|
456
|
+
visited.push(current);
|
|
457
|
+
}
|
|
560
458
|
// Check if the current node is an element.
|
|
561
459
|
switch (current.nodeType) {
|
|
562
460
|
case ELEMENT_NODE: {
|
|
563
461
|
// Check for and return the language attribute if present.
|
|
564
462
|
if (isHtml && current.hasAttribute('lang')) {
|
|
565
|
-
|
|
463
|
+
result = current.getAttribute('lang');
|
|
566
464
|
} else if (isXml && current.hasAttribute('xml:lang')) {
|
|
567
|
-
|
|
465
|
+
result = current.getAttribute('xml:lang');
|
|
568
466
|
}
|
|
569
467
|
break;
|
|
570
468
|
}
|
|
@@ -578,9 +476,12 @@ export const getLanguageAttribute = node => {
|
|
|
578
476
|
case DOCUMENT_NODE:
|
|
579
477
|
default: {
|
|
580
478
|
// Stop if we reach the root document node.
|
|
581
|
-
|
|
479
|
+
result = null;
|
|
582
480
|
}
|
|
583
481
|
}
|
|
482
|
+
if (result !== undefined) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
584
485
|
if (isShadow) {
|
|
585
486
|
current = current.host;
|
|
586
487
|
isShadow = false;
|
|
@@ -590,8 +491,13 @@ export const getLanguageAttribute = node => {
|
|
|
590
491
|
break;
|
|
591
492
|
}
|
|
592
493
|
}
|
|
593
|
-
|
|
594
|
-
|
|
494
|
+
if (result === undefined) {
|
|
495
|
+
result = null;
|
|
496
|
+
}
|
|
497
|
+
for (const visitedNode of visited) {
|
|
498
|
+
langCache.set(visitedNode, result);
|
|
499
|
+
}
|
|
500
|
+
return result;
|
|
595
501
|
};
|
|
596
502
|
|
|
597
503
|
/**
|
|
@@ -930,201 +836,101 @@ export const sortNodes = (nodes = []) => {
|
|
|
930
836
|
};
|
|
931
837
|
|
|
932
838
|
/**
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
* @
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if (!Array.isArray(selectors)) {
|
|
939
|
-
throw new TypeError(`Unexpected type ${getType(selectors)}`);
|
|
940
|
-
}
|
|
941
|
-
let selector = '';
|
|
942
|
-
if (selectors.length) {
|
|
943
|
-
const revSelectors = selectors.toReversed();
|
|
944
|
-
let child = verifyArray(revSelectors.shift(), 'String');
|
|
945
|
-
if (child.length === 1) {
|
|
946
|
-
[child] = child;
|
|
947
|
-
}
|
|
948
|
-
while (revSelectors.length) {
|
|
949
|
-
const parentArr = verifyArray(revSelectors.shift(), 'String');
|
|
950
|
-
if (!parentArr.length) {
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
|
-
let parent;
|
|
954
|
-
if (parentArr.length === 1) {
|
|
955
|
-
[parent] = parentArr;
|
|
956
|
-
if (!/^[>~+]/.test(parent) && /[\s>~+]/.test(parent)) {
|
|
957
|
-
parent = `:is(${parent})`;
|
|
958
|
-
}
|
|
959
|
-
} else {
|
|
960
|
-
parent = `:is(${parentArr.join(', ')})`;
|
|
961
|
-
}
|
|
962
|
-
if (selector.includes('\x26')) {
|
|
963
|
-
selector = selector.replace(/\x26/g, parent);
|
|
964
|
-
}
|
|
965
|
-
if (Array.isArray(child)) {
|
|
966
|
-
const items = [];
|
|
967
|
-
for (let item of child) {
|
|
968
|
-
if (item.includes('\x26')) {
|
|
969
|
-
if (/^[>~+]/.test(item)) {
|
|
970
|
-
item = `${parent} ${item.replace(/\x26/g, parent)} ${selector}`;
|
|
971
|
-
} else {
|
|
972
|
-
item = `${item.replace(/\x26/g, parent)} ${selector}`;
|
|
973
|
-
}
|
|
974
|
-
} else {
|
|
975
|
-
item = `${parent} ${item} ${selector}`;
|
|
976
|
-
}
|
|
977
|
-
items.push(item.trim());
|
|
978
|
-
}
|
|
979
|
-
selector = items.join(', ');
|
|
980
|
-
} else if (revSelectors.length) {
|
|
981
|
-
selector = `${child} ${selector}`;
|
|
982
|
-
} else {
|
|
983
|
-
if (child.includes('\x26')) {
|
|
984
|
-
if (/^[>~+]/.test(child)) {
|
|
985
|
-
selector = `${parent} ${child.replace(/\x26/g, parent)} ${selector}`;
|
|
986
|
-
} else {
|
|
987
|
-
selector = `${child.replace(/\x26/g, parent)} ${selector}`;
|
|
988
|
-
}
|
|
989
|
-
} else {
|
|
990
|
-
selector = `${parent} ${child} ${selector}`;
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
selector = selector.trim();
|
|
994
|
-
if (revSelectors.length) {
|
|
995
|
-
child = parentArr.length > 1 ? parentArr : parent;
|
|
996
|
-
} else {
|
|
997
|
-
break;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
selector = selector.replace(/\x26/g, ':scope').trim();
|
|
1001
|
-
}
|
|
1002
|
-
return selector;
|
|
1003
|
-
};
|
|
1004
|
-
|
|
1005
|
-
/**
|
|
1006
|
-
* Extract nested selectors from CSSRule.cssText.
|
|
1007
|
-
* @param {string} css - CSSRule.cssText.
|
|
1008
|
-
* @returns {Array.<Array.<string>>} - Array of nested selectors.
|
|
839
|
+
* Traverses AST nodes to find the most optimal seed selector
|
|
840
|
+
* (ID > Class > Tag).
|
|
841
|
+
* @param {Array} nodes - AST nodes to traverse.
|
|
842
|
+
* @param {object} [state] - The current state of the search.
|
|
843
|
+
* @returns {object} The search state containing the best seed.
|
|
1009
844
|
*/
|
|
1010
|
-
export const
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
845
|
+
export const findBestSeed = (nodes, state = { seed: null, priority: 0 }) => {
|
|
846
|
+
for (const node of nodes) {
|
|
847
|
+
if (state.priority === 3) {
|
|
848
|
+
return state;
|
|
849
|
+
}
|
|
850
|
+
if (Array.isArray(node)) {
|
|
851
|
+
findBestSeed(node, state);
|
|
852
|
+
} else if (node && typeof node === 'object') {
|
|
853
|
+
// ID Selector (Fastest: getElementById)
|
|
854
|
+
if (node.type === ID_SELECTOR) {
|
|
855
|
+
state.seed = { type: 'id', value: node.name };
|
|
856
|
+
state.priority = 3;
|
|
857
|
+
return state;
|
|
858
|
+
} else if (node.type === CLASS_SELECTOR && state.priority < 2) {
|
|
859
|
+
// Class Selector (Faster: getElementsByClassName)
|
|
860
|
+
state.seed = { type: 'class', value: node.name };
|
|
861
|
+
state.priority = 2;
|
|
862
|
+
} else if (
|
|
863
|
+
node.type === TYPE_SELECTOR &&
|
|
864
|
+
state.priority < 1 &&
|
|
865
|
+
node.name !== '*'
|
|
866
|
+
) {
|
|
867
|
+
// Type/Tag Selector (Excludes universal '*')
|
|
868
|
+
state.seed = { type: 'tag', value: node.name };
|
|
869
|
+
state.priority = 1;
|
|
870
|
+
}
|
|
871
|
+
if (node.children) {
|
|
872
|
+
findBestSeed(node.children, state);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return state;
|
|
1020
877
|
};
|
|
1021
878
|
|
|
1022
879
|
/**
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
* @param {
|
|
1026
|
-
* @
|
|
880
|
+
* Traces the DOM tree upwards and sideways from a seed element,
|
|
881
|
+
* populating the allowlist with safe paths for :has() evaluation.
|
|
882
|
+
* @param {object} current - The starting seed element.
|
|
883
|
+
* @param {WeakSet} list - The WeakSet to populate.
|
|
884
|
+
* @param {Set} visitedAncestors - The Set to track visited nodes.
|
|
885
|
+
* @returns {void}
|
|
1027
886
|
*/
|
|
1028
|
-
export const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
887
|
+
export const populateHasAllowlist = (current, list, visitedAncestors) => {
|
|
888
|
+
list.add(current);
|
|
889
|
+
while (
|
|
890
|
+
current &&
|
|
891
|
+
(current.nodeType === ELEMENT_NODE ||
|
|
892
|
+
current.nodeType === DOCUMENT_FRAGMENT_NODE)
|
|
893
|
+
) {
|
|
894
|
+
if (visitedAncestors.has(current)) {
|
|
895
|
+
break;
|
|
1035
896
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const tagMatch = rightmost.match(REG_TAG);
|
|
1051
|
-
if (tagMatch) {
|
|
1052
|
-
const tag = tagMatch[1];
|
|
1053
|
-
if (tag !== '*') {
|
|
1054
|
-
tagKey = caseSensitive ? tag : tag.toLowerCase();
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
897
|
+
visitedAncestors.add(current);
|
|
898
|
+
let sibling = current.previousElementSibling;
|
|
899
|
+
while (sibling) {
|
|
900
|
+
list.add(sibling);
|
|
901
|
+
sibling = sibling.previousElementSibling;
|
|
902
|
+
}
|
|
903
|
+
sibling = current.nextElementSibling;
|
|
904
|
+
while (sibling) {
|
|
905
|
+
list.add(sibling);
|
|
906
|
+
sibling = sibling.nextElementSibling;
|
|
907
|
+
}
|
|
908
|
+
current = current.parentNode;
|
|
909
|
+
if (current) {
|
|
910
|
+
list.add(current);
|
|
1057
911
|
}
|
|
1058
|
-
subjects.push({ id: idKey, className: classKey, tag: tagKey });
|
|
1059
912
|
}
|
|
1060
|
-
return subjects;
|
|
1061
913
|
};
|
|
1062
914
|
|
|
1063
915
|
/**
|
|
1064
|
-
*
|
|
1065
|
-
* @param {
|
|
1066
|
-
* @param {
|
|
1067
|
-
* @returns {
|
|
916
|
+
* Collects all descendant elements of a given node using a TreeWalker.
|
|
917
|
+
* @param {Document|DocumentFragment|Element} node - The node to start from.
|
|
918
|
+
* @param {Document} document - The Document used to create the TreeWalker.
|
|
919
|
+
* @returns {Array<Element>} An array containing all descendant elements.
|
|
1068
920
|
*/
|
|
1069
|
-
export const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (
|
|
1073
|
-
!selector ||
|
|
1074
|
-
typeof selector !== 'string' ||
|
|
1075
|
-
/null|undefined/.test(selector)
|
|
1076
|
-
) {
|
|
1077
|
-
return false;
|
|
1078
|
-
}
|
|
1079
|
-
// Exclude various complex or unsupported selectors early.
|
|
1080
|
-
// i.e. non-ASCII, escaped selectors, namespaced selectors, pseudo-elements.
|
|
1081
|
-
if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
|
|
1082
|
-
return false;
|
|
1083
|
-
}
|
|
1084
|
-
// Validate attribute selector integrity.
|
|
1085
|
-
if (selector.includes('[')) {
|
|
1086
|
-
const index = selector.lastIndexOf('[');
|
|
1087
|
-
if (selector.indexOf(']', index) === -1) {
|
|
1088
|
-
return false;
|
|
1089
|
-
}
|
|
921
|
+
export const collectAllDescendants = (node, document) => {
|
|
922
|
+
if (!node?.nodeType) {
|
|
923
|
+
throw new TypeError(`Unexpected type ${getType(node)}`);
|
|
1090
924
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
return REG_ATTR_SIMPLE.test(selector);
|
|
925
|
+
if (document?.nodeType !== DOCUMENT_NODE) {
|
|
926
|
+
throw new TypeError(`Unexpected type ${getType(document)}`);
|
|
1094
927
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
if (isQuerySelectorAll && REG_DESCEND.test(selector)) {
|
|
1102
|
-
return false;
|
|
1103
|
-
}
|
|
1104
|
-
// Determine if the selector has complex logical structures.
|
|
1105
|
-
const isComplex = isQuerySelectorAll ? false : REG_COMPLEX.test(selector);
|
|
1106
|
-
// Handle :has() specifically.
|
|
1107
|
-
if (selector.includes(':has(')) {
|
|
1108
|
-
if (isQuerySelectorAll) {
|
|
1109
|
-
return false;
|
|
1110
|
-
}
|
|
1111
|
-
if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
|
|
1112
|
-
return false;
|
|
1113
|
-
}
|
|
1114
|
-
return REG_END_WITH_HAS.test(selector);
|
|
1115
|
-
}
|
|
1116
|
-
// Handle :is() and :not().
|
|
1117
|
-
if (/(?:is|not)\(/.test(selector)) {
|
|
1118
|
-
if (isComplex) {
|
|
1119
|
-
return !REG_LOGIC_COMPLEX.test(selector);
|
|
1120
|
-
} else {
|
|
1121
|
-
return !REG_LOGIC_COMPOUND.test(selector);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
// Default check for other pseudo-classes against known list.
|
|
1125
|
-
if (REG_WO_LOGICAL.test(selector)) {
|
|
1126
|
-
return false;
|
|
1127
|
-
}
|
|
928
|
+
const walker = document.createTreeWalker(node, SHOW_ELEMENT);
|
|
929
|
+
const descendants = [];
|
|
930
|
+
let refNode = walker.nextNode();
|
|
931
|
+
while (refNode) {
|
|
932
|
+
descendants.push(refNode);
|
|
933
|
+
refNode = walker.nextNode();
|
|
1128
934
|
}
|
|
1129
|
-
return
|
|
935
|
+
return descendants;
|
|
1130
936
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export class DOMSelector {
|
|
2
2
|
constructor(window: Window, document: Document, opt?: object);
|
|
3
|
-
clear: () => void;
|
|
3
|
+
clear: (clearAll?: boolean) => void;
|
|
4
4
|
extractSubjects: (selector: string, caseSensitive?: boolean) => Array<{
|
|
5
5
|
id: string | null;
|
|
6
6
|
className: string | null;
|
|
7
7
|
tag: string | null;
|
|
8
8
|
}>;
|
|
9
|
+
supports: (selector: string) => boolean;
|
|
9
10
|
check: (selector: string, node: Element, opt?: object) => CheckResult;
|
|
10
11
|
matches: (selector: string, node: Element, opt?: object) => boolean;
|
|
11
12
|
closest: (selector: string, node: Element, opt?: object) => Element | null;
|