@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
@@ -16,7 +16,7 @@ export interface PreparedAnnotationChange {
16
16
  /** The edited container path (the block, `references`, or the run array). */
17
17
  path: string;
18
18
  /** The validated candidate document. */
19
- document: Record<string, unknown>;
19
+ presentation: Record<string, unknown>;
20
20
  /** A test guard plus one replace; empty when nothing changes. */
21
21
  patches: JsonPatchOperation[];
22
22
  changed: boolean;
@@ -26,7 +26,7 @@ export interface PreparedAnnotationChange {
26
26
  ids?: string[];
27
27
  removedCites?: string[];
28
28
  }
29
- export type AnnotationChange = (EditorChange & Omit<PreparedAnnotationChange, "document" | "patches">) | PreparedAnnotationChange;
29
+ export type AnnotationChange = (EditorChange & Omit<PreparedAnnotationChange, "presentation" | "patches">) | PreparedAnnotationChange;
30
30
 
31
31
  export interface CaptionTarget { blockPath: string; field: string; caption?: CaptionSettings }
32
32
  export interface CaptionState { blockPath: string; field: string; caption?: CaptionSettings }
@@ -42,30 +42,30 @@ export interface CitationState {
42
42
  /** Normalize a caption value to `{ text, position, align }`, or undefined. */
43
43
  export declare function normalizeCaption(value: unknown): CaptionSettings | undefined;
44
44
  /** Every block (and one-payload slide root) that can carry a caption, with its current caption. */
45
- export declare function captionTargets(document: unknown): CaptionTarget[];
46
- export declare function readCaption(document: unknown, blockPath: string | string[]): CaptionState;
47
- export declare function prepareCaption(document: unknown, blockPath: string | string[], caption: CaptionInput | null): PreparedAnnotationChange;
45
+ export declare function captionTargets(presentation: unknown): CaptionTarget[];
46
+ export declare function readCaption(presentation: unknown, blockPath: string | string[]): CaptionState;
47
+ export declare function prepareCaption(presentation: unknown, blockPath: string | string[], caption: CaptionInput | null): PreparedAnnotationChange;
48
48
  export declare function setCaption(editor: EditorSession, blockPath: string | string[], caption: CaptionInput | null, meta?: Record<string, unknown>): AnnotationChange;
49
49
 
50
- export declare function listReferences(document: unknown): ReferenceState[];
51
- export declare function prepareReference(document: unknown, reference: Reference): PreparedAnnotationChange;
50
+ export declare function listReferences(presentation: unknown): ReferenceState[];
51
+ export declare function prepareReference(presentation: unknown, reference: Reference): PreparedAnnotationChange;
52
52
  export declare function addReference(editor: EditorSession, reference: Reference, meta?: Record<string, unknown>): AnnotationChange;
53
- export declare function prepareReferenceUpdate(document: unknown, id: string, fields: { text?: RichText; url?: string | null }): PreparedAnnotationChange;
53
+ export declare function prepareReferenceUpdate(presentation: unknown, id: string, fields: { text?: RichText; url?: string | null }): PreparedAnnotationChange;
54
54
  export declare function updateReference(editor: EditorSession, id: string, fields: { text?: RichText; url?: string | null }, meta?: Record<string, unknown>): AnnotationChange;
55
55
  /** Refuses while a run cites the id unless `force`, which also removes those cites. */
56
- export declare function prepareReferenceRemoval(document: unknown, id: string, options?: { force?: boolean }): PreparedAnnotationChange;
56
+ export declare function prepareReferenceRemoval(presentation: unknown, id: string, options?: { force?: boolean }): PreparedAnnotationChange;
57
57
  export declare function removeReference(editor: EditorSession, id: string, options?: { force?: boolean }, meta?: Record<string, unknown>): AnnotationChange;
58
58
 
59
59
  /** The run at a run path (`slides.0.text.2`): its run array path segments, index and value. */
60
- export declare function runAt(document: unknown, runPath: string | string[]): { parts: string[]; index: number; run: string | Record<string, unknown> };
61
- export declare function prepareCite(document: unknown, runPath: string | string[], ids: string | string[]): PreparedAnnotationChange;
60
+ export declare function runAt(presentation: unknown, runPath: string | string[]): { parts: string[]; index: number; run: string | Record<string, unknown> };
61
+ export declare function prepareCite(presentation: unknown, runPath: string | string[], ids: string | string[]): PreparedAnnotationChange;
62
62
  export declare function citeRun(editor: EditorSession, runPath: string | string[], ids: string | string[], meta?: Record<string, unknown>): AnnotationChange;
63
- export declare function prepareUncite(document: unknown, runPath: string | string[]): PreparedAnnotationChange;
63
+ export declare function prepareUncite(presentation: unknown, runPath: string | string[]): PreparedAnnotationChange;
64
64
  export declare function unciteRun(editor: EditorSession, runPath: string | string[], meta?: Record<string, unknown>): AnnotationChange;
65
- export declare function prepareFootnote(document: unknown, runPath: string | string[], text: RichText | null): PreparedAnnotationChange;
65
+ export declare function prepareFootnote(presentation: unknown, runPath: string | string[], text: RichText | null): PreparedAnnotationChange;
66
66
  export declare function setFootnote(editor: EditorSession, runPath: string | string[], text: RichText | null, meta?: Record<string, unknown>): AnnotationChange;
67
67
 
68
- /** The deck numbering the engines draw (core `collectCitations`). Throws `annotations-unavailable` on a core without it. */
69
- export declare function listCitations(document: unknown): CitationState;
68
+ /** The deck numbering the engines draw (core `collectCitations`). */
69
+ export declare function listCitations(presentation: unknown): CitationState;
70
70
  /** An ordinary list slide of the cited references (core `referencesSlide`). */
71
- export declare function referencesSlideFor(document: unknown, options?: { title?: string }): Record<string, unknown>;
71
+ export declare function referencesSlideFor(presentation: unknown, options?: { title?: string }): Record<string, unknown>;
@@ -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
@@ -1,5 +1,5 @@
1
1
  // Image assets for the design controls (RR-06): turn a local file into an entry of the document's
2
- // `assets` map and reference it from a logo, watermark, slide image, background or header/footer
2
+ // `assets` map and reference it from a logo, watermark, background or header/footer
3
3
  // zone in the same undoable patch. The file is validated before anything changes: the type must be
4
4
  // PNG, JPEG, GIF, WebP or SVG, its bytes must match that type, and its size must be under a cap with a
5
5
  // clear error. A host that keeps images elsewhere passes `onAddAsset` and gets the bytes instead.
@@ -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
 
@@ -1,9 +1,12 @@
1
1
  import type { EditorSession } from "./index.js";
2
+ import type { ImageFocus, ImageOverlay } from "./image-options.js";
2
3
  import type { DimensionSwitchChange, DimensionSwitchOptions, PreparedDimensionSwitch } from "./switches.js";
3
4
 
4
5
  export declare const BACKGROUND_TYPES: readonly ["theme", "solid", "gradient", "image", "pattern"];
5
6
  export declare const THEME_BACKGROUND_SLOTS: readonly ["light1", "light2", "dark1", "dark2"];
6
- export declare const IMAGE_BACKGROUND_FITS: readonly ["cover", "contain", "tile"];
7
+ export declare const IMAGE_BACKGROUND_FITS: readonly ["cover", "contain", "stretch", "tile"];
8
+ /** Image sources the background string shorthand accepts: `asset:`, `https://`, `data:`, `./`, `../`. */
9
+ export declare const IMAGE_SOURCE: RegExp;
7
10
  /** Scheme slots and roles a ColorRef may name. */
8
11
  export declare const COLOR_NAMES: readonly string[];
9
12
  /** The 54 DrawingML preset patterns by family. */
@@ -19,14 +22,14 @@ export type BackgroundSpec =
19
22
  | { type: "theme"; slot: (typeof THEME_BACKGROUND_SLOTS)[number] }
20
23
  | { type: "solid"; color: string; opacity?: number }
21
24
  | { type: "gradient"; gradient: { angle?: number; stops: GradientStop[] }; opacity?: number }
22
- | { type: "image"; image: { src: string; fit?: (typeof IMAGE_BACKGROUND_FITS)[number] }; opacity?: number }
25
+ | { type: "image"; src: string; alt?: string; fit?: (typeof IMAGE_BACKGROUND_FITS)[number]; focus?: ImageFocus; opacity?: number; recolor?: "grayscale" | { dark: string; light: string }; overlay?: ImageOverlay }
23
26
  | { type: "pattern"; pattern: { preset: string; foregroundColor?: string; backgroundColor?: string }; opacity?: number };
24
27
 
25
28
  /** Whether `value` is a ColorRef: a hex color, a scheme slot or role name, or `var:<id>`. */
26
29
  export declare function isColorRef(value: unknown): boolean;
27
- /** Validate a background description and return the value to store (a theme slot or plain hex stays the shorthand string). Throws `invalid-background`. */
30
+ /** Validate a background description and return the value to store (a theme slot, a plain hex color or a plain cover image stays the shorthand string). Throws `invalid-background`. */
28
31
  export declare function normalizeBackground(spec: BackgroundSpec): string | Record<string, unknown>;
29
- export declare function prepareBackground(document: unknown, spec: BackgroundSpec | null, options?: DimensionSwitchOptions): PreparedDimensionSwitch;
32
+ export declare function prepareBackground(presentation: unknown, spec: BackgroundSpec | null, options?: DimensionSwitchOptions): PreparedDimensionSwitch;
30
33
  /** Set the background as one undoable transaction; `null` removes it. */
31
34
  export declare function setBackground(editor: EditorSession, spec: BackgroundSpec | null, options?: DimensionSwitchOptions): DimensionSwitchChange;
32
35
  export interface BackgroundState {
@@ -37,7 +40,11 @@ export interface BackgroundState {
37
40
  angle?: number;
38
41
  stops?: GradientStop[];
39
42
  src?: string;
43
+ alt?: string;
40
44
  fit?: string;
45
+ focus?: ImageFocus;
46
+ recolor?: "grayscale" | { dark: string; light: string };
47
+ overlay?: ImageOverlay;
41
48
  preset?: string;
42
49
  foregroundColor?: string;
43
50
  backgroundColor?: string;
@@ -45,4 +52,4 @@ export interface BackgroundState {
45
52
  value?: unknown;
46
53
  }
47
54
  /** The background that applies at a scope, flattened for a form. */
48
- export declare function readBackground(document: unknown, options?: { slideIndex?: number }): BackgroundState;
55
+ export declare function readBackground(presentation: unknown, options?: { slideIndex?: number }): BackgroundState;