Merge "Include PRETTIFY_CSS_URL on repository index landing page when README is present"
diff --git a/java/com/google/gitiles/doc/SimpleMermaidRenderer.java b/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
index 140fc44..1429e43 100644
--- a/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
+++ b/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
@@ -19,6 +19,7 @@
import static com.google.common.primitives.Doubles.max;
import com.google.common.base.Ascii;
+import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -98,6 +99,7 @@
public boolean isVirtual = false;
@Nullable public String customFill;
@Nullable public String customStroke;
+ @Nullable public String customColor;
public Node(String id) {
this.id = id;
@@ -127,6 +129,7 @@
public double height;
@Nullable public String customFill;
@Nullable public String customStroke;
+ @Nullable public String customColor;
public Subgraph(String id, String title) {
this.id = id;
@@ -609,6 +612,7 @@
String fill = null;
String stroke = null;
+ String color = null;
int p = 0;
while (p < rest.length()) {
int nextSep = rest.length();
@@ -628,6 +632,8 @@
fill = val;
} else if (key.equals("stroke")) {
stroke = val;
+ } else if (key.equals("color")) {
+ color = val;
}
}
p = nextSep + 1;
@@ -639,6 +645,9 @@
if (stroke != null && !isValidCssColor(stroke)) {
stroke = null;
}
+ if (color != null && !isValidCssColor(color)) {
+ color = null;
+ }
Subgraph sg = graph.lookupSubgraph(targetId);
if (sg != null) {
@@ -648,6 +657,9 @@
if (stroke != null) {
sg.customStroke = stroke;
}
+ if (color != null) {
+ sg.customColor = color;
+ }
}
Node n = graph.nodes.get(targetId);
if (n != null) {
@@ -657,6 +669,9 @@
if (stroke != null) {
n.customStroke = stroke;
}
+ if (color != null) {
+ n.customColor = color;
+ }
}
}
@@ -683,6 +698,109 @@
return v.matches("^(rgb|hsl)a?\\([0-9%,. ]+\\)$");
}
+ private static boolean isLightColor(@Nullable String color) {
+ if (isNullOrEmpty(color)) {
+ return true;
+ }
+ String c = color.trim().toLowerCase(Locale.ROOT);
+ if (c.startsWith("#")) {
+ try {
+ String hex = c.substring(1);
+ int r;
+ int g;
+ int b;
+ if (hex.length() == 3 || hex.length() == 4) {
+ r = Integer.parseInt(hex.substring(0, 1) + hex.substring(0, 1), 16);
+ g = Integer.parseInt(hex.substring(1, 2) + hex.substring(1, 2), 16);
+ b = Integer.parseInt(hex.substring(2, 3) + hex.substring(2, 3), 16);
+ } else if (hex.length() >= 6) {
+ r = Integer.parseInt(hex.substring(0, 2), 16);
+ g = Integer.parseInt(hex.substring(2, 4), 16);
+ b = Integer.parseInt(hex.substring(4, 6), 16);
+ } else {
+ return true;
+ }
+ return (0.299 * r + 0.587 * g + 0.114 * b) >= 128;
+ } catch (NumberFormatException e) {
+ return true;
+ }
+ }
+ if (c.startsWith("rgb")) {
+ int start = c.indexOf('(');
+ int end = c.indexOf(')');
+ if (start != -1 && end > start) {
+ List<String> parts = Splitter.on(',').splitToList(c.substring(start + 1, end));
+ if (parts.size() >= 3) {
+ try {
+ double r = parseColorComponent(parts.get(0));
+ double g = parseColorComponent(parts.get(1));
+ double b = parseColorComponent(parts.get(2));
+ return (0.299 * r + 0.587 * g + 0.114 * b) >= 128;
+ } catch (NumberFormatException e) {
+ return true;
+ }
+ }
+ }
+ }
+ if (c.startsWith("hsl")) {
+ int start = c.indexOf('(');
+ int end = c.indexOf(')');
+ if (start != -1 && end > start) {
+ List<String> parts = Splitter.on(',').splitToList(c.substring(start + 1, end));
+ if (parts.size() >= 3) {
+ try {
+ String lStr = parts.get(2).trim().replace("%", "");
+ double l = Double.parseDouble(lStr);
+ return l >= 50.0;
+ } catch (NumberFormatException e) {
+ return true;
+ }
+ }
+ }
+ }
+ switch (c) {
+ case "black",
+ "navy",
+ "darkblue",
+ "mediumblue",
+ "blue",
+ "darkgreen",
+ "green",
+ "teal",
+ "darkcyan",
+ "darkred",
+ "maroon",
+ "purple",
+ "indigo",
+ "darkmagenta",
+ "darkviolet",
+ "darkslateblue",
+ "saddlebrown",
+ "sienna",
+ "brown",
+ "darkslategray",
+ "darkslategrey",
+ "midnightblue",
+ "gray",
+ "grey",
+ "dimgray",
+ "dimgrey" -> {
+ return false;
+ }
+ default -> {
+ return true;
+ }
+ }
+ }
+
+ private static double parseColorComponent(String part) {
+ String p = part.trim();
+ if (p.endsWith("%")) {
+ return Double.parseDouble(p.substring(0, p.length() - 1)) * 2.55;
+ }
+ return Double.parseDouble(p);
+ }
+
private static void parseStatement(
CharScanner s, MermaidGraph graph, @Nullable Subgraph currentSubgraph) {
List<RawNodeToken> prevGroup = scanNodeGroup(s);
@@ -2188,13 +2306,14 @@
svg.append(
" <marker id=\"mermaid-arrow\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\""
+ " markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n");
- svg.append(" <path d=\"M 0 1.5 L 10 5 L 0 8.5 z\" fill=\"#64748b\" />\n");
+ svg.append(
+ " <path class=\"mermaid-arrow\" d=\"M 0 1.5 L 10 5 L 0 8.5 z\" fill=\"#64748b\" />\n");
svg.append(" </marker>\n");
svg.append(
" <filter id=\"node-shadow\" x=\"-5%\" y=\"-5%\" width=\"115%\" height=\"120%\">\n");
svg.append(
- " <feDropShadow dx=\"0\" dy=\"1.5\" stdDeviation=\"2\" flood-color=\"#0f172a\""
- + " flood-opacity=\"0.06\" />\n");
+ " <feDropShadow class=\"mermaid-shadow\" dx=\"0\" dy=\"1.5\" stdDeviation=\"2\""
+ + " flood-color=\"#0f172a\" flood-opacity=\"0.06\" />\n");
svg.append(" </filter>\n");
svg.append(" </defs>\n");
@@ -2246,11 +2365,21 @@
int depth = getSubgraphDepth(sg);
String fill = sg.customFill != null ? sg.customFill : (depth % 2 == 0 ? "#fafafa" : "#f8fafc");
String stroke = sg.customStroke != null ? sg.customStroke : "#cbd5e1";
+ String sgClass =
+ sg.customFill == null && sg.customStroke == null
+ ? (depth % 2 == 0 ? "mermaid-subgraph" : "mermaid-subgraph mermaid-subgraph--alt")
+ : (sg.customFill == null
+ ? (depth % 2 == 0
+ ? "mermaid-subgraph-fill"
+ : "mermaid-subgraph-fill mermaid-subgraph-fill--alt")
+ : (sg.customStroke == null ? "mermaid-subgraph-stroke" : ""));
+ String classAttr = sgClass.isEmpty() ? "" : String.format(" class=\"%s\"", sgClass);
svg.append(
String.format(
Locale.ROOT,
- " <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"8\" fill=\"%s\""
+ " <rect%s x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"8\" fill=\"%s\""
+ " stroke=\"%s\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\" />\n",
+ classAttr,
sg.x,
sg.y,
sg.width,
@@ -2258,13 +2387,28 @@
fill,
stroke));
if (sg.title != null && !sg.title.isEmpty()) {
+ String titleColor;
+ String titleClassAttr;
+ if (sg.customColor != null) {
+ titleColor = sg.customColor;
+ titleClassAttr = "";
+ } else if (sg.customFill != null) {
+ boolean light = isLightColor(sg.customFill);
+ titleColor = light ? "#334155" : "#bdc1c6";
+ titleClassAttr = "";
+ } else {
+ titleColor = "#334155";
+ titleClassAttr = " class=\"mermaid-subgraph-title\"";
+ }
svg.append(
String.format(
Locale.ROOT,
- " <text x=\"%.1f\" y=\"%.1f\" font-size=\"12\" font-weight=\"600\""
- + " fill=\"#334155\">%s</text>\n",
+ " <text%s x=\"%.1f\" y=\"%.1f\" font-size=\"12\" font-weight=\"600\""
+ + " fill=\"%s\">%s</text>\n",
+ titleClassAttr,
sg.x + 14,
sg.y + 18,
+ titleColor,
escapeXml(sg.title)));
}
}
@@ -2279,6 +2423,15 @@
String fill = n.customFill != null ? n.customFill : "#ffffff";
String stroke = n.customStroke != null ? n.customStroke : "#64748b";
+ String nodeClass =
+ n.customFill == null && n.customStroke == null
+ ? "mermaid-node"
+ : (n.customFill == null
+ ? "mermaid-node-fill"
+ : (n.customStroke == null ? "mermaid-node-stroke" : ""));
+ String classAttr = nodeClass.isEmpty() ? "" : String.format(" class=\"%s\"", nodeClass);
+ String strokeClassAttr =
+ n.customStroke == null ? " class=\"mermaid-node-stroke\"" : "";
// Shape Geometry
switch (n.shape) {
@@ -2287,8 +2440,9 @@
svg.append(
String.format(
Locale.ROOT,
- " <circle cx=\"%.1f\" cy=\"%.1f\" r=\"%.1f\" fill=\"%s\" stroke=\"%s\""
+ " <circle%s cx=\"%.1f\" cy=\"%.1f\" r=\"%.1f\" fill=\"%s\" stroke=\"%s\""
+ " stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+ classAttr,
n.x + r,
n.y + r,
r,
@@ -2301,8 +2455,9 @@
svg.append(
String.format(
Locale.ROOT,
- " <polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\" fill=\"%s\""
+ " <polygon%s points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\" fill=\"%s\""
+ " stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+ classAttr,
cx,
n.y,
n.x + n.width,
@@ -2320,9 +2475,10 @@
svg.append(
String.format(
Locale.ROOT,
- " <polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\""
+ " <polygon%s 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",
+ classAttr,
n.x + indent,
n.y,
n.x + n.width - indent,
@@ -2345,9 +2501,10 @@
svg.append(
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"
+ " <path%s 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",
+ classAttr,
n.x,
n.y + ry,
rxCyl,
@@ -2366,8 +2523,9 @@
svg.append(
String.format(
Locale.ROOT,
- " <path d=\"M %.1f %.1f a %.1f,%.1f 0 0,0 %.1f,0\" fill=\"none\" stroke=\"%s\""
+ " <path%s d=\"M %.1f %.1f a %.1f,%.1f 0 0,0 %.1f,0\" fill=\"none\" stroke=\"%s\""
+ " stroke-width=\"1.5\" />\n",
+ strokeClassAttr,
n.x,
n.y + ry,
rxCyl,
@@ -2380,9 +2538,10 @@
svg.append(
String.format(
Locale.ROOT,
- " <polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\""
+ " <polygon%s points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\""
+ " fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\""
+ " />\n",
+ classAttr,
n.x,
n.y,
n.x + n.width,
@@ -2400,8 +2559,9 @@
svg.append(
String.format(
Locale.ROOT,
- " <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"4\" fill=\"%s\""
+ " <rect%s x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"4\" fill=\"%s\""
+ " stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+ classAttr,
n.x,
n.y,
n.width,
@@ -2411,8 +2571,9 @@
svg.append(
String.format(
Locale.ROOT,
- " <line x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"%s\""
+ " <line%s x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"%s\""
+ " stroke-width=\"1.5\" />\n",
+ strokeClassAttr,
n.x + 10,
n.y,
n.x + 10,
@@ -2421,8 +2582,9 @@
svg.append(
String.format(
Locale.ROOT,
- " <line x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"%s\""
+ " <line%s x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"%s\""
+ " stroke-width=\"1.5\" />\n",
+ strokeClassAttr,
n.x + n.width - 10,
n.y,
n.x + n.width - 10,
@@ -2433,9 +2595,10 @@
svg.append(
String.format(
Locale.ROOT,
- " <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"%.1f\""
+ " <rect%s x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"%.1f\""
+ " fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\""
+ " filter=\"url(#node-shadow)\" />\n",
+ classAttr,
n.x,
n.y,
n.width,
@@ -2445,6 +2608,33 @@
stroke));
}
+ // Determine text colors and classes
+ String primaryTextColor;
+ String subtextColor;
+ String primaryTextClass;
+ String subtextClass;
+
+ if (n.customColor != null) {
+ primaryTextColor = n.customColor;
+ subtextColor = n.customColor;
+ primaryTextClass = "";
+ subtextClass = "";
+ } else if (n.customFill != null) {
+ boolean light = isLightColor(n.customFill);
+ primaryTextColor = light ? "#0f172a" : "#e8eaed";
+ subtextColor = light ? "#475569" : "#94a3b8";
+ primaryTextClass = "";
+ subtextClass = "";
+ } else {
+ primaryTextColor = "#0f172a";
+ subtextColor = "#475569";
+ primaryTextClass = "mermaid-node-text";
+ subtextClass = "mermaid-node-subtext";
+ }
+
+ String textClassAttr =
+ primaryTextClass.isEmpty() ? "" : String.format(" class=\"%s\"", primaryTextClass);
+
// Node Text using structured AST labelLines
double cx = n.x + n.width / 2.0;
double textYOffset = n.shape == NodeShape.CYLINDER ? 4.0 : 0.0;
@@ -2454,10 +2644,12 @@
svg.append(
String.format(
Locale.ROOT,
- " <text x=\"%.1f\" y=\"%.1f\" font-size=\"12\" font-weight=\"500\" fill=\"#0f172a\""
+ " <text%s x=\"%.1f\" y=\"%.1f\" font-size=\"12\" font-weight=\"500\" fill=\"%s\""
+ " text-anchor=\"middle\" dominant-baseline=\"central\">%s</text>\n",
+ textClassAttr,
cx,
n.y + textYOffset + n.height / 2.0,
+ primaryTextColor,
escapeXml(n.labelLines.get(0).trim())));
} else {
svg.append(
@@ -2468,13 +2660,17 @@
startTextY));
for (int i = 0; i < n.labelLines.size(); i++) {
String weight = i == 0 ? "600" : "400";
- String textColor = i == 0 ? "#0f172a" : "#475569";
+ String textColor = i == 0 ? primaryTextColor : subtextColor;
+ String tspanClass = i == 0 ? primaryTextClass : subtextClass;
+ String tspanClassAttr =
+ tspanClass.isEmpty() ? "" : String.format(" class=\"%s\"", tspanClass);
String fontSize = i == 0 ? "12" : "10.5";
svg.append(
String.format(
Locale.ROOT,
- " <tspan x=\"%.1f\" dy=\"%s\" font-size=\"%s\" font-weight=\"%s\""
+ " <tspan%s x=\"%.1f\" dy=\"%s\" font-size=\"%s\" font-weight=\"%s\""
+ " fill=\"%s\">%s</tspan>\n",
+ tspanClassAttr,
cx,
i == 0 ? "0" : "16",
fontSize,
@@ -2532,7 +2728,7 @@
svg.append(
String.format(
Locale.ROOT,
- " <path d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\""
+ " <path class=\"mermaid-edge\" d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\""
+ " stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
startX,
startY,
@@ -2581,7 +2777,7 @@
svg.append(
String.format(
Locale.ROOT,
- " <path d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\""
+ " <path class=\"mermaid-edge\" d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\""
+ " stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
startX,
startY,
@@ -2605,7 +2801,7 @@
svg.append(
String.format(
Locale.ROOT,
- " <line x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"#64748b\""
+ " <line class=\"mermaid-edge\" x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"#64748b\""
+ " stroke-width=\"%s\" %s%s/>\n",
startX,
startY,
@@ -2714,7 +2910,7 @@
svg.append(
String.format(
Locale.ROOT,
- " <path d=\"%s\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
+ " <path class=\"mermaid-edge\" d=\"%s\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
pathD,
strokeWidth,
strokeDash,
@@ -2809,7 +3005,7 @@
svg.append(
String.format(
Locale.ROOT,
- " <path d=\"%s\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
+ " <path class=\"mermaid-edge\" d=\"%s\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
pathD,
strokeWidth,
strokeDash,
@@ -2837,7 +3033,7 @@
svg.append(
String.format(
Locale.ROOT,
- " <path d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\""
+ " <path class=\"mermaid-edge\" d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\""
+ " stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
x1,
y1,
@@ -2867,7 +3063,7 @@
svg.append(
String.format(
Locale.ROOT,
- " <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"3\" fill=\"#ffffff\""
+ " <rect class=\"mermaid-edge-label-bg\" 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,
@@ -2876,7 +3072,7 @@
svg.append(
String.format(
Locale.ROOT,
- " <text x=\"%.1f\" y=\"%.1f\" font-size=\"10.5\" fill=\"#475569\""
+ " <text class=\"mermaid-edge-label-text\" x=\"%.1f\" y=\"%.1f\" font-size=\"10.5\" fill=\"#475569\""
+ " text-anchor=\"middle\" dominant-baseline=\"central\">%s</text>\n",
midX,
midY,
diff --git a/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java b/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java
index 2d32676..b38a1e6 100644
--- a/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java
+++ b/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java
@@ -1547,4 +1547,146 @@
List<SvgDoc.Rect2D> sgs = doc.getSubgraphBoundingBoxes();
assertThat(sgs).hasSize(3);
}
+
+ @Test
+ public void testThemeClassesOnDiagramElements() {
+ String code =
+ """
+ graph TD
+ subgraph Outer ["Container"]
+ A[Node A] -->|link label| B[Node B]
+ end
+ """;
+ SvgDoc doc = render(code);
+
+ // Arrow marker and drop shadow in defs
+ Element arrow =
+ (Element) doc.getElementsByTag("marker").get(0).getElementsByTagName("path").item(0);
+ assertThat(arrow.getAttribute("class")).isEqualTo("mermaid-arrow");
+ Element shadow =
+ (Element)
+ doc.getElementsByTag("filter").get(0).getElementsByTagName("feDropShadow").item(0);
+ assertThat(shadow.getAttribute("class")).isEqualTo("mermaid-shadow");
+
+ // Subgraph rect and title
+ Element sgRect = doc.getElementsByTag("rect").get(0);
+ assertThat(sgRect.getAttribute("class")).isEqualTo("mermaid-subgraph");
+ Element sgTitle = doc.findText("Container");
+ assertThat(sgTitle).isNotNull();
+ assertThat(sgTitle.getAttribute("class")).isEqualTo("mermaid-subgraph-title");
+
+ // Edge label badge rect and text (rendered with edge before nodes)
+ Element badgeBg = doc.getElementsByTag("rect").get(1);
+ assertThat(badgeBg.getAttribute("class")).isEqualTo("mermaid-edge-label-bg");
+ Element badgeText = doc.findText("link label");
+ assertThat(badgeText).isNotNull();
+ assertThat(badgeText.getAttribute("class")).isEqualTo("mermaid-edge-label-text");
+
+ // Nodes and text
+ Element nodeA = doc.getElementsByTag("rect").get(2);
+ assertThat(nodeA.getAttribute("class")).isEqualTo("mermaid-node");
+ Element textA = doc.findText("Node A");
+ assertThat(textA).isNotNull();
+ assertThat(textA.getAttribute("class")).isEqualTo("mermaid-node-text");
+
+ // Edge path
+ Element edge = doc.getEdgePaths().get(0);
+ assertThat(edge.getAttribute("class")).isEqualTo("mermaid-edge");
+ }
+
+ @Test
+ public void testCustomStyledNodesUseTargetedClasses() {
+ String code =
+ """
+ graph TD
+ A[Custom Fill] --> B[Custom Stroke]
+ B --> C[Custom Both]
+ style A fill:#ff0000
+ style B stroke:#00ff00
+ style C fill:#ff0000,stroke:#00ff00
+ """;
+ SvgDoc doc = render(code);
+
+ List<Element> rects = doc.getElementsByTag("rect");
+ Element nodeA = rects.get(0);
+ assertThat(nodeA.getAttribute("class")).isEqualTo("mermaid-node-stroke");
+ assertThat(nodeA.getAttribute("fill")).isEqualTo("#ff0000");
+
+ Element nodeB = rects.get(1);
+ assertThat(nodeB.getAttribute("class")).isEqualTo("mermaid-node-fill");
+ assertThat(nodeB.getAttribute("stroke")).isEqualTo("#00ff00");
+
+ Element nodeC = rects.get(2);
+ assertThat(nodeC.getAttribute("class")).isEmpty();
+ assertThat(nodeC.getAttribute("fill")).isEqualTo("#ff0000");
+ assertThat(nodeC.getAttribute("stroke")).isEqualTo("#00ff00");
+ }
+
+ @Test
+ public void testCustomStyledNodesPreserveTextContrast() {
+ String code =
+ """
+ graph TD
+ GitRepo[(Git Repositories)]
+ Cache[(In-Memory Cache)]
+ DarkNode[Dark Server]
+ StyledText[Explicit Text Color]
+ MultiLine["Primary Line<br/>Secondary Line"]
+ DefaultNode[Default Box]
+
+ style GitRepo fill:#e1bee7,stroke:#8e24aa
+ style Cache fill:#ffecb3,stroke:#ffa000
+ style DarkNode fill:#1e293b,stroke:#0f172a
+ style StyledText fill:#e1bee7,stroke:#8e24aa,color:#123456
+ style MultiLine fill:#ffecb3
+ """;
+ SvgDoc doc = render(code);
+
+ // Light custom fills (pastel purple, pastel yellow) should keep dark text without
+ // mermaid-node-text class, preventing text inversion to white in dark mode.
+ Element gitRepoText = doc.findText("Git Repositories");
+ assertThat(gitRepoText).isNotNull();
+ assertThat(gitRepoText.getAttribute("fill")).isEqualTo("#0f172a");
+ assertThat(gitRepoText.getAttribute("class")).isEmpty();
+
+ Element cacheText = doc.findText("In-Memory Cache");
+ assertThat(cacheText).isNotNull();
+ assertThat(cacheText.getAttribute("fill")).isEqualTo("#0f172a");
+ assertThat(cacheText.getAttribute("class")).isEmpty();
+
+ // Dark custom fill should use light text without theme class
+ Element darkNodeText = doc.findText("Dark Server");
+ assertThat(darkNodeText).isNotNull();
+ assertThat(darkNodeText.getAttribute("fill")).isEqualTo("#e8eaed");
+ assertThat(darkNodeText.getAttribute("class")).isEmpty();
+
+ // Explicit color directive should be honored
+ Element styledText = doc.findText("Explicit Text Color");
+ assertThat(styledText).isNotNull();
+ assertThat(styledText.getAttribute("fill")).isEqualTo("#123456");
+ assertThat(styledText.getAttribute("class")).isEmpty();
+
+ // Multiline labels on custom fill should use high-contrast text and subtext
+ List<Element> tspans = doc.getElementsByTag("tspan");
+ Element primaryTspan = null;
+ Element secondaryTspan = null;
+ for (Element tspan : tspans) {
+ if ("Primary Line".equals(tspan.getTextContent())) {
+ primaryTspan = tspan;
+ } else if ("Secondary Line".equals(tspan.getTextContent())) {
+ secondaryTspan = tspan;
+ }
+ }
+ assertThat(primaryTspan).isNotNull();
+ assertThat(primaryTspan.getAttribute("fill")).isEqualTo("#0f172a");
+ assertThat(primaryTspan.getAttribute("class")).isEmpty();
+ assertThat(secondaryTspan).isNotNull();
+ assertThat(secondaryTspan.getAttribute("fill")).isEqualTo("#475569");
+ assertThat(secondaryTspan.getAttribute("class")).isEmpty();
+
+ // Default unstyled nodes must keep semantic class to adapt with the theme
+ Element defaultText = doc.findText("Default Box");
+ assertThat(defaultText).isNotNull();
+ assertThat(defaultText.getAttribute("class")).isEqualTo("mermaid-node-text");
+ }
}
diff --git a/resources/com/google/gitiles/static/doc.css b/resources/com/google/gitiles/static/doc.css
index dde3aa7..e421cd2 100644
--- a/resources/com/google/gitiles/static/doc.css
+++ b/resources/com/google/gitiles/static/doc.css
@@ -45,6 +45,19 @@
--doc-aside-bg: #f9f9f9;
--doc-aside-border: #f2f2f2;
--doc-aside-text: #202124;
+ --mermaid-node-bg: #ffffff;
+ --mermaid-node-stroke: #64748b;
+ --mermaid-node-text: #0f172a;
+ --mermaid-node-subtext: #475569;
+ --mermaid-edge: #64748b;
+ --mermaid-edge-label-bg: #ffffff;
+ --mermaid-edge-label-text: #475569;
+ --mermaid-subgraph-bg: #fafafa;
+ --mermaid-subgraph-bg-alt: #f8fafc;
+ --mermaid-subgraph-stroke: #cbd5e1;
+ --mermaid-subgraph-text: #334155;
+ --mermaid-shadow-color: #0f172a;
+ --mermaid-shadow-opacity: 0.06;
}
@media (prefers-color-scheme: dark) {
@@ -76,6 +89,19 @@
--doc-aside-bg: #292a2d;
--doc-aside-border: #3c4043;
--doc-aside-text: #e8eaed;
+ --mermaid-node-bg: #2d3035;
+ --mermaid-node-stroke: #5f6368;
+ --mermaid-node-text: #e8eaed;
+ --mermaid-node-subtext: #9aa0a6;
+ --mermaid-edge: #9aa0a6;
+ --mermaid-edge-label-bg: #202124;
+ --mermaid-edge-label-text: #bdc1c6;
+ --mermaid-subgraph-bg: #25262a;
+ --mermaid-subgraph-bg-alt: #202124;
+ --mermaid-subgraph-stroke: #3c4043;
+ --mermaid-subgraph-text: #bdc1c6;
+ --mermaid-shadow-color: #000000;
+ --mermaid-shadow-opacity: 0.35;
}
}
@@ -107,6 +133,19 @@
--doc-aside-bg: #292a2d;
--doc-aside-border: #3c4043;
--doc-aside-text: #e8eaed;
+ --mermaid-node-bg: #2d3035;
+ --mermaid-node-stroke: #5f6368;
+ --mermaid-node-text: #e8eaed;
+ --mermaid-node-subtext: #9aa0a6;
+ --mermaid-edge: #9aa0a6;
+ --mermaid-edge-label-bg: #202124;
+ --mermaid-edge-label-text: #bdc1c6;
+ --mermaid-subgraph-bg: #25262a;
+ --mermaid-subgraph-bg-alt: #202124;
+ --mermaid-subgraph-stroke: #3c4043;
+ --mermaid-subgraph-text: #bdc1c6;
+ --mermaid-shadow-color: #000000;
+ --mermaid-shadow-opacity: 0.35;
}
.Site-Content--markdown {
@@ -386,3 +425,46 @@
display: inline-block;
vertical-align: middle;
}
+
+.mermaid-svg .mermaid-node,
+.mermaid-svg .mermaid-node-fill {
+ fill: var(--mermaid-node-bg);
+}
+.mermaid-svg .mermaid-node,
+.mermaid-svg .mermaid-node-stroke {
+ stroke: var(--mermaid-node-stroke);
+}
+.mermaid-svg .mermaid-node-text {
+ fill: var(--mermaid-node-text);
+}
+.mermaid-svg .mermaid-node-subtext {
+ fill: var(--mermaid-node-subtext);
+}
+.mermaid-svg .mermaid-edge {
+ stroke: var(--mermaid-edge);
+}
+.mermaid-svg .mermaid-arrow {
+ fill: var(--mermaid-edge);
+}
+.mermaid-svg .mermaid-edge-label-bg {
+ fill: var(--mermaid-edge-label-bg);
+}
+.mermaid-svg .mermaid-edge-label-text {
+ fill: var(--mermaid-edge-label-text);
+}
+.mermaid-svg .mermaid-subgraph,
+.mermaid-svg .mermaid-subgraph-fill {
+ fill: var(--mermaid-subgraph-bg);
+ stroke: var(--mermaid-subgraph-stroke);
+}
+.mermaid-svg .mermaid-subgraph--alt,
+.mermaid-svg .mermaid-subgraph-fill--alt {
+ fill: var(--mermaid-subgraph-bg-alt);
+}
+.mermaid-svg .mermaid-subgraph-title {
+ fill: var(--mermaid-subgraph-text);
+}
+.mermaid-svg .mermaid-shadow {
+ flood-color: var(--mermaid-shadow-color);
+ flood-opacity: var(--mermaid-shadow-opacity);
+}