/** * Logical -> physical font resolution. * * A document refers to a font by its *logical* family - the name Word wrote, e.g. * "Calibri". The browser may not have that font (it is proprietary), so SuperDoc * renders a reviewed *physical* fallback, such as metric-compatible Carlito for Calibri. The * logical name stays the source of truth (toolbar, export); only measurement and paint use the * physical family, and they MUST use the same one or text is measured in one font and painted in * another. * * The value reaching measure and paint is a CSS font-family *stack* the layout * builds via `toCssFontFamily`, e.g. "Calibri, sans-serif" - so resolution applies * to the PRIMARY family and keeps the remaining fallbacks ("Carlito, sans-serif"). * * Resolution is a {@link FontResolver} INSTANCE, not a global: each document gets its * own so two editors on one page can map the same logical family differently (a * customer `fonts.map`) without leaking across documents - the same per-document * isolation the registry already has per `FontFaceSet`. Every instance is seeded from DocFonts * evidence gated by the local bundled asset manifest. The module-level `resolve*` functions delegate * to a shared default instance for callers that have no document context (and for backward * compatibility). */ import { type BundledActivation } from './activation.js'; export type FontResolutionReason = /** No substitute is known; the requested family is used as-is. */ 'as_requested' /** * A real face is registered for this weight/style, either under the logical family itself * (customer `fonts.add`) or under a document-unique embedded-font alias. SuperDoc intentionally * renders the registered provider and bypasses bundled substitutes. Higher precedence than * `bundled_substitute`, lower than an explicit `custom_mapping`. Only the face-aware path yields * this. */ | 'registered_face' /** Replaced by a bundled metric-compatible clone. */ | 'bundled_substitute' /** Replaced by a runtime mapping set on this document's resolver (customer `fonts.map`). */ | 'custom_mapping' /** * Replaced by the closest bundled FAMILY, but NOT a metric-compatible clone: advances reflow and/or * the weight differs (e.g. Calibri Light -> Carlito, which has no Light face, so it renders at Regular * and reflows ~6.6%). A useful family fallback, never reported as metric-safe. Lower precedence than * `bundled_substitute`; sourced from docfonts rows with `policyAction: 'category_fallback'`. */ | 'category_fallback' /** * A substitute is known for the family but does NOT provide the requested face (weight/style), * so the family passes through UNsubstituted rather than faux-styling the substitute's Regular * onto a face it lacks. Reported non-metric. Only the face-aware path (`resolveFace` / * `resolvePhysicalFamilyForFace`) yields this; the family-only methods never do. */ | 'fallback_face_absent'; export interface FontResolution { /** The family the document asked for (preserved for toolbar/export). */ logicalFamily: string; /** The bare physical family that is actually loaded, measured, and painted. */ physicalFamily: string; reason: FontResolutionReason; /** * The real registered face used for loading when the requested face is synthetic. Omitted when the * requested face itself is the source face. */ sourceFace?: FaceKey; } /** A specific face within a family: the weight/style axis a run renders at. */ export interface FaceKey { weight: '400' | '700'; style: 'normal' | 'italic'; } /** * Face-availability oracle: does the PHYSICAL family actually provide this face? Backed by the * registry (bundled faces registered by `installBundledSubstitutes` + customer `fonts.add()` * faces). Injected rather than imported so font-system has no registry import cycle and each caller * passes its own document's registry lookup. */ export type HasFace = (physicalFamily: string, weight: '400' | '700', style: 'normal' | 'italic') => boolean; /** * Per-document logical -> physical font resolver. Seeded with bundled DocFonts fallbacks; * also holds per-instance runtime overrides (a customer `fonts.map`). Because each * document owns its instance, two documents can map the same logical family to different * physical families without interfering. Its {@link signature} (NOT the numeric * {@link version}) is the identity measure-cache keys and paint reuse signatures must fold in, * so two documents at the same version with different mappings never collide. */ export declare class FontResolver { #private; constructor(activation?: BundledActivation); /** * Apply this document's bundled-font activation, derived from the editor's `fonts` config. * Config-time only: the document font controller calls this before the first measure. Busts the * cached {@link signature} (and bumps {@link version}) so the new activation is reflected in both * resolution and the measure-cache key; a no-op when the activation signature is unchanged, so * re-applying the same config on a document swap neither reflows nor drops the cache. */ setActivation(activation: BundledActivation): void; /** * Map a logical family to a physical render family for this document, overriding the * bundled default (e.g. "Georgia" -> "Gelasio", or a customer family -> their font). * The physical family must be one the registry can load. * * A self-map (`map('Georgia', 'Georgia')`, normalized) is the absence of an override and is dropped. * Mapping to the bundled clone (`map('Calibri', 'Carlito')`) is NOT a no-op: it is stored as an * explicit pin so it outranks a registered real face for that family (`custom_mapping` > `registered_face`). */ map(logicalFamily: string, physicalFamily: string): void; /** Remove a runtime mapping; the family reverts to its bundled default (or identity). */ unmap(logicalFamily: string): void; /** * Bind a logical family to this document's UNIQUE physical render family for its embedded font, so * the face-aware ladder resolves the logical family to that physical name with reason * `registered_face`. The caller (the document font controller) must register the face under the same * physical name. Render-only: export keeps the logical family. The physical name must be unique to * this document so a shared FontFaceSet cannot cross-render another document's embedded bytes. */ mapEmbedded(logicalFamily: string, physicalFamily: string): void; /** Drop all embedded-family bindings (a document swap / teardown releases this document's embedded * fonts). Bumps {@link version} only if something was cleared. */ clearEmbedded(): void; /** * Drop all runtime overrides AND embedded bindings, reverting to the bundled-only map. Call on a * document swap (the same editor instance is reused, so the prior document's `fonts.map` and embedded * fonts must not leak into the next). Bumps {@link version} only if something was actually cleared. */ reset(): void; /** Monotonic version; bumps on every mapping change. A lightweight "did it change" signal. */ get version(): number; /** * Stable content signature of this resolver's runtime mappings - the deterministic, * order-independent serialization of its overrides. This (NOT {@link version}) is what * measure-cache keys and paint reuse signatures must fold in: two documents can both be at * version 1 with DIFFERENT mappings (Georgia->Gelasio vs Georgia->Tinos), and a numeric * version would collide; their signatures differ. Empty (no overrides) is `''`, so all * default documents share cache safely because they resolve identically. */ get signature(): string; /** * Structured resolution of a logical family (or CSS stack) to its bare physical render * family. The primary (first) family drives the result; this is what the load gate * awaits and what diagnostics report. */ resolveFontFamily(logicalFamily: string): FontResolution; /** * Resolve a CSS font-family value for MEASURE and PAINT: swap the primary family to its * physical substitute and keep the original fallbacks. "Calibri, sans-serif" -> * "Carlito, sans-serif"; "Calibri" -> "Carlito". An unmapped value is returned unchanged. */ resolvePhysicalFamily(cssFontFamily: string): string; /** * Face-aware structured resolution. Like {@link resolveFontFamily}, but a substitute applies only * when the physical family provides the requested face or docfonts explicitly names a synthetic * source face. Otherwise the logical family passes through with reason `fallback_face_absent`, so a * single-face substitute is never faux-styled by accident. The four-face shipped clones provide every * face, so they resolve identically to {@link resolveFontFamily}. */ resolveFace(logicalFamily: string, face: FaceKey, hasFace: HasFace): FontResolution; /** * Face-aware CSS-stack variant for MEASURE and PAINT - the face-scoped counterpart of * {@link resolvePhysicalFamily}. Swaps the primary family to its substitute when this exact face is * real or docfonts authorizes a synthetic source face. Otherwise returns the value unchanged * (logical family + its fallbacks), so a missing face is never faux-styled by accident. Measure and * paint MUST call this with the same face key, or text is measured in one face and painted in another. */ resolvePhysicalFamilyForFace(cssFontFamily: string, face: FaceKey, hasFace: HasFace): string; /** * The bare physical family the load gate must await - the primary family resolved to its * substitute. "Calibri, sans-serif" -> "Carlito"; "Calibri" -> "Carlito". */ resolvePrimaryPhysicalFamily(family: string): string; /** The deduped set of physical face families a set of logical families needs loaded. */ resolvePhysicalFamilies(families: Iterable): string[]; } /** * Create a per-document resolver seeded with bundled DocFonts fallbacks. Pass the document's * {@link BundledActivation} (from `deriveBundledActivation`) so substitution is gated on the pack * being configured and on any curation; omit it for the prior fully-active behavior. */ export declare function createFontResolver(activation?: BundledActivation): FontResolver; export declare function resolveFontFamily(logicalFamily: string): FontResolution; export declare function resolvePhysicalFamily(cssFontFamily: string): string; export declare function resolvePrimaryPhysicalFamily(family: string): string; export declare function resolvePhysicalFamilies(families: Iterable): string[]; export declare function resolveFace(logicalFamily: string, face: FaceKey, hasFace: HasFace): FontResolution; /** * Maps a logical CSS family to the physical render family for a SPECIFIC face (weight/style): a * per-document `fonts.map` override or a bundled substitute, but only when that substitute provides * the face or docfonts authorizes a synthetic source face. The one shared spelling for what was * duplicated across the painter, measuring, and planner packages. Face-aware so a single-face clone is * never mapped onto a Bold/Italic run it cannot render unless the evidence explicitly allows it. */ export type ResolvePhysicalFamily = (cssFontFamily: string, face: FaceKey) => string; /** * The per-document font identity that every measure and paint path needs, carried as ONE value so * the resolver and its signature cannot travel separately and drift: * - `resolvePhysical` maps logical -> physical for the document (glyph widths, vertical metrics, paint). * - `fontSignature` is the document's stable mapping identity; it keys every measure cache so two * documents (or two renders) with different `fonts.map` never reuse each other's measures. * * The contract: internal measure helpers take this as a REQUIRED argument and only outer * compatibility entry points (e.g. the exported `measureBlock`) default to * {@link DEFAULT_FONT_MEASURE_CONTEXT}. Bundling the resolver with its signature is what keeps an * internal measure path from silently falling back to the global resolver or pairing a per-document * signature with the wrong resolver, and lets every cache site derive its signature from the same * context that supplied the resolver. (The required-argument property holds as helpers adopt the * context; it is enforced, not assumed, by this pass.) */ export interface FontMeasureContext { resolvePhysical: ResolvePhysicalFamily; fontSignature: string; } /** * The global-resolver / empty-signature context. The behavior-preserving default for outer entry * points and non-document callers (tests, the global measure path). Frozen so a stray * `DEFAULT_FONT_MEASURE_CONTEXT.resolvePhysical = ...` cannot pollute every default-path document. */ export declare const DEFAULT_FONT_MEASURE_CONTEXT: FontMeasureContext;