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 | }
|
---|