@openpresentation/opf-editor 0.12.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +122 -80
  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 +16 -16
  6. package/dist/background-options.d.ts +12 -5
  7. package/dist/background-options.js +44 -19
  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 +30 -31
  15. package/dist/canvas.js +64 -68
  16. package/dist/catalogs.d.ts +48 -0
  17. package/dist/catalogs.js +217 -0
  18. package/dist/chart-data.d.ts +10 -10
  19. package/dist/chart-data.js +21 -21
  20. package/dist/chart-options-panel.d.ts +2 -2
  21. package/dist/chart-options-panel.js +82 -8
  22. package/dist/chart-options.d.ts +35 -9
  23. package/dist/chart-options.js +133 -40
  24. package/dist/checks.js +23 -0
  25. package/dist/color-authoring.js +1 -1
  26. package/dist/content-actions.d.ts +17 -17
  27. package/dist/content-actions.js +74 -73
  28. package/dist/content-controls.js +21 -21
  29. package/dist/data-grid.js +7 -8
  30. package/dist/data.d.ts +4 -4
  31. package/dist/data.js +5 -5
  32. package/dist/dataset-refs.d.ts +2 -2
  33. package/dist/dataset-refs.js +5 -5
  34. package/dist/design-controls.d.ts +12 -5
  35. package/dist/design-controls.js +312 -111
  36. package/dist/design-options.d.ts +20 -18
  37. package/dist/design-options.js +91 -99
  38. package/dist/edit-helpers.js +8 -7
  39. package/dist/exact-source.js +5 -5
  40. package/dist/export.d.ts +7 -7
  41. package/dist/export.js +48 -17
  42. package/dist/find-panel.js +5 -5
  43. package/dist/find-replace.d.ts +3 -3
  44. package/dist/find-replace.js +23 -20
  45. package/dist/font-gate.js +49 -58
  46. package/dist/galleries.js +50 -101
  47. package/dist/grid-model.d.ts +25 -27
  48. package/dist/grid-model.js +92 -102
  49. package/dist/image-crop.d.ts +4 -4
  50. package/dist/image-crop.js +28 -29
  51. package/dist/image-cropper.js +3 -3
  52. package/dist/image-options.d.ts +45 -0
  53. package/dist/image-options.js +195 -0
  54. package/dist/index.d.ts +48 -31
  55. package/dist/index.js +154 -126
  56. package/dist/json-editor.js +2 -2
  57. package/dist/json-menu.js +2 -2
  58. package/dist/json-options.d.ts +3 -3
  59. package/dist/json-options.js +44 -54
  60. package/dist/layout-handles.js +5 -5
  61. package/dist/layout-placeholders.js +4 -4
  62. package/dist/layout.d.ts +2 -2
  63. package/dist/layout.js +8 -7
  64. package/dist/numbering-panel.js +3 -5
  65. package/dist/numbering.d.ts +3 -5
  66. package/dist/numbering.js +14 -26
  67. package/dist/outline-view.js +24 -24
  68. package/dist/outline.d.ts +12 -12
  69. package/dist/outline.js +52 -50
  70. package/dist/persistence.d.ts +1 -1
  71. package/dist/persistence.js +19 -18
  72. package/dist/presentation-ids.js +2 -2
  73. package/dist/react.js +3 -3
  74. package/dist/review-panel.d.ts +30 -13
  75. package/dist/review-panel.js +160 -53
  76. package/dist/review.d.ts +36 -43
  77. package/dist/review.js +92 -53
  78. package/dist/rich-text-input.js +4 -3
  79. package/dist/rich-text-toolbar.js +5 -3
  80. package/dist/rich-text.js +5 -2
  81. package/dist/schema-inspector.d.ts +2 -2
  82. package/dist/schema-inspector.js +16 -11
  83. package/dist/schema.d.ts +1 -1
  84. package/dist/schema.js +26 -6
  85. package/dist/slide-manager.d.ts +2 -2
  86. package/dist/slide-manager.js +19 -19
  87. package/dist/slides.d.ts +21 -21
  88. package/dist/slides.js +104 -101
  89. package/dist/svelte.js +2 -3
  90. package/dist/switches.d.ts +37 -22
  91. package/dist/switches.js +282 -171
  92. package/dist/table-options.d.ts +8 -8
  93. package/dist/table-options.js +28 -27
  94. package/dist/table-structure.d.ts +10 -10
  95. package/dist/table-structure.js +21 -21
  96. package/dist/template-panel.d.ts +3 -3
  97. package/dist/template-panel.js +37 -13
  98. package/dist/templates.d.ts +22 -11
  99. package/dist/templates.js +48 -55
  100. package/dist/transfer.d.ts +15 -4
  101. package/dist/transfer.js +107 -161
  102. package/package.json +12 -6
  103. package/dist/font-defaults.js +0 -25
