@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/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
- getType,
16
- extractSubjectsRegExp
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
- * Clears the internal cache of finder results.
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 (e) {
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
- if (!node?.nodeType) {
117
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
118
- return this.#finder.onError(e, opt);
119
- } else if (node.nodeType !== ELEMENT_NODE) {
120
- const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
121
- return this.#finder.onError(e, opt);
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
- opt.check = true;
163
- opt.noexcept = true;
164
- opt.warn = false;
165
- 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);
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
- if (!node?.nodeType) {
177
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
178
- return this.#finder.onError(e, opt);
179
- } else if (node.nodeType !== ELEMENT_NODE) {
180
- const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
181
- return this.#finder.onError(e, opt);
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
- res = nodes.size;
278
+ return nodes.size > 0;
212
279
  } catch (e) {
213
280
  this.#finder.onError(e, opt);
214
281
  }
215
- return !!res;
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
- if (!node?.nodeType) {
227
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
228
- return this.#finder.onError(e, opt);
229
- } else if (node.nodeType !== ELEMENT_NODE) {
230
- const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
231
- return this.#finder.onError(e, opt);
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
- if (!node?.nodeType) {
286
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
287
- return this.#finder.onError(e, opt);
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
- document === this.#document &&
293
- document.contentType === 'text/html' &&
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
- [res] = [...nodes];
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
- if (!node?.nodeType) {
337
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
338
- return this.#finder.onError(e, opt);
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
- document === this.#document &&
344
- document.contentType === 'text/html' &&
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);
@@ -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
- // 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})+)`;
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
+ ]);