fix(settings): quote the chosen font family so odd names still apply - #820
Merged
Merged
Conversation
added 2 commits
September 22, 2026 12:37
A font family the user picked was interpolated into CSS bare — `font-family:
{settings.previewFont}, sans-serif` in the viewer, and the raw string handed to
Monaco — which is valid only while the name happens to be a sequence of CSS
identifiers. A family called `M+ 1c`, `04b03` or `Gill Sans (Body)` made the
whole declaration invalid, so the browser dropped it and the element kept the
font it inherited: the family is listed in the settings dropdown, selecting it
changes nothing, and the failure is indistinguishable from the font not being
installed.
`fontFamilyValue` quotes the name and escapes `"` and `\` in it, leaves the CSS
generics unquoted (quoted, the Linux defaults `Monospace` and `system-ui` would
become families nobody has), and returns the fallback alone for a blank
preference, which an empty `preview.font` key in localStorage can produce.
Monaco is not a second implementation of this: it quotes on its own only when
the name carries a space or a `+`, and skips its own quoting once the value
contains a quote character.
Measured in a WKWebView with the parent element at `40px Courier`: bare
`M+ 1c`, `04b03`, `Gill Sans (Body)` and `Helvetica!` each computed to
`Courier` at an identical 264.04px, while `Helvetica` rendered at 257.66px.
Quoted, all four keep their declaration and fall through to `sans-serif` at
257.66px.
…okalike list `fangsong` and `emoji` were in it. Neither is a bare generic in CSS Fonts 4: the script-specific generics are spelled `generic(fangsong)`, and `emoji` is not in the grammar at all. `fangsong` is the one that mattered — macOS ships `FangSong` and `STFangsong`, so exempting the name handed a user who picked their 仿宋 face an unquoted family name, which an engine that does treat the bare word as a keyword resolves to a generic instead of to their font. What is left is `<generic-font-complete>` plus `<generic-font-incomplete>` from §2.1.2. The comment now also says which names must never be added: a family named after a CSS-wide keyword has to be quoted per §2.1.1, and bare `inherit` or `default` is rejected outright — measured in a WKWebView, both drop the declaration while the quoted form is accepted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A font family picked in settings now reaches CSS quoted, so a name that is not a
bare CSS identifier applies instead of being silently discarded. Found while
working on #810, where @felhasznalonev reported that fonts activated by a
third-party font manager appear in the dropdown and do not load. This does not
close that issue — whether it is his case depends on the font's name, which I
have asked for there.
Mechanism
MarkdownViewer.svelteinterpolated the name bare —font-family: {settings.previewFont}, sans-serif— andeditorOptions.tshanded the rawstring to Monaco. That is valid CSS only while the name happens to be a
sequence of identifiers.
M+ 1c,04b03,Gill Sans (Body): each makes thewhole declaration invalid, so the browser drops it and the element keeps the
font it inherited. Nothing fails loudly — the family is in the dropdown,
selecting it changes nothing, and the user cannot tell that apart from the font
not being installed.
Measured in a
WKWebView, parent element at40px Courier:Monaco is not a second implementation of this.
BareFontInfo._wrapInQuotesquotes a family only when it carries a space or a
+, so04b03still reachedthe stylesheet bare, and it skips its own quoting entirely once the value
contains a quote character — which is what lets this one stand in front of it.
The two other cases
fontFamilyValuecovers: the CSS generics stay unquoted,because
"sans-serif"is a request for a family nobody has and becausedefaultFontsFor('linux')picksMonospaceandsystem-ui; and a blankpreference returns the fallback alone, since
stringSettingapplies anynon-null raw value and an empty
preview.fontkey used to producefont-family: , sans-serif, invalid like the rest.Scope
Only the two places a chosen family reaches CSS. Not touched:
font-familythemselves.var(--win-font)families in the components, which are not userinput.
get_system_fontsenumerates families throughfont_kitand that half works — the reporter's font was listed.
a
WKWebViewhere rendered a family that a separate process had registeredfrom a non-standard directory with
CTFontManagerRegisterFontsForURLatsession scope, and the shipped 2.7.6 bundle carries no entitlements, so there
is no App Sandbox gate to fail. That rules out the explanation I first gave
in Paragraph, icon, fonts, settings, sidebar, highlight #810; it does not yet explain his case.
Tests
scripts/fontFamily.test.ts, new: the quoting and escaping, the generics, theblank preference, and two source-shape assertions that both call sites go
through the helper. Reverting the two call sites and keeping the test goes red
on the shape test; the behaviour assertions run the helper and pass either way,
which is why the shape half is there.
The shape half is an absence claim as much as a presence one —
assert.doesNotMatch(viewerSource, /font-family: \{settings\./)— because asecond bare interpolation is the defect returning and running the helper that
does exist cannot observe one.
scripts/editorOptionWiring.test.ts: one fixture now spreads the fullSETTINGSinstead of the single setting under test. It passed a stub with noeditorFont, whicheditorOptionsFromSettingsdid not read before and doesnow — a
stringfield arriving asundefined, a shape the type forbids andproduction cannot hand it.
Verification
No Rust changed, so
cargo test/clippy/fmtwere left to CI. Notverified: Windows and Linux, and the reporter's own font — the numbers above are
a WKWebView on macOS 27 with names chosen to reproduce the parse failure, not
his install. A macOS test build is on my desktop for a run against a real
awkwardly-named font before this merges.