package/dist/index.js CHANGED
@@ -1,38 +1,42 @@
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, resolveSlideContext, stats } from "@openpresentation/opf";
6
+ import { checkFormat, errorFindings } from "./checks.js";
7
+ import { forkEditedRecords, forkNotice, listCatalogRecords, mergeCatalogs } from "./catalogs.js";
8
+
9
+ export {
10
+ REFERENCE_FINDING_CODES,
11
+ applyCatalogUpdate,
12
+ catalogRecordAt,
13
+ catalogRecordLabel,
14
+ catalogsFor,
15
+ checkCatalogUpdates,
16
+ editedCatalogRecords,
17
+ FORKED_RECORD_KINDS,
18
+ forkEditedRecords,
19
+ forkNotice,
20
+ listCatalogRecords,
21
+ mergeCatalogs,
22
+ moveToCustom,
23
+ prepareMoveToCustom,
24
+ prepareSave,
25
+ referenceFindings,
26
+ saveDocument,
27
+ } from "./catalogs.js";
28
+
29
+ // The options `composeSlide` and `paginateSlide` take for one slide of the open deck. Core's `resolveSlideContext` resolves the
30
+ // slide's canvas, layout, theme, colour scheme and font families the one way every engine does (slide design, deck design,
31
+ // theme, engine default), with the session's registered catalogs, so the editor measures and composes what the renderer draws
32
+ // and the exporter writes. `options.fonts` is the renderer's fonts handle (its `textMeasurement` measures); `options.catalogs`
33
+ // adds catalogs for this call; `options.onDiagnostic` hears each diagnostic (`unresolved-reference`);
34
+ // any other option overrides the resolved one (`layout`, ...).
35
+ function slideContext(presentation, slideIndex, catalogs, { fonts, catalogs: _extra, onDiagnostic, ...overrides } = {}) {
36
+ if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
37
+ const { options, diagnostics } = resolveSlideContext(presentation, slideIndex, { fonts, catalogs });
38
+ for (const diagnostic of diagnostics) onDiagnostic?.(diagnostic);
39
+ return { ...options, ...overrides };
36
40
  }
37
41
 
38
42
  export const packageName = "@openpresentation/opf-editor";
@@ -85,20 +89,20 @@ export function jsonPointerToOpfPath(pointer) {
85
89
  return parseJsonPointer(pointer).join(".");
86
90
  }
87
91
 
