@openpresentation/opf-render 0.13.0 → 0.14.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.
@@ -1,8 +1,8 @@
1
1
  // RR-28: the part of the slideshow player and the <opf-deck> element that has no DOM of its own. It reads an OPF document,
2
- // decides which slides play (hidden ones do not), renders each slide once with the renderer's own SVG output (renderSvg:
2
+ // decides which slides play (hidden ones do not), renders each slide once with the renderer's own SVG output (renderSlideSvg:
3
3
  // there is no second layout engine) behind a face-level font gate, and synchronizes windows over a BroadcastChannel.
4
4
  // Nothing here fetches anything but the font files the host's font root serves.
5
- import { renderSvg } from "./svg.js";
5
+ import { renderSlideSvg } from "./svg.js";
6
6
 
7
7
  /** Error raised for a deck that cannot be read, rendered or loaded. */
8
8
  export class DeckError extends Error {
@@ -173,38 +173,24 @@ export function localIsoDate(now = new Date()) {
173
173
 
174
174
  // --- font gate -----------------------------------------------------------------------------------------------------
175
175
 
176
- const MAX_FONT_ROUNDS = 4;
177
-
178
176
  /**
179
- * The gate every render goes through: nothing draws a document before the faces it needs are loaded. `registry` is a browser
180
- * font registry (`loadBrowserFontRegistry`); one without the lazy loaders gates nothing. Face level: only the vendored and
181
- * script faces the document draws are fetched, each hash-verified by the registry.
177
+ * The gate every render goes through: nothing draws a document before the faces it needs are loaded. `fonts` is a browser
178
+ * fonts handle (`loadFonts` from `/fonts-browser`); one without `pending` and `ensure` gates nothing. Face level: only the
179
+ * vendored and script faces the document draws are fetched, each hash-verified by the registry behind the handle.
182
180
  */
183
- export function createFontGate(registry) {
184
- const sources = (document, options) => {
185
- let lazy = [], scripts = [];
186
- try { scripts = registry?.pendingScripts?.(document, options) ?? []; } catch { /* the renderer reports the document itself */ }
187
- try { lazy = (registry?.pendingLazyFonts?.(document, options) ?? []).map((face) => face?.file ?? String(face)); } catch { /* likewise */ }
188
- return { lazy, scripts };
189
- };
181
+ export function createFontGate(fonts) {
182
+ // A document that does not resolve has nothing to load here: the renderer reports it when it draws.
183
+ const pending = (document, options) => { try { return fonts?.pending?.(document, options) ?? []; } catch { return []; } };
190
184
  return {
191
- pending(document, options) { const { lazy, scripts } = sources(document, options); return [...lazy, ...scripts]; },
185
+ pending,
192
186
  async ensure(document, { signal, renderOptions } = {}) {
193
- const call = { ...renderOptions, signal };
194
- try {
195
- for (let round = 0; round < MAX_FONT_ROUNDS; round++) {
196
- const { lazy, scripts } = sources(document, renderOptions);
197
- if (!lazy.length && !scripts.length) return;
198
- signal?.throwIfAborted?.();
199
- if (lazy.length) await registry.ensureLazyFonts?.(document, call);
200
- if (scripts.length) await registry.ensureScripts?.(document, call);
201
- }
202
- } catch (cause) {
187
+ if (!pending(document, renderOptions).length) return;
188
+ try { await fonts.ensure(document, { ...renderOptions, signal }); } catch (cause) {
203
189
  if (signal?.aborted) throw cause;
204
190
  throw new DeckError("fonts-unavailable", `Fonts for this deck could not be loaded: ${cause?.message ?? cause}`, { cause });
205
191
  }
206
- const left = sources(document, renderOptions);
207
- if (left.lazy.length || left.scripts.length) throw new DeckError("fonts-unavailable", `Fonts for this deck did not finish loading: ${[...left.lazy, ...left.scripts].join(", ")}.`);
192
+ const left = pending(document, renderOptions);
193
+ if (left.length) throw new DeckError("fonts-unavailable", `Fonts for this deck did not finish loading: ${left.join(", ")}.`);
208
194
  },
209
195
  };
210
196
  }
@@ -213,9 +199,9 @@ export function createFontGate(registry) {
213
199
 
214
200
  /**
215
201
  * One deck's rendering, shared by the element, the full-screen player and the speaker view: font loading once, each slide
216
- * rendered once by `renderSvg` and kept, so moving between slides never draws twice and every surface shows the same markup.
202
+ * rendered once by `renderSlideSvg` and kept, so moving between slides never draws twice and every surface shows the same markup.
217
203
  *
218
- * `fonts` is a browser font registry (or nothing: layout then uses estimated widths and the visitor's system sans-serif).
204
+ * `fonts` is a browser fonts handle (or nothing: layout then uses estimated widths and the visitor's system sans-serif).
219
205
  */
220
206
  export function createDeckStore({ document, fonts, renderOptions = {}, date } = {}) {
221
207
  const deck = parseDeckDocument(document);
@@ -224,7 +210,8 @@ export function createDeckStore({ document, fonts, renderOptions = {}, date } =
224
210
  const diagnostics = [];
225
211
  const options = () => ({
226
212
  ...renderOptions,
227
- ...(fonts?.textMeasurement ? { textMeasurement: fonts.textMeasurement } : {}),
213
+ // Inline slides use the page's own faces (the registry added them to the document), so only the measurement is passed: nothing is embedded.
214
+ ...(fonts?.textMeasurement ? { fonts: { textMeasurement: fonts.textMeasurement } } : {}),
228
215
  ...(renderOptions.date === undefined && date !== undefined ? { date } : {}),
229
216
  onDiagnostic(diagnostic) { diagnostics.push(diagnostic); renderOptions.onDiagnostic?.(diagnostic); },
230
217
  });
@@ -243,12 +230,12 @@ export function createDeckStore({ document, fonts, renderOptions = {}, date } =
243
230
  let svg = cache.get(index);
244
231
  if (svg === undefined) {
245
232
  try {
246
- svg = renderSvg(deck, { ...options(), slideIndex: index, validate: !validated });
233
+ svg = renderSlideSvg(deck, index, { ...options(), validate: !validated });
247
234
  } catch (cause) {
248
235
  if (cause instanceof DeckError) throw cause;
249
- // A document the schema rejects says where: the first issue's path and message.
250
- const issue = cause?.code === "invalid-opf" ? (cause.details?.issues ?? cause.issues)?.[0] : undefined;
251
- const where = issue ? ` (${issue.path || "/"}: ${issue.message})` : "";
236
+ // A document the schema rejects says where: the first finding's path and message.
237
+ const finding = cause?.code === "invalid-opf" ? cause.findings?.[0] : undefined;
238
+ const where = finding ? ` (${finding.path || "/"}: ${finding.message})` : "";
252
239
  throw new DeckError(cause?.code === "invalid-opf" ? "invalid-document" : "render-failed", `${cause?.message ?? "The slide could not be drawn."}${where}`, { cause });
253
240
  }
254
241
  validated = true;
package/dist/element.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { BrowserFontRegistry } from "./fonts-browser.js";
1
+ import type { BrowserFontsHandle } from "./fonts-browser.js";
2
+ import type { RenderFonts } from "./fonts.js";
2
3
  import type { RenderSvgOptions } from "./svg.js";
3
4
  import type { PlayerSession, PresentOptions, PlayerSlideChange } from "./player.js";
4
5
  export { DeckError } from "./player.js";
@@ -32,10 +33,13 @@ export interface OpfDeckElement extends HTMLElement {
32
33
  /** The 1-based number of the showing slide in the sequence that plays; set it to navigate. */
33
34
  slide: number;
34
35
  readonly total: number;
35
- fonts: string;
36
- /** A browser font registry the page already has. Wins over `fonts`. */
37
- fontRegistry: BrowserFontRegistry | undefined;
38
- /** Extra `renderSvg` options (catalogs, imageResolver, date, ...). Set before the deck loads, or call `reload()`. */
36
+ /**
37
+ * The fonts the deck draws with: the URL of the self-hosted font root (the `fonts` attribute), or a browser fonts handle the page
38
+ * already has (`loadFonts` from `/fonts-browser`), which wins over the attribute. The getter returns the handle when one is set, else the
39
+ * attribute's URL (an empty string without either).
40
+ */
41
+ fonts: string | BrowserFontsHandle;
42
+ /** Extra `renderSlideSvg` options (catalogs, imageResolver, date, ...). Set before the deck loads, or call `reload()`. */
39
43
  renderOptions: Partial<RenderSvgOptions>;
40
44
  includeHidden: boolean;
41
45
  thumbnails: boolean;
@@ -65,7 +69,8 @@ export interface RenderDeckHtmlOptions {
65
69
  includeHidden?: boolean;
66
70
  src?: string;
67
71
  slide?: number;
68
- fonts?: string;
72
+ /** A string is the font root URL written to the tag's `fonts` attribute; an object (a `loadFonts()` handle, or any `RenderFonts`) draws the markup with those fonts and writes no attribute. */
73
+ fonts?: string | RenderFonts;
69
74
  label?: string;
70
75
  thumbnails?: boolean;
71
76
  present?: boolean;
package/dist/element.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  prepareSlideSvg, sanitizeSvgTree, slideInfo, slideLabel, svgDimensions, plainText,
7
7
  } from "./deck-runtime.js";
8
8
  import { loadPreviewFonts } from "./preview-fonts.js";
9
- import { renderSvg } from "./svg.js";
9
+ import { renderSlideSvg } from "./svg.js";
10
10
 
11
11
  export { DeckError } from "./deck-runtime.js";
12
12
  export { loadPreviewFonts, previewBaseFaces, previewFontLayout } from "./preview-fonts.js";
@@ -74,7 +74,7 @@ function createElementClass() {
74
74
  return class OpfDeckElement extends Base {
75
75
  static get observedAttributes() { return ["src", "slide", "fonts", "thumbnails", "controls", "present", "include-hidden", "label", "keyboard"]; }
76
76
 
77
- #root; #parts = {}; #built = false; #store; #cursor; #deck; #generation = 0; #abort; #documentValue; #fontRegistry; #renderOptions = {};
77
+ #root; #parts = {}; #built = false; #store; #cursor; #deck; #generation = 0; #abort; #documentValue; #fonts; #renderOptions = {};
78
78
  #readyPromise = Promise.resolve(); #state = "idle"; #error; #observer; #thumbsBuilt = false; #pointer; #session; #announce = false;
79
79
  #slideAttribute; #sheet;
80
80
 
@@ -124,13 +124,20 @@ function createElementClass() {
124
124
  /** Number of slides in the sequence that plays. */
125
125
  get total() { return this.#cursor?.total ?? 0; }
126
126
 
127
- /** URL of the self-hosted font root (see the package README). Without it, layout uses estimated widths and system fonts. */
128
- get fonts() { return this.getAttribute("fonts") ?? ""; }
129
- set fonts(value) { if (value) this.setAttribute("fonts", String(value)); else this.removeAttribute("fonts"); }
130
-
131
- /** A browser font registry the page already has (`loadBrowserFontRegistry`). Wins over `fonts`. */
132
- get fontRegistry() { return this.#fontRegistry; }
133
- set fontRegistry(value) { this.#fontRegistry = value || undefined; if (this.isConnected && this.#built) this.#start(); }
127
+ /**
128
+ * The fonts the deck draws with: the URL of the self-hosted font root (the `fonts` attribute; see the package README), or a
129
+ * browser fonts handle the page already has (`loadFonts` from `/fonts-browser`), which wins over the attribute. Without either,
130
+ * layout uses estimated widths and system fonts.
131
+ */
132
+ get fonts() { return this.#fonts ?? this.getAttribute("fonts") ?? ""; }
133
+ set fonts(value) {
134
+ if (value && typeof value === "object") { this.#fonts = value; if (this.isConnected && this.#built) this.#start(); return; }
135
+ const had = this.#fonts !== undefined, before = this.getAttribute("fonts");
136
+ this.#fonts = undefined;
137
+ if (value) this.setAttribute("fonts", String(value)); else this.removeAttribute("fonts");
138
+ // Dropping a handle changes what draws even when the attribute keeps its value.
139
+ if (had && before === this.getAttribute("fonts") && this.isConnected && this.#built) this.#start();
140
+ }
134
141
 
135
142
  /** Extra `renderSvg` options (catalogs, imageResolver, date, ...). Set before the deck loads, or call `reload()`. */
136
143
  get renderOptions() { return this.#renderOptions; }
@@ -165,7 +172,7 @@ function createElementClass() {
165
172
  if (!this.#store || !this.#cursor) throw new DeckError("not-ready", "The deck is not loaded yet.");
166
173
  const { present } = await import("./player.js");
167
174
  const session = await present(this.#deck, {
168
- store: this.#store, fonts: this.#fontRegistry ?? this.#resolvedRegistry, renderOptions: this.#renderOptions,
175
+ store: this.#store, fonts: this.#fonts ?? this.#resolvedFonts, renderOptions: this.#renderOptions,
169
176
  includeHidden: this.includeHidden, startSlide: this.#cursor.number, returnFocus: this.#parts.viewport, ...options,
170
177
  });
171
178
  this.#session = session;
@@ -182,7 +189,7 @@ function createElementClass() {
182
189
 
183
190
  // --- loading ----------------------------------------------------------------------------------------------------
184
191
 
185
- #resolvedRegistry;
192
+ #resolvedFonts;
186
193
 
187
194
  #start() {
188
195
  this.#abort?.abort();
@@ -202,22 +209,22 @@ function createElementClass() {
202
209
  const deck = await this.#readDocument(signal);
203
210
  if (stale()) return;
204
211
  parseDeckDocument(deck);
205
- let registry = this.#fontRegistry;
206
- const root = this.fonts;
212
+ let registry = this.#fonts;
213
+ const root = this.getAttribute("fonts");
207
214
  if (!registry && root) {
208
215
  try { registry = await loadPreviewFonts(root, { signal }); } catch (cause) {
209
216
  if (stale()) return;
210
217
  this.#report(new DeckError("fonts-unavailable", `The fonts at ${root} could not be loaded; the deck draws with estimated layout and system fonts.`, { cause }), false);
211
218
  }
212
219
  }
213
- this.#resolvedRegistry = registry;
220
+ this.#resolvedFonts = registry;
214
221
  const date = this.#renderOptions.date ?? localIsoDate();
215
222
  let store = createDeckStore({ document: deck, fonts: registry, renderOptions: this.#renderOptions, date });
216
223
  try { await store.ready(signal); } catch (cause) {
217
224
  if (stale()) return;
218
225
  if (!registry) throw cause;
219
226
  this.#report(cause, false);
220
- this.#resolvedRegistry = undefined;
227
+ this.#resolvedFonts = undefined;
221
228
  store = createDeckStore({ document: deck, renderOptions: this.#renderOptions, date });
222
229
  }
223
230
  if (stale()) return;
@@ -491,7 +498,9 @@ const escapeHtml = (value) => String(value).replace(/&/g, "&amp;").replace(/</g,
491
498
  /**
492
499
  * Server-side markup for a deck: the slides as inline SVG inside the tag, which a page that has not loaded JavaScript (or a
493
500
  * crawler, or a reader) shows as it is, and which the element replaces with its own shadow DOM when it upgrades. Pure and
494
- * synchronous; layout uses estimated widths unless you pass `renderOptions.textMeasurement` (and `embeddedFonts`).
501
+ * synchronous; layout uses estimated widths unless you pass a fonts handle as `fonts` (an object: `loadFonts()` from
502
+ * `/fonts-browser`, or one with a `textMeasurement` and `embeddedFonts`). A `fonts` string is the font root URL written to the
503
+ * element's `fonts` attribute.
495
504
  *
496
505
  * `slides`: `"first"` (default: the first slide plus a list of titles), `"all"` or a list of 1-based slide numbers.
497
506
  * `embed` puts the document in an `application/opf+json` script child, so the element upgrades without a request.
@@ -502,13 +511,14 @@ export function renderDeckHtml(input, options = {}) {
502
511
  const sequence = presentableIndexes(deck, { includeHidden: options.includeHidden });
503
512
  const wanted = options.slides ?? "first";
504
513
  const shown = wanted === "all" ? sequence.map((_, position) => position) : wanted === "first" ? [0] : wanted.map((number) => number - 1).filter((position) => position >= 0 && position < sequence.length);
505
- const attributes = { src: options.src, slide: options.slide, fonts: options.fonts, label: options.label, ...options.attributes };
514
+ const handle = options.fonts !== null && typeof options.fonts === "object" ? options.fonts : undefined;
515
+ const attributes = { src: options.src, slide: options.slide, fonts: handle ? undefined : options.fonts, label: options.label, ...options.attributes };
506
516
  const attributeText = Object.entries(attributes).filter(([, value]) => value !== undefined && value !== false).map(([name, value]) => (value === true ? ` ${name}` : ` ${name}="${escapeHtml(value)}"`)).join("")
507
517
  + (options.thumbnails ? " thumbnails" : "") + (options.present ? " present" : "") + (options.includeHidden ? " include-hidden" : "");
508
518
  const date = options.renderOptions?.date ?? options.date;
509
519
  const figures = shown.map((position) => {
510
520
  const index = sequence[position];
511
- const svg = renderSvg(deck, { ...options.renderOptions, ...(date ? { date } : {}), slideIndex: index });
521
+ const svg = renderSlideSvg(deck, index, { ...options.renderOptions, ...(handle ? { fonts: handle } : {}), ...(date ? { date } : {}) });
512
522
  const { width, height } = svgDimensions(svg);
513
523
  const prepared = prepareSlideSvg(svg).replace(/^<svg\b/, `<svg style="display:block;width:100%;height:auto;aspect-ratio:${width} / ${height}"`);
514
524
  const label = slideLabel(slideInfo(deck, index), position + 1, sequence.length);
@@ -1,4 +1,5 @@
1
1
  import type { PdfDiagnostic, PdfMetadata, SvgToPdfOptions } from "./index.js";
2
+ import type { FontsHandle } from "./fonts.js";
2
3
  export type { PdfDiagnostic, PdfMetadata } from "./index.js";
3
4
 
4
5
  export interface BrowserSvgToPngOptions {
@@ -12,7 +13,12 @@ export interface BrowserSvgToPngOptions {
12
13
  export interface BrowserSvgToPdfOptions extends Pick<SvgToPdfOptions, "mode" | "background" | "serifFamily" | "metadata" | "tagged" | "compress" | "strict" | "rasterFallbackScale" | "defaultFontFamily" | "sansSerifFamily" | "monospaceFamily" | "onDiagnostic"> {
13
14
  /** Raster mode: pixel density of each page image (default 2). */
14
15
  scale?: number;
15
- /** Extra font faces as bytes (TrueType outlines). Faces an SVG embeds as `@font-face` data (renderSvg's `embeddedFonts`) need no entry here. */
16
+ /**
17
+ * The browser fonts handle (`loadFonts` from `/fonts-browser`): every face it holds can be embedded in the PDF, including script faces
18
+ * a standalone SVG does not carry. Faces an SVG embeds as `@font-face` data (the `embeddedFonts` of the handle `renderSvg` was given) need no entry.
19
+ */
20
+ fonts?: FontsHandle;
21
+ /** Extra font faces as bytes (TrueType outlines) when there is no handle. */
16
22
  fontData?: Array<{ data: Uint8Array; family?: string }>;
17
23
  /** Checked between pages; an abort rejects with the signal's reason (an `AbortError`). */
18
24
  signal?: AbortSignal;
@@ -5,8 +5,9 @@ import { OPFRenderError, packageName } from "./svg.js";
5
5
  // has the same two names for a page. It uses the same vector PDF converter as Node (fontkit, bidi-js and pako, all pure
6
6
  // JavaScript) over the SVG the preview draws, so the text stays real text in the same embedded subsets. What differs from
7
7
  // Node is only how pixels are made: a canvas decodes pictures and draws PNGs. No system font is read (a browser has no way
8
- // to hand one over) and nothing is fetched: faces come from the SVG's own @font-face data (renderSvg's `embeddedFonts`) or
9
- // from `fontData`.
8
+ // to hand one over) and nothing is fetched: faces come from the SVG's own @font-face data (the `embeddedFonts` of the fonts handle
9
+ // `renderSvg` was given), from the faces the `fonts` handle holds (`loadFonts` from `/fonts-browser`, so script faces the SVG does not
10
+ // embed reach the PDF too), or from `fontData`.
10
11
 
11
12
  const DEFAULT_DIMENSIONS = { width: 1280, height: 720 };
12
13
  const MAX_PIXELS = 40_000_000;
@@ -31,21 +32,21 @@ export async function svgToPng(svg, options = {}) {
31
32
  * Convert SVG slides to a PDF, one slide per page (1 SVG pixel is 1 PDF point, as in Node). `mode: "vector"` (default) writes
32
33
  * real text, paths, gradients and images; `"raster"` draws each slide as an image (`scale`, default 2). Takes the options of
33
34
  * the Node `svgToPdf` that make sense here (`metadata`, `tagged`, `strict`, `compress`, `onDiagnostic`, the generic family
34
- * names, `rasterFallbackScale`) and `fontData` (`[{ data, family? }]`, extra face bytes), `signal` (an AbortSignal, checked between
35
- * pages) and `onProgress({ page, pages })`.
35
+ * names, `rasterFallbackScale`), `fonts` (the handle `loadFonts()` returns: every face it holds can be embedded), `fontData`
36
+ * (`[{ data, family? }]`, extra face bytes without a handle), `signal` (an AbortSignal, checked between pages) and
37
+ * `onProgress({ page, pages })`.
36
38
  */
37
39
  export async function svgToPdf(svgs, options = {}) {
38
40
  const inputs = (Array.isArray(svgs) ? svgs : [svgs]).map(svgText);
39
41
  if (!inputs.length) throw new OPFRenderError("empty-pdf", "svgToPdf requires at least one SVG slide.");
40
42
  const mode = options.mode ?? "vector";
41
43
  if (mode !== "vector" && mode !== "raster") throw new OPFRenderError("invalid-conversion-option", 'mode must be "vector" or "raster".', { option: "mode", value: mode });
42
- if (options.loadSystemFonts === true) throw new OPFRenderError("pdf-system-fonts-unsupported", "A browser cannot hand system fonts to the PDF export: supply faces as @font-face data or `fontData`.", { option: "loadSystemFonts" });
43
44
  if (mode === "raster") return rasterPdf(inputs, options);
44
45
  const scale = positive(options.rasterFallbackScale, "rasterFallbackScale", 2);
45
46
  const fontCss = fontFaceCss(inputs);
46
47
  return svgsToVectorPdf(inputs, {
47
48
  fontFiles: [], fontDirs: [],
48
- fontData: Array.isArray(options.fontData) ? options.fontData : [],
49
+ fontData: [...(options.fonts?.registry?.exportFaces?.() ?? []), ...(Array.isArray(options.fontData) ? options.fontData : [])],
49
50
  imageCodec: canvasImageCodec(),
50
51
  defaultFontFamily: options.defaultFontFamily ?? "Roboto",
51
52
  sansSerifFamily: options.sansSerifFamily ?? options.defaultFontFamily ?? "Roboto",
@@ -1,8 +1,8 @@
1
- import { FONT_POLICY } from "./font-policy.js";
1
+ import { FONT_POLICY } from "@openpresentation/opf/font-policy";
2
2
  /** Curated substitution policy. Metric means upstream intent, not universal pixel identity. */
3
3
  const metric = (requestedFamily, substitutes, source, note = "Standard regular, bold, italic and bold italic styles; verify coverage and font versions.") => ({requestedFamily, substitutes, compatibility:"metric", weights:[400,700], source, note});
4
4
  const visual = (requestedFamily, substitutes, note = "Approximate appearance; measure again and expect reflow.") => ({requestedFamily, substitutes, compatibility:"visual", note});
5
- // Rules before FF-31. Families that the OPF font policy table (src/font-policy.js) lists take their
5
+ // Rules before FF-31. Families that the OPF font policy table (core `FONT_POLICY`) lists take their
6
6
  // rule from the table below; the remaining legacy rules keep working for other families.
7
7
  const LEGACY = [
8
8
  metric("Calibri",["Carlito"],"https://github.com/googlefonts/carlito"),
@@ -58,7 +58,7 @@ const describe = replacement => replacement.measured
58
58
  * preview never needs a download. Replacements drive measurement and drawing only; exporters keep
59
59
  * writing the chosen family.
60
60
  */
61
- const FROM_POLICY = FONT_POLICY.filter(row => row.replacement).map(row => ({
61
+ const FROM_POLICY = FONT_POLICY.families.filter(row => row.replacement).map(row => ({
62
62
  requestedFamily: row.family,
63
63
  substitutes: [row.replacement.family, ...(row.alternates ?? [])],
64
64
  compatibility: row.replacement.compatibility,
@@ -80,7 +80,7 @@ export const FONT_COMPATIBILITY = Object.freeze([...FROM_POLICY, ...LEGACY.filte
80
80
  // The row's metric claim is measured that way, so the renderer must shape (fonts.js) and draw (svg.js)
81
81
  // the face that way too. Keyed by the replacement family: a Georgia deck is measured and drawn with
82
82
  // Gelasio, and Gelasio requested by name is treated identically, so measurement and drawing never disagree.
83
- const DISABLED_FEATURES = new Map(FONT_POLICY.filter(row => row.replacement?.disabledFeatures?.length).map(row => [row.replacement.family.toLowerCase(), Object.freeze([...row.replacement.disabledFeatures])]));
83
+ const DISABLED_FEATURES = new Map(FONT_POLICY.families.filter(row => row.replacement?.disabledFeatures?.length).map(row => [row.replacement.family.toLowerCase(), Object.freeze([...row.replacement.disabledFeatures])]));
84
84
  /** OpenType feature tags to turn off for a preview face, from the font policy; undefined when none. */
85
85
  export function disabledFeaturesFor(family) {
86
86
  return typeof family === "string" ? DISABLED_FEATURES.get(family.trim().toLowerCase()) : undefined;