@openpresentation/opf-editor 0.12.1 → 0.14.2

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.
Files changed (95) hide show
  1. package/README.md +71 -60
  2. package/dist/annotations.d.ts +16 -16
  3. package/dist/annotations.js +71 -79
  4. package/dist/assets.d.ts +7 -7
  5. package/dist/assets.js +15 -15
  6. package/dist/background-options.d.ts +2 -2
  7. package/dist/background-options.js +6 -6
  8. package/dist/block-controls.js +7 -7
  9. package/dist/block-convert.d.ts +7 -7
  10. package/dist/block-convert.js +22 -21
  11. package/dist/blocks.d.ts +8 -8
  12. package/dist/blocks.js +41 -40
  13. package/dist/canvas-fields.js +35 -12
  14. package/dist/canvas.d.ts +22 -30
  15. package/dist/canvas.js +57 -64
  16. package/dist/chart-data.d.ts +10 -10
  17. package/dist/chart-data.js +21 -21
  18. package/dist/chart-options-panel.d.ts +2 -2
  19. package/dist/chart-options-panel.js +82 -8
  20. package/dist/chart-options.d.ts +35 -9
  21. package/dist/chart-options.js +133 -40
  22. package/dist/checks.js +21 -0
  23. package/dist/color-authoring.js +1 -1
  24. package/dist/content-actions.d.ts +15 -15
  25. package/dist/content-actions.js +71 -70
  26. package/dist/content-controls.js +12 -12
  27. package/dist/data-grid.js +7 -8
  28. package/dist/data.d.ts +4 -4
  29. package/dist/data.js +5 -5
  30. package/dist/dataset-refs.d.ts +2 -2
  31. package/dist/dataset-refs.js +5 -5
  32. package/dist/design-controls.js +61 -44
  33. package/dist/design-options.d.ts +16 -14
  34. package/dist/design-options.js +86 -74
  35. package/dist/edit-helpers.js +8 -7
  36. package/dist/exact-source.js +5 -5
  37. package/dist/export.d.ts +5 -7
  38. package/dist/export.js +40 -14
  39. package/dist/find-panel.js +5 -5
  40. package/dist/find-replace.d.ts +3 -3
  41. package/dist/find-replace.js +23 -20
  42. package/dist/font-gate.js +49 -58
  43. package/dist/galleries.js +36 -52
  44. package/dist/grid-model.d.ts +25 -27
  45. package/dist/grid-model.js +92 -102
  46. package/dist/image-crop.d.ts +4 -4
  47. package/dist/image-crop.js +20 -20
  48. package/dist/image-cropper.js +3 -3
  49. package/dist/index.d.ts +28 -28
  50. package/dist/index.js +74 -90
  51. package/dist/json-options.js +17 -16
  52. package/dist/layout-handles.js +5 -5
  53. package/dist/layout-placeholders.js +4 -4
  54. package/dist/layout.d.ts +2 -2
  55. package/dist/layout.js +8 -7
  56. package/dist/numbering-panel.js +3 -5
  57. package/dist/numbering.d.ts +3 -5
  58. package/dist/numbering.js +14 -26
  59. package/dist/outline-view.js +24 -24
  60. package/dist/outline.d.ts +12 -12
  61. package/dist/outline.js +52 -50
  62. package/dist/persistence.d.ts +1 -1
  63. package/dist/persistence.js +19 -18
  64. package/dist/presentation-ids.js +2 -2
  65. package/dist/react.js +3 -3
  66. package/dist/review-panel.d.ts +30 -13
  67. package/dist/review-panel.js +154 -53
  68. package/dist/review.d.ts +36 -43
  69. package/dist/review.js +92 -53
  70. package/dist/rich-text-input.js +4 -3
  71. package/dist/rich-text-toolbar.js +5 -3
  72. package/dist/rich-text.js +5 -2
  73. package/dist/schema-inspector.d.ts +2 -2
  74. package/dist/schema-inspector.js +16 -11
  75. package/dist/schema.d.ts +1 -1
  76. package/dist/schema.js +25 -5
  77. package/dist/slide-manager.d.ts +2 -2
  78. package/dist/slide-manager.js +18 -18
  79. package/dist/slides.d.ts +20 -20
  80. package/dist/slides.js +99 -97
  81. package/dist/svelte.js +1 -1
  82. package/dist/switches.d.ts +10 -10
  83. package/dist/switches.js +124 -114
  84. package/dist/table-options.d.ts +8 -8
  85. package/dist/table-options.js +28 -27
  86. package/dist/table-structure.d.ts +10 -10
  87. package/dist/table-structure.js +21 -21
  88. package/dist/template-panel.d.ts +3 -3
  89. package/dist/template-panel.js +37 -13
  90. package/dist/templates.d.ts +22 -11
  91. package/dist/templates.js +48 -55
  92. package/dist/transfer.d.ts +4 -4
  93. package/dist/transfer.js +49 -59
  94. package/package.json +8 -6
  95. package/dist/font-defaults.js +0 -25
