/* * This file is part of TREB. * * TREB is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any * later version. * * TREB is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along * with TREB. If not, see . * * Copyright 2022-2026 trebco, llc. * info@treb.app * */ /* interface StringMap { [index: string]: string; } */ type StringMap = Map; /** * defaults are global, since we assume they never change. created on demand. */ let default_properties: StringMap|undefined; /** * convert CSSStyleDeclaration to map */ const PropertyMap = (source: CSSStyleDeclaration): StringMap => { const map: StringMap = new Map(); // you can iterate this thing, although apparently ts won't allow // it because it's not in the spec? should probably play ball for (let i = 0; i < source.length; i++) { const key = source[i]; map.set(key, source.getPropertyValue(key)); } return map; } /** * get applied style as text (for attribute) */ const GetAppliedStyle = (node: Element, computed: CSSStyleDeclaration, defaults: StringMap) => { const applied: StringMap = new Map(); const computed_map = PropertyMap(computed); for (const [key, value] of computed_map.entries()) { if (value !== defaults.get(key)) { applied.set(key, value); } } const arr = Array.from(applied.entries()); return (arr.map(([key, value]) => `${key}: ${value}`).join('; ') + '; ' + (node.getAttribute('style') || '')).trim().replace(/"/g, '\''); }; /** * create a clone of the node with static styles applied */ const RenderNode = (node: Element, defaults: StringMap) => { const clone = node.cloneNode(false); const computed = getComputedStyle(node); const style = GetAppliedStyle(node, computed, defaults); (clone as HTMLElement).removeAttribute('class'); (clone as HTMLElement).setAttribute('style', style); let computed_map: StringMap|undefined; Array.prototype.forEach.call(node.childNodes, (child: Node) => { switch (child.nodeType) { case Node.ELEMENT_NODE: // here we use the parent as the default style, assuming the child will inherit // create on demand, if necessary if (!computed_map) { computed_map = PropertyMap(computed); } clone.appendChild(RenderNode(child as Element, computed_map)); break; case Node.TEXT_NODE: if (node.textContent) { clone.appendChild(document.createTextNode(node.textContent)); } break; case Node.COMMENT_NODE: // silently drop comments break; default: console.warn('unhandled node type in serialize', child); } }); return clone; }; /** * serialize a node by creating a clone with static styling that can be * used stand-alone (intended for svg, but could apply generically). */ export const SerializeHTML = (node: Element) => { if (!default_properties) { const defaults: StringMap = new Map(); // regarding document, in this case we're creating an iframe // specifically for isolation, and adding it to "document". // there's no reason to require the context document here (I think). const iframe = document.createElement('iframe'); iframe.style.width = '10px'; iframe.style.height = '10px'; iframe.style.position = 'absolute'; iframe.style.left = '-100px'; document.body.appendChild(iframe); const frame_document = iframe.contentDocument; if (frame_document) { const div = frame_document.createElement('div'); frame_document.body.appendChild(div); const computed = getComputedStyle(div); Array.prototype.forEach.call(computed, key => defaults.set(key, computed[key])); } document.body.removeChild(iframe); default_properties = defaults; } const rendered = RenderNode(node, default_properties); if (rendered instanceof Element && rendered.tagName === 'svg') { if (!rendered.hasAttribute('version')) { rendered.setAttribute('version', '1.1'); } if (!rendered.hasAttribute('xmlns')) { rendered.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); } if (!rendered.hasAttribute('xmlns:xlink')) { rendered.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); } } return rendered; }; // (self as any).SerializeHTML = SerializeHTML;