Skip to content

asamuzaK/domSelector

Repository files navigation

DOM Selector

build CodeQL npm (scoped)

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 selector
  • node object Element node
  • opt object? options
    • opt.noexcept boolean? no exception
    • opt.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 selector
  • node object Element node
  • opt object? options
    • opt.noexcept boolean? no exception
    • opt.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 selector
  • node object Document, DocumentFragment or Element node
  • opt object? options
    • opt.noexcept boolean? no exception
    • opt.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 selector
  • node object Document, DocumentFragment or Element node
  • opt object? options
    • opt.noexcept boolean? no exception
    • opt.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:open
E:closed
Partially supported Matching on <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‑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: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

matches()

Selector jsdom v24.0.0 (nwsapi) happy-dom linkeDom patched-jsdom (dom-selector) Result
simple selector:
matches('.content')
1,000,095 ops/sec ±0.29% 6,105 ops/sec ±0.45% 8,636 ops/sec ±0.36% 949,741 ops/sec ±0.21% jsdom is the fastest and 1.1 times faster than patched-jsdom.
compound selector:
matches('p.content[id]:is(:last-child, :only-child)')
592,559 ops/sec ±1.72% 5,932 ops/sec ±0.48% 8,724 ops/sec ±0.67% 517,958 ops/sec ±0.28% jsdom is the fastest and 1.1 times faster than patched-jsdom.
compound selector:
matches('p.content[id]:is(:invalid-nth-child, :only-child)')
N/A 5,893 ops/sec ±0.35% N/A 157,436 ops/sec ±0.22% patched-jsdom is the fastest.
compound selector:
matches('p.content[id]:not(:is(.foo, .bar))')
481,007 ops/sec ±0.24% N/A 8,232 ops/sec ±0.26% 405,841 ops/sec ±1.50% 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')
162,394 ops/sec ±1.46% N/A 7,686 ops/sec ±0.76% 143,901 ops/sec ±1.34% 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)')
N/A N/A 7,645 ops/sec ±0.91% 46,398 ops/sec ±1.03% patched-jsdom is the fastest.

closest()

Selector jsdom v24.0.0 (nwsapi) happy-dom linkeDom patched-jsdom (dom-selector) Result
simple selector:
closest('.container')
384,943 ops/sec ±0.17% 6,050 ops/sec ±0.26% 8,544 ops/sec ±0.49% 373,062 ops/sec ±2.06% jsdom is the fastest and 1.0 times faster than patched-jsdom.
compound selector:
closest('div.container[id]:not(.foo, .box)')
136,836 ops/sec ±1.49% N/A 8,201 ops/sec ±0.65% 133,472 ops/sec ±1.39% jsdom is the fastest and 1.0 times faster than patched-jsdom.
complex selector:
closest('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content')
151,300 ops/sec ±1.42% N/A 7,769 ops/sec ±0.48% 136,348 ops/sec ±1.36% 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)')
N/A N/A 7,648 ops/sec ±0.60% 32,514 ops/sec ±0.85% patched-jsdom is the fastest.

querySelector()

Selector jsdom v24.0.0 (nwsapi) happy-dom linkeDom patched-jsdom (dom-selector) Result
simple selector:
querySelector('.content')
36,453 ops/sec ±1.64% 7,377 ops/sec ±0.82% 9,762 ops/sec ±0.43% 30,278 ops/sec ±1.31% jsdom is the fastest and 1.2 times faster than patched-jsdom.
compound selector:
querySelector('p.content[id]:is(:last-child, :only-child)')
10,518 ops/sec ±1.26% 7,286 ops/sec ±0.67% 9,400 ops/sec ±0.54% 9,657 ops/sec ±1.35% jsdom is the fastest and 1.1 times faster than patched-jsdom.
complex selector:
querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content')
230 ops/sec ±1.38% N/A 1,309 ops/sec ±1.39% 732 ops/sec ±2.31% linkedom is the fastest and 1.8 times faster than patched-jsdom. patched-jsdom is 3.2 times faster than jsdom.
complex selector:
querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner:has(> .content)')
N/A N/A 1,626 ops/sec ±0.28% 493 ops/sec ±2.35% linkedom is the fastest and 3.3 times faster than patched-jsdom.

querySelectorAll()

Selector jsdom v24.0.0 (nwsapi) happy-dom linkeDom patched-jsdom (dom-selector) Result
simple selector:
querySelectorAll('.content')
3,194 ops/sec ±1.32% 794 ops/sec ±1.69% 1,213 ops/sec ±0.20% 3,526 ops/sec ±0.24% patched-jsdom is the fastest. patched-jsdom is 1.1 times faster than jsdom.
compound selector:
querySelectorAll('p.content[id]:is(:last-child, :only-child)')
1,056 ops/sec ±0.21% 946 ops/sec ±1.32% 1,166 ops/sec ±1.19% 1,063 ops/sec ±1.09% linkedom is the fastest and 1.1 times faster than patched-jsdom. patched-jsdom is 1.0 times faster than jsdom.
complex selector:
querySelectorAll('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content')
233 ops/sec ±1.24% N/A 428 ops/sec ±1.25% 819 ops/sec ±1.43% patched-jsdom is the fastest. patched-jsdom is 3.5 times faster than jsdom.
complex selector:
querySelectorAll('.box:first-child ~ .box:nth-of-type(4n+1) + .box .block.inner:has(> .content)')
N/A N/A 460 ops/sec ±0.20% 534 ops/sec ±1.83% patched-jsdom is the fastest.

Acknowledgments

The following resources have been of great help in the development of the DOM Selector.


Copyright (c) 2023 asamuzaK (Kazz)