@openpresentation/opf-editor 0.7.1 → 0.9.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 +50 -13
- package/dist/blocks.d.ts +2 -0
- package/dist/blocks.js +33 -0
- package/dist/canvas.d.ts +9 -0
- package/dist/canvas.js +33 -4
- package/dist/color-authoring.js +20 -0
- package/dist/exact-source.js +318 -0
- package/dist/font-defaults.js +25 -0
- package/dist/galleries.js +48 -8
- package/dist/index.d.ts +9 -2
- package/dist/index.js +21 -5
- package/dist/layout.d.ts +1 -1
- package/dist/layout.js +1 -1
- package/dist/presentation-ids.js +68 -0
- package/dist/rich-text-toolbar.js +2 -1
- package/dist/schema-inspector.js +4 -2
- package/dist/switches.d.ts +77 -0
- package/dist/switches.js +344 -0
- package/dist/transfer.js +5 -9
- package/package.json +50 -12
package/dist/switches.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// Dimension switches (FF-16, font-fidelity-everywhere). One operation per pptx.gallery
|
|
2
|
+
// dimension that turns "switch this dimension to X" into a validated JSON Patch, and applies
|
|
3
|
+
// it to an editor session as one undoable transaction. The preview recomposes and the PPTX
|
|
4
|
+
// export follows from the same document, so no dimension needs a special refresh path.
|
|
5
|
+
import { catalogSchemaNames, catalogs as bundledCatalogs, schemas } from "@openpresentation/opf";
|
|
6
|
+
import {
|
|
7
|
+
OPFEditorError,
|
|
8
|
+
createValuePatch,
|
|
9
|
+
applyJsonPatch,
|
|
10
|
+
getValueAtPath,
|
|
11
|
+
opfPathToJsonPointer,
|
|
12
|
+
splitOpfPath,
|
|
13
|
+
validateOpfDocument,
|
|
14
|
+
} from "./index.js";
|
|
15
|
+
import { createContentBlock, prepareBlockReplace } from "./blocks.js";
|
|
16
|
+
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
17
|
+
|
|
18
|
+
/** The 14 pptx.gallery dimensions (gallery-support.md), in the gallery's order. */
|
|
19
|
+
export const SWITCH_DIMENSIONS = Object.freeze([
|
|
20
|
+
"layouts",
|
|
21
|
+
"color-schemes",
|
|
22
|
+
"font-schemes",
|
|
23
|
+
"languages",
|
|
24
|
+
"backgrounds",
|
|
25
|
+
"narratives",
|
|
26
|
+
"charts",
|
|
27
|
+
"themes",
|
|
28
|
+
"audiences",
|
|
29
|
+
"tones",
|
|
30
|
+
"socials",
|
|
31
|
+
"headers-footers",
|
|
32
|
+
"blocks",
|
|
33
|
+
"image-treatments",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// Catalog kind behind each catalog-backed dimension.
|
|
37
|
+
const CATALOG_KIND = Object.freeze({
|
|
38
|
+
layouts: "layouts",
|
|
39
|
+
"color-schemes": "colorSchemes",
|
|
40
|
+
"font-schemes": "fontSchemes",
|
|
41
|
+
languages: "languages",
|
|
42
|
+
narratives: "narratives",
|
|
43
|
+
themes: "themes",
|
|
44
|
+
audiences: "audiences",
|
|
45
|
+
tones: "tones",
|
|
46
|
+
socials: "socialPlatforms",
|
|
47
|
+
charts: "chartTypes",
|
|
48
|
+
});
|
|
49
|
+
// Top-level document field for the simple metadata dimensions.
|
|
50
|
+
const ROOT_FIELD = Object.freeze({ languages: "language", narratives: "narrative", tones: "tone", audiences: "audience" });
|
|
51
|
+
// `design` keys for the design dimensions. A key set to `null` in the value is removed.
|
|
52
|
+
const DESIGN_KEYS = Object.freeze({
|
|
53
|
+
"color-schemes": ["colorScheme"],
|
|
54
|
+
"font-schemes": ["fontScheme"],
|
|
55
|
+
backgrounds: ["background"],
|
|
56
|
+
"headers-footers": ["header", "footer"],
|
|
57
|
+
"image-treatments": ["slideImage", "imageFill"],
|
|
58
|
+
});
|
|
59
|
+
const REGION = /^(?:(?:top|middle|bottom)(?:\+(?:top|middle|bottom))*(?::(?:left|center|right)(?:\+(?:left|center|right))*)?|(?:left|center|right)(?:\+(?:left|center|right))*)$/;
|
|
60
|
+
const BLOCK_KINDS = ["text", "list", "chart", "table", "metric", "quote", "code", "timeline", "group", "image", "video"];
|
|
61
|
+
|
|
62
|
+
function fail(code, message, details) {
|
|
63
|
+
return new OPFEditorError(code, message, details);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function same(a, b) {
|
|
67
|
+
return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
68
|
+
}
|
|
69
|
+
function canonical(value) {
|
|
70
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
71
|
+
if (value && typeof value === "object")
|
|
72
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function recordList(source) {
|
|
77
|
+
const records = Array.isArray(source) ? source : source?.records;
|
|
78
|
+
return Array.isArray(records) ? records.filter((record) => record && typeof record.id === "string") : [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Inline document records first (they override), then caller-loaded records, then the
|
|
82
|
+
// bundled catalog. Unlike getCatalogRecords, an inline catalog never hides bundled ids.
|
|
83
|
+
function findCatalogRecord(document, kind, id, options) {
|
|
84
|
+
return (
|
|
85
|
+
recordList(document.catalogs?.[kind]).find((record) => record.id === id) ??
|
|
86
|
+
recordList(options.catalogs?.[kind] ?? options.catalogSources?.[kind]).find((record) => record.id === id) ??
|
|
87
|
+
(options.record?.id === id ? options.record : undefined) ??
|
|
88
|
+
bundledCatalogs[kind]?.find((record) => record.id === id)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function requireCatalogId(document, dimension, id, options) {
|
|
93
|
+
const kind = CATALOG_KIND[dimension];
|
|
94
|
+
if (typeof id !== "string" || id.length === 0)
|
|
95
|
+
throw fail("invalid-catalog-id", `Switch ${dimension} to a non-empty catalog id.`, { dimension, id });
|
|
96
|
+
const record = findCatalogRecord(document, kind, id, options);
|
|
97
|
+
if (!record) throw fail("unknown-catalog-id", `Unknown ${kind} catalog ID: ${id}.`, { dimension, catalogKind: kind, id });
|
|
98
|
+
return record;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A gallery item supplies its own record; add it inline in the same transaction when the
|
|
102
|
+
// document and the bundled catalog do not already define the id.
|
|
103
|
+
function catalogRecordPatches(document, dimension, options) {
|
|
104
|
+
const record = options.record;
|
|
105
|
+
if (!record) return [];
|
|
106
|
+
const kind = CATALOG_KIND[dimension];
|
|
107
|
+
if (recordList(document.catalogs?.[kind]).some((entry) => entry.id === record.id) || bundledCatalogs[kind]?.some((entry) => entry.id === record.id)) return [];
|
|
108
|
+
const value = { $schema: schemas[catalogSchemaNames[kind]].$id, ...record };
|
|
109
|
+
if (!document.catalogs || typeof document.catalogs !== "object") return [{ op: "add", path: "/catalogs", value: { [kind]: { records: [value] } } }];
|
|
110
|
+
if (!document.catalogs[kind] || typeof document.catalogs[kind] !== "object")
|
|
111
|
+
return [{ op: "add", path: `/catalogs/${kind}`, value: { records: [value] } }];
|
|
112
|
+
if (!Array.isArray(document.catalogs[kind].records)) return [{ op: "add", path: `/catalogs/${kind}/records`, value: [value] }];
|
|
113
|
+
return [{ op: "add", path: `/catalogs/${kind}/records/-`, value }];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function slideAt(document, slideIndex, what = "this switch") {
|
|
117
|
+
if (!Number.isInteger(slideIndex) || !document.slides?.[slideIndex])
|
|
118
|
+
throw fail("slide-index-out-of-range", `Choose an existing slide for ${what}.`, { slideIndex });
|
|
119
|
+
return document.slides[slideIndex];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function rootPatch(document, field, value) {
|
|
123
|
+
return same(document[field], value) ? [] : createValuePatch(document, [field], value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Set (value) or remove (null) design keys at deck or slide scope.
|
|
127
|
+
function designPatches(document, base, entries) {
|
|
128
|
+
const design = getValueAtPath(document, base.length ? [...base, "design"] : ["design"]);
|
|
129
|
+
const at = (key) => opfPathToJsonPointer([...base, "design", key]);
|
|
130
|
+
const set = Object.entries(entries).filter(([, value]) => value !== undefined && value !== null);
|
|
131
|
+
if (!design || typeof design !== "object" || Array.isArray(design))
|
|
132
|
+
return set.length ? [{ op: "add", path: opfPathToJsonPointer([...base, "design"]), value: structuredClone(Object.fromEntries(set)) }] : [];
|
|
133
|
+
const patches = [];
|
|
134
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
135
|
+
if (value === undefined) continue;
|
|
136
|
+
const present = Object.hasOwn(design, key);
|
|
137
|
+
if (value === null) {
|
|
138
|
+
if (present) patches.push({ op: "remove", path: at(key) });
|
|
139
|
+
} else if (!present) patches.push({ op: "add", path: at(key), value: structuredClone(value) });
|
|
140
|
+
else if (!same(design[key], value)) patches.push({ op: "replace", path: at(key), value: structuredClone(value) });
|
|
141
|
+
}
|
|
142
|
+
return patches;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// A deck-level switch does nothing for a slide that carries its own value for that key.
|
|
146
|
+
function shadowedSlides(document, keys) {
|
|
147
|
+
return (document.slides ?? []).flatMap((slide, index) => (keys.some((key) => slide?.design?.[key] !== undefined) ? [index] : []));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findChartOwner(document, slideIndex) {
|
|
151
|
+
const visit = (node, path, depth) => {
|
|
152
|
+
if (!node || typeof node !== "object" || Array.isArray(node) || depth > 32) return undefined;
|
|
153
|
+
if (node.chart && typeof node.chart === "object") return path;
|
|
154
|
+
if (Array.isArray(node.blocks))
|
|
155
|
+
for (const [index, block] of node.blocks.entries()) {
|
|
156
|
+
const found = visit(block, [...path, "blocks", String(index)], depth + 1);
|
|
157
|
+
if (found) return found;
|
|
158
|
+
}
|
|
159
|
+
for (const [key, value] of Object.entries(node)) {
|
|
160
|
+
if (!REGION.test(key)) continue;
|
|
161
|
+
const found = visit(value, [...path, key], depth + 1);
|
|
162
|
+
if (found) return found;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
};
|
|
166
|
+
return visit(document.slides[slideIndex], ["slides", String(slideIndex)], 0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function socialsPatches(document, value, options) {
|
|
170
|
+
const owner = options.owner ?? "speaker";
|
|
171
|
+
if (!["speaker", "organization"].includes(owner)) throw fail("invalid-switch-value", "Socials owner must be 'speaker' or 'organization'.", { owner });
|
|
172
|
+
if (!value || typeof value !== "object" || typeof value.platform !== "string" || typeof value.handle !== "string" || !value.handle)
|
|
173
|
+
throw fail("invalid-switch-value", "Switch socials to { platform, handle } with a non-empty handle.", { value });
|
|
174
|
+
requireCatalogId(document, "socials", value.platform, options);
|
|
175
|
+
const host = document[owner];
|
|
176
|
+
const index = Array.isArray(host) ? (options.index ?? 0) : undefined;
|
|
177
|
+
const target = Array.isArray(host) ? host[index] : host;
|
|
178
|
+
if (!target || typeof target !== "object")
|
|
179
|
+
throw fail("missing-owner", `Add the ${owner} to the document before setting their socials.`, { owner });
|
|
180
|
+
const base = Array.isArray(host) ? [owner, String(index)] : [owner];
|
|
181
|
+
const socials = target.socials;
|
|
182
|
+
const patches = catalogRecordPatches(document, "socials", options);
|
|
183
|
+
if (!socials || typeof socials !== "object") return [...patches, { op: "add", path: opfPathToJsonPointer([...base, "socials"]), value: { [value.platform]: value.handle } }];
|
|
184
|
+
if (socials[value.platform] === value.handle) return patches;
|
|
185
|
+
return [...patches, { op: Object.hasOwn(socials, value.platform) ? "replace" : "add", path: opfPathToJsonPointer([...base, "socials", value.platform]), value: value.handle }];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function blockValue(value, options) {
|
|
189
|
+
if (typeof value === "string") {
|
|
190
|
+
if (!BLOCK_KINDS.includes(value)) throw fail("invalid-switch-value", `Unknown content block kind: ${value}.`, { value });
|
|
191
|
+
return createContentBlock(value, { source: options.source });
|
|
192
|
+
}
|
|
193
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw fail("invalid-switch-value", "Switch blocks to a content kind or a block object.", { value });
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Compute the patch that switches one dimension, without touching any session. The candidate
|
|
199
|
+
* document is validated; an invalid result throws unless the input was already invalid.
|
|
200
|
+
*/
|
|
201
|
+
export function prepareDimensionSwitch(document, dimension, value, options = {}) {
|
|
202
|
+
if (!SWITCH_DIMENSIONS.includes(dimension))
|
|
203
|
+
throw fail("unknown-dimension", `Unknown dimension: ${dimension}. Use one of ${SWITCH_DIMENSIONS.join(", ")}.`, { dimension });
|
|
204
|
+
if (!document || typeof document !== "object") throw fail("invalid-input", "Switch requires an OPF document object.");
|
|
205
|
+
if (options.record) {
|
|
206
|
+
const ids = dimension === "audiences" ? [value].flat() : dimension === "socials" ? [value?.platform] : [value];
|
|
207
|
+
if (!CATALOG_KIND[dimension] || !ids.includes(options.record.id))
|
|
208
|
+
throw fail("record-id-mismatch", `The supplied record id must equal a switched ${dimension} id.`, { dimension, id: options.record.id });
|
|
209
|
+
}
|
|
210
|
+
const before = validateOpfDocument(document);
|
|
211
|
+
const scopeIndex = options.slideIndex;
|
|
212
|
+
let patches = [];
|
|
213
|
+
let scope = "deck";
|
|
214
|
+
let shadowed = [];
|
|
215
|
+
let slideIndex;
|
|
216
|
+
|
|
217
|
+
if (dimension === "layouts") {
|
|
218
|
+
slideIndex = scopeIndex;
|
|
219
|
+
slideAt(document, slideIndex, "a layout switch");
|
|
220
|
+
scope = "slide";
|
|
221
|
+
const record = requireCatalogId(document, dimension, value, options);
|
|
222
|
+
patches = catalogRecordPatches(document, dimension, options);
|
|
223
|
+
const withRecord = patches.length ? applyJsonPatch(document, patches) : document;
|
|
224
|
+
if (withRecord.slides[slideIndex].layout !== value) {
|
|
225
|
+
const layoutOps = createValuePatch(withRecord, ["slides", String(slideIndex), "layout"], value);
|
|
226
|
+
const swapped = applyJsonPatch(withRecord, layoutOps);
|
|
227
|
+
const types = Array.isArray(record.placeholders) ? record.placeholders.map((placeholder) => placeholder.type) : undefined;
|
|
228
|
+
// Add the blank payloads the layout declares, as the JSON editor's layout choice does.
|
|
229
|
+
const populated = types ? populateLayoutPlaceholders(swapped, slideIndex, types) : swapped;
|
|
230
|
+
patches = same(populated.slides[slideIndex], swapped.slides[slideIndex])
|
|
231
|
+
? [...patches, ...layoutOps]
|
|
232
|
+
: [...patches, { op: "replace", path: `/slides/${slideIndex}`, value: populated.slides[slideIndex] }];
|
|
233
|
+
}
|
|
234
|
+
} else if (dimension === "charts") {
|
|
235
|
+
slideIndex = scopeIndex;
|
|
236
|
+
slideAt(document, slideIndex, "a chart switch");
|
|
237
|
+
scope = "slide";
|
|
238
|
+
requireCatalogId(document, dimension, value, options);
|
|
239
|
+
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(document, slideIndex);
|
|
240
|
+
if (options.path && (owner[0] !== "slides" || owner[1] !== String(slideIndex)))
|
|
241
|
+
throw fail("path-slide-mismatch", "options.path is not on the slide named by options.slideIndex.", { slideIndex, path: options.path });
|
|
242
|
+
const chart = owner && getValueAtPath(document, [...owner, "chart"]);
|
|
243
|
+
if (!chart || typeof chart !== "object") throw fail("chart-not-found", "This slide has no chart to switch. Insert a chart block first.", { slideIndex, path: options.path });
|
|
244
|
+
patches = [...catalogRecordPatches(document, dimension, options), ...(chart.type === value ? [] : createValuePatch(document, [...owner, "chart", "type"], value))];
|
|
245
|
+
} else if (dimension === "blocks") {
|
|
246
|
+
if (options.path === undefined) throw fail("missing-path", "Choose the block to replace with options.path.");
|
|
247
|
+
const block = blockValue(value, options);
|
|
248
|
+
const change = prepareBlockReplace(document, options.path, block);
|
|
249
|
+
patches = change.changed ? change.patches : [];
|
|
250
|
+
const parts = splitOpfPath(options.path);
|
|
251
|
+
if (parts[0] === "slides") slideIndex = Number(parts[1]);
|
|
252
|
+
scope = "block";
|
|
253
|
+
} else if (dimension === "socials") {
|
|
254
|
+
patches = socialsPatches(document, value, options);
|
|
255
|
+
} else if (ROOT_FIELD[dimension]) {
|
|
256
|
+
const field = ROOT_FIELD[dimension];
|
|
257
|
+
let next = value;
|
|
258
|
+
if (dimension === "audiences") {
|
|
259
|
+
next = Array.isArray(value) ? value : [value];
|
|
260
|
+
if (!next.length) throw fail("invalid-catalog-id", "Switch audiences to at least one catalog id.", { value });
|
|
261
|
+
for (const id of next) requireCatalogId(document, dimension, id, options);
|
|
262
|
+
} else requireCatalogId(document, dimension, value, options);
|
|
263
|
+
patches = [...catalogRecordPatches(document, dimension, options), ...rootPatch(document, field, next)];
|
|
264
|
+
} else {
|
|
265
|
+
// Design dimensions: deck scope by default, or one slide with options.slideIndex.
|
|
266
|
+
let entries;
|
|
267
|
+
if (dimension === "themes") {
|
|
268
|
+
const record = requireCatalogId(document, dimension, value, options);
|
|
269
|
+
entries = { theme: value };
|
|
270
|
+
if (options.bundle !== false) {
|
|
271
|
+
// The gallery's theme snippet writes the whole bundle, so an explicit deck choice
|
|
272
|
+
// does not keep the previous theme's fonts, colors or background.
|
|
273
|
+
for (const key of ["colorScheme", "fontScheme", "background", "dimensions"]) if (record[key] !== undefined) entries[key] = record[key];
|
|
274
|
+
}
|
|
275
|
+
patches = catalogRecordPatches(document, dimension, options);
|
|
276
|
+
} else if (dimension === "color-schemes" || dimension === "font-schemes") {
|
|
277
|
+
requireCatalogId(document, dimension, value, options);
|
|
278
|
+
entries = { [DESIGN_KEYS[dimension][0]]: value };
|
|
279
|
+
patches = catalogRecordPatches(document, dimension, options);
|
|
280
|
+
} else {
|
|
281
|
+
// A background is an object or a shorthand string (theme slot or hex color); the schema
|
|
282
|
+
// validates the candidate document, so a string that is neither is rejected below.
|
|
283
|
+
const valid = dimension === "backgrounds" ? typeof value === "string" || (Boolean(value) && typeof value === "object" && !Array.isArray(value)) : Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
284
|
+
if (!valid)
|
|
285
|
+
throw fail("invalid-switch-value", `Switch ${dimension} to ${dimension === "backgrounds" ? "a background object or shorthand string" : "an object with " + DESIGN_KEYS[dimension].join(" and ")}.`, { value });
|
|
286
|
+
if (dimension === "backgrounds") entries = { background: value };
|
|
287
|
+
else {
|
|
288
|
+
const unknown = Object.keys(value).filter((key) => !DESIGN_KEYS[dimension].includes(key));
|
|
289
|
+
if (unknown.length) throw fail("invalid-switch-value", `Unknown ${dimension} field: ${unknown[0]}.`, { value });
|
|
290
|
+
entries = Object.fromEntries(DESIGN_KEYS[dimension].filter((key) => value[key] !== undefined).map((key) => [key, value[key]]));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
let base = [];
|
|
294
|
+
if (scopeIndex !== undefined) {
|
|
295
|
+
slideAt(document, scopeIndex, `a ${dimension} switch`);
|
|
296
|
+
base = ["slides", String(scopeIndex)];
|
|
297
|
+
scope = "slide";
|
|
298
|
+
slideIndex = scopeIndex;
|
|
299
|
+
}
|
|
300
|
+
patches = [...patches, ...designPatches(document, base, entries)];
|
|
301
|
+
if (scope === "deck") {
|
|
302
|
+
const keys = Object.entries(entries).filter(([, entry]) => entry !== undefined).map(([key]) => key);
|
|
303
|
+
shadowed = shadowedSlides(document, keys);
|
|
304
|
+
if (options.clearSlideOverrides)
|
|
305
|
+
for (const index of shadowed)
|
|
306
|
+
for (const key of keys)
|
|
307
|
+
if (document.slides[index].design?.[key] !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", key]) });
|
|
308
|
+
if (options.clearSlideOverrides) shadowed = [];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const next = patches.length ? applyJsonPatch(document, patches) : document;
|
|
313
|
+
if (patches.length) {
|
|
314
|
+
const validation = validateOpfDocument(next);
|
|
315
|
+
if (!validation.valid && before.valid)
|
|
316
|
+
throw fail("invalid-opf-edit", validation.errors[0]?.message ?? "This switch produces an invalid document.", { issues: validation.errors, patches });
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
dimension,
|
|
320
|
+
scope,
|
|
321
|
+
...(slideIndex !== undefined ? { slideIndex } : {}),
|
|
322
|
+
document: structuredClone(next),
|
|
323
|
+
patches,
|
|
324
|
+
changed: patches.length > 0,
|
|
325
|
+
shadowed,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Switch one dimension of the session's document as a single undoable transaction. Returns the
|
|
331
|
+
* editor change plus `dimension`, `scope`, `changed` and `shadowed` (slides whose own design
|
|
332
|
+
* hides a deck-level switch). Switching to the current value commits nothing.
|
|
333
|
+
*/
|
|
334
|
+
export function switchDimension(editor, dimension, value, options = {}) {
|
|
335
|
+
if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function")
|
|
336
|
+
throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
337
|
+
const { meta, ...switchOptions } = options;
|
|
338
|
+
const prepared = prepareDimensionSwitch(editor.document, dimension, value, switchOptions);
|
|
339
|
+
const { document, patches, ...summary } = prepared;
|
|
340
|
+
void document;
|
|
341
|
+
if (!prepared.changed) return { ...summary, document: editor.document, patches: [], inversePatches: [], validation: editor.validation };
|
|
342
|
+
const change = editor.applyPatch(patches, { ...meta, source: meta?.source ?? "dimension-switch", dimension, scope: prepared.scope });
|
|
343
|
+
return { ...change, ...summary };
|
|
344
|
+
}
|
package/dist/transfer.js
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
validateOpfDocument,
|
|
6
6
|
} from "./index.js";
|
|
7
7
|
import { catalogs } from "@openpresentation/opf";
|
|
8
|
+
import { collectReservedPresentationIds, remapSlideTreeIds } from "./presentation-ids.js";
|
|
9
|
+
import { DEFAULT_FONT_SCHEME } from "./font-defaults.js";
|
|
8
10
|
export const MAX_OPF_BYTES = 20 * 1024 * 1024;
|
|
9
11
|
const clone = (value) => structuredClone(value);
|
|
10
12
|
export function assertOpf(document) {
|
|
@@ -135,7 +137,7 @@ export function prepareOpfImport(
|
|
|
135
137
|
...design,
|
|
136
138
|
theme: design.theme ?? "minimal",
|
|
137
139
|
colorScheme: design.colorScheme ?? theme.colorScheme ?? "cool-horizon",
|
|
138
|
-
fontScheme: design.fontScheme ?? theme.fontScheme ??
|
|
140
|
+
fontScheme: design.fontScheme ?? theme.fontScheme ?? DEFAULT_FONT_SCHEME,
|
|
139
141
|
dimensions: design.dimensions ?? theme.dimensions ?? "widescreen",
|
|
140
142
|
background: design.background ?? theme.background ?? "#FFFFFF",
|
|
141
143
|
header: design.header ?? false,
|
|
@@ -241,14 +243,8 @@ export function prepareOpfImport(
|
|
|
241
243
|
? `asset:${assetMap.get(asset.slice(6))}`
|
|
242
244
|
: asset;
|
|
243
245
|
}
|
|
244
|
-
const ids = new Set(document
|
|
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
|
-
}
|
|
246
|
+
const ids = new Set(collectReservedPresentationIds(document));
|
|
247
|
+
for (const slide of rewritten.slides) remapSlideTreeIds(slide, ids);
|
|
252
248
|
const index = Math.max(0, Math.min(document.slides.length, slideIndex + 1));
|
|
253
249
|
document.slides.splice(index, 0, ...rewritten.slides);
|
|
254
250
|
assertOpf(document);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -77,20 +77,27 @@
|
|
|
77
77
|
"./json-options": {
|
|
78
78
|
"types": "./dist/json-options.d.ts",
|
|
79
79
|
"default": "./dist/json-options.js"
|
|
80
|
+
},
|
|
81
|
+
"./switches": {
|
|
82
|
+
"types": "./dist/switches.d.ts",
|
|
83
|
+
"default": "./dist/switches.js"
|
|
80
84
|
}
|
|
81
85
|
},
|
|
82
86
|
"types": "./dist/index.d.ts",
|
|
83
87
|
"sideEffects": false,
|
|
84
88
|
"scripts": {
|
|
85
89
|
"build": "node scripts/build.mjs",
|
|
86
|
-
"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 && node --check test/rich-table-browser.mjs && node --check test/styled-table.mjs && node --check test/styled-table-browser.mjs && node --check examples/pptx-controls.js && node --check scripts/build-playground.mjs && node --check test/playground-pptx.mjs && node --check src/text-input.js && node --check src/rich-text.js && node --check src/rich-text-input.js && node --check test/rich-text.mjs && node --check test/rich-input-browser.mjs && node --check src/json-editor.js && node --check src/json-menu.js && node --check src/json-options.js && node --check src/json-source.js",
|
|
87
|
-
"test": "npm run build && node test/accepted-text.mjs && 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 && node test/styled-table.mjs && node test/json-source.mjs && node test/json-options.mjs",
|
|
90
|
+
"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/presentation-ids.js && node --check src/font-defaults.js && node --check src/color-authoring.js && node --check test/presentation-ids.mjs && node --check src/galleries.js && node --check examples/transfer-controls.js && node --check src/schema.js && node --check src/schema-inspector.js && node --check test/rich-table-browser.mjs && node --check test/styled-table.mjs && node --check test/styled-table-browser.mjs && node --check examples/pptx-controls.js && node --check scripts/build-playground.mjs && node --check test/playground-pptx.mjs && node --check test/playground-script-fonts.mjs && node --check test/playground-lazy-fonts.mjs && node --check src/text-input.js && node --check src/rich-text.js && node --check src/rich-text-input.js && node --check src/rich-text-toolbar.js && node --check test/rich-text.mjs && node --check test/rich-input-browser.mjs && node --check src/json-editor.js && node --check src/json-menu.js && node --check src/json-options.js && node --check src/json-source.js && node --check src/switches.js && node --check test/switches.mjs && node --check test/switches-export.mjs && node --check test/placeholder-selection.mjs && node --check test/selection-browser.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs && node --check src/exact-source.js && node --check test/exact-source.mjs && node --check test/playground-source.mjs",
|
|
91
|
+
"test": "npm run build && npm run check:font-hotlinks && node test/accepted-text.mjs && node test/smoke.mjs && node test/component-smoke.mjs && node test/canvas-fields.mjs && node test/transfer.mjs && node test/gallery-font-roles.mjs && node test/default-font-scheme.mjs && node test/presentation-ids.mjs && node test/schema.mjs && node test/rich-text.mjs && node test/layout.mjs && node test/blocks.mjs && node test/styled-table.mjs && node test/json-source.mjs && node test/json-options.mjs && node test/switches.mjs && node test/switches-export.mjs && node test/placeholder-selection.mjs && node test/exact-source.mjs",
|
|
88
92
|
"validate": "node scripts/validate-package.mjs",
|
|
89
93
|
"test:packed": "node test/packed-install.mjs",
|
|
90
94
|
"prepack": "npm run build",
|
|
95
|
+
"check:font-hotlinks": "node --test scripts/check-font-hotlinks.test.mjs && node scripts/check-font-hotlinks.mjs",
|
|
96
|
+
"check:font-hotlinks:built": "node scripts/check-font-hotlinks.mjs --built dist artifacts/playground",
|
|
91
97
|
"build:playground": "node scripts/build-playground.mjs",
|
|
92
|
-
"test:playground": "npm run build:playground && node test/playground-pptx.mjs",
|
|
98
|
+
"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-source.mjs",
|
|
93
99
|
"test:code-browser": "node test/code-browser.mjs",
|
|
100
|
+
"test:selection-browser": "node test/selection-browser.mjs",
|
|
94
101
|
"test:metric-browser": "node test/metric-browser.mjs",
|
|
95
102
|
"test:rich-input-browser": "node test/rich-input-browser.mjs",
|
|
96
103
|
"test:json": "npm run build && node test/json-source.mjs && node test/json-options.mjs",
|
|
@@ -98,19 +105,19 @@
|
|
|
98
105
|
},
|
|
99
106
|
"dependencies": {
|
|
100
107
|
"@codemirror/autocomplete": "6.20.3",
|
|
101
|
-
"@codemirror/commands": "6.11.
|
|
108
|
+
"@codemirror/commands": "6.11.1",
|
|
102
109
|
"@codemirror/lang-json": "6.0.2",
|
|
103
110
|
"@codemirror/language": "6.12.4",
|
|
104
111
|
"@codemirror/lint": "6.9.7",
|
|
105
112
|
"@codemirror/search": "6.7.2",
|
|
106
|
-
"@codemirror/state": "6.7.
|
|
107
|
-
"@codemirror/view": "6.43.
|
|
108
|
-
"@lezer/highlight": "1.2.
|
|
109
|
-
"@openpresentation/opf": "^0.
|
|
113
|
+
"@codemirror/state": "6.7.6",
|
|
114
|
+
"@codemirror/view": "6.43.13",
|
|
115
|
+
"@lezer/highlight": "1.2.4",
|
|
116
|
+
"@openpresentation/opf": "^0.11.1",
|
|
110
117
|
"jsonc-parser": "3.3.1"
|
|
111
118
|
},
|
|
112
119
|
"peerDependencies": {
|
|
113
|
-
"@openpresentation/opf-render": "^0.
|
|
120
|
+
"@openpresentation/opf-render": "^0.10.0"
|
|
114
121
|
},
|
|
115
122
|
"peerDependenciesMeta": {
|
|
116
123
|
"@openpresentation/opf-render": {
|
|
@@ -118,8 +125,39 @@
|
|
|
118
125
|
}
|
|
119
126
|
},
|
|
120
127
|
"devDependencies": {
|
|
121
|
-
"@
|
|
122
|
-
"@
|
|
128
|
+
"@expo-google-fonts/noto-naskh-arabic": "0.4.5",
|
|
129
|
+
"@expo-google-fonts/noto-nastaliq-urdu": "0.4.2",
|
|
130
|
+
"@expo-google-fonts/noto-sans": "0.4.2",
|
|
131
|
+
"@expo-google-fonts/noto-sans-arabic": "0.4.3",
|
|
132
|
+
"@expo-google-fonts/noto-sans-armenian": "0.4.2",
|
|
133
|
+
"@expo-google-fonts/noto-sans-bengali": "0.4.4",
|
|
134
|
+
"@expo-google-fonts/noto-sans-devanagari": "0.4.1",
|
|
135
|
+
"@expo-google-fonts/noto-sans-ethiopic": "0.4.2",
|
|
136
|
+
"@expo-google-fonts/noto-sans-georgian": "0.4.3",
|
|
137
|
+
"@expo-google-fonts/noto-sans-gujarati": "0.4.2",
|
|
138
|
+
"@expo-google-fonts/noto-sans-gurmukhi": "0.4.1",
|
|
139
|
+
"@expo-google-fonts/noto-sans-hebrew": "0.4.1",
|
|
140
|
+
"@expo-google-fonts/noto-sans-jp": "0.4.3",
|
|
141
|
+
"@expo-google-fonts/noto-sans-kannada": "0.4.3",
|
|
142
|
+
"@expo-google-fonts/noto-sans-khmer": "0.4.3",
|
|
143
|
+
"@expo-google-fonts/noto-sans-kr": "0.4.3",
|
|
144
|
+
"@expo-google-fonts/noto-sans-lao": "0.4.2",
|
|
145
|
+
"@expo-google-fonts/noto-sans-malayalam": "0.4.2",
|
|
146
|
+
"@expo-google-fonts/noto-sans-mongolian": "0.4.1",
|
|
147
|
+
"@expo-google-fonts/noto-sans-myanmar": "0.4.2",
|
|
148
|
+
"@expo-google-fonts/noto-sans-oriya": "0.4.2",
|
|
149
|
+
"@expo-google-fonts/noto-sans-sc": "0.4.3",
|
|
150
|
+
"@expo-google-fonts/noto-sans-sinhala": "0.4.2",
|
|
151
|
+
"@expo-google-fonts/noto-sans-syriac": "0.4.2",
|
|
152
|
+
"@expo-google-fonts/noto-sans-tamil": "0.4.3",
|
|
153
|
+
"@expo-google-fonts/noto-sans-tc": "0.4.3",
|
|
154
|
+
"@expo-google-fonts/noto-sans-telugu": "0.4.2",
|
|
155
|
+
"@expo-google-fonts/noto-sans-thaana": "0.4.2",
|
|
156
|
+
"@expo-google-fonts/noto-sans-thai": "0.4.2",
|
|
157
|
+
"@expo-google-fonts/noto-serif-hebrew": "0.4.2",
|
|
158
|
+
"@expo-google-fonts/noto-serif-tibetan": "0.4.2",
|
|
159
|
+
"@openpresentation/opf-pptx": "^0.10.0",
|
|
160
|
+
"@openpresentation/opf-render": "^0.10.0",
|
|
123
161
|
"esbuild": "0.28.2",
|
|
124
162
|
"playwright": "1.63.0"
|
|
125
163
|
}
|