@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 CHANGED
@@ -3,6 +3,7 @@
3
3
  [![build](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml)
4
4
  [![CodeQL](https://github.com/asamuzaK/domSelector/actions/workflows/codeql.yml/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/codeql.yml)
5
5
  [![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/dom-selector)](https://www.npmjs.com/package/@asamuzakjp/dom-selector)
6
+
6
7
  <!--
7
8
  [![release](https://img.shields.io/github/v/release/asamuzaK/domSelector)](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
- #### Table of Contents
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
- Implementation of [Element.matches()][62].
32
+ matches - [Element.matches()][64]
46
33
 
47
34
  #### Parameters
48
35
 
49
- - `selector` **[string][56]** CSS selector
50
- - `node` **[object][57]** Referenced Element node
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][58]** Result
42
+ Returns **[boolean][61]** result
53
43
 
54
44
 
55
- ### closest(selector, node)
45
+ ### closest(selector, node, opt)
56
46
 
57
- Implementation of [Element.closest()][63].
47
+ closest - [Element.closest()][65]
58
48
 
59
49
  #### Parameters
60
50
 
61
- - `selector` **[string][56]** CSS selector
62
- - `node` **[object][57]** Referenced Element node
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]?** Matched node
57
+ Returns **[object][60]?** matched node
65
58
 
66
59
 
67
- ### querySelector(selector, refPoint)
60
+ ### querySelector(selector, refPoint, opt)
68
61
 
69
- Implementation of [Document.querySelector()][64], [Element.querySelector()][65].
62
+ querySelector - [Document.querySelector()][66], [DocumentFragment.querySelector()][67], [Element.querySelector()][68]
70
63
 
71
64
  #### Parameters
72
65
 
73
- - `selector` **[string][56]** CSS selector
74
- - `refPoint` **[object][57]** Reference point. Document or Element node
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][57]?** Matched node
72
+ Returns **[object][60]?** matched node
77
73
 
78
74
 
79
- ### querySelectorAll(selector, refPoint)
75
+ ### querySelectorAll(selector, refPoint, opt)
80
76
 
81
- Implementation of [Document.querySelectorAll()][66], [Element.querySelectorAll()][67].
82
- **NOTE**: returns [Array][59], not [NodeList][61].
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][56]** CSS selector
87
- - `refPoint` **[object][57]** Reference point. Document or Element node
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][59]&lt;([object][57] \| [undefined][60])>** Array of matched nodes
88
+ Returns **[Array][62]&lt;([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
- [56]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
109
- [57]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
110
- [58]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
111
- [59]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
112
- [60]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
113
- [61]: https://developer.mozilla.org/docs/Web/API/NodeList
114
- [62]: https://developer.mozilla.org/docs/Web/API/Element/matches
115
- [63]: https://developer.mozilla.org/docs/Web/API/Element/closest
116
- [64]: https://developer.mozilla.org/docs/Web/API/Document/querySelector
117
- [65]: https://developer.mozilla.org/docs/Web/API/Element/querySelector
118
- [66]: https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll
119
- [67]: https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll
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.3",
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.8.1"
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
- // FIXME: custom element name is not fully implemented
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: identName
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 { name: astName, type: astType } = ast;
346
+ const { type: astType } = ast;
308
347
  const { classList, nodeType } = node;
309
348
  let res;
310
- if (astType === CLASS_SELECTOR && nodeType === ELEMENT_NODE &&
311
- classList.contains(astName)) {
312
- res = node;
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 { name: astName, type: astType } = ast;
365
+ const { type: astType } = ast;
325
366
  const { id, nodeType } = node;
326
367
  let res;
327
- if (astType === ID_SELECTOR && nodeType === ELEMENT_NODE &&
328
- astName === id) {
329
- res = node;
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
- const { name: astAttrName } = astName;
350
- const caseInsensitive = !(astFlags && /^s$/i.test(astFlags));
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 { name: astName, type: astType } = ast;
563
+ const { type: astType } = ast;
516
564
  const { lang, nodeType } = node;
517
565
  let res;
518
566
  if (astType === IDENTIFIER && nodeType === ELEMENT_NODE) {
519
- // TBD: what about deprecated xml:lang?
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, name: astName, type: astType } = ast;
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
- console.warn(`Unsupported pseudo-class ${astName}`);
605
- break;
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
- HTML_CUSTOM_ELEMENT.test(localName)) &&
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
- HTML_CUSTOM_ELEMENT.test(localName)) &&
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
- console.warn(`Unsupported pseudo-class ${astName}`);
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
- console.warn(`Unsupported pseudo-class ${astName}`);
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
- console.warn(`Unsupported pseudo-class ${astName}`);
870
- break;
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 { name: branchName } = branch;
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 && item.name === 'not') {
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 { name: itemName, type: itemType } = item;
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 { name: itemName, type: itemType } = item;
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
- const arr = this._match(this.#ast, this.#document);
1347
- const res = arr.length && arr.includes(this.#node);
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
- while (node) {
1360
- if (arr.includes(node)) {
1361
- res = node;
1362
- break;
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
- const arr = this._match(this.#ast, this.#node);
1375
- if (arr.length) {
1376
- const i = arr.findIndex(node => node === this.#node);
1377
- if (i >= 0) {
1378
- arr.splice(i, 1);
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 arr = this._match(this.#ast, this.#node);
1392
- if (arr.length) {
1393
- const i = arr.findIndex(node => node === this.#node);
1394
- if (i >= 0) {
1395
- arr.splice(i, 1);
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 [...new Set(arr)];
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
- if (selector === undefined || selector === null) {
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 (typeof selector !== 'string' || selector === '' ||
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): object | null;
2
- export function matches(selector: string, node: object): boolean;
3
- export function querySelector(selector: string, refPoint: object): object | null;
4
- export function querySelectorAll(selector: string, refPoint: object): Array<object | undefined>;
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>;
@@ -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;
@@ -1,4 +1,5 @@
1
1
  import { generate } from "css-tree";
2
2
  export function parseSelector(selector: string): object;
3
+ export function preprocess(...args: any[]): string;
3
4
  export function walkAST(ast?: object): Array<object | undefined>;
4
5
  export { generate as generateCSS };