Skip to content

fix(svg): non-ASCII text misaligns token background rects - #1346

Open
VXNCXNX wants to merge 1 commit into
alecthomas:masterfrom
VXNCXNX:fix/svg-rune-width
Open

fix(svg): non-ASCII text misaligns token background rects#1346
VXNCXNX wants to merge 1 commit into
alecthomas:masterfrom
VXNCXNX:fix/svg-rune-width

Conversation

@VXNCXNX

@VXNCXNX VXNCXNX commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

The SVG formatter measures line widths in bytes rather than characters, so any non-ASCII rune inflates the canvas and pushes token background rects out of alignment with the text they sit behind.

Tokenising x = ["éé", "ab"] with a style that sets a string background:

before: <svg width="136px" ...>
        <rect id="&quot;éé&quot;" x="4ch"  ... width="6ch" />
        <rect id="&quot;ab&quot;" x="12ch" ... width="4ch" />

after:  <svg width="120px" ...>
        <rect id="&quot;éé&quot;" x="4ch"  ... width="4ch" />
        <rect id="&quot;ab&quot;" x="10ch" ... width="4ch" />

"éé" is 4 characters and 6 bytes. The old code gives its rect a width of 6 cells, then starts the next rect 2 cells too far right, so the "ab" highlight is visibly offset from "ab". Every token after the first non-ASCII one on a line inherits that drift, and the <svg width> is overstated by the same amount.

x and width are emitted in ch units, which are character cells in a monospace font, so the count has to be in runes.

The fix

maxLineWidth and writeTokenBackgrounds computed the same expression with len(). Extracted it as tokenWidth using utf8.RuneCountInString, keeping the existing tab expansion.

Pure-ASCII output is unchanged. Rendering lexer.go from this repo to SVG before and after gives byte-identical files.

Verification

TestNonASCIIWidths in formatters/svg/svg_test.go builds a style with a string background so the rect path is exercised, then asserts the canvas width and both rect offsets.

Reverting svg.go fails it, and the failure output shows the misalignment directly:

Needle:   <svg width="120px"
Haystack: ... <svg width="136px" ...
          <rect id="&quot;éé&quot;" x="4ch" ... width="6ch" ... />
          <rect id="&quot;ab&quot;" x="12ch" ... width="4ch" ... />

go test ./... passes and golangci-lint run is clean.

Disclosure: written with AI assistance (Claude Code). I produced the before and after by rendering real files through both builds, and ran the revert check myself.

SVG formatter was using len() to measure line and token widths, which counts bytes. Non-ASCII runes would inflate the width and misalign token background rects. Now uses utf8.RuneCountInString via a shared tokenWidth helper.
@alecthomas

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9430a9706c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread formatters/svg/svg.go

// 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants