Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions formatters/svg/svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"path"
"slices"
"strings"
"unicode/utf8"

"github.com/alecthomas/chroma/v3"
)
Expand Down Expand Up @@ -134,12 +135,17 @@ func (f *Formatter) writeSVG(w io.Writer, style *chroma.Style, tokens []chroma.T
return err
}

// tokenWidth is the token's width in character cells, with tabs expanded.
func tokenWidth(token chroma.Token) int {
return utf8.RuneCountInString(strings.ReplaceAll(token.String(), ` `, " "))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat combining marks as zero-width

For decomposed Unicode text such as e\u0301, the combining accent has no horizontal advance in the rendered SVG, but RuneCountInString counts it as another character cell. Consequently, the token background is one ch too wide and every later background on the line is shifted, so the non-ASCII alignment bug remains for normalized NFD input; East Asian wide runes have the converse problem because they commonly occupy two cells but are counted once.

Useful? React with 👍 / 👎.

}

func maxLineWidth(lines [][]chroma.Token) int {
maxWidth := 0
for _, tokens := range lines {
length := 0
for _, token := range tokens {
length += len(strings.ReplaceAll(token.String(), ` `, " "))
length += tokenWidth(token)
}
if length > maxWidth {
maxWidth = length
Expand All @@ -155,7 +161,7 @@ func (f *Formatter) writeTokenBackgrounds(w io.Writer, lines [][]chroma.Token, s
for index, tokens := range lines {
lineLength := 0
for _, token := range tokens {
length := len(strings.ReplaceAll(token.String(), ` `, " "))
length := tokenWidth(token)
tokenBackground := style.Get(token.Type).Background
if tokenBackground.IsSet() && tokenBackground != style.Get(chroma.Background).Background {
if _, err := fmt.Fprintf(w, "<rect id=\"%s\" x=\"%dch\" y=\"%fem\" width=\"%dch\" height=\"1.2em\" fill=\"%s\" />\n", escapeString(token.String()), lineLength, 1.2*float64(index)+0.25, length, style.Get(token.Type).Background.String()); err != nil {
Expand Down
27 changes: 27 additions & 0 deletions formatters/svg/svg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,36 @@ import (
"strings"
"testing"

"github.com/alecthomas/chroma/v3"

assert "github.com/alecthomas/assert/v2"
)

func TestNonASCIIWidths(t *testing.T) {
style, err := chroma.NewStyle("test", chroma.StyleEntries{
chroma.Background: "#ffffff bg:#ffffff",
chroma.LiteralString: "bg:#fff0f0",
})
assert.NoError(t, err)

tokens := []chroma.Token{
{Type: chroma.Text, Value: "x = "},
{Type: chroma.LiteralString, Value: `"éé"`},
{Type: chroma.Text, Value: ", "},
{Type: chroma.LiteralString, Value: `"ab"`},
{Type: chroma.Text, Value: "\n"},
}

w := &strings.Builder{}
assert.NoError(t, New().Format(w, style, chroma.Literator(tokens...)))
out := w.String()

// Widths and offsets are in character cells, so multi-byte runes count once.
assert.Contains(t, out, `<svg width="120px"`)
assert.Contains(t, out, `x="4ch" y="0.250000em" width="4ch"`)
assert.Contains(t, out, `x="10ch" y="0.250000em" width="4ch"`)
}

func TestWriteFontStyle(t *testing.T) {
tests := []struct {
name string
Expand Down