@asamuzakjp/dom-selector 7.0.10 → 7.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/package.json CHANGED
@@ -50,7 +50,7 @@
50
50
  "neostandard": "^0.13.0",
51
51
  "prettier": "^3.8.3",
52
52
  "sinon": "^21.1.2",
53
- "typescript": "^6.0.2",
53
+ "typescript": "^6.0.3",
54
54
  "wpt-runner": "^7.0.0"
55
55
  },
56
56
  "overrides": {
@@ -67,7 +67,7 @@
67
67
  "bench:sizzle": "node benchmark/bench-sizzle.js",
68
68
  "build": "npm run tsc && npm run lint && npm test",
69
69
  "lint": "eslint --fix .",
70
- "test": "c8 --reporter=text mocha --parallel --exit test/**/*.test.js",
70
+ "test": "c8 --reporter=text mocha --exit test/**/*.test.js",
71
71
  "test:wpt": "node test/wpt/wpt-runner.js",
72
72
  "tsc": "node scripts/index clean --dir=types -i && npx tsc",
73
73
  "update:wpt": "git submodule update --init --recursive --remote"
@@ -75,5 +75,5 @@
75
75
  "engines": {
76
76
  "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
77
77
  },
78
- "version": "7.0.10"
78
+ "version": "7.1.1"
79
79
  }
package/src/index.js CHANGED
@@ -8,6 +8,7 @@
8
8
  /* import */
9
9
  import { GenerationalCache } from '@asamuzakjp/generational-cache';
10
10
  import { Finder } from './js/finder.js';
11
+ import { unescapeSelector, parseAstName } from './js/parser.js';
11
12
  import { filterSelector, getType, initNwsapi } from './js/utility.js';
12
13
 
13
14
  /* constants */
@@ -18,9 +19,13 @@ import {
18
19
  TARGET_ALL,
19
20
  TARGET_FIRST,
20
21
  TARGET_LINEAL,
21
- TARGET_SELF
22
+ TARGET_SELF,
23
+ COMBINATOR,
24
+ ID_SELECTOR,
25
+ CLASS_SELECTOR,
26
+ TYPE_SELECTOR
22
27
  } from './js/constant.js';
23
- const CACHE_SIZE = 1024;
28
+ const CACHE_SIZE = 2048;
24
29
 
25
30
  /**
26
31
  * @typedef {object} CheckResult
@@ -63,6 +68,59 @@ export class DOMSelector {
63
68
  this.#finder.clearResults(true);
64
69
  };
65
70
 
71
+ /**
72
+ * Parses a selector and extracts the rightmost subject keys (Id, Class, Tag).
73
+ * @param {string} selector - The CSS selector to parse.
74
+ * @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
75
+ */
76
+ extractSubjects = selector => {
77
+ if (!selector || typeof selector !== 'string') {
78
+ return [{ id: null, className: null, tag: null }];
79
+ }
80
+ const cacheKey = `extract_${selector}`;
81
+ let subjects = this.#cache.get(cacheKey);
82
+ if (subjects !== undefined) {
83
+ return subjects;
84
+ }
85
+ subjects = [];
86
+ try {
87
+ const ast = this.#finder.getAST(selector);
88
+ if (ast?.type === 'SelectorList') {
89
+ for (const selectorNode of ast.children) {
90
+ let idKey = null;
91
+ let classKey = null;
92
+ let tagKey = null;
93
+ let current = selectorNode.children.tail;
94
+ while (current) {
95
+ const node = current.data;
96
+ if (node.type === COMBINATOR) {
97
+ break;
98
+ }
99
+ if (node.type === ID_SELECTOR) {
100
+ idKey = idKey ?? unescapeSelector(node.name);
101
+ } else if (node.type === CLASS_SELECTOR) {
102
+ classKey = classKey ?? unescapeSelector(node.name);
103
+ } else if (node.type === TYPE_SELECTOR) {
104
+ const { localName } = parseAstName(unescapeSelector(node.name));
105
+ if (localName !== '*') {
106
+ tagKey = tagKey ?? localName.toLowerCase();
107
+ }
108
+ }
109
+ current = current.prev;
110
+ }
111
+ subjects.push({ id: idKey, className: classKey, tag: tagKey });
112
+ }
113
+ }
114
+ } catch (e) {
115
+ // fall through
116
+ }
117
+ if (!subjects.length) {
118
+ subjects.push({ id: null, className: null, tag: null });
119
+ }
120
+ this.#cache.set(cacheKey, subjects);
121
+ return subjects;
122
+ };
123
+
66
124
  /**
67
125
  * Checks if an element matches a CSS selector.
68
126
  * @param {string} selector - The CSS selector to check against.
package/src/js/finder.js CHANGED
@@ -1133,6 +1133,7 @@ export class Finder {
1133
1133
  const { target, type } = this.#event ?? {};
1134
1134
  if (
1135
1135
  /^(?:click|mouse(?:down|over|up))$/.test(type) &&
1136
+ target?.nodeType === ELEMENT_NODE &&
1136
1137
  node.contains(target)
1137
1138
  ) {
1138
1139
  matched.add(node);
@@ -1141,7 +1142,12 @@ export class Finder {
1141
1142
  }
1142
1143
  case 'active': {
1143
1144
  const { buttons, target, type } = this.#event ?? {};
1144
- if (type === 'mousedown' && buttons & 1 && node.contains(target)) {
1145
+ if (
1146
+ type === 'mousedown' &&
1147
+ buttons & 1 &&
1148
+ target?.nodeType === ELEMENT_NODE &&
1149
+ node.contains(target)
1150
+ ) {
1145
1151
  matched.add(node);
1146
1152
  }
1147
1153
  break;
package/src/js/utility.js CHANGED
@@ -1047,6 +1047,7 @@ export const initNwsapi = (window, document) => {
1047
1047
  */
