@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/README.md
CHANGED
|
@@ -119,10 +119,11 @@ See the table below for the full list of supported selectors.
|
|
|
119
119
|
* `selector` **{string}** CSS selector.
|
|
120
120
|
* **Returns** **{boolean}** `true` if the selector is supported, `false` otherwise.
|
|
121
121
|
|
|
122
|
-
### `clear()`
|
|
122
|
+
### `clear(clearAll?)`
|
|
123
123
|
|
|
124
|
-
Clears the internal
|
|
124
|
+
Clears the internal caches to free up memory.
|
|
125
125
|
|
|
126
|
+
* `clearAll` **{boolean}?** Whether to clear all caches. If false, only cached matching results are cleared. Defaults to `false`.
|
|
126
127
|
* **Returns** **{void}**
|
|
127
128
|
|
|
128
129
|
## Supported CSS selectors
|
|
@@ -162,7 +163,7 @@ Clears the internal cache of finder results to free up memory.
|
|
|
162
163
|
| `E:visited` | ✓ | Returns `false` or `null` to prevent fingerprinting. |
|
|
163
164
|
| `E:local-link` | ✓ | |
|
|
164
165
|
| `E:target` | ✓ | |
|
|
165
|
-
| `E:target-within` |
|
|
166
|
+
| `E:target-within` | | Removed from spec. |
|
|
166
167
|
| `E:scope` | ✓ | |
|
|
167
168
|
| `E:hover` | ✓ | |
|
|
168
169
|
| `E:active` | ✓ | |
|
|
@@ -173,7 +174,7 @@ Clears the internal cache of finder results to free up memory.
|
|
|
173
174
|
| `E:current(s)` | Unsupported | |
|
|
174
175
|
| `E:past` | Unsupported | |
|
|
175
176
|
| `E:future` | Unsupported | |
|
|
176
|
-
| `E:open
|
|
177
|
+
| `E:open` | Partially supported | Matching with `<select>`, e.g. `select:open`, or `<input type="color">` are not supported. |
|
|
177
178
|
| `E:popover-open` | Unsupported | |
|
|
178
179
|
| `E:enabled`<br>`E:disabled` | ✓ | |
|
|
179
180
|
| `E:read-write`<br>`E:read-only` | ✓ | |
|
package/package.json
CHANGED
|
@@ -25,29 +25,29 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@asamuzakjp/generational-cache": "^2.0.
|
|
28
|
+
"@asamuzakjp/generational-cache": "^2.0.4",
|
|
29
29
|
"bidi-js": "^1.0.3",
|
|
30
30
|
"css-tree": "^3.2.1",
|
|
31
31
|
"is-potential-custom-element-name": "^1.0.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/css-tree": "^2.3.11",
|
|
35
|
-
"@types/node": "^25.9.
|
|
35
|
+
"@types/node": "^25.9.3",
|
|
36
36
|
"c8": "^11.0.0",
|
|
37
37
|
"chai": "^6.2.2",
|
|
38
|
-
"commander": "^
|
|
38
|
+
"commander": "^15.0.0",
|
|
39
39
|
"eslint": "^9.39.4",
|
|
40
40
|
"eslint-config-prettier": "^10.1.8",
|
|
41
|
-
"eslint-plugin-jsdoc": "^63.0.
|
|
42
|
-
"eslint-plugin-prettier": "^5.5.
|
|
41
|
+
"eslint-plugin-jsdoc": "^63.0.2",
|
|
42
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
43
43
|
"eslint-plugin-regexp": "^3.1.0",
|
|
44
|
-
"eslint-plugin-unicorn": "^
|
|
44
|
+
"eslint-plugin-unicorn": "^65.0.1",
|
|
45
45
|
"globals": "^17.6.0",
|
|
46
46
|
"jsdom": "^29.1.1",
|
|
47
47
|
"mitata": "^1.0.34",
|
|
48
|
-
"mocha": "^11.7.
|
|
48
|
+
"mocha": "^11.7.6",
|
|
49
49
|
"neostandard": "^0.13.0",
|
|
50
|
-
"prettier": "^3.8.
|
|
50
|
+
"prettier": "^3.8.4",
|
|
51
51
|
"sinon": "^22.0.0",
|
|
52
52
|
"tinybench": "^6.0.2",
|
|
53
53
|
"typescript": "^6.0.3",
|
|
@@ -70,6 +70,12 @@
|
|
|
70
70
|
"bench": "node benchmark/bench.js",
|
|
71
71
|
"bench:cache": "node benchmark/bench-cache.js",
|
|
72
72
|
"bench:nwsapi": "node benchmark/bench-nwsapi.js",
|
|
73
|
+
"bench:ps-form": "node benchmark/bench-default.js && node benchmark/bench-indeterminate.js && node benchmark/bench-valid-invalid.js",
|
|
74
|
+
"bench:ps-has": "node benchmark/bench-has.js",
|
|
75
|
+
"bench:ps-nth": "node benchmark/bench-nth-child.js && node benchmark/bench-nth-of-type.js",
|
|
76
|
+
"bench:ps-traverse": "node benchmark/bench-dir.js && node benchmark/bench-lang.js",
|
|
77
|
+
"bench:ts": "node benchmark/bench-testing-library.js",
|
|
78
|
+
"bench:universal": "node benchmark/bench-universal.js",
|
|
73
79
|
"build": "npm run tsc && npm run lint && npm test",
|
|
74
80
|
"lint": "eslint --fix .",
|
|
75
81
|
"test": "c8 --reporter=text mocha --exit test/**/*.test.js",
|
|
@@ -80,5 +86,5 @@
|
|
|
80
86
|
"engines": {
|
|
81
87
|
"node": "^22.13.0 || >=24.0.0"
|
|
82
88
|
},
|
|
83
|
-
"version": "8.
|
|
89
|
+
"version": "8.1.2"
|
|
84
90
|
}
|
package/src/index.js
CHANGED
|
@@ -15,12 +15,15 @@ import {
|
|
|
15
15
|
filterSelector,
|
|
16
16
|
isSupportedAST
|
|
17
17
|
} from './js/selector.js';
|
|
18
|
-
import { getType } from './js/utility.js';
|
|
18
|
+
import { collectAllDescendants, getType } from './js/utility.js';
|
|
19
19
|
|
|
20
20
|
/* constants */
|
|
21
21
|
import {
|
|
22
|
+
ATTR_TYPE,
|
|
23
|
+
COMBO,
|
|
22
24
|
DOCUMENT_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,7 +66,9 @@ 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
|
}
|
|
@@ -82,10 +91,26 @@ export class DOMSelector {
|
|
|
82
91
|
};
|
|
83
92
|
|
|
84
93
|
/**
|
|
85
|
-
*
|
|
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.
|
|
86
108
|
* @returns {void}
|
|
87
109
|
*/
|
|
88
|
-
clear = () => {
|
|
110
|
+
clear = (clearAll = false) => {
|
|
111
|
+
if (clearAll) {
|
|
112
|
+
this.#cache.clear();
|
|
113
|
+
}
|
|
89
114
|
this.#finder.clearResults(true);
|
|
90
115
|
};
|
|
91
116
|
|
|
@@ -162,13 +187,16 @@ export class DOMSelector {
|
|
|
162
187
|
if (error) {
|
|
163
188
|
return this.#finder.onError(error, opt);
|
|
164
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
|
+
};
|
|
197
|
+
}
|
|
165
198
|
const document = node.ownerDocument;
|
|
166
|
-
if (
|
|
167
|
-
document === this.#document &&
|
|
168
|
-
document.contentType === 'text/html' &&
|
|
169
|
-
document.documentElement &&
|
|
170
|
-
node.parentNode
|
|
171
|
-
) {
|
|
199
|
+
if (node.parentNode && this.#canUseNwsapi(document)) {
|
|
172
200
|
const cacheKey = `check_${selector}`;
|
|
173
201
|
let filterMatches = this.#cache.get(cacheKey);
|
|
174
202
|
if (filterMatches === undefined) {
|
|
@@ -201,10 +229,13 @@ export class DOMSelector {
|
|
|
201
229
|
if (this.#idlUtils) {
|
|
202
230
|
node = this.#idlUtils.wrapperForImpl(node);
|
|
203
231
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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);
|
|
208
239
|
};
|
|
209
240
|
|
|
210
241
|
/**
|
|
@@ -219,13 +250,11 @@ export class DOMSelector {
|
|
|
219
250
|
if (error) {
|
|
220
251
|
return this.#finder.onError(error, opt);
|
|
221
252
|
}
|
|
253
|
+
if (REG_UNIVERSAL.test(selector)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
222
256
|
const document = node.ownerDocument;
|
|
223
|
-
if (
|
|
224
|
-
document === this.#document &&
|
|
225
|
-
document.contentType === 'text/html' &&
|
|
226
|
-
document.documentElement &&
|
|
227
|
-
node.parentNode
|
|
228
|
-
) {
|
|
257
|
+
if (node.parentNode && this.#canUseNwsapi(document)) {
|
|
229
258
|
const cacheKey = `matches_${selector}`;
|
|
230
259
|
let filterMatches = this.#cache.get(cacheKey);
|
|
231
260
|
if (filterMatches === undefined) {
|
|
@@ -241,17 +270,16 @@ export class DOMSelector {
|
|
|
241
270
|
}
|
|
242
271
|
}
|
|
243
272
|
}
|
|
244
|
-
let res;
|
|
245
273
|
try {
|
|
246
274
|
if (this.#idlUtils) {
|
|
247
275
|
node = this.#idlUtils.wrapperForImpl(node);
|
|
248
276
|
}
|
|
249
277
|
const nodes = this.#finder.setup(selector, node, opt).find(TARGET_SELF);
|
|
250
|
-
|
|
278
|
+
return nodes.size > 0;
|
|
251
279
|
} catch (e) {
|
|
252
280
|
this.#finder.onError(e, opt);
|
|
253
281
|
}
|
|
254
|
-
return
|
|
282
|
+
return false;
|
|
255
283
|
};
|
|
256
284
|
|
|
257
285
|
/**
|
|
@@ -266,13 +294,11 @@ export class DOMSelector {
|
|
|
266
294
|
if (error) {
|
|
267
295
|
return this.#finder.onError(error, opt);
|
|
268
296
|
}
|
|
297
|
+
if (REG_UNIVERSAL.test(selector)) {
|
|
298
|
+
return node;
|
|
299
|
+
}
|
|
269
300
|
const document = node.ownerDocument;
|
|
270
|
-
if (
|
|
271
|
-
document === this.#document &&
|
|
272
|
-
document.contentType === 'text/html' &&
|
|
273
|
-
document.documentElement &&
|
|
274
|
-
node.parentNode
|
|
275
|
-
) {
|
|
301
|
+
if (node.parentNode && this.#canUseNwsapi(document)) {
|
|
276
302
|
const cacheKey = `closest_${selector}`;
|
|
277
303
|
let filterMatches = this.#cache.get(cacheKey);
|
|
278
304
|
if (filterMatches === undefined) {
|
|
@@ -322,13 +348,15 @@ export class DOMSelector {
|
|
|
322
348
|
if (error) {
|
|
323
349
|
return this.#finder.onError(error, opt);
|
|
324
350
|
}
|
|
351
|
+
if (REG_UNIVERSAL.test(selector)) {
|
|
352
|
+
return node.firstElementChild;
|
|
353
|
+
}
|
|
354
|
+
/*
|
|
325
355
|
const document =
|
|
326
356
|
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
|
|
327
357
|
if (
|
|
328
|
-
node === this.#document &&
|
|
329
|
-
|
|
330
|
-
document.contentType === 'text/html' &&
|
|
331
|
-
document.documentElement
|
|
358
|
+
(node === this.#document || REG_TEST_LIB.test(selector)) &&
|
|
359
|
+
this.#canUseNwsapi(document)
|
|
332
360
|
) {
|
|
333
361
|
const cacheKey = `querySelector_${selector}`;
|
|
334
362
|
let filterMatches = this.#cache.get(cacheKey);
|
|
@@ -345,6 +373,7 @@ export class DOMSelector {
|
|
|
345
373
|
}
|
|
346
374
|
}
|
|
347
375
|
}
|
|
376
|
+
*/
|
|
348
377
|
let res;
|
|
349
378
|
try {
|
|
350
379
|
if (this.#idlUtils) {
|
|
@@ -352,7 +381,7 @@ export class DOMSelector {
|
|
|
352
381
|
}
|
|
353
382
|
const nodes = this.#finder.setup(selector, node, opt).find(TARGET_FIRST);
|
|
354
383
|
if (nodes.size) {
|
|
355
|
-
|
|
384
|
+
res = nodes.values().next().value;
|
|
356
385
|
}
|
|
357
386
|
} catch (e) {
|
|
358
387
|
this.#finder.onError(e, opt);
|
|
@@ -375,11 +404,12 @@ export class DOMSelector {
|
|
|
375
404
|
}
|
|
376
405
|
const document =
|
|
377
406
|
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
|
|
407
|
+
if (document && REG_UNIVERSAL.test(selector)) {
|
|
408
|
+
return collectAllDescendants(node, document);
|
|
409
|
+
}
|
|
378
410
|
if (
|
|
379
|
-
node === this.#document &&
|
|
380
|
-
|
|
381
|
-
document.contentType === 'text/html' &&
|
|
382
|
-
document.documentElement
|
|
411
|
+
(node === this.#document || REG_TEST_LIB.test(selector)) &&
|
|
412
|
+
this.#canUseNwsapi(document)
|
|
383
413
|
) {
|
|
384
414
|
const cacheKey = `querySelectorAll_${selector}`;
|
|
385
415
|
let filterMatches = this.#cache.get(cacheKey);
|
package/src/js/constant.js
CHANGED
|
@@ -70,11 +70,14 @@ 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})+)`;
|