@openpresentation/opf-editor 0.11.3 → 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 +82 -58
  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 +23 -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 +12 -10
  17. package/dist/chart-data.js +22 -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 +179 -10
  28. package/dist/data.d.ts +23 -2
  29. package/dist/data.js +40 -1
  30. package/dist/dataset-refs.d.ts +17 -0
  31. package/dist/dataset-refs.js +45 -0
  32. package/dist/design-controls.js +66 -47
  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 +35 -20
  42. package/dist/font-gate.js +49 -58
  43. package/dist/galleries.js +36 -52
  44. package/dist/grid-model.d.ts +99 -24
  45. package/dist/grid-model.js +577 -101
  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 +136 -109
  84. package/dist/table-options.d.ts +8 -8
  85. package/dist/table-options.js +41 -30
  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 +74 -54
  94. package/package.json +10 -6
  95. package/dist/font-defaults.js +0 -25
@@ -41,14 +41,14 @@ export interface ImageDescription {
41
41
  entry?: unknown;
42
42
  }
43
43
  /** What a path holds when it is a picture, or `{ error }` with the reason it is not croppable (not a picture, a missing asset, SVG). */
44
- export declare function describeImage(document: unknown, path: string): ImageDescription | { error: string };
45
- export declare function countAssetReferences(document: unknown, id: string): number;
44
+ export declare function describeImage(presentation: unknown, path: string): ImageDescription | { error: string };
45
+ export declare function countAssetReferences(presentation: unknown, id: string): number;
46
46
  export interface CroppedPixels { dataUri: string; mediaType: string; width: number; height: number; bytes?: number }
47
47
  export interface PreparedCrop { patches: JsonPatchOperation[]; assetId: string; reference: string; origin?: string; image: ImageDescription }
48
48
  /** The patches that apply a cropped picture: a new asset and the image pointing at it (pure). Throws `not-croppable`. */
49
- export declare function prepareCrop(document: unknown, path: string, pixels: CroppedPixels): PreparedCrop;
49
+ export declare function prepareCrop(presentation: unknown, path: string, pixels: CroppedPixels): PreparedCrop;
50
50
  /** The patches that put a cropped picture back to its original asset, or null. */
51
- export declare function prepareRestore(document: unknown, path: string): { patches: JsonPatchOperation[]; origin: string; image: ImageDescription } | null;
51
+ export declare function prepareRestore(presentation: unknown, path: string): { patches: JsonPatchOperation[]; origin: string; image: ImageDescription } | null;
52
52
  /** Restore the original as one undoable change; null when there is no original. */
53
53
  export declare function restoreOriginal(editor: EditorSession, path: string, meta?: Record<string, unknown>): (EditorChange & { origin: string }) | null;
54
54
  export interface LoadedImage { image: HTMLImageElement; width: number; height: number }
