/*
|
|
Module dependencies
|
|
*/
|
|
var ElementType = require('domelementtype');
|
|
var entities = require('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
|
|
*/
|
|
var foreignNames = require('./foreignNames.json');
|
|
foreignNames.elementNames.__proto__ = null; /* use as a simple dictionary */
|
|
foreignNames.attributeNames.__proto__ = null;
|
|
|
|
var unencodedElements = {
|
|
__proto__: null,
|
|
style: true,
|
|
script: true,
|
|
xmp: true,
|
|
iframe: true,
|
|
noembed: true,
|
|
noframes: true,
|
|
plaintext: true,
|
|
noscript: true
|
|
};
|
|
|
|
/*
|
|
Format attributes
|
|
*/
|
|
function formatAttrs(attributes, opts) {
|
|
if (!attributes) return;
|
|
|
|
var output = '';
|
|
var value;
|
|
|
|
// Loop through the attributes
|
|
for (var key in attributes) {
|
|
value = attributes[key];
|
|
if (output) {
|
|
output += ' ';
|
|
}
|
|
|
|
if (opts.xmlMode === 'foreign') {
|
|
/* fix up mixed-case attribute names */
|
|
key = foreignNames.attributeNames[key] || key;
|
|
}
|
|
output += key;
|
|
if ((value !== null && value !== '') || opts.xmlMode) {
|
|
output +=
|
|
'="' +
|
|
(opts.decodeEntities
|
|
? entities.encodeXML(value)
|
|
: value.replace(/\"/g, '"')) +
|
|
'"';
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
Self-enclosing tags (stolen from node-htmlparser)
|
|
*/
|
|
var singleTag = {
|
|
__proto__: null,
|
|
area: true,
|
|
base: true,
|
|
basefont: true,
|
|
br: true,
|
|
col: true,
|
|
command: true,
|
|
embed: true,
|
|
frame: true,
|
|
hr: true,
|
|
img: true,
|
|
input: true,
|
|
isindex: true,
|
|
keygen: true,
|
|
link: true,
|
|
meta: true,
|
|
param: true,
|
|
source: true,
|
|
track: true,
|
|
wbr: true
|
|
};
|
|
|
|
var render = (module.exports = function(dom, opts) {
|
|
if (!Array.isArray(dom) && !dom.cheerio) dom = [dom];
|
|
opts = opts || {};
|
|
|
|
var output = '';
|
|
|
|
for (var i = 0; i < dom.length; i++) {
|
|
var elem = dom[i];
|
|
|
|
if (elem.type === 'root') output += render(elem.children, opts);
|
|
else if (ElementType.isTag(elem)) output += renderTag(elem, opts);
|
|
else if (elem.type === ElementType.Directive)
|
|
output += renderDirective(elem);
|
|
else if (elem.type === ElementType.Comment) output += renderComment(elem);
|
|
else if (elem.type === ElementType.CDATA) output += renderCdata(elem);
|
|
else output += renderText(elem, opts);
|
|
}
|
|
|
|
return output;
|
|
});
|
|
|
|
var foreignModeIntegrationPoints = [
|
|
'mi',
|
|
'mo',
|
|
'mn',
|
|
'ms',
|
|
'mtext',
|
|
'annotation-xml',
|
|
'foreignObject',
|
|
'desc',
|
|
'title'
|
|
];
|
|
|
|
function renderTag(elem, opts) {
|
|
// Handle SVG / MathML in HTML
|
|
if (opts.xmlMode === 'foreign') {
|
|
/* fix up mixed-case element names */
|
|
elem.name = foreignNames.elementNames[elem.name] || elem.name;
|
|
/* exit foreign mode at integration points */
|
|
if (
|
|
elem.parent &&
|
|
foreignModeIntegrationPoints.indexOf(elem.parent.name) >= 0
|
|
)
|
|
opts = Object.assign({}, opts, { xmlMode: false });
|
|
}
|
|
if (!opts.xmlMode && ['svg', 'math'].indexOf(elem.name) >= 0) {
|
|
opts = Object.assign({}, opts, { xmlMode: 'foreign' });
|
|
}
|
|
|
|
var tag = '<' + elem.name;
|
|
var attribs = formatAttrs(elem.attribs, opts);
|
|
|
|
if (attribs) {
|
|
tag += ' ' + attribs;
|
|
}
|
|
|
|
if (opts.xmlMode && (!elem.children || elem.children.length === 0)) {
|
|
tag += '/>';
|
|
} else {
|
|
tag += '>';
|
|
if (elem.children) {
|
|
tag += render(elem.children, opts);
|
|
}
|
|
|
|
if (!singleTag[elem.name] || opts.xmlMode) {
|
|
tag += '</' + elem.name + '>';
|
|
}
|
|
}
|
|
|
|
return tag;
|
|
}
|
|
|
|
function renderDirective(elem) {
|
|
return '<' + elem.data + '>';
|
|
}
|
|
|
|
function renderText(elem, opts) {
|
|
var data = elem.data || '';
|
|
|
|
// if entities weren't decoded, no need to encode them back
|
|
if (
|
|
opts.decodeEntities &&
|
|
!(elem.parent && elem.parent.name in unencodedElements)
|
|
) {
|
|
data = entities.encodeXML(data);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
function renderCdata(elem) {
|
|
return '<![CDATA[' + elem.children[0].data + ']]>';
|
|
}
|
|
|
|
function renderComment(elem) {
|
|
return '<!--' + elem.data + '-->';
|
|
}
|