@openpresentation/opf-editor 0.12.1 → 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 +122 -80
- package/dist/annotations.d.ts +16 -16
- package/dist/annotations.js +71 -79
- package/dist/assets.d.ts +7 -7
- package/dist/assets.js +16 -16
- package/dist/background-options.d.ts +12 -5
- package/dist/background-options.js +44 -19
- 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 +30 -31
- package/dist/canvas.js +64 -68
- package/dist/catalogs.d.ts +48 -0
- package/dist/catalogs.js +217 -0
- 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 +23 -0
- package/dist/color-authoring.js +1 -1
- package/dist/content-actions.d.ts +17 -17
- package/dist/content-actions.js +74 -73
- package/dist/content-controls.js +21 -21
- 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.d.ts +12 -5
- package/dist/design-controls.js +312 -111
- package/dist/design-options.d.ts +20 -18
- package/dist/design-options.js +91 -99
- package/dist/edit-helpers.js +8 -7
- package/dist/exact-source.js +5 -5
- package/dist/export.d.ts +7 -7
- package/dist/export.js +48 -17
- 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 +50 -101
- 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 +28 -29
- package/dist/image-cropper.js +3 -3
- package/dist/image-options.d.ts +45 -0
- package/dist/image-options.js +195 -0
- package/dist/index.d.ts +48 -31
- package/dist/index.js +154 -126
- 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 +44 -54
- 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 +160 -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 +26 -6
- package/dist/slide-manager.d.ts +2 -2
- package/dist/slide-manager.js +19 -19
- package/dist/slides.d.ts +21 -21
- package/dist/slides.js +104 -101
- package/dist/svelte.js +2 -3
- package/dist/switches.d.ts +37 -22
- package/dist/switches.js +282 -171
- 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 +15 -4
- package/dist/transfer.js +107 -161
- package/package.json +12 -6
- package/dist/font-defaults.js +0 -25
package/dist/switches.js
CHANGED
|
@@ -2,30 +2,34 @@
|
|
|
2
2
|
// dimension that turns "switch this dimension to X" into a validated JSON Patch, and applies
|
|
3
3
|
// it to an editor session as one undoable transaction. The preview recomposes and the PPTX
|
|
4
4
|
// export follows from the same document, so no dimension needs a special refresh path.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
validateOpfDocument,
|
|
15
|
-
} from "./index.js";
|
|
5
|
+
//
|
|
6
|
+
// OPF 0.15 (FA-23): the catalog-backed dimensions (layouts, themes, colour and font schemes, narratives, audiences, purposes,
|
|
7
|
+
// tones) resolve through core with the host's registered catalogs (`options.catalogs`, `Catalog[]`) and write the reference core
|
|
8
|
+
// gives (`id` or `name:id`). Languages, chart types and social platforms are engine vocabularies, checked against core's
|
|
9
|
+
// tables; their labels come from the host's display metadata (`options.vocabularies`, the shape of `catalogDisplay` from
|
|
10
|
+
// `@openpresentation/opf/catalog`), never from data the editor ships.
|
|
11
|
+
import { CHART_TYPES, LANGUAGES, SOCIAL_PLATFORMS, isXYChartType, parseReference, resolveChartData, resolveReference } from "@openpresentation/opf";
|
|
12
|
+
import { chartOptionTarget } from "@openpresentation/opf/composition";
|
|
13
|
+
import { createValuePatch, applyJsonPatch, getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
|
|
16
14
|
import { createContentBlock, prepareBlockReplace } from "./blocks.js";
|
|
17
15
|
import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
|
|
18
16
|
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
19
17
|
import { blockConversionTargets, prepareBlockConversion } from "./block-convert.js";
|
|
18
|
+
import { checkFormat } from "./checks.js";
|
|
19
|
+
import { listCatalogRecords, mergeCatalogs } from "./catalogs.js";
|
|
20
|
+
import { IMAGE_TREATMENT_FIELDS, imageTreatmentPatches, readImageTreatments } from "./image-options.js";
|
|
20
21
|
|
|
21
|
-
/** The schema's DimensionPreset values (RR-41), the values of the slide-sizes switch. */
|
|
22
|
-
export const SLIDE_SIZE_PRESETS = Object.freeze(["16:9", "4:3", "16:10", "letter", "a4", "widescreen", "standard"]);
|
|
22
|
+
/** The schema's DimensionPreset values (RR-41, FA-13: with the social-feed ratios), the values of the slide-sizes switch. */
|
|
23
|
+
export const SLIDE_SIZE_PRESETS = Object.freeze(["16:9", "4:3", "16:10", "1:1", "4:5", "9:16", "letter", "a4", "widescreen", "standard"]);
|
|
23
24
|
|
|
24
25
|
// Slide size in inches per preset, as core's resolveCanvasDimensions composes it (for picker labels).
|
|
25
26
|
const SLIDE_SIZE_LABELS = Object.freeze({
|
|
26
27
|
"16:9": "16:9 (13.33 x 7.5 in)",
|
|
27
28
|
"4:3": "4:3 (10 x 7.5 in)",
|
|
28
29
|
"16:10": "16:10 (10 x 6.25 in)",
|
|
30
|
+
"1:1": "1:1 square (7.5 x 7.5 in)",
|
|
31
|
+
"4:5": "4:5 portrait (7.5 x 9.375 in)",
|
|
32
|
+
"9:16": "9:16 portrait (7.5 x 13.33 in)",
|
|
29
33
|
letter: "Letter (11 x 8.5 in)",
|
|
30
34
|
a4: "A4 (11.69 x 8.27 in)",
|
|
31
35
|
widescreen: "Widescreen (13.33 x 7.5 in)",
|
|
@@ -56,20 +60,19 @@ export const SWITCH_DIMENSIONS = Object.freeze([
|
|
|
56
60
|
"purposes",
|
|
57
61
|
]);
|
|
58
62
|
|
|
59
|
-
// Catalog kind behind each catalog-backed dimension.
|
|
63
|
+
// Catalog kind behind each catalog-backed dimension (core's content kinds).
|
|
60
64
|
const CATALOG_KIND = Object.freeze({
|
|
61
65
|
layouts: "layouts",
|
|
62
66
|
"color-schemes": "colorSchemes",
|
|
63
67
|
"font-schemes": "fontSchemes",
|
|
64
|
-
languages: "languages",
|
|
65
68
|
narratives: "narratives",
|
|
66
69
|
themes: "themes",
|
|
67
70
|
audiences: "audiences",
|
|
68
71
|
tones: "tones",
|
|
69
|
-
socials: "socialPlatforms",
|
|
70
|
-
charts: "chartTypes",
|
|
71
72
|
purposes: "purposes",
|
|
72
73
|
});
|
|
74
|
+
// Display metadata key (`options.vocabularies`) behind each engine-vocabulary dimension.
|
|
75
|
+
const VOCABULARY = Object.freeze({ languages: "languages", charts: "chartTypes", socials: "socialPlatforms" });
|
|
73
76
|
// Top-level document field for the simple metadata dimensions.
|
|
74
77
|
const ROOT_FIELD = Object.freeze({ languages: "language", narratives: "narrative", tones: "tone", audiences: "audience" });
|
|
75
78
|
// `design` keys for the design dimensions. A key set to `null` in the value is removed.
|
|
@@ -78,67 +81,98 @@ const DESIGN_KEYS = Object.freeze({
|
|
|
78
81
|
"font-schemes": ["fontScheme"],
|
|
79
82
|
backgrounds: ["background"],
|
|
80
83
|
"headers-footers": ["header", "footer"],
|
|
81
|
-
"image-treatments": ["slideImage", "imageFill"],
|
|
82
84
|
});
|
|
83
85
|
const REGION = /^(?:(?:top|middle|bottom)(?:\+(?:top|middle|bottom))*(?::(?:left|center|right)(?:\+(?:left|center|right))*)?|(?:left|center|right)(?:\+(?:left|center|right))*)$/;
|
|
84
86
|
const BLOCK_KINDS = ["text", "list", "chart", "table", "metric", "quote", "code", "timeline", "group", "image", "video"];
|
|
87
|
+
const RESERVED_GROUPS = new Set(["default", "custom"]);
|
|
88
|
+
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
const catalogsOf = (options) => mergeCatalogs(options.catalogs);
|
|
91
|
+
|
|
92
|
+
// A reference resolved the one way every engine resolves it (core's `resolveReference`, with the registered catalogs).
|
|
93
|
+
function resolveRecord(presentation, kind, reference, options) {
|
|
94
|
+
if (typeof reference !== "string" || !reference) return undefined;
|
|
95
|
+
return resolveReference(presentation, kind, reference, { catalogs: catalogsOf(options) });
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
/**
|
|
99
|
+
* A record the caller supplies with the switch (a gallery item carries its own): it is embedded in the same transaction, in
|
|
100
|
+
* the group whose `source` is `options.recordSource` (`default` when the document has no default group yet), else under `custom`,
|
|
101
|
+
* with no `$schema`, `id` or `x-*` display fields. Returns the patches and the reference to write.
|
|
102
|
+
*/
|
|
103
|
+
function suppliedRecord(presentation, kind, options) {
|
|
104
|
+
const record = options.record;
|
|
105
|
+
if (!record) return undefined;
|
|
106
|
+
const id = record.id;
|
|
107
|
+
const groups = isObject(presentation.catalogs) ? presentation.catalogs : undefined;
|
|
108
|
+
let group = "custom";
|
|
109
|
+
if (typeof options.recordSource === "string" && options.recordSource) {
|
|
110
|
+
const named = groups && Object.entries(groups).find(([, value]) => isObject(value) && value.source === options.recordSource);
|
|
111
|
+
if (named) group = named[0];
|
|
112
|
+
else if (!groups || groups.default === undefined) group = "default";
|
|
113
|
+
}
|
|
114
|
+
const reference = RESERVED_GROUPS.has(group) ? id : `${group}:${id}`;
|
|
115
|
+
const { $schema: _schema, id: _id, ...fields } = record;
|
|
116
|
+
const value = Object.fromEntries(Object.entries(structuredClone(fields)).filter(([key]) => !key.startsWith("x-")));
|
|
117
|
+
if (isObject(groups?.[group]?.[kind]) && Object.hasOwn(groups[group][kind], id)) return { patches: [], reference, record: groups[group][kind][id], group };
|
|
118
|
+
const path = ["catalogs", group, kind, id];
|
|
119
|
+
let patches;
|
|
120
|
+
if (!groups) patches = [{ op: "add", path: "/catalogs", value: { [group]: { ...(group === "default" ? { source: options.recordSource } : {}), [kind]: { [id]: value } } } }];
|
|
121
|
+
else if (!isObject(groups[group])) patches = [{ op: "add", path: opfPathToJsonPointer(path.slice(0, 2)), value: { ...(group === "default" ? { source: options.recordSource } : {}), [kind]: { [id]: value } } }];
|
|
122
|
+
else if (!isObject(groups[group][kind])) patches = [{ op: "add", path: opfPathToJsonPointer(path.slice(0, 3)), value: { [id]: value } }];
|
|
123
|
+
else patches = [{ op: "add", path: opfPathToJsonPointer(path), value }];
|
|
124
|
+
return { patches, reference, record: value, group };
|
|
100
125
|
}
|
|
101
126
|
|
|
102
|
-
|
|
127
|
+
// The record a switched reference names: a supplied record, else core's resolution. Unknown references are refused.
|
|
128
|
+
function requireRecord(presentation, dimension, reference, options) {
|
|
103
129
|
const kind = CATALOG_KIND[dimension];
|
|
104
|
-
if (typeof
|
|
105
|
-
throw fail("invalid-catalog-id", `Switch ${dimension} to a non-empty catalog
|
|
106
|
-
const
|
|
107
|
-
if (!
|
|
108
|
-
return
|
|
130
|
+
if (typeof reference !== "string" || reference.length === 0)
|
|
131
|
+
throw fail("invalid-catalog-id", `Switch ${dimension} to a non-empty catalog reference.`, { dimension, id: reference });
|
|
132
|
+
const resolved = resolveRecord(presentation, kind, reference, options);
|
|
133
|
+
if (!resolved) throw fail("unknown-catalog-id", `Unknown ${kind} catalog reference: ${reference}. Register the catalog that holds it, or add the record to the document.`, { dimension, catalogKind: kind, id: reference });
|
|
134
|
+
return resolved;
|
|
109
135
|
}
|
|
110
136
|
|
|
111
|
-
// A
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
137
|
+
// A switch to a supplied record writes the reference it was embedded under; otherwise the reference must resolve.
|
|
138
|
+
function catalogChoice(presentation, dimension, reference, options) {
|
|
139
|
+
const supplied = options.record && parseReference(reference)?.id === options.record.id ? suppliedRecord(presentation, CATALOG_KIND[dimension], options) : undefined;
|
|
140
|
+
if (supplied) return { patches: supplied.patches, reference: supplied.reference, record: supplied.record, group: supplied.group };
|
|
141
|
+
const resolved = requireRecord(presentation, dimension, reference, options);
|
|
142
|
+
return { patches: [], reference, record: resolved.record, group: resolved.group };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// A reference copied out of a record of group `group` into the document's design keeps naming that group's record.
|
|
146
|
+
function qualify(reference, group) {
|
|
147
|
+
if (typeof reference !== "string") return reference;
|
|
148
|
+
const parsed = parseReference(reference);
|
|
149
|
+
if (!parsed || parsed.group || RESERVED_GROUPS.has(group)) return reference;
|
|
150
|
+
return `${group}:${parsed.id}`;
|
|
151
|
+
}
|
|
152
|
+
function qualifiedValue(key, value, group) {
|
|
153
|
+
if (key === "colorScheme" || key === "fontScheme") {
|
|
154
|
+
if (typeof value === "string") return qualify(value, group);
|
|
155
|
+
if (isObject(value) && typeof value.id === "string") return { ...structuredClone(value), id: qualify(value.id, group) };
|
|
156
|
+
}
|
|
157
|
+
return structuredClone(value);
|
|
124
158
|
}
|
|
125
159
|
|
|
126
|
-
function slideAt(
|
|
127
|
-
if (!Number.isInteger(slideIndex) || !
|
|
160
|
+
function slideAt(presentation, slideIndex, what = "this switch") {
|
|
161
|
+
if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex])
|
|
128
162
|
throw fail("slide-index-out-of-range", `Choose an existing slide for ${what}.`, { slideIndex });
|
|
129
|
-
return
|
|
163
|
+
return presentation.slides[slideIndex];
|
|
130
164
|
}
|
|
131
165
|
|
|
132
|
-
function rootPatch(
|
|
133
|
-
return same(
|
|
166
|
+
function rootPatch(presentation, field, value) {
|
|
167
|
+
return same(presentation[field], value) ? [] : createValuePatch(presentation, [field], value);
|
|
134
168
|
}
|
|
135
169
|
|
|
136
170
|
// A deck-level switch does nothing for a slide that carries its own value for that key.
|
|
137
|
-
function shadowedSlides(
|
|
138
|
-
return (
|
|
171
|
+
function shadowedSlides(presentation, keys) {
|
|
172
|
+
return (presentation.slides ?? []).flatMap((slide, index) => (keys.some((key) => slide?.design?.[key] !== undefined) ? [index] : []));
|
|
139
173
|
}
|
|
140
174
|
|
|
141
|
-
function findChartOwner(
|
|
175
|
+
function findChartOwner(presentation, slideIndex) {
|
|
142
176
|
const visit = (node, path, depth) => {
|
|
143
177
|
if (!node || typeof node !== "object" || Array.isArray(node) || depth > 32) return undefined;
|
|
144
178
|
if (node.chart && typeof node.chart === "object") return path;
|
|
@@ -154,28 +188,34 @@ function findChartOwner(document, slideIndex) {
|
|
|
154
188
|
}
|
|
155
189
|
return undefined;
|
|
156
190
|
};
|
|
157
|
-
return visit(
|
|
191
|
+
return visit(presentation.slides[slideIndex], ["slides", String(slideIndex)], 0);
|
|
158
192
|
}
|
|
159
193
|
|
|
160
|
-
function socialsPatches(
|
|
194
|
+
function socialsPatches(presentation, value, options) {
|
|
161
195
|
const owner = options.owner ?? "speaker";
|
|
162
196
|
if (!["speaker", "organization"].includes(owner)) throw fail("invalid-switch-value", "Socials owner must be 'speaker' or 'organization'.", { owner });
|
|
163
197
|
if (!value || typeof value !== "object" || typeof value.platform !== "string" || typeof value.handle !== "string" || !value.handle)
|
|
164
198
|
throw fail("invalid-switch-value", "Switch socials to { platform, handle } with a non-empty handle.", { value });
|
|
165
|
-
|
|
166
|
-
|
|
199
|
+
if (!Object.hasOwn(SOCIAL_PLATFORMS, value.platform))
|
|
200
|
+
throw fail("unknown-catalog-id", `Unknown social platform: ${value.platform}. Use one of ${Object.keys(SOCIAL_PLATFORMS).join(", ")}.`, { dimension: "socials", id: value.platform });
|
|
201
|
+
const host = presentation[owner];
|
|
167
202
|
const index = Array.isArray(host) ? (options.index ?? 0) : undefined;
|
|
168
203
|
const target = Array.isArray(host) ? host[index] : host;
|
|
169
204
|
if (!target || typeof target !== "object")
|
|
170
205
|
throw fail("missing-owner", `Add the ${owner} to the document before setting their socials.`, { owner });
|
|
171
206
|
const base = Array.isArray(host) ? [owner, String(index)] : [owner];
|
|
172
207
|
const socials = target.socials;
|
|
173
|
-
const patches =
|
|
208
|
+
const patches = [];
|
|
174
209
|
if (!socials || typeof socials !== "object") return [...patches, { op: "add", path: opfPathToJsonPointer([...base, "socials"]), value: { [value.platform]: value.handle } }];
|
|
175
210
|
if (socials[value.platform] === value.handle) return patches;
|
|
176
211
|
return [...patches, { op: Object.hasOwn(socials, value.platform) ? "replace" : "add", path: opfPathToJsonPointer([...base, "socials", value.platform]), value: value.handle }];
|
|
177
212
|
}
|
|
178
213
|
|
|
214
|
+
// Purposes take free goal text, so only a supplied record is embedded; any other string is written as it is.
|
|
215
|
+
function catalogChoiceIfSupplied(presentation, dimension, value, options) {
|
|
216
|
+
return options.record ? catalogChoice(presentation, dimension, value, options) : undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
179
219
|
function blockValue(value, options) {
|
|
180
220
|
if (typeof value === "string") {
|
|
181
221
|
if (!BLOCK_KINDS.includes(value)) throw fail("invalid-switch-value", `Unknown content block kind: ${value}.`, { value });
|
|
@@ -189,16 +229,16 @@ function blockValue(value, options) {
|
|
|
189
229
|
* Compute the patch that switches one dimension, without touching any session. The candidate
|
|
190
230
|
* document is validated; an invalid result throws unless the input was already invalid.
|
|
191
231
|
*/
|
|
192
|
-
export function prepareDimensionSwitch(
|
|
232
|
+
export function prepareDimensionSwitch(presentation, dimension, value, options = {}) {
|
|
193
233
|
if (!SWITCH_DIMENSIONS.includes(dimension))
|
|
194
234
|
throw fail("unknown-dimension", `Unknown dimension: ${dimension}. Use one of ${SWITCH_DIMENSIONS.join(", ")}.`, { dimension });
|
|
195
|
-
if (!
|
|
235
|
+
if (!presentation || typeof presentation !== "object") throw fail("invalid-input", "Switch requires an OPF document object.");
|
|
196
236
|
if (options.record) {
|
|
197
|
-
const ids = dimension === "audiences" ? [value].flat() :
|
|
198
|
-
if (!CATALOG_KIND[dimension] || !ids.includes(options.record.id))
|
|
199
|
-
throw fail("record-id-mismatch", `The supplied record id must equal a switched ${dimension} id.`, { dimension, id: options.record
|
|
237
|
+
const ids = (dimension === "audiences" ? [value].flat() : [value]).map((entry) => (typeof entry === "string" ? parseReference(entry)?.id : undefined));
|
|
238
|
+
if (!CATALOG_KIND[dimension] || typeof options.record?.id !== "string" || !ids.includes(options.record.id))
|
|
239
|
+
throw fail("record-id-mismatch", `The supplied record id must equal a switched ${dimension} id.`, { dimension, id: options.record?.id });
|
|
200
240
|
}
|
|
201
|
-
const before =
|
|
241
|
+
const before = checkFormat(presentation);
|
|
202
242
|
const scopeIndex = options.slideIndex;
|
|
203
243
|
let patches = [];
|
|
204
244
|
let scope = "deck";
|
|
@@ -210,30 +250,28 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
|
|
|
210
250
|
// A preset string; a custom size (design.dimensions as an object with inches) is replaced by it.
|
|
211
251
|
if (!SLIDE_SIZE_PRESETS.includes(value)) throw fail("invalid-switch-value", `Switch slide-sizes to one of ${SLIDE_SIZE_PRESETS.join(", ")}.`, { value });
|
|
212
252
|
if (scopeIndex !== undefined) throw fail("deck-scope-only", "A presentation has one slide size: switch slide-sizes for the deck, without slideIndex.", { slideIndex: scopeIndex });
|
|
213
|
-
const own =
|
|
253
|
+
const own = presentation.design?.dimensions;
|
|
214
254
|
// {preset} alone is the same size as the bare preset string.
|
|
215
255
|
const sameSize = own && typeof own === "object" && !Array.isArray(own) && own.preset === value && Object.keys(own).length === 1;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (options.clearSlideOverrides) {
|
|
219
|
-
for (const index of shadowed) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", "dimensions"]) });
|
|
220
|
-
shadowed = [];
|
|
221
|
-
}
|
|
256
|
+
// A slide's design cannot set dimensions (FA-07), so no slide can shadow the deck's size.
|
|
257
|
+
patches = sameSize ? [] : designPatches(presentation, [], { dimensions: value });
|
|
222
258
|
} else if (dimension === "purposes") {
|
|
223
|
-
// A catalog
|
|
259
|
+
// A catalog reference, free-form goal text (no reference check: any goal is valid) or an inline Purpose object; the schema validates it.
|
|
224
260
|
if (typeof value !== "string" && !(value && typeof value === "object" && !Array.isArray(value)))
|
|
225
|
-
throw fail("invalid-switch-value", "Switch purposes to a catalog
|
|
261
|
+
throw fail("invalid-switch-value", "Switch purposes to a catalog reference, a goal string or a purpose object.", { value });
|
|
226
262
|
if (typeof value === "string" && value.length === 0) throw fail("invalid-switch-value", "Switch purposes to a non-empty goal.", { value });
|
|
227
|
-
|
|
263
|
+
const supplied = typeof value === "string" ? catalogChoiceIfSupplied(presentation, dimension, value, options) : undefined;
|
|
264
|
+
patches = [...(supplied?.patches ?? []), ...rootPatch(presentation, "purpose", supplied?.reference ?? value)];
|
|
228
265
|
} else if (dimension === "layouts") {
|
|
229
266
|
slideIndex = scopeIndex;
|
|
230
|
-
slideAt(
|
|
267
|
+
slideAt(presentation, slideIndex, "a layout switch");
|
|
231
268
|
scope = "slide";
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
269
|
+
const choice = catalogChoice(presentation, dimension, value, options);
|
|
270
|
+
const record = choice.record;
|
|
271
|
+
patches = choice.patches;
|
|
272
|
+
const withRecord = patches.length ? applyJsonPatch(presentation, patches) : presentation;
|
|
273
|
+
if (withRecord.slides[slideIndex].layout !== choice.reference) {
|
|
274
|
+
const layoutOps = createValuePatch(withRecord, ["slides", String(slideIndex), "layout"], choice.reference);
|
|
237
275
|
const swapped = applyJsonPatch(withRecord, layoutOps);
|
|
238
276
|
const types = Array.isArray(record.placeholders) ? record.placeholders.map((placeholder) => placeholder.type) : undefined;
|
|
239
277
|
// Add the blank payloads the layout declares, as the JSON editor's layout choice does.
|
|
@@ -244,64 +282,93 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
|
|
|
244
282
|
}
|
|
245
283
|
} else if (dimension === "charts") {
|
|
246
284
|
slideIndex = scopeIndex;
|
|
247
|
-
slideAt(
|
|
285
|
+
slideAt(presentation, slideIndex, "a chart switch");
|
|
248
286
|
scope = "slide";
|
|
249
|
-
|
|
250
|
-
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(
|
|
287
|
+
if (typeof value !== "string" || !CHART_TYPES.includes(value)) throw fail("unknown-catalog-id", `Unknown chart type: ${value}. Use one of ${CHART_TYPES.join(", ")}.`, { dimension, id: value });
|
|
288
|
+
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(presentation, slideIndex);
|
|
251
289
|
if (options.path && (owner[0] !== "slides" || owner[1] !== String(slideIndex)))
|
|
252
290
|
throw fail("path-slide-mismatch", "options.path is not on the slide named by options.slideIndex.", { slideIndex, path: options.path });
|
|
253
|
-
const chart = owner && getValueAtPath(
|
|
291
|
+
const chart = owner && getValueAtPath(presentation, [...owner, "chart"]);
|
|
254
292
|
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 });
|
|
255
|
-
patches =
|
|
293
|
+
patches = chart.type === value ? [] : [...createValuePatch(presentation, [...owner, "chart", "type"], value), ...staleMappingPatches(presentation, owner, chart, value), ...staleComboPatches(presentation, owner, chart, value)];
|
|
256
294
|
} else if (dimension === "blocks") {
|
|
257
295
|
if (options.path === undefined) throw fail("missing-path", "Choose the block to replace with options.path.");
|
|
258
296
|
// convert: true moves the block's own text into the new kind (block-convert.js) instead of replacing it.
|
|
259
297
|
if (options.convert) {
|
|
260
298
|
if (typeof value !== "string") throw fail("invalid-switch-value", "Convert a block to a content kind name.", { value });
|
|
261
|
-
const conversion = prepareBlockConversion(
|
|
299
|
+
const conversion = prepareBlockConversion(presentation, options.path, value, options.conversion);
|
|
262
300
|
patches = conversion.changed ? conversion.patches : [];
|
|
263
301
|
conversionLoss = conversion.loss;
|
|
264
302
|
} else {
|
|
265
303
|
const block = blockValue(value, options);
|
|
266
|
-
const change = prepareBlockReplace(
|
|
304
|
+
const change = prepareBlockReplace(presentation, options.path, block);
|
|
267
305
|
patches = change.changed ? change.patches : [];
|
|
268
306
|
}
|
|
269
307
|
const parts = splitOpfPath(options.path);
|
|
270
308
|
if (parts[0] === "slides") slideIndex = Number(parts[1]);
|
|
271
309
|
scope = "block";
|
|
272
310
|
} else if (dimension === "socials") {
|
|
273
|
-
patches = socialsPatches(
|
|
311
|
+
patches = socialsPatches(presentation, value, options);
|
|
312
|
+
} else if (dimension === "image-treatments") {
|
|
313
|
+
// The treatments of one image block (FA-22): fit, focus, shape, corner radius, border, opacity, recolor, overlay, aspect ratio, placement.
|
|
314
|
+
if (options.path === undefined) throw fail("missing-path", "Choose the image block to treat with options.path.");
|
|
315
|
+
patches = imageTreatmentPatches(presentation, options.path, value);
|
|
316
|
+
const parts = splitOpfPath(options.path);
|
|
317
|
+
if (parts[0] === "slides") slideIndex = Number(parts[1]);
|
|
318
|
+
scope = "block";
|
|
319
|
+
} else if (dimension === "languages") {
|
|
320
|
+
// A BCP-47 tag (en-US) or a language object ({ bcp47, ... }); the schema checks the tag. Language catalog ids are gone.
|
|
321
|
+
if (!(typeof value === "string" && value) && !(isObject(value) && typeof value.bcp47 === "string"))
|
|
322
|
+
throw fail("invalid-switch-value", "Switch languages to a BCP-47 tag such as en-US, or a language object with bcp47.", { value });
|
|
323
|
+
patches = rootPatch(presentation, "language", value);
|
|
274
324
|
} else if (ROOT_FIELD[dimension]) {
|
|
275
325
|
const field = ROOT_FIELD[dimension];
|
|
276
|
-
let next
|
|
326
|
+
let next;
|
|
277
327
|
if (dimension === "audiences") {
|
|
278
|
-
|
|
279
|
-
if (!
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
328
|
+
const list = Array.isArray(value) ? value : [value];
|
|
329
|
+
if (!list.length) throw fail("invalid-catalog-id", "Switch audiences to at least one catalog reference.", { value });
|
|
330
|
+
next = [];
|
|
331
|
+
for (const reference of list) {
|
|
332
|
+
const choice = catalogChoice(applyJsonPatch(presentation, patches), dimension, reference, options);
|
|
333
|
+
patches = [...patches, ...choice.patches];
|
|
334
|
+
next.push(choice.reference);
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
const choice = catalogChoice(presentation, dimension, value, options);
|
|
338
|
+
patches = choice.patches;
|
|
339
|
+
next = choice.reference;
|
|
340
|
+
}
|
|
341
|
+
patches = [...patches, ...rootPatch(presentation, field, next)];
|
|
283
342
|
} else {
|
|
284
343
|
// Design dimensions: deck scope by default, or one slide with options.slideIndex.
|
|
285
344
|
let entries;
|
|
286
345
|
if (dimension === "themes") {
|
|
287
|
-
const
|
|
288
|
-
|
|
346
|
+
const choice = catalogChoice(presentation, dimension, value, options);
|
|
347
|
+
const record = choice.record ?? {};
|
|
348
|
+
entries = { theme: choice.reference };
|
|
289
349
|
if (options.bundle !== false) {
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
350
|
+
// An explicit theme choice brings the theme's own colour scheme, font scheme and background (and, for the deck, its
|
|
351
|
+
// slide size): an override of those at the same scope is removed so the theme's applies. Nothing is copied out of the
|
|
352
|
+
// record, except on a slide whose deck sets the key: there the theme's value is written on the slide (its references
|
|
353
|
+
// qualified with the theme's catalog group), because the deck's design would otherwise win over the slide's theme.
|
|
354
|
+
// A slide's design cannot set dimensions (a PPTX has one slide size), so only a deck-scope switch touches them.
|
|
355
|
+
for (const key of ["colorScheme", "fontScheme", "background", ...(scopeIndex === undefined ? ["dimensions"] : [])]) {
|
|
356
|
+
if (record[key] === undefined) continue;
|
|
357
|
+
entries[key] = scopeIndex !== undefined && presentation.design?.[key] !== undefined ? qualifiedValue(key, record[key], choice.group) : null;
|
|
358
|
+
}
|
|
293
359
|
}
|
|
294
|
-
patches =
|
|
360
|
+
patches = choice.patches;
|
|
295
361
|
} else if (dimension === "color-schemes" || dimension === "font-schemes") {
|
|
296
|
-
|
|
362
|
+
const choice = catalogChoice(presentation, dimension, value, options);
|
|
363
|
+
value = choice.reference;
|
|
297
364
|
entries = { [DESIGN_KEYS[dimension][0]]: value };
|
|
298
365
|
if (dimension === "font-schemes") {
|
|
299
366
|
// An accent font is its own choice, not part of the scheme being left: it stays (in the object form).
|
|
300
|
-
const scopeBase = scopeIndex !== undefined &&
|
|
301
|
-
const own = getValueAtPath(
|
|
367
|
+
const scopeBase = scopeIndex !== undefined && presentation.slides?.[scopeIndex] ? ["slides", String(scopeIndex)] : [];
|
|
368
|
+
const own = getValueAtPath(presentation, [...scopeBase, "design", "fontScheme"]);
|
|
302
369
|
if (own && typeof own === "object" && own.accent !== undefined) entries.fontScheme = { id: value, accent: structuredClone(own.accent) };
|
|
303
370
|
}
|
|
304
|
-
patches =
|
|
371
|
+
patches = choice.patches;
|
|
305
372
|
} else {
|
|
306
373
|
// A background is an object or a shorthand string (theme slot or hex color); the schema
|
|
307
374
|
// validates the candidate document, so a string that is neither is rejected below.
|
|
@@ -318,29 +385,29 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
|
|
|
318
385
|
}
|
|
319
386
|
let base = [];
|
|
320
387
|
if (scopeIndex !== undefined) {
|
|
321
|
-
slideAt(
|
|
388
|
+
slideAt(presentation, scopeIndex, `a ${dimension} switch`);
|
|
322
389
|
base = ["slides", String(scopeIndex)];
|
|
323
390
|
scope = "slide";
|
|
324
391
|
slideIndex = scopeIndex;
|
|
325
392
|
}
|
|
326
|
-
patches = [...patches, ...designPatches(
|
|
393
|
+
patches = [...patches, ...designPatches(presentation, base, entries)];
|
|
327
394
|
if (scope === "deck") {
|
|
328
395
|
const keys = Object.entries(entries).filter(([, entry]) => entry !== undefined).map(([key]) => key);
|
|
329
|
-
shadowed = shadowedSlides(
|
|
396
|
+
shadowed = shadowedSlides(presentation, keys);
|
|
330
397
|
if (options.clearSlideOverrides)
|
|
331
398
|
for (const index of shadowed)
|
|
332
399
|
for (const key of keys)
|
|
333
|
-
if (
|
|
400
|
+
if (presentation.slides[index].design?.[key] !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", key]) });
|
|
334
401
|
if (options.clearSlideOverrides) shadowed = [];
|
|
335
402
|
}
|
|
336
403
|
}
|
|
337
404
|
|
|
338
|
-
const next = checkedDocument(
|
|
405
|
+
const next = checkedDocument(presentation, patches, before);
|
|
339
406
|
return {
|
|
340
407
|
dimension,
|
|
341
408
|
scope,
|
|
342
409
|
...(slideIndex !== undefined ? { slideIndex } : {}),
|
|
343
|
-
|
|
410
|
+
presentation: structuredClone(next),
|
|
344
411
|
patches,
|
|
345
412
|
changed: patches.length > 0,
|
|
346
413
|
shadowed,
|
|
@@ -349,7 +416,8 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
|
|
|
349
416
|
}
|
|
350
417
|
|
|
351
418
|
/**
|
|
352
|
-
* Switch one dimension of the session's document as a single undoable transaction
|
|
419
|
+
* Switch one dimension of the session's document as a single undoable transaction, resolving references with the session's
|
|
420
|
+
* registered catalogs (and `options.catalogs`). Returns the
|
|
353
421
|
* editor change plus `dimension`, `scope`, `changed` and `shadowed` (slides whose own design
|
|
354
422
|
* hides a deck-level switch). Switching to the current value commits nothing.
|
|
355
423
|
*/
|
|
@@ -357,90 +425,130 @@ export function switchDimension(editor, dimension, value, options = {}) {
|
|
|
357
425
|
if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function")
|
|
358
426
|
throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
359
427
|
const { meta, ...switchOptions } = options;
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
428
|
+
// The session's registered catalogs first, then any the caller adds for this switch: the one list core resolves with.
|
|
429
|
+
const prepared = prepareDimensionSwitch(editor.presentation, dimension, value, { ...switchOptions, catalogs: mergeCatalogs(editor.catalogs, options.catalogs) });
|
|
430
|
+
const { presentation, patches, ...summary } = prepared;
|
|
431
|
+
void presentation;
|
|
432
|
+
if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
364
433
|
const change = editor.applyPatch(patches, { ...meta, source: meta?.source ?? "dimension-switch", dimension, scope: prepared.scope });
|
|
365
434
|
return { ...change, ...summary };
|
|
366
435
|
}
|
|
367
436
|
|
|
368
437
|
// --- options for pickers and current values ---------------------------------------------------
|
|
369
438
|
|
|
370
|
-
|
|
439
|
+
// Host display metadata for an engine vocabulary (`options.vocabularies[key]`, the shape of `catalogDisplay`): a list of records
|
|
440
|
+
// with `id` (or `bcp47` for a language), or an object keyed by id. Returns a Map id -> record.
|
|
441
|
+
function displayRecords(options, key) {
|
|
442
|
+
const source = options.vocabularies?.[key];
|
|
443
|
+
const out = new Map();
|
|
444
|
+
if (Array.isArray(source)) {
|
|
445
|
+
for (const record of source) if (isObject(record) && typeof (record.id ?? record.bcp47) === "string") out.set(record.id ?? record.bcp47, record);
|
|
446
|
+
} else if (isObject(source)) {
|
|
447
|
+
for (const [id, record] of Object.entries(source)) out.set(isObject(record) && typeof record.bcp47 === "string" ? record.bcp47 : id, isObject(record) ? record : {});
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
const displayLabel = (record, id) => (typeof record?.name === "string" && record.name ? record.name : typeof record?.label === "string" && record.label ? record.label : id);
|
|
371
452
|
|
|
372
453
|
/**
|
|
373
|
-
* The values a picker can offer for a catalog-backed dimension
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* `
|
|
454
|
+
* The values a picker can offer for a dimension. A catalog-backed dimension lists core's `catalogRecords` (the document's embedded
|
|
455
|
+
* records first, then `options.catalogs`'), each `{ id, label, record, reference, group, source, origin }` with `id` the reference
|
|
456
|
+
* to write. `charts` lists core's `CHART_TYPES` and `socials` core's `SOCIAL_PLATFORMS`, labelled from `options.vocabularies`;
|
|
457
|
+
* `languages` lists core's `LANGUAGES` tags. `blocks` lists the content
|
|
458
|
+
* kinds. Use `compatibleChartTypes` to narrow `charts` to the types the chart's data can use.
|
|
377
459
|
*/
|
|
378
|
-
export function listSwitchOptions(
|
|
460
|
+
export function listSwitchOptions(presentation, dimension, options = {}) {
|
|
379
461
|
if (dimension === "blocks") return BLOCK_KINDS.map((kind) => ({ id: kind, label: kind[0].toUpperCase() + kind.slice(1) }));
|
|
380
462
|
if (dimension === "slide-sizes") return SLIDE_SIZE_PRESETS.map((preset) => ({ id: preset, label: SLIDE_SIZE_LABELS[preset] }));
|
|
463
|
+
if (dimension === "charts") {
|
|
464
|
+
const display = displayRecords(options, VOCABULARY.charts);
|
|
465
|
+
return CHART_TYPES.map((id) => ({ id, label: displayLabel(display.get(id), id), record: display.get(id) ?? {} }));
|
|
466
|
+
}
|
|
467
|
+
if (dimension === "socials") {
|
|
468
|
+
const display = displayRecords(options, VOCABULARY.socials);
|
|
469
|
+
return Object.keys(SOCIAL_PLATFORMS).map((id) => ({ id, label: displayLabel(display.get(id), id), record: { ...SOCIAL_PLATFORMS[id], ...display.get(id) } }));
|
|
470
|
+
}
|
|
471
|
+
if (dimension === "languages") {
|
|
472
|
+
// Core's language table (the tags engines know the script, direction and default fonts of), labelled from the host's display metadata.
|
|
473
|
+
const display = displayRecords(options, VOCABULARY.languages);
|
|
474
|
+
return LANGUAGES.map((language) => ({ id: language.tag, label: displayLabel(display.get(language.tag), language.tag), record: { ...language, ...display.get(language.tag) } }));
|
|
475
|
+
}
|
|
381
476
|
const kind = CATALOG_KIND[dimension];
|
|
382
477
|
if (!kind) return [];
|
|
383
|
-
|
|
384
|
-
const out = [];
|
|
385
|
-
for (const source of [document?.catalogs?.[kind], options.catalogs?.[kind] ?? options.catalogSources?.[kind], bundledCatalogs[kind]])
|
|
386
|
-
for (const record of recordList(source)) {
|
|
387
|
-
if (seen.has(record.id)) continue;
|
|
388
|
-
seen.add(record.id);
|
|
389
|
-
out.push({ id: record.id, label: labelOf(record), record });
|
|
390
|
-
}
|
|
391
|
-
return out;
|
|
478
|
+
return listCatalogRecords(presentation ?? {}, kind, { catalogs: catalogsOf(options) });
|
|
392
479
|
}
|
|
393
480
|
|
|
394
481
|
const SINGLE_SERIES_ONLY = new Set(["pieChart", "doughnutChart", "funnelChart", "treemapChart", "waterfallChart"]);
|
|
395
482
|
const MULTI_SERIES_CAPABLE = new Set(["barChart", "lineChart", "areaChart", "radarChart"]);
|
|
483
|
+
const STACKED_GROUPINGS = new Set(["stacked", "percentStacked"]);
|
|
396
484
|
const DISTRIBUTION_ELEMENTS = new Set(["histogramChart", "boxWhiskerChart", "mapChart"]);
|
|
397
485
|
|
|
398
486
|
// RR-54: `mapping.x` names the X column of an XY chart; a type without an X axis ignores it (core warns chart-mapping-adapted). Switching to such a
|
|
399
487
|
// type removes it, and the whole mapping when nothing else is left, so the document does not keep a warning it cannot act on.
|
|
400
|
-
function staleMappingPatches(
|
|
488
|
+
function staleMappingPatches(presentation, owner, chart, type) {
|
|
401
489
|
const mapping = chart.mapping;
|
|
402
490
|
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping) || mapping.x === undefined) return [];
|
|
403
|
-
if (
|
|
491
|
+
if (isXYChartType(type)) return [];
|
|
404
492
|
const { x: _x, ...rest } = mapping;
|
|
405
493
|
const parts = [...owner, "chart", "mapping"];
|
|
406
|
-
return Object.keys(rest).length ? createValuePatch(
|
|
494
|
+
return Object.keys(rest).length ? createValuePatch(presentation, parts, rest) : [{ op: "remove", path: opfPathToJsonPointer(parts) }];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// FA-15: `line`, `secondaryAxis` and the secondary axis title belong to combo charts; any other type ignores them (core warns
|
|
498
|
+
// chart-option-adapted). Switching away from combo removes them, and an `axisTitles` left empty.
|
|
499
|
+
function staleComboPatches(presentation, owner, chart, type) {
|
|
500
|
+
if (chartOptionTarget(type)?.kind === "combo") return [];
|
|
501
|
+
const parts = [...owner, "chart"];
|
|
502
|
+
const patches = ["line", "secondaryAxis"].filter((key) => chart[key] !== undefined).map((key) => ({ op: "remove", path: opfPathToJsonPointer([...parts, key]) }));
|
|
503
|
+
const titles = chart.axisTitles;
|
|
504
|
+
if (titles && typeof titles === "object" && !Array.isArray(titles) && titles.secondary !== undefined) {
|
|
505
|
+
const { secondary: _secondary, ...rest } = titles;
|
|
506
|
+
patches.push(Object.keys(rest).length ? { op: "remove", path: opfPathToJsonPointer([...parts, "axisTitles", "secondary"]) } : { op: "remove", path: opfPathToJsonPointer([...parts, "axisTitles"]) });
|
|
507
|
+
}
|
|
508
|
+
return patches;
|
|
407
509
|
}
|
|
408
510
|
|
|
409
|
-
function chartDataShape(chart,
|
|
511
|
+
function chartDataShape(chart, presentation) {
|
|
410
512
|
// RR-54: inline data, a dataset reference and a series mapping all resolve to the columns the renderers read: the first column labels
|
|
411
513
|
// the categories and every further column is a series.
|
|
412
|
-
|
|
413
|
-
const data = chart?.data;
|
|
414
|
-
return data && Array.isArray(data.columns) && Array.isArray(data.rows) ? { series: Math.max(0, data.columns.length - 1), categories: data.rows.length } : undefined;
|
|
415
|
-
}
|
|
416
|
-
const resolved = core.resolveChartData(chart, document);
|
|
514
|
+
const resolved = resolveChartData(chart, presentation);
|
|
417
515
|
if (!resolved.ok) return undefined;
|
|
418
516
|
return { series: Math.max(0, resolved.columns.length - 1), categories: resolved.rows.length };
|
|
419
517
|
}
|
|
420
518
|
|
|
421
519
|
/**
|
|
422
|
-
* Chart types the chart's inline data can use as it is, from the
|
|
520
|
+
* Chart types the chart's inline data can use as it is, from core's chart types and the host's chart display metadata: simple,
|
|
423
521
|
* non-geographic, non-distribution types whose series count fits the data (the first column labels
|
|
424
522
|
* the categories and each further column is a series; a type with N series needs exactly N value
|
|
425
|
-
* columns
|
|
523
|
+
* columns, except that a stacked or percent-stacked type and a combination such as combo take N or more;
|
|
524
|
+
* column, bar, line, area and radar take any number). Data that is read from
|
|
426
525
|
* an external source returns every simple type. This is data-shape compatibility, not a claim that
|
|
427
526
|
* an engine draws the type. `path` or `slideIndex` picks the chart (default: the slide's first).
|
|
428
527
|
* Each entry has `current: true` for the chart's present type, which is always listed.
|
|
429
528
|
*/
|
|
430
|
-
export function compatibleChartTypes(
|
|
529
|
+
export function compatibleChartTypes(presentation, options = {}) {
|
|
431
530
|
const slideIndex = options.slideIndex ?? 0;
|
|
432
|
-
const owner = options.path ? splitOpfPath(options.path) :
|
|
433
|
-
const chart = owner && getValueAtPath(
|
|
531
|
+
const owner = options.path ? splitOpfPath(options.path) : presentation.slides?.[slideIndex] ? findChartOwner(presentation, slideIndex) : undefined;
|
|
532
|
+
const chart = owner && getValueAtPath(presentation, [...owner, "chart"]);
|
|
434
533
|
if (!chart || typeof chart !== "object") return [];
|
|
435
|
-
const shape = chartDataShape(chart,
|
|
534
|
+
const shape = chartDataShape(chart, presentation);
|
|
436
535
|
const result = [];
|
|
437
|
-
for (const option of listSwitchOptions(
|
|
536
|
+
for (const option of listSwitchOptions(presentation, "charts", options)) {
|
|
438
537
|
const record = option.record;
|
|
439
538
|
const element = record.mappings?.openxml?.element;
|
|
440
539
|
const current = option.id === chart.type;
|
|
441
|
-
|
|
540
|
+
// Without the host's display metadata for a type (complexity, series, mappings) it is offered: compatibility is unknown, not refused.
|
|
541
|
+
const simple = record.complexity === undefined ? !DISTRIBUTION_ELEMENTS.has(element) : record.complexity === "simple" && !DISTRIBUTION_ELEMENTS.has(element);
|
|
542
|
+
// A combination (composition "mixed", the FA-15 combo chart) needs at least its series count, like a stacked type.
|
|
543
|
+
const atLeast = STACKED_GROUPINGS.has(record.mappings?.openxml?.grouping) || record.mappings?.openxml?.composition === "mixed";
|
|
442
544
|
const seriesOk =
|
|
443
|
-
!shape ||
|
|
545
|
+
!shape ||
|
|
546
|
+
!record.series ||
|
|
547
|
+
(record.series > 1
|
|
548
|
+
? atLeast
|
|
549
|
+
? shape.series >= record.series
|
|
550
|
+
: record.series === shape.series
|
|
551
|
+
: shape.series === 1 || (MULTI_SERIES_CAPABLE.has(element) && !SINGLE_SERIES_ONLY.has(element)));
|
|
444
552
|
if (current || (simple && seriesOk)) result.push({ id: option.id, label: option.label, current, record });
|
|
445
553
|
}
|
|
446
554
|
return result;
|
|
@@ -449,46 +557,49 @@ export function compatibleChartTypes(document, options = {}) {
|
|
|
449
557
|
/**
|
|
450
558
|
* The value a dimension currently has, for pickers: `{ value, scope }` where scope is "slide" when
|
|
451
559
|
* `slideIndex` names a slide whose own design sets it, else "deck". `value` is undefined when the
|
|
452
|
-
* dimension is unset. Catalog dimensions return the
|
|
453
|
-
*
|
|
560
|
+
* dimension is unset. Catalog dimensions return the reference (`id` or `name:id`) even when the document holds an
|
|
561
|
+
* object form with overrides. `options.catalogs` resolves the theme's slide size.
|
|
454
562
|
*/
|
|
455
|
-
export function currentSwitchValue(
|
|
563
|
+
export function currentSwitchValue(presentation, dimension, options = {}) {
|
|
456
564
|
const idOf = (reference) => (reference && typeof reference === "object" && !Array.isArray(reference) ? reference.id : reference);
|
|
457
565
|
const design = (key) => {
|
|
458
|
-
const slide = options.slideIndex !== undefined ?
|
|
566
|
+
const slide = options.slideIndex !== undefined ? presentation.slides?.[options.slideIndex]?.design?.[key] : undefined;
|
|
459
567
|
if (slide !== undefined) return { value: slide, scope: "slide" };
|
|
460
|
-
return
|
|
568
|
+
return presentation.design?.[key] === undefined ? { scope: "deck" } : { value: presentation.design[key], scope: "deck" };
|
|
461
569
|
};
|
|
462
|
-
if (dimension === "layouts") return { value:
|
|
570
|
+
if (dimension === "layouts") return { value: presentation.slides?.[options.slideIndex]?.layout, scope: "slide" };
|
|
463
571
|
if (dimension === "charts") {
|
|
464
|
-
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(
|
|
465
|
-
return { value: owner ? getValueAtPath(
|
|
572
|
+
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(presentation, options.slideIndex ?? 0);
|
|
573
|
+
return { value: owner ? getValueAtPath(presentation, [...owner, "chart", "type"]) : undefined, scope: "slide" };
|
|
466
574
|
}
|
|
467
575
|
if (dimension === "blocks") return { value: undefined, scope: "block" };
|
|
576
|
+
if (dimension === "image-treatments") {
|
|
577
|
+
// The treatments of the image block at `options.path` (FA-22); without a path there is nothing to read.
|
|
578
|
+
if (options.path === undefined) return { value: undefined, scope: "block" };
|
|
579
|
+
const state = readImageTreatments(presentation, options.path);
|
|
580
|
+
return { value: Object.fromEntries(IMAGE_TREATMENT_FIELDS.map((key) => [key, state[key]])), scope: "block" };
|
|
581
|
+
}
|
|
468
582
|
if (dimension === "slide-sizes") {
|
|
469
583
|
// The size the deck composes at: its own design.dimensions, else the theme's; a {preset} object reads as its preset.
|
|
470
584
|
// A custom size (inches without a preset) reads as the object itself. Unset reads as undefined (composed as widescreen).
|
|
471
|
-
|
|
472
|
-
let size = own.value;
|
|
473
|
-
let scope = own.scope;
|
|
585
|
+
let size = presentation.design?.dimensions;
|
|
474
586
|
if (size === undefined) {
|
|
475
|
-
const
|
|
476
|
-
size =
|
|
477
|
-
scope = "deck";
|
|
587
|
+
const theme = presentation.design?.theme;
|
|
588
|
+
size = typeof theme === "string" ? resolveRecord(presentation, "themes", theme, options)?.record?.dimensions : undefined;
|
|
478
589
|
}
|
|
479
|
-
return { value: size && typeof size === "object" && !Array.isArray(size) && Object.keys(size).length === 1 && size.preset ? size.preset : size, scope };
|
|
590
|
+
return { value: size && typeof size === "object" && !Array.isArray(size) && Object.keys(size).length === 1 && size.preset ? size.preset : size, scope: "deck" };
|
|
480
591
|
}
|
|
481
592
|
if (dimension === "purposes") {
|
|
482
|
-
// A catalog
|
|
483
|
-
const purpose =
|
|
593
|
+
// A catalog reference or goal text reads as itself, a Purpose object as its id (else the object).
|
|
594
|
+
const purpose = presentation.purpose;
|
|
484
595
|
return { value: purpose && typeof purpose === "object" && !Array.isArray(purpose) ? (purpose.id ?? purpose) : purpose, scope: "deck" };
|
|
485
596
|
}
|
|
486
597
|
if (dimension === "socials") {
|
|
487
|
-
const host =
|
|
598
|
+
const host = presentation[options.owner ?? "speaker"];
|
|
488
599
|
const target = Array.isArray(host) ? host[options.index ?? 0] : host;
|
|
489
600
|
return { value: target?.socials, scope: "deck" };
|
|
490
601
|
}
|
|
491
|
-
if (ROOT_FIELD[dimension]) return { value:
|
|
602
|
+
if (ROOT_FIELD[dimension]) return { value: presentation[ROOT_FIELD[dimension]], scope: "deck" };
|
|
492
603
|
if (dimension === "color-schemes") return { ...design("colorScheme"), value: idOf(design("colorScheme").value) };
|
|
493
604
|
if (dimension === "font-schemes") return { ...design("fontScheme"), value: idOf(design("fontScheme").value) };
|
|
494
605
|
if (dimension === "themes") return { ...design("theme"), value: idOf(design("theme").value) };
|