Skip to content

feat(fonts): prefer a registered real face over the bundled substitute (provider precedence)#3640

Merged
caio-pizzol merged 8 commits into
mainfrom
caio/font-provider-precedence
Jun 5, 2026
Merged

feat(fonts): prefer a registered real face over the bundled substitute (provider precedence)#3640
caio-pizzol merged 8 commits into
mainfrom
caio/font-provider-precedence

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

Makes the per-document font resolver prefer a real registered face for a logical family over the bundled metric-compatible clone. Before this, a document that registered a real Calibri via fonts.add still rendered Carlito unless the user explicitly mapped around it.

The face-aware resolution now follows one precedence ladder, applied per face:

  • explicit fonts.map override — when the mapped family actually provides the face
  • a registered real face for the logical family (registered_face)
  • bundled metric-compatible substitute
  • pass through (fallback_face_absent) when a provider was configured but none could supply the face
  • otherwise as requested

A subtle case the ladder fixes: if you map Calibri -> Tinos, Tinos lacks Bold, and a real Calibri Bold is registered, the resolver now renders the real Calibri Bold (registered_face) instead of reporting it missing. The override only consumes a face it can supply; otherwise the rest of the ladder still applies.

Adds the public FontResolutionReason value registered_face so the report distinguishes "rendered the real registered font" from a bundled substitute or a browser fallback. preload follows the same precedence, so it loads the real family rather than the clone.

Embedded DOCX fonts are not registered with the font system yet — the converter still injects them as @font-face CSS — so this PR changes precedence only for already-registered faces (fonts.add). Making embedded fonts first-class registry entries (with object-URL lifecycle and licensing handling) is a separate follow-up that will then flow through this same ladder.

…e (provider precedence)

The resolver picked the bundled clone (Calibri->Carlito) even when the document registered a real Calibri face via fonts.add, so a customer-provided font was ignored unless they explicitly mapped around it. Add a registered_face step to the face-aware ladder: explicit fonts.map override > registered real face for the logical family > bundled metric-compatible substitute > as-requested. resolveFace/resolvePhysicalFamilyForFace consult hasFace for the logical family before the clone, and preload follows the same precedence so it loads the real family, not the clone.

Adds the public FontResolutionReason value 'registered_face' so diagnostics distinguish 'rendered the real registered font' from a bundled substitute or browser fallback. Embedded DOCX fonts becoming registry entries is a separate follow-up; this only changes resolution precedence for already-registered faces.
Copy link
Copy Markdown

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

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: 0bc7dc4b92

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread shared/font-system/src/resolver.ts
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

The 'four-face clone substitutes' test passed an all-true hasFace oracle, which marks the logical family (Calibri) itself registered - so provider precedence correctly resolves it to registered_face (Calibri) instead of the bundled clone, and the test's expected Carlito faces no longer matched. Use a clone-only oracle (Carlito registered, Calibri not), matching the test's intent: the normal bundled-substitute case.
…probe matches

A quoted CSS primary like '"Calibri"' kept its quotes through splitStack, while hasFace normalizes quotes and reports the family registered - so resolveFace returned physicalFamily '"Calibri"'. The load/preload path feeds that into faceProbe, which quotes it again, so the browser probes a literal "Calibri" and never matches the registered face (a false fallback_used + reflow), worst for multi-word families. Add a case-preserving stripFamilyQuotes() (distinct from normalizeFamilyKey, which lowercases for keying) and apply it to the structured resolveFace/resolveFontFamily physicalFamily, which is a load/report family not a CSS stack. The CSS paint variants (resolvePhysicalFamily/resolvePhysicalFamilyForFace) keep the quoted stack, which is valid CSS, so measure and paint stay consistent.
The root vitest projects list omitted shared/font-system, and the other-packages CI job's --filter list omits @superdoc/font-system, so the package's resolver/registry/report/os2 tests never ran in CI - which is how a quoted-family regression and two mutually contradictory resolver tests went unnoticed. Add ./shared/font-system to the vitest projects so the (node-env, @font-system) suite runs under the existing other-packages job.
…weight/clone-pin)

Four correctness fixes to the provider-precedence ladder, all from the #3640 review: (A) hasFace now consults a register-only #providerFaceKeys set instead of #facesByFamily, which the await path also fills - so an awaited as_requested family no longer masquerades as registered_face on the next plan. (2) A registered face whose asset terminally fails drops out of hasFace (timed_out does not), and the gate replans once per newly-failed required face (fire-once, factored #scheduleAvailabilityReflow) so it demotes to the bundled clone instead of rendering broken forever. (3) An explicit map to the bundled clone is now STORED as a custom_mapping override (identity self-map still drops, compared with the resolver's family normalization), so the pin beats a registered real face. (4) The FontFace is built with the bucketed weight/style the key uses, so an off-bucket 500 face no longer answers the 400 query and renders at 500. Tests + the two contradictory resolver/controller map-to-default tests updated to the new contract.
After provider precedence, map({ Calibri: 'Carlito' }) is honored as a pin that outranks a registered real Calibri (custom_mapping > registered_face), not the old map-to-default no-op. Update the public fonts.map JSDoc and the resolver map() JSDoc so reviewers/consumers do not assume the prior behavior; a no-op is now only an identity self-map or a redundant same-target map.
… report fake

The face report fake registered a face when it only meant to give it a load status, so after the registered_face ladder rung a pass-through Georgia 700 (Gelasio lacks bold) reported registered_face instead of fallback_face_absent. Add setAwaitedFaceStatus (status, not registration) and use it for the pass-through, matching the real registry where awaiting a face does not make it a provider. Surfaced now that shared/font-system runs in CI.
A no-op is a self-map or a mapping identical to an already-stored override - not 'the target it already has', which read as if map-to-bundled-default were still a no-op. Clone pins are never no-ops.
@caio-pizzol caio-pizzol merged commit 3c73adb into main Jun 5, 2026
86 of 90 checks passed
@caio-pizzol caio-pizzol deleted the caio/font-provider-precedence branch June 5, 2026 12:32
@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Jun 5, 2026

🎉 This PR is included in superdoc-cli v0.16.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Jun 5, 2026

🎉 This PR is included in superdoc-sdk v1.15.0

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Jun 5, 2026

🎉 This PR is included in @superdoc-dev/mcp v0.11.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Jun 5, 2026

🎉 This PR is included in superdoc v1.39.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Jun 5, 2026

🎉 This PR is included in @superdoc-dev/react v1.10.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Jun 6, 2026

🎉 This PR is included in vscode-ext v2.11.0

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants