@docfonts/fallbacks 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,7 @@ const fallback = getRenderableFallback("Helvetica", {
25
25
  canRenderFamily: (family) => bundledFamilies.has(family),
26
26
  });
27
27
 
28
- // { substituteFamily: "Liberation Sans", policyAction: "substitute", verdict: "metric_safe", lineBreakSafe: true, evidenceId: "helvetica" }
28
+ // { substituteFamily: "Liberation Sans", policyAction: "substitute", verdict: "metric_safe", lineBreakSafe: true, evidenceId: "helvetica", generic: "sans-serif" }
29
29
  ```
30
30
 
31
31
  The result is `null` when there is nothing renderable from your available assets. Use `getFallbackDecision` when you need to know why.
@@ -38,10 +38,10 @@ Use `getFallbackDecision` for UI, diagnostics, and reporting. It distinguishes k
38
38
  import { getFallbackDecision } from "@docfonts/fallbacks";
39
39
 
40
40
  getFallbackDecision("Aptos");
41
- // { kind: "customer_supplied", evidenceId: "aptos" }
41
+ // { kind: "customer_supplied", evidenceId: "aptos", generic: "sans-serif" }
42
42
 
43
43
  getFallbackDecision("Tahoma");
44
- // { kind: "no_recommended_fallback", evidenceId: "tahoma" }
44
+ // { kind: "no_recommended_fallback", evidenceId: "tahoma", generic: "sans-serif" }
45
45
 
46
46
  getFallbackDecision("Made Up Font");
47
47
  // { kind: "unknown" }
@@ -49,7 +49,7 @@ getFallbackDecision("Made Up Font");
49
49
  getFallbackDecision("Georgia", {
50
50
  canRenderFamily: (family) => bundledFamilies.has(family),
51
51
  });
52
- // { kind: "asset_missing", substituteFamily: "Gelasio", verdict: "near_metric", evidenceId: "georgia" }
52
+ // { kind: "asset_missing", substituteFamily: "Gelasio", verdict: "near_metric", evidenceId: "georgia", generic: "serif" }
53
53
  ```
54
54
 
55
55
  Decision kinds:
@@ -86,6 +86,7 @@ Keys are normalized. Use `normalizeFamilyName` for lookups. Rows whose substitut
86
86
  - `lineBreakSafe` - true when advances preserve line breaks: `metric_safe`, `near_metric`, or monospace `cell_width_only`.
87
87
  - `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.
88
88
  - `evidenceId` - the stable id for the reviewed evidence row; look the full row up in `SUBSTITUTION_EVIDENCE`.
89
+ - `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.
89
90
  - `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.
90
91
 
91
92
  `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.
