@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.
- package/README.md +71 -60
- 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 +22 -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 +10 -10
- package/dist/chart-data.js +21 -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 +7 -8
- package/dist/data.d.ts +4 -4
- package/dist/data.js +5 -5
- package/dist/dataset-refs.d.ts +2 -2
- package/dist/dataset-refs.js +5 -5
- package/dist/design-controls.js +61 -44
- 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 +23 -20
- package/dist/font-gate.js +49 -58
- package/dist/galleries.js +36 -52
- package/dist/grid-model.d.ts +25 -27
- package/dist/grid-model.js +92 -102
- 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 +124 -114
- package/dist/table-options.d.ts +8 -8
- package/dist/table-options.js +28 -27
- 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 +49 -59
- package/package.json +8 -6
- package/dist/font-defaults.js +0 -25
package/dist/templates.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// with variables; filling it resolves the variables to a concrete deck (core resolveVariables).
|
|
3
3
|
// Everything here reads and writes the document through the session, so a fill, a declaration
|
|
4
4
|
// and a token insertion are validated, undoable JSON Patch edits like any other. Importing this
|
|
5
|
-
// module needs no DOM.
|
|
6
|
-
|
|
7
|
-
import * as core from "@openpresentation/opf";
|
|
5
|
+
// module needs no DOM.
|
|
6
|
+
import { coerceVariableValue, hasContentVariables, listBuiltinVariables, listVariables, resolveVariables, variableDeclarations } from "@openpresentation/opf";
|
|
8
7
|
import { OPFEditorError, getValueAtPath, opfPathToJsonPointer } from "./index.js";
|
|
9
8
|
|
|
10
9
|
const VARIABLE_ID = /^[a-z][a-z0-9-]*$/;
|
|
10
|
+
// A built-in variable name (core FA-04): `speakers`, or deck/speaker/organization plus one or two dotted segments.
|
|
11
|
+
const BUILTIN_NAME = /^(?:speakers|(?:deck|speaker|organization)(?:.[A-Za-z0-9_-]+){1,2})$/;
|
|
11
12
|
const KINDS = ["color", "text", "number", "date", "image", "url", "list"];
|
|
12
13
|
|
|
13
14
|
/** The form control each variable kind uses. */
|
|
@@ -25,12 +26,6 @@ function fail(code, message, details) {
|
|
|
25
26
|
return new OPFEditorError(code, message, details);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function requireCore() {
|
|
29
|
-
if (typeof core.resolveVariables !== "function" || typeof core.listVariables !== "function") {
|
|
30
|
-
throw fail("templates-unavailable", "Templates need a core release that ships resolveVariables (@openpresentation/opf after 0.11.4).");
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
29
|
function checkEditor(editor) {
|
|
35
30
|
if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function") {
|
|
36
31
|
throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
@@ -39,19 +34,22 @@ function checkEditor(editor) {
|
|
|
39
34
|
|
|
40
35
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
41
36
|
|
|
42
|
-
/** True when the core this editor runs on can resolve template variables. */
|
|
43
|
-
export function templatesAvailable() {
|
|
44
|
-
return typeof core.resolveVariables === "function" && typeof core.listVariables === "function";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
/** True when the document is marked as a template (`template: true`). */
|
|
48
|
-
export function isTemplateDocument(
|
|
49
|
-
return isObject(
|
|
38
|
+
export function isTemplateDocument(presentation) {
|
|
39
|
+
return isObject(presentation) && presentation.template === true;
|
|
50
40
|
}
|
|
51
41
|
|
|
52
42
|
/** True when the document is a template or declares a variable that is not a color. */
|
|
53
|
-
export function hasTemplateVariables(
|
|
54
|
-
return
|
|
43
|
+
export function hasTemplateVariables(presentation) {
|
|
44
|
+
return hasContentVariables(presentation);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The presentation's built-in variables (read-only values from its own speaker, organization and deck metadata) with
|
|
49
|
+
* kind, label, current value, whether the presentation has a source value and where each is used.
|
|
50
|
+
*/
|
|
51
|
+
export function listBuiltins(presentation) {
|
|
52
|
+
return listBuiltinVariables(presentation);
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
function humanize(id) {
|
|
@@ -80,10 +78,9 @@ export function valueToFieldText(kind, value) {
|
|
|
80
78
|
* applies again). Returns `{ ok: true, value }` (value undefined when cleared) or `{ ok: false, message }`.
|
|
81
79
|
*/
|
|
82
80
|
export function fieldTextToValue(kind, text) {
|
|
83
|
-
requireCore();
|
|
84
81
|
if (typeof text !== "string") return { ok: false, message: "Expected text." };
|
|
85
82
|
if (text.trim() === "") return { ok: true, value: undefined };
|
|
86
|
-
const coerced =
|
|
83
|
+
const coerced = coerceVariableValue(kind, kind === "text" ? text : text.trim());
|
|
87
84
|
return coerced.ok ? { ok: true, value: coerced.value } : { ok: false, message: `${coerced.message}.` };
|
|
88
85
|
}
|
|
89
86
|
|
|
@@ -92,10 +89,9 @@ export function fieldTextToValue(kind, text) {
|
|
|
92
89
|
* `status` is "filled" (a value was supplied), "default" (the declaration has a value), "unfilled"
|
|
93
90
|
* (required, no value) or "optional" (not required, no value).
|
|
94
91
|
*/
|
|
95
|
-
export function listTemplateFields(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return core.listVariables(document, values).map((info) => {
|
|
92
|
+
export function listTemplateFields(presentation, values = {}) {
|
|
93
|
+
const declared = new Map(variableDeclarations(presentation).map((declaration) => [declaration.id, declaration]));
|
|
94
|
+
return listVariables(presentation, values).map((info) => {
|
|
99
95
|
const declaration = declared.get(info.id);
|
|
100
96
|
const supplied = Object.hasOwn(values, info.id) && values[info.id] !== undefined && values[info.id] !== null;
|
|
101
97
|
const defaultValue = declaration?.value;
|
|
@@ -122,12 +118,12 @@ export function listTemplateFields(document, values = {}) {
|
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
/** The summary a panel shows: how many required variables still need a value. */
|
|
125
|
-
export function templateStatus(
|
|
126
|
-
const fields = listTemplateFields(
|
|
121
|
+
export function templateStatus(presentation, values = {}) {
|
|
122
|
+
const fields = listTemplateFields(presentation, values);
|
|
127
123
|
const required = fields.filter((field) => field.required);
|
|
128
124
|
const unfilled = fields.filter((field) => field.status === "unfilled").map((field) => field.id);
|
|
129
125
|
return {
|
|
130
|
-
template: isTemplateDocument(
|
|
126
|
+
template: isTemplateDocument(presentation),
|
|
131
127
|
fieldCount: fields.length,
|
|
132
128
|
requiredCount: required.length,
|
|
133
129
|
filledRequiredCount: required.length - unfilled.length,
|
|
@@ -140,9 +136,8 @@ export function templateStatus(document, values = {}) {
|
|
|
140
136
|
* Preview data for the supplied values: the concrete deck with each unfilled variable's example (or its
|
|
141
137
|
* token when it has none). Never throws for missing values; the diagnostics say what was unfilled.
|
|
142
138
|
*/
|
|
143
|
-
export function previewTemplate(
|
|
144
|
-
|
|
145
|
-
return core.resolveVariables(document, values, { examples: true, partial: true });
|
|
139
|
+
export function previewTemplate(presentation, values = {}) {
|
|
140
|
+
return resolveVariables(presentation, values, { examples: true, partial: true });
|
|
146
141
|
}
|
|
147
142
|
|
|
148
143
|
/**
|
|
@@ -151,34 +146,33 @@ export function previewTemplate(document, values = {}) {
|
|
|
151
146
|
* document, `subscribe` notifies listeners (a panel redraws on it).
|
|
152
147
|
*/
|
|
153
148
|
export function createTemplateFill(editor, options = {}) {
|
|
154
|
-
requireCore();
|
|
155
149
|
checkEditor(editor);
|
|
156
150
|
let values = {};
|
|
157
151
|
const listeners = new Set();
|
|
158
152
|
const emit = () => {
|
|
159
153
|
for (const listener of listeners) listener(api);
|
|
160
154
|
};
|
|
161
|
-
const declared = () => new Set(
|
|
162
|
-
const kindOf = (id) =>
|
|
155
|
+
const declared = () => new Set(variableDeclarations(editor.presentation).map((declaration) => declaration.id));
|
|
156
|
+
const kindOf = (id) => variableDeclarations(editor.presentation).find((declaration) => declaration.id === id)?.kind;
|
|
163
157
|
const api = {
|
|
164
158
|
get values() {
|
|
165
159
|
return structuredClone(values);
|
|
166
160
|
},
|
|
167
161
|
fields() {
|
|
168
|
-
return listTemplateFields(editor.
|
|
162
|
+
return listTemplateFields(editor.presentation, values);
|
|
169
163
|
},
|
|
170
164
|
status() {
|
|
171
|
-
return templateStatus(editor.
|
|
165
|
+
return templateStatus(editor.presentation, values);
|
|
172
166
|
},
|
|
173
167
|
preview() {
|
|
174
|
-
return previewTemplate(editor.
|
|
168
|
+
return previewTemplate(editor.presentation, values);
|
|
175
169
|
},
|
|
176
170
|
set(id, value) {
|
|
177
171
|
if (!declared().has(id)) throw fail("unknown-variable", `No variable '${id}' is declared.`, { id });
|
|
178
172
|
if (value === undefined || value === null) {
|
|
179
173
|
delete values[id];
|
|
180
174
|
} else {
|
|
181
|
-
const coerced =
|
|
175
|
+
const coerced = coerceVariableValue(kindOf(id), value);
|
|
182
176
|
if (!coerced.ok) throw fail("invalid-variable-value", `Value for '${id}': ${coerced.message}.`, { id });
|
|
183
177
|
values[id] = coerced.value;
|
|
184
178
|
}
|
|
@@ -218,8 +212,8 @@ export function createTemplateFill(editor, options = {}) {
|
|
|
218
212
|
* `partial` is set (the unfilled ones stay declared for a later pass).
|
|
219
213
|
*/
|
|
220
214
|
apply({ partial = false, meta = {} } = {}) {
|
|
221
|
-
const
|
|
222
|
-
const result =
|
|
215
|
+
const presentation = editor.presentation;
|
|
216
|
+
const result = resolveVariables(presentation, values, { partial, template: false });
|
|
223
217
|
const errors = result.diagnostics.filter((entry) => entry.severity === "error");
|
|
224
218
|
if (errors.length) {
|
|
225
219
|
const unfilled = errors.some((entry) => entry.code === "variable-unfilled");
|
|
@@ -239,27 +233,27 @@ export function createTemplateFill(editor, options = {}) {
|
|
|
239
233
|
return api;
|
|
240
234
|
}
|
|
241
235
|
|
|
242
|
-
/** The token text for a variable: `{{id}}`, or `{{id|format}}` with a one-off format. */
|
|
236
|
+
/** The token text for a variable or a built-in name: `{{id}}`, or `{{id|format}}` with a one-off format. */
|
|
243
237
|
export function variableToken(id, format) {
|
|
244
|
-
if (!VARIABLE_ID.test(id)) throw fail("invalid-variable-id", "A variable id is lowercase kebab-case: letters, digits and hyphens, starting with a letter.", { id });
|
|
238
|
+
if (!VARIABLE_ID.test(id) && !BUILTIN_NAME.test(id)) throw fail("invalid-variable-id", "A variable id is lowercase kebab-case: letters, digits and hyphens, starting with a letter.", { id });
|
|
245
239
|
return format ? `{{${id}|${format}}}` : `{{${id}}}`;
|
|
246
240
|
}
|
|
247
241
|
|
|
248
242
|
/** An unused variable id derived from a label, for a "new variable" form. */
|
|
249
|
-
export function suggestVariableId(
|
|
243
|
+
export function suggestVariableId(presentation, label = "value") {
|
|
250
244
|
const base = String(label).normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "") || "value";
|
|
251
|
-
const taken = new Set(isObject(
|
|
245
|
+
const taken = new Set(isObject(presentation?.variables) ? Object.keys(presentation.variables) : []);
|
|
252
246
|
if (!taken.has(base)) return base;
|
|
253
247
|
for (let n = 2; ; n++) if (!taken.has(`${base}-${n}`)) return `${base}-${n}`;
|
|
254
248
|
}
|
|
255
249
|
|
|
256
|
-
function declarationPatches(
|
|
250
|
+
function declarationPatches(presentation, id, declaration) {
|
|
257
251
|
if (!VARIABLE_ID.test(id)) throw fail("invalid-variable-id", "A variable id is lowercase kebab-case: letters, digits and hyphens, starting with a letter.", { id });
|
|
258
252
|
const entry = typeof declaration === "string" ? declaration : structuredClone(declaration);
|
|
259
253
|
if (typeof entry !== "string" && !(isObject(entry) && KINDS.includes(entry.type))) {
|
|
260
254
|
throw fail("invalid-variable", `A variable declaration is a hex color or an object whose type is one of ${KINDS.join(", ")}.`, { id });
|
|
261
255
|
}
|
|
262
|
-
const variables =
|
|
256
|
+
const variables = presentation.variables;
|
|
263
257
|
if (variables !== undefined && !isObject(variables)) throw fail("invalid-variable", "The document's variables field is not an object.", { id });
|
|
264
258
|
if (variables && Object.hasOwn(variables, id)) throw fail("variable-exists", `A variable '${id}' is already declared.`, { id });
|
|
265
259
|
return variables === undefined
|
|
@@ -269,22 +263,21 @@ function declarationPatches(document, id, declaration) {
|
|
|
269
263
|
|
|
270
264
|
/** Declare a variable as one undoable edit. The resulting document must validate (a required variable with no value needs `template: true`). */
|
|
271
265
|
export function declareVariable(editor, id, declaration, meta = {}) {
|
|
272
|
-
requireCore();
|
|
273
266
|
checkEditor(editor);
|
|
274
|
-
return editor.applyPatch(declarationPatches(editor.
|
|
267
|
+
return editor.applyPatch(declarationPatches(editor.presentation, id, declaration), { source: "template-variable", rejectInvalid: true, ...meta });
|
|
275
268
|
}
|
|
276
269
|
|
|
277
270
|
/** Mark the document as a template (or, with `false`, as a normal deck) as one undoable edit. */
|
|
278
271
|
export function setTemplate(editor, enabled, meta = {}) {
|
|
279
272
|
checkEditor(editor);
|
|
280
|
-
const
|
|
281
|
-
const present = Object.hasOwn(
|
|
273
|
+
const presentation = editor.presentation;
|
|
274
|
+
const present = Object.hasOwn(presentation, "template");
|
|
282
275
|
const patches = enabled
|
|
283
276
|
? [{ op: present ? "replace" : "add", path: "/template", value: true }]
|
|
284
277
|
: present
|
|
285
278
|
? [{ op: "remove", path: "/template" }]
|
|
286
279
|
: [];
|
|
287
|
-
if (!patches.length) return {
|
|
280
|
+
if (!patches.length) return { presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
288
281
|
return editor.applyPatch(patches, { source: "template-mode", rejectInvalid: true, ...meta });
|
|
289
282
|
}
|
|
290
283
|
|
|
@@ -295,18 +288,18 @@ export function setTemplate(editor, enabled, meta = {}) {
|
|
|
295
288
|
* the declaration, and both changes commit together.
|
|
296
289
|
*/
|
|
297
290
|
export function insertVariableToken(editor, path, id, { start, end, runIndex, format, declare, meta = {} } = {}) {
|
|
298
|
-
requireCore();
|
|
299
291
|
checkEditor(editor);
|
|
300
|
-
const
|
|
292
|
+
const presentation = editor.presentation;
|
|
301
293
|
const token = variableToken(id, format);
|
|
302
294
|
const patches = [];
|
|
303
|
-
|
|
295
|
+
// A built-in needs no declaration; an unknown built-in path fails the validated edit.
|
|
296
|
+
const declaredNow = BUILTIN_NAME.test(id) || (isObject(presentation.variables) && Object.hasOwn(presentation.variables, id));
|
|
304
297
|
if (!declaredNow) {
|
|
305
298
|
if (declare === undefined) throw fail("unknown-variable", `No variable '${id}' is declared. Declare it first, or pass its declaration.`, { id });
|
|
306
|
-
patches.push(...declarationPatches(
|
|
299
|
+
patches.push(...declarationPatches(presentation, id, declare));
|
|
307
300
|
}
|
|
308
301
|
const pointer = opfPathToJsonPointer(path);
|
|
309
|
-
const current = getValueAtPath(
|
|
302
|
+
const current = getValueAtPath(presentation, path);
|
|
310
303
|
const splice = (text) => {
|
|
311
304
|
const from = start === undefined ? text.length : start;
|
|
312
305
|
const to = end === undefined ? from : end;
|
package/dist/transfer.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ export declare const MAX_OPF_BYTES: number;
|
|
|
2
2
|
export interface OpfTransfer {
|
|
3
3
|
kind: "presentation" | "slides" | "selection";
|
|
4
4
|
value: unknown;
|
|
5
|
-
|
|
5
|
+
presentation?: any;
|
|
6
6
|
}
|
|
7
|
-
export declare function assertOpf(
|
|
7
|
+
export declare function assertOpf(presentation: any): any;
|
|
8
8
|
export declare function unwrapOpf(value: any): any;
|
|
9
9
|
export declare function parseOpfTransfer(text: string): OpfTransfer;
|
|
10
10
|
export declare function serializeOpfTransfer(
|
|
11
|
-
|
|
11
|
+
presentation: any,
|
|
12
12
|
options?: {
|
|
13
13
|
scope?: "presentation" | "slide" | "selection";
|
|
14
14
|
slideIndex?: number;
|
|
@@ -24,4 +24,4 @@ export declare function prepareOpfImport(
|
|
|
24
24
|
slideIndex?: number;
|
|
25
25
|
path?: string;
|
|
26
26
|
},
|
|
27
|
-
): {
|
|
27
|
+
): { presentation: any; slideIndex: number };
|
package/dist/transfer.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
createValuePatch,
|
|
4
|
-
getValueAtPath,
|
|
5
|
-
validateOpfDocument,
|
|
6
|
-
} from "./index.js";
|
|
7
|
-
import { catalogs } from "@openpresentation/opf";
|
|
1
|
+
import { applyJsonPatch, createValuePatch, getValueAtPath } from "./index.js";
|
|
2
|
+
import { catalogs, resolveSlideContext } from "@openpresentation/opf";
|
|
8
3
|
import { collectReservedPresentationIds, remapSlideTreeIds } from "./presentation-ids.js";
|
|
9
|
-
import { DEFAULT_FONT_SCHEME } from "
|
|
4
|
+
import { DEFAULT_FONT_SCHEME } from "@openpresentation/opf/composition";
|
|
5
|
+
import { checkFormat, firstErrorMessage } from "./checks.js";
|
|
10
6
|
export const MAX_OPF_BYTES = 20 * 1024 * 1024;
|
|
11
7
|
const clone = (value) => structuredClone(value);
|
|
12
|
-
export function assertOpf(
|
|
13
|
-
bounded(
|
|
14
|
-
const result =
|
|
8
|
+
export function assertOpf(presentation) {
|
|
9
|
+
bounded(presentation);
|
|
10
|
+
const result = checkFormat(presentation);
|
|
15
11
|
if (!result.valid)
|
|
16
|
-
throw new Error(result
|
|
17
|
-
return
|
|
12
|
+
throw new Error(firstErrorMessage(result, "Invalid OPF document."));
|
|
13
|
+
return presentation;
|
|
18
14
|
}
|
|
19
15
|
function bounded(value, depth = 0, budget = { remaining: 250000 }) {
|
|
20
16
|
if (depth > 64 || --budget.remaining < 0)
|
|
@@ -36,7 +32,7 @@ export function unwrapOpf(value, depth = 0) {
|
|
|
36
32
|
if (snippet) return unwrapOpf(snippet.value ?? JSON.parse(snippet.source), depth + 1);
|
|
37
33
|
}
|
|
38
34
|
if (value?.opf && typeof value.opf === "object") return unwrapOpf(value.opf, depth + 1);
|
|
39
|
-
if (value?.
|
|
35
|
+
if (value?.presentation) return unwrapOpf(value.presentation, depth + 1);
|
|
40
36
|
return value;
|
|
41
37
|
}
|
|
42
38
|
export function parseOpfTransfer(text) {
|
|
@@ -63,24 +59,24 @@ export function parseOpfTransfer(text) {
|
|
|
63
59
|
bounded(value);
|
|
64
60
|
if (value?.slides) {
|
|
65
61
|
assertOpf(value);
|
|
66
|
-
return { kind: "presentation", value,
|
|
62
|
+
return { kind: "presentation", value, presentation: clone(value) };
|
|
67
63
|
}
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
return { kind: "slides", value,
|
|
64
|
+
const presentation = { slides: Array.isArray(value) ? value : [value] };
|
|
65
|
+
if (checkFormat(presentation).valid)
|
|
66
|
+
return { kind: "slides", value, presentation };
|
|
71
67
|
return { kind: "selection", value };
|
|
72
68
|
}
|
|
73
69
|
export function serializeOpfTransfer(
|
|
74
|
-
|
|
70
|
+
presentation,
|
|
75
71
|
{ scope = "presentation", slideIndex = 0, path, format = "pretty" } = {},
|
|
76
72
|
) {
|
|
77
|
-
let value =
|
|
73
|
+
let value = presentation;
|
|
78
74
|
if (scope === "slide") {
|
|
79
|
-
if (!
|
|
75
|
+
if (!presentation.slides?.[slideIndex])
|
|
80
76
|
throw new Error("Select a slide first.");
|
|
81
|
-
value = { ...
|
|
77
|
+
value = { ...presentation, slides: [presentation.slides[slideIndex]] };
|
|
82
78
|
} else if (scope === "selection") {
|
|
83
|
-
value = getValueAtPath(
|
|
79
|
+
value = getValueAtPath(presentation, path);
|
|
84
80
|
if (value === undefined) throw new Error("Select some content first.");
|
|
85
81
|
}
|
|
86
82
|
const json = JSON.stringify(value, null, format === "compact" ? 0 : 2);
|
|
@@ -98,39 +94,34 @@ const catalogKeys = {
|
|
|
98
94
|
purposes: "purpose",
|
|
99
95
|
socialPlatforms: "platform",
|
|
100
96
|
};
|
|
101
|
-
function themeFor(document, reference) {
|
|
102
|
-
const id = typeof reference === "string" ? reference : reference?.id;
|
|
103
|
-
const record =
|
|
104
|
-
document.catalogs?.themes?.records?.find((item) => item.id === id) ??
|
|
105
|
-
catalogs.themes.find((item) => item.id === id);
|
|
106
|
-
return { ...record, ...(typeof reference === "object" ? reference : {}) };
|
|
107
|
-
}
|
|
108
97
|
export function prepareOpfImport(
|
|
109
98
|
current,
|
|
110
99
|
transfer,
|
|
111
100
|
{ mode = "insert", slideIndex = 0, path } = {},
|
|
112
101
|
) {
|
|
113
102
|
if (mode === "selection") {
|
|
114
|
-
const
|
|
103
|
+
const presentation = applyJsonPatch(
|
|
115
104
|
current,
|
|
116
105
|
createValuePatch(current, path, transfer.value),
|
|
117
106
|
);
|
|
118
|
-
assertOpf(
|
|
119
|
-
return {
|
|
107
|
+
assertOpf(presentation);
|
|
108
|
+
return { presentation, slideIndex };
|
|
120
109
|
}
|
|
121
|
-
if (!transfer.
|
|
110
|
+
if (!transfer.presentation)
|
|
122
111
|
throw new Error(
|
|
123
112
|
"This is a content fragment. Choose Replace selected content.",
|
|
124
113
|
);
|
|
125
114
|
if (mode === "replace")
|
|
126
|
-
return {
|
|
115
|
+
return { presentation: assertOpf(clone(transfer.presentation)), slideIndex: 0 };
|
|
127
116
|
if (mode !== "insert") throw new Error("Unknown import action.");
|
|
128
|
-
const incoming = clone(transfer.
|
|
129
|
-
|
|
117
|
+
const incoming = clone(transfer.presentation),
|
|
118
|
+
presentation = clone(current);
|
|
130
119
|
// Freeze the source deck defaults on inserted slides before changing their catalog ids.
|
|
131
|
-
incoming.slides = incoming.slides.map((slide) => {
|
|
132
|
-
|
|
133
|
-
const
|
|
120
|
+
incoming.slides = incoming.slides.map((slide, index) => {
|
|
121
|
+
// A slide's design cannot set dimensions (a PPTX has one slide size): the inserted slide takes the host deck's.
|
|
122
|
+
const { dimensions: _deckSize, ...design } = { ...incoming.design, ...slide.design };
|
|
123
|
+
// The theme record the slide resolves to, as core resolves it for every engine (an unknown id falls back to `minimal`).
|
|
124
|
+
const theme = resolveSlideContext(incoming, index).resolved.theme;
|
|
134
125
|
return {
|
|
135
126
|
...slide,
|
|
136
127
|
design: {
|
|
@@ -138,7 +129,6 @@ export function prepareOpfImport(
|
|
|
138
129
|
theme: design.theme ?? "minimal",
|
|
139
130
|
colorScheme: design.colorScheme ?? theme.colorScheme ?? "cool-horizon",
|
|
140
131
|
fontScheme: design.fontScheme ?? theme.fontScheme ?? DEFAULT_FONT_SCHEME,
|
|
141
|
-
dimensions: design.dimensions ?? theme.dimensions ?? "widescreen",
|
|
142
132
|
background: design.background ?? theme.background ?? "#FFFFFF",
|
|
143
133
|
header: design.header ?? false,
|
|
144
134
|
footer: design.footer ?? false,
|
|
@@ -149,7 +139,7 @@ export function prepareOpfImport(
|
|
|
149
139
|
assetMap = new Map();
|
|
150
140
|
let chartTypes = new Map();
|
|
151
141
|
for (const [kind, catalog] of Object.entries(incoming.catalogs ?? {})) {
|
|
152
|
-
const existing =
|
|
142
|
+
const existing = presentation.catalogs?.[kind];
|
|
153
143
|
if (
|
|
154
144
|
catalog.source &&
|
|
155
145
|
existing?.source &&
|
|
@@ -179,7 +169,7 @@ export function prepareOpfImport(
|
|
|
179
169
|
if (catalogKeys[kind]) mappings[catalogKeys[kind]] = names;
|
|
180
170
|
if (kind === "chartTypes") chartTypes = names;
|
|
181
171
|
}
|
|
182
|
-
const assetIds = new Set(Object.keys(
|
|
172
|
+
const assetIds = new Set(Object.keys(presentation.assets ?? {}));
|
|
183
173
|
for (const id of Object.keys(incoming.assets ?? {})) {
|
|
184
174
|
let next = id,
|
|
185
175
|
n = 2;
|
|
@@ -197,11 +187,11 @@ export function prepareOpfImport(
|
|
|
197
187
|
else if (key === "dataset" && typeof value === "string") usedDatasets.add(value);
|
|
198
188
|
};
|
|
199
189
|
collectDatasets(incoming.slides);
|
|
200
|
-
const datasetIds = new Set(Object.keys(
|
|
190
|
+
const datasetIds = new Set(Object.keys(presentation.datasets ?? {}));
|
|
201
191
|
for (const id of usedDatasets) {
|
|
202
192
|
const mine = incoming.datasets && Object.hasOwn(incoming.datasets, id) ? incoming.datasets[id] : undefined;
|
|
203
193
|
if (mine === undefined) continue;
|
|
204
|
-
if (datasetIds.has(id) && JSON.stringify(
|
|
194
|
+
if (datasetIds.has(id) && JSON.stringify(presentation.datasets[id]) === JSON.stringify(mine)) {
|
|
205
195
|
datasetMap.set(id, id);
|
|
206
196
|
continue;
|
|
207
197
|
}
|
|
@@ -247,21 +237,21 @@ export function prepareOpfImport(
|
|
|
247
237
|
};
|
|
248
238
|
const rewritten = rewrite(incoming);
|
|
249
239
|
if (incoming.catalogs) {
|
|
250
|
-
|
|
240
|
+
presentation.catalogs ??= {};
|
|
251
241
|
for (const [kind, catalog] of Object.entries(rewritten.catalogs))
|
|
252
|
-
|
|
253
|
-
...
|
|
242
|
+
presentation.catalogs[kind] = {
|
|
243
|
+
...presentation.catalogs[kind],
|
|
254
244
|
...catalog,
|
|
255
245
|
records: [
|
|
256
|
-
...(
|
|
246
|
+
...(presentation.catalogs[kind]?.records ?? []),
|
|
257
247
|
...(catalog.records ?? []),
|
|
258
248
|
],
|
|
259
249
|
};
|
|
260
250
|
}
|
|
261
251
|
if (incoming.assets) {
|
|
262
|
-
|
|
252
|
+
presentation.assets ??= {};
|
|
263
253
|
for (const [id, asset] of Object.entries(rewritten.assets))
|
|
264
|
-
|
|
254
|
+
presentation.assets[assetMap.get(id)] =
|
|
265
255
|
typeof asset === "string" &&
|
|
266
256
|
asset.startsWith("asset:") &&
|
|
267
257
|
assetMap.has(asset.slice(6))
|
|
@@ -269,14 +259,14 @@ export function prepareOpfImport(
|
|
|
269
259
|
: asset;
|
|
270
260
|
}
|
|
271
261
|
for (const [id, next] of datasetMap) {
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
262
|
+
if (presentation.datasets && Object.hasOwn(presentation.datasets, next)) continue;
|
|
263
|
+
presentation.datasets ??= {};
|
|
264
|
+
presentation.datasets[next] = clone(incoming.datasets[id]);
|
|
275
265
|
}
|
|
276
|
-
const ids = new Set(collectReservedPresentationIds(
|
|
266
|
+
const ids = new Set(collectReservedPresentationIds(presentation));
|
|
277
267
|
for (const slide of rewritten.slides) remapSlideTreeIds(slide, ids);
|
|
278
|
-
const index = Math.max(0, Math.min(
|
|
279
|
-
|
|
280
|
-
assertOpf(
|
|
281
|
-
return {
|
|
268
|
+
const index = Math.max(0, Math.min(presentation.slides.length, slideIndex + 1));
|
|
269
|
+
presentation.slides.splice(index, 0, ...rewritten.slides);
|
|
270
|
+
assertOpf(presentation);
|
|
271
|
+
return { presentation, slideIndex: index };
|
|
282
272
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
"check:font-hotlinks": "node --test scripts/check-font-hotlinks.test.mjs && node scripts/check-font-hotlinks.mjs",
|
|
219
219
|
"check:font-hotlinks:built": "node scripts/check-font-hotlinks.mjs --built dist artifacts/playground",
|
|
220
220
|
"build:playground": "node scripts/build-playground.mjs",
|
|
221
|
-
"test:playground": "npm run build:playground && npm run check:font-hotlinks:built && node test/playground-pptx.mjs && node test/playground-script-fonts.mjs artifacts/playground-script-fonts && node test/playground-lazy-fonts.mjs artifacts/playground-lazy-fonts && node test/playground-base-fonts.mjs artifacts/playground-base-fonts && node test/playground-source.mjs && node test/design-controls-browser.mjs && node test/design-gaps-browser.mjs && node test/chart-options-browser.mjs && node test/persistence-browser.mjs && node test/slides-browser.mjs && node test/template-panel-browser.mjs && node test/numbering-panel-browser.mjs && node test/playground-ensure-fonts.mjs artifacts/playground-ensure-fonts && node test/canvas-fonts-browser.mjs artifacts/canvas-fonts && node test/find-replace-browser.mjs && node test/image-crop-browser.mjs && node test/mobile-browser.mjs && node test/review-panel-browser.mjs && node test/playground-download.mjs artifacts/playground-download",
|
|
221
|
+
"test:playground": "npm run build:playground && npm run check:font-hotlinks:built && node test/playground-pptx.mjs && node test/playground-script-fonts.mjs artifacts/playground-script-fonts && node test/playground-lazy-fonts.mjs artifacts/playground-lazy-fonts && node test/playground-base-fonts.mjs artifacts/playground-base-fonts && node test/playground-source.mjs && node test/design-controls-browser.mjs && node test/design-gaps-browser.mjs && node test/chart-options-browser.mjs && node test/persistence-browser.mjs && node test/slides-browser.mjs && node test/template-panel-browser.mjs && node test/numbering-panel-browser.mjs && node test/playground-ensure-fonts.mjs artifacts/playground-ensure-fonts && node test/canvas-fonts-browser.mjs artifacts/canvas-fonts && node test/find-replace-browser.mjs && node test/image-crop-browser.mjs && node test/mobile-browser.mjs && node test/review-panel-browser.mjs && node test/review-hook-browser.mjs && node test/playground-download.mjs artifacts/playground-download",
|
|
222
222
|
"test:code-browser": "node test/code-browser.mjs",
|
|
223
223
|
"test:selection-browser": "node test/selection-browser.mjs",
|
|
224
224
|
"test:click-entry-browser": "node test/click-entry-browser.mjs",
|
|
@@ -238,6 +238,8 @@
|
|
|
238
238
|
"test:crop-browser": "node test/image-crop-browser.mjs",
|
|
239
239
|
"test:mobile-browser": "node test/mobile-browser.mjs",
|
|
240
240
|
"test:review-panel-browser": "node test/review-panel-browser.mjs",
|
|
241
|
+
"test:review-hook-browser": "node test/review-hook-browser.mjs",
|
|
242
|
+
"test:export-license-browser": "node test/export-license-browser.mjs",
|
|
241
243
|
"test:chart-options-browser": "node test/chart-options-browser.mjs",
|
|
242
244
|
"test:numbering-panel-browser": "node test/numbering-panel-browser.mjs"
|
|
243
245
|
},
|
|
@@ -251,11 +253,11 @@
|
|
|
251
253
|
"@codemirror/state": "6.7.6",
|
|
252
254
|
"@codemirror/view": "6.43.13",
|
|
253
255
|
"@lezer/highlight": "1.2.4",
|
|
254
|
-
"@openpresentation/opf": "^0.
|
|
256
|
+
"@openpresentation/opf": "^0.14.0",
|
|
255
257
|
"jsonc-parser": "3.3.1"
|
|
256
258
|
},
|
|
257
259
|
"peerDependencies": {
|
|
258
|
-
"@openpresentation/opf-render": "^0.
|
|
260
|
+
"@openpresentation/opf-render": "^0.14.0"
|
|
259
261
|
},
|
|
260
262
|
"peerDependenciesMeta": {
|
|
261
263
|
"@openpresentation/opf-render": {
|
|
@@ -297,8 +299,8 @@
|
|
|
297
299
|
"@expo-google-fonts/noto-sans-thai": "0.4.2",
|
|
298
300
|
"@expo-google-fonts/noto-serif-hebrew": "0.4.2",
|
|
299
301
|
"@expo-google-fonts/noto-serif-tibetan": "0.4.2",
|
|
300
|
-
"@openpresentation/opf-pptx": "^0.
|
|
301
|
-
"@openpresentation/opf-render": "^0.
|
|
302
|
+
"@openpresentation/opf-pptx": "^0.14.0",
|
|
303
|
+
"@openpresentation/opf-render": "^0.14.0",
|
|
302
304
|
"axe-core": "4.13.0",
|
|
303
305
|
"esbuild": "0.28.2",
|
|
304
306
|
"jszip": "3.10.2",
|
package/dist/font-defaults.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Last-resort font scheme id, shared with core pagination, opf-render and opf-pptx
|
|
3
|
-
* (`DEFAULT_FONT_SCHEME` in `@openpresentation/opf`). It applies only when neither the
|
|
4
|
-
* slide, the deck nor the resolved theme names a font scheme, so the editor measures and
|
|
5
|
-
* freezes the same fonts that preview and PowerPoint export use.
|
|
6
|
-
* Kept local until the published core exports the constant; a test asserts they agree.
|
|
7
|
-
*/
|
|
8
|
-
export const DEFAULT_FONT_SCHEME = "aptos";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Resolve a font-scheme reference by the shared rule (`resolveFontSchemeReference` in
|
|
12
|
-
* `@openpresentation/opf`). An id that matches no record yields one
|
|
13
|
-
* `unresolved-font-scheme` diagnostic and uses the DEFAULT_FONT_SCHEME record as the base,
|
|
14
|
-
* with sibling overrides on top. Kept local until a published core exports the function;
|
|
15
|
-
* a test compares both whenever the installed core has it.
|
|
16
|
-
*/
|
|
17
|
-
export function resolveFontSchemeReference(reference, lookup, path = "design.fontScheme") {
|
|
18
|
-
const overrides = reference && typeof reference === "object" && !Array.isArray(reference) ? reference : undefined;
|
|
19
|
-
const id = typeof reference === "string" ? reference : typeof overrides?.id === "string" ? overrides.id : undefined;
|
|
20
|
-
const found = id === undefined ? undefined : lookup(id);
|
|
21
|
-
const base = found ?? lookup(DEFAULT_FONT_SCHEME) ?? {};
|
|
22
|
-
const scheme = overrides ? { ...base, ...overrides } : { ...base };
|
|
23
|
-
if (id === undefined || found !== undefined) return { scheme };
|
|
24
|
-
return { scheme, diagnostic: { code: "unresolved-font-scheme", path, id, fallback: DEFAULT_FONT_SCHEME, message: `Font scheme '${id}' is not in the inline or bundled catalogs; using the default font scheme '${DEFAULT_FONT_SCHEME}'.` } };
|
|
25
|
-
}
|