mirror of https://github.com/fb55/css-select.git
Port to Typescript (#144)
parent
edbdbe5fd4
commit
d379a7c6e5
@ -1,3 +1,4 @@
|
||||
.vscode/
|
||||
node_modules/
|
||||
coverage/
|
||||
lib/
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"require": "ts-node/register",
|
||||
"check-leaks": true,
|
||||
"reporter": "spec"
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- lts/*
|
||||
|
||||
script: npm run coveralls
|
||||
after_success: npm run coverage
|
||||
|
@ -1,228 +0,0 @@
|
||||
export = CSSselect;
|
||||
|
||||
/**
|
||||
* Alias for CSSselect.selectAll(query, elems, options).
|
||||
* @see [CSSselect.compile] for supported selector queries.
|
||||
*/
|
||||
declare function CSSselect<Node, ElementNode extends Node>(
|
||||
query: CSSselect.Query,
|
||||
elems: Array<ElementNode> | ElementNode,
|
||||
options?: CSSselect.Options<Node, ElementNode>
|
||||
): Array<ElementNode>;
|
||||
|
||||
declare namespace CSSselect {
|
||||
type Predicate<Value> = (v: Value) => boolean;
|
||||
interface Adapter<Node, ElementNode extends Node> {
|
||||
/**
|
||||
* is the node a tag?
|
||||
*/
|
||||
isTag(node: Node): node is ElementNode;
|
||||
|
||||
/**
|
||||
* Does at least one of passed element nodes pass the test predicate?
|
||||
*/
|
||||
existsOne(
|
||||
test: Predicate<ElementNode>,
|
||||
elems: Array<ElementNode>
|
||||
): boolean;
|
||||
|
||||
/**
|
||||
* get the attribute value.
|
||||
*/
|
||||
getAttributeValue(elem: ElementNode, name: string): string;
|
||||
|
||||
/**
|
||||
* get the node's children
|
||||
*/
|
||||
getChildren(node: Node): Array<Node>;
|
||||
|
||||
/**
|
||||
* get the name of the tag
|
||||
*/
|
||||
getName(elem: ElementNode): string;
|
||||
|
||||
/**
|
||||
* get the parent of the node
|
||||
*/
|
||||
getParent(node: Node): Node;
|
||||
|
||||
/*
|
||||
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): Array<Node>;
|
||||
|
||||
/*
|
||||
* 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: Array<Node>): Array<Node>;
|
||||
|
||||
/**
|
||||
* finds all of the element nodes in the array that match the test predicate,
|
||||
* as well as any of their children that match it.
|
||||
*/
|
||||
findAll(
|
||||
test: Predicate<ElementNode>,
|
||||
nodes: Array<Node>
|
||||
): Array<ElementNode>;
|
||||
|
||||
/**
|
||||
* finds the first node in the array that matches the test predicate, or one
|
||||
* of its children.
|
||||
*/
|
||||
findOne(
|
||||
test: Predicate<ElementNode>,
|
||||
elems: Array<ElementNode>
|
||||
): ElementNode | undefined;
|
||||
|
||||
/**
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO default types to the domutil/httpparser2 types
|
||||
interface Options<Node, ElementNode extends Node> {
|
||||
/**
|
||||
* When enabled, tag names will be case-sensitive. Default: false.
|
||||
*/
|
||||
xmlMode?: boolean;
|
||||
/**
|
||||
* Limits the module to only use CSS3 selectors. Default: false.
|
||||
*/
|
||||
strict?: boolean;
|
||||
/**
|
||||
* The last function in the stack, will be called with the last element
|
||||
* that's looked at. Should return true.
|
||||
*/
|
||||
rootFunc?: (element: ElementNode) => true;
|
||||
/**
|
||||
* The adapter to use when interacting with the backing DOM structure. By
|
||||
* default it uses domutils.
|
||||
*/
|
||||
adapter?: Adapter<Node, ElementNode>;
|
||||
}
|
||||
|
||||
type CompiledQuery = (node: any) => boolean;
|
||||
type Query = string | CompiledQuery;
|
||||
|
||||
/**
|
||||
* Compiles the query, returns a function.
|
||||
*
|
||||
* Supported simple selectors:
|
||||
* * Universal (*)
|
||||
* * Tag (<tagname>)
|
||||
* * Attribute ([attr=foo]), with supported comparisons:
|
||||
* * [attr] (existential)
|
||||
* * =
|
||||
* * ~=
|
||||
* * |=
|
||||
* * *=
|
||||
* * ^=
|
||||
* * $=
|
||||
* * !=
|
||||
* * Can be case insensitive (E.g. [attr=foo i])
|
||||
* * Pseudos:
|
||||
* * :not
|
||||
* * :root
|
||||
* * :empty
|
||||
* * :[first|last]-child[-of-type]
|
||||
* * :only-of-type, :only-child
|
||||
* * :nth-[last-]child[-of-type]
|
||||
* * :link, :visited (the latter doesn't match any elements)
|
||||
* * :checked
|
||||
* * :enabled, :disabled
|
||||
* * :required, :optional
|
||||
* * Nonstandard Pseudos (available when strict mode is not enabled):
|
||||
* * `:contains`
|
||||
* * `:icontains` (case-insensitive version of :contains)
|
||||
* * `:has`
|
||||
* * `:parent`
|
||||
* * `:selected`
|
||||
* * `:header, :button, :input, :text, :checkbox, :file, :password, :reset, :radio etc.
|
||||
* * :matches
|
||||
*
|
||||
* Supported Combinators:
|
||||
*
|
||||
* * Descendant (` `)
|
||||
* * Child (`>`)
|
||||
* * Parent (`<`) (when strict mode is not enabled)
|
||||
* * Sibling (`~`)
|
||||
* * Adjacent (`+`)
|
||||
*/
|
||||
function compile(query: string): CompiledQuery;
|
||||
/**
|
||||
* @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 CSSselect.compile for supported selector queries.
|
||||
* @returns All matching elements.
|
||||
*/
|
||||
function selectAll<Node, ElementNode extends Node>(
|
||||
query: Query,
|
||||
elems: Array<ElementNode> | ElementNode,
|
||||
options?: Options<Node, ElementNode>
|
||||
): Array<ElementNode>;
|
||||
/**
|
||||
* @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 CSSselect.compile for supported selector queries.
|
||||
* @returns the first match, or null if there was no match.
|
||||
*/
|
||||
function selectOne<Node, ElementNode extends Node>(
|
||||
query: Query,
|
||||
elems: Array<ElementNode> | ElementNode,
|
||||
options?: Options<Node, ElementNode>
|
||||
): ElementNode | null;
|
||||
|
||||
/**
|
||||
* 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 CSSselect.compile for supported selector queries.
|
||||
* @returns
|
||||
*/
|
||||
function is<Node, ElementNode extends Node>(
|
||||
elem: ElementNode,
|
||||
query: Query,
|
||||
options?: Options<Node, ElementNode>
|
||||
): boolean;
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = CSSselect;
|
||||
|
||||
const DomUtils = require("domutils");
|
||||
const { falseFunc } = require("boolbase");
|
||||
const compileRaw = require("./lib/compile.js");
|
||||
|
||||
function wrapCompile(func) {
|
||||
return function addAdapter(selector, options = {}, context) {
|
||||
options.adapter = options.adapter || DomUtils;
|
||||
|
||||
return func(selector, options, context);
|
||||
};
|
||||
}
|
||||
|
||||
const compile = wrapCompile(compileRaw);
|
||||
const compileUnsafe = wrapCompile(compileRaw.compileUnsafe);
|
||||
|
||||
function getSelectorFunc(searchFunc) {
|
||||
return function select(query, elems, options = {}) {
|
||||
options.adapter = options.adapter || DomUtils;
|
||||
|
||||
if (typeof query !== "function") {
|
||||
query = compileUnsafe(query, options, elems);
|
||||
}
|
||||
if (query.shouldTestNextSiblings) {
|
||||
elems = appendNextSiblings(
|
||||
options.context || elems,
|
||||
options.adapter
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(elems)) elems = options.adapter.getChildren(elems);
|
||||
else elems = options.adapter.removeSubsets(elems);
|
||||
return searchFunc(query, elems, options);
|
||||
};
|
||||
}
|
||||
|
||||
function getNextSiblings(elem, adapter) {
|
||||
let siblings = adapter.getSiblings(elem);
|
||||
if (!Array.isArray(siblings)) return [];
|
||||
siblings = siblings.slice(0);
|
||||
while (siblings.shift() !== elem);
|
||||
return siblings;
|
||||
}
|
||||
|
||||
function appendNextSiblings(elems, adapter) {
|
||||
// Order matters because jQuery seems to check the children before the siblings
|
||||
if (!Array.isArray(elems)) elems = [elems];
|
||||
const newElems = elems.slice(0);
|
||||
|
||||
for (let i = 0, len = elems.length; i < len; i++) {
|
||||
const nextSiblings = getNextSiblings(newElems[i], adapter);
|
||||
newElems.push.apply(newElems, nextSiblings);
|
||||
}
|
||||
return newElems;
|
||||
}
|
||||
|
||||
const selectAll = getSelectorFunc((query, elems, options) =>
|
||||
query === falseFunc || !elems || elems.length === 0
|
||||
? []
|
||||
: options.adapter.findAll(query, elems)
|
||||
);
|
||||
|
||||
const selectOne = getSelectorFunc((query, elems, options) =>
|
||||
query === falseFunc || !elems || elems.length === 0
|
||||
? null
|
||||
: options.adapter.findOne(query, elems)
|
||||
);
|
||||
|
||||
function is(elem, query, options = {}) {
|
||||
options.adapter = options.adapter || DomUtils;
|
||||
return (typeof query === "function" ? query : compile(query, options))(
|
||||
elem
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
the exported interface
|
||||
*/
|
||||
function CSSselect(query, elems, options) {
|
||||
return selectAll(query, elems, options);
|
||||
}
|
||||
|
||||
CSSselect.compile = compile;
|
||||
CSSselect.filters = compileRaw.Pseudos.filters;
|
||||
CSSselect.pseudos = compileRaw.Pseudos.pseudos;
|
||||
|
||||
CSSselect.selectAll = selectAll;
|
||||
CSSselect.selectOne = selectOne;
|
||||
|
||||
CSSselect.is = is;
|
||||
|
||||
//legacy methods (might be removed)
|
||||
CSSselect.parse = compile;
|
||||
CSSselect.iterate = selectAll;
|
||||
|
||||
//hooks
|
||||
CSSselect._compileUnsafe = compileUnsafe;
|
||||
CSSselect._compileToken = compileRaw.compileToken;
|
@ -1,214 +0,0 @@
|
||||
/*
|
||||
compiles a selector to an executable function
|
||||
*/
|
||||
|
||||
module.exports = compile;
|
||||
|
||||
const { parse } = require("css-what");
|
||||
const { trueFunc, falseFunc } = require("boolbase");
|
||||
const sortRules = require("./sort.js");
|
||||
const procedure = require("./procedure.json");
|
||||
const Rules = require("./general.js");
|
||||
const Pseudos = require("./pseudos.js");
|
||||
|
||||
const { filters } = Pseudos;
|
||||
|
||||
function compile(selector, options, context) {
|
||||
const next = compileUnsafe(selector, options, context);
|
||||
return wrap(next, options);
|
||||
}
|
||||
|
||||
function wrap(next, { adapter }) {
|
||||
return (elem) => adapter.isTag(elem) && next(elem);
|
||||
}
|
||||
|
||||
function compileUnsafe(selector, options, context) {
|
||||
const token = parse(selector, options);
|
||||
return compileToken(token, options, context);
|
||||
}
|
||||
|
||||
function includesScopePseudo(t) {
|
||||
return (
|
||||
t.type === "pseudo" &&
|
||||
(t.name === "scope" ||
|
||||
(Array.isArray(t.data) &&
|
||||
t.data.some((data) => data.some(includesScopePseudo))))
|
||||
);
|
||||
}
|
||||
|
||||
const DESCENDANT_TOKEN = { type: "descendant" };
|
||||
const FLEXIBLE_DESCENDANT_TOKEN = { type: "_flexibleDescendant" };
|
||||
const SCOPE_TOKEN = { type: "pseudo", name: "scope" };
|
||||
const PLACEHOLDER_ELEMENT = {};
|
||||
|
||||
//CSS 4 Spec (Draft): 3.3.1. Absolutizing a Scope-relative Selector
|
||||
//http://www.w3.org/TR/selectors4/#absolutizing
|
||||
function absolutize(token, { adapter }, context) {
|
||||
//TODO better check if context is document
|
||||
const hasContext =
|
||||
!!context &&
|
||||
!!context.length &&
|
||||
context.every(
|
||||
(e) => e === PLACEHOLDER_ELEMENT || !!adapter.getParent(e)
|
||||
);
|
||||
|
||||
token.forEach((t) => {
|
||||
if (t.length > 0 && isTraversal(t[0]) && t[0].type !== "descendant") {
|
||||
//don't return in else branch
|
||||
} else if (
|
||||
hasContext &&
|
||||
!(Array.isArray(t)
|
||||
? t.some(includesScopePseudo)
|
||||
: includesScopePseudo(t))
|
||||
) {
|
||||
t.unshift(DESCENDANT_TOKEN);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
t.unshift(SCOPE_TOKEN);
|
||||
});
|
||||
}
|
||||
|
||||
function compileToken(token, options, context) {
|
||||
token = token.filter((t) => t.length > 0);
|
||||
|
||||
token.forEach(sortRules);
|
||||
|
||||
const isArrayContext = Array.isArray(context);
|
||||
|
||||
context = options.context || context;
|
||||
|
||||
if (context && !isArrayContext) context = [context];
|
||||
|
||||
absolutize(token, options, context);
|
||||
|
||||
let shouldTestNextSiblings = false;
|
||||
|
||||
const query = token
|
||||
.map((rules) => {
|
||||
if (rules[0] && rules[1] && rules[0].name === "scope") {
|
||||
const ruleType = rules[1].type;
|
||||
if (isArrayContext && ruleType === "descendant") {
|
||||
rules[1] = FLEXIBLE_DESCENDANT_TOKEN;
|
||||
} else if (ruleType === "adjacent" || ruleType === "sibling") {
|
||||
shouldTestNextSiblings = true;
|
||||
}
|
||||
}
|
||||
return compileRules(rules, options, context);
|
||||
})
|
||||
.reduce(reduceRules, falseFunc);
|
||||
|
||||
query.shouldTestNextSiblings = shouldTestNextSiblings;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function isTraversal(t) {
|
||||
return procedure[t.type] < 0;
|
||||
}
|
||||
|
||||
function compileRules(rules, options, context) {
|
||||
return rules.reduce((func, rule) => {
|
||||
if (func === falseFunc) return func;
|
||||
|
||||
if (!(rule.type in Rules)) {
|
||||
throw new Error(
|
||||
`Rule type ${rule.type} is not supported by css-select`
|
||||
);
|
||||
}
|
||||
|
||||
return Rules[rule.type](func, rule, options, context);
|
||||
}, options.rootFunc || trueFunc);
|
||||
}
|
||||
|
||||
function reduceRules(a, b) {
|
||||
if (b === falseFunc || a === trueFunc) {
|
||||
return a;
|
||||
}
|
||||
if (a === falseFunc || b === trueFunc) {
|
||||
return b;
|
||||
}
|
||||
|
||||
return (elem) => a(elem) || b(elem);
|
||||
}
|
||||
|
||||
function containsTraversal(t) {
|
||||
return t.some(isTraversal);
|
||||
}
|
||||
|
||||
//:not, :has and :matches have to compile selectors
|
||||
//doing this in lib/pseudos.js would lead to circular dependencies,
|
||||
//so we add them here
|
||||
filters.not = function (next, token, options, context) {
|
||||
const opts = {
|
||||
xmlMode: !!options.xmlMode,
|
||||
strict: !!options.strict,
|
||||
adapter: options.adapter,
|
||||
};
|
||||
|
||||
if (opts.strict) {
|
||||
if (token.length > 1 || token.some(containsTraversal)) {
|
||||
throw new Error(
|
||||
"complex selectors in :not aren't allowed in strict mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const func = compileToken(token, opts, context);
|
||||
|
||||
if (func === falseFunc) return next;
|
||||
if (func === trueFunc) return falseFunc;
|
||||
|
||||
return (elem) => !func(elem) && next(elem);
|
||||
};
|
||||
|
||||
filters.has = function (next, token, options) {
|
||||
const { adapter } = options;
|
||||
const opts = {
|
||||
xmlMode: !!options.xmlMode,
|
||||
strict: !!options.strict,
|
||||
adapter,
|
||||
};
|
||||
|
||||
//FIXME: Uses an array as a pointer to the current element (side effects)
|
||||
const context = token.some(containsTraversal)
|
||||
? [PLACEHOLDER_ELEMENT]
|
||||
: null;
|
||||
|
||||
let func = compileToken(token, opts, context);
|
||||
|
||||
if (func === falseFunc) return falseFunc;
|
||||
if (func === trueFunc) {
|
||||
return (elem) =>
|
||||
adapter.getChildren(elem).some(adapter.isTag) && next(elem);
|
||||
}
|
||||
|
||||
func = wrap(func, options);
|
||||
|
||||
if (context) {
|
||||
return (elem) =>
|
||||
next(elem) &&
|
||||
// TODO: Add note why this is done
|
||||
((context[0] = elem),
|
||||
adapter.existsOne(func, adapter.getChildren(elem)));
|
||||
}
|
||||
|
||||
return (elem) =>
|
||||
next(elem) && adapter.existsOne(func, adapter.getChildren(elem));
|
||||
};
|
||||
|
||||
filters.matches = function (next, token, options, context) {
|
||||
const opts = {
|
||||
xmlMode: options.xmlMode,
|
||||
strict: options.strict,
|
||||
adapter: options.adapter,
|
||||
rootFunc: next,
|
||||
};
|
||||
|
||||
return compileToken(token, opts, context);
|
||||
};
|
||||
|
||||
compile.compileToken = compileToken;
|
||||
compile.compileUnsafe = compileUnsafe;
|
||||
compile.Pseudos = Pseudos;
|
@ -1,104 +0,0 @@
|
||||
const attributes = require("./attributes.js");
|
||||
const Pseudos = require("./pseudos");
|
||||
|
||||
/*
|
||||
all available rules
|
||||
*/
|
||||
module.exports = {
|
||||
__proto__: null,
|
||||
|
||||
attribute: attributes.compile,
|
||||
pseudo: Pseudos.compile,
|
||||
|
||||
//tags
|
||||
tag(next, data, { adapter }) {
|
||||
const { name } = data;
|
||||
|
||||
return (elem) => adapter.getName(elem) === name && next(elem);
|
||||
},
|
||||
|
||||
//traversal
|
||||
descendant(next, data, { adapter }) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const isFalseCache =
|
||||
typeof WeakSet !== "undefined" ? new WeakSet() : null;
|
||||
|
||||
return function descendant(elem) {
|
||||
let found = false;
|
||||
|
||||
while (!found && (elem = adapter.getParent(elem))) {
|
||||
if (!isFalseCache || !isFalseCache.has(elem)) {
|
||||
found = next(elem);
|
||||
if (!found && isFalseCache) {
|
||||
isFalseCache.add(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
};
|
||||
},
|
||||
_flexibleDescendant(next, data, { adapter }) {
|
||||
// Include element itself, only used while querying an array
|
||||
return function descendant(elem) {
|
||||
let found = next(elem);
|
||||
|
||||
while (!found && (elem = adapter.getParent(elem))) {
|
||||
found = next(elem);
|
||||
}
|
||||
|
||||
return found;
|
||||
};
|
||||
},
|
||||
parent(next, data, options) {
|
||||
if (options.strict) {
|
||||
throw new Error("Parent selector isn't part of CSS3");
|
||||
}
|
||||
|
||||
const { adapter } = options;
|
||||
|
||||
return (elem) => adapter.getChildren(elem).some(test);
|
||||
|
||||
function test(elem) {
|
||||
return adapter.isTag(elem) && next(elem);
|
||||
}
|
||||
},
|
||||
child(next, data, { adapter }) {
|
||||
return function child(elem) {
|
||||
const parent = adapter.getParent(elem);
|
||||
return !!parent && next(parent);
|
||||
};
|
||||
},
|
||||
sibling(next, data, { adapter }) {
|
||||
return function sibling(elem) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
if (next(siblings[i])) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
},
|
||||
adjacent(next, data, { adapter }) {
|
||||
return function adjacent(elem) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
let lastElement;
|
||||
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
lastElement = siblings[i];
|
||||
}
|
||||
}
|
||||
|
||||
return !!lastElement && next(lastElement);
|
||||
};
|
||||
},
|
||||
universal(next) {
|
||||
return next;
|
||||
},
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"universal": 50,
|
||||
"tag": 30,
|
||||
"attribute": 1,
|
||||
"pseudo": 0,
|
||||
"descendant": -1,
|
||||
"child": -1,
|
||||
"parent": -1,
|
||||
"sibling": -1,
|
||||
"adjacent": -1
|
||||
}
|
@ -1,426 +0,0 @@
|
||||
/*
|
||||
pseudo selectors
|
||||
|
||||
---
|
||||
|
||||
they are available in two forms:
|
||||
* filters called when the selector
|
||||
is compiled and return a function
|
||||
that needs to return next()
|
||||
* pseudos get called on execution
|
||||
they need to return a boolean
|
||||
*/
|
||||
|
||||
const getNCheck = require("nth-check");
|
||||
const { trueFunc, falseFunc } = require("boolbase");
|
||||
const attributes = require("./attributes.js");
|
||||
|
||||
const checkAttrib = attributes.rules.equals;
|
||||
|
||||
function getAttribFunc(name, value) {
|
||||
const data = { name, value };
|
||||
return (next, rule, options) => checkAttrib(next, data, options);
|
||||
}
|
||||
|
||||
function getChildFunc(next, adapter) {
|
||||
return (elem) => !!adapter.getParent(elem) && next(elem);
|
||||
}
|
||||
|
||||
const filters = {
|
||||
contains(next, text, { adapter }) {
|
||||
return (elem) => next(elem) && adapter.getText(elem).includes(text);
|
||||
},
|
||||
icontains(next, text, options) {
|
||||
const itext = text.toLowerCase();
|
||||
const { adapter } = options;
|
||||
|
||||
return (elem) =>
|
||||
next(elem) && adapter.getText(elem).toLowerCase().includes(itext);
|
||||
},
|
||||
|
||||
//location specific methods
|
||||
"nth-child"(next, rule, { adapter }) {
|
||||
const func = getNCheck(rule);
|
||||
|
||||
if (func === falseFunc) return func;
|
||||
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 (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
else pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
"nth-last-child"(next, rule, { adapter }) {
|
||||
const func = getNCheck(rule);
|
||||
|
||||
if (func === falseFunc) return func;
|
||||
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 (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
else pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
"nth-of-type"(next, rule, { adapter }) {
|
||||
const func = getNCheck(rule);
|
||||
|
||||
if (func === falseFunc) return func;
|
||||
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++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem))
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
"nth-last-of-type"(next, rule, { adapter }) {
|
||||
const func = getNCheck(rule);
|
||||
|
||||
if (func === falseFunc) return func;
|
||||
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--) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem))
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
|
||||
//TODO determine the actual root element
|
||||
root(next, rule, { adapter }) {
|
||||
return (elem) => !adapter.getParent(elem) && next(elem);
|
||||
},
|
||||
|
||||
scope(next, rule, options, context) {
|
||||
const { adapter } = options;
|
||||
|
||||
if (!context || context.length === 0) {
|
||||
//equivalent to :root
|
||||
return filters.root(next, rule, options);
|
||||
}
|
||||
|
||||
function equals(a, b) {
|
||||
if (typeof adapter.equals === "function")
|
||||
return adapter.equals(a, b);
|
||||
|
||||
return a === b;
|
||||
}
|
||||
|
||||
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.indexOf(elem) >= 0 && next(elem);
|
||||
},
|
||||
|
||||
//jQuery extensions (others follow as pseudos)
|
||||
checkbox: getAttribFunc("type", "checkbox"),
|
||||
file: getAttribFunc("type", "file"),
|
||||
password: getAttribFunc("type", "password"),
|
||||
radio: getAttribFunc("type", "radio"),
|
||||
reset: getAttribFunc("type", "reset"),
|
||||
image: getAttribFunc("type", "image"),
|
||||
submit: getAttribFunc("type", "submit"),
|
||||
|
||||
//dynamic state pseudos. These depend on optional Adapter methods.
|
||||
hover(next, rule, { adapter }) {
|
||||
if (typeof adapter.isHovered === "function") {
|
||||
return (elem) => adapter.isHovered(elem) && next(elem);
|
||||
}
|
||||
|
||||
return falseFunc;
|
||||
},
|
||||
visited(next, rule, { adapter }) {
|
||||
if (typeof adapter.isVisited === "function") {
|
||||
return (elem) => adapter.isVisited(elem) && next(elem);
|
||||
}
|
||||
|
||||
return falseFunc;
|
||||
},
|
||||
active(next, rule, { adapter }) {
|
||||
if (typeof adapter.isActive === "function") {
|
||||
return (elem) => adapter.isActive(elem) && next(elem);
|
||||
}
|
||||
|
||||
return falseFunc;
|
||||
},
|
||||
};
|
||||
|
||||
//helper methods
|
||||
function getFirstElement(elems, adapter) {
|
||||
for (let i = 0; elems && i < elems.length; i++) {
|
||||
if (adapter.isTag(elems[i])) return elems[i];
|
||||
}
|
||||
}
|
||||
|
||||
//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) || elem.type === "text");
|
||||
},
|
||||
|
||||
"first-child"(elem, adapter) {
|
||||
return getFirstElement(adapter.getSiblings(elem), adapter) === elem;
|
||||
},
|
||||
"last-child"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = siblings.length - 1; i >= 0; i--) {
|
||||
if (siblings[i] === elem) return true;
|
||||
if (adapter.isTag(siblings[i])) break;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"first-of-type"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) return true;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"last-of-type"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = siblings.length - 1; i >= 0; i--) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) return true;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"only-of-type"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = 0, j = siblings.length; i < j; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) continue;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
"only-child"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i]) && siblings[i] !== elem)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
//:matches(a, area, link)[href]
|
||||
link(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "href");
|
||||
},
|
||||
//TODO: :any-link once the name is finalized (as an alias of :link)
|
||||
|
||||
//forms
|
||||
//to consider: :target
|
||||
|
||||
//:matches([selected], select:not([multiple]):not(> option[selected]) > option:first-of-type)
|
||||
selected(elem, adapter) {
|
||||
if (adapter.hasAttrib(elem, "selected")) return true;
|
||||
else if (adapter.getName(elem) !== "option") return false;
|
||||
|
||||
//the first <option> in a <select> is also selected
|
||||
const parent = adapter.getParent(elem);
|
||||
|
||||
if (
|
||||
!parent ||
|
||||
adapter.getName(parent) !== "select" ||
|
||||
adapter.hasAttrib(parent, "multiple")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const siblings = adapter.getChildren(parent);
|
||||
let sawElem = false;
|
||||
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) {
|
||||
sawElem = true;
|
||||
} else if (!sawElem) {
|
||||
return false;
|
||||
} else if (adapter.hasAttrib(siblings[i], "selected")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sawElem;
|
||||
},
|
||||
//https://html.spec.whatwg.org/multipage/scripting.html#disabled-elements
|
||||
//:matches(
|
||||
// :matches(button, input, select, textarea, menuitem, optgroup, option)[disabled],
|
||||
// optgroup[disabled] > option),
|
||||
// fieldset[disabled] * //TODO not child of first <legend>
|
||||
//)
|
||||
disabled(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "disabled");
|
||||
},
|
||||
enabled(elem, adapter) {
|
||||
return !adapter.hasAttrib(elem, "disabled");
|
||||
},
|
||||
//:matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem)
|
||||
checked(elem, adapter) {
|
||||
return (
|
||||
adapter.hasAttrib(elem, "checked") ||
|
||||
pseudos.selected(elem, adapter)
|
||||
);
|
||||
},
|
||||
//:matches(input, select, textarea)[required]
|
||||
required(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "required");
|
||||
},
|
||||
//:matches(input, select, textarea):not([required])
|
||||
optional(elem, adapter) {
|
||||
return !adapter.hasAttrib(elem, "required");
|
||||
},
|
||||
|
||||
//jQuery extensions
|
||||
|
||||
//:not(:empty)
|
||||
parent(elem, adapter) {
|
||||
return !pseudos.empty(elem, adapter);
|
||||
},
|
||||
//:matches(h1, h2, h3, h4, h5, h6)
|
||||
header: namePseudo(["h1", "h2", "h3", "h4", "h5", "h6"]),
|
||||
|
||||
//:matches(button, input[type=button])
|
||||
button(elem, adapter) {
|
||||
const name = adapter.getName(elem);
|
||||
return (
|
||||
name === "button" ||
|
||||
(name === "input" &&
|
||||
adapter.getAttributeValue(elem, "type") === "button")
|
||||
);
|
||||
},
|
||||
//:matches(input, textarea, select, button)
|
||||
input: namePseudo(["input", "textarea", "select", "button"]),
|
||||
//input:matches(:not([type!='']), [type='text' i])
|
||||
text(elem, adapter) {
|
||||
let attr;
|
||||
return (
|
||||
adapter.getName(elem) === "input" &&
|
||||
(!(attr = adapter.getAttributeValue(elem, "type")) ||
|
||||
attr.toLowerCase() === "text")
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
function namePseudo(names) {
|
||||
if (typeof Set !== "undefined") {
|
||||
// eslint-disable-next-line no-undef
|
||||
const nameSet = new Set(names);
|
||||
|
||||
return (elem, adapter) => nameSet.has(adapter.getName(elem));
|
||||
}
|
||||
|
||||
return (elem, adapter) => names.indexOf(adapter.getName(elem)) >= 0;
|
||||
}
|
||||
|
||||
function verifyArgs(func, name, subselect) {
|
||||
if (subselect === null) {
|
||||
if (func.length > 2 && name !== "scope") {
|
||||
throw new Error(`pseudo-selector :${name} requires an argument`);
|
||||
}
|
||||
} else {
|
||||
if (func.length === 2) {
|
||||
throw new Error(
|
||||
`pseudo-selector :${name} doesn't have any arguments`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//FIXME this feels hacky
|
||||
const re_CSS3 = /^(?:(?:nth|last|first|only)-(?:child|of-type)|root|empty|(?:en|dis)abled|checked|not)$/;
|
||||
|
||||
module.exports = {
|
||||
compile(next, data, options, context) {
|
||||
const { name } = data;
|
||||
const subselect = data.data;
|
||||
const { adapter } = options;
|
||||
|
||||
if (options.strict && !re_CSS3.test(name)) {
|
||||
throw new Error(`:${name} isn't part of CSS3`);
|
||||
}
|
||||
|
||||
if (typeof filters[name] === "function") {
|
||||
return filters[name](next, subselect, options, context);
|
||||
} else if (typeof pseudos[name] === "function") {
|
||||
const func = pseudos[name];
|
||||
|
||||
verifyArgs(func, name, subselect);
|
||||
|
||||
if (func === falseFunc) {
|
||||
return func;
|
||||
}
|
||||
|
||||
if (next === trueFunc) {
|
||||
return (elem) => func(elem, adapter, subselect);
|
||||
}
|
||||
|
||||
return (elem) => func(elem, adapter, subselect) && next(elem);
|
||||
} else {
|
||||
throw new Error(`unmatched pseudo-class :${name}`);
|
||||
}
|
||||
},
|
||||
filters,
|
||||
pseudos,
|
||||
};
|
@ -0,0 +1,163 @@
|
||||
import { InternalSelector } from "./types";
|
||||
import { parse, Selector } from "css-what";
|
||||
import { trueFunc, falseFunc } from "boolbase";
|
||||
import sortRules from "./sort";
|
||||
import { isTraversal } from "./procedure";
|
||||
import { compileGeneralSelector } from "./general";
|
||||
import {
|
||||
ensureIsTag,
|
||||
PLACEHOLDER_ELEMENT,
|
||||
} from "./pseudo-selectors/subselects";
|
||||
import type { CompiledQuery, InternalOptions } from "./types";
|
||||
|
||||
/**
|
||||
* Compiles a selector to an executable function.
|
||||
*
|
||||
* @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,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: ElementNode[]
|
||||
): CompiledQuery<ElementNode> {
|
||||
const next = compileUnsafe(selector, options, context);
|
||||
return ensureIsTag(next, options.adapter);
|
||||
}
|
||||
|
||||
export function compileUnsafe<Node, ElementNode extends Node>(
|
||||
selector: string,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: ElementNode[] | ElementNode
|
||||
): CompiledQuery<ElementNode> {
|
||||
const token = parse(selector, options);
|
||||
return compileToken<Node, ElementNode>(token, options, context);
|
||||
}
|
||||
|
||||
function includesScopePseudo(t: InternalSelector): boolean {
|
||||
return (
|
||||
t.type === "pseudo" &&
|
||||
(t.name === "scope" ||
|
||||
(Array.isArray(t.data) &&
|
||||
t.data.some((data) => data.some(includesScopePseudo))))
|
||||
);
|
||||
}
|
||||
|
||||
const DESCENDANT_TOKEN: Selector = { type: "descendant" };
|
||||
const FLEXIBLE_DESCENDANT_TOKEN: InternalSelector = {
|
||||
type: "_flexibleDescendant",
|
||||
};
|
||||
const SCOPE_TOKEN: Selector = { type: "pseudo", name: "scope", data: null };
|
||||
|
||||
/*
|
||||
* CSS 4 Spec (Draft): 3.3.1. Absolutizing a Scope-relative Selector
|
||||
* http://www.w3.org/TR/selectors4/#absolutizing
|
||||
*/
|
||||
function absolutize<Node, ElementNode extends Node>(
|
||||
token: InternalSelector[][],
|
||||
{ adapter }: InternalOptions<Node, ElementNode>,
|
||||
context?: ElementNode[]
|
||||
) {
|
||||
// TODO Use better check if the context is a document
|
||||
const hasContext = !!context?.every(
|
||||
(e) => e === PLACEHOLDER_ELEMENT || !!adapter.getParent(e)
|
||||
);
|
||||
|
||||
for (const t of token) {
|
||||
if (t.length > 0 && isTraversal(t[0]) && t[0].type !== "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>,
|
||||
context?: ElementNode[] | ElementNode
|
||||
): CompiledQuery<ElementNode> {
|
||||
token = token.filter((t) => t.length > 0);
|
||||
|
||||
token.forEach(sortRules);
|
||||
|
||||
context = options.context ?? context;
|
||||
const isArrayContext = Array.isArray(context);
|
||||
|
||||
const finalContext =
|
||||
context && (Array.isArray(context) ? context : [context]);
|
||||
|
||||
absolutize(token, options, finalContext);
|
||||
|
||||
let shouldTestNextSiblings = false;
|
||||
|
||||
const query = token
|
||||
.map((rules) => {
|
||||
if (rules.length >= 2) {
|
||||
const [first, second] = rules;
|
||||
|
||||
if (first.type !== "pseudo" || first.name !== "scope") {
|
||||
// Ignore
|
||||
} else if (isArrayContext && second.type === "descendant") {
|
||||
rules[1] = FLEXIBLE_DESCENDANT_TOKEN;
|
||||
} else if (
|
||||
second.type === "adjacent" ||
|
||||
second.type === "sibling"
|
||||
) {
|
||||
shouldTestNextSiblings = true;
|
||||
}
|
||||
}
|
||||
|
||||
return compileRules<Node, ElementNode>(
|
||||
rules,
|
||||
options,
|
||||
finalContext
|
||||
);
|
||||
})
|
||||
.reduce(reduceRules, falseFunc);
|
||||
|
||||
query.shouldTestNextSiblings = shouldTestNextSiblings;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function compileRules<Node, ElementNode extends Node>(
|
||||
rules: InternalSelector[],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: ElementNode[]
|
||||
): CompiledQuery<ElementNode> {
|
||||
return rules.reduce<CompiledQuery<ElementNode>>(
|
||||
(previous, rule) =>
|
||||
previous === falseFunc
|
||||
? falseFunc
|
||||
: compileGeneralSelector(
|
||||
previous,
|
||||
rule,
|
||||
options,
|
||||
context,
|
||||
compileToken
|
||||
),
|
||||
options.rootFunc ?? trueFunc
|
||||
);
|
||||
}
|
||||
|
||||
function reduceRules<Node, ElementNode extends Node>(
|
||||
a: CompiledQuery<ElementNode>,
|
||||
b: CompiledQuery<ElementNode>
|
||||
): CompiledQuery<ElementNode> {
|
||||
if (b === falseFunc || a === trueFunc) {
|
||||
return a;
|
||||
}
|
||||
if (a === falseFunc || b === trueFunc) {
|
||||
return b;
|
||||
}
|
||||
|
||||
return function combine(elem) {
|
||||
return a(elem) || b(elem);
|
||||
};
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
declare module "boolbase" {
|
||||
export function trueFunc(...args: unknown[]): true;
|
||||
export function falseFunc(...args: unknown[]): false;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
declare module "nth-check" {
|
||||
export default function nthCheck(
|
||||
formula: string
|
||||
): (position: number) => boolean;
|
||||
export function parse(formula: string): [number, number];
|
||||
export function compile(
|
||||
parsed: [number, number]
|
||||
): (position: number) => boolean;
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
import { attributeRules } from "./attributes";
|
||||
import { compilePseudoSelector } from "./pseudo-selectors";
|
||||
import type {
|
||||
CompiledQuery,
|
||||
InternalOptions,
|
||||
InternalSelector,
|
||||
CompileToken,
|
||||
} from "./types";
|
||||
|
||||
/*
|
||||
* All available rules
|
||||
*/
|
||||
|
||||
export function compileGeneralSelector<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
selector: InternalSelector,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context: ElementNode[] | undefined,
|
||||
compileToken: CompileToken<Node, ElementNode>
|
||||
): CompiledQuery<ElementNode> {
|
||||
const { adapter } = options;
|
||||
|
||||
switch (selector.type) {
|
||||
case "pseudo-element":
|
||||
throw new Error("Pseudo-elements are not supported by css-select");
|
||||
|
||||
case "attribute":
|
||||
if (
|
||||
options.strict &&
|
||||
(selector.ignoreCase || selector.action === "not")
|
||||
) {
|
||||
throw new Error("Unsupported attribute selector");
|
||||
}
|
||||
|
||||
return attributeRules[selector.action](next, selector, options);
|
||||
|
||||
case "pseudo":
|
||||
return compilePseudoSelector(
|
||||
next,
|
||||
selector,
|
||||
options,
|
||||
context,
|
||||
compileToken
|
||||
);
|
||||
|
||||
// Tags
|
||||
case "tag":
|
||||
return function tag(elem: ElementNode): boolean {
|
||||
return adapter.getName(elem) === selector.name && next(elem);
|
||||
};
|
||||
|
||||
// Traversal
|
||||
case "descendant":
|
||||
if (typeof WeakSet === "undefined") {
|
||||
return function descendant(elem: ElementNode): boolean {
|
||||
let current: ElementNode | null = elem;
|
||||
|
||||
while ((current = adapter.getParent(current))) {
|
||||
if (next(current)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error `ElementNode` is not extending object
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const isFalseCache = new WeakSet<ElementNode>();
|
||||
return function cachedDescendant(elem: ElementNode): boolean {
|
||||
let current: ElementNode | null = elem;
|
||||
|
||||
while ((current = adapter.getParent(current))) {
|
||||
if (!isFalseCache.has(elem)) {
|
||||
if (next(current)) return true;
|
||||
isFalseCache.add(current);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
} while ((current = adapter.getParent(current)));
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
case "parent":
|
||||
if (options.strict) {
|
||||
throw new Error("Parent selector isn't part of CSS3");
|
||||
}
|
||||
|
||||
return function parent(elem: ElementNode): boolean {
|
||||
return adapter
|
||||
.getChildren(elem)
|
||||
.some((elem) => adapter.isTag(elem) && next(elem));
|
||||
};
|
||||
|
||||
case "child":
|
||||
return function child(elem: ElementNode): boolean {
|
||||
const parent = adapter.getParent(elem);
|
||||
return !!parent && next(parent);
|
||||
};
|
||||
|
||||
case "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 (adapter.isTag(currentSibling)) {
|
||||
if (currentSibling === elem) break;
|
||||
if (next(currentSibling)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
case "adjacent":
|
||||
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 (adapter.isTag(currentSibling)) {
|
||||
if (currentSibling === elem) break;
|
||||
lastElement = currentSibling;
|
||||
}
|
||||
}
|
||||
|
||||
return !!lastElement && next(lastElement);
|
||||
};
|
||||
|
||||
case "universal":
|
||||
return next;
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
import * as DomUtils from "domutils";
|
||||
import { falseFunc } from "boolbase";
|
||||
import { compile as compileRaw, compileUnsafe, compileToken } from "./compile";
|
||||
|
||||
import {
|
||||
CompiledQuery,
|
||||
Options,
|
||||
InternalOptions,
|
||||
Query,
|
||||
Adapter,
|
||||
Predicate,
|
||||
} from "./types";
|
||||
|
||||
export type { Options };
|
||||
|
||||
const defaultOptions = { adapter: DomUtils };
|
||||
|
||||
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 = opts.adapter ?? DomUtils;
|
||||
|
||||
return opts as InternalOptions<Node, ElementNode>;
|
||||
}
|
||||
|
||||
function wrapCompile<Selector, Node, ElementNode extends Node>(
|
||||
func: (
|
||||
selector: Selector,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: Node[]
|
||||
) => CompiledQuery<ElementNode>
|
||||
) {
|
||||
return function addAdapter(
|
||||
selector: Selector,
|
||||
options?: Options<Node, ElementNode>,
|
||||
context?: Node[]
|
||||
) {
|
||||
const opts = convertOptionFormats(options);
|
||||
|
||||
return func(selector, opts, context);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the query, returns a function.
|
||||
*/
|
||||
export const compile = wrapCompile(compileRaw);
|
||||
export const _compileUnsafe = wrapCompile(compileUnsafe);
|
||||
export const _compileToken = wrapCompile(compileToken);
|
||||
|
||||
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: ElementNode[] | ElementNode,
|
||||
options?: Options<Node, ElementNode>
|
||||
): T {
|
||||
const opts = convertOptionFormats(options);
|
||||
let elems: ElementNode[] | ElementNode = elements;
|
||||
|
||||
if (typeof query !== "function") {
|
||||
query = compileUnsafe<Node, ElementNode>(query, opts, elems);
|
||||
}
|
||||
/*
|
||||
* Add siblings if the query requires them.
|
||||
* See https://github.com/fb55/css-select/pull/43#issuecomment-225414692
|
||||
*/
|
||||
if (query.shouldTestNextSiblings) {
|
||||
elems = appendNextSiblings(opts.context ?? elems, opts.adapter);
|
||||
}
|
||||
|
||||
const filteredElements = Array.isArray(elems)
|
||||
? opts.adapter.removeSubsets(elems)
|
||||
: opts.adapter.getChildren(elems);
|
||||
return searchFunc(query, filteredElements, opts);
|
||||
};
|
||||
}
|
||||
|
||||
function getNextSiblings<Node, ElementNode extends Node>(
|
||||
elem: Node,
|
||||
adapter: Adapter<Node, ElementNode>
|
||||
): Node[] {
|
||||
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);
|
||||
}
|
||||
|
||||
function appendNextSiblings<Node, ElementNode extends Node>(
|
||||
elem: ElementNode | ElementNode[],
|
||||
adapter: Adapter<Node, ElementNode>
|
||||
): ElementNode[] {
|
||||
// Order matters because jQuery seems to check the children before the siblings
|
||||
const elems = Array.isArray(elem) ? elem.slice(0) : [elem];
|
||||
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
const nextSiblings = getNextSiblings(elems[i], adapter);
|
||||
elems.push(
|
||||
...nextSiblings.filter((sibling): sibling is ElementNode =>
|
||||
adapter.isTag(sibling)
|
||||
)
|
||||
);
|
||||
}
|
||||
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 = getSelectorFunc(
|
||||
<Node, ElementNode extends Node>(
|
||||
query: Predicate<ElementNode>,
|
||||
elems: Node[] | null,
|
||||
options: InternalOptions<Node, ElementNode>
|
||||
) =>
|
||||
query === falseFunc || !elems || elems.length === 0
|
||||
? []
|
||||
: options.adapter.findAll(query, 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 the first match, or null if there was no match.
|
||||
*/
|
||||
export const selectOne = getSelectorFunc(
|
||||
<Node, ElementNode extends Node>(
|
||||
query: Predicate<ElementNode>,
|
||||
elems: Node[] | null,
|
||||
options: InternalOptions<Node, ElementNode>
|
||||
) =>
|
||||
query === falseFunc || !elems || elems.length === 0
|
||||
? null
|
||||
: options.adapter.findOne(query, elems)
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
const opts = convertOptionFormats(options);
|
||||
return (typeof query === "function" ? query : compileRaw(query, opts))(
|
||||
elem
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for selectAll(query, elems, options).
|
||||
* @see [compile] for supported selector queries.
|
||||
*/
|
||||
export default selectAll;
|
||||
|
||||
// Export filters and pseudos to allow users to supply their own.
|
||||
export { filters, pseudos } from "./pseudo-selectors";
|
@ -0,0 +1,20 @@
|
||||
import type { Traversal } from "css-what";
|
||||
import type { InternalSelector } from "./types";
|
||||
|
||||
export const procedure: Record<InternalSelector["type"], number> = {
|
||||
universal: 50,
|
||||
tag: 30,
|
||||
attribute: 1,
|
||||
pseudo: 0,
|
||||
"pseudo-element": 0, // Here to make TS happy, we don't support this.
|
||||
descendant: -1,
|
||||
child: -1,
|
||||
parent: -1,
|
||||
sibling: -1,
|
||||
adjacent: -1,
|
||||
_flexibleDescendant: -1,
|
||||
};
|
||||
|
||||
export function isTraversal(t: InternalSelector): t is Traversal {
|
||||
return procedure[t.type] < 0;
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
import getNCheck from "nth-check";
|
||||
import { trueFunc, falseFunc } from "boolbase";
|
||||
import { attributeRules } from "../attributes";
|
||||
import type { CompiledQuery, InternalOptions, Adapter } from "../types";
|
||||
import type { AttributeSelector } from "css-what";
|
||||
|
||||
export type Filter = <Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
text: string,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: ElementNode[]
|
||||
) => CompiledQuery<ElementNode>;
|
||||
|
||||
const checkAttrib = attributeRules.equals;
|
||||
|
||||
function getAttribFunc(name: string, value: string): Filter {
|
||||
const data: AttributeSelector = {
|
||||
type: "attribute",
|
||||
action: "equals",
|
||||
ignoreCase: false,
|
||||
name,
|
||||
value,
|
||||
};
|
||||
|
||||
return function attribFunc(next, _rule, options) {
|
||||
return checkAttrib(next, data, options);
|
||||
};
|
||||
}
|
||||
|
||||
function getChildFunc<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
adapter: Adapter<Node, ElementNode>
|
||||
): CompiledQuery<ElementNode> {
|
||||
return (elem) => !!adapter.getParent(elem) && next(elem);
|
||||
}
|
||||
|
||||
export const filters: Record<string, Filter> = {
|
||||
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 }) {
|
||||
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 (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
else pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
"nth-last-child"(next, rule, { adapter }) {
|
||||
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 (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
else pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
"nth-of-type"(next, rule, { adapter }) {
|
||||
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 (adapter.isTag(currentSibling)) {
|
||||
if (currentSibling === elem) break;
|
||||
if (
|
||||
adapter.getName(currentSibling) ===
|
||||
adapter.getName(elem)
|
||||
) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
"nth-last-of-type"(next, rule, { adapter }) {
|
||||
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 (adapter.isTag(currentSibling)) {
|
||||
if (currentSibling === elem) break;
|
||||
if (
|
||||
adapter.getName(currentSibling) ===
|
||||
adapter.getName(elem)
|
||||
) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
|
||||
// TODO determine the actual root element
|
||||
root(next, _rule, { adapter }) {
|
||||
return (elem) => !adapter.getParent(elem) && next(elem);
|
||||
},
|
||||
|
||||
scope<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
rule: string,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: ElementNode[]
|
||||
): CompiledQuery<ElementNode> {
|
||||
const { adapter } = options;
|
||||
|
||||
if (!context || context.length === 0) {
|
||||
// Equivalent to :root
|
||||
return filters.root(next, rule, options);
|
||||
}
|
||||
|
||||
const equals: (a: ElementNode, b: ElementNode) => boolean =
|
||||
typeof adapter.equals === "function"
|
||||
? adapter.equals
|
||||
: (a, b) => a === b;
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
// JQuery extensions (others follow as pseudos)
|
||||
checkbox: getAttribFunc("type", "checkbox"),
|
||||
file: getAttribFunc("type", "file"),
|
||||
password: getAttribFunc("type", "password"),
|
||||
radio: getAttribFunc("type", "radio"),
|
||||
reset: getAttribFunc("type", "reset"),
|
||||
image: getAttribFunc("type", "image"),
|
||||
submit: getAttribFunc("type", "submit"),
|
||||
|
||||
// Dynamic state pseudos. These depend on optional Adapter methods.
|
||||
hover(next, _rule, { adapter }) {
|
||||
const { isHovered } = adapter;
|
||||
if (typeof isHovered !== "function") {
|
||||
return falseFunc;
|
||||
}
|
||||
|
||||
return function hover(elem) {
|
||||
return isHovered(elem) && next(elem);
|
||||
};
|
||||
},
|
||||
visited(next, _rule, { adapter }) {
|
||||
const { isVisited } = adapter;
|
||||
if (typeof isVisited !== "function") {
|
||||
return falseFunc;
|
||||
}
|
||||
|
||||
return function visited(elem) {
|
||||
return isVisited(elem) && next(elem);
|
||||
};
|
||||
},
|
||||
active(next, _rule, { adapter }) {
|
||||
const { isActive } = adapter;
|
||||
if (typeof isActive !== "function") {
|
||||
return falseFunc;
|
||||
}
|
||||
|
||||
return function active(elem) {
|
||||
return isActive(elem) && next(elem);
|
||||
};
|
||||
},
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 { trueFunc, falseFunc } from "boolbase";
|
||||
import type { CompiledQuery, InternalOptions, CompileToken } from "../types";
|
||||
import type { PseudoSelector } from "css-what";
|
||||
import { filters } from "./filters";
|
||||
import { pseudos, verifyPseudoArgs } from "./pseudos";
|
||||
import { subselects } from "./subselects";
|
||||
|
||||
export { filters, pseudos };
|
||||
|
||||
// FIXME This is pretty hacky
|
||||
const reCSS3 = /^(?:(?:nth|last|first|only)-(?:child|of-type)|root|empty|(?:en|dis)abled|checked|not)$/;
|
||||
|
||||
export function compilePseudoSelector<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
selector: PseudoSelector,
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context: ElementNode[] | undefined,
|
||||
compileToken: CompileToken<Node, ElementNode>
|
||||
): CompiledQuery<ElementNode> {
|
||||
const { name, data } = selector;
|
||||
const { adapter } = options;
|
||||
|
||||
if (options.strict && !reCSS3.test(name)) {
|
||||
throw new Error(`:${name} isn't part of CSS3`);
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return subselects[name](next, data, options, context, compileToken);
|
||||
}
|
||||
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);
|
||||
|
||||
return pseudo === falseFunc
|
||||
? falseFunc
|
||||
: next === trueFunc
|
||||
? (elem) => pseudo(elem, adapter, data)
|
||||
: (elem) => pseudo(elem, adapter, data) && next(elem);
|
||||
}
|
||||
throw new Error(`unmatched pseudo-class :${name}`);
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
import { PseudoSelector } from "css-what";
|
||||
import type { Adapter } from "../types";
|
||||
|
||||
export type Pseudo = <Node, ElementNode extends Node>(
|
||||
elem: ElementNode,
|
||||
adapter: Adapter<Node, ElementNode>,
|
||||
subselect?: ElementNode | string | null
|
||||
) => boolean;
|
||||
|
||||
// While filters are precompiled, pseudos get called when they are needed
|
||||
export const pseudos: Record<string, Pseudo> = {
|
||||
empty(elem, adapter) {
|
||||
return !adapter.getChildren(elem).some(
|
||||
(elem) =>
|
||||
// FIXME: `getText` call is potentially expensive.
|
||||
adapter.isTag(elem) || adapter.getText(elem) !== ""
|
||||
);
|
||||
},
|
||||
|
||||
"first-child"(elem, adapter) {
|
||||
return (
|
||||
adapter.getSiblings(elem).find((elem) => adapter.isTag(elem)) ===
|
||||
elem
|
||||
);
|
||||
},
|
||||
"last-child"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = siblings.length - 1; i >= 0; i--) {
|
||||
if (siblings[i] === elem) return true;
|
||||
if (adapter.isTag(siblings[i])) break;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"first-of-type"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
const currentSibling = siblings[i];
|
||||
if (adapter.isTag(currentSibling)) {
|
||||
if (siblings[i] === elem) return true;
|
||||
if (adapter.getName(currentSibling) === adapter.getName(elem)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"last-of-type"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = siblings.length - 1; i >= 0; i--) {
|
||||
const currentSibling = siblings[i];
|
||||
if (adapter.isTag(currentSibling)) {
|
||||
if (siblings[i] === elem) return true;
|
||||
if (adapter.getName(currentSibling) === adapter.getName(elem)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"only-of-type"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = 0, j = siblings.length; i < j; i++) {
|
||||
const currentSibling = siblings[i];
|
||||
if (adapter.isTag(currentSibling)) {
|
||||
if (currentSibling === elem) continue;
|
||||
if (adapter.getName(currentSibling) === adapter.getName(elem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
"only-child"(elem, adapter) {
|
||||
const siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i]) && siblings[i] !== elem) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// :matches(a, area, link)[href]
|
||||
link(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "href");
|
||||
},
|
||||
// TODO: :any-link once the name is finalized (as an alias of :link)
|
||||
|
||||
/*
|
||||
* Forms
|
||||
* to consider: :target
|
||||
*/
|
||||
|
||||
// :matches([selected], select:not([multiple]):not(> option[selected]) > option:first-of-type)
|
||||
selected(elem, adapter) {
|
||||
if (adapter.hasAttrib(elem, "selected")) return true;
|
||||
else if (adapter.getName(elem) !== "option") return false;
|
||||
|
||||
// The first <option> in a <select> is also selected
|
||||
const parent = adapter.getParent(elem);
|
||||
|
||||
if (
|
||||
!parent ||
|
||||
adapter.getName(parent) !== "select" ||
|
||||
adapter.hasAttrib(parent, "multiple")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const siblings = adapter.getChildren(parent);
|
||||
let sawElem = false;
|
||||
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
const currentSibling = siblings[i];
|
||||
if (adapter.isTag(currentSibling)) {
|
||||
if (currentSibling === elem) {
|
||||
sawElem = true;
|
||||
} else if (!sawElem) {
|
||||
return false;
|
||||
} else if (adapter.hasAttrib(currentSibling, "selected")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sawElem;
|
||||
},
|
||||
/*
|
||||
* https://html.spec.whatwg.org/multipage/scripting.html#disabled-elements
|
||||
* :matches(
|
||||
* :matches(button, input, select, textarea, menuitem, optgroup, option)[disabled],
|
||||
* optgroup[disabled] > option),
|
||||
* fieldset[disabled] * //TODO not child of first <legend>
|
||||
* )
|
||||
*/
|
||||
disabled(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "disabled");
|
||||
},
|
||||
enabled(elem, adapter) {
|
||||
return !adapter.hasAttrib(elem, "disabled");
|
||||
},
|
||||
// :matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem)
|
||||
checked(elem, adapter) {
|
||||
return (
|
||||
adapter.hasAttrib(elem, "checked") ||
|
||||
pseudos.selected(elem, adapter)
|
||||
);
|
||||
},
|
||||
// :matches(input, select, textarea)[required]
|
||||
required(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "required");
|
||||
},
|
||||
// :matches(input, select, textarea):not([required])
|
||||
optional(elem, adapter) {
|
||||
return !adapter.hasAttrib(elem, "required");
|
||||
},
|
||||
|
||||
// JQuery extensions
|
||||
|
||||
// :not(:empty)
|
||||
parent(elem, adapter) {
|
||||
return !pseudos.empty(elem, adapter);
|
||||
},
|
||||
// :matches(h1, h2, h3, h4, h5, h6)
|
||||
header: namePseudo(["h1", "h2", "h3", "h4", "h5", "h6"]),
|
||||
|
||||
// :matches(button, input[type=button])
|
||||
button(elem, adapter) {
|
||||
const name = adapter.getName(elem);
|
||||
return (
|
||||
name === "button" ||
|
||||
(name === "input" &&
|
||||
adapter.getAttributeValue(elem, "type") === "button")
|
||||
);
|
||||
},
|
||||
// :matches(input, textarea, select, button)
|
||||
input: namePseudo(["input", "textarea", "select", "button"]),
|
||||
// `input:matches(:not([type!='']), [type='text' i])`
|
||||
text(elem, adapter) {
|
||||
const type = adapter.getAttributeValue(elem, "type");
|
||||
return (
|
||||
adapter.getName(elem) === "input" &&
|
||||
(!type || type.toLowerCase() === "text")
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
function namePseudo<Node, ElementNode extends Node>(names: string[]) {
|
||||
if (typeof Set !== "undefined") {
|
||||
const nameSet = new Set(names);
|
||||
|
||||
return (elem: ElementNode, adapter: Adapter<Node, ElementNode>) =>
|
||||
nameSet.has(adapter.getName(elem));
|
||||
}
|
||||
|
||||
return (elem: ElementNode, adapter: Adapter<Node, ElementNode>) =>
|
||||
names.includes(adapter.getName(elem));
|
||||
}
|
||||
|
||||
export function verifyPseudoArgs(
|
||||
func: Pseudo,
|
||||
name: string,
|
||||
subselect: PseudoSelector["data"]
|
||||
): void {
|
||||
if (subselect === null) {
|
||||
if (func.length > 2 && name !== "scope") {
|
||||
throw new Error(`pseudo-selector :${name} requires an argument`);
|
||||
}
|
||||
} else {
|
||||
if (func.length === 2) {
|
||||
throw new Error(
|
||||
`pseudo-selector :${name} doesn't have any arguments`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import { CompileToken } from "./../types";
|
||||
import type { Selector } from "css-what";
|
||||
import { trueFunc, falseFunc } from "boolbase";
|
||||
import type { CompiledQuery, InternalOptions, Adapter } from "../types";
|
||||
import { isTraversal } from "../procedure";
|
||||
|
||||
/** Used as a placeholder for :has. Will be replaced with the actual element. */
|
||||
export const PLACEHOLDER_ELEMENT = {};
|
||||
|
||||
function containsTraversal(t: Selector[]): boolean {
|
||||
return t.some(isTraversal);
|
||||
}
|
||||
|
||||
export function ensureIsTag<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
adapter: Adapter<Node, ElementNode>
|
||||
): CompiledQuery<ElementNode> {
|
||||
if (next === falseFunc) return next;
|
||||
return (elem: Node) => adapter.isTag(elem) && next(elem);
|
||||
}
|
||||
|
||||
export type Subselect = <Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
subselect: Selector[][],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context: ElementNode[] | undefined,
|
||||
compileToken: CompileToken<Node, ElementNode>
|
||||
) => CompiledQuery<ElementNode>;
|
||||
|
||||
/*
|
||||
* :not, :has and :matches 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` is an alias for `:matches`.
|
||||
*/
|
||||
is(next, token, options, context, compileToken) {
|
||||
return subselects.matches(next, token, options, context, compileToken);
|
||||
},
|
||||
matches(next, token, options, context, compileToken) {
|
||||
const opts = {
|
||||
xmlMode: !!options.xmlMode,
|
||||
strict: !!options.strict,
|
||||
adapter: options.adapter,
|
||||
rootFunc: next,
|
||||
};
|
||||
|
||||
return compileToken(token, opts, context);
|
||||
},
|
||||
not(next, token, options, context, compileToken) {
|
||||
const opts = {
|
||||
xmlMode: !!options.xmlMode,
|
||||
strict: !!options.strict,
|
||||
adapter: options.adapter,
|
||||
};
|
||||
|
||||
if (opts.strict) {
|
||||
if (token.length > 1 || token.some(containsTraversal)) {
|
||||
throw new Error(
|
||||
"complex selectors in :not aren't allowed in strict mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const func = compileToken(token, opts, context);
|
||||
|
||||
if (func === falseFunc) return next;
|
||||
if (func === trueFunc) return falseFunc;
|
||||
|
||||
return function not(elem) {
|
||||
return !func(elem) && next(elem);
|
||||
};
|
||||
},
|
||||
has<Node, ElementNode extends Node>(
|
||||
next: CompiledQuery<ElementNode>,
|
||||
subselect: Selector[][],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
_context: ElementNode[] | undefined,
|
||||
compileToken: CompileToken<Node, ElementNode>
|
||||
): CompiledQuery<ElementNode> {
|
||||
const { adapter } = options;
|
||||
const opts = {
|
||||
xmlMode: options.xmlMode,
|
||||
strict: options.strict,
|
||||
adapter,
|
||||
};
|
||||
|
||||
// @ts-expect-error Uses an array as a pointer to the current element (side effects)
|
||||
const context: ElementNode[] | undefined = subselect.some(
|
||||
containsTraversal
|
||||
)
|
||||
? [PLACEHOLDER_ELEMENT]
|
||||
: undefined;
|
||||
|
||||
const compiled = compileToken(subselect, opts, context);
|
||||
|
||||
if (compiled === falseFunc) return falseFunc;
|
||||
if (compiled === trueFunc) {
|
||||
return (elem) =>
|
||||
adapter.getChildren(elem).some(adapter.isTag) && next(elem);
|
||||
}
|
||||
|
||||
const hasElement = ensureIsTag(compiled, adapter);
|
||||
|
||||
if (context) {
|
||||
return (elem) =>
|
||||
next(elem) &&
|
||||
((context[0] = elem),
|
||||
adapter.existsOne(hasElement, adapter.getChildren(elem)));
|
||||
}
|
||||
|
||||
return (elem) =>
|
||||
next(elem) &&
|
||||
adapter.existsOne(hasElement, adapter.getChildren(elem));
|
||||
},
|
||||
};
|
@ -0,0 +1,145 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Does at least one of passed element nodes pass the test predicate?
|
||||
*/
|
||||
existsOne: (test: Predicate<ElementNode>, elems: Node[]) => boolean;
|
||||
|
||||
/**
|
||||
* 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) => ElementNode | 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[];
|
||||
|
||||
/*
|
||||
* 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[];
|
||||
|
||||
/**
|
||||
* Finds all of the element nodes in the array that match the test predicate,
|
||||
* as well as any of their children that match it.
|
||||
*/
|
||||
findAll: (test: Predicate<ElementNode>, nodes: Node[]) => ElementNode[];
|
||||
|
||||
/**
|
||||
* Finds the first node in the array that matches the test predicate, or one
|
||||
* of its children.
|
||||
*/
|
||||
findOne: (
|
||||
test: Predicate<ElementNode>,
|
||||
elems: Node[]
|
||||
) => ElementNode | null;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
import * as DomUtils from "domutils";
|
||||
import { Node, Element } from "domhandler";
|
||||
|
||||
// Ensure that DomUtils (the default adapter) matches the given specification
|
||||
DomUtils as Adapter<Node, Element>;
|
||||
|
||||
// TODO default types to the domutil/htmlparser2 types
|
||||
export interface Options<Node, ElementNode extends Node> {
|
||||
/**
|
||||
* When enabled, tag names will be case-sensitive. Default: false.
|
||||
*/
|
||||
xmlMode?: boolean;
|
||||
/**
|
||||
* Limits the module to only use CSS3 selectors. Default: false.
|
||||
*/
|
||||
strict?: boolean;
|
||||
/**
|
||||
* 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 domutils.
|
||||
*/
|
||||
adapter?: Adapter<Node, ElementNode>;
|
||||
/**
|
||||
* The context of the current query. Used to
|
||||
*/
|
||||
context?: ElementNode[];
|
||||
}
|
||||
|
||||
// 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>;
|
||||
}
|
||||
|
||||
export interface CompiledQuery<ElementNode> {
|
||||
(node: ElementNode): boolean;
|
||||
shouldTestNextSiblings?: boolean;
|
||||
}
|
||||
export type Query<ElementNode> = string | CompiledQuery<ElementNode>;
|
||||
export type CompileToken<Node, ElementNode extends Node> = (
|
||||
token: InternalSelector[][],
|
||||
options: InternalOptions<Node, ElementNode>,
|
||||
context?: ElementNode[]
|
||||
) => CompiledQuery<ElementNode>;
|
@ -1,5 +1,9 @@
|
||||
{
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"rules": {
|
||||
"capitalized-comments": 0,
|
||||
"multiline-comment-style": 0
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +0,0 @@
|
||||
--check-leaks
|
||||
--reporter spec
|
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,13 @@
|
||||
const ben = require("ben");
|
||||
const testString =
|
||||
'doo, *#foo > elem.bar[class$=bAz i]:not([ id *= "2" ]):nth-child(2n)';
|
||||
const helper = require("./helper.js");
|
||||
const { CSSselect } = helper;
|
||||
const { compile } = CSSselect;
|
||||
const helper = require("./helper");
|
||||
const CSSselect = require("../../src");
|
||||
const dom = helper.getDefaultDom();
|
||||
|
||||
//console.log("Parsing took:", ben(1e5, function(){compile(testString);}));
|
||||
const compiled = compile(testString);
|
||||
// console.log("Parsing took:", ben(1e5, function(){compile(testString);}));
|
||||
const compiled = CSSselect.compile(testString);
|
||||
console.log(
|
||||
"Executing took:",
|
||||
ben(1e6, () => {
|
||||
CSSselect(compiled, dom);
|
||||
}) * 1e3
|
||||
ben(1e6, () => CSSselect.selectAll(compiled, dom)) * 1e3
|
||||
);
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": []
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
"declaration": true /* Generates corresponding '.d.ts' file. */,
|
||||
"declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
"outDir": "lib" /* Redirect output structure to the directory. */,
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true /* Report errors on unused locals. */,
|
||||
"noUnusedParameters": true /* Report errors on unused parameters. */,
|
||||
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
|
||||
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
|
||||
|
||||
/* Module Resolution Options */
|
||||
"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
"paths": {
|
||||
"*": ["src/declarations/*", "*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/__fixtures__/*",
|
||||
"**/__tests__/*",
|
||||
"**/__snapshots__/*"
|
||||
]
|
||||
}
|
Loading…
Reference in new issue