const SINGLE_TAGS = [ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr' ]; const ATTRIBUTE_QUOTES_REQUIRED = /[\t\n\f\r "'`=<>]/; /** Render PostHTML Tree to HTML * * @param {Array|Object} tree PostHTML Tree @param {Object} options Options * * @return {String} HTML */ function render(tree, options) { /** Options * * @type {Object} * * @prop {Array} singleTags Custom single tags (selfClosing) * @prop {String} closingSingleTag Closing format for single tag @prop * @prop {Boolean} quoteAllAttributes If all attributes should be quoted. * Otherwise attributes will be unquoted when allowed. * @prop {Boolean} replaceQuote Replaces quotes in attribute values with `"e;` * * Formats: * * ``` tag: `

` ```, slash: `
` ```, ```default: `
` ``` */ options = options || {}; const singleTags = options.singleTags ? SINGLE_TAGS.concat(options.singleTags) : SINGLE_TAGS; const singleRegExp = singleTags.filter(tag => { return tag instanceof RegExp; }); const {closingSingleTag} = options; let {quoteAllAttributes} = options; if (quoteAllAttributes === undefined) { quoteAllAttributes = true; } let {replaceQuote} = options; if (replaceQuote === undefined) { replaceQuote = true; } let {quoteStyle} = options; if (quoteStyle === undefined) { quoteStyle = 2; } return html(tree); /** @private */ function isSingleTag(tag) { if (singleRegExp.length > 0) { return singleRegExp.some(reg => reg.test(tag)); } if (!singleTags.includes(tag.toLowerCase())) { return false; } return true; } /** @private */ function attrs(object) { let attr = ''; for (const key in object) { if (typeof object[key] === 'string') { if (quoteAllAttributes || object[key].match(ATTRIBUTE_QUOTES_REQUIRED)) { let attrValue = object[key]; if (replaceQuote) { attrValue = object[key].replace(/"/g, '"'); } attr += makeAttr(key, attrValue, quoteStyle); } else if (object[key] === '') { attr += ' ' + key; } else { attr += ' ' + key + '=' + object[key]; } } else if (object[key] === true) { attr += ' ' + key; } else if (typeof object[key] === 'number') { attr += makeAttr(key, object[key], quoteStyle); } } return attr; } /** @private */ function traverse(tree, cb) { if (tree !== undefined) { for (let i = 0, {length} = tree; i < length; i++) { traverse(cb(tree[i]), cb); } } } /** @private */ function makeAttr(key, attrValue, quoteStyle = 1) { if (quoteStyle === 1) { // Single Quote return ` ${key}='${attrValue}'`; } if (quoteStyle === 2) { // Double Quote return ` ${key}="${attrValue}"`; } // Smart Quote if (attrValue.includes('"')) { return ` ${key}='${attrValue}'`; } return ` ${key}="${attrValue}"`; } /** * HTML Stringifier * * @param {Array|Object} tree PostHTML Tree * * @return {String} result HTML */ function html(tree) { let result = ''; if (!Array.isArray(tree)) { tree = [tree]; } traverse(tree, node => { // Undefined, null, '', [], NaN if (node === undefined || node === null || node === false || node.length === 0 || Number.isNaN(node)) { return; } // Treat as new root tree if node is an array if (Array.isArray(node)) { result += html(node); return; } if (typeof node === 'string' || typeof node === 'number') { result += node; return; } // Skip node if (node.tag === false) { result += html(node.content); return; } const tag = node.tag || 'div'; result += '<' + tag; if (node.attrs) { result += attrs(node.attrs); } if (isSingleTag(tag)) { switch (closingSingleTag) { case 'tag': result += '>'; break; case 'slash': result += ' />'; break; default: result += '>'; } result += html(node.content); } else { result += '>' + html(node.content) + ''; } }); return result; } } /** * @module posthtml-render * * @version 1.1.5 * @license MIT */ module.exports = render;