@docfonts/fallbacks 0.1.0 → 0.3.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 +80 -25
- package/dist/data.d.ts +1 -1
- package/dist/fallbacks.d.ts +39 -20
- package/dist/fallbacks.js +131 -38
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/types.d.ts +63 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Document font substitution, measured.
|
|
4
4
|
|
|
5
|
-
Measured open-font fallbacks for proprietary document fonts
|
|
5
|
+
Measured open-font fallbacks for proprietary document fonts. Use it to decide whether a requested document font can render with an open family you actually ship.
|
|
6
6
|
|
|
7
|
-
It ships no fonts and no proprietary binaries
|
|
7
|
+
It ships no fonts and no proprietary binaries. It ships decisions: the recommended open family when one exists, the fidelity verdict, and the honest cases where no open family should be used.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -12,46 +12,101 @@ It ships no fonts and no proprietary binaries: only the measured evidence (which
|
|
|
12
12
|
npm install @docfonts/fallbacks
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
ESM-only. Use `import`, or let your bundler handle it. CommonJS `require()` is not supported.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## Render A Font
|
|
18
|
+
|
|
19
|
+
Use `getRenderableFallback` when you need one font family to render now. Pass `canRenderFamily` so docfonts only returns families your app can load.
|
|
18
20
|
|
|
19
21
|
```ts
|
|
20
|
-
import {
|
|
22
|
+
import { getRenderableFallback } from "@docfonts/fallbacks";
|
|
23
|
+
|
|
24
|
+
const fallback = getRenderableFallback("Helvetica", {
|
|
25
|
+
canRenderFamily: (family) => bundledFamilies.has(family),
|
|
26
|
+
});
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
// { family: "Liberation Sans", action: "substitute", verdict: "metric_safe", faithful: true, evidenceId: "helvetica" }
|
|
28
|
+
// { substituteFamily: "Liberation Sans", policyAction: "substitute", verdict: "metric_safe", lineBreakSafe: true, evidenceId: "helvetica" }
|
|
24
29
|
```
|
|
25
30
|
|
|
26
|
-
`
|
|
31
|
+
The result is `null` when there is nothing renderable from your available assets. Use `getFallbackDecision` when you need to know why.
|
|
32
|
+
|
|
33
|
+
## Explain A Decision
|
|
34
|
+
|
|
35
|
+
Use `getFallbackDecision` for UI, diagnostics, and reporting. It distinguishes known fonts with no recommended fallback from fonts docfonts has never seen.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { getFallbackDecision } from "@docfonts/fallbacks";
|
|
39
|
+
|
|
40
|
+
getFallbackDecision("Aptos");
|
|
41
|
+
// { kind: "customer_supplied", evidenceId: "aptos" }
|
|
42
|
+
|
|
43
|
+
getFallbackDecision("Tahoma");
|
|
44
|
+
// { kind: "no_recommended_fallback", evidenceId: "tahoma" }
|
|
45
|
+
|
|
46
|
+
getFallbackDecision("Made Up Font");
|
|
47
|
+
// { kind: "unknown" }
|
|
48
|
+
|
|
49
|
+
getFallbackDecision("Georgia", {
|
|
50
|
+
canRenderFamily: (family) => bundledFamilies.has(family),
|
|
51
|
+
});
|
|
52
|
+
// { kind: "asset_missing", substituteFamily: "Gelasio", verdict: "near_metric", evidenceId: "georgia" }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Decision kinds:
|
|
56
|
+
|
|
57
|
+
- `fallback` - render the returned `substituteFamily`.
|
|
58
|
+
- `asset_missing` - docfonts has a fallback, but your app does not load that family.
|
|
59
|
+
- `face_missing` - (face-aware lookups only) the family has a substitute, but not for the requested face. Route that face through your absence handling; do not substitute it.
|
|
60
|
+
- `no_recommended_fallback` - docfonts knows the font but recommends no renderable open family.
|
|
61
|
+
- `customer_supplied` - the real font should come from the customer or environment.
|
|
62
|
+
- `preserve_only` - keep the original family name. Do not substitute.
|
|
63
|
+
- `unknown` - docfonts has no evidence for this family.
|
|
64
|
+
|
|
65
|
+
## Create A Resolver Map
|
|
66
|
+
|
|
67
|
+
Use `createFallbackMap` when wiring a resolver. `canRenderFamily` is required because a resolver map must never point at fonts you cannot load.
|
|
27
68
|
|
|
28
69
|
```ts
|
|
29
|
-
import {
|
|
70
|
+
import { createFallbackMap, normalizeFamilyName } from "@docfonts/fallbacks";
|
|
30
71
|
|
|
31
|
-
const map =
|
|
32
|
-
|
|
33
|
-
|
|
72
|
+
const map = createFallbackMap({
|
|
73
|
+
canRenderFamily: (family) => bundledFamilies.has(family),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
map[normalizeFamilyName("Times New Roman")]; // { substituteFamily: "Liberation Serif", ... }
|
|
34
77
|
```
|
|
35
78
|
|
|
36
|
-
|
|
79
|
+
Keys are normalized. Use `normalizeFamilyName` for lookups. Rows whose substitute family is not available are omitted. Each entry carries `faces`: a Regular-only entry is only safe in a **face-aware** resolver (one that checks `faces` or uses `getRenderableFallbackForFace`), since applying it to bold/italic would route a face the substitute does not provide.
|
|
37
80
|
|
|
38
81
|
## What the fields mean
|
|
39
82
|
|
|
40
|
-
- `
|
|
41
|
-
- `
|
|
42
|
-
- `verdict` - the measured fidelity
|
|
43
|
-
- `
|
|
44
|
-
- `
|
|
83
|
+
- `substituteFamily` - the open family to render in place of the requested one.
|
|
84
|
+
- `policyAction` - what a renderer should do, not a quality claim. Use `verdict` for fidelity.
|
|
85
|
+
- `verdict` - the measured fidelity. Examples: `metric_safe`, `near_metric`, `cell_width_only`, `visual_only`.
|
|
86
|
+
- `lineBreakSafe` - true when advances preserve line breaks: `metric_safe`, `near_metric`, or monospace `cell_width_only`.
|
|
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
|
+
- `evidenceId` - the stable id for the reviewed evidence row; look the full row up in `SUBSTITUTION_EVIDENCE`.
|
|
45
89
|
|
|
46
|
-
|
|
90
|
+
`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.
|
|
47
91
|
|
|
48
|
-
##
|
|
92
|
+
## Face-aware routing (Regular-only substitutes)
|
|
49
93
|
|
|
50
|
-
The
|
|
51
|
-
but this package distributes no proprietary binaries or raw proprietary metrics.
|
|
94
|
+
Some substitutes provide only some faces - e.g. Baskerville Old Face -> Bacasime Antique is Regular-only. The family-level helpers above answer "which family", and every result carries `faces`, so a resolver must route per-face. The face-aware helpers do it for you:
|
|
52
95
|
|
|
53
|
-
|
|
96
|
+
```ts
|
|
97
|
+
import { getRenderableFallbackForFace } from "@docfonts/fallbacks";
|
|
98
|
+
const opts = { canRenderFamily: (family) => bundledFamilies.has(family) };
|
|
54
99
|
|
|
55
|
-
|
|
100
|
+
getRenderableFallbackForFace("Baskerville Old Face", "regular", opts)?.substituteFamily; // "Bacasime Antique"
|
|
101
|
+
getRenderableFallbackForFace("Baskerville Old Face", "bold", opts); // null (Regular-only)
|
|
102
|
+
```
|
|
56
103
|
|
|
57
|
-
|
|
104
|
+
`getFallbackDecisionForFace(family, face, options)` reports the reason - `face_missing` when the substitute exists but lacks that face. A covered face carries its OWN verdict, not the family's worst-face rollup (e.g. `Cambria` regular is `metric_safe` even though the family rolls up to `visual_only`).
|
|
105
|
+
|
|
106
|
+
The full structured rows are exported as `SUBSTITUTION_EVIDENCE` for richer reporting (faces, per-face verdicts, glyph exceptions).
|
|
107
|
+
|
|
108
|
+
## Provenance
|
|
109
|
+
|
|
110
|
+
The data comes from reviewed docfonts evidence. Measurements are produced against licensed originals, but this package distributes no proprietary binaries or raw proprietary metrics.
|
|
111
|
+
|
|
112
|
+
Built by the team behind SuperDoc. Standalone and neutral.
|
package/dist/data.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { SubstitutionEvidence } from "./types";
|
|
1
|
+
import type { SubstitutionEvidence } from "./types.js";
|
|
2
2
|
export declare const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[];
|
package/dist/fallbacks.d.ts
CHANGED
|
@@ -1,27 +1,46 @@
|
|
|
1
|
-
import type { FontFallback } from "./types";
|
|
1
|
+
import type { FaceSlot, FallbackDecision, FontFallback } from "./types.js";
|
|
2
2
|
/** Reports whether the consumer can actually render (i.e. bundles the asset for) a physical family. */
|
|
3
|
-
export type
|
|
4
|
-
/** Options for {@link
|
|
5
|
-
export interface
|
|
6
|
-
|
|
7
|
-
* When given, a row whose `physicalFamily` is not available resolves to null - the row stays inert
|
|
8
|
-
* until the consumer bundles it. When omitted, every row with a physical family is considered present.
|
|
9
|
-
*/
|
|
10
|
-
hasFamily?: HasFamily;
|
|
3
|
+
export type CanRenderFamily = (family: string) => boolean;
|
|
4
|
+
/** Options for {@link getRenderableFallback} and {@link createFallbackMap}: a render map must be asset-safe. */
|
|
5
|
+
export interface RenderableFallbackOptions {
|
|
6
|
+
canRenderFamily: CanRenderFamily;
|
|
11
7
|
}
|
|
12
|
-
/** Options for {@link
|
|
13
|
-
export interface
|
|
14
|
-
|
|
8
|
+
/** Options for {@link getFallbackDecision}. `canRenderFamily` is optional - omit it for the raw decision. */
|
|
9
|
+
export interface FallbackDecisionOptions {
|
|
10
|
+
canRenderFamily?: CanRenderFamily;
|
|
15
11
|
}
|
|
12
|
+
/** Normalize a family name to a lookup key: trim, strip surrounding quotes, lowercase (CSS-name safe). */
|
|
13
|
+
export declare function normalizeFamilyName(name: string): string;
|
|
16
14
|
/**
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
15
|
+
* The full, honest outcome for a requested family: a discriminated union (see {@link FallbackDecision}).
|
|
16
|
+
* Distinguishes an unknown font from a measured "no open substitute", a substitute you do not bundle,
|
|
17
|
+
* and the deliberate non-substitution policies. Case- and quote-insensitive.
|
|
19
18
|
*/
|
|
20
|
-
export declare function
|
|
19
|
+
export declare function getFallbackDecision(family: string, options?: FallbackDecisionOptions): FallbackDecision;
|
|
21
20
|
/**
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* are exactly the families it should remap. For the un-gated single recommendation, use {@link getFallback}.
|
|
21
|
+
* The open family to render for a requested font, or null when there is nothing the consumer can render
|
|
22
|
+
* (no row, no open substitute, a deliberate non-substitution policy, or a substitute it does not
|
|
23
|
+
* bundle). For the reasons behind a null - to report them in a UI - use {@link getFallbackDecision}.
|
|
26
24
|
*/
|
|
27
|
-
export declare function
|
|
25
|
+
export declare function getRenderableFallback(family: string, options: RenderableFallbackOptions): FontFallback | null;
|
|
26
|
+
/**
|
|
27
|
+
* Face-aware outcome for a requested family + RIBBI face. Like {@link getFallbackDecision} but adds
|
|
28
|
+
* `face_missing` when the substitute exists yet does not provide that face (a Regular-only row asked
|
|
29
|
+
* for bold/italic). A covered face's fallback carries that face's own verdict. Case- and quote-insensitive.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getFallbackDecisionForFace(family: string, face: FaceSlot, options?: FallbackDecisionOptions): FallbackDecision;
|
|
32
|
+
/**
|
|
33
|
+
* The open family to render for a requested font AND a specific face, or null when that face has no
|
|
34
|
+
* renderable substitute (face not covered, no row, a non-substitution policy, or not bundled). This is
|
|
35
|
+
* the face-SAFE lookup: a Regular-only substitute returns null for bold/italic instead of being routed
|
|
36
|
+
* to a face it does not have. Use {@link getFallbackDecisionForFace} to report the reason.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getRenderableFallbackForFace(family: string, face: FaceSlot, options: RenderableFallbackOptions): FontFallback | null;
|
|
39
|
+
/**
|
|
40
|
+
* A family-level substitute map: every fallback the consumer can render, keyed by the normalized
|
|
41
|
+
* (lowercased) logical family - normalize lookups with {@link normalizeFamilyName}. Only renderable
|
|
42
|
+
* rows are included. Each entry carries `faces`; a face-scoped (e.g. Regular-only) row is only safe in
|
|
43
|
+
* a FACE-AWARE resolver - one that checks `faces` or uses {@link getRenderableFallbackForFace} - since
|
|
44
|
+
* applying a Regular-only entry to bold/italic would route a face the substitute does not provide.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createFallbackMap(options: RenderableFallbackOptions): Record<string, FontFallback>;
|
package/dist/fallbacks.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Fallback lookups over the reviewed evidence:
|
|
3
|
+
* - getRenderableFallback / getFallbackDecision - family-level ("which family", + the full outcome).
|
|
4
|
+
* - getRenderableFallbackForFace / getFallbackDecisionForFace - face-SAFE: a Regular-only substitute
|
|
5
|
+
* returns null / `face_missing` for bold/italic instead of being wrongly routed to a face it lacks.
|
|
6
|
+
* - createFallbackMap - a family-level resolver map (asset-gated). Each entry carries `faces`, so a
|
|
7
|
+
* consumer can route per-face; for Regular-only rows it MUST, or use the face-aware lookups.
|
|
5
8
|
*/
|
|
6
|
-
import { SUBSTITUTION_EVIDENCE } from "./data";
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
+
import { SUBSTITUTION_EVIDENCE } from "./data.js";
|
|
10
|
+
/**
|
|
11
|
+
* Verdicts whose advances preserve line breaks: the proportional metric-grade bands, plus
|
|
12
|
+
* cell_width_only (a monospace whose cell width - and therefore every advance - matches). Glyph shapes
|
|
13
|
+
* may still differ (read `verdict`); line breaks do not move.
|
|
14
|
+
*/
|
|
15
|
+
const LINE_BREAK_SAFE_VERDICTS = new Set([
|
|
9
16
|
"metric_safe",
|
|
10
17
|
"near_metric",
|
|
11
|
-
|
|
12
|
-
/** Actions that mean "render this physical family". */
|
|
13
|
-
const RENDERABLE_ACTIONS = new Set([
|
|
14
|
-
"substitute",
|
|
15
|
-
"category_fallback",
|
|
18
|
+
"cell_width_only",
|
|
16
19
|
]);
|
|
17
20
|
/** Normalize a family name to a lookup key: trim, strip surrounding quotes, lowercase (CSS-name safe). */
|
|
18
|
-
function
|
|
21
|
+
export function normalizeFamilyName(name) {
|
|
19
22
|
return name
|
|
20
23
|
.trim()
|
|
21
24
|
.replace(/^['"]+|['"]+$/g, "")
|
|
@@ -23,43 +26,133 @@ function normalizeFamily(name) {
|
|
|
23
26
|
.toLowerCase();
|
|
24
27
|
}
|
|
25
28
|
/** Evidence rows indexed by normalized logical family, built once. */
|
|
26
|
-
const BY_LOGICAL = new Map(SUBSTITUTION_EVIDENCE.map((row) => [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const BY_LOGICAL = new Map(SUBSTITUTION_EVIDENCE.map((row) => [
|
|
30
|
+
normalizeFamilyName(row.logicalFamily),
|
|
31
|
+
row,
|
|
32
|
+
]));
|
|
33
|
+
/**
|
|
34
|
+
* Build the FontFallback for a row known to carry a renderable physical family. `verdict` is passed in
|
|
35
|
+
* so a face-aware caller can supply the per-face verdict (faceVerdicts[face]) instead of the worst-face
|
|
36
|
+
* top-level one - e.g. Cambria regular is metric_safe even though the family rolls up to visual_only.
|
|
37
|
+
*/
|
|
38
|
+
function buildFallback(row, physicalFamily, verdict) {
|
|
35
39
|
return {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
verdict
|
|
39
|
-
|
|
40
|
+
substituteFamily: physicalFamily,
|
|
41
|
+
policyAction: row.policyAction,
|
|
42
|
+
verdict,
|
|
43
|
+
lineBreakSafe: LINE_BREAK_SAFE_VERDICTS.has(verdict),
|
|
44
|
+
faces: row.faces,
|
|
40
45
|
evidenceId: row.evidenceId,
|
|
41
46
|
};
|
|
42
47
|
}
|
|
48
|
+
/** Decide a single row against the consumer's asset availability. Pure. */
|
|
49
|
+
function decideRow(row, canRenderFamily) {
|
|
50
|
+
const { policyAction, physicalFamily, verdict, evidenceId } = row;
|
|
51
|
+
// Deliberate non-substitution policies first: nothing renders in the original's place.
|
|
52
|
+
if (policyAction === "preserve_only")
|
|
53
|
+
return { kind: "preserve_only", evidenceId };
|
|
54
|
+
if (policyAction === "customer_supplied")
|
|
55
|
+
return { kind: "customer_supplied", evidenceId };
|
|
56
|
+
// substitute / category_fallback with no named open family: docfonts knows the font but recommends
|
|
57
|
+
// no renderable family - distinct from the `no_substitute` verdict (read the row for that nuance).
|
|
58
|
+
if (physicalFamily === null)
|
|
59
|
+
return { kind: "no_recommended_fallback", evidenceId };
|
|
60
|
+
// Named substitute the consumer does not bundle: surfaced so a UI can say which font to add.
|
|
61
|
+
if (canRenderFamily && !canRenderFamily(physicalFamily))
|
|
62
|
+
return {
|
|
63
|
+
kind: "asset_missing",
|
|
64
|
+
substituteFamily: physicalFamily,
|
|
65
|
+
verdict,
|
|
66
|
+
evidenceId,
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
kind: "fallback",
|
|
70
|
+
fallback: buildFallback(row, physicalFamily, verdict),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** True when a row actually scopes faces (any RIBBI face marked covered). An all-false `faces` means
|
|
74
|
+
* the row is NOT face-scoped - e.g. a category fallback whose physical font does have faces - so it
|
|
75
|
+
* must not be gated per-face, only a measured per-face substitute (Baskerville Regular-only) is. */
|
|
76
|
+
function isFaceScoped(row) {
|
|
77
|
+
const f = row.faces;
|
|
78
|
+
return f.regular || f.bold || f.italic || f.boldItalic;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Face-aware variant of {@link decideRow}: same family-level outcome, but when the family HAS a
|
|
82
|
+
* renderable substitute AND the row is face-scoped, gate on whether it provides the requested `face`.
|
|
83
|
+
* A face a face-scoped substitute does not cover yields `face_missing` (route it through absence
|
|
84
|
+
* handling); a covered face yields a fallback carrying that face's own verdict. A NON-face-scoped row
|
|
85
|
+
* (category fallback, all-false `faces`) renders for any face - it never becomes face_missing.
|
|
86
|
+
*/
|
|
87
|
+
function decideRowForFace(row, face, canRenderFamily) {
|
|
88
|
+
const base = decideRow(row, canRenderFamily);
|
|
89
|
+
// Non-fallback outcomes (asset_missing / no_recommended_fallback / policy) do not depend on the face.
|
|
90
|
+
if (base.kind !== "fallback")
|
|
91
|
+
return base;
|
|
92
|
+
if (isFaceScoped(row) && !row.faces[face])
|
|
93
|
+
return {
|
|
94
|
+
kind: "face_missing",
|
|
95
|
+
substituteFamily: base.fallback.substituteFamily,
|
|
96
|
+
evidenceId: row.evidenceId,
|
|
97
|
+
};
|
|
98
|
+
const faceVerdict = row.faceVerdicts?.[face] ?? row.verdict;
|
|
99
|
+
return {
|
|
100
|
+
kind: "fallback",
|
|
101
|
+
fallback: buildFallback(row, base.fallback.substituteFamily, faceVerdict),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The full, honest outcome for a requested family: a discriminated union (see {@link FallbackDecision}).
|
|
106
|
+
* Distinguishes an unknown font from a measured "no open substitute", a substitute you do not bundle,
|
|
107
|
+
* and the deliberate non-substitution policies. Case- and quote-insensitive.
|
|
108
|
+
*/
|
|
109
|
+
export function getFallbackDecision(family, options = {}) {
|
|
110
|
+
const row = BY_LOGICAL.get(normalizeFamilyName(family));
|
|
111
|
+
return row ? decideRow(row, options.canRenderFamily) : { kind: "unknown" };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The open family to render for a requested font, or null when there is nothing the consumer can render
|
|
115
|
+
* (no row, no open substitute, a deliberate non-substitution policy, or a substitute it does not
|
|
116
|
+
* bundle). For the reasons behind a null - to report them in a UI - use {@link getFallbackDecision}.
|
|
117
|
+
*/
|
|
118
|
+
export function getRenderableFallback(family, options) {
|
|
119
|
+
const decision = getFallbackDecision(family, options);
|
|
120
|
+
return decision.kind === "fallback" ? decision.fallback : null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Face-aware outcome for a requested family + RIBBI face. Like {@link getFallbackDecision} but adds
|
|
124
|
+
* `face_missing` when the substitute exists yet does not provide that face (a Regular-only row asked
|
|
125
|
+
* for bold/italic). A covered face's fallback carries that face's own verdict. Case- and quote-insensitive.
|
|
126
|
+
*/
|
|
127
|
+
export function getFallbackDecisionForFace(family, face, options = {}) {
|
|
128
|
+
const row = BY_LOGICAL.get(normalizeFamilyName(family));
|
|
129
|
+
return row
|
|
130
|
+
? decideRowForFace(row, face, options.canRenderFamily)
|
|
131
|
+
: { kind: "unknown" };
|
|
132
|
+
}
|
|
43
133
|
/**
|
|
44
|
-
* The open
|
|
45
|
-
*
|
|
134
|
+
* The open family to render for a requested font AND a specific face, or null when that face has no
|
|
135
|
+
* renderable substitute (face not covered, no row, a non-substitution policy, or not bundled). This is
|
|
136
|
+
* the face-SAFE lookup: a Regular-only substitute returns null for bold/italic instead of being routed
|
|
137
|
+
* to a face it does not have. Use {@link getFallbackDecisionForFace} to report the reason.
|
|
46
138
|
*/
|
|
47
|
-
export function
|
|
48
|
-
const
|
|
49
|
-
return
|
|
139
|
+
export function getRenderableFallbackForFace(family, face, options) {
|
|
140
|
+
const decision = getFallbackDecisionForFace(family, face, options);
|
|
141
|
+
return decision.kind === "fallback" ? decision.fallback : null;
|
|
50
142
|
}
|
|
51
143
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
144
|
+
* A family-level substitute map: every fallback the consumer can render, keyed by the normalized
|
|
145
|
+
* (lowercased) logical family - normalize lookups with {@link normalizeFamilyName}. Only renderable
|
|
146
|
+
* rows are included. Each entry carries `faces`; a face-scoped (e.g. Regular-only) row is only safe in
|
|
147
|
+
* a FACE-AWARE resolver - one that checks `faces` or uses {@link getRenderableFallbackForFace} - since
|
|
148
|
+
* applying a Regular-only entry to bold/italic would route a face the substitute does not provide.
|
|
56
149
|
*/
|
|
57
|
-
export function
|
|
150
|
+
export function createFallbackMap(options) {
|
|
58
151
|
const out = {};
|
|
59
152
|
for (const [key, row] of BY_LOGICAL) {
|
|
60
|
-
const
|
|
61
|
-
if (fallback)
|
|
62
|
-
out[key] = fallback;
|
|
153
|
+
const decision = decideRow(row, options.canRenderFamily);
|
|
154
|
+
if (decision.kind === "fallback")
|
|
155
|
+
out[key] = decision.fallback;
|
|
63
156
|
}
|
|
64
157
|
return out;
|
|
65
158
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Runtime fallback evidence and asset-aware lookup helpers. No font parser, research data, or runtime
|
|
3
3
|
* dependency.
|
|
4
4
|
*/
|
|
5
|
-
export { SUBSTITUTION_EVIDENCE } from "./data";
|
|
6
|
-
export {
|
|
7
|
-
export type { AdvanceDelta, FaceCoverage, FaceSlot, FontFallback, GlyphException, PolicyAction, SubstituteGates, SubstitutionEvidence, Verdict, } from "./types";
|
|
5
|
+
export { SUBSTITUTION_EVIDENCE } from "./data.js";
|
|
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";
|
package/dist/index.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Runtime fallback evidence and asset-aware lookup helpers. No font parser, research data, or runtime
|
|
3
3
|
* dependency.
|
|
4
4
|
*/
|
|
5
|
-
export { SUBSTITUTION_EVIDENCE } from "./data";
|
|
6
|
-
export {
|
|
5
|
+
export { SUBSTITUTION_EVIDENCE } from "./data.js";
|
|
6
|
+
export { createFallbackMap, getFallbackDecision, getFallbackDecisionForFace, getRenderableFallback, getRenderableFallbackForFace, normalizeFamilyName, } from "./fallbacks.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -38,7 +38,8 @@ export interface GlyphException {
|
|
|
38
38
|
note: string;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* One logical font's structured fallback evidence.
|
|
41
|
+
* One logical font's structured fallback evidence. The raw rows are exported as
|
|
42
|
+
* {@link SUBSTITUTION_EVIDENCE} for reporting; the helpers project the renderer-relevant fields.
|
|
42
43
|
*/
|
|
43
44
|
export interface SubstitutionEvidence {
|
|
44
45
|
/** docfonts evidence id, e.g. "cambria". */
|
|
@@ -65,24 +66,72 @@ export interface SubstitutionEvidence {
|
|
|
65
66
|
exportRule: "preserve_original_name";
|
|
66
67
|
}
|
|
67
68
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* row stays available via {@link SUBSTITUTION_EVIDENCE} for richer reporting.
|
|
69
|
+
* A resolved fallback: which open family to render, how it was chosen, and whether advances preserve
|
|
70
|
+
* line breaks. The full structured row stays available via {@link SUBSTITUTION_EVIDENCE} for reporting.
|
|
71
71
|
*/
|
|
72
72
|
export interface FontFallback {
|
|
73
|
-
/** the
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
|
|
73
|
+
/** the open family to render in place of the requested font. */
|
|
74
|
+
substituteFamily: string;
|
|
75
|
+
/**
|
|
76
|
+
* What a renderer should DO, independent of fidelity: `substitute` = render the named open family;
|
|
77
|
+
* `category_fallback` = render a lower-fidelity same-category family. NOT a quality claim - a
|
|
78
|
+
* `substitute` can still be top-level `visual_only` (e.g. Cambria). Read `verdict` for fidelity.
|
|
79
|
+
*/
|
|
80
|
+
policyAction: PolicyAction;
|
|
77
81
|
/** the worst-face fidelity verdict behind the choice. */
|
|
78
82
|
verdict: Verdict;
|
|
79
83
|
/**
|
|
80
|
-
* Coarse "
|
|
81
|
-
*
|
|
82
|
-
* drifts a few glyphs,
|
|
83
|
-
* Cambria). Read `verdict` (and
|
|
84
|
+
* Coarse "advances preserve line breaks" flag: true for metric_safe, near_metric, or monospace
|
|
85
|
+
* cell_width_only (cell width, and so every advance, matches). NOT a claim of a perfect/exact clone -
|
|
86
|
+
* near_metric drifts a few glyphs, cell_width_only keeps the advances but not the glyph shapes, and a
|
|
87
|
+
* row can roll up to a worse top-level verdict because of one face (see Cambria). Read `verdict` (and
|
|
88
|
+
* the row's `faceVerdicts`) for the precise tier.
|
|
84
89
|
*/
|
|
85
|
-
|
|
86
|
-
/**
|
|
90
|
+
lineBreakSafe: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Reviewed face coverage: which RIBBI faces this substitute is PROVEN to supply. A renderer MUST
|
|
93
|
+
* respect a face-scoped row: it can be Regular-only (e.g. Baskerville -> Bacasime, Cooper Black ->
|
|
94
|
+
* Caprasimo), and routing bold/italic to a face it lacks is wrong. NOTE: an all-false `faces` means
|
|
95
|
+
* the row is NOT face-scoped (e.g. a category fallback, whose physical font does have faces), NOT
|
|
96
|
+
* that the font has no faces - such rows render for any face. The face-aware helpers
|
|
97
|
+
* ({@link getRenderableFallbackForFace}) encode this rule for you.
|
|
98
|
+
*/
|
|
99
|
+
faces: FaceCoverage;
|
|
100
|
+
/** stable reviewed-evidence id; look the full row up in {@link SUBSTITUTION_EVIDENCE}. */
|
|
87
101
|
evidenceId: string;
|
|
88
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* The full, honest outcome of a fallback lookup. A discriminated union so a consumer can tell apart
|
|
105
|
+
* cases that a bare `FontFallback | null` collapses: docfonts has never heard of the font (`unknown`)
|
|
106
|
+
* vs knows it but recommends no renderable family (`no_recommended_fallback`), the substitute exists
|
|
107
|
+
* but the consumer does not bundle it (`asset_missing`), and the deliberate non-substitution policies
|
|
108
|
+
* (`preserve_only`, `customer_supplied`). The face-aware lookups add `face_missing`: a substitute is
|
|
109
|
+
* recommended for the family but does NOT provide the requested face. `evidenceId` on the terminal
|
|
110
|
+
* kinds points back into {@link SUBSTITUTION_EVIDENCE} for the full row (verdict, faces, ...).
|
|
111
|
+
*/
|
|
112
|
+
export type FallbackDecision = {
|
|
113
|
+
kind: "fallback";
|
|
114
|
+
fallback: FontFallback;
|
|
115
|
+
} | {
|
|
116
|
+
kind: "asset_missing";
|
|
117
|
+
substituteFamily: string;
|
|
118
|
+
verdict: Verdict;
|
|
119
|
+
evidenceId: string;
|
|
120
|
+
} | {
|
|
121
|
+
/** the family has a renderable substitute, but it does not provide the requested face - route
|
|
122
|
+
* this face through face-aware absence handling, do NOT substitute it. */
|
|
123
|
+
kind: "face_missing";
|
|
124
|
+
substituteFamily: string;
|
|
125
|
+
evidenceId: string;
|
|
126
|
+
} | {
|
|
127
|
+
kind: "no_recommended_fallback";
|
|
128
|
+
evidenceId: string;
|
|
129
|
+
} | {
|
|
130
|
+
kind: "customer_supplied";
|
|
131
|
+
evidenceId: string;
|
|
132
|
+
} | {
|
|
133
|
+
kind: "preserve_only";
|
|
134
|
+
evidenceId: string;
|
|
135
|
+
} | {
|
|
136
|
+
kind: "unknown";
|
|
137
|
+
};
|