@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
@@ -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
 
@@ -1,4 +1,5 @@
1
- import { catalogKinds, catalogSchemaNames, schemas, validateCatalogRecord, validatePresentation } from "@openpresentation/opf";
1
+ import { catalogKinds, catalogSchemaNames, schemas, validateCatalogRecord } from "@openpresentation/opf";
2
+ import { checkFormat, firstErrorMessage } from "./checks.js";
2
3
  import { getCatalogOptions } from "./index.js";
3
4
  import { schemaAtPath, schemaVariants } from "./schema.js";
4
5
  import { applyEdits, findNodeAtLocation, findNodeAtOffset, getNodePath, modify, parseTree } from "jsonc-parser";
@@ -19,8 +20,8 @@ function placeholderSummary(types) {
19
20
  return "Placeholders not specified";
20
21
  if (!types.length)
21
22
  return "No placeholders";
22
- const labels = { title: "Title", subtitle: "Subtitle", text: "Text", metric: "Metric", list: "List", picture: "Image", chart: "Chart", table: "Table", code: "Code", media: "Video", quote: "Quote", timeline: "Timeline", tag: "Tag", diagram: "Image" };
23
- const order = ["title", "subtitle", "text", "metric", "list", "picture", "chart", "table", "code", "media", "quote", "timeline", "tag", "diagram"];
23
+ const labels = { title: "Title", subtitle: "Subtitle", tag: "Tag", text: "Text", list: "List", image: "Image", video: "Video", chart: "Chart", table: "Table", code: "Code", metric: "Metric", quote: "Quote", timeline: "Timeline" };
24
+ const order = ["title", "subtitle", "tag", "text", "list", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
24
25
  return [...new Set(types)].sort((a, b) => order.indexOf(a) - order.indexOf(b)).map(type => {
25
26
  const count = types.filter(value => value === type).length;
26
27
  return `${labels[type] ?? type}${count > 1 ? ` × ${count}` : ""}`;
@@ -64,9 +65,9 @@ function describeOptions(choices, records, current, catalog) {
64
65
  // The installed schema identifies fields; a document field merely named "layout"
65
66
  // inside arbitrary metadata is not mistaken for an OPF layout reference.
66
67
  export function getJsonFieldContext(source, position, loadedCatalogs = {}) {
67
- let document;
68
+ let presentation;
68
69
  try {
69
- document = JSON.parse(source);
70
+ presentation = JSON.parse(source);
70
71
  }
71
72
  catch {
72
73
  return null;
@@ -84,18 +85,18 @@ export function getJsonFieldContext(source, position, loadedCatalogs = {}) {
84
85
  const path = getNodePath(node);
85
86
  if (!path.length)
86
87
  return null;
87
- const schema = schemaAtPath(document, path.map(String));
88
+ const schema = schemaAtPath(presentation, path.map(String));
88
89
  const variants = schemaVariants(schema);
89
90
  const description = String(schema.description ?? "");
90
91
  let catalog = catalogFor(description);
91
92
  // Audience arrays inherit the catalog context from the array property.
92
93
  if (!catalog && typeof path.at(-1) === "number")
93
- catalog = catalogFor(String(schemaAtPath(document, path.slice(0, -1).map(String)).description ?? ""));
94
+ catalog = catalogFor(String(schemaAtPath(presentation, path.slice(0, -1).map(String)).description ?? ""));
94
95
  const options = new Map();
95
96
  const records = new Map();
96
97
  let unloadedSource = false;
97
98
  if (catalog && typeof node.value === "string") {
98
- const entries = document.catalogs;
99
+ const entries = presentation.catalogs;
99
100
  const local = entries?.[catalog];
100
101
  unloadedSource = Boolean(local?.source);
101
102
  for (const option of getCatalogOptions(catalog)) {
@@ -148,18 +149,18 @@ export function replaceFieldOption(context, value) {
148
149
  if (!context.options.some(option => option.value === value))
149
150
  throw new Error("This choice is no longer available.");
150
151
  let next = context.source.slice(0, context.offset) + JSON.stringify(value) + context.source.slice(context.offset + context.length);
151
- const document = JSON.parse(next);
152
- if (context.path.reduce((current, key) => current?.[key], document) !== value)
152
+ const presentation = JSON.parse(next);
153
+ if (context.path.reduce((current, key) => current?.[key], presentation) !== value)
153
154
  throw new Error("This field has a duplicate key. Resolve it in the JSON before choosing an option.");
154
155
  const types = context.options.find(option => option.value === value).placeholderTypes;
155
156
  if (value !== context.value && types && context.catalog === 'layouts' && context.path.length === 3
156
- && context.path[0] === 'slides' && context.path[2] === 'layout' && validatePresentation(document).valid) {
157
- const populated = populateLayoutPlaceholders(document, context.path[1], types);
158
- next = updateSource(next, document, populated);
157
+ && context.path[0] === 'slides' && context.path[2] === 'layout' && checkFormat(presentation).valid) {
158
+ const populated = populateLayoutPlaceholders(presentation, context.path[1], types);
159
+ next = updateSource(next, presentation, populated);
159
160
  }
160
- const result = validatePresentation(JSON.parse(next));
161
- if (!result.valid && validatePresentation(JSON.parse(context.source)).valid)
162
- throw new Error(result.errors[0]?.message ?? "This option is not valid here.");
161
+ const result = checkFormat(JSON.parse(next));
162
+ if (!result.valid && checkFormat(JSON.parse(context.source)).valid)
163
+ throw new Error(firstErrorMessage(result, "This option is not valid here."));
163
164
  return next;
164
165
  }
165
166
 
@@ -22,11 +22,11 @@ export function createLayoutHandles(root, options) {
22
22
  const edit=active;
23
23
  edit.fraction=fraction;
24
24
  try{
25
- const prepared=prepareTrackResize(editor.document,active.flow,active.boundary,fraction);
26
- options.render(prepared.document);
25
+ const prepared=prepareTrackResize(editor.presentation,active.flow,active.boundary,fraction);
26
+ options.render(prepared.presentation);
27
27
  if(active!==edit)return false;
28
28
  edit.prepared=prepared;edit.valid=true;
29
- options.onDraft?.({document:prepared.document,path:active.flow.path+'.composition',value:prepared.composition});
29
+ options.onDraft?.({presentation:prepared.presentation,path:active.flow.path+'.composition',value:prepared.composition});
30
30
  if(active!==edit)return false;
31
31
  options.clearError?.();return true;
32
32
  }catch(error){if(active===edit)edit.valid=false;options.onError(error);return false;}
@@ -41,7 +41,7 @@ export function createLayoutHandles(root, options) {
41
41
  active=null;release(edit);committing=true;
42
42
  try{
43
43
  // Rebase unrelated document edits, retaining the guarded container.
44
- const prepared=prepareTrackResize(editor.document,edit.flow,edit.boundary,edit.fraction);
44
+ const prepared=prepareTrackResize(editor.presentation,edit.flow,edit.boundary,edit.fraction);
45
45
  if(edit.moved)editor.applyPatch(prepared.patches,{source:'canvas-layout',rejectInvalid:true});
46
46
  else renderCanonical();
47
47
  options.onCommit?.({path:edit.flow.path+'.composition',editor});return true;
@@ -56,7 +56,7 @@ export function createLayoutHandles(root, options) {
56
56
  if(event)layer.setPointerCapture(event.pointerId);
57
57
  return true;
58
58
  }
59
- function update(document,geometry) {
59
+ function update(presentation,geometry) {
60
60
  lastGeometry=geometry;
61
61
  layer.replaceChildren();
62
62
  if(!enabled||options.isTextEditing())return;
@@ -2,7 +2,7 @@ import {listBlockContainers,prepareBlockInsert} from './blocks.js';
2
2
 
3
3
  const payloadFields=['text','items','bullets','image','video','chart','table','code','metric','quote','timeline'];
4
4
  const headingFields=new Set(['title','subtitle','tag']);
5
- const fields={text:'text',list:'items',picture:'image',media:'video',diagram:'image',chart:'chart',table:'table',code:'code',metric:'metric',quote:'quote',timeline:'timeline'};
5
+ const fields={text:'text',list:'items',image:'image',video:'video',chart:'chart',table:'table',code:'code',metric:'metric',quote:'quote',timeline:'timeline'};
6
6
  const region=key=>/^(?:(?:top|middle|bottom)(?:\+(?:top|middle|bottom))*(?::(?:left|center|right)(?:\+(?:left|center|right))*)?|(?:left|center|right)(?:\+(?:left|center|right))*)$/.test(key);
7
7
 
8
8
  function emptyPayload(field){
@@ -16,8 +16,8 @@ function emptyPayload(field){
16
16
  }
17
17
 
18
18
  /** Supply authored slots without replacing content, metadata or region placement. */
19
- export function populateLayoutPlaceholders(document,slideIndex,types){
20
- let next=structuredClone(document),slide=next.slides[slideIndex];
19
+ export function populateLayoutPlaceholders(presentation,slideIndex,types){
20
+ let next=structuredClone(presentation),slide=next.slides[slideIndex];
21
21
  const counts=new Map();
22
22
  function count(host){
23
23
  if(!host||typeof host!=='object')return;
@@ -48,7 +48,7 @@ export function populateLayoutPlaceholders(document,slideIndex,types){
48
48
  const path=regions.length?`/slides/${slideIndex}/${regions.at(-1)}`:`/slides/${slideIndex}`;
49
49
  const containers=listBlockContainers(next,{slideIndex,includeImplicit:true});
50
50
  if(!containers.some(container=>container.path===path))throw new Error('This slide has no editable content container for the new placeholder.');
51
- next=prepareBlockInsert(next,path,{[field]:value}).document;
51
+ next=prepareBlockInsert(next,path,{[field]:value}).presentation;
52
52
  slide=next.slides[slideIndex];
53
53
  }
54
54
  }