Skip to content
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e1eeb1a
docs: add note syntax design and implementation plan
unstableneutron Jan 19, 2026
54fe750
feat(sequence): add Note data types and DiagramElement interface
unstableneutron Jan 19, 2026
45dc3fb
feat(sequence): add noteRegex and parseNote method
unstableneutron Jan 19, 2026
2e619af
feat(sequence): integrate note parsing into main loop
unstableneutron Jan 19, 2026
c4b7350
test(sequence): add parser tests for note syntax
unstableneutron Jan 19, 2026
5eb6a14
refactor(sequence): update render loop to iterate Elements
unstableneutron Jan 19, 2026
4cca5ce
feat(sequence): implement renderNoteOver
unstableneutron Jan 19, 2026
b1d8d19
fix(sequence): address code review issues in note rendering
unstableneutron Jan 19, 2026
d89e06e
docs: add block syntax design and implementation plan
unstableneutron Jan 19, 2026
4758b75
feat(sequence): add Block data types
unstableneutron Jan 19, 2026
d646f03
feat(sequence): add block regexes
unstableneutron Jan 19, 2026
934cde7
feat(sequence): implement parseBlock method
unstableneutron Jan 19, 2026
b5fa3ea
feat(sequence): integrate block parsing into main loop
unstableneutron Jan 19, 2026
0ca51ac
test(sequence): add parser tests for block syntax
unstableneutron Jan 19, 2026
44b01ea
feat(sequence): add renderBlock stub and helpers
unstableneutron Jan 19, 2026
50c1f58
feat(sequence): implement renderBlock
unstableneutron Jan 19, 2026
a5ceabb
test(sequence): add renderer tests for multi-section and nested blocks
unstableneutron Jan 19, 2026
0f4bec9
fix(sequence): address code review issues in block rendering
unstableneutron Jan 19, 2026
6cf06f7
feat(sequence): add distinct styling for each block type
unstableneutron Jan 19, 2026
be43709
refactor(sequence): address code review issues
unstableneutron Jan 19, 2026
6b16d71
Fix graph label aliases
unstableneutron Jan 20, 2026
68345b2
Add width fit config
unstableneutron Jan 20, 2026
27ac093
Use config graph direction
unstableneutron Jan 20, 2026
dc6f115
Feat(graph): wrap long node labels
unstableneutron Jan 20, 2026
a09dadc
Add edge label policies
unstableneutron Jan 20, 2026
764b792
Fit graphs to max width
unstableneutron Jan 20, 2026
a7a87f9
Fit sequences to max width
unstableneutron Jan 20, 2026
a2fb251
Add render options for width
unstableneutron Jan 20, 2026
0e82a48
feat: Add multi-line label and width control features
warnes Jan 24, 2026
d158448
feat: Add multi-line labels and width control with auto-fitting
warnes Jan 24, 2026
9c66cf2
fix: UTF-8 character handling in text rendering and wrapping
warnes Jan 24, 2026
f00e25d
feat: Add display width handling for CJK and emoji characters
warnes Jan 24, 2026
388ae79
feat: Add multi-line label control and enhanced UTF-8 support
warnes Jan 25, 2026
1161d5d
chore: rename vscode-config → agent-config in docs/workspace config
warnes Jun 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
608 changes: 608 additions & 0 deletions .github/copilot-instructions.md

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this AI artifact

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ mermaid-ascii
*.tar.gz
*.tar.zst
result
mermaid-ascii-fork
mermaid-ascii-go
mermaid-ascii-patched

# Backup files
*.bak
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MIT License

Copyright (c) 2023 Alexander Grooff
Copyright (c) 2026 Gregory R. Warnes

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this


Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
88 changes: 79 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Mermaid ASCII

Render mermaid diagrams in your terminal:
Render mermaid diagrams in your terminal.

## Features

This fork adds:
- **Multi-line node labels**: Use `<br/>` or `<br>` HTML tags in node labels to create multi-line text
- **Diagram width control**: Use `-w/--maxWidth` to constrain diagram width, with automatic layout fitting
- **Label alignment control**: Use `--center-multi-line-labels` to control how multi-line labels are centered (default: left-justified block)
- **Version flag**: Use `--version` to show version information
- **Enhanced UTF-8 support**: Proper handling of wide characters (CJK, emoji) in node labels

