Skip to content

Commit 9338a73

Browse files
cgreenoclaude
andcommitted
feat(graph): tolerant graph/flowchart type detection
The diagram type was matched with an exact string switch on the first line, so any leading/trailing whitespace, a trailing ";", a missing direction, or the reverse directions RL/BT caused an "unsupported graph type" error. Parse the declaration with strings.Fields instead: - ignore surrounding/repeated whitespace and a trailing ";" (mermaid allows "graph TD;") - default to top-down when no direction is given (matches mermaid) - accept TD/TB/BT/LR/RL (case-sensitive, like mermaid); RL/BT are drawn on their axis without the reversal - reject stray tokens after the direction instead of silently dropping them Adds a table-driven test covering these cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 823db56 commit 9338a73

2 files changed

Lines changed: 81 additions & 11 deletions

File tree

cmd/parse.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ type graphNodeSpec struct {
3838
}
3939

4040
type textEdge struct {
41-
parent textNode
42-
child textNode
43-
label string
41+
parent textNode
42+
child textNode
43+
label string
4444
isBidirectional bool
4545
}
4646

@@ -371,14 +371,33 @@ func mermaidFileToMap(mermaid, styleType string) (*graphProperties, error) {
371371
return &properties, errors.New("missing graph definition")
372372
}
373373

374-
// First line should either say "graph TD" or "graph LR"
375-
switch lines[0] {
376-
case "graph LR", "flowchart LR":
377-
properties.graphDirection = "LR"
378-
case "graph TD", "flowchart TD", "graph TB", "flowchart TB":
379-
properties.graphDirection = "TD"
380-
default:
381-
return &properties, fmt.Errorf("unsupported graph type '%s'. Supported types: graph TD, graph TB, graph LR, flowchart TD, flowchart TB, flowchart LR", lines[0])
374+
// The first line declares the diagram: "graph" or "flowchart" followed by an
375+
// optional direction (e.g. "flowchart LR", "graph TD", or a bare "graph").
376+
// strings.Fields collapses any surrounding or repeated whitespace, so
377+
// indented or trailing-padded declarations parse correctly; TrimRight drops a
378+
// trailing separator (mermaid allows "graph TD;").
379+
fields := strings.Fields(strings.TrimRight(lines[0], "; \t\r"))
380+
if len(fields) == 0 || (fields[0] != "graph" && fields[0] != "flowchart") {
381+
return &properties, fmt.Errorf("unsupported graph type '%s'. Supported types: 'graph' or 'flowchart' with an optional direction (TD, TB, BT, LR, RL)", strings.TrimSpace(lines[0]))
382+
}
383+
if len(fields) > 2 {
384+
return &properties, fmt.Errorf("unexpected tokens after graph direction: %q", strings.Join(fields[2:], " "))
385+
}
386+
387+
// Mermaid defaults to top-down when no direction is given. The renderer only
388+
// lays out along the horizontal (LR) or vertical (TD) axis; the reverse
389+
// directions RL and BT are accepted but drawn on their axis without the
390+
// reversal (RL renders left-to-right, BT top-down).
391+
properties.graphDirection = "TD"
392+
if len(fields) == 2 {
393+
switch fields[1] {
394+
case "LR", "RL":
395+
properties.graphDirection = "LR"
396+
case "TD", "TB", "BT":
397+
properties.graphDirection = "TD"
398+
default:
399+
return &properties, fmt.Errorf("unsupported graph direction '%s'. Supported directions: TD, TB, BT, LR, RL", fields[1])
400+
}
382401
}
383402
lines = lines[1:]
384403

cmd/parse_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,54 @@ func TestMermaidFileToMapUsesLatestExplicitLabel(t *testing.T) {
174174
t.Fatal("expected A label to remain explicit")
175175
}
176176
}
177+
178+
// TestGraphTypeDetection verifies that the diagram declaration line is parsed
179+
// tolerantly: surrounding whitespace, a missing direction (defaults to
180+
// top-down), and the reverse directions RL/BT are all accepted.
181+
func TestGraphTypeDetection(t *testing.T) {
182+
tests := []struct {
183+
name string
184+
input string
185+
wantDir string
186+
wantErr bool
187+
}{
188+
{"plain graph TD", "graph TD\nA --> B", "TD", false},
189+
{"flowchart LR", "flowchart LR\nA --> B", "LR", false},
190+
{"leading whitespace", " flowchart LR\n A --> B", "LR", false},
191+
{"trailing whitespace", "graph LR \nA --> B", "LR", false},
192+
{"indented graph TD", " graph TD\n A --> B", "TD", false},
193+
{"bare graph defaults to TD", "graph\nA --> B", "TD", false},
194+
{"bare flowchart defaults to TD", "flowchart\nA --> B", "TD", false},
195+
{"TB maps to TD", "flowchart TB\nA --> B", "TD", false},
196+
{"RL maps to LR axis", "graph RL\nA --> B", "LR", false},
197+
{"BT maps to TD axis", "flowchart BT\nA --> B", "TD", false},
198+
{"trailing semicolon bare", "graph;\nA --> B", "TD", false},
199+
{"trailing semicolon with direction", "graph TD;\nA --> B", "TD", false},
200+
{"flowchart LR semicolon", "flowchart LR;\nA --> B", "LR", false},
201+
{"CRLF line ending", "graph TD\r\nA --> B", "TD", false},
202+
{"tab separator", "graph\tLR\nA --> B", "LR", false},
203+
{"lowercase direction errors", "graph td\nA --> B", "", true},
204+
{"extra tokens error", "graph TD foo\nA --> B", "", true},
205+
{"unknown type errors", "sequenceDiagram\nA->>B: x", "", true},
206+
{"unknown direction errors", "graph SIDEWAYS\nA --> B", "", true},
207+
{"empty input errors", "", "", true},
208+
}
209+
210+
for _, tt := range tests {
211+
t.Run(tt.name, func(t *testing.T) {
212+
props, err := mermaidFileToMap(tt.input, "cli")
213+
if tt.wantErr {
214+
if err == nil {
215+
t.Fatalf("expected error, got direction %q", props.graphDirection)
216+
}
217+
return
218+
}
219+
if err != nil {
220+
t.Fatalf("unexpected error: %v", err)
221+
}
222+
if props.graphDirection != tt.wantDir {
223+
t.Errorf("direction = %q, want %q", props.graphDirection, tt.wantDir)
224+
}
225+
})
226+
}
227+
}

0 commit comments

Comments
 (0)