import { SerializedNodeWithId, NodeType, TagMap, ElementNode } from './types'; import { Mirror } from './utils'; const svgTagMap: TagMap = { altglyph: 'altGlyph', altglyphdef: 'altGlyphDef', altglyphitem: 'altGlyphItem', animatecolor: 'animateColor', animatemotion: 'animateMotion', animatetransform: 'animateTransform', clippath: 'clipPath', feblend: 'feBlend', fecolormatrix: 'feColorMatrix', fecomponenttransfer: 'feComponentTransfer', fecomposite: 'feComposite', feconvolvematrix: 'feConvolveMatrix', fediffuselighting: 'feDiffuseLighting', fedisplacementmap: 'feDisplacementMap', fedistantlight: 'feDistantLight', fedropshadow: 'feDropShadow', feflood: 'feFlood', fefunca: 'feFuncA', fefuncb: 'feFuncB', fefuncg: 'feFuncG', fefuncr: 'feFuncR', fegaussianblur: 'feGaussianBlur', feimage: 'feImage', femerge: 'feMerge', femergenode: 'feMergeNode', femorphology: 'feMorphology', feoffset: 'feOffset', fepointlight: 'fePointLight', fespecularlighting: 'feSpecularLighting', fespotlight: 'feSpotLight', fetile: 'feTile', feturbulence: 'feTurbulence', foreignobject: 'foreignObject', glyphref: 'glyphRef', lineargradient: 'linearGradient', radialgradient: 'radialGradient', }; export const EVOLV_SESSION_NODE_ID_DATA_KEY = 'data-evolv-session-node-id'; export const EVOLV_SESSION_NODE_HIGHLIGHTED_KEY = 'data-evolv-session-node-highlighted'; export const EVOLV_SESSION_NODE_BADGED_KEY = 'data-evolv-session-node-badged'; export const EVOLV_SESSION_NODE_TARGET_KEY = 'data-evolv-session-node-target'; export const DO_NOT_CAPTURE_DATA_TAG = 'data-evolv-do-not-capture'; export function toCamelDataKey(kebab: string): string { return kebab.slice(5).replace(/-(\w)/g, (_,c) => c.toUpperCase()); } function getTagName(n: ElementNode): string { return svgTagMap[n.tagName] ? svgTagMap[n.tagName] : n.tagName; } function buildNode( n: SerializedNodeWithId, options: { doc: Document; } ): Node | null { const { doc } = options; switch (n.type) { case NodeType.Document: return doc.implementation.createDocument(null, '', null); case NodeType.DocumentType: return doc.implementation.createDocumentType( n.name || 'html', n.publicId, n.systemId, ); case NodeType.Element: { const tagName = getTagName(n); const node: HTMLElement | SVGElement = n.isSVG ? doc.createElementNS('http://www.w3.org/2000/svg', tagName) : doc.createElement(tagName); node.dataset[toCamelDataKey(EVOLV_SESSION_NODE_ID_DATA_KEY)] = n.id; for (const name in n.attributes ?? []) { if (!Object.prototype.hasOwnProperty.call(n.attributes, name)) { continue; } let value = n.attributes[name]; if (tagName === 'option' && name === 'selected' && value === false) { continue; } value = typeof value === 'boolean' || typeof value === 'number' ? '' : value; switch (name) { case 'evolv_width': (node as HTMLElement).style.width = value; break; case 'evolv_height': (node as HTMLElement).style.height = value; break; case 'childNodes': break; default: node.setAttribute(name, value); break; } } return node; } case NodeType.Text: return doc.createTextNode(n.textContent ?? ''); case NodeType.CDATA: return doc.createCDATASection(n.textContent ?? ''); case NodeType.Comment: return doc.createComment(n.textContent ?? ''); default: return null; } } export function buildNodeWithMeta( n: SerializedNodeWithId, options: { doc: Document; mirror: Mirror; skipChild?: boolean; } ): Node | null { const { doc, mirror, skipChild = false, } = options; let node = buildNode(n, { doc }); if (!node) { return null; } if (n.rootId) { console.assert( (mirror.getNode(n.rootId) as Document) === doc, 'Target document should have the same root id.', ); } if (n.type === NodeType.Document) { doc.close(); doc.open(); if ( n.compatMode === 'BackCompat' && n.childNodes && n.childNodes[0].type !== NodeType.DocumentType ) { if ( n.childNodes[0].type === NodeType.Element && 'xmlns' in n.childNodes[0].attributes && n.childNodes[0].attributes.xmlns === 'http://www.w3.org/1999/xhtml' ) { doc.write( '', ); } else { doc.write( '', ); } } node = doc; } mirror.add(node, n); if ((n.type === NodeType.Document || n.type === NodeType.Element) && !skipChild) { for (const child of n.childNodes ?? []) { const childNode = buildNodeWithMeta(child, { doc, mirror, skipChild: false, }); if (!childNode) { console.warn('Failed to rebuild', child); continue; } node.appendChild(childNode); // I have no idea why this is broken or why this fixes it. // Svgs do not seem to render properly, but if I jiggle the innerHTML then it works. if (childNode.nodeName === 'SVG' && n.type === NodeType.Element) { const el = node as Element; el.innerHTML = el.innerHTML + ' '; } } } return node; } export function rebuild( n: SerializedNodeWithId, options: { doc: Document; mirror?: Mirror; } ): Node | null { const { doc, mirror = Mirror.create(), } = options; return buildNodeWithMeta(n, { doc, mirror, skipChild: false, }); }