## Installation

Expand Down Expand Up @@ -127,6 +136,60 @@ $ mermaid-ascii -f ./test.mermaid
│ │
└───┘

# Multi-line node labels (using <br/> or <br> tags)
$ cat test.mermaid
graph LR
A["First<br/>Second"] --> B["Line 1<br>Line 2<br>Line 3"]
$ mermaid-ascii -f ./test.mermaid
┌────────┐ ┌────────┐
│ First │ │ Line 1 │
│ Second ├────►│ Line 2 │
│ │ │ Line 3 │
└────────┘ └────────┘

# Multi-line labels with tree-like structure (default: left-justified)
$ cat test.mermaid
graph LR
A["┌─ TIMER<br/>├─> Step 1<br/>└─> Step 2"]
$ mermaid-ascii -f ./test.mermaid
┌────────────┐
│ ┌─ TIMER │
│ ├─> Step 1 │
│ └─> Step 2 │
└────────────┘

# Center each line individually with --center-multi-line-labels
$ cat test.mermaid
graph LR
A["┌─ TIMER<br/>├─> Step 1<br/>└─> Step 2"]
$ mermaid-ascii -f ./test.mermaid --center-multi-line-labels
┌────────────┐
│ ┌─ TIMER │
│ ├─> Step 1 │
│ └─> Step 2 │
└────────────┘

# Control diagram width
$ cat test.mermaid
graph LR
A --> B --> C --> D --> E
$ mermaid-ascii -f ./test.mermaid -w 50
┌───┐ ┌───┐ ┌───┐
│ │ │ │ │ │
│ A ├────►│ B ├────►│ C │
│ │ │ │ │ │
└─┬─┘ └───┘ └───┘
┌───┐ ┌───┐
│ │ │ │
│ D ├────►│ E │
│ │ │ │
└───┘ └───┘

# Top-down layout
$ cat test.mermaid
graph TD
Expand Down Expand Up @@ -355,13 +418,18 @@ Available Commands:
web HTTP server for rendering mermaid diagrams.

Flags:
-p, --borderPadding int Padding between text and border (default 1)
-c, --coords Show coordinates
-f, --file string Mermaid file to parse
-h, --help help for mermaid-ascii
-x, --paddingX int Horizontal space between nodes (default 5)
-y, --paddingY int Vertical space between nodes (default 5)
-v, --verbose Verbose output
-a, --ascii Don't use extended character set
-p, --borderPadding int Padding between text and border (default 1)
--center-multi-line-labels Center multi-line node labels as a block
-c, --coords Show coordinates
-f, --file string Mermaid file to parse (use '-' for stdin)
--fit Force automatic fitting even without width constraint
-h, --help help for mermaid-ascii
-w, --maxWidth int Maximum diagram width in characters (0 = unlimited)
-x, --paddingX int Horizontal space between nodes (default 5)
-y, --paddingY int Vertical space between nodes (default 5)
-v, --verbose Verbose output
--version Show version information

Use "mermaid-ascii [command] --help" for more information about a command.

Expand Down Expand Up @@ -509,10 +577,12 @@ Note that with `--coords` enabled, the grid-coords shown show the starting locat
### Graphs / Flowcharts ✅
- [x] Graph directions (`graph LR` and `graph TD`)
- [x] Labelled edges (like `A -->|label| B`)
- [x] Multi-line node labels (using `<br/>` or `<br>` tags)
- [x] Multiple arrows on one line (like `A --> B --> C`)
- [x] `A & B` syntax
- [x] `classDef` and `class` for colored output
- [x] Prevent arrows overlapping nodes
- [x] Control diagram width (via `-w/--maxWidth` flag)
- [ ] `subgraph` support
- [ ] Shapes other than rectangles
- [ ] Diagonal arrows
Expand Down Expand Up @@ -547,9 +617,9 @@ The baseline components for Mermaid work, but there are a lot of things that are
### Rendering

- [x] Prevent arrows overlapping nodes
- [x] Control maximum diagram width (via `-w/--maxWidth` flag)
- [ ] Diagonal arrows
- [ ] Place nodes in a more compact way
- [ ] Prevent rendering more than X characters wide (like default 80 for terminal width)

### Sequence Diagram Improvements

