191 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			191 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*
 | |
|  * 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}-->`;
 | |
| }
 | 
