markitect-main/node_modules/@asamuzakjp/dom-selector/README.md

10 KiB
Raw Blame History

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 - equivalent to 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 - equivalent to 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 - equivalent to Document.querySelector(), DocumentFragment.querySelector() and 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 - equivalent to Document.querySelectorAll(), DocumentFragment.querySelectorAll() and 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

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);
    };
  }
});

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.

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 for the latest results.

Acknowledgments

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


Copyright (c) 2023 asamuzaK (Kazz)