Spaces:
Running
Running
/* | |
* 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}-->`; | |
} | |