SvgDiagramRenderer.java
package com.taxonomy.export;
import com.taxonomy.diagram.DiagramScene;
import com.taxonomy.diagram.DiagramSceneEdge;
import com.taxonomy.diagram.DiagramSceneNode;
import java.util.List;
import java.util.Locale;
/** Produces a standalone, script-free SVG from a render-ready diagram scene. */
public class SvgDiagramRenderer {
public String render(DiagramScene scene) {
if (scene == null || scene.isEmpty()) {
throw new IllegalArgumentException("Architecture scene must contain at least one node");
}
StringBuilder svg = new StringBuilder(16_384);
svg.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
.append("<svg xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" ")
.append("viewBox=\"0 0 ").append(number(scene.width())).append(' ')
.append(number(scene.height())).append("\" width=\"")
.append(number(scene.width())).append("\" height=\"")
.append(number(scene.height())).append("\">\n")
.append(" <title>").append(xml(scene.title())).append("</title>\n")
.append(" <desc>Requirement-derived architecture diagram</desc>\n")
.append(" <defs><marker id=\"arrow\" markerWidth=\"10\" markerHeight=\"8\" ")
.append("refX=\"9\" refY=\"4\" orient=\"auto\"><path d=\"M0,0 L10,4 L0,8 z\" ")
.append("fill=\"#586174\"/></marker></defs>\n")
.append(" <rect width=\"100%\" height=\"100%\" fill=\"#ffffff\"/>\n");
for (DiagramSceneEdge edge : scene.edges()) {
svg.append(" <g class=\"architecture-edge\" data-edge-id=\"")
.append(xml(edge.id())).append("\">\n")
.append(" <line x1=\"").append(number(edge.sourceX()))
.append("\" y1=\"").append(number(edge.sourceY()))
.append("\" x2=\"").append(number(edge.targetX()))
.append("\" y2=\"").append(number(edge.targetY()))
.append("\" stroke=\"#586174\" stroke-width=\"")
.append(number(1.2 + Math.max(0.0, edge.relevance()) * 1.8))
.append("\" marker-end=\"url(#arrow)\"/>\n");
String relation = edge.relationType() == null ? "" : edge.relationType();
if (!relation.isBlank()) {
double x = (edge.sourceX() + edge.targetX()) / 2.0;
double y = (edge.sourceY() + edge.targetY()) / 2.0 - 6.0;
svg.append(" <text x=\"").append(number(x)).append("\" y=\"")
.append(number(y))
.append("\" text-anchor=\"middle\" font-family=\"Arial, sans-serif\" ")
.append("font-size=\"11\" fill=\"#394150\">")
.append(xml(relation)).append("</text>\n");
}
svg.append(" </g>\n");
}
for (DiagramSceneNode node : scene.nodes()) {
svg.append(" <g class=\"architecture-node\" data-node-id=\"")
.append(xml(node.id())).append("\" transform=\"translate(")
.append(number(node.x())).append(',').append(number(node.y())).append(")\">\n")
.append(" <rect width=\"").append(number(node.width()))
.append("\" height=\"").append(number(node.height()))
.append("\" rx=\"10\" fill=\"").append(fill(node.type()))
.append("\" stroke=\"").append(node.anchor() ? "#172554" : "#64748b")
.append("\" stroke-width=\"").append(node.anchor() ? "3" : "1.3")
.append("\"/>\n")
.append(" <text x=\"14\" y=\"22\" font-family=\"Arial, sans-serif\" ")
.append("font-size=\"11\" font-weight=\"bold\" fill=\"#334155\">")
.append(xml(node.id())).append("</text>\n");
List<String> lines = DiagramTextWrapper.wrap(
node.label(), 32, 2, "Unnamed architecture element");
for (int index = 0; index < lines.size(); index++) {
svg.append(" <text x=\"14\" y=\"")
.append(44 + index * 17)
.append("\" font-family=\"Arial, sans-serif\" font-size=\"13\" ")
.append("fill=\"#0f172a\">")
.append(xml(lines.get(index))).append("</text>\n");
}
svg.append(" <text x=\"").append(number(node.width() - 12))
.append("\" y=\"18\" text-anchor=\"end\" font-family=\"Arial, sans-serif\" ")
.append("font-size=\"10\" fill=\"#475569\">")
.append(xml(node.type())).append(" ยท ")
.append(Math.round(node.relevance() * 100.0)).append("%</text>\n")
.append(" </g>\n");
}
svg.append("</svg>\n");
return svg.toString();
}
private static String fill(String type) {
if (type == null) return "#f1f5f9";
return switch (type) {
case "Capabilities" -> "#dbeafe";
case "Business Processes", "Business Roles" -> "#dcfce7";
case "Core Services", "COI Services", "Services" -> "#fef3c7";
case "User Applications", "Applications" -> "#f3e8ff";
case "Information Products" -> "#ffe4e6";
case "Communications Services" -> "#cffafe";
default -> "#f1f5f9";
};
}
private static String xml(String value) {
if (value == null) return "";
return value.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
private static String number(double value) {
return String.format(Locale.ROOT, "%.2f", value);
}
}