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
4 changes: 3 additions & 1 deletion packages/fallbacks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const map = createFallbackMap({
map[normalizeFamilyName("Times New Roman")];
```

Some fallbacks are face-scoped. Use `getRenderableFallbackForFace`, or respect the returned `faces` field before applying a fallback to bold or italic text.
Some fallbacks are face-scoped. `faces` reports real face coverage. Use `getRenderableFallbackForFace` for styled text; a result may include `faceSource`, where `synthetic` means render from the indicated face and let your renderer synthesize the requested style.

## Fidelity fields

Expand All @@ -80,3 +80,5 @@ Some fallbacks are face-scoped. Use `getRenderableFallbackForFace`, or respect t
## Provenance

Measurements are produced against licensed originals. This package distributes no proprietary binaries, raw proprietary metrics, or font files.

`candidateLicense` is a candidate license id or expression: SPDX when exact, stable docfonts label otherwise.
143 changes: 123 additions & 20 deletions packages/fallbacks/fallbacks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ describe("getFallbackDecision", () => {
});

test("a measured 'no open font' is distinct from an unknown font", () => {
// The whole point of the decision API: docfonts MEASURED Aptos and Tahoma; it never heard of "Foo".
// The whole point of the decision API: docfonts MEASURED Aptos and Verdana; it never heard of "Foo".
expect(getFallbackDecision("Aptos")).toEqual({
kind: "customer_supplied",
evidenceId: "aptos",
generic: "sans-serif",
});
expect(getFallbackDecision("Tahoma")).toEqual({
expect(getFallbackDecision("Verdana")).toEqual({
kind: "no_recommended_fallback",
evidenceId: "tahoma",
evidenceId: "verdana",
generic: "sans-serif",
});
expect(getFallbackDecision("Cambria Math")).toEqual({
Expand Down Expand Up @@ -158,6 +158,20 @@ describe("normalizeFamilyName (public)", () => {
describe("face-aware lookups (Regular-only safety)", () => {
const renderAll = { canRenderFamily: () => true };

test("synthetic face sources stay separate from real face coverage", () => {
for (const row of SUBSTITUTION_EVIDENCE) {
if (!row.faceSources) continue;
for (const face of ["regular", "bold", "italic", "boldItalic"] as const) {
const source = row.faceSources[face];
if (source?.kind !== "synthetic") continue;
expect(row.faces[face], `${row.evidenceId} ${face}`).toBe(false);
expect(row.faceVerdicts?.[face], `${row.evidenceId} ${face}`).toBe(
"visual_only",
);
}
}
});

test("every FontFallback now carries the substitute's face coverage", () => {
// The family-level result is self-describing, so a map consumer can route per-face.
expect(
Expand Down Expand Up @@ -300,6 +314,87 @@ describe("glyphExceptions projection", () => {
});
});

describe("selected visual fallback rows", () => {
const renderAll = { canRenderFamily: () => true };

test("newly reviewed visual rows resolve to their selected families without claiming line-break safety", () => {
const expected = [
["Arial Black", "Archivo Black", "substitute"],
["Arial Rounded MT Bold", "Ubuntu", "category_fallback"],
["Bookman Old Style", "TeX Gyre Bonum", "substitute"],
["Century", "C059", "substitute"],
["Comic Sans MS", "Comic Relief", "category_fallback"],
["Garamond", "Cardo", "category_fallback"],
["Gill Sans MT Condensed", "PT Sans Narrow", "category_fallback"],
["Tahoma", "Noto Sans", "category_fallback"],
["Trebuchet MS", "PT Sans", "category_fallback"],
] as const;

for (const [logical, physical, policyAction] of expected) {
expect(getRenderableFallback(logical, renderAll), logical).toMatchObject({
substituteFamily: physical,
policyAction,
verdict: "visual_only",
lineBreakSafe: false,
});
}
});

test("rows with synthetic selected faces expose render instructions without marking those faces real", () => {
expect(
getRenderableFallbackForFace("Arial Black", "italic", renderAll),
).toMatchObject({
substituteFamily: "Archivo Black",
faceSource: { kind: "synthetic", from: "regular" },
faces: { regular: true, bold: false, italic: false, boldItalic: false },
});
expect(
getFallbackDecisionForFace("Arial Black", "bold", renderAll).kind,
).toBe("face_missing");

expect(
getRenderableFallbackForFace("Comic Sans MS", "boldItalic", renderAll),
).toMatchObject({
substituteFamily: "Comic Relief",
faceSource: { kind: "synthetic", from: "bold" },
faces: { regular: true, bold: true, italic: false, boldItalic: false },
});

expect(
getRenderableFallbackForFace(
"Gill Sans MT Condensed",
"italic",
renderAll,
),
).toMatchObject({
substituteFamily: "PT Sans Narrow",
faceSource: { kind: "synthetic", from: "regular" },
faces: { regular: true, bold: true, italic: false, boldItalic: false },
});
});
});

describe("candidate license metadata", () => {
test("uses public license identifiers or stable docfonts labels", () => {
const LICENSE_IDS = new Set([
"AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817",
"Apache-2.0",
"GPLv2-with-font-exception",
"GUST-Font-License-1.0",
"OFL-1.1",
"Ubuntu-font-1.0",
]);

for (const row of SUBSTITUTION_EVIDENCE) {
if (!row.candidateLicense) continue;
expect(
LICENSE_IDS.has(row.candidateLicense),
`${row.evidenceId} (${row.candidateLicense})`,
).toBe(true);
}
});
});

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

Expand Down Expand Up @@ -348,42 +443,45 @@ describe("advance measurement basis", () => {
});
});

describe("Cooper Black -> Caprasimo (Regular-only, metric_safe)", () => {
describe("Cooper Black -> Caprasimo (real Regular plus synthetic faces)", () => {
const renderAll = { canRenderFamily: () => true };
const onlyCaprasimo = { canRenderFamily: (f: string) => f === "Caprasimo" };

test("the family resolves to Caprasimo as an exact, line-break-safe Regular substitute", () => {
test("the family resolves to Caprasimo with Regular-only real face coverage", () => {
// Unlike Baskerville -> Bacasime (visual_only, NBSP reflows), Cooper measures 0% across the Latin
// core, so the row is metric_safe with no glyph exceptions.
// core for Regular. Styled faces are synthetic and intentionally roll the family to visual_only.
expect(getRenderableFallback("Cooper Black", renderAll)).toEqual({
substituteFamily: "Caprasimo",
policyAction: "substitute",
verdict: "metric_safe",
lineBreakSafe: true,
verdict: "visual_only",
lineBreakSafe: false,
faces: { regular: true, bold: false, italic: false, boldItalic: false },
evidenceId: "cooper-black",
generic: "serif",
});
});

test("Regular maps; bold/italic/boldItalic are face_missing (never faux-styled onto Caprasimo)", () => {
test("Regular maps as metric_safe; bold/italic/boldItalic map as synthetic visual_only faces", () => {
expect(
getRenderableFallbackForFace("Cooper Black", "regular", renderAll),
).toMatchObject({
substituteFamily: "Caprasimo",
verdict: "metric_safe",
lineBreakSafe: true,
});
expect(
getRenderableFallbackForFace("Cooper Black", "regular", renderAll)
?.substituteFamily,
).toBe("Caprasimo");
?.faceSource,
).toBeUndefined();
for (const face of ["bold", "italic", "boldItalic"] as const) {
expect(
getRenderableFallbackForFace("Cooper Black", face, renderAll),
`Cooper Black ${face}`,
).toBeNull();
expect(
getFallbackDecisionForFace("Cooper Black", face, renderAll),
`Cooper Black ${face} decision`,
).toEqual({
kind: "face_missing",
).toMatchObject({
substituteFamily: "Caprasimo",
evidenceId: "cooper-black",
generic: "serif",
verdict: "visual_only",
lineBreakSafe: false,
faceSource: { kind: "synthetic", from: "regular" },
});
}
});
Expand All @@ -395,13 +493,18 @@ describe("Cooper Black -> Caprasimo (Regular-only, metric_safe)", () => {
).toEqual({
kind: "asset_missing",
substituteFamily: "Caprasimo",
verdict: "metric_safe",
verdict: "visual_only",
evidenceId: "cooper-black",
generic: "serif",
});
expect(
getRenderableFallbackForFace("Cooper Black", "regular", onlyCaprasimo)
?.substituteFamily,
).toBe("Caprasimo");
expect(
getFallbackDecisionForFace("Cooper Black", "bold", {
canRenderFamily: () => false,
}).kind,
).toBe("asset_missing");
});
});
Loading
Loading