// A function that converts react-test-renderer's toJSON() result // to HTML usable for React.hydrate() // // (c) Vitaliy Filippov 2021+ // License: Dual-license MPL 1.1+ or GNU LGPL 3.0+ const enclosedTags = { br: true, hr: true, input: true, img: true, link: true, source: true, col: true, area: true, base: true, meta: true, embed: true, param: true, track: true, wbr: true, keygen: true, }; const boolAttrs = { checked: true, selected: true, readonly: true, defer: true, deferred: true, disabled: true, hidden: true, multiple: true, required: true, reversed: true, }; function renderToHtml(tree) { if (tree instanceof Array) { return tree.map(renderToHtml).join(''); } else if (tree instanceof Object) { if (typeof(tree.type) == 'string') { let tag = tree.type.toLowerCase(); let children = tree.children; let html = '<'+tag; let k, v; let esc = true; for (k in tree.props) { v = tree.props[k]; k = k.toLowerCase(); if (k == 'classname') { k = 'class'; } else if (k == 'htmlfor') { k = 'for'; } else if (k == 'xlinkhref') { k = 'xlink:href'; } else if (boolAttrs[k]) { if (v) html += ' '+k; continue; } else if (k == 'style' && v instanceof Object) { v = Object.keys(v).map(sk => sk.replace(/[A-Z]/g, m => '-'+m.toLowerCase())+':'+v[sk]).join(';'); } else if (k == 'value' && tag == 'textarea') { children = v; continue; } else if (k == 'dangerouslysetinnerhtml') { children = v == null ? '' : v; esc = false; continue; } if (v == null || typeof v == 'function') { continue; } html += ' '+k+'="'+htmlspecialchars(''+v)+'"'; } if (!enclosedTags[tag]) { html += '>'+(esc ? renderToHtml(children) : children)+''; } else { html += ' />'; } return html; } else { return renderToHtml(tree.children); } } else if (tree != null) { return htmlspecialchars(tree); } return ''; } function htmlspecialchars(text) { return (''+text).replace(/&/g, '&') .replace(/'/g, ''') // ' .replace(/"/g, '"') // " .replace(//g, '>'); } module.exports = renderToHtml;