1048
1048
  export const filterSelector = (selector, target) => {
1049
1049
  const isQuerySelectorAll = target === TARGET_ALL;
1050
+ // Basic validation and fast-fail for null/undefined/non-string values
1050
1051
  if (
1051
1052
  !selector ||
1052
1053
  typeof selector !== 'string' ||
@@ -1054,59 +1055,54 @@ export const filterSelector = (selector, target) => {
1054
1055
  ) {
1055
1056
  return false;
1056
1057
  }
1057
- // Exclude missing close square bracket.
1058
+ // Exclude various complex or unsupported selectors early
1059
+ // i.e. non-ASCII, escaped selectors, namespaced selectors and pseudo-elements
1060
+ if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
1061
+ return false;
1062
+ }
1063
+ // Validate attribute selector integrity
1058
1064
  if (selector.includes('[')) {
1059
1065
  const index = selector.lastIndexOf('[');
1060
- const sel = selector.substring(index);
1061
- if (sel.indexOf(']') < 0) {
1066
+ if (selector.indexOf(']', index) === -1) {
1062
1067
  return false;
1063
1068
  }
1064
1069
  }
1065
- // Match only simple attribute selector for TARGET_FIRST.
1070
+ // Target-specific early exits
1066
1071
  if (target === TARGET_FIRST) {
1067
- if (REG_ATTR_SIMPLE.test(selector)) {
1068
- return true;
1069
- }
1070
- return false;
1072
+ return REG_ATTR_SIMPLE.test(selector);
1071
1073
  }
1072
-
1073
- // Exclude simple tag selector for TARGET_ALL
1074
1074
  if (target === TARGET_ALL && REG_TAG_SIMPLE.test(selector)) {
1075
1075
  return false;
1076
1076
  }
1077
-
1078
- // Exclude various complex or unsupported selectors.
1079
- // - selectors containing '/'
1080
- // - namespaced selectors
1081
- // - escaped selectors
1082
- // - pseudo-element selectors
1083
- // - selectors containing non-ASCII
1084
- // - selectors containing control character other than whitespace
1085
- // - attribute selectors with case flag, e.g. [attr i]
1086
- // - attribute selectors with unclosed quotes
1087
- // - empty :is() or :where()
1088
- if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
1089
- return false;
1090
- }
1091
- // Include pseudo-classes that are known to work correctly.
1077
+ // Logic for pseudo-classes
1092
1078
  if (selector.includes(':')) {
1079
+ // Exclude descendant combinators in logical selectors for querySelectorAll
1093
1080
  if (isQuerySelectorAll && REG_DESCEND.test(selector)) {
1094
1081
  return false;
1095
1082
  }
1096
- const complex = isQuerySelectorAll ? false : REG_COMPLEX.test(selector);
1097
- if (!isQuerySelectorAll && /:has\(/.test(selector)) {
1098
- if (!complex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
1083
+ // Determine if the selector has complex logical structures
1084
+ const isComplex = isQuerySelectorAll ? false : REG_COMPLEX.test(selector);
1085
+ // Handle :has() specifically
1086
+ if (selector.includes(':has(')) {
1087
+ if (isQuerySelectorAll) {
1088
+ return false;
1089
+ }
1090
+ if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
1099
1091
  return false;
1100
1092
  }
1101
1093
  return REG_END_WITH_HAS.test(selector);
1102
- } else if (/:(?:is|not)\(/.test(selector)) {
1103
- if (complex) {
1094
+ }
1095
+ // Handle :is() and :not()
1096
+ if (/(?:is|not)\(/.test(selector)) {
1097
+ if (isComplex) {
1104
1098
  return !REG_LOGIC_COMPLEX.test(selector);
1105
1099
  } else {
1106
1100
  return !REG_LOGIC_COMPOUND.test(selector);
1107
1101
  }
1108
- } else {
1109
- return !REG_WO_LOGICAL.test(selector);
1102
+ }
1103
+ // Default check for other pseudo-classes against known list
1104
+ if (REG_WO_LOGICAL.test(selector)) {
1105
+ return false;
1110
1106
  }
1111
1107
  }
1112
1108
  return true;
package/types/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  export class DOMSelector {
2
2
  constructor(window: Window, document: Document, opt?: object);
3
3
  clear: () => void;
4
+ extractSubjects: (selector: string) => Array<{
5
+ id: string | null;
6
+ className: string | null;
7
+ tag: string | null;
8
+ }>;
4
9
  check: (selector: string, node: Element, opt?: object) => CheckResult;
5
10
  matches: (selector: string, node: Element, opt?: object) => boolean;
6
11
  closest: (selector: string, node: Element, opt?: object) => Element | null;