123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- import { SelectorType, AttributeAction, } from "./types";
- const reName = /^[^\\#]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\-\u00b0-\uFFFF])+/;
- const reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi;
- const actionTypes = new Map([
- [126 /* Tilde */, AttributeAction.Element],
- [94 /* Circumflex */, AttributeAction.Start],
- [36 /* Dollar */, AttributeAction.End],
- [42 /* Asterisk */, AttributeAction.Any],
- [33 /* ExclamationMark */, AttributeAction.Not],
- [124 /* Pipe */, AttributeAction.Hyphen],
- ]);
- // Pseudos, whose data property is parsed as well.
- const unpackPseudos = new Set([
- "has",
- "not",
- "matches",
- "is",
- "where",
- "host",
- "host-context",
- ]);
- /**
- * Checks whether a specific selector is a traversal.
- * This is useful eg. in swapping the order of elements that
- * are not traversals.
- *
- * @param selector Selector to check.
- */
- export function isTraversal(selector) {
- switch (selector.type) {
- case SelectorType.Adjacent:
- case SelectorType.Child:
- case SelectorType.Descendant:
- case SelectorType.Parent:
- case SelectorType.Sibling:
- case SelectorType.ColumnCombinator:
- return true;
- default:
- return false;
- }
- }
- const stripQuotesFromPseudos = new Set(["contains", "icontains"]);
- // Unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L152
- function funescape(_, escaped, escapedWhitespace) {
- const high = parseInt(escaped, 16) - 0x10000;
- // NaN means non-codepoint
- return high !== high || escapedWhitespace
- ? escaped
- : high < 0
- ? // BMP codepoint
- String.fromCharCode(high + 0x10000)
- : // Supplemental Plane codepoint (surrogate pair)
- String.fromCharCode((high >> 10) | 0xd800, (high & 0x3ff) | 0xdc00);
- }
- function unescapeCSS(str) {
- return str.replace(reEscape, funescape);
- }
- function isQuote(c) {
- return c === 39 /* SingleQuote */ || c === 34 /* DoubleQuote */;
- }
- function isWhitespace(c) {
- return (c === 32 /* Space */ ||
- c === 9 /* Tab */ ||
- c === 10 /* NewLine */ ||
- c === 12 /* FormFeed */ ||
- c === 13 /* CarriageReturn */);
- }
- /**
- * Parses `selector`, optionally with the passed `options`.
- *
- * @param selector Selector to parse.
- * @param options Options for parsing.
- * @returns Returns a two-dimensional array.
- * The first dimension represents selectors separated by commas (eg. `sub1, sub2`),
- * the second contains the relevant tokens for that selector.
- */
- export function parse(selector) {
- const subselects = [];
- const endIndex = parseSelector(subselects, `${selector}`, 0);
- if (endIndex < selector.length) {
- throw new Error(`Unmatched selector: ${selector.slice(endIndex)}`);
- }
- return subselects;
- }
- function parseSelector(subselects, selector, selectorIndex) {
- let tokens = [];
- function getName(offset) {
- const match = selector.slice(selectorIndex + offset).match(reName);
- if (!match) {
- throw new Error(`Expected name, found ${selector.slice(selectorIndex)}`);
- }
- const [name] = match;
- selectorIndex += offset + name.length;
- return unescapeCSS(name);
- }
- function stripWhitespace(offset) {
- selectorIndex += offset;
- while (selectorIndex < selector.length &&
- isWhitespace(selector.charCodeAt(selectorIndex))) {
- selectorIndex++;
- }
- }
- function readValueWithParenthesis() {
- selectorIndex += 1;
- const start = selectorIndex;
- let counter = 1;
- for (; counter > 0 && selectorIndex < selector.length; selectorIndex++) {
- if (selector.charCodeAt(selectorIndex) ===
- 40 /* LeftParenthesis */ &&
- !isEscaped(selectorIndex)) {
- counter++;
- }
- else if (selector.charCodeAt(selectorIndex) ===
- 41 /* RightParenthesis */ &&
- !isEscaped(selectorIndex)) {
- counter--;
- }
- }
- if (counter) {
- throw new Error("Parenthesis not matched");
- }
- return unescapeCSS(selector.slice(start, selectorIndex - 1));
- }
- function isEscaped(pos) {
- let slashCount = 0;
- while (selector.charCodeAt(--pos) === 92 /* BackSlash */)
- slashCount++;
- return (slashCount & 1) === 1;
- }
- function ensureNotTraversal() {
- if (tokens.length > 0 && isTraversal(tokens[tokens.length - 1])) {
- throw new Error("Did not expect successive traversals.");
- }
- }
- function addTraversal(type) {
- if (tokens.length > 0 &&
- tokens[tokens.length - 1].type === SelectorType.Descendant) {
- tokens[tokens.length - 1].type = type;
- return;
- }
- ensureNotTraversal();
- tokens.push({ type });
- }
- function addSpecialAttribute(name, action) {
- tokens.push({
- type: SelectorType.Attribute,
- name,
- action,
- value: getName(1),
- namespace: null,
- ignoreCase: "quirks",
- });
- }
- /**
- * We have finished parsing the current part of the selector.
- *
- * Remove descendant tokens at the end if they exist,
- * and return the last index, so that parsing can be
- * picked up from here.
- */
- function finalizeSubselector() {
- if (tokens.length &&
- tokens[tokens.length - 1].type === SelectorType.Descendant) {
- tokens.pop();
- }
- if (tokens.length === 0) {
- throw new Error("Empty sub-selector");
- }
- subselects.push(tokens);
- }
- stripWhitespace(0);
- if (selector.length === selectorIndex) {
- return selectorIndex;
- }
- loop: while (selectorIndex < selector.length) {
- const firstChar = selector.charCodeAt(selectorIndex);
- switch (firstChar) {
- // Whitespace
- case 32 /* Space */:
- case 9 /* Tab */:
- case 10 /* NewLine */:
- case 12 /* FormFeed */:
- case 13 /* CarriageReturn */: {
- if (tokens.length === 0 ||
- tokens[0].type !== SelectorType.Descendant) {
- ensureNotTraversal();
- tokens.push({ type: SelectorType.Descendant });
- }
- stripWhitespace(1);
- break;
- }
- // Traversals
- case 62 /* GreaterThan */: {
- addTraversal(SelectorType.Child);
- stripWhitespace(1);
- break;
- }
- case 60 /* LessThan */: {
- addTraversal(SelectorType.Parent);
- stripWhitespace(1);
- break;
- }
- case 126 /* Tilde */: {
- addTraversal(SelectorType.Sibling);
- stripWhitespace(1);
- break;
- }
- case 43 /* Plus */: {
- addTraversal(SelectorType.Adjacent);
- stripWhitespace(1);
- break;
- }
- // Special attribute selectors: .class, #id
- case 46 /* Period */: {
- addSpecialAttribute("class", AttributeAction.Element);
- break;
- }
- case 35 /* Hash */: {
- addSpecialAttribute("id", AttributeAction.Equals);
- break;
- }
- case 91 /* LeftSquareBracket */: {
- stripWhitespace(1);
- // Determine attribute name and namespace
- let name;
- let namespace = null;
- if (selector.charCodeAt(selectorIndex) === 124 /* Pipe */) {
- // Equivalent to no namespace
- name = getName(1);
- }
- else if (selector.startsWith("*|", selectorIndex)) {
- namespace = "*";
- name = getName(2);
- }
- else {
- name = getName(0);
- if (selector.charCodeAt(selectorIndex) === 124 /* Pipe */ &&
- selector.charCodeAt(selectorIndex + 1) !==
- 61 /* Equal */) {
- namespace = name;
- name = getName(1);
- }
- }
- stripWhitespace(0);
- // Determine comparison operation
- let action = AttributeAction.Exists;
- const possibleAction = actionTypes.get(selector.charCodeAt(selectorIndex));
- if (possibleAction) {
- action = possibleAction;
- if (selector.charCodeAt(selectorIndex + 1) !==
- 61 /* Equal */) {
- throw new Error("Expected `=`");
- }
- stripWhitespace(2);
- }
- else if (selector.charCodeAt(selectorIndex) === 61 /* Equal */) {
- action = AttributeAction.Equals;
- stripWhitespace(1);
- }
- // Determine value
- let value = "";
- let ignoreCase = null;
- if (action !== "exists") {
- if (isQuote(selector.charCodeAt(selectorIndex))) {
- const quote = selector.charCodeAt(selectorIndex);
- let sectionEnd = selectorIndex + 1;
- while (sectionEnd < selector.length &&
- (selector.charCodeAt(sectionEnd) !== quote ||
- isEscaped(sectionEnd))) {
- sectionEnd += 1;
- }
- if (selector.charCodeAt(sectionEnd) !== quote) {
- throw new Error("Attribute value didn't end");
- }
- value = unescapeCSS(selector.slice(selectorIndex + 1, sectionEnd));
- selectorIndex = sectionEnd + 1;
- }
- else {
- const valueStart = selectorIndex;
- while (selectorIndex < selector.length &&
- ((!isWhitespace(selector.charCodeAt(selectorIndex)) &&
- selector.charCodeAt(selectorIndex) !==
- 93 /* RightSquareBracket */) ||
- isEscaped(selectorIndex))) {
- selectorIndex += 1;
- }
- value = unescapeCSS(selector.slice(valueStart, selectorIndex));
- }
- stripWhitespace(0);
- // See if we have a force ignore flag
- const forceIgnore = selector.charCodeAt(selectorIndex) | 0x20;
- // If the forceIgnore flag is set (either `i` or `s`), use that value
- if (forceIgnore === 115 /* LowerS */) {
- ignoreCase = false;
- stripWhitespace(1);
- }
- else if (forceIgnore === 105 /* LowerI */) {
- ignoreCase = true;
- stripWhitespace(1);
- }
- }
- if (selector.charCodeAt(selectorIndex) !==
- 93 /* RightSquareBracket */) {
- throw new Error("Attribute selector didn't terminate");
- }
- selectorIndex += 1;
- const attributeSelector = {
- type: SelectorType.Attribute,
- name,
- action,
- value,
- namespace,
- ignoreCase,
- };
- tokens.push(attributeSelector);
- break;
- }
- case 58 /* Colon */: {
- if (selector.charCodeAt(selectorIndex + 1) === 58 /* Colon */) {
- tokens.push({
- type: SelectorType.PseudoElement,
- name: getName(2).toLowerCase(),
- data: selector.charCodeAt(selectorIndex) ===
- 40 /* LeftParenthesis */
- ? readValueWithParenthesis()
- : null,
- });
- continue;
- }
- const name = getName(1).toLowerCase();
- let data = null;
- if (selector.charCodeAt(selectorIndex) ===
- 40 /* LeftParenthesis */) {
- if (unpackPseudos.has(name)) {
- if (isQuote(selector.charCodeAt(selectorIndex + 1))) {
- throw new Error(`Pseudo-selector ${name} cannot be quoted`);
- }
- data = [];
- selectorIndex = parseSelector(data, selector, selectorIndex + 1);
- if (selector.charCodeAt(selectorIndex) !==
- 41 /* RightParenthesis */) {
- throw new Error(`Missing closing parenthesis in :${name} (${selector})`);
- }
- selectorIndex += 1;
- }
- else {
- data = readValueWithParenthesis();
- if (stripQuotesFromPseudos.has(name)) {
- const quot = data.charCodeAt(0);
- if (quot === data.charCodeAt(data.length - 1) &&
- isQuote(quot)) {
- data = data.slice(1, -1);
- }
- }
- data = unescapeCSS(data);
- }
- }
- tokens.push({ type: SelectorType.Pseudo, name, data });
- break;
- }
- case 44 /* Comma */: {
- finalizeSubselector();
- tokens = [];
- stripWhitespace(1);
- break;
- }
- default: {
- if (selector.startsWith("/*", selectorIndex)) {
- const endIndex = selector.indexOf("*/", selectorIndex + 2);
- if (endIndex < 0) {
- throw new Error("Comment was not terminated");
- }
- selectorIndex = endIndex + 2;
- // Remove leading whitespace
- if (tokens.length === 0) {
- stripWhitespace(0);
- }
- break;
- }
- let namespace = null;
- let name;
- if (firstChar === 42 /* Asterisk */) {
- selectorIndex += 1;
- name = "*";
- }
- else if (firstChar === 124 /* Pipe */) {
- name = "";
- if (selector.charCodeAt(selectorIndex + 1) === 124 /* Pipe */) {
- addTraversal(SelectorType.ColumnCombinator);
- stripWhitespace(2);
- break;
- }
- }
- else if (reName.test(selector.slice(selectorIndex))) {
- name = getName(0);
- }
- else {
- break loop;
- }
- if (selector.charCodeAt(selectorIndex) === 124 /* Pipe */ &&
- selector.charCodeAt(selectorIndex + 1) !== 124 /* Pipe */) {
- namespace = name;
- if (selector.charCodeAt(selectorIndex + 1) ===
- 42 /* Asterisk */) {
- name = "*";
- selectorIndex += 2;
- }
- else {
- name = getName(1);
- }
- }
- tokens.push(name === "*"
- ? { type: SelectorType.Universal, namespace }
- : { type: SelectorType.Tag, name, namespace });
- }
- }
- }
- finalizeSubselector();
- return selectorIndex;
- }
|