blob: d79729add39492815bfafb1f2958947b57dd5b9e [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001/*
2 Module dependencies
3*/
4var ElementType = require('domelementtype');
5var entities = require('entities');
6
7/* mixed-case SVG and MathML tags & attributes
8 recognized by the HTML parser, see
9 https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign
10*/
11var foreignNames = require('./foreignNames.json');
12foreignNames.elementNames.__proto__ = null; /* use as a simple dictionary */
13foreignNames.attributeNames.__proto__ = null;
14
15var unencodedElements = {
16 __proto__: null,
17 style: true,
18 script: true,
19 xmp: true,
20 iframe: true,
21 noembed: true,
22 noframes: true,
23 plaintext: true,
24 noscript: true
25};
26
27/*
28 Format attributes
29*/
30function formatAttrs(attributes, opts) {
31 if (!attributes) return;
32
33 var output = '';
34 var value;
35
36 // Loop through the attributes
37 for (var key in attributes) {
38 value = attributes[key];
39 if (output) {
40 output += ' ';
41 }
42
43 if (opts.xmlMode === 'foreign') {
44 /* fix up mixed-case attribute names */
45 key = foreignNames.attributeNames[key] || key;
46 }
47 output += key;
48 if ((value !== null && value !== '') || opts.xmlMode) {
49 output +=
50 '="' +
51 (opts.decodeEntities
52 ? entities.encodeXML(value)
53 : value.replace(/\"/g, '"')) +
54 '"';
55 }
56 }
57
58 return output;
59}
60
61/*
62 Self-enclosing tags (stolen from node-htmlparser)
63*/
64var singleTag = {
65 __proto__: null,
66 area: true,
67 base: true,
68 basefont: true,
69 br: true,
70 col: true,
71 command: true,
72 embed: true,
73 frame: true,
74 hr: true,
75 img: true,
76 input: true,
77 isindex: true,
78 keygen: true,
79 link: true,
80 meta: true,
81 param: true,
82 source: true,
83 track: true,
84 wbr: true
85};
86
87var render = (module.exports = function(dom, opts) {
88 if (!Array.isArray(dom) && !dom.cheerio) dom = [dom];
89 opts = opts || {};
90
91 var output = '';
92
93 for (var i = 0; i < dom.length; i++) {
94 var elem = dom[i];
95
96 if (elem.type === 'root') output += render(elem.children, opts);
97 else if (ElementType.isTag(elem)) output += renderTag(elem, opts);
98 else if (elem.type === ElementType.Directive)
99 output += renderDirective(elem);
100 else if (elem.type === ElementType.Comment) output += renderComment(elem);
101 else if (elem.type === ElementType.CDATA) output += renderCdata(elem);
102 else output += renderText(elem, opts);
103 }
104
105 return output;
106});
107
108var foreignModeIntegrationPoints = [
109 'mi',
110 'mo',
111 'mn',
112 'ms',
113 'mtext',
114 'annotation-xml',
115 'foreignObject',
116 'desc',
117 'title'
118];
119
120function renderTag(elem, opts) {
121 // Handle SVG / MathML in HTML
122 if (opts.xmlMode === 'foreign') {
123 /* fix up mixed-case element names */
124 elem.name = foreignNames.elementNames[elem.name] || elem.name;
125 /* exit foreign mode at integration points */
126 if (
127 elem.parent &&
128 foreignModeIntegrationPoints.indexOf(elem.parent.name) >= 0
129 )
130 opts = Object.assign({}, opts, { xmlMode: false });
131 }
132 if (!opts.xmlMode && ['svg', 'math'].indexOf(elem.name) >= 0) {
133 opts = Object.assign({}, opts, { xmlMode: 'foreign' });
134 }
135
136 var tag = '<' + elem.name;
137 var attribs = formatAttrs(elem.attribs, opts);
138
139 if (attribs) {
140 tag += ' ' + attribs;
141 }
142
143 if (opts.xmlMode && (!elem.children || elem.children.length === 0)) {
144 tag += '/>';
145 } else {
146 tag += '>';
147 if (elem.children) {
148 tag += render(elem.children, opts);
149 }
150
151 if (!singleTag[elem.name] || opts.xmlMode) {
152 tag += '</' + elem.name + '>';
153 }
154 }
155
156 return tag;
157}
158
159function renderDirective(elem) {
160 return '<' + elem.data + '>';
161}
162
163function renderText(elem, opts) {
164 var data = elem.data || '';
165
166 // if entities weren't decoded, no need to encode them back
167 if (
168 opts.decodeEntities &&
169 !(elem.parent && elem.parent.name in unencodedElements)
170 ) {
171 data = entities.encodeXML(data);
172 }
173
174 return data;
175}
176
177function renderCdata(elem) {
178 return '<![CDATA[' + elem.children[0].data + ']]>';
179}
180
181function renderComment(elem) {
182 return '<!--' + elem.data + '-->';
183}