UNPKG

css-select

Version:

a CSS selector compiler/engine

138 lines (124 loc) 4.61 kB
import type { PseudoSelector } from "css-what"; import type { InternalOptions } from "../types.js"; type Pseudo = <Node, ElementNode extends Node>( element: ElementNode, options: InternalOptions<Node, ElementNode>, subselect?: string | null, ) => boolean; /** * 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 /** Runtime pseudo selector implementations. */ export const pseudos: Record<string, Pseudo> = { empty(element, { adapter }) { const children = adapter.getChildren(element); return ( // First, make sure the tag does not have any element children. children.every((element) => !adapter.isTag(element)) && // Then, check that the text content is only whitespace. children.every((element) => // FIXME: `getText` call is potentially expensive. isDocumentWhiteSpace.test(adapter.getText(element)), ) ); }, "first-child"(element, { adapter, equals }) { if (adapter.prevElementSibling) { return adapter.prevElementSibling(element) == null; } const firstChild = adapter .getSiblings(element) .find((sibling) => adapter.isTag(sibling)); return firstChild != null && equals(element, firstChild); }, "last-child"(element, { adapter, equals }) { const siblings = adapter.getSiblings(element); for (let index = siblings.length - 1; index >= 0; index--) { if (equals(element, siblings[index])) { return true; } if (adapter.isTag(siblings[index])) { break; } } return false; }, "first-of-type"(element, { adapter, equals }) { const siblings = adapter.getSiblings(element); const elementName = adapter.getName(element); for (const currentSibling of siblings) { if (equals(element, currentSibling)) { return true; } if ( adapter.isTag(currentSibling) && adapter.getName(currentSibling) === elementName ) { break; } } return false; }, "last-of-type"(element, { adapter, equals }) { const siblings = adapter.getSiblings(element); const elementName = adapter.getName(element); for (let index = siblings.length - 1; index >= 0; index--) { const currentSibling = siblings[index]; if (equals(element, currentSibling)) { return true; } if ( adapter.isTag(currentSibling) && adapter.getName(currentSibling) === elementName ) { break; } } return false; }, "only-of-type"(element, { adapter, equals }) { const elementName = adapter.getName(element); return adapter .getSiblings(element) .every( (sibling) => equals(element, sibling) || !adapter.isTag(sibling) || adapter.getName(sibling) !== elementName, ); }, "only-child"(element, { adapter, equals }) { return adapter .getSiblings(element) .every( (sibling) => equals(element, sibling) || !adapter.isTag(sibling), ); }, }; /** * Validate pseudo selector argument arity. * @param pseudoClassCondition Pseudo-function implementation to wrap. * @param name Name of the pseudo selector. * @param subselect Subselector passed to the pseudo-function. * @param argumentIndex Index of the argument parser to apply. */ export function verifyPseudoArguments<T extends unknown[]>( pseudoClassCondition: (...parameters: T) => boolean, name: string, subselect: PseudoSelector["data"], argumentIndex: number, ): void { if (subselect === null) { if (pseudoClassCondition.length > argumentIndex) { throw new Error(`Pseudo-class :${name} requires an argument`); } } else if (pseudoClassCondition.length === argumentIndex) { throw new Error(`Pseudo-class :${name} doesn't have any arguments`); } }