Expand Down
14 changes: 14 additions & 0 deletions cmd/diagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,20 @@ func (gd *GraphDiagram) Render(config *diagram.Config) (string, error) {
}
gd.properties.styleType = styleType
gd.properties.useAscii = config.UseAscii
if config.GraphDirection != "" {
gd.properties.graphDirection = config.GraphDirection
}
gd.properties.paddingX = config.PaddingBetweenX
gd.properties.paddingY = config.PaddingBetweenY
gd.properties.boxBorderPadding = config.BoxBorderPadding
gd.properties.labelWrapWidth = config.LabelWrapWidth
gd.properties.edgeLabelPolicy = config.EdgeLabelPolicy
gd.properties.edgeLabelMaxWidth = config.EdgeLabelMaxWidth
gd.properties.centerMultiLineLabels = config.CenterMultiLineLabels

if config.FitPolicy == diagram.FitPolicyAuto && config.MaxWidth > 0 {
return fitGraphToWidth(gd.properties, config), nil
}

return drawMap(gd.properties), nil
}
Expand Down
22 changes: 11 additions & 11 deletions cmd/direction.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,23 @@ func (c drawingCoord) Direction(dir direction) drawingCoord {
return drawingCoord{x: c.x + dir.x, y: c.y + dir.y}
}

