@openpresentation/opf-editor 0.14.2 → 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.
- package/README.md +60 -29
- package/dist/assets.js +1 -1
- package/dist/background-options.d.ts +10 -3
- package/dist/background-options.js +38 -13
- package/dist/canvas.d.ts +8 -1
- package/dist/canvas.js +12 -9
- package/dist/catalogs.d.ts +48 -0
- package/dist/catalogs.js +217 -0
- package/dist/checks.js +9 -7
- package/dist/content-actions.d.ts +2 -2
- package/dist/content-actions.js +3 -3
- package/dist/content-controls.js +9 -9
- package/dist/design-controls.d.ts +12 -5
- package/dist/design-controls.js +260 -76
- package/dist/design-options.d.ts +4 -4
- package/dist/design-options.js +6 -26
- package/dist/export.d.ts +2 -0
- package/dist/export.js +8 -3
- package/dist/galleries.js +32 -67
- package/dist/image-crop.js +8 -9
- package/dist/image-options.d.ts +45 -0
- package/dist/image-options.js +195 -0
- package/dist/index.d.ts +25 -8
- package/dist/index.js +91 -47
- package/dist/json-editor.js +2 -2
- package/dist/json-menu.js +2 -2
- package/dist/json-options.d.ts +3 -3
- package/dist/json-options.js +30 -41
- package/dist/react.js +1 -1
- package/dist/review-panel.js +8 -2
- package/dist/schema-inspector.js +1 -1
- package/dist/schema.js +1 -1
- package/dist/slide-manager.js +1 -1
- package/dist/slides.d.ts +1 -1
- package/dist/slides.js +6 -5
- package/dist/svelte.js +1 -2
- package/dist/switches.d.ts +31 -16
- package/dist/switches.js +191 -90
- package/dist/transfer.d.ts +12 -1
- package/dist/transfer.js +87 -131
- package/package.json +9 -5
package/dist/transfer.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { applyJsonPatch, createValuePatch, getValueAtPath } from "./index.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ENGINE_DEFAULT_COLOR_SCHEME, ENGINE_DEFAULT_FONT_SCHEME, ENGINE_DEFAULT_THEME, copySlides, parseReference, resolveReference } from "@openpresentation/opf";
|
|
3
3
|
import { collectReservedPresentationIds, remapSlideTreeIds } from "./presentation-ids.js";
|
|
4
|
-
import { DEFAULT_FONT_SCHEME } from "@openpresentation/opf/composition";
|
|
5
4
|
import { checkFormat, firstErrorMessage } from "./checks.js";
|
|
5
|
+
import { mergeCatalogs, prepareSave } from "./catalogs.js";
|
|
6
6
|
export const MAX_OPF_BYTES = 20 * 1024 * 1024;
|
|
7
7
|
const clone = (value) => structuredClone(value);
|
|
8
8
|
export function assertOpf(presentation) {
|
|
@@ -66,15 +66,21 @@ export function parseOpfTransfer(text) {
|
|
|
66
66
|
return { kind: "slides", value, presentation };
|
|
67
67
|
return { kind: "selection", value };
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The OPF text to copy or save. A presentation or a slide is self-contained: core's `embed` adds every catalog record it
|
|
71
|
+
* references (from `catalogs`, the host's registered list), so it renders the same where no catalog is registered.
|
|
72
|
+
*/
|
|
69
73
|
export function serializeOpfTransfer(
|
|
70
74
|
presentation,
|
|
71
|
-
{ scope = "presentation", slideIndex = 0, path, format = "pretty" } = {},
|
|
75
|
+
{ scope = "presentation", slideIndex = 0, path, format = "pretty", catalogs } = {},
|
|
72
76
|
) {
|
|
73
77
|
let value = presentation;
|
|
74
78
|
if (scope === "slide") {
|
|
75
79
|
if (!presentation.slides?.[slideIndex])
|
|
76
80
|
throw new Error("Select a slide first.");
|
|
77
|
-
value = { ...presentation, slides: [presentation.slides[slideIndex]] };
|
|
81
|
+
value = prepareSave({ ...presentation, slides: [presentation.slides[slideIndex]] }, { catalogs }).document;
|
|
82
|
+
} else if (scope === "presentation") {
|
|
83
|
+
value = prepareSave(presentation, { catalogs }).document;
|
|
78
84
|
} else if (scope === "selection") {
|
|
79
85
|
value = getValueAtPath(presentation, path);
|
|
80
86
|
if (value === undefined) throw new Error("Select some content first.");
|
|
@@ -82,22 +88,59 @@ export function serializeOpfTransfer(
|
|
|
82
88
|
const json = JSON.stringify(value, null, format === "compact" ? 0 : 2);
|
|
83
89
|
return format === "markdown" ? `\`\`\`json\n${json}\n\`\`\`` : json;
|
|
84
90
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
91
|
+
const RESERVED_GROUPS = new Set(["default", "custom"]);
|
|
92
|
+
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
93
|
+
// A reference read out of a record of `group` keeps naming that group's record when it is written on a slide.
|
|
94
|
+
function qualified(value, group) {
|
|
95
|
+
const qualify = (reference) => {
|
|
96
|
+
const parsed = typeof reference === "string" ? parseReference(reference) : undefined;
|
|
97
|
+
return !parsed || parsed.group || RESERVED_GROUPS.has(group) ? reference : `${group}:${parsed.id}`;
|
|
98
|
+
};
|
|
99
|
+
if (typeof value === "string") return qualify(value);
|
|
100
|
+
if (isObject(value) && typeof value.id === "string") return { ...clone(value), id: qualify(value.id) };
|
|
101
|
+
return value === undefined ? undefined : clone(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Freeze the source deck's look on its slides before they move: each slide carries its colour scheme, font scheme, background
|
|
106
|
+
* and header/footer, so the target deck's design does not restyle it. A value the source takes from its theme is written with
|
|
107
|
+
* the theme's catalog group; one it takes from the engine default is written only when the target would not use the engine
|
|
108
|
+
* default too (core's `ENGINE_DEFAULT_*`, never a catalog id).
|
|
109
|
+
*/
|
|
110
|
+
function frozenSlides(incoming, current, catalogs) {
|
|
111
|
+
const target = isObject(current.design) ? current.design : {};
|
|
112
|
+
const targetDefault = (key) => target[key] === undefined && target.theme === undefined;
|
|
113
|
+
return incoming.slides.map((slide) => {
|
|
114
|
+
// A slide's design cannot set dimensions (a PPTX has one slide size): the inserted slide takes the host deck's.
|
|
115
|
+
const { dimensions: _deckSize, ...design } = { ...incoming.design, ...slide.design };
|
|
116
|
+
const theme = typeof design.theme === "string" ? resolveReference(incoming, "themes", design.theme, { catalogs }) : undefined;
|
|
117
|
+
const record = isObject(theme?.record) ? theme.record : {};
|
|
118
|
+
const next = { ...design };
|
|
119
|
+
const freeze = (key, engineDefault) => {
|
|
120
|
+
if (design[key] !== undefined) return;
|
|
121
|
+
if (record[key] !== undefined) next[key] = qualified(record[key], theme.group);
|
|
122
|
+
else if (!targetDefault(key)) next[key] = clone(engineDefault);
|
|
123
|
+
};
|
|
124
|
+
freeze("colorScheme", ENGINE_DEFAULT_COLOR_SCHEME);
|
|
125
|
+
freeze("fontScheme", ENGINE_DEFAULT_FONT_SCHEME);
|
|
126
|
+
freeze("background", ENGINE_DEFAULT_THEME.background);
|
|
127
|
+
next.header = design.header ?? false;
|
|
128
|
+
next.footer = design.footer ?? false;
|
|
129
|
+
return { ...slide, design: next };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Apply a parsed transfer to the current document. `mode` "selection" replaces the selected value, "replace" opens the transfer
|
|
135
|
+
* as the presentation, and "insert" copies its slides in after `slideIndex` with core's `copySlides`: catalog groups match by
|
|
136
|
+
* `source`, identical records are reused, a differing record is renamed, and `renamed` / `addedGroups` say what changed so the
|
|
137
|
+
* host can tell the user. Assets and shared datasets come along under free ids. `catalogs` are the host's registered catalogs.
|
|
138
|
+
* Returns `{ presentation, slideIndex, renamed, addedGroups }`.
|
|
139
|
+
*/
|
|
97
140
|
export function prepareOpfImport(
|
|
98
141
|
current,
|
|
99
142
|
transfer,
|
|
100
|
-
{ mode = "insert", slideIndex = 0, path } = {},
|
|
143
|
+
{ mode = "insert", slideIndex = 0, path, catalogs } = {},
|
|
101
144
|
) {
|
|
102
145
|
if (mode === "selection") {
|
|
103
146
|
const presentation = applyJsonPatch(
|
|
@@ -105,71 +148,21 @@ export function prepareOpfImport(
|
|
|
105
148
|
createValuePatch(current, path, transfer.value),
|
|
106
149
|
);
|
|
107
150
|
assertOpf(presentation);
|
|
108
|
-
return { presentation, slideIndex };
|
|
151
|
+
return { presentation, slideIndex, renamed: [], addedGroups: [] };
|
|
109
152
|
}
|
|
110
153
|
if (!transfer.presentation)
|
|
111
154
|
throw new Error(
|
|
112
155
|
"This is a content fragment. Choose Replace selected content.",
|
|
113
156
|
);
|
|
114
157
|
if (mode === "replace")
|
|
115
|
-
return { presentation: assertOpf(clone(transfer.presentation)), slideIndex: 0 };
|
|
158
|
+
return { presentation: assertOpf(clone(transfer.presentation)), slideIndex: 0, renamed: [], addedGroups: [] };
|
|
116
159
|
if (mode !== "insert") throw new Error("Unknown import action.");
|
|
160
|
+
const list = mergeCatalogs(catalogs);
|
|
117
161
|
const incoming = clone(transfer.presentation),
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
125
|
-
return {
|
|
126
|
-
...slide,
|
|
127
|
-
design: {
|
|
128
|
-
...design,
|
|
129
|
-
theme: design.theme ?? "minimal",
|
|
130
|
-
colorScheme: design.colorScheme ?? theme.colorScheme ?? "cool-horizon",
|
|
131
|
-
fontScheme: design.fontScheme ?? theme.fontScheme ?? DEFAULT_FONT_SCHEME,
|
|
132
|
-
background: design.background ?? theme.background ?? "#FFFFFF",
|
|
133
|
-
header: design.header ?? false,
|
|
134
|
-
footer: design.footer ?? false,
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
});
|
|
138
|
-
const mappings = {},
|
|
139
|
-
assetMap = new Map();
|
|
140
|
-
let chartTypes = new Map();
|
|
141
|
-
for (const [kind, catalog] of Object.entries(incoming.catalogs ?? {})) {
|
|
142
|
-
const existing = presentation.catalogs?.[kind];
|
|
143
|
-
if (
|
|
144
|
-
catalog.source &&
|
|
145
|
-
existing?.source &&
|
|
146
|
-
JSON.stringify(catalog.source) !== JSON.stringify(existing.source)
|
|
147
|
-
)
|
|
148
|
-
throw new Error(
|
|
149
|
-
`The ${kind} gallery sources conflict. Open this as a separate presentation instead.`,
|
|
150
|
-
);
|
|
151
|
-
if (catalog.source && !existing?.source)
|
|
152
|
-
throw new Error(
|
|
153
|
-
`Resolve ${kind} records into this OPF before inserting, or open it as a presentation.`,
|
|
154
|
-
);
|
|
155
|
-
const used = new Set(
|
|
156
|
-
[...(existing?.records ?? []), ...(catalogs[kind] ?? [])].map(
|
|
157
|
-
(record) => record.id,
|
|
158
|
-
),
|
|
159
|
-
);
|
|
160
|
-
const names = new Map();
|
|
161
|
-
for (const record of catalog.records ?? []) {
|
|
162
|
-
let id = `import-${record.id}`,
|
|
163
|
-
n = 2;
|
|
164
|
-
while (used.has(id)) id = `import-${record.id}-${n++}`;
|
|
165
|
-
used.add(id);
|
|
166
|
-
names.set(record.id, id);
|
|
167
|
-
record.id = id;
|
|
168
|
-
}
|
|
169
|
-
if (catalogKeys[kind]) mappings[catalogKeys[kind]] = names;
|
|
170
|
-
if (kind === "chartTypes") chartTypes = names;
|
|
171
|
-
}
|
|
172
|
-
const assetIds = new Set(Object.keys(presentation.assets ?? {}));
|
|
162
|
+
target = clone(current);
|
|
163
|
+
incoming.slides = frozenSlides(incoming, current, list);
|
|
164
|
+
const assetMap = new Map();
|
|
165
|
+
const assetIds = new Set(Object.keys(target.assets ?? {}));
|
|
173
166
|
for (const id of Object.keys(incoming.assets ?? {})) {
|
|
174
167
|
let next = id,
|
|
175
168
|
n = 2;
|
|
@@ -187,11 +180,11 @@ export function prepareOpfImport(
|
|
|
187
180
|
else if (key === "dataset" && typeof value === "string") usedDatasets.add(value);
|
|
188
181
|
};
|
|
189
182
|
collectDatasets(incoming.slides);
|
|
190
|
-
const datasetIds = new Set(Object.keys(
|
|
183
|
+
const datasetIds = new Set(Object.keys(target.datasets ?? {}));
|
|
191
184
|
for (const id of usedDatasets) {
|
|
192
185
|
const mine = incoming.datasets && Object.hasOwn(incoming.datasets, id) ? incoming.datasets[id] : undefined;
|
|
193
186
|
if (mine === undefined) continue;
|
|
194
|
-
if (datasetIds.has(id) && JSON.stringify(
|
|
187
|
+
if (datasetIds.has(id) && JSON.stringify(target.datasets[id]) === JSON.stringify(mine)) {
|
|
195
188
|
datasetMap.set(id, id);
|
|
196
189
|
continue;
|
|
197
190
|
}
|
|
@@ -203,70 +196,33 @@ export function prepareOpfImport(
|
|
|
203
196
|
}
|
|
204
197
|
const rewrite = (value, key = "") => {
|
|
205
198
|
if (Array.isArray(value)) return value.map((child) => rewrite(child, key));
|
|
206
|
-
if (value && typeof value === "object")
|
|
207
|
-
|
|
208
|
-
Object.entries(value).map(([childKey, child]) => [
|
|
209
|
-
childKey,
|
|
210
|
-
rewrite(child, childKey),
|
|
211
|
-
]),
|
|
212
|
-
);
|
|
213
|
-
if (key === "socials" && mappings.platform) {
|
|
214
|
-
for (const [id, child] of Object.entries(result)) {
|
|
215
|
-
if (mappings.platform.has(id)) {
|
|
216
|
-
delete result[id];
|
|
217
|
-
result[mappings.platform.get(id)] = child;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (mappings[key]?.has(value.id)) result.id = mappings[key].get(value.id);
|
|
222
|
-
if (value.data && chartTypes.has(value.type))
|
|
223
|
-
result.type = chartTypes.get(value.type);
|
|
224
|
-
return result;
|
|
225
|
-
}
|
|
199
|
+
if (value && typeof value === "object")
|
|
200
|
+
return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [childKey, rewrite(child, childKey)]));
|
|
226
201
|
if (typeof value === "string") {
|
|
227
202
|
if (key === "dataset" && datasetMap.has(value)) return datasetMap.get(value);
|
|
228
|
-
if (
|
|
229
|
-
if (
|
|
230
|
-
["src", "image", "video", "poster"].includes(key) &&
|
|
231
|
-
value.startsWith("asset:") &&
|
|
232
|
-
assetMap.has(value.slice(6))
|
|
233
|
-
)
|
|
203
|
+
if (["src", "image", "video", "poster", "background"].includes(key) && value.startsWith("asset:") && assetMap.has(value.slice(6)))
|
|
234
204
|
return `asset:${assetMap.get(value.slice(6))}`;
|
|
235
205
|
}
|
|
236
206
|
return value;
|
|
237
207
|
};
|
|
238
|
-
|
|
239
|
-
if (incoming.catalogs) {
|
|
240
|
-
presentation.catalogs ??= {};
|
|
241
|
-
for (const [kind, catalog] of Object.entries(rewritten.catalogs))
|
|
242
|
-
presentation.catalogs[kind] = {
|
|
243
|
-
...presentation.catalogs[kind],
|
|
244
|
-
...catalog,
|
|
245
|
-
records: [
|
|
246
|
-
...(presentation.catalogs[kind]?.records ?? []),
|
|
247
|
-
...(catalog.records ?? []),
|
|
248
|
-
],
|
|
249
|
-
};
|
|
250
|
-
}
|
|
208
|
+
incoming.slides = rewrite(incoming.slides);
|
|
251
209
|
if (incoming.assets) {
|
|
252
|
-
|
|
253
|
-
for (const [id, asset] of Object.entries(
|
|
254
|
-
|
|
255
|
-
typeof asset === "string" &&
|
|
256
|
-
asset.startsWith("asset:") &&
|
|
257
|
-
assetMap.has(asset.slice(6))
|
|
258
|
-
? `asset:${assetMap.get(asset.slice(6))}`
|
|
259
|
-
: asset;
|
|
210
|
+
target.assets ??= {};
|
|
211
|
+
for (const [id, asset] of Object.entries(rewrite(incoming.assets)))
|
|
212
|
+
target.assets[assetMap.get(id)] =
|
|
213
|
+
typeof asset === "string" && asset.startsWith("asset:") && assetMap.has(asset.slice(6)) ? `asset:${assetMap.get(asset.slice(6))}` : asset;
|
|
260
214
|
}
|
|
261
215
|
for (const [id, next] of datasetMap) {
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
216
|
+
if (target.datasets && Object.hasOwn(target.datasets, next)) continue;
|
|
217
|
+
target.datasets ??= {};
|
|
218
|
+
target.datasets[next] = clone(incoming.datasets[id]);
|
|
265
219
|
}
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
presentation
|
|
220
|
+
const index = Math.max(0, Math.min(target.slides.length, slideIndex + 1));
|
|
221
|
+
// Core copies the catalog records the slides use and rewrites their references (FA-20).
|
|
222
|
+
const copied = copySlides(incoming, target, incoming.slides.map((_, at) => at), { catalogs: list, at: index });
|
|
223
|
+
const presentation = clone(copied.document);
|
|
224
|
+
const ids = new Set(collectReservedPresentationIds({ ...presentation, slides: presentation.slides.filter((_, at) => !copied.slides.includes(at)) }));
|
|
225
|
+
for (const at of copied.slides) remapSlideTreeIds(presentation.slides[at], ids);
|
|
270
226
|
assertOpf(presentation);
|
|
271
|
-
return { presentation, slideIndex: index };
|
|
227
|
+
return { presentation, slideIndex: copied.slides[0] ?? index, renamed: copied.renamed ?? [], addedGroups: copied.addedGroups ?? [] };
|
|
272
228
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -118,6 +118,10 @@
|
|
|
118
118
|
"types": "./dist/background-options.d.ts",
|
|
119
119
|
"default": "./dist/background-options.js"
|
|
120
120
|
},
|
|
121
|
+
"./image-options": {
|
|
122
|
+
"types": "./dist/image-options.d.ts",
|
|
123
|
+
"default": "./dist/image-options.js"
|
|
124
|
+
},
|
|
121
125
|
"./review": {
|
|
122
126
|
"types": "./dist/review.d.ts",
|
|
123
127
|
"default": "./dist/review.js"
|
|
@@ -253,11 +257,11 @@
|
|
|
253
257
|
"@codemirror/state": "6.7.6",
|
|
254
258
|
"@codemirror/view": "6.43.13",
|
|
255
259
|
"@lezer/highlight": "1.2.4",
|
|
256
|
-
"@openpresentation/opf": "^0.
|
|
260
|
+
"@openpresentation/opf": "^0.15.1",
|
|
257
261
|
"jsonc-parser": "3.3.1"
|
|
258
262
|
},
|
|
259
263
|
"peerDependencies": {
|
|
260
|
-
"@openpresentation/opf-render": "^0.
|
|
264
|
+
"@openpresentation/opf-render": "^0.15.0"
|
|
261
265
|
},
|
|
262
266
|
"peerDependenciesMeta": {
|
|
263
267
|
"@openpresentation/opf-render": {
|
|
@@ -299,8 +303,8 @@
|
|
|
299
303
|
"@expo-google-fonts/noto-sans-thai": "0.4.2",
|
|
300
304
|
"@expo-google-fonts/noto-serif-hebrew": "0.4.2",
|
|
301
305
|
"@expo-google-fonts/noto-serif-tibetan": "0.4.2",
|
|
302
|
-
"@openpresentation/opf-pptx": "^0.
|
|
303
|
-
"@openpresentation/opf-render": "^0.
|
|
306
|
+
"@openpresentation/opf-pptx": "^0.15.0",
|
|
307
|
+
"@openpresentation/opf-render": "^0.15.0",
|
|
304
308
|
"axe-core": "4.13.0",
|
|
305
309
|
"esbuild": "0.28.2",
|
|
306
310
|
"jszip": "3.10.2",
|