@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 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.52.0",
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": "^48.0.1",
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.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.21.2"
54
+ "version": "0.23.2"
54
55
  }
@@ -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
- DOCUMENT_POSITION_CONTAINED_BY, ELEMENT_NODE, SYNTAX_ERR
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;