@openpresentation/opf-editor 0.12.1 → 0.14.2

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.
Files changed (95) hide show
  1. package/README.md +71 -60
  2. package/dist/annotations.d.ts +16 -16
  3. package/dist/annotations.js +71 -79
  4. package/dist/assets.d.ts +7 -7
  5. package/dist/assets.js +15 -15
  6. package/dist/background-options.d.ts +2 -2
  7. package/dist/background-options.js +6 -6
  8. package/dist/block-controls.js +7 -7
  9. package/dist/block-convert.d.ts +7 -7
  10. package/dist/block-convert.js +22 -21
  11. package/dist/blocks.d.ts +8 -8
  12. package/dist/blocks.js +41 -40
  13. package/dist/canvas-fields.js +35 -12
  14. package/dist/canvas.d.ts +22 -30
  15. package/dist/canvas.js +57 -64
  16. package/dist/chart-data.d.ts +10 -10
  17. package/dist/chart-data.js +21 -21
  18. package/dist/chart-options-panel.d.ts +2 -2
  19. package/dist/chart-options-panel.js +82 -8
  20. package/dist/chart-options.d.ts +35 -9
  21. package/dist/chart-options.js +133 -40
  22. package/dist/checks.js +21 -0
  23. package/dist/color-authoring.js +1 -1
  24. package/dist/content-actions.d.ts +15 -15
  25. package/dist/content-actions.js +71 -70
  26. package/dist/content-controls.js +12 -12
  27. package/dist/data-grid.js +7 -8
  28. package/dist/data.d.ts +4 -4
  29. package/dist/data.js +5 -5
  30. package/dist/dataset-refs.d.ts +2 -2
  31. package/dist/dataset-refs.js +5 -5
  32. package/dist/design-controls.js +61 -44
  33. package/dist/design-options.d.ts +16 -14
  34. package/dist/design-options.js +86 -74
  35. package/dist/edit-helpers.js +8 -7
  36. package/dist/exact-source.js +5 -5
  37. package/dist/export.d.ts +5 -7
  38. package/dist/export.js +40 -14
  39. package/dist/find-panel.js +5 -5
  40. package/dist/find-replace.d.ts +3 -3
  41. package/dist/find-replace.js +23 -20
  42. package/dist/font-gate.js +49 -58
  43. package/dist/galleries.js +36 -52
  44. package/dist/grid-model.d.ts +25 -27
  45. package/dist/grid-model.js +92 -102
  46. package/dist/image-crop.d.ts +4 -4
  47. package/dist/image-crop.js +20 -20
  48. package/dist/image-cropper.js +3 -3
  49. package/dist/index.d.ts +28 -28
  50. package/dist/index.js +74 -90
  51. package/dist/json-options.js +17 -16
  52. package/dist/layout-handles.js +5 -5
  53. package/dist/layout-placeholders.js +4 -4
  54. package/dist/layout.d.ts +2 -2
  55. package/dist/layout.js +8 -7
  56. package/dist/numbering-panel.js +3 -5
  57. package/dist/numbering.d.ts +3 -5
  58. package/dist/numbering.js +14 -26
  59. package/dist/outline-view.js +24 -24
  60. package/dist/outline.d.ts +12 -12
  61. package/dist/outline.js +52 -50
  62. package/dist/persistence.d.ts +1 -1
  63. package/dist/persistence.js +19 -18
  64. package/dist/presentation-ids.js +2 -2
  65. package/dist/react.js +3 -3
  66. package/dist/review-panel.d.ts +30 -13
  67. package/dist/review-panel.js +154 -53
  68. package/dist/review.d.ts +36 -43
  69. package/dist/review.js +92 -53
  70. package/dist/rich-text-input.js +4 -3
  71. package/dist/rich-text-toolbar.js +5 -3
  72. package/dist/rich-text.js +5 -2
  73. package/dist/schema-inspector.d.ts +2 -2
  74. package/dist/schema-inspector.js +16 -11
  75. package/dist/schema.d.ts +1 -1
  76. package/dist/schema.js +25 -5
  77. package/dist/slide-manager.d.ts +2 -2
  78. package/dist/slide-manager.js +18 -18
  79. package/dist/slides.d.ts +20 -20
  80. package/dist/slides.js +99 -97
  81. package/dist/svelte.js +1 -1
  82. package/dist/switches.d.ts +10 -10
  83. package/dist/switches.js +124 -114
  84. package/dist/table-options.d.ts +8 -8
  85. package/dist/table-options.js +28 -27
  86. package/dist/table-structure.d.ts +10 -10
  87. package/dist/table-structure.js +21 -21
  88. package/dist/template-panel.d.ts +3 -3
  89. package/dist/template-panel.js +37 -13
  90. package/dist/templates.d.ts +22 -11
  91. package/dist/templates.js +48 -55
  92. package/dist/transfer.d.ts +4 -4
  93. package/dist/transfer.js +49 -59
  94. package/package.json +8 -6
  95. package/dist/font-defaults.js +0 -25
