/* * Module dependencies */ import * as ElementType from "domelementtype"; import { encodeXML, escapeAttribute, escapeText } from "entities"; /** * Mixed-case SVG and MathML tags & attributes * recognized by the HTML parser. * * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign */ import { elementNames, attributeNames } from "./foreignNames.js"; const unencodedElements = new Set([ "style", "script", "xmp", "iframe", "noembed", "noframes", "plaintext", "noscript", ]); function replaceQuotes(value) { return value.replace(/"/g, """); } /** * Format attributes */ function formatAttributes(attributes, opts) { var _a; if (!attributes) return; const encode = ((_a = opts.encodeEntities) !== null && _a !== void 0 ? _a : opts.decodeEntities) === false ? replaceQuotes : opts.xmlMode || opts.encodeEntities !== "utf8" ? encodeXML : escapeAttribute; return Object.keys(attributes) .map((key) => { var _a, _b; const value = (_a = attributes[key]) !== null && _a !== void 0 ? _a : ""; if (opts.xmlMode === "foreign") { /* Fix up mixed-case attribute names */ key = (_b = attributeNames.get(key)) !== null && _b !== void 0 ? _b : key; } if (!opts.emptyAttrs && !opts.xmlMode && value === "") { return key; } return `${key}="${encode(value)}"`; }) .join(" "); } /** * Self-enclosing tags */ const singleTag = new Set([ "area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source", "track", "wbr", ]); /** * Renders a DOM node or an array of DOM nodes to a string. * * Can be thought of as the equivalent of the `outerHTML` of the passed node(s). * * @param node Node to be rendered. * @param options Changes serialization behavior */ export function render(node, options = {}) { const nodes = "length" in node ? node : [node]; let output = ""; for (let i = 0; i < nodes.length; i++) { output += renderNode(nodes[i], options); } return output; } export default render; function renderNode(node, options) { switch (node.type) { case ElementType.Root: return render(node.children, options); // @ts-expect-error We don't use `Doctype` yet case ElementType.Doctype: case ElementType.Directive: return renderDirective(node); case ElementType.Comment: return renderComment(node); case ElementType.CDATA: return renderCdata(node); case ElementType.Script: case ElementType.Style: case ElementType.Tag: return renderTag(node, options); case ElementType.Text: return renderText(node, options); } } const foreignModeIntegrationPoints = new Set([ "mi", "mo", "mn", "ms", "mtext", "annotation-xml", "foreignObject", "desc", "title", ]); const foreignElements = new Set(["svg", "math"]); function renderTag(elem, opts) { var _a; // Handle SVG / MathML in HTML if (opts.xmlMode === "foreign") { /* Fix up mixed-case element names */ elem.name = (_a = elementNames.get(elem.name)) !== null && _a !== void 0 ? _a : elem.name; /* Exit foreign mode at integration points */ if (elem.parent && foreignModeIntegrationPoints.has(elem.parent.name)) { opts = { ...opts, xmlMode: false }; } } if (!opts.xmlMode && foreignElements.has(elem.name)) { opts = { ...opts, xmlMode: "foreign" }; } let tag = `<${elem.name}`; const attribs = formatAttributes(elem.attribs, opts); if (attribs) { tag += ` ${attribs}`; } if (elem.children.length === 0 && (opts.xmlMode ? // In XML mode or foreign mode, and user hasn't explicitly turned off self-closing tags opts.selfClosingTags !== false : // User explicitly asked for self-closing tags, even in HTML mode opts.selfClosingTags && singleTag.has(elem.name))) { if (!opts.xmlMode) tag += " "; tag += "/>"; } else { tag += ">"; if (elem.children.length > 0) { tag += render(elem.children, opts); } if (opts.xmlMode || !singleTag.has(elem.name)) { tag += ``; } } return tag; } function renderDirective(elem) { return `<${elem.data}>`; } function renderText(elem, opts) { var _a; let data = elem.data || ""; // If entities weren't decoded, no need to encode them back if (((_a = opts.encodeEntities) !== null && _a !== void 0 ? _a : opts.decodeEntities) !== false && !(!opts.xmlMode && elem.parent && unencodedElements.has(elem.parent.name))) { data = opts.xmlMode || opts.encodeEntities !== "utf8" ? encodeXML(data) : escapeText(data); } return data; } function renderCdata(elem) { return ``; } function renderComment(elem) { return ``; }