@asamuzakjp/dom-selector 8.2.0 → 8.2.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/package.json CHANGED
@@ -25,20 +25,20 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@asamuzakjp/generational-cache": "^3.0.1",
29
28
  "bidi-js": "^1.0.3",
30
29
  "css-tree": "^3.2.1",
31
- "is-potential-custom-element-name": "^1.0.1"
30
+ "is-potential-custom-element-name": "^1.0.1",
31
+ "lru-cache": "^11.5.2"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/css-tree": "^2.3.11",
35
- "@types/node": "^26.0.1",
35
+ "@types/node": "^26.1.1",
36
36
  "c8": "^11.0.0",
37
37
  "chai": "^6.2.2",
38
38
  "commander": "^15.0.0",
39
- "eslint": "^9.39.4",
39
+ "eslint": "^9.39.5",
40
40
  "eslint-config-prettier": "^10.1.8",
41
- "eslint-plugin-jsdoc": "^63.0.10",
41
+ "eslint-plugin-jsdoc": "^63.0.13",
42
42
  "eslint-plugin-prettier": "^5.5.6",
43
43
  "eslint-plugin-regexp": "^3.1.1",
44
44
  "eslint-plugin-unicorn": "^65.0.1",
@@ -47,7 +47,7 @@
47
47
  "mitata": "^1.0.34",
48
48
  "mocha": "^11.7.6",
49
49
  "neostandard": "^0.13.0",
50
- "prettier": "^3.9.1",
50
+ "prettier": "^3.9.5",
51
51
  "sinon": "^22.0.0",
52
52
  "tinybench": "^6.0.2",
53
53
  "typescript": "^6.0.3",
@@ -88,5 +88,5 @@
88
88
  "engines": {
89
89
  "node": "^22.13.0 || >=24.0.0"
90
90
  },
91
- "version": "8.2.0"
91
+ "version": "8.2.1"
92
92
  }
package/src/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  /* import */
9
- import { GenerationalCache } from '@asamuzakjp/generational-cache';
9
+ import { LRUCache } from 'lru-cache';
10
10
  import { Finder } from './js/finder.js';
11
11
  import { Nwsapi } from './js/nwsapi.js';
12
12
  import { extractSubjectsAst } from './js/parser.js';
@@ -26,7 +26,7 @@ import {
26
26
  TARGET_LINEAL,
27
27
  TARGET_SELF
28
28
  } from './js/constant.js';
29
- const CACHE_SIZE = 2048;
29
+ const CACHE_SIZE = 4096;
30
30
 
31
31
  /* regexp */
32
32
  const REG_SELECTOR = /[[\]():\\"'`]/;
@@ -60,11 +60,11 @@ export class DOMSelector {
60
60
  this.#window = window;
61
61
  this.#document = document ?? window.document;
62
62
  this.#idlUtils = idlUtils;
63
- this.#cache = new GenerationalCache(cacheSize ?? CACHE_SIZE, {
64
- strictvalidate: false
63
+ this.#cache = new LRUCache({
64
+ max: cacheSize ?? CACHE_SIZE
65
65
  });
66
66
  this.#finder = new Finder(this.#window);
67
- this.#nwsapi = new Nwsapi(this.#window, this.#document);
67
+ this.#nwsapi = new Nwsapi(this.#window, this.#document, cacheSize);
68
68
  }
69
69
 
70
70
  /**
@@ -7,6 +7,8 @@ export const ATRULE = 'Atrule';
7
7
  export const ATTR_SELECTOR = 'AttributeSelector';
8
8
  export const CLASS_SELECTOR = 'ClassSelector';
9
9
  export const COMBINATOR = 'Combinator';
10
+ export const DIR_NEXT = 'next';
11
+ export const DIR_PREV = 'prev';
10
12
  export const IDENT = 'Identifier';
11
13
  export const ID_SELECTOR = 'IdSelector';
12
14
  export const NOT_SUPPORTED_ERR = 'NotSupportedError';
@@ -19,7 +19,6 @@ import {
19
19
  isCustomElement,
20
20
  isFocusVisible,
21
21
  isFocusableArea,
22
- isVisible,
23
22
  populateHasAllowlist,
24
23
  resolveContent,
25
24
  traverseNode
@@ -30,6 +29,8 @@ import {
30
29
  ATTR_SELECTOR,
31
30
  CLASS_SELECTOR,
32
31
  COMBINATOR,
32
+ DIR_NEXT,
33
+ DIR_PREV,
33
34
  DOCUMENT_FRAGMENT_NODE,
34
35
  ELEMENT_NODE,
35
36
  FORM_PARTS,
@@ -49,8 +50,6 @@ import {
49
50
  TEXT_NODE,
50
51
  TYPE_SELECTOR
51
52
  } from './constant.js';
52
- const DIR_NEXT = 'next';
53
- const DIR_PREV = 'prev';
54
53
  const KEYS_FORM = new Set([...FORM_PARTS, 'fieldset', 'form']);
55
54
  const KEYS_FORM_PS_VALID = new Set([...FORM_PARTS, 'form']);
56
55
  const KEYS_INPUT_CHECK = new Set(INPUT_CHECK);
@@ -141,6 +140,33 @@ export class Evaluator {
141
140
  this._registerEventListeners();
142
141
  }
143
142
 
143
+ /**
144
+ * Sets up the evaluator.
145
+ * @param {string} selector - The CSS selector.
146
+ * @param {object} node - Document, DocumentFragment, or Element.
147
+ * @param {object} [opt] - Options.
148
+ * @param {boolean} [opt.check] - Indicates if running in internal check().
149
+ * @param {boolean} [opt.noexcept] - If true, exceptions are not thrown.
150
+ * @param {boolean} [opt.warn] - If true, console warnings are enabled.
151
+ * @returns {object} The evaluator instance.
152
+ */
153
+ setup(selector, node, opt = {}) {
154
+ const { check, noexcept, warn } = opt;
155
+ this.check = !!check;
156
+ this.noexcept = !!noexcept;
157
+ this.warn = !!warn;
158
+ this.matchOpts = { warn: this.warn };
159
+ [this.document, this.root, this.shadow] = resolveContent(node);
160
+ this.node = node;
161
+ this.pseudoElements = [];
162
+ this.invalidate = false;
163
+ this.clearResults();
164
+ this.#documentURL = null;
165
+ this.#verifyShadowHost = false;
166
+ this.#walkers = null;
167
+ return this;
168
+ }
169
+
144
170
  /**
145
171
  * Handles errors.
146
172
  * @param {Error} e - The error object.
@@ -171,39 +197,12 @@ export class Evaluator {
171
197
  throw e;
172
198
  };
173
199
 
174
- /**
175
- * Sets up the evaluator.
176
- * @param {string} selector - The CSS selector.
177
- * @param {object} node - Document, DocumentFragment, or Element.
178
- * @param {object} [opt] - Options.
179
- * @param {boolean} [opt.check] - Indicates if running in internal check().
180
- * @param {boolean} [opt.noexcept] - If true, exceptions are not thrown.
181
- * @param {boolean} [opt.warn] - If true, console warnings are enabled.
182
- * @returns {object} The matcher instance.
183
- */
184
- setup(selector, node, opt = {}) {
185
- const { check, noexcept, warn } = opt;
186
- this.check = !!check;
187
- this.noexcept = !!noexcept;
188
- this.warn = !!warn;
189
- this.matchOpts = { warn: this.warn };
190
- [this.document, this.root, this.shadow] = resolveContent(node);
191
- this.node = node;
192
- this.pseudoElements = [];
193
- this.invalidate = false;
194
- this.clearResults();
195
- this.#documentURL = null;
196
- this.#verifyShadowHost = false;
197
- this.#walkers = null;
198
- return this;
199
- }
200
-
201
200
  /**
202
201
  * Clear cached results.
203
202
  * @param {boolean} all - Clear all results.
204
203
  * @returns {void}
205
204
  */
