@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 +3 -3
- package/src/index.js +60 -2
- package/src/js/finder.js +7 -1
- package/src/js/utility.js +28 -32
- package/types/index.d.ts +5 -0
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.
|
|
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 --
|
|
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.
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
-
|
|
1061
|
-
if (sel.indexOf(']') < 0) {
|
|
1066
|
+
if (selector.indexOf(']', index) === -1) {
|
|
1062
1067
|
return false;
|
|
1063
1068
|
}
|
|
1064
1069
|
}
|
|
1065
|
-
//
|
|
1070
|
+
// Target-specific early exits
|
|
1066
1071
|
if (target === TARGET_FIRST) {
|
|
1067
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
}
|
|
1103
|
-
|
|
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
|
-
}
|
|
1109
|
-
|
|
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;
|