package/dist/svelte.js CHANGED
@@ -52,7 +52,7 @@ export function opfCatalogSelect(node, params) {
52
52
  replaceOptions(node, getCatalogOptions(catalogKind, {
53
53
  catalogs: nextParams.catalogs,
54
54
  catalogSources: nextParams.catalogSources,
55
- presentation: nextParams.presentation ?? editor.document
55
+ presentation: nextParams.presentation ?? editor.presentation
56
56
  }));
57
57
 
58
58
  const sync = () => {
@@ -2,7 +2,7 @@ import type { ConvertOptions } from "@openpresentation/opf/convert";
2
2
  import type { EditorChange, EditorSession, JsonPatchOperation } from "./index.js";
3
3
 
4
4
  /** The schema's DimensionPreset values: the values of the slide-sizes switch. */
5
- export declare const SLIDE_SIZE_PRESETS: readonly ["16:9", "4:3", "16:10", "letter", "a4", "widescreen", "standard"];
5
+ export declare const SLIDE_SIZE_PRESETS: readonly ["16:9", "4:3", "16:10", "1:1", "4:5", "9:16", "letter", "a4", "widescreen", "standard"];
6
6
  export type SlideSizePreset = (typeof SLIDE_SIZE_PRESETS)[number];
7
7
 
8
8
  /** Every switchable dimension: the 14 pptx.gallery dimensions, then `slide-sizes` and `purposes`. */
@@ -36,9 +36,9 @@ export interface DimensionSwitchOptions {
36
36
  /** Extra caller-loaded catalog records by kind. */
37
37
  catalogs?: Record<string, unknown>;
38
38
  catalogSources?: Record<string, unknown>;
39
- /** themes: also write the theme's color scheme, font scheme, background and dimensions to the deck design (default true). */
39
+ /** themes: also write the theme's color scheme, font scheme and background (and, for the deck, dimensions) to the design (default true). A slide's design cannot set dimensions. */
40
40
  bundle?: boolean;
41
- /** Deck-scope design switches (and slide-sizes): remove slide-level values that would hide the switch. */
41
+ /** Deck-scope design switches: remove slide-level values that would hide the switch. */
42
42
  clearSlideOverrides?: boolean;
43
43
  /** socials: which document field holds the handle (default "speaker") and, for an array, which entry (default 0). */
44
44
  owner?: "speaker" | "organization";
@@ -66,7 +66,7 @@ export interface PreparedDimensionSwitch {
66
66
  dimension: SwitchDimension;
67
67
  scope: "deck" | "slide" | "block";
68
68
  slideIndex?: number;
69
- document: unknown;
69
+ presentation: unknown;
70
70
  patches: JsonPatchOperation[];
71
71
  changed: boolean;
72
72
  /** Slides whose own design hides a deck-level switch. */
@@ -75,8 +75,8 @@ export interface PreparedDimensionSwitch {
75
75
  loss?: string[];
76
76
  }
77
77
 
78
- export interface DimensionSwitchChange extends Omit<EditorChange, "document" | "patches"> {
79
- document: unknown;
78
+ export interface DimensionSwitchChange extends Omit<EditorChange, "presentation" | "patches"> {
79
+ presentation: unknown;
80
80
  patches: JsonPatchOperation[];
81
81
  dimension: SwitchDimension;
82
82
  scope: "deck" | "slide" | "block";
@@ -87,7 +87,7 @@ export interface DimensionSwitchChange extends Omit<EditorChange, "document" | "
87
87
  }
88
88
 
89
89
  /** Compute and validate the patch for one dimension without changing any session. */
90
- export declare function prepareDimensionSwitch(document: unknown, dimension: SwitchDimension, value: DimensionSwitchValue, options?: DimensionSwitchOptions): PreparedDimensionSwitch;
90
+ export declare function prepareDimensionSwitch(presentation: unknown, dimension: SwitchDimension, value: DimensionSwitchValue, options?: DimensionSwitchOptions): PreparedDimensionSwitch;
91
91
  /** Apply one dimension switch to a session as a single undoable transaction. */
92
92
  export declare function switchDimension(editor: EditorSession, dimension: SwitchDimension, value: DimensionSwitchValue, options?: DimensionSwitchOptions): DimensionSwitchChange;
93
93
 
@@ -101,9 +101,9 @@ export interface CompatibleChartType extends SwitchOption {
101
101
  current: boolean;
102
102
  }
103
103
  /** Values a picker can offer for a catalog-backed dimension (document inline records, caller catalogs, then the bundled catalog, without duplicates); `blocks` lists the content kinds and `slide-sizes` the presets (labelled with their inches). `purposes` lists the catalog; any other goal text is also a valid switch value. */
104
- export declare function listSwitchOptions(document: unknown, dimension: SwitchDimension, options?: Pick<DimensionSwitchOptions, "catalogs" | "catalogSources">): SwitchOption[];
104
+ export declare function listSwitchOptions(presentation: unknown, dimension: SwitchDimension, options?: Pick<DimensionSwitchOptions, "catalogs" | "catalogSources">): SwitchOption[];
105
105
  /** Chart types the chart's inline data can use as it is (data-shape compatibility, not an engine-support claim). `path` or `slideIndex` picks the chart. */
106
- export declare function compatibleChartTypes(document: unknown, options?: Pick<DimensionSwitchOptions, "slideIndex" | "path" | "catalogs" | "catalogSources">): CompatibleChartType[];
106
+ export declare function compatibleChartTypes(presentation: unknown, options?: Pick<DimensionSwitchOptions, "slideIndex" | "path" | "catalogs" | "catalogSources">): CompatibleChartType[];
107
107
  /** The value a dimension currently has: `{ value, scope }`, with the catalog 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. */
108
- export declare function currentSwitchValue(document: unknown, dimension: SwitchDimension, options?: Pick<DimensionSwitchOptions, "slideIndex" | "path" | "owner" | "index">): { value: unknown; scope: "deck" | "slide" | "block" };
108
+ export declare function currentSwitchValue(presentation: unknown, dimension: SwitchDimension, options?: Pick<DimensionSwitchOptions, "slideIndex" | "path" | "owner" | "index">): { value: unknown; scope: "deck" | "slide" | "block" };
109
109
  export { blockConversionTargets } from "./block-convert.js";
package/dist/switches.js CHANGED
@@ -2,30 +2,26 @@
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
- import * as core from "@openpresentation/opf";
6
- import { catalogSchemaNames, catalogs as bundledCatalogs, schemas } from "@openpresentation/opf";
7
- import {
8
- OPFEditorError,
9
- createValuePatch,
10
- applyJsonPatch,
11
- getValueAtPath,
12
- opfPathToJsonPointer,
13
- splitOpfPath,
14
- validateOpfDocument,
15
- } from "./index.js";
5
+ import { catalogSchemaNames, catalogs as bundledCatalogs, isXYChartType, resolveChartData, schemas } from "@openpresentation/opf";
6
+ import { chartOptionTarget } from "@openpresentation/opf/composition";
7
+ import { OPFEditorError, createValuePatch, applyJsonPatch, getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
16
8
  import { createContentBlock, prepareBlockReplace } from "./blocks.js";
17
9
  import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
18
10
  import { populateLayoutPlaceholders } from "./layout-placeholders.js";
19
11
  import { blockConversionTargets, prepareBlockConversion } from "./block-convert.js";
12
+ import { checkFormat } from "./checks.js";
20
13
 
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"]);
14
+ /** The schema's DimensionPreset values (RR-41, FA-13: with the social-feed ratios), the values of the slide-sizes switch. */
15
+ export const SLIDE_SIZE_PRESETS = Object.freeze(["16:9", "4:3", "16:10", "1:1", "4:5", "9:16", "letter", "a4", "widescreen", "standard"]);
23
16
 
24
17
  // Slide size in inches per preset, as core's resolveCanvasDimensions composes it (for picker labels).
25
18
  const SLIDE_SIZE_LABELS = Object.freeze({
26
19
  "16:9": "16:9 (13.33 x 7.5 in)",
27
20
  "4:3": "4:3 (10 x 7.5 in)",
28
21
  "16:10": "16:10 (10 x 6.25 in)",
22
+ "1:1": "1:1 square (7.5 x 7.5 in)",
23
+ "4:5": "4:5 portrait (7.5 x 9.375 in)",
24
+ "9:16": "9:16 portrait (7.5 x 13.33 in)",
29
25
  letter: "Letter (11 x 8.5 in)",
30
26
  a4: "A4 (11.69 x 8.27 in)",
31
27
  widescreen: "Widescreen (13.33 x 7.5 in)",
@@ -90,55 +86,55 @@ function recordList(source) {
90
86
 
91
87
  // Inline document records first (they override), then caller-loaded records, then the
92
88
  // bundled catalog. Unlike getCatalogRecords, an inline catalog never hides bundled ids.
93
- function findCatalogRecord(document, kind, id, options) {
89
+ function findCatalogRecord(presentation, kind, id, options) {
94
90
  return (
95
- recordList(document.catalogs?.[kind]).find((record) => record.id === id) ??
91
+ recordList(presentation.catalogs?.[kind]).find((record) => record.id === id) ??
96
92
  recordList(options.catalogs?.[kind] ?? options.catalogSources?.[kind]).find((record) => record.id === id) ??
97
93
  (options.record?.id === id ? options.record : undefined) ??
98
94
  bundledCatalogs[kind]?.find((record) => record.id === id)
99
95
  );
100
96
  }
101
97
 
102
- function requireCatalogId(document, dimension, id, options) {
98
+ function requireCatalogId(presentation, dimension, id, options) {
103
99
  const kind = CATALOG_KIND[dimension];
104
100
  if (typeof id !== "string" || id.length === 0)
105
101
  throw fail("invalid-catalog-id", `Switch ${dimension} to a non-empty catalog id.`, { dimension, id });
106
- const record = findCatalogRecord(document, kind, id, options);
102
+ const record = findCatalogRecord(presentation, kind, id, options);
107
103
  if (!record) throw fail("unknown-catalog-id", `Unknown ${kind} catalog ID: ${id}.`, { dimension, catalogKind: kind, id });
108
104
  return record;
109
105
  }
110
106
 
111
107
  // A gallery item supplies its own record; add it inline in the same transaction when the
112
108
  // document and the bundled catalog do not already define the id.
113
- function catalogRecordPatches(document, dimension, options) {
109
+ function catalogRecordPatches(presentation, dimension, options) {
114
110
  const record = options.record;
115
111
  if (!record) return [];
116
112
  const kind = CATALOG_KIND[dimension];
117
- if (recordList(document.catalogs?.[kind]).some((entry) => entry.id === record.id) || bundledCatalogs[kind]?.some((entry) => entry.id === record.id)) return [];
113
+ if (recordList(presentation.catalogs?.[kind]).some((entry) => entry.id === record.id) || bundledCatalogs[kind]?.some((entry) => entry.id === record.id)) return [];
118
114
  const value = { $schema: schemas[catalogSchemaNames[kind]].$id, ...record };
119
- if (!document.catalogs || typeof document.catalogs !== "object") return [{ op: "add", path: "/catalogs", value: { [kind]: { records: [value] } } }];
120
- if (!document.catalogs[kind] || typeof document.catalogs[kind] !== "object")
115
+ if (!presentation.catalogs || typeof presentation.catalogs !== "object") return [{ op: "add", path: "/catalogs", value: { [kind]: { records: [value] } } }];
116
+ if (!presentation.catalogs[kind] || typeof presentation.catalogs[kind] !== "object")
121
117
  return [{ op: "add", path: `/catalogs/${kind}`, value: { records: [value] } }];
122
- if (!Array.isArray(document.catalogs[kind].records)) return [{ op: "add", path: `/catalogs/${kind}/records`, value: [value] }];
118
+ if (!Array.isArray(presentation.catalogs[kind].records)) return [{ op: "add", path: `/catalogs/${kind}/records`, value: [value] }];
123
119
  return [{ op: "add", path: `/catalogs/${kind}/records/-`, value }];
124
120
  }
125
121
 
126
- function slideAt(document, slideIndex, what = "this switch") {
127
- if (!Number.isInteger(slideIndex) || !document.slides?.[slideIndex])
122
+ function slideAt(presentation, slideIndex, what = "this switch") {
123
+ if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex])
128
124
  throw fail("slide-index-out-of-range", `Choose an existing slide for ${what}.`, { slideIndex });
129
- return document.slides[slideIndex];
125
+ return presentation.slides[slideIndex];
130
126
  }
131
127
 
132
- function rootPatch(document, field, value) {
133
- return same(document[field], value) ? [] : createValuePatch(document, [field], value);
128
+ function rootPatch(presentation, field, value) {
129
+ return same(presentation[field], value) ? [] : createValuePatch(presentation, [field], value);
134
130
  }
135
131
 
136
132
  // A deck-level switch does nothing for a slide that carries its own value for that key.
137
- function shadowedSlides(document, keys) {
138
- return (document.slides ?? []).flatMap((slide, index) => (keys.some((key) => slide?.design?.[key] !== undefined) ? [index] : []));
133
+ function shadowedSlides(presentation, keys) {
134
+ return (presentation.slides ?? []).flatMap((slide, index) => (keys.some((key) => slide?.design?.[key] !== undefined) ? [index] : []));
139
135
  }
140
136
 
141
- function findChartOwner(document, slideIndex) {
137
+ function findChartOwner(presentation, slideIndex) {
142
138
  const visit = (node, path, depth) => {
143
139
  if (!node || typeof node !== "object" || Array.isArray(node) || depth > 32) return undefined;
144
140
  if (node.chart && typeof node.chart === "object") return path;
@@ -154,23 +150,23 @@ function findChartOwner(document, slideIndex) {
154
150
  }
155
151
  return undefined;
156
152
  };
157
- return visit(document.slides[slideIndex], ["slides", String(slideIndex)], 0);
153
+ return visit(presentation.slides[slideIndex], ["slides", String(slideIndex)], 0);
158
154
  }
159
155
 
160
- function socialsPatches(document, value, options) {
156
+ function socialsPatches(presentation, value, options) {
161
157
  const owner = options.owner ?? "speaker";
162
158
  if (!["speaker", "organization"].includes(owner)) throw fail("invalid-switch-value", "Socials owner must be 'speaker' or 'organization'.", { owner });
163
159
  if (!value || typeof value !== "object" || typeof value.platform !== "string" || typeof value.handle !== "string" || !value.handle)
164
160
  throw fail("invalid-switch-value", "Switch socials to { platform, handle } with a non-empty handle.", { value });
165
- requireCatalogId(document, "socials", value.platform, options);
166
- const host = document[owner];
161
+ requireCatalogId(presentation, "socials", value.platform, options);
162
+ const host = presentation[owner];
167
163
  const index = Array.isArray(host) ? (options.index ?? 0) : undefined;
168
164
  const target = Array.isArray(host) ? host[index] : host;
169
165
  if (!target || typeof target !== "object")
170
166
  throw fail("missing-owner", `Add the ${owner} to the document before setting their socials.`, { owner });
171
167
  const base = Array.isArray(host) ? [owner, String(index)] : [owner];
172
168
  const socials = target.socials;
173
- const patches = catalogRecordPatches(document, "socials", options);
169
+ const patches = catalogRecordPatches(presentation, "socials", options);
174
170
  if (!socials || typeof socials !== "object") return [...patches, { op: "add", path: opfPathToJsonPointer([...base, "socials"]), value: { [value.platform]: value.handle } }];
175
171
  if (socials[value.platform] === value.handle) return patches;
176
172
  return [...patches, { op: Object.hasOwn(socials, value.platform) ? "replace" : "add", path: opfPathToJsonPointer([...base, "socials", value.platform]), value: value.handle }];
@@ -189,16 +185,16 @@ function blockValue(value, options) {
189
185
  * Compute the patch that switches one dimension, without touching any session. The candidate
190
186
  * document is validated; an invalid result throws unless the input was already invalid.
191
187
  */
192
- export function prepareDimensionSwitch(document, dimension, value, options = {}) {
188
+ export function prepareDimensionSwitch(presentation, dimension, value, options = {}) {
193
189
  if (!SWITCH_DIMENSIONS.includes(dimension))
194
190
  throw fail("unknown-dimension", `Unknown dimension: ${dimension}. Use one of ${SWITCH_DIMENSIONS.join(", ")}.`, { dimension });
195
- if (!document || typeof document !== "object") throw fail("invalid-input", "Switch requires an OPF document object.");
191
+ if (!presentation || typeof presentation !== "object") throw fail("invalid-input", "Switch requires an OPF document object.");
196
192
  if (options.record) {
197
193
  const ids = dimension === "audiences" ? [value].flat() : dimension === "socials" ? [value?.platform] : [value];
198
194
  if (!CATALOG_KIND[dimension] || !ids.includes(options.record.id))
199
195
  throw fail("record-id-mismatch", `The supplied record id must equal a switched ${dimension} id.`, { dimension, id: options.record.id });
200
196
  }
201
- const before = validateOpfDocument(document);
197
+ const before = checkFormat(presentation);
202
198
  const scopeIndex = options.slideIndex;
203
199
  let patches = [];
204
200
  let scope = "deck";
@@ -210,28 +206,24 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
210
206
  // A preset string; a custom size (design.dimensions as an object with inches) is replaced by it.
211
207
  if (!SLIDE_SIZE_PRESETS.includes(value)) throw fail("invalid-switch-value", `Switch slide-sizes to one of ${SLIDE_SIZE_PRESETS.join(", ")}.`, { value });
212
208
  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 = document.design?.dimensions;
209
+ const own = presentation.design?.dimensions;
214
210
  // {preset} alone is the same size as the bare preset string.
215
211
  const sameSize = own && typeof own === "object" && !Array.isArray(own) && own.preset === value && Object.keys(own).length === 1;
216
- patches = sameSize ? [] : designPatches(document, [], { dimensions: value });
217
- shadowed = shadowedSlides(document, ["dimensions"]);
218
- if (options.clearSlideOverrides) {
219
- for (const index of shadowed) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", "dimensions"]) });
220
- shadowed = [];
221
- }
212
+ // A slide's design cannot set dimensions (FA-07), so no slide can shadow the deck's size.
213
+ patches = sameSize ? [] : designPatches(presentation, [], { dimensions: value });
222
214
  } else if (dimension === "purposes") {
223
215
  // A catalog id, free-form goal text (no id check: any goal is valid) or an inline Purpose object; the schema validates it.
224
216
  if (typeof value !== "string" && !(value && typeof value === "object" && !Array.isArray(value)))
225
217
  throw fail("invalid-switch-value", "Switch purposes to a catalog id, a goal string or a purpose object.", { value });
226
218
  if (typeof value === "string" && value.length === 0) throw fail("invalid-switch-value", "Switch purposes to a non-empty goal.", { value });
227
- patches = [...catalogRecordPatches(document, dimension, options), ...rootPatch(document, "purpose", value)];
219
+ patches = [...catalogRecordPatches(presentation, dimension, options), ...rootPatch(presentation, "purpose", value)];
228
220
  } else if (dimension === "layouts") {
229
221
  slideIndex = scopeIndex;
230
- slideAt(document, slideIndex, "a layout switch");
222
+ slideAt(presentation, slideIndex, "a layout switch");
231
223
  scope = "slide";
232
- const record = requireCatalogId(document, dimension, value, options);
233
- patches = catalogRecordPatches(document, dimension, options);
234
- const withRecord = patches.length ? applyJsonPatch(document, patches) : document;
224
+ const record = requireCatalogId(presentation, dimension, value, options);
225
+ patches = catalogRecordPatches(presentation, dimension, options);
226
+ const withRecord = patches.length ? applyJsonPatch(presentation, patches) : presentation;
235
227
  if (withRecord.slides[slideIndex].layout !== value) {
236
228
  const layoutOps = createValuePatch(withRecord, ["slides", String(slideIndex), "layout"], value);
237
229
  const swapped = applyJsonPatch(withRecord, layoutOps);
@@ -244,64 +236,65 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
244
236
  }
245
237
  } else if (dimension === "charts") {
246
238
  slideIndex = scopeIndex;
247
- slideAt(document, slideIndex, "a chart switch");
239
+ slideAt(presentation, slideIndex, "a chart switch");
248
240
  scope = "slide";
249
- requireCatalogId(document, dimension, value, options);
250
- const owner = options.path ? splitOpfPath(options.path) : findChartOwner(document, slideIndex);
241
+ requireCatalogId(presentation, dimension, value, options);
242
+ const owner = options.path ? splitOpfPath(options.path) : findChartOwner(presentation, slideIndex);
251
243
  if (options.path && (owner[0] !== "slides" || owner[1] !== String(slideIndex)))
252
244
  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(document, [...owner, "chart"]);
245
+ const chart = owner && getValueAtPath(presentation, [...owner, "chart"]);
254
246
  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 = [...catalogRecordPatches(document, dimension, options), ...(chart.type === value ? [] : [...createValuePatch(document, [...owner, "chart", "type"], value), ...staleMappingPatches(document, owner, chart, value)])];
247
+ patches = [...catalogRecordPatches(presentation, dimension, options), ...(chart.type === value ? [] : [...createValuePatch(presentation, [...owner, "chart", "type"], value), ...staleMappingPatches(presentation, owner, chart, value), ...staleComboPatches(presentation, owner, chart, value)])];
256
248
  } else if (dimension === "blocks") {
257
249
  if (options.path === undefined) throw fail("missing-path", "Choose the block to replace with options.path.");
258
250
  // convert: true moves the block's own text into the new kind (block-convert.js) instead of replacing it.
259
251
  if (options.convert) {
260
252
  if (typeof value !== "string") throw fail("invalid-switch-value", "Convert a block to a content kind name.", { value });
261
- const conversion = prepareBlockConversion(document, options.path, value, options.conversion);
253
+ const conversion = prepareBlockConversion(presentation, options.path, value, options.conversion);
262
254
  patches = conversion.changed ? conversion.patches : [];
263
255
  conversionLoss = conversion.loss;
264
256
  } else {
265
257
  const block = blockValue(value, options);
266
- const change = prepareBlockReplace(document, options.path, block);
258
+ const change = prepareBlockReplace(presentation, options.path, block);
267
259
  patches = change.changed ? change.patches : [];
268
260
  }
269
261
  const parts = splitOpfPath(options.path);
270
262
  if (parts[0] === "slides") slideIndex = Number(parts[1]);
271
263
  scope = "block";
272
264
  } else if (dimension === "socials") {
273
- patches = socialsPatches(document, value, options);
265
+ patches = socialsPatches(presentation, value, options);
274
266
  } else if (ROOT_FIELD[dimension]) {
275
267
  const field = ROOT_FIELD[dimension];
276
268
  let next = value;
277
269
  if (dimension === "audiences") {
278
270
  next = Array.isArray(value) ? value : [value];
279
271
  if (!next.length) throw fail("invalid-catalog-id", "Switch audiences to at least one catalog id.", { value });
280
- for (const id of next) requireCatalogId(document, dimension, id, options);
281
- } else requireCatalogId(document, dimension, value, options);
282
- patches = [...catalogRecordPatches(document, dimension, options), ...rootPatch(document, field, next)];
272
+ for (const id of next) requireCatalogId(presentation, dimension, id, options);
273
+ } else requireCatalogId(presentation, dimension, value, options);
274
+ patches = [...catalogRecordPatches(presentation, dimension, options), ...rootPatch(presentation, field, next)];
283
275
  } else {
284
276
  // Design dimensions: deck scope by default, or one slide with options.slideIndex.
285
277
  let entries;
286
278
  if (dimension === "themes") {
287
- const record = requireCatalogId(document, dimension, value, options);
279
+ const record = requireCatalogId(presentation, dimension, value, options);
288
280
  entries = { theme: value };
289
281
  if (options.bundle !== false) {
290
282
  // The gallery's theme snippet writes the whole bundle, so an explicit deck choice
291
283
  // does not keep the previous theme's fonts, colors or background.
292
- for (const key of ["colorScheme", "fontScheme", "background", "dimensions"]) if (record[key] !== undefined) entries[key] = record[key];
284
+ // A slide's design cannot set dimensions (a PPTX has one slide size), so only a deck-scope switch writes the theme's.
285
+ for (const key of ["colorScheme", "fontScheme", "background", ...(scopeIndex === undefined ? ["dimensions"] : [])]) if (record[key] !== undefined) entries[key] = record[key];
293
286
  }
294
- patches = catalogRecordPatches(document, dimension, options);
287
+ patches = catalogRecordPatches(presentation, dimension, options);
295
288
  } else if (dimension === "color-schemes" || dimension === "font-schemes") {
296
- requireCatalogId(document, dimension, value, options);
289
+ requireCatalogId(presentation, dimension, value, options);
297
290
  entries = { [DESIGN_KEYS[dimension][0]]: value };
298
291
  if (dimension === "font-schemes") {
299
292
  // 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 && document.slides?.[scopeIndex] ? ["slides", String(scopeIndex)] : [];
301
- const own = getValueAtPath(document, [...scopeBase, "design", "fontScheme"]);
293
+ const scopeBase = scopeIndex !== undefined && presentation.slides?.[scopeIndex] ? ["slides", String(scopeIndex)] : [];
294
+ const own = getValueAtPath(presentation, [...scopeBase, "design", "fontScheme"]);
302
295
  if (own && typeof own === "object" && own.accent !== undefined) entries.fontScheme = { id: value, accent: structuredClone(own.accent) };
303
296
  }
304
- patches = catalogRecordPatches(document, dimension, options);
297
+ patches = catalogRecordPatches(presentation, dimension, options);
305
298
  } else {
306
299
  // A background is an object or a shorthand string (theme slot or hex color); the schema
307
300
  // validates the candidate document, so a string that is neither is rejected below.
@@ -318,29 +311,29 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
318
311
  }
319
312
  let base = [];
320
313
  if (scopeIndex !== undefined) {
321
- slideAt(document, scopeIndex, `a ${dimension} switch`);
314
+ slideAt(presentation, scopeIndex, `a ${dimension} switch`);
322
315
  base = ["slides", String(scopeIndex)];
323
316
  scope = "slide";
324
317
  slideIndex = scopeIndex;
325
318
  }
326
- patches = [...patches, ...designPatches(document, base, entries)];
319
+ patches = [...patches, ...designPatches(presentation, base, entries)];
327
320
  if (scope === "deck") {
328
321
  const keys = Object.entries(entries).filter(([, entry]) => entry !== undefined).map(([key]) => key);
329
- shadowed = shadowedSlides(document, keys);
322
+ shadowed = shadowedSlides(presentation, keys);
330
323
  if (options.clearSlideOverrides)
331
324
  for (const index of shadowed)
332
325
  for (const key of keys)
333
- if (document.slides[index].design?.[key] !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", key]) });
326
+ if (presentation.slides[index].design?.[key] !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", key]) });
334
327
  if (options.clearSlideOverrides) shadowed = [];
335
328
  }
336
329
  }
337
330
 
338
- const next = checkedDocument(document, patches, before);
331
+ const next = checkedDocument(presentation, patches, before);
339
332
  return {
340
333
  dimension,
341
334
  scope,
342
335
  ...(slideIndex !== undefined ? { slideIndex } : {}),
343
- document: structuredClone(next),
336
+ presentation: structuredClone(next),
344
337
  patches,
345
338
  changed: patches.length > 0,
346
339
  shadowed,
@@ -357,17 +350,17 @@ export function switchDimension(editor, dimension, value, options = {}) {
357
350
  if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function")
358
351
  throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
359
352
  const { meta, ...switchOptions } = options;
360
- const prepared = prepareDimensionSwitch(editor.document, dimension, value, switchOptions);
361
- const { document, patches, ...summary } = prepared;
362
- void document;
363
- if (!prepared.changed) return { ...summary, document: editor.document, patches: [], inversePatches: [], validation: editor.validation };
353
+ const prepared = prepareDimensionSwitch(editor.presentation, dimension, value, switchOptions);
354
+ const { presentation, patches, ...summary } = prepared;
355
+ void presentation;
356
+ if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
364
357
  const change = editor.applyPatch(patches, { ...meta, source: meta?.source ?? "dimension-switch", dimension, scope: prepared.scope });
365
358
  return { ...change, ...summary };
366
359
  }
367
360
 
368
361
  // --- options for pickers and current values ---------------------------------------------------
369
362
 
370
- const labelOf = (record) => record.label ?? record.name ?? record.id;
363
+ const labelOf = (record) => record.name ?? record.id;
371
364
 
372
365
  /**
373
366
  * The values a picker can offer for a catalog-backed dimension, in document order: the document's
@@ -375,14 +368,14 @@ const labelOf = (record) => record.label ?? record.name ?? record.id;
375
368
  * without duplicates. `blocks` lists the content kinds. `charts` lists every chart type; use
376
369
  * `compatibleChartTypes` to narrow it to the types the chart's data can use.
377
370
  */
378
- export function listSwitchOptions(document, dimension, options = {}) {
371
+ export function listSwitchOptions(presentation, dimension, options = {}) {
379
372
  if (dimension === "blocks") return BLOCK_KINDS.map((kind) => ({ id: kind, label: kind[0].toUpperCase() + kind.slice(1) }));
380
373
  if (dimension === "slide-sizes") return SLIDE_SIZE_PRESETS.map((preset) => ({ id: preset, label: SLIDE_SIZE_LABELS[preset] }));
381
374
  const kind = CATALOG_KIND[dimension];
382
375
  if (!kind) return [];
383
376
  const seen = new Set();
384
377
  const out = [];
385
- for (const source of [document?.catalogs?.[kind], options.catalogs?.[kind] ?? options.catalogSources?.[kind], bundledCatalogs[kind]])
378
+ for (const source of [presentation?.catalogs?.[kind], options.catalogs?.[kind] ?? options.catalogSources?.[kind], bundledCatalogs[kind]])
386
379
  for (const record of recordList(source)) {
387
380
  if (seen.has(record.id)) continue;
388
381
  seen.add(record.id);
@@ -393,27 +386,38 @@ export function listSwitchOptions(document, dimension, options = {}) {
393
386
 
394
387
  const SINGLE_SERIES_ONLY = new Set(["pieChart", "doughnutChart", "funnelChart", "treemapChart", "waterfallChart"]);
395
388
  const MULTI_SERIES_CAPABLE = new Set(["barChart", "lineChart", "areaChart", "radarChart"]);
389
+ const STACKED_GROUPINGS = new Set(["stacked", "percentStacked"]);
396
390
  const DISTRIBUTION_ELEMENTS = new Set(["histogramChart", "boxWhiskerChart", "mapChart"]);
397
391
 
398
392
  // 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
393
  // 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(document, owner, chart, type) {
394
+ function staleMappingPatches(presentation, owner, chart, type) {
401
395
  const mapping = chart.mapping;
402
396
  if (!mapping || typeof mapping !== "object" || Array.isArray(mapping) || mapping.x === undefined) return [];
403
- if (typeof core.isXYChartType !== "function" || core.isXYChartType(type)) return [];
397
+ if (isXYChartType(type)) return [];
404
398
  const { x: _x, ...rest } = mapping;
405
399
  const parts = [...owner, "chart", "mapping"];
406
- return Object.keys(rest).length ? createValuePatch(document, parts, rest) : [{ op: "remove", path: opfPathToJsonPointer(parts) }];
400
+ return Object.keys(rest).length ? createValuePatch(presentation, parts, rest) : [{ op: "remove", path: opfPathToJsonPointer(parts) }];
407
401
  }
408
402
 
409
- function chartDataShape(chart, document) {
403
+ // FA-15: `line`, `secondaryAxis` and the secondary axis title belong to combo charts; any other type ignores them (core warns
404
+ // chart-option-adapted). Switching away from combo removes them, and an `axisTitles` left empty.
405
+ function staleComboPatches(presentation, owner, chart, type) {
406
+ if (chartOptionTarget(type)?.kind === "combo") return [];
407
+ const parts = [...owner, "chart"];
408
+ const patches = ["line", "secondaryAxis"].filter((key) => chart[key] !== undefined).map((key) => ({ op: "remove", path: opfPathToJsonPointer([...parts, key]) }));
409
+ const titles = chart.axisTitles;
410
+ if (titles && typeof titles === "object" && !Array.isArray(titles) && titles.secondary !== undefined) {
411
+ const { secondary: _secondary, ...rest } = titles;
412
+ patches.push(Object.keys(rest).length ? { op: "remove", path: opfPathToJsonPointer([...parts, "axisTitles", "secondary"]) } : { op: "remove", path: opfPathToJsonPointer([...parts, "axisTitles"]) });
413
+ }
414
+ return patches;
415
+ }
416
+
417
+ function chartDataShape(chart, presentation) {
410
418
  // RR-54: inline data, a dataset reference and a series mapping all resolve to the columns the renderers read: the first column labels
411
419
  // the categories and every further column is a series.
412
- if (typeof core.resolveChartData !== "function") {
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);
420
+ const resolved = resolveChartData(chart, presentation);
417
421
  if (!resolved.ok) return undefined;
418
422
  return { series: Math.max(0, resolved.columns.length - 1), categories: resolved.rows.length };
419
423
  }
@@ -422,25 +426,34 @@ function chartDataShape(chart, document) {
422
426
  * Chart types the chart's inline data can use as it is, from the chartTypes catalog: simple,
423
427
  * non-geographic, non-distribution types whose series count fits the data (the first column labels
424
428
  * the categories and each further column is a series; a type with N series needs exactly N value
425
- * columns; column, bar, line, area and radar take any number). Data that is read from
429
+ * columns, except that a stacked or percent-stacked type and a combination such as combo take N or more;
430
+ * column, bar, line, area and radar take any number). Data that is read from
426
431
  * an external source returns every simple type. This is data-shape compatibility, not a claim that
427
432
  * an engine draws the type. `path` or `slideIndex` picks the chart (default: the slide's first).
428
433
  * Each entry has `current: true` for the chart's present type, which is always listed.
429
434
  */
430
- export function compatibleChartTypes(document, options = {}) {
435
+ export function compatibleChartTypes(presentation, options = {}) {
431
436
  const slideIndex = options.slideIndex ?? 0;
432
- const owner = options.path ? splitOpfPath(options.path) : document.slides?.[slideIndex] ? findChartOwner(document, slideIndex) : undefined;
433
- const chart = owner && getValueAtPath(document, [...owner, "chart"]);
437
+ const owner = options.path ? splitOpfPath(options.path) : presentation.slides?.[slideIndex] ? findChartOwner(presentation, slideIndex) : undefined;
438
+ const chart = owner && getValueAtPath(presentation, [...owner, "chart"]);
434
439
  if (!chart || typeof chart !== "object") return [];
435
- const shape = chartDataShape(chart, document);
440
+ const shape = chartDataShape(chart, presentation);
436
441
  const result = [];
437
- for (const option of listSwitchOptions(document, "charts", options)) {
442
+ for (const option of listSwitchOptions(presentation, "charts", options)) {
438
443
  const record = option.record;
439
444
  const element = record.mappings?.openxml?.element;
440
445
  const current = option.id === chart.type;
441
- const simple = record.complexity === "simple" && record.mappings?.openxml?.composition !== "mixed" && !DISTRIBUTION_ELEMENTS.has(element);
446
+ const simple = record.complexity === "simple" && !DISTRIBUTION_ELEMENTS.has(element);
447
+ // A combination (composition "mixed", the FA-15 combo chart) needs at least its series count, like a stacked type.
448
+ const atLeast = STACKED_GROUPINGS.has(record.mappings?.openxml?.grouping) || record.mappings?.openxml?.composition === "mixed";
442
449
  const seriesOk =
443
- !shape || !record.series || (record.series > 1 ? record.series === shape.series : shape.series === 1 || (MULTI_SERIES_CAPABLE.has(element) && !SINGLE_SERIES_ONLY.has(element)));
450
+ !shape ||
451
+ !record.series ||
452
+ (record.series > 1
453
+ ? atLeast
454
+ ? shape.series >= record.series
455
+ : record.series === shape.series
456
+ : shape.series === 1 || (MULTI_SERIES_CAPABLE.has(element) && !SINGLE_SERIES_ONLY.has(element)));
444
457
  if (current || (simple && seriesOk)) result.push({ id: option.id, label: option.label, current, record });
445
458
  }
446
459
  return result;
@@ -452,43 +465,40 @@ export function compatibleChartTypes(document, options = {}) {
452
465
  * dimension is unset. Catalog dimensions return the catalog id even when the document holds an
453
466
  * inline object.
454
467
  */
455
- export function currentSwitchValue(document, dimension, options = {}) {
468
+ export function currentSwitchValue(presentation, dimension, options = {}) {
456
469
  const idOf = (reference) => (reference && typeof reference === "object" && !Array.isArray(reference) ? reference.id : reference);
457
470
  const design = (key) => {
458
- const slide = options.slideIndex !== undefined ? document.slides?.[options.slideIndex]?.design?.[key] : undefined;
471
+ const slide = options.slideIndex !== undefined ? presentation.slides?.[options.slideIndex]?.design?.[key] : undefined;
459
472
  if (slide !== undefined) return { value: slide, scope: "slide" };
460
- return document.design?.[key] === undefined ? { scope: "deck" } : { value: document.design[key], scope: "deck" };
473
+ return presentation.design?.[key] === undefined ? { scope: "deck" } : { value: presentation.design[key], scope: "deck" };
461
474
  };
462
- if (dimension === "layouts") return { value: document.slides?.[options.slideIndex]?.layout, scope: "slide" };
475
+ if (dimension === "layouts") return { value: presentation.slides?.[options.slideIndex]?.layout, scope: "slide" };
463
476
  if (dimension === "charts") {
464
- const owner = options.path ? splitOpfPath(options.path) : findChartOwner(document, options.slideIndex ?? 0);
465
- return { value: owner ? getValueAtPath(document, [...owner, "chart", "type"]) : undefined, scope: "slide" };
477
+ const owner = options.path ? splitOpfPath(options.path) : findChartOwner(presentation, options.slideIndex ?? 0);
478
+ return { value: owner ? getValueAtPath(presentation, [...owner, "chart", "type"]) : undefined, scope: "slide" };
466
479
  }
467
480
  if (dimension === "blocks") return { value: undefined, scope: "block" };
468
481
  if (dimension === "slide-sizes") {
469
482
  // The size the deck composes at: its own design.dimensions, else the theme's; a {preset} object reads as its preset.
470
483
  // A custom size (inches without a preset) reads as the object itself. Unset reads as undefined (composed as widescreen).
471
- const own = design("dimensions");
472
- let size = own.value;
473
- let scope = own.scope;
484
+ let size = presentation.design?.dimensions;
474
485
  if (size === undefined) {
475
- const themeId = idOf(design("theme").value);
476
- size = themeId ? findCatalogRecord(document, "themes", themeId, {})?.dimensions : undefined;
477
- scope = "deck";
486
+ const themeId = idOf(presentation.design?.theme);
487
+ size = themeId ? findCatalogRecord(presentation, "themes", themeId, {})?.dimensions : undefined;
478
488
  }
479
- return { value: size && typeof size === "object" && !Array.isArray(size) && Object.keys(size).length === 1 && size.preset ? size.preset : size, scope };
489
+ return { value: size && typeof size === "object" && !Array.isArray(size) && Object.keys(size).length === 1 && size.preset ? size.preset : size, scope: "deck" };
480
490
  }
481
491
  if (dimension === "purposes") {
482
492
  // A catalog id or goal text reads as itself, a Purpose object as its id (else the object).
483
- const purpose = document.purpose;
493
+ const purpose = presentation.purpose;
484
494
  return { value: purpose && typeof purpose === "object" && !Array.isArray(purpose) ? (purpose.id ?? purpose) : purpose, scope: "deck" };
485
495
  }
486
496
  if (dimension === "socials") {
487
- const host = document[options.owner ?? "speaker"];
497
+ const host = presentation[options.owner ?? "speaker"];
488
498
  const target = Array.isArray(host) ? host[options.index ?? 0] : host;
489
499
  return { value: target?.socials, scope: "deck" };
490
500
  }
491
- if (ROOT_FIELD[dimension]) return { value: document[ROOT_FIELD[dimension]], scope: "deck" };
501
+ if (ROOT_FIELD[dimension]) return { value: presentation[ROOT_FIELD[dimension]], scope: "deck" };
492
502
  if (dimension === "color-schemes") return { ...design("colorScheme"), value: idOf(design("colorScheme").value) };
493
503
  if (dimension === "font-schemes") return { ...design("fontScheme"), value: idOf(design("fontScheme").value) };
494
504
  if (dimension === "themes") return { ...design("theme"), value: idOf(design("theme").value) };