@openpresentation/opf-editor 0.11.2 → 0.12.1

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/dist/switches.js CHANGED
@@ -2,6 +2,7 @@
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";
5
6
  import { catalogSchemaNames, catalogs as bundledCatalogs, schemas } from "@openpresentation/opf";
6
7
  import {
7
8
  OPFEditorError,
@@ -251,7 +252,7 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
251
252
  throw fail("path-slide-mismatch", "options.path is not on the slide named by options.slideIndex.", { slideIndex, path: options.path });
252
253
  const chart = owner && getValueAtPath(document, [...owner, "chart"]);
253
254
  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 });
254
- patches = [...catalogRecordPatches(document, dimension, options), ...(chart.type === value ? [] : createValuePatch(document, [...owner, "chart", "type"], value))];
255
+ patches = [...catalogRecordPatches(document, dimension, options), ...(chart.type === value ? [] : [...createValuePatch(document, [...owner, "chart", "type"], value), ...staleMappingPatches(document, owner, chart, value)])];
255
256
  } else if (dimension === "blocks") {
256
257
  if (options.path === undefined) throw fail("missing-path", "Choose the block to replace with options.path.");
257
258
  // convert: true moves the block's own text into the new kind (block-convert.js) instead of replacing it.
@@ -394,11 +395,27 @@ const SINGLE_SERIES_ONLY = new Set(["pieChart", "doughnutChart", "funnelChart",
394
395
  const MULTI_SERIES_CAPABLE = new Set(["barChart", "lineChart", "areaChart", "radarChart"]);
395
396
  const DISTRIBUTION_ELEMENTS = new Set(["histogramChart", "boxWhiskerChart", "mapChart"]);
396
397
 
397
- function chartDataShape(chart) {
398
- const data = chart?.data;
399
- if (!data || !Array.isArray(data.columns) || !Array.isArray(data.rows)) return undefined;
400
- // The renderers read the first column as the category label and every further column as a series.
401
- return { series: Math.max(0, data.columns.length - 1), categories: data.rows.length };
398
+ // 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
+ // 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) {
401
+ const mapping = chart.mapping;
402
+ if (!mapping || typeof mapping !== "object" || Array.isArray(mapping) || mapping.x === undefined) return [];
403
+ if (typeof core.isXYChartType !== "function" || core.isXYChartType(type)) return [];
404
+ const { x: _x, ...rest } = mapping;
405
+ const parts = [...owner, "chart", "mapping"];
406
+ return Object.keys(rest).length ? createValuePatch(document, parts, rest) : [{ op: "remove", path: opfPathToJsonPointer(parts) }];
407
+ }
408
+
409
+ function chartDataShape(chart, document) {
410
+ // RR-54: inline data, a dataset reference and a series mapping all resolve to the columns the renderers read: the first column labels
411
+ // 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);
417
+ if (!resolved.ok) return undefined;
418
+ return { series: Math.max(0, resolved.columns.length - 1), categories: resolved.rows.length };
402
419
  }
403
420
 
404
421
  /**
@@ -415,7 +432,7 @@ export function compatibleChartTypes(document, options = {}) {
415
432
  const owner = options.path ? splitOpfPath(options.path) : document.slides?.[slideIndex] ? findChartOwner(document, slideIndex) : undefined;
416
433
  const chart = owner && getValueAtPath(document, [...owner, "chart"]);
417
434
  if (!chart || typeof chart !== "object") return [];
418
- const shape = chartDataShape(chart);
435
+ const shape = chartDataShape(chart, document);
419
436
  const result = [];
420
437
  for (const option of listSwitchOptions(document, "charts", options)) {
421
438
  const record = option.record;
@@ -32,7 +32,11 @@ const RULE_COLOR = "textSecondary";
32
32
 
33
33
  const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
34
34
  const isStyled = (cell) => isObject(cell) && Object.hasOwn(cell, "value");
35
- const valueOf = (cell) => (isStyled(cell) ? cell.value : cell);
35
+ // RR-54: a header may be a DataColumn `{ name, format? }`. Styling or merging it makes it the equivalent styled header `{ value: name, format?, ... }`
36
+ // (a styled header's `format` is the column's format, the same as a DataColumn's).
37
+ const isColumnObject = (cell) => isObject(cell) && Object.hasOwn(cell, "name") && !Object.hasOwn(cell, "value");
38
+ const asStyled = (cell) => (isColumnObject(cell) ? { value: cell.name, ...(typeof cell.format === "string" ? { format: cell.format } : {}) } : cell);
39
+ const valueOf = (cell) => (isStyled(cell) ? cell.value : isColumnObject(cell) ? cell.name : cell);
36
40
  const isEmptyValue = (value) => value === null || value === undefined || value === "" || (Array.isArray(value) && value.every((run) => (typeof run === "string" ? run === "" : run?.text === "")));
37
41
  const clone = (value) => structuredClone(value);
38
42
 
@@ -58,6 +62,8 @@ export function parseTableCellPath(path) {
58
62
  function tableAt(document, tablePath) {
59
63
  const parts = splitOpfPath(tablePath);
60
64
  const table = getValueAtPath(document, parts);
65
+ if (isObject(table) && !Array.isArray(table.rows) && typeof table.dataset === "string")
66
+ throw fail("table-dataset-backed", `This table shows the shared dataset '${table.dataset}', which holds no cell styles or merged cells. Use a copy of the data (in the data grid) to style it.`, { tablePath, dataset: table.dataset });
61
67
  if (!isObject(table) || !Array.isArray(table.rows)) throw fail("table-not-found", "Choose a table (a path ending in .table).", { tablePath });
62
68
  return { parts, table };
63
69
  }
@@ -143,14 +149,17 @@ function joinedValue(values) {
143
149
  return parts.map(plainText).join(" ");
144
150
  }
145
151
 
146
- function withCell(raw, patch) {
152
+ function withCell(cell, patch) {
147
153
  // A styled cell with no style and no spans collapses back to the plain value.
154
+ const raw = asStyled(cell);
148
155
  const merged = isStyled(raw) ? { ...raw } : { value: raw };
149
156
  for (const [key, value] of Object.entries(patch)) {
150
157
  if (value === undefined || value === null || (key === "style" && isObject(value) && !Object.keys(value).length)) delete merged[key];
151
158
  else merged[key] = value;
152
159
  }
153
160
  const keys = Object.keys(merged);
161
+ // A DataColumn header that gained no style or span stays a DataColumn.
162
+ if (isColumnObject(cell) && keys.every((key) => key === "value" || key === "format")) return cell;
154
163
  return keys.length === 1 && keys[0] === "value" ? merged.value : merged;
155
164
  }
156
165
 
@@ -226,7 +235,8 @@ export function prepareTableMerge(document, tablePath, cell, span, options = {})
226
235
  for (const entry of covered) entry.line[entry.column] = null;
227
236
  const anchorLine = lineOf(table, target);
228
237
  const raw = anchorLine[target.column];
229
- anchorLine[target.column] = withCell(isStyled(raw) ? { ...raw, value: anchorValue } : anchorValue, { colSpan: colSpan > 1 ? colSpan : undefined, rowSpan: rowSpan > 1 ? rowSpan : undefined });
238
+ const base = asStyled(raw);
239
+ anchorLine[target.column] = withCell(isStyled(base) ? { ...base, value: anchorValue } : anchorValue, { colSpan: colSpan > 1 ? colSpan : undefined, rowSpan: rowSpan > 1 ? rowSpan : undefined });
230
240
  return { merged: true, joined: filled.length > 0 };
231
241
  }, { action: "merge", cell, span: { colSpan, rowSpan } });
232
242
  }
package/dist/transfer.js CHANGED
@@ -187,6 +187,30 @@ export function prepareOpfImport(
187
187
  assetIds.add(next);
188
188
  assetMap.set(id, next);
189
189
  }
190
+ // RR-54: a chart or table that shows a shared dataset (`{ dataset: id }`) brings that dataset along. A dataset the deck already holds
191
+ // 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.
192
+ const datasetMap = new Map();
193
+ const usedDatasets = new Set();
194
+ const collectDatasets = (value, key = "") => {
195
+ if (Array.isArray(value)) value.forEach((child) => collectDatasets(child, key));
196
+ else if (value && typeof value === "object") for (const [childKey, child] of Object.entries(value)) collectDatasets(child, childKey);
197
+ else if (key === "dataset" && typeof value === "string") usedDatasets.add(value);
198
+ };
199
+ collectDatasets(incoming.slides);
200
+ const datasetIds = new Set(Object.keys(document.datasets ?? {}));
201
+ for (const id of usedDatasets) {
202
+ const mine = incoming.datasets && Object.hasOwn(incoming.datasets, id) ? incoming.datasets[id] : undefined;
203
+ if (mine === undefined) continue;
204
+ if (datasetIds.has(id) && JSON.stringify(document.datasets[id]) === JSON.stringify(mine)) {
205
+ datasetMap.set(id, id);
206
+ continue;
207
+ }
208
+ let next = id,
209
+ n = 2;
210
+ while (datasetIds.has(next)) next = `${id}-${n++}`;
211
+ datasetIds.add(next);
212
+ datasetMap.set(id, next);
213
+ }
190
214
  const rewrite = (value, key = "") => {
191
215
  if (Array.isArray(value)) return value.map((child) => rewrite(child, key));
192
216
  if (value && typeof value === "object") {
@@ -210,6 +234,7 @@ export function prepareOpfImport(
210
234
  return result;
211
235
  }
212
236
  if (typeof value === "string") {
237
+ if (key === "dataset" && datasetMap.has(value)) return datasetMap.get(value);
213
238
  if (mappings[key]?.has(value)) return mappings[key].get(value);
214
239
  if (
215
240
  ["src", "image", "video", "poster"].includes(key) &&
@@ -243,6 +268,11 @@ export function prepareOpfImport(
243
268
  ? `asset:${assetMap.get(asset.slice(6))}`
244
269
  : asset;
245
270
  }
271
+ for (const [id, next] of datasetMap) {
272
+ if (document.datasets && Object.hasOwn(document.datasets, next)) continue;
273
+ document.datasets ??= {};
274
+ document.datasets[next] = clone(incoming.datasets[id]);
275
+ }
246
276
  const ids = new Set(collectReservedPresentationIds(document));
247
277
  for (const slide of rewritten.slides) remapSlideTreeIds(slide, ids);
248
278
  const index = Math.max(0, Math.min(document.slides.length, slideIndex + 1));
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.11.2",
3
+ "version": "0.12.1",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "private": false,
8
8
  "engines": {
9
- "node": "24.x"
9
+ "node": ">=22"
10
10
  },
11
11
  "repository": {
12
12
  "type": "git",
@@ -207,8 +207,11 @@
207
207
  "sideEffects": false,
208
208
  "scripts": {
209
209
  "build": "node scripts/build.mjs",
210
- "typecheck": "node --check src/index.js && node --check src/annotations.js && node --check test/annotations.mjs && node --check src/react.js && node --check src/svelte.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/component-smoke.mjs && node --check src/canvas.js && node --check src/canvas-fields.js && node --check test/canvas-fields.mjs && node --check test/browser-canvas.mjs && node --check src/transfer.js && node --check src/presentation-ids.js && node --check src/font-defaults.js && node --check src/color-authoring.js && node --check test/presentation-ids.mjs && node --check src/galleries.js && node --check examples/transfer-controls.js && node --check src/schema.js && node --check src/schema-inspector.js && node --check test/rich-table-browser.mjs && node --check test/styled-table.mjs && node --check test/styled-table-browser.mjs && node --check examples/pptx-controls.js && node --check scripts/build-playground.mjs && node --check test/playground-pptx.mjs && node --check test/playground-script-fonts.mjs && node --check test/playground-lazy-fonts.mjs && node --check test/playground-base-fonts.mjs && node --check src/text-input.js && node --check src/rich-text.js && node --check src/rich-text-input.js && node --check src/rich-text-toolbar.js && node --check test/rich-text.mjs && node --check test/rich-input-browser.mjs && node --check src/json-editor.js && node --check src/json-menu.js && node --check src/json-options.js && node --check src/json-source.js && node --check src/switches.js && node --check test/switches.mjs && node --check test/switches-export.mjs && node --check test/placeholder-selection.mjs && node --check test/selection-browser.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs && node --check src/exact-source.js && node --check test/exact-source.mjs && node --check test/playground-source.mjs && node --check src/font-gate.js && node --check test/font-gate.mjs && node --check test/font-gate-registry.mjs && node --check test/playground-ensure-fonts.mjs && node --check test/canvas-fonts-browser.mjs && node --check src/text-pointer.js && node --check test/click-entry-browser.mjs && node --check src/edit-helpers.js && node --check src/block-convert.js && node --check src/design-options.js && node --check src/table-options.js && node --check src/design-controls.js && node --check test/block-convert.mjs && node --check test/design-options.mjs && node --check test/table-options.mjs && node --check test/switches-ui.mjs && node --check test/design-controls-browser.mjs && node --check src/assets.js && node --check src/chart-options.js && node --check src/chart-options-panel.js && node --check examples/chart-controls.js && node --check test/chart-options.mjs && node --check test/chart-options-browser.mjs && node --check src/background-options.js && node --check test/assets.mjs && node --check test/background-options.mjs && node --check test/design-gaps-browser.mjs && node --check src/templates.js && node --check src/template-panel.js && node --check examples/template-controls.js && node --check test/templates.mjs && node --check test/template-panel-browser.mjs && node --check src/grid-text.js && node --check src/grid-model.js && node --check src/table-structure.js && node --check src/chart-data.js && node --check src/data-grid.js && node --check test/data-grid.mjs && node --check test/data-grid-browser.mjs && node --check src/slides.js && node --check src/outline.js && node --check src/slide-manager.js && node --check src/outline-view.js && node --check test/slides.mjs && node --check test/outline.mjs && node --check test/slides-browser.mjs && node --check src/persistence.js && node --check src/persistence-ui.js && node --check test/persistence.mjs && node --check test/persistence-browser.mjs && node --check src/find-replace.js && node --check src/find-panel.js && node --check src/image-crop.js && node --check src/image-cropper.js && node --check examples/find-controls.js && node --check examples/crop-controls.js && node --check examples/mobile-controls.js && node --check test/find-replace.mjs && node --check test/image-crop.mjs && node --check test/support/playground-harness.mjs && node --check test/find-replace-browser.mjs && node --check test/image-crop-browser.mjs && node --check test/mobile-browser.mjs && node --check src/content-actions.js && node --check src/content-controls.js && node --check test/content-actions.mjs && node --check test/font-gate-latin-families.mjs && node --check src/review.js && node --check src/review-panel.js && node --check examples/review-controls.js && node --check test/review.mjs && node --check test/review-panel-browser.mjs && node --check test/patch.mjs && node --check src/export.js && node --check src/zip.js && node --check examples/download-controls.js && node --check test/export.mjs && node --check test/playground-download.mjs && node --check src/numbering.js && node --check src/numbering-panel.js && node --check examples/numbering-controls.js && node --check test/numbering.mjs && node --check test/numbering-panel-browser.mjs",
211
- "test": "npm run build && npm run check:font-hotlinks && node test/accepted-text.mjs && node test/smoke.mjs && node test/component-smoke.mjs && node test/canvas-fields.mjs && node test/transfer.mjs && node test/gallery-font-roles.mjs && node test/default-font-scheme.mjs && node test/presentation-ids.mjs && node test/schema.mjs && node test/rich-text.mjs && node test/layout.mjs && node test/blocks.mjs && node test/styled-table.mjs && node test/json-source.mjs && node test/json-options.mjs && node test/switches.mjs && node test/block-convert.mjs && node test/design-options.mjs && node test/templates.mjs && node test/table-options.mjs && node test/annotations.mjs && node test/switches-ui.mjs && node test/assets.mjs && node test/chart-options.mjs && node test/background-options.mjs && node test/slides.mjs && node test/outline.mjs && node test/persistence.mjs && node test/switches-export.mjs && node test/placeholder-selection.mjs && node test/exact-source.mjs && node test/font-gate.mjs && node test/data-grid.mjs && node test/font-gate-registry.mjs && node test/find-replace.mjs && node test/image-crop.mjs && node test/content-actions.mjs && node test/font-gate-latin-families.mjs && node test/review.mjs && node test/patch.mjs && node test/export.mjs && node test/numbering.mjs",
210
+ "typecheck": "node scripts/check-syntax.mjs src test scripts examples",
211
+ "test": "npm run build && npm run check:font-hotlinks && node scripts/run-tests.mjs && npm run check:runners && npm run check:changes",
212
+ "test:contract": "npm run build && node scripts/run-tests.mjs --suite contract",
213
+ "check:runners": "node --test scripts/run-tests.test.mjs",
214
+ "check:changes": "node --test scripts/changelog-fragments.test.mjs && node scripts/changelog-fragments.mjs check",
212
215
  "validate": "node scripts/validate-package.mjs",
213
216
  "test:packed": "node test/packed-install.mjs",
214
217
  "prepack": "npm run build",
@@ -227,6 +230,8 @@
227
230
  "test:template-panel-browser": "node test/template-panel-browser.mjs",
228
231
  "test:design-gaps-browser": "node test/design-gaps-browser.mjs",
229
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",
230
235
  "test:slides-browser": "node test/slides-browser.mjs",
231
236
  "test:persistence-browser": "node test/persistence-browser.mjs",
232
237
  "test:find-browser": "node test/find-replace-browser.mjs",
@@ -246,11 +251,11 @@
246
251
  "@codemirror/state": "6.7.6",
247
252
  "@codemirror/view": "6.43.13",
248
253
  "@lezer/highlight": "1.2.4",
249
- "@openpresentation/opf": "^0.12.0",
254
+ "@openpresentation/opf": "^0.13.0",
250
255
  "jsonc-parser": "3.3.1"
251
256
  },
252
257
  "peerDependencies": {
253
- "@openpresentation/opf-render": "^0.12.0"
258
+ "@openpresentation/opf-render": "^0.13.1"
254
259
  },
255
260
  "peerDependenciesMeta": {
256
261
  "@openpresentation/opf-render": {
@@ -292,10 +297,11 @@
292
297
  "@expo-google-fonts/noto-sans-thai": "0.4.2",
293
298
  "@expo-google-fonts/noto-serif-hebrew": "0.4.2",
294
299
  "@expo-google-fonts/noto-serif-tibetan": "0.4.2",
295
- "@openpresentation/opf-pptx": "^0.12.0",
296
- "@openpresentation/opf-render": "^0.12.0",
300
+ "@openpresentation/opf-pptx": "^0.13.2",
301
+ "@openpresentation/opf-render": "^0.13.1",
297
302
  "axe-core": "4.13.0",
298
303
  "esbuild": "0.28.2",
304
+ "jszip": "3.10.2",
299
305
  "pdfjs-dist": "6.3.289",
300
306
  "playwright": "1.63.0",
301
307
  "@expo-google-fonts/noto-color-emoji": "0.4.6",