@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/js/utility.js CHANGED
@@ -4,38 +4,25 @@
4
4
 
5
5
  /* import */
6
6
  import bidiFactory from 'bidi-js';
7
- import * as cssTree from 'css-tree';
8
7
  import isCustomElementName from 'is-potential-custom-element-name';
9
8
 
10
9
  /* constants */
11
10
  import {
12
- ATRULE,
13
- COMBO,
14
- COMPOUND_I,
15
- DESCEND,
11
+ CLASS_SELECTOR,
16
12
  DOCUMENT_FRAGMENT_NODE,
17
13
  DOCUMENT_NODE,
18
14
  DOCUMENT_POSITION_CONTAINS,
19
15
  DOCUMENT_POSITION_PRECEDING,
20
16
  ELEMENT_NODE,
21
- HAS_COMPOUND,
17
+ ID_SELECTOR,
22
18
  INPUT_BUTTON,
23
19
  INPUT_EDIT,
24
20
  INPUT_LTR,
25
21
  INPUT_TEXT,
26
- KEYS_LOGICAL,
27
- LOGIC_COMPLEX,
28
- LOGIC_COMPOUND,
29
- N_TH,
30
- PSEUDO_CLASS,
31
- RULE,
32
- SCOPE,
33
- SELECTOR_LIST,
34
- TAG_TYPE,
35
- TARGET_ALL,
36
- TARGET_FIRST,
22
+ SHOW_ELEMENT,
37
23
  TEXT_NODE,
38
24
  TYPE_FROM,
25
+ TYPE_SELECTOR,
39
26
  TYPE_TO
40
27
  } from './constant.js';
41
28
  const KEYS_DIR_AUTO = new Set([...INPUT_BUTTON, ...INPUT_TEXT, 'hidden']);
@@ -58,105 +45,9 @@ const KEYS_NODE_FOCUSABLE_SVG = new Set([
58
45
  'symbol',
59
46
  'title'
60
47
  ]);
