Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/fallbacks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const fallback = getRenderableFallback("Helvetica", {
canRenderFamily: (family) => bundledFamilies.has(family),
});

// { substituteFamily: "Liberation Sans", policyAction: "substitute", verdict: "metric_safe", lineBreakSafe: true, evidenceId: "helvetica" }
// { substituteFamily: "Liberation Sans", policyAction: "substitute", verdict: "metric_safe", lineBreakSafe: true, evidenceId: "helvetica", generic: "sans-serif" }
```

The result is `null` when there is nothing renderable from your available assets. Use `getFallbackDecision` when you need to know why.
Expand All @@ -38,18 +38,18 @@ Use `getFallbackDecision` for UI, diagnostics, and reporting. It distinguishes k
import { getFallbackDecision } from "@docfonts/fallbacks";

getFallbackDecision("Aptos");
// { kind: "customer_supplied", evidenceId: "aptos" }
// { kind: "customer_supplied", evidenceId: "aptos", generic: "sans-serif" }

getFallbackDecision("Tahoma");
// { kind: "no_recommended_fallback", evidenceId: "tahoma" }
// { kind: "no_recommended_fallback", evidenceId: "tahoma", generic: "sans-serif" }

getFallbackDecision("Made Up Font");
// { kind: "unknown" }

getFallbackDecision("Georgia", {
canRenderFamily: (family) => bundledFamilies.has(family),
});
// { kind: "asset_missing", substituteFamily: "Gelasio", verdict: "near_metric", evidenceId: "georgia" }
// { kind: "asset_missing", substituteFamily: "Gelasio", verdict: "near_metric", evidenceId: "georgia", generic: "serif" }
```

Decision kinds:
Expand Down Expand Up @@ -86,6 +86,7 @@ Keys are normalized. Use `normalizeFamilyName` for lookups. Rows whose substitut
- `lineBreakSafe` - true when advances preserve line breaks: `metric_safe`, `near_metric`, or monospace `cell_width_only`.
- `faces` - reviewed face coverage for this evidence row. If any face is `true`, respect it as face-scoped coverage (a row can be Regular-only). If all faces are `false`, the row is **not** face-scoped (e.g. a category fallback whose physical font does have faces) and the face-aware helpers treat it as renderable for any face.
- `evidenceId` - the stable id for the reviewed evidence row; look the full row up in `SUBSTITUTION_EVIDENCE`.
- `generic` - the logical font's broad CSS category (`serif`, `sans-serif`, or `monospace`), for a last-resort generic `font-family` keyword when no named substitute renders. Also present on the known (non-`unknown`) decision kinds.
- `glyphExceptions` - named glyph-level divergences that qualify this fallback (e.g. one codepoint reflows), or omitted when none. A family lookup carries all of the row's; a face lookup (`getRenderableFallbackForFace`) carries only that face's, so Cambria Regular shows none while Bold Italic shows its grave-accent exception.

`cell_width_only` keeps monospace advances stable, but glyph shapes can still differ. A `substitute` can still have a lower-fidelity `verdict` when one face or glyph is qualified. The verdict is the fidelity signal.
Expand Down
39 changes: 38 additions & 1 deletion packages/fallbacks/fallbacks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
SUBSTITUTION_EVIDENCE,
} from "./src/index";

// A consumer that ships exactly the five families the reference renderer bundles.
// A consumer that ships exactly these five open fallback families.
const BUNDLED = new Set([
"Carlito",
"Caladea",
Expand All @@ -37,6 +37,7 @@ describe("getFallbackDecision", () => {
lineBreakSafe: true,
faces: { regular: true, bold: true, italic: true, boldItalic: true },
evidenceId: "helvetica",
generic: "sans-serif",
},
});
});
Expand All @@ -48,6 +49,7 @@ describe("getFallbackDecision", () => {
substituteFamily: "Gelasio",
verdict: "near_metric",
evidenceId: "georgia",
generic: "serif",
});
});

