@asamuzakjp/dom-selector 0.8.1 → 0.10.0
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 +47 -47
- package/package.json +4 -3
- package/src/index.js +20 -8
- package/src/js/matcher.js +166 -60
- package/src/js/parser.js +27 -8
- package/types/index.d.ts +16 -4
- package/types/js/matcher.d.ts +4 -1
- package/types/js/parser.d.ts +1 -0
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml)
|
|
4
4
|
[](https://github.com/asamuzaK/domSelector/actions/workflows/codeql.yml)
|
|
5
5
|
[](https://www.npmjs.com/package/@asamuzakjp/dom-selector)
|
|
6
|
+
|
|
6
7
|
<!--
|
|
7
8
|
[](https://github.com/asamuzaK/domSelector/releases)
|
|
8
9
|
-->
|
|
@@ -10,14 +11,12 @@
|
|
|
10
11
|
Retrieve DOM node from the given CSS selector.
|
|
11
12
|
**Experimental**
|
|
12
13
|
|
|
13
|
-
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
16
|
```console
|
|
17
17
|
npm i @asamuzakjp/dom-selector
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
## Usage
|
|
22
21
|
|
|
23
22
|
```javascript
|
|
@@ -28,65 +27,65 @@ const {
|
|
|
28
27
|
|
|
29
28
|
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- [matches][1]
|
|
34
|
-
- [Parameters][2]
|
|
35
|
-
- [closest][3]
|
|
36
|
-
- [Parameters][4]
|
|
37
|
-
- [querySelector][5]
|
|
38
|
-
- [Parameters][6]
|
|
39
|
-
- [querySelectorAll][7]
|
|
40
|
-
- [Parameters][8]
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
### matches(selector, node)
|
|
30
|
+
### matches(selector, node, opt)
|
|
44
31
|
|
|
45
|
-
|
|
32
|
+
matches - [Element.matches()][64]
|
|
46
33
|
|
|
47
34
|
#### Parameters
|
|
48
35
|
|
|
49
|
-
- `selector` **[string][
|
|
50
|
-
- `node` **[object][
|
|
36
|
+
- `selector` **[string][59]** CSS selector
|
|
37
|
+
- `node` **[object][60]** Element node
|
|
38
|
+
- `opt` **[object][60]?** options
|
|
39
|
+
- `opt.globalObject` **[object][60]?** global object, e.g. `window`, `globalThis`
|
|
40
|
+
- `opt.jsdom` **[boolean][61]?** is jsdom
|
|
51
41
|
|
|
52
|
-
Returns **[boolean][
|
|
42
|
+
Returns **[boolean][61]** result
|
|
53
43
|
|
|
54
44
|
|
|
55
|
-
### closest(selector, node)
|
|
45
|
+
### closest(selector, node, opt)
|
|
56
46
|
|
|
57
|
-
|
|
47
|
+
closest - [Element.closest()][65]
|
|
58
48
|
|
|
59
49
|
#### Parameters
|
|
60
50
|
|
|
61
|
-
- `selector` **[string][
|
|
62
|
-
- `node` **[object][
|
|
51
|
+
- `selector` **[string][59]** CSS selector
|
|
52
|
+
- `node` **[object][60]** Element node
|
|
53
|
+
- `opt` **[object][60]?** options
|
|
54
|
+
- `opt.globalObject` **[object][60]?** global object, e.g. `window`, `globalThis`
|
|
55
|
+
- `opt.jsdom` **[boolean][61]?** is jsdom
|
|
63
56
|
|
|
64
|
-
Returns **[object][
|
|
57
|
+
Returns **[object][60]?** matched node
|
|
65
58
|
|
|
66
59
|
|
|
67
|
-
### querySelector(selector, refPoint)
|
|
60
|
+
### querySelector(selector, refPoint, opt)
|
|
68
61
|
|
|
69
|
-
|
|
62
|
+
querySelector - [Document.querySelector()][66], [DocumentFragment.querySelector()][67], [Element.querySelector()][68]
|
|
70
63
|
|
|
71
64
|
#### Parameters
|
|
72
65
|
|
|
73
|
-
- `selector` **[string][
|
|
74
|
-
- `refPoint` **[object][
|
|
66
|
+
- `selector` **[string][59]** CSS selector
|
|
67
|
+
- `refPoint` **[object][60]** Document, DocumentFragment or Element node
|
|
68
|
+
- `opt` **[object][60]?** options
|
|
69
|
+
- `opt.globalObject` **[object][60]?** global object, e.g. `window`, `globalThis`
|
|
70
|
+
- `opt.jsdom` **[boolean][61]?** is jsdom
|
|
75
71
|
|
|
76
|
-
Returns **[object][
|
|
72
|
+
Returns **[object][60]?** matched node
|
|
77
73
|
|
|
78
74
|
|
|
79
|
-
### querySelectorAll(selector, refPoint)
|
|
75
|
+
### querySelectorAll(selector, refPoint, opt)
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
**NOTE**: returns
|
|
77
|
+
querySelectorAll - [Document.querySelectorAll()][69], [Document.querySelectorAll()][70], [Element.querySelectorAll()][71]
|
|
78
|
+
**NOTE**: returns Array, not NodeList
|
|
83
79
|
|
|
84
80
|
#### Parameters
|
|
85
81
|
|
|
86
|
-
- `selector` **[string][
|
|
87
|
-
- `refPoint` **[object][
|
|
82
|
+
- `selector` **[string][59]** CSS selector
|
|
83
|
+
- `refPoint` **[object][60]** Document, DocumentFragment or Element node
|
|
84
|
+
- `opt` **[object][60]?** options
|
|
85
|
+
- `opt.globalObject` **[object][60]?** global object, e.g. `window`, `globalThis`
|
|
86
|
+
- `opt.jsdom` **[boolean][61]?** is jsdom
|
|
88
87
|
|
|
89
|
-
Returns **[Array][
|
|
88
|
+
Returns **[Array][62]<([object][60] \| [undefined][63])>** array of matched nodes
|
|
90
89
|
|
|
91
90
|
|
|
92
91
|
## Acknowledgments
|
|
@@ -105,15 +104,16 @@ The following resources have been of great help in the development of the DOM Se
|
|
|
105
104
|
[6]: #parameters-2
|
|
106
105
|
[7]: #queryselectorall
|
|
107
106
|
[8]: #parameters-3
|
|
108
|
-
[
|
|
109
|
-
[
|
|
110
|
-
[
|
|
111
|
-
[
|
|
112
|
-
[
|
|
113
|
-
[
|
|
114
|
-
[
|
|
115
|
-
[
|
|
116
|
-
[
|
|
117
|
-
[
|
|
118
|
-
[
|
|
119
|
-
[
|
|
107
|
+
[59]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
|
|
108
|
+
[60]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
|
|
109
|
+
[61]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
|
|
110
|
+
[62]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
|
|
111
|
+
[63]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
|
|
112
|
+
[64]: https://developer.mozilla.org/docs/Web/API/Element/matches
|
|
113
|
+
[65]: https://developer.mozilla.org/docs/Web/API/Element/closest
|
|
114
|
+
[66]: https://developer.mozilla.org/docs/Web/API/Document/querySelector
|
|
115
|
+
[67]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelector
|
|
116
|
+
[68]: https://developer.mozilla.org/docs/Web/API/Element/querySelector
|
|
117
|
+
[69]: https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll
|
|
118
|
+
[70]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelectorAll
|
|
119
|
+
[71]: https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll
|
package/package.json
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"types": "types/index.d.ts",
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"css-tree": "^2.3.1",
|
|
22
|
-
"domexception": "^4.0.0"
|
|
22
|
+
"domexception": "^4.0.0",
|
|
23
|
+
"is-potential-custom-element-name": "^1.0.1"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/css-tree": "^2.3.1",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"chai": "^4.3.7",
|
|
28
29
|
"eslint": "^8.40.0",
|
|
29
30
|
"eslint-config-standard": "^17.0.0",
|
|
30
|
-
"eslint-plugin-jsdoc": "^44.2.
|
|
31
|
+
"eslint-plugin-jsdoc": "^44.2.4",
|
|
31
32
|
"eslint-plugin-regexp": "^1.15.0",
|
|
32
33
|
"eslint-plugin-unicorn": "^47.0.0",
|
|
33
34
|
"jsdom": "^22.0.0",
|
|
@@ -41,5 +42,5 @@
|
|
|
41
42
|
"test": "c8 --reporter=text mocha --exit test/**/*.test.js",
|
|
42
43
|
"tsc": "npx tsc"
|
|
43
44
|
},
|
|
44
|
-
"version": "0.
|
|
45
|
+
"version": "0.10.0"
|
|
45
46
|
}
|
package/src/index.js
CHANGED
|
@@ -13,10 +13,13 @@ const { Matcher } = require('./js/matcher.js');
|
|
|
13
13
|
* matches - Element.matches()
|
|
14
14
|
* @param {string} selector - CSS selector
|
|
15
15
|
* @param {object} node - Element node
|
|
16
|
+
* @param {object} [opt] - options
|
|
17
|
+
* @param {object} [opt.globalObject] - global object
|
|
18
|
+
* @param {boolean} [opt.jsdom] - is jsdom
|
|
16
19
|
* @returns {boolean} - result
|
|
17
20
|
*/
|
|
18
|
-
const matches = (selector, node) => {
|
|
19
|
-
const matcher = new Matcher(selector, node);
|
|
21
|
+
const matches = (selector, node, opt) => {
|
|
22
|
+
const matcher = new Matcher(selector, node, opt);
|
|
20
23
|
return matcher.matches();
|
|
21
24
|
};
|
|
22
25
|
|
|
@@ -24,10 +27,13 @@ const matches = (selector, node) => {
|
|
|
24
27
|
* closest - Element.closest()
|
|
25
28
|
* @param {string} selector - CSS selector
|
|
26
29
|
* @param {object} node - Element node
|
|
30
|
+
* @param {object} [opt] - options
|
|
31
|
+
* @param {object} [opt.globalObject] - global object
|
|
32
|
+
* @param {boolean} [opt.jsdom] - is jsdom
|
|
27
33
|
* @returns {?object} - matched node
|
|
28
34
|
*/
|
|
29
|
-
const closest = (selector, node) => {
|
|
30
|
-
const matcher = new Matcher(selector, node);
|
|
35
|
+
const closest = (selector, node, opt) => {
|
|
36
|
+
const matcher = new Matcher(selector, node, opt);
|
|
31
37
|
return matcher.closest();
|
|
32
38
|
};
|
|
33
39
|
|
|
@@ -35,10 +41,13 @@ const closest = (selector, node) => {
|
|
|
35
41
|
* querySelector - Document.querySelector(), Element.querySelector()
|
|
36
42
|
* @param {string} selector - CSS selector
|
|
37
43
|
* @param {object} refPoint - Document or Element node
|
|
44
|
+
* @param {object} [opt] - options
|
|
45
|
+
* @param {object} [opt.globalObject] - global object
|
|
46
|
+
* @param {boolean} [opt.jsdom] - is jsdom
|
|
38
47
|
* @returns {?object} - matched node
|
|
39
48
|
*/
|
|
40
|
-
const querySelector = (selector, refPoint) => {
|
|
41
|
-
const matcher = new Matcher(selector, refPoint);
|
|
49
|
+
const querySelector = (selector, refPoint, opt) => {
|
|
50
|
+
const matcher = new Matcher(selector, refPoint, opt);
|
|
42
51
|
return matcher.querySelector();
|
|
43
52
|
};
|
|
44
53
|
|
|
@@ -47,10 +56,13 @@ const querySelector = (selector, refPoint) => {
|
|
|
47
56
|
* NOTE: returns Array, not NodeList
|
|
48
57
|
* @param {string} selector - CSS selector
|
|
49
58
|
* @param {object} refPoint - Document or Element node
|
|
59
|
+
* @param {object} [opt] - options
|
|
60
|
+
* @param {object} [opt.globalObject] - global object
|
|
61
|
+
* @param {boolean} [opt.jsdom] - is jsdom
|
|
50
62
|
* @returns {Array.<object|undefined>} - array of matched nodes
|
|
51
63
|
*/
|
|
52
|
-
const querySelectorAll = (selector, refPoint) => {
|
|
53
|
-
const matcher = new Matcher(selector, refPoint);
|
|
64
|
+
const querySelectorAll = (selector, refPoint, opt) => {
|
|
65
|
+
const matcher = new Matcher(selector, refPoint, opt);
|
|
54
66
|
return matcher.querySelectorAll();
|
|
55
67
|
};
|
|
56
68
|
|
package/src/js/matcher.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
/* import */
|
|
7
|
+
const isCustomElementName = require('is-potential-custom-element-name');
|
|
7
8
|
const DOMException = require('./domexception.js');
|
|
8
9
|
const { generateCSS, parseSelector, walkAST } = require('./parser.js');
|
|
9
10
|
|
|
@@ -19,14 +20,51 @@ const FILTER_SHOW_ELEMENT = 1;
|
|
|
19
20
|
const TEXT_NODE = 3;
|
|
20
21
|
|
|
21
22
|
/* regexp */
|
|
22
|
-
|
|
23
|
-
// @see https://html.spec.whatwg.org/#valid-custom-element-name
|
|
24
|
-
const HTML_CUSTOM_ELEMENT = /^[a-z][\d._a-z]*-[\d\-._a-z]*$/;
|
|
23
|
+
const HEX_CAPTURE = /^([\da-f]{1,6}\s?)/i;
|
|
25
24
|
const HTML_FORM_INPUT = /^(?:(?:inpu|selec)t|textarea)$/;
|
|
26
25
|
const HTML_FORM_PARTS = /^(?:button|fieldset|opt(?:group|ion))$/;
|
|
27
26
|
const HTML_INTERACT = /^d(?:etails|ialog)$/;
|
|
28
27
|
const PSEUDO_FUNC = /^(?:(?:ha|i)s|not|where)$/;
|
|
29
28
|
const PSEUDO_NTH = /^nth-(?:last-)?(?:child|of-type)$/;
|
|
29
|
+
const REPLACE_CHAR = /[\0\uD800-\uDFFF]/g;
|
|
30
|
+
const WHITESPACE = /^[\n\r\f]/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* unescape selector
|
|
34
|
+
* @param {string} selector - CSS selector
|
|
35
|
+
* @returns {?string} - unescaped selector
|
|
36
|
+
*/
|
|
37
|
+
const unescapeSelector = (selector = '') => {
|
|
38
|
+
if (typeof selector === 'string' &&
|
|
39
|
+
selector.indexOf(String.fromCharCode(0x5c), 0) >= 0) {
|
|
40
|
+
const arr = selector.split('\\');
|
|
41
|
+
const l = arr.length;
|
|
42
|
+
for (let i = 0; i < l; i++) {
|
|
43
|
+
let item = arr[i];
|
|
44
|
+
if (i === l - 1 && item === '') {
|
|
45
|
+
item = '\uFFFD';
|
|
46
|
+
} else {
|
|
47
|
+
const hexExists = HEX_CAPTURE.exec(item);
|
|
48
|
+
if (hexExists) {
|
|
49
|
+
const [, hex] = hexExists;
|
|
50
|
+
let str;
|
|
51
|
+
try {
|
|
52
|
+
str = String.fromCodePoint(`0x${hex.trim()}`)
|
|
53
|
+
.replace(REPLACE_CHAR, '\uFFFD');
|
|
54
|
+
} catch (e) {
|
|
55
|
+
str = '\uFFFD';
|
|
56
|
+
}
|
|
57
|
+
item = item.replace(`${hex}`, str);
|
|
58
|
+
} else if (WHITESPACE.test(item)) {
|
|
59
|
+
item = '\\' + item;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
arr[i] = item;
|
|
63
|
+
}
|
|
64
|
+
selector = arr.join('');
|
|
65
|
+
}
|
|
66
|
+
return selector;
|
|
67
|
+
};
|
|
30
68
|
|
|
31
69
|
/**
|
|
32
70
|
* collect nth child
|
|
@@ -197,11 +235,12 @@ const matchAnPlusB = (nthName, ast = {}, node = {}) => {
|
|
|
197
235
|
nth: {
|
|
198
236
|
a,
|
|
199
237
|
b,
|
|
200
|
-
name:
|
|
238
|
+
name: nthIdentName
|
|
201
239
|
},
|
|
202
240
|
selector: astSelector,
|
|
203
241
|
type: astType
|
|
204
242
|
} = ast;
|
|
243
|
+
const identName = unescapeSelector(nthIdentName);
|
|
205
244
|
const { nodeType } = node;
|
|
206
245
|
if (astType === NTH && nodeType === ELEMENT_NODE) {
|
|
207
246
|
const anbMap = new Map();
|
|
@@ -267,7 +306,7 @@ const matchTypeSelector = (ast = {}, node = {}) => {
|
|
|
267
306
|
const { localName, nodeType, ownerDocument, prefix } = node;
|
|
268
307
|
let res;
|
|
269
308
|
if (astType === TYPE_SELECTOR && nodeType === ELEMENT_NODE) {
|
|
270
|
-
let astName = ast.name;
|
|
309
|
+
let astName = unescapeSelector(ast.name);
|
|
271
310
|
let astPrefix, astNodeName, nodePrefix, nodeName;
|
|
272
311
|
if (/\|/.test(astName)) {
|
|
273
312
|
[astPrefix, astNodeName] = astName.split('|');
|
|
@@ -304,12 +343,14 @@ const matchTypeSelector = (ast = {}, node = {}) => {
|
|
|
304
343
|
* @returns {?object} - matched node
|
|
305
344
|
*/
|
|
306
345
|
const matchClassSelector = (ast = {}, node = {}) => {
|
|
307
|
-
const {
|
|
346
|
+
const { type: astType } = ast;
|
|
308
347
|
const { classList, nodeType } = node;
|
|
309
348
|
let res;
|
|
310
|
-
if (astType === CLASS_SELECTOR && nodeType === ELEMENT_NODE
|
|
311
|
-
|
|
312
|
-
|
|
349
|
+
if (astType === CLASS_SELECTOR && nodeType === ELEMENT_NODE) {
|
|
350
|
+
const astName = unescapeSelector(ast.name);
|
|
351
|
+
if (classList.contains(astName)) {
|
|
352
|
+
res = node;
|
|
353
|
+
}
|
|
313
354
|
}
|
|
314
355
|
return res || null;
|
|
315
356
|
};
|
|
@@ -321,12 +362,14 @@ const matchClassSelector = (ast = {}, node = {}) => {
|
|
|
321
362
|
* @returns {?object} - matched node
|
|
322
363
|
*/
|
|
323
364
|
const matchIDSelector = (ast = {}, node = {}) => {
|
|
324
|
-
const {
|
|
365
|
+
const { type: astType } = ast;
|
|
325
366
|
const { id, nodeType } = node;
|
|
326
367
|
let res;
|
|
327
|
-
if (astType === ID_SELECTOR && nodeType === ELEMENT_NODE
|
|
328
|
-
|
|
329
|
-
|
|
368
|
+
if (astType === ID_SELECTOR && nodeType === ELEMENT_NODE) {
|
|
369
|
+
const astName = unescapeSelector(ast.name);
|
|
370
|
+
if (astName === id) {
|
|
371
|
+
res = node;
|
|
372
|
+
}
|
|
330
373
|
}
|
|
331
374
|
return res || null;
|
|
332
375
|
};
|
|
@@ -346,10 +389,15 @@ const matchAttributeSelector = (ast = {}, node = {}) => {
|
|
|
346
389
|
let res;
|
|
347
390
|
if (astType === ATTRIBUTE_SELECTOR && nodeType === ELEMENT_NODE &&
|
|
348
391
|
attributes?.length) {
|
|
349
|
-
|
|
350
|
-
|
|
392
|
+
if (typeof astFlags === 'string' && !/^[is]$/i.test(astFlags)) {
|
|
393
|
+
throw new DOMException('invalid attribute selector', 'SyntaxError');
|
|
394
|
+
}
|
|
395
|
+
const caseInsensitive =
|
|
396
|
+
!(typeof astFlags === 'string' && /^s$/i.test(astFlags));
|
|
351
397
|
const attrValues = [];
|
|
352
398
|
const l = attributes.length;
|
|
399
|
+
let { name: astAttrName } = astName;
|
|
400
|
+
astAttrName = unescapeSelector(astAttrName);
|
|
353
401
|
// namespaced
|
|
354
402
|
if (/\|/.test(astAttrName)) {
|
|
355
403
|
const [astAttrPrefix, astAttrLocalName] = astAttrName.split('|');
|
|
@@ -512,11 +560,12 @@ const matchAttributeSelector = (ast = {}, node = {}) => {
|
|
|
512
560
|
* @returns {?object} - matched node
|
|
513
561
|
*/
|
|
514
562
|
const matchLanguagePseudoClass = (ast = {}, node = {}) => {
|
|
515
|
-
const {
|
|
563
|
+
const { type: astType } = ast;
|
|
516
564
|
const { lang, nodeType } = node;
|
|
517
565
|
let res;
|
|
518
566
|
if (astType === IDENTIFIER && nodeType === ELEMENT_NODE) {
|
|
519
|
-
|
|
567
|
+
const astName = unescapeSelector(ast.name);
|
|
568
|
+
// TBD: what about xml:lang?
|
|
520
569
|
if (astName === '') {
|
|
521
570
|
if (node.getAttribute('lang') === '') {
|
|
522
571
|
res = node;
|
|
@@ -574,10 +623,11 @@ const matchPseudoClassSelector = (
|
|
|
574
623
|
node = {},
|
|
575
624
|
refPoint = {}
|
|
576
625
|
) => {
|
|
577
|
-
const { children: astChildren,
|
|
626
|
+
const { children: astChildren, type: astType } = ast;
|
|
578
627
|
const { localName, nodeType, ownerDocument, parentNode } = node;
|
|
579
628
|
const matched = [];
|
|
580
629
|
if (astType === PSEUDO_CLASS_SELECTOR && nodeType === ELEMENT_NODE) {
|
|
630
|
+
const astName = unescapeSelector(ast.name);
|
|
581
631
|
if (Array.isArray(astChildren)) {
|
|
582
632
|
const [astChildAst] = astChildren;
|
|
583
633
|
// :nth-child(), :nth-last-child(), nth-of-type(), :nth-last-of-type()
|
|
@@ -601,8 +651,8 @@ const matchPseudoClassSelector = (
|
|
|
601
651
|
case 'current':
|
|
602
652
|
case 'nth-col':
|
|
603
653
|
case 'nth-last-col':
|
|
604
|
-
|
|
605
|
-
|
|
654
|
+
throw new DOMException(`Unsupported pseudo-class ${astName}`,
|
|
655
|
+
'NotSupportedError');
|
|
606
656
|
default:
|
|
607
657
|
throw new DOMException(`Unknown pseudo-class ${astName}`,
|
|
608
658
|
'SyntaxError');
|
|
@@ -688,7 +738,7 @@ const matchPseudoClassSelector = (
|
|
|
688
738
|
case 'disabled':
|
|
689
739
|
if ((HTML_FORM_INPUT.test(localName) ||
|
|
690
740
|
HTML_FORM_PARTS.test(localName) ||
|
|
691
|
-
|
|
741
|
+
isCustomElementName(localName)) &&
|
|
692
742
|
node.hasAttribute('disabled')) {
|
|
693
743
|
matched.push(node);
|
|
694
744
|
}
|
|
@@ -696,7 +746,7 @@ const matchPseudoClassSelector = (
|
|
|
696
746
|
case 'enabled':
|
|
697
747
|
if ((HTML_FORM_INPUT.test(localName) ||
|
|
698
748
|
HTML_FORM_PARTS.test(localName) ||
|
|
699
|
-
|
|
749
|
+
isCustomElementName(localName)) &&
|
|
700
750
|
!node.hasAttribute('disabled')) {
|
|
701
751
|
matched.push(node);
|
|
702
752
|
}
|
|
@@ -731,7 +781,8 @@ const matchPseudoClassSelector = (
|
|
|
731
781
|
}
|
|
732
782
|
// FIXME:
|
|
733
783
|
if (isMultiple) {
|
|
734
|
-
|
|
784
|
+
throw new DOMException(`Unsupported pseudo-class ${astName}`,
|
|
785
|
+
'NotSupportedError');
|
|
735
786
|
} else {
|
|
736
787
|
const firstOpt = parentNode.firstElementChild;
|
|
737
788
|
const defaultOpt = [];
|
|
@@ -757,7 +808,8 @@ const matchPseudoClassSelector = (
|
|
|
757
808
|
node.getAttribute('type') === 'submit')) ||
|
|
758
809
|
(/^input$/.test(localName) && node.hasAttribute('type') &&
|
|
759
810
|
/^(?:image|submit)$/.test(node.getAttribute('type')))) {
|
|
760
|
-
|
|
811
|
+
throw new DOMException(`Unsupported pseudo-class ${astName}`,
|
|
812
|
+
'NotSupportedError');
|
|
761
813
|
}
|
|
762
814
|
break;
|
|
763
815
|
case 'required':
|
|
@@ -866,8 +918,8 @@ const matchPseudoClassSelector = (
|
|
|
866
918
|
case 'user-valid':
|
|
867
919
|
case 'valid':
|
|
868
920
|
case 'volume-locked':
|
|
869
|
-
|
|
870
|
-
|
|
921
|
+
throw new DOMException(`Unsupported pseudo-class ${astName}`,
|
|
922
|
+
'NotSupportedError');
|
|
871
923
|
default:
|
|
872
924
|
throw new DOMException(`Unknown pseudo-class ${astName}`,
|
|
873
925
|
'SyntaxError');
|
|
@@ -886,17 +938,22 @@ class Matcher {
|
|
|
886
938
|
#document;
|
|
887
939
|
#node;
|
|
888
940
|
#selector;
|
|
941
|
+
#warn;
|
|
889
942
|
|
|
890
943
|
/**
|
|
891
944
|
* construct
|
|
892
945
|
* @param {string} selector - CSS selector
|
|
893
946
|
* @param {object} refPoint - reference point
|
|
947
|
+
* @param {object} [opt] - options
|
|
948
|
+
* @param {object} [opt.warn] - console warn
|
|
894
949
|
*/
|
|
895
|
-
constructor(selector, refPoint) {
|
|
950
|
+
constructor(selector, refPoint, opt = {}) {
|
|
951
|
+
const { warn } = opt;
|
|
896
952
|
this.#ast = parseSelector(selector);
|
|
897
953
|
this.#document = refPoint?.ownerDocument ?? refPoint;
|
|
898
954
|
this.#node = refPoint;
|
|
899
955
|
this.#selector = selector;
|
|
956
|
+
this.#warn = !!warn;
|
|
900
957
|
}
|
|
901
958
|
|
|
902
959
|
/**
|
|
@@ -1075,7 +1132,7 @@ class Matcher {
|
|
|
1075
1132
|
const ast = walkAST(branch);
|
|
1076
1133
|
let res;
|
|
1077
1134
|
if (ast.length) {
|
|
1078
|
-
const
|
|
1135
|
+
const branchName = unescapeSelector(branch.name);
|
|
1079
1136
|
switch (branchName) {
|
|
1080
1137
|
// :has()
|
|
1081
1138
|
case 'has': {
|
|
@@ -1096,7 +1153,7 @@ class Matcher {
|
|
|
1096
1153
|
itemLeaves.push(item, firstItem);
|
|
1097
1154
|
}
|
|
1098
1155
|
if (firstItem.type === PSEUDO_CLASS_SELECTOR &&
|
|
1099
|
-
firstItem.name === 'has') {
|
|
1156
|
+
unescapeSelector(firstItem.name) === 'has') {
|
|
1100
1157
|
matched = false;
|
|
1101
1158
|
break;
|
|
1102
1159
|
}
|
|
@@ -1122,7 +1179,8 @@ class Matcher {
|
|
|
1122
1179
|
const [item, ...items] = astItem;
|
|
1123
1180
|
// NOTE: according to MDN, :not() can not contain :not()
|
|
1124
1181
|
// but spec says nothing about that?
|
|
1125
|
-
if (item.type === PSEUDO_CLASS_SELECTOR &&
|
|
1182
|
+
if (item.type === PSEUDO_CLASS_SELECTOR &&
|
|
1183
|
+
unescapeSelector(item.name) === 'not') {
|
|
1126
1184
|
matched = true;
|
|
1127
1185
|
break;
|
|
1128
1186
|
}
|
|
@@ -1176,7 +1234,7 @@ class Matcher {
|
|
|
1176
1234
|
if (Array.isArray(children) && children.length) {
|
|
1177
1235
|
const [firstChild] = children;
|
|
1178
1236
|
if (firstChild.type === PSEUDO_CLASS_SELECTOR &&
|
|
1179
|
-
PSEUDO_FUNC.test(firstChild.name) &&
|
|
1237
|
+
PSEUDO_FUNC.test(unescapeSelector(firstChild.name)) &&
|
|
1180
1238
|
node.nodeType === ELEMENT_NODE) {
|
|
1181
1239
|
const iteratorLeaf = {
|
|
1182
1240
|
name: '*',
|
|
@@ -1195,7 +1253,7 @@ class Matcher {
|
|
|
1195
1253
|
let iteratorLeaf;
|
|
1196
1254
|
if (firstChild.type === COMBINATOR ||
|
|
1197
1255
|
(firstChild.type === PSEUDO_CLASS_SELECTOR &&
|
|
1198
|
-
PSEUDO_NTH.test(firstChild.name))) {
|
|
1256
|
+
PSEUDO_NTH.test(unescapeSelector(firstChild.name)))) {
|
|
1199
1257
|
iteratorLeaf = {
|
|
1200
1258
|
name: '*',
|
|
1201
1259
|
type: TYPE_SELECTOR
|
|
@@ -1211,7 +1269,8 @@ class Matcher {
|
|
|
1211
1269
|
if (items.length) {
|
|
1212
1270
|
if (items.length === 1) {
|
|
1213
1271
|
const item = items.shift();
|
|
1214
|
-
const {
|
|
1272
|
+
const { type: itemType } = item;
|
|
1273
|
+
const itemName = unescapeSelector(item.name);
|
|
1215
1274
|
if (itemType === PSEUDO_CLASS_SELECTOR &&
|
|
1216
1275
|
PSEUDO_FUNC.test(itemName)) {
|
|
1217
1276
|
nextNode = this._matchLogicalPseudoFunc(item, nextNode);
|
|
@@ -1228,7 +1287,8 @@ class Matcher {
|
|
|
1228
1287
|
} else {
|
|
1229
1288
|
do {
|
|
1230
1289
|
const item = items.shift();
|
|
1231
|
-
const {
|
|
1290
|
+
const { type: itemType } = item;
|
|
1291
|
+
const itemName = unescapeSelector(item.name);
|
|
1232
1292
|
if (itemType === PSEUDO_CLASS_SELECTOR &&
|
|
1233
1293
|
PSEUDO_FUNC.test(itemName)) {
|
|
1234
1294
|
nextNode = this._matchLogicalPseudoFunc(item, nextNode);
|
|
@@ -1239,9 +1299,9 @@ class Matcher {
|
|
|
1239
1299
|
const [nextItem] = items;
|
|
1240
1300
|
if (nextItem.type === COMBINATOR ||
|
|
1241
1301
|
(nextItem.type === PSEUDO_CLASS_SELECTOR &&
|
|
1242
|
-
PSEUDO_NTH.test(nextItem.name)) ||
|
|
1302
|
+
PSEUDO_NTH.test(unescapeSelector(nextItem.name))) ||
|
|
1243
1303
|
(nextItem.type === PSEUDO_CLASS_SELECTOR &&
|
|
1244
|
-
PSEUDO_FUNC.test(nextItem.name))) {
|
|
1304
|
+
PSEUDO_FUNC.test(unescapeSelector(nextItem.name)))) {
|
|
1245
1305
|
break;
|
|
1246
1306
|
} else {
|
|
1247
1307
|
leaves.push(items.shift());
|
|
@@ -1255,7 +1315,7 @@ class Matcher {
|
|
|
1255
1315
|
const [i] = items;
|
|
1256
1316
|
for (const j of arr) {
|
|
1257
1317
|
if (i.type === PSEUDO_CLASS_SELECTOR &&
|
|
1258
|
-
PSEUDO_FUNC.test(i.name)) {
|
|
1318
|
+
PSEUDO_FUNC.test(unescapeSelector(i.name))) {
|
|
1259
1319
|
if (this._matchLogicalPseudoFunc(i, j)) {
|
|
1260
1320
|
matched.push(j);
|
|
1261
1321
|
}
|
|
@@ -1321,7 +1381,7 @@ class Matcher {
|
|
|
1321
1381
|
}
|
|
1322
1382
|
break;
|
|
1323
1383
|
case PSEUDO_CLASS_SELECTOR:
|
|
1324
|
-
if (!PSEUDO_FUNC.test(name)) {
|
|
1384
|
+
if (!PSEUDO_FUNC.test(unescapeSelector(name))) {
|
|
1325
1385
|
const arr = matchPseudoClassSelector(ast, node, this.#node);
|
|
1326
1386
|
if (arr.length) {
|
|
1327
1387
|
matched.push(...arr);
|
|
@@ -1343,8 +1403,19 @@ class Matcher {
|
|
|
1343
1403
|
* @returns {boolean} - matched node
|
|
1344
1404
|
*/
|
|
1345
1405
|
matches() {
|
|
1346
|
-
|
|
1347
|
-
|
|
1406
|
+
let res;
|
|
1407
|
+
try {
|
|
1408
|
+
const arr = this._match(this.#ast, this.#document);
|
|
1409
|
+
res = arr.length && arr.includes(this.#node);
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
if (e instanceof DOMException && e.name === 'NotSupportedError') {
|
|
1412
|
+
if (this.#warn) {
|
|
1413
|
+
console.warn(e.message);
|
|
1414
|
+
}
|
|
1415
|
+
} else {
|
|
1416
|
+
throw e;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1348
1419
|
return !!res;
|
|
1349
1420
|
}
|
|
1350
1421
|
|
|
@@ -1353,15 +1424,25 @@ class Matcher {
|
|
|
1353
1424
|
* @returns {?object} - matched node
|
|
1354
1425
|
*/
|
|
1355
1426
|
closest() {
|
|
1356
|
-
const arr = this._match(this.#ast, this.#document);
|
|
1357
|
-
let node = this.#node;
|
|
1358
1427
|
let res;
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1428
|
+
try {
|
|
1429
|
+
const arr = this._match(this.#ast, this.#document);
|
|
1430
|
+
let node = this.#node;
|
|
1431
|
+
while (node) {
|
|
1432
|
+
if (arr.includes(node)) {
|
|
1433
|
+
res = node;
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
node = node.parentNode;
|
|
1437
|
+
}
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
if (e instanceof DOMException && e.name === 'NotSupportedError') {
|
|
1440
|
+
if (this.#warn) {
|
|
1441
|
+
console.warn(e.message);
|
|
1442
|
+
}
|
|
1443
|
+
} else {
|
|
1444
|
+
throw e;
|
|
1363
1445
|
}
|
|
1364
|
-
node = node.parentNode;
|
|
1365
1446
|
}
|
|
1366
1447
|
return res || null;
|
|
1367
1448
|
}
|
|
@@ -1371,14 +1452,25 @@ class Matcher {
|
|
|
1371
1452
|
* @returns {?object} - matched node
|
|
1372
1453
|
*/
|
|
1373
1454
|
querySelector() {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
const
|
|
1377
|
-
if (
|
|
1378
|
-
arr.
|
|
1455
|
+
let res;
|
|
1456
|
+
try {
|
|
1457
|
+
const arr = this._match(this.#ast, this.#node);
|
|
1458
|
+
if (arr.length) {
|
|
1459
|
+
const i = arr.findIndex(node => node === this.#node);
|
|
1460
|
+
if (i >= 0) {
|
|
1461
|
+
arr.splice(i, 1);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
[res] = arr;
|
|
1465
|
+
} catch (e) {
|
|
1466
|
+
if (e instanceof DOMException && e.name === 'NotSupportedError') {
|
|
1467
|
+
if (this.#warn) {
|
|
1468
|
+
console.warn(e.message);
|
|
1469
|
+
}
|
|
1470
|
+
} else {
|
|
1471
|
+
throw e;
|
|
1379
1472
|
}
|
|
1380
1473
|
}
|
|
1381
|
-
const [res] = arr;
|
|
1382
1474
|
return res || null;
|
|
1383
1475
|
}
|
|
1384
1476
|
|
|
@@ -1388,14 +1480,27 @@ class Matcher {
|
|
|
1388
1480
|
* @returns {Array.<object|undefined>} - collection of matched nodes
|
|
1389
1481
|
*/
|
|
1390
1482
|
querySelectorAll() {
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
const
|
|
1394
|
-
if (
|
|
1395
|
-
arr.
|
|
1483
|
+
const res = [];
|
|
1484
|
+
try {
|
|
1485
|
+
const arr = this._match(this.#ast, this.#node);
|
|
1486
|
+
if (arr.length) {
|
|
1487
|
+
const i = arr.findIndex(node => node === this.#node);
|
|
1488
|
+
if (i >= 0) {
|
|
1489
|
+
arr.splice(i, 1);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const a = new Set(arr);
|
|
1493
|
+
res.push(...a);
|
|
1494
|
+
} catch (e) {
|
|
1495
|
+
if (e instanceof DOMException && e.name === 'NotSupportedError') {
|
|
1496
|
+
if (this.#warn) {
|
|
1497
|
+
console.warn(e.message);
|
|
1498
|
+
}
|
|
1499
|
+
} else {
|
|
1500
|
+
throw e;
|
|
1396
1501
|
}
|
|
1397
1502
|
}
|
|
1398
|
-
return
|
|
1503
|
+
return res;
|
|
1399
1504
|
}
|
|
1400
1505
|
};
|
|
1401
1506
|
|
|
@@ -1409,5 +1514,6 @@ module.exports = {
|
|
|
1409
1514
|
matchIDSelector,
|
|
1410
1515
|
matchLanguagePseudoClass,
|
|
1411
1516
|
matchPseudoClassSelector,
|
|
1412
|
-
matchTypeSelector
|
|
1517
|
+
matchTypeSelector,
|
|
1518
|
+
unescapeSelector
|
|
1413
1519
|
};
|
package/src/js/parser.js
CHANGED
|
@@ -5,27 +5,45 @@
|
|
|
5
5
|
|
|
6
6
|
/* import */
|
|
7
7
|
const { generate, parse, toPlainObject, walk } = require('css-tree');
|
|
8
|
-
const { SELECTOR } = require('./constant.js');
|
|
9
8
|
const DOMException = require('./domexception.js');
|
|
10
9
|
|
|
11
10
|
/* constants */
|
|
11
|
+
const { SELECTOR } = require('./constant.js');
|
|
12
12
|
const TYPE_FROM = 8;
|
|
13
13
|
const TYPE_TO = -1;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* preprocess
|
|
17
|
+
* @see https://drafts.csswg.org/css-syntax-3/#input-preprocessing
|
|
18
|
+
* @param {...*} args - arguments
|
|
19
|
+
* @returns {string} - filtered selector string
|
|
20
|
+
*/
|
|
21
|
+
const preprocess = (...args) => {
|
|
22
|
+
if (!args.length) {
|
|
23
|
+
throw new TypeError('1 argument required, but only 0 present');
|
|
24
|
+
}
|
|
25
|
+
let [selector] = args;
|
|
26
|
+
if (typeof selector !== 'string') {
|
|
27
|
+
if (selector === undefined || selector === null) {
|
|
28
|
+
selector = Object.prototype.toString.call(selector)
|
|
29
|
+
.slice(TYPE_FROM, TYPE_TO).toLowerCase();
|
|
30
|
+
} else {
|
|
31
|
+
throw new DOMException(`invalid selector ${selector}`, 'SyntaxError');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return selector.replace(/\f|\r\n?/g, '\n')
|
|
35
|
+
.replace(/[\0\uD800-\uDFFF]/g, '\uFFFD').trim();
|
|
36
|
+
};
|
|
37
|
+
|
|
15
38
|
/**
|
|
16
39
|
* create AST from CSS selector
|
|
17
40
|
* @param {string} selector - CSS selector
|
|
18
41
|
* @returns {object} - AST
|
|
19
42
|
*/
|
|
20
43
|
const parseSelector = selector => {
|
|
21
|
-
|
|
22
|
-
selector = Object.prototype.toString.call(selector)
|
|
23
|
-
.slice(TYPE_FROM, TYPE_TO).toLowerCase();
|
|
24
|
-
}
|
|
44
|
+
selector = preprocess(selector);
|
|
25
45
|
// invalid selectors
|
|
26
|
-
if (
|
|
27
|
-
selector.startsWith('>') || selector.endsWith(',') ||
|
|
28
|
-
selector.includes('= ')) {
|
|
46
|
+
if (selector === '' || /^\s*>/.test(selector) || /,\s*$/.test(selector)) {
|
|
29
47
|
throw new DOMException(`invalid selector ${selector}`, 'SyntaxError');
|
|
30
48
|
}
|
|
31
49
|
let res;
|
|
@@ -70,5 +88,6 @@ const walkAST = (ast = {}) => {
|
|
|
70
88
|
module.exports = {
|
|
71
89
|
generateCSS: generate,
|
|
72
90
|
parseSelector,
|
|
91
|
+
preprocess,
|
|
73
92
|
walkAST
|
|
74
93
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
export function closest(selector: string, node: object
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export function closest(selector: string, node: object, opt?: {
|
|
2
|
+
globalObject?: object;
|
|
3
|
+
jsdom?: boolean;
|
|
4
|
+
}): object | null;
|
|
5
|
+
export function matches(selector: string, node: object, opt?: {
|
|
6
|
+
globalObject?: object;
|
|
7
|
+
jsdom?: boolean;
|
|
8
|
+
}): boolean;
|
|
9
|
+
export function querySelector(selector: string, refPoint: object, opt?: {
|
|
10
|
+
globalObject?: object;
|
|
11
|
+
jsdom?: boolean;
|
|
12
|
+
}): object | null;
|
|
13
|
+
export function querySelectorAll(selector: string, refPoint: object, opt?: {
|
|
14
|
+
globalObject?: object;
|
|
15
|
+
jsdom?: boolean;
|
|
16
|
+
}): Array<object | undefined>;
|
package/types/js/matcher.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export class Matcher {
|
|
2
|
-
constructor(selector: string, refPoint: object
|
|
2
|
+
constructor(selector: string, refPoint: object, opt?: {
|
|
3
|
+
warn?: object;
|
|
4
|
+
});
|
|
3
5
|
_createIterator(ast?: object, root?: object): object;
|
|
4
6
|
_parseAST(ast: object, node: object): Array<object | undefined>;
|
|
5
7
|
_matchAdjacentLeaves(leaves: Array<object>, node: object): object | null;
|
|
@@ -32,3 +34,4 @@ export function matchIDSelector(ast?: object, node?: object): object | null;
|
|
|
32
34
|
export function matchLanguagePseudoClass(ast?: object, node?: object): object | null;
|
|
33
35
|
export function matchPseudoClassSelector(ast?: object, node?: object, refPoint?: object): Array<object | undefined>;
|
|
34
36
|
export function matchTypeSelector(ast?: object, node?: object): object | null;
|
|
37
|
+
export function unescapeSelector(selector?: string): string | null;
|
package/types/js/parser.d.ts
CHANGED