@openpresentation/opf-editor 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -29
- package/dist/assets.js +1 -1
- package/dist/background-options.d.ts +10 -3
- package/dist/background-options.js +38 -13
- package/dist/canvas.d.ts +8 -1
- package/dist/canvas.js +12 -9
- package/dist/catalogs.d.ts +48 -0
- package/dist/catalogs.js +217 -0
- package/dist/checks.js +9 -7
- package/dist/content-actions.d.ts +2 -2
- package/dist/content-actions.js +3 -3
- package/dist/content-controls.js +9 -9
- package/dist/design-controls.d.ts +12 -5
- package/dist/design-controls.js +260 -76
- package/dist/design-options.d.ts +4 -4
- package/dist/design-options.js +6 -26
- package/dist/export.d.ts +2 -0
- package/dist/export.js +8 -3
- package/dist/galleries.js +32 -67
- package/dist/image-crop.js +8 -9
- package/dist/image-options.d.ts +45 -0
- package/dist/image-options.js +195 -0
- package/dist/index.d.ts +25 -8
- package/dist/index.js +91 -47
- package/dist/json-editor.js +2 -2
- package/dist/json-menu.js +2 -2
- package/dist/json-options.d.ts +3 -3
- package/dist/json-options.js +30 -41
- package/dist/react.js +1 -1
- package/dist/review-panel.js +8 -2
- package/dist/schema-inspector.js +1 -1
- package/dist/schema.js +1 -1
- package/dist/slide-manager.js +1 -1
- package/dist/slides.d.ts +1 -1
- package/dist/slides.js +6 -5
- package/dist/svelte.js +1 -2
- package/dist/switches.d.ts +31 -16
- package/dist/switches.js +191 -90
- package/dist/transfer.d.ts +12 -1
- package/dist/transfer.js +87 -131
- package/package.json +9 -5
package/dist/switches.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ConvertOptions } from "@openpresentation/opf/convert";
|
|
2
|
+
import type { Catalog } from "@openpresentation/opf";
|
|
2
3
|
import type { EditorChange, EditorSession, JsonPatchOperation } from "./index.js";
|
|
3
4
|
|
|
5
|
+
/** Display metadata for the engine vocabularies, in the shape of `catalogDisplay` (`@openpresentation/opf/catalog`): records by id, or a list of records with `id` (`bcp47` for a language). */
|
|
6
|
+
export interface SwitchVocabularies {
|
|
7
|
+
languages?: unknown;
|
|
8
|
+
chartTypes?: unknown;
|
|
9
|
+
socialPlatforms?: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
/** The schema's DimensionPreset values: the values of the slide-sizes switch. */
|
|
5
13
|
export declare const SLIDE_SIZE_PRESETS: readonly ["16:9", "4:3", "16:10", "1:1", "4:5", "9:16", "letter", "a4", "widescreen", "standard"];
|
|
6
14
|
export type SlideSizePreset = (typeof SLIDE_SIZE_PRESETS)[number];
|
|
@@ -29,14 +37,17 @@ export type SwitchDimension = (typeof SWITCH_DIMENSIONS)[number];
|
|
|
29
37
|
export interface DimensionSwitchOptions {
|
|
30
38
|
/** Required for layouts and charts; for design dimensions it scopes the switch to one slide (default: the deck). */
|
|
31
39
|
slideIndex?: number;
|
|
32
|
-
/** blocks: the complete block (`slides.0.blocks.1`) or one-payload slide/region to replace. charts: the block holding the chart (default: the slide's first chart). */
|
|
40
|
+
/** blocks: the complete block (`slides.0.blocks.1`) or one-payload slide/region to replace. charts: the block holding the chart (default: the slide's first chart). image-treatments: the image block. */
|
|
33
41
|
path?: string | string[];
|
|
34
|
-
/** A gallery item's catalog record,
|
|
42
|
+
/** A gallery item's catalog record, embedded in the same transaction (under the group of `recordSource`, else `custom`) when it is not embedded yet. */
|
|
35
43
|
record?: Record<string, unknown> & { id: string };
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
/** The catalog source of `record` (for example "https://www.pptx.gallery"). */
|
|
45
|
+
recordSource?: string;
|
|
46
|
+
/** Host catalogs (`Catalog[]`): references resolve in the document, then in these. */
|
|
47
|
+
catalogs?: readonly Catalog[];
|
|
48
|
+
/** Display metadata for languages, chart types and social platforms (labels in pickers). */
|
|
49
|
+
vocabularies?: SwitchVocabularies;
|
|
50
|
+
/** themes: the theme's own color scheme, font scheme and background (and, for the deck, dimensions) apply: overrides of those at the switched scope are removed; on a slide whose deck sets one, the theme's value is written on the slide (default true). */
|
|
40
51
|
bundle?: boolean;
|
|
41
52
|
/** Deck-scope design switches: remove slide-level values that would hide the switch. */
|
|
42
53
|
clearSlideOverrides?: boolean;
|
|
@@ -55,12 +66,11 @@ export interface DimensionSwitchOptions {
|
|
|
55
66
|
|
|
56
67
|
export type DimensionSwitchValue =
|
|
57
68
|
| null /* backgrounds: remove it */
|
|
58
|
-
| string /* slide-sizes: a SlideSizePreset. purposes: a catalog
|
|
59
|
-
| string[] /* audiences: catalog
|
|
69
|
+
| string /* slide-sizes: a SlideSizePreset. purposes: a catalog reference or free-form goal text. languages: a BCP-47 tag. Catalog dimensions: a reference, `id` or `name:id`. */
|
|
70
|
+
| string[] /* audiences: catalog references */
|
|
60
71
|
| { platform: string; handle: string }
|
|
61
72
|
| { header?: unknown; footer?: unknown }
|
|
62
|
-
|
|
|
63
|
-
| Record<string, unknown>;
|
|
73
|
+
| Record<string, unknown> /* image-treatments: fit, focus, aspectRatio, shape, cornerRadius, border, opacity, recolor, overlay, placement (null removes) */;
|
|
64
74
|
|
|
65
75
|
export interface PreparedDimensionSwitch {
|
|
66
76
|
dimension: SwitchDimension;
|
|
@@ -92,18 +102,23 @@ export declare function prepareDimensionSwitch(presentation: unknown, dimension:
|
|
|
92
102
|
export declare function switchDimension(editor: EditorSession, dimension: SwitchDimension, value: DimensionSwitchValue, options?: DimensionSwitchOptions): DimensionSwitchChange;
|
|
93
103
|
|
|
94
104
|
export interface SwitchOption {
|
|
105
|
+
/** The value to switch to: for a catalog dimension the reference, `id` or `name:id`. */
|
|
95
106
|
id: string;
|
|
96
107
|
label: string;
|
|
97
108
|
record?: Record<string, unknown>;
|
|
109
|
+
reference?: string;
|
|
110
|
+
group?: string;
|
|
111
|
+
source?: string;
|
|
112
|
+
origin?: "document" | "host";
|
|
98
113
|
}
|
|
99
114
|
export interface CompatibleChartType extends SwitchOption {
|
|
100
115
|
/** True for the chart's present type, which is always listed. */
|
|
101
116
|
current: boolean;
|
|
102
117
|
}
|
|
103
|
-
/** Values a picker can offer
|
|
104
|
-
export declare function listSwitchOptions(presentation: unknown, dimension: SwitchDimension, options?: Pick<DimensionSwitchOptions, "catalogs" | "
|
|
105
|
-
/** Chart types the chart's inline data can use as it is (data-shape compatibility
|
|
106
|
-
export declare function compatibleChartTypes(presentation: unknown, options?: Pick<DimensionSwitchOptions, "slideIndex" | "path" | "catalogs" | "
|
|
107
|
-
/** The value a dimension currently has: `{ value, scope }`, with the
|
|
108
|
-
export declare function currentSwitchValue(presentation: unknown, dimension: SwitchDimension, options?: Pick<DimensionSwitchOptions, "slideIndex" | "path" | "owner" | "index">): { value: unknown; scope: "deck" | "slide" | "block" };
|
|
118
|
+
/** Values a picker can offer: a catalog dimension lists core's `catalogRecords` (embedded records, then `catalogs`); `charts` and `socials` list core's vocabularies, `languages` core's `LANGUAGES` tags; `blocks` lists the content kinds and `slide-sizes` the presets. `purposes` lists the catalogs; any other goal text is also a valid switch value. */
|
|
119
|
+
export declare function listSwitchOptions(presentation: unknown, dimension: SwitchDimension, options?: Pick<DimensionSwitchOptions, "catalogs" | "vocabularies">): SwitchOption[];
|
|
120
|
+
/** Chart types the chart's inline data can use as it is (data-shape compatibility from `vocabularies.chartTypes`, not an engine-support claim). `path` or `slideIndex` picks the chart. */
|
|
121
|
+
export declare function compatibleChartTypes(presentation: unknown, options?: Pick<DimensionSwitchOptions, "slideIndex" | "path" | "catalogs" | "vocabularies">): CompatibleChartType[];
|
|
122
|
+
/** The value a dimension currently has: `{ value, scope }`, with the reference (`id` or `name:id`) for catalog dimensions. `slide-sizes` reads the deck's design.dimensions, else its theme's (a preset string, or the object for a custom size); `purposes` reads the goal text or Purpose id. */
|
|
123
|
+
export declare function currentSwitchValue(presentation: unknown, dimension: SwitchDimension, options?: Pick<DimensionSwitchOptions, "slideIndex" | "path" | "owner" | "index" | "catalogs">): { value: unknown; scope: "deck" | "slide" | "block" };
|
|
109
124
|
export { blockConversionTargets } from "./block-convert.js";
|
package/dist/switches.js
CHANGED
|
@@ -2,14 +2,22 @@
|
|
|
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
|
-
|
|
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";
|
|
6
12
|
import { chartOptionTarget } from "@openpresentation/opf/composition";
|
|
7
|
-
import {
|
|
13
|
+
import { createValuePatch, applyJsonPatch, getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
|
|
8
14
|
import { createContentBlock, prepareBlockReplace } from "./blocks.js";
|
|
9
15
|
import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
|
|
10
16
|
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
11
17
|
import { blockConversionTargets, prepareBlockConversion } from "./block-convert.js";
|
|
12
18
|
import { checkFormat } from "./checks.js";
|
|
19
|
+
import { listCatalogRecords, mergeCatalogs } from "./catalogs.js";
|
|
20
|
+
import { IMAGE_TREATMENT_FIELDS, imageTreatmentPatches, readImageTreatments } from "./image-options.js";
|
|
13
21
|
|
|
14
22
|
/** The schema's DimensionPreset values (RR-41, FA-13: with the social-feed ratios), the values of the slide-sizes switch. */
|
|
15
23
|
export const SLIDE_SIZE_PRESETS = Object.freeze(["16:9", "4:3", "16:10", "1:1", "4:5", "9:16", "letter", "a4", "widescreen", "standard"]);
|
|
@@ -52,20 +60,19 @@ export const SWITCH_DIMENSIONS = Object.freeze([
|
|
|
52
60
|
"purposes",
|
|
53
61
|
]);
|
|
54
62
|
|
|
55
|
-
// Catalog kind behind each catalog-backed dimension.
|
|
63
|
+
// Catalog kind behind each catalog-backed dimension (core's content kinds).
|
|
56
64
|
const CATALOG_KIND = Object.freeze({
|
|
57
65
|
layouts: "layouts",
|
|
58
66
|
"color-schemes": "colorSchemes",
|
|
59
67
|
"font-schemes": "fontSchemes",
|
|
60
|
-
languages: "languages",
|
|
61
68
|
narratives: "narratives",
|
|
62
69
|
themes: "themes",
|
|
63
70
|
audiences: "audiences",
|
|
64
71
|
tones: "tones",
|
|
65
|
-
socials: "socialPlatforms",
|
|
66
|
-
charts: "chartTypes",
|
|
67
72
|
purposes: "purposes",
|
|
68
73
|
});
|
|
74
|
+
// Display metadata key (`options.vocabularies`) behind each engine-vocabulary dimension.
|
|
75
|
+
const VOCABULARY = Object.freeze({ languages: "languages", charts: "chartTypes", socials: "socialPlatforms" });
|
|
69
76
|
// Top-level document field for the simple metadata dimensions.
|
|
70
77
|
const ROOT_FIELD = Object.freeze({ languages: "language", narratives: "narrative", tones: "tone", audiences: "audience" });
|
|
71
78
|
// `design` keys for the design dimensions. A key set to `null` in the value is removed.
|
|
@@ -74,49 +81,80 @@ const DESIGN_KEYS = Object.freeze({
|
|
|
74
81
|
"font-schemes": ["fontScheme"],
|
|
75
82
|
backgrounds: ["background"],
|
|
76
83
|
"headers-footers": ["header", "footer"],
|
|
77
|
-
"image-treatments": ["slideImage", "imageFill"],
|
|
78
84
|
});
|
|
79
85
|
const REGION = /^(?:(?:top|middle|bottom)(?:\+(?:top|middle|bottom))*(?::(?:left|center|right)(?:\+(?:left|center|right))*)?|(?:left|center|right)(?:\+(?:left|center|right))*)$/;
|
|
80
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);
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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) });
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 };
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
|
|
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) {
|
|
99
129
|
const kind = CATALOG_KIND[dimension];
|
|
100
|
-
if (typeof
|
|
101
|
-
throw fail("invalid-catalog-id", `Switch ${dimension} to a non-empty catalog
|
|
102
|
-
const
|
|
103
|
-
if (!
|
|
104
|
-
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;
|
|
105
135
|
}
|
|
106
136
|
|
|
107
|
-
// A
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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);
|
|
120
158
|
}
|
|
121
159
|
|
|
122
160
|
function slideAt(presentation, slideIndex, what = "this switch") {
|
|
@@ -158,7 +196,8 @@ function socialsPatches(presentation, value, options) {
|
|
|
158
196
|
if (!["speaker", "organization"].includes(owner)) throw fail("invalid-switch-value", "Socials owner must be 'speaker' or 'organization'.", { owner });
|
|
159
197
|
if (!value || typeof value !== "object" || typeof value.platform !== "string" || typeof value.handle !== "string" || !value.handle)
|
|
160
198
|
throw fail("invalid-switch-value", "Switch socials to { platform, handle } with a non-empty handle.", { value });
|
|
161
|
-
|
|
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 });
|
|
162
201
|
const host = presentation[owner];
|
|
163
202
|
const index = Array.isArray(host) ? (options.index ?? 0) : undefined;
|
|
164
203
|
const target = Array.isArray(host) ? host[index] : host;
|
|
@@ -166,12 +205,17 @@ function socialsPatches(presentation, value, options) {
|
|
|
166
205
|
throw fail("missing-owner", `Add the ${owner} to the document before setting their socials.`, { owner });
|
|
167
206
|
const base = Array.isArray(host) ? [owner, String(index)] : [owner];
|
|
168
207
|
const socials = target.socials;
|
|
169
|
-
const patches =
|
|
208
|
+
const patches = [];
|
|
170
209
|
if (!socials || typeof socials !== "object") return [...patches, { op: "add", path: opfPathToJsonPointer([...base, "socials"]), value: { [value.platform]: value.handle } }];
|
|
171
210
|
if (socials[value.platform] === value.handle) return patches;
|
|
172
211
|
return [...patches, { op: Object.hasOwn(socials, value.platform) ? "replace" : "add", path: opfPathToJsonPointer([...base, "socials", value.platform]), value: value.handle }];
|
|
173
212
|
}
|
|
174
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
|
+
|
|
175
219
|
function blockValue(value, options) {
|
|
176
220
|
if (typeof value === "string") {
|
|
177
221
|
if (!BLOCK_KINDS.includes(value)) throw fail("invalid-switch-value", `Unknown content block kind: ${value}.`, { value });
|
|
@@ -190,9 +234,9 @@ export function prepareDimensionSwitch(presentation, dimension, value, options =
|
|
|
190
234
|
throw fail("unknown-dimension", `Unknown dimension: ${dimension}. Use one of ${SWITCH_DIMENSIONS.join(", ")}.`, { dimension });
|
|
191
235
|
if (!presentation || typeof presentation !== "object") throw fail("invalid-input", "Switch requires an OPF document object.");
|
|
192
236
|
if (options.record) {
|
|
193
|
-
const ids = dimension === "audiences" ? [value].flat() :
|
|
194
|
-
if (!CATALOG_KIND[dimension] || !ids.includes(options.record.id))
|
|
195
|
-
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 });
|
|
196
240
|
}
|
|
197
241
|
const before = checkFormat(presentation);
|
|
198
242
|
const scopeIndex = options.slideIndex;
|
|
@@ -212,20 +256,22 @@ export function prepareDimensionSwitch(presentation, dimension, value, options =
|
|
|
212
256
|
// A slide's design cannot set dimensions (FA-07), so no slide can shadow the deck's size.
|
|
213
257
|
patches = sameSize ? [] : designPatches(presentation, [], { dimensions: value });
|
|
214
258
|
} else if (dimension === "purposes") {
|
|
215
|
-
// 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.
|
|
216
260
|
if (typeof value !== "string" && !(value && typeof value === "object" && !Array.isArray(value)))
|
|
217
|
-
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 });
|
|
218
262
|
if (typeof value === "string" && value.length === 0) throw fail("invalid-switch-value", "Switch purposes to a non-empty goal.", { value });
|
|
219
|
-
|
|
263
|
+
const supplied = typeof value === "string" ? catalogChoiceIfSupplied(presentation, dimension, value, options) : undefined;
|
|
264
|
+
patches = [...(supplied?.patches ?? []), ...rootPatch(presentation, "purpose", supplied?.reference ?? value)];
|
|
220
265
|
} else if (dimension === "layouts") {
|
|
221
266
|
slideIndex = scopeIndex;
|
|
222
267
|
slideAt(presentation, slideIndex, "a layout switch");
|
|
223
268
|
scope = "slide";
|
|
224
|
-
const
|
|
225
|
-
|
|
269
|
+
const choice = catalogChoice(presentation, dimension, value, options);
|
|
270
|
+
const record = choice.record;
|
|
271
|
+
patches = choice.patches;
|
|
226
272
|
const withRecord = patches.length ? applyJsonPatch(presentation, patches) : presentation;
|
|
227
|
-
if (withRecord.slides[slideIndex].layout !==
|
|
228
|
-
const layoutOps = createValuePatch(withRecord, ["slides", String(slideIndex), "layout"],
|
|
273
|
+
if (withRecord.slides[slideIndex].layout !== choice.reference) {
|
|
274
|
+
const layoutOps = createValuePatch(withRecord, ["slides", String(slideIndex), "layout"], choice.reference);
|
|
229
275
|
const swapped = applyJsonPatch(withRecord, layoutOps);
|
|
230
276
|
const types = Array.isArray(record.placeholders) ? record.placeholders.map((placeholder) => placeholder.type) : undefined;
|
|
231
277
|
// Add the blank payloads the layout declares, as the JSON editor's layout choice does.
|
|
@@ -238,13 +284,13 @@ export function prepareDimensionSwitch(presentation, dimension, value, options =
|
|
|
238
284
|
slideIndex = scopeIndex;
|
|
239
285
|
slideAt(presentation, slideIndex, "a chart switch");
|
|
240
286
|
scope = "slide";
|
|
241
|
-
|
|
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 });
|
|
242
288
|
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(presentation, slideIndex);
|
|
243
289
|
if (options.path && (owner[0] !== "slides" || owner[1] !== String(slideIndex)))
|
|
244
290
|
throw fail("path-slide-mismatch", "options.path is not on the slide named by options.slideIndex.", { slideIndex, path: options.path });
|
|
245
291
|
const chart = owner && getValueAtPath(presentation, [...owner, "chart"]);
|
|
246
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 });
|
|
247
|
-
patches =
|
|
293
|
+
patches = chart.type === value ? [] : [...createValuePatch(presentation, [...owner, "chart", "type"], value), ...staleMappingPatches(presentation, owner, chart, value), ...staleComboPatches(presentation, owner, chart, value)];
|
|
248
294
|
} else if (dimension === "blocks") {
|
|
249
295
|
if (options.path === undefined) throw fail("missing-path", "Choose the block to replace with options.path.");
|
|
250
296
|
// convert: true moves the block's own text into the new kind (block-convert.js) instead of replacing it.
|
|
@@ -263,30 +309,58 @@ export function prepareDimensionSwitch(presentation, dimension, value, options =
|
|
|
263
309
|
scope = "block";
|
|
264
310
|
} else if (dimension === "socials") {
|
|
265
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);
|
|
266
324
|
} else if (ROOT_FIELD[dimension]) {
|
|
267
325
|
const field = ROOT_FIELD[dimension];
|
|
268
|
-
let next
|
|
326
|
+
let next;
|
|
269
327
|
if (dimension === "audiences") {
|
|
270
|
-
|
|
271
|
-
if (!
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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)];
|
|
275
342
|
} else {
|
|
276
343
|
// Design dimensions: deck scope by default, or one slide with options.slideIndex.
|
|
277
344
|
let entries;
|
|
278
345
|
if (dimension === "themes") {
|
|
279
|
-
const
|
|
280
|
-
|
|
346
|
+
const choice = catalogChoice(presentation, dimension, value, options);
|
|
347
|
+
const record = choice.record ?? {};
|
|
348
|
+
entries = { theme: choice.reference };
|
|
281
349
|
if (options.bundle !== false) {
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
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
|
+
}
|
|
286
359
|
}
|
|
287
|
-
patches =
|
|
360
|
+
patches = choice.patches;
|
|
288
361
|
} else if (dimension === "color-schemes" || dimension === "font-schemes") {
|
|
289
|
-
|
|
362
|
+
const choice = catalogChoice(presentation, dimension, value, options);
|
|
363
|
+
value = choice.reference;
|
|
290
364
|
entries = { [DESIGN_KEYS[dimension][0]]: value };
|
|
291
365
|
if (dimension === "font-schemes") {
|
|
292
366
|
// An accent font is its own choice, not part of the scheme being left: it stays (in the object form).
|
|
@@ -294,7 +368,7 @@ export function prepareDimensionSwitch(presentation, dimension, value, options =
|
|
|
294
368
|
const own = getValueAtPath(presentation, [...scopeBase, "design", "fontScheme"]);
|
|
295
369
|
if (own && typeof own === "object" && own.accent !== undefined) entries.fontScheme = { id: value, accent: structuredClone(own.accent) };
|
|
296
370
|
}
|
|
297
|
-
patches =
|
|
371
|
+
patches = choice.patches;
|
|
298
372
|
} else {
|
|
299
373
|
// A background is an object or a shorthand string (theme slot or hex color); the schema
|
|
300
374
|
// validates the candidate document, so a string that is neither is rejected below.
|
|
@@ -342,7 +416,8 @@ export function prepareDimensionSwitch(presentation, dimension, value, options =
|
|
|
342
416
|
}
|
|
343
417
|
|
|
344
418
|
/**
|
|
345
|
-
* 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
|
|
346
421
|
* editor change plus `dimension`, `scope`, `changed` and `shadowed` (slides whose own design
|
|
347
422
|
* hides a deck-level switch). Switching to the current value commits nothing.
|
|
348
423
|
*/
|
|
@@ -350,7 +425,8 @@ export function switchDimension(editor, dimension, value, options = {}) {
|
|
|
350
425
|
if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function")
|
|
351
426
|
throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
352
427
|
const { meta, ...switchOptions } = options;
|
|
353
|
-
|
|
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) });
|
|
354
430
|
const { presentation, patches, ...summary } = prepared;
|
|
355
431
|
void presentation;
|
|
356
432
|
if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
@@ -360,28 +436,46 @@ export function switchDimension(editor, dimension, value, options = {}) {
|
|
|
360
436
|
|
|
361
437
|
// --- options for pickers and current values ---------------------------------------------------
|
|
362
438
|
|
|
363
|
-
|
|
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);
|
|
364
452
|
|
|
365
453
|
/**
|
|
366
|
-
* The values a picker can offer for a catalog-backed dimension
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* `
|
|
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.
|
|
370
459
|
*/
|
|
371
460
|
export function listSwitchOptions(presentation, dimension, options = {}) {
|
|
372
461
|
if (dimension === "blocks") return BLOCK_KINDS.map((kind) => ({ id: kind, label: kind[0].toUpperCase() + kind.slice(1) }));
|
|
373
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
|
+
}
|
|
374
476
|
const kind = CATALOG_KIND[dimension];
|
|
375
477
|
if (!kind) return [];
|
|
376
|
-
|
|
377
|
-
const out = [];
|
|
378
|
-
for (const source of [presentation?.catalogs?.[kind], options.catalogs?.[kind] ?? options.catalogSources?.[kind], bundledCatalogs[kind]])
|
|
379
|
-
for (const record of recordList(source)) {
|
|
380
|
-
if (seen.has(record.id)) continue;
|
|
381
|
-
seen.add(record.id);
|
|
382
|
-
out.push({ id: record.id, label: labelOf(record), record });
|
|
383
|
-
}
|
|
384
|
-
return out;
|
|
478
|
+
return listCatalogRecords(presentation ?? {}, kind, { catalogs: catalogsOf(options) });
|
|
385
479
|
}
|
|
386
480
|
|
|
387
481
|
const SINGLE_SERIES_ONLY = new Set(["pieChart", "doughnutChart", "funnelChart", "treemapChart", "waterfallChart"]);
|
|
@@ -423,7 +517,7 @@ function chartDataShape(chart, presentation) {
|
|
|
423
517
|
}
|
|
424
518
|
|
|
425
519
|
/**
|
|
426
|
-
* 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,
|
|
427
521
|
* non-geographic, non-distribution types whose series count fits the data (the first column labels
|
|
428
522
|
* the categories and each further column is a series; a type with N series needs exactly N value
|
|
429
523
|
* columns, except that a stacked or percent-stacked type and a combination such as combo take N or more;
|
|
@@ -443,7 +537,8 @@ export function compatibleChartTypes(presentation, options = {}) {
|
|
|
443
537
|
const record = option.record;
|
|
444
538
|
const element = record.mappings?.openxml?.element;
|
|
445
539
|
const current = option.id === chart.type;
|
|
446
|
-
|
|
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);
|
|
447
542
|
// A combination (composition "mixed", the FA-15 combo chart) needs at least its series count, like a stacked type.
|
|
448
543
|
const atLeast = STACKED_GROUPINGS.has(record.mappings?.openxml?.grouping) || record.mappings?.openxml?.composition === "mixed";
|
|
449
544
|
const seriesOk =
|
|
@@ -462,8 +557,8 @@ export function compatibleChartTypes(presentation, options = {}) {
|
|
|
462
557
|
/**
|
|
463
558
|
* The value a dimension currently has, for pickers: `{ value, scope }` where scope is "slide" when
|
|
464
559
|
* `slideIndex` names a slide whose own design sets it, else "deck". `value` is undefined when the
|
|
465
|
-
* dimension is unset. Catalog dimensions return the
|
|
466
|
-
*
|
|
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.
|
|
467
562
|
*/
|
|
468
563
|
export function currentSwitchValue(presentation, dimension, options = {}) {
|
|
469
564
|
const idOf = (reference) => (reference && typeof reference === "object" && !Array.isArray(reference) ? reference.id : reference);
|
|
@@ -478,18 +573,24 @@ export function currentSwitchValue(presentation, dimension, options = {}) {
|
|
|
478
573
|
return { value: owner ? getValueAtPath(presentation, [...owner, "chart", "type"]) : undefined, scope: "slide" };
|
|
479
574
|
}
|
|
480
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
|
+
}
|
|
481
582
|
if (dimension === "slide-sizes") {
|
|
482
583
|
// The size the deck composes at: its own design.dimensions, else the theme's; a {preset} object reads as its preset.
|
|
483
584
|
// A custom size (inches without a preset) reads as the object itself. Unset reads as undefined (composed as widescreen).
|
|
484
585
|
let size = presentation.design?.dimensions;
|
|
485
586
|
if (size === undefined) {
|
|
486
|
-
const
|
|
487
|
-
size =
|
|
587
|
+
const theme = presentation.design?.theme;
|
|
588
|
+
size = typeof theme === "string" ? resolveRecord(presentation, "themes", theme, options)?.record?.dimensions : undefined;
|
|
488
589
|
}
|
|
489
590
|
return { value: size && typeof size === "object" && !Array.isArray(size) && Object.keys(size).length === 1 && size.preset ? size.preset : size, scope: "deck" };
|
|
490
591
|
}
|
|
491
592
|
if (dimension === "purposes") {
|
|
492
|
-
// A catalog
|
|
593
|
+
// A catalog reference or goal text reads as itself, a Purpose object as its id (else the object).
|
|
493
594
|
const purpose = presentation.purpose;
|
|
494
595
|
return { value: purpose && typeof purpose === "object" && !Array.isArray(purpose) ? (purpose.id ?? purpose) : purpose, scope: "deck" };
|
|
495
596
|
}
|
package/dist/transfer.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Catalog, CopiedRecordRename } from "@openpresentation/opf";
|
|
1
2
|
export declare const MAX_OPF_BYTES: number;
|
|
2
3
|
export interface OpfTransfer {
|
|
3
4
|
kind: "presentation" | "slides" | "selection";
|
|
@@ -14,6 +15,8 @@ export declare function serializeOpfTransfer(
|
|
|
14
15
|
slideIndex?: number;
|
|
15
16
|
path?: string;
|
|
16
17
|
format?: "pretty" | "compact" | "markdown";
|
|
18
|
+
/** Host catalogs: a presentation or slide is embedded (core `embed`) so it renders the same where none is registered. */
|
|
19
|
+
catalogs?: readonly Catalog[];
|
|
17
20
|
},
|
|
18
21
|
): string;
|
|
19
22
|
export declare function prepareOpfImport(
|
|
@@ -23,5 +26,13 @@ export declare function prepareOpfImport(
|
|
|
23
26
|
mode?: "insert" | "replace" | "selection";
|
|
24
27
|
slideIndex?: number;
|
|
25
28
|
path?: string;
|
|
29
|
+
/** Host catalogs, for resolving and copying the inserted slides' records (core `copySlides`). */
|
|
30
|
+
catalogs?: readonly Catalog[];
|
|
26
31
|
},
|
|
27
|
-
): {
|
|
32
|
+
): {
|
|
33
|
+
presentation: any;
|
|
34
|
+
slideIndex: number;
|
|
35
|
+
/** Records that did not keep their reference while inserting: tell the user. */
|
|
36
|
+
renamed: CopiedRecordRename[];
|
|
37
|
+
addedGroups: { name: string; source?: string }[];
|
|
38
|
+
};
|