• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import { Selector, SelectorType, AttributeAction } from "./types";
2
3const attribValChars = ["\\", '"'];
4const pseudoValChars = [...attribValChars, "(", ")"];
5
6const charsToEscapeInAttributeValue = new Set(
7    attribValChars.map((c) => c.charCodeAt(0))
8);
9const charsToEscapeInPseudoValue = new Set(
10    pseudoValChars.map((c) => c.charCodeAt(0))
11);
12const charsToEscapeInName = new Set(
13    [
14        ...pseudoValChars,
15        "~",
16        "^",
17        "$",
18        "*",
19        "+",
20        "!",
21        "|",
22        ":",
23        "[",
24        "]",
25        " ",
26        ".",
27    ].map((c) => c.charCodeAt(0))
28);
29
30/**
31 * Turns `selector` back into a string.
32 *
33 * @param selector Selector to stringify.
34 */
35export function stringify(selector: Selector[][]): string {
36    return selector
37        .map((token) => token.map(stringifyToken).join(""))
38        .join(", ");
39}
40
41function stringifyToken(
42    token: Selector,
43    index: number,
44    arr: Selector[]
45): string {
46    switch (token.type) {
47        // Simple types
48        case SelectorType.Child:
49            return index === 0 ? "> " : " > ";
50        case SelectorType.Parent:
51            return index === 0 ? "< " : " < ";
52        case SelectorType.Sibling:
53            return index === 0 ? "~ " : " ~ ";
54        case SelectorType.Adjacent:
55            return index === 0 ? "+ " : " + ";
56        case SelectorType.Descendant:
57            return " ";
58        case SelectorType.ColumnCombinator:
59            return index === 0 ? "|| " : " || ";
60        case SelectorType.Universal:
61            // Return an empty string if the selector isn't needed.
62            return token.namespace === "*" &&
63                index + 1 < arr.length &&
64                "name" in arr[index + 1]
65                ? ""
66                : `${getNamespace(token.namespace)}*`;
67
68        case SelectorType.Tag:
69            return getNamespacedName(token);
70
71        case SelectorType.PseudoElement:
72            return `::${escapeName(token.name, charsToEscapeInName)}${
73                token.data === null
74                    ? ""
75                    : `(${escapeName(token.data, charsToEscapeInPseudoValue)})`
76            }`;
77
78        case SelectorType.Pseudo:
79            return `:${escapeName(token.name, charsToEscapeInName)}${
80                token.data === null
81                    ? ""
82                    : `(${
83                          typeof token.data === "string"
84                              ? escapeName(
85                                    token.data,
86                                    charsToEscapeInPseudoValue
87                                )
88                              : stringify(token.data)
89                      })`
90            }`;
91
92        case SelectorType.Attribute: {
93            if (
94                token.name === "id" &&
95                token.action === AttributeAction.Equals &&
96                token.ignoreCase === "quirks" &&
97                !token.namespace
98            ) {
99                return `#${escapeName(token.value, charsToEscapeInName)}`;
100            }
101            if (
102                token.name === "class" &&
103                token.action === AttributeAction.Element &&
104                token.ignoreCase === "quirks" &&
105                !token.namespace
106            ) {
107                return `.${escapeName(token.value, charsToEscapeInName)}`;
108            }
109
110            const name = getNamespacedName(token);
111
112            if (token.action === AttributeAction.Exists) {
113                return `[${name}]`;
114            }
115
116            return `[${name}${getActionValue(token.action)}="${escapeName(
117                token.value,
118                charsToEscapeInAttributeValue
119            )}"${
120                token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s"
121            }]`;
122        }
123    }
124}
125
126function getActionValue(action: AttributeAction): string {
127    switch (action) {
128        case AttributeAction.Equals:
129            return "";
130        case AttributeAction.Element:
131            return "~";
132        case AttributeAction.Start:
133            return "^";
134        case AttributeAction.End:
135            return "$";
136        case AttributeAction.Any:
137            return "*";
138        case AttributeAction.Not:
139            return "!";
140        case AttributeAction.Hyphen:
141            return "|";
142        case AttributeAction.Exists:
143            throw new Error("Shouldn't be here");
144    }
145}
146
147function getNamespacedName(token: {
148    name: string;
149    namespace: string | null;
150}): string {
151    return `${getNamespace(token.namespace)}${escapeName(
152        token.name,
153        charsToEscapeInName
154    )}`;
155}
156
157function getNamespace(namespace: string | null): string {
158    return namespace !== null
159        ? `${
160              namespace === "*"
161                  ? "*"
162                  : escapeName(namespace, charsToEscapeInName)
163          }|`
164        : "";
165}
166
167function escapeName(str: string, charsToEscape: Set<number>): string {
168    let lastIdx = 0;
169    let ret = "";
170
171    for (let i = 0; i < str.length; i++) {
172        if (charsToEscape.has(str.charCodeAt(i))) {
173            ret += `${str.slice(lastIdx, i)}\\${str.charAt(i)}`;
174            lastIdx = i + 1;
175        }
176    }
177
178    return ret.length > 0 ? ret + str.slice(lastIdx) : str;
179}
180