@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/src/js/matcher.js CHANGED
@@ -116,37 +116,42 @@ export const matchPseudoElementSelector = (
116
116
  * Matches the :dir() pseudo-class against an element's directionality.
117
117
  * @param {object} ast - The AST object for the pseudo-class.
118
118
  * @param {object} node - The element node to match against.
119
+ * @param {WeakMap} [dirCache] - Cache for directionality.
119
120
  * @throws {TypeError} If the AST does not contain a valid direction value.
120
121
  * @returns {boolean} - True if the directionality matches, otherwise false.
121
122
  */
122
- export const matchDirectionPseudoClass = (ast, node) => {
123
+ export const matchDirectionPseudoClass = (
124
+ ast,
125
+ node,
126
+ dirCache = new WeakMap()
127
+ ) => {
123
128
  const { name } = ast;
124
- // The :dir() pseudo-class requires a direction argument (e.g., "ltr").
125
129
  if (!name) {
126
130
  const type = name === '' ? '(empty String)' : getType(name);
127
131
  throw new TypeError(`Unexpected ast type ${type}`);
128
132
  }
129
- // Get the computed directionality of the element.
130
- const dir = getDirectionality(node);
131
- // Compare the expected direction with the element's actual direction.
133
+ const dir = getDirectionality(node, dirCache);
132
134
  return name === dir;
133
135
  };
134
136
 
135
137
  /**
136
- * Matches the :lang() pseudo-class against an element's language.
137
- * @see https://datatracker.ietf.org/doc/html/rfc4647#section-3.3.1
138
- * @param {object} ast - The AST object for the pseudo-class.
138
+ * Matches the :lang() pseudo-class against an element's language attribute.
139
+ * @param {object} ast - The AST object for the pseudo-class child.
139
140
  * @param {object} node - The element node to match against.
140
- * @returns {boolean} - True if the language matches, otherwise false.
141
+ * @param {WeakMap} [langCache] - Cache for language attributes.
142
+ * @throws {TypeError} If the AST does not contain a valid language value.
143
+ * @returns {boolean} - True if the language attribute matches, otherwise false.
141
144
  */
142
- export const matchLanguagePseudoClass = (ast, node) => {
143
- // Get the effective language attribute for the current node.
144
- const elementLang = getLanguageAttribute(node);
145
- // If the element has no language, it cannot match a specific pattern.
145
+ export const matchLanguagePseudoClass = (
146
+ ast,
147
+ node,
148
+ langCache = new WeakMap()
149
+ ) => {
150
+ const elementLang = getLanguageAttribute(node, langCache);
146
151
  if (elementLang === null) {
147
152
  return false;
148
153
  }
149
- // Use cached regex.
154
+ // Use cached regex if available
150
155
  if (ast._langRegex !== undefined) {
151
156
  if (ast._langPattern === '*') {
152
157
  return elementLang !== '';
@@ -158,30 +163,24 @@ export const matchLanguagePseudoClass = (ast, node) => {
158
163
  }
159
164
  const { name, type, value } = ast;
160
165
  let langPattern;
161
- // Determine the language pattern from the AST.
162
166
  if (type === STRING && value) {
163
167
  langPattern = value;
164
168
  } else if (type === IDENT && name) {
165
169
  langPattern = unescapeSelector(name);
166
170
  }
167
- // Cache lang pattern.
168
171
  ast._langPattern = langPattern;
169
- // If no valid language pattern is provided, it cannot match.
170
172
  if (typeof langPattern !== 'string') {
171
173
  ast._langRegex = null;
172
174
  return false;
173
175
  }
174
- // Handle the universal selector '*' for :lang.
175
176
  if (langPattern === '*') {
176
177
  ast._langRegex = null;
177
178
  return elementLang !== '';
178
179
  }
179
- // Validate the provided language pattern structure.
180
180
  if (!REG_LANG_VALID.test(langPattern)) {
181
181
  ast._langRegex = null;
182
182
  return false;
183
183
  }
184
- // Build a regex for extended language range matching.
185
184
  let matcherRegex;
186
185
  if (langPattern.indexOf('-') > -1) {
187
186
  const [langMain, langSub, ...langRest] = langPattern.split('-');
@@ -199,8 +198,8 @@ export const matchLanguagePseudoClass = (ast, node) => {
199
198
  } else {
200
199
  matcherRegex = new RegExp(`^${langPattern}${LANG_PART}$`, 'i');
201
200
  }
201
+ // Store compiled regex in AST for subsequent matches
202
202
  ast._langRegex = matcherRegex;
203
- // Test the element's language against the constructed regex.
204
203
  return matcherRegex.test(elementLang);
205
204
  };
206
205
 
@@ -208,7 +207,7 @@ export const matchLanguagePseudoClass = (ast, node) => {
208
207
  * Matches the :disabled and :enabled pseudo-classes.
209
208
  * @param {string} astName - pseudo-class name
210
209
  * @param {object} node - Element node
211
- * @returns {boolean} - True if matched
210
+ * @returns {boolean} - True if the pseudo-class matches, otherwise false.
212
211
  */
213
212
  export const matchDisabledPseudoClass = (astName, node) => {
214
213
  const { localName, parentNode } = node;
@@ -265,7 +264,7 @@ export const matchDisabledPseudoClass = (astName, node) => {
265
264
  * Match the :read-only and :read-write pseudo-classes
266
265
  * @param {string} astName - pseudo-class name
267
266
  * @param {object} node - Element node
268
- * @returns {boolean} - True if matched
267
+ * @returns {boolean} - True if the pseudo-class matches, otherwise false.
269
268
  */
270
269
  export const matchReadOnlyPseudoClass = (astName, node) => {
271
270
  const { localName } = node;
@@ -328,6 +327,42 @@ export const matchAttributeSelector = (
328
327
  globalObject
329
328
  );
330
329
  }
330
+ if (astMatcher === null && !astFlags && typeof astName?.name === 'string') {
331
+ const rawName = unescapeSelector(astName.name);
332
+ if (rawName.indexOf('|') === -1) {
333
+ if (node.hasAttribute(rawName)) {
334
+ return true;
335
+ }
336
+ const attrs = node.attributes;
337
+ if (!attrs || attrs.length === 0) {
338
+ return false;
339
+ }
340
+ const isHTML = node.ownerDocument.contentType === 'text/html';
341
+ const checkName = isHTML ? rawName.toLowerCase() : rawName;
342
+ for (let i = 0, len = attrs.length; i < len; i++) {
343
+ let itemName = attrs[i].name;
344
+ if (isHTML) {
345
+ itemName = itemName.toLowerCase();
346
+ }
347
+ const colonIdx = itemName.indexOf(':');
348
+ if (colonIdx > -1) {
349
+ const itemPrefix = itemName.substring(0, colonIdx);
350
+ const itemLocalName = itemName
351
+ .substring(colonIdx + 1)
352
+ .replace(/^:/, '');
353
+ if (itemPrefix === 'xml' && itemLocalName === 'lang') {
354
+ continue;
355
+ }
356
+ if (checkName === itemLocalName) {
357
+ return true;
358
+ }
359
+ } else if (checkName === itemName) {
360
+ return true;
361
+ }
362
+ }
363
+ return false;
364
+ }
365
+ }
331
366
  const { attributes } = node;
332
367
  // An element with no attributes cannot match.
333
368
  if (!attributes || !attributes.length) {
@@ -546,7 +581,7 @@ export const matchAttributeSelector = (
546
581
  * @param {boolean} [opt.check] - running in internal check()
547
582
  * @param {boolean} [opt.forgive] - forgive undeclared namespace
548
583
  * @param {object} [opt.globalObject] - The global object.
549
- * @returns {boolean} - result
584
+ * @returns {boolean} - True if the type selector matches, otherwise false.
550
585
  */
551
586
  export const matchTypeSelector = (
552
587
  ast,
package/src/js/nwsapi.js CHANGED
@@ -413,6 +413,7 @@ export class Nwsapi {
413
413
  #selectLambdas;
414
414
  #selectResolvers;
415
415
  #snapshot;
416
+ #uidCounter = 0;
416
417
  #window;
417
418
 
418
419
  /* static */
@@ -463,10 +464,14 @@ export class Nwsapi {
463
464
  */
464
465
  constructor(window, document, cacheSize = CACHE_SIZE) {
465
466
  this.#window = window;
466
- this.#matchLambdas = new GenerationalCache(cacheSize);
467
- this.#selectLambdas = new GenerationalCache(cacheSize);
468
- this.#matchResolvers = new GenerationalCache(cacheSize);
469
- this.#selectResolvers = new GenerationalCache(cacheSize);
467
+ const cacheOpt = {
468
+ cacheFunction: true,
469
+ strictValidate: false
470
+ };
471
+ this.#matchLambdas = new GenerationalCache(cacheSize, cacheOpt);
472
+ this.#selectLambdas = new GenerationalCache(cacheSize, cacheOpt);
473
+ this.#matchResolvers = new GenerationalCache(cacheSize, cacheOpt);
474
+ this.#selectResolvers = new GenerationalCache(cacheSize, cacheOpt);
470
475
  this.#nthChildState = {
471
476
  idx: 0,
472
477
  len: 0,
@@ -488,8 +493,6 @@ export class Nwsapi {
488
493
  isContentEditable,
489
494
  isIndeterminate,
490
495
  isTarget,
491
- match: (selectors, element, callback) =>
492
- this.match(selectors, element, callback),
493
496
  nthElement: (element, dir) =>
494
497
  solveNth(element, dir, this.#nthChildState, false),
495
498
  nthOfType: (element, dir) =>
@@ -988,12 +991,27 @@ export class Nwsapi {
988
991
  const expr = match[2]
989
992
  .replace(REX.commaGroup, ',')
990
993
  .replace(REX.trimSpaces, '');
991
- const escapedExpr = expr.replace(/\\/g, '\\\\').replace(/\x22/g, '\\"');
992
- if (pseudoName === 'is') {
993
- return `if(s.match("${escapedExpr}",e)){${source}}`;
994
- } else if (pseudoName === 'not') {
995
- return `if(!s.match("${escapedExpr}",e)){${source}}`;
994
+ if (pseudoName === 'is' || pseudoName === 'not') {
995
+ const subExprs = expr.match(REX.splitGroup) || [expr];
996
+ const uid = ++this.#uidCounter;
997
+ const label = `l_${uid}`;
998
+ let code = `{ let r_${uid}=false, e_${uid}=e, n_${uid}=n, o_${uid}=o; ${label}: { `;
999
+ for (let i = 0; i < subExprs.length; i++) {
1000
+ const subCode = this._compileSelector(
1001
+ subExprs[i],
1002
+ `r_${uid}=true; break ${label};`,
1003
+ false
1004
+ );
1005
+ code += `{ ${subCode} } e=e_${uid}; n=n_${uid}; o=o_${uid}; `;
1006
+ }
1007
+ code += `} e=e_${uid}; n=n_${uid}; o=o_${uid}; `;
1008
+ if (pseudoName === 'is') {
1009
+ return `${code} if(r_${uid}){${source}} }`;
1010
+ } else {
1011
+ return `${code} if(!r_${uid}){${source}} }`;
1012
+ }
996
1013
  } else if (pseudoName === 'has') {
1014
+ const escapedExpr = expr.replace(/\\/g, '\\\\').replace(/\x22/g, '\\"');
997
1015
  this.#matchResolvers.clear();
998
1016
  return `if(e.querySelector(":scope ${escapedExpr}")){${source}}`;
999
1017
  }
@@ -23,18 +23,17 @@ import {
23
23
  PS_ELEMENT_SELECTOR,
24
24
  SELECTOR,
25
25
  SYNTAX_ERR,
26
- TAG_TYPE,
27
- TARGET_ALL,
28
- TARGET_FIRST
26
+ TARGET_ALL
29
27
  } from './constant.js';
30
28
 
31
29
  /* 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
30
  const REG_EXCLUDE_BASIC =
35
31
  /[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
32
+ const REG_EXCLUDE_QSA = new RegExp(
33
+ `(?:^(?:[A-Z]|\\.)[\\w-]*$|${COMPOUND_I}${DESCEND}${COMPOUND_I})`,
34
+ 'i'
35
+ );
36
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
37
  const REG_LOGIC_COMPLEX = new RegExp(
39
38
  `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
40
39
  );
@@ -266,7 +265,6 @@ export const extractSubjectsRegExp = (selector, caseSensitive) => {
266
265
  * @returns {boolean} - True if the selector is valid for nwsapi.
267
266
  */
268
267
  export const filterSelector = (selector, target) => {
269
- const isQuerySelectorAll = target === TARGET_ALL;
270
268
  // Basic validation and fast-fail for null/undefined/non-string values.
271
269
  if (
272
270
  !selector ||
@@ -279,6 +277,10 @@ export const filterSelector = (selector, target) => {
279
277
  if (REG_INVALID_SYNTAX.test(selector)) {
280
278
  return false;
281
279
  }
280
+ // Target-specific early exits.
281
+ if (target === TARGET_ALL && REG_EXCLUDE_QSA.test(selector)) {
282
+ return false;
283
+ }
282
284
  // Exclude various complex or unsupported selectors early.
283
285
  // i.e. non-ASCII, escaped selectors, namespaced selectors, pseudo-elements.
284
286
  if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
@@ -291,26 +293,13 @@ export const filterSelector = (selector, target) => {
291
293
  return false;
292
294
  }
293
295
  }
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
296
  // Logic for pseudo-classes.
302
297
  if (selector.includes(':')) {
303
- // Exclude descendant combinators in logical selectors for querySelectorAll.
304
- if (isQuerySelectorAll && REG_DESCEND.test(selector)) {
305
- return false;
306
- }
307
298
  // Determine if the selector has complex logical structures.
308
- const isComplex = isQuerySelectorAll ? false : REG_COMPLEX.test(selector);
299
+ const isComplex =
300
+ target === TARGET_ALL ? false : REG_COMPLEX.test(selector);
309
301
  // Handle :has() specifically.
310
302
  if (selector.includes(':has(')) {
311
- if (isQuerySelectorAll) {
312
- return false;
313
- }
314
303
  if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
315
304
  return false;
316
305
  }
package/src/js/utility.js CHANGED
@@ -8,17 +8,21 @@ import isCustomElementName from 'is-potential-custom-element-name';
8
8
 
9
9
  /* constants */
10
10
  import {
11
+ CLASS_SELECTOR,
11
12
  DOCUMENT_FRAGMENT_NODE,
12
13
  DOCUMENT_NODE,
13
14
  DOCUMENT_POSITION_CONTAINS,
14
15
  DOCUMENT_POSITION_PRECEDING,
15
16
  ELEMENT_NODE,
17
+ ID_SELECTOR,
16
18
  INPUT_BUTTON,
17
19
  INPUT_EDIT,
18
20
  INPUT_LTR,
19
21
  INPUT_TEXT,
22
+ SHOW_ELEMENT,
20
23
  TEXT_NODE,
21
24
  TYPE_FROM,
25
+ TYPE_SELECTOR,
22
26
  TYPE_TO
23
27
  } from './constant.js';
24
28
  const KEYS_DIR_AUTO = new Set([...INPUT_BUTTON, ...INPUT_TEXT, 'hidden']);
@@ -297,19 +301,24 @@ export const getSlottedTextContent = node => {
297
301
  * Get directionality of a node.
298
302
  * @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
299
303
  * @param {object} node - The Element node.
304
+ * @param {WeakMap} [dirCache] - Cache for directionality.
300
305
  * @returns {?string} - 'ltr' or 'rtl'.
301
306
  */
302
- export const getDirectionality = node => {
307
+ export const getDirectionality = (node, dirCache = new WeakMap()) => {
303
308
  if (!node?.nodeType) {
304
309
  throw new TypeError(`Unexpected type ${getType(node)}`);
305
310
  }
306
311
  if (node.nodeType !== ELEMENT_NODE) {
307
312
  return null;
308
313
  }
314
+ if (dirCache.has(node)) {
315
+ return dirCache.get(node);
316
+ }
309
317
  const { dir: dirAttr, localName, parentNode } = node;
310
318
  const { getEmbeddingLevels } = bidiFactory();
319
+ let result = 'ltr';
311
320
  if (dirAttr === 'ltr' || dirAttr === 'rtl') {
312
- return dirAttr;
321
+ result = dirAttr;
313
322
  } else if (dirAttr === 'auto') {
314
323
  let text = '';
315
324
  switch (localName) {
@@ -317,7 +326,8 @@ export const getDirectionality = node => {
317
326
  if (!node.type || KEYS_DIR_AUTO.has(node.type)) {
318
327
  text = node.value;
319
328
  } else if (KEYS_DIR_LTR.has(node.type)) {
320
- return 'ltr';
329
+ result = 'ltr';
330
+ text = null; // Flag to skip text evaluation
321
331
  }
322
332
  break;
323
333
  }
@@ -357,21 +367,23 @@ export const getDirectionality = node => {
357
367
  }
358
368
  }
359
369
  }
360
- if (text) {
361
- const {
362
- paragraphs: [{ level }]
363
- } = getEmbeddingLevels(text);
364
- if (level % 2 === 1) {
365
- return 'rtl';
366
- }
367
- } else if (parentNode) {
368
- const { nodeType: parentNodeType } = parentNode;
369
- if (parentNodeType === ELEMENT_NODE) {
370
- return getDirectionality(parentNode);
370
+ if (text !== null) {
371
+ if (text) {
372
+ const {
373
+ paragraphs: [{ level }]
374
+ } = getEmbeddingLevels(text);
375
+ if (level % 2 === 1) {
376
+ result = 'rtl';
377
+ }
378
+ } else if (parentNode) {
379
+ const { nodeType: parentNodeType } = parentNode;
380
+ if (parentNodeType === ELEMENT_NODE) {
381
+ result = getDirectionality(parentNode, dirCache);
382
+ }
371
383
  }
372
384
  }
373
385
  } else if (localName === 'input' && node.type === 'tel') {
374
- return 'ltr';
386
+ result = 'ltr';
375
387
  } else if (localName === 'bdi') {
376
388
  const text = node.textContent.trim();
377
389
  if (text) {
@@ -379,7 +391,7 @@ export const getDirectionality = node => {
379
391
  paragraphs: [{ level }]
380
392
  } = getEmbeddingLevels(text);
381
393
  if (level % 2 === 1) {
382
- return 'rtl';
394
+ result = 'rtl';
383
395
  }
384
396
  }
385
397
  } else if (parentNode) {
@@ -390,47 +402,67 @@ export const getDirectionality = node => {
390
402
  paragraphs: [{ level }]
391
403
  } = getEmbeddingLevels(text);
392
404
  if (level % 2 === 1) {
393
- return 'rtl';
405
+ result = 'rtl';
406
+ } else {
407
+ result = 'ltr';
408
+ }
409
+ } else {
410
+ const { nodeType: parentNodeType } = parentNode;
411
+ if (parentNodeType === ELEMENT_NODE) {
412
+ result = getDirectionality(parentNode, dirCache);
394
413
  }
395
- return 'ltr';
396
414
  }
397
- }
398
- const { nodeType: parentNodeType } = parentNode;
399
- if (parentNodeType === ELEMENT_NODE) {
400
- return getDirectionality(parentNode);
415
+ } else {
416
+ const { nodeType: parentNodeType } = parentNode;
417
+ if (parentNodeType === ELEMENT_NODE) {
418
+ result = getDirectionality(parentNode, dirCache);
419
+ }
401
420
  }
402
421
  }
403
- return 'ltr';
422
+ dirCache.set(node, result);
423
+ return result;
404
424
  };
405
425
 
406
426
  /**
407
- * Traverses up the DOM tree to find the language attribute for a node.
408
- * It checks for 'lang' in HTML and 'xml:lang' in XML contexts.
409
- * @param {object} node - The starting element node.
410
- * @returns {string|null} The language attribute value, or null if not found.
427
+ * Get language attribute of a node.
428
+ * @param {object} node - The Element node.
429
+ * @param {WeakMap} [langCache] - Cache for language attributes.
430
+ * @returns {?string} - Language attribute value.
411
431
  */
412
- export const getLanguageAttribute = node => {
432
+ export const getLanguageAttribute = (node, langCache = new WeakMap()) => {
413
433
  if (!node?.nodeType) {
414
434
  throw new TypeError(`Unexpected type ${getType(node)}`);
415
435
  }
416
436
  if (node.nodeType !== ELEMENT_NODE) {
417
437
  return null;
418
438
  }
439
+ if (langCache.has(node)) {
440
+ return langCache.get(node);
441
+ }
419
442
  const { contentType } = node.ownerDocument;
420
443
  const isHtml = REG_IS_HTML.test(contentType);
421
444
  const isXml = REG_IS_XML.test(contentType);
422
445
  let isShadow = false;
446
+ let result;
447
+ const visited = [];
423
448
  // Traverse up from the current node to the root.
424
449
  let current = node;
425
450
  while (current) {
451
+ if (current.nodeType === ELEMENT_NODE && langCache.has(current)) {
452
+ result = langCache.get(current);
453
+ break;
454
+ }
455
+ if (current.nodeType === ELEMENT_NODE) {
456
+ visited.push(current);
457
+ }
426
458
  // Check if the current node is an element.
427
459
  switch (current.nodeType) {
428
460
  case ELEMENT_NODE: {
429
461
  // Check for and return the language attribute if present.
430
462
  if (isHtml && current.hasAttribute('lang')) {
431
- return current.getAttribute('lang');
463
+ result = current.getAttribute('lang');
432
464
  } else if (isXml && current.hasAttribute('xml:lang')) {
433
- return current.getAttribute('xml:lang');
465
+ result = current.getAttribute('xml:lang');
434
466
  }
435
467
  break;
436
468
  }
@@ -444,9 +476,12 @@ export const getLanguageAttribute = node => {
444
476
  case DOCUMENT_NODE:
445
477
  default: {
446
478
  // Stop if we reach the root document node.
447
- return null;
479
+ result = null;
448
480
  }
449
481
  }
482
+ if (result !== undefined) {
483
+ break;
484
+ }
450
485
  if (isShadow) {
451
486
  current = current.host;
452
487
  isShadow = false;
@@ -456,8 +491,13 @@ export const getLanguageAttribute = node => {
456
491
  break;
457
492
  }
458
493
  }
459
- // No language attribute was found in the hierarchy.
460
- return null;
494
+ if (result === undefined) {
495
+ result = null;
496
+ }
497
+ for (const visitedNode of visited) {
498
+ langCache.set(visitedNode, result);
499
+ }
500
+ return result;
461
501
  };
462
502
 
463
503
  /**
@@ -794,3 +834,103 @@ export const sortNodes = (nodes = []) => {
794
834
  }
795
835
  return arr;
796
836
  };
837
+
838
+ /**
839
+ * Traverses AST nodes to find the most optimal seed selector
840
+ * (ID > Class > Tag).
841
+ * @param {Array} nodes - AST nodes to traverse.
842
+ * @param {object} [state] - The current state of the search.
843
+ * @returns {object} The search state containing the best seed.
844
+ */
845
+ export const findBestSeed = (nodes, state = { seed: null, priority: 0 }) => {
846
+ for (const node of nodes) {
847
+ if (state.priority === 3) {
848
+ return state;
849
+ }
850
+ if (Array.isArray(node)) {
851
+ findBestSeed(node, state);
852
+ } else if (node && typeof node === 'object') {
853
+ // ID Selector (Fastest: getElementById)
854
+ if (node.type === ID_SELECTOR) {
855
+ state.seed = { type: 'id', value: node.name };
856
+ state.priority = 3;
857
+ return state;
858
+ } else if (node.type === CLASS_SELECTOR && state.priority < 2) {
859
+ // Class Selector (Faster: getElementsByClassName)
860
+ state.seed = { type: 'class', value: node.name };
861
+ state.priority = 2;
862
+ } else if (
863
+ node.type === TYPE_SELECTOR &&
864
+ state.priority < 1 &&
865
+ node.name !== '*'
866
+ ) {
867
+ // Type/Tag Selector (Excludes universal '*')
868
+ state.seed = { type: 'tag', value: node.name };
869
+ state.priority = 1;
870
+ }
871
+ if (node.children) {
872
+ findBestSeed(node.children, state);
873
+ }
874
+ }
875
+ }
876
+ return state;
877
+ };
878
+
879
+ /**
880
+ * Traces the DOM tree upwards and sideways from a seed element,
881
+ * populating the allowlist with safe paths for :has() evaluation.
882
+ * @param {object} current - The starting seed element.
883
+ * @param {WeakSet} list - The WeakSet to populate.
884
+ * @param {Set} visitedAncestors - The Set to track visited nodes.
885
+ * @returns {void}
886
+ */
887
+ export const populateHasAllowlist = (current, list, visitedAncestors) => {
888
+ list.add(current);
889
+ while (
890
+ current &&
891
+ (current.nodeType === ELEMENT_NODE ||
892
+ current.nodeType === DOCUMENT_FRAGMENT_NODE)
893
+ ) {
894
+ if (visitedAncestors.has(current)) {
895
+ break;
896
+ }
897
+ visitedAncestors.add(current);
898
+ let sibling = current.previousElementSibling;
899
+ while (sibling) {
900
+ list.add(sibling);
901
+ sibling = sibling.previousElementSibling;
902
+ }
903
+ sibling = current.nextElementSibling;
904
+ while (sibling) {
905
+ list.add(sibling);
906
+ sibling = sibling.nextElementSibling;
907
+ }
908
+ current = current.parentNode;
909
+ if (current) {
910
+ list.add(current);
911
+ }
912
+ }
913
+ };
914
+
915
+ /**
916
+ * Collects all descendant elements of a given node using a TreeWalker.
917
+ * @param {Document|DocumentFragment|Element} node - The node to start from.
918
+ * @param {Document} document - The Document used to create the TreeWalker.
919
+ * @returns {Array<Element>} An array containing all descendant elements.
920
+ */
921
+ export const collectAllDescendants = (node, document) => {
922
+ if (!node?.nodeType) {
923
+ throw new TypeError(`Unexpected type ${getType(node)}`);
924
+ }
925
+ if (document?.nodeType !== DOCUMENT_NODE) {
926
+ throw new TypeError(`Unexpected type ${getType(document)}`);
927
+ }
928
+ const walker = document.createTreeWalker(node, SHOW_ELEMENT);
929
+ const descendants = [];
930
+ let refNode = walker.nextNode();
931
+ while (refNode) {
932
+ descendants.push(refNode);
933
+ refNode = walker.nextNode();
934
+ }
935
+ return descendants;
936
+ };
package/types/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export class DOMSelector {
2
2
  constructor(window: Window, document: Document, opt?: object);
3
- clear: () => void;
3
+ clear: (clearAll?: boolean) => void;
4
4
  extractSubjects: (selector: string, caseSensitive?: boolean) => Array<{
5
5
  id: string | null;
6
6
  className: string | null;
@@ -54,8 +54,10 @@ export const DESCEND: "\\s?[\\s>]\\s?";
54
54
  export const SIBLING: "\\s?[+~]\\s?";
55
55
  export const LOGIC_IS: ":is\\(\\s*[^)]+\\s*\\)";
56
56
  export const N_TH: "nth-(?:last-)?(?:child|of-type)\\(\\s*(?:even|odd|[+-]?(?:(?:0|[1-9]\\d*)n?|n)|(?:[+-]?(?:0|[1-9]\\d*))?n\\s*[+-]\\s*(?:0|[1-9]\\d*))\\s*\\)";
57
+ export const ATTR_TYPE: "\\[[^|\\]]+\\]";
57
58
  export const SUB_TYPE: "\\[[^|\\]]+\\]|[#.:][\\w-]+";
58
59
  export const SUB_TYPE_WO_PSEUDO: "\\[[^|\\]]+\\]|[#.][\\w-]+";
60
+ export const TAG_TYPE_WO_UNIVERSAL: "[A-Za-z][\\w-]*";
59
61
  export const TAG_TYPE: "\\*|[A-Za-z][\\w-]*";
60
62
  export const TAG_TYPE_I: "\\*|[A-Z][\\w-]*";
61
63
  export const COMPOUND: "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)";
@@ -22,12 +22,15 @@ export class Finder {
22
22
  private _collectNthOfType;
23
23
  private _matchAnPlusB;
24
24
  private _matchHasPseudoFunc;
25
+ private _buildHasAllowlist;
25
26
  private _evaluateHasPseudo;
26
27
  private _matchLogicalPseudoFunc;
28
+ private _evaluateLogicalPseudo;
29
+ private _evaluatePseudoClassFunc;
27
30
  private _matchPseudoClassSelector;
28
31
  private _evaluateHostPseudo;
29
32
  private _evaluateHostContextPseudo;
30
- private _matchShadowHostPseudoClass;
33
+ private _evaluateShadowHost;
31
34
  private _matchSelectorForElement;
32
35
  private _matchSelectorForShadowRoot;
33
36
  private _matchSelector;
@@ -3,8 +3,8 @@ export function matchPseudoElementSelector(astName: string, astType: string, { f
3
3
  globalObject?: object | undefined;
4
4
  warn?: boolean | undefined;
5
5
  }): void;
6
- export function matchDirectionPseudoClass(ast: object, node: object): boolean;
7
- export function matchLanguagePseudoClass(ast: object, node: object): boolean;
6
+ export function matchDirectionPseudoClass(ast: object, node: object, dirCache?: WeakMap<any, any>): boolean;
7
+ export function matchLanguagePseudoClass(ast: object, node: object, langCache?: WeakMap<any, any>): boolean;
8
8
  export function matchDisabledPseudoClass(astName: string, node: object): boolean;
9
9
  export function matchReadOnlyPseudoClass(astName: string, node: object): boolean;
10
10
  export function matchAttributeSelector(ast: object, node: object, { check, forgive, globalObject }?: {