[79a0317] | 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 | }
|
---|