3123 lines
91 KiB
JavaScript
3123 lines
91 KiB
JavaScript
/**
|
|
* 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;
|
|
};
|
|
}
|