Skip to content

fix(settings): quote the chosen font family so odd names still apply - #820

Merged
PathGao merged 2 commits into
masterfrom
fix/font-family-quoting
Sep 22, 2026
Merged

PathGao merged 2 commits into
masterfrom
fix/font-family-quoting

Conversation

@PathGao

@PathGao PathGao commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

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.svelte interpolated the name bare — font-family: {settings.previewFont}, sans-serif — and editorOptions.ts handed the raw
string 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 the
whole 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 at 40px Courier:

font-family: Helvetica, sans-serif          -> Helvetica   257.66px
font-family: M+ 1c, sans-serif              -> Courier     264.04px   declaration dropped
font-family: 04b03, sans-serif              -> Courier     264.04px   declaration dropped
font-family: Gill Sans (Body), sans-serif   -> Courier     264.04px   declaration dropped
font-family: Helvetica!, sans-serif         -> Courier     264.04px   declaration dropped
(no font-family at all)                     -> Courier     264.04px

font-family: 'M+ 1c', sans-serif            -> sans-serif  257.66px   survives
font-family: '04b03', sans-serif            -> sans-serif  257.66px   survives
font-family: 'Gill Sans (Body)', sans-serif -> sans-serif  257.66px   survives
font-family: 'Helvetica!', sans-serif       -> sans-serif  257.66px   survives

Monaco is not a second implementation of this. BareFontInfo._wrapInQuotes
quotes a family only when it carries a space or a +, so 04b03 still reached
the 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 fontFamilyValue covers: the CSS generics stay unquoted,
because "sans-serif" is a request for a family nobody has and because
defaultFontsFor('linux') picks Monospace and system-ui; and a blank
preference returns the fallback alone, since stringSetting applies any
non-null raw value and an empty preview.font key used to produce
font-family: , sans-serif, invalid like the rest.

Scope

Only the two places a chosen family reaches CSS. Not touched:

  • The export and print paths, which do not write font-family themselves.
  • The static var(--win-font) families in the components, which are not user
    input.
  • The font list. get_system_fonts enumerates families through font_kit
    and that half works — the reporter's font was listed.
  • Whether a font file kept only in a cloud folder can be read at all. It can:
    a WKWebView here rendered a family that a separate process had registered
    from a non-standard directory with CTFontManagerRegisterFontsForURL at
    session 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, the
blank 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 a
second 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 full
SETTINGS instead of the single setting under test. It passed a stub with no
editorFont, which editorOptionsFromSettings did not read before and does
now — a string field arriving as undefined, a shape the type forbids and
production cannot hand it.

Verification

npm run check       840 files, 0 errors, 0 warnings
npm test            1033 pass, 0 fail
npm run test:vitest 445 pass, 51 files

No Rust changed, so cargo test / clippy / fmt were left to CI. Not
verified: 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.

PathGao 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.
@PathGao
PathGao merged commit c57252b into master Sep 22, 2026
4 checks passed
@PathGao
PathGao deleted the fix/font-family-quoting branch September 22, 2026 22:04
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