import { Fragment, isValidElement, type ReactNode } from "react"; import { VedicChartSvgDrawing } from "@/components/personal-report/vedic-chart-svg"; import type { NorthIndianChartModel } from "./report-chart-block"; import { projectOrdinaryReportMarkdown } from "./report-public-projection"; function escapeXml(value: unknown): string { return String(value).replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]!); } /** Deliberately only serializes our static SVG React tree, never arbitrary HTML. */ function serializeSvgNode(node: ReactNode): string { if (node == null || typeof node === "boolean") return ""; if (typeof node === "string" || typeof node === "number") return escapeXml(node); if (Array.isArray(node)) return node.map(serializeSvgNode).join(""); if (!isValidElement>(node)) return ""; if (node.type === Fragment) return serializeSvgNode(node.props.children as ReactNode); if (typeof node.type === "function") { return serializeSvgNode((node.type as (props: Record) => ReactNode)(node.props)); } if (typeof node.type !== "string" || !/^(svg|g|polygon|text|line|defs|clipPath)$/.test(node.type)) return ""; const attributes = Object.entries(node.props).filter(([name, value]) => name !== "children" && name !== "key" && value != null && !/^on/.test(name)).map(([name, value]) => { const attribute = name === "className" ? "class" : name === "viewBox" ? name : name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`); return ` ${attribute}="${escapeXml(value)}"`; }).join(""); return `<${node.type}${attributes}>${serializeSvgNode(node.props.children as ReactNode)}`; } /** Offline fallback for reports without the engine's already-safe SVG. Reuse the * reader's glyph layout and geometry, not a second chart renderer. Presentation * attributes survive Markdown viewers without the application's stylesheet. * Clipping definitions are not in the ordinary SVG allowlist; remove them rather * than widening that security boundary. The renderer already bounds occupants. */ export function reportChartExportSvg(chart: NorthIndianChartModel, ariaLabel: string): string { const rendered = serializeSvgNode(VedicChartSvgDrawing({ chart, ariaLabel, chartId: "export-chart" })) .replace(/[\s\S]*?<\/defs>/g, "") .replace(/ clip-path="[^"]*"/g, "") .replace(/ { if (classes.includes("personal-report-chart-cell")) return 'fill="none" stroke="currentColor" stroke-width="1.25"'; if (classes.includes("personal-report-chart-retrograde")) return 'stroke="currentColor" stroke-width="1.9"'; const size = classes.includes("personal-report-chart-legend-glyph") ? 14 : classes.includes("personal-report-chart-glyph") ? 22 : classes.includes("personal-report-chart-asc") || classes.includes("personal-report-chart-overflow") ? 13 : classes.includes("personal-report-chart-legend") || classes.includes("personal-report-chart-occupant-text") ? 12 : null; return size ? `fill="currentColor" font-size="${size}"` : ""; }); return projectOrdinaryReportMarkdown(rendered); }