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