Skip to content

Commit ef12989

Browse files
igor-chernikovjunie-agent
andcommitted
Fix multibyte UTF-8 (Cyrillic) rendering in drawText
Make drawText rune-aware: size using runewidth.StringWidth and iterate over runes instead of bytes so multibyte characters render in a single cell instead of being split into invalid byte fragments. Add a regression test for Cyrillic node and edge labels. Co-authored-by: Junie <junie@jetbrains.com>
1 parent fba8b40 commit ef12989

2 files changed

Lines changed: 37 additions & 5 deletions

File tree

cmd/draw.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,21 @@ func (g *graph) drawEdge(e *edge) (*drawing, *drawing, *drawing, *drawing, *draw
4848
}
4949

5050
func (d *drawing) drawText(start drawingCoord, text string) {
51-
// Increase dimensions if necessary.
52-
d.increaseSize(start.x+len(text), start.y)
53-
log.Debug("Drawing '", text, "' from ", start, " to ", drawingCoord{x: start.x + len(text), y: start.y})
54-
for x := 0; x < len(text); x++ {
55-
(*d)[x+start.x][start.y] = string(text[x])
51+
// Increase dimensions if necessary. Use the visual width so multibyte
52+
// (e.g. Cyrillic, CJK) runes reserve the correct number of cells.
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+
// Iterate over runes (not bytes) so multibyte characters are placed in a
57+
// single cell instead of being split into invalid byte fragments.
58+
textX := start.x
59+
for _, r := range text {
60+
runeWidth := Max(runewidth.RuneWidth(r), 1)
61+
(*d)[textX][start.y] = string(r)
62+
for offset := 1; offset < runeWidth; offset++ {
63+
(*d)[textX+offset][start.y] = ""
64+
}
65+
textX += runeWidth
5666
}
5767
}
5868

cmd/render_graph_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ func TestRenderGraphSeparatesBidirectionalEdgeLabelsTD(t *testing.T) {
146146
}
147147
}
148148

149+
func TestRenderGraphRendersCyrillicNodeAndEdgeLabels(t *testing.T) {
150+
config := diagram.NewTestConfig(true, "cli")
151+
output, err := RenderDiagram("graph LR\nA[Привет]-->|метка|B[Мир]", config)
152+
if err != nil {
153+
t.Fatalf("RenderDiagram() error = %v", err)
154+
}
155+
156+
for _, want := range []string{"Привет", "метка", "Мир"} {
157+
if !strings.Contains(output, want) {
158+
t.Fatalf("expected output to contain Cyrillic label %q\noutput:\n%s", want, output)
159+
}
160+
}
161+
162+
// The replacement character indicates a multibyte rune was split into
163+
// invalid byte fragments during rendering.
164+
if strings.ContainsRune(output, '\uFFFD') {
165+
t.Fatalf("expected no replacement characters in Cyrillic output\noutput:\n%s", output)
166+
}
167+
168+
assertUniformDisplayWidth(t, output)
169+
}
170+
149171
func assertUniformDisplayWidth(t *testing.T, output string) {
150172
t.Helper()
151173

0 commit comments

Comments
 (0)