@openpresentation/opf-editor 0.0.1 → 0.1.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 +79 -0
- package/dist/block-controls.js +143 -0
- package/dist/blocks.d.ts +13 -0
- package/dist/blocks.js +125 -0
- package/dist/canvas-fields.js +63 -0
- package/dist/canvas.d.ts +67 -0
- package/dist/canvas.js +748 -0
- package/dist/data.d.ts +2 -0
- package/dist/data.js +1 -0
- package/dist/galleries.d.ts +37 -0
- package/dist/galleries.js +237 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +77 -6
- package/dist/layout-handles.js +116 -0
- package/dist/layout.d.ts +10 -0
- package/dist/layout.js +34 -0
- package/dist/rich-text-toolbar.js +123 -0
- package/dist/rich-text.d.ts +10 -0
- package/dist/rich-text.js +71 -0
- package/dist/schema-inspector.d.ts +3 -0
- package/dist/schema-inspector.js +100 -0
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +102 -0
- package/dist/transfer.d.ts +27 -0
- package/dist/transfer.js +256 -0
- package/package.json +38 -6
package/dist/transfer.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyJsonPatch,
|
|
3
|
+
createValuePatch,
|
|
4
|
+
getValueAtPath,
|
|
5
|
+
validateOpfDocument,
|
|
6
|
+
} from "./index.js";
|
|
7
|
+
import { catalogs } from "@openpresentation/opf";
|
|
8
|
+
export const MAX_OPF_BYTES = 20 * 1024 * 1024;
|
|
9
|
+
const clone = (value) => structuredClone(value);
|
|
10
|
+
export function assertOpf(document) {
|
|
11
|
+
bounded(document);
|
|
12
|
+
const result = validateOpfDocument(document);
|
|
13
|
+
if (!result.valid)
|
|
14
|
+
throw new Error(result.errors[0]?.message ?? "Invalid OPF document.");
|
|
15
|
+
return document;
|
|
16
|
+
}
|
|
17
|
+
function bounded(value, depth = 0, budget = { remaining: 250000 }) {
|
|
18
|
+
if (depth > 64 || --budget.remaining < 0)
|
|
19
|
+
throw new Error("OPF exceeds the supported nesting or item limit.");
|
|
20
|
+
if (value && typeof value === "object")
|
|
21
|
+
for (const [key, child] of Object.entries(value)) {
|
|
22
|
+
if (["__proto__", "prototype", "constructor"].includes(key))
|
|
23
|
+
throw new Error(`Unsupported object key: ${key}`);
|
|
24
|
+
bounded(child, depth + 1, budget);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function unwrapOpf(value, depth = 0) {
|
|
28
|
+
if (depth > 64) throw new Error("OPF exceeds the supported nesting limit.");
|
|
29
|
+
if (value?.slides) return value;
|
|
30
|
+
if (value?.opfSnippet) return unwrapOpf(value.opfSnippet, depth + 1);
|
|
31
|
+
if (Array.isArray(value?.opf)) {
|
|
32
|
+
const snippet =
|
|
33
|
+
value.opf.find((item) => item.name === "default") ?? value.opf[0];
|
|
34
|
+
if (snippet) return unwrapOpf(snippet.value ?? JSON.parse(snippet.source), depth + 1);
|
|
35
|
+
}
|
|
36
|
+
if (value?.opf && typeof value.opf === "object") return unwrapOpf(value.opf, depth + 1);
|
|
37
|
+
if (value?.document) return unwrapOpf(value.document, depth + 1);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
export function parseOpfTransfer(text) {
|
|
41
|
+
if (
|
|
42
|
+
typeof text !== "string" ||
|
|
43
|
+
new TextEncoder().encode(text).length > MAX_OPF_BYTES
|
|
44
|
+
)
|
|
45
|
+
throw new Error("Choose OPF smaller than 20 MB.");
|
|
46
|
+
let source = text.replace(/^\uFEFF/, "").trim();
|
|
47
|
+
if (source.includes("```")) {
|
|
48
|
+
const fences = [...source.matchAll(/```(?:json|opf)?\s*\n([\s\S]*?)```/gi)];
|
|
49
|
+
if (fences.length !== 1)
|
|
50
|
+
throw new Error("Paste one JSON or OPF code block at a time.");
|
|
51
|
+
source = fences[0][1].trim();
|
|
52
|
+
}
|
|
53
|
+
let value;
|
|
54
|
+
try {
|
|
55
|
+
value = JSON.parse(source);
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error("Paste valid OPF JSON, a slide, or one JSON code block.");
|
|
58
|
+
}
|
|
59
|
+
bounded(value);
|
|
60
|
+
value = unwrapOpf(value);
|
|
61
|
+
bounded(value);
|
|
62
|
+
if (value?.slides) {
|
|
63
|
+
assertOpf(value);
|
|
64
|
+
return { kind: "presentation", value, document: clone(value) };
|
|
65
|
+
}
|
|
66
|
+
const document = { slides: Array.isArray(value) ? value : [value] };
|
|
67
|
+
if (validateOpfDocument(document).valid)
|
|
68
|
+
return { kind: "slides", value, document };
|
|
69
|
+
return { kind: "selection", value };
|
|
70
|
+
}
|
|
71
|
+
export function serializeOpfTransfer(
|
|
72
|
+
document,
|
|
73
|
+
{ scope = "presentation", slideIndex = 0, path, format = "pretty" } = {},
|
|
74
|
+
) {
|
|
75
|
+
let value = document;
|
|
76
|
+
if (scope === "slide") {
|
|
77
|
+
if (!document.slides?.[slideIndex])
|
|
78
|
+
throw new Error("Select a slide first.");
|
|
79
|
+
value = { ...document, slides: [document.slides[slideIndex]] };
|
|
80
|
+
} else if (scope === "selection") {
|
|
81
|
+
value = getValueAtPath(document, path);
|
|
82
|
+
if (value === undefined) throw new Error("Select some content first.");
|
|
83
|
+
}
|
|
84
|
+
const json = JSON.stringify(value, null, format === "compact" ? 0 : 2);
|
|
85
|
+
return format === "markdown" ? `\`\`\`json\n${json}\n\`\`\`` : json;
|
|
86
|
+
}
|
|
87
|
+
const catalogKeys = {
|
|
88
|
+
layouts: "layout",
|
|
89
|
+
themes: "theme",
|
|
90
|
+
fontSchemes: "fontScheme",
|
|
91
|
+
colorSchemes: "colorScheme",
|
|
92
|
+
narratives: "narrative",
|
|
93
|
+
languages: "language",
|
|
94
|
+
tones: "tone",
|
|
95
|
+
audiences: "audience",
|
|
96
|
+
purposes: "purpose",
|
|
97
|
+
socialPlatforms: "platform",
|
|
98
|
+
};
|
|
99
|
+
function themeFor(document, reference) {
|
|
100
|
+
const id = typeof reference === "string" ? reference : reference?.id;
|
|
101
|
+
const record =
|
|
102
|
+
document.catalogs?.themes?.records?.find((item) => item.id === id) ??
|
|
103
|
+
catalogs.themes.find((item) => item.id === id);
|
|
104
|
+
return { ...record, ...(typeof reference === "object" ? reference : {}) };
|
|
105
|
+
}
|
|
106
|
+
export function prepareOpfImport(
|
|
107
|
+
current,
|
|
108
|
+
transfer,
|
|
109
|
+
{ mode = "insert", slideIndex = 0, path } = {},
|
|
110
|
+
) {
|
|
111
|
+
if (mode === "selection") {
|
|
112
|
+
const document = applyJsonPatch(
|
|
113
|
+
current,
|
|
114
|
+
createValuePatch(current, path, transfer.value),
|
|
115
|
+
);
|
|
116
|
+
assertOpf(document);
|
|
117
|
+
return { document, slideIndex };
|
|
118
|
+
}
|
|
119
|
+
if (!transfer.document)
|
|
120
|
+
throw new Error(
|
|
121
|
+
"This is a content fragment. Choose Replace selected content.",
|
|
122
|
+
);
|
|
123
|
+
if (mode === "replace")
|
|
124
|
+
return { document: assertOpf(clone(transfer.document)), slideIndex: 0 };
|
|
125
|
+
if (mode !== "insert") throw new Error("Unknown import action.");
|
|
126
|
+
const incoming = clone(transfer.document),
|
|
127
|
+
document = clone(current);
|
|
128
|
+
// Freeze the source deck defaults on inserted slides before changing their catalog ids.
|
|
129
|
+
incoming.slides = incoming.slides.map((slide) => {
|
|
130
|
+
const design = { ...incoming.design, ...slide.design };
|
|
131
|
+
const theme = themeFor(incoming, design.theme ?? "minimal");
|
|
132
|
+
return {
|
|
133
|
+
...slide,
|
|
134
|
+
design: {
|
|
135
|
+
...design,
|
|
136
|
+
theme: design.theme ?? "minimal",
|
|
137
|
+
colorScheme: design.colorScheme ?? theme.colorScheme ?? "cool-horizon",
|
|
138
|
+
fontScheme: design.fontScheme ?? theme.fontScheme ?? "roboto",
|
|
139
|
+
dimensions: design.dimensions ?? theme.dimensions ?? "widescreen",
|
|
140
|
+
background: design.background ?? theme.background ?? "#FFFFFF",
|
|
141
|
+
header: design.header ?? false,
|
|
142
|
+
footer: design.footer ?? false,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
const mappings = {},
|
|
147
|
+
assetMap = new Map();
|
|
148
|
+
let chartTypes = new Map();
|
|
149
|
+
for (const [kind, catalog] of Object.entries(incoming.catalogs ?? {})) {
|
|
150
|
+
const existing = document.catalogs?.[kind];
|
|
151
|
+
if (
|
|
152
|
+
catalog.source &&
|
|
153
|
+
existing?.source &&
|
|
154
|
+
JSON.stringify(catalog.source) !== JSON.stringify(existing.source)
|
|
155
|
+
)
|
|
156
|
+
throw new Error(
|
|
157
|
+
`The ${kind} gallery sources conflict. Open this as a separate presentation instead.`,
|
|
158
|
+
);
|
|
159
|
+
if (catalog.source && !existing?.source)
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Resolve ${kind} records into this OPF before inserting, or open it as a presentation.`,
|
|
162
|
+
);
|
|
163
|
+
const used = new Set(
|
|
164
|
+
[...(existing?.records ?? []), ...(catalogs[kind] ?? [])].map(
|
|
165
|
+
(record) => record.id,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
const names = new Map();
|
|
169
|
+
for (const record of catalog.records ?? []) {
|
|
170
|
+
let id = `import-${record.id}`,
|
|
171
|
+
n = 2;
|
|
172
|
+
while (used.has(id)) id = `import-${record.id}-${n++}`;
|
|
173
|
+
used.add(id);
|
|
174
|
+
names.set(record.id, id);
|
|
175
|
+
record.id = id;
|
|
176
|
+
}
|
|
177
|
+
if (catalogKeys[kind]) mappings[catalogKeys[kind]] = names;
|
|
178
|
+
if (kind === "chartTypes") chartTypes = names;
|
|
179
|
+
}
|
|
180
|
+
const assetIds = new Set(Object.keys(document.assets ?? {}));
|
|
181
|
+
for (const id of Object.keys(incoming.assets ?? {})) {
|
|
182
|
+
let next = id,
|
|
183
|
+
n = 2;
|
|
184
|
+
while (assetIds.has(next)) next = `${id}-${n++}`;
|
|
185
|
+
assetIds.add(next);
|
|
186
|
+
assetMap.set(id, next);
|
|
187
|
+
}
|
|
188
|
+
const rewrite = (value, key = "") => {
|
|
189
|
+
if (Array.isArray(value)) return value.map((child) => rewrite(child, key));
|
|
190
|
+
if (value && typeof value === "object") {
|
|
191
|
+
const result = Object.fromEntries(
|
|
192
|
+
Object.entries(value).map(([childKey, child]) => [
|
|
193
|
+
childKey,
|
|
194
|
+
rewrite(child, childKey),
|
|
195
|
+
]),
|
|
196
|
+
);
|
|
197
|
+
if (key === "socials" && mappings.platform) {
|
|
198
|
+
for (const [id, child] of Object.entries(result)) {
|
|
199
|
+
if (mappings.platform.has(id)) {
|
|
200
|
+
delete result[id];
|
|
201
|
+
result[mappings.platform.get(id)] = child;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (mappings[key]?.has(value.id)) result.id = mappings[key].get(value.id);
|
|
206
|
+
if (value.data && chartTypes.has(value.type))
|
|
207
|
+
result.type = chartTypes.get(value.type);
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === "string") {
|
|
211
|
+
if (mappings[key]?.has(value)) return mappings[key].get(value);
|
|
212
|
+
if (
|
|
213
|
+
["src", "image", "video", "poster"].includes(key) &&
|
|
214
|
+
value.startsWith("asset:") &&
|
|
215
|
+
assetMap.has(value.slice(6))
|
|
216
|
+
)
|
|
217
|
+
return `asset:${assetMap.get(value.slice(6))}`;
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
};
|
|
221
|
+
const rewritten = rewrite(incoming);
|
|
222
|
+
if (incoming.catalogs) {
|
|
223
|
+
document.catalogs ??= {};
|
|
224
|
+
for (const [kind, catalog] of Object.entries(rewritten.catalogs))
|
|
225
|
+
document.catalogs[kind] = {
|
|
226
|
+
...document.catalogs[kind],
|
|
227
|
+
...catalog,
|
|
228
|
+
records: [
|
|
229
|
+
...(document.catalogs[kind]?.records ?? []),
|
|
230
|
+
...(catalog.records ?? []),
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (incoming.assets) {
|
|
235
|
+
document.assets ??= {};
|
|
236
|
+
for (const [id, asset] of Object.entries(rewritten.assets))
|
|
237
|
+
document.assets[assetMap.get(id)] =
|
|
238
|
+
typeof asset === "string" &&
|
|
239
|
+
asset.startsWith("asset:") &&
|
|
240
|
+
assetMap.has(asset.slice(6))
|
|
241
|
+
? `asset:${assetMap.get(asset.slice(6))}`
|
|
242
|
+
: asset;
|
|
243
|
+
}
|
|
244
|
+
const ids = new Set(document.slides.map((slide) => slide.id).filter(Boolean));
|
|
245
|
+
for (const slide of rewritten.slides)
|
|
246
|
+
if (slide.id) {
|
|
247
|
+
const base = slide.id;
|
|
248
|
+
let n = 2;
|
|
249
|
+
while (ids.has(slide.id)) slide.id = `${base}-${n++}`;
|
|
250
|
+
ids.add(slide.id);
|
|
251
|
+
}
|
|
252
|
+
const index = Math.max(0, Math.min(document.slides.length, slideIndex + 1));
|
|
253
|
+
document.slides.splice(index, 0, ...rewritten.slides);
|
|
254
|
+
assertOpf(document);
|
|
255
|
+
return { document, slideIndex: index };
|
|
256
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,22 +37,54 @@
|
|
|
37
37
|
"types": "./dist/svelte.d.ts",
|
|
38
38
|
"default": "./dist/svelte.js"
|
|
39
39
|
},
|
|
40
|
-
"./package.json": "./package.json"
|
|
40
|
+
"./package.json": "./package.json",
|
|
41
|
+
"./canvas": {
|
|
42
|
+
"types": "./dist/canvas.d.ts",
|
|
43
|
+
"default": "./dist/canvas.js"
|
|
44
|
+
},
|
|
45
|
+
"./transfer": {
|
|
46
|
+
"types": "./dist/transfer.d.ts",
|
|
47
|
+
"default": "./dist/transfer.js"
|
|
48
|
+
},
|
|
49
|
+
"./galleries": {
|
|
50
|
+
"types": "./dist/galleries.d.ts",
|
|
51
|
+
"default": "./dist/galleries.js"
|
|
52
|
+
},
|
|
53
|
+
"./schema": {
|
|
54
|
+
"types": "./dist/schema.d.ts",
|
|
55
|
+
"default": "./dist/schema.js"
|
|
56
|
+
},
|
|
57
|
+
"./schema-inspector": {
|
|
58
|
+
"types": "./dist/schema-inspector.d.ts",
|
|
59
|
+
"default": "./dist/schema-inspector.js"
|
|
60
|
+
},
|
|
61
|
+
"./data": {
|
|
62
|
+
"types": "./dist/data.d.ts",
|
|
63
|
+
"default": "./dist/data.js"
|
|
64
|
+
},
|
|
65
|
+
"./rich-text": {
|
|
66
|
+
"types": "./dist/rich-text.d.ts",
|
|
67
|
+
"default": "./dist/rich-text.js"
|
|
68
|
+
},
|
|
69
|
+
"./layout": {
|
|
70
|
+
"types": "./dist/layout.d.ts",
|
|
71
|
+
"default": "./dist/layout.js"
|
|
72
|
+
}
|
|
41
73
|
},
|
|
42
74
|
"types": "./dist/index.d.ts",
|
|
43
75
|
"sideEffects": false,
|
|
44
76
|
"scripts": {
|
|
45
77
|
"build": "node scripts/build.mjs",
|
|
46
|
-
"typecheck": "node --check src/index.js && node --check src/react.js && node --check src/svelte.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/component-smoke.mjs",
|
|
47
|
-
"test": "npm run build && node test/smoke.mjs && node test/component-smoke.mjs",
|
|
78
|
+
"typecheck": "node --check src/index.js && node --check src/react.js && node --check src/svelte.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/component-smoke.mjs && node --check src/canvas.js && node --check src/canvas-fields.js && node --check test/canvas-fields.mjs && node --check test/browser-canvas.mjs && node --check src/transfer.js && node --check src/galleries.js && node --check examples/transfer-controls.js && node --check src/schema.js && node --check src/schema-inspector.js",
|
|
79
|
+
"test": "npm run build && node test/smoke.mjs && node test/component-smoke.mjs && node test/canvas-fields.mjs && node test/transfer.mjs && node test/schema.mjs && node test/rich-text.mjs && node test/layout.mjs && node test/blocks.mjs",
|
|
48
80
|
"validate": "node scripts/validate-package.mjs",
|
|
49
81
|
"prepack": "npm run build"
|
|
50
82
|
},
|
|
51
83
|
"dependencies": {
|
|
52
|
-
"@openpresentation/opf": "^0.
|
|
84
|
+
"@openpresentation/opf": "^0.4.0"
|
|
53
85
|
},
|
|
54
86
|
"peerDependencies": {
|
|
55
|
-
"@openpresentation/opf-render": "
|
|
87
|
+
"@openpresentation/opf-render": "^0.1.0"
|
|
56
88
|
},
|
|
57
89
|
"peerDependenciesMeta": {
|
|
58
90
|
"@openpresentation/opf-render": {
|