stringify.js 4.4 KB

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