@asamuzakjp/dom-selector 8.0.0 → 8.0.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 +169 -155
- package/package.json +7 -6
- package/src/index.js +69 -33
- package/src/js/constant.js +98 -0
- package/src/js/finder.js +19 -36
- package/src/js/parser.js +16 -7
- package/src/js/selector.js +333 -0
- package/src/js/utility.js +0 -334
- package/types/index.d.ts +1 -0
- package/types/js/constant.d.ts +3 -0
- package/types/js/parser.d.ts +1 -1
- package/types/js/selector.d.ts +12 -0
- package/types/js/utility.d.ts +0 -10
package/src/js/constant.js
CHANGED
|
@@ -127,3 +127,101 @@ export const INPUT_LTR = Object.freeze([
|
|
|
127
127
|
|
|
128
128
|
/* logical combination pseudo-classes */
|
|
129
129
|
export const KEYS_LOGICAL = new Set(['has', 'is', 'not', 'where']);
|
|
130
|
+
|
|
131
|
+
/* lists of supported / unsupported pseudo-classes and pseudo-elements */
|
|
132
|
+
export const KEYS_PS_CLASS_SUPPORTED = new Set([
|
|
133
|
+
'active',
|
|
134
|
+
'any-link',
|
|
135
|
+
'checked',
|
|
136
|
+
'closed',
|
|
137
|
+
'default',
|
|
138
|
+
'defined',
|
|
139
|
+
'dir',
|
|
140
|
+
'disabled',
|
|
141
|
+
'empty',
|
|
142
|
+
'enabled',
|
|
143
|
+
'first-child',
|
|
144
|
+
'first-of-type',
|
|
145
|
+
'focus',
|
|
146
|
+
'focus-visible',
|
|
147
|
+
'focus-within',
|
|
148
|
+
'has',
|
|
149
|
+
'host',
|
|
150
|
+
'host-context',
|
|
151
|
+
'hover',
|
|
152
|
+
'in-range',
|
|
153
|
+
'indeterminate',
|
|
154
|
+
'invalid',
|
|
155
|
+
'is',
|
|
156
|
+
'lang',
|
|
157
|
+
'last-child',
|
|
158
|
+
'last-of-type',
|
|
159
|
+
'link',
|
|
160
|
+
'local-link',
|
|
161
|
+
'not',
|
|
162
|
+
'nth-child',
|
|
163
|
+
'nth-last-child',
|
|
164
|
+
'nth-last-of-type',
|
|
165
|
+
'nth-of-type',
|
|
166
|
+
'only-child',
|
|
167
|
+
'only-of-type',
|
|
168
|
+
'open',
|
|
169
|
+
'optional',
|
|
170
|
+
'out-of-range',
|
|
171
|
+
'placeholder-shown',
|
|
172
|
+
'read-only',
|
|
173
|
+
'read-write',
|
|
174
|
+
'required',
|
|
175
|
+
'root',
|
|
176
|
+
'scope',
|
|
177
|
+
'state',
|
|
178
|
+
'target',
|
|
179
|
+
'target-within',
|
|
180
|
+
'valid',
|
|
181
|
+
'visited',
|
|
182
|
+
'where'
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
export const KEYS_PS_CLASS_UNSUPPORTED = new Set([
|
|
186
|
+
'autofill',
|
|
187
|
+
'blank',
|
|
188
|
+
'buffering',
|
|
189
|
+
'contains',
|
|
190
|
+
'current',
|
|
191
|
+
'fullscreen',
|
|
192
|
+
'future',
|
|
193
|
+
'has-slotted',
|
|
194
|
+
'heading',
|
|
195
|
+
'modal',
|
|
196
|
+
'muted',
|
|
197
|
+
'nth-col',
|
|
198
|
+
'nth-last-col',
|
|
199
|
+
'past',
|
|
200
|
+
'paused',
|
|
201
|
+
'picture-in-picture',
|
|
202
|
+
'playing',
|
|
203
|
+
'popover-open',
|
|
204
|
+
'seeking',
|
|
205
|
+
'stalled',
|
|
206
|
+
'user-invalid',
|
|
207
|
+
'user-valid',
|
|
208
|
+
'volume-locked',
|
|
209
|
+
'-webkit-autofill'
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
export const KEYS_PS_ELEMENT_UNSUPPORTED = new Set([
|
|
213
|
+
'after',
|
|
214
|
+
'backdrop',
|
|
215
|
+
'before',
|
|
216
|
+
'cue',
|
|
217
|
+
'cue-region',
|
|
218
|
+
'file-selector-button',
|
|
219
|
+
'first-letter',
|
|
220
|
+
'first-line',
|
|
221
|
+
'marker',
|
|
222
|
+
'part',
|
|
223
|
+
'placeholder',
|
|
224
|
+
'selection',
|
|
225
|
+
'slotted',
|
|
226
|
+
'target-text'
|
|
227
|
+
]);
|
package/src/js/finder.js
CHANGED
|
@@ -13,16 +13,15 @@ import {
|
|
|
13
13
|
matchTypeSelector
|
|
14
14
|
} from './matcher.js';
|
|
15
15
|
import {
|
|
16
|
-
findAST,
|
|
17
16
|
generateCSS,
|
|
18
17
|
parseSelector,
|
|
19
18
|
sortAST,
|
|
20
19
|
unescapeSelector,
|
|
21
20
|
walkAST
|
|
22
21
|
} from './parser.js';
|
|
22
|
+
import { createHasValidator, isInvalidCombinator } from './selector.js';
|
|
23
23
|
import {
|
|
24
24
|
filterNodesByAnB,
|
|
25
|
-
findLogicalWithNestedHas,
|
|
26
25
|
generateException,
|
|
27
26
|
isCustomElement,
|
|
28
27
|
isFocusVisible,
|
|
@@ -310,18 +309,18 @@ export class Finder {
|
|
|
310
309
|
for (let i = 0; i < l; i++) {
|
|
311
310
|
const items = [...branches[i]];
|
|
312
311
|
const branch = [];
|
|
312
|
+
let prevType = null;
|
|
313
313
|
let item = items.shift();
|
|
314
|
-
if (item
|
|
314
|
+
if (item) {
|
|
315
315
|
const leaves = new Set();
|
|
316
316
|
while (item) {
|
|
317
|
+
const isLast = items.length === 0;
|
|
318
|
+
if (isInvalidCombinator(item.type, prevType, isLast)) {
|
|
319
|
+
const msg = `Invalid selector ${selector}`;
|
|
320
|
+
this.onError(generateException(msg, SYNTAX_ERR, this.#window));
|
|
321
|
+
return { ast: [], descendant: false, invalidate: false };
|
|
322
|
+
}
|
|
317
323
|
if (item.type === COMBINATOR) {
|
|
318
|
-
const [nextItem] = items;
|
|
319
|
-
if (!nextItem || nextItem.type === COMBINATOR) {
|
|
320
|
-
const msg = `Invalid selector ${selector}`;
|
|
321
|
-
this.onError(generateException(msg, SYNTAX_ERR, this.#window));
|
|
322
|
-
// Stop processing on invalid selector.
|
|
323
|
-
return { ast: [], descendant: false, invalidate: false };
|
|
324
|
-
}
|
|
325
324
|
if (item.name === ' ' || item.name === '>') {
|
|
326
325
|
descendant = true;
|
|
327
326
|
}
|
|
@@ -339,7 +338,8 @@ export class Finder {
|
|
|
339
338
|
}
|
|
340
339
|
leaves.add(item);
|
|
341
340
|
}
|
|
342
|
-
|
|
341
|
+
prevType = item.type;
|
|
342
|
+
if (!isLast) {
|
|
343
343
|
item = items.shift();
|
|
344
344
|
} else {
|
|
345
345
|
branch.push({ combo: null, leaves: sortAST(leaves) });
|
|
@@ -384,16 +384,22 @@ export class Finder {
|
|
|
384
384
|
}
|
|
385
385
|
} else {
|
|
386
386
|
this.#selectorAST = parseSelector(selector);
|
|
387
|
-
const { branches, info } = walkAST(
|
|
387
|
+
const { branches, info } = walkAST(
|
|
388
|
+
this.#selectorAST,
|
|
389
|
+
true,
|
|
390
|
+
createHasValidator(this.#window)
|
|
391
|
+
);
|
|
388
392
|
const {
|
|
389
393
|
hasHasPseudoFunc,
|
|
390
394
|
hasLogicalPseudoFunc,
|
|
391
395
|
hasNthChildOfSelector,
|
|
392
|
-
hasStatePseudoClass
|
|
396
|
+
hasStatePseudoClass,
|
|
397
|
+
hasUnsupportedPseudoClass
|
|
393
398
|
} = info;
|
|
394
399
|
this.#invalidate =
|
|
395
400
|
hasHasPseudoFunc ||
|
|
396
401
|
hasStatePseudoClass ||
|
|
402
|
+
hasUnsupportedPseudoClass ||
|
|
397
403
|
!!(hasLogicalPseudoFunc && hasNthChildOfSelector);
|
|
398
404
|
const processed = this._processSelectorBranches(branches, selector);
|
|
399
405
|
ast = processed.ast;
|
|
@@ -825,29 +831,6 @@ export class Finder {
|
|
|
825
831
|
} else {
|
|
826
832
|
const { branches } = walkAST(ast);
|
|
827
833
|
if (astName === 'has') {
|
|
828
|
-
// Check for nested :has().
|
|
829
|
-
let forgiven = false;
|
|
830
|
-
const l = astChildren.length;
|
|
831
|
-
for (let i = 0; i < l; i++) {
|
|
832
|
-
const child = astChildren[i];
|
|
833
|
-
const item = findAST(child, findLogicalWithNestedHas);
|
|
834
|
-
if (item) {
|
|
835
|
-
const itemName = item.name;
|
|
836
|
-
if (itemName === 'is' || itemName === 'where') {
|
|
837
|
-
forgiven = true;
|
|
838
|
-
break;
|
|
839
|
-
} else {
|
|
840
|
-
const css = generateCSS(ast);
|
|
841
|
-
const msg = `Invalid selector ${css}`;
|
|
842
|
-
return this.onError(
|
|
843
|
-
generateException(msg, SYNTAX_ERR, this.#window)
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
if (forgiven) {
|
|
849
|
-
return matched;
|
|
850
|
-
}
|
|
851
834
|
astData = {
|
|
852
835
|
astName,
|
|
853
836
|
branches
|
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,333 @@
|
|
|
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
|
+
DESCEND,
|
|
15
|
+
HAS_COMPOUND,
|
|
16
|
+
KEYS_LOGICAL,
|
|
17
|
+
KEYS_PS_CLASS_SUPPORTED,
|
|
18
|
+
LOGIC_COMPLEX,
|
|
19
|
+
LOGIC_COMPOUND,
|
|
20
|
+
N_TH,
|
|
21
|
+
PSEUDO_CLASS,
|
|
22
|
+
PS_CLASS_SELECTOR,
|
|
23
|
+
PS_ELEMENT_SELECTOR,
|
|
24
|
+
SELECTOR,
|
|
25
|
+
SYNTAX_ERR,
|
|
26
|
+
TAG_TYPE,
|
|
27
|
+
TARGET_ALL,
|
|
28
|
+
TARGET_FIRST
|
|
29
|
+
} from './constant.js';
|
|
30
|
+
|
|
31
|
+
/* 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
|
+
const REG_EXCLUDE_BASIC =
|
|
35
|
+
/[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
|
|
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
|
+
const REG_LOGIC_COMPLEX = new RegExp(
|
|
39
|
+
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
|
|
40
|
+
);
|
|
41
|
+
const REG_LOGIC_COMPOUND = new RegExp(
|
|
42
|
+
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
|
|
43
|
+
);
|
|
44
|
+
const REG_LOGIC_HAS_COMPOUND = new RegExp(
|
|
45
|
+
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
|
|
46
|
+
);
|
|
47
|
+
const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
|
|
48
|
+
const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
|
|
49
|
+
const REG_COMBO = new RegExp(COMBO);
|
|
50
|
+
const REG_ID = /#(\D[^#.*]+)/g;
|
|
51
|
+
const REG_CLASS = /\.(\D[^#.*]+)/g;
|
|
52
|
+
const REG_TAG = /^([^#.]+)/;
|
|
53
|
+
const REG_INVALID_SYNTAX =
|
|
54
|
+
/[+~>]\s*[+~>]|^\s*[+~>]|[+~>]\s*$|^\s*,|,\s*,|,\s*$/;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find a nested :has() pseudo-class.
|
|
58
|
+
* @param {object} leaf - The AST leaf to check.
|
|
59
|
+
* @returns {?object} The leaf if it's :has, otherwise null.
|
|
60
|
+
*/
|
|
61
|
+
export const findNestedHas = leaf => leaf.name === 'has';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Find a logical pseudo-class that contains a nested :has().
|
|
65
|
+
* @param {object} leaf - The AST leaf to check.
|
|
66
|
+
* @returns {?object} The leaf if it matches, otherwise null.
|
|
67
|
+
*/
|
|
68
|
+
export const findLogicalWithNestedHas = leaf => {
|
|
69
|
+
if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
|
|
70
|
+
return leaf;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validates nesting restrictions within :has() arguments.
|
|
77
|
+
* @param {Array<object>} astChildren - The AST nodes representing the :has() arguments.
|
|
78
|
+
* @returns {boolean} False if there's an invalid nesting constraint violation.
|
|
79
|
+
*/
|
|
80
|
+
export const validateHasNesting = astChildren => {
|
|
81
|
+
const l = astChildren.length;
|
|
82
|
+
for (let i = 0; i < l; i++) {
|
|
83
|
+
const item = cssTree.find(astChildren[i], findLogicalWithNestedHas);
|
|
84
|
+
if (item) {
|
|
85
|
+
// If nested :has() is wrapped inside :is() or :where(), it is forgiven.
|
|
86
|
+
if (item.name !== 'is' && item.name !== 'where') {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a callback function to validate :has() nesting during AST walk.
|
|
96
|
+
* @param {object} globalObj - The global window object.
|
|
97
|
+
* @returns {function(object): void} The callback function for walkAST.
|
|
98
|
+
*/
|
|
99
|
+
export const createHasValidator = globalObj => node => {
|
|
100
|
+
if (
|
|
101
|
+
node.type === PS_CLASS_SELECTOR &&
|
|
102
|
+
node.name.toLowerCase() === 'has' &&
|
|
103
|
+
!validateHasNesting(Array.from(node.children || []))
|
|
104
|
+
) {
|
|
105
|
+
const css = cssTree.generate(node);
|
|
106
|
+
throw generateException(
|
|
107
|
+
`Disallowed nested :has() pseudo-class: ${css}`,
|
|
108
|
+
SYNTAX_ERR,
|
|
109
|
+
globalObj
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a combinator node is invalid (leading, trailing, or consecutive).
|
|
116
|
+
* @param {string} type - The current node type.
|
|
117
|
+
* @param {string|null} prevType - The previous node type.
|
|
118
|
+
* @param {boolean} isLast - Whether the current node is the last in the list.
|
|
119
|
+
* @returns {boolean} True if the combinator is invalid.
|
|
120
|
+
*/
|
|
121
|
+
export const isInvalidCombinator = (type, prevType, isLast) =>
|
|
122
|
+
type === COMBINATOR &&
|
|
123
|
+
(prevType === null || prevType === COMBINATOR || isLast);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Checks if a given AST is supported by the DOMSelector engine.
|
|
127
|
+
* @param {object} ast - The AST to validate.
|
|
128
|
+
* @returns {boolean} True if the selector is fully supported.
|
|
129
|
+
*/
|
|
130
|
+
export const isSupportedAST = ast => {
|
|
131
|
+
let isSupported = true;
|
|
132
|
+
const walk = (
|
|
133
|
+
node,
|
|
134
|
+
context = { insideHas: false, insideForgiving: false }
|
|
135
|
+
) => {
|
|
136
|
+
if (!isSupported || !node) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const nextContext = { ...context };
|
|
140
|
+
if (node.type === PS_ELEMENT_SELECTOR) {
|
|
141
|
+
isSupported = false;
|
|
142
|
+
return;
|
|
143
|
+
} else if (node.type === PS_CLASS_SELECTOR) {
|
|
144
|
+
let name = node.name;
|
|
145
|
+
if (name && typeof name === 'string') {
|
|
146
|
+
name = name.toLowerCase();
|
|
147
|
+
}
|
|
148
|
+
if (!KEYS_PS_CLASS_SUPPORTED.has(name)) {
|
|
149
|
+
isSupported = false;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (name === 'has') {
|
|
153
|
+
if (context.insideHas && !context.insideForgiving) {
|
|
154
|
+
isSupported = false;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
nextContext.insideHas = true;
|
|
158
|
+
} else if (name === 'is' || name === 'where') {
|
|
159
|
+
nextContext.insideForgiving = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (node.children) {
|
|
163
|
+
let prevType = null;
|
|
164
|
+
if (node.children.head !== undefined) {
|
|
165
|
+
let current = node.children.head;
|
|
166
|
+
while (current) {
|
|
167
|
+
if (!current.data) {
|
|
168
|
+
current = current.next;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const childType = current.data.type;
|
|
172
|
+
if (
|
|
173
|
+
node.type === SELECTOR &&
|
|
174
|
+
isInvalidCombinator(childType, prevType, !current.next)
|
|
175
|
+
) {
|
|
176
|
+
if (!(prevType === null && context.insideHas)) {
|
|
177
|
+
isSupported = false;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
prevType = childType;
|
|
182
|
+
walk(current.data, nextContext);
|
|
183
|
+
if (!isSupported) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
current = current.next;
|
|
187
|
+
}
|
|
188
|
+
} else if (Array.isArray(node.children)) {
|
|
189
|
+
const l = node.children.length;
|
|
190
|
+
for (let i = 0; i < l; i++) {
|
|
191
|
+
const child = node.children[i];
|
|
192
|
+
if (!child) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const childType = child.type;
|
|
196
|
+
if (
|
|
197
|
+
node.type === SELECTOR &&
|
|
198
|
+
isInvalidCombinator(childType, prevType, i === l - 1)
|
|
199
|
+
) {
|
|
200
|
+
if (!(prevType === null && context.insideHas)) {
|
|
201
|
+
isSupported = false;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
prevType = childType;
|
|
206
|
+
walk(child, nextContext);
|
|
207
|
+
if (!isSupported) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (node.selector) {
|
|
214
|
+
walk(node.selector, nextContext);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
walk(ast);
|
|
218
|
+
return isSupported;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extracts the rightmost subject keys (id, class, tag) from a selector.
|
|
223
|
+
* @param {string} selector - The CSS selector string to parse.
|
|
224
|
+
* @param {boolean} caseSensitive - True if the tag should be case-sensitive.
|
|
225
|
+
* @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
|
|
226
|
+
*/
|
|
227
|
+
export const extractSubjectsRegExp = (selector, caseSensitive) => {
|
|
228
|
+
const subjects = [];
|
|
229
|
+
const groups = selector.split(',');
|
|
230
|
+
for (let i = 0; i < groups.length; i++) {
|
|
231
|
+
const group = groups[i].trim();
|
|
232
|
+
if (!group) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const compounds = group.split(REG_COMBO);
|
|
236
|
+
const rightmost = compounds[compounds.length - 1];
|
|
237
|
+
let idKey = null;
|
|
238
|
+
let classKey = null;
|
|
239
|
+
let tagKey = null;
|
|
240
|
+
if (rightmost) {
|
|
241
|
+
const idMatch = rightmost.match(REG_ID);
|
|
242
|
+
if (idMatch) {
|
|
243
|
+
idKey = idMatch[idMatch.length - 1].slice(1);
|
|
244
|
+
}
|
|
245
|
+
const classMatch = rightmost.match(REG_CLASS);
|
|
246
|
+
if (classMatch) {
|
|
247
|
+
classKey = classMatch[classMatch.length - 1].slice(1);
|
|
248
|
+
}
|
|
249
|
+
const tagMatch = rightmost.match(REG_TAG);
|
|
250
|
+
if (tagMatch) {
|
|
251
|
+
const tag = tagMatch[1];
|
|
252
|
+
if (tag !== '*') {
|
|
253
|
+
tagKey = caseSensitive ? tag : tag.toLowerCase();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
subjects.push({ id: idKey, className: classKey, tag: tagKey });
|
|
258
|
+
}
|
|
259
|
+
return subjects;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Filter a selector for use with nwsapi.
|
|
264
|
+
* @param {string} selector - The selector string.
|
|
265
|
+
* @param {string} target - The target type.
|
|
266
|
+
* @returns {boolean} - True if the selector is valid for nwsapi.
|
|
267
|
+
*/
|
|
268
|
+
export const filterSelector = (selector, target) => {
|
|
269
|
+
const isQuerySelectorAll = target === TARGET_ALL;
|
|
270
|
+
// Basic validation and fast-fail for null/undefined/non-string values.
|
|
271
|
+
if (
|
|
272
|
+
!selector ||
|
|
273
|
+
typeof selector !== 'string' ||
|
|
274
|
+
/null|undefined/.test(selector)
|
|
275
|
+
) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
// Validate syntax.
|
|
279
|
+
if (REG_INVALID_SYNTAX.test(selector)) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
// Exclude various complex or unsupported selectors early.
|
|
283
|
+
// i.e. non-ASCII, escaped selectors, namespaced selectors, pseudo-elements.
|
|
284
|
+
if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
// Validate attribute selector integrity.
|
|
288
|
+
if (selector.includes('[')) {
|
|
289
|
+
const index = selector.lastIndexOf('[');
|
|
290
|
+
if (selector.indexOf(']', index) === -1) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
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
|
+
// Logic for pseudo-classes.
|
|
302
|
+
if (selector.includes(':')) {
|
|
303
|
+
// Exclude descendant combinators in logical selectors for querySelectorAll.
|
|
304
|
+
if (isQuerySelectorAll && REG_DESCEND.test(selector)) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
// Determine if the selector has complex logical structures.
|
|
308
|
+
const isComplex = isQuerySelectorAll ? false : REG_COMPLEX.test(selector);
|
|
309
|
+
// Handle :has() specifically.
|
|
310
|
+
if (selector.includes(':has(')) {
|
|
311
|
+
if (isQuerySelectorAll) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
return REG_END_WITH_HAS.test(selector);
|
|
318
|
+
}
|
|
319
|
+
// Handle :is() and :not().
|
|
320
|
+
if (/(?:is|not)\(/.test(selector)) {
|
|
321
|
+
if (isComplex) {
|
|
322
|
+
return !REG_LOGIC_COMPLEX.test(selector);
|
|
323
|
+
} else {
|
|
324
|
+
return !REG_LOGIC_COMPOUND.test(selector);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Default check for other pseudo-classes against known list.
|
|
328
|
+
if (REG_WO_LOGICAL.test(selector)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
333
|
+
};
|