Skip to content

fix(svg): escape the configured font family into the SVG document - #1381

Open
youdie006 wants to merge 1 commit into
alecthomas:masterfrom
youdie006:svg-escape-font-family
Open

youdie006 wants to merge 1 commit into
alecthomas:masterfrom
youdie006:svg-escape-font-family

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

svg.FontFamily(...) and svg.EmbedFont(...) take a font stack from the caller. It is interpolated raw into the font-family attribute of <g> (formatters/svg/svg.go:112) and into the @font-face rule (formatters/svg/svg.go:197). The ordinary CSS spelling of a stack contains a double quote, which closes the attribute; an ampersand in a family name is an unterminated entity. Either way the formatter returns a document that is not well-formed XML, so a strict SVG consumer renders nothing.

f := svg.New(svg.FontFamily(`"Fira Code", monospace`))
<g font-family=""Fira Code", monospace" font-size="14px" fill="#f8f8f2">

escapeString cannot be reused here. svgEscaper (formatters/svg/svg.go:77-84) maps a space to &#160; and a tab to four of them, for the xml:space="preserve" text nodes it was written for; running a font stack through it renames the family. So this adds an XML escaper for &, <, > and " next to it and applies it at both sites.

The sibling eight lines down already escapes what it puts in an attribute (formatters/svg/svg.go:168, rect id="%s"). The same class was fixed on the HTML side in 879e833 ("escape configured values in HTML attributes"), which changed four sites, all in formatters/html/html.go.

The registered svg formatter uses "Liberation Mono" (formatters/api.go:26) and the zero-value default is Consolas, Monaco, ... (formatters/svg/svg.go:59). Neither contains " or &, so no existing output changes.

Verification

go test ./formatters/svg/ -run TestFontFamilyIsEscaped -count=1, Go 1.26.3. md5sum formatters/svg/svg.go on every row; the test file is byte-identical across rows at 5e0ba8c2b6ad41c269870f02349cced8.

variant md5 svg.go failing subtests
this PR 2d9746d432eb76f310f408de1049fca3 none (pass)
pristine HEAD fd7cfd7305ef8d88f313b9fafd3cc560 both
escape the attribute only 113e6f0928b675be34712e88321eca62 both
escape the @font-face only c3b0041fb443c393aefe698501011def both
escapeString instead, both sites 9fd197170b9151d2435111f7ebe10bf3 both
drop & from the new escaper 8da314ea4c875640155f8c36361f0ddc Foo & Bar Mono
drop " from the new escaper b0578e88e626ab112be0cbf916bbb838 "Fira Code", monospace

Rows 3 and 4 are the N-1 rows: each site is independently necessary. The last two rows fail different subtests, so neither input is redundant.

The escapeString row fails on value transparency rather than well-formedness: the document parses, but the family comes back with U+00A0 where the caller wrote U+0020, so the two strings print identically and only the equality check catches it.

The test parses the output with encoding/xml in strict mode and asserts the font-family attribute round-trips byte-identical to the configured value, and that the <style> text contains font-family: '<value>';.

Also run: go test ./... -count=1 exit 0, 7 ok / 0 FAIL. gofmt -l formatters/svg/ empty. I did not run just check-generated; nothing generated was touched. There is no changelog in this repo to update.

Not covered: a literal ' inside the family name still ends the CSS string in the @font-face rule. The XML parser hands &#39; back as ', so that is a CSS-level quoting question rather than an XML one, and I left it alone. Happy to extend the escaper if you want that handled too.

Disclosure: this change was written with AI assistance (Claude). The measurements and mutants above were reproduced and checked by me before opening this PR.

FontFamily and EmbedFont take a font stack from the caller and it was
interpolated raw into the `font-family` attribute of `<g>` and into the
`@font-face` rule. The ordinary CSS spelling of a stack contains a
double quote, which closes the attribute, and an ampersand in a family
name is an unterminated entity, so the formatter returned a document
that is not well-formed XML.

escapeString cannot be reused here: svgEscaper maps a space to &alecthomas#160;
for xml:space="preserve" text nodes, which would rename the family.
Add an XML escaper for &, <, > and " alongside it and apply it at both
sites. The registered svg formatter and the zero-value default contain
neither character, so their output is unchanged.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-15T16:28:09.338893Z 9f0fa68 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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.

1 participant