@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
@@ -6,15 +6,16 @@
6
6
  // canvas redraws and Undo restores the document. Nothing here invents text or ids: a reference is
7
7
  // what the caller supplies, and removing a cited reference refuses unless `force` also removes its
8
8
  // cites. Numbering comes from core (`collectCitations`), so the panel shows what the engines draw.
9
- import { getValueAtPath, opfPathToJsonPointer, splitOpfPath, validateOpfDocument } from "./index.js";
9
+ import { getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
10
10
  import { checkedDocument, fail } from "./edit-helpers.js";
11
- // Namespace import: a core published before RR-34 has no citation helpers; the verbs then throw annotations-unavailable.
12
- import * as opfCore from "@openpresentation/opf";
11
+ import { captionSettings, collectCitations, referencesSlide, walkCitationRuns } from "@openpresentation/opf/composition";
12
+ import { checkFormat } from "./checks.js";
13
13
 
14
14
  export const CAPTION_POSITIONS = Object.freeze(["below", "above"]);
15
15
  export const CAPTION_ALIGNMENTS = Object.freeze(["left", "center", "right"]);
16
16
  export const CAPTIONABLE_FIELDS = Object.freeze(["image", "chart", "table", "video"]);
17
- const TEXT_FIELDS = ["text", "bullets", "items"];
17
+ // Fields whose runs can carry a marker: the slide heading group (FA-10) plus text, quote text, bullets and items.
18
+ const TEXT_FIELDS = ["title", "subtitle", "tag", "text", "bullets", "items"];
18
19
 
19
20
  const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
20
21
  const clone = (value) => structuredClone(value);
@@ -24,29 +25,29 @@ function checkEditor(editor) {
24
25
  if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function") throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
25
26
  }
26
27
  function commit(editor, prepared, meta = {}) {
27
- const { document, patches, ...summary } = prepared;
28
- void document;
29
- if (!prepared.changed) return { ...summary, document: editor.document, patches: [], inversePatches: [], validation: editor.validation };
28
+ const { presentation, patches, ...summary } = prepared;
29
+ void presentation;
30
+ if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
30
31
  const change = editor.applyPatch(patches, { ...meta, source: meta.source ?? "annotation", action: prepared.action, path: prepared.path });
31
32
  return { ...change, ...summary };
32
33
  }
33
34
  // One guarded replace of the container at `parts`; the candidate must validate when the document did.
34
- function transaction(document, parts, next, extra) {
35
- const current = getValueAtPath(document, parts);
35
+ function transaction(presentation, parts, next, extra) {
36
+ const current = getValueAtPath(presentation, parts);
36
37
  const pointer = opfPathToJsonPointer(parts);
37
38
  const changed = JSON.stringify(current) !== JSON.stringify(next);
38
39
  const patches = changed ? [{ op: "test", path: pointer, value: clone(current) }, { op: "replace", path: pointer, value: clone(next) }] : [];
39
- const before = validateOpfDocument(document);
40
- const result = changed ? checkedDocument(document, patches, before) : document;
41
- return { ...extra, path: parts.join("."), document: clone(result), patches, changed };
40
+ const before = checkFormat(presentation);
41
+ const result = changed ? checkedDocument(presentation, patches, before) : presentation;
42
+ return { ...extra, path: parts.join("."), presentation: clone(result), patches, changed };
42
43
  }
43
44
 
44
45
  // --- captions -----------------------------------------------------------------------------------
45
46
 
46
- function captionHost(document, blockPath) {
47
+ function captionHost(presentation, blockPath) {
47
48
  let parts;
48
49
  try { parts = splitOpfPath(blockPath); } catch { throw fail("invalid-path", `Invalid block path ${JSON.stringify(blockPath)}.`); }
49
- const host = getValueAtPath(document, parts);
50
+ const host = getValueAtPath(presentation, parts);
50
51
  if (!isObject(host)) throw fail("invalid-path", `No block at ${parts.join(".")}.`);
51
52
  if (Array.isArray(host.blocks)) throw fail("caption-unsupported", "A group cannot carry a caption; choose one of its blocks.", { path: parts.join(".") });
52
53
  const fields = CAPTIONABLE_FIELDS.filter((field) => host[field] !== undefined);
@@ -56,15 +57,11 @@ function captionHost(document, blockPath) {
56
57
 
57
58
  /** Normalize a caption value to `{ text, position, align }`, or undefined. */
58
59
  export function normalizeCaption(value) {
59
- if (typeof opfCore.captionSettings === "function") return opfCore.captionSettings(value);
60
- if (value === undefined || value === null) return undefined;
61
- if (typeof value === "string" || Array.isArray(value)) return { text: value, position: "below", align: "left" };
62
- if (!isObject(value) || value.text === undefined) return undefined;
63
- return { text: value.text, position: value.position === "above" ? "above" : "below", align: CAPTION_ALIGNMENTS.includes(value.align) ? value.align : "left" };
60
+ return captionSettings(value);
64
61
  }
65
62
 
66
63
  /** Every block (and one-payload slide root) that can carry a caption, with its current caption. */
67
- export function captionTargets(document) {
64
+ export function captionTargets(presentation) {
68
65
  const targets = [];
69
66
  const visit = (host, path) => {
70
67
  if (!isObject(host)) return;
@@ -72,7 +69,7 @@ export function captionTargets(document) {
72
69
  const fields = CAPTIONABLE_FIELDS.filter((field) => host[field] !== undefined);
73
70
  if (fields.length === 1) targets.push({ blockPath: path, field: fields[0], caption: normalizeCaption(host.caption) });
74
71
  };
75
- (document?.slides ?? []).forEach((slide, index) => {
72
+ (presentation?.slides ?? []).forEach((slide, index) => {
76
73
  if (!isObject(slide)) return;
77
74
  const regions = Object.keys(slide).filter((key) => /^(top|middle|bottom|left|center|right)([+:]|$)/.test(key) && isObject(slide[key])).sort();
78
75
  if (regions.length) for (const key of regions) visit(slide[key], `slides.${index}.${key}`);
@@ -82,8 +79,8 @@ export function captionTargets(document) {
82
79
  }
83
80
 
84
81
  /** The caption of the block at `blockPath` (`{ field, caption }`; `caption` undefined when none). */
85
- export function readCaption(document, blockPath) {
86
- const { host, field } = captionHost(document, blockPath);
82
+ export function readCaption(presentation, blockPath) {
83
+ const { host, field } = captionHost(presentation, blockPath);
87
84
  return { blockPath: splitOpfPath(blockPath).join("."), field, caption: normalizeCaption(host.caption) };
88
85
  }
89
86
 
@@ -91,8 +88,8 @@ export function readCaption(document, blockPath) {
91
88
  * Set (a string, TextRun[] or `{ text, position?, align? }`) or remove (`null`) the caption of the
92
89
  * block at `blockPath`. A caption at the default position and alignment is stored in its short form.
93
90
  */
94
- export function prepareCaption(document, blockPath, caption) {
95
- const { parts, host, field } = captionHost(document, blockPath);
91
+ export function prepareCaption(presentation, blockPath, caption) {
92
+ const { parts, host, field } = captionHost(presentation, blockPath);
96
93
  const next = clone(host);
97
94
  if (caption === null || caption === undefined) delete next.caption;
98
95
  else {
@@ -102,21 +99,17 @@ export function prepareCaption(document, blockPath, caption) {
102
99
  throw fail("invalid-caption", "A caption is a string, TextRun[] or { text, position: below|above, align: left|center|right }.");
103
100
  next.caption = settings.position === "below" && settings.align === "left" ? clone(settings.text) : { text: clone(settings.text), position: settings.position, align: settings.align };
104
101
  }
105
- return transaction(document, parts, next, { action: "caption", field });
102
+ return transaction(presentation, parts, next, { action: "caption", field });
106
103
  }
107
104
  export function setCaption(editor, blockPath, caption, meta = {}) {
108
105
  checkEditor(editor);
109
- return commit(editor, prepareCaption(editor.document, blockPath, caption), meta);
106
+ return commit(editor, prepareCaption(editor.presentation, blockPath, caption), meta);
110
107
  }
111
108
 
112
109
  // --- references ---------------------------------------------------------------------------------
113
110
 
114
- function citationsOf(document) {
115
- if (typeof opfCore.collectCitations !== "function") throw fail("annotations-unavailable", "The installed @openpresentation/opf has no citation helpers (needs the core that ships RR-34).");
116
- return opfCore.collectCitations(document);
117
- }
118
- function referenceList(document) {
119
- return Array.isArray(document?.references) ? document.references : [];
111
+ function referenceList(presentation) {
112
+ return Array.isArray(presentation?.references) ? presentation.references : [];
120
113
  }
121
114
  function checkReference(reference, existing, { allowExisting = false } = {}) {
122
115
  if (!isObject(reference) || typeof reference.id !== "string" || !reference.id) throw fail("invalid-reference", "A reference needs a non-empty string id.");
@@ -129,46 +122,46 @@ function checkReference(reference, existing, { allowExisting = false } = {}) {
129
122
  }
130
123
 
131
124
  /** The deck's references with their marker numbers and whether a run cites them. */
132
- export function listReferences(document) {
133
- const list = referenceList(document);
134
- const citations = typeof opfCore.collectCitations === "function" ? opfCore.collectCitations(document) : undefined;
125
+ export function listReferences(presentation) {
126
+ const list = referenceList(presentation);
127
+ const citations = collectCitations(presentation);
135
128
  return list.map((reference, index) => {
136
- const note = citations?.references.find((item) => item.id === reference?.id);
129
+ const note = citations.references.find((item) => item.id === reference?.id);
137
130
  return { index, id: reference?.id, text: reference?.text, url: reference?.url, cited: Boolean(note), ...(note ? { number: note.number } : {}) };
138
131
  });
139
132
  }
140
133
  /** Append a reference `{ id, text, url? }`; ids must be unique. */
141
- export function prepareReference(document, reference) {
142
- const list = referenceList(document);
134
+ export function prepareReference(presentation, reference) {
135
+ const list = referenceList(presentation);
143
136
  const next = [...clone(list), checkReference(reference, list)];
144
- return transaction(document, ["references"], next, { action: "add-reference", id: reference.id });
137
+ return transaction(presentation, ["references"], next, { action: "add-reference", id: reference.id });
145
138
  }
146
139
  export function addReference(editor, reference, meta = {}) {
147
140
  checkEditor(editor);
148
- return commit(editor, prepareReference(editor.document, reference), meta);
141
+ return commit(editor, prepareReference(editor.presentation, reference), meta);
149
142
  }
150
143
  /** Change the text and/or url of the reference `id` (`url: null` removes the url); the id itself does not change. */
151
- export function prepareReferenceUpdate(document, id, fields) {
152
- const list = referenceList(document), index = list.findIndex((item) => item?.id === id);
144
+ export function prepareReferenceUpdate(presentation, id, fields) {
145
+ const list = referenceList(presentation), index = list.findIndex((item) => item?.id === id);
153
146
  if (index < 0) throw fail("unknown-reference", `No reference '${id}'.`, { id });
154
147
  const merged = { ...list[index], ...(fields?.text !== undefined ? { text: fields.text } : {}), ...(fields?.url !== undefined ? { url: fields.url } : {}) };
155
148
  if (fields?.url === null) delete merged.url;
156
149
  const next = clone(list);
157
150
  next[index] = checkReference(merged, list, { allowExisting: true });
158
- return transaction(document, ["references"], next, { action: "update-reference", id });
151
+ return transaction(presentation, ["references"], next, { action: "update-reference", id });
159
152
  }
160
153
  export function updateReference(editor, id, fields, meta = {}) {
161
154
  checkEditor(editor);
162
- return commit(editor, prepareReferenceUpdate(editor.document, id, fields), meta);
155
+ return commit(editor, prepareReferenceUpdate(editor.presentation, id, fields), meta);
163
156
  }
164
157
  /** Remove the reference `id`. Refuses while a run cites it unless `force`, which also removes those cites. */
165
- export function prepareReferenceRemoval(document, id, { force = false } = {}) {
166
- const list = referenceList(document), index = list.findIndex((item) => item?.id === id);
158
+ export function prepareReferenceRemoval(presentation, id, { force = false } = {}) {
159
+ const list = referenceList(presentation), index = list.findIndex((item) => item?.id === id);
167
160
  if (index < 0) throw fail("unknown-reference", `No reference '${id}'.`, { id });
168
161
  const citing = [];
169
- if (typeof opfCore.walkCitationRuns === "function") (document.slides ?? []).forEach((slide, slideIndex) => opfCore.walkCitationRuns(slide, `slides.${slideIndex}`, (entry) => { if (entry.cite.includes(id)) citing.push(entry.path); }));
162
+ (presentation.slides ?? []).forEach((slide, slideIndex) => walkCitationRuns(slide, `slides.${slideIndex}`, (entry) => { if (entry.cite.includes(id)) citing.push(entry.path); }));
170
163
  if (citing.length && !force) throw fail("reference-cited", `Reference '${id}' is cited by ${citing.length} run${citing.length === 1 ? "" : "s"}; pass force to remove the citations too.`, { id, runs: citing });
171
- const next = clone(document);
164
+ const next = clone(presentation);
172
165
  next.references = list.filter((_, position) => position !== index);
173
166
  if (!next.references.length) delete next.references;
174
167
  for (const runPath of citing) {
@@ -177,15 +170,15 @@ export function prepareReferenceRemoval(document, id, { force = false } = {}) {
177
170
  runs[runIndex] = withoutCite(run, id);
178
171
  }
179
172
  const pointer = "";
180
- const changed = JSON.stringify(document) !== JSON.stringify(next);
181
- const patches = changed ? [{ op: "test", path: pointer, value: clone(document) }, { op: "replace", path: pointer, value: clone(next) }] : [];
182
- const before = validateOpfDocument(document);
183
- const result = changed ? checkedDocument(document, patches, before) : document;
184
- return { action: "remove-reference", id, removedCites: citing, path: "references", document: clone(result), patches, changed };
173
+ const changed = JSON.stringify(presentation) !== JSON.stringify(next);
174
+ const patches = changed ? [{ op: "test", path: pointer, value: clone(presentation) }, { op: "replace", path: pointer, value: clone(next) }] : [];
175
+ const before = checkFormat(presentation);
176
+ const result = changed ? checkedDocument(presentation, patches, before) : presentation;
177
+ return { action: "remove-reference", id, removedCites: citing, path: "references", presentation: clone(result), patches, changed };
185
178
  }
186
179
  export function removeReference(editor, id, options = {}, meta = {}) {
187
180
  checkEditor(editor);
188
- return commit(editor, prepareReferenceRemoval(editor.document, id, options), meta);
181
+ return commit(editor, prepareReferenceRemoval(editor.presentation, id, options), meta);
189
182
  }
190
183
 
191
184
  // --- citations and footnotes --------------------------------------------------------------------
@@ -199,56 +192,56 @@ function withoutCite(run, id) {
199
192
  const simplifyRun = (run) => Object.keys(run).length === 1 && typeof run.text === "string" ? run.text : run;
200
193
 
201
194
  /** The run at `runPath` (`slides.0.text.2`, `slides.0.blocks.1.items.0.text.1`): its run array path, index and value. */
202
- export function runAt(document, runPath) {
195
+ export function runAt(presentation, runPath) {
203
196
  let parts;
204
197
  try { parts = splitOpfPath(runPath); } catch { throw fail("invalid-path", `Invalid run path ${JSON.stringify(runPath)}.`); }
205
198
  if (parts.length < 3 || !/^(0|[1-9]\d*)$/.test(parts[parts.length - 1])) throw fail("invalid-path", `${parts.join(".")} is not a run path.`);
206
199
  const arrayParts = parts.slice(0, -1), index = Number(parts[parts.length - 1]);
207
- const runs = getValueAtPath(document, arrayParts);
200
+ const runs = getValueAtPath(presentation, arrayParts);
208
201
  const field = [...arrayParts].reverse().find((segment) => TEXT_FIELDS.includes(segment));
209
- if (!Array.isArray(runs) || !field || !(index in runs)) throw fail("invalid-path", `${parts.join(".")} is not a run of a text, bullets or items payload.`);
202
+ if (!Array.isArray(runs) || !field || !(index in runs)) throw fail("invalid-path", `${parts.join(".")} is not a run of a title, subtitle, tag, text, bullets or items field.`);
210
203
  const run = runs[index];
211
204
  if (typeof run !== "string" && !(isObject(run) && typeof run.text === "string")) throw fail("invalid-path", `${parts.join(".")} is not a text run.`);
212
205
  return { parts: arrayParts, index, run };
213
206
  }
214
207
 
215
208
  /** Make the run at `runPath` cite `ids` (a string or array; replaces its current cites). The ids must exist in references. */
216
- export function prepareCite(document, runPath, ids) {
209
+ export function prepareCite(presentation, runPath, ids) {
217
210
  const list = Array.isArray(ids) ? ids : [ids];
218
211
  if (!list.length || !list.every((id) => typeof id === "string" && id)) throw fail("invalid-reference", "cite needs one or more reference ids.");
219
- const known = new Set(referenceList(document).map((item) => item?.id));
212
+ const known = new Set(referenceList(presentation).map((item) => item?.id));
220
213
  const unknown = list.filter((id) => !known.has(id));
221
214
  if (unknown.length) throw fail("unknown-reference", `Unknown reference${unknown.length === 1 ? "" : "s"} ${unknown.map((id) => `'${id}'`).join(", ")}; add them with addReference first.`, { ids: unknown });
222
- const { parts, index, run } = runAt(document, runPath);
223
- const runs = clone(getValueAtPath(document, parts));
215
+ const { parts, index, run } = runAt(presentation, runPath);
216
+ const runs = clone(getValueAtPath(presentation, parts));
224
217
  const next = typeof run === "string" ? { text: run } : { ...clone(run) };
225
218
  if (!next.text) throw fail("invalid-run", "An empty run cannot carry a marker.", { path: runPath });
226
219
  const unique = [...new Set(list)];
227
220
  next.cite = unique.length === 1 ? unique[0] : unique;
228
221
  runs[index] = next;
229
- return transaction(document, parts, runs, { action: "cite", runPath: [...parts, index].join("."), ids: unique });
222
+ return transaction(presentation, parts, runs, { action: "cite", runPath: [...parts, index].join("."), ids: unique });
230
223
  }
231
224
  export function citeRun(editor, runPath, ids, meta = {}) {
232
225
  checkEditor(editor);
233
- return commit(editor, prepareCite(editor.document, runPath, ids), meta);
226
+ return commit(editor, prepareCite(editor.presentation, runPath, ids), meta);
234
227
  }
235
228
  /** Remove every cite from the run at `runPath`. */
236
- export function prepareUncite(document, runPath) {
237
- const { parts, index, run } = runAt(document, runPath);
238
- const runs = clone(getValueAtPath(document, parts));
229
+ export function prepareUncite(presentation, runPath) {
230
+ const { parts, index, run } = runAt(presentation, runPath);
231
+ const runs = clone(getValueAtPath(presentation, parts));
239
232
  const next = typeof run === "string" ? run : { ...clone(run) };
240
233
  if (typeof next === "object") delete next.cite;
241
234
  runs[index] = typeof next === "object" ? simplifyRun(next) : next;
242
- return transaction(document, parts, runs, { action: "uncite", runPath: [...parts, index].join(".") });
235
+ return transaction(presentation, parts, runs, { action: "uncite", runPath: [...parts, index].join(".") });
243
236
  }
244
237
  export function unciteRun(editor, runPath, meta = {}) {
245
238
  checkEditor(editor);
246
- return commit(editor, prepareUncite(editor.document, runPath), meta);
239
+ return commit(editor, prepareUncite(editor.presentation, runPath), meta);
247
240
  }
248
241
  /** Set (a string or TextRun[]) or remove (`null`) the inline footnote of the run at `runPath`. */
249
- export function prepareFootnote(document, runPath, text) {
250
- const { parts, index, run } = runAt(document, runPath);
251
- const runs = clone(getValueAtPath(document, parts));
242
+ export function prepareFootnote(presentation, runPath, text) {
243
+ const { parts, index, run } = runAt(presentation, runPath);
244
+ const runs = clone(getValueAtPath(presentation, parts));
252
245
  const next = typeof run === "string" ? { text: run } : { ...clone(run) };
253
246
  if (text === null || text === undefined) delete next.footnote;
254
247
  else {
@@ -257,16 +250,16 @@ export function prepareFootnote(document, runPath, text) {
257
250
  next.footnote = clone(text);
258
251
  }
259
252
  runs[index] = simplifyRun(next);
260
- return transaction(document, parts, runs, { action: "footnote", runPath: [...parts, index].join(".") });
253
+ return transaction(presentation, parts, runs, { action: "footnote", runPath: [...parts, index].join(".") });
261
254
  }
262
255
  export function setFootnote(editor, runPath, text, meta = {}) {
263
256
  checkEditor(editor);
264
- return commit(editor, prepareFootnote(editor.document, runPath, text), meta);
257
+ return commit(editor, prepareFootnote(editor.presentation, runPath, text), meta);
265
258
  }
266
259
 
267
260
  /** The deck numbering the engines draw: notes in number order, cited references, per-slide markers, unused ids. */
268
- export function listCitations(document) {
269
- const citations = citationsOf(document);
261
+ export function listCitations(presentation) {
262
+ const citations = collectCitations(presentation);
270
263
  return {
271
264
  notes: citations.notes.map((note) => ({ ...note })),
272
265
  references: citations.references.map((note) => ({ ...note })),
@@ -275,7 +268,6 @@ export function listCitations(document) {
275
268
  };
276
269
  }
277
270
  /** An ordinary list slide of the cited references (core `referencesSlide`), ready for `prepareBlockInsert` or a slide insert. */
278
- export function referencesSlideFor(document, options = {}) {
279
- if (typeof opfCore.referencesSlide !== "function") throw fail("annotations-unavailable", "The installed @openpresentation/opf has no referencesSlide (needs the core that ships RR-34).");
280
- return opfCore.referencesSlide(document, options);
271
+ export function referencesSlideFor(presentation, options = {}) {
272
+ return referencesSlide(presentation, options);
281
273
  }
package/dist/assets.d.ts CHANGED
@@ -28,8 +28,8 @@ export interface ImageUploadOptions {
28
28
  onAddAsset?: (image: { name: string; mediaType: string; bytes: Uint8Array; size: number; alt?: string; file: unknown }) => string | { src: string } | Promise<string | { src: string }>;
29
29
  meta?: Record<string, unknown>;
30
30
  }
31
- export interface ImageUploadChange extends Omit<EditorChange, "document" | "patches"> {
32
- document: unknown;
31
+ export interface ImageUploadChange extends Omit<EditorChange, "presentation" | "patches"> {
32
+ presentation: unknown;
33
33
  patches: JsonPatchOperation[];
34
34
  /** The new asset's id (absent with `onAddAsset`). */
35
35
  assetId?: string;
@@ -46,21 +46,21 @@ export declare function checkImageBytes(bytes: Uint8Array, options?: { name?: st
46
46
  export declare function readImageFile(file: { name?: string; type?: string; size?: number; arrayBuffer(): Promise<ArrayBuffer> }, options?: { maxBytes?: number }): Promise<ValidatedImage>;
47
47
  export declare function imageDataUri(bytes: Uint8Array, mediaType: string): string;
48
48
  /** A free asset id for a file name (`logo`, then `logo-2`). */
49
- export declare function uniqueAssetId(document: unknown, name: string): string;
49
+ export declare function uniqueAssetId(presentation: unknown, name: string): string;
50
50
  /** The patch that adds one validated image to `assets`. */
51
- export declare function prepareImageAsset(document: unknown, image: ValidatedImage, options?: { alt?: string; id?: string }): PreparedImageAsset;
51
+ export declare function prepareImageAsset(presentation: unknown, image: ValidatedImage, options?: { alt?: string; id?: string }): PreparedImageAsset;
52
52
  /**
53
- * Add an uploaded image and use it in one undoable transaction. `build(reference, document)` returns the prepared change
53
+ * Add an uploaded image and use it in one undoable transaction. `build(reference, presentation)` returns the prepared change
54
54
  * that uses the image (any `prepare...` function of this package); its patches run after the asset patch.
55
55
  */
56
56
  export declare function applyImageUpload(
57
57
  editor: EditorSession,
58
58
  file: { name?: string; type?: string; size?: number; arrayBuffer(): Promise<ArrayBuffer> },
59
- build: (reference: string, document: any) => { patches: JsonPatchOperation[] },
59
+ build: (reference: string, presentation: any) => { patches: JsonPatchOperation[] },
60
60
  options?: ImageUploadOptions,
61
61
  ): Promise<ImageUploadChange>;
62
62
  /** The patch that sets (or, for an empty string, removes) an asset's alt text. */
63
- export declare function prepareAssetAlt(document: unknown, assetId: string, alt: string): { assetId: string; patches: JsonPatchOperation[]; changed: boolean };
63
+ export declare function prepareAssetAlt(presentation: unknown, assetId: string, alt: string): { assetId: string; patches: JsonPatchOperation[]; changed: boolean };
64
64
  /** Set an asset's alt text as one undoable transaction. */
65
65
  export declare function setAssetAlt(editor: EditorSession, assetId: string, alt: string, meta?: Record<string, unknown>): unknown;
66
66
  /** The `assets` id a reference names (`asset:logo` gives `logo`), or undefined for a URL or data address. */
package/dist/assets.js CHANGED
@@ -90,9 +90,9 @@ export function imageDataUri(bytes, mediaType) {
90
90
  }
91
91
 
92
92
  /** An asset id for a file name that is free in the document (`logo`, then `logo-2`, ...). */
93
- export function uniqueAssetId(document, name) {
93
+ export function uniqueAssetId(presentation, name) {
94
94
  const base = String(name ?? "image").replace(/\.[a-z0-9]+$/i, "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "image";
95
- const taken = document?.assets && typeof document.assets === "object" ? document.assets : {};
95
+ const taken = presentation?.assets && typeof presentation.assets === "object" ? presentation.assets : {};
96
96
  if (!Object.hasOwn(taken, base)) return base;
97
97
  for (let n = 2; ; n += 1) if (!Object.hasOwn(taken, `${base}-${n}`)) return `${base}-${n}`;
98
98
  }
@@ -101,19 +101,19 @@ export function uniqueAssetId(document, name) {
101
101
  * The patch that adds one validated image to `assets` (the entry is `{ src: dataUri, mediaType, title, alt? }`),
102
102
  * with the `asset:<id>` reference to use. Pure: touches no session.
103
103
  */
104
- export function prepareImageAsset(document, image, { alt, id } = {}) {
105
- const assetId = id ?? uniqueAssetId(document, image.name);
106
- if (document?.assets && Object.hasOwn(document.assets, assetId)) throw fail("asset-exists", `An asset named "${assetId}" already exists.`, { id: assetId });
104
+ export function prepareImageAsset(presentation, image, { alt, id } = {}) {
105
+ const assetId = id ?? uniqueAssetId(presentation, image.name);
106
+ if (presentation?.assets && Object.hasOwn(presentation.assets, assetId)) throw fail("asset-exists", `An asset named "${assetId}" already exists.`, { id: assetId });
107
107
  const entry = { src: imageDataUri(image.bytes, image.mediaType), mediaType: image.mediaType, title: image.name };
108
108
  if (typeof alt === "string" && alt.trim()) entry.alt = alt.trim();
109
- const patches = document?.assets && typeof document.assets === "object" && !Array.isArray(document.assets)
109
+ const patches = presentation?.assets && typeof presentation.assets === "object" && !Array.isArray(presentation.assets)
110
110
  ? [{ op: "add", path: opfPathToJsonPointer(["assets", assetId]), value: entry }]
111
111
  : [{ op: "add", path: "/assets", value: { [assetId]: entry } }];
112
112
  return { id: assetId, reference: `asset:${assetId}`, entry, patches };
113
113
  }
114
114
 
115
115
  /**
116
- * Add an uploaded image and use it in one undoable transaction. `build(reference, document)` returns the
116
+ * Add an uploaded image and use it in one undoable transaction. `build(reference, presentation)` returns the
117
117
  * prepared change that uses the image (any `prepare...` function of this package, for example
118
118
  * `(ref, doc) => prepareLogoVariant(doc, "light", ref)`); its patches run after the asset patch.
119
119
  *
@@ -134,23 +134,23 @@ export async function applyImageUpload(editor, file, build, options = {}) {
134
134
  if (typeof reference !== "string" || !reference.trim()) throw fail("invalid-asset-reference", "The host's onAddAsset must return the image's reference (a web address or an asset: id).", {});
135
135
  } else {
136
136
  // Build against the document as it is after the read: the file may have taken a moment to arrive.
137
- const added = prepareImageAsset(editor.document, image, { alt });
137
+ const added = prepareImageAsset(editor.presentation, image, { alt });
138
138
  reference = added.reference;
139
139
  assetPatches = added.patches;
140
140
  assetId = added.id;
141
141
  }
142
- const document = assetPatches.length ? applyJsonPatch(editor.document, assetPatches) : editor.document;
143
- const prepared = build(reference, document);
142
+ const presentation = assetPatches.length ? applyJsonPatch(editor.presentation, assetPatches) : editor.presentation;
143
+ const prepared = build(reference, presentation);
144
144
  const patches = [...assetPatches, ...prepared.patches];
145
145
  const summary = { assetId, reference, changed: patches.length > 0, prepared };
146
- if (!patches.length) return { ...summary, document: editor.document, patches: [], inversePatches: [], validation: editor.validation };
146
+ if (!patches.length) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
147
147
  const change = editor.applyPatch(patches, { ...meta, source: meta?.source ?? "image-upload", ...(assetId ? { assetId } : {}), reference });
148
148
  return { ...change, ...summary };
149
149
  }
150
150
 
151
151
  /** The patch that sets (or, for an empty string, removes) an asset's alt text. The asset must exist in `assets`. */
152
- export function prepareAssetAlt(document, assetId, alt) {
153
- const entry = getValueAtPath(document, ["assets", assetId]);
152
+ export function prepareAssetAlt(presentation, assetId, alt) {
153
+ const entry = getValueAtPath(presentation, ["assets", assetId]);
154
154
  if (entry === undefined) throw fail("unknown-asset", `There is no asset named "${assetId}".`, { assetId });
155
155
  const object = typeof entry === "string" ? { src: entry } : { ...entry };
156
156
  const text = typeof alt === "string" ? alt.trim() : "";
@@ -164,8 +164,8 @@ export function prepareAssetAlt(document, assetId, alt) {
164
164
  /** Set an asset's alt text as one undoable transaction. */
165
165
  export function setAssetAlt(editor, assetId, alt, meta = {}) {
166
166
  if (!editor || typeof editor.applyPatch !== "function") throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
167
- const prepared = prepareAssetAlt(editor.document, assetId, alt);
168
- if (!prepared.changed) return { ...prepared, document: editor.document, inversePatches: [], validation: editor.validation };
167
+ const prepared = prepareAssetAlt(editor.presentation, assetId, alt);
168
+ if (!prepared.changed) return { ...prepared, presentation: editor.presentation, inversePatches: [], validation: editor.validation };
169
169
  return { ...editor.applyPatch(prepared.patches, { ...meta, source: meta.source ?? "asset-alt", assetId }), assetId, changed: true };
170
170
  }
171
171
 
@@ -26,7 +26,7 @@ export type BackgroundSpec =
26
26
  export declare function isColorRef(value: unknown): boolean;
27
27
  /** Validate a background description and return the value to store (a theme slot or plain hex stays the shorthand string). Throws `invalid-background`. */
28
28
  export declare function normalizeBackground(spec: BackgroundSpec): string | Record<string, unknown>;
29
- export declare function prepareBackground(document: unknown, spec: BackgroundSpec | null, options?: DimensionSwitchOptions): PreparedDimensionSwitch;
29
+ export declare function prepareBackground(presentation: unknown, spec: BackgroundSpec | null, options?: DimensionSwitchOptions): PreparedDimensionSwitch;
30
30
  /** Set the background as one undoable transaction; `null` removes it. */
31
31
  export declare function setBackground(editor: EditorSession, spec: BackgroundSpec | null, options?: DimensionSwitchOptions): DimensionSwitchChange;
32
32
  export interface BackgroundState {
@@ -45,4 +45,4 @@ export interface BackgroundState {
45
45
  value?: unknown;
46
46
  }
47
47
  /** The background that applies at a scope, flattened for a form. */
48
- export declare function readBackground(document: unknown, options?: { slideIndex?: number }): BackgroundState;
48
+ export declare function readBackground(presentation: unknown, options?: { slideIndex?: number }): BackgroundState;
@@ -11,7 +11,7 @@ export const IMAGE_BACKGROUND_FITS = Object.freeze(["cover", "contain", "tile"])
11
11
  /** Scheme slots and roles a ColorRef may name (the schema's ColorRef enum), besides hex colors and `var:<id>`. */
12
12
  export const COLOR_NAMES = Object.freeze([
13
13
  "accent1", "accent2", "accent3", "accent4", "accent5", "accent6", "dark1", "dark2", "light1", "light2", "hyperlink", "followedHyperlink",
14
- "primary", "secondary", "accent", "background", "surface", "text", "textSecondary",
14
+ "primary", "secondary", "accent", "background", "surface", "surfaceAlt", "text", "textSecondary",
15
15
  ]);
16
16
  /**
17
17
  * The 54 DrawingML preset patterns (ECMA-376 ST_PresetPatternVal) by family. PPTX export writes them as
@@ -102,8 +102,8 @@ export function normalizeBackground(spec) {
102
102
  }
103
103
 
104
104
  /** Compute the patch that sets (or, for `null`, removes) the background, without touching a session. Options as `prepareDimensionSwitch` (`slideIndex`, `clearSlideOverrides`). */
105
- export function prepareBackground(document, spec, options = {}) {
106
- return prepareDimensionSwitch(document, "backgrounds", spec === null ? null : normalizeBackground(spec), options);
105
+ export function prepareBackground(presentation, spec, options = {}) {
106
+ return prepareDimensionSwitch(presentation, "backgrounds", spec === null ? null : normalizeBackground(spec), options);
107
107
  }
108
108
 
109
109
  /** Set the background as one undoable transaction; `null` removes it so the theme's (or the deck's) shows again. */
@@ -116,9 +116,9 @@ export function setBackground(editor, spec, options = {}) {
116
116
  * `{ type, slot?, color?, opacity?, angle?, stops?, src?, fit?, preset?, foregroundColor?, backgroundColor?, scope, value }`.
117
117
  * `type` is undefined when nothing is set; `scope` is "slide" when the slide sets its own, else "deck".
118
118
  */
119
- export function readBackground(document, { slideIndex } = {}) {
120
- const own = slideIndex === undefined ? undefined : document.slides?.[slideIndex]?.design?.background;
121
- const value = own !== undefined ? own : document.design?.background;
119
+ export function readBackground(presentation, { slideIndex } = {}) {
120
+ const own = slideIndex === undefined ? undefined : presentation.slides?.[slideIndex]?.design?.background;
121
+ const value = own !== undefined ? own : presentation.design?.background;
122
122
  const scope = own !== undefined ? "slide" : "deck";
123
123
  if (value === undefined) return { scope };
124
124
  if (typeof value === "string") return THEME_BACKGROUND_SLOTS.includes(value) ? { type: "theme", slot: value, scope, value } : { type: "solid", color: value, scope, value };
@@ -13,18 +13,18 @@ export function createBlockControls(root, options) {
13
13
  function apply(prepared){
14
14
  if(!prepared?.changed){cancel();return true;}
15
15
  // Retain changes outside the guarded containers and preflight the exact candidate.
16
- const candidate=applyJsonPatch(editor.document,prepared.patches);
16
+ const candidate=applyJsonPatch(editor.presentation,prepared.patches);
17
17
  options.validate(candidate);
18
18
  committing=true;
19
19
  try{cancel();editor.applyPatch(prepared.patches,{source:'canvas-block-edit',rejectInvalid:true});options.onMove?.(prepared.path);([ ...layer.querySelectorAll("[data-block-path]")].find(node=>node.dataset.blockPath===prepared.path)??layer.querySelector('[data-block-add]'))?.focus();options.clearError?.();options.onCommit?.({path:prepared.path,editor});return true;}
20
20
  finally{committing=false;}
21
21
  }
22
- function move(from,to,index,base=editor.document){
22
+ function move(from,to,index,base=editor.presentation){
23
23
  try{return apply(prepareBlockMove(base,from,to,index));}catch(error){cancel();options.onError(error);return false;}
24
24
  }
25
25
  function open(path){
26
26
  if(!options.beforeEdit())return;
27
- cancel();returnFocus=doc.activeElement;path=opfPathToJsonPointer(path);const base=editor.document,parts=splitOpfPath(path),container=opfPathToJsonPointer(parts.slice(0,-2)),index=Number(parts.at(-1));
27
+ cancel();returnFocus=doc.activeElement;path=opfPathToJsonPointer(path);const base=editor.presentation,parts=splitOpfPath(path),container=opfPathToJsonPointer(parts.slice(0,-2)),index=Number(parts.at(-1));
28
28
  const source=getValueAtPath(base,[...parts.slice(0,-2),'blocks']);
29
29
  if(parts.at(-2)!=='blocks'||!Number.isInteger(index)||index<0||!Array.isArray(source)||index>=source.length){options.onError(new Error('Choose an existing content block.'));return;}
30
30
  const count=source.length;
@@ -56,7 +56,7 @@ export function createBlockControls(root, options) {
56
56
  }
57
57
  function openInsert(containerPath,index) {
58
58
  if(!options.beforeEdit())return;
59
- cancel();returnFocus=doc.activeElement;const base=editor.document;
59
+ cancel();returnFocus=doc.activeElement;const base=editor.presentation;
60
60
  const containers=listBlockContainers(base,{slideIndex:options.slideIndex(),includeImplicit:true});
61
61
  if(!containers.length){options.onError(new Error('No content container is available on this slide.'));return;}
62
62
  const requested=containerPath===undefined?containers[0].path:opfPathToJsonPointer(containerPath);
@@ -91,7 +91,7 @@ export function createBlockControls(root, options) {
91
91
  panel.append(add,close);panel.onkeydown=event=>{if(event.key==='Escape'){event.preventDefault();event.stopPropagation();cancel();}};
92
92
  layer.append(panel);kind.focus();
93
93
  }
94
- function update(document,nextGeometry){
94
+ function update(presentation,nextGeometry){
95
95
  geometry=nextGeometry;
96
96
  if(drag)return;
97
97
  cancel();layer.replaceChildren();cells=[];
@@ -99,7 +99,7 @@ export function createBlockControls(root, options) {
99
99
  const add=doc.createElement('button');add.type='button';add.textContent='+ Add content';add.setAttribute('aria-label','Add content');add.dataset.blockAdd='';
100
100
  add.style.cssText='position:absolute;right:8px;top:8px;pointer-events:auto;padding:5px 9px;background:#f6f3fc;color:#675394;border:1px solid #cfc4e7;border-radius:5px;font:12px system-ui;cursor:pointer';add.onclick=()=>openInsert();layer.append(add);
101
101
  for(const flow of geometry.flows??[]){
102
- const blocks=getValueAtPath(document,[...splitOpfPath(flow.path),'blocks']);
102
+ const blocks=getValueAtPath(presentation,[...splitOpfPath(flow.path),'blocks']);
103
103
  if(!Array.isArray(blocks)||blocks.length!==flow.itemCount)continue;
104
104
  blocks.forEach((block,index)=>{
105
105
  const col=flow.columns[index%flow.columns.length],row=flow.rows[Math.floor(index/flow.columns.length)];if(!col||!row)return;
@@ -116,7 +116,7 @@ export function createBlockControls(root, options) {
116
116
  node.onkeydown=event=>{if(['ArrowLeft','ArrowUp','ArrowRight','ArrowDown'].includes(event.key)){event.preventDefault();event.stopPropagation();if(!options.beforeEdit())return;const delta=['ArrowLeft','ArrowUp'].includes(event.key)?-1:1;if(index+delta>=0&&index+delta<blocks.length)move(path,container,index+(delta>0?2:-1));}};
117
117
  node.ondragstart=event=>{
118
118
  if(!options.beforeEdit()){event.preventDefault();return;}
119
- cancel();drag={from:path,container,index,base:editor.document,signature:signature(container),cells:[...cells],prepared:null};
119
+ cancel();drag={from:path,container,index,base:editor.presentation,signature:signature(container),cells:[...cells],prepared:null};
120
120
  event.dataTransfer.effectAllowed='move';event.dataTransfer.setData('text/plain',path);
121
121
  };
122
122
  node.ondragend=()=>{cancel();options.render();};layer.append(node);
@@ -32,7 +32,7 @@ export interface BlockConversionTarget {
32
32
  reason?: string;
33
33
  }
34
34
  export interface PreparedBlockConversion {
35
- document: unknown;
35
+ presentation: unknown;
36
36
  patches: JsonPatchOperation[];
37
37
  path: string;
38
38
  changed: boolean;
@@ -41,8 +41,8 @@ export interface PreparedBlockConversion {
41
41
  from: BlockKind;
42
42
  to: BlockKind;
43
43
  }
44
- export interface BlockConversionChange extends Omit<EditorChange, "document" | "patches"> {
45
- document: unknown;
44
+ export interface BlockConversionChange extends Omit<EditorChange, "presentation" | "patches"> {
45
+ presentation: unknown;
46
46
  patches: JsonPatchOperation[];
47
47
  lossless: boolean;
48
48
  loss: string[];
@@ -53,12 +53,12 @@ export interface BlockConversionChange extends Omit<EditorChange, "document" | "
53
53
  }
54
54
 
55
55
  /** The single content payload of the block at `path`, or undefined (image-only, groups and blocks with several fields have none to convert). */
56
- export declare function readBlockContent(document: unknown, path: string | string[]): BlockContent | undefined;
56
+ export declare function readBlockContent(presentation: unknown, path: string | string[]): BlockContent | undefined;
57
57
  /** The block that contains a selected path (slides.0.blocks.1.text maps to slides.0.blocks.1), or undefined. */
58
- export declare function blockPathForSelection(document: unknown, selectedPath: string): string | undefined;
58
+ export declare function blockPathForSelection(presentation: unknown, selectedPath: string): string | undefined;
59
59
  /** The kinds the block can convert to, each with its loss report or the reason it is unavailable. */
60
- export declare function blockConversionTargets(document: unknown, path: string | string[]): BlockConversionTarget[];
60
+ export declare function blockConversionTargets(presentation: unknown, path: string | string[]): BlockConversionTarget[];
61
61
  /** Compute the conversion patch without touching a session. Throws `block-not-convertible` for an unsupported pair or content that does not fit. */
62
- export declare function prepareBlockConversion(document: unknown, path: string | string[], kind: BlockKind, options?: ConvertOptions): PreparedBlockConversion;
62
+ export declare function prepareBlockConversion(presentation: unknown, path: string | string[], kind: BlockKind, options?: ConvertOptions): PreparedBlockConversion;
63
63
  /** Convert one block as a single undoable transaction. */
64
64
  export declare function convertBlock(editor: EditorSession, path: string | string[], kind: BlockKind, meta?: Record<string, unknown>, options?: ConvertOptions): BlockConversionChange;