# DOM Selector [![build](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml) [![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) [![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/dom-selector)](https://www.npmjs.com/package/@asamuzakjp/dom-selector) A CSS selector engine built for strict specification compliance. ## Features * **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`). * **Utility Functions**: Provides utility methods alongside standard querying, such as `check()` for AST evaluation and `extractSubjects()` for extracting subject keys from selectors. * **jsdom's Default Engine**: Adopted as the CSS selector engine for [jsdom](https://github.com/jsdom/jsdom). ## Install ``` console npm i @asamuzakjp/dom-selector ``` ## Usage ``` javascript import { DOMSelector } from '@asamuzakjp/dom-selector'; import { JSDOM } from 'jsdom'; const { window } = new JSDOM(); // Destructuring methods (all methods are bound to the instance) const { check, closest, extractSubjects, matches, querySelector, querySelectorAll, supports } = new DOMSelector(window); ``` ## API ### `new DOMSelector(window, document?, opt?)` Creates an instance of the DOMSelector. * `window` **{Window}** The window object. * `document` **{Document}?** The document object. Defaults to window.document. * `opt` **{object}?** Options: * `opt.cacheSize` **{number}?** Maximum number of items to store in the internal cache. Default is 2048. ### `matches(selector, node, opt?)` Equivalent to [Element.matches()](https://developer.mozilla.org/docs/Web/API/Element/matches). * `selector` **{string}** CSS selector. * `node` **{Element}** Element node. * `opt` **{object}?** Options: * `opt.noexcept` **{boolean}?** Do not throw exceptions. * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class). * **Returns** **{boolean}** `true` if matched, `false` otherwise. ### `closest(selector, node, opt?)` Equivalent to [Element.closest()](https://developer.mozilla.org/docs/Web/API/Element/closest). * `selector` **{string}** CSS selector. * `node` **{Element}** Element node. * `opt` **{object}?** Options: * `opt.noexcept` **{boolean}?** Do not throw exceptions. * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class). * **Returns** **{Element | null}** The matched ancestor node or `null`. ### `querySelector(selector, node, opt?)` 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). * `selector` **{string}** CSS selector. * `node` **{Document | DocumentFragment | Element}** Node to find within. * `opt` **{object}?** Options: * `opt.noexcept` **{boolean}?** Do not throw exceptions. * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class). * **Returns** **{Element | null}** The matched node or `null`. ### `querySelectorAll(selector, node, opt?)` 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). **NOTE**: Returns a standard `Array`, not a `NodeList`. * `selector` **{string}** CSS selector. * `node` **{Document | DocumentFragment | Element}** Node to find within. * `opt` **{object}?** Options: * `opt.noexcept` **{boolean}?** Do not throw exceptions. * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class). * **Returns** **{Array}** Array of matched nodes. ### `check(selector, node, opt?)` Checks if an element matches a CSS selector and returns additional abstract syntax tree (AST) information. **NOTE**: Any pseudo-elements in the selector are excluded from the matching evaluation. * `selector` **{string}** CSS selector. * `node` **{Element}** Element node. * `opt` **{object}?** Options: * `opt.noexcept` **{boolean}?** Do not throw exceptions. * `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class). * **Returns** **{object}** An object containing the following properties: * `match` **{boolean}** `true` if the element matches the selector, `false` otherwise. * `pseudoElement` **{string | null}** The pseudo-element extracted from the selector, if any. * `ast` **{object | null}** The parsed AST object. ### `extractSubjects(selector, caseSensitive?)` Parses a selector and extracts the rightmost subject keys (Id, Class, Tag). * `selector` **{string}** CSS selector. * `caseSensitive` **{boolean}?** `true` if the tag key should be case sensitive. Defaults to `false`. * **Returns** **{Array\<{id: string|null, className: string|null, tag: string|null}\>}** An array of extracted keys. ### `supports(selector)` Checks if the given CSS selector is supported by this engine. See the table below for the full list of supported selectors. * `selector` **{string}** CSS selector. * **Returns** **{boolean}** `true` if the selector is supported, `false` otherwise. ### `clear(clearAll?)` Clears the internal caches to free up memory. * `clearAll` **{boolean}?** Whether to clear all caches. If false, only cached matching results are cleared. Defaults to `false`. * **Returns** **{void}** ## Supported CSS selectors | Pattern | Supported | Note | | :--- | :---: | :--- | | `*` | ✓ | | | `E` | ✓ | | | ns\|E | ✓ | | | *\|E | ✓ | | | \|E | ✓ | | | `E F` | ✓ | | | `E > F` | ✓ | | | `E + F` | ✓ | | | `E ~ F` | ✓ | | | F \|\| E | Unsupported | | | `E.warning` | ✓ | | | `E#myid` | ✓ | | | `E[foo]` | ✓ | | | `E[foo="bar"]` | ✓ | | | `E[foo="bar" i]` | ✓ | | | `E[foo="bar" s]` | ✓ | | | `E[foo~="bar"]` | ✓ | | | `E[foo^="bar"]` | ✓ | | | `E[foo$="bar"]` | ✓ | | | `E[foo*="bar"]` | ✓ | | | E[foo\|="en"] | ✓ | | | `E:is(s1, s2, …)` | ✓ | | | `E:not(s1, s2, …)` | ✓ | | | `E:where(s1, s2, …)` | ✓ | | | `E:has(rs1, rs2, …)` | ✓ | | | `E:defined` | Partially supported | Matching with MathML is not yet supported. | | `E:dir(ltr)` | ✓ | | | `E:lang(en)` | ✓ | | | `E:any-link` | ✓ | | | `E:link` | ✓ | | | `E:visited` | ✓ | Returns `false` or `null` to prevent fingerprinting. | | `E:local-link` | ✓ | | | `E:target` | ✓ | | | `E:target-within` | | Removed from spec. | | `E:scope` | ✓ | | | `E:hover` | ✓ | | | `E:active` | ✓ | | | `E:focus` | ✓ | | | `E:focus-visible` | ✓ | | | `E:focus-within` | ✓ | | | `E:current` | Unsupported | | | `E:current(s)` | Unsupported | | | `E:past` | Unsupported | | | `E:future` | Unsupported | | | `E:open` | Partially supported | Matching with `` are not supported. | | `E:popover-open` | Unsupported | | | `E:enabled`
`E:disabled` | ✓ | | | `E:read-write`
`E:read-only` | ✓ | | | `E:placeholder-shown` | ✓ | | | `E:default` | ✓ | | | `E:checked` | ✓ | | | `E:indeterminate` | ✓ | | | `E:blank` | Unsupported | | | `E:valid`
`E:invalid` | ✓ | | | `E:in-range`
`E:out-of-range` | ✓ | | | `E:required`
`E:optional` | ✓ | | | `E:user-valid`
`E:user-invalid` | Unsupported | | | `E:root` | ✓ | | | `E:empty` | ✓ | | | `E:nth-child(n [of S]?)` | ✓ | | | `E:nth-last-child(n [of S]?)` | ✓ | | | `E:first-child` | ✓ | | | `E:last-child` | ✓ | | | `E:only-child` | ✓ | | | `E:nth-of-type(n)` | ✓ | | | `E:nth-last-of-type(n)` | ✓ | | | `E:first-of-type` | ✓ | | | `E:last-of-type` | ✓ | | | `E:only-of-type` | ✓ | | | `E:nth-col(n)` | Unsupported | | | `E:nth-last-col(n)` | Unsupported | | | `CE:state(v)` | ✓ | \*1 | | `:host` | ✓ | | | `:host(s)` | ✓ | | | `:host(:state(v))` | ✓ | \*1 | | `:host:has(rs1, rs2, ...)` | ✓ | | | `:host(s):has(rs1, rs2, ...)` | ✓ | | | `:host-context(s)` | ✓ | | | `:host-context(s):has(rs1, rs2, ...)` | ✓ | | | `&` | ✓ | Only supports outermost `&`, i.e. equivalent to `:scope` | \*1: `ElementInternals.states`, i.e. `CustomStateSet`, is not implemented in jsdom, so you need to apply a patch in the custom element constructor. ``` javascript class LabeledCheckbox extends window.HTMLElement { #internals; constructor() { super(); this.#internals = this.attachInternals(); // patch CustomStateSet if (!this.#internals.states) { this.#internals.states = new Set(); } this.addEventListener('click', this._onClick.bind(this)); } get checked() { return this.#internals.states.has('checked'); } set checked(flag) { if (flag) { this.#internals.states.add('checked'); } else { this.#internals.states.delete('checked'); } } _onClick(event) { this.checked = !this.checked; } } ``` ## Performance See [benchmark](https://github.com/asamuzaK/domSelector/actions/workflows/benchmark.yml) for the latest results. ## Acknowledgments The following resources have been of great help in the development of the DOM Selector. * [CSSTree](https://github.com/csstree/csstree) * [selery](https://github.com/danburzo/selery) * [jsdom](https://github.com/jsdom/jsdom) * [nwsapi](https://github.com/dperini/nwsapi) --- Copyright (c) 2023 [asamuzaK (Kazz)](https://github.com/asamuzaK/)