61
- const REG_ATTR_SIMPLE = /^\[[A-Z\d-]{1,255}(?:="?[A-Z\d\s-]{1,255}"?)?\]$/i;
62
- const REG_TAG_SIMPLE = new RegExp(`^(?:${TAG_TYPE})$`);
63
- const REG_EXCLUDE_BASIC =
64
- /[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
65
- const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
66
- const REG_DESCEND = new RegExp(`${COMPOUND_I}${DESCEND}${COMPOUND_I}`, 'i');
67
- const REG_LOGIC_COMPLEX = new RegExp(
68
- `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
69
- );
70
- const REG_LOGIC_COMPOUND = new RegExp(
71
- `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
72
- );
73
- const REG_LOGIC_HAS_COMPOUND = new RegExp(
74
- `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
75
- );
76
- const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
77
- const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
78
48
  const REG_IS_HTML = /^(?:application\/xhtml\+x|text\/ht)ml$/;
79
49
  const REG_IS_XML =
80
50
  /^(?:application\/(?:[\w\-.]+\+)?|image\/[\w\-.]+\+|text\/)xml$/;
81
- const REG_COMBO = new RegExp(COMBO);
82
- const REG_ID = /#(\D[^#.*]+)/g;
83
- const REG_CLASS = /\.(\D[^#.*]+)/g;
84
- const REG_TAG = /^([^#.]+)/;
85
-
86
- /**
87
- * Manages state for extracting nested selectors from a CSS AST.
88
- */
89
- class SelectorExtractor {
90
- constructor() {
91
- this.selectors = [];
92
- this.isScoped = false;
93
- }
94
-
95
- /**
96
- * Walker enter function.
97
- * @param {object} node - The AST node.
98
- */
99
- enter(node) {
100
- switch (node.type) {
101
- case ATRULE: {
102
- if (node.name === 'scope') {
103
- this.isScoped = true;
104
- }
105
- break;
106
- }
107
- case SCOPE: {
108
- const { children, type } = node.root;
109
- const arr = [];
110
- if (type === SELECTOR_LIST) {
111
- for (const child of children) {
112
- const selector = cssTree.generate(child);
113
- arr.push(selector);
114
- }
115
- this.selectors.push(arr);
116
- }
117
- break;
118
- }
119
- case RULE: {
120
- const { children, type } = node.prelude;
121
- const arr = [];
122
- if (type === SELECTOR_LIST) {
123
- let hasAmp = false;
124
- for (const child of children) {
125
- const selector = cssTree.generate(child);
126
- if (this.isScoped && !hasAmp) {
127
- hasAmp = /\x26/.test(selector);
128
- }
129
- arr.push(selector);
130
- }
131
- if (this.isScoped) {
132
- if (hasAmp) {
133
- this.selectors.push(arr);
134
- /* FIXME:
135
- } else {
136
- this.selectors = arr;
137
- this.isScoped = false;
138
- */
139
- }
140
- } else {
141
- this.selectors.push(arr);
142
- }
143
- }
144
- }
145
- }
146
- }
147
-
148
- /**
149
- * Walker leave function.
150
- * @param {object} node - The AST node.
151
- */
152
- leave(node) {
153
- if (node.type === ATRULE) {
154
- if (node.name === 'scope') {
155
- this.isScoped = false;
156
- }
157
- }
158
- }
159
- }
160
51
 
161
52
  /**
162
53
  * Get type of an object.
@@ -199,27 +90,6 @@ export const generateException = (msg, name, globalObject = globalThis) => {
199
90
  return new globalObject.DOMException(msg, name);
200
91
  };
201
92
 
202
- /**
203
- * Find a nested :has() pseudo-class.
204
- * @param {object} leaf - The AST leaf to check.
205
- * @returns {?object} The leaf if it's :has, otherwise null.
206
- */
207
- export const findNestedHas = leaf => {
208
- return leaf.name === 'has';
209
- };
210
-
211
- /**
212
- * Find a logical pseudo-class that contains a nested :has().
213
- * @param {object} leaf - The AST leaf to check.
214
- * @returns {?object} The leaf if it matches, otherwise null.
215
- */
216
- export const findLogicalWithNestedHas = leaf => {
217
- if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
218
- return leaf;
219
- }
220
- return null;
221
- };
222
-
223
93
  /**
224
94
  * Filter a list of nodes based on An+B logic
225
95
  * @param {Array.<object>} nodes - array of nodes to filter
@@ -431,19 +301,24 @@ export const getSlottedTextContent = node => {
431
301
  * Get directionality of a node.
432
302
  * @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
433
303
  * @param {object} node - The Element node.
304
+ * @param {WeakMap} [dirCache] - Cache for directionality.
434
305
  * @returns {?string} - 'ltr' or 'rtl'.
435
306
  */
436
- export const getDirectionality = node => {
307
+ export const getDirectionality = (node, dirCache = new WeakMap()) => {
437
308
  if (!node?.nodeType) {
438
309
  throw new TypeError(`Unexpected type ${getType(node)}`);
439
310
  }
440
311
  if (node.nodeType !== ELEMENT_NODE) {
441
312
  return null;
442
313
  }
314
+ if (dirCache.has(node)) {
315
+ return dirCache.get(node);
316
+ }
443
317
  const { dir: dirAttr, localName, parentNode } = node;
444
318
  const { getEmbeddingLevels } = bidiFactory();
319
+ let result = 'ltr';
445
320
  if (dirAttr === 'ltr' || dirAttr === 'rtl') {
446
- return dirAttr;
321
+ result = dirAttr;
447
322
  } else if (dirAttr === 'auto') {
448
323
  let text = '';
449
324
  switch (localName) {
@@ -451,7 +326,8 @@ export const getDirectionality = node => {
451
326
  if (!node.type || KEYS_DIR_AUTO.has(node.type)) {
452
327
  text = node.value;
453
328
  } else if (KEYS_DIR_LTR.has(node.type)) {
454
- return 'ltr';
329
+ result = 'ltr';
330
+ text = null; // Flag to skip text evaluation
455
331
  }
456
332
  break;
457
333
  }
@@ -491,21 +367,23 @@ export const getDirectionality = node => {
491
367
  }
492
368
  }
493
369
  }
494
- if (text) {
495
- const {
496
- paragraphs: [{ level }]
497
- } = getEmbeddingLevels(text);
498
- if (level % 2 === 1) {
499
- return 'rtl';
500
- }
501
- } else if (parentNode) {
502
- const { nodeType: parentNodeType } = parentNode;
503
- if (parentNodeType === ELEMENT_NODE) {
504
- 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
+ }
505
383
  }
506
384
  }
507
385
  } else if (localName === 'input' && node.type === 'tel') {
508
- return 'ltr';
386
+ result = 'ltr';
509
387
  } else if (localName === 'bdi') {
510
388
  const text = node.textContent.trim();
511
389
  if (text) {
@@ -513,7 +391,7 @@ export const getDirectionality = node => {
513
391
  paragraphs: [{ level }]
514
392
  } = getEmbeddingLevels(text);
515
393
  if (level % 2 === 1) {
516
- return 'rtl';
394
+ result = 'rtl';
517
395
  }
518
396
  }
519
397
  } else if (parentNode) {
@@ -524,47 +402,67 @@ export const getDirectionality = node => {
524
402
  paragraphs: [{ level }]
525
403
  } = getEmbeddingLevels(text);
526
404
  if (level % 2 === 1) {
527
- 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);
528
413
  }
529
- return 'ltr';
530
414
  }
531
- }
532
- const { nodeType: parentNodeType } = parentNode;
533
- if (parentNodeType === ELEMENT_NODE) {
534
- return getDirectionality(parentNode);
415
+ } else {
416
+ const { nodeType: parentNodeType } = parentNode;
417
+ if (parentNodeType === ELEMENT_NODE) {
418
+ result = getDirectionality(parentNode, dirCache);
419
+ }
535
420
  }
536
421
  }
537
- return 'ltr';
422
+ dirCache.set(node, result);
423
+ return result;
538
424
  };
539
425
 
540
426
  /**
541
- * Traverses up the DOM tree to find the language attribute for a node.
542
- * It checks for 'lang' in HTML and 'xml:lang' in XML contexts.
543
- * @param {object} node - The starting element node.
544
- * @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.
545
431
  */
546
- export const getLanguageAttribute = node => {
432
+ export const getLanguageAttribute = (node, langCache = new WeakMap()) => {
547
433
  if (!node?.nodeType) {
548
434
  throw new TypeError(`Unexpected type ${getType(node)}`);
549
435
  }
550
436
  if (node.nodeType !== ELEMENT_NODE) {
551
437
  return null;
552
438
  }
439
+ if (langCache.has(node)) {
440
+ return langCache.get(node);
441
+ }
553
442
  const { contentType } = node.ownerDocument;
554
443
  const isHtml = REG_IS_HTML.test(contentType);
555
444
  const isXml = REG_IS_XML.test(contentType);
556
445
  let isShadow = false;
446
+ let result;
447
+ const visited = [];
557
448
  // Traverse up from the current node to the root.
558
449
  let current = node;
559
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
+ }
560
458
  // Check if the current node is an element.
561
459
  switch (current.nodeType) {
562
460
  case ELEMENT_NODE: {
563
461
  // Check for and return the language attribute if present.
564
462
  if (isHtml && current.hasAttribute('lang')) {
565
- return current.getAttribute('lang');
463
+ result = current.getAttribute('lang');
566
464
  } else if (isXml && current.hasAttribute('xml:lang')) {
567
- return current.getAttribute('xml:lang');
465
+ result = current.getAttribute('xml:lang');
568
466
  }
569
467
  break;
570
468
  }
@@ -578,9 +476,12 @@ export const getLanguageAttribute = node => {
578
476
  case DOCUMENT_NODE:
579
477
  default: {
580
478
  // Stop if we reach the root document node.
581
- return null;
479
+ result = null;
582
480
  }
583
481
  }
482
+ if (result !== undefined) {
483
+ break;
484
+ }
584
485
  if (isShadow) {
585
486
  current = current.host;
586
487
  isShadow = false;
@@ -590,8 +491,13 @@ export const getLanguageAttribute = node => {
590
491
  break;
591
492
  }
592
493
  }
593
- // No language attribute was found in the hierarchy.
594
- 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;
595
501
  };
596
502
 
597
503
  /**
@@ -930,201 +836,101 @@ export const sortNodes = (nodes = []) => {
930
836
  };
931
837
 
932
838
  /**
933
- * Concat an array of nested selectors into an equivalent single selector.
934
- * @param {Array.<Array.<string>>} selectors - [parents, children, ...].
935
- * @returns {string} - The concatenated selector.
936
- */
937
- export const concatNestedSelectors = selectors => {
938
- if (!Array.isArray(selectors)) {
939
- throw new TypeError(`Unexpected type ${getType(selectors)}`);
940
- }
941
- let selector = '';
942
- if (selectors.length) {
943
- const revSelectors = selectors.toReversed();
944
- let child = verifyArray(revSelectors.shift(), 'String');
945
- if (child.length === 1) {
946
- [child] = child;
947
- }
948
- while (revSelectors.length) {
949
- const parentArr = verifyArray(revSelectors.shift(), 'String');
950
- if (!parentArr.length) {
951
- continue;
952
- }
953
- let parent;
954
- if (parentArr.length === 1) {
955
- [parent] = parentArr;
956
- if (!/^[>~+]/.test(parent) && /[\s>~+]/.test(parent)) {
957
- parent = `:is(${parent})`;
958
- }
959
- } else {
960
- parent = `:is(${parentArr.join(', ')})`;
961
- }
962
- if (selector.includes('\x26')) {
963
- selector = selector.replace(/\x26/g, parent);
964
- }
965
- if (Array.isArray(child)) {
966
- const items = [];
967
- for (let item of child) {
968
- if (item.includes('\x26')) {
969
- if (/^[>~+]/.test(item)) {
970
- item = `${parent} ${item.replace(/\x26/g, parent)} ${selector}`;
971
- } else {
972
- item = `${item.replace(/\x26/g, parent)} ${selector}`;
973
- }
974
- } else {
975
- item = `${parent} ${item} ${selector}`;
976
- }
977
- items.push(item.trim());
978
- }
979
- selector = items.join(', ');
980
- } else if (revSelectors.length) {
981
- selector = `${child} ${selector}`;
982
- } else {
983
- if (child.includes('\x26')) {
984
- if (/^[>~+]/.test(child)) {
985
- selector = `${parent} ${child.replace(/\x26/g, parent)} ${selector}`;
986
- } else {
987
- selector = `${child.replace(/\x26/g, parent)} ${selector}`;
988
- }
989
- } else {
990
- selector = `${parent} ${child} ${selector}`;
991
- }
992
- }
993
- selector = selector.trim();
994
- if (revSelectors.length) {
995
- child = parentArr.length > 1 ? parentArr : parent;
996
- } else {
997
- break;
998
- }
999
- }
1000
- selector = selector.replace(/\x26/g, ':scope').trim();
1001
- }
1002
- return selector;
1003
- };
1004
-
1005
- /**
1006
- * Extract nested selectors from CSSRule.cssText.
1007
- * @param {string} css - CSSRule.cssText.
1008
- * @returns {Array.<Array.<string>>} - Array of nested selectors.
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.
1009
844
  */
1010
- export const extractNestedSelectors = css => {
1011
- const ast = cssTree.parse(css, {
1012
- context: 'rule'
1013
- });
1014
- const extractor = new SelectorExtractor();
1015
- cssTree.walk(ast, {
1016
- enter: extractor.enter.bind(extractor),
1017
- leave: extractor.leave.bind(extractor)
1018
- });
1019
- return extractor.selectors;
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;
1020
877
  };
1021
878
 
1022
879
  /**
1023
- * Extracts the rightmost subject keys (id, class, tag) from a selector.
1024
- * @param {string} selector - The CSS selector string to parse.
1025
- * @param {boolean} caseSensitive - True if the tag should be case-sensitive.
1026
- * @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
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}
1027
886
  */
1028
- export const extractSubjectsRegExp = (selector, caseSensitive) => {
1029
- const subjects = [];
1030
- const groups = selector.split(',');
1031
- for (let i = 0; i < groups.length; i++) {
1032
- const group = groups[i].trim();
1033
- if (!group) {
1034
- continue;
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;
1035
896
  }
1036
- const compounds = group.split(REG_COMBO);
1037
- const rightmost = compounds[compounds.length - 1];
1038
- let idKey = null;
1039
- let classKey = null;
1040
- let tagKey = null;
1041
- if (rightmost) {
1042
- const idMatch = rightmost.match(REG_ID);
1043
- if (idMatch) {
1044
- idKey = idMatch[idMatch.length - 1].slice(1);
1045
- }
1046
- const classMatch = rightmost.match(REG_CLASS);
1047
- if (classMatch) {
1048
- classKey = classMatch[classMatch.length - 1].slice(1);
1049
- }
1050
- const tagMatch = rightmost.match(REG_TAG);
1051
- if (tagMatch) {
1052
- const tag = tagMatch[1];
1053
- if (tag !== '*') {
1054
- tagKey = caseSensitive ? tag : tag.toLowerCase();
1055
- }
1056
- }
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);
1057
911
  }
1058
- subjects.push({ id: idKey, className: classKey, tag: tagKey });
1059
912
  }
1060
- return subjects;
1061
913
  };
1062
914
 
1063
915
  /**
1064
- * Filter a selector for use with nwsapi.
1065
- * @param {string} selector - The selector string.
1066
- * @param {string} target - The target type.
1067
- * @returns {boolean} - True if the selector is valid for nwsapi.
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.
1068
920
  */
1069
- export const filterSelector = (selector, target) => {
1070
- const isQuerySelectorAll = target === TARGET_ALL;
1071
- // Basic validation and fast-fail for null/undefined/non-string values.
1072
- if (
1073
- !selector ||
1074
- typeof selector !== 'string' ||
1075
- /null|undefined/.test(selector)
1076
- ) {
1077
- return false;
1078
- }
1079
- // Exclude various complex or unsupported selectors early.
1080
- // i.e. non-ASCII, escaped selectors, namespaced selectors, pseudo-elements.
1081
- if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
1082
- return false;
1083
- }
1084
- // Validate attribute selector integrity.
1085
- if (selector.includes('[')) {
1086
- const index = selector.lastIndexOf('[');
1087
- if (selector.indexOf(']', index) === -1) {
1088
- return false;
1089
- }
921
+ export const collectAllDescendants = (node, document) => {
922
+ if (!node?.nodeType) {
923
+ throw new TypeError(`Unexpected type ${getType(node)}`);
1090
924
  }
1091
- // Target-specific early exits.
1092
- if (target === TARGET_FIRST) {
1093
- return REG_ATTR_SIMPLE.test(selector);
925
+ if (document?.nodeType !== DOCUMENT_NODE) {
926
+ throw new TypeError(`Unexpected type ${getType(document)}`);
1094
927
  }
1095
- if (target === TARGET_ALL && REG_TAG_SIMPLE.test(selector)) {
1096
- return false;
1097
- }
1098
- // Logic for pseudo-classes.
1099
- if (selector.includes(':')) {
1100
- // Exclude descendant combinators in logical selectors for querySelectorAll.
1101
- if (isQuerySelectorAll && REG_DESCEND.test(selector)) {
1102
- return false;
1103
- }
1104
- // Determine if the selector has complex logical structures.
1105
- const isComplex = isQuerySelectorAll ? false : REG_COMPLEX.test(selector);
1106
- // Handle :has() specifically.
1107
- if (selector.includes(':has(')) {
1108
- if (isQuerySelectorAll) {
1109
- return false;
1110
- }
1111
- if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
1112
- return false;
1113
- }
1114
- return REG_END_WITH_HAS.test(selector);
1115
- }
1116
- // Handle :is() and :not().
1117
- if (/(?:is|not)\(/.test(selector)) {
1118
- if (isComplex) {
1119
- return !REG_LOGIC_COMPLEX.test(selector);
1120
- } else {
1121
- return !REG_LOGIC_COMPOUND.test(selector);
1122
- }
1123
- }
1124
- // Default check for other pseudo-classes against known list.
1125
- if (REG_WO_LOGICAL.test(selector)) {
1126
- return false;
1127
- }
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();
1128
934
  }
1129
- return true;
935
+ return descendants;
1130
936
  };
package/types/index.d.ts CHANGED
@@ -1,11 +1,12 @@
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;
7
7
  tag: string | null;
8
8
  }>;
9
+ supports: (selector: string) => boolean;
9
10
  check: (selector: string, node: Element, opt?: object) => CheckResult;
10
11
  matches: (selector: string, node: Element, opt?: object) => boolean;
11
12
  closest: (selector: string, node: Element, opt?: object) => Element | null;