206
- clearResults(all = false) {
205
+ clearResults = (all = false) => {
207
206
  this.#anbCache = null;
208
207
  this.#focusWithinCache = null;
209
208
  this.#invalidateResults = null;
@@ -217,7 +216,7 @@ export class Evaluator {
217
216
  this.#filterLeavesCache = null;
218
217
  this.#results = new WeakMap();
219
218
  }
220
- }
219
+ };
221
220
 
222
221
  /**
223
222
  * Matches a selector.
@@ -253,9 +252,11 @@ export class Evaluator {
253
252
  }
254
253
  const results = this.invalidate ? this.#invalidateResults : this.#results;
255
254
  let result = results.get(leaves);
256
- if (result && result.has(node)) {
257
- const { matched } = result.get(node);
258
- return matched;
255
+ if (result) {
256
+ const nodeResult = result.get(node);
257
+ if (nodeResult) {
258
+ return nodeResult.matched;
259
+ }
259
260
  }
260
261
  let cacheable = true;
261
262
  if (node.nodeType === ELEMENT_NODE && KEYS_FORM.has(node.localName)) {
@@ -307,10 +308,11 @@ export class Evaluator {
307
308
  if (!this.#filterLeavesCache) {
308
309
  this.#filterLeavesCache = new WeakMap();
309
310
  }
310
- if (this.#filterLeavesCache.has(leaves)) {
311
- return this.#filterLeavesCache.get(leaves);
311
+ let filterLeaves = this.#filterLeavesCache.get(leaves);
312
+ if (filterLeaves) {
313
+ return filterLeaves;
312
314
  }
313
- const filterLeaves = leaves.slice(1);
315
+ filterLeaves = leaves.slice(1);
314
316
  this.#filterLeavesCache.set(leaves, filterLeaves);
315
317
  return filterLeaves;
316
318
  };
@@ -518,7 +520,7 @@ export class Evaluator {
518
520
  this.#psDefaultCache = new WeakMap();
519
521
  }
520
522
  let defaultSubmit = this.#psDefaultCache.get(form);
521
- if (!defaultSubmit) {
523
+ if (defaultSubmit === undefined) {
522
524
  const walker = this.createTreeWalker(form, { force: true });
523
525
  let refNode = traverseNode(form, walker);
524
526
  refNode = walker.firstChild();
@@ -587,7 +589,7 @@ export class Evaluator {
587
589
  this.#psIndeterminateCache = new WeakMap();
588
590
  }
589
591
  let parentCache = this.#psIndeterminateCache.get(parent);
590
- if (!parentCache) {
592
+ if (parentCache === undefined) {
591
593
  parentCache = new Map();
592
594
  this.#psIndeterminateCache.set(parent, parentCache);
593
595
  }
@@ -644,7 +646,7 @@ export class Evaluator {
644
646
  this.#psValidCache = new WeakMap();
645
647
  }
646
648
  let valid = this.#psValidCache.get(node);
647
- if (valid === undefined && !this.#psValidCache.has(node)) {
649
+ if (valid === undefined) {
648
650
  const walker = this.createTreeWalker(node, { force: true });
649
651
  let refNode = traverseNode(node, walker);
650
652
  refNode = walker.firstChild();
@@ -919,6 +921,9 @@ export class Evaluator {
919
921
  if (!this.#focusWithinCache) {
920
922
  this.#focusWithinCache = new Set();
921
923
  let currentFocus = this.document.activeElement;
924
+ while (currentFocus?.shadowRoot?.activeElement) {
925
+ currentFocus = currentFocus.shadowRoot.activeElement;
926
+ }
922
927
  if (currentFocus && isFocusableArea(currentFocus)) {
923
928
  while (currentFocus) {
924
929
  this.#focusWithinCache.add(currentFocus);
@@ -933,23 +938,6 @@ export class Evaluator {
933
938
  break;
934
939
  }
935
940
  }
936
- } else if (currentFocus && currentFocus.shadowRoot) {
937
- let shadowFocus = currentFocus.shadowRoot.activeElement;
938
- if (shadowFocus) {
939
- while (shadowFocus) {
940
- this.#focusWithinCache.add(shadowFocus);
941
- if (shadowFocus.parentNode) {
942
- shadowFocus = shadowFocus.parentNode;
943
- } else if (
944
- shadowFocus.nodeType === DOCUMENT_FRAGMENT_NODE &&
945
- shadowFocus.host
946
- ) {
947
- shadowFocus = shadowFocus.host;
948
- } else {
949
- break;
950
- }
951
- }
952
- }
953
941
  }
954
942
  }
955
943
  return this.#focusWithinCache.has(node);
@@ -1047,10 +1035,11 @@ export class Evaluator {
1047
1035
  if (!this.#walkers) {
1048
1036
  this.#walkers = new WeakMap();
1049
1037
  }
1050
- if (this.#walkers.has(node)) {
1051
- return this.#walkers.get(node);
1038
+ let walker = this.#walkers.get(node);
1039
+ if (walker) {
1040
+ return walker;
1052
1041
  }
1053
- const walker = this.document.createTreeWalker(node, whatToShow);
1042
+ walker = this.document.createTreeWalker(node, whatToShow);
1054
1043
  this.#walkers.set(node, walker);
1055
1044
  return walker;
1056
1045
  };
@@ -1116,7 +1105,11 @@ export class Evaluator {
1116
1105
  case ' ':
1117
1106
  default: {
1118
1107
  if (dir === DIR_NEXT) {
1119
- for (const refNode of this._findDescendantNodes(leaves, node, opt)) {
1108
+ for (const refNode of this.yieldFindDescendantNodes(
1109
+ leaves,
1110
+ node,
1111
+ opt
1112
+ )) {
1120
1113
  yield refNode;
1121
1114
  }
1122
1115
  } else {
@@ -1138,6 +1131,99 @@ export class Evaluator {
1138
1131
  }
1139
1132
  }
1140
1133
 
1134
+ /**
1135
+ * Traverses all descendant nodes and yields matches.
1136
+ * @param {object} baseNode - The base Element node or Element.shadowRoot.
1137
+ * @param {Array.<object>} leaves - The AST leaves.
1138
+ * @param {object} opt - Options.
1139
+ * @yields {object} The matched node.
1140
+ */
1141
+ *yieldTraverseAllDescendants(baseNode, leaves, opt) {
1142
+ const walker = this.createTreeWalker(baseNode);
1143
+ traverseNode(baseNode, walker);
1144
+ let currentNode = walker.firstChild();
1145
+ while (currentNode) {
1146
+ if (this.matchLeaves(leaves, currentNode, opt)) {
1147
+ yield currentNode;
1148
+ }
1149
+ currentNode = walker.nextNode();
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Finds descendant nodes and yields matches.
1155
+ * @param {Array.<object>} leaves - The AST leaves.
1156
+ * @param {object} baseNode - The base Element node or Element.shadowRoot.
1157
+ * @param {object} opt - Options.
1158
+ * @yields {object} The matched node.
1159
+ */
1160
+ *yieldFindDescendantNodes(leaves, baseNode, opt) {
1161
+ const [{ name, type: leafType }] = leaves;
1162
+ const leafName = unescapeSelector(name);
1163
+ const filterLeaves = this.getFilterLeaves(leaves);
1164
+ const isSimple = filterLeaves.length === 0;
1165
+
1166
+ switch (leafType) {
1167
+ case ID_SELECTOR: {
1168
+ if (
1169
+ !this.shadow &&
1170
+ baseNode.nodeType === ELEMENT_NODE &&
1171
+ this.root.nodeType !== ELEMENT_NODE
1172
+ ) {
1173
+ const foundNode = this.root.getElementById(leafName);
1174
+ if (
1175
+ foundNode &&
1176
+ foundNode !== baseNode &&
1177
+ baseNode.contains(foundNode)
1178
+ ) {
1179
+ if (isSimple || this.matchLeaves(filterLeaves, foundNode, opt)) {
1180
+ yield foundNode;
1181
+ }
1182
+ }
1183
+ return;
1184
+ }
1185
+ break;
1186
+ }
1187
+ case CLASS_SELECTOR: {
1188
+ if (typeof baseNode.getElementsByClassName === 'function') {
1189
+ const collection = baseNode.getElementsByClassName(leafName);
1190
+ for (let i = 0, len = collection.length; i < len; i++) {
1191
+ const foundNode = collection[i];
1192
+ if (isSimple || this.matchLeaves(filterLeaves, foundNode, opt)) {
1193
+ yield foundNode;
1194
+ }
1195
+ }
1196
+ return;
1197
+ }
1198
+ break;
1199
+ }
1200
+ case TYPE_SELECTOR: {
1201
+ if (
1202
+ typeof baseNode.getElementsByTagName === 'function' &&
1203
+ !leafName.includes('|')
1204
+ ) {
1205
+ const collection = baseNode.getElementsByTagName(leafName);
1206
+ for (let i = 0, len = collection.length; i < len; i++) {
1207
+ const foundNode = collection[i];
1208
+ if (isSimple || this.matchLeaves(filterLeaves, foundNode, opt)) {
1209
+ yield foundNode;
1210
+ }
1211
+ }
1212
+ return;
1213
+ }
1214
+ break;
1215
+ }
1216
+ case PS_ELEMENT_SELECTOR: {
1217
+ matchPseudoElementSelector(leafName, leafType, opt);
1218
+ return;
1219
+ }
1220
+ default: {
1221
+ // no-op
1222
+ }
1223
+ }
1224
+ yield* this.yieldTraverseAllDescendants(baseNode, leaves, opt);
1225
+ }
1226
+
1141
1227
  /**
1142
1228
  * Handles focus events.
1143
1229
  * @private
@@ -1201,16 +1287,37 @@ export class Evaluator {
1201
1287
  * @returns {Array.<Array.<object>>} The selector branches.
1202
1288
  */
1203
1289
  _getSelectorBranches = selector => {
1204
- if (this.#astCache.has(selector)) {
1205
- return this.#astCache.get(selector);
1290
+ let branches = this.#astCache.get(selector);
1291
+ if (branches) {
1292
+ return branches;
1206
1293
  }
1207
- const { branches } = walkAST(selector);
1294
+ const walkedResult = walkAST(selector);
1295
+ branches = walkedResult.branches;
1208
1296
  this.#astCache.set(selector, branches);
1209
1297
  return branches;
1210
1298
  };
1211
1299
 
1212
1300
  /**
1213
- * Evaluates An+B mathematically (O(1) without generating new arrays/sets).
1301
+ * Checks if a node matches any of the given selector branches.
1302
+ * @private
1303
+ * @param {Array.<Array.<object>>} branches - The selector branches to test.
1304
+ * @param {object} node - The element node to match against.
1305
+ * @param {object} [opt] - Optional parameters.
1306
+ * @returns {boolean} True if any branch matches, otherwise false.
1307
+ */
1308
+ _filterNthChildOfSelectorBranches = (branches, node, opt) => {
1309
+ let filterMatch = false;
1310
+ for (const branch of branches) {
1311
+ if (this.matchLeaves(branch, node, opt)) {
1312
+ filterMatch = true;
1313
+ break;
1314
+ }
1315
+ }
1316
+ return filterMatch;
1317
+ };
1318
+
1319
+ /**
1320
+ * Evaluates An+B mathematically.
1214
1321
  * @private
1215
1322
  * @param {object} ast - The AST.
1216
1323
  * @param {object} node - The Element node.
@@ -1219,16 +1326,32 @@ export class Evaluator {
1219
1326
  * @returns {boolean} True if matches, otherwise false.
1220
1327
  */
1221
1328
  _matchAnPlusB = (ast, node, nthName, opt) => {
1329
+ const {
1330
+ localName,
1331
+ namespaceURI,
1332
+ nextElementSibling,
1333
+ parentNode,
1334
+ previousElementSibling
1335
+ } = node;
1336
+ if (!parentNode && node !== this.root) {
1337
+ return false;
1338
+ }
1222
1339
  if (!this.#anbCache) {
1223
1340
  this.#anbCache = new WeakMap();
1224
1341
  }
1225
1342
  let anb = this.#anbCache.get(ast);
1226
- if (!anb) {
1343
+ if (anb === undefined) {
1227
1344
  const {
1228
1345
  nth: { a, b, name: nthIdentName },
1229
1346
  selector
1230
1347
  } = ast;
1231
- anb = { a: 0, b: 0, selector: null };
1348
+ anb = {
1349
+ a: 0,
1350
+ b: 0,
1351
+ isLast: nthName.includes('last'),
1352
+ isOfType: nthName.includes('of-type'),
1353
+ selector: null
1354
+ };
1232
1355
  if (nthIdentName) {
1233
1356
  if (nthIdentName === 'even') {
1234
1357
  anb.a = 2;
@@ -1238,78 +1361,62 @@ export class Evaluator {
1238
1361
  anb.b = 1;
1239
1362
  }
1240
1363
  } else {
1241
- anb.a = typeof a === 'string' && /-?\d+/.test(a) ? a * 1 : 0;
1242
- anb.b = typeof b === 'string' && /-?\d+/.test(b) ? b * 1 : 0;
1364
+ const intA = parseInt(a);
1365
+ if (Number.isInteger(intA)) {
1366
+ anb.a = intA;
1367
+ }
1368
+ const intB = parseInt(b);
1369
+ if (Number.isInteger(intB)) {
1370
+ anb.b = intB;
1371
+ }
1243
1372
  }
1244
- if (
1245
- selector &&
1246
- (nthName === 'nth-child' || nthName === 'nth-last-child')
1247
- ) {
1373
+ if (selector && /^nth-(?:last-)?child$/.test(nthName)) {
1248
1374
  anb.selector = selector;
1249
1375
  }
1250
1376
  this.#anbCache.set(ast, anb);
1251
1377
  }
1252
- if (
1253
- nthName !== 'nth-child' &&
1254
- nthName !== 'nth-last-child' &&
1255
- nthName !== 'nth-of-type' &&
1256
- nthName !== 'nth-last-of-type'
1257
- ) {
1258
- return false;
1259
- }
1260
- const isLast = nthName.includes('last');
1261
- const isOfType = nthName.includes('of-type');
1262
- const hasFilter = !!anb.selector;
1263
- if (hasFilter) {
1264
- const selectorBranches = this._getSelectorBranches(anb.selector);
1265
- let filterMatch = false;
1266
- for (let i = 0; i < selectorBranches.length; i++) {
1267
- if (this.matchLeaves(selectorBranches[i], node, opt)) {
1268
- filterMatch = true;
1269
- break;
1270
- }
1271
- }
1378
+ const { a, b, isLast, isOfType, selector: anbSelector } = anb;
1379
+ const startNode = isLast ? nextElementSibling : previousElementSibling;
1380
+ let pos = 1;
1381
+ if (anbSelector) {
1382
+ const selectorBranches = this._getSelectorBranches(anbSelector);
1383
+ const filterMatch = this._filterNthChildOfSelectorBranches(
1384
+ selectorBranches,
1385
+ node,
1386
+ opt
1387
+ );
1272
1388
  if (!filterMatch) {
1273
1389
  return false;
1274
1390
  }
1275
- }
1276
- const { parentNode } = node;
1277
- if (!parentNode && node !== this.root) {
1278
- return false;
1279
- }
1280
- let pos = 1;
1281
- let current = isLast
1282
- ? node.nextElementSibling
1283
- : node.previousElementSibling;
1284
- while (current) {
1285
- let match = true;
1286
- if (isOfType) {
1287
- match =
1288
- current.localName === node.localName &&
1289
- current.namespaceURI === node.namespaceURI;
1290
- } else if (hasFilter) {
1291
- const selectorBranches = this._getSelectorBranches(anb.selector);
1292
- let filterMatch = false;
1293
- for (let i = 0; i < selectorBranches.length; i++) {
1294
- if (this.matchLeaves(selectorBranches[i], current, opt)) {
1295
- filterMatch = true;
1296
- break;
1297
- }
1391
+ let current = startNode;
1392
+ while (current) {
1393
+ if (
1394
+ this._filterNthChildOfSelectorBranches(selectorBranches, current, opt)
1395
+ ) {
1396
+ pos++;
1298
1397
  }
1299
- if (filterMatch && (this.node === current || isVisible(current))) {
1300
- match = true;
1398
+ current = isLast
1399
+ ? current.nextElementSibling
1400
+ : current.previousElementSibling;
1401
+ }
1402
+ } else {
1403
+ let current = startNode;
1404
+ while (current) {
1405
+ if (isOfType) {
1406
+ if (
1407
+ current.localName === localName &&
1408
+ current.namespaceURI === namespaceURI
1409
+ ) {
1410
+ pos++;
1411
+ }
1301
1412
  } else {
1302
- match = false;
1413
+ pos++;
1303
1414
  }
1415
+ current = isLast
1416
+ ? current.nextElementSibling
1417
+ : current.previousElementSibling;
1304
1418
  }
1305
- if (match) {
1306
- pos++;
1307
- }
1308
- current = isLast
1309
- ? current.nextElementSibling
1310
- : current.previousElementSibling;
1311
1419
  }
1312
- const { a, b } = anb;
1313
1420
  if (a === 0) {
1314
1421
  return pos === b;
1315
1422
  }
@@ -1576,7 +1683,7 @@ export class Evaluator {
1576
1683
  this.#psHasFilterCache = new WeakMap();
1577
1684
  }
1578
1685
  let rootCache = this.#psHasFilterCache.get(this.root);
1579
- if (!rootCache) {
1686
+ if (rootCache === undefined) {
1580
1687
  rootCache = new WeakMap();
1581
1688
  this.#psHasFilterCache.set(this.root, rootCache);
1582
1689
  }
@@ -2032,118 +2139,4 @@ export class Evaluator {
2032
2139
  }
2033
2140
  return false;
2034
2141
  };
2035
-
2036
- /**
2037
- * Traverses all descendant nodes and collects matches.
2038
- * @private
2039
- * @param {object} baseNode - The base Element node or Element.shadowRoot.
2040
- * @param {Array.<object>} leaves - The AST leaves.
2041
- * @param {object} opt - Options.
2042
- * @returns {Set.<object>} A collection of matched nodes.
2043
- */
2044
- _traverseAllDescendants = (baseNode, leaves, opt) => {
2045
- const walker = this.createTreeWalker(baseNode);
2046
- traverseNode(baseNode, walker);
2047
- let currentNode = walker.firstChild();
2048
- const nodes = new Set();
2049
- while (currentNode) {
2050
- if (this.matchLeaves(leaves, currentNode, opt)) {
2051
- nodes.add(currentNode);
2052
- }
2053
- currentNode = walker.nextNode();
2054
- }
2055
- return nodes;
2056
- };
2057
-
2058
- /**
2059
- * Finds descendant nodes.
2060
- * @private
2061
- * @param {Array.<object>} leaves - The AST leaves.
2062
- * @param {object} baseNode - The base Element node or Element.shadowRoot.
2063
- * @param {object} opt - Options.
2064
- * @returns {Set.<object>} A collection of matched nodes.
2065
- */
2066
- _findDescendantNodes = (leaves, baseNode, opt) => {
2067
- const [leaf] = leaves;
2068
- const filterLeaves = this.getFilterLeaves(leaves);
2069
- const { type: leafType } = leaf;
2070
- switch (leafType) {
2071
- case ID_SELECTOR: {
2072
- const canUseGetElementById =
2073
- !this.shadow &&
2074
- baseNode.nodeType === ELEMENT_NODE &&
2075
- this.root.nodeType !== ELEMENT_NODE;
2076
- if (canUseGetElementById) {
2077
- const leafName = unescapeSelector(leaf.name);
2078
- const nodes = new Set();
2079
- const foundNode = this.root.getElementById(leafName);
2080
- if (
2081
- foundNode &&
2082
- foundNode !== baseNode &&
2083
- baseNode.contains(foundNode)
2084
- ) {
2085
- const isCompoundSelector = filterLeaves.length > 0;
2086
- if (
2087
- !isCompoundSelector ||
2088
- this.matchLeaves(filterLeaves, foundNode, opt)
2089
- ) {
2090
- nodes.add(foundNode);
2091
- }
2092
- }
2093
- return nodes;
2094
- }
2095
- break;
2096
- }
2097
- case CLASS_SELECTOR: {
2098
- if (typeof baseNode.getElementsByClassName === 'function') {
2099
- const leafName = unescapeSelector(leaf.name);
2100
- const collection = baseNode.getElementsByClassName(leafName);
2101
- const nodes = new Set();
2102
- const isCompoundSelector = filterLeaves.length > 0;
2103
- for (let i = 0, len = collection.length; i < len; i++) {
2104
- const foundNode = collection[i];
2105
- if (
2106
- !isCompoundSelector ||
2107
- this.matchLeaves(filterLeaves, foundNode, opt)
2108
- ) {
2109
- nodes.add(foundNode);
2110
- }
2111
- }
2112
- return nodes;
2113
- }
2114
- break;
2115
- }
2116
- case TYPE_SELECTOR: {
2117
- const leafName = unescapeSelector(leaf.name);
2118
- if (
2119
- typeof baseNode.getElementsByTagName === 'function' &&
2120
- !leafName.includes('|')
2121
- ) {
2122
- const collection = baseNode.getElementsByTagName(leafName);
2123
- const nodes = new Set();
2124
- const isCompoundSelector = filterLeaves.length > 0;
2125
- for (let i = 0, len = collection.length; i < len; i++) {
2126
- const foundNode = collection[i];
2127
- if (
2128
- !isCompoundSelector ||
2129
- this.matchLeaves(filterLeaves, foundNode, opt)
2130
- ) {
2131
- nodes.add(foundNode);
2132
- }
2133
- }
2134
- return nodes;
2135
- }
2136
- break;
2137
- }
2138
- case PS_ELEMENT_SELECTOR: {
2139
- const leafName = unescapeSelector(leaf.name);
2140
- matchPseudoElementSelector(leafName, leafType, opt);
2141
- return new Set();
2142
- }
2143
- default: {
2144
- // no-op
2145
- }
2146
- }
2147
- return this._traverseAllDescendants(baseNode, leaves, opt);
2148
- };
2149
2142
  }
package/src/js/finder.js CHANGED
@@ -19,6 +19,8 @@ import { generateException, sortNodes, traverseNode } from './utility.js';
19
19
  import {
20
20
  CLASS_SELECTOR,
21
21
  COMBINATOR,
22
+ DIR_NEXT,
23
+ DIR_PREV,
22
24
  DOCUMENT_FRAGMENT_NODE,
23
25
  ELEMENT_NODE,
24
26
  ID_SELECTOR,
@@ -30,8 +32,6 @@ import {
30
32
  TARGET_SELF,
31
33
  TYPE_SELECTOR
32
34
  } from './constant.js';
33
- const DIR_NEXT = 'next';
34
- const DIR_PREV = 'prev';
35
35
 
36
36
  /**
37
37
  * Finder
@@ -70,6 +70,117 @@ export class Finder extends Evaluator {
70
70
  return this;
71
71
  }
72
72
 
73
+ /**
74
+ * Finds matched nodes.
75
+ * @param {string} targetType - The target type.
76
+ * @returns {Set.<object>|object} A collection of matched nodes.
77
+ */
78
+ find = targetType => {
79
+ let collection;
80
+ try {
81
+ collection = this._collectNodes(targetType);
82
+ } catch (e) {
83
+ if (this.check) {
84
+ return {
85
+ ast: this.#selectorAST,
86
+ match: false,
87
+ pseudoElement: this.pseudoElements.length
88
+ ? this.pseudoElements.join('')
89
+ : null
90
+ };
91
+ } else {
92
+ throw e;
93
+ }
94
+ }
95
+ const [[...branches], collectedNodes] = collection;
96
+ const l = branches.length;
97
+ let sort = l > 1 && targetType === TARGET_ALL;
98
+ let nodes = new Set();
99
+ for (let i = 0; i < l; i++) {
100
+ const { branch, dir, find } = branches[i];
101
+ if (!branch.length || !find) {
102
+ continue;
103
+ }
104
+ const entryNodes = collectedNodes[i];
105
+ const lastIndex = branch.length - 1;
106
+ if (lastIndex === 0) {
107
+ if (
108
+ (targetType === TARGET_ALL || targetType === TARGET_FIRST) &&
109
+ this.node.nodeType === ELEMENT_NODE
110
+ ) {
111
+ for (const node of entryNodes) {
112
+ if (node !== this.node) {
113
+ if (targetType === TARGET_ALL || this.node.contains(node)) {
114
+ nodes.add(node);
115
+ if (targetType === TARGET_FIRST) {
116
+ break;
117
+ }
118
+ }
119
+ }
120
+ }
121
+ } else if (targetType === TARGET_ALL) {
122
+ if (nodes.size) {
123
+ for (const node of entryNodes) {
124
+ nodes.add(node);
125
+ }
126
+ sort = true;
127
+ } else {
128
+ nodes = new Set(entryNodes);
129
+ }
130
+ } else {
131
+ if (entryNodes.length) {
132
+ nodes.add(entryNodes[0]);
133
+ }
134
+ }
135
+ } else if (targetType === TARGET_ALL) {
136
+ const newNodes = this._processComplexBranchAll(branch, entryNodes, dir);
137
+ if (nodes.size) {
138
+ for (const newNode of newNodes) {
139
+ nodes.add(newNode);
140
+ }
141
+ sort = true;
142
+ } else {
143
+ nodes = newNodes;
144
+ }
145
+ } else {
146
+ const matchedNode = this._processComplexBranchFirst(
147
+ branch,
148
+ entryNodes,
149
+ dir,
150
+ targetType
151
+ );
152
+ if (matchedNode) {
153
+ nodes.add(matchedNode);
154
+ }
155
+ }
156
+ }
157
+ if (this.check) {
158
+ return {
159
+ ast: this.#selectorAST,
160
+ match: nodes.size > 0,
161
+ pseudoElement: this.pseudoElements.length
162
+ ? this.pseudoElements.join('')
163
+ : null
164
+ };
165
+ }
166
+ if (targetType === TARGET_FIRST || targetType === TARGET_ALL) {
167
+ nodes.delete(this.node);
168
+ }
169
+ if ((sort || targetType === TARGET_FIRST) && nodes.size > 1) {
170
+ return new Set(sortNodes(nodes));
171
+ }
172
+ return nodes;
173
+ };
174
+
175
+ /**
176
+ * Gets AST for selector.
177
+ * @param {string} selector - The selector text.
178
+ * @returns {object} The AST for the selector.
179
+ */
180
+ getAST = selector => {
181
+ return parseSelector(selector);
182
+ };
183
+
73
184
  /**
74
185
  * Processes selector branches into the internal AST structure.
75
186
  * @private
@@ -1014,115 +1125,4 @@ export class Finder extends Evaluator {
1014
1125
  }
1015
1126
  return null;
1016
1127
  };
1017
-
1018
- /**
1019
- * Finds matched nodes.
1020
- * @param {string} targetType - The target type.
1021
- * @returns {Set.<object>|object} A collection of matched nodes.
1022
- */
1023
- find = targetType => {
1024
- let collection;
1025
- try {
1026
- collection = this._collectNodes(targetType);
1027
- } catch (e) {
1028
- if (this.check) {
1029
- return {
1030
- ast: this.#selectorAST,
1031
- match: false,
1032
- pseudoElement: this.pseudoElements.length
1033
- ? this.pseudoElements.join('')
1034
- : null
1035
- };
1036
- } else {
1037
- throw e;
1038
- }
1039
- }
1040
- const [[...branches], collectedNodes] = collection;
1041
- const l = branches.length;
1042
- let sort = l > 1 && targetType === TARGET_ALL;
1043
- let nodes = new Set();
1044
- for (let i = 0; i < l; i++) {
1045
- const { branch, dir, find } = branches[i];
1046
- if (!branch.length || !find) {
1047
- continue;
1048
- }
1049
- const entryNodes = collectedNodes[i];
1050
- const lastIndex = branch.length - 1;
1051
- if (lastIndex === 0) {
1052
- if (
1053
- (targetType === TARGET_ALL || targetType === TARGET_FIRST) &&
1054
- this.node.nodeType === ELEMENT_NODE
1055
- ) {
1056
- for (const node of entryNodes) {
1057
- if (node !== this.node) {
1058
- if (targetType === TARGET_ALL || this.node.contains(node)) {
1059
- nodes.add(node);
1060
- if (targetType === TARGET_FIRST) {
1061
- break;
1062
- }
1063
- }
1064
- }
1065
- }
1066
- } else if (targetType === TARGET_ALL) {
1067
- if (nodes.size) {
1068
- for (const node of entryNodes) {
1069
- nodes.add(node);
1070
- }
1071
- sort = true;
1072
- } else {
1073
- nodes = new Set(entryNodes);
1074
- }
1075
- } else {
1076
- if (entryNodes.length) {
1077
- nodes.add(entryNodes[0]);
1078
- }
1079
- }
1080
- } else if (targetType === TARGET_ALL) {
1081
- const newNodes = this._processComplexBranchAll(branch, entryNodes, dir);
1082
- if (nodes.size) {
1083
- for (const newNode of newNodes) {
1084
- nodes.add(newNode);
1085
- }
1086
- sort = true;
1087
- } else {
1088
- nodes = newNodes;
1089
- }
1090
- } else {
1091
- const matchedNode = this._processComplexBranchFirst(
1092
- branch,
1093
- entryNodes,
1094
- dir,
1095
- targetType
1096
- );
1097
- if (matchedNode) {
1098
- nodes.add(matchedNode);
1099
- }
1100
- }
1101
- }
1102
- if (this.check) {
1103
- return {
1104
- ast: this.#selectorAST,
1105
- match: nodes.size > 0,
1106
- pseudoElement: this.pseudoElements.length
1107
- ? this.pseudoElements.join('')
1108
- : null
1109
- };
1110
- }
1111
- if (targetType === TARGET_FIRST || targetType === TARGET_ALL) {
1112
- nodes.delete(this.node);
1113
- }
1114
- if ((sort || targetType === TARGET_FIRST) && nodes.size > 1) {
1115
- return new Set(sortNodes(nodes));
1116
- }
1117
- return nodes;
1118
- };
1119
-
1120
- /**
1121
- * Gets AST for selector.
1122
- * @param {string} selector - The selector text.
1123
- * @returns {object} The AST for the selector.
1124
- */
1125
- getAST = selector => {
1126
- return parseSelector(selector);
1127
- };
1128
1128
  }
package/src/js/matcher.js CHANGED
@@ -33,6 +33,8 @@ const KEYS_FORM_PS_DISABLED = new Set([
33
33
  'option'
34
34
  ]);
35
35
  const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
36
+
37
+ /* regexp */
36
38
  const REG_LANG_VALID = new RegExp(`^(?:\\*-)?${ALPHA_NUM}${LANG_PART}$`, 'i');
37
39
 
38
40
  /**
package/src/js/nwsapi.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  /* import */
8
- import { GenerationalCache } from '@asamuzakjp/generational-cache';
8
+ import { LRUCache } from 'lru-cache';
9
9
  import { isContentEditable } from './utility.js';
10
10
 
11
11
  /* constants */
@@ -465,13 +465,12 @@ export class Nwsapi {
465
465
  constructor(window, document, cacheSize = CACHE_SIZE) {
466
466
  this.#window = window;
467
467
  const cacheOpt = {
468
- cacheFunction: true,
469
- strictValidate: false
468
+ max: cacheSize
470
469
  };
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
+ this.#matchLambdas = new LRUCache(cacheOpt);
471
+ this.#selectLambdas = new LRUCache(cacheOpt);
472
+ this.#matchResolvers = new LRUCache(cacheOpt);
473
+ this.#selectResolvers = new LRUCache(cacheOpt);
475
474
  this.#nthChildState = {
476
475
  idx: 0,
477
476
  len: 0,
package/src/js/parser.js CHANGED
@@ -55,10 +55,12 @@ const KEYS_PS_CLASS_STATE = new Set([
55
55
  'valid'
56
56
  ]);
57
57
  const KEYS_SHADOW_HOST = new Set(['host', 'host-context']);
58
+ const U_FFFD = '\uFFFD';
59
+
60
+ /* regexp */
58
61
  const REG_EMPTY_PS_FUNC =
59
62
  /(?<=:(?:dir|has|host(?:-context)?|is|lang|not|nth-(?:last-)?(?:child|of-type)|where))\(\s+\)/g;
60
63
  const REG_SHADOW_PS_ELEMENT = /^part|slotted$/;
61
- const U_FFFD = '\uFFFD';
62
64
 
63
65
  /**
64
66
  * Unescapes a CSS selector string.
package/src/js/utility.js CHANGED
@@ -45,6 +45,8 @@ const KEYS_NODE_FOCUSABLE_SVG = new Set([
45
45
  'symbol',
46
46
  'title'
47
47
  ]);
48
+
49
+ /* regexp */
48
50
  const REG_IS_HTML = /^(?:application\/xhtml\+x|text\/ht)ml$/;
49
51
  const REG_IS_XML =
50
52
  /^(?:application\/(?:[\w\-.]+\+)?|image\/[\w\-.]+\+|text\/)xml$/;
@@ -2,6 +2,8 @@ export const ATRULE: "Atrule";
2
2
  export const ATTR_SELECTOR: "AttributeSelector";
3
3
  export const CLASS_SELECTOR: "ClassSelector";
4
4
  export const COMBINATOR: "Combinator";
5
+ export const DIR_NEXT: "next";
6
+ export const DIR_PREV: "prev";
5
7
  export const IDENT: "Identifier";
6
8
  export const ID_SELECTOR: "IdSelector";
7
9
  export const NOT_SUPPORTED_ERR: "NotSupportedError";
@@ -2,9 +2,6 @@ export class Evaluator {
2
2
  constructor(window: object);
3
3
  window: object;
4
4
  documentCache: WeakMap<WeakKey, any>;
5
- onError: (e: Error, opt?: {
6
- noexcept?: boolean | undefined;
7
- }) => void;
8
5
  setup(selector: string, node: object, opt?: {
9
6
  check?: boolean | undefined;
10
7
  noexcept?: boolean | undefined;
@@ -19,7 +16,10 @@ export class Evaluator {
19
16
  node: object | undefined;
20
17
  pseudoElements: any[] | undefined;
21
18
  invalidate: boolean | undefined;
22
- clearResults(all?: boolean): void;
19
+ onError: (e: Error, opt?: {
20
+ noexcept?: boolean | undefined;
21
+ }) => void;
22
+ clearResults: (all?: boolean) => void;
23
23
  matchSelector: (ast: object, node: object, opt: object) => boolean;
24
24
  matchLeaves: (leaves: Array<object>, node: object, opt: object) => boolean;
25
25
  getFilterLeaves: (leaves: Array<object>) => Array<object>;
@@ -35,11 +35,14 @@ export class Evaluator {
35
35
  yieldCombinatorMatches(twig: object, node: object, opt?: {
36
36
  dir?: string | undefined;
37
37
  }): Generator<any, void, unknown>;
38
+ yieldTraverseAllDescendants(baseNode: object, leaves: Array<object>, opt: object): Generator<any, void, unknown>;
39
+ yieldFindDescendantNodes(leaves: Array<object>, baseNode: object, opt: object): Generator<any, void, unknown>;
38
40
  private _handleFocusEvent;
39
41
  private _handleKeyboardEvent;
40
42
  private _handleMouseEvent;
41
43
  private _registerEventListeners;
42
44
  private _getSelectorBranches;
45
+ private _filterNthChildOfSelectorBranches;
43
46
  private _matchAnPlusB;
44
47
  private _hasCombinatorMatch;
45
48
  private _matchHasPseudoFunc;
@@ -52,7 +55,5 @@ export class Evaluator {
52
55
  private _evaluateHostContextPseudo;
53
56
  private _matchSelectorForElement;
54
57
  private _matchSelectorForShadowRoot;
55
- private _traverseAllDescendants;
56
- private _findDescendantNodes;
57
58
  #private;
58
59
  }
@@ -1,4 +1,6 @@
1
1
  export class Finder extends Evaluator {
2
+ find: (targetType: string) => Set<object> | object;
3
+ getAST: (selector: string) => object;
2
4
  private _processSelectorBranches;
3
5
  private _correspond;
4
6
  private _traverseAndCollectNodes;
@@ -19,8 +21,6 @@ export class Finder extends Evaluator {
19
21
  private _hasValidPathPrev;
20
22
  private _processComplexBranchAll;
21
23
  private _processComplexBranchFirst;
22
- find: (targetType: string) => Set<object> | object;
23
- getAST: (selector: string) => object;
24
24
  #private;
25
25
  }
26
26
  import { Evaluator } from './evaluator.js';