Skip to content

Commit

Permalink
feat: Add pseudos option
Browse files Browse the repository at this point in the history
This option allows users to specify pseudo-classes for
  • Loading branch information
fb55 committed Apr 24, 2022
1 parent 0c1cdd8 commit 29c9aa2
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 571 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ All options are optional.
sometimes greatly improving querying performance. Disable this if your
document can change in between queries with the same compiled selector.
Default: `true`.
- `pseudos`: A map of pseudo-selectors to functions or strings.

#### Custom Adapters

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,5 @@ export function is<Node, ElementNode extends Node>(
export default selectAll;

// Export filters, pseudos and aliases to allow users to supply their own.
/** @deprecated Use the `pseudos` option instead. */
export { filters, pseudos, aliases } from "./pseudo-selectors/index.js";
28 changes: 19 additions & 9 deletions src/pseudo-selectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
* of `next()` and your code.
* Pseudos should be used to implement simple checks.
*/
import boolbase from "boolbase";
import type { CompiledQuery, InternalOptions, CompileToken } from "../types.js";
import { parse, PseudoSelector } from "css-what";
import { filters } from "./filters.js";
Expand All @@ -38,27 +37,38 @@ export function compilePseudoSelector<Node, ElementNode extends Node>(

return subselects[name](next, data, options, context, compileToken);
}
if (name in aliases) {

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(aliases[name]);
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);
verifyPseudoArgs(pseudo, name, data, 2);

return pseudo === boolbase.falseFunc
? boolbase.falseFunc
: next === boolbase.trueFunc
? (elem) => pseudo(elem, options, data)
: (elem) => pseudo(elem, options, data) && next(elem);
return (elem) => pseudo(elem, options, data) && next(elem);
}

throw new Error(`Unknown pseudo-class :${name}`);
}
17 changes: 9 additions & 8 deletions src/pseudo-selectors/pseudos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { InternalOptions } from "../types.js";
export type Pseudo = <Node, ElementNode extends Node>(
elem: ElementNode,
options: InternalOptions<Node, ElementNode>,
subselect?: ElementNode | string | null
subselect?: string | null
) => boolean;

// While filters are precompiled, pseudos get called when they are needed
Expand Down Expand Up @@ -92,16 +92,17 @@ export const pseudos: Record<string, Pseudo> = {
},
};

export function verifyPseudoArgs(
func: Pseudo,
export function verifyPseudoArgs<T extends Array<unknown>>(
func: (...args: T) => boolean,
name: string,
subselect: PseudoSelector["data"]
subselect: PseudoSelector["data"],
argIndex: number
): void {
if (subselect === null) {
if (func.length > 2) {
throw new Error(`pseudo-selector :${name} requires an argument`);
if (func.length > argIndex) {
throw new Error(`Pseudo-class :${name} requires an argument`);
}
} else if (func.length === 2) {
throw new Error(`pseudo-selector :${name} doesn't have any arguments`);
} else if (func.length === argIndex) {
throw new Error(`Pseudo-class :${name} doesn't have any arguments`);
}
}
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ export interface Options<Node, ElementNode extends Node> {
* @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)
>;
/**
* The last function in the stack, will be called with the last element
* that's looked at.
Expand Down
18 changes: 18 additions & 0 deletions test/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,24 @@ describe("API", () => {

delete CSSselect.pseudos["foovalue"];
});

it("should throw if parameter is supplied for user-provided pseudo", () =>
expect(() =>
CSSselect.compile(":foovalue(boo)", {
pseudos: { foovalue: "tag" },
})
).toThrow("doesn't have any arguments"));

it("should throw if no parameter is supplied for user-provided pseudo", () =>
expect(() =>
CSSselect.compile(":foovalue", {
pseudos: {
foovalue(_el, data) {
return data != null;
},
},
})
).toThrow("requires an argument"));
});

describe("unsatisfiable and universally valid selectors", () => {
Expand Down
Loading

0 comments on commit 29c9aa2

Please sign in to comment.