@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/README.md +15 -2
- package/dist/block-convert.js +3 -2
- package/dist/chart-data.d.ts +2 -0
- package/dist/chart-data.js +1 -0
- package/dist/data-grid.js +177 -7
- package/dist/data.d.ts +21 -0
- package/dist/data.js +39 -0
- package/dist/dataset-refs.d.ts +17 -0
- package/dist/dataset-refs.js +45 -0
- package/dist/design-controls.js +5 -3
- package/dist/find-replace.js +14 -2
- package/dist/grid-model.d.ts +83 -6
- package/dist/grid-model.js +533 -47
- package/dist/switches.js +24 -7
- package/dist/table-options.js +13 -3
- package/dist/transfer.js +30 -0
- package/package.json +14 -8
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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;
|
package/dist/table-options.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
|
211
|
-
"test": "npm run build && npm run check:font-hotlinks && node
|
|
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.
|
|
254
|
+
"@openpresentation/opf": "^0.13.0",
|
|
250
255
|
"jsonc-parser": "3.3.1"
|
|
251
256
|
},
|
|
252
257
|
"peerDependencies": {
|
|
253
|
-
"@openpresentation/opf-render": "^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.
|
|
296
|
-
"@openpresentation/opf-render": "^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",
|