@asamuzakjp/dom-selector 8.0.0 → 8.0.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
@@ -4,175 +4,213 @@
4
4
  [![CodeQL](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql)
5
5
  [![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/dom-selector)](https://www.npmjs.com/package/@asamuzakjp/dom-selector)
6
6
 
7
- A CSS selector engine.
7
+ A CSS selector engine built for strict specification compliance.
8
+
9
+ ## Features
10
+
11
+ * **Strict Specification Compliance**: Strictly adheres to modern web standards. It accurately parses, evaluates, and extracts elements across complex combinations of pseudo-classes and HTML attributes. Features comprehensive support for CSS Selectors Level 4 (e.g., `:is()`, `:not()`, `:where()`, `:has()`) and Shadow DOM pseudo-classes (`:host`, `:host-context`).
12
+ * **Utility Functions**: Provides utility methods alongside standard querying, such as `check()` for AST evaluation and `extractSubjects()` for extracting subject keys from selectors.
13
+ * **jsdom's Default Engine**: Adopted as the CSS selector engine for [jsdom](https://github.com/jsdom/jsdom).
8
14
 
9
15
  ## Install
10
16
 
11
- ```console
17
+ ``` console
12
18
  npm i @asamuzakjp/dom-selector
13
19
  ```
14
20
 
15
21
  ## Usage
16
22
 
17
- ```javascript
23
+ ``` javascript
18
24
  import { DOMSelector } from '@asamuzakjp/dom-selector';
19
25
  import { JSDOM } from 'jsdom';
20
26
 
21
27
  const { window } = new JSDOM();
28
+
29
+ // Destructuring methods (all methods are bound to the instance)
22
30
  const {
23
- closest, matches, querySelector, querySelectorAll
31
+ check, closest, extractSubjects, matches, querySelector, querySelectorAll, supports
24
32
  } = new DOMSelector(window);
25
33
  ```
26
34
 
27
- <!-- Generated by documentation.js. Update this documentation by updating the source code. -->
35
+ ## API
36
+
37
+ ### `new DOMSelector(window, document?, opt?)`
38
+
39
+ Creates an instance of the DOMSelector.
28
40
 
29
- ### matches(selector, node, opt)
41
+ * `window` **{Window}** The window object.
42
+ * `document` **{Document}?** The document object. Defaults to window.document.
43
+ * `opt` **{object}?** Options:
44
+ * `opt.cacheSize` **{number}?** Maximum number of items to store in the internal cache. Default is 2048.
30
45
 
31
- matches - equivalent to [Element.matches()][64]
46
+ ### `matches(selector, node, opt?)`
32
47
 
33
- #### Parameters
48
+ Equivalent to [Element.matches()](https://developer.mozilla.org/docs/Web/API/Element/matches).
34
49
 
35
- - `selector` **[string][59]** CSS selector
36
- - `node` **[object][60]** Element node
37
- - `opt` **[object][60]?** options
38
- - `opt.noexcept` **[boolean][61]?** no exception
39
- - `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
50
+ * `selector` **{string}** CSS selector.
51
+ * `node` **{Element}** Element node.
52
+ * `opt` **{object}?** Options:
53
+ * `opt.noexcept` **{boolean}?** Do not throw exceptions.
54
+ * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
55
+ * **Returns** **{boolean}** `true` if matched, `false` otherwise.
40
56
 
41
- Returns **[boolean][61]** `true` if matched, `false` otherwise
57
+ ### `closest(selector, node, opt?)`
42
58
 
59
+ Equivalent to [Element.closest()](https://developer.mozilla.org/docs/Web/API/Element/closest).
43
60
 
44
- ### closest(selector, node, opt)
61
+ * `selector` **{string}** CSS selector.
62
+ * `node` **{Element}** Element node.
63
+ * `opt` **{object}?** Options:
64
+ * `opt.noexcept` **{boolean}?** Do not throw exceptions.
65
+ * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
66
+ * **Returns** **{Element | null}** The matched ancestor node or `null`.
45
67
 
46
- closest - equivalent to [Element.closest()][65]
68
+ ### `querySelector(selector, node, opt?)`
47
69
 
48
- #### Parameters
70
+ Equivalent to [Document.querySelector()](https://developer.mozilla.org/docs/Web/API/Document/querySelector), [DocumentFragment.querySelector()](https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelector) and [Element.querySelector()](https://developer.mozilla.org/docs/Web/API/Element/querySelector).
49
71
 
50
- - `selector` **[string][59]** CSS selector
51
- - `node` **[object][60]** Element node
52
- - `opt` **[object][60]?** options
53
- - `opt.noexcept` **[boolean][61]?** no exception
54
- - `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
72
+ * `selector` **{string}** CSS selector.
73
+ * `node` **{Document | DocumentFragment | Element}** Node to find within.
74
+ * `opt` **{object}?** Options:
75
+ * `opt.noexcept` **{boolean}?** Do not throw exceptions.
76
+ * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
77
+ * **Returns** **{Element | null}** The matched node or `null`.
55
78
 
56
- Returns **[object][60]?** matched node
79
+ ### `querySelectorAll(selector, node, opt?)`
57
80
 
81
+ Equivalent to [Document.querySelectorAll()](https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll), [DocumentFragment.querySelectorAll()](https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelectorAll) and [Element.querySelectorAll()](https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll).
82
+ **NOTE**: Returns a standard `Array`, not a `NodeList`.
58
83
 
59
- ### querySelector(selector, node, opt)
84
+ * `selector` **{string}** CSS selector.
85
+ * `node` **{Document | DocumentFragment | Element}** Node to find within.
86
+ * `opt` **{object}?** Options:
87
+ * `opt.noexcept` **{boolean}?** Do not throw exceptions.
88
+ * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
89
+ * **Returns** **{Array}** Array of matched nodes.
60
90
 
61
- querySelector - equivalent to [Document.querySelector()][66], [DocumentFragment.querySelector()][67] and [Element.querySelector()][68]
91
+ ### `check(selector, node, opt?)`
62
92
 
63
- #### Parameters
93
+ Checks if an element matches a CSS selector and returns additional abstract syntax tree (AST) information.
94
+ **NOTE**: Any pseudo-elements in the selector are excluded from the matching evaluation.
64
95
 
65
- - `selector` **[string][59]** CSS selector
66
- - `node` **[object][60]** Document, DocumentFragment or Element node
67
- - `opt` **[object][60]?** options
68
- - `opt.noexcept` **[boolean][61]?** no exception
69
- - `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
96
+ * `selector` **{string}** CSS selector.
97
+ * `node` **{Element}** Element node.
98
+ * `opt` **{object}?** Options:
99
+ * `opt.noexcept` **{boolean}?** Do not throw exceptions.
100
+ * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
101
+ * **Returns** **{object}** An object containing the following properties:
102
+ * `match` **{boolean}** `true` if the element matches the selector, `false` otherwise.
103
+ * `pseudoElement` **{string | null}** The pseudo-element extracted from the selector, if any.
104
+ * `ast` **{object | null}** The parsed AST object.
70
105
 
71
- Returns **[object][60]?** matched node
106
+ ### `extractSubjects(selector, caseSensitive?)`
72
107
 
108
+ Parses a selector and extracts the rightmost subject keys (Id, Class, Tag).
73
109
 
74
- ### querySelectorAll(selector, node, opt)
110
+ * `selector` **{string}** CSS selector.
111
+ * `caseSensitive` **{boolean}?** `true` if the tag key should be case sensitive. Defaults to `false`.
112
+ * **Returns** **{Array\<{id: string|null, className: string|null, tag: string|null}\>}** An array of extracted keys.
75
113
 
76
- querySelectorAll - equivalent to [Document.querySelectorAll()][69], [DocumentFragment.querySelectorAll()][70] and [Element.querySelectorAll()][71]
77
- **NOTE**: returns Array, not NodeList
114
+ ### `supports(selector)`
78
115
 
79
- #### Parameters
116
+ Checks if the given CSS selector is supported by this engine.
117
+ See the table below for the full list of supported selectors.
80
118
 
81
- - `selector` **[string][59]** CSS selector
82
- - `node` **[object][60]** Document, DocumentFragment or Element node
83
- - `opt` **[object][60]?** options
84
- - `opt.noexcept` **[boolean][61]?** no exception
85
- - `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
119
+ * `selector` **{string}** CSS selector.
120
+ * **Returns** **{boolean}** `true` if the selector is supported, `false` otherwise.
86
121
 
87
- Returns **[Array][62]&lt;([object][60] \| [undefined][63])>** array of matched nodes
122
+ ### `clear()`
88
123
 
124
+ Clears the internal cache of finder results to free up memory.
125
+
126
+ * **Returns** **{void}**
89
127
 
90
128
  ## Supported CSS selectors
91
129
 
92
- |Pattern|Supported|Note|
93
- |:--------|:-------:|:--------|
94
- |\*|✓| |
95
- |E|✓| |
96
- |ns\|E|✓| |
97
- |\*\|E|✓| |
98
- |\|E|✓| |
99
- |E&nbsp;F|✓| |
100
- |E > F|✓| |
101
- |E + F|✓| |
102
- |E ~ F|✓| |
103
- |F \|\| E|Unsupported| |
104
- |E.warning|✓| |
105
- |E#myid|✓| |
106
- |E\[foo\]|✓| |
107
- |E\[foo="bar"\]|✓| |
108
- |E\[foo="bar"&nbsp;i\]|✓| |
109
- |E\[foo="bar"&nbsp;s\]|✓| |
110
- |E\[foo~="bar"\]|✓| |
111
- |E\[foo^="bar"\]|✓| |
112
- |E\[foo$="bar"\]|✓| |
113
- |E\[foo*="bar"\]|✓| |
114
- |E\[foo\|="en"\]|✓| |
115
- |E:is(s1, s2, …)|✓| |
116
- |E:not(s1, s2, …)|✓| |
117
- |E:where(s1, s2, …)|✓| |
118
- |E:has(rs1, rs2, …)|✓| |
119
- |E:defined|Partially supported|Matching with MathML is not yet supported.|
120
- |E:dir(ltr)|✓| |
121
- |E:lang(en)|✓| |
122
- |E:any&#8209;link|✓| |
123
- |E:link|✓| |
124
- |E:visited|✓|Returns `false` or `null` to prevent fingerprinting.|
125
- |E:local&#8209;link|✓| |
126
- |E:target|✓| |
127
- |E:target&#8209;within|✓| |
128
- |E:scope|✓| |
129
- |E:hover|✓| |
130
- |E:active|✓| |
131
- |E:focus|✓| |
132
- |E:focus&#8209;visible|✓| |
133
- |E:focus&#8209;within|✓| |
134
- |E:current|Unsupported| |
135
- |E:current(s)|Unsupported| |
136
- |E:past|Unsupported| |
137
- |E:future|Unsupported| |
138
- |E:open<br>E:closed|Partially supported|Matching with &lt;select&gt;, e.g. `select:open`, is not supported.|
139
- |E:popover-open|Unsupported| |
140
- |E:enabled<br>E:disabled|✓| |
141
- |E:read&#8209;write<br>E:read&#8209;only|✓| |
142
- |E:placeholder&#8209;shown|✓| |
143
- |E:default|✓| |
144
- |E:checked|✓| |
145
- |E:indeterminate|✓| |
146
- |E:blank|Unsupported| |
147
- |E:valid<br>E:invalid|✓| |
148
- |E:in-range<br>E:out-of-range|✓| |
149
- |E:required<br>E:optional|✓| |
150
- |E:user&#8209;valid<br>E:user&#8209;invalid|Unsupported| |
151
- |E:root|✓| |
152
- |E:empty|✓| |
153
- |E:nth&#8209;child(n&nbsp;[of&nbsp;S]?)|✓| |
154
- |E:nth&#8209;last&#8209;child(n&nbsp;[of&nbsp;S]?)|✓| |
155
- |E:first&#8209;child|✓| |
156
- |E:last&#8209;child|✓| |
157
- |E:only&#8209;child|✓| |
158
- |E:nth&#8209;of&#8209;type(n)|✓| |
159
- |E:nth&#8209;last&#8209;of&#8209;type(n)|✓| |
160
- |E:first&#8209;of&#8209;type|✓| |
161
- |E:last&#8209;of&#8209;type|✓| |
162
- |E:only&#8209;of&#8209;type|✓| |
163
- |E:nth&#8209;col(n)|Unsupported| |
164
- |E:nth&#8209;last&#8209;col(n)|Unsupported| |
165
- |CE:state(v)|✓|*1|
166
- |:host|✓| |
167
- |:host(s)|✓| |
168
- |:host(:state(v))|✓|*1|
169
- |:host:has(rs1, rs2, ...)|✓| |
170
- |:host(s):has(rs1, rs2, ...)|✓| |
171
- |:host&#8209;context(s)|✓| |
172
- |:host&#8209;context(s):has(rs1, rs2, ...)|✓| |
173
- |&amp;|✓|Only supports outermost `&`, i.e. equivalent to `:scope`|
174
-
175
- *1: `ElementInternals.states`, i.e. `CustomStateSet`, is not implemented in jsdom, so you need to apply a patch in the custom element constructor.
130
+ | Pattern | Supported | Note |
131
+ | :--- | :---: | :--- |
132
+ | `*` | ✓ | |
133
+ | `E` | ✓ | |
134
+ | <code>ns\|E</code> | ✓ | |
135
+ | <code>*\|E</code> | ✓ | |
136
+ | <code>\|E</code> | ✓ | |
137
+ | `E F` | ✓ | |
138
+ | `E > F` | ✓ | |
139
+ | `E + F` | ✓ | |
140
+ | `E ~ F` | ✓ | |
141
+ | <code>F \|\| E</code> | Unsupported | |
142
+ | `E.warning` | ✓ | |
143
+ | `E#myid` | ✓ | |
144
+ | `E[foo]` | ✓ | |
145
+ | `E[foo="bar"]` | ✓ | |
146
+ | `E[foo="bar" i]` | ✓ | |
147
+ | `E[foo="bar" s]` | ✓ | |
148
+ | `E[foo~="bar"]` | ✓ | |
149
+ | `E[foo^="bar"]` | ✓ | |
150
+ | `E[foo$="bar"]` | ✓ | |
151
+ | `E[foo*="bar"]` | ✓ | |
152
+ | <code>E[foo\|="en"]</code> | ✓ | |
153
+ | `E:is(s1, s2, …)` | ✓ | |
154
+ | `E:not(s1, s2, …)` | ✓ | |
155
+ | `E:where(s1, s2, …)` | ✓ | |
156
+ | `E:has(rs1, rs2, …)` | ✓ | |
157
+ | `E:defined` | Partially supported | Matching with MathML is not yet supported. |
158
+ | `E:dir(ltr)` | ✓ | |
159
+ | `E:lang(en)` | ✓ | |
160
+ | `E:any-link` | ✓ | |
161
+ | `E:link` | ✓ | |
162
+ | `E:visited` | ✓ | Returns `false` or `null` to prevent fingerprinting. |
163
+ | `E:local-link` | ✓ | |
164
+ | `E:target` | ✓ | |
165
+ | `E:target-within` | ✓ | |
166
+ | `E:scope` | ✓ | |
167
+ | `E:hover` | ✓ | |
168
+ | `E:active` | ✓ | |
169
+ | `E:focus` | ✓ | |
170
+ | `E:focus-visible` | ✓ | |
171
+ | `E:focus-within` | ✓ | |
172
+ | `E:current` | Unsupported | |
173
+ | `E:current(s)` | Unsupported | |
174
+ | `E:past` | Unsupported | |
175
+ | `E:future` | Unsupported | |
176
+ | `E:open`<br>`E:closed` | Partially supported | Matching with `<select>`, e.g. `select:open`, is not supported. |
177
+ | `E:popover-open` | Unsupported | |
178
+ | `E:enabled`<br>`E:disabled` | ✓ | |
179
+ | `E:read-write`<br>`E:read-only` | ✓ | |
180
+ | `E:placeholder-shown` | ✓ | |
181
+ | `E:default` | ✓ | |
182
+ | `E:checked` | ✓ | |
183
+ | `E:indeterminate` | ✓ | |
184
+ | `E:blank` | Unsupported | |
185
+ | `E:valid`<br>`E:invalid` | ✓ | |
186
+ | `E:in-range`<br>`E:out-of-range` | ✓ | |
187
+ | `E:required`<br>`E:optional` | ✓ | |
188
+ | `E:user-valid`<br>`E:user-invalid` | Unsupported | |
189
+ | `E:root` | ✓ | |
190
+ | `E:empty` | ✓ | |
191
+ | `E:nth-child(n [of S]?)` | ✓ | |
192
+ | `E:nth-last-child(n [of S]?)` | ✓ | |
193
+ | `E:first-child` | ✓ | |
194
+ | `E:last-child` | ✓ | |
195
+ | `E:only-child` | ✓ | |
196
+ | `E:nth-of-type(n)` | ✓ | |
197
+ | `E:nth-last-of-type(n)` | ✓ | |
198
+ | `E:first-of-type` | ✓ | |
199
+ | `E:last-of-type` | ✓ | |
200
+ | `E:only-of-type` | ✓ | |
201
+ | `E:nth-col(n)` | Unsupported | |
202
+ | `E:nth-last-col(n)` | Unsupported | |
203
+ | `CE:state(v)` | ✓ | \*1 |
204
+ | `:host` | ✓ | |
205
+ | `:host(s)` | ✓ | |
206
+ | `:host(:state(v))` | ✓ | \*1 |
207
+ | `:host:has(rs1, rs2, ...)` | ✓ | |
208
+ | `:host(s):has(rs1, rs2, ...)` | ✓ | |
209
+ | `:host-context(s)` | ✓ | |
210
+ | `:host-context(s):has(rs1, rs2, ...)` | ✓ | |
211
+ | `&` | ✓ | Only supports outermost `&`, i.e. equivalent to `:scope` |
212
+
213
+ \*1: `ElementInternals.states`, i.e. `CustomStateSet`, is not implemented in jsdom, so you need to apply a patch in the custom element constructor.
176
214
 
177
215
  ``` javascript
178
216
  class LabeledCheckbox extends window.HTMLElement {
@@ -202,43 +240,19 @@ class LabeledCheckbox extends window.HTMLElement {
202
240
  }
203
241
  ```
204
242
 
205
-
206
243
  ## Performance
207
244
 
208
245
  See [benchmark](https://github.com/asamuzaK/domSelector/actions/workflows/benchmark.yml) for the latest results.
209
246
 
210
-
211
247
  ## Acknowledgments
212
248
 
213
249
  The following resources have been of great help in the development of the DOM Selector.
214
250
 
215
- - [CSSTree](https://github.com/csstree/csstree)
216
- - [selery](https://github.com/danburzo/selery)
217
- - [jsdom](https://github.com/jsdom/jsdom)
218
- - [nwsapi](https://github.com/dperini/nwsapi)
251
+ * [CSSTree](https://github.com/csstree/csstree)
252
+ * [selery](https://github.com/danburzo/selery)
253
+ * [jsdom](https://github.com/jsdom/jsdom)
254
+ * [nwsapi](https://github.com/dperini/nwsapi)
219
255
 
220
256
  ---
221
- Copyright (c) 2023 [asamuzaK (Kazz)](https://github.com/asamuzaK/)
222
-
223
257
 
224
- [1]: #matches
225
- [2]: #parameters
226
- [3]: #closest
227
- [4]: #parameters-1
228
- [5]: #queryselector
229
- [6]: #parameters-2
230
- [7]: #queryselectorall
231
- [8]: #parameters-3
232
- [59]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
233
- [60]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
234
- [61]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
235
- [62]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
236
- [63]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
237
- [64]: https://developer.mozilla.org/docs/Web/API/Element/matches
238
- [65]: https://developer.mozilla.org/docs/Web/API/Element/closest
239
- [66]: https://developer.mozilla.org/docs/Web/API/Document/querySelector
240
- [67]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelector
241
- [68]: https://developer.mozilla.org/docs/Web/API/Element/querySelector
242
- [69]: https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll
243
- [70]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelectorAll
244
- [71]: https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll
258
+ Copyright (c) 2023 [asamuzaK (Kazz)](https://github.com/asamuzaK/)
package/package.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "types"
17
17
  ],
18
18
  "type": "module",
19
+ "types": "./types/index.d.ts",
19
20
  "exports": {
20
21
  ".": {
21
22
  "types": "./types/index.d.ts",
@@ -24,20 +25,20 @@
24
25
  "./package.json": "./package.json"
25
26
  },
26
27
  "dependencies": {
27
- "@asamuzakjp/generational-cache": "^1.0.1",
28
+ "@asamuzakjp/generational-cache": "^2.0.2",
28
29
  "bidi-js": "^1.0.3",
29
30
  "css-tree": "^3.2.1",
30
31
  "is-potential-custom-element-name": "^1.0.1"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/css-tree": "^2.3.11",
34
- "@types/node": "^25.6.0",
35
+ "@types/node": "^25.9.1",
35
36
  "c8": "^11.0.0",
36
37
  "chai": "^6.2.2",
37
38
  "commander": "^14.0.3",
38
39
  "eslint": "^9.39.4",
39
40
  "eslint-config-prettier": "^10.1.8",
40
- "eslint-plugin-jsdoc": "^62.9.0",
41
+ "eslint-plugin-jsdoc": "^63.0.0",
41
42
  "eslint-plugin-prettier": "^5.5.5",
42
43
  "eslint-plugin-regexp": "^3.1.0",
43
44
  "eslint-plugin-unicorn": "^64.0.0",
@@ -47,8 +48,8 @@
47
48
  "mocha": "^11.7.5",
48
49
  "neostandard": "^0.13.0",
49
50
  "prettier": "^3.8.3",
50
- "sinon": "^21.1.2",
51
- "tinybench": "^6.0.1",
51
+ "sinon": "^22.0.0",
52
+ "tinybench": "^6.0.2",
52
53
  "typescript": "^6.0.3",
53
54
  "wpt-runner": "^7.0.0"
54
55
  },
@@ -79,5 +80,5 @@
79
80
  "engines": {
80
81
  "node": "^22.13.0 || >=24.0.0"
81
82
  },
82
- "version": "8.0.0"
83
+ "version": "8.0.2"
83
84
  }
package/src/index.js CHANGED
@@ -11,15 +11,15 @@ import { Finder } from './js/finder.js';
11
11
  import { Nwsapi } from './js/nwsapi.js';
12
12
  import { extractSubjectsAst } from './js/parser.js';
13
13
  import {
14
+ extractSubjectsRegExp,
14
15
  filterSelector,
15
- getType,
16
- extractSubjectsRegExp
17
- } from './js/utility.js';
16
+ isSupportedAST
17
+ } from './js/selector.js';
18
+ import { getType } from './js/utility.js';
18
19
 
19
20
  /* constants */
20
21
  import {
21
22
  DOCUMENT_NODE,
22
- DOCUMENT_FRAGMENT_NODE,
23
23
  ELEMENT_NODE,
24
24
  TARGET_ALL,
25
25
  TARGET_FIRST,
@@ -64,6 +64,23 @@ export class DOMSelector {
64
64
  this.#nwsapi = new Nwsapi(this.#window, this.#document);
65
65
  }
66
66
 
67
+ /**
68
+ * Validates a node and returns an Error if invalid.
69
+ * @private
70
+ * @param {Document|DocumentFragment|Element} node - The node to check.
71
+ * @param {boolean} [element] - `true` if the node must be an Element.
72
+ * @returns {TypeError|null} Returns a TypeError if invalid, otherwise null.
73
+ */
74
+ #validateNodeType = (node, element = false) => {
75
+ if (!node?.nodeType) {
76
+ return new this.#window.TypeError(`Unexpected type ${getType(node)}`);
77
+ }
78
+ if (element && node.nodeType !== ELEMENT_NODE) {
79
+ return new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
80
+ }
81
+ return null;
82
+ };
83
+
67
84
  /**
68
85
  * Clears the internal cache of finder results.
69
86
  * @returns {void}
@@ -94,7 +111,7 @@ export class DOMSelector {
94
111
  try {
95
112
  const ast = this.#finder.getAST(selector);
96
113
  subjects = extractSubjectsAst(ast);
97
- } catch (e) {
114
+ } catch {
98
115
  // fall through
99
116
  }
100
117
  }
@@ -105,6 +122,34 @@ export class DOMSelector {
105
122
  return subjects;
106
123
  };
107
124
 
125
+ /**
126
+ * Checks if the given CSS selector is supported by this engine.
127
+ * @param {string} selector - The CSS selector to check.
128
+ * @returns {boolean} `true` if the selector is supported, `false` otherwise.
129
+ */
130
+ supports = selector => {
131
+ if (typeof selector !== 'string') {
132
+ return false;
133
+ }
134
+ const cacheKey = `supports_${selector}`;
135
+ let isSupported = this.#cache.get(cacheKey);
136
+ if (isSupported !== undefined) {
137
+ return isSupported;
138
+ }
139
+ if (filterSelector(selector, TARGET_SELF)) {
140
+ isSupported = true;
141
+ } else {
142
+ try {
143
+ const ast = this.#finder.getAST(selector);
144
+ isSupported = isSupportedAST(ast);
145
+ } catch {
146
+ isSupported = false;
147
+ }
148
+ }
149
+ this.#cache.set(cacheKey, isSupported);
150
+ return isSupported;
151
+ };
152
+
108
153
  /**
109
154
  * Checks if an element matches a CSS selector.
110
155
  * @param {string} selector - The CSS selector to check against.
@@ -113,12 +158,9 @@ export class DOMSelector {
113
158
  * @returns {CheckResult} An object containing the check result.
114
159
  */
115
160
  check = (selector, node, opt = {}) => {
116
- if (!node?.nodeType) {
117
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
118
- return this.#finder.onError(e, opt);
119
- } else if (node.nodeType !== ELEMENT_NODE) {
120
- const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
121
- return this.#finder.onError(e, opt);
161
+ const error = this.#validateNodeType(node, true);
162
+ if (error) {
163
+ return this.#finder.onError(error, opt);
122
164
  }
123
165
  const document = node.ownerDocument;
124
166
  if (
@@ -173,12 +215,9 @@ export class DOMSelector {
173
215
  * @returns {boolean} `true` if the element matches, or `false` otherwise.
174
216
  */
175
217
  matches = (selector, node, opt = {}) => {
176
- if (!node?.nodeType) {
177
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
178
- return this.#finder.onError(e, opt);
179
- } else if (node.nodeType !== ELEMENT_NODE) {
180
- const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
181
- return this.#finder.onError(e, opt);
218
+ const error = this.#validateNodeType(node, true);
219
+ if (error) {
220
+ return this.#finder.onError(error, opt);
182
221
  }
183
222
  const document = node.ownerDocument;
184
223
  if (
@@ -223,12 +262,9 @@ export class DOMSelector {
223
262
  * @returns {?Element} The first matching ancestor element, or `null`.
224
263
  */
225
264
  closest = (selector, node, opt = {}) => {
226
- if (!node?.nodeType) {
227
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
228
- return this.#finder.onError(e, opt);
229
- } else if (node.nodeType !== ELEMENT_NODE) {
230
- const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
231
- return this.#finder.onError(e, opt);
265
+ const error = this.#validateNodeType(node, true);
266
+ if (error) {
267
+ return this.#finder.onError(error, opt);
232
268
  }
233
269
  const document = node.ownerDocument;
234
270
  if (
@@ -282,17 +318,17 @@ export class DOMSelector {
282
318
  * @returns {?Element} The first matching element, or `null`.
283
319
  */
284
320
  querySelector = (selector, node, opt = {}) => {
285
- if (!node?.nodeType) {
286
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
287
- return this.#finder.onError(e, opt);
321
+ const error = this.#validateNodeType(node);
322
+ if (error) {
323
+ return this.#finder.onError(error, opt);
288
324
  }
289
325
  const document =
290
326
  node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
291
327
  if (
328
+ node === this.#document &&
292
329
  document === this.#document &&
293
330
  document.contentType === 'text/html' &&
294
- document.documentElement &&
295
- (node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
331
+ document.documentElement
296
332
  ) {
297
333
  const cacheKey = `querySelector_${selector}`;
298
334
  let filterMatches = this.#cache.get(cacheKey);
@@ -333,17 +369,17 @@ export class DOMSelector {
333
369
  * @returns {Array<Element>} An array of elements, or an empty array.
334
370
  */
335
371
  querySelectorAll = (selector, node, opt = {}) => {
336
- if (!node?.nodeType) {
337
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
338
- return this.#finder.onError(e, opt);
372
+ const error = this.#validateNodeType(node);
373
+ if (error) {
374
+ return this.#finder.onError(error, opt);
339
375
  }
340
376
  const document =
341
377
  node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
342
378
  if (
379
+ node === this.#document &&
343
380
  document === this.#document &&
344
381
  document.contentType === 'text/html' &&
345
- document.documentElement &&
346
- (node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
382
+ document.documentElement
347
383
  ) {
348
384
  const cacheKey = `querySelectorAll_${selector}`;
349
385
  let filterMatches = this.#cache.get(cacheKey);