A CSS selector engine.
npm i @asamuzakjp/dom-selector
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const { window } = new JSDOM();
const {
closest, matches, querySelector, querySelectorAll
} = new DOMSelector(window);
matches - same functionality as Element.matches()
Returns boolean true
if matched, false
otherwise
closest - same functionality as Element.closest()
Returns object? matched node
querySelector - same functionality as Document.querySelector(), DocumentFragment.querySelector(), Element.querySelector()
-
selector
string CSS selector -
node
object Document, DocumentFragment or Element node -
opt
object? options
Returns object? matched node
querySelectorAll - same functionality as Document.querySelectorAll(), DocumentFragment.querySelectorAll(), Element.querySelectorAll()
NOTE: returns Array, not NodeList
-
selector
string CSS selector -
node
object Document, DocumentFragment or Element node -
opt
object? options
Returns Array<(object | undefined)> array of matched nodes
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 | Partially supported | Matching with MathML is not yet supported. |
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 | ✓ | Enabled if a mousedown / pointerdown event is passed as an option. |
E:hover | ✓ | Enabled if a mouseover / pointerover event is passed as an option. |
E:focus | ✓ | |
E:focus‑within | ✓ | |
E:focus‑visible | ✓ | Enabled if a keydown event is passed as an option. |
E:open E:closed |
Partially supported | Matching with <select>, e.g. select:open , is not supported. |
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‑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 F | ✓ | |
E > F | ✓ | |
E + F | ✓ | |
E ~ F | ✓ | |
F || E | Unsupported | |
E:nth‑col(n) | Unsupported | |
E:nth‑last‑col(n) | Unsupported | |
E:popover-open | ✓ | |
E:host | ✓ | |
E:host(s) | ✓ | |
E:host‑context(s) | ✓ |
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);
};
}
});
See benchmark for the latest results.
F
: Failed because the selector is not supported or the result is incorrect.
Selector | jsdom v24.1.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|---|---|---|---|---|
simple selector:matches('.content')
|
989,361 ops/sec ±0.27% | 7,447 ops/sec ±1.10% | 9,037 ops/sec ±1.74% | 947,094 ops/sec ±0.24% | jsdom is the fastest and 1.0 times faster than patched-jsdom. |
compound selector:matches('p.content[id]:is(:last-child, :only-child)')
|
581,187 ops/sec ±0.21% | 7,137 ops/sec ±0.82% | 8,808 ops/sec ±1.24% | 492,756 ops/sec ±0.25% | jsdom is the fastest and 1.2 times faster than patched-jsdom. |
compound selector:matches('p.content[id]:is(:invalid-nth-child, :only-child)')
|
F | 7,144 ops/sec ±0.51% | F | 145,203 ops/sec ±0.94% | patched-jsdom is the fastest. |
compound selector:matches('p.content[id]:not(:is(.foo, .bar))')
|
472,261 ops/sec ±1.64% | 7,199 ops/sec ±0.58% | 8,612 ops/sec ±0.51% | 403,920 ops/sec ±0.29% | jsdom is the fastest and 1.2 times faster than patched-jsdom. |
complex selector:matches('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content')
|
152,011 ops/sec ±0.66% | F | 5,737 ops/sec ±0.43% | 135,540 ops/sec ±0.41% | jsdom is the fastest and 1.1 times faster than patched-jsdom. |
complex selector:matches('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner:has(> .content)')
|
F | F | 5,767 ops/sec ±0.47% | 39,996 ops/sec ±0.79% | patched-jsdom is the fastest. |
complex selector within logical pseudo-class:matches(':is(.box > .content, .block > .content)')
|
416,215 ops/sec ±0.49% | F | 6,105 ops/sec ±0.29% | 358,945 ops/sec ±0.29% | jsdom is the fastest and 1.2 times faster than patched-jsdom. |
Selector | jsdom v24.1.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|---|---|---|---|---|
simple selector:closest('.container')
|
367,877 ops/sec ±1.14% | 7,218 ops/sec ±0.76% | 9,181 ops/sec ±0.64% | 356,762 ops/sec ±1.02% | jsdom is the fastest and 1.0 times faster than patched-jsdom. |
compound selector:closest('div.container[id]:not(.foo, .box)')
|
134,650 ops/sec ±0.32% | F | 8,549 ops/sec ±0.62% | 125,888 ops/sec ±3.16% | jsdom is the fastest and 1.1 times faster than patched-jsdom. |
complex selector:closest('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content')
|
134,654 ops/sec ±2.02% | F | 5,751 ops/sec ±1.01% | 119,709 ops/sec ±2.00% | jsdom is the fastest and 1.1 times faster than patched-jsdom. |
complex selector:closest('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner:has(> .content)')
|
F | F | 5,596 ops/sec ±0.81% | 28,732 ops/sec ±1.13% | patched-jsdom is the fastest. |
complex selector within logical pseudo-class:closest(':is(.container > .content, .container > .box)')
|
192,885 ops/sec ±1.15% | 4,685 ops/sec ±1.30% | 5,927 ops/sec ±0.64% | 181,927 ops/sec ±1.39% | jsdom is the fastest and 1.1 times faster than patched-jsdom. |
Selector | jsdom v24.1.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|---|---|---|---|---|
simple selector:querySelector('.content')
|
27,400 ops/sec ±0.96% | 9,042 ops/sec ±1.19% | 10,550 ops/sec ±0.69% | 25,885 ops/sec ±1.48% | jsdom is the fastest and 1.1 times faster than patched-jsdom. |
compound selector:querySelector('p.content[id]:is(:last-child, :only-child)')
|
8,685 ops/sec ±1.47% | 8,723 ops/sec ±1.20% | 9,836 ops/sec ±0.69% | 8,418 ops/sec ±1.41% | linkedom is the fastest and 1.2 times faster than patched-jsdom. jsdom is 1.0 times faster than patched-jsdom. |
complex selector:querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content')
|
206 ops/sec ±1.48% | F | 1,289 ops/sec ±0.42% | 709 ops/sec ±1.75% | linkedom is the fastest and 1.8 times faster than patched-jsdom. patched-jsdom is 3.4 times faster than jsdom. |
complex selector:querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner:has(> .content)')
|
F | F | 1,598 ops/sec ±0.35% | 476 ops/sec ±1.94% | linkedom is the fastest and 3.4 times faster than patched-jsdom. |
complex selector within logical pseudo-class:querySelector(':is(.box > .content, .block > .content)')
|
2,921 ops/sec ±1.78% | F | 9,782 ops/sec ±0.52% | 97,513 ops/sec ±1.46% | patched-jsdom is the fastest. patched-jsdom is 33.4 times faster than jsdom. |
Selector | jsdom v24.1.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|---|---|---|---|---|
simple selector:querySelectorAll('.content')
|
2,549 ops/sec ±0.55% | 729 ops/sec ±0.48% | 1,188 ops/sec ±1.57% | 3,153 ops/sec ±0.95% | patched-jsdom is the fastest. patched-jsdom is 1.2 times faster than jsdom. |
compound selector:querySelectorAll('p.content[id]:is(:last-child, :only-child)')
|
880 ops/sec ±1.32% | 706 ops/sec ±1.55% | 1,156 ops/sec ±0.93% | 938 ops/sec ±1.27% | linkedom is the fastest and 1.2 times faster than patched-jsdom. 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')
|
196 ops/sec ±0.73% | F | 414 ops/sec ±0.24% | 797 ops/sec ±1.66% | patched-jsdom is the fastest. patched-jsdom is 4.1 times faster than jsdom. |
complex selector:querySelectorAll('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner:has(> .content)')
|
F | F | 451 ops/sec ±0.36% | 504 ops/sec ±1.88% | patched-jsdom is the fastest. |
complex selector within logical pseudo-class:querySelectorAll(':is(.box > .content, .block > .content)')
|
273 ops/sec ±0.89% | F | 498 ops/sec ±1.38% | 236 ops/sec ±2.36% | linkedom is the fastest and 2.1 times faster than patched-jsdom. jsdom is 1.2 times faster than patched-jsdom. |
The following resources have been of great help in the development of the DOM Selector.
Copyright (c) 2023 asamuzaK (Kazz)