127 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			127 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import { SelectorType, AttributeAction } from "./types";
 | 
						|
const attribValChars = ["\\", '"'];
 | 
						|
const pseudoValChars = [...attribValChars, "(", ")"];
 | 
						|
const charsToEscapeInAttributeValue = new Set(attribValChars.map((c) => c.charCodeAt(0)));
 | 
						|
const charsToEscapeInPseudoValue = new Set(pseudoValChars.map((c) => c.charCodeAt(0)));
 | 
						|
const charsToEscapeInName = new Set([
 | 
						|
    ...pseudoValChars,
 | 
						|
    "~",
 | 
						|
    "^",
 | 
						|
    "$",
 | 
						|
    "*",
 | 
						|
    "+",
 | 
						|
    "!",
 | 
						|
    "|",
 | 
						|
    ":",
 | 
						|
    "[",
 | 
						|
    "]",
 | 
						|
    " ",
 | 
						|
    ".",
 | 
						|
].map((c) => c.charCodeAt(0)));
 | 
						|
/**
 | 
						|
 * Turns `selector` back into a string.
 | 
						|
 *
 | 
						|
 * @param selector Selector to stringify.
 | 
						|
 */
 | 
						|
export function stringify(selector) {
 | 
						|
    return selector
 | 
						|
        .map((token) => token.map(stringifyToken).join(""))
 | 
						|
        .join(", ");
 | 
						|
}
 | 
						|
function stringifyToken(token, index, arr) {
 | 
						|
    switch (token.type) {
 | 
						|
        // Simple types
 | 
						|
        case SelectorType.Child:
 | 
						|
            return index === 0 ? "> " : " > ";
 | 
						|
        case SelectorType.Parent:
 | 
						|
            return index === 0 ? "< " : " < ";
 | 
						|
        case SelectorType.Sibling:
 | 
						|
            return index === 0 ? "~ " : " ~ ";
 | 
						|
        case SelectorType.Adjacent:
 | 
						|
            return index === 0 ? "+ " : " + ";
 | 
						|
        case SelectorType.Descendant:
 | 
						|
            return " ";
 | 
						|
        case SelectorType.ColumnCombinator:
 | 
						|
            return index === 0 ? "|| " : " || ";
 | 
						|
        case SelectorType.Universal:
 | 
						|
            // Return an empty string if the selector isn't needed.
 | 
						|
            return token.namespace === "*" &&
 | 
						|
                index + 1 < arr.length &&
 | 
						|
                "name" in arr[index + 1]
 | 
						|
                ? ""
 | 
						|
                : `${getNamespace(token.namespace)}*`;
 | 
						|
        case SelectorType.Tag:
 | 
						|
            return getNamespacedName(token);
 | 
						|
        case SelectorType.PseudoElement:
 | 
						|
            return `::${escapeName(token.name, charsToEscapeInName)}${token.data === null
 | 
						|
                ? ""
 | 
						|
                : `(${escapeName(token.data, charsToEscapeInPseudoValue)})`}`;
 | 
						|
        case SelectorType.Pseudo:
 | 
						|
            return `:${escapeName(token.name, charsToEscapeInName)}${token.data === null
 | 
						|
                ? ""
 | 
						|
                : `(${typeof token.data === "string"
 | 
						|
                    ? escapeName(token.data, charsToEscapeInPseudoValue)
 | 
						|
                    : stringify(token.data)})`}`;
 | 
						|
        case SelectorType.Attribute: {
 | 
						|
            if (token.name === "id" &&
 | 
						|
                token.action === AttributeAction.Equals &&
 | 
						|
                token.ignoreCase === "quirks" &&
 | 
						|
                !token.namespace) {
 | 
						|
                return `#${escapeName(token.value, charsToEscapeInName)}`;
 | 
						|
            }
 | 
						|
            if (token.name === "class" &&
 | 
						|
                token.action === AttributeAction.Element &&
 | 
						|
                token.ignoreCase === "quirks" &&
 | 
						|
                !token.namespace) {
 | 
						|
                return `.${escapeName(token.value, charsToEscapeInName)}`;
 | 
						|
            }
 | 
						|
            const name = getNamespacedName(token);
 | 
						|
            if (token.action === AttributeAction.Exists) {
 | 
						|
                return `[${name}]`;
 | 
						|
            }
 | 
						|
            return `[${name}${getActionValue(token.action)}="${escapeName(token.value, charsToEscapeInAttributeValue)}"${token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s"}]`;
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
function getActionValue(action) {
 | 
						|
    switch (action) {
 | 
						|
        case AttributeAction.Equals:
 | 
						|
            return "";
 | 
						|
        case AttributeAction.Element:
 | 
						|
            return "~";
 | 
						|
        case AttributeAction.Start:
 | 
						|
            return "^";
 | 
						|
        case AttributeAction.End:
 | 
						|
            return "$";
 | 
						|
        case AttributeAction.Any:
 | 
						|
            return "*";
 | 
						|
        case AttributeAction.Not:
 | 
						|
            return "!";
 | 
						|
        case AttributeAction.Hyphen:
 | 
						|
            return "|";
 | 
						|
        case AttributeAction.Exists:
 | 
						|
            throw new Error("Shouldn't be here");
 | 
						|
    }
 | 
						|
}
 | 
						|
function getNamespacedName(token) {
 | 
						|
    return `${getNamespace(token.namespace)}${escapeName(token.name, charsToEscapeInName)}`;
 | 
						|
}
 | 
						|
function getNamespace(namespace) {
 | 
						|
    return namespace !== null
 | 
						|
        ? `${namespace === "*"
 | 
						|
            ? "*"
 | 
						|
            : escapeName(namespace, charsToEscapeInName)}|`
 | 
						|
        : "";
 | 
						|
}
 | 
						|
function escapeName(str, charsToEscape) {
 | 
						|
    let lastIdx = 0;
 | 
						|
    let ret = "";
 | 
						|
    for (let i = 0; i < str.length; i++) {
 | 
						|
        if (charsToEscape.has(str.charCodeAt(i))) {
 | 
						|
            ret += `${str.slice(lastIdx, i)}\\${str.charAt(i)}`;
 | 
						|
            lastIdx = i + 1;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return ret.length > 0 ? ret + str.slice(lastIdx) : str;
 | 
						|
}
 |