@openpresentation/opf-editor 0.11.3 → 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 +82 -58
  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 +23 -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 +12 -10
  17. package/dist/chart-data.js +22 -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 +179 -10
  28. package/dist/data.d.ts +23 -2
  29. package/dist/data.js +40 -1
  30. package/dist/dataset-refs.d.ts +17 -0
  31. package/dist/dataset-refs.js +45 -0
  32. package/dist/design-controls.js +66 -47
  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 +35 -20
  42. package/dist/font-gate.js +49 -58
  43. package/dist/galleries.js +36 -52
  44. package/dist/grid-model.d.ts +99 -24
  45. package/dist/grid-model.js +577 -101
  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 +136 -109
  84. package/dist/table-options.d.ts +8 -8
  85. package/dist/table-options.js +41 -30
  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 +74 -54
  94. package/package.json +10 -6
  95. package/dist/font-defaults.js +0 -25
package/dist/templates.js CHANGED
@@ -2,12 +2,13 @@
2
2
  // with variables; filling it resolves the variables to a concrete deck (core resolveVariables).
3
3
  // Everything here reads and writes the document through the session, so a fill, a declaration
4
4
  // and a token insertion are validated, undoable JSON Patch edits like any other. Importing this
5
- // module needs no DOM. Core is read from the namespace so an older core still loads this module;
6
- // the functions then throw "templates-unavailable".
7
- import * as core from "@openpresentation/opf";
5
+ // module needs no DOM.
6
+ import { coerceVariableValue, hasContentVariables, listBuiltinVariables, listVariables, resolveVariables, variableDeclarations } from "@openpresentation/opf";
8
7
  import { OPFEditorError, getValueAtPath, opfPathToJsonPointer } from "./index.js";
9
8
 
10
9
  const VARIABLE_ID = /^[a-z][a-z0-9-]*$/;
10
+ // A built-in variable name (core FA-04): `speakers`, or deck/speaker/organization plus one or two dotted segments.
11
+ const BUILTIN_NAME = /^(?:speakers|(?:deck|speaker|organization)(?:.[A-Za-z0-9_-]+){1,2})$/;
11
12
  const KINDS = ["color", "text", "number", "date", "image", "url", "list"];
12
13
 
13
14
  /** The form control each variable kind uses. */
@@ -25,12 +26,6 @@ function fail(code, message, details) {
25
26
  return new OPFEditorError(code, message, details);
26
27
  }
27
28
 
