@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 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 cache of finder results to free up memory.
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`<br>`E:closed` | Partially supported | Matching with `<select>`, e.g. `select:open`, is not supported. |
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.2",
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.1",
35
+ "@types/node": "^25.9.3",
36
36
  "c8": "^11.0.0",
37
37
  "chai": "^6.2.2",
38
- "commander": "^14.0.3",
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.0",
42
- "eslint-plugin-prettier": "^5.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": "^64.0.0",
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.5",
48
+ "mocha": "^11.7.6",
49
49
  "neostandard": "^0.13.0",
50
- "prettier": "^3.8.3",
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.0.2"
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
- * Clears the internal cache of finder results.
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
- opt.check = true;
205
- opt.noexcept = true;
206
- opt.warn = false;
207
- return this.#finder.setup(selector, node, opt).find(TARGET_SELF);
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
- res = nodes.size;
278
+ return nodes.size > 0;
251
279
  } catch (e) {
252
280
  this.#finder.onError(e, opt);
253
281
  }
254
- return !!res;
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
- document === this.#document &&
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
- [res] = [...nodes];
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
- document === this.#document &&
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);
@@ -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
- // SUB_TYPE: attr, id, class, pseudo-class, note that [foo|=bar] is excluded
74
- export const SUB_TYPE = '\\[[^|\\]]+\\]|[#.:][\\w-]+';
75
- export const SUB_TYPE_WO_PSEUDO = '\\[[^|\\]]+\\]|[#.][\\w-]+';
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 TAG_TYPE = '\\*|[A-Za-z][\\w-]*';
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})+)`;