index.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /*
  2. * Module dependencies
  3. */
  4. import * as ElementType from "domelementtype";
  5. import { encodeXML, escapeAttribute, escapeText } from "entities";
  6. /**
  7. * Mixed-case SVG and MathML tags & attributes
  8. * recognized by the HTML parser.
  9. *
  10. * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign
  11. */
  12. import { elementNames, attributeNames } from "./foreignNames.js";
  13. const unencodedElements = new Set([
  14. "style",
  15. "script",
  16. "xmp",
  17. "iframe",
  18. "noembed",
  19. "noframes",
  20. "plaintext",
  21. "noscript",
  22. ]);
  23. function replaceQuotes(value) {
  24. return value.replace(/"/g, """);
  25. }
  26. /**
  27. * Format attributes
  28. */
  29. function formatAttributes(attributes, opts) {
  30. var _a;
  31. if (!attributes)
  32. return;
  33. const encode = ((_a = opts.encodeEntities) !== null && _a !== void 0 ? _a : opts.decodeEntities) === false
  34. ? replaceQuotes
  35. : opts.xmlMode || opts.encodeEntities !== "utf8"
  36. ? encodeXML
  37. : escapeAttribute;
  38. return Object.keys(attributes)
  39. .map((key) => {
  40. var _a, _b;
  41. const value = (_a = attributes[key]) !== null && _a !== void 0 ? _a : "";
  42. if (opts.xmlMode === "foreign") {
  43. /* Fix up mixed-case attribute names */
  44. key = (_b = attributeNames.get(key)) !== null && _b !== void 0 ? _b : key;
  45. }
  46. if (!opts.emptyAttrs && !opts.xmlMode && value === "") {
  47. return key;
  48. }
  49. return `${key}="${encode(value)}"`;
  50. })
  51. .join(" ");
  52. }
  53. /**
  54. * Self-enclosing tags
  55. */
  56. const singleTag = new Set([
  57. "area",
  58. "base",
  59. "basefont",
  60. "br",
  61. "col",
  62. "command",
  63. "embed",
  64. "frame",
  65. "hr",
  66. "img",
  67. "input",
  68. "isindex",
  69. "keygen",
  70. "link",
  71. "meta",
  72. "param",
  73. "source",
  74. "track",
  75. "wbr",
  76. ]);
  77. /**
  78. * Renders a DOM node or an array of DOM nodes to a string.
  79. *
  80. * Can be thought of as the equivalent of the `outerHTML` of the passed node(s).
  81. *
  82. * @param node Node to be rendered.
  83. * @param options Changes serialization behavior
  84. */
  85. export function render(node, options = {}) {
  86. const nodes = "length" in node ? node : [node];
  87. let output = "";
  88. for (let i = 0; i < nodes.length; i++) {
  89. output += renderNode(nodes[i], options);
  90. }
  91. return output;
  92. }
  93. export default render;
  94. function renderNode(node, options) {
  95. switch (node.type) {
  96. case ElementType.Root:
  97. return render(node.children, options);
  98. // @ts-expect-error We don't use `Doctype` yet
  99. case ElementType.Doctype:
  100. case ElementType.Directive:
  101. return renderDirective(node);
  102. case ElementType.Comment:
  103. return renderComment(node);
  104. case ElementType.CDATA:
  105. return renderCdata(node);
  106. case ElementType.Script:
  107. case ElementType.Style:
  108. case ElementType.Tag:
  109. return renderTag(node, options);
  110. case ElementType.Text:
  111. return renderText(node, options);
  112. }
  113. }
  114. const foreignModeIntegrationPoints = new Set([
  115. "mi",
  116. "mo",
  117. "mn",
  118. "ms",
  119. "mtext",
  120. "annotation-xml",
  121. "foreignObject",
  122. "desc",
  123. "title",
  124. ]);
  125. const foreignElements = new Set(["svg", "math"]);
  126. function renderTag(elem, opts) {
  127. var _a;
  128. // Handle SVG / MathML in HTML
  129. if (opts.xmlMode === "foreign") {
  130. /* Fix up mixed-case element names */
  131. elem.name = (_a = elementNames.get(elem.name)) !== null && _a !== void 0 ? _a : elem.name;
  132. /* Exit foreign mode at integration points */
  133. if (elem.parent &&
  134. foreignModeIntegrationPoints.has(elem.parent.name)) {
  135. opts = { ...opts, xmlMode: false };
  136. }
  137. }
  138. if (!opts.xmlMode && foreignElements.has(elem.name)) {
  139. opts = { ...opts, xmlMode: "foreign" };
  140. }
  141. let tag = `<${elem.name}`;
  142. const attribs = formatAttributes(elem.attribs, opts);
  143. if (attribs) {
  144. tag += ` ${attribs}`;
  145. }
  146. if (elem.children.length === 0 &&
  147. (opts.xmlMode
  148. ? // In XML mode or foreign mode, and user hasn't explicitly turned off self-closing tags
  149. opts.selfClosingTags !== false
  150. : // User explicitly asked for self-closing tags, even in HTML mode
  151. opts.selfClosingTags && singleTag.has(elem.name))) {
  152. if (!opts.xmlMode)
  153. tag += " ";
  154. tag += "/>";
  155. }
  156. else {
  157. tag += ">";
  158. if (elem.children.length > 0) {
  159. tag += render(elem.children, opts);
  160. }
  161. if (opts.xmlMode || !singleTag.has(elem.name)) {
  162. tag += `</${elem.name}>`;
  163. }
  164. }
  165. return tag;
  166. }
  167. function renderDirective(elem) {
  168. return `<${elem.data}>`;
  169. }
  170. function renderText(elem, opts) {
  171. var _a;
  172. let data = elem.data || "";
  173. // If entities weren't decoded, no need to encode them back
  174. if (((_a = opts.encodeEntities) !== null && _a !== void 0 ? _a : opts.decodeEntities) !== false &&
  175. !(!opts.xmlMode &&
  176. elem.parent &&
  177. unencodedElements.has(elem.parent.name))) {
  178. data =
  179. opts.xmlMode || opts.encodeEntities !== "utf8"
  180. ? encodeXML(data)
  181. : escapeText(data);
  182. }
  183. return data;
  184. }
  185. function renderCdata(elem) {
  186. return `<![CDATA[${elem.children[0].data}]]>`;
  187. }
  188. function renderComment(elem) {
  189. return `<!--${elem.data}-->`;
  190. }