DOM Selector
![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/dom-selector)
A CSS selector engine.
Install
npm i @asamuzakjp/dom-selector
Usage
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 - same functionality as Element.matches()
Parameters
selector
string CSS selectornode
object Element nodeopt
object? options
opt.noexcept
boolean? no exceptionopt.warn
boolean? console warn e.g. unsupported pseudo-class
Returns boolean true
if matched, false
otherwise
closest(selector, node, opt)
closest - same functionality as Element.closest()
Parameters
selector
string CSS selectornode
object Element nodeopt
object? options
opt.noexcept
boolean? no exceptionopt.warn
boolean? console warn e.g. unsupported pseudo-class
Returns object? matched node
querySelector(selector, node, opt)
querySelector - same functionality as Document.querySelector(), DocumentFragment.querySelector(), Element.querySelector()
Parameters
selector
string CSS selectornode
object Document, DocumentFragment or Element nodeopt
object? options
opt.noexcept
boolean? no exceptionopt.warn
boolean? console warn e.g. unsupported pseudo-class
Returns object? matched node
querySelectorAll(selector, node, opt)
querySelectorAll - same functionality as Document.querySelectorAll(), DocumentFragment.querySelectorAll(), Element.querySelectorAll()
NOTE: returns Array, not NodeList
Parameters
selector
string CSS selectornode
object Document, DocumentFragment or Element nodeopt
object? options
opt.noexcept
boolean? no exceptionopt.warn
boolean? console warn e.g. unsupported pseudo-class
Returns Array<(object | undefined)> array of matched nodes
Supported CSS selectors
Pattern | Supported | Note |
---|
* | ✓ | |
ns|E | ✓ | |
*|E | ✓ | |
|E | ✓ | |
E | ✓ | |
E:not(s1, s2, …) | ✓ | |
E:is(s1, s2, …) | ✓ | |
E:where(s1, s2, …) | ✓ | |
E:has(rs1, rs2, …) | ✓ | |
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:defined | Unsupported | |
E:dir(ltr) | ✓ | |
E:lang(en) | Partially supported | Comma-separated list of language codes, e.g. :lang(en, fr) , is not yet supported. |
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:current | Unsupported | |
E:current(s) | Unsupported | |
E:past | Unsupported | |
E:future | Unsupported | |
E:active | Unsupported | |
E:hover | Unsupported | |
E:focus | ✓ | |
E:focus‑within | ✓ | |
E:focus‑visible | Unsupported | |
E:enabled E:disabled | ✓ | |
E:read‑write E:read‑only | ✓ | |
E:placeholder‑shown | ✓ | |
E:default | ✓ | |
E:checked | ✓ | |
E:indeterminate | ✓ | |
E:valid E:invalid | ✓ | |
E:required E:optional | ✓ | |
E:blank | Unsupported | |
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 F | ✓ | |
E > F | ✓ | |
E + F | ✓ | |
E ~ F | ✓ | |
F || E | Unsupported | |
E:nth‑col(n) | Unsupported | |
E:nth‑last‑col(n) | Unsupported | |
:host | ✓ | |
:host(s) | ✓ | |
:host‑context(s) | ✓ | |
Monkey patch jsdom
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const dom = new JSDOM('', {
runScripts: 'dangerously',
url: 'http://localhost/',
beforeParse: window => {
const {
closest, matches, querySelector, querySelectorAll
} = new DOMSelector(window);
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);
};
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);
};
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);
};
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);
};
}
});
Performance
matches()
Selector | jsdom v24.0.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|
simple selector:
matches('.content') | 2,573,273 ops/sec ±1.01% | 1,673,318 ops/sec ±0.30% | 665,946 ops/sec ±0.26% | 288,434 ops/sec ±0.25% | jsdom is the fastest and 8.9 times faster than patched-jsdom. |
compound selector:
matches('p.content[id]:only-child') | 1,176,008 ops/sec ±0.67% | 442,284 ops/sec ±1.85% | 303,185 ops/sec ±1.27% | 243,190 ops/sec ±0.22% | jsdom is the fastest and 4.8 times faster than patched-jsdom. |
complex selector:
matches('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') | 155,130 ops/sec ±0.19% | N/A | 72,721 ops/sec ±0.23% | 96,817 ops/sec ±1.52% | jsdom is the fastest and 1.6 times faster than patched-jsdom. |
closest()
Selector | jsdom v24.0.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|
simple selector:
closest('.container') | 562,510 ops/sec ±1.59% | 355,759 ops/sec ±1.74% | 390,352 ops/sec ±2.16% | 194,428 ops/sec ±0.78% | jsdom is the fastest and 2.9 times faster than patched-jsdom. |
compound selector:
closest('div.container[id]:not(.box)') | 200,674 ops/sec ±1.52% | 77,134 ops/sec ±1.84% | 163,407 ops/sec ±1.69% | 117,207 ops/sec ±1.12% | jsdom is the fastest and 1.7 times faster than patched-jsdom. |
complex selector:
closest('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') | 151,390 ops/sec ±0.60% | N/A | 72,140 ops/sec ±1.33% | 96,834 ops/sec ±0.95% | jsdom is the fastest and 1.6 times faster than patched-jsdom. |
querySelector()
Selector | jsdom v24.0.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|
simple selector:
querySelector('.content') | 3,020 ops/sec ±0.84% | 374,668 ops/sec ±1.32% | 326,366 ops/sec ±1.67% | 74,776 ops/sec ±0.85% | happydom is the fastest and 5.0 times faster than patched-jsdom. patched-jsdom is 24.8 times faster than jsdom. |
compound selector:
querySelector('p.content[id]:only-child') | 1,183 ops/sec ±1.24% | 375,791 ops/sec ±1.66% | 253,707 ops/sec ±1.68% | 76,218 ops/sec ±0.84% | happydom is the fastest and 4.9 times faster than patched-jsdom. patched-jsdom is 64.4 times faster than jsdom. |
complex selector:
querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') | 227 ops/sec ±1.27% | N/A | 1,338 ops/sec ±0.35% | 766 ops/sec ±2.66% | linkedom is the fastest and 1.7 times faster than patched-jsdom. patched-jsdom is 3.4 times faster than jsdom. |
querySelectorAll()
Selector | jsdom v24.0.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|
simple selector:
querySelectorAll('.content') | 2,805 ops/sec ±0.63% | 768 ops/sec ±1.68% | 1,189 ops/sec ±0.22% | 3,565 ops/sec ±0.24% | patched-jsdom is the fastest. patched-jsdom is 1.3 times faster than jsdom. |
compound selector:
querySelectorAll('p.content[id]:only-child') | 1,151 ops/sec ±0.37% | 917 ops/sec ±2.18% | 1,067 ops/sec ±0.27% | 1,262 ops/sec ±0.25% | patched-jsdom is the fastest. patched-jsdom is 1.1 times faster than jsdom. |
complex selector:
querySelectorAll('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') | 225 ops/sec ±0.46% | N/A | 433 ops/sec ±0.23% | 760 ops/sec ±0.53% | patched-jsdom is the fastest. patched-jsdom is 3.4 times faster than jsdom. |
Acknowledgments
The following resources have been of great help in the development of the DOM Selector.
Copyright (c) 2023 asamuzaK (Kazz)