aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/@asamuzakjp/dom-selector/src/js
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
commit76cb9c2a39d477a64824a985ade40507e3bbade1 (patch)
tree41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/node_modules/@asamuzakjp/dom-selector/src/js
parent819a39a21ac992b1393244a4c283bbb125208c69 (diff)
downloadneko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.gz
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.bz2
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.zip
feat(vanilla): add testing infrastructure and tests (NK-wjnczv)
Diffstat (limited to 'vanilla/node_modules/@asamuzakjp/dom-selector/src/js')
-rw-r--r--vanilla/node_modules/@asamuzakjp/dom-selector/src/js/constant.js129
-rw-r--r--vanilla/node_modules/@asamuzakjp/dom-selector/src/js/finder.js3122
-rw-r--r--vanilla/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js587
-rw-r--r--vanilla/node_modules/@asamuzakjp/dom-selector/src/js/parser.js431
-rw-r--r--vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js1107
5 files changed, 5376 insertions, 0 deletions
diff --git a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/constant.js b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/constant.js
new file mode 100644
index 0000000..02986ec
--- /dev/null
+++ b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/constant.js
@@ -0,0 +1,129 @@
+/**
+ * constant.js
+ */
+
+/* string */
+export const ATRULE = 'Atrule';
+export const ATTR_SELECTOR = 'AttributeSelector';
+export const CLASS_SELECTOR = 'ClassSelector';
+export const COMBINATOR = 'Combinator';
+export const IDENT = 'Identifier';
+export const ID_SELECTOR = 'IdSelector';
+export const NOT_SUPPORTED_ERR = 'NotSupportedError';
+export const NTH = 'Nth';
+export const OPERATOR = 'Operator';
+export const PS_CLASS_SELECTOR = 'PseudoClassSelector';
+export const PS_ELEMENT_SELECTOR = 'PseudoElementSelector';
+export const RULE = 'Rule';
+export const SCOPE = 'Scope';
+export const SELECTOR = 'Selector';
+export const SELECTOR_LIST = 'SelectorList';
+export const STRING = 'String';
+export const SYNTAX_ERR = 'SyntaxError';
+export const TARGET_ALL = 'all';
+export const TARGET_FIRST = 'first';
+export const TARGET_LINEAL = 'lineal';
+export const TARGET_SELF = 'self';
+export const TYPE_SELECTOR = 'TypeSelector';
+
+/* numeric */
+export const BIT_01 = 1;
+export const BIT_02 = 2;
+export const BIT_04 = 4;
+export const BIT_08 = 8;
+export const BIT_16 = 0x10;
+export const BIT_32 = 0x20;
+export const BIT_FFFF = 0xffff;
+export const DUO = 2;
+export const HEX = 16;
+export const TYPE_FROM = 8;
+export const TYPE_TO = -1;
+
+/* Node */
+export const ELEMENT_NODE = 1;
+export const TEXT_NODE = 3;
+export const DOCUMENT_NODE = 9;
+export const DOCUMENT_FRAGMENT_NODE = 11;
+export const DOCUMENT_POSITION_PRECEDING = 2;
+export const DOCUMENT_POSITION_CONTAINS = 8;
+export const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
+
+/* NodeFilter */
+export const SHOW_ALL = 0xffffffff;
+export const SHOW_CONTAINER = 0x501;
+export const SHOW_DOCUMENT = 0x100;
+export const SHOW_DOCUMENT_FRAGMENT = 0x400;
+export const SHOW_ELEMENT = 1;
+
+/* selectors */
+export const ALPHA_NUM = '[A-Z\\d]+';
+export const CHILD_IDX = '(?:first|last|only)-(?:child|of-type)';
+export const DIGIT = '(?:0|[1-9]\\d*)';
+export const LANG_PART = `(?:-${ALPHA_NUM})*`;
+export const PSEUDO_CLASS = `(?:any-)?link|${CHILD_IDX}|checked|empty|indeterminate|read-(?:only|write)|target`;
+export const ANB = `[+-]?(?:${DIGIT}n?|n)|(?:[+-]?${DIGIT})?n\\s*[+-]\\s*${DIGIT}`;
+// combinators
+export const COMBO = '\\s?[\\s>~+]\\s?';
+export const DESCEND = '\\s?[\\s>]\\s?';
+export const SIBLING = '\\s?[+~]\\s?';
+// LOGIC_IS: :is()
+export const LOGIC_IS = `:is\\(\\s*[^)]+\\s*\\)`;
+// N_TH: excludes An+B with selector list, e.g. :nth-child(2n+1 of .foo)
+export const N_TH = `nth-(?:last-)?(?:child|of-type)\\(\\s*(?:even|odd|${ANB})\\s*\\)`;
+// SUB_TYPE: attr, id, class, pseudo-class, note that [foo|=bar] is excluded
+export const SUB_TYPE = '\\[[^|\\]]+\\]|[#.:][\\w-]+';
+export const SUB_TYPE_WO_PSEUDO = '\\[[^|\\]]+\\]|[#.][\\w-]+';
+// TAG_TYPE: *, tag
+export const TAG_TYPE = '\\*|[A-Za-z][\\w-]*';
+export const TAG_TYPE_I = '\\*|[A-Z][\\w-]*';
+export const COMPOUND = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE})+)`;
+export const COMPOUND_L = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE}|${LOGIC_IS})+)`;
+export const COMPOUND_I = `(?:${TAG_TYPE_I}|(?:${TAG_TYPE_I})?(?:${SUB_TYPE})+)`;
+export const COMPOUND_WO_PSEUDO = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE_WO_PSEUDO})+)`;
+export const COMPLEX = `${COMPOUND}(?:${COMBO}${COMPOUND})*`;
+export const COMPLEX_L = `${COMPOUND_L}(?:${COMBO}${COMPOUND_L})*`;
+export const HAS_COMPOUND = `has\\([\\s>]?\\s*${COMPOUND_WO_PSEUDO}\\s*\\)`;
+export const LOGIC_COMPOUND = `(?:is|not)\\(\\s*${COMPOUND_L}(?:\\s*,\\s*${COMPOUND_L})*\\s*\\)`;
+export const LOGIC_COMPLEX = `(?:is|not)\\(\\s*${COMPLEX_L}(?:\\s*,\\s*${COMPLEX_L})*\\s*\\)`;
+
+/* forms and input types */
+export const FORM_PARTS = Object.freeze([
+ 'button',
+ 'input',
+ 'select',
+ 'textarea'
+]);
+export const INPUT_BUTTON = Object.freeze(['button', 'reset', 'submit']);
+export const INPUT_CHECK = Object.freeze(['checkbox', 'radio']);
+export const INPUT_DATE = Object.freeze([
+ 'date',
+ 'datetime-local',
+ 'month',
+ 'time',
+ 'week'
+]);
+export const INPUT_TEXT = Object.freeze([
+ 'email',
+ 'password',
+ 'search',
+ 'tel',
+ 'text',
+ 'url'
+]);
+export const INPUT_EDIT = Object.freeze([
+ ...INPUT_DATE,
+ ...INPUT_TEXT,
+ 'number'
+]);
+export const INPUT_LTR = Object.freeze([
+ ...INPUT_CHECK,
+ 'color',
+ 'date',
+ 'image',
+ 'number',
+ 'range',
+ 'time'
+]);
+
+/* logical combination pseudo-classes */
+export const KEYS_LOGICAL = new Set(['has', 'is', 'not', 'where']);
diff --git a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/finder.js b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/finder.js
new file mode 100644
index 0000000..05538d7
--- /dev/null
+++ b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/finder.js
@@ -0,0 +1,3122 @@
+/**
+ * finder.js
+ */
+
+/* import */
+import {
+ matchAttributeSelector,
+ matchDirectionPseudoClass,
+ matchDisabledPseudoClass,
+ matchLanguagePseudoClass,
+ matchPseudoElementSelector,
+ matchReadOnlyPseudoClass,
+ matchTypeSelector
+} from './matcher.js';
+import {
+ findAST,
+ generateCSS,
+ parseSelector,
+ sortAST,
+ unescapeSelector,
+ walkAST
+} from './parser.js';
+import {
+ filterNodesByAnB,
+ findLogicalWithNestedHas,
+ generateException,
+ isCustomElement,
+ isFocusVisible,
+ isFocusableArea,
+ isVisible,
+ resolveContent,
+ sortNodes,
+ traverseNode
+} from './utility.js';
+
+/* constants */
+import {
+ ATTR_SELECTOR,
+ CLASS_SELECTOR,
+ COMBINATOR,
+ DOCUMENT_FRAGMENT_NODE,
+ ELEMENT_NODE,
+ FORM_PARTS,
+ ID_SELECTOR,
+ INPUT_CHECK,
+ INPUT_DATE,
+ INPUT_EDIT,
+ INPUT_TEXT,
+ KEYS_LOGICAL,
+ NOT_SUPPORTED_ERR,
+ PS_CLASS_SELECTOR,
+ PS_ELEMENT_SELECTOR,
+ SHOW_ALL,
+ SHOW_CONTAINER,
+ SYNTAX_ERR,
+ TARGET_ALL,
+ TARGET_FIRST,
+ TARGET_LINEAL,
+ TARGET_SELF,
+ TEXT_NODE,
+ TYPE_SELECTOR
+} from './constant.js';
+const DIR_NEXT = 'next';
+const DIR_PREV = 'prev';
+const KEYS_FORM = new Set([...FORM_PARTS, 'fieldset', 'form']);
+const KEYS_FORM_PS_VALID = new Set([...FORM_PARTS, 'form']);
+const KEYS_INPUT_CHECK = new Set(INPUT_CHECK);
+const KEYS_INPUT_PLACEHOLDER = new Set([...INPUT_TEXT, 'number']);
+const KEYS_INPUT_RANGE = new Set([...INPUT_DATE, 'number', 'range']);
+const KEYS_INPUT_REQUIRED = new Set([...INPUT_CHECK, ...INPUT_EDIT, 'file']);
+const KEYS_INPUT_RESET = new Set(['button', 'reset']);
+const KEYS_INPUT_SUBMIT = new Set(['image', 'submit']);
+const KEYS_MODIFIER = new Set([
+ 'Alt',
+ 'AltGraph',
+ 'CapsLock',
+ 'Control',
+ 'Fn',
+ 'FnLock',
+ 'Hyper',
+ 'Meta',
+ 'NumLock',
+ 'ScrollLock',
+ 'Shift',
+ 'Super',
+ 'Symbol',
+ 'SymbolLock'
+]);
+const KEYS_PS_UNCACHE = new Set([
+ 'any-link',
+ 'defined',
+ 'dir',
+ 'link',
+ 'scope'
+]);
+const KEYS_PS_NTH_OF_TYPE = new Set([
+ 'first-of-type',
+ 'last-of-type',
+ 'only-of-type'
+]);
+
+/**
+ * Finder
+ * NOTE: #ast[i] corresponds to #nodes[i]
+ */
+export class Finder {
+ /* private fields */
+ #ast;
+ #astCache;
+ #check;
+ #descendant;
+ #document;
+ #documentCache;
+ #documentURL;
+ #event;
+ #eventHandlers;
+ #focus;
+ #invalidate;
+ #invalidateResults;
+ #lastFocusVisible;
+ #node;
+ #nodeWalker;
+ #nodes;
+ #noexcept;
+ #pseudoElement;
+ #results;
+ #root;
+ #rootWalker;
+ #scoped;
+ #selector;
+ #shadow;
+ #verifyShadowHost;
+ #walkers;
+ #warn;
+ #window;
+
+ /**
+ * constructor
+ * @param {object} window - The window object.
+ */
+ constructor(window) {
+ this.#window = window;
+ this.#astCache = new WeakMap();
+ this.#documentCache = new WeakMap();
+ this.#event = null;
+ this.#focus = null;
+ this.#lastFocusVisible = null;
+ this.#eventHandlers = new Set([
+ {
+ keys: ['focus', 'focusin'],
+ handler: this._handleFocusEvent
+ },
+ {
+ keys: ['keydown', 'keyup'],
+ handler: this._handleKeyboardEvent
+ },
+ {
+ keys: ['mouseover', 'mousedown', 'mouseup', 'click', 'mouseout'],
+ handler: this._handleMouseEvent
+ }
+ ]);
+ this._registerEventListeners();
+ this.clearResults(true);
+ }
+
+ /**
+ * Handles errors.
+ * @param {Error} e - The error object.
+ * @param {object} [opt] - Options.
+ * @param {boolean} [opt.noexcept] - If true, exceptions are not thrown.
+ * @throws {Error} Throws an error.
+ * @returns {void}
+ */
+ onError = (e, opt = {}) => {
+ const noexcept = opt.noexcept ?? this.#noexcept;
+ if (noexcept) {
+ return;
+ }
+ const isDOMException =
+ e instanceof DOMException || e instanceof this.#window.DOMException;
+ if (isDOMException) {
+ if (e.name === NOT_SUPPORTED_ERR) {
+ if (this.#warn) {
+ console.warn(e.message);
+ }
+ return;
+ }
+ throw new this.#window.DOMException(e.message, e.name);
+ }
+ if (e.name in this.#window) {
+ throw new this.#window[e.name](e.message, { cause: e });
+ }
+ throw e;
+ };
+
+ /**
+ * Sets up the finder.
+ * @param {string} selector - The CSS selector.
+ * @param {object} node - Document, DocumentFragment, or Element.
+ * @param {object} [opt] - Options.
+ * @param {boolean} [opt.check] - Indicates if running in internal check().
+ * @param {boolean} [opt.noexcept] - If true, exceptions are not thrown.
+ * @param {boolean} [opt.warn] - If true, console warnings are enabled.
+ * @returns {object} The finder instance.
+ */
+ setup = (selector, node, opt = {}) => {
+ const { check, noexcept, warn } = opt;
+ this.#check = !!check;
+ this.#noexcept = !!noexcept;
+ this.#warn = !!warn;
+ [this.#document, this.#root, this.#shadow] = resolveContent(node);
+ this.#documentURL = null;
+ this.#node = node;
+ this.#scoped =
+ this.#node !== this.#root && this.#node.nodeType === ELEMENT_NODE;
+ this.#selector = selector;
+ [this.#ast, this.#nodes] = this._correspond(selector);
+ this.#pseudoElement = [];
+ this.#walkers = new WeakMap();
+ this.#nodeWalker = null;
+ this.#rootWalker = null;
+ this.#verifyShadowHost = null;
+ this.clearResults();
+ return this;
+ };
+
+ /**
+ * Clear cached results.
+ * @param {boolean} all - clear all results
+ * @returns {void}
+ */
+ clearResults = (all = false) => {
+ this.#invalidateResults = new WeakMap();
+ if (all) {
+ this.#results = new WeakMap();
+ }
+ };
+
+ /**
+ * Handles focus events.
+ * @private
+ * @param {Event} evt - The event object.
+ * @returns {void}
+ */
+ _handleFocusEvent = evt => {
+ this.#focus = evt;
+ };
+
+ /**
+ * Handles keyboard events.
+ * @private
+ * @param {Event} evt - The event object.
+ * @returns {void}
+ */
+ _handleKeyboardEvent = evt => {
+ const { key } = evt;
+ if (!KEYS_MODIFIER.has(key)) {
+ this.#event = evt;
+ }
+ };
+
+ /**
+ * Handles mouse events.
+ * @private
+ * @param {Event} evt - The event object.
+ * @returns {void}
+ */
+ _handleMouseEvent = evt => {
+ this.#event = evt;
+ };
+
+ /**
+ * Registers event listeners.
+ * @private
+ * @returns {Array.<void>} An array of return values from addEventListener.
+ */
+ _registerEventListeners = () => {
+ const opt = {
+ capture: true,
+ passive: true
+ };
+ const func = [];
+ for (const eventHandler of this.#eventHandlers) {
+ const { keys, handler } = eventHandler;
+ const l = keys.length;
+ for (let i = 0; i < l; i++) {
+ const key = keys[i];
+ func.push(this.#window.addEventListener(key, handler, opt));
+ }
+ }
+ return func;
+ };
+
+ /**
+ * Processes selector branches into the internal AST structure.
+ * @private
+ * @param {Array.<Array.<object>>} branches - The branches from walkAST.
+ * @param {string} selector - The original selector for error reporting.
+ * @returns {{ast: Array, descendant: boolean}}
+ * An object with the AST, descendant flag.
+ */
+ _processSelectorBranches = (branches, selector) => {
+ let descendant = false;
+ const ast = [];
+ const l = branches.length;
+ for (let i = 0; i < l; i++) {
+ const items = [...branches[i]];
+ const branch = [];
+ let item = items.shift();
+ if (item && item.type !== COMBINATOR) {
+ const leaves = new Set();
+ while (item) {
+ if (item.type === COMBINATOR) {
+ const [nextItem] = items;
+ if (!nextItem || nextItem.type === COMBINATOR) {
+ const msg = `Invalid selector ${selector}`;
+ this.onError(generateException(msg, SYNTAX_ERR, this.#window));
+ // Stop processing on invalid selector.
+ return { ast: [], descendant: false, invalidate: false };
+ }
+ if (item.name === ' ' || item.name === '>') {
+ descendant = true;
+ }
+ branch.push({ combo: item, leaves: sortAST(leaves) });
+ leaves.clear();
+ } else {
+ if (item.name && typeof item.name === 'string') {
+ const unescapedName = unescapeSelector(item.name);
+ if (unescapedName !== item.name) {
+ item.name = unescapedName;
+ }
+ if (/[|:]/.test(unescapedName)) {
+ item.namespace = true;
+ }
+ }
+ leaves.add(item);
+ }
+ if (items.length) {
+ item = items.shift();
+ } else {
+ branch.push({ combo: null, leaves: sortAST(leaves) });
+ leaves.clear();
+ break;
+ }
+ }
+ }
+ ast.push({ branch, dir: null, filtered: false, find: false });
+ }
+ return { ast, descendant };
+ };
+
+ /**
+ * Corresponds AST and nodes.
+ * @private
+ * @param {string} selector - The CSS selector.
+ * @returns {Array.<Array.<object>>} An array with the AST and nodes.
+ */
+ _correspond = selector => {
+ const nodes = [];
+ this.#descendant = false;
+ this.#invalidate = false;
+ let ast;
+ if (this.#documentCache.has(this.#document)) {
+ const cachedItem = this.#documentCache.get(this.#document);
+ if (cachedItem && cachedItem.has(`${selector}`)) {
+ const item = cachedItem.get(`${selector}`);
+ ast = item.ast;
+ this.#descendant = item.descendant;
+ this.#invalidate = item.invalidate;
+ }
+ }
+ if (ast) {
+ const l = ast.length;
+ for (let i = 0; i < l; i++) {
+ ast[i].dir = null;
+ ast[i].filtered = false;
+ ast[i].find = false;
+ nodes[i] = [];
+ }
+ } else {
+ let cssAst;
+ try {
+ cssAst = parseSelector(selector);
+ } catch (e) {
+ return this.onError(e);
+ }
+ const { branches, info } = walkAST(cssAst);
+ const {
+ hasHasPseudoFunc,
+ hasLogicalPseudoFunc,
+ hasNthChildOfSelector,
+ hasStatePseudoClass
+ } = info;
+ this.#invalidate =
+ hasHasPseudoFunc ||
+ hasStatePseudoClass ||
+ !!(hasLogicalPseudoFunc && hasNthChildOfSelector);
+ const processed = this._processSelectorBranches(branches, selector);
+ ast = processed.ast;
+ this.#descendant = processed.descendant;
+ let cachedItem;
+ if (this.#documentCache.has(this.#document)) {
+ cachedItem = this.#documentCache.get(this.#document);
+ } else {
+ cachedItem = new Map();
+ }
+ cachedItem.set(`${selector}`, {
+ ast,
+ descendant: this.#descendant,
+ invalidate: this.#invalidate
+ });
+ this.#documentCache.set(this.#document, cachedItem);
+ // Initialize nodes array for each branch.
+ for (let i = 0; i < ast.length; i++) {
+ nodes[i] = [];
+ }
+ }
+ return [ast, nodes];
+ };
+
+ /**
+ * Creates a TreeWalker.
+ * @private
+ * @param {object} node - The Document, DocumentFragment, or Element node.
+ * @param {object} [opt] - Options.
+ * @param {boolean} [opt.force] - Force creation of a new TreeWalker.
+ * @param {number} [opt.whatToShow] - The NodeFilter whatToShow value.
+ * @returns {object} The TreeWalker object.
+ */
+ _createTreeWalker = (node, opt = {}) => {
+ const { force = false, whatToShow = SHOW_CONTAINER } = opt;
+ if (force) {
+ return this.#document.createTreeWalker(node, whatToShow);
+ } else if (this.#walkers.has(node)) {
+ return this.#walkers.get(node);
+ }
+ const walker = this.#document.createTreeWalker(node, whatToShow);
+ this.#walkers.set(node, walker);
+ return walker;
+ };
+
+ /**
+ * Gets selector branches from cache or parses them.
+ * @private
+ * @param {object} selector - The AST.
+ * @returns {Array.<Array.<object>>} The selector branches.
+ */
+ _getSelectorBranches = selector => {
+ if (this.#astCache.has(selector)) {
+ return this.#astCache.get(selector);
+ }
+ const { branches } = walkAST(selector);
+ this.#astCache.set(selector, branches);
+ return branches;
+ };
+
+ /**
+ * Gets the children of a node, optionally filtered by a selector.
+ * @private
+ * @param {object} parentNode - The parent element.
+ * @param {Array.<Array.<object>>} selectorBranches - The selector branches.
+ * @param {object} [opt] - Options.
+ * @returns {Array.<object>} An array of child nodes.
+ */
+ _getFilteredChildren = (parentNode, selectorBranches, opt = {}) => {
+ const children = [];
+ const walker = this._createTreeWalker(parentNode, { force: true });
+ let childNode = walker.firstChild();
+ while (childNode) {
+ if (selectorBranches) {
+ if (isVisible(childNode)) {
+ let isMatch = false;
+ const l = selectorBranches.length;
+ for (let i = 0; i < l; i++) {
+ const leaves = selectorBranches[i];
+ if (this._matchLeaves(leaves, childNode, opt)) {
+ isMatch = true;
+ break;
+ }
+ }
+ if (isMatch) {
+ children.push(childNode);
+ }
+ }
+ } else {
+ children.push(childNode);
+ }
+ childNode = walker.nextSibling();
+ }
+ return children;
+ };
+
+ /**
+ * Collects nth-child nodes.
+ * @private
+ * @param {object} anb - An+B options.
+ * @param {number} anb.a - The 'a' value.
+ * @param {number} anb.b - The 'b' value.
+ * @param {boolean} [anb.reverse] - If true, reverses the order.
+ * @param {object} [anb.selector] - The AST.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _collectNthChild = (anb, node, opt = {}) => {
+ const { a, b, selector } = anb;
+ const { parentNode } = node;
+ if (!parentNode) {
+ const matchedNode = new Set();
+ if (node === this.#root && a * 1 + b * 1 === 1) {
+ if (selector) {
+ const selectorBranches = this._getSelectorBranches(selector);
+ const l = selectorBranches.length;
+ for (let i = 0; i < l; i++) {
+ const leaves = selectorBranches[i];
+ if (this._matchLeaves(leaves, node, opt)) {
+ matchedNode.add(node);
+ break;
+ }
+ }
+ } else {
+ matchedNode.add(node);
+ }
+ }
+ return matchedNode;
+ }
+ const selectorBranches = selector
+ ? this._getSelectorBranches(selector)
+ : null;
+ const children = this._getFilteredChildren(
+ parentNode,
+ selectorBranches,
+ opt
+ );
+ const matchedNodes = filterNodesByAnB(children, anb);
+ return new Set(matchedNodes);
+ };
+
+ /**
+ * Collects nth-of-type nodes.
+ * @private
+ * @param {object} anb - An+B options.
+ * @param {number} anb.a - The 'a' value.
+ * @param {number} anb.b - The 'b' value.
+ * @param {boolean} [anb.reverse] - If true, reverses the order.
+ * @param {object} node - The Element node.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _collectNthOfType = (anb, node) => {
+ const { parentNode } = node;
+ if (!parentNode) {
+ if (node === this.#root && anb.a * 1 + anb.b * 1 === 1) {
+ return new Set([node]);
+ }
+ return new Set();
+ }
+ const typedSiblings = [];
+ const walker = this._createTreeWalker(parentNode, { force: true });
+ let sibling = walker.firstChild();
+ while (sibling) {
+ if (
+ sibling.localName === node.localName &&
+ sibling.namespaceURI === node.namespaceURI &&
+ sibling.prefix === node.prefix
+ ) {
+ typedSiblings.push(sibling);
+ }
+ sibling = walker.nextSibling();
+ }
+ const matchedNodes = filterNodesByAnB(typedSiblings, anb);
+ return new Set(matchedNodes);
+ };
+
+ /**
+ * Matches An+B.
+ * @private
+ * @param {object} ast - The AST.
+ * @param {object} node - The Element node.
+ * @param {string} nthName - The name of the nth pseudo-class.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _matchAnPlusB = (ast, node, nthName, opt = {}) => {
+ const {
+ nth: { a, b, name: nthIdentName },
+ selector
+ } = ast;
+ const anbMap = new Map();
+ if (nthIdentName) {
+ if (nthIdentName === 'even') {
+ anbMap.set('a', 2);
+ anbMap.set('b', 0);
+ } else if (nthIdentName === 'odd') {
+ anbMap.set('a', 2);
+ anbMap.set('b', 1);
+ }
+ if (nthName.indexOf('last') > -1) {
+ anbMap.set('reverse', true);
+ }
+ } else {
+ if (typeof a === 'string' && /-?\d+/.test(a)) {
+ anbMap.set('a', a * 1);
+ } else {
+ anbMap.set('a', 0);
+ }
+ if (typeof b === 'string' && /-?\d+/.test(b)) {
+ anbMap.set('b', b * 1);
+ } else {
+ anbMap.set('b', 0);
+ }
+ if (nthName.indexOf('last') > -1) {
+ anbMap.set('reverse', true);
+ }
+ }
+ if (nthName === 'nth-child' || nthName === 'nth-last-child') {
+ if (selector) {
+ anbMap.set('selector', selector);
+ }
+ const anb = Object.fromEntries(anbMap);
+ const nodes = this._collectNthChild(anb, node, opt);
+ return nodes;
+ } else if (nthName === 'nth-of-type' || nthName === 'nth-last-of-type') {
+ const anb = Object.fromEntries(anbMap);
+ const nodes = this._collectNthOfType(anb, node);
+ return nodes;
+ }
+ return new Set();
+ };
+
+ /**
+ * Matches the :has() pseudo-class function.
+ * @private
+ * @param {Array.<object>} astLeaves - The AST leaves.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {boolean} The result.
+ */
+ _matchHasPseudoFunc = (astLeaves, node, opt = {}) => {
+ if (Array.isArray(astLeaves) && astLeaves.length) {
+ // Prepare a copy to avoid astLeaves being consumed.
+ const leaves = [...astLeaves];
+ const [leaf] = leaves;
+ const { type: leafType } = leaf;
+ let combo;
+ if (leafType === COMBINATOR) {
+ combo = leaves.shift();
+ } else {
+ combo = {
+ name: ' ',
+ type: COMBINATOR
+ };
+ }
+ const twigLeaves = [];
+ while (leaves.length) {
+ const [item] = leaves;
+ const { type: itemType } = item;
+ if (itemType === COMBINATOR) {
+ break;
+ } else {
+ twigLeaves.push(leaves.shift());
+ }
+ }
+ const twig = {
+ combo,
+ leaves: twigLeaves
+ };
+ opt.dir = DIR_NEXT;
+ const nodes = this._matchCombinator(twig, node, opt);
+ if (nodes.size) {
+ if (leaves.length) {
+ let bool = false;
+ for (const nextNode of nodes) {
+ bool = this._matchHasPseudoFunc(leaves, nextNode, opt);
+ if (bool) {
+ break;
+ }
+ }
+ return bool;
+ }
+ return true;
+ }
+ }
+ return false;
+ };
+
+ /**
+ * Evaluates the :has() pseudo-class.
+ * @private
+ * @param {object} astData - The AST data.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {?object} The matched node.
+ */
+ _evaluateHasPseudo = (astData, node, opt) => {
+ const { branches } = astData;
+ let bool = false;
+ const l = branches.length;
+ for (let i = 0; i < l; i++) {
+ const leaves = branches[i];
+ bool = this._matchHasPseudoFunc(leaves, node, opt);
+ if (bool) {
+ break;
+ }
+ }
+ if (!bool) {
+ return null;
+ }
+ if (
+ (opt.isShadowRoot || this.#shadow) &&
+ node.nodeType === DOCUMENT_FRAGMENT_NODE
+ ) {
+ return this.#verifyShadowHost ? node : null;
+ }
+ return node;
+ };
+
+ /**
+ * Matches logical pseudo-class functions.
+ * @private
+ * @param {object} astData - The AST data.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {?object} The matched node.
+ */
+ _matchLogicalPseudoFunc = (astData, node, opt = {}) => {
+ const { astName, branches, twigBranches } = astData;
+ // Handle :has().
+ if (astName === 'has') {
+ return this._evaluateHasPseudo(astData, node, opt);
+ }
+ // Handle :is(), :not(), :where().
+ const isShadowRoot =
+ (opt.isShadowRoot || this.#shadow) &&
+ node.nodeType === DOCUMENT_FRAGMENT_NODE;
+ // Check for invalid shadow root.
+ if (isShadowRoot) {
+ let invalid = false;
+ for (const branch of branches) {
+ if (branch.length > 1) {
+ invalid = true;
+ break;
+ } else if (astName === 'not') {
+ const [{ type: childAstType }] = branch;
+ if (childAstType !== PS_CLASS_SELECTOR) {
+ invalid = true;
+ break;
+ }
+ }
+ }
+ if (invalid) {
+ return null;
+ }
+ }
+ opt.forgive = astName === 'is' || astName === 'where';
+ const l = twigBranches.length;
+ let bool;
+ for (let i = 0; i < l; i++) {
+ const branch = twigBranches[i];
+ const lastIndex = branch.length - 1;
+ const { leaves } = branch[lastIndex];
+ bool = this._matchLeaves(leaves, node, opt);
+ if (bool && lastIndex > 0) {
+ let nextNodes = new Set([node]);
+ for (let j = lastIndex - 1; j >= 0; j--) {
+ const twig = branch[j];
+ const arr = [];
+ opt.dir = DIR_PREV;
+ for (const nextNode of nextNodes) {
+ const m = this._matchCombinator(twig, nextNode, opt);
+ if (m.size) {
+ arr.push(...m);
+ }
+ }
+ if (arr.length) {
+ if (j === 0) {
+ bool = true;
+ } else {
+ nextNodes = new Set(arr);
+ }
+ } else {
+ bool = false;
+ break;
+ }
+ }
+ }
+ if (bool) {
+ break;
+ }
+ }
+ if (astName === 'not') {
+ if (bool) {
+ return null;
+ }
+ return node;
+ } else if (bool) {
+ return node;
+ }
+ return null;
+ };
+
+ /**
+ * match pseudo-class selector
+ * @private
+ * @see https://html.spec.whatwg.org/#pseudo-classes
+ * @param {object} ast - AST
+ * @param {object} node - Element node
+ * @param {object} [opt] - options
+ * @returns {Set.<object>} - collection of matched nodes
+ */
+ _matchPseudoClassSelector(ast, node, opt = {}) {
+ const { children: astChildren, name: astName } = ast;
+ const { localName, parentNode } = node;
+ const { forgive, warn = this.#warn } = opt;
+ const matched = new Set();
+ // :has(), :is(), :not(), :where()
+ if (Array.isArray(astChildren) && KEYS_LOGICAL.has(astName)) {
+ if (!astChildren.length && astName !== 'is' && astName !== 'where') {
+ const css = generateCSS(ast);
+ const msg = `Invalid selector ${css}`;
+ return this.onError(generateException(msg, SYNTAX_ERR, this.#window));
+ }
+ let astData;
+ if (this.#astCache.has(ast)) {
+ astData = this.#astCache.get(ast);
+ } else {
+ const { branches } = walkAST(ast);
+ if (astName === 'has') {
+ // Check for nested :has().
+ let forgiven = false;
+ const l = astChildren.length;
+ for (let i = 0; i < l; i++) {
+ const child = astChildren[i];
+ const item = findAST(child, findLogicalWithNestedHas);
+ if (item) {
+ const itemName = item.name;
+ if (itemName === 'is' || itemName === 'where') {
+ forgiven = true;
+ break;
+ } else {
+ const css = generateCSS(ast);
+ const msg = `Invalid selector ${css}`;
+ return this.onError(
+ generateException(msg, SYNTAX_ERR, this.#window)
+ );
+ }
+ }
+ }
+ if (forgiven) {
+ return matched;
+ }
+ astData = {
+ astName,
+ branches
+ };
+ } else {
+ const twigBranches = [];
+ const l = branches.length;
+ for (let i = 0; i < l; i++) {
+ const [...leaves] = branches[i];
+ const branch = [];
+ const leavesSet = new Set();
+ let item = leaves.shift();
+ while (item) {
+ if (item.type === COMBINATOR) {
+ branch.push({
+ combo: item,
+ leaves: [...leavesSet]
+ });
+ leavesSet.clear();
+ } else if (item) {
+ leavesSet.add(item);
+ }
+ if (leaves.length) {
+ item = leaves.shift();
+ } else {
+ branch.push({
+ combo: null,
+ leaves: [...leavesSet]
+ });
+ leavesSet.clear();
+ break;
+ }
+ }
+ twigBranches.push(branch);
+ }
+ astData = {
+ astName,
+ branches,
+ twigBranches
+ };
+ this.#astCache.set(ast, astData);
+ }
+ }
+ const res = this._matchLogicalPseudoFunc(astData, node, opt);
+ if (res) {
+ matched.add(res);
+ }
+ } else if (Array.isArray(astChildren)) {
+ // :nth-child(), :nth-last-child(), nth-of-type(), :nth-last-of-type()
+ if (/^nth-(?:last-)?(?:child|of-type)$/.test(astName)) {
+ if (astChildren.length !== 1) {
+ const css = generateCSS(ast);
+ return this.onError(
+ generateException(
+ `Invalid selector ${css}`,
+ SYNTAX_ERR,
+ this.#window
+ )
+ );
+ }
+ const [branch] = astChildren;
+ const nodes = this._matchAnPlusB(branch, node, astName, opt);
+ return nodes;
+ } else {
+ switch (astName) {
+ // :dir()
+ case 'dir': {
+ if (astChildren.length !== 1) {
+ const css = generateCSS(ast);
+ return this.onError(
+ generateException(
+ `Invalid selector ${css}`,
+ SYNTAX_ERR,
+ this.#window
+ )
+ );
+ }
+ const [astChild] = astChildren;
+ const res = matchDirectionPseudoClass(astChild, node);
+ if (res) {
+ matched.add(node);
+ }
+ break;
+ }
+ // :lang()
+ case 'lang': {
+ if (!astChildren.length) {
+ const css = generateCSS(ast);
+ return this.onError(
+ generateException(
+ `Invalid selector ${css}`,
+ SYNTAX_ERR,
+ this.#window
+ )
+ );
+ }
+ let bool;
+ for (const astChild of astChildren) {
+ bool = matchLanguagePseudoClass(astChild, node);
+ if (bool) {
+ break;
+ }
+ }
+ if (bool) {
+ matched.add(node);
+ }
+ break;
+ }
+ // :state()
+ case 'state': {
+ if (isCustomElement(node)) {
+ const [{ value: stateValue }] = astChildren;
+ if (stateValue) {
+ if (node[stateValue]) {
+ matched.add(node);
+ } else {
+ for (const i in node) {
+ const prop = node[i];
+ if (prop instanceof this.#window.ElementInternals) {
+ if (prop?.states?.has(stateValue)) {
+ matched.add(node);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+ case 'current':
+ case 'heading':
+ case 'nth-col':
+ case 'nth-last-col': {
+ if (warn) {
+ this.onError(
+ generateException(
+ `Unsupported pseudo-class :${astName}()`,
+ NOT_SUPPORTED_ERR,
+ this.#window
+ )
+ );
+ }
+ break;
+ }
+ // Ignore :host() and :host-context().
+ case 'host':
+ case 'host-context': {
+ break;
+ }
+ // Deprecated in CSS Selectors 3.
+ case 'contains': {
+ if (warn) {
+ this.onError(
+ generateException(
+ `Unknown pseudo-class :${astName}()`,
+ NOT_SUPPORTED_ERR,
+ this.#window
+ )
+ );
+ }
+ break;
+ }
+ default: {
+ if (!forgive) {
+ this.onError(
+ generateException(
+ `Unknown pseudo-class :${astName}()`,
+ SYNTAX_ERR,
+ this.#window
+ )
+ );
+ }
+ }
+ }
+ }
+ } else if (KEYS_PS_NTH_OF_TYPE.has(astName)) {
+ if (node === this.#root) {
+ matched.add(node);
+ } else if (parentNode) {
+ switch (astName) {
+ case 'first-of-type': {
+ const [node1] = this._collectNthOfType(
+ {
+ a: 0,
+ b: 1
+ },
+ node
+ );
+ if (node1) {
+ matched.add(node1);
+ }
+ break;
+ }
+ case 'last-of-type': {
+ const [node1] = this._collectNthOfType(
+ {
+ a: 0,
+ b: 1,
+ reverse: true
+ },
+ node
+ );
+ if (node1) {
+ matched.add(node1);
+ }
+ break;
+ }
+ // 'only-of-type' is handled by default.
+ default: {
+ const [node1] = this._collectNthOfType(
+ {
+ a: 0,
+ b: 1
+ },
+ node
+ );
+ if (node1 === node) {
+ const [node2] = this._collectNthOfType(
+ {
+ a: 0,
+ b: 1,
+ reverse: true
+ },
+ node
+ );
+ if (node2 === node) {
+ matched.add(node);
+ }
+ }
+ }
+ }
+ }
+ } else {
+ switch (astName) {
+ case 'disabled':
+ case 'enabled': {
+ const isMatch = matchDisabledPseudoClass(astName, node);
+ if (isMatch) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'read-only':
+ case 'read-write': {
+ const isMatch = matchReadOnlyPseudoClass(astName, node);
+ if (isMatch) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'any-link':
+ case 'link': {
+ if (
+ (localName === 'a' || localName === 'area') &&
+ node.hasAttribute('href')
+ ) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'local-link': {
+ if (
+ (localName === 'a' || localName === 'area') &&
+ node.hasAttribute('href')
+ ) {
+ if (!this.#documentURL) {
+ this.#documentURL = new URL(this.#document.URL);
+ }
+ const { href, origin, pathname } = this.#documentURL;
+ const attrURL = new URL(node.getAttribute('href'), href);
+ if (attrURL.origin === origin && attrURL.pathname === pathname) {
+ matched.add(node);
+ }
+ }
+ break;
+ }
+ case 'visited': {
+ // prevent fingerprinting
+ break;
+ }
+ case 'hover': {
+ const { target, type } = this.#event ?? {};
+ if (
+ /^(?:click|mouse(?:down|over|up))$/.test(type) &&
+ node.contains(target)
+ ) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'active': {
+ const { buttons, target, type } = this.#event ?? {};
+ if (type === 'mousedown' && buttons & 1 && node.contains(target)) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'target': {
+ if (!this.#documentURL) {
+ this.#documentURL = new URL(this.#document.URL);
+ }
+ const { hash } = this.#documentURL;
+ if (
+ node.id &&
+ hash === `#${node.id}` &&
+ this.#document.contains(node)
+ ) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'target-within': {
+ if (!this.#documentURL) {
+ this.#documentURL = new URL(this.#document.URL);
+ }
+ const { hash } = this.#documentURL;
+ if (hash) {
+ const id = hash.replace(/^#/, '');
+ let current = this.#document.getElementById(id);
+ while (current) {
+ if (current === node) {
+ matched.add(node);
+ break;
+ }
+ current = current.parentNode;
+ }
+ }
+ break;
+ }
+ case 'scope': {
+ if (this.#node.nodeType === ELEMENT_NODE) {
+ if (!this.#shadow && node === this.#node) {
+ matched.add(node);
+ }
+ } else if (node === this.#document.documentElement) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'focus': {
+ const activeElement = this.#document.activeElement;
+ if (node === activeElement && isFocusableArea(node)) {
+ matched.add(node);
+ } else if (activeElement.shadowRoot) {
+ const activeShadowElement = activeElement.shadowRoot.activeElement;
+ let current = activeShadowElement;
+ while (current) {
+ if (current.nodeType === DOCUMENT_FRAGMENT_NODE) {
+ const { host } = current;
+ if (host === activeElement) {
+ if (isFocusableArea(node)) {
+ matched.add(node);
+ } else {
+ matched.add(host);
+ }
+ }
+ break;
+ } else {
+ current = current.parentNode;
+ }
+ }
+ }
+ break;
+ }
+ case 'focus-visible': {
+ if (node === this.#document.activeElement && isFocusableArea(node)) {
+ let bool;
+ if (isFocusVisible(node)) {
+ bool = true;
+ } else if (this.#focus) {
+ const { relatedTarget, target: focusTarget } = this.#focus;
+ if (focusTarget === node) {
+ if (isFocusVisible(relatedTarget)) {
+ bool = true;
+ } else if (this.#event) {
+ const {
+ altKey: eventAltKey,
+ ctrlKey: eventCtrlKey,
+ key: eventKey,
+ metaKey: eventMetaKey,
+ target: eventTarget,
+ type: eventType
+ } = this.#event;
+ // this.#event is irrelevant if eventTarget === relatedTarget
+ if (eventTarget === relatedTarget) {
+ if (this.#lastFocusVisible === null) {
+ bool = true;
+ } else if (focusTarget === this.#lastFocusVisible) {
+ bool = true;
+ }
+ } else if (eventKey === 'Tab') {
+ if (
+ (eventType === 'keydown' && eventTarget !== node) ||
+ (eventType === 'keyup' && eventTarget === node)
+ ) {
+ if (eventTarget === focusTarget) {
+ if (this.#lastFocusVisible === null) {
+ bool = true;
+ } else if (
+ eventTarget === this.#lastFocusVisible &&
+ relatedTarget === null
+ ) {
+ bool = true;
+ }
+ } else {
+ bool = true;
+ }
+ }
+ } else if (eventKey) {
+ if (
+ (eventType === 'keydown' || eventType === 'keyup') &&
+ !eventAltKey &&
+ !eventCtrlKey &&
+ !eventMetaKey &&
+ eventTarget === node
+ ) {
+ bool = true;
+ }
+ }
+ } else if (
+ relatedTarget === null ||
+ relatedTarget === this.#lastFocusVisible
+ ) {
+ bool = true;
+ }
+ }
+ }
+ if (bool) {
+ this.#lastFocusVisible = node;
+ matched.add(node);
+ } else if (this.#lastFocusVisible === node) {
+ this.#lastFocusVisible = null;
+ }
+ }
+ break;
+ }
+ case 'focus-within': {
+ const activeElement = this.#document.activeElement;
+ if (node.contains(activeElement) && isFocusableArea(activeElement)) {
+ matched.add(node);
+ } else if (activeElement.shadowRoot) {
+ const activeShadowElement = activeElement.shadowRoot.activeElement;
+ if (node.contains(activeShadowElement)) {
+ matched.add(node);
+ } else {
+ let current = activeShadowElement;
+ while (current) {
+ if (current.nodeType === DOCUMENT_FRAGMENT_NODE) {
+ const { host } = current;
+ if (host === activeElement && node.contains(host)) {
+ matched.add(node);
+ }
+ break;
+ } else {
+ current = current.parentNode;
+ }
+ }
+ }
+ }
+ break;
+ }
+ case 'open':
+ case 'closed': {
+ if (localName === 'details' || localName === 'dialog') {
+ if (node.hasAttribute('open')) {
+ if (astName === 'open') {
+ matched.add(node);
+ }
+ } else if (astName === 'closed') {
+ matched.add(node);
+ }
+ }
+ break;
+ }
+ case 'placeholder-shown': {
+ let placeholder;
+ if (node.placeholder) {
+ placeholder = node.placeholder;
+ } else if (node.hasAttribute('placeholder')) {
+ placeholder = node.getAttribute('placeholder');
+ }
+ if (typeof placeholder === 'string' && !/[\r\n]/.test(placeholder)) {
+ let targetNode;
+ if (localName === 'textarea') {
+ targetNode = node;
+ } else if (localName === 'input') {
+ if (node.hasAttribute('type')) {
+ if (KEYS_INPUT_PLACEHOLDER.has(node.getAttribute('type'))) {
+ targetNode = node;
+ }
+ } else {
+ targetNode = node;
+ }
+ }
+ if (targetNode && node.value === '') {
+ matched.add(node);
+ }
+ }
+ break;
+ }
+ case 'checked': {
+ const attrType = node.getAttribute('type');
+ if (
+ (node.checked &&
+ localName === 'input' &&
+ (attrType === 'checkbox' || attrType === 'radio')) ||
+ (node.selected && localName === 'option')
+ ) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'indeterminate': {
+ if (
+ (node.indeterminate &&
+ localName === 'input' &&
+ node.type === 'checkbox') ||
+ (localName === 'progress' && !node.hasAttribute('value'))
+ ) {
+ matched.add(node);
+ } else if (
+ localName === 'input' &&
+ node.type === 'radio' &&
+ !node.hasAttribute('checked')
+ ) {
+ const nodeName = node.name;
+ let parent = node.parentNode;
+ while (parent) {
+ if (parent.localName === 'form') {
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ if (!parent) {
+ parent = this.#document.documentElement;
+ }
+ const walker = this._createTreeWalker(parent);
+ let refNode = traverseNode(parent, walker);
+ refNode = walker.firstChild();
+ let checked;
+ while (refNode) {
+ if (
+ refNode.localName === 'input' &&
+ refNode.getAttribute('type') === 'radio'
+ ) {
+ if (refNode.hasAttribute('name')) {
+ if (refNode.getAttribute('name') === nodeName) {
+ checked = !!refNode.checked;
+ }
+ } else {
+ checked = !!refNode.checked;
+ }
+ if (checked) {
+ break;
+ }
+ }
+ refNode = walker.nextNode();
+ }
+ if (!checked) {
+ matched.add(node);
+ }
+ }
+ break;
+ }
+ case 'default': {
+ // button[type="submit"], input[type="submit"], input[type="image"]
+ const attrType = node.getAttribute('type');
+ if (
+ (localName === 'button' &&
+ !(node.hasAttribute('type') && KEYS_INPUT_RESET.has(attrType))) ||
+ (localName === 'input' &&
+ node.hasAttribute('type') &&
+ KEYS_INPUT_SUBMIT.has(attrType))
+ ) {
+ let form = node.parentNode;
+ while (form) {
+ if (form.localName === 'form') {
+ break;
+ }
+ form = form.parentNode;
+ }
+ if (form) {
+ const walker = this._createTreeWalker(form);
+ let refNode = traverseNode(form, walker);
+ refNode = walker.firstChild();
+ while (refNode) {
+ const nodeName = refNode.localName;
+ const nodeAttrType = refNode.getAttribute('type');
+ let m;
+ if (nodeName === 'button') {
+ m = !(
+ refNode.hasAttribute('type') &&
+ KEYS_INPUT_RESET.has(nodeAttrType)
+ );
+ } else if (nodeName === 'input') {
+ m =
+ refNode.hasAttribute('type') &&
+ KEYS_INPUT_SUBMIT.has(nodeAttrType);
+ }
+ if (m) {
+ if (refNode === node) {
+ matched.add(node);
+ }
+ break;
+ }
+ refNode = walker.nextNode();
+ }
+ }
+ // input[type="checkbox"], input[type="radio"]
+ } else if (
+ localName === 'input' &&
+ node.hasAttribute('type') &&
+ node.hasAttribute('checked') &&
+ KEYS_INPUT_CHECK.has(attrType)
+ ) {
+ matched.add(node);
+ // option
+ } else if (localName === 'option' && node.hasAttribute('selected')) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'valid':
+ case 'invalid': {
+ if (KEYS_FORM_PS_VALID.has(localName)) {
+ let valid;
+ if (node.checkValidity()) {
+ if (node.maxLength >= 0) {
+ if (node.maxLength >= node.value.length) {
+ valid = true;
+ }
+ } else {
+ valid = true;
+ }
+ }
+ if (valid) {
+ if (astName === 'valid') {
+ matched.add(node);
+ }
+ } else if (astName === 'invalid') {
+ matched.add(node);
+ }
+ } else if (localName === 'fieldset') {
+ const walker = this._createTreeWalker(node);
+ let refNode = traverseNode(node, walker);
+ refNode = walker.firstChild();
+ let valid;
+ if (!refNode) {
+ valid = true;
+ } else {
+ while (refNode) {
+ if (KEYS_FORM_PS_VALID.has(refNode.localName)) {
+ if (refNode.checkValidity()) {
+ if (refNode.maxLength >= 0) {
+ valid = refNode.maxLength >= refNode.value.length;
+ } else {
+ valid = true;
+ }
+ } else {
+ valid = false;
+ }
+ if (!valid) {
+ break;
+ }
+ }
+ refNode = walker.nextNode();
+ }
+ }
+ if (valid) {
+ if (astName === 'valid') {
+ matched.add(node);
+ }
+ } else if (astName === 'invalid') {
+ matched.add(node);
+ }
+ }
+ break;
+ }
+ case 'in-range':
+ case 'out-of-range': {
+ const attrType = node.getAttribute('type');
+ if (
+ localName === 'input' &&
+ !(node.readonly || node.hasAttribute('readonly')) &&
+ !(node.disabled || node.hasAttribute('disabled')) &&
+ KEYS_INPUT_RANGE.has(attrType)
+ ) {
+ const flowed =
+ node.validity.rangeUnderflow || node.validity.rangeOverflow;
+ if (astName === 'out-of-range' && flowed) {
+ matched.add(node);
+ } else if (
+ astName === 'in-range' &&
+ !flowed &&
+ (node.hasAttribute('min') ||
+ node.hasAttribute('max') ||
+ attrType === 'range')
+ ) {
+ matched.add(node);
+ }
+ }
+ break;
+ }
+ case 'required':
+ case 'optional': {
+ let required;
+ let optional;
+ if (localName === 'select' || localName === 'textarea') {
+ if (node.required || node.hasAttribute('required')) {
+ required = true;
+ } else {
+ optional = true;
+ }
+ } else if (localName === 'input') {
+ if (node.hasAttribute('type')) {
+ const attrType = node.getAttribute('type');
+ if (KEYS_INPUT_REQUIRED.has(attrType)) {
+ if (node.required || node.hasAttribute('required')) {
+ required = true;
+ } else {
+ optional = true;
+ }
+ } else {
+ optional = true;
+ }
+ } else if (node.required || node.hasAttribute('required')) {
+ required = true;
+ } else {
+ optional = true;
+ }
+ }
+ if (astName === 'required' && required) {
+ matched.add(node);
+ } else if (astName === 'optional' && optional) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'root': {
+ if (node === this.#document.documentElement) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'empty': {
+ if (node.hasChildNodes()) {
+ const walker = this._createTreeWalker(node, {
+ force: true,
+ whatToShow: SHOW_ALL
+ });
+ let refNode = walker.firstChild();
+ let bool;
+ while (refNode) {
+ bool =
+ refNode.nodeType !== ELEMENT_NODE &&
+ refNode.nodeType !== TEXT_NODE;
+ if (!bool) {
+ break;
+ }
+ refNode = walker.nextSibling();
+ }
+ if (bool) {
+ matched.add(node);
+ }
+ } else {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'first-child': {
+ if (
+ (parentNode && node === parentNode.firstElementChild) ||
+ node === this.#root
+ ) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'last-child': {
+ if (
+ (parentNode && node === parentNode.lastElementChild) ||
+ node === this.#root
+ ) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'only-child': {
+ if (
+ (parentNode &&
+ node === parentNode.firstElementChild &&
+ node === parentNode.lastElementChild) ||
+ node === this.#root
+ ) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'defined': {
+ if (node.hasAttribute('is') || localName.includes('-')) {
+ if (isCustomElement(node)) {
+ matched.add(node);
+ }
+ // NOTE: MathMLElement is not implemented in jsdom.
+ } else if (
+ node instanceof this.#window.HTMLElement ||
+ node instanceof this.#window.SVGElement
+ ) {
+ matched.add(node);
+ }
+ break;
+ }
+ case 'popover-open': {
+ if (node.popover && isVisible(node)) {
+ matched.add(node);
+ }
+ break;
+ }
+ // Ignore :host.
+ case 'host': {
+ break;
+ }
+ // Legacy pseudo-elements.
+ case 'after':
+ case 'before':
+ case 'first-letter':
+ case 'first-line': {
+ if (warn) {
+ this.onError(
+ generateException(
+ `Unsupported pseudo-element ::${astName}`,
+ NOT_SUPPORTED_ERR,
+ this.#window
+ )
+ );
+ }
+ break;
+ }
+ // Not supported.
+ case 'autofill':
+ case 'blank':
+ case 'buffering':
+ case 'current':
+ case 'fullscreen':
+ case 'future':
+ case 'has-slotted':
+ case 'heading':
+ case 'modal':
+ case 'muted':
+ case 'past':
+ case 'paused':
+ case 'picture-in-picture':
+ case 'playing':
+ case 'seeking':
+ case 'stalled':
+ case 'user-invalid':
+ case 'user-valid':
+ case 'volume-locked':
+ case '-webkit-autofill': {
+ if (warn) {
+ this.onError(
+ generateException(
+ `Unsupported pseudo-class :${astName}`,
+ NOT_SUPPORTED_ERR,
+ this.#window
+ )
+ );
+ }
+ break;
+ }
+ default: {
+ if (astName.startsWith('-webkit-')) {
+ if (warn) {
+ this.onError(
+ generateException(
+ `Unsupported pseudo-class :${astName}`,
+ NOT_SUPPORTED_ERR,
+ this.#window
+ )
+ );
+ }
+ } else if (!forgive) {
+ this.onError(
+ generateException(
+ `Unknown pseudo-class :${astName}`,
+ SYNTAX_ERR,
+ this.#window
+ )
+ );
+ }
+ }
+ }
+ }
+ return matched;
+ }
+
+ /**
+ * Evaluates the :host() pseudo-class.
+ * @private
+ * @param {Array.<object>} leaves - The AST leaves.
+ * @param {object} host - The host element.
+ * @param {object} ast - The original AST for error reporting.
+ * @returns {boolean} True if matched.
+ */
+ _evaluateHostPseudo = (leaves, host, ast) => {
+ const l = leaves.length;
+ for (let i = 0; i < l; i++) {
+ const leaf = leaves[i];
+ if (leaf.type === COMBINATOR) {
+ const css = generateCSS(ast);
+ const msg = `Invalid selector ${css}`;
+ this.onError(generateException(msg, SYNTAX_ERR, this.#window));
+ return false;
+ }
+ if (!this._matchSelector(leaf, host).has(host)) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ /**
+ * Evaluates the :host-context() pseudo-class.
+ * @private
+ * @param {Array.<object>} leaves - The AST leaves.
+ * @param {object} host - The host element.
+ * @param {object} ast - The original AST for error reporting.
+ * @returns {boolean} True if matched.
+ */
+ _evaluateHostContextPseudo = (leaves, host, ast) => {
+ let parent = host;
+ while (parent) {
+ let bool;
+ const l = leaves.length;
+ for (let i = 0; i < l; i++) {
+ const leaf = leaves[i];
+ if (leaf.type === COMBINATOR) {
+ const css = generateCSS(ast);
+ const msg = `Invalid selector ${css}`;
+ this.onError(generateException(msg, SYNTAX_ERR, this.#window));
+ return false;
+ }
+ bool = this._matchSelector(leaf, parent).has(parent);
+ if (!bool) {
+ break;
+ }
+ }
+ if (bool) {
+ return true;
+ }
+ parent = parent.parentNode;
+ }
+ return false;
+ };
+
+ /**
+ * Matches shadow host pseudo-classes.
+ * @private
+ * @param {object} ast - The AST.
+ * @param {object} node - The DocumentFragment node.
+ * @returns {?object} The matched node.
+ */
+ _matchShadowHostPseudoClass = (ast, node) => {
+ const { children: astChildren, name: astName } = ast;
+ // Handle simple pseudo-class (no arguments).
+ if (!Array.isArray(astChildren)) {
+ if (astName === 'host') {
+ return node;
+ }
+ const msg = `Invalid selector :${astName}`;
+ return this.onError(generateException(msg, SYNTAX_ERR, this.#window));
+ }
+ // Handle functional pseudo-class like :host(...).
+ if (astName !== 'host' && astName !== 'host-context') {
+ const msg = `Invalid selector :${astName}()`;
+ return this.onError(generateException(msg, SYNTAX_ERR, this.#window));
+ }
+ if (astChildren.length !== 1) {
+ const css = generateCSS(ast);
+ const msg = `Invalid selector ${css}`;
+ return this.onError(generateException(msg, SYNTAX_ERR, this.#window));
+ }
+ const { host } = node;
+ const { branches } = walkAST(astChildren[0]);
+ const [branch] = branches;
+ const [...leaves] = branch;
+ let isMatch = false;
+ if (astName === 'host') {
+ isMatch = this._evaluateHostPseudo(leaves, host, ast);
+ // astName === 'host-context'.
+ } else {
+ isMatch = this._evaluateHostContextPseudo(leaves, host, ast);
+ }
+ return isMatch ? node : null;
+ };
+
+ /**
+ * Matches a selector for element nodes.
+ * @private
+ * @param {object} ast - The AST.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _matchSelectorForElement = (ast, node, opt = {}) => {
+ const { type: astType } = ast;
+ const astName = unescapeSelector(ast.name);
+ const matched = new Set();
+ switch (astType) {
+ case ATTR_SELECTOR: {
+ if (matchAttributeSelector(ast, node, opt)) {
+ matched.add(node);
+ }
+ break;
+ }
+ case ID_SELECTOR: {
+ if (node.id === astName) {
+ matched.add(node);
+ }
+ break;
+ }
+ case CLASS_SELECTOR: {
+ if (node.classList.contains(astName)) {
+ matched.add(node);
+ }
+ break;
+ }
+ case PS_CLASS_SELECTOR: {
+ return this._matchPseudoClassSelector(ast, node, opt);
+ }
+ case TYPE_SELECTOR: {
+ if (matchTypeSelector(ast, node, opt)) {
+ matched.add(node);
+ }
+ break;
+ }
+ // PS_ELEMENT_SELECTOR is handled by default.
+ default: {
+ try {
+ if (opt.check) {
+ const css = generateCSS(ast);
+ this.#pseudoElement.push(css);
+ matched.add(node);
+ } else {
+ matchPseudoElementSelector(astName, astType, opt);
+ }
+ } catch (e) {
+ this.onError(e);
+ }
+ }
+ }
+ return matched;
+ };
+
+ /**
+ * Matches a selector for a shadow root.
+ * @private
+ * @param {object} ast - The AST.
+ * @param {object} node - The DocumentFragment node.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _matchSelectorForShadowRoot = (ast, node, opt = {}) => {
+ const { name: astName } = ast;
+ if (KEYS_LOGICAL.has(astName)) {
+ opt.isShadowRoot = true;
+ return this._matchPseudoClassSelector(ast, node, opt);
+ }
+ const matched = new Set();
+ if (astName === 'host' || astName === 'host-context') {
+ const res = this._matchShadowHostPseudoClass(ast, node, opt);
+ if (res) {
+ this.#verifyShadowHost = true;
+ matched.add(res);
+ }
+ }
+ return matched;
+ };
+
+ /**
+ * Matches a selector.
+ * @private
+ * @param {object} ast - The AST.
+ * @param {object} node - The Document, DocumentFragment, or Element node.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _matchSelector = (ast, node, opt = {}) => {
+ if (node.nodeType === ELEMENT_NODE) {
+ return this._matchSelectorForElement(ast, node, opt);
+ }
+ if (
+ this.#shadow &&
+ node.nodeType === DOCUMENT_FRAGMENT_NODE &&
+ ast.type === PS_CLASS_SELECTOR
+ ) {
+ return this._matchSelectorForShadowRoot(ast, node, opt);
+ }
+ return new Set();
+ };
+
+ /**
+ * Matches leaves.
+ * @private
+ * @param {Array.<object>} leaves - The AST leaves.
+ * @param {object} node - The node.
+ * @param {object} [opt] - Options.
+ * @returns {boolean} The result.
+ */
+ _matchLeaves = (leaves, node, opt = {}) => {
+ const results = this.#invalidate ? this.#invalidateResults : this.#results;
+ let result = results.get(leaves);
+ if (result && result.has(node)) {
+ const { matched } = result.get(node);
+ return matched;
+ }
+ let cacheable = true;
+ if (node.nodeType === ELEMENT_NODE && KEYS_FORM.has(node.localName)) {
+ cacheable = false;
+ }
+ let bool;
+ const l = leaves.length;
+ for (let i = 0; i < l; i++) {
+ const leaf = leaves[i];
+ switch (leaf.type) {
+ case ATTR_SELECTOR:
+ case ID_SELECTOR: {
+ cacheable = false;
+ break;
+ }
+ case PS_CLASS_SELECTOR: {
+ if (KEYS_PS_UNCACHE.has(leaf.name)) {
+ cacheable = false;
+ }
+ break;
+ }
+ default: {
+ // No action needed for other types.
+ }
+ }
+ bool = this._matchSelector(leaf, node, opt).has(node);
+ if (!bool) {
+ break;
+ }
+ }
+ if (cacheable) {
+ if (!result) {
+ result = new WeakMap();
+ }
+ result.set(node, {
+ matched: bool
+ });
+ results.set(leaves, result);
+ }
+ return bool;
+ };
+
+ /**
+ * Traverses all descendant nodes and collects matches.
+ * @private
+ * @param {object} baseNode - The base Element node or Element.shadowRoot.
+ * @param {Array.<object>} leaves - The AST leaves.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _traverseAllDescendants = (baseNode, leaves, opt = {}) => {
+ const walker = this._createTreeWalker(baseNode);
+ traverseNode(baseNode, walker);
+ let currentNode = walker.firstChild();
+ const nodes = new Set();
+ while (currentNode) {
+ if (this._matchLeaves(leaves, currentNode, opt)) {
+ nodes.add(currentNode);
+ }
+ currentNode = walker.nextNode();
+ }
+ return nodes;
+ };
+
+ /**
+ * Finds descendant nodes.
+ * @private
+ * @param {Array.<object>} leaves - The AST leaves.
+ * @param {object} baseNode - The base Element node or Element.shadowRoot.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _findDescendantNodes = (leaves, baseNode, opt = {}) => {
+ const [leaf, ...filterLeaves] = leaves;
+ const { type: leafType } = leaf;
+ switch (leafType) {
+ case ID_SELECTOR: {
+ const canUseGetElementById =
+ !this.#shadow &&
+ baseNode.nodeType === ELEMENT_NODE &&
+ this.#root.nodeType !== ELEMENT_NODE;
+ if (canUseGetElementById) {
+ const leafName = unescapeSelector(leaf.name);
+ const nodes = new Set();
+ const foundNode = this.#root.getElementById(leafName);
+ if (
+ foundNode &&
+ foundNode !== baseNode &&
+ baseNode.contains(foundNode)
+ ) {
+ const isCompoundSelector = filterLeaves.length > 0;
+ if (
+ !isCompoundSelector ||
+ this._matchLeaves(filterLeaves, foundNode, opt)
+ ) {
+ nodes.add(foundNode);
+ }
+ }
+ return nodes;
+ }
+ // Fallback to default traversal if fast path is not applicable.
+ return this._traverseAllDescendants(baseNode, leaves, opt);
+ }
+ case PS_ELEMENT_SELECTOR: {
+ const leafName = unescapeSelector(leaf.name);
+ matchPseudoElementSelector(leafName, leafType, opt);
+ return new Set();
+ }
+ default: {
+ return this._traverseAllDescendants(baseNode, leaves, opt);
+ }
+ }
+ };
+
+ /**
+ * Matches the descendant combinator ' '.
+ * @private
+ * @param {object} twig - The twig object.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _matchDescendantCombinator = (twig, node, opt = {}) => {
+ const { leaves } = twig;
+ const { parentNode } = node;
+ const { dir } = opt;
+ if (dir === DIR_NEXT) {
+ return this._findDescendantNodes(leaves, node, opt);
+ }
+ // DIR_PREV
+ const ancestors = [];
+ let refNode = parentNode;
+ while (refNode) {
+ if (this._matchLeaves(leaves, refNode, opt)) {
+ ancestors.push(refNode);
+ }
+ refNode = refNode.parentNode;
+ }
+ if (ancestors.length) {
+ // Reverse to maintain document order.
+ return new Set(ancestors.reverse());
+ }
+ return new Set();
+ };
+
+ /**
+ * Matches the child combinator '>'.
+ * @private
+ * @param {object} twig - The twig object.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _matchChildCombinator = (twig, node, opt = {}) => {
+ const { leaves } = twig;
+ const { dir } = opt;
+ const { parentNode } = node;
+ const matched = new Set();
+ if (dir === DIR_NEXT) {
+ let refNode = node.firstElementChild;
+ while (refNode) {
+ if (this._matchLeaves(leaves, refNode, opt)) {
+ matched.add(refNode);
+ }
+ refNode = refNode.nextElementSibling;
+ }
+ } else {
+ // DIR_PREV
+ if (parentNode && this._matchLeaves(leaves, parentNode, opt)) {
+ matched.add(parentNode);
+ }
+ }
+ return matched;
+ };
+
+ /**
+ * Matches the adjacent sibling combinator '+'.
+ * @private
+ * @param {object} twig - The twig object.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _matchAdjacentSiblingCombinator = (twig, node, opt = {}) => {
+ const { leaves } = twig;
+ const { dir } = opt;
+ const matched = new Set();
+ const refNode =
+ dir === DIR_NEXT ? node.nextElementSibling : node.previousElementSibling;
+ if (refNode && this._matchLeaves(leaves, refNode, opt)) {
+ matched.add(refNode);
+ }
+ return matched;
+ };
+
+ /**
+ * Matches the general sibling combinator '~'.
+ * @private
+ * @param {object} twig - The twig object.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _matchGeneralSiblingCombinator = (twig, node, opt = {}) => {
+ const { leaves } = twig;
+ const { dir } = opt;
+ const matched = new Set();
+ let refNode =
+ dir === DIR_NEXT ? node.nextElementSibling : node.previousElementSibling;
+ while (refNode) {
+ if (this._matchLeaves(leaves, refNode, opt)) {
+ matched.add(refNode);
+ }
+ refNode =
+ dir === DIR_NEXT
+ ? refNode.nextElementSibling
+ : refNode.previousElementSibling;
+ }
+ return matched;
+ };
+
+ /**
+ * Matches a combinator.
+ * @private
+ * @param {object} twig - The twig object.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ _matchCombinator = (twig, node, opt = {}) => {
+ const {
+ combo: { name: comboName }
+ } = twig;
+ switch (comboName) {
+ case '+': {
+ return this._matchAdjacentSiblingCombinator(twig, node, opt);
+ }
+ case '~': {
+ return this._matchGeneralSiblingCombinator(twig, node, opt);
+ }
+ case '>': {
+ return this._matchChildCombinator(twig, node, opt);
+ }
+ case ' ':
+ default: {
+ return this._matchDescendantCombinator(twig, node, opt);
+ }
+ }
+ };
+
+ /**
+ * Traverses with a TreeWalker and collects nodes matching the leaves.
+ * @private
+ * @param {TreeWalker} walker - The TreeWalker instance to use.
+ * @param {Array} leaves - The AST leaves to match against.
+ * @param {object} options - Traversal options.
+ * @param {Node} options.startNode - The node to start traversal from.
+ * @param {string} options.targetType - The type of target ('all' or 'first').
+ * @param {Node} [options.boundaryNode] - The node to stop traversal at.
+ * @param {boolean} [options.force] - Force traversal to the next node.
+ * @returns {Array.<Node>} An array of matched nodes.
+ */
+ _traverseAndCollectNodes = (walker, leaves, options) => {
+ const { boundaryNode, force, startNode, targetType } = options;
+ const collectedNodes = [];
+ let currentNode = traverseNode(startNode, walker, !!force);
+ if (!currentNode) {
+ return [];
+ }
+ // Adjust starting node.
+ if (currentNode.nodeType !== ELEMENT_NODE) {
+ currentNode = walker.nextNode();
+ } else if (currentNode === startNode && currentNode !== this.#root) {
+ currentNode = walker.nextNode();
+ }
+ const matchOpt = {
+ warn: this.#warn
+ };
+ while (currentNode) {
+ // Stop when we reach the boundary.
+ if (boundaryNode) {
+ if (currentNode === boundaryNode) {
+ break;
+ } else if (
+ targetType === TARGET_ALL &&
+ !boundaryNode.contains(currentNode)
+ ) {
+ break;
+ }
+ }
+ if (
+ this._matchLeaves(leaves, currentNode, matchOpt) &&
+ currentNode !== this.#node
+ ) {
+ collectedNodes.push(currentNode);
+ // Stop after the first match if not collecting all.
+ if (targetType !== TARGET_ALL) {
+ break;
+ }
+ }
+ currentNode = walker.nextNode();
+ }
+ return collectedNodes;
+ };
+
+ /**
+ * Finds matched node(s) preceding this.#node.
+ * @private
+ * @param {Array.<object>} leaves - The AST leaves.
+ * @param {object} node - The node to start from.
+ * @param {object} opt - Options.
+ * @param {boolean} [opt.force] - If true, traverses only to the next node.
+ * @param {string} [opt.targetType] - The target type.
+ * @returns {Array.<object>} A collection of matched nodes.
+ */
+ _findPrecede = (leaves, node, opt = {}) => {
+ const { force, targetType } = opt;
+ if (!this.#rootWalker) {
+ this.#rootWalker = this._createTreeWalker(this.#root);
+ }
+ return this._traverseAndCollectNodes(this.#rootWalker, leaves, {
+ force,
+ targetType,
+ boundaryNode: this.#node,
+ startNode: node
+ });
+ };
+
+ /**
+ * Finds matched node(s) in #nodeWalker.
+ * @private
+ * @param {Array.<object>} leaves - The AST leaves.
+ * @param {object} node - The node to start from.
+ * @param {object} opt - Options.
+ * @param {boolean} [opt.precede] - If true, finds preceding nodes.
+ * @returns {Array.<object>} A collection of matched nodes.
+ */
+ _findNodeWalker = (leaves, node, opt = {}) => {
+ const { precede, ...traversalOpts } = opt;
+ if (precede) {
+ const precedeNodes = this._findPrecede(leaves, this.#root, opt);
+ if (precedeNodes.length) {
+ return precedeNodes;
+ }
+ }
+ if (!this.#nodeWalker) {
+ this.#nodeWalker = this._createTreeWalker(this.#node);
+ }
+ return this._traverseAndCollectNodes(this.#nodeWalker, leaves, {
+ startNode: node,
+ ...traversalOpts
+ });
+ };
+
+ /**
+ * Matches the node itself.
+ * @private
+ * @param {Array} leaves - The AST leaves.
+ * @param {boolean} check - Indicates if running in internal check().
+ * @returns {Array} An array containing [nodes, filtered, pseudoElement].
+ */
+ _matchSelf = (leaves, check = false) => {
+ const options = { check, warn: this.#warn };
+ const matched = this._matchLeaves(leaves, this.#node, options);
+ const nodes = matched ? [this.#node] : [];
+ return [nodes, matched, this.#pseudoElement];
+ };
+
+ /**
+ * Finds lineal nodes (self and ancestors).
+ * @private
+ * @param {Array} leaves - The AST leaves.
+ * @param {object} opt - Options.
+ * @returns {Array} An array containing [nodes, filtered].
+ */
+ _findLineal = (leaves, opt) => {
+ const { complex } = opt;
+ const nodes = [];
+ const options = { warn: this.#warn };
+ const selfMatched = this._matchLeaves(leaves, this.#node, options);
+ if (selfMatched) {
+ nodes.push(this.#node);
+ }
+ if (!selfMatched || complex) {
+ let currentNode = this.#node.parentNode;
+ while (currentNode) {
+ if (this._matchLeaves(leaves, currentNode, options)) {
+ nodes.push(currentNode);
+ }
+ currentNode = currentNode.parentNode;
+ }
+ }
+ const filtered = nodes.length > 0;
+ return [nodes, filtered];
+ };
+
+ /**
+ * Finds entry nodes for pseudo-element selectors.
+ * @private
+ * @param {object} leaf - The pseudo-element leaf from the AST.
+ * @param {Array.<object>} filterLeaves - Leaves for compound selectors.
+ * @param {string} targetType - The type of target to find.
+ * @returns {object} The result { nodes, filtered, pending }.
+ */
+ _findEntryNodesForPseudoElement = (leaf, filterLeaves, targetType) => {
+ let nodes = [];
+ let filtered = false;
+ if (targetType === TARGET_SELF && this.#check) {
+ const css = generateCSS(leaf);
+ this.#pseudoElement.push(css);
+ if (filterLeaves.length) {
+ [nodes, filtered] = this._matchSelf(filterLeaves, this.#check);
+ } else {
+ nodes.push(this.#node);
+ filtered = true;
+ }
+ } else {
+ matchPseudoElementSelector(leaf.name, leaf.type, { warn: this.#warn });
+ }
+ return { nodes, filtered, pending: false };
+ };
+
+ /**
+ * Finds entry nodes for ID selectors.
+ * @private
+ * @param {object} twig - The current twig from the AST branch.
+ * @param {string} targetType - The type of target to find.
+ * @param {object} opt - Additional options for finding nodes.
+ * @returns {object} The result { nodes, filtered, pending }.
+ */
+ _findEntryNodesForId = (twig, targetType, opt) => {
+ const { leaves } = twig;
+ const [leaf, ...filterLeaves] = leaves;
+ const { complex, precede } = opt;
+ let nodes = [];
+ let filtered = false;
+ if (targetType === TARGET_SELF) {
+ [nodes, filtered] = this._matchSelf(leaves);
+ } else if (targetType === TARGET_LINEAL) {
+ [nodes, filtered] = this._findLineal(leaves, { complex });
+ } else if (
+ targetType === TARGET_FIRST &&
+ this.#root.nodeType !== ELEMENT_NODE
+ ) {
+ const node = this.#root.getElementById(leaf.name);
+ if (node) {
+ if (filterLeaves.length) {
+ if (this._matchLeaves(filterLeaves, node, { warn: this.#warn })) {
+ nodes.push(node);
+ filtered = true;
+ }
+ } else {
+ nodes.push(node);
+ filtered = true;
+ }
+ }
+ } else {
+ nodes = this._findNodeWalker(leaves, this.#node, { precede, targetType });
+ filtered = nodes.length > 0;
+ }
+ return { nodes, filtered, pending: false };
+ };
+
+ /**
+ * Finds entry nodes for class selectors.
+ * @private
+ * @param {Array.<object>} leaves - The AST leaves for the selector.
+ * @param {string} targetType - The type of target to find.
+ * @param {object} opt - Additional options for finding nodes.
+ * @returns {object} The result { nodes, filtered, pending }.
+ */
+ _findEntryNodesForClass = (leaves, targetType, opt) => {
+ const { complex, precede } = opt;
+ let nodes = [];
+ let filtered = false;
+ if (targetType === TARGET_SELF) {
+ [nodes, filtered] = this._matchSelf(leaves);
+ } else if (targetType === TARGET_LINEAL) {
+ [nodes, filtered] = this._findLineal(leaves, { complex });
+ } else {
+ nodes = this._findNodeWalker(leaves, this.#node, { precede, targetType });
+ filtered = nodes.length > 0;
+ }
+ return { nodes, filtered, pending: false };
+ };
+
+ /**
+ * Finds entry nodes for type selectors.
+ * @private
+ * @param {Array.<object>} leaves - The AST leaves for the selector.
+ * @param {string} targetType - The type of target to find.
+ * @param {object} opt - Additional options for finding nodes.
+ * @returns {object} The result { nodes, filtered, pending }.
+ */
+ _findEntryNodesForType = (leaves, targetType, opt) => {
+ const { complex, precede } = opt;
+ let nodes = [];
+ let filtered = false;
+ if (targetType === TARGET_SELF) {
+ [nodes, filtered] = this._matchSelf(leaves);
+ } else if (targetType === TARGET_LINEAL) {
+ [nodes, filtered] = this._findLineal(leaves, { complex });
+ } else {
+ nodes = this._findNodeWalker(leaves, this.#node, { precede, targetType });
+ filtered = nodes.length > 0;
+ }
+ return { nodes, filtered, pending: false };
+ };
+
+ /**
+ * Finds entry nodes for other selector types (default case).
+ * @private
+ * @param {object} twig - The current twig from the AST branch.
+ * @param {string} targetType - The type of target to find.
+ * @param {object} opt - Additional options for finding nodes.
+ * @returns {object} The result { nodes, filtered, pending }.
+ */
+ _findEntryNodesForOther = (twig, targetType, opt) => {
+ const { leaves } = twig;
+ const [leaf, ...filterLeaves] = leaves;
+ const { complex, precede } = opt;
+ let nodes = [];
+ let filtered = false;
+ let pending = false;
+ if (targetType !== TARGET_LINEAL && /host(?:-context)?/.test(leaf.name)) {
+ let shadowRoot = null;
+ if (this.#shadow && this.#node.nodeType === DOCUMENT_FRAGMENT_NODE) {
+ shadowRoot = this._matchShadowHostPseudoClass(leaf, this.#node);
+ } else if (filterLeaves.length && this.#node.nodeType === ELEMENT_NODE) {
+ shadowRoot = this._matchShadowHostPseudoClass(
+ leaf,
+ this.#node.shadowRoot
+ );
+ }
+ if (shadowRoot) {
+ let bool = true;
+ const l = filterLeaves.length;
+ for (let i = 0; i < l; i++) {
+ const filterLeaf = filterLeaves[i];
+ switch (filterLeaf.name) {
+ case 'host':
+ case 'host-context': {
+ const matchedNode = this._matchShadowHostPseudoClass(
+ filterLeaf,
+ shadowRoot
+ );
+ bool = matchedNode === shadowRoot;
+ break;
+ }
+ case 'has': {
+ bool = this._matchPseudoClassSelector(
+ filterLeaf,
+ shadowRoot,
+ {}
+ ).has(shadowRoot);
+ break;
+ }
+ default: {
+ bool = false;
+ }
+ }
+ if (!bool) {
+ break;
+ }
+ }
+ if (bool) {
+ nodes.push(shadowRoot);
+ filtered = true;
+ }
+ }
+ } else if (targetType === TARGET_SELF) {
+ [nodes, filtered] = this._matchSelf(leaves);
+ } else if (targetType === TARGET_LINEAL) {
+ [nodes, filtered] = this._findLineal(leaves, { complex });
+ } else if (targetType === TARGET_FIRST) {
+ nodes = this._findNodeWalker(leaves, this.#node, { precede, targetType });
+ filtered = nodes.length > 0;
+ } else {
+ pending = true;
+ }
+ return { nodes, filtered, pending };
+ };
+
+ /**
+ * Finds entry nodes.
+ * @private
+ * @param {object} twig - The twig object.
+ * @param {string} targetType - The target type.
+ * @param {object} [opt] - Options.
+ * @param {boolean} [opt.complex] - If true, the selector is complex.
+ * @param {string} [opt.dir] - The find direction.
+ * @returns {object} An object with nodes and their state.
+ */
+ _findEntryNodes = (twig, targetType, opt = {}) => {
+ const { leaves } = twig;
+ const [leaf, ...filterLeaves] = leaves;
+ const { complex = false, dir = DIR_PREV } = opt;
+ const precede =
+ dir === DIR_NEXT &&
+ this.#node.nodeType === ELEMENT_NODE &&
+ this.#node !== this.#root;
+ let result;
+ switch (leaf.type) {
+ case PS_ELEMENT_SELECTOR: {
+ result = this._findEntryNodesForPseudoElement(
+ leaf,
+ filterLeaves,
+ targetType
+ );
+ break;
+ }
+ case ID_SELECTOR: {
+ result = this._findEntryNodesForId(twig, targetType, {
+ complex,
+ precede
+ });
+ break;
+ }
+ case CLASS_SELECTOR: {
+ result = this._findEntryNodesForClass(leaves, targetType, {
+ complex,
+ precede
+ });
+ break;
+ }
+ case TYPE_SELECTOR: {
+ result = this._findEntryNodesForType(leaves, targetType, {
+ complex,
+ precede
+ });
+ break;
+ }
+ default: {
+ result = this._findEntryNodesForOther(twig, targetType, {
+ complex,
+ precede
+ });
+ }
+ }
+ return {
+ compound: filterLeaves.length > 0,
+ filtered: result.filtered,
+ nodes: result.nodes,
+ pending: result.pending
+ };
+ };
+
+ /**
+ * Determines the direction and starting twig for a selector branch.
+ * @private
+ * @param {Array.<object>} branch - The AST branch.
+ * @param {string} targetType - The type of target to find.
+ * @returns {object} An object with the direction and starting twig.
+ */
+ _determineTraversalStrategy = (branch, targetType) => {
+ const branchLen = branch.length;
+ const firstTwig = branch[0];
+ const lastTwig = branch[branchLen - 1];
+ if (branchLen === 1) {
+ return { dir: DIR_PREV, twig: firstTwig };
+ }
+ // Complex selector (branchLen > 1).
+ const {
+ leaves: [{ name: firstName, type: firstType }]
+ } = firstTwig;
+ const {
+ leaves: [{ name: lastName, type: lastType }]
+ } = lastTwig;
+ const { combo: firstCombo } = firstTwig;
+ if (
+ this.#selector.includes(':scope') ||
+ lastType === PS_ELEMENT_SELECTOR ||
+ lastType === ID_SELECTOR
+ ) {
+ return { dir: DIR_PREV, twig: lastTwig };
+ }
+ if (firstType === ID_SELECTOR) {
+ return { dir: DIR_NEXT, twig: firstTwig };
+ }
+ if (firstName === '*' && firstType === TYPE_SELECTOR) {
+ return { dir: DIR_PREV, twig: lastTwig };
+ }
+ if (lastName === '*' && lastType === TYPE_SELECTOR) {
+ return { dir: DIR_NEXT, twig: firstTwig };
+ }
+ if (branchLen === 2) {
+ if (targetType === TARGET_FIRST) {
+ return { dir: DIR_PREV, twig: lastTwig };
+ }
+ const { name: comboName } = firstCombo;
+ if (comboName === '+' || comboName === '~') {
+ return { dir: DIR_PREV, twig: lastTwig };
+ }
+ } else if (branchLen > 2 && this.#scoped && targetType === TARGET_FIRST) {
+ if (lastType === TYPE_SELECTOR) {
+ return { dir: DIR_PREV, twig: lastTwig };
+ }
+ let isChildOrDescendant = false;
+ for (const { combo } of branch) {
+ if (combo) {
+ const { name: comboName } = combo;
+ isChildOrDescendant = comboName === '>' || comboName === ' ';
+ if (!isChildOrDescendant) {
+ break;
+ }
+ }
+ }
+ if (isChildOrDescendant) {
+ return { dir: DIR_PREV, twig: lastTwig };
+ }
+ }
+ // Default strategy for complex selectors.
+ return { dir: DIR_NEXT, twig: firstTwig };
+ };
+
+ /**
+ * Processes pending items not resolved with a direct strategy.
+ * @private
+ * @param {Set.<Map>} pendingItems - The set of pending items.
+ */
+ _processPendingItems = pendingItems => {
+ if (!pendingItems.size) {
+ return;
+ }
+ if (!this.#rootWalker) {
+ this.#rootWalker = this._createTreeWalker(this.#root);
+ }
+ const walker = this.#rootWalker;
+ let node = this.#root;
+ if (this.#scoped) {
+ node = this.#node;
+ }
+ let nextNode = traverseNode(node, walker);
+ while (nextNode) {
+ const isWithinScope =
+ this.#node.nodeType !== ELEMENT_NODE ||
+ nextNode === this.#node ||
+ this.#node.contains(nextNode);
+ if (isWithinScope) {
+ for (const pendingItem of pendingItems) {
+ const { leaves } = pendingItem.get('twig');
+ if (this._matchLeaves(leaves, nextNode, { warn: this.#warn })) {
+ const index = pendingItem.get('index');
+ this.#ast[index].filtered = true;
+ this.#ast[index].find = true;
+ this.#nodes[index].push(nextNode);
+ }
+ }
+ } else if (this.#scoped) {
+ break;
+ }
+ nextNode = walker.nextNode();
+ }
+ };
+
+ /**
+ * Collects nodes.
+ * @private
+ * @param {string} targetType - The target type.
+ * @returns {Array.<Array.<object>>} An array containing the AST and nodes.
+ */
+ _collectNodes = targetType => {
+ const ast = this.#ast.values();
+ if (targetType === TARGET_ALL || targetType === TARGET_FIRST) {
+ const pendingItems = new Set();
+ let i = 0;
+ for (const { branch } of ast) {
+ const complex = branch.length > 1;
+ const { dir, twig } = this._determineTraversalStrategy(
+ branch,
+ targetType
+ );
+ const { compound, filtered, nodes, pending } = this._findEntryNodes(
+ twig,
+ targetType,
+ { complex, dir }
+ );
+ if (nodes.length) {
+ this.#ast[i].find = true;
+ this.#nodes[i] = nodes;
+ } else if (pending) {
+ pendingItems.add(
+ new Map([
+ ['index', i],
+ ['twig', twig]
+ ])
+ );
+ }
+ this.#ast[i].dir = dir;
+ this.#ast[i].filtered = filtered || !compound;
+ i++;
+ }
+ this._processPendingItems(pendingItems);
+ } else {
+ let i = 0;
+ for (const { branch } of ast) {
+ const twig = branch[branch.length - 1];
+ const complex = branch.length > 1;
+ const dir = DIR_PREV;
+ const { compound, filtered, nodes } = this._findEntryNodes(
+ twig,
+ targetType,
+ { complex, dir }
+ );
+ if (nodes.length) {
+ this.#ast[i].find = true;
+ this.#nodes[i] = nodes;
+ }
+ this.#ast[i].dir = dir;
+ this.#ast[i].filtered = filtered || !compound;
+ i++;
+ }
+ }
+ return [this.#ast, this.#nodes];
+ };
+
+ /**
+ * Gets combined nodes.
+ * @private
+ * @param {object} twig - The twig object.
+ * @param {object} nodes - A collection of nodes.
+ * @param {string} dir - The direction.
+ * @returns {Array.<object>} A collection of matched nodes.
+ */
+ _getCombinedNodes = (twig, nodes, dir) => {
+ const arr = [];
+ const options = {
+ dir,
+ warn: this.#warn
+ };
+ for (const node of nodes) {
+ const matched = this._matchCombinator(twig, node, options);
+ if (matched.size) {
+ arr.push(...matched);
+ }
+ }
+ return arr;
+ };
+
+ /**
+ * Matches a node in the 'next' direction.
+ * @private
+ * @param {Array} branch - The branch.
+ * @param {Set.<object>} nodes - A collection of Element nodes.
+ * @param {object} opt - Options.
+ * @param {object} opt.combo - The combo object.
+ * @param {number} opt.index - The index.
+ * @returns {?object} The matched node.
+ */
+ _matchNodeNext = (branch, nodes, opt) => {
+ const { combo, index } = opt;
+ const { combo: nextCombo, leaves } = branch[index];
+ const twig = {
+ combo,
+ leaves
+ };
+ const nextNodes = new Set(this._getCombinedNodes(twig, nodes, DIR_NEXT));
+ if (nextNodes.size) {
+ if (index === branch.length - 1) {
+ const [nextNode] = sortNodes(nextNodes);
+ return nextNode;
+ }
+ return this._matchNodeNext(branch, nextNodes, {
+ combo: nextCombo,
+ index: index + 1
+ });
+ }
+ return null;
+ };
+
+ /**
+ * Matches a node in the 'previous' direction.
+ * @private
+ * @param {Array} branch - The branch.
+ * @param {object} node - The Element node.
+ * @param {object} opt - Options.
+ * @param {number} opt.index - The index.
+ * @returns {?object} The node.
+ */
+ _matchNodePrev = (branch, node, opt) => {
+ const { index } = opt;
+ const twig = branch[index];
+ const nodes = new Set([node]);
+ const nextNodes = new Set(this._getCombinedNodes(twig, nodes, DIR_PREV));
+ if (nextNodes.size) {
+ if (index === 0) {
+ return node;
+ }
+ let matched;
+ for (const nextNode of nextNodes) {
+ matched = this._matchNodePrev(branch, nextNode, {
+ index: index - 1
+ });
+ if (matched) {
+ break;
+ }
+ }
+ if (matched) {
+ return node;
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Processes a complex selector branch to find all matching nodes.
+ * @private
+ * @param {Array} branch - The selector branch from the AST.
+ * @param {Array} entryNodes - The initial set of nodes to start from.
+ * @param {string} dir - The direction of traversal ('next' or 'prev').
+ * @returns {Set.<object>} A set of all matched nodes.
+ */
+ _processComplexBranchAll = (branch, entryNodes, dir) => {
+ const matchedNodes = new Set();
+ const branchLen = branch.length;
+ const lastIndex = branchLen - 1;
+
+ if (dir === DIR_NEXT) {
+ const { combo: firstCombo } = branch[0];
+ for (const node of entryNodes) {
+ let combo = firstCombo;
+ let nextNodes = new Set([node]);
+ for (let j = 1; j < branchLen; j++) {
+ const { combo: nextCombo, leaves } = branch[j];
+ const twig = { combo, leaves };
+ const nodesArr = this._getCombinedNodes(twig, nextNodes, dir);
+ if (nodesArr.length) {
+ if (j === lastIndex) {
+ for (const nextNode of nodesArr) {
+ matchedNodes.add(nextNode);
+ }
+ }
+ combo = nextCombo;
+ nextNodes = new Set(nodesArr);
+ } else {
+ // No further matches down this path.
+ nextNodes.clear();
+ break;
+ }
+ }
+ }
+ // DIR_PREV
+ } else {
+ for (const node of entryNodes) {
+ let nextNodes = new Set([node]);
+ for (let j = lastIndex - 1; j >= 0; j--) {
+ const twig = branch[j];
+ const nodesArr = this._getCombinedNodes(twig, nextNodes, dir);
+ if (nodesArr.length) {
+ // The entry node is the final match
+ if (j === 0) {
+ matchedNodes.add(node);
+ }
+ nextNodes = new Set(nodesArr);
+ } else {
+ // No further matches down this path.
+ nextNodes.clear();
+ break;
+ }
+ }
+ }
+ }
+ return matchedNodes;
+ };
+
+ /**
+ * Processes a complex selector branch to find the first matching node.
+ * @private
+ * @param {Array} branch - The selector branch from the AST.
+ * @param {Array} entryNodes - The initial set of nodes to start from.
+ * @param {string} dir - The direction of traversal ('next' or 'prev').
+ * @param {string} targetType - The type of search (e.g., 'first').
+ * @returns {?object} The first matched node, or null.
+ */
+ _processComplexBranchFirst = (branch, entryNodes, dir, targetType) => {
+ const branchLen = branch.length;
+ const lastIndex = branchLen - 1;
+ // DIR_NEXT logic for finding the first match.
+ if (dir === DIR_NEXT) {
+ const { combo: entryCombo } = branch[0];
+ for (const node of entryNodes) {
+ const matchedNode = this._matchNodeNext(branch, new Set([node]), {
+ combo: entryCombo,
+ index: 1
+ });
+ if (matchedNode) {
+ if (this.#node.nodeType === ELEMENT_NODE) {
+ if (
+ matchedNode !== this.#node &&
+ this.#node.contains(matchedNode)
+ ) {
+ return matchedNode;
+ }
+ } else {
+ return matchedNode;
+ }
+ }
+ }
+ // Fallback logic if no direct match found.
+ const { leaves: entryLeaves } = branch[0];
+ const [entryNode] = entryNodes;
+ if (this.#node.contains(entryNode)) {
+ let [refNode] = this._findNodeWalker(entryLeaves, entryNode, {
+ targetType
+ });
+ while (refNode) {
+ const matchedNode = this._matchNodeNext(branch, new Set([refNode]), {
+ combo: entryCombo,
+ index: 1
+ });
+ if (matchedNode) {
+ if (this.#node.nodeType === ELEMENT_NODE) {
+ if (
+ matchedNode !== this.#node &&
+ this.#node.contains(matchedNode)
+ ) {
+ return matchedNode;
+ }
+ } else {
+ return matchedNode;
+ }
+ }
+ [refNode] = this._findNodeWalker(entryLeaves, refNode, {
+ targetType,
+ force: true
+ });
+ }
+ }
+ // DIR_PREV logic for finding the first match.
+ } else {
+ for (const node of entryNodes) {
+ const matchedNode = this._matchNodePrev(branch, node, {
+ index: lastIndex - 1
+ });
+ if (matchedNode) {
+ return matchedNode;
+ }
+ }
+ // Fallback for TARGET_FIRST.
+ if (targetType === TARGET_FIRST) {
+ const { leaves: entryLeaves } = branch[lastIndex];
+ const [entryNode] = entryNodes;
+ let [refNode] = this._findNodeWalker(entryLeaves, entryNode, {
+ targetType
+ });
+ while (refNode) {
+ const matchedNode = this._matchNodePrev(branch, refNode, {
+ index: lastIndex - 1
+ });
+ if (matchedNode) {
+ return refNode;
+ }
+ [refNode] = this._findNodeWalker(entryLeaves, refNode, {
+ targetType,
+ force: true
+ });
+ }
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Finds matched nodes.
+ * @param {string} targetType - The target type.
+ * @returns {Set.<object>} A collection of matched nodes.
+ */
+ find = targetType => {
+ const [[...branches], collectedNodes] = this._collectNodes(targetType);
+ const l = branches.length;
+ let sort =
+ l > 1 && targetType === TARGET_ALL && this.#selector.includes(':scope');
+ let nodes = new Set();
+ for (let i = 0; i < l; i++) {
+ const { branch, dir, find } = branches[i];
+ if (!branch.length || !find) {
+ continue;
+ }
+ const entryNodes = collectedNodes[i];
+ const lastIndex = branch.length - 1;
+ // Handle simple selectors (no combinators).
+ if (lastIndex === 0) {
+ if (
+ (targetType === TARGET_ALL || targetType === TARGET_FIRST) &&
+ this.#node.nodeType === ELEMENT_NODE
+ ) {
+ for (const node of entryNodes) {
+ if (node !== this.#node && this.#node.contains(node)) {
+ nodes.add(node);
+ if (targetType === TARGET_FIRST) {
+ break;
+ }
+ }
+ }
+ } else if (targetType === TARGET_ALL) {
+ if (nodes.size) {
+ for (const node of entryNodes) {
+ nodes.add(node);
+ }
+ sort = true;
+ } else {
+ nodes = new Set(entryNodes);
+ }
+ } else {
+ if (entryNodes.length) {
+ nodes.add(entryNodes[0]);
+ }
+ }
+ // Handle complex selectors.
+ } else {
+ if (targetType === TARGET_ALL) {
+ const newNodes = this._processComplexBranchAll(
+ branch,
+ entryNodes,
+ dir
+ );
+ if (nodes.size) {
+ for (const newNode of newNodes) {
+ nodes.add(newNode);
+ }
+ sort = true;
+ } else {
+ nodes = newNodes;
+ }
+ } else {
+ const matchedNode = this._processComplexBranchFirst(
+ branch,
+ entryNodes,
+ dir,
+ targetType
+ );
+ if (matchedNode) {
+ nodes.add(matchedNode);
+ }
+ }
+ }
+ }
+ if (this.#check) {
+ const match = !!nodes.size;
+ let pseudoElement;
+ if (this.#pseudoElement.length) {
+ pseudoElement = this.#pseudoElement.join('');
+ } else {
+ pseudoElement = null;
+ }
+ return { match, pseudoElement };
+ }
+ if (targetType === TARGET_FIRST || targetType === TARGET_ALL) {
+ nodes.delete(this.#node);
+ }
+ if ((sort || targetType === TARGET_FIRST) && nodes.size > 1) {
+ return new Set(sortNodes(nodes));
+ }
+ return nodes;
+ };
+}
diff --git a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js
new file mode 100644
index 0000000..6395560
--- /dev/null
+++ b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js
@@ -0,0 +1,587 @@
+/**
+ * matcher.js
+ */
+
+/* import */
+import { generateCSS, parseAstName, unescapeSelector } from './parser.js';
+import {
+ generateException,
+ getDirectionality,
+ getLanguageAttribute,
+ getType,
+ isContentEditable,
+ isCustomElement,
+ isNamespaceDeclared
+} from './utility.js';
+
+/* constants */
+import {
+ ALPHA_NUM,
+ FORM_PARTS,
+ IDENT,
+ INPUT_EDIT,
+ LANG_PART,
+ NOT_SUPPORTED_ERR,
+ PS_ELEMENT_SELECTOR,
+ STRING,
+ SYNTAX_ERR
+} from './constant.js';
+const KEYS_FORM_PS_DISABLED = new Set([
+ ...FORM_PARTS,
+ 'fieldset',
+ 'optgroup',
+ 'option'
+]);
+const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
+const REG_LANG_VALID = new RegExp(`^(?:\\*-)?${ALPHA_NUM}${LANG_PART}$`, 'i');
+const REG_TAG_NAME = /[A-Z][\\w-]*/i;
+
+/**
+ * Validates a pseudo-element selector.
+ * @param {string} astName - The name of the pseudo-element from the AST.
+ * @param {string} astType - The type of the selector from the AST.
+ * @param {object} [opt] - Optional parameters.
+ * @param {boolean} [opt.forgive] - If true, ignores unknown pseudo-elements.
+ * @param {boolean} [opt.warn] - If true, throws an error for unsupported ones.
+ * @throws {DOMException} If the selector is invalid or unsupported.
+ * @returns {void}
+ */
+export const matchPseudoElementSelector = (astName, astType, opt = {}) => {
+ const { forgive, globalObject, warn } = opt;
+ if (astType !== PS_ELEMENT_SELECTOR) {
+ // Ensure the AST node is a pseudo-element selector.
+ throw new TypeError(`Unexpected ast type ${getType(astType)}`);
+ }
+ switch (astName) {
+ case 'after':
+ case 'backdrop':
+ case 'before':
+ case 'cue':
+ case 'cue-region':
+ case 'first-letter':
+ case 'first-line':
+ case 'file-selector-button':
+ case 'marker':
+ case 'placeholder':
+ case 'selection':
+ case 'target-text': {
+ // Warn if the pseudo-element is known but unsupported.
+ if (warn) {
+ throw generateException(
+ `Unsupported pseudo-element ::${astName}`,
+ NOT_SUPPORTED_ERR,
+ globalObject
+ );
+ }
+ break;
+ }
+ case 'part':
+ case 'slotted': {
+ // Warn if the functional pseudo-element is known but unsupported.
+ if (warn) {
+ throw generateException(
+ `Unsupported pseudo-element ::${astName}()`,
+ NOT_SUPPORTED_ERR,
+ globalObject
+ );
+ }
+ break;
+ }
+ default: {
+ // Handle vendor-prefixed or unknown pseudo-elements.
+ if (astName.startsWith('-webkit-')) {
+ if (warn) {
+ throw generateException(
+ `Unsupported pseudo-element ::${astName}`,
+ NOT_SUPPORTED_ERR,
+ globalObject
+ );
+ }
+ // Throw an error for unknown pseudo-elements if not forgiven.
+ } else if (!forgive) {
+ throw generateException(
+ `Unknown pseudo-element ::${astName}`,
+ SYNTAX_ERR,
+ globalObject
+ );
+ }
+ }
+ }
+};
+
+/**
+ * Matches the :dir() pseudo-class against an element's directionality.
+ * @param {object} ast - The AST object for the pseudo-class.
+ * @param {object} node - The element node to match against.
+ * @throws {TypeError} If the AST does not contain a valid direction value.
+ * @returns {boolean} - True if the directionality matches, otherwise false.
+ */
+export const matchDirectionPseudoClass = (ast, node) => {
+ const { name } = ast;
+ // The :dir() pseudo-class requires a direction argument (e.g., "ltr").
+ if (!name) {
+ const type = name === '' ? '(empty String)' : getType(name);
+ throw new TypeError(`Unexpected ast type ${type}`);
+ }
+ // Get the computed directionality of the element.
+ const dir = getDirectionality(node);
+ // Compare the expected direction with the element's actual direction.
+ return name === dir;
+};
+
+/**
+ * Matches the :lang() pseudo-class against an element's language.
+ * @see https://datatracker.ietf.org/doc/html/rfc4647#section-3.3.1
+ * @param {object} ast - The AST object for the pseudo-class.
+ * @param {object} node - The element node to match against.
+ * @returns {boolean} - True if the language matches, otherwise false.
+ */
+export const matchLanguagePseudoClass = (ast, node) => {
+ const { name, type, value } = ast;
+ let langPattern;
+ // Determine the language pattern from the AST.
+ if (type === STRING && value) {
+ langPattern = value;
+ } else if (type === IDENT && name) {
+ langPattern = unescapeSelector(name);
+ }
+ // If no valid language pattern is provided, it cannot match.
+ if (typeof langPattern !== 'string') {
+ return false;
+ }
+ // Get the effective language attribute for the current node.
+ const elementLang = getLanguageAttribute(node);
+ // If the element has no language, it cannot match a specific pattern.
+ if (elementLang === null) {
+ return false;
+ }
+ // Handle the universal selector '*' for :lang.
+ if (langPattern === '*') {
+ // It matches any language unless attribute is not empty.
+ return elementLang !== '';
+ }
+ // Validate the provided language pattern structure.
+ if (!REG_LANG_VALID.test(langPattern)) {
+ return false;
+ }
+ // Build a regex for extended language range matching.
+ let matcherRegex;
+ if (langPattern.indexOf('-') > -1) {
+ // Handle complex patterns with wildcards and sub-tags (e.g., '*-US').
+ const [langMain, langSub, ...langRest] = langPattern.split('-');
+ const extendedMain =
+ langMain === '*' ? `${ALPHA_NUM}${LANG_PART}` : `${langMain}${LANG_PART}`;
+ const extendedSub = `-${langSub}${LANG_PART}`;
+ let extendedRest = '';
+ // Use a standard for loop for performance as per the rules.
+ for (let i = 0; i < langRest.length; i++) {
+ extendedRest += `-${langRest[i]}${LANG_PART}`;
+ }
+ matcherRegex = new RegExp(
+ `^${extendedMain}${extendedSub}${extendedRest}$`,
+ 'i'
+ );
+ } else {
+ // Handle simple language patterns (e.g., 'en').
+ matcherRegex = new RegExp(`^${langPattern}${LANG_PART}$`, 'i');
+ }
+ // Test the element's language against the constructed regex.
+ return matcherRegex.test(elementLang);
+};
+
+/**
+ * Matches the :disabled and :enabled pseudo-classes.
+ * @param {string} astName - pseudo-class name
+ * @param {object} node - Element node
+ * @returns {boolean} - True if matched
+ */
+export const matchDisabledPseudoClass = (astName, node) => {
+ const { localName, parentNode } = node;
+ if (
+ !KEYS_FORM_PS_DISABLED.has(localName) &&
+ !isCustomElement(node, { formAssociated: true })
+ ) {
+ return false;
+ }
+ let isDisabled = false;
+ if (node.disabled || node.hasAttribute('disabled')) {
+ isDisabled = true;
+ } else if (localName === 'option') {
+ if (
+ parentNode &&
+ parentNode.localName === 'optgroup' &&
+ (parentNode.disabled || parentNode.hasAttribute('disabled'))
+ ) {
+ isDisabled = true;
+ }
+ } else if (localName !== 'optgroup') {
+ let current = parentNode;
+ while (current) {
+ if (
+ current.localName === 'fieldset' &&
+ (current.disabled || current.hasAttribute('disabled'))
+ ) {
+ // The first <legend> in a disabled <fieldset> is not disabled.
+ let legend;
+ let element = current.firstElementChild;
+ while (element) {
+ if (element.localName === 'legend') {
+ legend = element;
+ break;
+ }
+ element = element.nextElementSibling;
+ }
+ if (!legend || !legend.contains(node)) {
+ isDisabled = true;
+ }
+ // Found the containing fieldset, stop searching up.
+ break;
+ }
+ current = current.parentNode;
+ }
+ }
+ if (astName === 'disabled') {
+ return isDisabled;
+ }
+ return !isDisabled;
+};
+
+/**
+ * Match the :read-only and :read-write pseudo-classes
+ * @param {string} astName - pseudo-class name
+ * @param {object} node - Element node
+ * @returns {boolean} - True if matched
+ */
+export const matchReadOnlyPseudoClass = (astName, node) => {
+ const { localName } = node;
+ let isReadOnly = false;
+ switch (localName) {
+ case 'textarea':
+ case 'input': {
+ const isEditableInput = !node.type || KEYS_INPUT_EDIT.has(node.type);
+ if (localName === 'textarea' || isEditableInput) {
+ isReadOnly =
+ node.readOnly ||
+ node.hasAttribute('readonly') ||
+ node.disabled ||
+ node.hasAttribute('disabled');
+ } else {
+ // Non-editable input types are always read-only
+ isReadOnly = true;
+ }
+ break;
+ }
+ default: {
+ isReadOnly = !isContentEditable(node);
+ }
+ }
+ if (astName === 'read-only') {
+ return isReadOnly;
+ }
+ return !isReadOnly;
+};
+
+/**
+ * Matches an attribute selector against an element.
+ * This function handles various attribute matchers like '=', '~=', '^=', etc.,
+ * and considers namespaces and case sensitivity based on document type.
+ * @param {object} ast - The AST for the attribute selector.
+ * @param {object} node - The element node to match against.
+ * @param {object} [opt] - Optional parameters.
+ * @param {boolean} [opt.check] - True if running in an internal check.
+ * @param {boolean} [opt.forgive] - True to forgive certain syntax errors.
+ * @returns {boolean} - True if the attribute selector matches, otherwise false.
+ */
+export const matchAttributeSelector = (ast, node, opt = {}) => {
+ const {
+ flags: astFlags,
+ matcher: astMatcher,
+ name: astName,
+ value: astValue
+ } = ast;
+ const { check, forgive, globalObject } = opt;
+ // Validate selector flags ('i' or 's').
+ if (typeof astFlags === 'string' && !/^[is]$/i.test(astFlags) && !forgive) {
+ const css = generateCSS(ast);
+ throw generateException(
+ `Invalid selector ${css}`,
+ SYNTAX_ERR,
+ globalObject
+ );
+ }
+ const { attributes } = node;
+ // An element with no attributes cannot match.
+ if (!attributes || !attributes.length) {
+ return false;
+ }
+ // Determine case sensitivity based on document type and flags.
+ const contentType = node.ownerDocument.contentType;
+ let caseInsensitive;
+ if (contentType === 'text/html') {
+ if (typeof astFlags === 'string' && /^s$/i.test(astFlags)) {
+ caseInsensitive = false;
+ } else {
+ caseInsensitive = true;
+ }
+ } else if (typeof astFlags === 'string' && /^i$/i.test(astFlags)) {
+ caseInsensitive = true;
+ } else {
+ caseInsensitive = false;
+ }
+ // Prepare the attribute name from the selector for matching.
+ let astAttrName = unescapeSelector(astName.name);
+ if (caseInsensitive) {
+ astAttrName = astAttrName.toLowerCase();
+ }
+ // A set to store the values of attributes whose names match.
+ const attrValues = new Set();
+ // Handle namespaced attribute names (e.g., [*|attr], [ns|attr]).
+ if (astAttrName.indexOf('|') > -1) {
+ const { prefix: astPrefix, localName: astLocalName } =
+ parseAstName(astAttrName);
+ for (const item of attributes) {
+ let { name: itemName, value: itemValue } = item;
+ if (caseInsensitive) {
+ itemName = itemName.toLowerCase();
+ itemValue = itemValue.toLowerCase();
+ }
+ switch (astPrefix) {
+ case '': {
+ if (astLocalName === itemName) {
+ attrValues.add(itemValue);
+ }
+ break;
+ }
+ case '*': {
+ if (itemName.indexOf(':') > -1) {
+ const [, ...restItemName] = itemName.split(':');
+ const itemLocalName = restItemName.join(':').replace(/^:/, '');
+ if (itemLocalName === astLocalName) {
+ attrValues.add(itemValue);
+ }
+ } else if (astLocalName === itemName) {
+ attrValues.add(itemValue);
+ }
+ break;
+ }
+ default: {
+ if (!check) {
+ if (forgive) {
+ return false;
+ }
+ const css = generateCSS(ast);
+ throw generateException(
+ `Invalid selector ${css}`,
+ SYNTAX_ERR,
+ globalObject
+ );
+ }
+ if (itemName.indexOf(':') > -1) {
+ const [itemPrefix, ...restItemName] = itemName.split(':');
+ const itemLocalName = restItemName.join(':').replace(/^:/, '');
+ // Ignore the 'xml:lang' attribute.
+ if (itemPrefix === 'xml' && itemLocalName === 'lang') {
+ continue;
+ } else if (
+ astPrefix === itemPrefix &&
+ astLocalName === itemLocalName
+ ) {
+ const namespaceDeclared = isNamespaceDeclared(astPrefix, node);
+ if (namespaceDeclared) {
+ attrValues.add(itemValue);
+ }
+ }
+ }
+ }
+ }
+ }
+ // Handle non-namespaced attribute names.
+ } else {
+ for (let { name: itemName, value: itemValue } of attributes) {
+ if (caseInsensitive) {
+ itemName = itemName.toLowerCase();
+ itemValue = itemValue.toLowerCase();
+ }
+ if (itemName.indexOf(':') > -1) {
+ const [itemPrefix, ...restItemName] = itemName.split(':');
+ const itemLocalName = restItemName.join(':').replace(/^:/, '');
+ // The attribute is starting with ':'.
+ if (!itemPrefix && astAttrName === `:${itemLocalName}`) {
+ attrValues.add(itemValue);
+ // Ignore the 'xml:lang' attribute.
+ } else if (itemPrefix === 'xml' && itemLocalName === 'lang') {
+ continue;
+ } else if (astAttrName === itemLocalName) {
+ attrValues.add(itemValue);
+ }
+ } else if (astAttrName === itemName) {
+ attrValues.add(itemValue);
+ }
+ }
+ }
+ if (!attrValues.size) {
+ return false;
+ }
+ // Prepare the value from the selector's RHS for comparison.
+ const { name: astIdentValue, value: astStringValue } = astValue ?? {};
+ let attrValue;
+ if (astIdentValue) {
+ if (caseInsensitive) {
+ attrValue = astIdentValue.toLowerCase();
+ } else {
+ attrValue = astIdentValue;
+ }
+ } else if (astStringValue) {
+ if (caseInsensitive) {
+ attrValue = astStringValue.toLowerCase();
+ } else {
+ attrValue = astStringValue;
+ }
+ } else if (astStringValue === '') {
+ attrValue = astStringValue;
+ }
+ // Perform the final match based on the specified matcher.
+ switch (astMatcher) {
+ case '=': {
+ return typeof attrValue === 'string' && attrValues.has(attrValue);
+ }
+ case '~=': {
+ if (attrValue && typeof attrValue === 'string') {
+ for (const value of attrValues) {
+ const item = new Set(value.split(/\s+/));
+ if (item.has(attrValue)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ case '|=': {
+ if (attrValue && typeof attrValue === 'string') {
+ for (const value of attrValues) {
+ if (value === attrValue || value.startsWith(`${attrValue}-`)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ case '^=': {
+ if (attrValue && typeof attrValue === 'string') {
+ for (const value of attrValues) {
+ if (value.startsWith(`${attrValue}`)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ case '$=': {
+ if (attrValue && typeof attrValue === 'string') {
+ for (const value of attrValues) {
+ if (value.endsWith(`${attrValue}`)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ case '*=': {
+ if (attrValue && typeof attrValue === 'string') {
+ for (const value of attrValues) {
+ if (value.includes(`${attrValue}`)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ case null:
+ default: {
+ // This case handles attribute existence checks (e.g., '[disabled]').
+ return true;
+ }
+ }
+};
+
+/**
+ * match type selector
+ * @param {object} ast - AST
+ * @param {object} node - Element node
+ * @param {object} [opt] - options
+ * @param {boolean} [opt.check] - running in internal check()
+ * @param {boolean} [opt.forgive] - forgive undeclared namespace
+ * @returns {boolean} - result
+ */
+export const matchTypeSelector = (ast, node, opt = {}) => {
+ const astName = unescapeSelector(ast.name);
+ const { localName, namespaceURI, prefix } = node;
+ const { check, forgive, globalObject } = opt;
+ let { prefix: astPrefix, localName: astLocalName } = parseAstName(
+ astName,
+ node
+ );
+ if (
+ node.ownerDocument.contentType === 'text/html' &&
+ (!namespaceURI || namespaceURI === 'http://www.w3.org/1999/xhtml') &&
+ REG_TAG_NAME.test(localName)
+ ) {
+ astPrefix = astPrefix.toLowerCase();
+ astLocalName = astLocalName.toLowerCase();
+ }
+ let nodePrefix;
+ let nodeLocalName;
+ // just in case that the namespaced content is parsed as text/html
+ if (localName.indexOf(':') > -1) {
+ [nodePrefix, nodeLocalName] = localName.split(':');
+ } else {
+ nodePrefix = prefix || '';
+ nodeLocalName = localName;
+ }
+ switch (astPrefix) {
+ case '': {
+ if (
+ !nodePrefix &&
+ !namespaceURI &&
+ (astLocalName === '*' || astLocalName === nodeLocalName)
+ ) {
+ return true;
+ }
+ return false;
+ }
+ case '*': {
+ if (astLocalName === '*' || astLocalName === nodeLocalName) {
+ return true;
+ }
+ return false;
+ }
+ default: {
+ if (!check) {
+ if (forgive) {
+ return false;
+ }
+ const css = generateCSS(ast);
+ throw generateException(
+ `Invalid selector ${css}`,
+ SYNTAX_ERR,
+ globalObject
+ );
+ }
+ const astNS = node.lookupNamespaceURI(astPrefix);
+ const nodeNS = node.lookupNamespaceURI(nodePrefix);
+ if (astNS === nodeNS && astPrefix === nodePrefix) {
+ if (astLocalName === '*' || astLocalName === nodeLocalName) {
+ return true;
+ }
+ return false;
+ } else if (!forgive && !astNS) {
+ throw generateException(
+ `Undeclared namespace ${astPrefix}`,
+ SYNTAX_ERR,
+ globalObject
+ );
+ }
+ return false;
+ }
+ }
+};
diff --git a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/parser.js b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/parser.js
new file mode 100644
index 0000000..bf06d9f
--- /dev/null
+++ b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/parser.js
@@ -0,0 +1,431 @@
+/**
+ * parser.js
+ */
+
+/* import */
+import * as cssTree from 'css-tree';
+import { getType } from './utility.js';
+
+/* constants */
+import {
+ ATTR_SELECTOR,
+ BIT_01,
+ BIT_02,
+ BIT_04,
+ BIT_08,
+ BIT_16,
+ BIT_32,
+ BIT_FFFF,
+ CLASS_SELECTOR,
+ DUO,
+ HEX,
+ ID_SELECTOR,
+ KEYS_LOGICAL,
+ NTH,
+ PS_CLASS_SELECTOR,
+ PS_ELEMENT_SELECTOR,
+ SELECTOR,
+ SYNTAX_ERR,
+ TYPE_SELECTOR
+} from './constant.js';
+const AST_SORT_ORDER = new Map([
+ [PS_ELEMENT_SELECTOR, BIT_01],
+ [ID_SELECTOR, BIT_02],
+ [CLASS_SELECTOR, BIT_04],
+ [TYPE_SELECTOR, BIT_08],
+ [ATTR_SELECTOR, BIT_16],
+ [PS_CLASS_SELECTOR, BIT_32]
+]);
+const KEYS_PS_CLASS_STATE = new Set([
+ 'checked',
+ 'closed',
+ 'disabled',
+ 'empty',
+ 'enabled',
+ 'in-range',
+ 'indeterminate',
+ 'invalid',
+ 'open',
+ 'out-of-range',
+ 'placeholder-shown',
+ 'read-only',
+ 'read-write',
+ 'valid'
+]);
+const KEYS_SHADOW_HOST = new Set(['host', 'host-context']);
+const REG_EMPTY_PS_FUNC =
+ /(?<=:(?:dir|has|host(?:-context)?|is|lang|not|nth-(?:last-)?(?:child|of-type)|where))\(\s+\)/g;
+const REG_SHADOW_PS_ELEMENT = /^part|slotted$/;
+const U_FFFD = '\uFFFD';
+
+/**
+ * Unescapes a CSS selector string.
+ * @param {string} selector - The CSS selector to unescape.
+ * @returns {string} The unescaped selector string.
+ */
+export const unescapeSelector = (selector = '') => {
+ if (typeof selector === 'string' && selector.indexOf('\\', 0) >= 0) {
+ const arr = selector.split('\\');
+ const selectorItems = [arr[0]];
+ const l = arr.length;
+ for (let i = 1; i < l; i++) {
+ const item = arr[i];
+ if (item === '' && i === l - 1) {
+ selectorItems.push(U_FFFD);
+ } else {
+ const hexExists = /^([\da-f]{1,6}\s?)/i.exec(item);
+ if (hexExists) {
+ const [, hex] = hexExists;
+ let str;
+ try {
+ const low = parseInt('D800', HEX);
+ const high = parseInt('DFFF', HEX);
+ const deci = parseInt(hex, HEX);
+ if (deci === 0 || (deci >= low && deci <= high)) {
+ str = U_FFFD;
+ } else {
+ str = String.fromCodePoint(deci);
+ }
+ } catch (e) {
+ str = U_FFFD;
+ }
+ let postStr = '';
+ if (item.length > hex.length) {
+ postStr = item.substring(hex.length);
+ }
+ selectorItems.push(`${str}${postStr}`);
+ // whitespace
+ } else if (/^[\n\r\f]/.test(item)) {
+ selectorItems.push(`\\${item}`);
+ } else {
+ selectorItems.push(item);
+ }
+ }
+ }
+ return selectorItems.join('');
+ }
+ return selector;
+};
+
+/**
+ * Preprocesses a selector string according to the specification.
+ * @see https://drafts.csswg.org/css-syntax-3/#input-preprocessing
+ * @param {string} value - The value to preprocess.
+ * @returns {string} The preprocessed selector string.
+ */
+export const preprocess = value => {
+ // Non-string values will be converted to string.
+ if (typeof value !== 'string') {
+ if (value === undefined || value === null) {
+ return getType(value).toLowerCase();
+ } else if (Array.isArray(value)) {
+ return value.join(',');
+ } else if (Object.hasOwn(value, 'toString')) {
+ return value.toString();
+ } else {
+ throw new DOMException(`Invalid selector ${value}`, SYNTAX_ERR);
+ }
+ }
+ let selector = value;
+ let index = 0;
+ while (index >= 0) {
+ // @see https://drafts.csswg.org/selectors/#id-selectors
+ index = selector.indexOf('#', index);
+ if (index < 0) {
+ break;
+ }
+ const preHash = selector.substring(0, index + 1);
+ let postHash = selector.substring(index + 1);
+ const codePoint = postHash.codePointAt(0);
+ if (codePoint > BIT_FFFF) {
+ const str = `\\${codePoint.toString(HEX)} `;
+ if (postHash.length === DUO) {
+ postHash = str;
+ } else {
+ postHash = `${str}${postHash.substring(DUO)}`;
+ }
+ }
+ selector = `${preHash}${postHash}`;
+ index++;
+ }
+ return selector
+ .replace(/\f|\r\n?/g, '\n')
+ .replace(/[\0\uD800-\uDFFF]|\\$/g, U_FFFD)
+ .replace(/\x26/g, ':scope');
+};
+
+/**
+ * Creates an Abstract Syntax Tree (AST) from a CSS selector string.
+ * @param {string} sel - The CSS selector string.
+ * @returns {object} The parsed AST object.
+ */
+export const parseSelector = sel => {
+ const selector = preprocess(sel);
+ // invalid selectors
+ if (/^$|^\s*>|,\s*$/.test(selector)) {
+ throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
+ }
+ try {
+ const ast = cssTree.parse(selector, {
+ context: 'selectorList',
+ parseCustomProperty: true
+ });
+ return cssTree.toPlainObject(ast);
+ } catch (e) {
+ const { message } = e;
+ if (
+ /^(?:"\]"|Attribute selector [()\s,=~^$*|]+) is expected$/.test(
+ message
+ ) &&
+ !selector.endsWith(']')
+ ) {
+ const index = selector.lastIndexOf('[');
+ const selPart = selector.substring(index);
+ if (selPart.includes('"')) {
+ const quotes = selPart.match(/"/g).length;
+ if (quotes % 2) {
+ return parseSelector(`${selector}"]`);
+ }
+ return parseSelector(`${selector}]`);
+ }
+ return parseSelector(`${selector}]`);
+ } else if (message === '")" is expected') {
+ // workaround for https://github.com/csstree/csstree/issues/283
+ if (REG_EMPTY_PS_FUNC.test(selector)) {
+ return parseSelector(`${selector.replaceAll(REG_EMPTY_PS_FUNC, '()')}`);
+ } else if (!selector.endsWith(')')) {
+ return parseSelector(`${selector})`);
+ } else {
+ throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
+ }
+ } else {
+ throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
+ }
+ }
+};
+
+/**
+ * Walks the provided AST to collect selector branches and gather information
+ * about its contents.
+ * @param {object} ast - The AST to traverse.
+ * @returns {{branches: Array<object>, info: object}} An object containing the selector branches and info.
+ */
+export const walkAST = (ast = {}) => {
+ const branches = new Set();
+ const info = {
+ hasForgivenPseudoFunc: false,
+ hasHasPseudoFunc: false,
+ hasLogicalPseudoFunc: false,
+ hasNotPseudoFunc: false,
+ hasNthChildOfSelector: false,
+ hasNestedSelector: false,
+ hasStatePseudoClass: false
+ };
+ const opt = {
+ enter(node) {
+ switch (node.type) {
+ case CLASS_SELECTOR: {
+ if (/^-?\d/.test(node.name)) {
+ throw new DOMException(
+ `Invalid selector .${node.name}`,
+ SYNTAX_ERR
+ );
+ }
+ break;
+ }
+ case ID_SELECTOR: {
+ if (/^-?\d/.test(node.name)) {
+ throw new DOMException(
+ `Invalid selector #${node.name}`,
+ SYNTAX_ERR
+ );
+ }
+ break;
+ }
+ case PS_CLASS_SELECTOR: {
+ if (KEYS_LOGICAL.has(node.name)) {
+ info.hasNestedSelector = true;
+ info.hasLogicalPseudoFunc = true;
+ if (node.name === 'has') {
+ info.hasHasPseudoFunc = true;
+ } else if (node.name === 'not') {
+ info.hasNotPseudoFunc = true;
+ } else {
+ info.hasForgivenPseudoFunc = true;
+ }
+ } else if (KEYS_PS_CLASS_STATE.has(node.name)) {
+ info.hasStatePseudoClass = true;
+ } else if (
+ KEYS_SHADOW_HOST.has(node.name) &&
+ Array.isArray(node.children) &&
+ node.children.length
+ ) {
+ info.hasNestedSelector = true;
+ }
+ break;
+ }
+ case PS_ELEMENT_SELECTOR: {
+ if (REG_SHADOW_PS_ELEMENT.test(node.name)) {
+ info.hasNestedSelector = true;
+ }
+ break;
+ }
+ case NTH: {
+ if (node.selector) {
+ info.hasNestedSelector = true;
+ info.hasNthChildOfSelector = true;
+ }
+ break;
+ }
+ case SELECTOR: {
+ branches.add(node.children);
+ break;
+ }
+ default:
+ }
+ }
+ };
+ cssTree.walk(ast, opt);
+ if (info.hasNestedSelector === true) {
+ cssTree.findAll(ast, (node, item, list) => {
+ if (list) {
+ if (node.type === PS_CLASS_SELECTOR && KEYS_LOGICAL.has(node.name)) {
+ const itemList = list.filter(i => {
+ const { name, type } = i;
+ return type === PS_CLASS_SELECTOR && KEYS_LOGICAL.has(name);
+ });
+ for (const { children } of itemList) {
+ // SelectorList
+ for (const { children: grandChildren } of children) {
+ // Selector
+ for (const { children: greatGrandChildren } of grandChildren) {
+ if (branches.has(greatGrandChildren)) {
+ branches.delete(greatGrandChildren);
+ }
+ }
+ }
+ }
+ } else if (
+ node.type === PS_CLASS_SELECTOR &&
+ KEYS_SHADOW_HOST.has(node.name) &&
+ Array.isArray(node.children) &&
+ node.children.length
+ ) {
+ const itemList = list.filter(i => {
+ const { children, name, type } = i;
+ const res =
+ type === PS_CLASS_SELECTOR &&
+ KEYS_SHADOW_HOST.has(name) &&
+ Array.isArray(children) &&
+ children.length;
+ return res;
+ });
+ for (const { children } of itemList) {
+ // Selector
+ for (const { children: grandChildren } of children) {
+ if (branches.has(grandChildren)) {
+ branches.delete(grandChildren);
+ }
+ }
+ }
+ } else if (
+ node.type === PS_ELEMENT_SELECTOR &&
+ REG_SHADOW_PS_ELEMENT.test(node.name)
+ ) {
+ const itemList = list.filter(i => {
+ const { name, type } = i;
+ const res =
+ type === PS_ELEMENT_SELECTOR && REG_SHADOW_PS_ELEMENT.test(name);
+ return res;
+ });
+ for (const { children } of itemList) {
+ // Selector
+ for (const { children: grandChildren } of children) {
+ if (branches.has(grandChildren)) {
+ branches.delete(grandChildren);
+ }
+ }
+ }
+ } else if (node.type === NTH && node.selector) {
+ const itemList = list.filter(i => {
+ const { selector, type } = i;
+ const res = type === NTH && selector;
+ return res;
+ });
+ for (const { selector } of itemList) {
+ const { children } = selector;
+ // Selector
+ for (const { children: grandChildren } of children) {
+ if (branches.has(grandChildren)) {
+ branches.delete(grandChildren);
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+ return {
+ info,
+ branches: [...branches]
+ };
+};
+
+/**
+ * Comparison function for sorting AST nodes based on specificity.
+ * @param {object} a - The first AST node.
+ * @param {object} b - The second AST node.
+ * @returns {number} -1, 0 or 1, depending on the sort order.
+ */
+export const compareASTNodes = (a, b) => {
+ const bitA = AST_SORT_ORDER.get(a.type);
+ const bitB = AST_SORT_ORDER.get(b.type);
+ if (bitA === bitB) {
+ return 0;
+ } else if (bitA > bitB) {
+ return 1;
+ } else {
+ return -1;
+ }
+};
+
+/**
+ * Sorts a collection of AST nodes based on CSS specificity rules.
+ * @param {Array<object>} asts - A collection of AST nodes to sort.
+ * @returns {Array<object>} A new array containing the sorted AST nodes.
+ */
+export const sortAST = asts => {
+ const arr = [...asts];
+ if (arr.length > 1) {
+ arr.sort(compareASTNodes);
+ }
+ return arr;
+};
+
+/**
+ * Parses a type selector's name, which may include a namespace prefix.
+ * @param {string} selector - The type selector name (e.g., 'ns|E' or 'E').
+ * @returns {{prefix: string, localName: string}} An object with `prefix` and
+ * `localName` properties.
+ */
+export const parseAstName = selector => {
+ let prefix;
+ let localName;
+ if (selector && typeof selector === 'string') {
+ if (selector.indexOf('|') > -1) {
+ [prefix, localName] = selector.split('|');
+ } else {
+ prefix = '*';
+ localName = selector;
+ }
+ } else {
+ throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
+ }
+ return {
+ prefix,
+ localName
+ };
+};
+
+/* Re-exported from css-tree. */
+export { find as findAST, generate as generateCSS } from 'css-tree';
diff --git a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js
new file mode 100644
index 0000000..ce141a4
--- /dev/null
+++ b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js
@@ -0,0 +1,1107 @@
+/**
+ * utility.js
+ */
+
+/* import */
+import nwsapi from '@asamuzakjp/nwsapi';
+import bidiFactory from 'bidi-js';
+import * as cssTree from 'css-tree';
+import isCustomElementName from 'is-potential-custom-element-name';
+
+/* constants */
+import {
+ ATRULE,
+ COMBO,
+ COMPOUND_I,
+ DESCEND,
+ DOCUMENT_FRAGMENT_NODE,
+ DOCUMENT_NODE,
+ DOCUMENT_POSITION_CONTAINS,
+ DOCUMENT_POSITION_PRECEDING,
+ ELEMENT_NODE,
+ HAS_COMPOUND,
+ INPUT_BUTTON,
+ INPUT_EDIT,
+ INPUT_LTR,
+ INPUT_TEXT,
+ KEYS_LOGICAL,
+ LOGIC_COMPLEX,
+ LOGIC_COMPOUND,
+ N_TH,
+ PSEUDO_CLASS,
+ RULE,
+ SCOPE,
+ SELECTOR_LIST,
+ SIBLING,
+ TARGET_ALL,
+ TARGET_FIRST,
+ TEXT_NODE,
+ TYPE_FROM,
+ TYPE_TO
+} from './constant.js';
+const KEYS_DIR_AUTO = new Set([...INPUT_BUTTON, ...INPUT_TEXT, 'hidden']);
+const KEYS_DIR_LTR = new Set(INPUT_LTR);
+const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
+const KEYS_NODE_DIR_EXCLUDE = new Set(['bdi', 'script', 'style', 'textarea']);
+const KEYS_NODE_FOCUSABLE = new Set(['button', 'select', 'textarea']);
+const KEYS_NODE_FOCUSABLE_SVG = new Set([
+ 'clipPath',
+ 'defs',
+ 'desc',
+ 'linearGradient',
+ 'marker',
+ 'mask',
+ 'metadata',
+ 'pattern',
+ 'radialGradient',
+ 'script',
+ 'style',
+ 'symbol',
+ 'title'
+]);
+const REG_EXCLUDE_BASIC =
+ /[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
+const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
+const REG_DESCEND = new RegExp(`${COMPOUND_I}${DESCEND}${COMPOUND_I}`, 'i');
+const REG_SIBLING = new RegExp(`${COMPOUND_I}${SIBLING}${COMPOUND_I}`, 'i');
+const REG_LOGIC_COMPLEX = new RegExp(
+ `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
+);
+const REG_LOGIC_COMPOUND = new RegExp(
+ `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
+);
+const REG_LOGIC_HAS_COMPOUND = new RegExp(
+ `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
+);
+const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
+const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
+const REG_IS_HTML = /^(?:application\/xhtml\+x|text\/ht)ml$/;
+const REG_IS_XML =
+ /^(?:application\/(?:[\w\-.]+\+)?|image\/[\w\-.]+\+|text\/)xml$/;
+
+/**
+ * Manages state for extracting nested selectors from a CSS AST.
+ */
+class SelectorExtractor {
+ constructor() {
+ this.selectors = [];
+ this.isScoped = false;
+ }
+
+ /**
+ * Walker enter function.
+ * @param {object} node - The AST node.
+ */
+ enter(node) {
+ switch (node.type) {
+ case ATRULE: {
+ if (node.name === 'scope') {
+ this.isScoped = true;
+ }
+ break;
+ }
+ case SCOPE: {
+ const { children, type } = node.root;
+ const arr = [];
+ if (type === SELECTOR_LIST) {
+ for (const child of children) {
+ const selector = cssTree.generate(child);
+ arr.push(selector);
+ }
+ this.selectors.push(arr);
+ }
+ break;
+ }
+ case RULE: {
+ const { children, type } = node.prelude;
+ const arr = [];
+ if (type === SELECTOR_LIST) {
+ let hasAmp = false;
+ for (const child of children) {
+ const selector = cssTree.generate(child);
+ if (this.isScoped && !hasAmp) {
+ hasAmp = /\x26/.test(selector);
+ }
+ arr.push(selector);
+ }
+ if (this.isScoped) {
+ if (hasAmp) {
+ this.selectors.push(arr);
+ /* FIXME:
+ } else {
+ this.selectors = arr;
+ this.isScoped = false;
+ */
+ }
+ } else {
+ this.selectors.push(arr);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Walker leave function.
+ * @param {object} node - The AST node.
+ */
+ leave(node) {
+ if (node.type === ATRULE) {
+ if (node.name === 'scope') {
+ this.isScoped = false;
+ }
+ }
+ }
+}
+
+/**
+ * Get type of an object.
+ * @param {object} o - Object to check.
+ * @returns {string} - Type of the object.
+ */
+export const getType = o =>
+ Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO);
+
+/**
+ * Verify array contents.
+ * @param {Array} arr - The array.
+ * @param {string} type - Expected type, e.g. 'String'.
+ * @throws {TypeError} - Throws if array or its items are of unexpected type.
+ * @returns {Array} - The verified array.
+ */
+export const verifyArray = (arr, type) => {
+ if (!Array.isArray(arr)) {
+ throw new TypeError(`Unexpected type ${getType(arr)}`);
+ }
+ if (typeof type !== 'string') {
+ throw new TypeError(`Unexpected type ${getType(type)}`);
+ }
+ for (const item of arr) {
+ if (getType(item) !== type) {
+ throw new TypeError(`Unexpected type ${getType(item)}`);
+ }
+ }
+ return arr;
+};
+
+/**
+ * Generate a DOMException.
+ * @param {string} msg - The error message.
+ * @param {string} name - The error name.
+ * @param {object} globalObject - The global object (e.g., window).
+ * @returns {DOMException} The generated DOMException object.
+ */
+export const generateException = (msg, name, globalObject = globalThis) => {
+ return new globalObject.DOMException(msg, name);
+};
+
+/**
+ * Find a nested :has() pseudo-class.
+ * @param {object} leaf - The AST leaf to check.
+ * @returns {?object} The leaf if it's :has, otherwise null.
+ */
+export const findNestedHas = leaf => {
+ return leaf.name === 'has';
+};
+
+/**
+ * Find a logical pseudo-class that contains a nested :has().
+ * @param {object} leaf - The AST leaf to check.
+ * @returns {?object} The leaf if it matches, otherwise null.
+ */
+export const findLogicalWithNestedHas = leaf => {
+ if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
+ return leaf;
+ }
+ return null;
+};
+
+/**
+ * Filter a list of nodes based on An+B logic
+ * @param {Array.<object>} nodes - array of nodes to filter
+ * @param {object} anb - An+B options
+ * @param {number} anb.a - a
+ * @param {number} anb.b - b
+ * @param {boolean} [anb.reverse] - reverse order
+ * @returns {Array.<object>} - array of matched nodes
+ */
+export const filterNodesByAnB = (nodes, anb) => {
+ const { a, b, reverse } = anb;
+ const processedNodes = reverse ? [...nodes].reverse() : nodes;
+ const l = nodes.length;
+ const matched = [];
+ if (a === 0) {
+ if (b > 0 && b <= l) {
+ matched.push(processedNodes[b - 1]);
+ }
+ return matched;
+ }
+ let startIndex = b - 1;
+ if (a > 0) {
+ while (startIndex < 0) {
+ startIndex += a;
+ }
+ for (let i = startIndex; i < l; i += a) {
+ matched.push(processedNodes[i]);
+ }
+ } else if (startIndex >= 0) {
+ for (let i = startIndex; i >= 0; i += a) {
+ matched.push(processedNodes[i]);
+ }
+ return matched.reverse();
+ }
+ return matched;
+};
+
+/**
+ * Resolve content document, root node, and check if it's in a shadow DOM.
+ * @param {object} node - Document, DocumentFragment, or Element node.
+ * @returns {Array.<object|boolean>} - [document, root, isInShadow].
+ */
+export const resolveContent = node => {
+ if (!node?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(node)}`);
+ }
+ let document;
+ let root;
+ let shadow;
+ switch (node.nodeType) {
+ case DOCUMENT_NODE: {
+ document = node;
+ root = node;
+ break;
+ }
+ case DOCUMENT_FRAGMENT_NODE: {
+ const { host, mode, ownerDocument } = node;
+ document = ownerDocument;
+ root = node;
+ shadow = host && (mode === 'close' || mode === 'open');
+ break;
+ }
+ case ELEMENT_NODE: {
+ document = node.ownerDocument;
+ let refNode = node;
+ while (refNode) {
+ const { host, mode, nodeType, parentNode } = refNode;
+ if (nodeType === DOCUMENT_FRAGMENT_NODE) {
+ shadow = host && (mode === 'close' || mode === 'open');
+ break;
+ } else if (parentNode) {
+ refNode = parentNode;
+ } else {
+ break;
+ }
+ }
+ root = refNode;
+ break;
+ }
+ default: {
+ throw new TypeError(`Unexpected node ${node.nodeName}`);
+ }
+ }
+ return [document, root, !!shadow];
+};
+
+/**
+ * Traverse node tree with a TreeWalker.
+ * @param {object} node - The target node.
+ * @param {object} walker - The TreeWalker instance.
+ * @param {boolean} [force] - Traverse only to the next node.
+ * @returns {?object} - The current node if found, otherwise null.
+ */
+export const traverseNode = (node, walker, force = false) => {
+ if (!node?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(node)}`);
+ }
+ if (!walker) {
+ return null;
+ }
+ let refNode = walker.currentNode;
+ if (refNode === node) {
+ return refNode;
+ } else if (force || refNode.contains(node)) {
+ refNode = walker.nextNode();
+ while (refNode) {
+ if (refNode === node) {
+ break;
+ }
+ refNode = walker.nextNode();
+ }
+ return refNode;
+ } else {
+ if (refNode !== walker.root) {
+ let bool;
+ while (refNode) {
+ if (refNode === node) {
+ bool = true;
+ break;
+ } else if (refNode === walker.root || refNode.contains(node)) {
+ break;
+ }
+ refNode = walker.parentNode();
+ }
+ if (bool) {
+ return refNode;
+ }
+ }
+ if (node.nodeType === ELEMENT_NODE) {
+ let bool;
+ while (refNode) {
+ if (refNode === node) {
+ bool = true;
+ break;
+ }
+ refNode = walker.nextNode();
+ }
+ if (bool) {
+ return refNode;
+ }
+ }
+ }
+ return null;
+};
+
+/**
+ * Check if a node is a custom element.
+ * @param {object} node - The Element node.
+ * @param {object} [opt] - Options.
+ * @returns {boolean} - True if it's a custom element.
+ */
+export const isCustomElement = (node, opt = {}) => {
+ if (!node?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(node)}`);
+ }
+ if (node.nodeType !== ELEMENT_NODE) {
+ return false;
+ }
+ const { localName, ownerDocument } = node;
+ const { formAssociated } = opt;
+ const window = ownerDocument.defaultView;
+ let elmConstructor;
+ const attr = node.getAttribute('is');
+ if (attr) {
+ elmConstructor =
+ isCustomElementName(attr) && window.customElements.get(attr);
+ } else {
+ elmConstructor =
+ isCustomElementName(localName) && window.customElements.get(localName);
+ }
+ if (elmConstructor) {
+ if (formAssociated) {
+ return !!elmConstructor.formAssociated;
+ }
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Get slotted text content.
+ * @param {object} node - The Element node (likely a <slot>).
+ * @returns {?string} - The text content.
+ */
+export const getSlottedTextContent = node => {
+ if (!node?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(node)}`);
+ }
+ if (typeof node.assignedNodes !== 'function') {
+ return null;
+ }
+ const nodes = node.assignedNodes();
+ if (nodes.length) {
+ let text = '';
+ const l = nodes.length;
+ for (let i = 0; i < l; i++) {
+ const item = nodes[i];
+ text = item.textContent.trim();
+ if (text) {
+ break;
+ }
+ }
+ return text;
+ }
+ return node.textContent.trim();
+};
+
+/**
+ * Get directionality of a node.
+ * @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
+ * @param {object} node - The Element node.
+ * @returns {?string} - 'ltr' or 'rtl'.
+ */
+export const getDirectionality = node => {
+ if (!node?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(node)}`);
+ }
+ if (node.nodeType !== ELEMENT_NODE) {
+ return null;
+ }
+ const { dir: dirAttr, localName, parentNode } = node;
+ const { getEmbeddingLevels } = bidiFactory();
+ if (dirAttr === 'ltr' || dirAttr === 'rtl') {
+ return dirAttr;
+ } else if (dirAttr === 'auto') {
+ let text = '';
+ switch (localName) {
+ case 'input': {
+ if (!node.type || KEYS_DIR_AUTO.has(node.type)) {
+ text = node.value;
+ } else if (KEYS_DIR_LTR.has(node.type)) {
+ return 'ltr';
+ }
+ break;
+ }
+ case 'slot': {
+ text = getSlottedTextContent(node);
+ break;
+ }
+ case 'textarea': {
+ text = node.value;
+ break;
+ }
+ default: {
+ const items = [].slice.call(node.childNodes);
+ for (const item of items) {
+ const {
+ dir: itemDir,
+ localName: itemLocalName,
+ nodeType: itemNodeType,
+ textContent: itemTextContent
+ } = item;
+ if (itemNodeType === TEXT_NODE) {
+ text = itemTextContent.trim();
+ } else if (
+ itemNodeType === ELEMENT_NODE &&
+ !KEYS_NODE_DIR_EXCLUDE.has(itemLocalName) &&
+ (!itemDir || (itemDir !== 'ltr' && itemDir !== 'rtl'))
+ ) {
+ if (itemLocalName === 'slot') {
+ text = getSlottedTextContent(item);
+ } else {
+ text = itemTextContent.trim();
+ }
+ }
+ if (text) {
+ break;
+ }
+ }
+ }
+ }
+ if (text) {
+ const {
+ paragraphs: [{ level }]
+ } = getEmbeddingLevels(text);
+ if (level % 2 === 1) {
+ return 'rtl';
+ }
+ } else if (parentNode) {
+ const { nodeType: parentNodeType } = parentNode;
+ if (parentNodeType === ELEMENT_NODE) {
+ return getDirectionality(parentNode);
+ }
+ }
+ } else if (localName === 'input' && node.type === 'tel') {
+ return 'ltr';
+ } else if (localName === 'bdi') {
+ const text = node.textContent.trim();
+ if (text) {
+ const {
+ paragraphs: [{ level }]
+ } = getEmbeddingLevels(text);
+ if (level % 2 === 1) {
+ return 'rtl';
+ }
+ }
+ } else if (parentNode) {
+ if (localName === 'slot') {
+ const text = getSlottedTextContent(node);
+ if (text) {
+ const {
+ paragraphs: [{ level }]
+ } = getEmbeddingLevels(text);
+ if (level % 2 === 1) {
+ return 'rtl';
+ }
+ return 'ltr';
+ }
+ }
+ const { nodeType: parentNodeType } = parentNode;
+ if (parentNodeType === ELEMENT_NODE) {
+ return getDirectionality(parentNode);
+ }
+ }
+ return 'ltr';
+};
+
+/**
+ * Traverses up the DOM tree to find the language attribute for a node.
+ * It checks for 'lang' in HTML and 'xml:lang' in XML contexts.
+ * @param {object} node - The starting element node.
+ * @returns {string|null} The language attribute value, or null if not found.
+ */
+export const getLanguageAttribute = node => {
+ if (!node?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(node)}`);
+ }
+ if (node.nodeType !== ELEMENT_NODE) {
+ return null;
+ }
+ const { contentType } = node.ownerDocument;
+ const isHtml = REG_IS_HTML.test(contentType);
+ const isXml = REG_IS_XML.test(contentType);
+ let isShadow = false;
+ // Traverse up from the current node to the root.
+ let current = node;
+ while (current) {
+ // Check if the current node is an element.
+ switch (current.nodeType) {
+ case ELEMENT_NODE: {
+ // Check for and return the language attribute if present.
+ if (isHtml && current.hasAttribute('lang')) {
+ return current.getAttribute('lang');
+ } else if (isXml && current.hasAttribute('xml:lang')) {
+ return current.getAttribute('xml:lang');
+ }
+ break;
+ }
+ case DOCUMENT_FRAGMENT_NODE: {
+ // Continue traversal if the current node is a shadow root.
+ if (current.host) {
+ isShadow = true;
+ }
+ break;
+ }
+ case DOCUMENT_NODE:
+ default: {
+ // Stop if we reach the root document node.
+ return null;
+ }
+ }
+ if (isShadow) {
+ current = current.host;
+ isShadow = false;
+ } else if (current.parentNode) {
+ current = current.parentNode;
+ } else {
+ break;
+ }
+ }
+ // No language attribute was found in the hierarchy.
+ return null;
+};
+
+/**
+ * Check if content is editable.
+ * NOTE: Not implemented in jsdom https://github.com/jsdom/jsdom/issues/1670
+ * @param {object} node - The Element node.
+ * @returns {boolean} - True if content is editable.
+ */
+export const isContentEditable = node => {
+ if (!node?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(node)}`);
+ }
+ if (node.nodeType !== ELEMENT_NODE) {
+ return false;
+ }
+ if (typeof node.isContentEditable === 'boolean') {
+ return node.isContentEditable;
+ } else if (node.ownerDocument.designMode === 'on') {
+ return true;
+ } else {
+ let attr;
+ if (node.hasAttribute('contenteditable')) {
+ attr = node.getAttribute('contenteditable');
+ } else {
+ attr = 'inherit';
+ }
+ switch (attr) {
+ case '':
+ case 'true': {
+ return true;
+ }
+ case 'plaintext-only': {
+ // FIXME:
+ // @see https://github.com/w3c/editing/issues/470
+ // @see https://github.com/whatwg/html/issues/10651
+ return true;
+ }
+ case 'false': {
+ return false;
+ }
+ default: {
+ if (node?.parentNode?.nodeType === ELEMENT_NODE) {
+ return isContentEditable(node.parentNode);
+ }
+ return false;
+ }
+ }
+ }
+};
+
+/**
+ * Check if a node is visible.
+ * @param {object} node - The Element node.
+ * @returns {boolean} - True if the node is visible.
+ */
+export const isVisible = node => {
+ if (node?.nodeType !== ELEMENT_NODE) {
+ return false;
+ }
+ const window = node.ownerDocument.defaultView;
+ const { display, visibility } = window.getComputedStyle(node);
+ if (display !== 'none' && visibility === 'visible') {
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Check if focus is visible on the node.
+ * @param {object} node - The Element node.
+ * @returns {boolean} - True if focus is visible.
+ */
+export const isFocusVisible = node => {
+ if (node?.nodeType !== ELEMENT_NODE) {
+ return false;
+ }
+ const { localName, type } = node;
+ switch (localName) {
+ case 'input': {
+ if (!type || KEYS_INPUT_EDIT.has(type)) {
+ return true;
+ }
+ return false;
+ }
+ case 'textarea': {
+ return true;
+ }
+ default: {
+ return isContentEditable(node);
+ }
+ }
+};
+
+/**
+ * Check if an area is focusable.
+ * @param {object} node - The Element node.
+ * @returns {boolean} - True if the area is focusable.
+ */
+export const isFocusableArea = node => {
+ if (node?.nodeType !== ELEMENT_NODE) {
+ return false;
+ }
+ if (!node.isConnected) {
+ return false;
+ }
+ const window = node.ownerDocument.defaultView;
+ if (node instanceof window.HTMLElement) {
+ if (Number.isInteger(parseInt(node.getAttribute('tabindex')))) {
+ return true;
+ }
+ if (isContentEditable(node)) {
+ return true;
+ }
+ const { localName, parentNode } = node;
+ switch (localName) {
+ case 'a': {
+ if (node.href || node.hasAttribute('href')) {
+ return true;
+ }
+ return false;
+ }
+ case 'iframe': {
+ return true;
+ }
+ case 'input': {
+ if (
+ node.disabled ||
+ node.hasAttribute('disabled') ||
+ node.hidden ||
+ node.hasAttribute('hidden')
+ ) {
+ return false;
+ }
+ return true;
+ }
+ case 'summary': {
+ if (parentNode.localName === 'details') {
+ let child = parentNode.firstElementChild;
+ let bool = false;
+ while (child) {
+ if (child.localName === 'summary') {
+ bool = child === node;
+ break;
+ }
+ child = child.nextElementSibling;
+ }
+ return bool;
+ }
+ return false;
+ }
+ default: {
+ if (
+ KEYS_NODE_FOCUSABLE.has(localName) &&
+ !(node.disabled || node.hasAttribute('disabled'))
+ ) {
+ return true;
+ }
+ }
+ }
+ } else if (node instanceof window.SVGElement) {
+ if (Number.isInteger(parseInt(node.getAttributeNS(null, 'tabindex')))) {
+ const ns = 'http://www.w3.org/2000/svg';
+ let bool;
+ let refNode = node;
+ while (refNode.namespaceURI === ns) {
+ bool = KEYS_NODE_FOCUSABLE_SVG.has(refNode.localName);
+ if (bool) {
+ break;
+ }
+ if (refNode?.parentNode?.namespaceURI === ns) {
+ refNode = refNode.parentNode;
+ } else {
+ break;
+ }
+ }
+ if (bool) {
+ return false;
+ }
+ return true;
+ }
+ if (
+ node.localName === 'a' &&
+ (node.href || node.hasAttributeNS(null, 'href'))
+ ) {
+ return true;
+ }
+ }
+ return false;
+};
+
+/**
+ * Check if a node is focusable.
+ * NOTE: Not applied, needs fix in jsdom itself.
+ * @see https://github.com/whatwg/html/pull/8392
+ * @see https://phabricator.services.mozilla.com/D156219
+ * @see https://github.com/jsdom/jsdom/issues/3029
+ * @see https://github.com/jsdom/jsdom/issues/3464
+ * @param {object} node - The Element node.
+ * @returns {boolean} - True if the node is focusable.
+ */
+export const isFocusable = node => {
+ if (node?.nodeType !== ELEMENT_NODE) {
+ return false;
+ }
+ const window = node.ownerDocument.defaultView;
+ let refNode = node;
+ let res = true;
+ while (refNode) {
+ if (refNode.disabled || refNode.hasAttribute('disabled')) {
+ res = false;
+ break;
+ }
+ if (refNode.hidden || refNode.hasAttribute('hidden')) {
+ res = false;
+ }
+ const { contentVisibility, display, visibility } =
+ window.getComputedStyle(refNode);
+ if (
+ display === 'none' ||
+ visibility !== 'visible' ||
+ (contentVisibility === 'hidden' && refNode !== node)
+ ) {
+ res = false;
+ } else {
+ res = true;
+ }
+ if (res && refNode?.parentNode?.nodeType === ELEMENT_NODE) {
+ refNode = refNode.parentNode;
+ } else {
+ break;
+ }
+ }
+ return res;
+};
+
+/**
+ * Get namespace URI.
+ * @param {string} ns - The namespace prefix.
+ * @param {object} node - The Element node.
+ * @returns {?string} - The namespace URI.
+ */
+export const getNamespaceURI = (ns, node) => {
+ if (typeof ns !== 'string') {
+ throw new TypeError(`Unexpected type ${getType(ns)}`);
+ } else if (!node?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(node)}`);
+ }
+ if (!ns || node.nodeType !== ELEMENT_NODE) {
+ return null;
+ }
+ const { attributes } = node;
+ let res;
+ for (const attr of attributes) {
+ const { name, namespaceURI, prefix, value } = attr;
+ if (name === `xmlns:${ns}`) {
+ res = value;
+ } else if (prefix === ns) {
+ res = namespaceURI;
+ }
+ if (res) {
+ break;
+ }
+ }
+ return res ?? null;
+};
+
+/**
+ * Check if a namespace is declared.
+ * @param {string} ns - The namespace.
+ * @param {object} node - The Element node.
+ * @returns {boolean} - True if the namespace is declared.
+ */
+export const isNamespaceDeclared = (ns = '', node = {}) => {
+ if (!ns || typeof ns !== 'string' || node?.nodeType !== ELEMENT_NODE) {
+ return false;
+ }
+ if (node.lookupNamespaceURI(ns)) {
+ return true;
+ }
+ const root = node.ownerDocument.documentElement;
+ let parent = node;
+ let res;
+ while (parent) {
+ res = getNamespaceURI(ns, parent);
+ if (res || parent === root) {
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ return !!res;
+};
+
+/**
+ * Check if nodeA precedes and/or contains nodeB.
+ * @param {object} nodeA - The first Element node.
+ * @param {object} nodeB - The second Element node.
+ * @returns {boolean} - True if nodeA precedes nodeB.
+ */
+export const isPreceding = (nodeA, nodeB) => {
+ if (!nodeA?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(nodeA)}`);
+ } else if (!nodeB?.nodeType) {
+ throw new TypeError(`Unexpected type ${getType(nodeB)}`);
+ }
+ if (nodeA.nodeType !== ELEMENT_NODE || nodeB.nodeType !== ELEMENT_NODE) {
+ return false;
+ }
+ const posBit = nodeB.compareDocumentPosition(nodeA);
+ const res =
+ posBit & DOCUMENT_POSITION_PRECEDING || posBit & DOCUMENT_POSITION_CONTAINS;
+ return !!res;
+};
+
+/**
+ * Comparison function for sorting nodes based on document position.
+ * @param {object} a - The first node.
+ * @param {object} b - The second node.
+ * @returns {number} - Sort order.
+ */
+export const compareNodes = (a, b) => {
+ if (isPreceding(b, a)) {
+ return 1;
+ }
+ return -1;
+};
+
+/**
+ * Sort a collection of nodes.
+ * @param {Array.<object>|Set.<object>} nodes - Collection of nodes.
+ * @returns {Array.<object>} - Collection of sorted nodes.
+ */
+export const sortNodes = (nodes = []) => {
+ const arr = [...nodes];
+ if (arr.length > 1) {
+ arr.sort(compareNodes);
+ }
+ return arr;
+};
+
+/**
+ * Concat an array of nested selectors into an equivalent single selector.
+ * @param {Array.<Array.<string>>} selectors - [parents, children, ...].
+ * @returns {string} - The concatenated selector.
+ */
+export const concatNestedSelectors = selectors => {
+ if (!Array.isArray(selectors)) {
+ throw new TypeError(`Unexpected type ${getType(selectors)}`);
+ }
+ let selector = '';
+ if (selectors.length) {
+ const revSelectors = selectors.toReversed();
+ let child = verifyArray(revSelectors.shift(), 'String');
+ if (child.length === 1) {
+ [child] = child;
+ }
+ while (revSelectors.length) {
+ const parentArr = verifyArray(revSelectors.shift(), 'String');
+ if (!parentArr.length) {
+ continue;
+ }
+ let parent;
+ if (parentArr.length === 1) {
+ [parent] = parentArr;
+ if (!/^[>~+]/.test(parent) && /[\s>~+]/.test(parent)) {
+ parent = `:is(${parent})`;
+ }
+ } else {
+ parent = `:is(${parentArr.join(', ')})`;
+ }
+ if (selector.includes('\x26')) {
+ selector = selector.replace(/\x26/g, parent);
+ }
+ if (Array.isArray(child)) {
+ const items = [];
+ for (let item of child) {
+ if (item.includes('\x26')) {
+ if (/^[>~+]/.test(item)) {
+ item = `${parent} ${item.replace(/\x26/g, parent)} ${selector}`;
+ } else {
+ item = `${item.replace(/\x26/g, parent)} ${selector}`;
+ }
+ } else {
+ item = `${parent} ${item} ${selector}`;
+ }
+ items.push(item.trim());
+ }
+ selector = items.join(', ');
+ } else if (revSelectors.length) {
+ selector = `${child} ${selector}`;
+ } else {
+ if (child.includes('\x26')) {
+ if (/^[>~+]/.test(child)) {
+ selector = `${parent} ${child.replace(/\x26/g, parent)} ${selector}`;
+ } else {
+ selector = `${child.replace(/\x26/g, parent)} ${selector}`;
+ }
+ } else {
+ selector = `${parent} ${child} ${selector}`;
+ }
+ }
+ selector = selector.trim();
+ if (revSelectors.length) {
+ child = parentArr.length > 1 ? parentArr : parent;
+ } else {
+ break;
+ }
+ }
+ selector = selector.replace(/\x26/g, ':scope').trim();
+ }
+ return selector;
+};
+
+/**
+ * Extract nested selectors from CSSRule.cssText.
+ * @param {string} css - CSSRule.cssText.
+ * @returns {Array.<Array.<string>>} - Array of nested selectors.
+ */
+export const extractNestedSelectors = css => {
+ const ast = cssTree.parse(css, {
+ context: 'rule'
+ });
+ const extractor = new SelectorExtractor();
+ cssTree.walk(ast, {
+ enter: extractor.enter.bind(extractor),
+ leave: extractor.leave.bind(extractor)
+ });
+ return extractor.selectors;
+};
+
+/**
+ * Initialize nwsapi.
+ * @param {object} window - The Window object.
+ * @param {object} document - The Document object.
+ * @returns {object} - The nwsapi instance.
+ */
+export const initNwsapi = (window, document) => {
+ if (!window?.DOMException) {
+ throw new TypeError(`Unexpected global object ${getType(window)}`);
+ }
+ if (document?.nodeType !== DOCUMENT_NODE) {
+ document = window.document;
+ }
+ const nw = nwsapi({
+ document,
+ DOMException: window.DOMException
+ });
+ nw.configure({
+ LOGERRORS: false
+ });
+ return nw;
+};
+
+/**
+ * Filter a selector for use with nwsapi.
+ * @param {string} selector - The selector string.
+ * @param {string} target - The target type.
+ * @returns {boolean} - True if the selector is valid for nwsapi.
+ */
+export const filterSelector = (selector, target) => {
+ const isQuerySelectorType = target === TARGET_FIRST || target === TARGET_ALL;
+ if (
+ !selector ||
+ typeof selector !== 'string' ||
+ /null|undefined/.test(selector)
+ ) {
+ return false;
+ }
+ // Exclude missing close square bracket.
+ if (selector.includes('[')) {
+ const index = selector.lastIndexOf('[');
+ const sel = selector.substring(index);
+ if (sel.indexOf(']') < 0) {
+ return false;
+ }
+ }
+ // Exclude various complex or unsupported selectors.
+ // - selectors containing '/'
+ // - namespaced selectors
+ // - escaped selectors
+ // - pseudo-element selectors
+ // - selectors containing non-ASCII
+ // - selectors containing control character other than whitespace
+ // - attribute selectors with case flag, e.g. [attr i]
+ // - attribute selectors with unclosed quotes
+ // - empty :is() or :where()
+ if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
+ return false;
+ }
+ // Include pseudo-classes that are known to work correctly.
+ if (selector.includes(':')) {
+ let complex = false;
+ if (target !== isQuerySelectorType) {
+ complex = REG_COMPLEX.test(selector);
+ }
+ if (
+ isQuerySelectorType &&
+ REG_DESCEND.test(selector) &&
+ !REG_SIBLING.test(selector)
+ ) {
+ return false;
+ } else if (!isQuerySelectorType && /:has\(/.test(selector)) {
+ if (!complex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
+ return false;
+ }
+ return REG_END_WITH_HAS.test(selector);
+ } else if (/:(?:is|not)\(/.test(selector)) {
+ if (complex) {
+ return !REG_LOGIC_COMPLEX.test(selector);
+ } else {
+ return !REG_LOGIC_COMPOUND.test(selector);
+ }
+ } else {
+ return !REG_WO_LOGICAL.test(selector);
+ }
+ }
+ return true;
+};