Skip to content

Commit bddc54a

Browse files
ColtWindyclaude
andcommitted
Fix graph diagram CJK/Unicode character rendering
- Use runewidth.StringWidth() for display width calculation instead of len() which returns byte count - Use runewidth.RuneWidth() to properly position wide characters - Fill subsequent positions with empty string for wide CJK characters that occupy 2 terminal columns - Add Korean language test case (korean_nodes.txt) This fixes double-encoding bug where UTF-8 bytes were incorrectly interpreted as Unicode code points, causing garbled output like "íì¤í¸" instead of "테스트". Affected functions: - drawText(): text rendering - drawBox(): node label rendering - drawSubgraphLabel(): subgraph label rendering - setColumnWidth(): box width calculation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent dc0429e commit bddc54a

3 files changed

Lines changed: 73 additions & 12 deletions

File tree

cmd/draw.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66

77
"github.com/gookit/color"
8+
"github.com/mattn/go-runewidth"
89
log "github.com/sirupsen/logrus"
910
)
1011

@@ -48,10 +49,19 @@ func (g *graph) drawEdge(e *edge) (*drawing, *drawing, *drawing, *drawing, *draw
4849

4950
func (d *drawing) drawText(start drawingCoord, text string) {
5051
// Increase dimensions if necessary.
51-
d.increaseSize(start.x+len(text), start.y)
52-
log.Debug("Drawing '", text, "' from ", start, " to ", drawingCoord{x: start.x + len(text), y: start.y})
53-
for x := 0; x < len(text); x++ {
54-
(*d)[x+start.x][start.y] = string(text[x])
52+
// Use display width for proper CJK/Unicode character support
53+
textWidth := runewidth.StringWidth(text)
54+
d.increaseSize(start.x+textWidth, start.y)
55+
log.Debug("Drawing '", text, "' from ", start, " to ", drawingCoord{x: start.x + textWidth, y: start.y})
56+
pos := 0
57+
for _, char := range text {
58+
(*d)[start.x+pos][start.y] = string(char)
59+
charWidth := runewidth.RuneWidth(char)
60+
// Fill subsequent positions with empty string for wide characters
61+
for i := 1; i < charWidth; i++ {
62+
(*d)[start.x+pos+i][start.y] = ""
63+
}
64+
pos += charWidth
5565
}
5666
}
5767

@@ -228,10 +238,19 @@ func drawBox(n *node, g graph) *drawing {
228238
boxDrawing[to.x][to.y] = "+" // Bottom right corner
229239
}
230240
// Draw text
241+
// Use display width for proper CJK/Unicode character support
242+
nameWidth := runewidth.StringWidth(n.name)
231243
textY := from.y + h/2
232-
textX := from.x + w/2 - CeilDiv(len(n.name), 2) + 1
233-
for x := 0; x < len(n.name); x++ {
234-
boxDrawing[textX+x][textY] = wrapTextInColor(string(n.name[x]), n.styleClass.styles["color"], g.styleType)
244+
textX := from.x + w/2 - CeilDiv(nameWidth, 2) + 1
245+
pos := 0
246+
for _, char := range n.name {
247+
boxDrawing[textX+pos][textY] = wrapTextInColor(string(char), n.styleClass.styles["color"], g.styleType)
248+
charWidth := runewidth.RuneWidth(char)
249+
// Fill subsequent positions with empty string for wide characters
250+
for i := 1; i < charWidth; i++ {
251+
boxDrawing[textX+pos+i][textY] = ""
252+
}
253+
pos += charWidth
235254
}
236255

237256
return &boxDrawing
@@ -317,15 +336,26 @@ func drawSubgraphLabel(sg *subgraph, g graph) (*drawing, drawingCoord) {
317336
labelDrawing := *(mkDrawing(width, height))
318337

319338
// Draw label centered at top
339+
// Use display width for proper CJK/Unicode character support
340+
nameWidth := runewidth.StringWidth(sg.name)
320341
labelY := from.y + 1
321-
labelX := from.x + width/2 - len(sg.name)/2
342+
labelX := from.x + width/2 - nameWidth/2
322343
if labelX < from.x+1 {
323344
labelX = from.x + 1
324345
}
325-
for i, char := range sg.name {
326-
if labelX+i < to.x {
327-
labelDrawing[labelX+i][labelY] = string(char)
346+
pos := 0
347+
for _, char := range sg.name {
348+
if labelX+pos < to.x {
349+
labelDrawing[labelX+pos][labelY] = string(char)
350+
charWidth := runewidth.RuneWidth(char)
351+
// Fill subsequent positions with empty string for wide characters
352+
for i := 1; i < charWidth; i++ {
353+
if labelX+pos+i < to.x {
354+
labelDrawing[labelX+pos+i][labelY] = ""
355+
}
356+
}
328357
}
358+
pos += runewidth.RuneWidth(char)
329359
}
330360

331361
// Return label drawing and its offset position

cmd/mapping_node.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"github.com/mattn/go-runewidth"
45
log "github.com/sirupsen/logrus"
56
)
67

@@ -36,7 +37,8 @@ func (g *graph) setColumnWidth(n *node) {
3637
// - 2x padding
3738
// - 2x margin
3839
col1 := 1
39-
col2 := 2*boxBorderPadding + len(n.name)
40+
// Use display width for proper CJK/Unicode character support
41+
col2 := 2*boxBorderPadding + runewidth.StringWidth(n.name)
4042
col3 := 1
4143
colsToBePlaced := []int{col1, col2, col3}
4244
rowsToBePlaced := []int{1, 1 + 2*boxBorderPadding, 1} // Border, padding + line, border
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
graph TD
2+
감각 --> 사고
3+
사고 --> 행동
4+
---
5+
┌──────┐
6+
│ │
7+
│ 감각 │
8+
│ │
9+
└───┬──┘
10+
11+
12+
13+
14+
15+
┌──────┐
16+
│ │
17+
│ 사고 │
18+
│ │
19+
└───┬──┘
20+
21+
22+
23+
24+
25+
┌──────┐
26+
│ │
27+
│ 행동 │
28+
│ │
29+
└──────┘

0 commit comments

Comments
 (0)