Format Mermaid SVG numbers with Locale.ROOT
SimpleMermaidRenderer built SVG coordinates with
String.format("%.1f", ...) under the default locale, so on a JVM whose
region uses a comma decimal separator (e.g. de_DE) it emitted "28,0"
instead of "28.0". SVG requires '.' as the decimal separator, so that
markup is invalid; the renderer tests, which reparse coordinates with
Double.parseDouble, throw NumberFormatException for the same reason.
Pass Locale.ROOT to every String.format call so numeric output is
always '.'-separated and independent of the machine locale.
Alternative considered: forcing an en-US locale from .bazelrc or
overriding the JVM/system default locale. That does not make the
renderer correct on its own, and it still leaves production output
dependent on process-wide locale configuration. The formatting point
that emits SVG numbers should declare its locale explicitly instead.
Change-Id: Ia493dd426d3a1dffb5b7059df69c5a0001b1dab1
diff --git a/java/com/google/gitiles/doc/SimpleMermaidRenderer.java b/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
index 576b72f..da7aca5 100644
--- a/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
+++ b/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
@@ -23,6 +23,7 @@
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -2079,7 +2080,7 @@
StringBuilder svg = new StringBuilder(4096);
svg.append(
- String.format(
+ String.format(Locale.ROOT,
"<svg class=\"mermaid-svg\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 %.0f %.0f\" style=\"max-width: %.0fpx; width: 100%%; height: auto;\">\n",
width, height, width));
@@ -2142,12 +2143,12 @@
String fill = sg.customFill != null ? sg.customFill : (depth % 2 == 0 ? "#fafafa" : "#f8fafc");
String stroke = sg.customStroke != null ? sg.customStroke : "#cbd5e1";
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"8\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\" />\n",
sg.x, sg.y, sg.width, sg.height, fill, stroke));
if (sg.title != null && !sg.title.isEmpty()) {
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <text x=\"%.1f\" y=\"%.1f\" font-size=\"12\" font-weight=\"600\" fill=\"#334155\">%s</text>\n",
sg.x + 14, sg.y + 18, escapeXml(sg.title)));
}
@@ -2168,21 +2169,21 @@
if (n.shape == NodeShape.CIRCLE) {
double r = n.width / 2.0;
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <circle cx=\"%.1f\" cy=\"%.1f\" r=\"%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
n.x + r, n.y + r, r, fill, stroke));
} else if (n.shape == NodeShape.DIAMOND) {
double cx = n.x + n.width / 2.0;
double cy = n.y + n.height / 2.0;
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
cx, n.y, n.x + n.width, cy, cx, n.y + n.height, n.x, cy, fill, stroke));
} else if (n.shape == NodeShape.HEXAGON) {
double h2 = n.height / 2.0;
double indent = 16;
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
n.x + indent, n.y,
n.x + n.width - indent, n.y,
@@ -2195,35 +2196,35 @@
double rxCyl = n.width / 2.0;
double h = n.height;
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <path d=\"M %.1f %.1f a %.1f,%.1f 0 1,0 %.1f,0 a %.1f,%.1f 0 1,0 -%.1f,0 l 0,%.1f a %.1f,%.1f 0 0,0 %.1f,0 l 0,-%.1f Z\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
n.x, n.y + ry, rxCyl, ry, n.width, rxCyl, ry, n.width, h - ry * 2, rxCyl, ry, n.width, h - ry * 2, fill, stroke));
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <path d=\"M %.1f %.1f a %.1f,%.1f 0 0,0 %.1f,0\" fill=\"none\" stroke=\"%s\" stroke-width=\"1.5\" />\n",
n.x, n.y + ry, rxCyl, ry, n.width, stroke));
} else if (n.shape == NodeShape.FLAG) {
double notch = 12;
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
n.x, n.y, n.x + n.width, n.y, n.x + n.width - notch, n.y + n.height / 2.0, n.x + n.width, n.y + n.height, n.x, n.y + n.height, fill, stroke));
} else if (n.shape == NodeShape.SUBROUTINE) {
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"4\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
n.x, n.y, n.width, n.height, fill, stroke));
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <line x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"%s\" stroke-width=\"1.5\" />\n",
n.x + 10, n.y, n.x + 10, n.y + n.height, stroke));
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <line x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"%s\" stroke-width=\"1.5\" />\n",
n.x + n.width - 10, n.y, n.x + n.width - 10, n.y + n.height, stroke));
} else {
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
n.x, n.y, n.width, n.height, rx, fill, stroke));
}
@@ -2235,12 +2236,12 @@
if (n.labelLines.size() == 1) {
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <text x=\"%.1f\" y=\"%.1f\" font-size=\"12\" font-weight=\"500\" fill=\"#0f172a\" text-anchor=\"middle\" dominant-baseline=\"central\">%s</text>\n",
cx, n.y + textYOffset + n.height / 2.0, escapeXml(n.labelLines.get(0).trim())));
} else {
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <text x=\"%.1f\" y=\"%.1f\" font-size=\"12\" text-anchor=\"middle\">\n",
cx, startTextY));
for (int i = 0; i < n.labelLines.size(); i++) {
@@ -2248,7 +2249,7 @@
String textColor = i == 0 ? "#0f172a" : "#475569";
String fontSize = i == 0 ? "12" : "10.5";
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <tspan x=\"%.1f\" dy=\"%s\" font-size=\"%s\" font-weight=\"%s\" fill=\"%s\">%s</tspan>\n",
cx, i == 0 ? "0" : "16", fontSize, weight, textColor, escapeXml(n.labelLines.get(i).trim())));
}
@@ -2291,7 +2292,7 @@
double loopOffset = Math.max(35.0, labelW / 2.0 + 20.0);
double cpY = maxBottom + loopOffset;
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <path d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
startX, startY, startX + 20, cpY, endX - 20, cpY, endX, endY, strokeWidth, strokeDash, marker));
if (se.label != null && !se.label.trim().isEmpty()) {
@@ -2322,7 +2323,7 @@
double loopOffset = Math.max(35.0, labelW / 2.0 + 20.0);
double cpX = maxRight + loopOffset;
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <path d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
startX, startY, cpX, startY + 20, cpX, endY - 20, endX, endY, strokeWidth, strokeDash, marker));
if (se.label != null && !se.label.trim().isEmpty()) {
@@ -2334,7 +2335,7 @@
}
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <line x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
startX, startY, endX, endY, strokeWidth, strokeDash, marker));
@@ -2413,18 +2414,18 @@
py.add(dst.y);
StringBuilder pathD = new StringBuilder();
- pathD.append(String.format("M %.1f %.1f", px.get(0), py.get(0)));
+ pathD.append(String.format(Locale.ROOT,"M %.1f %.1f", px.get(0), py.get(0)));
for (int i = 0; i < px.size() - 1; i++) {
double xA = px.get(i), yA = py.get(i);
double xB = px.get(i + 1), yB = py.get(i + 1);
double dy = yB - yA;
pathD.append(
- String.format(
+ String.format(Locale.ROOT,
" C %.1f %.1f, %.1f %.1f, %.1f %.1f",
xA, yA + dy * 0.5, xB, yB - dy * 0.5, xB, yB));
}
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <path d=\"%s\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
pathD.toString(), strokeWidth, strokeDash, marker));
@@ -2496,18 +2497,18 @@
py.add(dst.y + dst.height / 2.0);
StringBuilder pathD = new StringBuilder();
- pathD.append(String.format("M %.1f %.1f", px.get(0), py.get(0)));
+ pathD.append(String.format(Locale.ROOT,"M %.1f %.1f", px.get(0), py.get(0)));
for (int i = 0; i < px.size() - 1; i++) {
double xA = px.get(i), yA = py.get(i);
double xB = px.get(i + 1), yB = py.get(i + 1);
double dx = xB - xA;
pathD.append(
- String.format(
+ String.format(Locale.ROOT,
" C %.1f %.1f, %.1f %.1f, %.1f %.1f",
xA + dx * 0.5, yA, xB - dx * 0.5, yB, xB, yB));
}
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <path d=\"%s\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
pathD.toString(), strokeWidth, strokeDash, marker));
@@ -2530,7 +2531,7 @@
}
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <path d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
x1, y1, cp1x, cp1y, cp2x, cp2y, x2, y2, strokeWidth, strokeDash, marker));
@@ -2547,11 +2548,11 @@
double rectW = textLen + 12;
double rectH = 18;
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"3\" fill=\"#ffffff\" fill-opacity=\"0.95\" />\n",
midX - rectW / 2.0, midY - rectH / 2.0, rectW, rectH));
svg.append(
- String.format(
+ String.format(Locale.ROOT,
" <text x=\"%.1f\" y=\"%.1f\" font-size=\"10.5\" fill=\"#475569\" text-anchor=\"middle\" dominant-baseline=\"central\">%s</text>\n",
midX, midY, escapeXml(label)));
}