@asamuzakjp/dom-selector
v4.4.5
Published
A CSS selector engine.
Downloads
1,306,331
Readme
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
Returns boolean true
if matched, false
otherwise
closest(selector, node, opt)
closest - same functionality as Element.closest()
Parameters
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
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
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|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:openE:closed|Partially supported|Matching with <select>, e.g. select:open
, is not supported.|
|E:enabledE:disabled|✓| |
|E:read‑writeE:read‑only|✓| |
|E:placeholder‑shown|✓| |
|E:default|✓| |
|E:checked|✓| |
|E:indeterminate|✓| |
|E:validE:invalid|✓| |
|E:requiredE:optional|✓| |
|E:blank|Unsupported| |
|E:user‑validE: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)|✓| |
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 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);
};
}
});
Performance
See benchmark for the latest results.
F
: Failed because the selector is not supported or the result is incorrect.
matches()
|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.|
closest()
|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.|
querySelector()
|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.|
querySelectorAll()
|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.|
Acknowledgments
The following resources have been of great help in the development of the DOM Selector.
Copyright (c) 2023 asamuzaK (Kazz)