88
- export function getValueAtPath(document, path, fallback) {
89
- const found = readPointer(document, splitOpfPath(path));
92
+ export function getValueAtPath(presentation, path, fallback) {
93
+ const found = readPointer(presentation, splitOpfPath(path));
90
94
  return found.found ? found.value : fallback;
91
95
  }
92
96
 
93
- export function hasValueAtPath(document, path) {
94
- return readPointer(document, splitOpfPath(path)).found;
97
+ export function hasValueAtPath(presentation, path) {
98
+ return readPointer(presentation, splitOpfPath(path)).found;
95
99
  }
96
100
 
97
- export function createValuePatch(document, path, value) {
101
+ export function createValuePatch(presentation, path, value) {
98
102
  const pointer = normalizePatchPath(path);
99
103
  return [
100
104
  {
101
- op: hasValueAtPath(document, pointer) ? "replace" : "add",
105
+ op: hasValueAtPath(presentation, pointer) ? "replace" : "add",
102
106
  path: pointer,
103
107
  value: clone(value)
104
108
  }
@@ -108,40 +112,43 @@ export function createValuePatch(document, path, value) {
108
112
  // Patch semantics (RFC 6902, pointers, inverse patches) live in core's
109
113
  // "@openpresentation/opf/patch"; the editor adds its path spellings (dotted OPF
110
114
  // paths), its error type and its clone policy on top.
111
- export function applyJsonPatch(document, operations) {
115
+ export function applyJsonPatch(presentation, operations) {
112
116
  assertPatchOperations(operations);
113
117
  const patch = operations.map(normalizeOperation);
114
118
  try {
115
- return applyCorePatch(document, patch);
119
+ return applyCorePatch(presentation, patch);
116
120
  } catch (error) {
117
121
  throw editorPatchError(error);
118
122
  }
119
123
  }
120
124
 
121
- export function invertJsonPatch(document, operations) {
125
+ export function invertJsonPatch(presentation, operations) {
122
126
  assertPatchOperations(operations);
123
127
  const patch = operations.map(normalizeOperation);
124
128
  try {
125
- return invertPatch(document, patch);
129
+ return invertPatch(presentation, patch);
126
130
  } catch (error) {
127
131
  throw editorPatchError(error);
128
132
  }
129
133
  }
130
134
 
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
- };
135
+ /**
136
+ * Facts about the open presentation, from core's `stats`: slide, layout and section counts, payload kinds, words and notes coverage,
137
+ * images and their alt text, charts, tables, citations, fonts and an estimated speaking time (`options`: `perSlide`, `values`,
138
+ * `wordsPerMinute`). They are facts, never findings: no severities and no judgment about whether a number is too high, so a deck-info
139
+ * view can show them next to the Review panel. It reads the JSON only, so it also works on a document that fails validation.
140
+ */
141
+ export function deckStats(editor, options) {
142
+ assertEditorSession(editor);
143
+ return stats(editor.presentation, options);
140
144
  }
141
145
 
142
146
  export function createEditorSession(input, options = {}) {
143
- let document = parseInput(input);
144
- let validation = validateOpfDocument(document, options.validate ?? validatePresentation);
147
+ let presentation = parseInput(input);
148
+ // The host's registered catalogs (FA-23): one list, handed to every core call, the renderer and the exporter.
149
+ let catalogs = mergeCatalogs(options.catalogs);
150
+ const check = (document) => checkFormat(document, { catalogs });
151
+ let validation = check(presentation);
145
152
  const undoStack = [];
146
153
  const redoStack = [];
147
154
  const listeners = new Set();
@@ -156,27 +163,44 @@ export function createEditorSession(input, options = {}) {
156
163
 
157
164
  function commitPatch(operations, meta = {}) {
158
165
  assertPatchOperations(operations);
159
- const patches = operations.map(normalizeOperation);
160
- const before = document;
166
+ let patches = operations.map(normalizeOperation);
167
+ const before = presentation;
161
168
  let next, inversePatches;
162
169
  try {
163
- ({ document: next, inverse: inversePatches } = applyPatchWithInverse(before, patches));
170
+ ({ presentation: next, inverse: inversePatches } = applyPatchWithInverse(before, patches));
164
171
  } catch (error) {
165
172
  throw editorPatchError(error);
166
173
  }
167
- const nextValidation = validateOpfDocument(next, options.validate ?? validatePresentation);
174
+ // Owner rule (FA-23): an edit of a record under catalogs.default or a named group forks it into catalogs.custom (every reference
175
+ // follows, the original is dropped), in this same undo step. Catalog updates and moves change catalog records on purpose.
176
+ let forks = [];
177
+ if (meta.catalogRecordEdit !== "in-place") {
178
+ const forked = forkEditedRecords(before, next, { catalogs, forkIds: meta.forkIds });
179
+ if (forked.forks.length) {
180
+ forks = forked.forks;
181
+ for (const key of new Set([...Object.keys(next), ...Object.keys(forked.document)]))
182
+ if (!jsonEqual(next[key], forked.document[key])) patches = [...patches, forked.document[key] === undefined ? { op: "remove", path: formatPointer([key]) } : { op: Object.hasOwn(next, key) ? "replace" : "add", path: formatPointer([key]), value: clone(forked.document[key]) }];
183
+ try {
184
+ ({ presentation: next, inverse: inversePatches } = applyPatchWithInverse(before, patches));
185
+ } catch (error) {
186
+ throw editorPatchError(error);
187
+ }
188
+ meta = { ...meta, forked: forks, notice: forks.map(forkNotice).join(" ") };
189
+ }
190
+ }
191
+ const nextValidation = check(next);
168
192
 
169
193
  if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
170
194
  throw new OPFEditorError("invalid-opf-edit", "OPF edit produced an invalid document.", {
171
- issues: nextValidation.errors,
195
+ issues: errorFindings(nextValidation),
172
196
  patches
173
197
  });
174
198
  }
175
199
 
176
200
  if (patches.every(patch => patch.op === "test")) return {
177
- document: clone(document), patches: clonePatchOperations(patches), inversePatches: [], validation: nextValidation
201
+ presentation: clone(presentation), patches: clonePatchOperations(patches), inversePatches: [], validation: nextValidation
178
202
  };
179
- document = next;
203
+ presentation = next;
180
204
  validation = nextValidation;
181
205
  undoStack.push({ patches: clonePatchOperations(patches), inversePatches, meta });
182
206
  redoStack.length = 0;
@@ -189,20 +213,35 @@ export function createEditorSession(input, options = {}) {
189
213
  });
190
214
 
191
215
  return {
192
- document: clone(document),
216
+ presentation: clone(presentation),
193
217
  patches: clonePatchOperations(patches),
194
218
  inversePatches: clonePatchOperations(inversePatches),
195
- validation
219
+ validation,
220
+ ...(forks.length ? { forked: forks, notice: meta.notice } : {})
196
221
  };
197
222
  }
198
223
 
199
224
  const editor = {
200
- get document() {
201
- return clone(document);
225
+ get presentation() {
226
+ return clone(presentation);
202
227
  },
203
228
  get validation() {
204
229
  return validation;
205
230
  },
231
+ /** The catalogs the host registered (`Catalog[]`, frozen): the first is the host default. */
232
+ get catalogs() {
233
+ return catalogs;
234
+ },
235
+ /**
236
+ * Replace the registered catalogs (for example after the host loads a company catalog). The document is not changed; it is
237
+ * re-validated against the new list (reference findings depend on it) and one `catalogs` event tells every view to redraw.
238
+ */
239
+ setCatalogs(next, meta = {}) {
240
+ catalogs = mergeCatalogs(next);
241
+ validation = check(presentation);
242
+ emit({ type: "catalogs", patches: [], validation, meta });
243
+ return catalogs;
244
+ },
206
245
  get canUndo() {
207
246
  return undoStack.length > 0;
208
247
  },
@@ -211,7 +250,7 @@ export function createEditorSession(input, options = {}) {
211
250
  },
212
251
  snapshot() {
213
252
  return snapshotCache ??= freezeSnapshot({
214
- document: clone(document),
253
+ presentation: clone(presentation),
215
254
  validation: clone(validation),
216
255
  canUndo: undoStack.length > 0,
217
256
  canRedo: redoStack.length > 0,
@@ -227,33 +266,37 @@ export function createEditorSession(input, options = {}) {
227
266
  return () => listeners.delete(listener);
228
267
  },
229
268
  get(path, fallback) {
230
- return getValueAtPath(document, path, fallback);
269
+ return getValueAtPath(presentation, path, fallback);
231
270
  },
232
271
  set(path, value, meta = {}) {
233
- return commitPatch(createValuePatch(document, path, value), {
272
+ return commitPatch(createValuePatch(presentation, path, value), {
234
273
  ...meta,
235
274
  path: normalizePatchPath(path)
236
275
  });
237
276
  },
238
277
  composeSlide(slideIndex, options = {}) {
239
- return composeSlide(document.slides?.[slideIndex], resolveCompositionOptions(document, slideIndex, options));
278
+ return composeSlide(presentation.slides?.[slideIndex], slideContext(presentation, slideIndex, mergeCatalogs(catalogs, options.catalogs), options));
240
279
  },
241
280
  paginateSlide(slideIndex, options = {}, meta = {}) {
242
- const resolved = resolveCompositionOptions(document, slideIndex, options);
243
- const pagination = paginateSlide(document.slides[slideIndex], {
281
+ const list = mergeCatalogs(catalogs, options.catalogs);
282
+ // Core pagination reads the measurement from `fonts`, so the resolved context hands it over that way.
283
+ const { textMeasurement, ...resolved } = slideContext(presentation, slideIndex, list, options);
284
+ const pagination = paginateSlide(presentation.slides[slideIndex], {
244
285
  ...resolved,
245
- reservedIds: collectReservedPresentationIds(document),
286
+ catalogs: list,
287
+ fonts: options.fonts ?? (textMeasurement ? { textMeasurement } : undefined),
288
+ reservedIds: collectReservedPresentationIds(presentation),
246
289
  });
247
290
  // Pagination can persist a readability policy without adding a page. Commit
248
291
  // 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];
292
+ if (pagination.slides.length === 1 && jsonEqual(pagination.slides[0], presentation.slides[slideIndex])) return { change: null, pagination };
293
+ const slides = [...presentation.slides];
251
294
  slides.splice(slideIndex, 1, ...pagination.slides);
252
295
  const change = editor.set('slides', slides, { ...meta, rejectInvalid: true, pagination: pagination.pages });
253
296
  return { change, pagination };
254
297
  },
255
298
  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.");
299
+ if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
257
300
  return editor.set(`slides.${slideIndex}.composition`, composition, { ...meta, rejectInvalid: true });
258
301
  },
259
302
  setGroupComposition(path, composition, meta = {}) {
@@ -268,7 +311,7 @@ export function createEditorSession(input, options = {}) {
268
311
  applyPatch: commitPatch,
269
312
  /**
270
313
  * 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.
314
+ * Together with `presentation` this is everything `restoreState` needs to bring a session back, for hosts that persist work.
272
315
  */
273
316
  exportHistory() {
274
317
  const entry = (item) => ({ patches: clonePatchOperations(item.patches), inversePatches: clonePatchOperations(item.inversePatches), meta: clone(item.meta ?? {}) });
@@ -281,11 +324,11 @@ export function createEditorSession(input, options = {}) {
281
324
  * before anything changes, so a stale or damaged copy never corrupts the session. Emits one `restore` event.
282
325
  */
283
326
  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);
327
+ if (!state || typeof state !== "object") throw new OPFEditorError("invalid-state", "restoreState needs { presentation, undo?, redo? }.");
328
+ const next = parseInput(state.presentation);
329
+ const nextValidation = check(next);
287
330
  if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
288
- throw new OPFEditorError("invalid-opf-edit", "The restored document is not valid OPF.", { issues: nextValidation.errors });
331
+ throw new OPFEditorError("invalid-opf-edit", "The restored document is not valid OPF.", { issues: errorFindings(nextValidation) });
289
332
  }
290
333
  const undo = state.undo ?? [], redo = state.redo ?? [];
291
334
  const wellFormed = (entries) => Array.isArray(entries) && entries.every((item) => item && Array.isArray(item.patches) && Array.isArray(item.inversePatches));
@@ -307,20 +350,20 @@ export function createEditorSession(input, options = {}) {
307
350
  throw new OPFEditorError("invalid-history", "The undo history does not belong to this document.", { cause: error instanceof Error ? error.message : String(error) });
308
351
  }
309
352
  const entry = (item) => ({ patches: item.patches.map(normalizeOperation), inversePatches: item.inversePatches.map(normalizeOperation), meta: clone(item.meta ?? {}) });
310
- document = next;
353
+ presentation = next;
311
354
  validation = nextValidation;
312
355
  undoStack.length = 0;
313
356
  redoStack.length = 0;
314
357
  undoStack.push(...undo.map(entry));
315
358
  redoStack.push(...redo.map(entry));
316
359
  emit({ type: "restore", patches: [], validation, meta });
317
- return { document: clone(document), patches: [], validation };
360
+ return { presentation: clone(presentation), patches: [], validation };
318
361
  },
319
362
  undo(meta = {}) {
320
363
  const entry = undoStack.pop();
321
364
  if (!entry) return null;
322
- document = applyJsonPatch(document, entry.inversePatches);
323
- validation = validateOpfDocument(document, options.validate ?? validatePresentation);
365
+ presentation = applyJsonPatch(presentation, entry.inversePatches);
366
+ validation = check(presentation);
324
367
  redoStack.push(entry);
325
368
  emit({
326
369
  type: "undo",
@@ -330,7 +373,7 @@ export function createEditorSession(input, options = {}) {
330
373
  meta
331
374
  });
332
375
  return {
333
- document: clone(document),
376
+ presentation: clone(presentation),
334
377
  patches: clonePatchOperations(entry.inversePatches),
335
378
  redoPatches: clonePatchOperations(entry.patches),
336
379
  validation
@@ -339,8 +382,8 @@ export function createEditorSession(input, options = {}) {
339
382
  redo(meta = {}) {
340
383
  const entry = redoStack.pop();
341
384
  if (!entry) return null;
342
- document = applyJsonPatch(document, entry.patches);
343
- validation = validateOpfDocument(document, options.validate ?? validatePresentation);
385
+ presentation = applyJsonPatch(presentation, entry.patches);
386
+ validation = check(presentation);
344
387
  undoStack.push(entry);
345
388
  emit({
346
389
  type: "redo",
@@ -350,7 +393,7 @@ export function createEditorSession(input, options = {}) {
350
393
  meta
351
394
  });
352
395
  return {
353
- document: clone(document),
396
+ presentation: clone(presentation),
354
397
  patches: clonePatchOperations(entry.patches),
355
398
  inversePatches: clonePatchOperations(entry.inversePatches),
356
399
  validation
@@ -431,29 +474,29 @@ export function createSvgTraceBinding(root, editor, options = {}) {
431
474
  };
432
475
  }
433
476
 
477
+ /**
478
+ * The records a catalog control can offer for `catalogKind`, from core's `catalogRecords`: the document's embedded records,
479
+ * then the registered catalogs' (`options.catalogs`, merged with `options.editor`'s). Each is the record itself.
480
+ */
434
481
  export function getCatalogRecords(catalogKind, options = {}) {
435
- assertCatalogKind(catalogKind);
436
- const presentationCatalog = options.presentation?.catalogs?.[catalogKind];
437
- const explicitCatalog = options.catalogs?.[catalogKind] ?? options.catalogSources?.[catalogKind];
438
-
439
- return firstRecords(presentationCatalog)
440
- ?? firstRecords(explicitCatalog)
441
- ?? firstRecords(bundledCatalogs[catalogKind])
442
- ?? [];
482
+ return getCatalogOptions(catalogKind, options).map((option) => option.record);
443
483
  }
444
484
 
485
+ /**
486
+ * Picker options for `catalogKind`: `{ id, reference, label, record, group, source, origin }`, where `id` (= `reference`) is the
487
+ * reference to write, `id` or `name:id`. `options.presentation` is the document (default: `options.editor`'s), `options.catalogs`
488
+ * the host catalogs (merged after the session's registered ones).
489
+ */
445
490
  export function getCatalogOptions(catalogKind, options = {}) {
446
- return getCatalogRecords(catalogKind, options).map((record) => ({
447
- id: record.id,
448
- label: catalogLabel(record),
449
- record
450
- }));
491
+ assertCatalogKind(catalogKind);
492
+ const presentation = options.presentation ?? options.editor?.presentation ?? {};
493
+ return listCatalogRecords(presentation, catalogKind, { catalogs: mergeCatalogs(options.editor?.catalogs, options.catalogs) });
451
494
  }
452
495
 
453
496
  export function assertCatalogId(catalogKind, id, options = {}) {
454
497
  assertCatalogKind(catalogKind);
455
498
  if (typeof id !== "string" || id.length === 0) {
456
- throw new OPFEditorError("invalid-catalog-id", "Catalog control values must be non-empty catalog IDs.", {
499
+ throw new OPFEditorError("invalid-catalog-id", "Catalog control values must be non-empty catalog references.", {
457
500
  catalogKind,
458
501
  id
459
502
  });
@@ -461,7 +504,7 @@ export function assertCatalogId(catalogKind, id, options = {}) {
461
504
 
462
505
  const optionsById = new Set(getCatalogOptions(catalogKind, options).map((option) => option.id));
463
506
  if (!optionsById.has(id)) {
464
- throw new OPFEditorError("unknown-catalog-id", `Unknown ${catalogKind} catalog ID: ${id}.`, {
507
+ throw new OPFEditorError("unknown-catalog-id", `Unknown ${catalogKind} catalog reference: ${id}.`, {
465
508
  catalogKind,
466
509
  id
467
510
  });
@@ -470,16 +513,16 @@ export function assertCatalogId(catalogKind, id, options = {}) {
470
513
  return id;
471
514
  }
472
515
 
516
+ /** Write the catalog reference `id` at `path` (an object reference keeps its overrides and takes the new `id`). */
473
517
  export function setCatalogId(editor, path, catalogKind, id, meta = {}) {
474
518
  assertEditorSession(editor);
475
519
  const catalogId = assertCatalogId(catalogKind, id, {
476
- presentation: meta.presentation ?? editor.document,
477
- catalogs: meta.catalogs,
478
- catalogSources: meta.catalogSources
520
+ presentation: meta.presentation ?? editor.presentation,
521
+ catalogs: mergeCatalogs(editor.catalogs, meta.catalogs)
479
522
  });
480
523
  const current = editor.get(path);
481
524
  const value = catalogReferenceValue(current, catalogId);
482
- const { catalogs, catalogSources, presentation, ...commitMeta } = meta;
525
+ const { catalogs: _catalogs, presentation: _presentation, ...commitMeta } = meta;
483
526
  return editor.set(path, value, {
484
527
  ...commitMeta,
485
528
  catalogKind,
@@ -533,8 +576,8 @@ export function createCatalogSelect(editor, options) {
533
576
  if (options?.label) select.setAttribute("aria-label", options.label);
534
577
 
535
578
  for (const option of getCatalogOptions(catalogKind, {
536
- ...options,
537
- presentation: options?.presentation ?? editor.document
579
+ catalogs: mergeCatalogs(editor.catalogs, options?.catalogs),
580
+ presentation: options?.presentation ?? editor.presentation
538
581
  })) {
539
582
  const optionElement = documentRef.createElement("option");
540
583
  optionElement.value = option.id;
@@ -549,7 +592,6 @@ export function createCatalogSelect(editor, options) {
549
592
  const onChange = () => {
550
593
  setCatalogId(editor, path, catalogKind, select.value, {
551
594
  catalogs: options?.catalogs,
552
- catalogSources: options?.catalogSources,
553
595
  presentation: options?.presentation,
554
596
  source: "catalog-select"
555
597
  });
@@ -636,12 +678,12 @@ function normalizeOperation(operation) {
636
678
 
637
679
  // Core patch errors keep their stable codes; the editor reports them as OPFEditorError.
638
680
  function editorPatchError(error) {
639
- if (!(error instanceof PatchError)) return error;
681
+ if (!(error instanceof OPFPatchError)) return error;
640
682
  const details = {};
641
683
  if (error.path !== undefined) details.path = error.path;
642
684
  if (error.operation !== undefined) details.operation = error.operation;
643
685
  if (error.index !== undefined) details.index = error.index;
644
- if (error.validation) details.issues = error.validation.errors;
686
+ if (error.validation) details.issues = errorFindings(error.validation);
645
687
  return new OPFEditorError(error.code, error.message, details);
646
688
  }
647
689
 
@@ -693,20 +735,6 @@ function assertCatalogKind(catalogKind) {
693
735
  }
694
736
  }
695
737
 
696
- function firstRecords(value) {
697
- if (Array.isArray(value)) return value.filter(isCatalogRecord);
698
- if (Array.isArray(value?.records)) return value.records.filter(isCatalogRecord);
699
- return null;
700
- }
701
-
702
- function isCatalogRecord(record) {
703
- return Boolean(record && typeof record === "object" && typeof record.id === "string");
704
- }
705
-
706
- function catalogLabel(record) {
707
- return String(record.name ?? record.title ?? record.label ?? record.id);
708
- }
709
-
710
738
  function catalogReferenceValue(current, id) {
711
739
  if (current && typeof current === "object" && !Array.isArray(current)) {
712
740
  return { ...current, id };
@@ -103,7 +103,7 @@ function skipDelimiter(view, delimiter) {
103
103
  }
104
104
  function mountJsonCodeEditor(parent, options) {
105
105
  const attributes = new Compartment(), indent = new Compartment();
106
- let disposed=false, menu=null, catalogs=options.catalogs??{}, generation=0;
106
+ let disposed=false, menu=null, catalogs=options.catalogs??[], generation=0;
107
107
  function closeOptions(){generation++;menu?.destroy();menu=null;}
108
108
  async function openOptions(left,top){
109
109
  closeOptions();
@@ -244,7 +244,7 @@ function mountJsonCodeEditor(parent, options) {
244
244
  return {
245
245
  api,
246
246
  openOptions,
247
- setCatalogs(value){catalogs=value??{};closeOptions();},
247
+ setCatalogs(value){catalogs=value??[];closeOptions();},
248
248
  update(code, invalid=false, field=null, describedBy) {
249
249
  if(code!==api.getValue())closeOptions();
250
250
  const current = view.state.doc.toString(), normalized=textInputMap(code).text;
package/dist/json-menu.js CHANGED
@@ -26,9 +26,9 @@ export function mountJsonFieldMenu(parent,api,catalogs,left,top,onClose){
26
26
  const error=add('p',undefined,'json-field-empty');error.setAttribute('role','alert');error.hidden=true;
27
27
  if(isLayout){
28
28
  const help=add('details',undefined,'json-field-catalog-help');add('summary','Where do these layouts come from?',undefined,help);
29
- add('p','Standard OPF: included with OPF. Provided by app: extra records supplied by the host app. In this document: records in your JSON, which override matching IDs. External galleries are not automatically loaded.',undefined,help);
29
+ add('p','Provided by app: records from the catalogs the host app registered. In this document: records embedded in your JSON, which take precedence. Nothing is fetched; saving embeds every record the document uses.',undefined,help);
30
30
  }
31
- add('p',context.unloadedSource?'Using available records. External catalog URLs are not loaded here.':'Choose an option or press Escape to keep editing JSON.','json-field-footnote');
31
+ add('p',context.unloadedSource?'Using the embedded records: a catalog this document names is not registered here.':'Choose an option or press Escape to keep editing JSON.','json-field-footnote');
32
32
  let selected=Math.max(0,context.options.findIndex(option=>option.value===context.value)),choices=[],disposed=false;
33
33
  function close(restore=false){if(disposed)return;disposed=true;observer.disconnect();root.remove();doc.removeEventListener('pointerdown',outside);doc.removeEventListener('wheel',outside,true);doc.removeEventListener('touchmove',outside,true);win.removeEventListener('resize',resize);onClose();if(restore){api.focus();api.setSelection(...selection);}}
34
34
  function outside(event){if(!root.contains(event.target))close();}
@@ -1,11 +1,11 @@
1
- /** Already-loaded catalog records by kind. No remote catalog URLs are fetched. */
2
- export type JsonCatalogContext = Readonly<Record<string, readonly Record<string, unknown>[]>>;
1
+ /** The host's catalogs (core `Catalog[]`): records offered after the document's embedded ones. */
2
+ export type JsonCatalogContext = readonly import("@openpresentation/opf").Catalog[];
3
3
  export type JsonOptionValue = string | number | boolean;
4
4
  export interface JsonFieldOption {
5
5
  value: JsonOptionValue;
6
6
  label: string;
7
7
  description?: string;
8
- source: 'Built-in catalog' | 'Loaded catalog' | 'Document catalog' | 'Schema' | 'Current value';
8
+ source: 'Loaded catalog' | 'Document catalog' | 'Schema' | 'Current value';
9
9
  suggested?: boolean;
10
10
  /** User-facing provenance; source remains stable for existing consumers. */
11
11
  sourceLabel?: string;