func selfReferenceDirection(e *edge) (direction, direction, direction, direction) {
if graphDirection == "LR" {
func (g *graph) selfReferenceDirection(e *edge) (direction, direction, direction, direction) {
if g.graphDirection == "LR" {
return Right, Down, Down, Right
}
return Down, Right, Right, Down
}

func determineStartAndEndDir(e *edge) (direction, direction, direction, direction) {
func (g *graph) determineStartAndEndDir(e *edge) (direction, direction, direction, direction) {
if e.from == e.to {
return selfReferenceDirection(e)
return g.selfReferenceDirection(e)
}
d := determineDirection(genericCoord(*e.from.gridCoord), genericCoord(*e.to.gridCoord))
var preferredDir, preferredOppositeDir, alternativeDir, alternativeOppositeDir direction

// Check if this is a backwards flowing edge
isBackwards := false
if graphDirection == "LR" {
if g.graphDirection == "LR" {
// In LR mode, backwards flow is when edge goes from right to left (Left direction)
isBackwards = (d == Left || d == UpperLeft || d == LowerLeft)
} else { // TD mode
Expand All @@ -75,7 +75,7 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
// For backwards edges, use special start positions: Down in LR mode, Right in TD mode
switch d {
case LowerRight:
if graphDirection == "LR" {
if g.graphDirection == "LR" {
preferredDir = Down
preferredOppositeDir = Left
alternativeDir = Right
Expand All @@ -87,7 +87,7 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
alternativeOppositeDir = Left
}
case UpperRight:
if graphDirection == "LR" {
if g.graphDirection == "LR" {
preferredDir = Up
preferredOppositeDir = Left
alternativeDir = Right
Expand All @@ -99,7 +99,7 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
alternativeOppositeDir = Left
}
case LowerLeft:
if graphDirection == "LR" {
if g.graphDirection == "LR" {
// Backwards flow in LR mode - start from Down, arrive at Down
preferredDir = Down
preferredOppositeDir = Down // Edge goes to bottom of destination
Expand All @@ -112,7 +112,7 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
alternativeOppositeDir = Right
}
case UpperLeft:
if graphDirection == "LR" {
if g.graphDirection == "LR" {
// Backwards flow in LR mode - start from Down, arrive at Down
preferredDir = Down
preferredOppositeDir = Down // Edge goes to bottom of destination
Expand All @@ -128,13 +128,13 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
default:
// Handle direct backwards flow cases
if isBackwards {
if graphDirection == "LR" && d == Left {
if g.graphDirection == "LR" && d == Left {
// Direct left flow in LR mode - start from Down, arrive at Down
preferredDir = Down
preferredOppositeDir = Down // Edge goes to bottom of destination
alternativeDir = Left
alternativeOppositeDir = Right
} else if graphDirection == "TD" && d == Up {
} else if g.graphDirection == "TD" && d == Up {
// Direct up flow in TD mode - start from Right, arrive at Right
preferredDir = Right
preferredOppositeDir = Right // Edge goes to right of destination
Expand Down
87 changes: 83 additions & 4 deletions cmd/draw.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/gookit/color"
"github.com/mattn/go-runewidth"
log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -155,7 +156,14 @@ func drawMap(properties *graphProperties) string {
g.setStyleClasses(properties)
g.paddingX = properties.paddingX
g.paddingY = properties.paddingY
g.graphDirection = properties.graphDirection
g.boxBorderPadding = properties.boxBorderPadding
g.labelWrapWidth = properties.labelWrapWidth
g.edgeLabelPolicy = properties.edgeLabelPolicy
g.edgeLabelMaxWidth = properties.edgeLabelMaxWidth
g.useAscii = properties.useAscii
g.centerMultiLineLabels = properties.centerMultiLineLabels
g.setLabelLines()
g.setSubgraphs(properties.subgraphs)
g.createMapping()
d := g.draw()
Expand Down Expand Up @@ -228,10 +236,81 @@ func drawBox(n *node, g graph) *drawing {
boxDrawing[to.x][to.y] = "+" // Bottom right corner
}
// Draw text
textY := from.y + h/2
textX := from.x + w/2 - CeilDiv(len(n.name), 2) + 1
for x := 0; x < len(n.name); x++ {
boxDrawing[textX+x][textY] = wrapTextInColor(string(n.name[x]), n.styleClass.styles["color"], g.styleType)
labelLines := n.labelLines
if len(labelLines) == 0 {
labelLines = []string{n.name}
}
innerLeft := from.x + 1
innerRight := to.x - 1
innerTop := from.y + 1
innerBottom := to.y - 1
innerWidth := innerRight - innerLeft + 1
innerHeight := innerBottom - innerTop + 1
startY := innerTop
if innerHeight > len(labelLines) {
startY = innerTop + (innerHeight-len(labelLines))/2
}
maxLines := Min(len(labelLines), innerHeight)
isMultiLine := len(labelLines) > 1

// When centerMultiLineLabels is false and we have multiple lines,
// pad all lines to the same width before centering as a block
linesToDraw := labelLines
if isMultiLine && !g.centerMultiLineLabels {
// Find max line width accounting for character widths
maxLineWidth := 0
for _, line := range labelLines[:maxLines] {
lineWidth := runewidth.StringWidth(line)
if lineWidth > maxLineWidth {
maxLineWidth = lineWidth
}
}
// Pad each line to max width with trailing spaces
linesToDraw = make([]string, len(labelLines))
for i, line := range labelLines {
lineWidth := runewidth.StringWidth(line)
if lineWidth < maxLineWidth {
linesToDraw[i] = line + strings.Repeat(" ", maxLineWidth-lineWidth)
} else {
linesToDraw[i] = line
}
}
}

for lineIdx := 0; lineIdx < maxLines; lineIdx++ {
line := linesToDraw[lineIdx]
runes := []rune(line)
// Use display width (accounts for CJK full-width chars, emoji, etc.)
lineWidth := runewidth.StringWidth(line)

startX := innerLeft
// Center single-line labels, or multi-line when centerMultiLineLabels is true
if (!isMultiLine || g.centerMultiLineLabels) && innerWidth > lineWidth {
startX = innerLeft + (innerWidth-lineWidth)/2
} else if isMultiLine && !g.centerMultiLineLabels && innerWidth > lineWidth {
// Block centering: center the entire padded block
startX = innerLeft + (innerWidth-lineWidth)/2
}

// Place characters at display positions - wide chars (CJK, emoji) occupy 2 display columns
displayPos := 0
for _, r := range runes {
charWidth := runewidth.RuneWidth(r)
// Check if character will fit in remaining display width
if displayPos+charWidth > innerWidth {
break
}
if startX+displayPos > innerRight {
break
}
// Place character at its display position
boxDrawing[startX+displayPos][startY+lineIdx] = wrapTextInColor(string(r), n.styleClass.styles["color"], g.styleType)
// For wide characters, clear the next cell (the character spans it visually)
if charWidth > 1 && startX+displayPos+1 <= innerRight {
boxDrawing[startX+displayPos+1][startY+lineIdx] = ""
}
displayPos += charWidth
}
}

return &boxDrawing
Expand Down
Loading