@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/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
  }
package/src/js/parser.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  HEX,
23
23
  ID_SELECTOR,
24
24
  KEYS_LOGICAL,
25
+ KEYS_PS_CLASS_SUPPORTED,
25
26
  NTH,
26
27
  PS_CLASS_SELECTOR,
27
28
  PS_ELEMENT_SELECTOR,
@@ -219,9 +220,10 @@ export const parseSelector = sel => {
219
220
  * about its contents.
220
221
  * @param {object} ast - The AST to traverse.
221
222
  * @param {boolean} toObject - True if converts ast to object, false otherwise.
223
+ * @param {function(object): void} [callback] - Optional callback for each node.
222
224
  * @returns {{branches: Array<object>, info: object}} An object containing the selector branches and info.
223
225
  */
224
- export const walkAST = (ast = {}, toObject = false) => {
226
+ export const walkAST = (ast = {}, toObject = false, callback = null) => {
225
227
  const branches = new Set();
226
228
  const info = {
227
229
  hasForgivenPseudoFunc: false,
@@ -230,10 +232,14 @@ export const walkAST = (ast = {}, toObject = false) => {
230
232
  hasNotPseudoFunc: false,
231
233
  hasNthChildOfSelector: false,
232
234
  hasNestedSelector: false,
233
- hasStatePseudoClass: false
235
+ hasStatePseudoClass: false,
236
+ hasUnsupportedPseudoClass: false
234
237
  };
235
238
  const opt = {
236
239
  enter(node) {
240
+ if (typeof callback === 'function') {
241
+ callback(node);
242
+ }
237
243
  switch (node.type) {
238
244
  case CLASS_SELECTOR: {
239
245
  if (/^-?\d/.test(node.name)) {
@@ -254,24 +260,27 @@ export const walkAST = (ast = {}, toObject = false) => {
254
260
  break;
255
261
  }
256
262
  case PS_CLASS_SELECTOR: {
257
- if (KEYS_LOGICAL.has(node.name)) {
263
+ const name = node.name.toLowerCase();
264
+ if (KEYS_LOGICAL.has(name)) {
258
265
  info.hasNestedSelector = true;
259
266
  info.hasLogicalPseudoFunc = true;
260
- if (node.name === 'has') {
267
+ if (name === 'has') {
261
268
  info.hasHasPseudoFunc = true;
262
- } else if (node.name === 'not') {
269
+ } else if (name === 'not') {
263
270
  info.hasNotPseudoFunc = true;
264
271
  } else {
265
272
  info.hasForgivenPseudoFunc = true;
266
273
  }
267
- } else if (KEYS_PS_CLASS_STATE.has(node.name)) {
274
+ } else if (KEYS_PS_CLASS_STATE.has(name)) {
268
275
  info.hasStatePseudoClass = true;
269
276
  } else if (
270
- KEYS_SHADOW_HOST.has(node.name) &&
277
+ KEYS_SHADOW_HOST.has(name) &&
271
278
  Array.isArray(node.children) &&
272
279
  node.children.length
273
280
  ) {
274
281
  info.hasNestedSelector = true;
282
+ } else if (!KEYS_PS_CLASS_SUPPORTED.has(name)) {
283
+ info.hasUnsupportedPseudoClass = true;
275
284
  }
276
285
  break;
277
286
  }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * selector.js
3
+ */
4
+
5
+ /* import */
6
+ import * as cssTree from 'css-tree';
7
+ import { generateException } from './utility.js';
8
+
9
+ /* constants */
10
+ import {
11
+ COMBINATOR,
12
+ COMBO,
13
+ COMPOUND_I,
14
+ COMPOUND_L_I,
15
+ DESCEND,
16
+ HAS_COMPOUND,
17
+ KEYS_LOGICAL,
18
+ KEYS_PS_CLASS_SUPPORTED,
19
+ LOGIC_COMPLEX,
20
+ LOGIC_COMPOUND,
21
+ N_TH,
22
+ PSEUDO_CLASS,
23
+ PS_CLASS_SELECTOR,
24
+ PS_ELEMENT_SELECTOR,
25
+ SELECTOR,
26
+ SYNTAX_ERR,
27
+ TARGET_ALL
28
+ } from './constant.js';
29
+
30
+ /* regexp */
31
+ const REG_EXCLUDE_BASIC =
32
+ /[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
33
+ const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
34
+ const REG_COMPOUND = new RegExp(`^${COMPOUND_L_I}$`, 'i');
35
+ const REG_DESCEND = new RegExp(`${COMPOUND_I}${DESCEND}${COMPOUND_I}`, 'i');
36
+ const REG_LOGIC_COMPLEX = new RegExp(
37
+ `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
38
+ );
39
+ const REG_LOGIC_COMPOUND = new RegExp(
40
+ `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
41
+ );
42
+ const REG_LOGIC_HAS_COMPOUND = new RegExp(
43
+ `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
44
+ );
45
+ const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
46
+ const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
47
+ const REG_COMBO = new RegExp(COMBO);
48
+ const REG_ID = /#(\D[^#.*]+)/g;
49
+ const REG_CLASS = /\.(\D[^#.*]+)/g;
50
+ const REG_TAG = /^([^#.]+)/;
51
+ const REG_INVALID_SYNTAX =
52
+ /[+~>]\s*[+~>]|^\s*[+~>]|[+~>]\s*$|^\s*,|,\s*,|,\s*$/;
53
+
54
+ /**
55
+ * Find a nested :has() pseudo-class.
56
+ * @param {object} leaf - The AST leaf to check.
57
+ * @returns {?object} The leaf if it's :has, otherwise null.
58
+ */
59
+ export const findNestedHas = leaf => leaf.name === 'has';
60
+
61
+ /**
62
+ * Find a logical pseudo-class that contains a nested :has().
63
+ * @param {object} leaf - The AST leaf to check.
64
+ * @returns {?object} The leaf if it matches, otherwise null.
65
+ */
66
+ export const findLogicalWithNestedHas = leaf => {
67
+ if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
68
+ return leaf;
69
+ }
70
+ return null;
71
+ };
72
+
73
+ /**
74
+ * Validates nesting restrictions within :has() arguments.
75
+ * @param {Array<object>} astChildren - The AST nodes representing the :has() arguments.
76
+ * @returns {boolean} False if there's an invalid nesting constraint violation.
77
+ */
78
+ export const validateHasNesting = astChildren => {
79
+ const l = astChildren.length;
80
+ for (let i = 0; i < l; i++) {
81
+ const item = cssTree.find(astChildren[i], findLogicalWithNestedHas);
82
+ if (item) {
83
+ // If nested :has() is wrapped inside :is() or :where(), it is forgiven.
84
+ if (item.name !== 'is' && item.name !== 'where') {
85
+ return false;
86
+ }
87
+ }
88
+ }
89
+ return true;
90
+ };
91
+
92
+ /**
93
+ * Creates a callback function to validate :has() nesting during AST walk.
94
+ * @param {object} globalObj - The global window object.
95
+ * @returns {function(object): void} The callback function for walkAST.
96
+ */
97
+ export const createHasValidator = globalObj => node => {
98
+ if (
99
+ node.type === PS_CLASS_SELECTOR &&
100
+ node.name.toLowerCase() === 'has' &&
101
+ !validateHasNesting(Array.from(node.children || []))
102
+ ) {
103
+ const css = cssTree.generate(node);
104
+ throw generateException(
105
+ `Disallowed nested :has() pseudo-class: ${css}`,
106
+ SYNTAX_ERR,
107
+ globalObj
108
+ );
109
+ }
110
+ };
111
+
112
+ /**
113
+ * Check if a combinator node is invalid (leading, trailing, or consecutive).
114
+ * @param {string} type - The current node type.
115
+ * @param {string|null} prevType - The previous node type.
116
+ * @param {boolean} isLast - Whether the current node is the last in the list.
117
+ * @returns {boolean} True if the combinator is invalid.
118
+ */
119
+ export const isInvalidCombinator = (type, prevType, isLast) =>
120
+ type === COMBINATOR &&
121
+ (prevType === null || prevType === COMBINATOR || isLast);
122
+
123
+ /**
124
+ * Checks if a given AST is supported by the DOMSelector engine.
125
+ * @param {object} ast - The AST to validate.
126
+ * @returns {boolean} True if the selector is fully supported.
127
+ */
128
+ export const isSupportedAST = ast => {
129
+ let isSupported = true;
130
+ const walk = (
131
+ node,
132
+ context = { insideHas: false, insideForgiving: false }
133
+ ) => {
134
+ if (!isSupported || !node) {
135
+ return;
136
+ }
137
+ const nextContext = { ...context };
138
+ if (node.type === PS_ELEMENT_SELECTOR) {
139
+ isSupported = false;
140
+ return;
141
+ } else if (node.type === PS_CLASS_SELECTOR) {
142
+ let name = node.name;
143
+ if (name && typeof name === 'string') {
144
+ name = name.toLowerCase();
145
+ }
146
+ if (!KEYS_PS_CLASS_SUPPORTED.has(name)) {
147
+ isSupported = false;
148
+ return;
149
+ }
150
+ if (name === 'has') {
151
+ if (context.insideHas && !context.insideForgiving) {
152
+ isSupported = false;
153
+ return;
154
+ }
155
+ nextContext.insideHas = true;
156
+ } else if (name === 'is' || name === 'where') {
157
+ nextContext.insideForgiving = true;
158
+ }
159
+ }
160
+ if (node.children) {
161
+ let prevType = null;
162
+ if (node.children.head !== undefined) {
163
+ let current = node.children.head;
164
+ while (current) {
165
+ if (!current.data) {
166
+ current = current.next;
167
+ continue;
168
+ }
169
+ const childType = current.data.type;
170
+ if (
171
+ node.type === SELECTOR &&
172
+ isInvalidCombinator(childType, prevType, !current.next)
173
+ ) {
174
+ if (!(prevType === null && context.insideHas)) {
175
+ isSupported = false;
176
+ return;
177
+ }
178
+ }
179
+ prevType = childType;
180
+ walk(current.data, nextContext);
181
+ if (!isSupported) {
182
+ return;
183
+ }
184
+ current = current.next;
185
+ }
186
+ } else if (Array.isArray(node.children)) {
187
+ const l = node.children.length;
188
+ for (let i = 0; i < l; i++) {
189
+ const child = node.children[i];
190
+ if (!child) {
191
+ continue;
192
+ }
193
+ const childType = child.type;
194
+ if (
195
+ node.type === SELECTOR &&
196
+ isInvalidCombinator(childType, prevType, i === l - 1)
197
+ ) {
198
+ if (!(prevType === null && context.insideHas)) {
199
+ isSupported = false;
200
+ return;
201
+ }
202
+ }
203
+ prevType = childType;
204
+ walk(child, nextContext);
205
+ if (!isSupported) {
206
+ return;
207
+ }
208
+ }
209
+ }
210
+ }
211
+ if (node.selector) {
212
+ walk(node.selector, nextContext);
213
+ }
214
+ };
215
+ walk(ast);
216
+ return isSupported;
217
+ };
218
+
219
+ /**
220
+ * Extracts the rightmost subject keys (id, class, tag) from a selector.
221
+ * @param {string} selector - The CSS selector string to parse.
222
+ * @param {boolean} caseSensitive - True if the tag should be case-sensitive.
223
+ * @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
224
+ */
225
+ export const extractSubjectsRegExp = (selector, caseSensitive) => {
226
+ const subjects = [];
227
+ const groups = selector.split(',');
228
+ for (let i = 0; i < groups.length; i++) {
229
+ const group = groups[i].trim();
230
+ if (!group) {
231
+ continue;
232
+ }
233
+ const compounds = group.split(REG_COMBO);
234
+ const rightmost = compounds[compounds.length - 1];
235
+ let idKey = null;
236
+ let classKey = null;
237
+ let tagKey = null;
238
+ if (rightmost) {
239
+ const idMatch = rightmost.match(REG_ID);
240
+ if (idMatch) {
241
+ idKey = idMatch[idMatch.length - 1].slice(1);
242
+ }
243
+ const classMatch = rightmost.match(REG_CLASS);
244
+ if (classMatch) {
245
+ classKey = classMatch[classMatch.length - 1].slice(1);
246
+ }
247
+ const tagMatch = rightmost.match(REG_TAG);
248
+ if (tagMatch) {
249
+ const tag = tagMatch[1];
250
+ if (tag !== '*') {
251
+ tagKey = caseSensitive ? tag : tag.toLowerCase();
252
+ }
253
+ }
254
+ }
255
+ subjects.push({ id: idKey, className: classKey, tag: tagKey });
256
+ }
257
+ return subjects;
258
+ };
259
+
260
+ /**
261
+ * Filter a selector for use with nwsapi.
262
+ * @param {string} selector - The selector string.
263
+ * @param {string} target - The target type.
264
+ * @returns {boolean} - True if the selector is valid for nwsapi.
265
+ */
266
+ export const filterSelector = (selector, target) => {
267
+ const isQuerySelectorAll = target === TARGET_ALL;
268
+ // Basic validation and fast-fail for null/undefined/non-string values.
269
+ if (
270
+ !selector ||
271
+ typeof selector !== 'string' ||
272
+ /null|undefined/.test(selector)
273
+ ) {
274
+ return false;
275
+ }
276
+ // Validate syntax.
277
+ if (REG_INVALID_SYNTAX.test(selector)) {
278
+ return false;
279
+ }
280
+ // Exclude various complex or unsupported selectors early.
281
+ // i.e. non-ASCII, escaped selectors, namespaced selectors, pseudo-elements.
282
+ if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
283
+ return false;
284
+ }
285
+ // Validate attribute selector integrity.
286
+ if (selector.includes('[')) {
287
+ const index = selector.lastIndexOf('[');
288
+ if (selector.indexOf(']', index) === -1) {
289
+ return false;
290
+ }
291
+ }
292
+ // Target-specific early exits.
293
+ if (target === TARGET_ALL && !REG_COMPOUND.test(selector)) {
294
+ return false;
295
+ }
296
+ // Logic for pseudo-classes.
297
+ if (selector.includes(':')) {
298
+ // Exclude descendant combinators in logical selectors for querySelectorAll.
299
+ if (isQuerySelectorAll && REG_DESCEND.test(selector)) {
300
+ return false;
301
+ }
302
+ // Determine if the selector has complex logical structures.
303
+ const isComplex = isQuerySelectorAll ? false : REG_COMPLEX.test(selector);
304
+ // Handle :has() specifically.
305
+ if (selector.includes(':has(')) {
306
+ if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
307
+ return false;
308
+ }
309
+ return REG_END_WITH_HAS.test(selector);
310
+ }
311
+ // Handle :is() and :not().
312
+ if (/(?:is|not)\(/.test(selector)) {
313
+ if (isComplex) {
314
+ return !REG_LOGIC_COMPLEX.test(selector);
315
+ } else {
316
+ return !REG_LOGIC_COMPOUND.test(selector);
317
+ }
318
+ }
319
+ // Default check for other pseudo-classes against known list.
320
+ if (REG_WO_LOGICAL.test(selector)) {
321
+ return false;
322
+ }
323
+ }
324
+ return true;
325
+ };