Skip to content

feat(fonts): add bundled font substitutes and readiness reporting#3608

Open
caio-pizzol wants to merge 12 commits into
mainfrom
caio/font-load-gate-t3
Open

feat(fonts): add bundled font substitutes and readiness reporting#3608
caio-pizzol wants to merge 12 commits into
mainfrom
caio/font-load-gate-t3

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol commented Jun 2, 2026

Adds core font-rendering support: documents using common Word fonts the browser lacks (Calibri, Cambria, Arial, Times New Roman, Courier New) render with metric-compatible open substitutes loaded before measurement, instead of silently paginating against a browser fallback that drifts page counts by machine. Also adds a read-only diagnostics surface for what each font resolved to and whether it loaded. Core font-rendering support, not full Word font coverage.

What changed

  • New @superdoc/font-system: logical→physical resolver (Calibri→Carlito, …), a per-FontFaceSet registry + load-state, and a resolver/registry-based report.
  • Font asset system: a data-only manifest + a provider that registers url(<base>/<file>) faces (no binary imports in shared runtime), and a vite plugin that serves the pack at /fonts/* in dev and emits it as separate dist/fonts/* assets in build. Font bytes are never inlined into the JS bundle; the manifest scales to the full (~40-font) rollout by adding rows.
  • Asset base resolution so the bundled fonts are deployable across CDN, npm/bundler, and SSR: fonts.assetBaseUrl plus a sync fonts.resolveAssetUrl config, resolved resolveAssetUrl > assetBaseUrl > CDN script-relative > /fonts/. The CDN <script> build captures its own script URL and defaults the base to ./fonts/. A substitute whose asset fails to load surfaces in diagnostics (missing: true, loadStatus: 'failed') and warns once with the URL, so a misconfigured base is never silent.
  • Load-before-measure gate: awaits the faces a document needs before first measurement; a face arriving after a timed-out first paint invalidates the measurement caches and reflows once. A font-config epoch is folded into measure + paint reuse signatures so a late load reaches the DOM.
  • Read surface: superdoc.fonts.getReport() / getMissingFonts() / getDocumentFonts() (pull) and onReport() (snapshot-then-subscribe). A new authoritative fonts-changed event / onFontsChanged config carries {documentFonts, resolutions, missingFonts, loadSummary, source, version}. Legacy fonts-resolved early probe unchanged.

Verified

  • Per-font painted line breaks match Word for Calibri/Cambria/Arial/Times/Courier; .woff2 metric preservation checked by a committed script.
  • CDN bundle under the size budget with zero inlined font data; pack emits as separate dist/fonts/* assets (woff2 + OFL/Apache license texts); the substitutes load live from /fonts/*.woff2 and the report shows them loaded.
  • Repo typecheck + the public-contract gate (check:public:superdoc, 13 stages) green; unit tests for resolver, gate, report, asset-URL resolution, and the SuperDoc relay/onReport.

Known limitation

  • The bundled pack registers once per document FontFaceSet, so multiple SuperDoc instances on one page share one font config; the first wins and a later, conflicting fonts.assetBaseUrl is ignored (with a console warning, not silently).

Not included (out of scope)

  • Aptos (no metric-compatible clone yet)
  • Custom-font write API (add/map/preload)
  • Embedded-font handling
  • Toolbar / font-menu parity
  • Legal sign-off

Legal — release blocker
The bundled clones (Carlito, Caladea, Liberation Sans/Serif/Mono) ship with their OFL-1.1 / Apache-2.0 license texts (dist/fonts/), but shipping them publicly needs legal sign-off. Treat as a blocker for any public release that includes the bundled assets.

Render common Word fonts the browser lacks (Calibri, Cambria, Arial, Times New
Roman, Courier New) with metric-compatible open substitutes loaded before
measurement, via a manifest-driven asset provider that emits font files as
separate assets instead of inlining them into JS. Adds a read-only fonts
diagnostics surface (superdoc.fonts.*, fonts-changed / onReport). Runtime asset
base defaults to /fonts/; a configurable/script-relative base is a follow-up.
Excludes Aptos, the write API, embedded fonts, and legal sign-off.
@codecov-commenter

This comment was marked as outdated.

Make the bundled font assets deployable across CDN, npm/bundler, and SSR
instead of assuming a root-served /fonts/. Adds a fonts.assetBaseUrl plus a
sync fonts.resolveAssetUrl config; resolution order is resolveAssetUrl >
assetBaseUrl > CDN script-relative > /fonts/. The CDN build captures its own
script URL in cdn-entry and defaults the base to ./fonts/ next to
superdoc.min.js. A substitute whose asset fails to load now surfaces in
diagnostics (missing=true, loadStatus=failed) and warns once with the URL, so a
misconfigured base is never silent. The cdn/laravel/nextjs examples serve the
bundled fonts so their smoke tests have no 404s.
# Conflicts:
#	packages/superdoc/src/core/SuperDoc.ts
…antics

Two review fixes for the asset-base work. The bundled pack installs once per
document FontFaceSet (shared across SuperDoc instances on a page); a later
install with a different assetBaseUrl/resolveAssetUrl was silently dropped - it
now keeps the first config and warns on a genuine conflict instead. And the
report's `missing` flag now means a SETTLED non-loaded state (failed /
timed_out / fallback_used), not transient unloaded/loading, so an early
getReport() pull before the gate settles no longer over-reports.
…placeholder font

Two CI failures introduced by the font work on this branch:
- cdn-entry.js imports @superdoc/font-system (for the script-relative asset base),
  which the superdoc Vitest env could not resolve - the alias is intentionally
  kept out of getAliases (it breaks the vite-plugin-dts build) and only added in
  the CDN config. Add a Vitest-scoped alias so cdn-entry.test.js resolves it; the
  dts build is unaffected.
- The empty-block-SDT placeholder paints fontFamily through resolvePhysicalFamily
  like all painted text, so Arial renders as Liberation Sans. Update the stale
  assertion to expect the physical family (logical stays for export, not DOM).
…tion

The load-before-measure font gate adds an await ahead of incrementalLayout, so
dispatching the Ctrl+Alt+H shortcut on "incrementalLayout called" alone now races
the layout-applied state and the header/footer session cannot yet see the missing
region. Add the same render-settle step the sibling header/footer tests use, and
await the blocked event. Test-only; matches the file's existing pattern.
…t pages

Header/footer regions come from resolved layout pages (rebuildRegions),
not the headers[] array, so headers=[] left a per-page region and the
shortcut took the activation path. With the font gate making layout timing
deterministic, the page-mount poll recursed under the synchronous rAF mock
and overflowed. Use a layout with no pages (the real no-region condition)
and drop the flaky setTimeout settle.
@caio-pizzol caio-pizzol marked this pull request as ready for review June 3, 2026 11:26
@caio-pizzol caio-pizzol requested a review from a team as a code owner June 3, 2026 11:26
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: b3ede38546

ℹ️ 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/registry.ts
Comment thread packages/superdoc/src/core/SuperDoc.ts Outdated
Adds the patch coverage codecov flagged on #3608, test-only (no product change):
- cdn-entry: the document.currentScript -> ./fonts/ asset-base default, both the
  success path and the best-effort catch, via module re-eval with the heavy SuperDoc
  graph stubbed and @superdoc/font-system left real.
- SuperDoc.fonts: getReport / getMissingFonts / getDocumentFonts (incl. the logical
  family map) and the empty-arrays-when-no-active-editor fallback.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 81 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread shared/font-system/scripts/verify-bundled-metrics.py Outdated
Comment thread packages/superdoc/vite-plugin-bundled-fonts.mjs Outdated
After a document swap an old editor can still emit fonts-changed (a timed-out font
finishing later) with no document id to disambiguate; the relay forwarded it as the
active document's report and poisoned the onReport cache. Gate the relay callback to
the active editor so superdoc.on('fonts-changed') / fonts.onReport() reflect the
active document. Addresses the P2 review comment on SuperDoc.ts:1573.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/superdoc/src/core/SuperDoc.ts Outdated
Public-contract accuracy fixes, no runtime behavior change:
- classify the six new root font exports (SuperDocFontsApi, FontsConfig,
  FontsChangedPayload, FontResolutionRecord, FontAssetUrlContext,
  FontAssetUrlResolver) in the root snapshot (json + md). AGENTS.md mandate;
  the closure gate skips unclassified names so this was otherwise silent.
- remove the never-emitted 'config-change' from FontsChangedPayload.source
  (only 'initial' | 'late-load' is emitted). Re-add when the write API emits it.
- fix the stale FontReadinessGate.resolveFamilies "identity until T1" comment;
  this PR wires resolvePhysicalFamilies as the resolver.
- fix the fonts.onReport doc that overpromised "regardless of timing"; it
  replays only if a report has resolved, nothing pre-settle after a swap.
- make verify-bundled-metrics.py fail on skipped source .ttf unless ALLOW_SKIPS,
  so a CI run can never report success after validating nothing.
Classifying the six font types as type-only root exports requires matching
AssertNotAny<T> coverage in the consumer fixture (SD-3213a gate); without it
the public-types fixture check fails. Completes the classification change.
…rrors

Two review follow-ups:
- The active-editor guard on the fonts-changed relay covered the live event but
  not the cached replay-on-wire, so creating an inactive editor with a cached
  payload could still poison the onReport cache. Factor the rule into one
  #fontReportSurfaces predicate used by both paths; add a regression test for
  creating an inactive editor with a cached report.
- The dev bundled-fonts middleware piped a read stream with no error handler, so
  a mid-read failure could crash the Vite dev server. Respond 500 (if nothing was
  sent) and close on stream error.
@caio-pizzol caio-pizzol self-assigned this Jun 3, 2026
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