@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/index.js
CHANGED
|
@@ -11,16 +11,19 @@ import { Finder } from './js/finder.js';
|
|
|
11
11
|
import { Nwsapi } from './js/nwsapi.js';
|
|
12
12
|
import { extractSubjectsAst } from './js/parser.js';
|
|
13
13
|
import {
|
|
14
|
+
extractSubjectsRegExp,
|
|
14
15
|
filterSelector,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from './js/utility.js';
|
|
16
|
+
isSupportedAST
|
|
17
|
+
} from './js/selector.js';
|
|
18
|
+
import { collectAllDescendants, getType } from './js/utility.js';
|
|
18
19
|
|
|
19
20
|
/* constants */
|
|
20
21
|
import {
|
|
22
|
+
ATTR_TYPE,
|
|
23
|
+
COMBO,
|
|
21
24
|
DOCUMENT_NODE,
|
|
22
|
-
DOCUMENT_FRAGMENT_NODE,
|
|
23
25
|
ELEMENT_NODE,
|
|
26
|
+
TAG_TYPE_WO_UNIVERSAL,
|
|
24
27
|
TARGET_ALL,
|
|
25
28
|
TARGET_FIRST,
|
|
26
29
|
TARGET_LINEAL,
|
|
@@ -30,6 +33,10 @@ const CACHE_SIZE = 2048;
|
|
|
30
33
|
|
|
31
34
|
/* regexp */
|
|
32
35
|
const REG_SELECTOR = /[[\]():\\"'`]/;
|
|
36
|
+
const REG_TEST_LIB = new RegExp(
|
|
37
|
+
`^(?:${TAG_TYPE_WO_UNIVERSAL}|[*]?${ATTR_TYPE}(?:\\s*,\\s*${TAG_TYPE_WO_UNIVERSAL}${COMBO}${TAG_TYPE_WO_UNIVERSAL})?)$`
|
|
38
|
+
);
|
|
39
|
+
const REG_UNIVERSAL = /^(?:\*\|)?\*$/;
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
42
|
* @typedef {object} CheckResult
|
|
@@ -59,16 +66,51 @@ export class DOMSelector {
|
|
|
59
66
|
this.#window = window;
|
|
60
67
|
this.#document = document ?? window.document;
|
|
61
68
|
this.#idlUtils = idlUtils;
|
|
62
|
-
this.#cache = new GenerationalCache(cacheSize ?? CACHE_SIZE
|
|
69
|
+
this.#cache = new GenerationalCache(cacheSize ?? CACHE_SIZE, {
|
|
70
|
+
strictvalidate: false
|
|
71
|
+
});
|
|
63
72
|
this.#finder = new Finder(this.#window);
|
|
64
73
|
this.#nwsapi = new Nwsapi(this.#window, this.#document);
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
/**
|
|
68
|
-
*
|
|
77
|
+
* Validates a node and returns an Error if invalid.
|
|
78
|
+
* @private
|
|
79
|
+
* @param {Document|DocumentFragment|Element} node - The node to check.
|
|
80
|
+
* @param {boolean} [element] - `true` if the node must be an Element.
|
|
81
|
+
* @returns {TypeError|null} Returns a TypeError if invalid, otherwise null.
|
|
82
|
+
*/
|
|
83
|
+
#validateNodeType = (node, element = false) => {
|
|
84
|
+
if (!node?.nodeType) {
|
|
85
|
+
return new this.#window.TypeError(`Unexpected type ${getType(node)}`);
|
|
86
|
+
}
|
|
87
|
+
if (element && node.nodeType !== ELEMENT_NODE) {
|
|
88
|
+
return new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Determines whether Nwsapi can be used for the given document.
|
|
95
|
+
* @private
|
|
96
|
+
* @param {Document} doc - The document object to check.
|
|
97
|
+
* @returns {boolean} `true` if Nwsapi can be used, otherwise `false`.
|
|
98
|
+
*/
|
|
99
|
+
#canUseNwsapi = doc =>
|
|
100
|
+
doc === this.#document &&
|
|
101
|
+
doc.contentType === 'text/html' &&
|
|
102
|
+
doc.documentElement;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Clears the internal caches.
|
|
106
|
+
* @param {boolean} [clearAll] - Whether to clear all caches. If false,
|
|
107
|
+
* only cached matching results are cleared.
|
|
69
108
|
* @returns {void}
|
|
70
109
|
*/
|
|
71
|
-
clear = () => {
|
|
110
|
+
clear = (clearAll = false) => {
|
|
111
|
+
if (clearAll) {
|
|
112
|
+
this.#cache.clear();
|
|
113
|
+
}
|
|
72
114
|
this.#finder.clearResults(true);
|
|
73
115
|
};
|
|
74
116
|
|
|
@@ -94,7 +136,7 @@ export class DOMSelector {
|
|
|
94
136
|
try {
|
|
95
137
|
const ast = this.#finder.getAST(selector);
|
|
96
138
|
subjects = extractSubjectsAst(ast);
|
|
97
|
-
} catch
|
|
139
|
+
} catch {
|
|
98
140
|
// fall through
|
|
99
141
|
}
|
|
100
142
|
}
|
|
@@ -105,6 +147,34 @@ export class DOMSelector {
|
|
|
105
147
|
return subjects;
|
|
106
148
|
};
|
|
107
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Checks if the given CSS selector is supported by this engine.
|
|
152
|
+
* @param {string} selector - The CSS selector to check.
|
|
153
|
+
* @returns {boolean} `true` if the selector is supported, `false` otherwise.
|
|
154
|
+
*/
|
|
155
|
+
supports = selector => {
|
|
156
|
+
if (typeof selector !== 'string') {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const cacheKey = `supports_${selector}`;
|
|
160
|
+
let isSupported = this.#cache.get(cacheKey);
|
|
161
|
+
if (isSupported !== undefined) {
|
|
162
|
+
return isSupported;
|
|
163
|
+
}
|
|
164
|
+
if (filterSelector(selector, TARGET_SELF)) {
|
|
165
|
+
isSupported = true;
|
|
166
|
+
} else {
|
|
167
|
+
try {
|
|
168
|
+
const ast = this.#finder.getAST(selector);
|
|
169
|
+
isSupported = isSupportedAST(ast);
|
|
170
|
+
} catch {
|
|
171
|
+
isSupported = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.#cache.set(cacheKey, isSupported);
|
|
175
|
+
return isSupported;
|
|
176
|
+
};
|
|
177
|
+
|
|
108
178
|
/**
|
|
109
179
|
* Checks if an element matches a CSS selector.
|
|
110
180
|
* @param {string} selector - The CSS selector to check against.
|
|
@@ -113,20 +183,20 @@ export class DOMSelector {
|
|
|
113
183
|
* @returns {CheckResult} An object containing the check result.
|
|
114
184
|
*/
|
|
115
185
|
check = (selector, node, opt = {}) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return this.#finder.onError(
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
186
|
+
const error = this.#validateNodeType(node, true);
|
|
187
|
+
if (error) {
|
|
188
|
+
return this.#finder.onError(error, opt);
|
|
189
|
+
}
|
|
190
|
+
if (REG_UNIVERSAL.test(selector)) {
|
|
191
|
+
const ast = this.#finder.getAST(selector);
|
|
192
|
+
return {
|
|
193
|
+
ast,
|
|
194
|
+
match: true,
|
|
195
|
+
pseudoElement: null
|
|
196
|
+
};
|
|
122
197
|
}
|
|
123
198
|
const document = node.ownerDocument;
|
|
124
|
-
if (
|
|
125
|
-
document === this.#document &&
|
|
126
|
-
document.contentType === 'text/html' &&
|
|
127
|
-
document.documentElement &&
|
|
128
|
-
node.parentNode
|
|
129
|
-
) {
|
|
199
|
+
if (node.parentNode && this.#canUseNwsapi(document)) {
|
|
130
200
|
const cacheKey = `check_${selector}`;
|
|
131
201
|
let filterMatches = this.#cache.get(cacheKey);
|
|
132
202
|
if (filterMatches === undefined) {
|
|
@@ -159,10 +229,13 @@ export class DOMSelector {
|
|
|
159
229
|
if (this.#idlUtils) {
|
|
160
230
|
node = this.#idlUtils.wrapperForImpl(node);
|
|
161
231
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
232
|
+
const options = {
|
|
233
|
+
...opt,
|
|
234
|
+
check: true,
|
|
235
|
+
noexcept: true,
|
|
236
|
+
warn: false
|
|
237
|
+
};
|
|
238
|
+
return this.#finder.setup(selector, node, options).find(TARGET_SELF);
|
|
166
239
|
};
|
|
167
240
|
|
|
168
241
|
/**
|
|
@@ -173,20 +246,15 @@ export class DOMSelector {
|
|
|
173
246
|
* @returns {boolean} `true` if the element matches, or `false` otherwise.
|
|
174
247
|
*/
|
|
175
248
|
matches = (selector, node, opt = {}) => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return this.#finder.onError(
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return
|
|
249
|
+
const error = this.#validateNodeType(node, true);
|
|
250
|
+
if (error) {
|
|
251
|
+
return this.#finder.onError(error, opt);
|
|
252
|
+
}
|
|
253
|
+
if (REG_UNIVERSAL.test(selector)) {
|
|
254
|
+
return true;
|
|
182
255
|
}
|
|
183
256
|
const document = node.ownerDocument;
|
|
184
|
-
if (
|
|
185
|
-
document === this.#document &&
|
|
186
|
-
document.contentType === 'text/html' &&
|
|
187
|
-
document.documentElement &&
|
|
188
|
-
node.parentNode
|
|
189
|
-
) {
|
|
257
|
+
if (node.parentNode && this.#canUseNwsapi(document)) {
|
|
190
258
|
const cacheKey = `matches_${selector}`;
|
|
191
259
|
let filterMatches = this.#cache.get(cacheKey);
|
|
192
260
|
if (filterMatches === undefined) {
|
|
@@ -202,17 +270,16 @@ export class DOMSelector {
|
|
|
202
270
|
}
|
|
203
271
|
}
|
|
204
272
|
}
|
|
205
|
-
let res;
|
|
206
273
|
try {
|
|
207
274
|
if (this.#idlUtils) {
|
|
208
275
|
node = this.#idlUtils.wrapperForImpl(node);
|
|
209
276
|
}
|
|
210
277
|
const nodes = this.#finder.setup(selector, node, opt).find(TARGET_SELF);
|
|
211
|
-
|
|
278
|
+
return nodes.size > 0;
|
|
212
279
|
} catch (e) {
|
|
213
280
|
this.#finder.onError(e, opt);
|
|
214
281
|
}
|
|
215
|
-
return
|
|
282
|
+
return false;
|
|
216
283
|
};
|
|
217
284
|
|
|
218
285
|
/**
|
|
@@ -223,20 +290,15 @@ export class DOMSelector {
|
|
|
223
290
|
* @returns {?Element} The first matching ancestor element, or `null`.
|
|
224
291
|
*/
|
|
225
292
|
closest = (selector, node, opt = {}) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return this.#finder.onError(
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return
|
|
293
|
+
const error = this.#validateNodeType(node, true);
|
|
294
|
+
if (error) {
|
|
295
|
+
return this.#finder.onError(error, opt);
|
|
296
|
+
}
|
|
297
|
+
if (REG_UNIVERSAL.test(selector)) {
|
|
298
|
+
return node;
|
|
232
299
|
}
|
|
233
300
|
const document = node.ownerDocument;
|
|
234
|
-
if (
|
|
235
|
-
document === this.#document &&
|
|
236
|
-
document.contentType === 'text/html' &&
|
|
237
|
-
document.documentElement &&
|
|
238
|
-
node.parentNode
|
|
239
|
-
) {
|
|
301
|
+
if (node.parentNode && this.#canUseNwsapi(document)) {
|
|
240
302
|
const cacheKey = `closest_${selector}`;
|
|
241
303
|
let filterMatches = this.#cache.get(cacheKey);
|
|
242
304
|
if (filterMatches === undefined) {
|
|
@@ -282,17 +344,19 @@ export class DOMSelector {
|
|
|
282
344
|
* @returns {?Element} The first matching element, or `null`.
|
|
283
345
|
*/
|
|
284
346
|
querySelector = (selector, node, opt = {}) => {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return this.#finder.onError(
|
|
347
|
+
const error = this.#validateNodeType(node);
|
|
348
|
+
if (error) {
|
|
349
|
+
return this.#finder.onError(error, opt);
|
|
350
|
+
}
|
|
351
|
+
if (REG_UNIVERSAL.test(selector)) {
|
|
352
|
+
return node.firstElementChild;
|
|
288
353
|
}
|
|
354
|
+
/*
|
|
289
355
|
const document =
|
|
290
356
|
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
|
|
291
357
|
if (
|
|
292
|
-
|
|
293
|
-
document
|
|
294
|
-
document.documentElement &&
|
|
295
|
-
(node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
|
|
358
|
+
(node === this.#document || REG_TEST_LIB.test(selector)) &&
|
|
359
|
+
this.#canUseNwsapi(document)
|
|
296
360
|
) {
|
|
297
361
|
const cacheKey = `querySelector_${selector}`;
|
|
298
362
|
let filterMatches = this.#cache.get(cacheKey);
|
|
@@ -309,6 +373,7 @@ export class DOMSelector {
|
|
|
309
373
|
}
|
|
310
374
|
}
|
|
311
375
|
}
|
|
376
|
+
*/
|
|
312
377
|
let res;
|
|
313
378
|
try {
|
|
314
379
|
if (this.#idlUtils) {
|
|
@@ -316,7 +381,7 @@ export class DOMSelector {
|
|
|
316
381
|
}
|
|
317
382
|
const nodes = this.#finder.setup(selector, node, opt).find(TARGET_FIRST);
|
|
318
383
|
if (nodes.size) {
|
|
319
|
-
|
|
384
|
+
res = nodes.values().next().value;
|
|
320
385
|
}
|
|
321
386
|
} catch (e) {
|
|
322
387
|
this.#finder.onError(e, opt);
|
|
@@ -333,17 +398,18 @@ export class DOMSelector {
|
|
|
333
398
|
* @returns {Array<Element>} An array of elements, or an empty array.
|
|
334
399
|
*/
|
|
335
400
|
querySelectorAll = (selector, node, opt = {}) => {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return this.#finder.onError(
|
|
401
|
+
const error = this.#validateNodeType(node);
|
|
402
|
+
if (error) {
|
|
403
|
+
return this.#finder.onError(error, opt);
|
|
339
404
|
}
|
|
340
405
|
const document =
|
|
341
406
|
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
|
|
407
|
+
if (document && REG_UNIVERSAL.test(selector)) {
|
|
408
|
+
return collectAllDescendants(node, document);
|
|
409
|
+
}
|
|
342
410
|
if (
|
|
343
|
-
|
|
344
|
-
document
|
|
345
|
-
document.documentElement &&
|
|
346
|
-
(node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
|
|
411
|
+
(node === this.#document || REG_TEST_LIB.test(selector)) &&
|
|
412
|
+
this.#canUseNwsapi(document)
|
|
347
413
|
) {
|
|
348
414
|
const cacheKey = `querySelectorAll_${selector}`;
|
|
349
415
|
let filterMatches = this.#cache.get(cacheKey);
|
package/src/js/constant.js
CHANGED
|
@@ -70,14 +70,18 @@ export const SIBLING = '\\s?[+~]\\s?';
|
|
|
70
70
|
export const LOGIC_IS = `:is\\(\\s*[^)]+\\s*\\)`;
|
|
71
71
|
// N_TH: excludes An+B with selector list, e.g. :nth-child(2n+1 of .foo)
|
|
72
72
|
export const N_TH = `nth-(?:last-)?(?:child|of-type)\\(\\s*(?:even|odd|${ANB})\\s*\\)`;
|
|
73
|
-
//
|
|
74
|
-
export const
|
|
75
|
-
|
|
73
|
+
// ATTR_TYPE: excludes [foo|=bar]
|
|
74
|
+
export const ATTR_TYPE = '\\[[^|\\]]+\\]';
|
|
75
|
+
// SUB_TYPE: attr, id, class, pseudo-class
|
|
76
|
+
export const SUB_TYPE = `${ATTR_TYPE}|[#.:][\\w-]+`;
|
|
77
|
+
export const SUB_TYPE_WO_PSEUDO = `${ATTR_TYPE}|[#.][\\w-]+`;
|
|
76
78
|
// TAG_TYPE: *, tag
|
|
77
|
-
export const
|
|
79
|
+
export const TAG_TYPE_WO_UNIVERSAL = '[A-Za-z][\\w-]*';
|
|
80
|
+
export const TAG_TYPE = `\\*|${TAG_TYPE_WO_UNIVERSAL}`;
|
|
78
81
|
export const TAG_TYPE_I = '\\*|[A-Z][\\w-]*';
|
|
79
82
|
export const COMPOUND = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE})+)`;
|
|
80
83
|
export const COMPOUND_L = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE}|${LOGIC_IS})+)`;
|
|
84
|
+
export const COMPOUND_L_I = `(?:(?:${TAG_TYPE_I})?(?:${SUB_TYPE}|${LOGIC_IS})+)`;
|
|
81
85
|
export const COMPOUND_I = `(?:${TAG_TYPE_I}|(?:${TAG_TYPE_I})?(?:${SUB_TYPE})+)`;
|
|
82
86
|
export const COMPOUND_WO_PSEUDO = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE_WO_PSEUDO})+)`;
|
|
83
87
|
export const COMPLEX = `${COMPOUND}(?:${COMBO}${COMPOUND})*`;
|
|
@@ -127,3 +131,101 @@ export const INPUT_LTR = Object.freeze([
|
|
|
127
131
|
|
|
128
132
|
/* logical combination pseudo-classes */
|
|
129
133
|
export const KEYS_LOGICAL = new Set(['has', 'is', 'not', 'where']);
|
|
134
|
+
|
|
135
|
+
/* lists of supported / unsupported pseudo-classes and pseudo-elements */
|
|
136
|
+
export const KEYS_PS_CLASS_SUPPORTED = new Set([
|
|
137
|
+
'active',
|
|
138
|
+
'any-link',
|
|
139
|
+
'checked',
|
|
140
|
+
'closed',
|
|
141
|
+
'default',
|
|
142
|
+
'defined',
|
|
143
|
+
'dir',
|
|
144
|
+
'disabled',
|
|
145
|
+
'empty',
|
|
146
|
+
'enabled',
|
|
147
|
+
'first-child',
|
|
148
|
+
'first-of-type',
|
|
149
|
+
'focus',
|
|
150
|
+
'focus-visible',
|
|
151
|
+
'focus-within',
|
|
152
|
+
'has',
|
|
153
|
+
'host',
|
|
154
|
+
'host-context',
|
|
155
|
+
'hover',
|
|
156
|
+
'in-range',
|
|
157
|
+
'indeterminate',
|
|
158
|
+
'invalid',
|
|
159
|
+
'is',
|
|
160
|
+
'lang',
|
|
161
|
+
'last-child',
|
|
162
|
+
'last-of-type',
|
|
163
|
+
'link',
|
|
164
|
+
'local-link',
|
|
165
|
+
'not',
|
|
166
|
+
'nth-child',
|
|
167
|
+
'nth-last-child',
|
|
168
|
+
'nth-last-of-type',
|
|
169
|
+
'nth-of-type',
|
|
170
|
+
'only-child',
|
|
171
|
+
'only-of-type',
|
|
172
|
+
'open',
|
|
173
|
+
'optional',
|
|
174
|
+
'out-of-range',
|
|
175
|
+
'placeholder-shown',
|
|
176
|
+
'read-only',
|
|
177
|
+
'read-write',
|
|
178
|
+
'required',
|
|
179
|
+
'root',
|
|
180
|
+
'scope',
|
|
181
|
+
'state',
|
|
182
|
+
'target',
|
|
183
|
+
'target-within',
|
|
184
|
+
'valid',
|
|
185
|
+
'visited',
|
|
186
|
+
'where'
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
export const KEYS_PS_CLASS_UNSUPPORTED = new Set([
|
|
190
|
+
'autofill',
|
|
191
|
+
'blank',
|
|
192
|
+
'buffering',
|
|
193
|
+
'contains',
|
|
194
|
+
'current',
|
|
195
|
+
'fullscreen',
|
|
196
|
+
'future',
|
|
197
|
+
'has-slotted',
|
|
198
|
+
'heading',
|
|
199
|
+
'modal',
|
|
200
|
+
'muted',
|
|
201
|
+
'nth-col',
|
|
202
|
+
'nth-last-col',
|
|
203
|
+
'past',
|
|
204
|
+
'paused',
|
|
205
|
+
'picture-in-picture',
|
|
206
|
+
'playing',
|
|
207
|
+
'popover-open',
|
|
208
|
+
'seeking',
|
|
209
|
+
'stalled',
|
|
210
|
+
'user-invalid',
|
|
211
|
+
'user-valid',
|
|
212
|
+
'volume-locked',
|
|
213
|
+
'-webkit-autofill'
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
export const KEYS_PS_ELEMENT_UNSUPPORTED = new Set([
|
|
217
|
+
'after',
|
|
218
|
+
'backdrop',
|
|
219
|
+
'before',
|
|
220
|
+
'cue',
|
|
221
|
+
'cue-region',
|
|
222
|
+
'file-selector-button',
|
|
223
|
+
'first-letter',
|
|
224
|
+
'first-line',
|
|
225
|
+
'marker',
|
|
226
|
+
'part',
|
|
227
|
+
'placeholder',
|
|
228
|
+
'selection',
|
|
229
|
+
'slotted',
|
|
230
|
+
'target-text'
|
|
231
|
+
]);
|