28
- function requireCore() {
29
- if (typeof core.resolveVariables !== "function" || typeof core.listVariables !== "function") {
30
- throw fail("templates-unavailable", "Templates need a core release that ships resolveVariables (@openpresentation/opf after 0.11.4).");
31
- }
32
- }
33
-
34
29
  function checkEditor(editor) {
35
30
  if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function") {
36
31
  throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
@@ -39,19 +34,22 @@ function checkEditor(editor) {
39
34
 
40
35
  const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
41
36
 
42
- /** True when the core this editor runs on can resolve template variables. */
43
- export function templatesAvailable() {
44
- return typeof core.resolveVariables === "function" && typeof core.listVariables === "function";
45
- }
46
-
47
37
  /** True when the document is marked as a template (`template: true`). */
48
- export function isTemplateDocument(document) {
49
- return isObject(document) && document.template === true;
38
+ export function isTemplateDocument(presentation) {
39
+ return isObject(presentation) && presentation.template === true;
50
40
  }
51
41
 
52
42
  /** True when the document is a template or declares a variable that is not a color. */
53
- export function hasTemplateVariables(document) {
54
- return templatesAvailable() && core.hasContentVariables(document);
43
+ export function hasTemplateVariables(presentation) {
44
+ return hasContentVariables(presentation);
45
+ }
46
+
47
+ /**
48
+ * The presentation's built-in variables (read-only values from its own speaker, organization and deck metadata) with
49
+ * kind, label, current value, whether the presentation has a source value and where each is used.
50
+ */
51
+ export function listBuiltins(presentation) {
52
+ return listBuiltinVariables(presentation);
55
53
  }
56
54
 
57
55
  function humanize(id) {
@@ -80,10 +78,9 @@ export function valueToFieldText(kind, value) {
80
78
  * applies again). Returns `{ ok: true, value }` (value undefined when cleared) or `{ ok: false, message }`.
81
79
  */
82
80
  export function fieldTextToValue(kind, text) {
83
- requireCore();
84
81
  if (typeof text !== "string") return { ok: false, message: "Expected text." };
85
82
  if (text.trim() === "") return { ok: true, value: undefined };
86
- const coerced = core.coerceVariableValue(kind, kind === "text" ? text : text.trim());
83
+ const coerced = coerceVariableValue(kind, kind === "text" ? text : text.trim());
87
84
  return coerced.ok ? { ok: true, value: coerced.value } : { ok: false, message: `${coerced.message}.` };
88
85
  }
89
86
 
@@ -92,10 +89,9 @@ export function fieldTextToValue(kind, text) {
92
89
  * `status` is "filled" (a value was supplied), "default" (the declaration has a value), "unfilled"
93
90
  * (required, no value) or "optional" (not required, no value).
94
91
  */
95
- export function listTemplateFields(document, values = {}) {
96
- requireCore();
97
- const declared = new Map(core.variableDeclarations(document).map((declaration) => [declaration.id, declaration]));
98
- return core.listVariables(document, values).map((info) => {
92
+ export function listTemplateFields(presentation, values = {}) {
93
+ const declared = new Map(variableDeclarations(presentation).map((declaration) => [declaration.id, declaration]));
94
+ return listVariables(presentation, values).map((info) => {
99
95
  const declaration = declared.get(info.id);
100
96
  const supplied = Object.hasOwn(values, info.id) && values[info.id] !== undefined && values[info.id] !== null;
101
97
  const defaultValue = declaration?.value;
@@ -122,12 +118,12 @@ export function listTemplateFields(document, values = {}) {
122
118
  }
123
119
 
124
120
  /** The summary a panel shows: how many required variables still need a value. */
125
- export function templateStatus(document, values = {}) {
126
- const fields = listTemplateFields(document, values);
121
+ export function templateStatus(presentation, values = {}) {
122
+ const fields = listTemplateFields(presentation, values);
127
123
  const required = fields.filter((field) => field.required);
128
124
  const unfilled = fields.filter((field) => field.status === "unfilled").map((field) => field.id);
129
125
  return {
130
- template: isTemplateDocument(document),
126
+ template: isTemplateDocument(presentation),
131
127
  fieldCount: fields.length,
132
128
  requiredCount: required.length,
133
129
  filledRequiredCount: required.length - unfilled.length,
@@ -140,9 +136,8 @@ export function templateStatus(document, values = {}) {
140
136
  * Preview data for the supplied values: the concrete deck with each unfilled variable's example (or its
141
137
  * token when it has none). Never throws for missing values; the diagnostics say what was unfilled.
142
138
  */
143
- export function previewTemplate(document, values = {}) {
144
- requireCore();
145
- return core.resolveVariables(document, values, { examples: true, partial: true });
139
+ export function previewTemplate(presentation, values = {}) {
140
+ return resolveVariables(presentation, values, { examples: true, partial: true });
146
141
  }
147
142
 
148
143
  /**
@@ -151,34 +146,33 @@ export function previewTemplate(document, values = {}) {
151
146
  * document, `subscribe` notifies listeners (a panel redraws on it).
152
147
  */
153
148
  export function createTemplateFill(editor, options = {}) {
154
- requireCore();
155
149
  checkEditor(editor);
156
150
  let values = {};
157
151
  const listeners = new Set();
158
152
  const emit = () => {
159
153
  for (const listener of listeners) listener(api);
160
154
  };
161
- const declared = () => new Set(core.variableDeclarations(editor.document).map((declaration) => declaration.id));
162
- const kindOf = (id) => core.variableDeclarations(editor.document).find((declaration) => declaration.id === id)?.kind;
155
+ const declared = () => new Set(variableDeclarations(editor.presentation).map((declaration) => declaration.id));
156
+ const kindOf = (id) => variableDeclarations(editor.presentation).find((declaration) => declaration.id === id)?.kind;
163
157
  const api = {
164
158
  get values() {
165
159
  return structuredClone(values);
166
160
  },
167
161
  fields() {
168
- return listTemplateFields(editor.document, values);
162
+ return listTemplateFields(editor.presentation, values);
169
163
  },
170
164
  status() {
171
- return templateStatus(editor.document, values);
165
+ return templateStatus(editor.presentation, values);
172
166
  },
173
167
  preview() {
174
- return previewTemplate(editor.document, values);
168
+ return previewTemplate(editor.presentation, values);
175
169
  },
176
170
  set(id, value) {
177
171
  if (!declared().has(id)) throw fail("unknown-variable", `No variable '${id}' is declared.`, { id });
178
172
  if (value === undefined || value === null) {
179
173
  delete values[id];
180
174
  } else {
181
- const coerced = core.coerceVariableValue(kindOf(id), value);
175
+ const coerced = coerceVariableValue(kindOf(id), value);
182
176
  if (!coerced.ok) throw fail("invalid-variable-value", `Value for '${id}': ${coerced.message}.`, { id });
183
177
  values[id] = coerced.value;
184
178
  }
@@ -218,8 +212,8 @@ export function createTemplateFill(editor, options = {}) {
218
212
  * `partial` is set (the unfilled ones stay declared for a later pass).
219
213
  */
220
214
  apply({ partial = false, meta = {} } = {}) {
221
- const document = editor.document;
222
- const result = core.resolveVariables(document, values, { partial, template: false });
215
+ const presentation = editor.presentation;
216
+ const result = resolveVariables(presentation, values, { partial, template: false });
223
217
  const errors = result.diagnostics.filter((entry) => entry.severity === "error");
224
218
  if (errors.length) {
225
219
  const unfilled = errors.some((entry) => entry.code === "variable-unfilled");
@@ -239,27 +233,27 @@ export function createTemplateFill(editor, options = {}) {
239
233
  return api;
240
234
  }
241
235
 
242
- /** The token text for a variable: `{{id}}`, or `{{id|format}}` with a one-off format. */
236
+ /** The token text for a variable or a built-in name: `{{id}}`, or `{{id|format}}` with a one-off format. */
243
237
  export function variableToken(id, format) {
244
- if (!VARIABLE_ID.test(id)) throw fail("invalid-variable-id", "A variable id is lowercase kebab-case: letters, digits and hyphens, starting with a letter.", { id });
238
+ if (!VARIABLE_ID.test(id) && !BUILTIN_NAME.test(id)) throw fail("invalid-variable-id", "A variable id is lowercase kebab-case: letters, digits and hyphens, starting with a letter.", { id });
245
239
  return format ? `{{${id}|${format}}}` : `{{${id}}}`;
246
240
  }
247
241
 
248
242
  /** An unused variable id derived from a label, for a "new variable" form. */
249
- export function suggestVariableId(document, label = "value") {
243
+ export function suggestVariableId(presentation, label = "value") {
250
244
  const base = String(label).normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "") || "value";
251
- const taken = new Set(isObject(document?.variables) ? Object.keys(document.variables) : []);
245
+ const taken = new Set(isObject(presentation?.variables) ? Object.keys(presentation.variables) : []);
252
246
  if (!taken.has(base)) return base;
253
247
  for (let n = 2; ; n++) if (!taken.has(`${base}-${n}`)) return `${base}-${n}`;
254
248
  }
255
249
 
256
- function declarationPatches(document, id, declaration) {
250
+ function declarationPatches(presentation, id, declaration) {
257
251
  if (!VARIABLE_ID.test(id)) throw fail("invalid-variable-id", "A variable id is lowercase kebab-case: letters, digits and hyphens, starting with a letter.", { id });
258
252
  const entry = typeof declaration === "string" ? declaration : structuredClone(declaration);
259
253
  if (typeof entry !== "string" && !(isObject(entry) && KINDS.includes(entry.type))) {
260
254
  throw fail("invalid-variable", `A variable declaration is a hex color or an object whose type is one of ${KINDS.join(", ")}.`, { id });
261
255
  }
262
- const variables = document.variables;
256
+ const variables = presentation.variables;
263
257
  if (variables !== undefined && !isObject(variables)) throw fail("invalid-variable", "The document's variables field is not an object.", { id });
264
258
  if (variables && Object.hasOwn(variables, id)) throw fail("variable-exists", `A variable '${id}' is already declared.`, { id });
265
259
  return variables === undefined
@@ -269,22 +263,21 @@ function declarationPatches(document, id, declaration) {
269
263
 
270
264
  /** Declare a variable as one undoable edit. The resulting document must validate (a required variable with no value needs `template: true`). */
271
265
  export function declareVariable(editor, id, declaration, meta = {}) {
272
- requireCore();
273
266
  checkEditor(editor);
274
- return editor.applyPatch(declarationPatches(editor.document, id, declaration), { source: "template-variable", rejectInvalid: true, ...meta });
267
+ return editor.applyPatch(declarationPatches(editor.presentation, id, declaration), { source: "template-variable", rejectInvalid: true, ...meta });
275
268
  }
276
269
 
277
270
  /** Mark the document as a template (or, with `false`, as a normal deck) as one undoable edit. */
278
271
  export function setTemplate(editor, enabled, meta = {}) {
279
272
  checkEditor(editor);
280
- const document = editor.document;
281
- const present = Object.hasOwn(document, "template");
273
+ const presentation = editor.presentation;
274
+ const present = Object.hasOwn(presentation, "template");
282
275
  const patches = enabled
283
276
  ? [{ op: present ? "replace" : "add", path: "/template", value: true }]
284
277
  : present
285
278
  ? [{ op: "remove", path: "/template" }]
286
279
  : [];
287
- if (!patches.length) return { document, patches: [], inversePatches: [], validation: editor.validation };
280
+ if (!patches.length) return { presentation, patches: [], inversePatches: [], validation: editor.validation };
288
281
  return editor.applyPatch(patches, { source: "template-mode", rejectInvalid: true, ...meta });
289
282
  }
290
283
 
@@ -295,18 +288,18 @@ export function setTemplate(editor, enabled, meta = {}) {
295
288
  * the declaration, and both changes commit together.
296
289
  */
297
290
  export function insertVariableToken(editor, path, id, { start, end, runIndex, format, declare, meta = {} } = {}) {
298
- requireCore();
299
291
  checkEditor(editor);
300
- const document = editor.document;
292
+ const presentation = editor.presentation;
301
293
  const token = variableToken(id, format);
302
294
  const patches = [];
303
- const declaredNow = isObject(document.variables) && Object.hasOwn(document.variables, id);
295
+ // A built-in needs no declaration; an unknown built-in path fails the validated edit.
296
+ const declaredNow = BUILTIN_NAME.test(id) || (isObject(presentation.variables) && Object.hasOwn(presentation.variables, id));
304
297
  if (!declaredNow) {
305
298
  if (declare === undefined) throw fail("unknown-variable", `No variable '${id}' is declared. Declare it first, or pass its declaration.`, { id });
306
- patches.push(...declarationPatches(document, id, declare));
299
+ patches.push(...declarationPatches(presentation, id, declare));
307
300
  }
308
301
  const pointer = opfPathToJsonPointer(path);
309
- const current = getValueAtPath(document, path);
302
+ const current = getValueAtPath(presentation, path);
310
303
  const splice = (text) => {
311
304
  const from = start === undefined ? text.length : start;
312
305
  const to = end === undefined ? from : end;
@@ -2,13 +2,13 @@ export declare const MAX_OPF_BYTES: number;
2
2
  export interface OpfTransfer {
3
3
  kind: "presentation" | "slides" | "selection";
4
4
  value: unknown;
5
- document?: any;
5
+ presentation?: any;
6
6
  }
7
- export declare function assertOpf(document: any): any;
7
+ export declare function assertOpf(presentation: any): any;
8
8
  export declare function unwrapOpf(value: any): any;
9
9
  export declare function parseOpfTransfer(text: string): OpfTransfer;
10
10
  export declare function serializeOpfTransfer(
11
- document: any,
11
+ presentation: any,
12
12
  options?: {
13
13
  scope?: "presentation" | "slide" | "selection";
14
14
  slideIndex?: number;
@@ -24,4 +24,4 @@ export declare function prepareOpfImport(
24
24
  slideIndex?: number;
25
25
  path?: string;
26
26
  },
27
- ): { document: any; slideIndex: number };
27
+ ): { presentation: any; slideIndex: number };
package/dist/transfer.js CHANGED
@@ -1,20 +1,16 @@
1
- import {
2
- applyJsonPatch,
3
- createValuePatch,
4
- getValueAtPath,
5
- validateOpfDocument,
6
- } from "./index.js";
7
- import { catalogs } from "@openpresentation/opf";
1
+ import { applyJsonPatch, createValuePatch, getValueAtPath } from "./index.js";
2
+ import { catalogs, resolveSlideContext } from "@openpresentation/opf";
8
3
  import { collectReservedPresentationIds, remapSlideTreeIds } from "./presentation-ids.js";
9
- import { DEFAULT_FONT_SCHEME } from "./font-defaults.js";
4
+ import { DEFAULT_FONT_SCHEME } from "@openpresentation/opf/composition";
5
+ import { checkFormat, firstErrorMessage } from "./checks.js";
10
6
  export const MAX_OPF_BYTES = 20 * 1024 * 1024;
11
7
  const clone = (value) => structuredClone(value);
12
- export function assertOpf(document) {
13
- bounded(document);
14
- const result = validateOpfDocument(document);
8
+ export function assertOpf(presentation) {
9
+ bounded(presentation);
10
+ const result = checkFormat(presentation);
15
11
  if (!result.valid)
16
- throw new Error(result.errors[0]?.message ?? "Invalid OPF document.");
17
- return document;
12
+ throw new Error(firstErrorMessage(result, "Invalid OPF document."));
13
+ return presentation;
18
14
  }
19
15
  function bounded(value, depth = 0, budget = { remaining: 250000 }) {
20
16
  if (depth > 64 || --budget.remaining < 0)
@@ -36,7 +32,7 @@ export function unwrapOpf(value, depth = 0) {
36
32
  if (snippet) return unwrapOpf(snippet.value ?? JSON.parse(snippet.source), depth + 1);
37
33
  }
38
34
  if (value?.opf && typeof value.opf === "object") return unwrapOpf(value.opf, depth + 1);
39
- if (value?.document) return unwrapOpf(value.document, depth + 1);
35
+ if (value?.presentation) return unwrapOpf(value.presentation, depth + 1);
40
36
  return value;
41
37
  }
42
38
  export function parseOpfTransfer(text) {
@@ -63,24 +59,24 @@ export function parseOpfTransfer(text) {
63
59
  bounded(value);
64
60
  if (value?.slides) {
65
61
  assertOpf(value);
66
- return { kind: "presentation", value, document: clone(value) };
62
+ return { kind: "presentation", value, presentation: clone(value) };
67
63
  }
68
- const document = { slides: Array.isArray(value) ? value : [value] };
69
- if (validateOpfDocument(document).valid)
70
- return { kind: "slides", value, document };
64
+ const presentation = { slides: Array.isArray(value) ? value : [value] };
65
+ if (checkFormat(presentation).valid)
66
+ return { kind: "slides", value, presentation };
71
67
  return { kind: "selection", value };
72
68
  }
73
69
  export function serializeOpfTransfer(
74
- document,
70
+ presentation,
75
71
  { scope = "presentation", slideIndex = 0, path, format = "pretty" } = {},
76
72
  ) {
77
- let value = document;
73
+ let value = presentation;
78
74
  if (scope === "slide") {
79
- if (!document.slides?.[slideIndex])
75
+ if (!presentation.slides?.[slideIndex])
80
76
  throw new Error("Select a slide first.");
81
- value = { ...document, slides: [document.slides[slideIndex]] };
77
+ value = { ...presentation, slides: [presentation.slides[slideIndex]] };
82
78
  } else if (scope === "selection") {
83
- value = getValueAtPath(document, path);
79
+ value = getValueAtPath(presentation, path);
84
80
  if (value === undefined) throw new Error("Select some content first.");
85
81
  }
86
82
  const json = JSON.stringify(value, null, format === "compact" ? 0 : 2);
@@ -98,39 +94,34 @@ const catalogKeys = {
98
94
  purposes: "purpose",
99
95
  socialPlatforms: "platform",
100
96
  };
101
- function themeFor(document, reference) {
102
- const id = typeof reference === "string" ? reference : reference?.id;
103
- const record =
104
- document.catalogs?.themes?.records?.find((item) => item.id === id) ??
105
- catalogs.themes.find((item) => item.id === id);
106
- return { ...record, ...(typeof reference === "object" ? reference : {}) };
107
- }
108
97
  export function prepareOpfImport(
109
98
  current,
110
99
  transfer,
111
100
  { mode = "insert", slideIndex = 0, path } = {},
112
101
  ) {
113
102
  if (mode === "selection") {
114
- const document = applyJsonPatch(
103
+ const presentation = applyJsonPatch(
115
104
  current,
116
105
  createValuePatch(current, path, transfer.value),
117
106
  );
118
- assertOpf(document);
119
- return { document, slideIndex };
107
+ assertOpf(presentation);
108
+ return { presentation, slideIndex };
120
109
  }
121
- if (!transfer.document)
110
+ if (!transfer.presentation)
122
111
  throw new Error(
123
112
  "This is a content fragment. Choose Replace selected content.",
124
113
  );
125
114
  if (mode === "replace")
126
- return { document: assertOpf(clone(transfer.document)), slideIndex: 0 };
115
+ return { presentation: assertOpf(clone(transfer.presentation)), slideIndex: 0 };
127
116
  if (mode !== "insert") throw new Error("Unknown import action.");
128
- const incoming = clone(transfer.document),
129
- document = clone(current);
117
+ const incoming = clone(transfer.presentation),
118
+ presentation = clone(current);
130
119
  // Freeze the source deck defaults on inserted slides before changing their catalog ids.
131
- incoming.slides = incoming.slides.map((slide) => {
132
- const design = { ...incoming.design, ...slide.design };
133
- const theme = themeFor(incoming, design.theme ?? "minimal");
120
+ incoming.slides = incoming.slides.map((slide, index) => {
121
+ // A slide's design cannot set dimensions (a PPTX has one slide size): the inserted slide takes the host deck's.
122
+ const { dimensions: _deckSize, ...design } = { ...incoming.design, ...slide.design };
123
+ // The theme record the slide resolves to, as core resolves it for every engine (an unknown id falls back to `minimal`).
124
+ const theme = resolveSlideContext(incoming, index).resolved.theme;
134
125
  return {
135
126
  ...slide,
136
127
  design: {
@@ -138,7 +129,6 @@ export function prepareOpfImport(
138
129
  theme: design.theme ?? "minimal",
139
130
  colorScheme: design.colorScheme ?? theme.colorScheme ?? "cool-horizon",
140
131
  fontScheme: design.fontScheme ?? theme.fontScheme ?? DEFAULT_FONT_SCHEME,
141
- dimensions: design.dimensions ?? theme.dimensions ?? "widescreen",
142
132
  background: design.background ?? theme.background ?? "#FFFFFF",
143
133
  header: design.header ?? false,
144
134
  footer: design.footer ?? false,
@@ -149,7 +139,7 @@ export function prepareOpfImport(
149
139
  assetMap = new Map();
150
140
  let chartTypes = new Map();
151
141
  for (const [kind, catalog] of Object.entries(incoming.catalogs ?? {})) {
152
- const existing = document.catalogs?.[kind];
142
+ const existing = presentation.catalogs?.[kind];
153
143
  if (
154
144
  catalog.source &&
155
145
  existing?.source &&
@@ -179,7 +169,7 @@ export function prepareOpfImport(
179
169
  if (catalogKeys[kind]) mappings[catalogKeys[kind]] = names;
180
170
  if (kind === "chartTypes") chartTypes = names;
181
171
  }
182
- const assetIds = new Set(Object.keys(document.assets ?? {}));
172
+ const assetIds = new Set(Object.keys(presentation.assets ?? {}));
183
173
  for (const id of Object.keys(incoming.assets ?? {})) {
184
174
  let next = id,
185
175
  n = 2;
@@ -187,6 +177,30 @@ export function prepareOpfImport(
187
177
  assetIds.add(next);
188
178
  assetMap.set(id, next);
189
179
  }
180
+ // RR-54: a chart or table that shows a shared dataset (`{ dataset: id }`) brings that dataset along. A dataset the deck already holds
181
+ // with the same content is shared; one with the same id and other content is stored under a free id and the references follow it.
182
+ const datasetMap = new Map();
183
+ const usedDatasets = new Set();
184
+ const collectDatasets = (value, key = "") => {
185
+ if (Array.isArray(value)) value.forEach((child) => collectDatasets(child, key));
186
+ else if (value && typeof value === "object") for (const [childKey, child] of Object.entries(value)) collectDatasets(child, childKey);
187
+ else if (key === "dataset" && typeof value === "string") usedDatasets.add(value);
188
+ };
189
+ collectDatasets(incoming.slides);
190
+ const datasetIds = new Set(Object.keys(presentation.datasets ?? {}));
191
+ for (const id of usedDatasets) {
192
+ const mine = incoming.datasets && Object.hasOwn(incoming.datasets, id) ? incoming.datasets[id] : undefined;
193
+ if (mine === undefined) continue;
194
+ if (datasetIds.has(id) && JSON.stringify(presentation.datasets[id]) === JSON.stringify(mine)) {
195
+ datasetMap.set(id, id);
196
+ continue;
197
+ }
198
+ let next = id,
199
+ n = 2;
200
+ while (datasetIds.has(next)) next = `${id}-${n++}`;
201
+ datasetIds.add(next);
202
+ datasetMap.set(id, next);
203
+ }
190
204
  const rewrite = (value, key = "") => {
191
205
  if (Array.isArray(value)) return value.map((child) => rewrite(child, key));
192
206
  if (value && typeof value === "object") {
@@ -210,6 +224,7 @@ export function prepareOpfImport(
210
224
  return result;
211
225
  }
212
226
  if (typeof value === "string") {
227
+ if (key === "dataset" && datasetMap.has(value)) return datasetMap.get(value);
213
228
  if (mappings[key]?.has(value)) return mappings[key].get(value);
214
229
  if (
215
230
  ["src", "image", "video", "poster"].includes(key) &&
@@ -222,31 +237,36 @@ export function prepareOpfImport(
222
237
  };
223
238
  const rewritten = rewrite(incoming);
224
239
  if (incoming.catalogs) {
225
- document.catalogs ??= {};
240
+ presentation.catalogs ??= {};
226
241
  for (const [kind, catalog] of Object.entries(rewritten.catalogs))
227
- document.catalogs[kind] = {
228
- ...document.catalogs[kind],
242
+ presentation.catalogs[kind] = {
243
+ ...presentation.catalogs[kind],
229
244
  ...catalog,
230
245
  records: [
231
- ...(document.catalogs[kind]?.records ?? []),
246
+ ...(presentation.catalogs[kind]?.records ?? []),
232
247
  ...(catalog.records ?? []),
233
248
  ],
234
249
  };
235
250
  }
236
251
  if (incoming.assets) {
237
- document.assets ??= {};
252
+ presentation.assets ??= {};
238
253
  for (const [id, asset] of Object.entries(rewritten.assets))
239
- document.assets[assetMap.get(id)] =
254
+ presentation.assets[assetMap.get(id)] =
240
255
  typeof asset === "string" &&
241
256
  asset.startsWith("asset:") &&
242
257
  assetMap.has(asset.slice(6))
243
258
  ? `asset:${assetMap.get(asset.slice(6))}`
244
259
  : asset;
245
260
  }
246
- const ids = new Set(collectReservedPresentationIds(document));
261
+ for (const [id, next] of datasetMap) {
262
+ if (presentation.datasets && Object.hasOwn(presentation.datasets, next)) continue;
263
+ presentation.datasets ??= {};
264
+ presentation.datasets[next] = clone(incoming.datasets[id]);
265
+ }
266
+ const ids = new Set(collectReservedPresentationIds(presentation));
247
267
  for (const slide of rewritten.slides) remapSlideTreeIds(slide, ids);
248
- const index = Math.max(0, Math.min(document.slides.length, slideIndex + 1));
249
- document.slides.splice(index, 0, ...rewritten.slides);
250
- assertOpf(document);
251
- return { document, slideIndex: index };
268
+ const index = Math.max(0, Math.min(presentation.slides.length, slideIndex + 1));
269
+ presentation.slides.splice(index, 0, ...rewritten.slides);
270
+ assertOpf(presentation);
271
+ return { presentation, slideIndex: index };
252
272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.11.3",
3
+ "version": "0.14.2",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -218,7 +218,7 @@
218
218
  "check:font-hotlinks": "node --test scripts/check-font-hotlinks.test.mjs && node scripts/check-font-hotlinks.mjs",
219
219
  "check:font-hotlinks:built": "node scripts/check-font-hotlinks.mjs --built dist artifacts/playground",
220
220
  "build:playground": "node scripts/build-playground.mjs",
221
- "test:playground": "npm run build:playground && npm run check:font-hotlinks:built && node test/playground-pptx.mjs && node test/playground-script-fonts.mjs artifacts/playground-script-fonts && node test/playground-lazy-fonts.mjs artifacts/playground-lazy-fonts && node test/playground-base-fonts.mjs artifacts/playground-base-fonts && node test/playground-source.mjs && node test/design-controls-browser.mjs && node test/design-gaps-browser.mjs && node test/chart-options-browser.mjs && node test/persistence-browser.mjs && node test/slides-browser.mjs && node test/template-panel-browser.mjs && node test/numbering-panel-browser.mjs && node test/playground-ensure-fonts.mjs artifacts/playground-ensure-fonts && node test/canvas-fonts-browser.mjs artifacts/canvas-fonts && node test/find-replace-browser.mjs && node test/image-crop-browser.mjs && node test/mobile-browser.mjs && node test/review-panel-browser.mjs && node test/playground-download.mjs artifacts/playground-download",
221
+ "test:playground": "npm run build:playground && npm run check:font-hotlinks:built && node test/playground-pptx.mjs && node test/playground-script-fonts.mjs artifacts/playground-script-fonts && node test/playground-lazy-fonts.mjs artifacts/playground-lazy-fonts && node test/playground-base-fonts.mjs artifacts/playground-base-fonts && node test/playground-source.mjs && node test/design-controls-browser.mjs && node test/design-gaps-browser.mjs && node test/chart-options-browser.mjs && node test/persistence-browser.mjs && node test/slides-browser.mjs && node test/template-panel-browser.mjs && node test/numbering-panel-browser.mjs && node test/playground-ensure-fonts.mjs artifacts/playground-ensure-fonts && node test/canvas-fonts-browser.mjs artifacts/canvas-fonts && node test/find-replace-browser.mjs && node test/image-crop-browser.mjs && node test/mobile-browser.mjs && node test/review-panel-browser.mjs && node test/review-hook-browser.mjs && node test/playground-download.mjs artifacts/playground-download",
222
222
  "test:code-browser": "node test/code-browser.mjs",
223
223
  "test:selection-browser": "node test/selection-browser.mjs",
224
224
  "test:click-entry-browser": "node test/click-entry-browser.mjs",
@@ -230,12 +230,16 @@
230
230
  "test:template-panel-browser": "node test/template-panel-browser.mjs",
231
231
  "test:design-gaps-browser": "node test/design-gaps-browser.mjs",
232
232
  "test:data-grid-browser": "node test/data-grid-browser.mjs",
233
+ "test:data-grid-datasets-browser": "node test/data-grid-datasets-browser.mjs",
234
+ "test:data-import-dataset-browser": "node test/data-import-dataset-browser.mjs",
233
235
  "test:slides-browser": "node test/slides-browser.mjs",
234
236
  "test:persistence-browser": "node test/persistence-browser.mjs",
235
237
  "test:find-browser": "node test/find-replace-browser.mjs",
236
238
  "test:crop-browser": "node test/image-crop-browser.mjs",
237
239
  "test:mobile-browser": "node test/mobile-browser.mjs",
238
240
  "test:review-panel-browser": "node test/review-panel-browser.mjs",
241
+ "test:review-hook-browser": "node test/review-hook-browser.mjs",
242
+ "test:export-license-browser": "node test/export-license-browser.mjs",
239
243
  "test:chart-options-browser": "node test/chart-options-browser.mjs",
240
244
  "test:numbering-panel-browser": "node test/numbering-panel-browser.mjs"
241
245
  },
@@ -249,11 +253,11 @@
249
253
  "@codemirror/state": "6.7.6",
250
254
  "@codemirror/view": "6.43.13",
251
255
  "@lezer/highlight": "1.2.4",
252
- "@openpresentation/opf": "^0.12.2",
256
+ "@openpresentation/opf": "^0.14.0",
253
257
  "jsonc-parser": "3.3.1"
254
258
  },
255
259
  "peerDependencies": {
256
- "@openpresentation/opf-render": "^0.12.2"
260
+ "@openpresentation/opf-render": "^0.14.0"
257
261
  },
258
262
  "peerDependenciesMeta": {
259
263
  "@openpresentation/opf-render": {
@@ -295,8 +299,8 @@
295
299
  "@expo-google-fonts/noto-sans-thai": "0.4.2",
296
300
  "@expo-google-fonts/noto-serif-hebrew": "0.4.2",
297
301
  "@expo-google-fonts/noto-serif-tibetan": "0.4.2",
298
- "@openpresentation/opf-pptx": "^0.12.4",
299
- "@openpresentation/opf-render": "^0.12.2",
302
+ "@openpresentation/opf-pptx": "^0.14.0",
303
+ "@openpresentation/opf-render": "^0.14.0",
300
304
  "axe-core": "4.13.0",
301
305
  "esbuild": "0.28.2",
302
306
  "jszip": "3.10.2",
@@ -1,25 +0,0 @@
1
- /**
2
- * Last-resort font scheme id, shared with core pagination, opf-render and opf-pptx
3
- * (`DEFAULT_FONT_SCHEME` in `@openpresentation/opf`). It applies only when neither the
4
- * slide, the deck nor the resolved theme names a font scheme, so the editor measures and
5
- * freezes the same fonts that preview and PowerPoint export use.
6
- * Kept local until the published core exports the constant; a test asserts they agree.
7
- */
8
- export const DEFAULT_FONT_SCHEME = "aptos";
9
-
10
- /**
11
- * Resolve a font-scheme reference by the shared rule (`resolveFontSchemeReference` in
12
- * `@openpresentation/opf`). An id that matches no record yields one
13
- * `unresolved-font-scheme` diagnostic and uses the DEFAULT_FONT_SCHEME record as the base,
14
- * with sibling overrides on top. Kept local until a published core exports the function;
15
- * a test compares both whenever the installed core has it.
16
- */
17
- export function resolveFontSchemeReference(reference, lookup, path = "design.fontScheme") {
18
- const overrides = reference && typeof reference === "object" && !Array.isArray(reference) ? reference : undefined;
19
- const id = typeof reference === "string" ? reference : typeof overrides?.id === "string" ? overrides.id : undefined;
20
- const found = id === undefined ? undefined : lookup(id);
21
- const base = found ?? lookup(DEFAULT_FONT_SCHEME) ?? {};
22
- const scheme = overrides ? { ...base, ...overrides } : { ...base };
23
- if (id === undefined || found !== undefined) return { scheme };
24
- return { scheme, diagnostic: { code: "unresolved-font-scheme", path, id, fallback: DEFAULT_FONT_SCHEME, message: `Font scheme '${id}' is not in the inline or bundled catalogs; using the default font scheme '${DEFAULT_FONT_SCHEME}'.` } };
25
- }