UNPKG

dropflow

Version:

A small CSS2 document renderer built from specifications

1,140 lines (1,139 loc) 40.9 kB
// fb55/css-select by Felix Böhm // // selectAll from index.ts and all dependencies from all files were inlined here // with no modifications other than style changes and imports/exports (at time // of writing) // // The MIT License (MIT) // // Copyright (c) 2016 Nik Coughlin // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // fb55/nth-check by Felix Böhm // // Copyright (c) Felix Böhm // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // // Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY // OUT OF THE USE OF THIS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import { parse, AttributeAction, SelectorType } from './style-selector.js'; function trueFunc() { return true; } function falseFunc() { return false; } /** * Returns a function that checks if an elements index matches the given rule * highly optimized to return the fastest solution. * * @param parsed A tuple [a, b], as returned by `parse`. * @returns A highly optimized function that returns whether an index matches the nth-check. * @example * * ```js * const check = nthCheck.compile([2, 3]); * * check(0); // `false` * check(1); // `false` * check(2); // `true` * check(3); // `false` * check(4); // `true` * check(5); // `false` * check(6); // `true` * ``` */ function compileNcheck(parsed) { const a = parsed[0]; // Subtract 1 from `b`, to convert from one- to zero-indexed. const b = parsed[1] - 1; /* * When `b <= 0`, `a * n` won't be lead to any matches for `a < 0`. * Besides, the specification states that no elements are * matched when `a` and `b` are 0. * * `b < 0` here as we subtracted 1 from `b` above. */ if (b < 0 && a <= 0) return falseFunc; // When `a` is in the range -1..1, it matches any element (so only `b` is checked). if (a === -1) return index => index <= b; if (a === 0) return index => index === b; // When `b <= 0` and `a === 1`, they match any element. if (a === 1) return b < 0 ? trueFunc : (index) => index >= b; /* * Otherwise, modulo can be used to check if there is a match. * * Modulo doesn't care about the sign, so let's use `a`s absolute value. */ const absA = Math.abs(a); // Get `b mod a`, + a if this is negative. const bMod = ((b % absA) + absA) % absA; return a > 1 ? index => index >= b && index % absA === bMod : index => index <= b && index % absA === bMod; } // Following http://www.w3.org/TR/css3-selectors/#nth-child-pseudo // Whitespace as per https://www.w3.org/TR/selectors-3/#lex is " \t\r\n\f" const whitespace = new Set([9, 10, 12, 13, 32]); const ZERO = '0'.charCodeAt(0); const NINE = '9'.charCodeAt(0); /** * Parses an expression. * * @throws An `Error` if parsing fails. * @returns An array containing the integer step size and the integer offset of the nth rule. * @example nthCheck.parse('2n+3'); // returns [2, 3] */ function parseNCheck(formula) { formula = formula.trim().toLowerCase(); if (formula === 'even') { return [2, 0]; } else if (formula === 'odd') { return [2, 1]; } // Parse [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]? let idx = 0; let a = 0; let sign = readSign(); let number = readNumber(); if (idx < formula.length && formula.charAt(idx) === 'n') { idx++; a = sign * (number ?? 1); skipWhitespace(); if (idx < formula.length) { sign = readSign(); skipWhitespace(); number = readNumber(); } else { sign = number = 0; } } // Throw if there is anything else if (number === null || idx < formula.length) { throw new Error(`n-th rule couldn't be parsed ('${formula}')`); } return [a, sign * number]; function readSign() { if (formula.charAt(idx) === '-') { idx++; return -1; } if (formula.charAt(idx) === '+') { idx++; } return 1; } function readNumber() { const start = idx; let value = 0; while (idx < formula.length && formula.charCodeAt(idx) >= ZERO && formula.charCodeAt(idx) <= NINE) { value = value * 10 + (formula.charCodeAt(idx) - ZERO); idx++; } // Return `null` if we didn't read anything. return idx === start ? null : value; } function skipWhitespace() { while (idx < formula.length && whitespace.has(formula.charCodeAt(idx))) { idx++; } } } function getNCheck(formula) { return compileNcheck(parseNCheck(formula)); } const defaultEquals = (a, b) => a === b; function convertOptionFormats(options) { /* * We force one format of options to the other one. */ // @ts-expect-error Default options may have incompatible `Node` / `ElementNode`. const opts = options ?? defaultOptions; // @ts-expect-error Same as above. opts.adapter ??= DomUtils; // @ts-expect-error `equals` does not exist on `Options` opts.equals ??= opts.adapter?.equals ?? defaultEquals; return opts; } function getNextSiblings(elem, adapter) { const siblings = adapter.getSiblings(elem); if (siblings.length <= 1) return []; const elemIndex = siblings.indexOf(elem); if (elemIndex < 0 || elemIndex === siblings.length - 1) return []; return siblings.slice(elemIndex + 1).filter(adapter.isTag); } function appendNextSiblings(elem, adapter) { // Order matters because jQuery seems to check the children before the siblings const elems = Array.isArray(elem) ? elem.slice(0) : [elem]; const elemsLength = elems.length; for (let i = 0; i < elemsLength; i++) { const nextSiblings = getNextSiblings(elems[i], adapter); elems.push(...nextSiblings); } return elems; } function prepareContext(elems, adapter, shouldTestNextSiblings = false) { /* * Add siblings if the query requires them. * See https://github.com/fb55/css-select/pull/43#issuecomment-225414692 */ if (shouldTestNextSiblings) { elems = appendNextSiblings(elems, adapter); } return Array.isArray(elems) ? adapter.removeSubsets(elems) : adapter.getChildren(elems); } const procedure = new Map([ [SelectorType.Universal, 50], [SelectorType.Tag, 30], [SelectorType.Attribute, 1], [SelectorType.Pseudo, 0], ]); function isTraversal(token) { return !procedure.has(token.type); } const attributes = new Map([ [AttributeAction.Exists, 10], [AttributeAction.Equals, 8], [AttributeAction.Not, 7], [AttributeAction.Start, 6], [AttributeAction.End, 6], [AttributeAction.Any, 5], ]); /** * Sort the parts of the passed selector, * as there is potential for optimization * (some types of selectors are faster than others) * * @param arr Selector to sort */ function sortByProcedure(arr) { const procs = arr.map(getProcedure); for (let i = 1; i < arr.length; i++) { const procNew = procs[i]; if (procNew < 0) continue; for (let j = i - 1; j >= 0 && procNew < procs[j]; j--) { const token = arr[j + 1]; arr[j + 1] = arr[j]; arr[j] = token; procs[j + 1] = procs[j]; procs[j] = procNew; } } } function getProcedure(token) { let proc = procedure.get(token.type) ?? -1; if (token.type === SelectorType.Attribute) { proc = attributes.get(token.action) ?? 4; if (token.action === AttributeAction.Equals && token.name === 'id') { // Prefer ID selectors (eg. #ID) proc = 9; } if (token.ignoreCase) { /* * IgnoreCase adds some overhead, prefer 'normal' token * this is a binary operation, to ensure it's still an int */ proc >>= 1; } } else if (token.type === SelectorType.Pseudo) { if (!token.data) { proc = 3; } else if (token.name === 'has' || token.name === 'contains') { proc = 0; // Expensive in any case } else if (Array.isArray(token.data)) { // Eg. :matches, :not proc = Math.min(...token.data.map((d) => Math.min(...d.map(getProcedure)))); // If we have traversals, try to avoid executing this selector if (proc < 0) { proc = 0; } } else { proc = 2; } } return proc; } const DESCENDANT_TOKEN = { type: SelectorType.Descendant }; const FLEXIBLE_DESCENDANT_TOKEN = { type: '_flexibleDescendant', }; const SCOPE_TOKEN = { type: SelectorType.Pseudo, name: 'scope', data: null, }; /** Used as a placeholder for :has. Will be replaced with the actual element. */ const PLACEHOLDER_ELEMENT = {}; function includesScopePseudo(t) { return (t.type === SelectorType.Pseudo && (t.name === 'scope' || (Array.isArray(t.data) && t.data.some((data) => data.some(includesScopePseudo))))); } /* * CSS 4 Spec (Draft): 3.4.1. Absolutizing a Relative Selector * http://www.w3.org/TR/selectors4/#absolutizing */ function absolutize(token, { adapter }, context) { // TODO Use better check if the context is a document const hasContext = !!context?.every((e) => { const parent = adapter.isTag(e) && adapter.getParent(e); return e === PLACEHOLDER_ELEMENT || (parent && adapter.isTag(parent)); }); for (const t of token) { if (t.length > 0 && isTraversal(t[0]) && t[0].type !== SelectorType.Descendant) { // Don't continue in else branch } else if (hasContext && !t.some(includesScopePseudo)) { t.unshift(DESCENDANT_TOKEN); } else { continue; } t.unshift(SCOPE_TOKEN); } } /** * Attributes that are case-insensitive in HTML. * * @private * @see https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors */ const caseInsensitiveAttributes = new Set([ 'accept', 'accept-charset', 'align', 'alink', 'axis', 'bgcolor', 'charset', 'checked', 'clear', 'codetype', 'color', 'compact', 'declare', 'defer', 'dir', 'direction', 'disabled', 'enctype', 'face', 'frame', 'hreflang', 'http-equiv', 'lang', 'language', 'link', 'media', 'method', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'rel', 'rev', 'rules', 'scope', 'scrolling', 'selected', 'shape', 'target', 'text', 'type', 'valign', 'valuetype', 'vlink', ]); function shouldIgnoreCase(selector, options) { return typeof selector.ignoreCase === 'boolean' ? selector.ignoreCase : selector.ignoreCase === 'quirks' ? !!options.quirksMode : !options.xmlMode && caseInsensitiveAttributes.has(selector.name); } /** * All reserved characters in a regex, used for escaping. * * Taken from XRegExp, (c) 2007-2020 Steven Levithan under the MIT license * https://github.com/slevithan/xregexp/blob/95eeebeb8fac8754d54eafe2b4743661ac1cf028/src/xregexp.js#L794 */ const reChars = /[-[\]{}()*+?.,\\^$|#\s]/g; function escapeRegex(value) { return value.replace(reChars, '\\$&'); } /** * Attribute selectors */ const attributeRules = { equals(next, data, options) { const { adapter } = options; const { name } = data; let { value } = data; if (shouldIgnoreCase(data, options)) { value = value.toLowerCase(); return (elem) => { const attr = adapter.getAttributeValue(elem, name); return (attr != null && attr.length === value.length && attr.toLowerCase() === value && next(elem)); }; } return (elem) => adapter.getAttributeValue(elem, name) === value && next(elem); }, hyphen(next, data, options) { const { adapter } = options; const { name } = data; let { value } = data; const len = value.length; if (shouldIgnoreCase(data, options)) { value = value.toLowerCase(); return function hyphenIC(elem) { const attr = adapter.getAttributeValue(elem, name); return (attr != null && (attr.length === len || attr.charAt(len) === '-') && attr.substr(0, len).toLowerCase() === value && next(elem)); }; } return function hyphen(elem) { const attr = adapter.getAttributeValue(elem, name); return (attr != null && (attr.length === len || attr.charAt(len) === '-') && attr.substr(0, len) === value && next(elem)); }; }, element(next, data, options) { const { adapter } = options; const { name, value } = data; if (/\s/.test(value)) { return falseFunc; } const regex = new RegExp(`(?:^|\\s)${escapeRegex(value)}(?:$|\\s)`, shouldIgnoreCase(data, options) ? 'i' : ''); return function element(elem) { const attr = adapter.getAttributeValue(elem, name); return (attr != null && attr.length >= value.length && regex.test(attr) && next(elem)); }; }, exists(next, { name }, { adapter }) { return (elem) => adapter.hasAttrib(elem, name) && next(elem); }, start(next, data, options) { const { adapter } = options; const { name } = data; let { value } = data; const len = value.length; if (len === 0) { return falseFunc; } if (shouldIgnoreCase(data, options)) { value = value.toLowerCase(); return (elem) => { const attr = adapter.getAttributeValue(elem, name); return (attr != null && attr.length >= len && attr.substr(0, len).toLowerCase() === value && next(elem)); }; } return (elem) => !!adapter.getAttributeValue(elem, name)?.startsWith(value) && next(elem); }, end(next, data, options) { const { adapter } = options; const { name } = data; let { value } = data; const len = -value.length; if (len === 0) { return falseFunc; } if (shouldIgnoreCase(data, options)) { value = value.toLowerCase(); return (elem) => adapter .getAttributeValue(elem, name) ?.substr(len) .toLowerCase() === value && next(elem); } return (elem) => !!adapter.getAttributeValue(elem, name)?.endsWith(value) && next(elem); }, any(next, data, options) { const { adapter } = options; const { name, value } = data; if (value === '') { return falseFunc; } if (shouldIgnoreCase(data, options)) { const regex = new RegExp(escapeRegex(value), 'i'); return function anyIC(elem) { const attr = adapter.getAttributeValue(elem, name); return (attr != null && attr.length >= value.length && regex.test(attr) && next(elem)); }; } return (elem) => !!adapter.getAttributeValue(elem, name)?.includes(value) && next(elem); }, not(next, data, options) { const { adapter } = options; const { name } = data; let { value } = data; if (value === '') { return (elem) => !!adapter.getAttributeValue(elem, name) && next(elem); } else if (shouldIgnoreCase(data, options)) { value = value.toLowerCase(); return (elem) => { const attr = adapter.getAttributeValue(elem, name); return ((attr == null || attr.length !== value.length || attr.toLowerCase() !== value) && next(elem)); }; } return (elem) => adapter.getAttributeValue(elem, name) !== value && next(elem); }, }; function copyOptions(options) { // Not copied: context, rootFunc return { xmlMode: !!options.xmlMode, lowerCaseAttributeNames: !!options.lowerCaseAttributeNames, lowerCaseTags: !!options.lowerCaseTags, quirksMode: !!options.quirksMode, cacheResults: !!options.cacheResults, pseudos: options.pseudos, adapter: options.adapter, equals: options.equals, }; } const is = (next, token, options, context, compileToken) => { const func = compileToken(token, copyOptions(options), context); return func === trueFunc ? next : func === falseFunc ? falseFunc : (elem) => func(elem) && next(elem); }; function ensureIsTag(next, adapter) { if (next === falseFunc) return falseFunc; return (elem) => adapter.isTag(elem) && next(elem); } /* * :not, :has, :is, :matches and :where have to compile selectors * doing this in src/pseudos.ts would lead to circular dependencies, * so we add them here */ const subselects = { is, /** * `:matches` and `:where` are aliases for `:is`. */ matches: is, where: is, not(next, token, options, context, compileToken) { const func = compileToken(token, copyOptions(options), context); return func === falseFunc ? next : func === trueFunc ? falseFunc : (elem) => !func(elem) && next(elem); }, has(next, subselect, options, _context, compileToken) { const { adapter } = options; const opts = copyOptions(options); opts.relativeSelector = true; const context = subselect.some((s) => s.some(isTraversal)) ? // Used as a placeholder. Will be replaced with the actual element. [PLACEHOLDER_ELEMENT] : undefined; const compiled = compileToken(subselect, opts, context); if (compiled === falseFunc) return falseFunc; const hasElement = ensureIsTag(compiled, adapter); // If `compiled` is `trueFunc`, we can skip this. if (context && compiled !== trueFunc) { /* * `shouldTestNextSiblings` will only be true if the query starts with * a traversal (sibling or adjacent). That means we will always have a context. */ const { shouldTestNextSiblings = false } = compiled; return (elem) => { if (!next(elem)) return false; context[0] = elem; const childs = adapter.getChildren(elem); const nextElements = shouldTestNextSiblings ? [...childs, ...getNextSiblings(elem, adapter)] : childs; return adapter.existsOne(hasElement, nextElements); }; } return (elem) => next(elem) && adapter.existsOne(hasElement, adapter.getChildren(elem)); }, }; /** * Aliases are pseudos that are expressed as selectors. */ const aliases = { // Links 'any-link': ':is(a, area, link)[href]', link: ':any-link:not(:visited)', // Forms // https://html.spec.whatwg.org/multipage/scripting.html#disabled-elements disabled: `:is( :is(button, input, select, textarea, optgroup, option)[disabled], optgroup[disabled] > option, fieldset[disabled]:not(fieldset[disabled] legend:first-of-type *) )`, enabled: ':not(:disabled)', checked: ':is(:is(input[type=radio], input[type=checkbox])[checked], option:selected)', required: ':is(input, select, textarea)[required]', optional: ':is(input, select, textarea):not([required])', // JQuery extensions // https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-selectedness selected: 'option:is([selected], select:not([multiple]):not(:has(> option[selected])) > :first-of-type)', checkbox: '[type=checkbox]', file: '[type=file]', password: '[type=password]', radio: '[type=radio]', reset: '[type=reset]', image: '[type=image]', submit: '[type=submit]', parent: ':not(:empty)', header: ':is(h1, h2, h3, h4, h5, h6)', button: ':is(button, input[type=button])', input: ':is(input, textarea, select, button)', text: 'input:is(:not([type!=""]), [type=text])', }; function getChildFunc(next, adapter) { return (elem) => { const parent = adapter.getParent(elem); return parent != null && adapter.isTag(parent) && next(elem); }; } /** * Dynamic state pseudos. These depend on optional Adapter methods. * * @param name The name of the adapter method to call. * @returns Pseudo for the `filters` object. */ function dynamicStatePseudo(name) { return function dynamicPseudo(next, _rule, { adapter }) { const func = adapter[name]; if (typeof func !== 'function') { return falseFunc; } return function active(elem) { return func(elem) && next(elem); }; }; } const filters = { contains(next, text, { adapter }) { return function contains(elem) { return next(elem) && adapter.getText(elem).includes(text); }; }, icontains(next, text, { adapter }) { const itext = text.toLowerCase(); return function icontains(elem) { return (next(elem) && adapter.getText(elem).toLowerCase().includes(itext)); }; }, // Location specific methods 'nth-child'(next, rule, { adapter, equals }) { const func = getNCheck(rule); if (func === falseFunc) return falseFunc; if (func === trueFunc) return getChildFunc(next, adapter); return function nthChild(elem) { const siblings = adapter.getSiblings(elem); let pos = 0; for (let i = 0; i < siblings.length; i++) { if (equals(elem, siblings[i])) break; if (adapter.isTag(siblings[i])) { pos++; } } return func(pos) && next(elem); }; }, 'nth-last-child'(next, rule, { adapter, equals }) { const func = getNCheck(rule); if (func === falseFunc) return falseFunc; if (func === trueFunc) return getChildFunc(next, adapter); return function nthLastChild(elem) { const siblings = adapter.getSiblings(elem); let pos = 0; for (let i = siblings.length - 1; i >= 0; i--) { if (equals(elem, siblings[i])) break; if (adapter.isTag(siblings[i])) { pos++; } } return func(pos) && next(elem); }; }, 'nth-of-type'(next, rule, { adapter, equals }) { const func = getNCheck(rule); if (func === falseFunc) return falseFunc; if (func === trueFunc) return getChildFunc(next, adapter); return function nthOfType(elem) { const siblings = adapter.getSiblings(elem); let pos = 0; for (let i = 0; i < siblings.length; i++) { const currentSibling = siblings[i]; if (equals(elem, currentSibling)) break; if (adapter.isTag(currentSibling) && adapter.getName(currentSibling) === adapter.getName(elem)) { pos++; } } return func(pos) && next(elem); }; }, 'nth-last-of-type'(next, rule, { adapter, equals }) { const func = getNCheck(rule); if (func === falseFunc) return falseFunc; if (func === trueFunc) return getChildFunc(next, adapter); return function nthLastOfType(elem) { const siblings = adapter.getSiblings(elem); let pos = 0; for (let i = siblings.length - 1; i >= 0; i--) { const currentSibling = siblings[i]; if (equals(elem, currentSibling)) break; if (adapter.isTag(currentSibling) && adapter.getName(currentSibling) === adapter.getName(elem)) { pos++; } } return func(pos) && next(elem); }; }, // TODO determine the actual root element root(next, _rule, { adapter }) { return (elem) => { const parent = adapter.getParent(elem); return (parent == null || !adapter.isTag(parent)) && next(elem); }; }, scope(next, rule, options, context) { const { equals } = options; if (!context || context.length === 0) { // Equivalent to :root return filters['root'](next, rule, options); } if (context.length === 1) { // NOTE: can't be unpacked, as :has uses this for side-effects return (elem) => equals(context[0], elem) && next(elem); } return (elem) => context.includes(elem) && next(elem); }, hover: dynamicStatePseudo('isHovered'), visited: dynamicStatePseudo('isVisited'), active: dynamicStatePseudo('isActive'), }; /** * CSS limits the characters considered as whitespace to space, tab & line * feed. We add carriage returns as htmlparser2 doesn't normalize them to * line feeds. * * @see {@link https://www.w3.org/TR/css-text-3/#white-space} */ const isDocumentWhiteSpace = /^[ \t\r\n]*$/; // While filters are precompiled, pseudos get called when they are needed const pseudos = { empty(elem, { adapter }) { return !adapter.getChildren(elem).some((elem) => adapter.isTag(elem) || // FIXME: `getText` call is potentially expensive. !isDocumentWhiteSpace.test(adapter.getText(elem))); }, 'first-child'(elem, { adapter, equals }) { if (adapter.prevElementSibling) { return adapter.prevElementSibling(elem) == null; } const firstChild = adapter .getSiblings(elem) .find((elem) => adapter.isTag(elem)); return firstChild != null && equals(elem, firstChild); }, 'last-child'(elem, { adapter, equals }) { const siblings = adapter.getSiblings(elem); for (let i = siblings.length - 1; i >= 0; i--) { if (equals(elem, siblings[i])) return true; if (adapter.isTag(siblings[i])) break; } return false; }, 'first-of-type'(elem, { adapter, equals }) { const siblings = adapter.getSiblings(elem); const elemName = adapter.getName(elem); for (let i = 0; i < siblings.length; i++) { const currentSibling = siblings[i]; if (equals(elem, currentSibling)) return true; if (adapter.isTag(currentSibling) && adapter.getName(currentSibling) === elemName) { break; } } return false; }, 'last-of-type'(elem, { adapter, equals }) { const siblings = adapter.getSiblings(elem); const elemName = adapter.getName(elem); for (let i = siblings.length - 1; i >= 0; i--) { const currentSibling = siblings[i]; if (equals(elem, currentSibling)) return true; if (adapter.isTag(currentSibling) && adapter.getName(currentSibling) === elemName) { break; } } return false; }, 'only-of-type'(elem, { adapter, equals }) { const elemName = adapter.getName(elem); return adapter .getSiblings(elem) .every((sibling) => equals(elem, sibling) || !adapter.isTag(sibling) || adapter.getName(sibling) !== elemName); }, 'only-child'(elem, { adapter, equals }) { return adapter .getSiblings(elem) .every((sibling) => equals(elem, sibling) || !adapter.isTag(sibling)); }, }; function verifyPseudoArgs(func, name, subselect, argIndex) { if (subselect === null) { if (func.length > argIndex) { throw new Error(`Pseudo-class :${name} requires an argument`); } } else if (func.length === argIndex) { throw new Error(`Pseudo-class :${name} doesn't have any arguments`); } } function compilePseudoSelector(next, selector, options, context, compileToken) { const { name, data } = selector; if (Array.isArray(data)) { if (!(name in subselects)) { throw new Error(`Unknown pseudo-class :${name}(${data})`); } return subselects[name](next, data, options, context, compileToken); } const userPseudo = options.pseudos?.[name]; const stringPseudo = typeof userPseudo === 'string' ? userPseudo : aliases[name]; if (typeof stringPseudo === 'string') { if (data != null) { throw new Error(`Pseudo ${name} doesn't have any arguments`); } // The alias has to be parsed here, to make sure options are respected. const alias = parse(stringPseudo); return subselects['is'](next, alias, options, context, compileToken); } if (typeof userPseudo === 'function') { verifyPseudoArgs(userPseudo, name, data, 1); return (elem) => userPseudo(elem, data) && next(elem); } if (name in filters) { return filters[name](next, data, options, context); } if (name in pseudos) { const pseudo = pseudos[name]; verifyPseudoArgs(pseudo, name, data, 2); return (elem) => pseudo(elem, options, data) && next(elem); } throw new Error(`Unknown pseudo-class :${name}`); } function getElementParent(node, adapter) { const parent = adapter.getParent(node); if (parent && adapter.isTag(parent)) { return parent; } return null; } /* * All available rules */ function compileGeneralSelector(next, selector, options, context, compileToken) { const { adapter, equals } = options; switch (selector.type) { case SelectorType.PseudoElement: { throw new Error('Pseudo-elements are not supported by css-select'); } case SelectorType.ColumnCombinator: { throw new Error('Column combinators are not yet supported by css-select'); } case SelectorType.Attribute: { if (selector.namespace != null) { throw new Error('Namespaced attributes are not yet supported by css-select'); } if (!options.xmlMode || options.lowerCaseAttributeNames) { selector.name = selector.name.toLowerCase(); } return attributeRules[selector.action](next, selector, options); } case SelectorType.Pseudo: { return compilePseudoSelector(next, selector, options, context, compileToken); } // Tags case SelectorType.Tag: { if (selector.namespace != null) { throw new Error('Namespaced tag names are not yet supported by css-select'); } let { name } = selector; if (!options.xmlMode || options.lowerCaseTags) { name = name.toLowerCase(); } return function tag(elem) { return adapter.getName(elem) === name && next(elem); }; } // Traversal case SelectorType.Descendant: { if (options.cacheResults === false || typeof WeakSet === 'undefined') { return function descendant(elem) { let current = elem; while ((current = getElementParent(current, adapter))) { if (next(current)) { return true; } } return false; }; } // @ts-expect-error `ElementNode` is not extending object const isFalseCache = new WeakSet(); return function cachedDescendant(elem) { let current = elem; while ((current = getElementParent(current, adapter))) { if (!isFalseCache.has(current)) { if (adapter.isTag(current) && next(current)) { return true; } isFalseCache.add(current); } } return false; }; } case '_flexibleDescendant': { // Include element itself, only used while querying an array return function flexibleDescendant(elem) { let current = elem; do { if (next(current)) return true; } while ((current = getElementParent(current, adapter))); return false; }; } case SelectorType.Parent: { return function parent(elem) { return adapter .getChildren(elem) .some((elem) => adapter.isTag(elem) && next(elem)); }; } case SelectorType.Child: { return function child(elem) { const parent = adapter.getParent(elem); return parent != null && adapter.isTag(parent) && next(parent); }; } case SelectorType.Sibling: { return function sibling(elem) { const siblings = adapter.getSiblings(elem); for (let i = 0; i < siblings.length; i++) { const currentSibling = siblings[i]; if (equals(elem, currentSibling)) break; if (adapter.isTag(currentSibling) && next(currentSibling)) { return true; } } return false; }; } case SelectorType.Adjacent: { if (adapter.prevElementSibling) { return function adjacent(elem) { const previous = adapter.prevElementSibling(elem); return previous != null && next(previous); }; } return function adjacent(elem) { const siblings = adapter.getSiblings(elem); let lastElement; for (let i = 0; i < siblings.length; i++) { const currentSibling = siblings[i]; if (equals(elem, currentSibling)) break; if (adapter.isTag(currentSibling)) { lastElement = currentSibling; } } return !!lastElement && next(lastElement); }; } case SelectorType.Universal: { if (selector.namespace != null && selector.namespace !== '*') { throw new Error('Namespaced universal selectors are not yet supported by css-select'); } return next; } } } function compileRules(rules, options, context, rootFunc) { return rules.reduce((previous, rule) => previous === falseFunc ? falseFunc : compileGeneralSelector(previous, rule, options, context, compileToken), rootFunc); } function compileToken(token, options, ctx) { token.forEach(sortByProcedure); const { context = ctx, rootFunc = trueFunc } = options; const isArrayContext = Array.isArray(context); const finalContext = context && (Array.isArray(context) ? context : [context]); // Check if the selector is relative if (options.relativeSelector !== false) { absolutize(token, options, finalContext); } else if (token.some((t) => t.length > 0 && isTraversal(t[0]))) { throw new Error('Relative selectors are not allowed when the `relativeSelector` option is disabled'); } let shouldTestNextSiblings = false; const query = token .map((rules) => { if (rules.length >= 2) { const [first, second] = rules; if (first.type !== SelectorType.Pseudo || first.name !== 'scope') { // Ignore } else if (isArrayContext && second.type === SelectorType.Descendant) { rules[1] = FLEXIBLE_DESCENDANT_TOKEN; } else if (second.type === SelectorType.Adjacent || second.type === SelectorType.Sibling) { shouldTestNextSiblings = true; } } return compileRules(rules, options, finalContext, rootFunc); }) .reduce((a, b) => b === falseFunc || a === rootFunc ? a : a === falseFunc || b === rootFunc ? b : function combine(elem) { return a(elem) || b(elem); }, falseFunc); query.shouldTestNextSiblings = shouldTestNextSiblings; return query; } function compileUnsafe(selector, options, context) { const token = typeof selector === 'string' ? parse(selector) : selector; return compileToken(token, options, context); } function getSelectorFunc(searchFunc) { return function select(query, elements, options) { const opts = convertOptionFormats(options); if (typeof query !== 'function') { query = compileUnsafe(query, opts, elements); } const filteredElements = prepareContext(elements, opts.adapter, query.shouldTestNextSiblings); return searchFunc(query, filteredElements, opts); }; } export const query = getSelectorFunc((query, elems, options) => query === falseFunc || !elems || elems.length === 0 ? null : options.adapter.findOne(query, elems)); export const queryAll = getSelectorFunc((query, elems, options) => query === falseFunc || !elems || elems.length === 0 ? [] : options.adapter.findAll(query, elems));