"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");
}
}