@@ -177,7 +177,7 @@ export function createFindPanel(container, options) {
177
177
 
178
178
  function search(keep) {
179
179
  const before = current >= 0 ? result.matches[current] : undefined;
180
- result = findMatches(editor.document, state.query, sessionOptions());
180
+ result = findMatches(editor.presentation, state.query, sessionOptions());
181
181
  setError(result.error);
182
182
  if (!result.matches.length) current = -1;
183
183
  else if (keep && before) {
@@ -231,8 +231,8 @@ export function createFindPanel(container, options) {
231
231
  function doReplaceOne() {
232
232
  const match = result.matches[current];
233
233
  if (!match || !finishCommit()) return;
234
- const order = new Map(collectSearchFields(editor.document, sessionOptions()).map((field, index) => [field.pointer, index]));
235
- const fieldText = textOf(getValueAtPath(editor.document, match.pointer));
234
+ const order = new Map(collectSearchFields(editor.presentation, sessionOptions()).map((field, index) => [field.pointer, index]));
235
+ const fieldText = textOf(getValueAtPath(editor.presentation, match.pointer));
236
236
  const written = match.start + expandReplacement(state.replacement, match, fieldText, state.regex).length;
237
237
  try {
238
238
  replaceMatch(editor, match, state.query, state.replacement, sessionOptions());
@@ -246,8 +246,8 @@ export function createFindPanel(container, options) {
246
246
  setError("");
247
247
  // Move on to the next match after the replaced text, in reading order (never the text just written).
248
248
  const replacedAt = order.get(match.pointer) ?? -1;
249
- result = findMatches(editor.document, state.query, sessionOptions());
250
- const nextOrder = new Map(collectSearchFields(editor.document, sessionOptions()).map((field, index) => [field.pointer, index]));
249
+ result = findMatches(editor.presentation, state.query, sessionOptions());
250
+ const nextOrder = new Map(collectSearchFields(editor.presentation, sessionOptions()).map((field, index) => [field.pointer, index]));
251
251
  let next = result.matches.findIndex((candidate) => {
252
252
  const position = nextOrder.get(candidate.pointer) ?? Infinity;
253
253
  return position > replacedAt || (position === replacedAt && candidate.start >= written);
@@ -80,11 +80,11 @@ export interface TextEdit {
80
80
  /** The plain text of a string or a run array. */
81
81
  export declare function textOf(value: unknown): string;
82
82
  /** Every searchable text field of a document, in reading order. */
83
- export declare function collectSearchFields(document: unknown, options?: CollectOptions): SearchField[];
83
+ export declare function collectSearchFields(presentation: unknown, options?: CollectOptions): SearchField[];
84
84
  /** Compile a search into a global RegExp; `{ error }` for an invalid regular expression, `{ empty: true }` for an empty query. */
85
85
  export declare function compileSearch(query: string, options?: SearchOptions): { regex?: RegExp; error?: string; empty?: boolean };
86
86
  /** Search a document's text. */
87
- export declare function findMatches(document: unknown, query: string, options?: SearchOptions): SearchResult;
87
+ export declare function findMatches(presentation: unknown, query: string, options?: SearchOptions): SearchResult;
88
88
  /** Expand a replacement for one match (`$&`, `$1`, `$<name>`, `$$` in regex mode; literal otherwise). */
89
89
  export declare function expandReplacement(template: string, match: SearchMatch, fieldText: string, regex?: boolean): string;
90
90
  /**
@@ -93,7 +93,7 @@ export declare function expandReplacement(template: string, match: SearchMatch,
93
93
  */
94
94
  export declare function applyTextEdits<T extends string | unknown[]>(value: T, edits: TextEdit[]): T;
95
95
  /** The patches that replace the given matches. Throws `stale-match` when the text changed. */
96
- export declare function planReplace(document: unknown, matches: SearchMatch[], replacement: string, options?: { regex?: boolean }): ReplacePlan;
96
+ export declare function planReplace(presentation: unknown, matches: SearchMatch[], replacement: string, options?: { regex?: boolean }): ReplacePlan;
97
97
  /** Replace every match as one undoable session change. Throws `invalid-search` for a bad regular expression. */
98
98
  export declare function replaceAll(editor: EditorSession, query: string, replacement: string, options?: SearchOptions): ReplaceResult;
99
99
  /** Replace one match as one undoable change. Throws `stale-match` when it is no longer in the document. */
@@ -81,6 +81,8 @@ function payloadFields(fields, state, base, payload, prefix = "") {
81
81
  else if (isObject(payload.quote)) {
82
82
  field(fields, state, at("quote", "text"), payload.quote.text, `${prefix}Quote`);
83
83
  field(fields, state, at("quote", "attribution"), payload.quote.attribution, `${prefix}Quote attribution`);
84
+ field(fields, state, at("quote", "role"), payload.quote.role, `${prefix}Quote role`);
85
+ if (isObject(payload.quote.photo)) field(fields, state, at("quote", "photo", "alt"), payload.quote.photo.alt, `${prefix}Quote photo alt text`, "alt");
84
86
  field(fields, state, at("quote", "source"), payload.quote.source, `${prefix}Quote source`);
85
87
  }
86
88
  const timeline = payload.timeline;
@@ -104,6 +106,7 @@ function payloadFields(fields, state, base, payload, prefix = "") {
104
106
  if (typeof value === "string") field(fields, state, at("chart", "data", "rows", String(r), String(c)), value, `${prefix}Chart row ${r + 1}, column ${c + 1}`, "chart");
105
107
  }));
106
108
  }
109
+ if (isObject(payload.chart)) field(fields, state, at("chart", "alt"), payload.chart.alt, `${prefix}Chart alt text`, "alt");
107
110
  const table = payload.table;
108
111
  if (isObject(table)) {
109
112
  if (Array.isArray(table.columns)) table.columns.forEach((cell, c) => (isObject(cell) && !("value" in cell) ? field(fields, state, at("table", "columns", String(c), "name"), cell.name, `${prefix}Table header ${c + 1}`, "table") : cellFields(fields, state, at("table", "columns", String(c)), cell, `${prefix}Table header ${c + 1}`)));
@@ -131,27 +134,27 @@ function furnitureFields(fields, state, base, design) {
131
134
  * the deck, `pointer` is the JSON pointer of the string or run array, and `runs` says it is a run array. Options:
132
135
  * `notes: false` leaves speaker notes out, `slideIndex` limits the list to one slide.
133
136
  */
134
- export function collectSearchFields(document, options = {}) {
137
+ export function collectSearchFields(presentation, options = {}) {
135
138
  const fields = [];
136
- if (!isObject(document)) return fields;
139
+ if (!isObject(presentation)) return fields;
137
140
  const only = Number.isInteger(options.slideIndex) ? options.slideIndex : undefined;
138
141
  if (only === undefined) {
139
142
  const deck = { slideIndex: -1 };
140
- field(fields, deck, ["name"], document.name, "Presentation name", "deck");
141
- field(fields, deck, ["description"], document.description, "Presentation description", "deck");
142
- furnitureFields(fields, deck, [], document.design);
143
+ field(fields, deck, ["name"], presentation.name, "Presentation name", "deck");
144
+ field(fields, deck, ["description"], presentation.description, "Presentation description", "deck");
145
+ furnitureFields(fields, deck, [], presentation.design);
143
146
  }
144
147
  // RR-54: the text cells of a shared dataset belong to the deck. Its column names are left alone (`fields` and `mapping` address them).
145
- if (only === undefined && isObject(document.datasets)) {
148
+ if (only === undefined && isObject(presentation.datasets)) {
146
149
  const deck = { slideIndex: -1 };
147
- for (const [id, dataset] of Object.entries(document.datasets)) {
150
+ for (const [id, dataset] of Object.entries(presentation.datasets)) {
148
151
  if (!isObject(dataset) || !Array.isArray(dataset.rows)) continue;
149
152
  dataset.rows.forEach((row, r) => Array.isArray(row) && row.forEach((value, c) => {
150
153
  if (typeof value === "string") field(fields, deck, ["datasets", id, "rows", String(r), String(c)], value, `Dataset ${id}, row ${r + 1}, column ${c + 1}`, "dataset");
151
154
  }));
152
155
  }
153
156
  }
154
- (Array.isArray(document.slides) ? document.slides : []).forEach((slide, slideIndex) => {
157
+ (Array.isArray(presentation.slides) ? presentation.slides : []).forEach((slide, slideIndex) => {
155
158
  if (only !== undefined && slideIndex !== only) return;
156
159
  if (!isObject(slide)) return;
157
160
  const state = { slideIndex };
@@ -203,12 +206,12 @@ function snippet(text, start, end) {
203
206
  * are offsets into the field's text. Options: those of `compileSearch`, plus `notes` and `slideIndex` of
204
207
  * `collectSearchFields`. Matches that are empty (a regular expression that matches nothing) are skipped.
205
208
  */
206
- export function findMatches(document, query, options = {}) {
209
+ export function findMatches(presentation, query, options = {}) {
207
210
  const compiled = compileSearch(query, options);
208
211
  const result = { matches: [], fieldCount: 0, slideCount: 0, truncated: false, error: compiled.error };
209
212
  if (!compiled.regex) return result;
210
213
  const slides = new Set();
211
- const fields = collectSearchFields(document, options);
214
+ const fields = collectSearchFields(presentation, options);
212
215
  outer: for (const item of fields) {
213
216
  compiled.regex.lastIndex = 0;
214
217
  let found;
@@ -320,8 +323,8 @@ export function applyTextEdits(value, edits) {
320
323
  return result;
321
324
  }
322
325
 
323
- function readField(document, match) {
324
- const value = getValueAtPath(document, match.pointer);
326
+ function readField(presentation, match) {
327
+ const value = getValueAtPath(presentation, match.pointer);
325
328
  return typeof value === "string" || Array.isArray(value) ? value : undefined;
326
329
  }
327
330
 
@@ -330,7 +333,7 @@ function readField(document, match) {
330
333
  * `{ patches, count, fieldCount }`; a replacement that leaves a field unchanged adds no patch. Throws `stale-match`
331
334
  * when a match no longer reads the same text in the document.
332
335
  */
333
- export function planReplace(document, matches, replacement, options = {}) {
336
+ export function planReplace(presentation, matches, replacement, options = {}) {
334
337
  const byField = new Map();
335
338
  for (const match of matches) {
336
339
  if (!byField.has(match.pointer)) byField.set(match.pointer, []);
@@ -339,7 +342,7 @@ export function planReplace(document, matches, replacement, options = {}) {
339
342
  const patches = [];
340
343
  let count = 0;
341
344
  for (const [pointer, list] of byField) {
342
- const value = readField(document, list[0]);
345
+ const value = readField(presentation, list[0]);
343
346
  const text = value === undefined ? undefined : textOf(value);
344
347
  const ordered = [...list].sort((a, b) => a.start - b.start);
345
348
  if (text === undefined || ordered.some((match) => text.slice(match.start, match.end) !== match.text)) {
@@ -353,8 +356,8 @@ export function planReplace(document, matches, replacement, options = {}) {
353
356
  return { patches, count, fieldCount: byField.size };
354
357
  }
355
358
 
356
- function search(document, query, options) {
357
- const found = findMatches(document, query, options);
359
+ function search(presentation, query, options) {
360
+ const found = findMatches(presentation, query, options);
358
361
  if (found.error) throw new OPFEditorError("invalid-search", found.error, {});
359
362
  return found;
360
363
  }
@@ -364,8 +367,8 @@ function search(document, query, options) {
364
367
  * the session's result (or null when nothing changed). One undo restores every replaced field.
365
368
  */
366
369
  export function replaceAll(editor, query, replacement, options = {}) {
367
- const found = search(editor.document, query, options);
368
- const plan = planReplace(editor.document, found.matches, String(replacement ?? ""), options);
370
+ const found = search(editor.presentation, query, options);
371
+ const plan = planReplace(editor.presentation, found.matches, String(replacement ?? ""), options);
369
372
  if (!plan.patches.length) return { count: 0, fieldCount: 0, change: null };
370
373
  const change = editor.applyPatch(plan.patches, { source: "find-replace", rejectInvalid: true, replaced: plan.count });
371
374
  return { count: plan.count, fieldCount: plan.fieldCount, change };
@@ -376,10 +379,10 @@ export function replaceAll(editor, query, replacement, options = {}) {
376
379
  * current document first: `stale-match` is thrown when the text changed underneath it.
377
380
  */
378
381
  export function replaceMatch(editor, match, query, replacement, options = {}) {
379
- const found = search(editor.document, query, options);
382
+ const found = search(editor.presentation, query, options);
380
383
  const current = found.matches.find((candidate) => candidate.pointer === match.pointer && candidate.start === match.start && candidate.end === match.end && candidate.text === match.text);
381
384
  if (!current) throw new OPFEditorError("stale-match", "That match is no longer in the document. Search again.", { path: match.path });
382
- const plan = planReplace(editor.document, [current], String(replacement ?? ""), options);
385
+ const plan = planReplace(editor.presentation, [current], String(replacement ?? ""), options);
383
386
  if (!plan.patches.length) return { count: 0, change: null };
384
387
  const change = editor.applyPatch(plan.patches, { source: "find-replace", rejectInvalid: true, replaced: 1 });
385
388
  return { count: 1, change };
package/dist/font-gate.js CHANGED
@@ -1,75 +1,65 @@
1
- // Fonts before pixels. A document can need faces the browser font registry has not loaded yet: script faces for the
2
- // languages it draws (Japanese, Arabic, ...) and vendored preview faces for the font families it resolves (Intos for
3
- // Aptos, Open Sans, Barlow, ...). The renderer cannot draw such a document until those faces are loaded (a deck that
4
- // sets Aptos and contains Japanese fails with "Intos Display cannot display U+65E5"), so nothing may render or measure
5
- // a new document before `ensure` resolves. This module is the one gate every document-replacing path goes through.
1
+ // Fonts before pixels. A document can need faces the browser has not loaded yet: script faces for the languages it draws
2
+ // (Japanese, Arabic, ...) and vendored preview faces for the font families it resolves (Intos for Aptos, Open Sans, Barlow,
3
+ // ...). The renderer cannot draw such a document until those faces are loaded (a deck that sets Aptos and contains Japanese
4
+ // fails with "Intos Display cannot display U+65E5"), so nothing may render or measure a new document before `ensure`
5
+ // resolves. The renderer's fonts handle (`loadFonts()` from `@openpresentation/opf-render/fonts-browser`) is the source of
6
+ // truth: its `pending(presentation, renderOptions)` and `ensure(presentation, options)` know what is missing and load it. The
7
+ // canvas and `exportDeck` take that handle as `fonts` and gate every document-replacing path through this module.
6
8
 
7
9
  /** Error code of a document whose required faces are still loading. */
8
10
  export const FONTS_PENDING = "fonts-pending";
9
11
  /** Error code of a document whose required faces could not be loaded. */
10
12
  export const FONTS_UNAVAILABLE = "fonts-unavailable";
11
13
 
12
- // A load that cannot finish (a face the registry never reports as loaded) stops after this many rounds.
13
- const MAX_ROUNDS = 4;
14
+ const GATE = Symbol("opf-editor.fontGate");
14
15
 
15
16
  /**
16
- * Wrap a browser font registry (`loadBrowserFontRegistry` from `@openpresentation/opf-render/fonts-browser`).
17
- * Registries without the lazy loaders (renderer 0.10.0 and older) gate nothing and keep the fonts they have.
17
+ * The fonts a preview draws with: the handle's text measurement and nothing else. A full handle also lists `embeddedFonts`, which
18
+ * the renderer writes into every SVG it returns (an export needs that, so the SVG carries its faces); a preview inside a page
19
+ * that already loaded the faces would repeat megabytes of base64 in every canvas, thumbnail and panel preview. The measurement
20
+ * is read from the handle at each use, so faces it loads later still measure.
21
+ */
22
+ export function previewFonts(fonts) {
23
+ if (!fonts || typeof fonts !== "object") return undefined;
24
+ return { get textMeasurement() { return fonts.textMeasurement; } };
25
+ }
26
+
27
+ /**
28
+ * The gate over a fonts handle: the two calls the canvas and the export need, with the editor's error contract.
29
+ *
30
+ * - `pending(presentation, renderOptions?)` lists the faces (vendored files and script packages) the document needs that are
31
+ * not loaded. Synchronous; empty means the document can render now. Never throws: a document the renderer rejects for other
32
+ * reasons reports nothing pending and fails with its own validation message when rendered. A handle without `pending`
33
+ * (a plain `{ textMeasurement }`) gates nothing.
34
+ * - `ensure(presentation, { signal, renderOptions })` loads what is missing through the handle's `ensure`, then checks that
35
+ * nothing is left. It rejects with a `fonts-unavailable` error (the handle's error is its `cause`), never retries by itself,
36
+ * and rethrows an abort as it is.
18
37
  *
19
- * - `pending(document, renderOptions?)` lists the faces (script packages and vendored files) the document needs that are
20
- * not loaded. Synchronous; empty means the document can render now. Never throws: a document the renderer rejects for
21
- * other reasons reports nothing pending and fails with its own validation message when rendered. Script faces and
22
- * vendored faces are asked separately, so one that cannot be answered does not hide the other.
23
- * - `ensure(document, { signal, renderOptions })` loads vendored faces first (script fallback depends on them), then script faces,
24
- * and repeats until nothing is pending. It makes a bounded number of rounds and rejects with a `fonts-unavailable`
25
- * error (the registry's error is its `cause`); it never retries by itself, a later call is a new attempt.
26
- * - `run(document, handlers)` is `whenFontsReady` on this gate.
27
- * - `renderOptions` (per call, or `createFontGate(registry, { renderOptions })` as an object or a getter) are the options the
28
- * host renders with (`catalogs`, ...). They are passed to the registry, so a layout or font scheme that only the host's
29
- * catalogs know resolves the same way there (renderer 0.11.5 and newer; older renderers ignore them).
38
+ * `renderOptions` are the options the host renders with (`catalogs`, ...); they reach the handle, so a layout or font scheme
39
+ * that only the host's catalogs know resolves the same way there. Hand a gate back unchanged.
30
40
  */
31
- export function createFontGate(registry, gateOptions = {}) {
32
- // FF-41: the render options (`catalogs` above all) the document resolves with. The registry resolves a document the way the
33
- // host renders it only when it gets the same options, so every call passes them: the gate's defaults (an object, or a
34
- // function called each time, for options that change like `canvas.setRenderOptions`) under the call's own.
35
- const defaults = () => (typeof gateOptions.renderOptions === "function" ? gateOptions.renderOptions() : gateOptions.renderOptions) ?? {};
36
- const optionsFor = (renderOptions) => ({ ...defaults(), ...renderOptions });
37
- // Each source answers on its own: a document one loader cannot resolve (an unresolvable layout throws in the renderer's
38
- // lazy font loader) must not hide what the other still reports pending.
39
- const sources = (document, renderOptions) => {
40
- const options = optionsFor(renderOptions);
41
- let scripts = [], lazy = [];
42
- try { scripts = registry?.pendingScripts?.(document, options) ?? []; } catch { /* the renderer reports the document itself when it renders */ }
43
- try { lazy = (registry?.pendingLazyFonts?.(document, options) ?? []).map((face) => face?.file ?? String(face)); } catch { /* likewise */ }
44
- return { lazy, scripts };
45
- };
46
- const pending = (document, renderOptions) => {
47
- const { lazy, scripts } = sources(document, renderOptions);
48
- return [...lazy, ...scripts];
41
+ export function fontGate(fonts) {
42
+ if (!fonts || typeof fonts !== "object") return undefined;
43
+ if (fonts[GATE]) return fonts;
44
+ const pending = (presentation, renderOptions) => {
45
+ try { return fonts.pending?.(presentation, renderOptions) ?? []; } catch { return []; /* the renderer reports the document itself when it renders */ }
49
46
  };
50
47
  const gate = {
48
+ [GATE]: true,
51
49
  pending,
52
- async ensure(document, options = {}) {
53
- const { signal, renderOptions } = options;
54
- const call = { ...optionsFor(renderOptions), signal };
50
+ async ensure(presentation, { signal, renderOptions } = {}) {
51
+ if (!pending(presentation, renderOptions).length) return;
55
52
  try {
56
- for (let round = 0; round < MAX_ROUNDS; round++) {
57
- const { lazy, scripts } = sources(document, renderOptions);
58
- if (!lazy.length && !scripts.length) break;
59
- signal?.throwIfAborted?.();
60
- // Vendored faces first (script fallback depends on them). A source with nothing pending is not asked to load.
61
- if (lazy.length) await registry.ensureLazyFonts?.(document, call);
62
- if (scripts.length) await registry.ensureScripts?.(document, call);
63
- }
53
+ signal?.throwIfAborted?.();
54
+ await fonts.ensure(presentation, { ...renderOptions, signal });
64
55
  } catch (cause) {
65
56
  if (signal?.aborted) throw cause;
66
57
  throw fontsUnavailable(cause?.message ?? String(cause), cause);
67
58
  }
68
- const left = pending(document, renderOptions);
59
+ const left = pending(presentation, renderOptions);
69
60
  if (left.length) throw fontsUnavailable(`${left.join(", ")} did not finish loading.`);
70
61
  },
71
62
  };
72
- gate.run = (document, handlers) => whenFontsReady(gate, document, handlers);
73
63
  return gate;
74
64
  }
75
65
 
@@ -88,15 +78,16 @@ export function fontsPendingError(pending) {
88
78
  }
89
79
 
90
80
  /**
91
- * Ensure fonts for a document, then run `ready`: the one "ensure fonts, then render" helper.
81
+ * Ensure fonts for a presentation, then run `ready`: the one "ensure fonts, then render" helper.
92
82
  * With nothing pending, `ready` runs synchronously, so documents in already-loaded fonts render at once. Otherwise
93
83
  * `loading(pending)` runs, the faces load and `ready` runs after them. `isCurrent()` is checked after the wait, so a
94
84
  * result for a superseded document is dropped. A load failure, and an exception thrown by `ready`, go to `failed(error)`
95
- * (they are rethrown when there is no `failed`). `renderOptions` are passed to the gate. `gate` is any object with
96
- * `pending(document, renderOptions)` and `ensure(document, { renderOptions })`;
97
- * without one the document is treated as ready. Resolves once `ready` or `failed` has run.
85
+ * (they are rethrown when there is no `failed`). `renderOptions` reach the fonts handle. `fonts` is the renderer's fonts
86
+ * handle (anything with `pending(presentation, renderOptions)` and `ensure(presentation, options)`); without one the
87
+ * document is treated as ready. Resolves once `ready` or `failed` has run.
98
88
  */
99
- export function whenFontsReady(gate, document, { isCurrent = () => true, loading, ready, failed, renderOptions } = {}) {
89
+ export function whenFontsReady(fonts, presentation, { isCurrent = () => true, loading, ready, failed, renderOptions } = {}) {
90
+ const gate = fontGate(fonts);
100
91
  const finish = () => {
101
92
  try {
102
93
  ready?.();
@@ -105,13 +96,13 @@ export function whenFontsReady(gate, document, { isCurrent = () => true, loading
105
96
  failed(error);
106
97
  }
107
98
  };
108
- const left = gate?.pending?.(document, renderOptions) ?? [];
99
+ const left = gate?.pending(presentation, renderOptions) ?? [];
109
100
  if (!left.length) {
110
101
  finish();
111
102
  return Promise.resolve();
112
103
  }
113
104
  loading?.(left);
114
- return Promise.resolve(gate.ensure(document, { renderOptions })).then(
105
+ return Promise.resolve(gate.ensure(presentation, { renderOptions })).then(
115
106
  () => {
116
107
  if (isCurrent()) finish();
117
108
  },
package/dist/galleries.js CHANGED
@@ -119,28 +119,15 @@ export async function loadOpfGallery(input, options = {}) {
119
119
  }
120
120
  const FONT_SCHEME_ENUMS = {
121
121
  type: ["sans-serif", "serif", "monospace"],
122
- app: ["PowerPoint", "Google Slides"],
123
- languageFamily: ["latin", "ea", "cs"],
122
+ app: ["powerpoint", "google-slides"],
123
+ languageFamily: ["latin", "ea", "cs", "eastAsian", "complexScript"],
124
124
  };
125
- // A Font role object ({ family, weight?, style?, letterSpacing? }), or undefined.
125
+ // A font role is a family name (a non-empty string), or undefined.
126
126
  function fontRole(value) {
127
- if (typeof value === "string") return value ? { family: value } : undefined;
128
- if (!value || typeof value !== "object" || typeof value.family !== "string")
129
- return undefined;
130
- const role = { family: value.family };
131
- if (typeof value.weight === "number" && Number.isFinite(value.weight))
132
- role.weight = value.weight;
133
- if (value.style === "normal" || value.style === "italic")
134
- role.style = value.style;
135
- if (
136
- typeof value.letterSpacing === "number" &&
137
- Number.isFinite(value.letterSpacing)
138
- )
139
- role.letterSpacing = value.letterSpacing;
140
- return role;
127
+ return typeof value === "string" && value ? value : undefined;
141
128
  }
142
129
  // Keep every font-scheme role that the catalog record schema defines: the OOXML
143
- // pair and `code`. Gallery `heading`/`body` (string or Font object) map onto
130
+ // pair and `code`. Gallery `heading`/`body` (family names) map onto
144
131
  // major/minor only, never role objects, so a later inline design.fontScheme
145
132
  // major/minor override still wins (resolveFontFamilies checks roles before the
146
133
  // pair). `accent` is not part of the record schema and is dropped.
@@ -152,11 +139,11 @@ function fontSchemeRecord(source, id) {
152
139
  major:
153
140
  typeof source.major === "string"
154
141
  ? source.major
155
- : fontRole(source.heading)?.family,
142
+ : fontRole(source.heading),
156
143
  minor:
157
144
  typeof source.minor === "string"
158
145
  ? source.minor
159
- : fontRole(source.body)?.family,
146
+ : fontRole(source.body),
160
147
  };
161
148
  for (const [field, values] of Object.entries(FONT_SCHEME_ENUMS))
162
149
  if (values.includes(source[field])) record[field] = source[field];
@@ -164,7 +151,7 @@ function fontSchemeRecord(source, id) {
164
151
  if (code) record.code = code;
165
152
  return record;
166
153
  }
167
- function attachDefinition(document, descriptor) {
154
+ function attachDefinition(presentation, descriptor) {
168
155
  const source = descriptor.metadata?.source,
169
156
  category = descriptor.category ?? descriptor.metadata?.category;
170
157
  if (!source) return;
@@ -179,33 +166,30 @@ function attachDefinition(document, descriptor) {
179
166
  if (!id) return;
180
167
  if (
181
168
  catalogs[kind]?.some((record) => record.id === id) ||
182
- document.catalogs?.[kind]?.records?.some((record) => record.id === id)
169
+ presentation.catalogs?.[kind]?.records?.some((record) => record.id === id)
183
170
  )
184
171
  return;
185
172
  let record;
186
173
  if (kind === "layouts") {
187
- const n = Math.min(
188
- 12,
189
- Number(String(source.contentMultiple ?? "").replace("x", "")) || 1,
190
- );
191
- const type =
192
- { Image: "picture", Chart: "chart", List: "list" }[source.contentType] ??
193
- "text";
174
+ // A gallery layout item is a layout record: its placeholders, design hints and composition are the
175
+ // record's own. Only an item that declares no placeholders gets the default title and text.
176
+ const placeholders = Array.isArray(source.placeholders)
177
+ ? source.placeholders
178
+ .filter((placeholder) => typeof placeholder?.type === "string")
179
+ .map((placeholder) => ({ type: placeholder.type }))
180
+ : [{ type: "title" }, { type: "text" }];
181
+ const plain = (value) =>
182
+ value && typeof value === "object" && !Array.isArray(value)
183
+ ? structuredClone(value)
184
+ : undefined;
194
185
  record = {
195
186
  $schema: "https://openpresentation.org/schema/opf-layout/v1",
196
187
  id,
197
188
  name: source.label ?? source.name ?? id,
198
- placeholders: [
199
- { type: "title" },
200
- ...Array.from({ length: n }, () => ({ type })),
201
- ],
202
- ...(n > 1
203
- ? {
204
- composition: {
205
- mode: n <= 3 ? "row" : "grid",
206
- ...(n > 3 ? { columns: 2 } : {}),
207
- },
208
- }
189
+ ...(plain(source.design) ? { design: plain(source.design) } : {}),
190
+ placeholders,
191
+ ...(plain(source.composition)
192
+ ? { composition: plain(source.composition) }
209
193
  : {}),
210
194
  };
211
195
  } else if (kind === "fontSchemes") record = fontSchemeRecord(source, id);
@@ -215,10 +199,10 @@ function attachDefinition(document, descriptor) {
215
199
  $schema: `https://openpresentation.org/schema/opf-${kind === "themes" ? "theme" : "color-scheme"}/v1`,
216
200
  };
217
201
  }
218
- document.catalogs ??= {};
219
- document.catalogs[kind] ??= { records: [] };
220
- document.catalogs[kind].records ??= [];
221
- document.catalogs[kind].records.push(record);
202
+ presentation.catalogs ??= {};
203
+ presentation.catalogs[kind] ??= { records: [] };
204
+ presentation.catalogs[kind].records ??= [];
205
+ presentation.catalogs[kind].records.push(record);
222
206
  }
223
207
  export async function loadOpfGalleryItem(item, { gallery, ...options } = {}) {
224
208
  const base = gallery ?? options.base;
@@ -234,10 +218,10 @@ export async function loadOpfGalleryItem(item, { gallery, ...options } = {}) {
234
218
  );
235
219
  if (!descriptor) throw new Error("This gallery item was not found.");
236
220
  }
237
- const document = structuredClone(unwrapOpf(descriptor));
238
- if (!document?.slides)
221
+ const presentation = structuredClone(unwrapOpf(descriptor));
222
+ if (!presentation?.slides)
239
223
  throw new Error("This gallery item does not include a presentation.");
240
- attachDefinition(document, descriptor);
224
+ attachDefinition(presentation, descriptor);
241
225
  // PPTX.gallery's older descriptors omit inline layout records. Resolve only named
242
226
  // layout references from its known, same-origin registry; no arbitrary dependency crawl.
243
227
  if (
@@ -246,13 +230,13 @@ export async function loadOpfGalleryItem(item, { gallery, ...options } = {}) {
246
230
  ) {
247
231
  const missing = [
248
232
  ...new Set(
249
- document.slides
233
+ presentation.slides
250
234
  .map((slide) => slide.layout)
251
235
  .filter(
252
236
  (id) =>
253
237
  typeof id === "string" &&
254
238
  !catalogs.layouts.some((record) => record.id === id) &&
255
- !document.catalogs?.layouts?.records?.some(
239
+ !presentation.catalogs?.layouts?.records?.some(
256
240
  (record) => record.id === id,
257
241
  ),
258
242
  ),
@@ -269,9 +253,9 @@ export async function loadOpfGalleryItem(item, { gallery, ...options } = {}) {
269
253
  `/registry/layouts/${id}.json`,
270
254
  { ...options, base: url, origin: new URL(url).origin },
271
255
  );
272
- attachDefinition(document, dependency);
256
+ attachDefinition(presentation, dependency);
273
257
  }
274
258
  }
275
- assertOpf(document);
276
- return document;
259
+ assertOpf(presentation);
260
+ return presentation;
277
261
  }
@@ -34,18 +34,18 @@ export interface PreparedGridChange {
34
34
  path: string;
35
35
  /** The id of the shared dataset the edit changes, when the chart or table shows one (RR-54). */
36
36
  dataset?: string;
37
- document: unknown;
37
+ presentation: unknown;
38
38
  patches: JsonPatchOperation[];
39
39
  changed: boolean;
40
40
  [key: string]: unknown;
41
41
  }
42
- export interface GridChange extends Omit<EditorChange, "document" | "patches"> {
42
+ export interface GridChange extends Omit<EditorChange, "presentation" | "patches"> {
43
43
  action: string;
44
44
  kind: "chart" | "table";
45
45
  path: string;
46
46
  /** The id of the shared dataset the edit changed, when the chart or table shows one. */
47
47
  dataset?: string;
48
- document: unknown;
48
+ presentation: unknown;
49
49
  patches: JsonPatchOperation[];
50
50
  changed: boolean;
51
51
  [key: string]: unknown;
@@ -136,11 +136,11 @@ export declare function chartColumnRoles(columnCount: number, chartType?: string
136
136
  /** The text a cell shows. */
137
137
  export declare function cellText(raw: unknown, kind: "chart" | "table", decimal?: "." | ","): string;
138
138
  /**
139
- * Find the data a path points at (`…chart`, `…chart.data`, `…table`). Throws `grid-target-not-found`, `table-not-found`, `chart-data-source` or
139
+ * Find the data a path points at (`…chart`, `…chart.data`, `…table`). Throws `grid-target-not-found`, `table-not-found` or
140
140
  * `dataset-unavailable`. For a chart or table that shows a shared dataset, `data` is the dataset, `dataParts` is `["datasets", id]` and `dataset`
141
141
  * says which columns `fields` selects (`indices`, into the dataset's own columns) and where the reference is (`refParts`).
142
142
  */
143
- export declare function locateGridData(document: unknown, path: string | string[]): {
143
+ export declare function locateGridData(presentation: unknown, path: string | string[]): {
144
144
  kind: "chart" | "table";
145
145
  parts: string[];
146
146
  dataParts: string[];
@@ -150,48 +150,46 @@ export declare function locateGridData(document: unknown, path: string | string[
150
150
  dataset?: { id: string; fields?: string[]; indices: number[]; refParts: string[]; ref: Record<string, unknown> };
151
151
  };
152
152
  /** The chart or table a selection path belongs to, or undefined. */
153
- export declare function resolveDataGridTarget(document: unknown, selectedPath: string): DataGridTarget | undefined;
153
+ export declare function resolveDataGridTarget(presentation: unknown, selectedPath: string): DataGridTarget | undefined;
154
154
  /** The grid view of a chart or table: lines of cells with spans, covered positions, text, warnings. */
155
- export declare function describeDataGrid(document: unknown, path: string, options?: GridOptions): DataGridDescription;
155
+ export declare function describeDataGrid(presentation: unknown, path: string, options?: GridOptions): DataGridDescription;
156
156
  /** The smallest guarded patch from `before` to `after` at `parts`. */
157
157
  export declare function gridPatches(parts: string[], before: unknown, after: unknown): JsonPatchOperation[];
158
158
  /** The problems the edits would have, without applying them or validating the document. */
159
- export declare function gridCellIssues(document: unknown, path: string, edits: GridCellEdit[], options?: GridOptions): GridIssue[];
159
+ export declare function gridCellIssues(presentation: unknown, path: string, edits: GridCellEdit[], options?: GridOptions): GridIssue[];
160
160
  /** The text of a range (`{ from, to }` addresses; the whole grid when omitted) as TSV, or another delimiter. */
161
- export declare function gridRangeText(document: unknown, path: string, range?: { from: GridAddress; to: GridAddress }, options?: GridOptions): string;
161
+ export declare function gridRangeText(presentation: unknown, path: string, range?: { from: GridAddress; to: GridAddress }, options?: GridOptions): string;
162
162
 
163
- export declare function prepareGridCells(document: unknown, path: string, edits: GridCellEdit[], options?: GridOptions): PreparedGridChange;
164
- export declare function preparePaste(document: unknown, path: string, anchor: GridAddress, source: string | string[][], options?: GridOptions): PreparedGridChange;
165
- export declare function prepareInsertRows(document: unknown, path: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
166
- export declare function prepareDeleteRows(document: unknown, path: string, indices: number[], options?: GridOptions): PreparedGridChange;
167
- export declare function prepareMoveRows(document: unknown, path: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
168
- export declare function prepareInsertColumns(document: unknown, path: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
169
- export declare function prepareDeleteColumns(document: unknown, path: string, indices: number[], options?: GridOptions): PreparedGridChange;
170
- export declare function prepareMoveColumns(document: unknown, path: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
171
- export declare function prepareSortRows(document: unknown, path: string, column: number, options?: GridSortOptions): PreparedGridChange;
172
- export declare function prepareSetHeader(document: unknown, path: string, enabled: boolean, options?: { use?: "first-row" | "new" }): PreparedGridChange;
173
- export declare function prepareTranspose(document: unknown, path: string): PreparedGridChange;
163
+ export declare function prepareGridCells(presentation: unknown, path: string, edits: GridCellEdit[], options?: GridOptions): PreparedGridChange;
164
+ export declare function preparePaste(presentation: unknown, path: string, anchor: GridAddress, source: string | string[][], options?: GridOptions): PreparedGridChange;
165
+ export declare function prepareInsertRows(presentation: unknown, path: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
166
+ export declare function prepareDeleteRows(presentation: unknown, path: string, indices: number[], options?: GridOptions): PreparedGridChange;
167
+ export declare function prepareMoveRows(presentation: unknown, path: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
168
+ export declare function prepareInsertColumns(presentation: unknown, path: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
169
+ export declare function prepareDeleteColumns(presentation: unknown, path: string, indices: number[], options?: GridOptions): PreparedGridChange;
170
+ export declare function prepareMoveColumns(presentation: unknown, path: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
171
+ export declare function prepareSortRows(presentation: unknown, path: string, column: number, options?: GridSortOptions): PreparedGridChange;
172
+ export declare function prepareSetHeader(presentation: unknown, path: string, enabled: boolean, options?: { use?: "first-row" | "new" }): PreparedGridChange;
173
+ export declare function prepareTranspose(presentation: unknown, path: string): PreparedGridChange;
174
174
  /** Why `format` is not a valid number format ("#,##0", "0.0%", "$#,##0.00"), or undefined when it is valid or empty (empty clears the format). */
175
175
  export declare function columnFormatError(format: unknown): string | undefined;
176
- /** Whether the installed core has the RR-54 chart and table data contract (number formats, datasets, strict chart numbers). An older core edits documents that use none of it. */
177
- export declare const supportsChartTableData: boolean;
178
176
  /**
179
177
  * Set or clear (`null` or "") the number format of column `column` (its header cell). A string header becomes `{ name, format }` and returns to a string
180
178
  * when the format is cleared. Throws `number-format-invalid` with the reason, or `grid-no-header` for a table without a header row.
181
179
  */
182
- export declare function prepareGridColumnFormat(document: unknown, path: string, column: number, format: string | null | undefined, options?: GridOptions): PreparedGridChange;
180
+ export declare function prepareGridColumnFormat(presentation: unknown, path: string, column: number, format: string | null | undefined, options?: GridOptions): PreparedGridChange;
183
181
  /** The columns a chart plots and how they got that role (the chart's `mapping`, or core's positional default). */
184
- export declare function describeChartMapping(document: unknown, path: string): ChartMappingDescription;
182
+ export declare function describeChartMapping(presentation: unknown, path: string): ChartMappingDescription;
185
183
  /**
186
184
  * Set a chart's category, X and series columns by name; what `wanted` leaves out keeps its current value. A field that equals the default is not
187
185
  * written, and `chart.mapping` is removed when nothing is left. One patch. Throws `chart-mapping-unknown-column`, `chart-mapping-x-unsupported`,
188
186
  * `chart-mapping-conflict` or `chart-mapping-no-series`.
189
187
  */
190
- export declare function prepareChartMapping(document: unknown, path: string, wanted?: ChartMappingColumns): PreparedGridChange;
188
+ export declare function prepareChartMapping(presentation: unknown, path: string, wanted?: ChartMappingColumns): PreparedGridChange;
191
189
  /** Replace the chart's or table's dataset reference with its own inline copy of the data. The dataset stays for the other items. One patch. */
192
- export declare function prepareDetachDataset(document: unknown, path: string): PreparedGridChange;
190
+ export declare function prepareDetachDataset(presentation: unknown, path: string): PreparedGridChange;
193
191
  /** The charts and tables that use dataset `id`, in document order. */
194
- export declare function datasetUsage(document: unknown, id: string): Array<{ kind: "chart" | "table"; path: string; parts: string[] }>;
192
+ export declare function datasetUsage(presentation: unknown, id: string): Array<{ kind: "chart" | "table"; path: string; parts: string[] }>;
195
193
 
196
194
  export declare function setGridCells(editor: EditorSession, path: string, edits: GridCellEdit[], options?: GridOptions): GridChange;
197
195
  export declare function pasteGridText(editor: EditorSession, path: string, anchor: GridAddress, source: string | string[][], options?: GridOptions): GridChange;