Expand All @@ -56,14 +58,17 @@ describe("getFallbackDecision", () => {
expect(getFallbackDecision("Aptos")).toEqual({
kind: "customer_supplied",
evidenceId: "aptos",
generic: "sans-serif",
});
expect(getFallbackDecision("Tahoma")).toEqual({
kind: "no_recommended_fallback",
evidenceId: "tahoma",
generic: "sans-serif",
});
expect(getFallbackDecision("Cambria Math")).toEqual({
kind: "preserve_only",
evidenceId: "cambria-math",
generic: "serif",
});
expect(getFallbackDecision("Foo Unknown Font")).toEqual({
kind: "unknown",
Expand Down Expand Up @@ -124,6 +129,10 @@ describe("createFallbackMap", () => {
"times new roman",
]);
expect(map.helvetica.substituteFamily).toBe("Liberation Sans");
// Each entry carries the logical font's generic, so a face-aware resolver can emit a keyword.
expect(map.helvetica.generic).toBe("sans-serif");
expect(map["times new roman"].generic).toBe("serif");
expect(map["courier new"].generic).toBe("monospace");
// Georgia/Arial Narrow/Baskerville point at un-bundled families, so they are absent.
expect(map.georgia).toBeUndefined();
});
Expand Down Expand Up @@ -186,6 +195,7 @@ describe("face-aware lookups (Regular-only safety)", () => {
kind: "face_missing",
substituteFamily: "Bacasime Antique",
evidenceId: "baskerville-old-face",
generic: "serif",
});
});

Expand Down Expand Up @@ -290,6 +300,30 @@ describe("glyphExceptions projection", () => {
});
});

describe("generic CSS family metadata", () => {
const renderAll = { canRenderFamily: () => true };

test("every evidence row carries one of the broad generic categories", () => {
const GENERICS = new Set(["serif", "sans-serif", "monospace"]);
for (const row of SUBSTITUTION_EVIDENCE) {
expect(
GENERICS.has(row.generic),
`${row.evidenceId} (${row.generic})`,
).toBe(true);
}
});

test("a resolved fallback projects the logical font's generic", () => {
expect(getRenderableFallback("Cambria", renderAll)?.generic).toBe("serif");
expect(getRenderableFallback("Calibri", renderAll)?.generic).toBe(
"sans-serif",
);
expect(getRenderableFallback("Consolas", renderAll)?.generic).toBe(
"monospace",
);
});
});

describe("Cooper Black -> Caprasimo (Regular-only, metric_safe)", () => {
const renderAll = { canRenderFamily: () => true };
const onlyCaprasimo = { canRenderFamily: (f: string) => f === "Caprasimo" };
Expand All @@ -304,6 +338,7 @@ describe("Cooper Black -> Caprasimo (Regular-only, metric_safe)", () => {
lineBreakSafe: true,
faces: { regular: true, bold: false, italic: false, boldItalic: false },
evidenceId: "cooper-black",
generic: "serif",
});
});

