Skip to content

Commit fe99176

Browse files
Merge pull request #90 from NCMBianchi/fix/issue-81
2 parents de4a908 + 7fcfa22 commit fe99176

41 files changed

Lines changed: 890 additions & 34 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,42 @@ $ mermaid-ascii -f ./test.mermaid
162162
│ │
163163
└─────────┘
164164

165+
# Other edge types
166+
Besides the default `-->` arrow, flowchart edges also accept `-.->` (dotted), `==>` (thick), `---` (open, no arrowhead), `--o` (circle head), and `--x` (cross head):
167+
$ cat test.mermaid
168+
graph TD
169+
A -.-> B
170+
A ==> C
171+
B --- C
172+
B --o|example| D
173+
D --x C
174+
$ mermaid-ascii -f ./test.mermaid
175+
┌──────────┐
176+
│ │
177+
│ A │━━━━━━━┓
178+
│ │ ┃
179+
└─────┬────┘ ┃
180+
┆ ┃
181+
┆ ┃
182+
┆ ┃
183+
┆ ┃
184+
▼ ▼
185+
┌──────────┐ ┌───┐
186+
│ │ │ │
187+
│ B ├─────┤ C │
188+
│ │ │ │
189+
└─────┬────┘ └───┘
190+
│ ✕
191+
│ │
192+
example │
193+
│ │
194+
● │
195+
┌──────────┐ │
196+
│ │ │
197+
│ D ├───────┘
198+
│ │
199+
└──────────┘
200+
165201
# Read from stdin
166202
$ cat test.mermaid | mermaid-ascii
167203
┌───┐ ┌───┐ ┌───┐

cmd/arrow.go

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,28 @@ func (g *graph) drawArrow(from gridCoord, to gridCoord, e *edge) (*drawing, *dra
111111
}
112112
log.Debugf("Drawing arrow from %v to %v with path %v", from, to, e.path)
113113
dLabel := g.drawArrowLabel(e)
114-
dPath, linesDrawn, lineDirs := g.drawPath(e.path)
115-
dBoxStart := g.drawBoxStart(e.path, linesDrawn[0])
116-
dArrowHead := g.drawArrowHead(linesDrawn[len(linesDrawn)-1], lineDirs[len(lineDirs)-1])
117-
if e.isBidirectional && len(linesDrawn) > 0 {
118-
dStartArrowHead := g.drawArrowHead(reverseDrawingLine(linesDrawn[0]), lineDirs[0].getOpposite())
114+
dPath, linesDrawn, lineDirs := g.drawPath(e.path, e.stroke)
115+
dBoxStart := g.drawBoxStart(e.path, linesDrawn[0], e.stroke)
116+
skipHead := e.head == headNone
117+
if skipHead && len(linesDrawn) > 0 {
118+
// headNone (open links like ---) has no arrowhead glyph to mark the
119+
// connection at the destination, unlike every other head; touch up
120+
// the destination box's own border the same way drawBoxStart does
121+
// for the source, so the join is visible there too.
122+
dBoxEnd := g.drawBoxEnd(linesDrawn[len(linesDrawn)-1], lineDirs[len(lineDirs)-1], e.stroke)
123+
dBoxStart = g.mergeDrawings(dBoxStart, drawingCoord{0, 0}, dBoxEnd)
124+
}
125+
var dArrowHead *drawing
126+
if skipHead {
127+
dArrowHead = copyCanvas(g.drawing)
128+
} else {
129+
dArrowHead = g.drawArrowHead(linesDrawn[len(linesDrawn)-1], lineDirs[len(lineDirs)-1], e.head)
130+
}
131+
if e.isBidirectional && !skipHead && len(linesDrawn) > 0 {
132+
dStartArrowHead := g.drawArrowHead(reverseDrawingLine(linesDrawn[0]), lineDirs[0].getOpposite(), e.head)
119133
dArrowHead = g.mergeDrawings(dArrowHead, drawingCoord{0, 0}, dStartArrowHead)
120134
}
121-
dCorners := g.drawCorners(e.path)
135+
dCorners := g.drawCorners(e.path, e.stroke)
122136
return dPath, dBoxStart, dArrowHead, dCorners, dLabel
123137
}
124138

@@ -160,7 +174,7 @@ func mergePath(path []gridCoord) []gridCoord {
160174
return newPath
161175
}
162176

