"use strict"; const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; /** * Convert the rendered CMap surface into a standalone SVG snapshot. * * HTML concept cards remain inside an XHTML foreignObject while connector SVG * elements stay vector based. The renderer reads the current DOM only; it has * no knowledge of the editor model or persistence layer. */ export class CmapSnapshotSvgRenderer { constructor({ stylesheetFilter = (sheet) => Boolean(sheet.href?.includes("cmap.css")) } = {}) { this.stylesheetFilter = stylesheetFilter; } render(surface, { width = null, height = null, includeStyles = true } = {}) { if (!surface || typeof surface.cloneNode !== "function") { throw new TypeError("A rendered CMap surface is required"); } const dimensions = this.dimensions(surface, width, height); const clone = surface.cloneNode(true); this.removeInteractiveElements(clone); clone.removeAttribute("id"); clone.style.zoom = "1"; clone.style.transform = "none"; clone.style.width = `${dimensions.width}px`; clone.style.height = `${dimensions.height}px`; const svg = document.createElementNS(SVG_NAMESPACE, "svg"); svg.setAttribute("xmlns", SVG_NAMESPACE); svg.setAttribute("xmlns:xhtml", XHTML_NAMESPACE); svg.setAttribute("version", "1.1"); svg.setAttribute("width", String(dimensions.width)); svg.setAttribute("height", String(dimensions.height)); svg.setAttribute("viewBox", `0 0 ${dimensions.width} ${dimensions.height}`); svg.setAttribute("preserveAspectRatio", "xMinYMin meet"); if (includeStyles) { const style = document.createElementNS(SVG_NAMESPACE, "style"); style.textContent = this.stylesheetText(); svg.append(style); } const foreignObject = document.createElementNS(SVG_NAMESPACE, "foreignObject"); foreignObject.setAttribute("x", "0"); foreignObject.setAttribute("y", "0"); foreignObject.setAttribute("width", String(dimensions.width)); foreignObject.setAttribute("height", String(dimensions.height)); clone.setAttribute("xmlns", XHTML_NAMESPACE); foreignObject.append(clone); svg.append(foreignObject); return new XMLSerializer().serializeToString(svg); } dimensions(surface, requestedWidth, requestedHeight) { const width = Number(requestedWidth) || Math.ceil(Math.max( surface.scrollWidth || 0, surface.getBoundingClientRect?.().width || 0, ...Array.from(surface.children || []).map((child) => (Number.parseFloat(child.style.left) || 0) + (child.offsetWidth || 0)) )); const height = Number(requestedHeight) || Math.ceil(Math.max( surface.scrollHeight || 0, surface.getBoundingClientRect?.().height || 0, ...Array.from(surface.children || []).map((child) => (Number.parseFloat(child.style.top) || 0) + (child.offsetHeight || 0)) )); return { width: Math.max(1, width || 1), height: Math.max(1, height || 1) }; } removeInteractiveElements(root) { for (const element of root.querySelectorAll( ".rw-cmap-handle, .rw-cmap-resize-handle, .rw-cmap-relation-handle, " + ".rw-cmap-edit-handle, .rw-cmap-submap-toggle, .rw-cmap-submap-open")) { element.remove(); } for (const element of root.querySelectorAll("[aria-selected], [data-rw-cmap-bound]") ) { element.removeAttribute("aria-selected"); element.removeAttribute("data-rw-cmap-bound"); } } stylesheetText() { if (typeof document === "undefined") return ""; const rules = []; for (const sheet of Array.from(document.styleSheets || [])) { if (!this.stylesheetFilter(sheet)) continue; try { rules.push(...Array.from(sheet.cssRules || []).map((rule) => rule.cssText)); } catch (_error) { // Cross-origin stylesheets cannot be inspected and are skipped. } } return rules.join("\n"); } }