Expand All @@ -324,6 +359,7 @@ describe("Cooper Black -> Caprasimo (Regular-only, metric_safe)", () => {
kind: "face_missing",
substituteFamily: "Caprasimo",
evidenceId: "cooper-black",
generic: "serif",
});
}
});
Expand All @@ -337,6 +373,7 @@ describe("Cooper Black -> Caprasimo (Regular-only, metric_safe)", () => {
substituteFamily: "Caprasimo",
verdict: "metric_safe",
evidenceId: "cooper-black",
generic: "serif",
});
expect(
getRenderableFallbackForFace("Cooper Black", "regular", onlyCaprasimo)
Expand Down
23 changes: 23 additions & 0 deletions packages/fallbacks/records.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[
{
"evidenceId": "calibri",
"generic": "sans-serif",
"logicalFamily": "Calibri",
"physicalFamily": "Carlito",
"verdict": "metric_safe",
Expand Down Expand Up @@ -30,6 +31,7 @@
},
{
"evidenceId": "cambria",
"generic": "serif",
"logicalFamily": "Cambria",
"physicalFamily": "Caladea",
"verdict": "visual_only",
Expand Down Expand Up @@ -75,6 +77,7 @@
},
{
"evidenceId": "arial",
"generic": "sans-serif",
"logicalFamily": "Arial",
"physicalFamily": "Liberation Sans",
"verdict": "metric_safe",
Expand All @@ -101,6 +104,7 @@
},
{
"evidenceId": "times-new-roman",
"generic": "serif",
"logicalFamily": "Times New Roman",
"physicalFamily": "Liberation Serif",
"verdict": "metric_safe",
Expand Down Expand Up @@ -129,6 +133,7 @@
},
{
"evidenceId": "courier-new",
"generic": "monospace",
"logicalFamily": "Courier New",
"physicalFamily": "Liberation Mono",
"verdict": "metric_safe",
Expand Down Expand Up @@ -157,6 +162,7 @@
},
{
"evidenceId": "georgia",
"generic": "serif",
"logicalFamily": "Georgia",
"physicalFamily": "Gelasio",
"verdict": "near_metric",
Expand Down Expand Up @@ -212,6 +218,7 @@
},
{
"evidenceId": "arial-narrow",
"generic": "sans-serif",
"logicalFamily": "Arial Narrow",
"physicalFamily": "Liberation Sans Narrow",
"verdict": "visual_only",
Expand Down Expand Up @@ -257,6 +264,7 @@
},
{
"evidenceId": "aptos",
"generic": "sans-serif",
"logicalFamily": "Aptos",
"physicalFamily": null,
"verdict": "no_substitute",
Expand All @@ -279,6 +287,7 @@
},
{
"evidenceId": "consolas",
"generic": "monospace",
"logicalFamily": "Consolas",
"physicalFamily": "Inconsolata SemiExpanded",
"verdict": "cell_width_only",
Expand Down Expand Up @@ -307,6 +316,7 @@
},
{
"evidenceId": "verdana",
"generic": "sans-serif",
"logicalFamily": "Verdana",
"physicalFamily": null,
"verdict": "visual_only",
Expand All @@ -329,6 +339,7 @@
},
{
"evidenceId": "tahoma",
"generic": "sans-serif",
"logicalFamily": "Tahoma",
"physicalFamily": null,
"verdict": "visual_only",
Expand All @@ -351,6 +362,7 @@
},
{
"evidenceId": "trebuchet-ms",
"generic": "sans-serif",
"logicalFamily": "Trebuchet MS",
"physicalFamily": null,
"verdict": "visual_only",
Expand All @@ -373,6 +385,7 @@
},
{
"evidenceId": "comic-sans-ms",
"generic": "sans-serif",
"logicalFamily": "Comic Sans MS",
"physicalFamily": "Comic Neue",
"verdict": "visual_only",
Expand Down Expand Up @@ -401,6 +414,7 @@
},
{
"evidenceId": "candara",
"generic": "sans-serif",
"logicalFamily": "Candara",
"physicalFamily": null,
"verdict": "visual_only",
Expand All @@ -423,6 +437,7 @@
},
{
"evidenceId": "constantia",
"generic": "serif",
"logicalFamily": "Constantia",
"physicalFamily": null,
"verdict": "visual_only",
Expand All @@ -445,6 +460,7 @@
},
{
"evidenceId": "corbel",
"generic": "sans-serif",
"logicalFamily": "Corbel",
"physicalFamily": null,
"verdict": "visual_only",
Expand All @@ -467,6 +483,7 @@
},
{
"evidenceId": "lucida-console",
"generic": "monospace",
"logicalFamily": "Lucida Console",
"physicalFamily": "Cousine",
"verdict": "cell_width_only",
Expand All @@ -493,6 +510,7 @@
},
{
"evidenceId": "aptos-display",
"generic": "sans-serif",
"logicalFamily": "Aptos Display",
"physicalFamily": null,
"verdict": "customer_supplied",
Expand All @@ -514,6 +532,7 @@
},
{
"evidenceId": "cambria-math",
"generic": "serif",
"logicalFamily": "Cambria Math",
"physicalFamily": null,
"verdict": "preserve_only",
Expand All @@ -535,6 +554,7 @@
},
{
"evidenceId": "helvetica",
"generic": "sans-serif",
"logicalFamily": "Helvetica",
"physicalFamily": "Liberation Sans",
"verdict": "metric_safe",
Expand Down Expand Up @@ -563,6 +583,7 @@
},
{
"evidenceId": "calibri-light",
"generic": "sans-serif",
"logicalFamily": "Calibri Light",
"physicalFamily": "Carlito",
"verdict": "visual_only",
Expand All @@ -589,6 +610,7 @@
},
{
"evidenceId": "baskerville-old-face",
"generic": "serif",
"logicalFamily": "Baskerville Old Face",
"physicalFamily": "Bacasime Antique",
"verdict": "visual_only",
Expand Down Expand Up @@ -628,6 +650,7 @@
},
{
"evidenceId": "cooper-black",
"generic": "serif",
"logicalFamily": "Cooper Black",
"physicalFamily": "Caprasimo",
"verdict": "metric_safe",
Expand Down
Loading
Loading