Primeiro commit do projeto Angular
This commit is contained in:
+274
@@ -0,0 +1,274 @@
|
||||
import * as boolbase from "boolbase";
|
||||
import type { AttributeAction, AttributeSelector } from "css-what";
|
||||
import type { CompiledQuery, InternalOptions } from "./types.js";
|
||||
|
||||
/**
|
||||
* 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: string): string {
|
||||
return value.replace(reChars, "\\$&");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Node, ElementNode extends Node>(
|
||||
selector: AttributeSelector,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
): boolean {
|
||||
return typeof selector.ignoreCase === "boolean"
|
||||
? selector.ignoreCase
|
||||
: selector.ignoreCase === "quirks"
|
||||
? !!options.quirksMode
|
||||
: !options.xmlMode && caseInsensitiveAttributes.has(selector.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute selectors
|
||||
*/
|
||||
export const attributeRules: Record<
|
||||
AttributeAction,
|
||||
<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
data: AttributeSelector,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
) => CompiledQuery<ElementNode>
|
||||
> = {
|
||||
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 boolbase.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 boolbase.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 boolbase.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 boolbase.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);
|
||||
}
|
||||
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);
|
||||
},
|
||||
};
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import * as boolbase from "boolbase";
|
||||
import type { Selector } from "css-what";
|
||||
import { SelectorType } from "css-what";
|
||||
import { compileGeneralSelector } from "./general.js";
|
||||
import { getElementParent } from "./helpers/querying.js";
|
||||
import {
|
||||
getQuality,
|
||||
includesScopePseudo,
|
||||
isTraversal,
|
||||
sortRules,
|
||||
} from "./helpers/selectors.js";
|
||||
import { PLACEHOLDER_ELEMENT } from "./pseudo-selectors/subselects.js";
|
||||
import type {
|
||||
CompiledQuery,
|
||||
InternalOptions,
|
||||
InternalSelector,
|
||||
Predicate,
|
||||
} from "./types.js";
|
||||
|
||||
const DESCENDANT_TOKEN: Selector = { type: SelectorType.Descendant };
|
||||
const FLEXIBLE_DESCENDANT_TOKEN: InternalSelector = {
|
||||
type: "_flexibleDescendant",
|
||||
};
|
||||
const SCOPE_TOKEN: Selector = {
|
||||
type: SelectorType.Pseudo,
|
||||
name: "scope",
|
||||
data: null,
|
||||
};
|
||||
|
||||
/*
|
||||
* CSS 4 Spec (Draft): 3.4.1. Absolutizing a Relative Selector
|
||||
* http://www.w3.org/TR/selectors4/#absolutizing
|
||||
*/
|
||||
function absolutize<Node, ElementNode extends Node>(
|
||||
token: InternalSelector[][],
|
||||
{ adapter }: InternalOptions<Node, ElementNode>,
|
||||
context?: Node[],
|
||||
) {
|
||||
// TODO Use better check if the context is a document
|
||||
const hasContext = !!context?.every(
|
||||
(e) =>
|
||||
e === PLACEHOLDER_ELEMENT ||
|
||||
(adapter.isTag(e) && getElementParent(e, adapter) !== null),
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export function compileToken<Node, ElementNode extends Node>(
|
||||
token: InternalSelector[][],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
ctx?: Node[] | Node,
|
||||
): CompiledQuery<ElementNode> {
|
||||
token.forEach(sortRules);
|
||||
|
||||
const { context = ctx, rootFunc = boolbase.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;
|
||||
let query: CompiledQuery<ElementNode> = boolbase.falseFunc;
|
||||
|
||||
combineLoop: for (const rules of token) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
let next = rootFunc;
|
||||
let hasExpensiveSubselector = false;
|
||||
|
||||
for (const rule of rules) {
|
||||
next = compileGeneralSelector(
|
||||
next,
|
||||
rule,
|
||||
options,
|
||||
finalContext,
|
||||
compileToken,
|
||||
hasExpensiveSubselector,
|
||||
);
|
||||
|
||||
const quality = getQuality(rule);
|
||||
|
||||
if (quality === 0) {
|
||||
hasExpensiveSubselector = true;
|
||||
}
|
||||
|
||||
// If the sub-selector won't match any elements, skip it.
|
||||
if (next === boolbase.falseFunc) {
|
||||
continue combineLoop;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a function that always returns true, we can stop here.
|
||||
if (next === rootFunc) {
|
||||
return rootFunc;
|
||||
}
|
||||
|
||||
query = query === boolbase.falseFunc ? next : or(query, next);
|
||||
}
|
||||
|
||||
query.shouldTestNextSiblings = shouldTestNextSiblings;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function or<T>(a: Predicate<T>, b: Predicate<T>): Predicate<T> {
|
||||
return (elem) => a(elem) || b(elem);
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
import { SelectorType } from "css-what";
|
||||
import { attributeRules } from "./attributes.js";
|
||||
import { getElementParent } from "./helpers/querying.js";
|
||||
import { compilePseudoSelector } from "./pseudo-selectors/index.js";
|
||||
import type {
|
||||
CompiledQuery,
|
||||
CompileToken,
|
||||
InternalOptions,
|
||||
InternalSelector,
|
||||
} from "./types.js";
|
||||
|
||||
/*
|
||||
* All available rules
|
||||
*/
|
||||
|
||||
export function compileGeneralSelector<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
selector: InternalSelector,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context: Node[] | undefined,
|
||||
compileToken: CompileToken<Node, ElementNode>,
|
||||
hasExpensiveSubselector: boolean,
|
||||
): CompiledQuery<ElementNode> {
|
||||
const { adapter, equals, cacheResults } = 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: ElementNode): boolean {
|
||||
return adapter.getName(elem) === name && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
// Traversal
|
||||
case SelectorType.Descendant: {
|
||||
if (
|
||||
!hasExpensiveSubselector ||
|
||||
cacheResults === false ||
|
||||
typeof WeakMap === "undefined"
|
||||
) {
|
||||
return function descendant(elem: ElementNode): boolean {
|
||||
let current: ElementNode | null = elem;
|
||||
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: TODO
|
||||
while ((current = getElementParent(current, adapter))) {
|
||||
if (next(current)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
const resultCache = new WeakMap<
|
||||
// @ts-expect-error `ElementNode` is not extending object
|
||||
ElementNode,
|
||||
{ matches: boolean }
|
||||
>();
|
||||
return function cachedDescendant(elem: ElementNode): boolean {
|
||||
let current: ElementNode | null = elem;
|
||||
let result;
|
||||
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: TODO
|
||||
while ((current = getElementParent(current, adapter))) {
|
||||
const cached = resultCache.get(current);
|
||||
|
||||
if (cached === undefined) {
|
||||
result ??= { matches: false };
|
||||
result.matches = next(current);
|
||||
resultCache.set(current, result);
|
||||
if (result.matches) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (result) {
|
||||
result.matches = cached.matches;
|
||||
}
|
||||
return cached.matches;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
case "_flexibleDescendant": {
|
||||
// Include element itself, only used while querying an array
|
||||
return function flexibleDescendant(elem: ElementNode): boolean {
|
||||
let current: ElementNode | null = elem;
|
||||
|
||||
do {
|
||||
if (next(current)) {
|
||||
return true;
|
||||
}
|
||||
current = getElementParent(current, adapter);
|
||||
} while (current);
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
case SelectorType.Parent: {
|
||||
return function parent(elem: ElementNode): boolean {
|
||||
return adapter
|
||||
.getChildren(elem)
|
||||
.some((elem) => adapter.isTag(elem) && next(elem));
|
||||
};
|
||||
}
|
||||
case SelectorType.Child: {
|
||||
return function child(elem: ElementNode): boolean {
|
||||
const parent = getElementParent(elem, adapter);
|
||||
return parent !== null && next(parent);
|
||||
};
|
||||
}
|
||||
case SelectorType.Sibling: {
|
||||
return function sibling(elem: ElementNode): boolean {
|
||||
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: ElementNode): boolean {
|
||||
const previous = adapter.prevElementSibling!(elem);
|
||||
return previous != null && next(previous);
|
||||
};
|
||||
}
|
||||
|
||||
return function adjacent(elem: ElementNode): boolean {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import * as boolbase from "boolbase";
|
||||
import type { Element, Node } from "domhandler";
|
||||
import * as DomUtils from "domutils";
|
||||
import { parseDocument } from "htmlparser2";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import * as CSSselect from "../index.js";
|
||||
import type { InternalOptions } from "../types.js";
|
||||
import { cacheParentResults } from "./cache.js";
|
||||
|
||||
const cacheParentResultsOptions = {
|
||||
adapter: DomUtils,
|
||||
} as unknown as InternalOptions<Node, Element>;
|
||||
|
||||
describe("cacheParentResults", () => {
|
||||
it("should rely on parent for matches", () => {
|
||||
const documentWithoutFoo = parseDocument(
|
||||
"<a><b><c><d><e>bar</e></d></c><f><g>bar</g></f></b></a>",
|
||||
);
|
||||
|
||||
const fn = vi.fn((elem) => DomUtils.getText(elem).includes("foo"));
|
||||
const hasfoo = cacheParentResults<Node, Element>(
|
||||
boolbase.trueFunc,
|
||||
cacheParentResultsOptions,
|
||||
fn,
|
||||
);
|
||||
|
||||
const options = {
|
||||
pseudos: {
|
||||
hasfoo,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
CSSselect.selectAll<Node, Element>(
|
||||
":hasfoo",
|
||||
documentWithoutFoo,
|
||||
options,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should cache results for subtrees", () => {
|
||||
const documentWithFoo = parseDocument(
|
||||
"<a><b><c><d><e>foo</e></d></c><f><g>bar</g></f></b></a>",
|
||||
);
|
||||
|
||||
const fn = vi.fn((elem) => DomUtils.getText(elem).includes("foo"));
|
||||
const hasfoo = cacheParentResults<Node, Element>(
|
||||
boolbase.trueFunc,
|
||||
cacheParentResultsOptions,
|
||||
fn,
|
||||
);
|
||||
|
||||
const options = {
|
||||
pseudos: {
|
||||
hasfoo,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
CSSselect.selectAll<Node, Element>(
|
||||
":hasfoo",
|
||||
documentWithFoo,
|
||||
options,
|
||||
),
|
||||
).toHaveLength(5);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
|
||||
it("should use cached result for multiple matches", () => {
|
||||
const documentWithFoo = parseDocument(
|
||||
"<a><b><c><d><e>foo</e></d></c><f><g>bar</g></f></b></a>",
|
||||
);
|
||||
|
||||
const fn = vi.fn((elem) => DomUtils.getText(elem).includes("foo"));
|
||||
const hasfoo = cacheParentResults<Node, Element>(
|
||||
boolbase.trueFunc,
|
||||
cacheParentResultsOptions,
|
||||
fn,
|
||||
);
|
||||
|
||||
const options = {
|
||||
pseudos: {
|
||||
hasfoo,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
CSSselect.selectAll<Node, Element>(
|
||||
":hasfoo :hasfoo",
|
||||
documentWithFoo,
|
||||
options,
|
||||
),
|
||||
).toHaveLength(4);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import type { CompiledQuery, InternalOptions } from "../types.js";
|
||||
import { getElementParent } from "./querying.js";
|
||||
|
||||
/**
|
||||
* Some selectors such as `:contains` and (non-relative) `:has` will only be
|
||||
* able to match elements if their parents match the selector (as they contain
|
||||
* a subset of the elements that the parent contains).
|
||||
*
|
||||
* This function wraps the given `matches` function in a function that caches
|
||||
* the results of the parent elements, so that the `matches` function only
|
||||
* needs to be called once for each subtree.
|
||||
*/
|
||||
export function cacheParentResults<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
{ adapter, cacheResults }: InternalOptions<Node, ElementNode>,
|
||||
matches: (elem: ElementNode) => boolean,
|
||||
): CompiledQuery<ElementNode> {
|
||||
if (cacheResults === false || typeof WeakMap === "undefined") {
|
||||
return (elem) => next(elem) && matches(elem);
|
||||
}
|
||||
|
||||
// Use a cache to avoid re-checking children of an element.
|
||||
|
||||
// @ts-expect-error `Node` is not extending object
|
||||
const resultCache = new WeakMap<Node, boolean>();
|
||||
|
||||
function addResultToCache(elem: ElementNode) {
|
||||
const result = matches(elem);
|
||||
|
||||
resultCache.set(elem, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return function cachedMatcher(elem) {
|
||||
if (!next(elem)) {
|
||||
return false;
|
||||
}
|
||||
if (resultCache.has(elem)) {
|
||||
return resultCache.get(elem)!;
|
||||
}
|
||||
|
||||
// Check all of the element's parents.
|
||||
let node = elem;
|
||||
|
||||
do {
|
||||
const parent = getElementParent(node, adapter);
|
||||
|
||||
if (parent === null) {
|
||||
return addResultToCache(elem);
|
||||
}
|
||||
|
||||
node = parent;
|
||||
} while (!resultCache.has(node));
|
||||
|
||||
return resultCache.get(node)! && addResultToCache(elem);
|
||||
};
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import type { Adapter, InternalOptions, Predicate } from "../types.js";
|
||||
|
||||
/**
|
||||
* Find all elements matching the query. If not in XML mode, the query will ignore
|
||||
* the contents of `<template>` elements.
|
||||
*
|
||||
* @param query - Function that returns true if the element matches the query.
|
||||
* @param elems - Nodes to query. If a node is an element, its children will be queried.
|
||||
* @param options - Options for querying the document.
|
||||
* @returns All matching elements.
|
||||
*/
|
||||
export function findAll<Node, ElementNode extends Node>(
|
||||
query: Predicate<ElementNode>,
|
||||
elems: Node[],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
): ElementNode[] {
|
||||
const { adapter, xmlMode = false } = options;
|
||||
const result: ElementNode[] = [];
|
||||
/** Stack of the arrays we are looking at. */
|
||||
const nodeStack = [elems];
|
||||
/** Stack of the indices within the arrays. */
|
||||
const indexStack = [0];
|
||||
|
||||
for (;;) {
|
||||
// First, check if the current array has any more elements to look at.
|
||||
if (indexStack[0] >= nodeStack[0].length) {
|
||||
// If we have no more arrays to look at, we are done.
|
||||
if (nodeStack.length === 1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
nodeStack.shift();
|
||||
indexStack.shift();
|
||||
|
||||
// Loop back to the start to continue with the next array.
|
||||
continue;
|
||||
}
|
||||
|
||||
const elem = nodeStack[0][indexStack[0]++];
|
||||
|
||||
if (!adapter.isTag(elem)) {
|
||||
continue;
|
||||
}
|
||||
if (query(elem)) {
|
||||
result.push(elem);
|
||||
}
|
||||
|
||||
if (xmlMode || adapter.getName(elem) !== "template") {
|
||||
/*
|
||||
* Add the children to the stack. We are depth-first, so this is
|
||||
* the next array we look at.
|
||||
*/
|
||||
const children = adapter.getChildren(elem);
|
||||
|
||||
if (children.length > 0) {
|
||||
nodeStack.unshift(children);
|
||||
indexStack.unshift(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first element matching the query. If not in XML mode, the query will ignore
|
||||
* the contents of `<template>` elements.
|
||||
*
|
||||
* @param query - Function that returns true if the element matches the query.
|
||||
* @param elems - Nodes to query. If a node is an element, its children will be queried.
|
||||
* @param options - Options for querying the document.
|
||||
* @returns The first matching element, or null if there was no match.
|
||||
*/
|
||||
export function findOne<Node, ElementNode extends Node>(
|
||||
query: Predicate<ElementNode>,
|
||||
elems: Node[],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
): ElementNode | null {
|
||||
const { adapter, xmlMode = false } = options;
|
||||
/** Stack of the arrays we are looking at. */
|
||||
const nodeStack = [elems];
|
||||
/** Stack of the indices within the arrays. */
|
||||
const indexStack = [0];
|
||||
|
||||
for (;;) {
|
||||
// First, check if the current array has any more elements to look at.
|
||||
if (indexStack[0] >= nodeStack[0].length) {
|
||||
// If we have no more arrays to look at, we are done.
|
||||
if (nodeStack.length === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
nodeStack.shift();
|
||||
indexStack.shift();
|
||||
|
||||
// Loop back to the start to continue with the next array.
|
||||
continue;
|
||||
}
|
||||
|
||||
const elem = nodeStack[0][indexStack[0]++];
|
||||
|
||||
if (!adapter.isTag(elem)) {
|
||||
continue;
|
||||
}
|
||||
if (query(elem)) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
if (xmlMode || adapter.getName(elem) !== "template") {
|
||||
/*
|
||||
* Add the children to the stack. We are depth-first, so this is
|
||||
* the next array we look at.
|
||||
*/
|
||||
const children = adapter.getChildren(elem);
|
||||
|
||||
if (children.length > 0) {
|
||||
nodeStack.unshift(children);
|
||||
indexStack.unshift(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getNextSiblings<Node, ElementNode extends Node>(
|
||||
elem: Node,
|
||||
adapter: Adapter<Node, ElementNode>,
|
||||
): ElementNode[] {
|
||||
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);
|
||||
}
|
||||
|
||||
export function getElementParent<Node, ElementNode extends Node>(
|
||||
node: ElementNode,
|
||||
adapter: Adapter<Node, ElementNode>,
|
||||
): ElementNode | null {
|
||||
const parent = adapter.getParent(node);
|
||||
return parent != null && adapter.isTag(parent) ? parent : null;
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { parse, stringify } from "css-what";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sortRules } from "./selectors.js";
|
||||
|
||||
/**
|
||||
* Sorts the rules of a selector and turns it back into a string.
|
||||
*
|
||||
* Note that the order of the rules might not be legal, and the resulting
|
||||
* string might not be parseable again.
|
||||
*
|
||||
* @param selector Selector to sort
|
||||
* @returns Sorted selector, which might not be a valid selector anymore.
|
||||
*/
|
||||
function parseSortStringify(selector: string): string {
|
||||
const parsed = parse(selector);
|
||||
|
||||
for (const token of parsed) {
|
||||
sortRules(token);
|
||||
}
|
||||
|
||||
return stringify(parsed);
|
||||
}
|
||||
|
||||
describe("sortRules", () => {
|
||||
it("should move tag selectors last", () => {
|
||||
expect(parseSortStringify("div[class]:empty")).toBe(":empty[class]div");
|
||||
});
|
||||
|
||||
it("should move universal selectors last", () => {
|
||||
expect(parseSortStringify("*[class]")).toBe("[class]*");
|
||||
});
|
||||
|
||||
it("should sort attribute selectors", () => {
|
||||
expect(
|
||||
parseSortStringify(
|
||||
".foo#bar[foo=bar][foo^=bar][foo$=bar][foo!=bar][foo=bar i][foo!=bar s]",
|
||||
),
|
||||
).toBe(
|
||||
'.foo#bar[foo="bar" i][foo^="bar"][foo$="bar"][foo!="bar"][foo!="bar" s][foo="bar"]',
|
||||
);
|
||||
});
|
||||
|
||||
it("should sort pseudo selectors", () => {
|
||||
expect(
|
||||
parseSortStringify(
|
||||
":not(:empty):empty:contains(a):icontains(a):has(div):is(div):is(foo bar):is([foo])",
|
||||
),
|
||||
).toBe(
|
||||
":contains(a):icontains(a):has(div):is(foo bar):not(:empty):empty:is([foo]):is(div)",
|
||||
);
|
||||
});
|
||||
|
||||
it("should support traversals", () => {
|
||||
expect(
|
||||
parseSortStringify("div > *:empty[foo] + [bar=foo i]:is(div)"),
|
||||
).toBe('div > :empty[foo]* + [bar="foo" i]:is(div)');
|
||||
});
|
||||
});
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
AttributeAction,
|
||||
type AttributeSelector,
|
||||
isTraversal as isTraversalBase,
|
||||
SelectorType,
|
||||
type Traversal,
|
||||
} from "css-what";
|
||||
import type { InternalSelector } from "../types.js";
|
||||
|
||||
export function isTraversal(token: InternalSelector): token is Traversal {
|
||||
return token.type === "_flexibleDescendant" || isTraversalBase(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function sortRules(arr: InternalSelector[]): void {
|
||||
const ratings = arr.map(getQuality);
|
||||
for (let i = 1; i < arr.length; i++) {
|
||||
const procNew = ratings[i];
|
||||
|
||||
if (procNew < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use insertion sort to move the token to the correct position.
|
||||
for (let j = i; j > 0 && procNew < ratings[j - 1]; j--) {
|
||||
const token = arr[j];
|
||||
arr[j] = arr[j - 1];
|
||||
arr[j - 1] = token;
|
||||
ratings[j] = ratings[j - 1];
|
||||
ratings[j - 1] = procNew;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAttributeQuality(token: AttributeSelector): number {
|
||||
switch (token.action) {
|
||||
case AttributeAction.Exists: {
|
||||
return 10;
|
||||
}
|
||||
case AttributeAction.Equals: {
|
||||
// Prefer ID selectors (eg. #ID)
|
||||
return token.name === "id" ? 9 : 8;
|
||||
}
|
||||
case AttributeAction.Not: {
|
||||
return 7;
|
||||
}
|
||||
case AttributeAction.Start: {
|
||||
return 6;
|
||||
}
|
||||
case AttributeAction.End: {
|
||||
return 6;
|
||||
}
|
||||
case AttributeAction.Any: {
|
||||
return 5;
|
||||
}
|
||||
case AttributeAction.Hyphen: {
|
||||
return 4;
|
||||
}
|
||||
case AttributeAction.Element: {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the quality of the passed token. The higher the number, the
|
||||
* faster the token is to execute.
|
||||
*
|
||||
* @param token Token to get the quality of.
|
||||
* @returns The token's quality.
|
||||
*/
|
||||
export function getQuality(token: InternalSelector): number {
|
||||
switch (token.type) {
|
||||
case SelectorType.Universal: {
|
||||
return 50;
|
||||
}
|
||||
case SelectorType.Tag: {
|
||||
return 30;
|
||||
}
|
||||
case SelectorType.Attribute: {
|
||||
return Math.floor(
|
||||
getAttributeQuality(token) /
|
||||
// `ignoreCase` adds some overhead, half the result if applicable.
|
||||
(token.ignoreCase ? 2 : 1),
|
||||
);
|
||||
}
|
||||
case SelectorType.Pseudo: {
|
||||
return !token.data
|
||||
? 3
|
||||
: token.name === "has" ||
|
||||
token.name === "contains" ||
|
||||
token.name === "icontains"
|
||||
? // Expensive in any case — run as late as possible.
|
||||
0
|
||||
: Array.isArray(token.data)
|
||||
? // Eg. `:is`, `:not`
|
||||
Math.max(
|
||||
// If we have traversals, try to avoid executing this selector
|
||||
0,
|
||||
Math.min(
|
||||
...token.data.map((d) =>
|
||||
Math.min(...d.map(getQuality)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: 2;
|
||||
}
|
||||
default: {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function includesScopePseudo(t: InternalSelector): boolean {
|
||||
return (
|
||||
t.type === SelectorType.Pseudo &&
|
||||
(t.name === "scope" ||
|
||||
(Array.isArray(t.data) &&
|
||||
t.data.some((data) => data.some(includesScopePseudo))))
|
||||
);
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
import * as boolbase from "boolbase";
|
||||
import { parse, type Selector } from "css-what";
|
||||
import type {
|
||||
Element as DomHandlerElement,
|
||||
AnyNode as DomHandlerNode,
|
||||
} from "domhandler";
|
||||
import * as DomUtils from "domutils";
|
||||
import { compileToken } from "./compile.js";
|
||||
import { findAll, findOne, getNextSiblings } from "./helpers/querying.js";
|
||||
import type {
|
||||
Adapter,
|
||||
CompiledQuery,
|
||||
InternalOptions,
|
||||
Options,
|
||||
Predicate,
|
||||
Query,
|
||||
} from "./types.js";
|
||||
|
||||
export type { Options };
|
||||
|
||||
const defaultEquals = <Node>(a: Node, b: Node) => a === b;
|
||||
const defaultOptions: InternalOptions<DomHandlerNode, DomHandlerElement> = {
|
||||
adapter: DomUtils,
|
||||
equals: defaultEquals,
|
||||
};
|
||||
|
||||
function convertOptionFormats<Node, ElementNode extends Node>(
|
||||
options?: Options<Node, ElementNode>,
|
||||
): InternalOptions<Node, ElementNode> {
|
||||
/*
|
||||
* We force one format of options to the other one.
|
||||
*/
|
||||
// @ts-expect-error Default options may have incompatible `Node` / `ElementNode`.
|
||||
const opts: Options<Node, ElementNode> = 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 as InternalOptions<Node, ElementNode>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles a selector to an executable function.
|
||||
*
|
||||
* The returned function checks if each passed node is an element. Use
|
||||
* `_compileUnsafe` to skip this check.
|
||||
*
|
||||
* @param selector Selector to compile.
|
||||
* @param options Compilation options.
|
||||
* @param context Optional context for the selector.
|
||||
*/
|
||||
export function compile<Node, ElementNode extends Node>(
|
||||
selector: string | Selector[][],
|
||||
options?: Options<Node, ElementNode>,
|
||||
context?: Node[] | Node,
|
||||
): CompiledQuery<Node> {
|
||||
const opts = convertOptionFormats(options);
|
||||
const next = _compileUnsafe(selector, opts, context);
|
||||
|
||||
return next === boolbase.falseFunc
|
||||
? boolbase.falseFunc
|
||||
: (elem: Node) => opts.adapter.isTag(elem) && next(elem);
|
||||
}
|
||||
/**
|
||||
* Like `compile`, but does not add a check if elements are tags.
|
||||
*/
|
||||
export function _compileUnsafe<Node, ElementNode extends Node>(
|
||||
selector: string | Selector[][],
|
||||
options?: Options<Node, ElementNode>,
|
||||
context?: Node[] | Node,
|
||||
): CompiledQuery<ElementNode> {
|
||||
return _compileToken<Node, ElementNode>(
|
||||
typeof selector === "string" ? parse(selector) : selector,
|
||||
options,
|
||||
context,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @deprecated Use `_compileUnsafe` instead.
|
||||
*/
|
||||
export function _compileToken<Node, ElementNode extends Node>(
|
||||
selector: Selector[][],
|
||||
options?: Options<Node, ElementNode>,
|
||||
context?: Node[] | Node,
|
||||
): CompiledQuery<ElementNode> {
|
||||
return compileToken<Node, ElementNode>(
|
||||
selector,
|
||||
convertOptionFormats(options),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
function getSelectorFunc<Node, ElementNode extends Node, T>(
|
||||
searchFunc: (
|
||||
query: Predicate<ElementNode>,
|
||||
elems: Array<Node>,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
) => T,
|
||||
) {
|
||||
return function select(
|
||||
query: Query<ElementNode>,
|
||||
elements: Node[] | Node,
|
||||
options?: Options<Node, ElementNode>,
|
||||
): T {
|
||||
const opts = convertOptionFormats(options);
|
||||
|
||||
if (typeof query !== "function") {
|
||||
query = _compileUnsafe<Node, ElementNode>(query, opts, elements);
|
||||
}
|
||||
|
||||
const filteredElements = prepareContext(
|
||||
elements,
|
||||
opts.adapter,
|
||||
query.shouldTestNextSiblings,
|
||||
);
|
||||
return searchFunc(query, filteredElements, opts);
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareContext<Node, ElementNode extends Node>(
|
||||
elems: Node | Node[],
|
||||
adapter: Adapter<Node, ElementNode>,
|
||||
shouldTestNextSiblings = false,
|
||||
): Node[] {
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
function appendNextSiblings<Node, ElementNode extends Node>(
|
||||
elem: Node | Node[],
|
||||
adapter: Adapter<Node, ElementNode>,
|
||||
): Node[] {
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Node The generic Node type for the DOM adapter being used.
|
||||
* @template ElementNode The Node type for elements for the DOM adapter being used.
|
||||
* @param elems Elements to query. If it is an element, its children will be queried.
|
||||
* @param query can be either a CSS selector string or a compiled query function.
|
||||
* @param [options] options for querying the document.
|
||||
* @see compile for supported selector queries.
|
||||
* @returns All matching elements.
|
||||
*
|
||||
*/
|
||||
export const selectAll: <Node, ElementNode extends Node>(
|
||||
query: Query<ElementNode>,
|
||||
elements: Node | Node[],
|
||||
options?: Options<Node, ElementNode> | undefined,
|
||||
) => ElementNode[] = getSelectorFunc(
|
||||
<Node, ElementNode extends Node>(
|
||||
query: Predicate<ElementNode>,
|
||||
elems: Node[] | null,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
): ElementNode[] =>
|
||||
query === boolbase.falseFunc || !elems || elems.length === 0
|
||||
? []
|
||||
: findAll(query, elems, options),
|
||||
);
|
||||
|
||||
/**
|
||||
* @template Node The generic Node type for the DOM adapter being used.
|
||||
* @template ElementNode The Node type for elements for the DOM adapter being used.
|
||||
* @param elems Elements to query. If it is an element, its children will be queried.
|
||||
* @param query can be either a CSS selector string or a compiled query function.
|
||||
* @param [options] options for querying the document.
|
||||
* @see compile for supported selector queries.
|
||||
* @returns the first match, or null if there was no match.
|
||||
*/
|
||||
export const selectOne: <Node, ElementNode extends Node>(
|
||||
query: Query<ElementNode>,
|
||||
elements: Node | Node[],
|
||||
options?: Options<Node, ElementNode> | undefined,
|
||||
) => ElementNode | null = getSelectorFunc(
|
||||
<Node, ElementNode extends Node>(
|
||||
query: Predicate<ElementNode>,
|
||||
elems: Node[] | null,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
): ElementNode | null =>
|
||||
query === boolbase.falseFunc || !elems || elems.length === 0
|
||||
? null
|
||||
: findOne(query, elems, options),
|
||||
);
|
||||
|
||||
/**
|
||||
* Tests whether or not an element is matched by query.
|
||||
*
|
||||
* @template Node The generic Node type for the DOM adapter being used.
|
||||
* @template ElementNode The Node type for elements for the DOM adapter being used.
|
||||
* @param elem The element to test if it matches the query.
|
||||
* @param query can be either a CSS selector string or a compiled query function.
|
||||
* @param [options] options for querying the document.
|
||||
* @see compile for supported selector queries.
|
||||
* @returns
|
||||
*/
|
||||
export function is<Node, ElementNode extends Node>(
|
||||
elem: ElementNode,
|
||||
query: Query<ElementNode>,
|
||||
options?: Options<Node, ElementNode>,
|
||||
): boolean {
|
||||
return (typeof query === "function" ? query : compile(query, options))(
|
||||
elem,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for selectAll(query, elems, options).
|
||||
* @see [compile] for supported selector queries.
|
||||
*/
|
||||
export default selectAll;
|
||||
|
||||
// Export filters, pseudos and aliases to allow users to supply their own.
|
||||
/** @deprecated Use the `pseudos` option instead. */
|
||||
export { aliases, filters, pseudos } from "./pseudo-selectors/index.js";
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Only text controls can be made read-only, since for other controls (such
|
||||
* as checkboxes and buttons) there is no useful distinction between being
|
||||
* read-only and being disabled.
|
||||
*
|
||||
* @see {@link https://html.spec.whatwg.org/multipage/input.html#attr-input-readonly}
|
||||
*/
|
||||
const textControl =
|
||||
"input:is([type=text i],[type=search i],[type=url i],[type=tel i],[type=email i],[type=password i],[type=date i],[type=month i],[type=week i],[type=time i],[type=datetime-local i],[type=number i])";
|
||||
|
||||
/**
|
||||
* Aliases are pseudos that are expressed as selectors.
|
||||
*/
|
||||
export const aliases: Record<string, string> = {
|
||||
// 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], :selected)",
|
||||
required: ":is(input, select, textarea)[required]",
|
||||
optional: ":is(input, select, textarea):not([required])",
|
||||
|
||||
"read-only": `[readonly]:is(textarea, ${textControl})`,
|
||||
"read-write": `:not([readonly]):is(textarea, ${textControl})`,
|
||||
|
||||
// JQuery extensions
|
||||
|
||||
/**
|
||||
* `:selected` matches option elements that have the `selected` attribute,
|
||||
* or are the first option element in a select element that does not have
|
||||
* the `multiple` attribute and does not have any option elements with the
|
||||
* `selected` attribute.
|
||||
*
|
||||
* @see 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])",
|
||||
};
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
import * as boolbase from "boolbase";
|
||||
import getNCheck from "nth-check";
|
||||
import { cacheParentResults } from "../helpers/cache.js";
|
||||
import { getElementParent } from "../helpers/querying.js";
|
||||
import type { CompiledQuery, InternalOptions } from "../types.js";
|
||||
|
||||
type Filter = <Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
text: string,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: Node[],
|
||||
) => CompiledQuery<ElementNode>;
|
||||
|
||||
export const filters: Record<string, Filter> = {
|
||||
contains(next, text, options) {
|
||||
const { getText } = options.adapter;
|
||||
|
||||
return cacheParentResults(next, options, (elem) =>
|
||||
getText(elem).includes(text),
|
||||
);
|
||||
},
|
||||
icontains(next, text, options) {
|
||||
const itext = text.toLowerCase();
|
||||
const { getText } = options.adapter;
|
||||
|
||||
return cacheParentResults(next, options, (elem) =>
|
||||
getText(elem).toLowerCase().includes(itext),
|
||||
);
|
||||
},
|
||||
|
||||
// Location specific methods
|
||||
"nth-child"(next, rule, { adapter, equals }) {
|
||||
const func = getNCheck(rule);
|
||||
|
||||
if (func === boolbase.falseFunc) {
|
||||
return boolbase.falseFunc;
|
||||
}
|
||||
if (func === boolbase.trueFunc) {
|
||||
return (elem) =>
|
||||
getElementParent(elem, adapter) !== null && next(elem);
|
||||
}
|
||||
|
||||
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 === boolbase.falseFunc) {
|
||||
return boolbase.falseFunc;
|
||||
}
|
||||
if (func === boolbase.trueFunc) {
|
||||
return (elem) =>
|
||||
getElementParent(elem, adapter) !== null && next(elem);
|
||||
}
|
||||
|
||||
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 === boolbase.falseFunc) {
|
||||
return boolbase.falseFunc;
|
||||
}
|
||||
if (func === boolbase.trueFunc) {
|
||||
return (elem) =>
|
||||
getElementParent(elem, adapter) !== null && next(elem);
|
||||
}
|
||||
|
||||
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 === boolbase.falseFunc) {
|
||||
return boolbase.falseFunc;
|
||||
}
|
||||
if (func === boolbase.trueFunc) {
|
||||
return (elem) =>
|
||||
getElementParent(elem, adapter) !== null && next(elem);
|
||||
}
|
||||
|
||||
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) => getElementParent(elem, adapter) === null && next(elem);
|
||||
},
|
||||
|
||||
scope<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
rule: string,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: Node[],
|
||||
): CompiledQuery<ElementNode> {
|
||||
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"),
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: "isHovered" | "isVisited" | "isActive",
|
||||
): Filter {
|
||||
return function dynamicPseudo(next, _rule, { adapter }) {
|
||||
const func = adapter[name];
|
||||
|
||||
if (typeof func !== "function") {
|
||||
return boolbase.falseFunc;
|
||||
}
|
||||
|
||||
return function active(elem) {
|
||||
return func(elem) && next(elem);
|
||||
};
|
||||
};
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Pseudo selectors
|
||||
*
|
||||
* Pseudo selectors are available in three forms:
|
||||
*
|
||||
* 1. Filters are called when the selector is compiled and return a function
|
||||
* that has to return either false, or the results of `next()`.
|
||||
* 2. Pseudos are called on execution. They have to return a boolean.
|
||||
* 3. Subselects work like filters, but have an embedded selector that will be run separately.
|
||||
*
|
||||
* Filters are great if you want to do some pre-processing, or change the call order
|
||||
* of `next()` and your code.
|
||||
* Pseudos should be used to implement simple checks.
|
||||
*/
|
||||
|
||||
import { type PseudoSelector, parse } from "css-what";
|
||||
import type { CompiledQuery, CompileToken, InternalOptions } from "../types.js";
|
||||
import { aliases } from "./aliases.js";
|
||||
import { filters } from "./filters.js";
|
||||
import { pseudos, verifyPseudoArgs } from "./pseudos.js";
|
||||
import { subselects } from "./subselects.js";
|
||||
|
||||
export { filters, pseudos, aliases };
|
||||
|
||||
export function compilePseudoSelector<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
selector: PseudoSelector,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context: Node[] | undefined,
|
||||
compileToken: CompileToken<Node, ElementNode>,
|
||||
): CompiledQuery<ElementNode> {
|
||||
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 as string, 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}`);
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import type { PseudoSelector } from "css-what";
|
||||
import type { InternalOptions } from "../types.js";
|
||||
|
||||
type Pseudo = <Node, ElementNode extends Node>(
|
||||
elem: 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
|
||||
export const pseudos: Record<string, Pseudo> = {
|
||||
empty(elem, { adapter }) {
|
||||
const children = adapter.getChildren(elem);
|
||||
return (
|
||||
// First, make sure the tag does not have any element children.
|
||||
children.every((elem) => !adapter.isTag(elem)) &&
|
||||
// Then, check that the text content is only whitespace.
|
||||
children.every((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),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export function verifyPseudoArgs<T extends Array<unknown>>(
|
||||
func: (...args: T) => boolean,
|
||||
name: string,
|
||||
subselect: PseudoSelector["data"],
|
||||
argIndex: number,
|
||||
): void {
|
||||
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`);
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import * as boolbase from "boolbase";
|
||||
import type { Selector } from "css-what";
|
||||
import { cacheParentResults } from "../helpers/cache.js";
|
||||
import { findOne, getNextSiblings } from "../helpers/querying.js";
|
||||
import { includesScopePseudo, isTraversal } from "../helpers/selectors.js";
|
||||
import type { CompiledQuery, CompileToken, InternalOptions } from "../types.js";
|
||||
|
||||
/** Used as a placeholder for :has. Will be replaced with the actual element. */
|
||||
export const PLACEHOLDER_ELEMENT = {};
|
||||
|
||||
type Subselect = <Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
subselect: Selector[][],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context: Node[] | undefined,
|
||||
compileToken: CompileToken<Node, ElementNode>,
|
||||
) => CompiledQuery<ElementNode>;
|
||||
|
||||
/**
|
||||
* Check if the selector has any properties that rely on the current element.
|
||||
* If not, we can cache the result of the selector.
|
||||
*
|
||||
* We can't cache selectors that start with a traversal (e.g. `>`, `+`, `~`),
|
||||
* or include a `:scope`.
|
||||
*
|
||||
* @param selector - The selector to check.
|
||||
* @returns Whether the selector has any properties that rely on the current element.
|
||||
*/
|
||||
function hasDependsOnCurrentElement(selector: Selector[][]) {
|
||||
return selector.some(
|
||||
(sel) =>
|
||||
sel.length > 0 &&
|
||||
(isTraversal(sel[0]) || sel.some(includesScopePseudo)),
|
||||
);
|
||||
}
|
||||
|
||||
function copyOptions<Node, ElementNode extends Node>(
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
): InternalOptions<Node, ElementNode> {
|
||||
// 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: Subselect = (next, token, options, context, compileToken) => {
|
||||
const func = compileToken(token, copyOptions(options), context);
|
||||
|
||||
return func === boolbase.trueFunc
|
||||
? next
|
||||
: func === boolbase.falseFunc
|
||||
? boolbase.falseFunc
|
||||
: (elem) => func(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
|
||||
*/
|
||||
export const subselects: Record<string, Subselect> = {
|
||||
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 === boolbase.falseFunc
|
||||
? next
|
||||
: func === boolbase.trueFunc
|
||||
? boolbase.falseFunc
|
||||
: (elem) => !func(elem) && next(elem);
|
||||
},
|
||||
has<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
subselect: Selector[][],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
_context: Node[] | undefined,
|
||||
compileToken: CompileToken<Node, ElementNode>,
|
||||
): CompiledQuery<ElementNode> {
|
||||
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 as unknown as ElementNode]
|
||||
: undefined;
|
||||
const skipCache = hasDependsOnCurrentElement(subselect);
|
||||
|
||||
const compiled = compileToken(subselect, opts, context);
|
||||
|
||||
if (compiled === boolbase.falseFunc) {
|
||||
return boolbase.falseFunc;
|
||||
}
|
||||
|
||||
// If `compiled` is `trueFunc`, we can skip this.
|
||||
if (context && compiled !== boolbase.trueFunc) {
|
||||
return skipCache
|
||||
? (elem) => {
|
||||
if (!next(elem)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
context[0] = elem;
|
||||
const childs = adapter.getChildren(elem);
|
||||
|
||||
return (
|
||||
findOne(
|
||||
compiled,
|
||||
compiled.shouldTestNextSiblings
|
||||
? [
|
||||
...childs,
|
||||
...getNextSiblings(elem, adapter),
|
||||
]
|
||||
: childs,
|
||||
options,
|
||||
) !== null
|
||||
);
|
||||
}
|
||||
: cacheParentResults(next, options, (elem) => {
|
||||
context[0] = elem;
|
||||
|
||||
return (
|
||||
findOne(
|
||||
compiled,
|
||||
adapter.getChildren(elem),
|
||||
options,
|
||||
) !== null
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const hasOne = (elem: ElementNode) =>
|
||||
findOne(compiled, adapter.getChildren(elem), options) !== null;
|
||||
|
||||
return skipCache
|
||||
? (elem) => next(elem) && hasOne(elem)
|
||||
: cacheParentResults(next, options, hasOne);
|
||||
},
|
||||
};
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import type { Selector } from "css-what";
|
||||
|
||||
export type InternalSelector = Selector | { type: "_flexibleDescendant" };
|
||||
|
||||
export type Predicate<Value> = (v: Value) => boolean;
|
||||
export interface Adapter<Node, ElementNode extends Node> {
|
||||
/**
|
||||
* Is the node a tag?
|
||||
*/
|
||||
isTag: (node: Node) => node is ElementNode;
|
||||
|
||||
/**
|
||||
* Get the attribute value.
|
||||
*/
|
||||
getAttributeValue: (elem: ElementNode, name: string) => string | undefined;
|
||||
|
||||
/**
|
||||
* Get the node's children
|
||||
*/
|
||||
getChildren: (node: Node) => Node[];
|
||||
|
||||
/**
|
||||
* Get the name of the tag
|
||||
*/
|
||||
getName: (elem: ElementNode) => string;
|
||||
|
||||
/**
|
||||
* Get the parent of the node
|
||||
*/
|
||||
getParent: (node: ElementNode) => Node | null;
|
||||
|
||||
/**
|
||||
* Get the siblings of the node. Note that unlike jQuery's `siblings` method,
|
||||
* this is expected to include the current node as well
|
||||
*/
|
||||
getSiblings: (node: Node) => Node[];
|
||||
|
||||
/**
|
||||
* Returns the previous element sibling of a node.
|
||||
*/
|
||||
prevElementSibling?: (node: Node) => ElementNode | null;
|
||||
|
||||
/**
|
||||
* Get the text content of the node, and its children if it has any.
|
||||
*/
|
||||
getText: (node: Node) => string;
|
||||
|
||||
/**
|
||||
* Does the element have the named attribute?
|
||||
*/
|
||||
hasAttrib: (elem: ElementNode, name: string) => boolean;
|
||||
|
||||
/**
|
||||
* Takes an array of nodes, and removes any duplicates, as well as any
|
||||
* nodes whose ancestors are also in the array.
|
||||
*/
|
||||
removeSubsets: (nodes: Node[]) => Node[];
|
||||
|
||||
/**
|
||||
* The adapter can also optionally include an equals method, if your DOM
|
||||
* structure needs a custom equality test to compare two objects which refer
|
||||
* to the same underlying node. If not provided, `css-select` will fall back to
|
||||
* `a === b`.
|
||||
*/
|
||||
equals?: (a: Node, b: Node) => boolean;
|
||||
|
||||
/**
|
||||
* Is the element in hovered state?
|
||||
*/
|
||||
isHovered?: (elem: ElementNode) => boolean;
|
||||
|
||||
/**
|
||||
* Is the element in visited state?
|
||||
*/
|
||||
isVisited?: (elem: ElementNode) => boolean;
|
||||
|
||||
/**
|
||||
* Is the element in active state?
|
||||
*/
|
||||
isActive?: (elem: ElementNode) => boolean;
|
||||
}
|
||||
|
||||
export interface Options<Node, ElementNode extends Node> {
|
||||
/**
|
||||
* When enabled, tag names will be case-sensitive.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
xmlMode?: boolean;
|
||||
/**
|
||||
* Lower-case attribute names.
|
||||
*
|
||||
* @default !xmlMode
|
||||
*/
|
||||
lowerCaseAttributeNames?: boolean;
|
||||
/**
|
||||
* Lower-case tag names.
|
||||
*
|
||||
* @default !xmlMode
|
||||
*/
|
||||
lowerCaseTags?: boolean;
|
||||
/**
|
||||
* Is the document in quirks mode?
|
||||
*
|
||||
* This will lead to .className and #id being case-insensitive.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
quirksMode?: boolean;
|
||||
/**
|
||||
* Pseudo-classes that override the default ones.
|
||||
*
|
||||
* Maps from names to either strings of functions.
|
||||
* - A string value is a selector that the element must match to be selected.
|
||||
* - A function is called with the element as its first argument, and optional
|
||||
* parameters second. If it returns true, the element is selected.
|
||||
*/
|
||||
pseudos?:
|
||||
| Record<
|
||||
string,
|
||||
string | ((elem: ElementNode, value?: string | null) => boolean)
|
||||
>
|
||||
| undefined;
|
||||
/**
|
||||
* The last function in the stack, will be called with the last element
|
||||
* that's looked at.
|
||||
*/
|
||||
rootFunc?: (element: ElementNode) => boolean;
|
||||
/**
|
||||
* The adapter to use when interacting with the backing DOM structure. By
|
||||
* default it uses the `domutils` module.
|
||||
*/
|
||||
adapter?: Adapter<Node, ElementNode> | undefined;
|
||||
/**
|
||||
* The context of the current query. Used to limit the scope of searches.
|
||||
* Can be matched directly using the `:scope` pseudo-class.
|
||||
*/
|
||||
context?: Node | Node[];
|
||||
/**
|
||||
* Indicates whether to consider the selector as a relative selector.
|
||||
*
|
||||
* Relative selectors that don't include a `:scope` pseudo-class behave
|
||||
* as if they have a `:scope ` prefix (a `:scope` pseudo-class, followed by
|
||||
* a descendant selector).
|
||||
*
|
||||
* If relative selectors are disabled, selectors starting with a traversal
|
||||
* will lead to an error.
|
||||
*
|
||||
* @default true
|
||||
* @see {@link https://www.w3.org/TR/selectors-4/#relative}
|
||||
*/
|
||||
relativeSelector?: boolean;
|
||||
/**
|
||||
* Allow css-select to cache results for some selectors, sometimes greatly
|
||||
* improving querying performance. Disable this if your document can
|
||||
* change in between queries with the same compiled selector.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
cacheResults?: boolean;
|
||||
}
|
||||
|
||||
// Internally, we want to ensure that no propterties are accessed on the passed objects
|
||||
export interface InternalOptions<Node, ElementNode extends Node>
|
||||
extends Options<Node, ElementNode> {
|
||||
adapter: Adapter<Node, ElementNode>;
|
||||
equals: (a: Node, b: Node) => boolean;
|
||||
}
|
||||
|
||||
export interface CompiledQuery<ElementNode> {
|
||||
(node: ElementNode): boolean;
|
||||
shouldTestNextSiblings?: boolean;
|
||||
}
|
||||
export type Query<ElementNode> =
|
||||
| string
|
||||
| CompiledQuery<ElementNode>
|
||||
| Selector[][];
|
||||
export type CompileToken<Node, ElementNode extends Node> = (
|
||||
token: InternalSelector[][],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: Node[] | Node,
|
||||
) => CompiledQuery<ElementNode>;
|
||||
Reference in New Issue
Block a user