css-select
Version:
a CSS selector compiler/engine
276 lines (248 loc) • 7.97 kB
text/typescript
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;
const whitespaceRe = /\s/;
function escapeRegex(value: string): string {
return value.replace(reChars, "\\$&");
}
/**
* Attributes that are case-insensitive in HTML.
* @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 (element) => {
const attribute = adapter.getAttributeValue(element, name);
return (
attribute != null &&
attribute.length === value.length &&
attribute.toLowerCase() === value &&
next(element)
);
};
}
return (element) =>
adapter.getAttributeValue(element, name) === value && next(element);
},
hyphen(next, data, options) {
const { adapter } = options;
const { name } = data;
let { value } = data;
const { length } = value;
if (shouldIgnoreCase(data, options)) {
value = value.toLowerCase();
return function hyphenIC(element) {
const attribute = adapter.getAttributeValue(element, name);
return (
attribute != null &&
(attribute.length === length ||
attribute.charAt(length) === "-") &&
attribute.substr(0, length).toLowerCase() === value &&
next(element)
);
};
}
return function hyphen(element) {
const attribute = adapter.getAttributeValue(element, name);
return (
attribute != null &&
(attribute.length === length ||
attribute.charAt(length) === "-") &&
attribute.substr(0, length) === value &&
next(element)
);
};
},
element(next, data, options) {
const { adapter } = options;
const { name, value } = data;
if (whitespaceRe.test(value)) {
return boolbase.falseFunc;
}
const regex = new RegExp(
`(?:^|\\s)${escapeRegex(value)}(?:$|\\s)`,
shouldIgnoreCase(data, options) ? "i" : "",
);
return function element(node) {
const attribute = adapter.getAttributeValue(node, name);
return (
attribute != null &&
attribute.length >= value.length &&
regex.test(attribute) &&
next(node)
);
};
},
exists(next, { name }, { adapter }) {
return (element) => adapter.hasAttrib(element, name) && next(element);
},
start(next, data, options) {
const { adapter } = options;
const { name } = data;
let { value } = data;
const { length } = value;
if (length === 0) {
return boolbase.falseFunc;
}
if (shouldIgnoreCase(data, options)) {
value = value.toLowerCase();
return (element) => {
const attribute = adapter.getAttributeValue(element, name);
return (
attribute != null &&
attribute.length >= length &&
attribute.substr(0, length).toLowerCase() === value &&
next(element)
);
};
}
return (element) =>
!!adapter.getAttributeValue(element, name)?.startsWith(value) &&
next(element);
},
end(next, data, options) {
const { adapter } = options;
const { name } = data;
let { value } = data;
const length = -value.length;
if (length === 0) {
return boolbase.falseFunc;
}
if (shouldIgnoreCase(data, options)) {
value = value.toLowerCase();
return (element) =>
adapter
.getAttributeValue(element, name)
?.substr(length)
.toLowerCase() === value && next(element);
}
return (element) =>
!!adapter.getAttributeValue(element, name)?.endsWith(value) &&
next(element);
},
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(element) {
const attribute = adapter.getAttributeValue(element, name);
return (
attribute != null &&
attribute.length >= value.length &&
regex.test(attribute) &&
next(element)
);
};
}
return (element) =>
!!adapter.getAttributeValue(element, name)?.includes(value) &&
next(element);
},
not(next, data, options) {
const { adapter } = options;
const { name } = data;
let { value } = data;
if (value === "") {
return (element) =>
!!adapter.getAttributeValue(element, name) && next(element);
}
if (shouldIgnoreCase(data, options)) {
value = value.toLowerCase();
return (element) => {
const attribute = adapter.getAttributeValue(element, name);
return (
(attribute == null ||
attribute.length !== value.length ||
attribute.toLowerCase() !== value) &&
next(element)
);
};
}
return (element) =>
adapter.getAttributeValue(element, name) !== value && next(element);
},
};