package/dist/data.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export const SUBSTITUTION_EVIDENCE = [
2
2
  {
3
3
  "evidenceId": "calibri",
4
+ "generic": "sans-serif",
4
5
  "logicalFamily": "Calibri",
5
6
  "physicalFamily": "Carlito",
6
7
  "verdict": "metric_safe",
@@ -30,6 +31,7 @@ export const SUBSTITUTION_EVIDENCE = [
30
31
  },
31
32
  {
32
33
  "evidenceId": "cambria",
34
+ "generic": "serif",
33
35
  "logicalFamily": "Cambria",
34
36
  "physicalFamily": "Caladea",
35
37
  "verdict": "visual_only",
@@ -75,6 +77,7 @@ export const SUBSTITUTION_EVIDENCE = [
75
77
  },
76
78
  {
77
79
  "evidenceId": "arial",
80
+ "generic": "sans-serif",
78
81
  "logicalFamily": "Arial",
79
82
  "physicalFamily": "Liberation Sans",
80
83
  "verdict": "metric_safe",
@@ -103,6 +106,7 @@ export const SUBSTITUTION_EVIDENCE = [
103
106
  },
104
107
  {
105
108
  "evidenceId": "times-new-roman",
109
+ "generic": "serif",
106
110
  "logicalFamily": "Times New Roman",
107
111
  "physicalFamily": "Liberation Serif",
108
112
  "verdict": "metric_safe",
@@ -131,6 +135,7 @@ export const SUBSTITUTION_EVIDENCE = [
131
135
  },
132
136
  {
133
137
  "evidenceId": "courier-new",
138
+ "generic": "monospace",
134
139
  "logicalFamily": "Courier New",
135
140
  "physicalFamily": "Liberation Mono",
136
141
  "verdict": "metric_safe",
@@ -159,6 +164,7 @@ export const SUBSTITUTION_EVIDENCE = [
159
164
  },
160
165
  {
161
166
  "evidenceId": "georgia",
167
+ "generic": "serif",
162
168
  "logicalFamily": "Georgia",
163
169
  "physicalFamily": "Gelasio",
164
170
  "verdict": "near_metric",
@@ -214,6 +220,7 @@ export const SUBSTITUTION_EVIDENCE = [
214
220
  },
215
221
  {
216
222
  "evidenceId": "arial-narrow",
223
+ "generic": "sans-serif",
217
224
  "logicalFamily": "Arial Narrow",
218
225
  "physicalFamily": "Liberation Sans Narrow",
219
226
  "verdict": "visual_only",
@@ -259,6 +266,7 @@ export const SUBSTITUTION_EVIDENCE = [
259
266
  },
260
267
  {
261
268
  "evidenceId": "aptos",
269
+ "generic": "sans-serif",
262
270
  "logicalFamily": "Aptos",
263
271
  "physicalFamily": null,
264
272
  "verdict": "no_substitute",
@@ -283,6 +291,7 @@ export const SUBSTITUTION_EVIDENCE = [
283
291
  },
284
292
  {
285
293
  "evidenceId": "consolas",
294
+ "generic": "monospace",
286
295
  "logicalFamily": "Consolas",
287
296
  "physicalFamily": "Inconsolata SemiExpanded",
288
297
  "verdict": "cell_width_only",
@@ -311,6 +320,7 @@ export const SUBSTITUTION_EVIDENCE = [
311
320
  },
312
321
  {
313
322
  "evidenceId": "verdana",
323
+ "generic": "sans-serif",
314
324
  "logicalFamily": "Verdana",
315
325
  "physicalFamily": null,
316
326
  "verdict": "visual_only",
@@ -335,6 +345,7 @@ export const SUBSTITUTION_EVIDENCE = [
335
345
  },
336
346
  {
337
347
  "evidenceId": "tahoma",
348
+ "generic": "sans-serif",
338
349
  "logicalFamily": "Tahoma",
339
350
  "physicalFamily": null,
340
351
  "verdict": "visual_only",
@@ -359,6 +370,7 @@ export const SUBSTITUTION_EVIDENCE = [
359
370
  },
360
371
  {
361
372
  "evidenceId": "trebuchet-ms",
373
+ "generic": "sans-serif",
362
374
  "logicalFamily": "Trebuchet MS",
363
375
  "physicalFamily": null,
364
376
  "verdict": "visual_only",
@@ -383,6 +395,7 @@ export const SUBSTITUTION_EVIDENCE = [
383
395
  },
384
396
  {
385
397
  "evidenceId": "comic-sans-ms",
398
+ "generic": "sans-serif",
386
399
  "logicalFamily": "Comic Sans MS",
387
400
  "physicalFamily": "Comic Neue",
388
401
  "verdict": "visual_only",
@@ -411,6 +424,7 @@ export const SUBSTITUTION_EVIDENCE = [
411
424
  },
412
425
  {
413
426
  "evidenceId": "candara",
427
+ "generic": "sans-serif",
414
428
  "logicalFamily": "Candara",
415
429
  "physicalFamily": null,
416
430
  "verdict": "visual_only",
@@ -435,6 +449,7 @@ export const SUBSTITUTION_EVIDENCE = [
435
449
  },
436
450
  {
437
451
  "evidenceId": "constantia",
452
+ "generic": "serif",
438
453
  "logicalFamily": "Constantia",
439
454
  "physicalFamily": null,
440
455
  "verdict": "visual_only",
@@ -459,6 +474,7 @@ export const SUBSTITUTION_EVIDENCE = [
459
474
  },
460
475
  {
461
476
  "evidenceId": "corbel",
477
+ "generic": "sans-serif",
462
478
  "logicalFamily": "Corbel",
463
479
  "physicalFamily": null,
464
480
  "verdict": "visual_only",
@@ -483,6 +499,7 @@ export const SUBSTITUTION_EVIDENCE = [
483
499
  },
484
500
  {
485
501
  "evidenceId": "lucida-console",
502
+ "generic": "monospace",
486
503
  "logicalFamily": "Lucida Console",
487
504
  "physicalFamily": "Cousine",
488
505
  "verdict": "cell_width_only",
@@ -511,6 +528,7 @@ export const SUBSTITUTION_EVIDENCE = [
511
528
  },
512
529
  {
513
530
  "evidenceId": "aptos-display",
531
+ "generic": "sans-serif",
514
532
  "logicalFamily": "Aptos Display",
515
533
  "physicalFamily": null,
516
534
  "verdict": "customer_supplied",
@@ -532,6 +550,7 @@ export const SUBSTITUTION_EVIDENCE = [
532
550
  },
533
551
  {
534
552
  "evidenceId": "cambria-math",
553
+ "generic": "serif",
535
554
  "logicalFamily": "Cambria Math",
536
555
  "physicalFamily": null,
537
556
  "verdict": "preserve_only",
@@ -553,6 +572,7 @@ export const SUBSTITUTION_EVIDENCE = [
553
572
  },
554
573
  {
555
574
  "evidenceId": "helvetica",
575
+ "generic": "sans-serif",
556
576
  "logicalFamily": "Helvetica",
557
577
  "physicalFamily": "Liberation Sans",
558
578
  "verdict": "metric_safe",
@@ -581,6 +601,7 @@ export const SUBSTITUTION_EVIDENCE = [
581
601
  },
582
602
  {
583
603
  "evidenceId": "calibri-light",
604
+ "generic": "sans-serif",
584
605
  "logicalFamily": "Calibri Light",
585
606
  "physicalFamily": "Carlito",
586
607
  "verdict": "visual_only",
@@ -609,6 +630,7 @@ export const SUBSTITUTION_EVIDENCE = [
609
630
  },
610
631
  {
611
632
  "evidenceId": "baskerville-old-face",
633
+ "generic": "serif",
612
634
  "logicalFamily": "Baskerville Old Face",
613
635
  "physicalFamily": "Bacasime Antique",
614
636
  "verdict": "visual_only",
@@ -645,5 +667,37 @@ export const SUBSTITUTION_EVIDENCE = [
645
667
  "note": "Bacasime Antique Regular's no-break space (U+00A0) advance diverges ~49% from Baskerville Old Face; lines containing NBSP reflow. Every other Latin-core glyph is advance-identical, which is why this is visual_only with a single named exception, not near_metric."
646
668
  }
647
669
  ]
670
+ },
671
+ {
672
+ "evidenceId": "cooper-black",
673
+ "generic": "serif",
674
+ "logicalFamily": "Cooper Black",
675
+ "physicalFamily": "Caprasimo",
676
+ "verdict": "metric_safe",
677
+ "faces": {
678
+ "regular": true,
679
+ "bold": false,
680
+ "italic": false,
681
+ "boldItalic": false
682
+ },
683
+ "gates": {
684
+ "static": "pass",
685
+ "metric": "pass",
686
+ "layout": "not_run",
687
+ "ship": "not_run"
688
+ },
689
+ "policyAction": "substitute",
690
+ "measurementRefs": [
691
+ "cooper-black_regular__caprasimo#regular#w400#786ab84e#analytic_advance#2026-06-05"
692
+ ],
693
+ "exportRule": "preserve_original_name",
694
+ "advance": {
695
+ "meanDelta": 0,
696
+ "maxDelta": 0
697
+ },
698
+ "candidateLicense": "OFL-1.1",
699
+ "faceVerdicts": {
700
+ "regular": "metric_safe"
701
+ }
648
702
  }
649
703
  ];
package/dist/fallbacks.js CHANGED
@@ -52,6 +52,7 @@ function buildFallback(row, physicalFamily, verdict, faceSlot) {
52
52
  lineBreakSafe: LINE_BREAK_SAFE_VERDICTS.has(verdict),
53
53
  faces: row.faces,
54
54
  evidenceId: row.evidenceId,
55
+ generic: row.generic,
55
56
  ...(glyphExceptions && glyphExceptions.length > 0
56
57
  ? { glyphExceptions }
57
58
  : {}),
@@ -59,16 +60,16 @@ function buildFallback(row, physicalFamily, verdict, faceSlot) {
59
60
  }
60
61
  /** Decide a single row against the consumer's asset availability. Pure. */
61
62
  function decideRow(row, canRenderFamily) {
62
- const { policyAction, physicalFamily, verdict, evidenceId } = row;
63
+ const { policyAction, physicalFamily, verdict, evidenceId, generic } = row;
63
64
  // Deliberate non-substitution policies first: nothing renders in the original's place.
64
65
  if (policyAction === "preserve_only")
65
- return { kind: "preserve_only", evidenceId };
66
+ return { kind: "preserve_only", evidenceId, generic };
66
67
  if (policyAction === "customer_supplied")
67
- return { kind: "customer_supplied", evidenceId };
68
+ return { kind: "customer_supplied", evidenceId, generic };
68
69
  // substitute / category_fallback with no named open family: docfonts knows the font but recommends
69
70
  // no renderable family - distinct from the `no_substitute` verdict (read the row for that nuance).
70
71
  if (physicalFamily === null)
71
- return { kind: "no_recommended_fallback", evidenceId };
72
+ return { kind: "no_recommended_fallback", evidenceId, generic };
72
73
  // Named substitute the consumer does not bundle: surfaced so a UI can say which font to add.
73
74
  if (canRenderFamily && !canRenderFamily(physicalFamily))
74
75
  return {
@@ -76,6 +77,7 @@ function decideRow(row, canRenderFamily) {
76
77
  substituteFamily: physicalFamily,
77
78
  verdict,
78
79
  evidenceId,
80
+ generic,
79
81
  };
80
82
  return {
81
83
  kind: "fallback",
@@ -106,6 +108,7 @@ function decideRowForFace(row, face, canRenderFamily) {
106
108
  kind: "face_missing",
107
109
  substituteFamily: base.fallback.substituteFamily,
108
110
  evidenceId: row.evidenceId,
111
+ generic: row.generic,
109
112
  };
110
113
  const faceVerdict = row.faceVerdicts?.[face] ?? row.verdict;
111
114
  return {
package/dist/index.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  */
5
5
  export { SUBSTITUTION_EVIDENCE } from "./data.js";
6
6
  export { type CanRenderFamily, createFallbackMap, type FallbackDecisionOptions, getFallbackDecision, getFallbackDecisionForFace, getRenderableFallback, getRenderableFallbackForFace, normalizeFamilyName, type RenderableFallbackOptions, } from "./fallbacks.js";
7
- export type { AdvanceDelta, FaceCoverage, FaceSlot, FallbackDecision, FontFallback, GlyphException, PolicyAction, SubstituteGates, SubstitutionEvidence, Verdict, } from "./types.js";
7
+ export type { AdvanceDelta, CssGeneric, FaceCoverage, FaceSlot, FallbackDecision, FontFallback, GlyphException, PolicyAction, SubstituteGates, SubstitutionEvidence, Verdict, } from "./types.js";
package/dist/types.d.ts CHANGED
@@ -10,6 +10,12 @@ export type PolicyAction = "substitute" | "category_fallback" | "preserve_only"
10
10
  export type GateStatus = "pass" | "not_run" | "fail";
11
11
  /** RIBBI face slot - the renderer's coarse face bucket. */
12
12
  export type FaceSlot = "regular" | "bold" | "italic" | "boldItalic";
13
+ /**
14
+ * CSS generic family for the logical font: the broad category a renderer can drop in as a
15
+ * last-resort `font-family` keyword when no named substitute renders. Only the categories consumers
16
+ * currently need.
17
+ */
18
+ export type CssGeneric = "serif" | "sans-serif" | "monospace";
13
19
  /** Advance-width divergence vs the proprietary oracle, as fractions (0 = identical advances). */
14
20
  export interface AdvanceDelta {
15
21
  meanDelta: number;
@@ -46,6 +52,8 @@ export interface SubstitutionEvidence {
46
52
  evidenceId: string;
47
53
  /** the proprietary family the document asks for, e.g. "Cambria". */
48
54
  logicalFamily: string;
55
+ /** the logical font's broad CSS category, for a last-resort generic `font-family` keyword. */
56
+ generic: CssGeneric;
49
57
  /** the physical substitute rendered in its place; null when no candidate is recommended. */
50
58
  physicalFamily: string | null;
51
59
  /** worst-face fidelity verdict (the public summary; see `faceVerdicts` when faces disagree). */
@@ -99,6 +107,8 @@ export interface FontFallback {
99
107
  faces: FaceCoverage;
100
108
  /** stable reviewed-evidence id; look the full row up in {@link SUBSTITUTION_EVIDENCE}. */
101
109
  evidenceId: string;
110
+ /** the logical font's broad CSS category, for a last-resort generic `font-family` keyword. */
111
+ generic: CssGeneric;
102
112
  /**
103
113
  * Named glyph-level divergences that qualify this fallback (e.g. one codepoint reflows). Scoped to
104
114
  * the lookup: a family lookup ({@link getRenderableFallback}) carries ALL of the row's exceptions; a
@@ -115,7 +125,9 @@ export interface FontFallback {
115
125
  * but the consumer does not bundle it (`asset_missing`), and the deliberate non-substitution policies
116
126
  * (`preserve_only`, `customer_supplied`). The face-aware lookups add `face_missing`: a substitute is
117
127
  * recommended for the family but does NOT provide the requested face. `evidenceId` on the terminal
118
- * kinds points back into {@link SUBSTITUTION_EVIDENCE} for the full row (verdict, faces, ...).
128
+ * kinds points back into {@link SUBSTITUTION_EVIDENCE} for the full row (verdict, faces, ...). The
129
+ * known (non-`unknown`) kinds also carry the logical font's `generic`, so a consumer with no
130
+ * renderable substitute can still emit a same-category generic `font-family` keyword.
119
131
  */
120
132
  export type FallbackDecision = {
121
133
  kind: "fallback";
@@ -125,21 +137,26 @@ export type FallbackDecision = {
125
137
  substituteFamily: string;
126
138
  verdict: Verdict;
127
139
  evidenceId: string;
140
+ generic: CssGeneric;
128
141
  } | {
129
142
  /** the family has a renderable substitute, but it does not provide the requested face - route
130
143
  * this face through face-aware absence handling, do NOT substitute it. */
131
144
  kind: "face_missing";
132
145
  substituteFamily: string;
133
146
  evidenceId: string;
147
+ generic: CssGeneric;
134
148
  } | {
135
149
  kind: "no_recommended_fallback";
136
150
  evidenceId: string;
151
+ generic: CssGeneric;
137
152
  } | {
138
153
  kind: "customer_supplied";
139
154
  evidenceId: string;
155
+ generic: CssGeneric;
140
156
  } | {
141
157
  kind: "preserve_only";
142
158
  evidenceId: string;
159
+ generic: CssGeneric;
143
160
  } | {
144
161
  kind: "unknown";
145
162
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docfonts/fallbacks",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Measured open-font fallbacks for proprietary document fonts.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,6 +35,8 @@
35
35
  "directory": "packages/fallbacks"
36
36
  },
37
37
  "scripts": {
38
+ "gen:data": "bun run scripts/generate-data.ts",
39
+ "acquire": "bun run scripts/acquire.ts",
38
40
  "build": "tsc -p tsconfig.build.json",
39
41
  "prepack": "bun run build"
40
42
  },