163-
func (g *graph) drawPath(path []gridCoord) (*drawing, [][]drawingCoord, []direction) {
177+
func (g *graph) drawPath(path []gridCoord, stroke edgeStroke) (*drawing, [][]drawingCoord, []direction) {
164178
d := copyCanvas(g.drawing)
165179
previousCoord := path[0]
166180
linesDrawn := make([][]drawingCoord, 0)
@@ -174,7 +188,7 @@ func (g *graph) drawPath(path []gridCoord) (*drawing, [][]drawingCoord, []direct
174188
continue
175189
}
176190
dir := determineDirection(genericCoord(previousCoord), genericCoord(nextCoord))
177-
s := g.drawLine(d, previousDrawingCoord, nextDrawingCoord, 1, -1)
191+
s := g.drawLine(d, previousDrawingCoord, nextDrawingCoord, 1, -1, stroke)
178192
if len(s) == 0 {
179193
// drawLine may return no coords if offsets collapse the line. Use at least one point so arrow and junction logic
180194
// can still infer a direction.
@@ -187,7 +201,7 @@ func (g *graph) drawPath(path []gridCoord) (*drawing, [][]drawingCoord, []direct
187201
return d, linesDrawn, lineDirs
188202
}
189203

190-
func (g *graph) drawBoxStart(path []gridCoord, firstLine []drawingCoord) *drawing {
204+
func (g *graph) drawBoxStart(path []gridCoord, firstLine []drawingCoord, stroke edgeStroke) *drawing {
191205
d := *(copyCanvas(g.drawing))
192206
from := firstLine[0]
193207
dir := determineDirection(genericCoord(path[0]), genericCoord(path[1]))
@@ -197,6 +211,13 @@ func (g *graph) drawBoxStart(path []gridCoord, firstLine []drawingCoord) *drawin
197211
return &d
198212
}
199213

214+
// A heavy line meeting a light box border looks mismatched through a
215+
// light T-junction glyph (e.g. "├"); leave the box's own light border
216+
// character in place instead.
217+
if stroke == strokeThick {
218+
return &d
219+
}
220+
200221
switch dir {
201222
case Up:
202223
d[from.x][from.y+1] = "┴"
@@ -210,7 +231,32 @@ func (g *graph) drawBoxStart(path []gridCoord, firstLine []drawingCoord) *drawin
210231
return &d
211232
}
212233

213-
func (g *graph) drawArrowHead(line []drawingCoord, fallback direction) *drawing {
234+
// drawBoxEnd is drawBoxStart's mirror for the destination end of a path.
235+
func (g *graph) drawBoxEnd(lastLine []drawingCoord, dir direction, stroke edgeStroke) *drawing {
236+
d := *(copyCanvas(g.drawing))
237+
if len(lastLine) == 0 {
238+
return &d
239+
}
240+
to := lastLine[len(lastLine)-1]
241+
242+
if g.useAscii || stroke == strokeThick {
243+
return &d
244+
}
245+
246+
switch dir {
247+
case Up:
248+
d[to.x][to.y-1] = "┬"
249+
case Down:
250+
d[to.x][to.y+1] = "┴"
251+
case Left:
252+
d[to.x-1][to.y] = "├"
253+
case Right:
254+
d[to.x+1][to.y] = "┤"
255+
}
256+
return &d
257+
}
258+
259+
func (g *graph) drawArrowHead(line []drawingCoord, fallback direction, head edgeHead) *drawing {
214260
d := *(copyCanvas(g.drawing))
215261
if len(line) == 0 {
216262
return &d
@@ -222,6 +268,22 @@ func (g *graph) drawArrowHead(line []drawingCoord, fallback direction) *drawing
222268
dir = fallback
223269
}
224270

271+
// Circle and cross heads are a single glyph regardless of travel direction.
272+
if head == headCircle || head == headCross {
273+
char := "o"
274+
if head == headCross {
275+
char = "x"
276+
}
277+
if !g.useAscii {
278+
char = "●"
279+
if head == headCross {
280+
char = "✕"
281+
}
282+
}
283+
d[lastPos.x][lastPos.y] = char
284+
return &d
285+
}
286+
225287
var char string
226288
if !g.useAscii {
227289
switch dir {
@@ -293,8 +355,12 @@ func (g *graph) drawArrowHead(line []drawingCoord, fallback direction) *drawing
293355
return &d
294356
}
295357

296-
func (g *graph) drawCorners(path []gridCoord) *drawing {
358+
func (g *graph) drawCorners(path []gridCoord, stroke edgeStroke) *drawing {
297359
d := copyCanvas(g.drawing)
360+
// Thick edges get the heavy box-drawing corner glyphs to match their
361+
// heavy line/arrowhead; ASCII has no distinct "thick corner" (still
362+
// "+"), and dotted corners are left as plain light corners.
363+
thick := !g.useAscii && stroke == strokeThick
298364
for idx, coord := range path {
299365
// Skip the first and last step
300366
if idx == 0 || idx == len(path)-1 {
@@ -310,12 +376,24 @@ func (g *graph) drawCorners(path []gridCoord) *drawing {
310376
switch {
311377
case (prevDir == Right && nextDir == Down) || (prevDir == Up && nextDir == Left):
312378
corner = "┐"
379+
if thick {
380+
corner = "┓"
381+
}
313382
case (prevDir == Right && nextDir == Up) || (prevDir == Down && nextDir == Left):
314383
corner = "┘"
384+
if thick {
385+
corner = "┛"
386+
}
315387
case (prevDir == Left && nextDir == Down) || (prevDir == Up && nextDir == Right):
316388
corner = "┌"
389+
if thick {
390+
corner = "┏"
391+
}
317392
case (prevDir == Left && nextDir == Up) || (prevDir == Down && nextDir == Right):
318393
corner = "└"
394+
if thick {
395+
corner = "┗"
396+
}
319397
default:
320398
corner = "+"
321399
}
@@ -353,7 +431,7 @@ func insetLine(line []drawingCoord, insetStart, insetEnd int) []drawingCoord {
353431
}
354432
endInset := insetEnd
355433
if insetEnd > 0 {
356-
endInset = insetEnd - 1 // shift label right ~1 char so the start dash survives the centered placement
434+
endInset = insetEnd - 1 // shift label right ~1 char so the start dash survives the centered placement
357435
}
358436
dir := determineDirection(genericCoord(line[0]), genericCoord(line[1]))
359437
a, b := line[0], line[1]

cmd/draw.go

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,32 +66,48 @@ func (d *drawing) drawText(start drawingCoord, text string) {
6666
}
6767
}
6868

69-
func (g *graph) drawLine(d *drawing, from drawingCoord, to drawingCoord, offsetFrom int, offsetTo int) []drawingCoord {
69+
func (g *graph) drawLine(d *drawing, from drawingCoord, to drawingCoord, offsetFrom int, offsetTo int, stroke edgeStroke) []drawingCoord {
7070
// Offset determines how far from the actual coord the line should start/stop.
7171
direction := determineDirection(genericCoord(from), genericCoord(to))
7272
drawnCoords := make([]drawingCoord, 0)
73-
log.Debug("Drawing line from ", from, " to ", to, " direction: ", direction, " offsetFrom: ", offsetFrom, " offsetTo: ", offsetTo)
73+
log.Debug("Drawing line from ", from, " to ", to, " direction: ", direction, " offsetFrom: ", offsetFrom, " offsetTo: ", offsetTo, " stroke: ", stroke)
7474
if !g.useAscii {
75+
// Vertical glyph: solid "│", dotted "┆", thick "┃" - Unicode's own
76+
// dedicated dashed/heavy box-drawing glyphs, so (unlike ASCII) no
77+
// skip-cell pattern is needed for dotted; the glyph is dashed on its
78+
// own. Horizontal glyph: solid "─", dotted "┄", thick "━".
79+
vertical := "│"
80+
if stroke == strokeThick {
81+
vertical = "┃"
82+
} else if stroke == strokeDotted {
83+
vertical = "┆"
84+
}
85+
horizontal := "─"
86+
if stroke == strokeThick {
87+
horizontal = "━"
88+
} else if stroke == strokeDotted {
89+
horizontal = "┄"
90+
}
7591
switch direction {
7692
case Up:
7793
for y := from.y - offsetFrom; y >= to.y-offsetTo; y-- {
7894
drawnCoords = append(drawnCoords, drawingCoord{from.x, y})
79-
(*d)[from.x][y] = "│"
95+
(*d)[from.x][y] = vertical
8096
}
8197
case Down:
8298
for y := from.y + offsetFrom; y <= to.y+offsetTo; y++ {
8399
drawnCoords = append(drawnCoords, drawingCoord{from.x, y})
84-
(*d)[from.x][y] = "│"
100+
(*d)[from.x][y] = vertical
85101
}
86102
case Left:
87103
for x := from.x - offsetFrom; x >= to.x-offsetTo; x-- {
88104
drawnCoords = append(drawnCoords, drawingCoord{x, from.y})
89-
(*d)[x][from.y] = "─"
105+
(*d)[x][from.y] = horizontal
90106
}
91107
case Right:
92108
for x := from.x + offsetFrom; x <= to.x+offsetTo; x++ {
93109
drawnCoords = append(drawnCoords, drawingCoord{x, from.y})
94-
(*d)[x][from.y] = "─"
110+
(*d)[x][from.y] = horizontal
95111
}
96112
case UpperLeft:
97113
for x, y := from.x, from.y-offsetFrom; x >= to.x-offsetTo && y >= to.y-offsetTo; x, y = x-1, y-1 {
@@ -115,26 +131,45 @@ func (g *graph) drawLine(d *drawing, from drawingCoord, to drawingCoord, offsetF
115131
}
116132
}
117133
} else {
134+
// Vertical glyphs: solid "|", dotted ":" (every row - ASCII has no
135+
// dashed-vertical glyph to space out), thick "I" (a plain, dot-less
136+
// stroke; ASCII has no true double-width vertical). Horizontal
137+
// glyph: solid/dotted share "-" (dotted skips every other cell
138+
// below), thick "=".
139+
vertical := "|"
140+
if stroke == strokeThick {
141+
vertical = "I"
142+
} else if stroke == strokeDotted {
143+
vertical = ":"
144+
}
145+
horizontal := "-"
146+
if stroke == strokeThick {
147+
horizontal = "="
148+
}
118149
switch direction {
119150
case Up:
120151
for y := from.y - offsetFrom; y >= to.y-offsetTo; y-- {
121152
drawnCoords = append(drawnCoords, drawingCoord{from.x, y})
122-
(*d)[from.x][y] = "|"
153+
(*d)[from.x][y] = vertical
123154
}
124155
case Down:
125156
for y := from.y + offsetFrom; y <= to.y+offsetTo; y++ {
126157
drawnCoords = append(drawnCoords, drawingCoord{from.x, y})
127-
(*d)[from.x][y] = "|"
158+
(*d)[from.x][y] = vertical
128159
}
129160
case Left:
130-
for x := from.x - offsetFrom; x >= to.x-offsetTo; x-- {
161+
for i, x := 0, from.x-offsetFrom; x >= to.x-offsetTo; i, x = i+1, x-1 {
131162
drawnCoords = append(drawnCoords, drawingCoord{x, from.y})
132-
(*d)[x][from.y] = "-"
163+
if stroke != strokeDotted || i%2 == 0 {
164+
(*d)[x][from.y] = horizontal
165+
}
133166
}
134167
case Right:
135-
for x := from.x + offsetFrom; x <= to.x+offsetTo; x++ {
168+
for i, x := 0, from.x+offsetFrom; x <= to.x+offsetTo; i, x = i+1, x+1 {
136169
drawnCoords = append(drawnCoords, drawingCoord{x, from.y})
137-
(*d)[x][from.y] = "-"
170+
if stroke != strokeDotted || i%2 == 0 {
171+
(*d)[x][from.y] = horizontal
172+
}
138173
}
139174
case UpperLeft:
140175
for x, y := from.x, from.y-offsetFrom; x >= to.x-offsetTo && y >= to.y-offsetTo; x, y = x-1, y-1 {

cmd/graph.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ func mkGraph(data *orderedmap.OrderedMap[string, []textEdge], nodeSpecs map[stri
106106
to: childNode,
107107
text: textEdge.label,
108108
isBidirectional: textEdge.isBidirectional,
109+
stroke: textEdge.stroke,
110+
head: textEdge.head,
109111
}
110112
g.edges = append(g.edges, &e)
111113
}

cmd/mapping_edge.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,36 @@ import (
66
log "github.com/sirupsen/logrus"
77
)
88

9+
// edgeStroke is the line style drawn along an edge's path. Only ASCII
10+
// output (g.useAscii) currently distinguishes them; Unicode output always
11+
// renders a solid line regardless of stroke.
12+
type edgeStroke int
13+
14+
const (
15+
strokeSolid edgeStroke = iota
16+
strokeDotted
17+
strokeThick
18+
)
19+
20+
// edgeHead is the glyph drawn at an edge's arrowhead end. headNone means no
21+
// glyph is drawn at all (an open link like ---). Only ASCII output currently
22+
// distinguishes them; Unicode output always draws a directional arrowhead.
23+
type edgeHead int
24+
25+
const (
26+
headArrow edgeHead = iota
27+
headCircle
28+
headCross
29+
headNone
30+
)
31+
932
type edge struct {
1033
from *node
1134
to *node
1235
text string
1336
isBidirectional bool
37+
stroke edgeStroke
38+
head edgeHead
1439
path []gridCoord
1540
labelLine []gridCoord
1641
startDir direction

0 commit comments

Comments
 (0)