@asamuzakjp/dom-selector 8.0.2 → 8.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/package.json +15 -9
- package/src/index.js +68 -38
- package/src/js/constant.js +7 -4
- package/src/js/finder.js +1134 -1017
- package/src/js/matcher.js +59 -24
- package/src/js/nwsapi.js +29 -11
- package/src/js/selector.js +11 -22
- package/src/js/utility.js +173 -33
- package/types/index.d.ts +1 -1
- package/types/js/constant.d.ts +2 -0
- package/types/js/finder.d.ts +4 -1
- package/types/js/matcher.d.ts +2 -2
- package/types/js/utility.d.ts +5 -2
package/src/js/matcher.js
CHANGED
|
@@ -116,37 +116,42 @@ export const matchPseudoElementSelector = (
|
|
|
116
116
|
* Matches the :dir() pseudo-class against an element's directionality.
|
|
117
117
|
* @param {object} ast - The AST object for the pseudo-class.
|
|
118
118
|
* @param {object} node - The element node to match against.
|
|
119
|
+
* @param {WeakMap} [dirCache] - Cache for directionality.
|
|
119
120
|
* @throws {TypeError} If the AST does not contain a valid direction value.
|
|
120
121
|
* @returns {boolean} - True if the directionality matches, otherwise false.
|
|
121
122
|
*/
|
|
122
|
-
export const matchDirectionPseudoClass = (
|
|
123
|
+
export const matchDirectionPseudoClass = (
|
|
124
|
+
ast,
|
|
125
|
+
node,
|
|
126
|
+
dirCache = new WeakMap()
|
|
127
|
+
) => {
|
|
123
128
|
const { name } = ast;
|
|
124
|
-
// The :dir() pseudo-class requires a direction argument (e.g., "ltr").
|
|
125
129
|
if (!name) {
|
|
126
130
|
const type = name === '' ? '(empty String)' : getType(name);
|
|
127
131
|
throw new TypeError(`Unexpected ast type ${type}`);
|
|
128
132
|
}
|
|
129
|
-
|
|
130
|
-
const dir = getDirectionality(node);
|
|
131
|
-
// Compare the expected direction with the element's actual direction.
|
|
133
|
+
const dir = getDirectionality(node, dirCache);
|
|
132
134
|
return name === dir;
|
|
133
135
|
};
|
|
134
136
|
|
|
135
137
|
/**
|
|
136
|
-
* Matches the :lang() pseudo-class against an element's language.
|
|
137
|
-
* @
|
|
138
|
-
* @param {object} ast - The AST object for the pseudo-class.
|
|
138
|
+
* Matches the :lang() pseudo-class against an element's language attribute.
|
|
139
|
+
* @param {object} ast - The AST object for the pseudo-class child.
|
|
139
140
|
* @param {object} node - The element node to match against.
|
|
140
|
-
* @
|
|
141
|
+
* @param {WeakMap} [langCache] - Cache for language attributes.
|
|
142
|
+
* @throws {TypeError} If the AST does not contain a valid language value.
|
|
143
|
+
* @returns {boolean} - True if the language attribute matches, otherwise false.
|
|
141
144
|
*/
|
|
142
|
-
export const matchLanguagePseudoClass = (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
export const matchLanguagePseudoClass = (
|
|
146
|
+
ast,
|
|
147
|
+
node,
|
|
148
|
+
langCache = new WeakMap()
|
|
149
|
+
) => {
|
|
150
|
+
const elementLang = getLanguageAttribute(node, langCache);
|
|
146
151
|
if (elementLang === null) {
|
|
147
152
|
return false;
|
|
148
153
|
}
|
|
149
|
-
// Use cached regex
|
|
154
|
+
// Use cached regex if available
|
|
150
155
|
if (ast._langRegex !== undefined) {
|
|
151
156
|
if (ast._langPattern === '*') {
|
|
152
157
|
return elementLang !== '';
|
|
@@ -158,30 +163,24 @@ export const matchLanguagePseudoClass = (ast, node) => {
|
|
|
158
163
|
}
|
|
159
164
|
const { name, type, value } = ast;
|
|
160
165
|
let langPattern;
|
|
161
|
-
// Determine the language pattern from the AST.
|
|
162
166
|
if (type === STRING && value) {
|
|
163
167
|
langPattern = value;
|
|
164
168
|
} else if (type === IDENT && name) {
|
|
165
169
|
langPattern = unescapeSelector(name);
|
|
166
170
|
}
|
|
167
|
-
// Cache lang pattern.
|
|
168
171
|
ast._langPattern = langPattern;
|
|
169
|
-
// If no valid language pattern is provided, it cannot match.
|
|
170
172
|
if (typeof langPattern !== 'string') {
|
|
171
173
|
ast._langRegex = null;
|
|
172
174
|
return false;
|
|
173
175
|
}
|
|
174
|
-
// Handle the universal selector '*' for :lang.
|
|
175
176
|
if (langPattern === '*') {
|
|
176
177
|
ast._langRegex = null;
|
|
177
178
|
return elementLang !== '';
|
|
178
179
|
}
|
|
179
|
-
// Validate the provided language pattern structure.
|
|
180
180
|
if (!REG_LANG_VALID.test(langPattern)) {
|
|
181
181
|
ast._langRegex = null;
|
|
182
182
|
return false;
|
|
183
183
|
}
|
|
184
|
-
// Build a regex for extended language range matching.
|
|
185
184
|
let matcherRegex;
|
|
186
185
|
if (langPattern.indexOf('-') > -1) {
|
|
187
186
|
const [langMain, langSub, ...langRest] = langPattern.split('-');
|
|
@@ -199,8 +198,8 @@ export const matchLanguagePseudoClass = (ast, node) => {
|
|
|
199
198
|
} else {
|
|
200
199
|
matcherRegex = new RegExp(`^${langPattern}${LANG_PART}$`, 'i');
|
|
201
200
|
}
|
|
201
|
+
// Store compiled regex in AST for subsequent matches
|
|
202
202
|
ast._langRegex = matcherRegex;
|
|
203
|
-
// Test the element's language against the constructed regex.
|
|
204
203
|
return matcherRegex.test(elementLang);
|
|
205
204
|
};
|
|
206
205
|
|
|
@@ -208,7 +207,7 @@ export const matchLanguagePseudoClass = (ast, node) => {
|
|
|
208
207
|
* Matches the :disabled and :enabled pseudo-classes.
|
|
209
208
|
* @param {string} astName - pseudo-class name
|
|
210
209
|
* @param {object} node - Element node
|
|
211
|
-
* @returns {boolean} - True if
|
|
210
|
+
* @returns {boolean} - True if the pseudo-class matches, otherwise false.
|
|
212
211
|
*/
|
|
213
212
|
export const matchDisabledPseudoClass = (astName, node) => {
|
|
214
213
|
const { localName, parentNode } = node;
|
|
@@ -265,7 +264,7 @@ export const matchDisabledPseudoClass = (astName, node) => {
|
|
|
265
264
|
* Match the :read-only and :read-write pseudo-classes
|
|
266
265
|
* @param {string} astName - pseudo-class name
|
|
267
266
|
* @param {object} node - Element node
|
|
268
|
-
* @returns {boolean} - True if
|
|
267
|
+
* @returns {boolean} - True if the pseudo-class matches, otherwise false.
|
|
269
268
|
*/
|
|
270
269
|
export const matchReadOnlyPseudoClass = (astName, node) => {
|
|
271
270
|
const { localName } = node;
|
|
@@ -328,6 +327,42 @@ export const matchAttributeSelector = (
|
|
|
328
327
|
globalObject
|
|
329
328
|
);
|
|
330
329
|
}
|
|
330
|
+
if (astMatcher === null && !astFlags && typeof astName?.name === 'string') {
|
|
331
|
+
const rawName = unescapeSelector(astName.name);
|
|
332
|
+
if (rawName.indexOf('|') === -1) {
|
|
333
|
+
if (node.hasAttribute(rawName)) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
const attrs = node.attributes;
|
|
337
|
+
if (!attrs || attrs.length === 0) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
const isHTML = node.ownerDocument.contentType === 'text/html';
|
|
341
|
+
const checkName = isHTML ? rawName.toLowerCase() : rawName;
|
|
342
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
343
|
+
let itemName = attrs[i].name;
|
|
344
|
+
if (isHTML) {
|
|
345
|
+
itemName = itemName.toLowerCase();
|
|
346
|
+
}
|
|
347
|
+
const colonIdx = itemName.indexOf(':');
|
|
348
|
+
if (colonIdx > -1) {
|
|
349
|
+
const itemPrefix = itemName.substring(0, colonIdx);
|
|
350
|
+
const itemLocalName = itemName
|
|
351
|
+
.substring(colonIdx + 1)
|
|
352
|
+
.replace(/^:/, '');
|
|
353
|
+
if (itemPrefix === 'xml' && itemLocalName === 'lang') {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (checkName === itemLocalName) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
} else if (checkName === itemName) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
331
366
|
const { attributes } = node;
|
|
332
367
|
// An element with no attributes cannot match.
|
|
333
368
|
if (!attributes || !attributes.length) {
|
|
@@ -546,7 +581,7 @@ export const matchAttributeSelector = (
|
|
|
546
581
|
* @param {boolean} [opt.check] - running in internal check()
|
|
547
582
|
* @param {boolean} [opt.forgive] - forgive undeclared namespace
|
|
548
583
|
* @param {object} [opt.globalObject] - The global object.
|
|
549
|
-
* @returns {boolean} -
|
|
584
|
+
* @returns {boolean} - True if the type selector matches, otherwise false.
|
|
550
585
|
*/
|
|
551
586
|
export const matchTypeSelector = (
|
|
552
587
|
ast,
|
package/src/js/nwsapi.js
CHANGED
|
@@ -413,6 +413,7 @@ export class Nwsapi {
|
|
|
413
413
|
#selectLambdas;
|
|
414
414
|
#selectResolvers;
|
|
415
415
|
#snapshot;
|
|
416
|
+
#uidCounter = 0;
|
|
416
417
|
#window;
|
|
417
418
|
|
|
418
419
|
/* static */
|
|
@@ -463,10 +464,14 @@ export class Nwsapi {
|
|
|
463
464
|
*/
|
|
464
465
|
constructor(window, document, cacheSize = CACHE_SIZE) {
|
|
465
466
|
this.#window = window;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
467
|
+
const cacheOpt = {
|
|
468
|
+
cacheFunction: true,
|
|
469
|
+
strictValidate: false
|
|
470
|
+
};
|
|
471
|
+
this.#matchLambdas = new GenerationalCache(cacheSize, cacheOpt);
|
|
472
|
+
this.#selectLambdas = new GenerationalCache(cacheSize, cacheOpt);
|
|
473
|
+
this.#matchResolvers = new GenerationalCache(cacheSize, cacheOpt);
|
|
474
|
+
this.#selectResolvers = new GenerationalCache(cacheSize, cacheOpt);
|
|
470
475
|
this.#nthChildState = {
|
|
471
476
|
idx: 0,
|
|
472
477
|
len: 0,
|
|
@@ -488,8 +493,6 @@ export class Nwsapi {
|
|
|
488
493
|
isContentEditable,
|
|
489
494
|
isIndeterminate,
|
|
490
495
|
isTarget,
|
|
491
|
-
match: (selectors, element, callback) =>
|
|
492
|
-
this.match(selectors, element, callback),
|
|
493
496
|
nthElement: (element, dir) =>
|
|
494
497
|
solveNth(element, dir, this.#nthChildState, false),
|
|
495
498
|
nthOfType: (element, dir) =>
|
|
@@ -988,12 +991,27 @@ export class Nwsapi {
|
|
|
988
991
|
const expr = match[2]
|
|
989
992
|
.replace(REX.commaGroup, ',')
|
|
990
993
|
.replace(REX.trimSpaces, '');
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
994
|
+
if (pseudoName === 'is' || pseudoName === 'not') {
|
|
995
|
+
const subExprs = expr.match(REX.splitGroup) || [expr];
|
|
996
|
+
const uid = ++this.#uidCounter;
|
|
997
|
+
const label = `l_${uid}`;
|
|
998
|
+
let code = `{ let r_${uid}=false, e_${uid}=e, n_${uid}=n, o_${uid}=o; ${label}: { `;
|
|
999
|
+
for (let i = 0; i < subExprs.length; i++) {
|
|
1000
|
+
const subCode = this._compileSelector(
|
|
1001
|
+
subExprs[i],
|
|
1002
|
+
`r_${uid}=true; break ${label};`,
|
|
1003
|
+
false
|
|
1004
|
+
);
|
|
1005
|
+
code += `{ ${subCode} } e=e_${uid}; n=n_${uid}; o=o_${uid}; `;
|
|
1006
|
+
}
|
|
1007
|
+
code += `} e=e_${uid}; n=n_${uid}; o=o_${uid}; `;
|
|
1008
|
+
if (pseudoName === 'is') {
|
|
1009
|
+
return `${code} if(r_${uid}){${source}} }`;
|
|
1010
|
+
} else {
|
|
1011
|
+
return `${code} if(!r_${uid}){${source}} }`;
|
|
1012
|
+
}
|
|
996
1013
|
} else if (pseudoName === 'has') {
|
|
1014
|
+
const escapedExpr = expr.replace(/\\/g, '\\\\').replace(/\x22/g, '\\"');
|
|
997
1015
|
this.#matchResolvers.clear();
|
|
998
1016
|
return `if(e.querySelector(":scope ${escapedExpr}")){${source}}`;
|
|
999
1017
|
}
|
package/src/js/selector.js
CHANGED
|
@@ -23,18 +23,17 @@ import {
|
|
|
23
23
|
PS_ELEMENT_SELECTOR,
|
|
24
24
|
SELECTOR,
|
|
25
25
|
SYNTAX_ERR,
|
|
26
|
-
|
|
27
|
-
TARGET_ALL,
|
|
28
|
-
TARGET_FIRST
|
|
26
|
+
TARGET_ALL
|
|
29
27
|
} from './constant.js';
|
|
30
28
|
|
|
31
29
|
/* regexp */
|
|
32
|
-
const REG_ATTR_SIMPLE = /^\[[A-Z\d-]{1,255}(?:="?[A-Z\d\s-]{1,255}"?)?\]$/i;
|
|
33
|
-
const REG_TAG_SIMPLE = new RegExp(`^(?:${TAG_TYPE})$`);
|
|
34
30
|
const REG_EXCLUDE_BASIC =
|
|
35
31
|
/[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
|
|
32
|
+
const REG_EXCLUDE_QSA = new RegExp(
|
|
33
|
+
`(?:^(?:[A-Z]|\\.)[\\w-]*$|${COMPOUND_I}${DESCEND}${COMPOUND_I})`,
|
|
34
|
+
'i'
|
|
35
|
+
);
|
|
36
36
|
const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
|
|
37
|
-
const REG_DESCEND = new RegExp(`${COMPOUND_I}${DESCEND}${COMPOUND_I}`, 'i');
|
|
38
37
|
const REG_LOGIC_COMPLEX = new RegExp(
|
|
39
38
|
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
|
|
40
39
|
);
|
|
@@ -266,7 +265,6 @@ export const extractSubjectsRegExp = (selector, caseSensitive) => {
|
|
|
266
265
|
* @returns {boolean} - True if the selector is valid for nwsapi.
|
|
267
266
|
*/
|
|
268
267
|
export const filterSelector = (selector, target) => {
|
|
269
|
-
const isQuerySelectorAll = target === TARGET_ALL;
|
|
270
268
|
// Basic validation and fast-fail for null/undefined/non-string values.
|
|
271
269
|
if (
|
|
272
270
|
!selector ||
|
|
@@ -279,6 +277,10 @@ export const filterSelector = (selector, target) => {
|
|
|
279
277
|
if (REG_INVALID_SYNTAX.test(selector)) {
|
|
280
278
|
return false;
|
|
281
279
|
}
|
|
280
|
+
// Target-specific early exits.
|
|
281
|
+
if (target === TARGET_ALL && REG_EXCLUDE_QSA.test(selector)) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
282
284
|
// Exclude various complex or unsupported selectors early.
|
|
283
285
|
// i.e. non-ASCII, escaped selectors, namespaced selectors, pseudo-elements.
|
|
284
286
|
if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
|
|
@@ -291,26 +293,13 @@ export const filterSelector = (selector, target) => {
|
|
|
291
293
|
return false;
|
|
292
294
|
}
|
|
293
295
|
}
|
|
294
|
-
// Target-specific early exits.
|
|
295
|
-
if (target === TARGET_FIRST) {
|
|
296
|
-
return REG_ATTR_SIMPLE.test(selector);
|
|
297
|
-
}
|
|
298
|
-
if (target === TARGET_ALL && REG_TAG_SIMPLE.test(selector)) {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
296
|
// Logic for pseudo-classes.
|
|
302
297
|
if (selector.includes(':')) {
|
|
303
|
-
// Exclude descendant combinators in logical selectors for querySelectorAll.
|
|
304
|
-
if (isQuerySelectorAll && REG_DESCEND.test(selector)) {
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
298
|
// Determine if the selector has complex logical structures.
|
|
308
|
-
const isComplex =
|
|
299
|
+
const isComplex =
|
|
300
|
+
target === TARGET_ALL ? false : REG_COMPLEX.test(selector);
|
|
309
301
|
// Handle :has() specifically.
|
|
310
302
|
if (selector.includes(':has(')) {
|
|
311
|
-
if (isQuerySelectorAll) {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
303
|
if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
|
|
315
304
|
return false;
|
|
316
305
|
}
|
package/src/js/utility.js
CHANGED
|
@@ -8,17 +8,21 @@ import isCustomElementName from 'is-potential-custom-element-name';
|
|
|
8
8
|
|
|
9
9
|
/* constants */
|
|
10
10
|
import {
|
|
11
|
+
CLASS_SELECTOR,
|
|
11
12
|
DOCUMENT_FRAGMENT_NODE,
|
|
12
13
|
DOCUMENT_NODE,
|
|
13
14
|
DOCUMENT_POSITION_CONTAINS,
|
|
14
15
|
DOCUMENT_POSITION_PRECEDING,
|
|
15
16
|
ELEMENT_NODE,
|
|
17
|
+
ID_SELECTOR,
|
|
16
18
|
INPUT_BUTTON,
|
|
17
19
|
INPUT_EDIT,
|
|
18
20
|
INPUT_LTR,
|
|
19
21
|
INPUT_TEXT,
|
|
22
|
+
SHOW_ELEMENT,
|
|
20
23
|
TEXT_NODE,
|
|
21
24
|
TYPE_FROM,
|
|
25
|
+
TYPE_SELECTOR,
|
|
22
26
|
TYPE_TO
|
|
23
27
|
} from './constant.js';
|
|
24
28
|
const KEYS_DIR_AUTO = new Set([...INPUT_BUTTON, ...INPUT_TEXT, 'hidden']);
|
|
@@ -297,19 +301,24 @@ export const getSlottedTextContent = node => {
|
|
|
297
301
|
* Get directionality of a node.
|
|
298
302
|
* @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
|
|
299
303
|
* @param {object} node - The Element node.
|
|
304
|
+
* @param {WeakMap} [dirCache] - Cache for directionality.
|
|
300
305
|
* @returns {?string} - 'ltr' or 'rtl'.
|
|
301
306
|
*/
|
|
302
|
-
export const getDirectionality = node => {
|
|
307
|
+
export const getDirectionality = (node, dirCache = new WeakMap()) => {
|
|
303
308
|
if (!node?.nodeType) {
|
|
304
309
|
throw new TypeError(`Unexpected type ${getType(node)}`);
|
|
305
310
|
}
|
|
306
311
|
if (node.nodeType !== ELEMENT_NODE) {
|
|
307
312
|
return null;
|
|
308
313
|
}
|
|
314
|
+
if (dirCache.has(node)) {
|
|
315
|
+
return dirCache.get(node);
|
|
316
|
+
}
|
|
309
317
|
const { dir: dirAttr, localName, parentNode } = node;
|
|
310
318
|
const { getEmbeddingLevels } = bidiFactory();
|
|
319
|
+
let result = 'ltr';
|
|
311
320
|
if (dirAttr === 'ltr' || dirAttr === 'rtl') {
|
|
312
|
-
|
|
321
|
+
result = dirAttr;
|
|
313
322
|
} else if (dirAttr === 'auto') {
|
|
314
323
|
let text = '';
|
|
315
324
|
switch (localName) {
|
|
@@ -317,7 +326,8 @@ export const getDirectionality = node => {
|
|
|
317
326
|
if (!node.type || KEYS_DIR_AUTO.has(node.type)) {
|
|
318
327
|
text = node.value;
|
|
319
328
|
} else if (KEYS_DIR_LTR.has(node.type)) {
|
|
320
|
-
|
|
329
|
+
result = 'ltr';
|
|
330
|
+
text = null; // Flag to skip text evaluation
|
|
321
331
|
}
|
|
322
332
|
break;
|
|
323
333
|
}
|
|
@@ -357,21 +367,23 @@ export const getDirectionality = node => {
|
|
|
357
367
|
}
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
|
-
if (text) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
}
|
|
371
383
|
}
|
|
372
384
|
}
|
|
373
385
|
} else if (localName === 'input' && node.type === 'tel') {
|
|
374
|
-
|
|
386
|
+
result = 'ltr';
|
|
375
387
|
} else if (localName === 'bdi') {
|
|
376
388
|
const text = node.textContent.trim();
|
|
377
389
|
if (text) {
|
|
@@ -379,7 +391,7 @@ export const getDirectionality = node => {
|
|
|
379
391
|
paragraphs: [{ level }]
|
|
380
392
|
} = getEmbeddingLevels(text);
|
|
381
393
|
if (level % 2 === 1) {
|
|
382
|
-
|
|
394
|
+
result = 'rtl';
|
|
383
395
|
}
|
|
384
396
|
}
|
|
385
397
|
} else if (parentNode) {
|
|
@@ -390,47 +402,67 @@ export const getDirectionality = node => {
|
|
|
390
402
|
paragraphs: [{ level }]
|
|
391
403
|
} = getEmbeddingLevels(text);
|
|
392
404
|
if (level % 2 === 1) {
|
|
393
|
-
|
|
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);
|
|
394
413
|
}
|
|
395
|
-
return 'ltr';
|
|
396
414
|
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
415
|
+
} else {
|
|
416
|
+
const { nodeType: parentNodeType } = parentNode;
|
|
417
|
+
if (parentNodeType === ELEMENT_NODE) {
|
|
418
|
+
result = getDirectionality(parentNode, dirCache);
|
|
419
|
+
}
|
|
401
420
|
}
|
|
402
421
|
}
|
|
403
|
-
|
|
422
|
+
dirCache.set(node, result);
|
|
423
|
+
return result;
|
|
404
424
|
};
|
|
405
425
|
|
|
406
426
|
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* @param {
|
|
410
|
-
* @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.
|
|
411
431
|
*/
|
|
412
|
-
export const getLanguageAttribute = node => {
|
|
432
|
+
export const getLanguageAttribute = (node, langCache = new WeakMap()) => {
|
|
413
433
|
if (!node?.nodeType) {
|
|
414
434
|
throw new TypeError(`Unexpected type ${getType(node)}`);
|
|
415
435
|
}
|
|
416
436
|
if (node.nodeType !== ELEMENT_NODE) {
|
|
417
437
|
return null;
|
|
418
438
|
}
|
|
439
|
+
if (langCache.has(node)) {
|
|
440
|
+
return langCache.get(node);
|
|
441
|
+
}
|
|
419
442
|
const { contentType } = node.ownerDocument;
|
|
420
443
|
const isHtml = REG_IS_HTML.test(contentType);
|
|
421
444
|
const isXml = REG_IS_XML.test(contentType);
|
|
422
445
|
let isShadow = false;
|
|
446
|
+
let result;
|
|
447
|
+
const visited = [];
|
|
423
448
|
// Traverse up from the current node to the root.
|
|
424
449
|
let current = node;
|
|
425
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
|
+
}
|
|
426
458
|
// Check if the current node is an element.
|
|
427
459
|
switch (current.nodeType) {
|
|
428
460
|
case ELEMENT_NODE: {
|
|
429
461
|
// Check for and return the language attribute if present.
|
|
430
462
|
if (isHtml && current.hasAttribute('lang')) {
|
|
431
|
-
|
|
463
|
+
result = current.getAttribute('lang');
|
|
432
464
|
} else if (isXml && current.hasAttribute('xml:lang')) {
|
|
433
|
-
|
|
465
|
+
result = current.getAttribute('xml:lang');
|
|
434
466
|
}
|
|
435
467
|
break;
|
|
436
468
|
}
|
|
@@ -444,9 +476,12 @@ export const getLanguageAttribute = node => {
|
|
|
444
476
|
case DOCUMENT_NODE:
|
|
445
477
|
default: {
|
|
446
478
|
// Stop if we reach the root document node.
|
|
447
|
-
|
|
479
|
+
result = null;
|
|
448
480
|
}
|
|
449
481
|
}
|
|
482
|
+
if (result !== undefined) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
450
485
|
if (isShadow) {
|
|
451
486
|
current = current.host;
|
|
452
487
|
isShadow = false;
|
|
@@ -456,8 +491,13 @@ export const getLanguageAttribute = node => {
|
|
|
456
491
|
break;
|
|
457
492
|
}
|
|
458
493
|
}
|
|
459
|
-
|
|
460
|
-
|
|
494
|
+
if (result === undefined) {
|
|
495
|
+
result = null;
|
|
496
|
+
}
|
|
497
|
+
for (const visitedNode of visited) {
|
|
498
|
+
langCache.set(visitedNode, result);
|
|
499
|
+
}
|
|
500
|
+
return result;
|
|
461
501
|
};
|
|
462
502
|
|
|
463
503
|
/**
|
|
@@ -794,3 +834,103 @@ export const sortNodes = (nodes = []) => {
|
|
|
794
834
|
}
|
|
795
835
|
return arr;
|
|
796
836
|
};
|
|
837
|
+
|
|
838
|
+
/**
|
|
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.
|
|
844
|
+
*/
|
|
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;
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
/**
|
|
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}
|
|
886
|
+
*/
|
|
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;
|
|
896
|
+
}
|
|
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);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
/**
|
|
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.
|
|
920
|
+
*/
|
|
921
|
+
export const collectAllDescendants = (node, document) => {
|
|
922
|
+
if (!node?.nodeType) {
|
|
923
|
+
throw new TypeError(`Unexpected type ${getType(node)}`);
|
|
924
|
+
}
|
|
925
|
+
if (document?.nodeType !== DOCUMENT_NODE) {
|
|
926
|
+
throw new TypeError(`Unexpected type ${getType(document)}`);
|
|
927
|
+
}
|
|
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();
|
|
934
|
+
}
|
|
935
|
+
return descendants;
|
|
936
|
+
};
|
package/types/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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;
|
package/types/js/constant.d.ts
CHANGED
|
@@ -54,8 +54,10 @@ export const DESCEND: "\\s?[\\s>]\\s?";
|
|
|
54
54
|
export const SIBLING: "\\s?[+~]\\s?";
|
|
55
55
|
export const LOGIC_IS: ":is\\(\\s*[^)]+\\s*\\)";
|
|
56
56
|
export const N_TH: "nth-(?:last-)?(?:child|of-type)\\(\\s*(?:even|odd|[+-]?(?:(?:0|[1-9]\\d*)n?|n)|(?:[+-]?(?:0|[1-9]\\d*))?n\\s*[+-]\\s*(?:0|[1-9]\\d*))\\s*\\)";
|
|
57
|
+
export const ATTR_TYPE: "\\[[^|\\]]+\\]";
|
|
57
58
|
export const SUB_TYPE: "\\[[^|\\]]+\\]|[#.:][\\w-]+";
|
|
58
59
|
export const SUB_TYPE_WO_PSEUDO: "\\[[^|\\]]+\\]|[#.][\\w-]+";
|
|
60
|
+
export const TAG_TYPE_WO_UNIVERSAL: "[A-Za-z][\\w-]*";
|
|
59
61
|
export const TAG_TYPE: "\\*|[A-Za-z][\\w-]*";
|
|
60
62
|
export const TAG_TYPE_I: "\\*|[A-Z][\\w-]*";
|
|
61
63
|
export const COMPOUND: "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)";
|
package/types/js/finder.d.ts
CHANGED
|
@@ -22,12 +22,15 @@ export class Finder {
|
|
|
22
22
|
private _collectNthOfType;
|
|
23
23
|
private _matchAnPlusB;
|
|
24
24
|
private _matchHasPseudoFunc;
|
|
25
|
+
private _buildHasAllowlist;
|
|
25
26
|
private _evaluateHasPseudo;
|
|
26
27
|
private _matchLogicalPseudoFunc;
|
|
28
|
+
private _evaluateLogicalPseudo;
|
|
29
|
+
private _evaluatePseudoClassFunc;
|
|
27
30
|
private _matchPseudoClassSelector;
|
|
28
31
|
private _evaluateHostPseudo;
|
|
29
32
|
private _evaluateHostContextPseudo;
|
|
30
|
-
private
|
|
33
|
+
private _evaluateShadowHost;
|
|
31
34
|
private _matchSelectorForElement;
|
|
32
35
|
private _matchSelectorForShadowRoot;
|
|
33
36
|
private _matchSelector;
|
package/types/js/matcher.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export function matchPseudoElementSelector(astName: string, astType: string, { f
|
|
|
3
3
|
globalObject?: object | undefined;
|
|
4
4
|
warn?: boolean | undefined;
|
|
5
5
|
}): void;
|
|
6
|
-
export function matchDirectionPseudoClass(ast: object, node: object): boolean;
|
|
7
|
-
export function matchLanguagePseudoClass(ast: object, node: object): boolean;
|
|
6
|
+
export function matchDirectionPseudoClass(ast: object, node: object, dirCache?: WeakMap<any, any>): boolean;
|
|
7
|
+
export function matchLanguagePseudoClass(ast: object, node: object, langCache?: WeakMap<any, any>): boolean;
|
|
8
8
|
export function matchDisabledPseudoClass(astName: string, node: object): boolean;
|
|
9
9
|
export function matchReadOnlyPseudoClass(astName: string, node: object): boolean;
|
|
10
10
|
export function matchAttributeSelector(ast: object, node: object, { check, forgive, globalObject }?: {
|