You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			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;
 | |
| }
 |