From 7c5d2cdd47d61c123d9c82d969f8d87228c12adb Mon Sep 17 00:00:00 2001 From: Djordje Lukic Date: Mon, 20 Jul 2026 23:20:50 +0200 Subject: [PATCH] feat: support Mermaid state diagram directions Signed-off-by: Djordje Lukic --- pkg/mermaid/README.md | 2 +- pkg/mermaid/parser_state.go | 16 ++++++ pkg/mermaid/parser_test.go | 23 +++++++++ pkg/tui/components/mermaid/render.go | 2 +- pkg/tui/components/mermaid/render_test.go | 50 +++++++++++++++++++ .../state bottom top direction.golden | 18 +++++++ .../state left right direction.golden | 3 ++ .../state right left direction.golden | 3 ++ .../state top bottom direction.golden | 18 +++++++ .../testdata/state top down direction.golden | 18 +++++++ 10 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 pkg/tui/components/mermaid/testdata/state bottom top direction.golden create mode 100644 pkg/tui/components/mermaid/testdata/state left right direction.golden create mode 100644 pkg/tui/components/mermaid/testdata/state right left direction.golden create mode 100644 pkg/tui/components/mermaid/testdata/state top bottom direction.golden create mode 100644 pkg/tui/components/mermaid/testdata/state top down direction.golden diff --git a/pkg/mermaid/README.md b/pkg/mermaid/README.md index 7aac24e788..43f3901233 100644 --- a/pkg/mermaid/README.md +++ b/pkg/mermaid/README.md @@ -98,5 +98,5 @@ Mermaid blocks remain syntax-highlighted code blocks. | Concurrent states | ❌ | ❌ | | | State notes | ❌ | ❌ | | | Choice, fork, and join pseudostates | ❌ | ❌ | | -| State direction declarations | ❌ | ❌ | | +| State direction declarations | ✅ | ✅ | `TD`, `TB`, `BT`, `LR`, and `RL` are supported | | Styling and classes | ❌ | ❌ | | diff --git a/pkg/mermaid/parser_state.go b/pkg/mermaid/parser_state.go index 98701ee9d3..321b5c2a8a 100644 --- a/pkg/mermaid/parser_state.go +++ b/pkg/mermaid/parser_state.go @@ -6,6 +6,13 @@ func parseState(doc *Document, statements []string) { pseudoState := 0 for _, statement := range statements { cursor := mermaidCursor{input: statement} + if cursor.consumeKeyword("direction") { + direction, ok := cursor.readIdentifier() + if ok && cursor.eof() && validMermaidDirection(direction) { + doc.Direction = strings.ToUpper(direction) + } + continue + } if cursor.consumeKeyword("state") { label, ok := cursor.readQuoted() if !ok || !cursor.consumeKeyword("as") { @@ -51,6 +58,15 @@ func parseState(doc *Document, statements []string) { } } +func validMermaidDirection(direction string) bool { + switch strings.ToUpper(direction) { + case "TD", "TB", "BT", "LR", "RL": + return true + default: + return false + } +} + func mermaidPseudoStateID(kind string, index int) string { return "__" + kind + "_" + strings.Repeat("_", index) } diff --git a/pkg/mermaid/parser_test.go b/pkg/mermaid/parser_test.go index 9f53935f9b..b2b726e684 100644 --- a/pkg/mermaid/parser_test.go +++ b/pkg/mermaid/parser_test.go @@ -141,6 +141,29 @@ Working --> [*]: complete`) assert.Equal(t, "End", doc.Nodes[doc.Edges[2].To].Label) } +func TestParseStateDiagramDirection(t *testing.T) { + t.Parallel() + + tests := []struct { + direction string + want string + }{ + {direction: "LR", want: "LR"}, + {direction: "rl", want: "RL"}, + {direction: "TB", want: "TB"}, + {direction: "BT", want: "BT"}, + } + for _, tt := range tests { + t.Run(tt.direction, func(t *testing.T) { + t.Parallel() + + doc, err := Parse("stateDiagram-v2\ndirection " + tt.direction + "\n[*] --> Idle\nIdle --> [*]") + require.NoError(t, err) + assert.Equal(t, tt.want, doc.Direction) + }) + } +} + func TestSplitMermaidStatementsHonorsQuotedAndDelimitedSemicolons(t *testing.T) { t.Parallel() diff --git a/pkg/tui/components/mermaid/render.go b/pkg/tui/components/mermaid/render.go index c9b3f54579..d7508ec3f4 100644 --- a/pkg/tui/components/mermaid/render.go +++ b/pkg/tui/components/mermaid/render.go @@ -26,7 +26,7 @@ func Render(source string, width int) (string, bool) { case mermaidparser.DiagramSequence: return drawMermaidSequence(document.Edges, document.SequenceEvents, document.Participants, labels, width), true case mermaidparser.DiagramState: - return drawMermaidGraph(document.Edges, document.NodeOrder, labels, width), true + return drawMermaidFlowchart(document.Edges, document.NodeOrder, labels, document.Direction, width), true default: return "", false } diff --git a/pkg/tui/components/mermaid/render_test.go b/pkg/tui/components/mermaid/render_test.go index 224e0b2595..283fd4f87a 100644 --- a/pkg/tui/components/mermaid/render_test.go +++ b/pkg/tui/components/mermaid/render_test.go @@ -243,6 +243,56 @@ Note over A,B: Waiting for work Note right of B: After`, width: 60, }, + { + name: "state top down direction", + source: `stateDiagram-v2 +state "Waiting for work" as Waiting +direction TD +[*] --> Waiting +Waiting --> Running: start +Running --> [*]: finish`, + width: 80, + }, + { + name: "state top bottom direction", + source: `stateDiagram-v2 +state "Waiting for work" as Waiting +direction TB +[*] --> Waiting +Waiting --> Running: start +Running --> [*]: finish`, + width: 80, + }, + { + name: "state bottom top direction", + source: `stateDiagram-v2 +state "Waiting for work" as Waiting +direction BT +[*] --> Waiting +Waiting --> Running: start +Running --> [*]: finish`, + width: 80, + }, + { + name: "state left right direction", + source: `stateDiagram-v2 +state "Waiting for work" as Waiting +direction LR +[*] --> Waiting +Waiting --> Running: start +Running --> [*]: finish`, + width: 80, + }, + { + name: "state right left direction", + source: `stateDiagram-v2 +state "Waiting for work" as Waiting +direction RL +[*] --> Waiting +Waiting --> Running: start +Running --> [*]: finish`, + width: 80, + }, { name: "state lifecycle and cycle", source: `stateDiagram-v2 diff --git a/pkg/tui/components/mermaid/testdata/state bottom top direction.golden b/pkg/tui/components/mermaid/testdata/state bottom top direction.golden new file mode 100644 index 0000000000..ea8a8b016b --- /dev/null +++ b/pkg/tui/components/mermaid/testdata/state bottom top direction.golden @@ -0,0 +1,18 @@ + ╭──────╮ + │ End │ + ╰──────╯ + ▲ + │ finish + ╭────┴────╮ + │ Running │ + ╰─────────╯ + ▲ + │ start + ╭─────────┴────────╮ + │ Waiting for work │ + ╰──────────────────╯ + ▲ + │ + ╭───┴───╮ + │ Start │ + ╰───────╯ \ No newline at end of file diff --git a/pkg/tui/components/mermaid/testdata/state left right direction.golden b/pkg/tui/components/mermaid/testdata/state left right direction.golden new file mode 100644 index 0000000000..9db9d6262e --- /dev/null +++ b/pkg/tui/components/mermaid/testdata/state left right direction.golden @@ -0,0 +1,3 @@ + ╭───────╮ ╭──────────────────╮ ╭─────────╮ ╭──────╮ + │ Start ├────▶│ Waiting for work ├──start──▶│ Running ├──finish──▶│ End │ + ╰───────╯ ╰──────────────────╯ ╰─────────╯ ╰──────╯ \ No newline at end of file diff --git a/pkg/tui/components/mermaid/testdata/state right left direction.golden b/pkg/tui/components/mermaid/testdata/state right left direction.golden new file mode 100644 index 0000000000..e21ce667b2 --- /dev/null +++ b/pkg/tui/components/mermaid/testdata/state right left direction.golden @@ -0,0 +1,3 @@ + ╭──────╮ ╭─────────╮ ╭──────────────────╮ ╭───────╮ + │ End │◀──finish──┤ Running │◀──start──┤ Waiting for work │◀────┤ Start │ + ╰──────╯ ╰─────────╯ ╰──────────────────╯ ╰───────╯ \ No newline at end of file diff --git a/pkg/tui/components/mermaid/testdata/state top bottom direction.golden b/pkg/tui/components/mermaid/testdata/state top bottom direction.golden new file mode 100644 index 0000000000..213b4c2e07 --- /dev/null +++ b/pkg/tui/components/mermaid/testdata/state top bottom direction.golden @@ -0,0 +1,18 @@ + ╭───────╮ + │ Start │ + ╰───┬───╯ + │ + ▼ + ╭──────────────────╮ + │ Waiting for work │ + ╰─────────┬────────╯ + │ start + ▼ + ╭─────────╮ + │ Running │ + ╰────┬────╯ + │ finish + ▼ + ╭──────╮ + │ End │ + ╰──────╯ \ No newline at end of file diff --git a/pkg/tui/components/mermaid/testdata/state top down direction.golden b/pkg/tui/components/mermaid/testdata/state top down direction.golden new file mode 100644 index 0000000000..213b4c2e07 --- /dev/null +++ b/pkg/tui/components/mermaid/testdata/state top down direction.golden @@ -0,0 +1,18 @@ + ╭───────╮ + │ Start │ + ╰───┬───╯ + │ + ▼ + ╭──────────────────╮ + │ Waiting for work │ + ╰─────────┬────────╯ + │ start + ▼ + ╭─────────╮ + │ Running │ + ╰────┬────╯ + │ finish + ▼ + ╭──────╮ + │ End │ + ╰──────╯ \ No newline at end of file