@asamuzakjp/dom-selector 0.21.2 → 0.23.2
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/README.md +106 -0
- package/package.json +6 -5
- package/src/js/dom-util.js +141 -8
- package/src/js/matcher.js +433 -347
- package/src/js/parser.js +2 -2
- package/types/js/dom-util.d.ts +2 -0
- package/types/js/matcher.d.ts +5 -5
package/README.md
CHANGED
|
@@ -85,6 +85,112 @@ querySelectorAll - same functionality as [Document.querySelectorAll()][69], [Doc
|
|
|
85
85
|
Returns **[Array][62]<([object][60] \| [undefined][63])>** array of matched nodes
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
## Monkey patch jsdom
|
|
89
|
+
|
|
90
|
+
``` javascript
|
|
91
|
+
import { JSDOM } from 'jsdom';
|
|
92
|
+
import {
|
|
93
|
+
closest, matches, querySelector, querySelectorAll
|
|
94
|
+
} from '@asamuzakjp/dom-selector';
|
|
95
|
+
|
|
96
|
+
const dom = new JSDOM('', {
|
|
97
|
+
runScripts: 'dangerously',
|
|
98
|
+
url: 'http://localhost/',
|
|
99
|
+
beforeParse: window => {
|
|
100
|
+
window.Element.prototype.matches = function (...args) {
|
|
101
|
+
if (!args.length) {
|
|
102
|
+
throw new window.TypeError('1 argument required, but only 0 present.');
|
|
103
|
+
}
|
|
104
|
+
const [selector] = args;
|
|
105
|
+
return matches(selector, this);
|
|
106
|
+
};
|
|
107
|
+
window.Element.prototype.closest = function (...args) {
|
|
108
|
+
if (!args.length) {
|
|
109
|
+
throw new window.TypeError('1 argument required, but only 0 present.');
|
|
110
|
+
}
|
|
111
|
+
const [selector] = args;
|
|
112
|
+
return closest(selector, this);
|
|
113
|
+
};
|
|
114
|
+
window.Document.prototype.querySelector = function (...args) {
|
|
115
|
+
if (!args.length) {
|
|
116
|
+
throw new window.TypeError('1 argument required, but only 0 present.');
|
|
117
|
+
}
|
|
118
|
+
const [selector] = args;
|
|
119
|
+
return querySelector(selector, this);
|
|
120
|
+
};
|
|
121
|
+
window.DocumentFragment.prototype.querySelector = function (...args) {
|
|
122
|
+
if (!args.length) {
|
|
123
|
+
throw new window.TypeError('1 argument required, but only 0 present.');
|
|
124
|
+
}
|
|
125
|
+
const [selector] = args;
|
|
126
|
+
return querySelector(selector, this);
|
|
127
|
+
};
|
|
128
|
+
window.Element.prototype.querySelector = function (...args) {
|
|
129
|
+
if (!args.length) {
|
|
130
|
+
throw new window.TypeError('1 argument required, but only 0 present.');
|
|
131
|
+
}
|
|
132
|
+
const [selector] = args;
|
|
133
|
+
return querySelector(selector, this);
|
|
134
|
+
};
|
|
135
|
+
window.Document.prototype.querySelectorAll = function (...args) {
|
|
136
|
+
if (!args.length) {
|
|
137
|
+
throw new window.TypeError('1 argument required, but only 0 present.');
|
|
138
|
+
}
|
|
139
|
+
const [selector] = args;
|
|
140
|
+
return querySelectorAll(selector, this);
|
|
141
|
+
};
|
|
142
|
+
window.DocumentFragment.prototype.querySelectorAll = function (...args) {
|
|
143
|
+
if (!args.length) {
|
|
144
|
+
throw new window.TypeError('1 argument required, but only 0 present.');
|
|
145
|
+
}
|
|
146
|
+
const [selector] = args;
|
|
147
|
+
return querySelectorAll(selector, this);
|
|
148
|
+
};
|
|
149
|
+
window.Element.prototype.querySelectorAll = function (...args) {
|
|
150
|
+
if (!args.length) {
|
|
151
|
+
throw new window.TypeError('1 argument required, but only 0 present.');
|
|
152
|
+
}
|
|
153
|
+
const [selector] = args;
|
|
154
|
+
return querySelectorAll(selector, this);
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Performance
|
|
161
|
+
|
|
162
|
+
|selector|jsdom|patched-jsdom|result|
|
|
163
|
+
|:------------|:------------|:------------|:------------|
|
|
164
|
+
|matches('.container.box')|1,704,793 ops/sec ±2.10%|95,691 ops/sec ±2.40%|jsdom is 17.8 times faster. patched-jsdom took 0.010msec.|
|
|
165
|
+
|matches('.container:not(.box)')|906,712 ops/sec ±2.02%|57,603 ops/sec ±4.57%|jsdom is 15.7 times faster. patched-jsdom took 0.017msec.|
|
|
166
|
+
|matches('.box + .box')|1,566,175 ops/sec ±2.17%|90,759 ops/sec ±5.32%|jsdom is 17.3 times faster. patched-jsdom took 0.011msec.|
|
|
167
|
+
|matches('.box ~ .box')|1,585,314 ops/sec ±2.06%|90,257 ops/sec ±3.06%|jsdom is 17.6 times faster. patched-jsdom took 0.011msec.|
|
|
168
|
+
|matches('.box > .block')|830,259 ops/sec ±2.29%|79,630 ops/sec ±2.76%|jsdom is 10.4 times faster. patched-jsdom took 0.013msec.|
|
|
169
|
+
|matches('.box .content')|1,465,248 ops/sec ±2.74%|95,500 ops/sec ±2.36%|jsdom is 15.3 times faster. patched-jsdom took 0.010msec.|
|
|
170
|
+
|matches('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner > .content')|1,531,871 ops/sec ±2.68%|30,324 ops/sec ±3.09%|* jsdom is 50.5 times faster. patched-jsdom took 0.033msec.|
|
|
171
|
+
|closest('.container.box')|398,561 ops/sec ±2.74%|46,478 ops/sec ±2.31%|jsdom is 8.6 times faster. patched-jsdom took 0.022msec.|
|
|
172
|
+
|closest('.container:not(.box)')|220,109 ops/sec ±2.58%|28,449 ops/sec ±4.44%|jsdom is 7.7 times faster. patched-jsdom took 0.035msec.|
|
|
173
|
+
|closest('.box + .box')|355,897 ops/sec ±1.53%|43,752 ops/sec ±1.70%|jsdom is 8.1 times faster. patched-jsdom took 0.023msec.|
|
|
174
|
+
|closest('.box ~ .box')|124,255 ops/sec ±1.87%|25,663 ops/sec ±2.76%|jsdom is 4.8 times faster. patched-jsdom took 0.039msec.|
|
|
175
|
+
|closest('.box > .block')|387,622 ops/sec ±1.59%|40,747 ops/sec ±3.41%|jsdom is 9.5 times faster. patched-jsdom took 0.025msec.|
|
|
176
|
+
|closest('.box .content')|235,499 ops/sec ±2.42%|50,113 ops/sec ±2.81%|jsdom is 4.7 times faster. patched-jsdom took 0.020msec.|
|
|
177
|
+
|closest('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner > .content')|235,201 ops/sec ±2.80%|24,940 ops/sec ±2.26%|jsdom is 9.4 times faster. patched-jsdom took 0.040msec.|
|
|
178
|
+
|querySelector('.container.box')|59,348 ops/sec ±3.98%|20,617 ops/sec ±2.05%|jsdom is 2.9 times faster. patched-jsdom took 0.049msec.|
|
|
179
|
+
|querySelector('.container:not(.box)')|51,310 ops/sec ±2.61%|15,210 ops/sec ±1.42%|jsdom is 3.4 times faster. patched-jsdom took 0.066msec.|
|
|
180
|
+
|querySelector('.box + .box')|52,099 ops/sec ±1.32%|16,644 ops/sec ±1.21%|jsdom is 3.1 times faster. patched-jsdom took 0.060msec.|
|
|
181
|
+
|jsdom querySelector('.box ~ .box')|53,329 ops/sec ±2.51%|7,801 ops/sec ±2.10%|jsdom is 6.8 times faster. patched-jsdom took 0.128msec.|
|
|
182
|
+
|querySelector('.box > .block')|755 ops/sec ±2.34%|2,492 ops/sec ±1.13%|patched-jsdom is 3.3 times faster. patched-jsdom took 0.401msec.|
|
|
183
|
+
|querySelector('.box .content')|386 ops/sec ±2.96%|187 ops/sec ±1.12%|jsdom is 2.1 times faster. patched-jsdom took 5.359msec.|
|
|
184
|
+
| querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner > .content')|158 ops/sec ±2.40%|287 ops/sec ±2.03%|patched-jsdom is 1.8 times faster. patched-jsdom took 3.486msec.|
|
|
185
|
+
|querySelectorAll('.container.box')|73,206 ops/sec ±3.78%|19,290 ops/sec ±1.55%|jsdom is 3.8 times faster. patched-jsdom took 0.052msec.|
|
|
186
|
+
|querySelectorAll('.container:not(.box)')|65,761 ops/sec ±3.31%|14,269 ops/sec ±2.86%|jsdom is 4.6 times faster. patched-jsdom took 0.070msec.|
|
|
187
|
+
|querySelectorAll('.box + .box')|69,019 ops/sec ±1.54%|17,941 ops/sec ±1.48%|jsdom is 3.8 times faster. patched-jsdom took 0.056msec.|
|
|
188
|
+
|querySelectorAll('.box ~ .box')|64,329 ops/sec ±1.25%|7,748 ops/sec ±3.19%|jsdom is 8.3 times faster. patched-jsdom took 0.129msec.|
|
|
189
|
+
|jsdom querySelectorAll('.box > .block')|764 ops/sec ±1.90%|2,086 ops/sec ±1.49%|patched-jsdom is 2.7 times faster. patched-jsdom took 0.479msec.|
|
|
190
|
+
|querySelectorAll('.box .content')|370 ops/sec ±1.67%|165 ops/sec ±2.22%|jsdom is 2.3 times faster. patched-jsdom took 6.076msec.|
|
|
191
|
+
|querySelectorAll('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner > .content')|165 ops/sec ±1.13%|292 ops/sec ±1.79%|patched-jsdom is 1.8 times faster. patched-jsdom took 3.425msec.|
|
|
192
|
+
|
|
193
|
+
|
|
88
194
|
## Acknowledgments
|
|
89
195
|
|
|
90
196
|
The following resources have been of great help in the development of the DOM Selector.
|
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"type": "module",
|
|
20
20
|
"types": "types/index.d.ts",
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"bidi-js": "^1.0.3",
|
|
22
23
|
"css-tree": "^2.3.1",
|
|
23
24
|
"is-potential-custom-element-name": "^1.0.1"
|
|
24
25
|
},
|
|
@@ -27,16 +28,16 @@
|
|
|
27
28
|
"benchmark": "^2.1.4",
|
|
28
29
|
"c8": "^8.0.1",
|
|
29
30
|
"chai": "^4.3.10",
|
|
30
|
-
"eslint": "^8.
|
|
31
|
+
"eslint": "^8.53.0",
|
|
31
32
|
"eslint-config-standard": "^17.1.0",
|
|
32
33
|
"eslint-plugin-import": "^2.29.0",
|
|
33
34
|
"eslint-plugin-jsdoc": "^46.8.2",
|
|
34
35
|
"eslint-plugin-regexp": "^2.1.1",
|
|
35
|
-
"eslint-plugin-unicorn": "^
|
|
36
|
+
"eslint-plugin-unicorn": "^49.0.0",
|
|
36
37
|
"jsdom": "^22.1.0",
|
|
37
38
|
"mocha": "^10.2.0",
|
|
38
39
|
"npm-run-all": "^4.1.5",
|
|
39
|
-
"sinon": "^17.0.
|
|
40
|
+
"sinon": "^17.0.1",
|
|
40
41
|
"typescript": "^5.2.2",
|
|
41
42
|
"wpt-runner": "^5.0.0"
|
|
42
43
|
},
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
"test-wpt": "npm run update-wpt && node test/wpt/wpt-runner.js",
|
|
49
50
|
"tsc": "npx tsc",
|
|
50
51
|
"update": "npm-run-all -s update-*",
|
|
51
|
-
"update-wpt": "git submodule update --init --recursive"
|
|
52
|
+
"update-wpt": "git submodule update --init --recursive --remote"
|
|
52
53
|
},
|
|
53
|
-
"version": "0.
|
|
54
|
+
"version": "0.23.2"
|
|
54
55
|
}
|
package/src/js/dom-util.js
CHANGED
|
@@ -2,10 +2,148 @@
|
|
|
2
2
|
* dom-util.js
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/* import */
|
|
6
|
+
import bidiFactory from 'bidi-js';
|
|
7
|
+
|
|
5
8
|
/* constants */
|
|
6
9
|
import {
|
|
7
|
-
|
|
10
|
+
DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, DOCUMENT_POSITION_CONTAINED_BY,
|
|
11
|
+
ELEMENT_NODE, SYNTAX_ERR
|
|
8
12
|
} from './constant.js';
|
|
13
|
+
const LTR = 'ltr';
|
|
14
|
+
const RTL = 'rtl';
|
|
15
|
+
|
|
16
|
+
/* regexp */
|
|
17
|
+
const INPUT_TYPE =
|
|
18
|
+
/^(?:(?:butto|hidde)n|(?:emai|te|ur)l|(?:rese|submi|tex)t|password|search)$/;
|
|
19
|
+
const SHADOW_MODE = /^(?:close|open)$/;
|
|
20
|
+
|
|
21
|
+
/* bidi */
|
|
22
|
+
const bidi = bidiFactory();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* get slotted text content
|
|
26
|
+
* @param {object} node - Element node
|
|
27
|
+
* @returns {?string} - text content
|
|
28
|
+
*/
|
|
29
|
+
export const getSlottedTextContent = (node = {}) => {
|
|
30
|
+
let res;
|
|
31
|
+
if (node.nodeType === ELEMENT_NODE && node.localName === 'slot') {
|
|
32
|
+
let parent = node.parentNode;
|
|
33
|
+
let bool;
|
|
34
|
+
while (parent) {
|
|
35
|
+
const { host, mode, nodeType, parentNode } = parent;
|
|
36
|
+
if (nodeType === DOCUMENT_FRAGMENT_NODE && host &&
|
|
37
|
+
mode && SHADOW_MODE.test(mode)) {
|
|
38
|
+
bool = true;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
parent = parentNode;
|
|
42
|
+
}
|
|
43
|
+
if (bool) {
|
|
44
|
+
const nodes = node.assignedNodes();
|
|
45
|
+
if (nodes.length) {
|
|
46
|
+
for (const item of nodes) {
|
|
47
|
+
res = item.textContent.trim();
|
|
48
|
+
if (res) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
res = node.textContent.trim();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return res ?? null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* get directionality of node
|
|
62
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
|
|
63
|
+
* @param {object} node - Element node
|
|
64
|
+
* @returns {?string} - result
|
|
65
|
+
*/
|
|
66
|
+
export const getDirectionality = (node = {}) => {
|
|
67
|
+
let res;
|
|
68
|
+
if (node.nodeType === ELEMENT_NODE) {
|
|
69
|
+
const { dir: nodeDir, localName, parentNode } = node;
|
|
70
|
+
if (/^(?:ltr|rtl)$/.test(nodeDir)) {
|
|
71
|
+
res = nodeDir;
|
|
72
|
+
} else if (nodeDir === 'auto') {
|
|
73
|
+
let text;
|
|
74
|
+
if (localName === 'textarea') {
|
|
75
|
+
text = node.value;
|
|
76
|
+
} else if (localName === 'input' &&
|
|
77
|
+
(!node.type || INPUT_TYPE.test(node.type))) {
|
|
78
|
+
text = node.value;
|
|
79
|
+
} else if (localName === 'slot') {
|
|
80
|
+
text = getSlottedTextContent(node);
|
|
81
|
+
} else {
|
|
82
|
+
text = node.textContent.trim();
|
|
83
|
+
}
|
|
84
|
+
if (text) {
|
|
85
|
+
const { paragraphs: [{ level }] } = bidi.getEmbeddingLevels(text);
|
|
86
|
+
if (level % 2 === 1) {
|
|
87
|
+
res = RTL;
|
|
88
|
+
} else {
|
|
89
|
+
res = LTR;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!res) {
|
|
93
|
+
if (parentNode) {
|
|
94
|
+
const { nodeType: parentNodeType } = parentNode;
|
|
95
|
+
if (parentNodeType === ELEMENT_NODE) {
|
|
96
|
+
res = getDirectionality(parentNode);
|
|
97
|
+
} else if (parentNodeType === DOCUMENT_NODE ||
|
|
98
|
+
parentNodeType === DOCUMENT_FRAGMENT_NODE) {
|
|
99
|
+
res = LTR;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
res = LTR;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else if (localName === 'bdi') {
|
|
106
|
+
const text = node.textContent.trim();
|
|
107
|
+
if (text) {
|
|
108
|
+
const { paragraphs: [{ level }] } = bidi.getEmbeddingLevels(text);
|
|
109
|
+
if (level % 2 === 1) {
|
|
110
|
+
res = RTL;
|
|
111
|
+
} else {
|
|
112
|
+
res = LTR;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!(res || parentNode)) {
|
|
116
|
+
res = LTR;
|
|
117
|
+
}
|
|
118
|
+
} else if (localName === 'input' && node.type === 'tel') {
|
|
119
|
+
res = LTR;
|
|
120
|
+
} else if (parentNode) {
|
|
121
|
+
if (localName === 'slot') {
|
|
122
|
+
const text = getSlottedTextContent(node);
|
|
123
|
+
if (text) {
|
|
124
|
+
const { paragraphs: [{ level }] } = bidi.getEmbeddingLevels(text);
|
|
125
|
+
if (level % 2 === 1) {
|
|
126
|
+
res = RTL;
|
|
127
|
+
} else {
|
|
128
|
+
res = LTR;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!res) {
|
|
133
|
+
const { nodeType: parentNodeType } = parentNode;
|
|
134
|
+
if (parentNodeType === ELEMENT_NODE) {
|
|
135
|
+
res = getDirectionality(parentNode);
|
|
136
|
+
} else if (parentNodeType === DOCUMENT_NODE ||
|
|
137
|
+
parentNodeType === DOCUMENT_FRAGMENT_NODE) {
|
|
138
|
+
res = LTR;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
res = LTR;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return res ?? null;
|
|
146
|
+
};
|
|
9
147
|
|
|
10
148
|
/**
|
|
11
149
|
* is content editable
|
|
@@ -72,11 +210,10 @@ export const isNamespaceDeclared = (ns = '', node = {}) => {
|
|
|
72
210
|
* @returns {boolean} - result
|
|
73
211
|
*/
|
|
74
212
|
export const isSameOrDescendant = (node = {}, root = {}) => {
|
|
75
|
-
const { nodeType, ownerDocument } = node;
|
|
76
213
|
let res;
|
|
77
|
-
if (nodeType === ELEMENT_NODE && ownerDocument) {
|
|
214
|
+
if (node.nodeType === ELEMENT_NODE && node.ownerDocument) {
|
|
78
215
|
if (!root || root.nodeType !== ELEMENT_NODE) {
|
|
79
|
-
root = ownerDocument;
|
|
216
|
+
root = node.ownerDocument;
|
|
80
217
|
}
|
|
81
218
|
if (node === root) {
|
|
82
219
|
res = true;
|
|
@@ -98,10 +235,6 @@ export const selectorToNodeProps = (selector, node) => {
|
|
|
98
235
|
if (selector && typeof selector === 'string') {
|
|
99
236
|
if (/\|/.test(selector)) {
|
|
100
237
|
[prefix, tagName] = selector.split('|');
|
|
101
|
-
if (prefix && prefix !== '*' &&
|
|
102
|
-
node && !isNamespaceDeclared(prefix, node)) {
|
|
103
|
-
throw new DOMException(`invalid selector ${selector}`, SYNTAX_ERR);
|
|
104
|
-
}
|
|
105
238
|
} else {
|
|
106
239
|
prefix = '*';
|
|
107
240
|
tagName = selector;
|