@@ -156,11 +156,11 @@ export const isFullRect = (rect, bounds) => rect.x < 0.5 && rect.y < 0.5 && Math
156
156
  * (the field is `{ src, ... }`); `assetSrc` is what `src` resolves to (the asset's own `src`), and `origin` is the asset a
157
157
  * crop of this picture was made from, when there is one.
158
158
  */
159
- export function describeImage(document, path) {
159
+ export function describeImage(presentation, path) {
160
160
  const segments = String(path).split(".");
161
161
  const last = segments.at(-1);
162
162
  if (!["image", "slideImage"].includes(last)) return { error: "This is not a picture." };
163
- const value = getValueAtPath(document, path);
163
+ const value = getValueAtPath(presentation, path);
164
164
  const pointer = opfPathToJsonPointer(segments);
165
165
  let form;
166
166
  let src;
@@ -177,7 +177,7 @@ export function describeImage(document, path) {
177
177
  let assetSrc = src;
178
178
  let entry;
179
179
  if (assetId !== undefined) {
180
- entry = getValueAtPath(document, ["assets", assetId]);
180
+ entry = getValueAtPath(presentation, ["assets", assetId]);
181
181
  if (entry === undefined) return { error: `The picture asset "${assetId}" is missing from this presentation.` };
182
182
  assetSrc = typeof entry === "string" ? entry : entry?.src;
183
183
  if (typeof assetSrc !== "string") return { error: `The picture asset "${assetId}" has no source.` };
@@ -193,22 +193,22 @@ export function describeImage(document, path) {
193
193
  }
194
194
 
195
195
  /** How many places in the document use `asset:<id>` (the `assets` map itself is not counted). */
196
- export function countAssetReferences(document, id) {
196
+ export function countAssetReferences(presentation, id) {
197
197
  const reference = `asset:${id}`;
198
198
  let count = 0;
199
199
  const walk = (value) => {
200
200
  if (typeof value === "string") count += value === reference ? 1 : 0;
201
201
  else if (Array.isArray(value)) value.forEach(walk);
202
- else if (isObject(value)) for (const [key, child] of Object.entries(value)) if (key !== "assets" || value !== document) walk(child);
202
+ else if (isObject(value)) for (const [key, child] of Object.entries(value)) if (key !== "assets" || value !== presentation) walk(child);
203
203
  };
204
- walk(document);
204
+ walk(presentation);
205
205
  return count;
206
206
  }
207
207
 
208
208
  // ---- the change --------------------------------------------------------------------------------------------------------
209
209
 
210
- function assetPatches(document, id, entry) {
211
- const assets = document?.assets;
210
+ function assetPatches(presentation, id, entry) {
211
+ const assets = presentation?.assets;
212
212
  return isObject(assets)
213
213
  ? [{ op: "add", path: opfPathToJsonPointer(["assets", id]), value: entry }]
214
214
  : [{ op: "add", path: "/assets", value: { [id]: entry } }];
@@ -219,33 +219,33 @@ function assetPatches(document, id, entry) {
219
219
  * pointing at it. `pixels` is `{ dataUri, mediaType, width, height }` (from `cropImagePixels`). A previous crop's asset that
220
220
  * nothing else uses is removed in the same change. Pure.
221
221
  */
222
- export function prepareCrop(document, path, pixels) {
223
- const image = describeImage(document, path);
222
+ export function prepareCrop(presentation, path, pixels) {
223
+ const image = describeImage(presentation, path);
224
224
  if (image.error) throw fail("not-croppable", image.error, { path });
225
225
  const origin = image.origin ?? image.assetId;
226
- const id = uniqueAssetId(document, `${origin ?? "image"}-crop`);
226
+ const id = uniqueAssetId(presentation, `${origin ?? "image"}-crop`);
227
227
  const title = (isObject(image.entry) && typeof image.entry.title === "string" ? image.entry.title : origin ?? "Picture").replace(/ \(cropped\)$/, "");
228
228
  const entry = { src: pixels.dataUri, mediaType: pixels.mediaType, title: `${title} (cropped)` };
229
229
  if (image.alt) entry.alt = image.alt;
230
230
  if (origin) entry.description = `Cropped from asset:${origin}`;
231
- const patches = assetPatches(document, id, entry);
231
+ const patches = assetPatches(presentation, id, entry);
232
232
  patches.push({ op: "replace", path: image.srcPointer, value: `asset:${id}` });
233
- if (image.origin && image.assetId && countAssetReferences(document, image.assetId) === 1) patches.push({ op: "remove", path: opfPathToJsonPointer(["assets", image.assetId]) });
233
+ if (image.origin && image.assetId && countAssetReferences(presentation, image.assetId) === 1) patches.push({ op: "remove", path: opfPathToJsonPointer(["assets", image.assetId]) });
234
234
  return { patches, assetId: id, reference: `asset:${id}`, origin, image };
235
235
  }
236
236
 
237
237
  /** The patches that put a cropped picture back to the asset it was cropped from, or null when there is none. */
238
- export function prepareRestore(document, path) {
239
- const image = describeImage(document, path);
240
- if (image.error || !image.origin || getValueAtPath(document, ["assets", image.origin]) === undefined) return null;
238
+ export function prepareRestore(presentation, path) {
239
+ const image = describeImage(presentation, path);
240
+ if (image.error || !image.origin || getValueAtPath(presentation, ["assets", image.origin]) === undefined) return null;
241
241
  const patches = [{ op: "replace", path: image.srcPointer, value: `asset:${image.origin}` }];
242
- if (countAssetReferences(document, image.assetId) === 1) patches.push({ op: "remove", path: opfPathToJsonPointer(["assets", image.assetId]) });
242
+ if (countAssetReferences(presentation, image.assetId) === 1) patches.push({ op: "remove", path: opfPathToJsonPointer(["assets", image.assetId]) });
243
243
  return { patches, origin: image.origin, image };
244
244
  }
245
245
 
246
246
  /** Put a cropped picture back to its original as one undoable change. Returns null when there is no original to restore. */
247
247
  export function restoreOriginal(editor, path, meta = {}) {
248
- const prepared = prepareRestore(editor.document, path);
248
+ const prepared = prepareRestore(editor.presentation, path);
249
249
  if (!prepared) return null;
250
250
  return { ...editor.applyPatch(prepared.patches, { ...meta, source: meta.source ?? "image-restore", path }), origin: prepared.origin };
251
251
  }
@@ -325,12 +325,12 @@ export async function cropImagePixels(loaded, rect, { mediaType, maxBytes = DEFA
325
325
  * `{ assetId, reference, width, height }`.
326
326
  */
327
327
  export async function applyCrop(editor, path, rect, options = {}) {
328
- const image = describeImage(editor.document, path);
328
+ const image = describeImage(editor.presentation, path);
329
329
  if (image.error) throw fail("not-croppable", image.error, { path });
330
330
  const loaded = options.loaded ?? await loadImagePixels(image.assetSrc);
331
331
  const pixels = await cropImagePixels(loaded, rect, { mediaType: image.mediaType, maxBytes: options.maxBytes });
332
332
  // The document may have changed while the picture was encoding.
333
- const prepared = prepareCrop(editor.document, path, pixels);
333
+ const prepared = prepareCrop(editor.presentation, path, pixels);
334
334
  const change = editor.applyPatch(prepared.patches, { ...options.meta, source: options.meta?.source ?? "image-crop", path, assetId: prepared.assetId });
335
335
  return { ...change, assetId: prepared.assetId, reference: prepared.reference, width: pixels.width, height: pixels.height };
336
336
  }
@@ -121,7 +121,7 @@ export function createImageCropper(root, overlay, options) {
121
121
 
122
122
  function placePill() {
123
123
  const target = selected && !session ? frameImage(selected) : null;
124
- if (!target || !editor || describeImage(editor.document, selected).error) { pill.hidden = true; return; }
124
+ if (!target || !editor || describeImage(editor.presentation, selected).error) { pill.hidden = true; return; }
125
125
  const box = target.getBoundingClientRect();
126
126
  const base = root.getBoundingClientRect();
127
127
  if (!box.width || !box.height) { pill.hidden = true; return; }
@@ -135,7 +135,7 @@ export function createImageCropper(root, overlay, options) {
135
135
  async function open(path, openOptions = {}) {
136
136
  if (destroyed || session) return false;
137
137
  if (options.beforeOpen && !options.beforeOpen()) return false;
138
- const image = describeImage(editor.document, path);
138
+ const image = describeImage(editor.presentation, path);
139
139
  if (image.error) {
140
140
  options.report?.(new Error(image.error));
141
141
  return false;
@@ -483,7 +483,7 @@ export function createImageCropper(root, overlay, options) {
483
483
  aspectSelect.querySelector('option[value="frame"]').disabled = !frame;
484
484
  s.rect = fullRect(s.loaded.width, s.loaded.height);
485
485
  layout();
486
- restoreButton.hidden = !prepareRestore(editor.document, path);
486
+ restoreButton.hidden = !prepareRestore(editor.presentation, path);
487
487
  if (s.tool === "focus") {
488
488
  s.tool = "crop";
489
489
  setTool("focus");
package/dist/index.d.ts CHANGED
@@ -1,8 +1,17 @@
1
1
  import type { PaginationOptions, PaginationResult } from "@openpresentation/opf/pagination";
2
2
  import type { Composition, ComposeSlideOptions, SlideComposition } from "@openpresentation/opf/composition";
3
- /** Reported when a font-scheme id matches no inline or bundled record; the default font scheme (`aptos`) is the base. */
4
- export interface FontSchemeDiagnostic { code: "unresolved-font-scheme"; path: string; message: string; id: string; fallback: string }
5
- export interface EditorDiagnosticOptions { onDiagnostic?: (diagnostic: FontSchemeDiagnostic) => void }
3
+ import type { Fonts, PresentationStats, SlideContextDiagnostic, StatsOptions, ValidationReport } from "@openpresentation/opf";
4
+ /**
5
+ * What the editor adds to the options of `composeSlide` and `paginateSlide`: `fonts` is the renderer's fonts handle (`loadFonts()`), whose
6
+ * `textMeasurement` measures the text; `catalogs` are host catalog records; `onDiagnostic` hears every `unresolved-font-scheme`,
7
+ * `unresolved-layout`, `unresolved-theme` and `unresolved-color-scheme` diagnostic of core's `resolveSlideContext`. Any other option overrides
8
+ * the resolved one.
9
+ */
10
+ export interface EditorDiagnosticOptions {
11
+ fonts?: Fonts;
12
+ catalogs?: Record<string, readonly unknown[]>;
13
+ onDiagnostic?: (diagnostic: SlideContextDiagnostic) => void;
14
+ }
6
15
  export declare const packageName = "@openpresentation/opf-editor";
7
16
 
8
17
  export declare const releaseLane: Readonly<{
@@ -29,16 +38,9 @@ export type JsonPatchOperation =
29
38
  | { op: "copy"; from: string; path: string }
30
39
  | { op: "test"; path: string; value: unknown };
31
40
 
32
- export interface OPFValidationSummary {
33
- valid: boolean;
34
- errors: unknown[];
35
- warnings: unknown[];
36
- result: unknown;
37
- }
38
-
39
41
  export interface EditorSnapshot {
40
- document: unknown;
41
- validation: OPFValidationSummary;
42
+ presentation: unknown;
43
+ validation: ValidationReport;
42
44
  canUndo: boolean;
43
45
  canRedo: boolean;
44
46
  undoDepth: number;
@@ -46,11 +48,11 @@ export interface EditorSnapshot {
46
48
  }
47
49
 
48
50
  export interface EditorChange {
49
- document: unknown;
51
+ presentation: unknown;
50
52
  patches: JsonPatchOperation[];
51
53
  inversePatches?: JsonPatchOperation[];
52
54
  redoPatches?: JsonPatchOperation[];
53
- validation: OPFValidationSummary;
55
+ validation: ValidationReport;
54
56
  }
55
57
 
56
58
  export interface EditorEvent {
@@ -58,7 +60,7 @@ export interface EditorEvent {
58
60
  patches: JsonPatchOperation[];
59
61
  inversePatches?: JsonPatchOperation[];
60
62
  redoPatches?: JsonPatchOperation[];
61
- validation: OPFValidationSummary;
63
+ validation: ValidationReport;
62
64
  meta?: Record<string, unknown>;
63
65
  snapshot: EditorSnapshot;
64
66
  }
@@ -70,8 +72,8 @@ export interface EditorSession {
70
72
  paginateSlide(slideIndex: number, options?: PaginationOptions & EditorDiagnosticOptions, meta?: Record<string, unknown>): { change: EditorChange | null; pagination: PaginationResult };
71
73
  composeSlide(slideIndex: number, options?: ComposeSlideOptions & EditorDiagnosticOptions): SlideComposition;
72
74
  setComposition(slideIndex: number, composition: Composition, meta?: Record<string, unknown>): EditorChange;
73
- readonly document: unknown;
74
- readonly validation: OPFValidationSummary;
75
+ readonly presentation: unknown;
76
+ readonly validation: ValidationReport;
75
77
  readonly canUndo: boolean;
76
78
  readonly canRedo: boolean;
77
79
  snapshot(): EditorSnapshot;
@@ -84,13 +86,13 @@ export interface EditorSession {
84
86
  /** The undo and redo stacks as plain data (oldest entry first), for hosts that persist work. */
85
87
  exportHistory(): EditorHistory;
86
88
  /** Replace the document and, optionally, the history in one step (one `restore` event). A history that does not replay against the document throws `invalid-history` before anything changes. */
87
- restoreState(state: { document: unknown; undo?: EditorHistoryEntry[]; redo?: EditorHistoryEntry[] }, meta?: Record<string, unknown>): EditorChange;
89
+ restoreState(state: { presentation: unknown; undo?: EditorHistoryEntry[]; redo?: EditorHistoryEntry[] }, meta?: Record<string, unknown>): EditorChange;
88
90
  undo(meta?: Record<string, unknown>): EditorChange | null;
89
91
  redo(meta?: Record<string, unknown>): EditorChange | null;
90
92
  }
91
93
 
92
94
  export interface CreateEditorSessionOptions {
93
- validate?: (document: unknown) => unknown;
95
+ /** Refuse every edit whose result has an error finding (the default per edit is `meta.rejectInvalid`). */
94
96
  rejectInvalid?: boolean;
95
97
  }
96
98
 
@@ -160,20 +162,18 @@ export declare function opfPathToJsonPointer(path: string | string[]): string;
160
162
 
161
163
  export declare function jsonPointerToOpfPath(pointer: string): string;
162
164
 
163
- export declare function getValueAtPath(document: unknown, path: string | string[], fallback?: unknown): unknown;
164
-
165
- export declare function hasValueAtPath(document: unknown, path: string | string[]): boolean;
165
+ export declare function getValueAtPath(presentation: unknown, path: string | string[], fallback?: unknown): unknown;
166
166
 
167
- export declare function createValuePatch(document: unknown, path: string | string[], value: unknown): JsonPatchOperation[];
167
+ export declare function hasValueAtPath(presentation: unknown, path: string | string[]): boolean;
168
168
 
169
- export declare function applyJsonPatch(document: unknown, operations: JsonPatchOperation[]): unknown;
169
+ export declare function createValuePatch(presentation: unknown, path: string | string[], value: unknown): JsonPatchOperation[];
170
170
 
171
- export declare function invertJsonPatch(document: unknown, operations: JsonPatchOperation[]): JsonPatchOperation[];
171
+ export declare function applyJsonPatch(presentation: unknown, operations: JsonPatchOperation[]): unknown;
172
172
 
173
- export declare function validateOpfDocument(document: unknown, validator?: (document: unknown) => unknown): OPFValidationSummary;
173
+ export declare function invertJsonPatch(presentation: unknown, operations: JsonPatchOperation[]): JsonPatchOperation[];
174
174
 
175
- /** Font families the slide resolves to (slide, deck, theme, then the shared default scheme). */
176
- export declare function resolveSlideFonts(document: unknown, slideIndex?: number, options?: EditorDiagnosticOptions): { heading?: string; body?: string; code?: string; [role: string]: string | undefined };
175
+ /** Core's `stats` of the session's document: neutral facts (counts, words, notes coverage, images and alt text, speaking time), never severities. Reads the JSON only. */
176
+ export declare function deckStats(editor: EditorSession, options?: StatsOptions): PresentationStats;
177
177
 
178
178
  export declare function createEditorSession(input: unknown, options?: CreateEditorSessionOptions): EditorSession;
179
179
 
package/dist/index.js CHANGED
@@ -1,38 +1,20 @@
1
1
  import { paginateSlide } from "@openpresentation/opf/pagination";
2
- import { PatchError, applyPatch as applyCorePatch, applyPatchWithInverse, formatPointer, invertPatch, jsonEqual, parsePointer, readPointer } from "@openpresentation/opf/patch";
2
+ import { OPFPatchError, applyPatch as applyCorePatch, applyPatchWithInverse, formatPointer, invertPatch, jsonEqual, parsePointer, readPointer } from "@openpresentation/opf/patch";
3
3
  import { collectReservedPresentationIds } from "./presentation-ids.js";
4
- import { DEFAULT_FONT_SCHEME, resolveFontSchemeReference } from "./font-defaults.js";
5
- import { composeSlide, resolveCanvasDimensions, resolveFontFamilies } from "@openpresentation/opf/composition";
6
- import {
7
- catalogKinds,
8
- catalogs as bundledCatalogs,
9
- validatePresentation
10
- } from "@openpresentation/opf";
11
-
12
- function resolveCompositionOptions(document, slideIndex, { onDiagnostic, ...options } = {}) {
13
- const slide = document.slides?.[slideIndex];
14
- if (!Number.isInteger(slideIndex) || !slide) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
15
- const inline = document.catalogs?.layouts?.records ?? [];
16
- const layout = options.layout ?? inline.find(record => record.id === slide.layout)
17
- ?? bundledCatalogs.layouts.find(record => record.id === slide.layout);
18
- const themeReference = slide.design?.theme ?? document.design?.theme ?? "minimal";
19
- const themeId = typeof themeReference === "string" ? themeReference : themeReference.id;
20
- const theme = document.catalogs?.themes?.records?.find(record => record.id === themeId)
21
- ?? bundledCatalogs.themes.find(record => record.id === themeId);
22
- const reference = slide.design?.fontScheme ?? document.design?.fontScheme ?? theme?.fontScheme ?? DEFAULT_FONT_SCHEME;
23
- const fontPath = slide.design?.fontScheme !== undefined ? `slides.${slideIndex}.design.fontScheme` : document.design?.fontScheme !== undefined ? "design.fontScheme" : slide.design?.theme !== undefined ? `slides.${slideIndex}.design.theme` : "design.theme";
24
- const { scheme: fontScheme, diagnostic } = resolveFontSchemeReference(reference, id => document.catalogs?.fontSchemes?.records?.find(record=>record.id===id) ?? bundledCatalogs.fontSchemes.find(record=>record.id===id), fontPath);
25
- if (diagnostic) onDiagnostic?.(diagnostic);
26
- return { ...resolveCanvasDimensions(slide.design?.dimensions ?? document.design?.dimensions ?? theme?.dimensions), fonts:resolveFontFamilies(fontScheme), contentAlignment:slide.design?.contentAlignment??document.design?.contentAlignment, titleAlignment:slide.design?.titleAlignment??document.design?.titleAlignment, contentBox:slide.design?.contentBox??document.design?.contentBox, presentation:document, ...options, layout, slideIndex };
27
- }
28
-
29
- /**
30
- * The font families a slide resolves to (`heading`, `body`, `code`), by the same slide, deck,
31
- * theme, default order that composition, pagination and export use. Hosts read this after a
32
- * font-scheme or theme switch to show the fonts the preview now uses.
33
- */
34
- export function resolveSlideFonts(document, slideIndex = 0, options = {}) {
35
- return resolveCompositionOptions(document, slideIndex, options).fonts;
4
+ import { composeSlide } from "@openpresentation/opf/composition";
5
+ import { catalogKinds, catalogs as bundledCatalogs, resolveSlideContext, stats } from "@openpresentation/opf";
6
+ import { checkFormat, errorFindings } from "./checks.js";
7
+
8
+ // The options `composeSlide` and `paginateSlide` take for one slide of the open deck. Core's `resolveSlideContext` resolves the
9
+ // slide's canvas, layout, theme, colour scheme and font families the one way every engine does (slide design, deck design,
10
+ // theme, default), so the editor measures and composes what the renderer draws and the exporter writes. `options.fonts` is the
11
+ // renderer's fonts handle (its `textMeasurement` measures); `options.onDiagnostic` hears each `unresolved-*` diagnostic;
12
+ // any other option overrides the resolved one (`layout`, ...).
13
+ function slideContext(presentation, slideIndex, { fonts, catalogs, onDiagnostic, ...overrides } = {}) {
14
+ if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
15
+ const { options, diagnostics } = resolveSlideContext(presentation, slideIndex, { fonts, catalogs });
16
+ for (const diagnostic of diagnostics) onDiagnostic?.(diagnostic);
17
+ return { ...options, ...overrides };
36
18
  }
37
19
 
38
20
  export const packageName = "@openpresentation/opf-editor";
@@ -85,20 +67,20 @@ export function jsonPointerToOpfPath(pointer) {
85
67
  return parseJsonPointer(pointer).join(".");
86
68
  }
87
69
 
88
- export function getValueAtPath(document, path, fallback) {
89
- const found = readPointer(document, splitOpfPath(path));
70
+ export function getValueAtPath(presentation, path, fallback) {
71
+ const found = readPointer(presentation, splitOpfPath(path));
90
72
  return found.found ? found.value : fallback;
91
73
  }
92
74
 
93
- export function hasValueAtPath(document, path) {
94
- return readPointer(document, splitOpfPath(path)).found;
75
+ export function hasValueAtPath(presentation, path) {
76
+ return readPointer(presentation, splitOpfPath(path)).found;
95
77
  }
96
78
 
97
- export function createValuePatch(document, path, value) {
79
+ export function createValuePatch(presentation, path, value) {
98
80
  const pointer = normalizePatchPath(path);
99
81
  return [
100
82
  {
101
- op: hasValueAtPath(document, pointer) ? "replace" : "add",
83
+ op: hasValueAtPath(presentation, pointer) ? "replace" : "add",
102
84
  path: pointer,
103
85
  value: clone(value)
104
86
  }
@@ -108,40 +90,40 @@ export function createValuePatch(document, path, value) {
108
90
  // Patch semantics (RFC 6902, pointers, inverse patches) live in core's
109
91
  // "@openpresentation/opf/patch"; the editor adds its path spellings (dotted OPF
110
92
  // paths), its error type and its clone policy on top.
111
- export function applyJsonPatch(document, operations) {
93
+ export function applyJsonPatch(presentation, operations) {
112
94
  assertPatchOperations(operations);
113
95
  const patch = operations.map(normalizeOperation);
114
96
  try {
115
- return applyCorePatch(document, patch);
97
+ return applyCorePatch(presentation, patch);
116
98
  } catch (error) {
117
99
  throw editorPatchError(error);
118
100
  }
119
101
  }
120
102
 
121
- export function invertJsonPatch(document, operations) {
103
+ export function invertJsonPatch(presentation, operations) {
122
104
  assertPatchOperations(operations);
123
105
  const patch = operations.map(normalizeOperation);
124
106
  try {
125
- return invertPatch(document, patch);
107
+ return invertPatch(presentation, patch);
126
108
  } catch (error) {
127
109
  throw editorPatchError(error);
128
110
  }
129
111
  }
130
112
 
131
- export function validateOpfDocument(document, validator = validatePresentation) {
132
- const result = validator(document);
133
- const valid = Boolean(result?.valid);
134
- return {
135
- valid,
136
- errors: Array.isArray(result?.errors) ? result.errors : [],
137
- warnings: Array.isArray(result?.warnings) ? result.warnings : [],
138
- result
139
- };
113
+ /**
114
+ * Facts about the open presentation, from core's `stats`: slide, layout and section counts, payload kinds, words and notes coverage,
115
+ * images and their alt text, charts, tables, citations, fonts and an estimated speaking time (`options`: `perSlide`, `values`,
116
+ * `wordsPerMinute`). They are facts, never findings: no severities and no judgment about whether a number is too high, so a deck-info
117
+ * view can show them next to the Review panel. It reads the JSON only, so it also works on a document that fails validation.
118
+ */
119
+ export function deckStats(editor, options) {
120
+ assertEditorSession(editor);
121
+ return stats(editor.presentation, options);
140
122
  }
141
123
 
142
124
  export function createEditorSession(input, options = {}) {
143
- let document = parseInput(input);
144
- let validation = validateOpfDocument(document, options.validate ?? validatePresentation);
125
+ let presentation = parseInput(input);
126
+ let validation = checkFormat(presentation);
145
127
  const undoStack = [];
146
128
  const redoStack = [];
147
129
  const listeners = new Set();
@@ -157,26 +139,26 @@ export function createEditorSession(input, options = {}) {
157
139
  function commitPatch(operations, meta = {}) {
158
140
  assertPatchOperations(operations);
159
141
  const patches = operations.map(normalizeOperation);
160
- const before = document;
142
+ const before = presentation;
161
143
  let next, inversePatches;
162
144
  try {
163
- ({ document: next, inverse: inversePatches } = applyPatchWithInverse(before, patches));
145
+ ({ presentation: next, inverse: inversePatches } = applyPatchWithInverse(before, patches));
164
146
  } catch (error) {
165
147
  throw editorPatchError(error);
166
148
  }
167
- const nextValidation = validateOpfDocument(next, options.validate ?? validatePresentation);
149
+ const nextValidation = checkFormat(next);
168
150
 
169
151
  if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
170
152
  throw new OPFEditorError("invalid-opf-edit", "OPF edit produced an invalid document.", {
171
- issues: nextValidation.errors,
153
+ issues: errorFindings(nextValidation),
172
154
  patches
173
155
  });
174
156
  }
175
157
 
176
158
  if (patches.every(patch => patch.op === "test")) return {
177
- document: clone(document), patches: clonePatchOperations(patches), inversePatches: [], validation: nextValidation
159
+ presentation: clone(presentation), patches: clonePatchOperations(patches), inversePatches: [], validation: nextValidation
178
160
  };
179
- document = next;
161
+ presentation = next;
180
162
  validation = nextValidation;
181
163
  undoStack.push({ patches: clonePatchOperations(patches), inversePatches, meta });
182
164
  redoStack.length = 0;
@@ -189,7 +171,7 @@ export function createEditorSession(input, options = {}) {
189
171
  });
190
172
 
191
173
  return {
192
- document: clone(document),
174
+ presentation: clone(presentation),
193
175
  patches: clonePatchOperations(patches),
194
176
  inversePatches: clonePatchOperations(inversePatches),
195
177
  validation
@@ -197,8 +179,8 @@ export function createEditorSession(input, options = {}) {
197
179
  }
198
180
 
199
181
  const editor = {
200
- get document() {
201
- return clone(document);
182
+ get presentation() {
183
+ return clone(presentation);
202
184
  },
203
185
  get validation() {
204
186
  return validation;
@@ -211,7 +193,7 @@ export function createEditorSession(input, options = {}) {
211
193
  },
212
194
  snapshot() {
213
195
  return snapshotCache ??= freezeSnapshot({
214
- document: clone(document),
196
+ presentation: clone(presentation),
215
197
  validation: clone(validation),
216
198
  canUndo: undoStack.length > 0,
217
199
  canRedo: redoStack.length > 0,
@@ -227,33 +209,35 @@ export function createEditorSession(input, options = {}) {
227
209
  return () => listeners.delete(listener);
228
210
  },
229
211
  get(path, fallback) {
230
- return getValueAtPath(document, path, fallback);
212
+ return getValueAtPath(presentation, path, fallback);
231
213
  },
232
214
  set(path, value, meta = {}) {
233
- return commitPatch(createValuePatch(document, path, value), {
215
+ return commitPatch(createValuePatch(presentation, path, value), {
234
216
  ...meta,
235
217
  path: normalizePatchPath(path)
236
218
  });
237
219
  },
238
220
  composeSlide(slideIndex, options = {}) {
239
- return composeSlide(document.slides?.[slideIndex], resolveCompositionOptions(document, slideIndex, options));
221
+ return composeSlide(presentation.slides?.[slideIndex], slideContext(presentation, slideIndex, options));
240
222
  },
241
223
  paginateSlide(slideIndex, options = {}, meta = {}) {
242
- const resolved = resolveCompositionOptions(document, slideIndex, options);
243
- const pagination = paginateSlide(document.slides[slideIndex], {
224
+ // Core pagination reads the measurement from `fonts`, so the resolved context hands it over that way.
225
+ const { textMeasurement, ...resolved } = slideContext(presentation, slideIndex, options);
226
+ const pagination = paginateSlide(presentation.slides[slideIndex], {
244
227
  ...resolved,
245
- reservedIds: collectReservedPresentationIds(document),
228
+ fonts: options.fonts ?? (textMeasurement ? { textMeasurement } : undefined),
229
+ reservedIds: collectReservedPresentationIds(presentation),
246
230
  });
247
231
  // Pagination can persist a readability policy without adding a page. Commit
248
232
  // that change too, so preview/export use the same minimum that was evaluated.
249
- if (pagination.slides.length === 1 && jsonEqual(pagination.slides[0], document.slides[slideIndex])) return { change: null, pagination };
250
- const slides = [...document.slides];
233
+ if (pagination.slides.length === 1 && jsonEqual(pagination.slides[0], presentation.slides[slideIndex])) return { change: null, pagination };
234
+ const slides = [...presentation.slides];
251
235
  slides.splice(slideIndex, 1, ...pagination.slides);
252
236
  const change = editor.set('slides', slides, { ...meta, rejectInvalid: true, pagination: pagination.pages });
253
237
  return { change, pagination };
254
238
  },
255
239
  setComposition(slideIndex, composition, meta = {}) {
256
- if (!Number.isInteger(slideIndex) || !document.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
240
+ if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
257
241
  return editor.set(`slides.${slideIndex}.composition`, composition, { ...meta, rejectInvalid: true });
258
242
  },
259
243
  setGroupComposition(path, composition, meta = {}) {
@@ -268,7 +252,7 @@ export function createEditorSession(input, options = {}) {
268
252
  applyPatch: commitPatch,
269
253
  /**
270
254
  * The undo and redo stacks as plain data (oldest entry first): `{ undo: [{ patches, inversePatches, meta }], redo: [...] }`.
271
- * Together with `document` this is everything `restoreState` needs to bring a session back, for hosts that persist work.
255
+ * Together with `presentation` this is everything `restoreState` needs to bring a session back, for hosts that persist work.
272
256
  */
273
257
  exportHistory() {
274
258
  const entry = (item) => ({ patches: clonePatchOperations(item.patches), inversePatches: clonePatchOperations(item.inversePatches), meta: clone(item.meta ?? {}) });
@@ -281,11 +265,11 @@ export function createEditorSession(input, options = {}) {
281
265
  * before anything changes, so a stale or damaged copy never corrupts the session. Emits one `restore` event.
282
266
  */
283
267
  restoreState(state, meta = {}) {
284
- if (!state || typeof state !== "object") throw new OPFEditorError("invalid-state", "restoreState needs { document, undo?, redo? }.");
285
- const next = parseInput(state.document);
286
- const nextValidation = validateOpfDocument(next, options.validate ?? validatePresentation);
268
+ if (!state || typeof state !== "object") throw new OPFEditorError("invalid-state", "restoreState needs { presentation, undo?, redo? }.");
269
+ const next = parseInput(state.presentation);
270
+ const nextValidation = checkFormat(next);
287
271
  if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
288
- throw new OPFEditorError("invalid-opf-edit", "The restored document is not valid OPF.", { issues: nextValidation.errors });
272
+ throw new OPFEditorError("invalid-opf-edit", "The restored document is not valid OPF.", { issues: errorFindings(nextValidation) });
289
273
  }
290
274
  const undo = state.undo ?? [], redo = state.redo ?? [];
291
275
  const wellFormed = (entries) => Array.isArray(entries) && entries.every((item) => item && Array.isArray(item.patches) && Array.isArray(item.inversePatches));
@@ -307,20 +291,20 @@ export function createEditorSession(input, options = {}) {
307
291
  throw new OPFEditorError("invalid-history", "The undo history does not belong to this document.", { cause: error instanceof Error ? error.message : String(error) });
308
292
  }
309
293
  const entry = (item) => ({ patches: item.patches.map(normalizeOperation), inversePatches: item.inversePatches.map(normalizeOperation), meta: clone(item.meta ?? {}) });
310
- document = next;
294
+ presentation = next;
311
295
  validation = nextValidation;
312
296
  undoStack.length = 0;
313
297
  redoStack.length = 0;
314
298
  undoStack.push(...undo.map(entry));
315
299
  redoStack.push(...redo.map(entry));
316
300
  emit({ type: "restore", patches: [], validation, meta });
317
- return { document: clone(document), patches: [], validation };
301
+ return { presentation: clone(presentation), patches: [], validation };
318
302
  },
319
303
  undo(meta = {}) {
320
304
  const entry = undoStack.pop();
321
305
  if (!entry) return null;
322
- document = applyJsonPatch(document, entry.inversePatches);
323
- validation = validateOpfDocument(document, options.validate ?? validatePresentation);
306
+ presentation = applyJsonPatch(presentation, entry.inversePatches);
307
+ validation = checkFormat(presentation);
324
308
  redoStack.push(entry);
325
309
  emit({
326
310
  type: "undo",
@@ -330,7 +314,7 @@ export function createEditorSession(input, options = {}) {
330
314
  meta
331
315
  });
332
316
  return {
333
- document: clone(document),
317
+ presentation: clone(presentation),
334
318
  patches: clonePatchOperations(entry.inversePatches),
335
319
  redoPatches: clonePatchOperations(entry.patches),
336
320
  validation
@@ -339,8 +323,8 @@ export function createEditorSession(input, options = {}) {
339
323
  redo(meta = {}) {
340
324
  const entry = redoStack.pop();
341
325
  if (!entry) return null;
342
- document = applyJsonPatch(document, entry.patches);
343
- validation = validateOpfDocument(document, options.validate ?? validatePresentation);
326
+ presentation = applyJsonPatch(presentation, entry.patches);
327
+ validation = checkFormat(presentation);
344
328
  undoStack.push(entry);
345
329
  emit({
346
330
  type: "redo",
@@ -350,7 +334,7 @@ export function createEditorSession(input, options = {}) {
350
334
  meta
351
335
  });
352
336
  return {
353
- document: clone(document),
337
+ presentation: clone(presentation),
354
338
  patches: clonePatchOperations(entry.patches),
355
339
  inversePatches: clonePatchOperations(entry.inversePatches),
356
340
  validation
@@ -473,7 +457,7 @@ export function assertCatalogId(catalogKind, id, options = {}) {
473
457
  export function setCatalogId(editor, path, catalogKind, id, meta = {}) {
474
458
  assertEditorSession(editor);
475
459
  const catalogId = assertCatalogId(catalogKind, id, {
476
- presentation: meta.presentation ?? editor.document,
460
+ presentation: meta.presentation ?? editor.presentation,
477
461
  catalogs: meta.catalogs,
478
462
  catalogSources: meta.catalogSources
479
463
  });
@@ -534,7 +518,7 @@ export function createCatalogSelect(editor, options) {
534
518
 
535
519
  for (const option of getCatalogOptions(catalogKind, {
536
520
  ...options,
537
- presentation: options?.presentation ?? editor.document
521
+ presentation: options?.presentation ?? editor.presentation
538
522
  })) {
539
523
  const optionElement = documentRef.createElement("option");
540
524
  optionElement.value = option.id;
@@ -636,12 +620,12 @@ function normalizeOperation(operation) {
636
620
 
637
621
  // Core patch errors keep their stable codes; the editor reports them as OPFEditorError.
638
622
  function editorPatchError(error) {
639
- if (!(error instanceof PatchError)) return error;
623
+ if (!(error instanceof OPFPatchError)) return error;
640
624
  const details = {};
641
625
  if (error.path !== undefined) details.path = error.path;
642
626
  if (error.operation !== undefined) details.operation = error.operation;
643
627
  if (error.index !== undefined) details.index = error.index;
644
- if (error.validation) details.issues = error.validation.errors;
628
+ if (error.validation) details.issues = errorFindings(error.validation);
645
629
  return new OPFEditorError(error.code, error.message, details);
646
630
  }
647
631