Skip to content

Commit 3bbb0f5

Browse files
committed
Merge branch 'feature/graph-frontends' into experimental/terminal-renderer
2 parents ac2bbf9 + a1a9d49 commit 3bbb0f5

3 files changed

Lines changed: 84 additions & 7 deletions

File tree

Sources/MermaidLayout/DippinParser.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -507,10 +507,15 @@ private final class DippinParserImpl {
507507
return t
508508
}
509509

510-
/// Tidies a condition for display: strips quotes around string operands so
511-
/// `ctx.outcome == "success"` reads `ctx.outcome == success`.
510+
/// A concise edge label from a `when` condition. A single equality
511+
/// (`ctx.field == "value"`) collapses to just the value — the branch
512+
/// outcome, which reads like a decision edge (success / fail / reject) and
513+
/// keeps the diagram legible. Anything else (inequality, compound
514+
/// `and`/`or`) falls back to the quote-stripped condition.
512515
private func cleanCondition(_ s: String) -> String {
513-
quotedTokens(s).map(dequote).joined(separator: " ")
516+
let tokens = quotedTokens(s).map(dequote)
517+
if tokens.count == 3, tokens[1] == "==", !tokens[2].isEmpty { return tokens[2] }
518+
return tokens.joined(separator: " ")
514519
}
515520

516521
/// The last path component of a subgraph ref (`a/b/quality_loop.dip` →

Tests/MermaidLayoutTests/DippinParserTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ final class DippinParserTests: XCTestCase {
174174

175175
func testWhenConditionBecomesEdgeLabel() {
176176
let chart = parse(rich)
177-
XCTAssertEqual(edge(chart, "Severe", "ReviewFan")?.label, "ctx.severity == high")
178-
XCTAssertEqual(edge(chart, "Severe", "Remediate")?.label, "ctx.severity == low")
179-
XCTAssertEqual(edge(chart, "Remediate", "Postmortem")?.label, "ctx.outcome == success")
177+
XCTAssertEqual(edge(chart, "Severe", "ReviewFan")?.label, "high")
178+
XCTAssertEqual(edge(chart, "Severe", "Remediate")?.label, "low")
179+
XCTAssertEqual(edge(chart, "Remediate", "Postmortem")?.label, "success")
180180
// A plain edge has no label.
181181
XCTAssertNil(edge(chart, "Intake", "Classify")?.label)
182182
}
@@ -199,7 +199,7 @@ final class DippinParserTests: XCTestCase {
199199
// forming the loop; its `when` condition still labels it.
200200
let back = edge(chart, "Remediate", "Classify")
201201
XCTAssertNotNil(back)
202-
XCTAssertEqual(back?.label, "ctx.outcome == fail")
202+
XCTAssertEqual(back?.label, "fail")
203203
}
204204

205205
func testBareRestartIsLabelledRestart() {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Regression coverage for the "never trap on a parsed diagram" contract as it
2+
// applies to the non-Mermaid front-ends (Dippin, DOT). A parsed `Flowchart`
3+
// wrapped in a `MermaidDiagram` must render to `pngData` for both themes without
4+
// trapping — no force-unwrap on a back-edge, an `exit:`-only node, or a shape
5+
// with no measured size may reach the layout/scene/draw path.
6+
#if canImport(AppKit) || canImport(UIKit) || canImport(SilicaCairo)
7+
import XCTest
8+
@testable import MermaidRender
9+
@testable import MermaidLayout
10+
11+
final class GraphFrontendRenderTests: XCTestCase {
12+
13+
/// The Dippin workflow that surfaced the report: conditional back-edges
14+
/// (TestsPass -> Draft, Approve -> Draft) plus a node reached only through a
15+
/// loop-back. Must parse and render (non-nil PNG) in light and dark.
16+
func testDippinWorkflowRendersBothThemes() throws {
17+
let src = """
18+
workflow Review
19+
goal: "draft, test, ship"
20+
start: Draft
21+
exit: Ship
22+
agent Draft
23+
model: claude-opus-4-6
24+
tool Test
25+
conditional TestsPass
26+
label: "Tests pass?"
27+
human Approve
28+
agent Ship
29+
edges
30+
Draft -> Test
31+
Test -> TestsPass
32+
TestsPass -> Approve when ctx.outcome == "success"
33+
TestsPass -> Draft when ctx.outcome == "fail"
34+
Approve -> Ship when ctx.choice == "approve"
35+
Approve -> Draft when ctx.choice == "reject"
36+
"""
37+
let chart = try XCTUnwrap(DippinParser.parse(src), "Dippin source failed to parse")
38+
let diagram = MermaidDiagram.flowchart(chart)
39+
for prefersDark in [false, true] {
40+
let theme = DiagramTheme(prefersDark: prefersDark)
41+
let png = try XCTUnwrap(
42+
MermaidRenderer.pngData(diagram: diagram, theme: theme),
43+
"Dippin diagram rendered nil (prefersDark=\(prefersDark))")
44+
XCTAssertEqual(Array(png.prefix(4)), [0x89, 0x50, 0x4E, 0x47],
45+
"not a PNG (prefersDark=\(prefersDark))")
46+
}
47+
}
48+
49+
/// A DOT source through the same already-parsed render path — the other
50+
/// graph front-end — must likewise render both themes without trapping.
51+
func testDOTGraphRendersBothThemes() throws {
52+
let src = """
53+
digraph G {
54+
Draft -> Test;
55+
Test -> TestsPass;
56+
TestsPass -> Draft;
57+
TestsPass -> Ship;
58+
}
59+
"""
60+
let chart = try XCTUnwrap(DOTParser.parse(src), "DOT source failed to parse")
61+
let diagram = MermaidDiagram.flowchart(chart)
62+
for prefersDark in [false, true] {
63+
let theme = DiagramTheme(prefersDark: prefersDark)
64+
let png = try XCTUnwrap(
65+
MermaidRenderer.pngData(diagram: diagram, theme: theme),
66+
"DOT diagram rendered nil (prefersDark=\(prefersDark))")
67+
XCTAssertEqual(Array(png.prefix(4)), [0x89, 0x50, 0x4E, 0x47],
68+
"not a PNG (prefersDark=\(prefersDark))")
69+
}
70+
}
71+
}
72+
#endif

0 commit comments

Comments
 (0)