| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- /*!
- * DOM Selector - A CSS selector engine.
- * @license MIT
- * @copyright asamuzaK (Kazz)
- * @see {@link https://github.com/asamuzaK/domSelector/blob/main/LICENSE}
- */
- /* import */
- import { GenerationalCache } from '@asamuzakjp/generational-cache';
- import { Finder } from './js/finder.js';
- import { unescapeSelector, parseAstName } from './js/parser.js';
- import { filterSelector, getType, initNwsapi } from './js/utility.js';
- /* constants */
- import {
- DOCUMENT_NODE,
- DOCUMENT_FRAGMENT_NODE,
- ELEMENT_NODE,
- TARGET_ALL,
- TARGET_FIRST,
- TARGET_LINEAL,
- TARGET_SELF,
- COMBINATOR,
- ID_SELECTOR,
- CLASS_SELECTOR,
- TYPE_SELECTOR
- } from './js/constant.js';
- const CACHE_SIZE = 2048;
- /**
- * @typedef {object} CheckResult
- * @property {boolean} match - The match result.
- * @property {string?} pseudoElement - The pseudo-element, if any.
- * @property {object?} ast - The AST object.
- */
- /* DOMSelector */
- export class DOMSelector {
- /* private fields */
- #window;
- #document;
- #finder;
- #idlUtils;
- #nwsapi;
- #cache;
- /**
- * Creates an instance of DOMSelector.
- * @param {Window} window - The window object.
- * @param {Document} document - The document object.
- * @param {object} [opt] - Options.
- */
- constructor(window, document, opt = {}) {
- const { cacheSize, idlUtils } = opt;
- this.#window = window;
- this.#document = document ?? window.document;
- this.#finder = new Finder(window);
- this.#idlUtils = idlUtils;
- this.#nwsapi = initNwsapi(window, document);
- this.#cache = new GenerationalCache(cacheSize ?? CACHE_SIZE);
- }
- /**
- * Clears the internal cache of finder results.
- * @returns {void}
- */
- clear = () => {
- this.#finder.clearResults(true);
- };
- /**
- * Parses a selector and extracts the rightmost subject keys (Id, Class, Tag).
- * @param {string} selector - The CSS selector to parse.
- * @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
- */
- extractSubjects = selector => {
- if (!selector || typeof selector !== 'string') {
- return [{ id: null, className: null, tag: null }];
- }
- const cacheKey = `extract_${selector}`;
- let subjects = this.#cache.get(cacheKey);
- if (subjects !== undefined) {
- return subjects;
- }
- subjects = [];
- try {
- const ast = this.#finder.getAST(selector);
- if (ast?.type === 'SelectorList') {
- for (const selectorNode of ast.children) {
- let idKey = null;
- let classKey = null;
- let tagKey = null;
- let current = selectorNode.children.tail;
- while (current) {
- const node = current.data;
- if (node.type === COMBINATOR) {
- break;
- }
- if (node.type === ID_SELECTOR) {
- idKey = idKey ?? unescapeSelector(node.name);
- } else if (node.type === CLASS_SELECTOR) {
- classKey = classKey ?? unescapeSelector(node.name);
- } else if (node.type === TYPE_SELECTOR) {
- const { localName } = parseAstName(unescapeSelector(node.name));
- if (localName !== '*') {
- tagKey = tagKey ?? localName.toLowerCase();
- }
- }
- current = current.prev;
- }
- subjects.push({ id: idKey, className: classKey, tag: tagKey });
- }
- }
- } catch (e) {
- // fall through
- }
- if (!subjects.length) {
- subjects.push({ id: null, className: null, tag: null });
- }
- this.#cache.set(cacheKey, subjects);
- return subjects;
- };
- /**
- * Checks if an element matches a CSS selector.
- * @param {string} selector - The CSS selector to check against.
- * @param {Element} node - The element node to check.
- * @param {object} [opt] - Optional parameters.
- * @returns {CheckResult} An object containing the check result.
- */
- check = (selector, node, opt = {}) => {
- if (!node?.nodeType) {
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
- return this.#finder.onError(e, opt);
- } else if (node.nodeType !== ELEMENT_NODE) {
- const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
- return this.#finder.onError(e, opt);
- }
- const document = node.ownerDocument;
- if (
- document === this.#document &&
- document.contentType === 'text/html' &&
- document.documentElement &&
- node.parentNode
- ) {
- const cacheKey = `check_${selector}`;
- let filterMatches = this.#cache.get(cacheKey);
- if (filterMatches === undefined) {
- filterMatches = filterSelector(selector, TARGET_SELF);
- this.#cache.set(cacheKey, filterMatches);
- }
- if (filterMatches) {
- try {
- const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
- const match = this.#nwsapi.match(selector, n);
- let ast = null;
- if (match) {
- const astCacheKey = `check_ast_${selector}`;
- ast = this.#cache.get(astCacheKey);
- if (ast === undefined) {
- ast = this.#finder.getAST(selector);
- this.#cache.set(astCacheKey, ast);
- }
- }
- return {
- match,
- ast,
- pseudoElement: null
- };
- } catch (e) {
- // fall through
- }
- }
- }
- if (this.#idlUtils) {
- node = this.#idlUtils.wrapperForImpl(node);
- }
- opt.check = true;
- opt.noexcept = true;
- opt.warn = false;
- return this.#finder.setup(selector, node, opt).find(TARGET_SELF);
- };
- /**
- * Returns true if the element matches the selector.
- * @param {string} selector - The CSS selector to match against.
- * @param {Element} node - The element node to test.
- * @param {object} [opt] - Optional parameters.
- * @returns {boolean} `true` if the element matches, or `false` otherwise.
- */
- matches = (selector, node, opt = {}) => {
- if (!node?.nodeType) {
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
- return this.#finder.onError(e, opt);
- } else if (node.nodeType !== ELEMENT_NODE) {
- const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
- return this.#finder.onError(e, opt);
- }
- const document = node.ownerDocument;
- if (
- document === this.#document &&
- document.contentType === 'text/html' &&
- document.documentElement &&
- node.parentNode
- ) {
- const cacheKey = `matches_${selector}`;
- let filterMatches = this.#cache.get(cacheKey);
- if (filterMatches === undefined) {
- filterMatches = filterSelector(selector, TARGET_SELF);
- this.#cache.set(cacheKey, filterMatches);
- }
- if (filterMatches) {
- try {
- const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
- return this.#nwsapi.match(selector, n);
- } catch (e) {
- // fall through
- }
- }
- }
- let res;
- try {
- if (this.#idlUtils) {
- node = this.#idlUtils.wrapperForImpl(node);
- }
- const nodes = this.#finder.setup(selector, node, opt).find(TARGET_SELF);
- res = nodes.size;
- } catch (e) {
- this.#finder.onError(e, opt);
- }
- return !!res;
- };
- /**
- * Traverses up the DOM tree to find the first node that matches the selector.
- * @param {string} selector - The CSS selector to match against.
- * @param {Element} node - The element from which to start traversing.
- * @param {object} [opt] - Optional parameters.
- * @returns {?Element} The first matching ancestor element, or `null`.
- */
- closest = (selector, node, opt = {}) => {
- if (!node?.nodeType) {
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
- return this.#finder.onError(e, opt);
- } else if (node.nodeType !== ELEMENT_NODE) {
- const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
- return this.#finder.onError(e, opt);
- }
- const document = node.ownerDocument;
- if (
- document === this.#document &&
- document.contentType === 'text/html' &&
- document.documentElement &&
- node.parentNode
- ) {
- const cacheKey = `closest_${selector}`;
- let filterMatches = this.#cache.get(cacheKey);
- if (filterMatches === undefined) {
- filterMatches = filterSelector(selector, TARGET_LINEAL);
- this.#cache.set(cacheKey, filterMatches);
- }
- if (filterMatches) {
- try {
- const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
- return this.#nwsapi.closest(selector, n);
- } catch (e) {
- // fall through
- }
- }
- }
- let res;
- try {
- if (this.#idlUtils) {
- node = this.#idlUtils.wrapperForImpl(node);
- }
- const nodes = this.#finder.setup(selector, node, opt).find(TARGET_LINEAL);
- if (nodes.size) {
- let refNode = node;
- while (refNode) {
- if (nodes.has(refNode)) {
- res = refNode;
- break;
- }
- refNode = refNode.parentNode;
- }
- }
- } catch (e) {
- this.#finder.onError(e, opt);
- }
- return res ?? null;
- };
- /**
- * Returns the first element within the subtree that matches the selector.
- * @param {string} selector - The CSS selector to match.
- * @param {Document|DocumentFragment|Element} node - The node to find within.
- * @param {object} [opt] - Optional parameters.
- * @returns {?Element} The first matching element, or `null`.
- */
- querySelector = (selector, node, opt = {}) => {
- if (!node?.nodeType) {
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
- return this.#finder.onError(e, opt);
- }
- const document =
- node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
- if (
- document === this.#document &&
- document.contentType === 'text/html' &&
- document.documentElement &&
- (node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
- ) {
- const cacheKey = `querySelector_${selector}`;
- let filterMatches = this.#cache.get(cacheKey);
- if (filterMatches === undefined) {
- filterMatches = filterSelector(selector, TARGET_FIRST);
- this.#cache.set(cacheKey, filterMatches);
- }
- if (filterMatches) {
- try {
- const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
- return this.#nwsapi.first(selector, n);
- } catch (e) {
- // fall through
- }
- }
- }
- let res;
- try {
- if (this.#idlUtils) {
- node = this.#idlUtils.wrapperForImpl(node);
- }
- const nodes = this.#finder.setup(selector, node, opt).find(TARGET_FIRST);
- if (nodes.size) {
- [res] = [...nodes];
- }
- } catch (e) {
- this.#finder.onError(e, opt);
- }
- return res ?? null;
- };
- /**
- * Returns an array of elements within the subtree that match the selector.
- * Note: This method returns an Array, not a NodeList.
- * @param {string} selector - The CSS selector to match.
- * @param {Document|DocumentFragment|Element} node - The node to find within.
- * @param {object} [opt] - Optional parameters.
- * @returns {Array<Element>} An array of elements, or an empty array.
- */
- querySelectorAll = (selector, node, opt = {}) => {
- if (!node?.nodeType) {
- const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
- return this.#finder.onError(e, opt);
- }
- const document =
- node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
- if (
- document === this.#document &&
- document.contentType === 'text/html' &&
- document.documentElement &&
- (node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
- ) {
- const cacheKey = `querySelectorAll_${selector}`;
- let filterMatches = this.#cache.get(cacheKey);
- if (filterMatches === undefined) {
- filterMatches = filterSelector(selector, TARGET_ALL);
- this.#cache.set(cacheKey, filterMatches);
- }
- if (filterMatches) {
- try {
- const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
- return this.#nwsapi.select(selector, n);
- } catch (e) {
- // fall through
- }
- }
- }
- let res;
- try {
- if (this.#idlUtils) {
- node = this.#idlUtils.wrapperForImpl(node);
- }
- const nodes = this.#finder.setup(selector, node, opt).find(TARGET_ALL);
- if (nodes.size) {
- res = [...nodes];
- }
- } catch (e) {
- this.#finder.onError(e, opt);
- }
- return res ?? [];
- };
- }
|