@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/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/parser.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
HEX,
|
|
23
23
|
ID_SELECTOR,
|
|
24
24
|
KEYS_LOGICAL,
|
|
25
|
+
KEYS_PS_CLASS_SUPPORTED,
|
|
25
26
|
NTH,
|
|
26
27
|
PS_CLASS_SELECTOR,
|
|
27
28
|
PS_ELEMENT_SELECTOR,
|
|
@@ -219,9 +220,10 @@ export const parseSelector = sel => {
|
|
|
219
220
|
* about its contents.
|
|
220
221
|
* @param {object} ast - The AST to traverse.
|
|
221
222
|
* @param {boolean} toObject - True if converts ast to object, false otherwise.
|
|
223
|
+
* @param {function(object): void} [callback] - Optional callback for each node.
|
|
222
224
|
* @returns {{branches: Array<object>, info: object}} An object containing the selector branches and info.
|
|
223
225
|
*/
|
|
224
|
-
export const walkAST = (ast = {}, toObject = false) => {
|
|
226
|
+
export const walkAST = (ast = {}, toObject = false, callback = null) => {
|
|
225
227
|
const branches = new Set();
|
|
226
228
|
const info = {
|
|
227
229
|
hasForgivenPseudoFunc: false,
|
|
@@ -230,10 +232,14 @@ export const walkAST = (ast = {}, toObject = false) => {
|
|
|
230
232
|
hasNotPseudoFunc: false,
|
|
231
233
|
hasNthChildOfSelector: false,
|
|
232
234
|
hasNestedSelector: false,
|
|
233
|
-
hasStatePseudoClass: false
|
|
235
|
+
hasStatePseudoClass: false,
|
|
236
|
+
hasUnsupportedPseudoClass: false
|
|
234
237
|
};
|
|
235
238
|
const opt = {
|
|
236
239
|
enter(node) {
|
|
240
|
+
if (typeof callback === 'function') {
|
|
241
|
+
callback(node);
|
|
242
|
+
}
|
|
237
243
|
switch (node.type) {
|
|
238
244
|
case CLASS_SELECTOR: {
|
|
239
245
|
if (/^-?\d/.test(node.name)) {
|
|
@@ -254,24 +260,27 @@ export const walkAST = (ast = {}, toObject = false) => {
|
|
|
254
260
|
break;
|
|
255
261
|
}
|
|
256
262
|
case PS_CLASS_SELECTOR: {
|
|
257
|
-
|
|
263
|
+
const name = node.name.toLowerCase();
|
|
264
|
+
if (KEYS_LOGICAL.has(name)) {
|
|
258
265
|
info.hasNestedSelector = true;
|
|
259
266
|
info.hasLogicalPseudoFunc = true;
|
|
260
|
-
if (
|
|
267
|
+
if (name === 'has') {
|
|
261
268
|
info.hasHasPseudoFunc = true;
|
|
262
|
-
} else if (
|
|
269
|
+
} else if (name === 'not') {
|
|
263
270
|
info.hasNotPseudoFunc = true;
|
|
264
271
|
} else {
|
|
265
272
|
info.hasForgivenPseudoFunc = true;
|
|
266
273
|
}
|
|
267
|
-
} else if (KEYS_PS_CLASS_STATE.has(
|
|
274
|
+
} else if (KEYS_PS_CLASS_STATE.has(name)) {
|
|
268
275
|
info.hasStatePseudoClass = true;
|
|
269
276
|
} else if (
|
|
270
|
-
KEYS_SHADOW_HOST.has(
|
|
277
|
+
KEYS_SHADOW_HOST.has(name) &&
|
|
271
278
|
Array.isArray(node.children) &&
|
|
272
279
|
node.children.length
|
|
273
280
|
) {
|
|
274
281
|
info.hasNestedSelector = true;
|
|
282
|
+
} else if (!KEYS_PS_CLASS_SUPPORTED.has(name)) {
|
|
283
|
+
info.hasUnsupportedPseudoClass = true;
|
|
275
284
|
}
|
|
276
285
|
break;
|
|
277
286
|
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selector.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* import */
|
|
6
|
+
import * as cssTree from 'css-tree';
|
|
7
|
+
import { generateException } from './utility.js';
|
|
8
|
+
|
|
9
|
+
/* constants */
|
|
10
|
+
import {
|
|
11
|
+
COMBINATOR,
|
|
12
|
+
COMBO,
|
|
13
|
+
COMPOUND_I,
|
|
14
|
+
COMPOUND_L_I,
|
|
15
|
+
DESCEND,
|
|
16
|
+
HAS_COMPOUND,
|
|
17
|
+
KEYS_LOGICAL,
|
|
18
|
+
KEYS_PS_CLASS_SUPPORTED,
|
|
19
|
+
LOGIC_COMPLEX,
|
|
20
|
+
LOGIC_COMPOUND,
|
|
21
|
+
N_TH,
|
|
22
|
+
PSEUDO_CLASS,
|
|
23
|
+
PS_CLASS_SELECTOR,
|
|
24
|
+
PS_ELEMENT_SELECTOR,
|
|
25
|
+
SELECTOR,
|
|
26
|
+
SYNTAX_ERR,
|
|
27
|
+
TARGET_ALL
|
|
28
|
+
} from './constant.js';
|
|
29
|
+
|
|
30
|
+
/* regexp */
|
|
31
|
+
const REG_EXCLUDE_BASIC =
|
|
32
|
+
/[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
|
|
33
|
+
const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
|
|
34
|
+
const REG_COMPOUND = new RegExp(`^${COMPOUND_L_I}$`, 'i');
|
|
35
|
+
const REG_DESCEND = new RegExp(`${COMPOUND_I}${DESCEND}${COMPOUND_I}`, 'i');
|
|
36
|
+
const REG_LOGIC_COMPLEX = new RegExp(
|
|
37
|
+
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
|
|
38
|
+
);
|
|
39
|
+
const REG_LOGIC_COMPOUND = new RegExp(
|
|
40
|
+
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
|
|
41
|
+
);
|
|
42
|
+
const REG_LOGIC_HAS_COMPOUND = new RegExp(
|
|
43
|
+
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
|
|
44
|
+
);
|
|
45
|
+
const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
|
|
46
|
+
const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
|
|
47
|
+
const REG_COMBO = new RegExp(COMBO);
|
|
48
|
+
const REG_ID = /#(\D[^#.*]+)/g;
|
|
49
|
+
const REG_CLASS = /\.(\D[^#.*]+)/g;
|
|
50
|
+
const REG_TAG = /^([^#.]+)/;
|
|
51
|
+
const REG_INVALID_SYNTAX =
|
|
52
|
+
/[+~>]\s*[+~>]|^\s*[+~>]|[+~>]\s*$|^\s*,|,\s*,|,\s*$/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find a nested :has() pseudo-class.
|
|
56
|
+
* @param {object} leaf - The AST leaf to check.
|
|
57
|
+
* @returns {?object} The leaf if it's :has, otherwise null.
|
|
58
|
+
*/
|
|
59
|
+
export const findNestedHas = leaf => leaf.name === 'has';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find a logical pseudo-class that contains a nested :has().
|
|
63
|
+
* @param {object} leaf - The AST leaf to check.
|
|
64
|
+
* @returns {?object} The leaf if it matches, otherwise null.
|
|
65
|
+
*/
|
|
66
|
+
export const findLogicalWithNestedHas = leaf => {
|
|
67
|
+
if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
|
|
68
|
+
return leaf;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validates nesting restrictions within :has() arguments.
|
|
75
|
+
* @param {Array<object>} astChildren - The AST nodes representing the :has() arguments.
|
|
76
|
+
* @returns {boolean} False if there's an invalid nesting constraint violation.
|
|
77
|
+
*/
|
|
78
|
+
export const validateHasNesting = astChildren => {
|
|
79
|
+
const l = astChildren.length;
|
|
80
|
+
for (let i = 0; i < l; i++) {
|
|
81
|
+
const item = cssTree.find(astChildren[i], findLogicalWithNestedHas);
|
|
82
|
+
if (item) {
|
|
83
|
+
// If nested :has() is wrapped inside :is() or :where(), it is forgiven.
|
|
84
|
+
if (item.name !== 'is' && item.name !== 'where') {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates a callback function to validate :has() nesting during AST walk.
|
|
94
|
+
* @param {object} globalObj - The global window object.
|
|
95
|
+
* @returns {function(object): void} The callback function for walkAST.
|
|
96
|
+
*/
|
|
97
|
+
export const createHasValidator = globalObj => node => {
|
|
98
|
+
if (
|
|
99
|
+
node.type === PS_CLASS_SELECTOR &&
|
|
100
|
+
node.name.toLowerCase() === 'has' &&
|
|
101
|
+
!validateHasNesting(Array.from(node.children || []))
|
|
102
|
+
) {
|
|
103
|
+
const css = cssTree.generate(node);
|
|
104
|
+
throw generateException(
|
|
105
|
+
`Disallowed nested :has() pseudo-class: ${css}`,
|
|
106
|
+
SYNTAX_ERR,
|
|
107
|
+
globalObj
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if a combinator node is invalid (leading, trailing, or consecutive).
|
|
114
|
+
* @param {string} type - The current node type.
|
|
115
|
+
* @param {string|null} prevType - The previous node type.
|
|
116
|
+
* @param {boolean} isLast - Whether the current node is the last in the list.
|
|
117
|
+
* @returns {boolean} True if the combinator is invalid.
|
|
118
|
+
*/
|
|
119
|
+
export const isInvalidCombinator = (type, prevType, isLast) =>
|
|
120
|
+
type === COMBINATOR &&
|
|
121
|
+
(prevType === null || prevType === COMBINATOR || isLast);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Checks if a given AST is supported by the DOMSelector engine.
|
|
125
|
+
* @param {object} ast - The AST to validate.
|
|
126
|
+
* @returns {boolean} True if the selector is fully supported.
|
|
127
|
+
*/
|
|
128
|
+
export const isSupportedAST = ast => {
|
|
129
|
+
let isSupported = true;
|
|
130
|
+
const walk = (
|
|
131
|
+
node,
|
|
132
|
+
context = { insideHas: false, insideForgiving: false }
|
|
133
|
+
) => {
|
|
134
|
+
if (!isSupported || !node) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const nextContext = { ...context };
|
|
138
|
+
if (node.type === PS_ELEMENT_SELECTOR) {
|
|
139
|
+
isSupported = false;
|
|
140
|
+
return;
|
|
141
|
+
} else if (node.type === PS_CLASS_SELECTOR) {
|
|
142
|
+
let name = node.name;
|
|
143
|
+
if (name && typeof name === 'string') {
|
|
144
|
+
name = name.toLowerCase();
|
|
145
|
+
}
|
|
146
|
+
if (!KEYS_PS_CLASS_SUPPORTED.has(name)) {
|
|
147
|
+
isSupported = false;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (name === 'has') {
|
|
151
|
+
if (context.insideHas && !context.insideForgiving) {
|
|
152
|
+
isSupported = false;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
nextContext.insideHas = true;
|
|
156
|
+
} else if (name === 'is' || name === 'where') {
|
|
157
|
+
nextContext.insideForgiving = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (node.children) {
|
|
161
|
+
let prevType = null;
|
|
162
|
+
if (node.children.head !== undefined) {
|
|
163
|
+
let current = node.children.head;
|
|
164
|
+
while (current) {
|
|
165
|
+
if (!current.data) {
|
|
166
|
+
current = current.next;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const childType = current.data.type;
|
|
170
|
+
if (
|
|
171
|
+
node.type === SELECTOR &&
|
|
172
|
+
isInvalidCombinator(childType, prevType, !current.next)
|
|
173
|
+
) {
|
|
174
|
+
if (!(prevType === null && context.insideHas)) {
|
|
175
|
+
isSupported = false;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
prevType = childType;
|
|
180
|
+
walk(current.data, nextContext);
|
|
181
|
+
if (!isSupported) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
current = current.next;
|
|
185
|
+
}
|
|
186
|
+
} else if (Array.isArray(node.children)) {
|
|
187
|
+
const l = node.children.length;
|
|
188
|
+
for (let i = 0; i < l; i++) {
|
|
189
|
+
const child = node.children[i];
|
|
190
|
+
if (!child) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const childType = child.type;
|
|
194
|
+
if (
|
|
195
|
+
node.type === SELECTOR &&
|
|
196
|
+
isInvalidCombinator(childType, prevType, i === l - 1)
|
|
197
|
+
) {
|
|
198
|
+
if (!(prevType === null && context.insideHas)) {
|
|
199
|
+
isSupported = false;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
prevType = childType;
|
|
204
|
+
walk(child, nextContext);
|
|
205
|
+
if (!isSupported) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (node.selector) {
|
|
212
|
+
walk(node.selector, nextContext);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
walk(ast);
|
|
216
|
+
return isSupported;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Extracts the rightmost subject keys (id, class, tag) from a selector.
|
|
221
|
+
* @param {string} selector - The CSS selector string to parse.
|
|
222
|
+
* @param {boolean} caseSensitive - True if the tag should be case-sensitive.
|
|
223
|
+
* @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
|
|
224
|
+
*/
|
|
225
|
+
export const extractSubjectsRegExp = (selector, caseSensitive) => {
|
|
226
|
+
const subjects = [];
|
|
227
|
+
const groups = selector.split(',');
|
|
228
|
+
for (let i = 0; i < groups.length; i++) {
|
|
229
|
+
const group = groups[i].trim();
|
|
230
|
+
if (!group) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const compounds = group.split(REG_COMBO);
|
|
234
|
+
const rightmost = compounds[compounds.length - 1];
|
|
235
|
+
let idKey = null;
|
|
236
|
+
let classKey = null;
|
|
237
|
+
let tagKey = null;
|
|
238
|
+
if (rightmost) {
|
|
239
|
+
const idMatch = rightmost.match(REG_ID);
|
|
240
|
+
if (idMatch) {
|
|
241
|
+
idKey = idMatch[idMatch.length - 1].slice(1);
|
|
242
|
+
}
|
|
243
|
+
const classMatch = rightmost.match(REG_CLASS);
|
|
244
|
+
if (classMatch) {
|
|
245
|
+
classKey = classMatch[classMatch.length - 1].slice(1);
|
|
246
|
+
}
|
|
247
|
+
const tagMatch = rightmost.match(REG_TAG);
|
|
248
|
+
if (tagMatch) {
|
|
249
|
+
const tag = tagMatch[1];
|
|
250
|
+
if (tag !== '*') {
|
|
251
|
+
tagKey = caseSensitive ? tag : tag.toLowerCase();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
subjects.push({ id: idKey, className: classKey, tag: tagKey });
|
|
256
|
+
}
|
|
257
|
+
return subjects;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Filter a selector for use with nwsapi.
|
|
262
|
+
* @param {string} selector - The selector string.
|
|
263
|
+
* @param {string} target - The target type.
|
|
264
|
+
* @returns {boolean} - True if the selector is valid for nwsapi.
|
|
265
|
+
*/
|
|
266
|
+
export const filterSelector = (selector, target) => {
|
|
267
|
+
const isQuerySelectorAll = target === TARGET_ALL;
|
|
268
|
+
// Basic validation and fast-fail for null/undefined/non-string values.
|
|
269
|
+
if (
|
|
270
|
+
!selector ||
|
|
271
|
+
typeof selector !== 'string' ||
|
|
272
|
+
/null|undefined/.test(selector)
|
|
273
|
+
) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
// Validate syntax.
|
|
277
|
+
if (REG_INVALID_SYNTAX.test(selector)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
// Exclude various complex or unsupported selectors early.
|
|
281
|
+
// i.e. non-ASCII, escaped selectors, namespaced selectors, pseudo-elements.
|
|
282
|
+
if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
// Validate attribute selector integrity.
|
|
286
|
+
if (selector.includes('[')) {
|
|
287
|
+
const index = selector.lastIndexOf('[');
|
|
288
|
+
if (selector.indexOf(']', index) === -1) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Target-specific early exits.
|
|
293
|
+
if (target === TARGET_ALL && !REG_COMPOUND.test(selector)) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
// Logic for pseudo-classes.
|
|
297
|
+
if (selector.includes(':')) {
|
|
298
|
+
// Exclude descendant combinators in logical selectors for querySelectorAll.
|
|
299
|
+
if (isQuerySelectorAll && REG_DESCEND.test(selector)) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
// Determine if the selector has complex logical structures.
|
|
303
|
+
const isComplex = isQuerySelectorAll ? false : REG_COMPLEX.test(selector);
|
|
304
|
+
// Handle :has() specifically.
|
|
305
|
+
if (selector.includes(':has(')) {
|
|
306
|
+
if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return REG_END_WITH_HAS.test(selector);
|
|
310
|
+
}
|
|
311
|
+
// Handle :is() and :not().
|
|
312
|
+
if (/(?:is|not)\(/.test(selector)) {
|
|
313
|
+
if (isComplex) {
|
|
314
|
+
return !REG_LOGIC_COMPLEX.test(selector);
|
|
315
|
+
} else {
|
|
316
|
+
return !REG_LOGIC_COMPOUND.test(selector);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Default check for other pseudo-classes against known list.
|
|
320
|
+
if (REG_WO_LOGICAL.test(selector)) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
};
|