Files
Jyotisha/frontend/src/lib/report-chart-export.tsx
T

49 lines
3.2 KiB
TypeScript

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 => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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<Record<string, unknown>>(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<string, unknown>) => 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)}</${node.type}>`;
}
/** 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(/<defs>[\s\S]*?<\/defs>/g, "")
.replace(/ clip-path="[^"]*"/g, "")
.replace(/<svg\b/, '<svg xmlns="http://www.w3.org/2000/svg" height="452"')
.replace(/class="([^"]*)"/g, (_match, classes: string) => {
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);
}