# 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. ## Install ```console npm i @asamuzakjp/dom-selector ``` ## Usage ```javascript import { DOMSelector } from '@asamuzakjp/dom-selector'; import { JSDOM } from 'jsdom'; const { window } = new JSDOM(); const { closest, matches, querySelector, querySelectorAll } = new DOMSelector(window); ``` ### matches(selector, node, opt) matches - equivalent to [Element.matches()][64] #### Parameters - `selector` **[string][59]** CSS selector - `node` **[object][60]** Element node - `opt` **[object][60]?** options - `opt.noexcept` **[boolean][61]?** no exception - `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class Returns **[boolean][61]** `true` if matched, `false` otherwise ### closest(selector, node, opt) closest - equivalent to [Element.closest()][65] #### Parameters - `selector` **[string][59]** CSS selector - `node` **[object][60]** Element node - `opt` **[object][60]?** options - `opt.noexcept` **[boolean][61]?** no exception - `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class Returns **[object][60]?** matched node ### querySelector(selector, node, opt) querySelector - equivalent to [Document.querySelector()][66], [DocumentFragment.querySelector()][67] and [Element.querySelector()][68] #### Parameters - `selector` **[string][59]** CSS selector - `node` **[object][60]** Document, DocumentFragment or Element node - `opt` **[object][60]?** options - `opt.noexcept` **[boolean][61]?** no exception - `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class Returns **[object][60]?** matched node ### querySelectorAll(selector, node, opt) querySelectorAll - equivalent to [Document.querySelectorAll()][69], [DocumentFragment.querySelectorAll()][70] and [Element.querySelectorAll()][71] **NOTE**: returns Array, not NodeList #### Parameters - `selector` **[string][59]** CSS selector - `node` **[object][60]** Document, DocumentFragment or Element node - `opt` **[object][60]?** options - `opt.noexcept` **[boolean][61]?** no exception - `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class Returns **[Array][62]<([object][60] \| [undefined][63])>** array of matched nodes ## Monkey patch jsdom ``` javascript import { DOMSelector } from '@asamuzakjp/dom-selector'; import { JSDOM } from 'jsdom'; const dom = new JSDOM('', { runScripts: 'dangerously', url: 'http://localhost/', beforeParse: window => { const domSelector = new DOMSelector(window); const matches = domSelector.matches.bind(domSelector); window.Element.prototype.matches = function (...args) { if (!args.length) { throw new window.TypeError('1 argument required, but only 0 present.'); } const [selector] = args; return matches(selector, this); }; const closest = domSelector.closest.bind(domSelector); window.Element.prototype.closest = function (...args) { if (!args.length) { throw new window.TypeError('1 argument required, but only 0 present.'); } const [selector] = args; return closest(selector, this); }; const querySelector = domSelector.querySelector.bind(domSelector); window.Document.prototype.querySelector = function (...args) { if (!args.length) { throw new window.TypeError('1 argument required, but only 0 present.'); } const [selector] = args; return querySelector(selector, this); }; window.DocumentFragment.prototype.querySelector = function (...args) { if (!args.length) { throw new window.TypeError('1 argument required, but only 0 present.'); } const [selector] = args; return querySelector(selector, this); }; window.Element.prototype.querySelector = function (...args) { if (!args.length) { throw new window.TypeError('1 argument required, but only 0 present.'); } const [selector] = args; return querySelector(selector, this); }; const querySelectorAll = domSelector.querySelectorAll.bind(domSelector); window.Document.prototype.querySelectorAll = function (...args) { if (!args.length) { throw new window.TypeError('1 argument required, but only 0 present.'); } const [selector] = args; return querySelectorAll(selector, this); }; window.DocumentFragment.prototype.querySelectorAll = function (...args) { if (!args.length) { throw new window.TypeError('1 argument required, but only 0 present.'); } const [selector] = args; return querySelectorAll(selector, this); }; window.Element.prototype.querySelectorAll = function (...args) { if (!args.length) { throw new window.TypeError('1 argument required, but only 0 present.'); } const [selector] = args; return querySelectorAll(selector, this); }; } }); ``` ## 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|✓| | |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
E:closed|Partially supported|Matching with <select>, e.g. `select:open`, is not supported.| |E:popover-open|✓| | |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/) [1]: #matches [2]: #parameters [3]: #closest [4]: #parameters-1 [5]: #queryselector [6]: #parameters-2 [7]: #queryselectorall [8]: #parameters-3 [59]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String [60]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [61]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean [62]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array [63]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined [64]: https://developer.mozilla.org/docs/Web/API/Element/matches [65]: https://developer.mozilla.org/docs/Web/API/Element/closest [66]: https://developer.mozilla.org/docs/Web/API/Document/querySelector [67]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelector [68]: https://developer.mozilla.org/docs/Web/API/Element/querySelector [69]: https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll [70]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelectorAll [71]: https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll