| /* |
| * 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 += `</${elem.name}>`; |
| } |
| } |
| 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 `<![CDATA[${elem.children[0].data}]]>`; |
| } |
| function renderComment(elem) { |
| return `<!--${elem.data}-->`; |
| } |