@openpresentation/opf-pptx 0.18.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPENDENCY-NOTES.md +2 -0
- package/README.md +7 -3
- package/dist/content-topology.js +63 -6
- package/dist/data-provenance.js +7 -4
- package/dist/document-provenance.js +23 -9
- package/dist/index.d.ts +8 -5
- package/dist/index.js +199 -20
- package/dist/master-furniture.js +25 -7
- package/dist/source-run-styles.js +71 -0
- package/dist/template-layouts.js +514 -0
- package/package.json +5 -4
package/DEPENDENCY-NOTES.md
CHANGED
|
@@ -31,3 +31,5 @@ After `npm ci`, run `npm test` and `npm run test:packed`. The packed check insta
|
|
|
31
31
|
Run `npm run test:browser` for real browser WebP pixel preservation and rich, conditional and merged/styled table import/preview/export/reimport checks. Local Windows runs use installed Microsoft Edge; other environments first run `npx playwright install --with-deps chromium`. CI and release jobs run the Chromium checks on their built candidate. These assertions cover the named fixtures, not arbitrary Office files.
|
|
32
32
|
|
|
33
33
|
For a comparison against the preceding release, create a separate empty directory, run `npm init -y` there, then `npm install --ignore-scripts --no-audit --no-fund @openpresentation/opf-pptx@0.5.0`. From this repository run `npm run test:compare-published -- <absolute-consumer-directory> <report.json>`. The script verifies the reference version and registry lockfile integrity, then compares every installed core example's PPTX bytes. The older reference deliberately contains the dependency being removed; it is only a comparison input. Explicit replacement images and fallback fonts make the comparison reproducible. Matching bytes do not establish original-asset fidelity or native viewer equivalence.
|
|
34
|
+
|
|
35
|
+
RR-81 final 0.19 acceptance uses local core candidate e21c472d88057d9d90cfa507fe91b89063f3f29a and gallery 2 candidate e0a7eee2dfe996e680b34a907cabfe41905e6d99, with renderer 9fbddcb78dbebcb87355922ca92af7d26c24c48d. The gallery is imported directly by the converter. Its final direct dependency declaration and registry lock reconciliation are release gates pending the actual gallery 2 registry release; local linking is not evidence of a published dependency graph. An attempted lock-only install was rejected by automatic approval review because gallery 2 registry reconciliation was explicitly deferred; no interim gallery 1 lock entry was adopted. Packed browser/package-consumer checks against released dependencies remain gated by the existing requiresUnreleasedCore 0.19 contract.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OPF PPTX
|
|
2
2
|
|
|
3
|
-
Catalogs (
|
|
3
|
+
Catalogs (OPF 0.19): export resolves the document's embedded records and host-registered catalogs (`Catalog[]`, never fetched). Register gallery 2 explicitly for export: `import { gallery } from "@openpresentation/gallery"; toPptx(deck, { catalogs: [gallery] })`. Bare ids resolve in the document's custom/default records, then the registered default catalog; `catalogs.default:false` closes that fallback. When import `catalogs` is omitted, foreign PowerPoint layout type/name mapping uses the default catalog from installed `@openpresentation/gallery`; theme and colour-scheme recovery still requires explicitly registered catalogs; `fromPptx(bytes, { catalogs: [] })` explicitly opts out. An unresolved reference emits core's `unresolved-reference` diagnostic and uses engine defaults; `strictReferences:true` refuses the export with `OPFPptxError`. Native layout tags preserve valid catalog sources and opt-outs, and damaged identity/source metadata emits diagnostics instead of creating invalid catalog groups.
|
|
4
4
|
|
|
5
5
|
API names (RR-55, unreleased; requires core 0.14, and opf-render 0.14 for the optional peer): `toPptx(presentation, { fonts })` takes the fonts handle that opf-render's `loadFonts()` returns (`@openpresentation/opf-render/fonts-node`; core's `Fonts`, an object with at least `textMeasurement`) and no longer reads a top-level `textMeasurement`. `fromPptx(bytes, { signals: true })` resolves to `{ presentation, signals }` (it was `{ document, signals }`). `OPFPptxError` carries `findings` (the `error` findings of core's `format` check, or the variable errors) and `diagnostics` (the layout diagnostics of a `layout-overflow`), not `issues`. `inventoryPptxTypefaces` and `checkPptxTypefaces` are `inventoryTypefaces` and `checkTypefaces`, and `checkTypefaces` takes `{ families }` where it took `{ fonts }`. Each slide is composed with the options of core's `resolveSlideContext`, so the deck's resolved font families (slide design, deck design, theme, then the engine default) are the ones the layout is measured in; a reference that matches no record was reported per kind and never refused the export (0.15 replaces this with one `unresolved-reference` diagnostic and core's engine defaults; see Catalogs above): the default `minimal` theme and `cool-horizon` colour scheme were used, and a slide with no layout, or an unknown one, is composed with no layout record (automatic composition, as in the preview; it used to compose against the `blank` layout, so a layout-less cover is now centred vertically). The package no longer falls back when core lacks a function: it requires core 0.14.
|
|
6
6
|
|
|
@@ -102,7 +102,7 @@ The first exporter keeps the public API stable while using `pptxgenjs` internall
|
|
|
102
102
|
- Root payloads, `blocks[]`, and promoted region keys become editable slide objects in deterministic regions. Promoted keys use the OPF 3x3 region vocabulary (`top`, `middle`, `bottom`, `left`, `center`, `right`).
|
|
103
103
|
- Text, lists, metrics, quotes, timelines, code, tables, and inline-data charts are emitted as editable PowerPoint text, table, and chart objects. Content ColorRef values (hex, scheme slots/roles, and `var:<id>`) resolve through core `resolveColorRef()`. Slot and role names become native `a:schemeClr` references when the exported theme holds that exact color; hex, `var:<id>` and slide-override colors stay `a:srgbClr` (see [Theme color scheme](#theme-color-scheme)). Each classic chart type the core catalog keeps (one per Aspose.Slides `ChartType`: clustered/stacked/100% stacked column and bar, line and stacked line with or without markers, area/stacked/100% area, pie, doughnut, scatter with markers, radar/radar with markers/filled radar) is written as its exact native construct; deprecated core ids export as their replacement; other ids keep the previous best-effort mapping. The chartex types treemap, histogram, pareto, box & whisker, waterfall and funnel are written as native Office 2016 chartex parts (confirmed in desktop PowerPoint on 2026-09-30; see [Chartex charts](#chartex-charts-ff-22b)); the map (`world`) stays a clustered column chart by default and reports `chart-data-adapted` (`chartex-fallback`) until PowerPoint accepts its regionMap part (`chartex: "native"` writes it anyway, `chartex: "fallback"` writes clustered columns for every chartex id). A pie or doughnut chart, or a one-series chartex construct, plots only its first series and reports `chart-data-adapted` (`series-dropped`) when it has more. Every classic chart part follows the `dml-chart.xsd` child sequences (opf-pptx#175): each series is written in its `CT_*Ser` order (a line series has `c:marker` before `c:dLbls`), a radar series carries no `c:invertIfNegative`, and the scatter X `c:valAx` carries no category-axis elements.
|
|
104
104
|
- A timeline event's `status` (`done`, `current`, `planned`) is drawn from the deck's colors with core's shared shapes (the same as the preview): a filled `ellipse` marker for `done` and for no status, a `current` marker inside a ring (a second `ellipse`, tagged with its marker) with a bold label, and a `planned` marker that is an `ellipse` with the background fill and an accent outline, with muted text kept at 4.5:1 or more. Import restores `status` from the marker tags, not from the shapes' look, and drops the timeline provenance (keeping plain text) when a current event's ring is missing or duplicated.
|
|
105
|
-
- A chart whose data is one column of values (a histogram or dot plot) has no category column. A histogram or Pareto chart bins the values itself (PowerPoint's histogram with an explicit automatic bin count, no diagnostic) and re-imports as authored; any other chart type plots the values against their row numbers and reports `chart-data-adapted` (`row-numbers`). With `chartex: "fallback"` a histogram is binned into equal-width bins (Sturges' count, at most 50) and written as a column chart of the counts (`histogram-binned`; the binned counts do not round-trip). Cells parse as in every chart (core `chartNumber`: numbers and plain decimal strings; "12%", "$5" and "1,234" are not numbers) and cells that hold no number are skipped, not plotted as 0. Chart data that cannot be plotted (no numbers, no rows, or an external data source), an empty table and unsupported content keep a placeholder frame with a plain-language description (never a dump of the source data or URLs) and report `chart-data-unplottable` or `content-placeholder` with a `reason`; content is never dropped without a diagnostic.
|
|
105
|
+
- A chart whose data is one column of values (a histogram or dot plot) has no category column. A histogram or Pareto chart bins the values itself (PowerPoint's histogram with an explicit automatic bin count, no diagnostic) and re-imports as authored; any other chart type plots the values against their row numbers and reports `chart-data-adapted` (`row-numbers`). Full provenance restores the authored data form while the native caches are unchanged; edited caches, `references-only` and no-provenance imports retain the observed row-number categories and values. With `chartex: "fallback"` a histogram is binned into equal-width bins (Sturges' count, at most 50) and written as a column chart of the counts (`histogram-binned`; the binned counts do not round-trip). Cells parse as in every chart (core `chartNumber`: numbers and plain decimal strings; "12%", "$5" and "1,234" are not numbers) and cells that hold no number are skipped, not plotted as 0. Chart data that cannot be plotted (no numbers, no rows, or an external data source), an empty table and unsupported content keep a placeholder frame with a plain-language description (never a dump of the source data or URLs) and report `chart-data-unplottable` or `content-placeholder` with a `reason`; content is never dropped without a diagnostic.
|
|
106
106
|
- Image assets are embedded only when supplied as data URIs, local paths, or host-resolved bytes/paths. Remote asset URLs are never fetched by the runtime path. A picture's native description is the authored `alt` (never a stand-in such as `preencoded.png` or the file path), and a distinct asset `title` is the picture's native `title`; `fromPptx` reads both back.
|
|
107
107
|
- A chart cell that holds no number (`null`, an empty or non-numeric string, a boolean) is a gap in the native chart: its cache has no point at that row (`c:ptCount` keeps the row count), its workbook cell is blank, and it re-imports as `null`. An actual zero stays zero.
|
|
108
108
|
- ZIP entries, generated chart/workbook part names, core-property timestamps, and nested chart workbook timestamps are normalized for reproducible bytes. [Export determinism](docs/export-determinism.md) lists the tested time zone, locale, clock and host-font controls and each known variance (WebP conversion, font registries, ICU segmentation, timestamps).
|
|
@@ -426,7 +426,7 @@ These stay `a:srgbClr`:
|
|
|
426
426
|
- translucent colors;
|
|
427
427
|
- other engine-derived chrome: card fills and borders, chart panels, series and labels, timeline connectors and markers, and image and media placeholders, plus default text on a card or a literal background. These colors are contrast-selected or derived from roles, not named by the document, and are not yet theme references.
|
|
428
428
|
|
|
429
|
-
Import reads the first slide master's theme and matches it against the host default catalog only (the first of `fromPptx`'s `catalogs
|
|
429
|
+
Import reads the first slide master's theme and matches it against the host default catalog only (the first of `fromPptx`'s `catalogs`): recovered bare ids resolve in a document that declares no catalogs. Theme and colour-scheme recovery does not use the foreign-layout default: omitted catalogs and explicit `catalogs: []` both leave recovery unregistered. An exact twelve-slot match with one of its colour schemes returns that id; the `clrScheme` name breaks ties. Otherwise the importer returns inline slots, relative to the scheme the `clrScheme` is named after when there is one (`{id:'boost', accent1:'#123456'}`). A theme whose name equals one of its themes' names maps to `design.theme` only if the package's color scheme or heading/body fonts match that theme (its references resolved in its own group first); otherwise `theme-unverified` is reported. With omitted catalogs or explicit `catalogs: []`, or a match only in another registered catalog, the colour scheme imports inline and no theme id is recovered; an FF-32 stored reference still restores. A missing theme, or slots that are not opaque sRGB/system colors, report `unsupported-theme-colors` on `design.colorScheme`. Slide colors are still imported as resolved hex. Role overrides such as `primary` have no theme slot and are not recovered. `test/theme-colors.mjs` covers all 14 gallery schemes and 4 gallery themes (registered as `defaultCatalog`), import without catalogs, override, foreign and damaged themes, and the literal-color boundaries. `test/table-theme-colors.mjs` covers named and default table chrome on all 14 schemes and four backgrounds, literals, override slides, the link colors, re-import and a theme switch in the package. This establishes package structure and round-trip, not PowerPoint rendering.
|
|
430
430
|
|
|
431
431
|
## JPEG orientation on import
|
|
432
432
|
|
|
@@ -478,6 +478,8 @@ Version 0.8.0 exports editable native lines with explicit four-space tab stops.
|
|
|
478
478
|
|
|
479
479
|
Version 0.9.1 exports core's accepted header/footer text and picture geometry as editable tagged slide shapes. Since RR-11 the footer's first text, date and slide number are native PowerPoint Header & Footer placeholders ([core issue87](https://github.com/OpenPresentation/opf/issues/87), [native header and footer](docs/native-header-footer.md)); releases up to 0.11.7 wrote every part as a tagged shape. Dedicated `OPF_FURNITURE_V1` tags describe roles, line boundaries, inactive flags and global/local scope. A slide manifest represents empty or disabled definitions without adding a visible shape. Tags contain no original text, image bytes or alt text. Reimport reads those values from current native shapes; complete groups recover literal dates and the authored zone `text` with its slide-scoped tokens while the current words still match this slide.
|
|
480
480
|
|
|
481
|
+
A deck-level header or footer that every slide overrides or suppresses has no current native words to recover. Its dormant literal defaults are not stored or restored; each slide's disabled, empty or inactive definition still round-trips. Re-exporting the imported deck keeps those definitions suppressed. Keep the original OPF if you need to edit or reuse never-rendered global defaults later. Adding native furniture imports the new current content rather than bringing back those hidden defaults.
|
|
482
|
+
|
|
481
483
|
A header or footer zone's `text` is the author's own string (FA-31): generated values are `{{ }}` variables in it. Each `{{slide.number}}` is a native PowerPoint slide-number field (`<a:fld type="slidenum">`) when its line fits one accepted text line, so PowerPoint renumbers it when slides move; the text around it (`"A-{{slide.number}}"`, `"Page {{slide.number}} of {{deck.slideCount}}"`) is ordinary runs. `{{deck.slideCount}}` (the exported slide count after pagination) and `{{slide.section}}` are fixed text, because PowerPoint has no slide-count or section field; the deck-wide variables (`{{organization.name}}`, `{{customer}}`) resolve before export like any other string. In body text every slide-scoped token is fixed text with the slide's value. Each slide is composed and drawn from core's substituted slide, so text is measured with the real value. A current date (`date: true`) needs the host's `date` option (ISO `YYYY-MM-DD`); it becomes a native `datetime1`–`datetime7` field when its `dateFormat` matches that en-US field type, and otherwise fixed text with a `furniture-date-fixed` diagnostic. An eligible date or number range that spans accepted text lines stays static and reports `furniture-field-fixed` at its source path; no native field is created by merging shapes. A string date with `dateFormat` is fixed text. The slide manifest records `dateFormat` settings and, for a zone whose `{{ }}` tokens are all built-in variables, that authored text as given to `toPptx`, before the deck-wide pass (`templates`); it never records rendered dates. Reimport keeps a format or the authored text only while the current native text still matches it (a fixed date that parses back to the same ISO date; the authored text drawn for this slide equals the current words: deck-wide built-ins resolved from the stored organization and speakers, then the slide's number, count and section), and a live date keeps the pattern of its current field type. Otherwise the current words import as literal text, with each native slide-number field as `{{slide.number}}` (typed words that spell a token are escaped as `\{{`). Hiding furniture on a title slide is a slide-level `design.footer: false`/`design.header: false`, which round-trips as before.
|
|
482
484
|
|
|
483
485
|
When a supported current-date field wraps across lines, full provenance records a versioned static-date reason, its format and exact line fingerprints, without cached date words. Complete unchanged soft-wrapped lines can then recover `date: true` and the authored format on reimport, so a later OPF export can use a new host date. The original PPTX date remains static: this does not establish native refresh or Office compatibility. Actual current native date fields take precedence. Edited or cleared text, damaged evidence, older unmarked exports, `references-only` and `provenance: false` remain literal/current-content fallbacks. Fingerprints detect changed text; writable tags are not authentication. Native formatting and geometry are not reconstructed.
|
|
@@ -487,3 +489,5 @@ Missing, duplicated or inconsistent furniture tags fall back to current native c
|
|
|
487
489
|
Native header and footer placeholders (RR-11): the first footer text becomes a `ftr` placeholder, the first footer date a `dt` placeholder (a live `datetime1`-`datetime7` field for `date: true`) and the first footer slide number an `sldNum` placeholder, each at core's exact box with its furniture tag; the slide master and layout carry the three placeholders and `p:hf` flags, and the notes master records that notes pages carry a page number only. A zone `text` with a `{{slide.number}}` is the `sldNum` placeholder (its other words are fixed runs); any other zone `text` is the `ftr` placeholder. Header parts, socials, images and logos, second text/date/number parts and multi-line text stay tagged shapes (PowerPoint has no object for them). A footer text that shares its zone with a logo is only as wide as its words (core's row layout), so its slide placeholder has that box while the master and layout placeholder spans the zone. `fromPptx` reads the placeholders back with or without provenance, and a deck changed through PowerPoint's Insert > Header & Footer dialog imports the change. Every deck, with or without a footer, carries the three master and layout placeholders (flags off when unused), so Insert > Header & Footer works on a deck with no footer. The mapping table, the dialog-edit table and the vetoable decisions are in [docs/native-header-footer.md](docs/native-header-footer.md).
|
|
488
490
|
|
|
489
491
|
Master furniture (RR-72): a part with no placeholder (a logo or other zone image, socials, header text, a second footer text) that is drawn the same on two or more slides is written once. It goes on the slide master when every slide that shows such furniture draws it, else on a layout shared by the slides that do (one layout per distinct set, so a logo's `onLight` and `onDark` assets get a layout per background tone); a slide that hides it (`design.footer: false`) uses a layout with `showMasterSp="0"`. A part on one slide only, a part with a live field and a part that slide content covers stay on the slide (the last with `furniture-on-slide`, because PowerPoint draws master shapes beneath slide content). The lifted shapes keep their furniture tags (with the slot in place of the slide part), and each slide manifest lists them under `shared`, so `fromPptx` reads them from the layout or master: a master logo returns as its `var:organization.logo.*` reference on every slide. Importers before 0.18 ignore `shared`; shapes on a master or layout without OPF tags (a third-party template) are not imported. See [Master furniture](docs/native-header-footer.md#master-furniture-rr-72).
|
|
492
|
+
|
|
493
|
+
Multi-series Pareto exports in `chartex: "auto"` as an editable clustered column chart containing every authored series, with a `chart-data-adapted` / `chartex-fallback` diagnostic. This preserves data rather than claiming native multi-series Pareto support. Explicit `chartex: "native"` rejects it with `unsupported-chart-data`; single-series native Pareto is unchanged. `chartex: "fallback"` also retains all series. Native acceptance of any new behavior remains separate.
|
package/dist/content-topology.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {sourceRunStyles, validateSourceRunStyles, restoreSourceRunStyles} from './source-run-styles.js';
|
|
1
2
|
// Content topology (spec-gap closure P1): the structure of a slide's content
|
|
2
3
|
// survives a PPTX round trip through the OPF_SLIDE_V1 record.
|
|
3
4
|
//
|
|
@@ -18,8 +19,10 @@
|
|
|
18
19
|
// | {form: 'root', fields: [{field, box?}]} // root shorthand with several payloads
|
|
19
20
|
// | {form: 'blocks', blocks: Node[]}
|
|
20
21
|
// | {form: 'regions', regions: {[key]: Node}} // key = promoted region key
|
|
21
|
-
// Node = {t: 'group', id?, ext?, comp?, typed?, blocks: Node[]}
|
|
22
|
-
// | {t: 'leaf', k, id?, ext?, typed?, bullets?, box?, lines?, wrap?} // box = [x, y, w, h] reference px, 1 decimal
|
|
22
|
+
// Node = {t: 'group', id?, ext?, comp?, typed?, pin?, in?, blocks: Node[]}
|
|
23
|
+
// | {t: 'leaf', k, id?, ext?, typed?, pin?, in?, bullets?, box?, lines?, wrap?} // box = [x, y, w, h] reference px, 1 decimal
|
|
24
|
+
// pin = the block's authored `region` pin (OPF 0.19 layout templates, RR-81), restored onto the block
|
|
25
|
+
// in = the template region a root block was drawn in (RR-81): where its shapes are, for editors and diagnostics; never restored
|
|
23
26
|
// typed = true when the authored block spelled out its `type`
|
|
24
27
|
// bullets = true when the authored list block used the `bullets` key (import names every list `items`), also under `type: 'text'`
|
|
25
28
|
// lines = N (>= 2) when a rich-text `text` payload exported as N native line
|
|
@@ -62,6 +65,8 @@ export const MAX_NODES = 256;
|
|
|
62
65
|
export const MAX_ID_LENGTH = 256;
|
|
63
66
|
// Native bounds may sit this far outside a stored leaf box (reference px).
|
|
64
67
|
export const BOX_TOLERANCE = 3;
|
|
68
|
+
// A template region name (OPF 0.19), as a `region` pin or the region a block was drawn in.
|
|
69
|
+
const REGION_NAME = /^[a-z][a-z0-9-]{0,63}$/;
|
|
65
70
|
|
|
66
71
|
const kindOfField = field => field === 'items' || field === 'bullets' ? 'list' : field;
|
|
67
72
|
const isGroup = block => object(block) && (block.type === 'group' || (block.type === undefined && Array.isArray(block.blocks)));
|
|
@@ -240,6 +245,12 @@ export function contentTopology(slide, items, slideIndex, report = () => {}) {
|
|
|
240
245
|
if (lines) wrapped.set(item.path, wrapRecord(lines));
|
|
241
246
|
}
|
|
242
247
|
const base = `slides.${slideIndex}`;
|
|
248
|
+
// RR-81: the template region each root block was drawn in (core's items[].region).
|
|
249
|
+
const regionOf = new Map();
|
|
250
|
+
for (const item of items ?? []) {
|
|
251
|
+
const root = typeof item?.path === 'string' && typeof item.region === 'string' ? /^(slides\.\d+\.blocks\.\d+)(?:\.|$)/.exec(item.path)?.[1] : undefined;
|
|
252
|
+
if (root && REGION_NAME.test(item.region) && !regionOf.has(root)) regionOf.set(root, item.region);
|
|
253
|
+
}
|
|
243
254
|
let nodes = 0;
|
|
244
255
|
const fail = reason => { throw new TopologyError(reason); };
|
|
245
256
|
const leafBox = path => {
|
|
@@ -256,6 +267,8 @@ export function contentTopology(slide, items, slideIndex, report = () => {}) {
|
|
|
256
267
|
}
|
|
257
268
|
if (object(block.extensions)) node.ext = clone(block.extensions);
|
|
258
269
|
if (typeof block.type === 'string') node.typed = true;
|
|
270
|
+
if (typeof block.region === 'string' && REGION_NAME.test(block.region)) node.pin = block.region;
|
|
271
|
+
if (regionOf.has(path)) node.in = regionOf.get(path);
|
|
259
272
|
return node;
|
|
260
273
|
};
|
|
261
274
|
const node = (block, path, depth) => {
|
|
@@ -274,6 +287,7 @@ export function contentTopology(slide, items, slideIndex, report = () => {}) {
|
|
|
274
287
|
const box = leafBox(path);
|
|
275
288
|
if (box) leaf.box = box;
|
|
276
289
|
if (block.bullets !== undefined && block.items === undefined && ['list', 'text'].includes(kind)) leaf.bullets = true;
|
|
290
|
+
if (kind === 'text') { const styles = sourceRunStyles(block.text); if (styles) leaf.sourceStyles = styles; }
|
|
277
291
|
if (kind === 'text' && wrapped.has(`${path}.text`)) Object.assign(leaf, wrapped.get(`${path}.text`));
|
|
278
292
|
return leaf;
|
|
279
293
|
};
|
|
@@ -290,7 +304,8 @@ export function contentTopology(slide, items, slideIndex, report = () => {}) {
|
|
|
290
304
|
const fields = ROOT_PAYLOAD_FIELDS.filter(name => slide[name] !== undefined).map(field => {
|
|
291
305
|
const box = boxes.get(`${base}.${field}`);
|
|
292
306
|
const lines = field === 'text' ? wrapped.get(`${base}.text`) : undefined;
|
|
293
|
-
|
|
307
|
+
const sourceStyles = field === 'text' ? sourceRunStyles(slide.text) : undefined;
|
|
308
|
+
return box ? {field, ...(sourceStyles ? {sourceStyles} : {}), box: [box.x, box.y, box.width, box.height].map(round1), ...lines} : {field, ...(sourceStyles ? {sourceStyles} : {})};
|
|
294
309
|
});
|
|
295
310
|
if (!fields.length) return undefined;
|
|
296
311
|
if (fields.length === 1) return {form: 'root', ...fields[0]};
|
|
@@ -355,6 +370,43 @@ export function listLineBreaks(items, omit = () => {}) {
|
|
|
355
370
|
return lists.length && lists.length <= MAX_LIST_PATHS && total <= MAX_LIST_BREAKS ? lists : undefined;
|
|
356
371
|
}
|
|
357
372
|
|
|
373
|
+
// Line identity only: no source words or formatting. Each row is [entry, role, first].
|
|
374
|
+
export function listLineRoles(items, omit = () => {}) {
|
|
375
|
+
const lists = [];
|
|
376
|
+
for (const item of items ?? []) {
|
|
377
|
+
if (!['items', 'bullets'].includes(item?.field) || !Array.isArray(item?.text?.listEntries)) continue;
|
|
378
|
+
if (!item.text.listEntries.some(entry => entry.description?.richLines?.length)) continue;
|
|
379
|
+
const lines = [];
|
|
380
|
+
item.text.listEntries.forEach((entry, index) => {
|
|
381
|
+
for (const [role, text] of [['text', entry.text], ['description', entry.description]])
|
|
382
|
+
text?.richLines?.forEach((_, line) => lines.push([index, role, role === 'text' && line === 0]));
|
|
383
|
+
});
|
|
384
|
+
if (lines.length) lists.push([item.path, lines]);
|
|
385
|
+
}
|
|
386
|
+
if (!lists.length) return undefined;
|
|
387
|
+
try { return validateListRoles(lists); } catch {
|
|
388
|
+
for (const [path] of lists) omit(path, 'list line roles exceed the bounded record or cannot be recorded exactly');
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function validateListRoles(value) {
|
|
394
|
+
if (!Array.isArray(value) || !value.length || value.length > MAX_LIST_PATHS) throw Error('Invalid list roles.');
|
|
395
|
+
const paths = new Set(); let total = 0;
|
|
396
|
+
for (const row of value) {
|
|
397
|
+
if (!Array.isArray(row) || row.length !== 2 || typeof row[0] !== 'string' || !LIST_PATH.test(row[0]) || paths.has(row[0]) || !Array.isArray(row[1]) || !row[1].length) throw Error('Invalid list roles path.');
|
|
398
|
+
paths.add(row[0]); let entry = -1, description = false;
|
|
399
|
+
for (const line of row[1]) {
|
|
400
|
+
if (!Array.isArray(line) || line.length !== 3 || !Number.isSafeInteger(line[0]) || !['text', 'description'].includes(line[1]) || typeof line[2] !== 'boolean') throw Error('Invalid list role.');
|
|
401
|
+
if (line[2]) { if (line[0] !== entry + 1 || line[1] !== 'text') throw Error('Invalid list entry.'); entry++; description = false; }
|
|
402
|
+
else if (line[0] !== entry || entry < 0 || (description && line[1] === 'text')) throw Error('Invalid list continuation.');
|
|
403
|
+
if (line[1] === 'description') description = true;
|
|
404
|
+
if (++total > MAX_LIST_BREAKS) throw Error('Too many list roles.');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
|
|
358
410
|
/** Throws when `value` is not a well-formed `lists` record (untrusted input). Returns it otherwise. */
|
|
359
411
|
export function validateListBreaks(value) {
|
|
360
412
|
if (!Array.isArray(value) || !value.length || value.length > MAX_LIST_PATHS) throw Error('Invalid list breaks record.');
|
|
@@ -417,9 +469,9 @@ const validWrapOf = value => validLines(value.lines) && validWrap(value.wrap) &&
|
|
|
417
469
|
export const wrapOf = node => node.wrap ?? (node.lines === undefined ? undefined : {lines: node.lines});
|
|
418
470
|
/** The leaf fields that record `wrapped` ({lines, gaps?} from wrappedLines). */
|
|
419
471
|
const wrapRecord = wrapped => wrapped.gaps ? {wrap: wrapped} : {lines: wrapped.lines};
|
|
420
|
-
const validField = value => object(value) && ROOT_PAYLOAD_FIELDS.includes(value.field) && validBox(value.box) && validWrapOf(value);
|
|
472
|
+
const validField = value => { if (value?.sourceStyles !== undefined) validateSourceRunStyles(value.sourceStyles); return object(value) && ROOT_PAYLOAD_FIELDS.includes(value.field) && validBox(value.box) && validWrapOf(value); };
|
|
421
473
|
/** The root payload leaves of a root-form record: [{field, box?}]. */
|
|
422
|
-
export const rootFields = topology => topology.fields ?? [{field: topology.field, box: topology.box, lines: topology.lines, wrap: topology.wrap}];
|
|
474
|
+
export const rootFields = topology => topology.fields ?? [{field: topology.field, box: topology.box, lines: topology.lines, wrap: topology.wrap, sourceStyles: topology.sourceStyles}];
|
|
423
475
|
|
|
424
476
|
/** Throws when `value` is not a well-formed topology record. Returns it otherwise. */
|
|
425
477
|
export function validateTopology(value) {
|
|
@@ -431,6 +483,7 @@ export function validateTopology(value) {
|
|
|
431
483
|
if (!validId(item.id)) throw Error('Invalid content node id.');
|
|
432
484
|
if (item.ext !== undefined && !object(item.ext)) throw Error('Invalid content node extensions.');
|
|
433
485
|
if (item.typed !== undefined && item.typed !== true) throw Error('Invalid content node type flag.');
|
|
486
|
+
if ((item.pin !== undefined && !REGION_NAME.test(item.pin ?? '')) || (item.in !== undefined && !REGION_NAME.test(item.in ?? ''))) throw Error('Invalid content node region.');
|
|
434
487
|
if (item.t === 'group') {
|
|
435
488
|
if (depth >= MAX_GROUP_DEPTH) throw Error(`Content groups nest deeper than ${MAX_GROUP_DEPTH} levels.`);
|
|
436
489
|
if (item.comp !== undefined && !object(item.comp)) throw Error('Invalid group composition.');
|
|
@@ -443,6 +496,7 @@ export function validateTopology(value) {
|
|
|
443
496
|
if (item.comp !== undefined) throw Error('A content leaf has no composition.');
|
|
444
497
|
if (!validBox(item.box)) throw Error('Invalid content box.');
|
|
445
498
|
if (item.bullets !== undefined && (item.bullets !== true || !['list', 'text'].includes(item.k))) throw Error('Invalid bullets flag.');
|
|
499
|
+
if (item.sourceStyles !== undefined) validateSourceRunStyles(item.sourceStyles);
|
|
446
500
|
if (!validWrapOf(item)) throw Error('Invalid wrapped line record.');
|
|
447
501
|
};
|
|
448
502
|
switch (value.form) {
|
|
@@ -504,7 +558,7 @@ const textRuns = value => typeof value === 'string' || (Array.isArray(value) &&
|
|
|
504
558
|
* {reason} when a block matches no leaf or several blocks land on one leaf.
|
|
505
559
|
* Leaves with no block are dropped: empty payloads export nothing.
|
|
506
560
|
*/
|
|
507
|
-
export function rebuildContent(topology, blocks, bounds, placed) {
|
|
561
|
+
export function rebuildContent(topology, blocks, bounds, placed, intactStyles = {}) {
|
|
508
562
|
if (!Array.isArray(blocks) || !Array.isArray(bounds) || blocks.length !== bounds.length) return {reason: 'the imported blocks carry no native bounds'};
|
|
509
563
|
const leaves = [];
|
|
510
564
|
const collect = item => {
|
|
@@ -558,12 +612,14 @@ export function rebuildContent(topology, blocks, bounds, placed) {
|
|
|
558
612
|
const payloadOf = leaf => {
|
|
559
613
|
if (!leaf.matches.length) return undefined;
|
|
560
614
|
const payload = clone(leaf.payload ?? blocks[leaf.matches[0]]);
|
|
615
|
+
if (leaf.kind === 'text' && leaf.node.sourceStyles) payload.text = restoreSourceRunStyles(payload.text, leaf.node.sourceStyles, intactStyles);
|
|
561
616
|
// The imported block always names its type; the authored one may have left it implicit.
|
|
562
617
|
if (!leaf.node.typed) delete payload.type;
|
|
563
618
|
// The imported block names every list `items`; a payload authored as `bullets` takes its key back (a bullet has no description).
|
|
564
619
|
if (leaf.node.bullets === true) bulletsKey(payload, leaf.node.typed);
|
|
565
620
|
if (leaf.node.id !== undefined) { payload.id = leaf.node.id; ids.push(leaf.node.id); }
|
|
566
621
|
if (leaf.node.ext !== undefined) payload.extensions = clone(leaf.node.ext);
|
|
622
|
+
if (leaf.node.pin !== undefined) payload.region = leaf.node.pin;
|
|
567
623
|
return payload;
|
|
568
624
|
};
|
|
569
625
|
const build = item => {
|
|
@@ -577,6 +633,7 @@ export function rebuildContent(topology, blocks, bounds, placed) {
|
|
|
577
633
|
if (item.id !== undefined) { group.id = item.id; ids.push(item.id); }
|
|
578
634
|
if (item.ext !== undefined) group.extensions = clone(item.ext);
|
|
579
635
|
if (item.comp !== undefined) group.composition = clone(item.comp);
|
|
636
|
+
if (item.pin !== undefined) group.region = item.pin;
|
|
580
637
|
group.blocks = children;
|
|
581
638
|
return group;
|
|
582
639
|
};
|
package/dist/data-provenance.js
CHANGED
|
@@ -211,9 +211,12 @@ export function tableEnvironmentEvidence(entries) {
|
|
|
211
211
|
/** The hash of a dataset, or undefined when the map does not hold the id. */
|
|
212
212
|
const datasetHash = (datasets, id) => own(datasets, id) ? hash(canonical(datasets[id])) : undefined;
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
// Synthetic categories are recorded only when the exporter actually applied
|
|
215
|
+
// row-numbers. Binned histogram fallbacks do not opt in: their native values are
|
|
216
|
+
// counts, so restoring raw samples would give the imported column chart other data.
|
|
217
|
+
/** Authored chart data whose source form the native cache alone cannot preserve. */
|
|
218
|
+
export function chartDataRecord(chart, datasets, {rowNumbers = false} = {}) {
|
|
219
|
+
if (!rowNumbers && !chartUsesDataFields(chart) && !object(chart?.highlight)) return undefined;
|
|
217
220
|
const dataset = isDatasetRef(chart.data) ? datasetHash(datasets, chart.data.dataset) : undefined;
|
|
218
221
|
return {v: 1, kind: 'chart', data: clone(chart.data), ...(chart.mapping !== undefined ? {mapping: clone(chart.mapping)} : {}), ...(object(chart.highlight) ? {highlight: clone(chart.highlight)} : {}), ...(dataset ? {dataset} : {})};
|
|
219
222
|
}
|
|
@@ -415,7 +418,7 @@ const validWith = (payload, datasets) => isValidFormat({slides: [{title: 'Data',
|
|
|
415
418
|
*/
|
|
416
419
|
export function restoreChartData(chart, record, evidence, {datasets, report}) {
|
|
417
420
|
if (!record.evidence.includes(evidence)) {
|
|
418
|
-
report({code: 'chart-data-provenance-changed', message: 'The chart\'s cached data or number formats changed after export (for example in Edit Data), so the dataset reference, column mapping, column formats and data source recorded at export were not restored; the chart imports the values and formats it now shows.' + (record.highlight ? ' The chart highlight (series and categories named at export) was not restored either: its names refer to the data as exported.' : '')});
|
|
421
|
+
report({code: 'chart-data-provenance-changed', message: 'The chart\'s cached data or number formats changed after export (for example in Edit Data), so the authored data form, dataset reference, column mapping, column formats and data source recorded at export were not restored; the chart imports the values and formats it now shows.' + (record.highlight ? ' The chart highlight (series and categories named at export) was not restored either: its names refer to the data as exported.' : '')});
|
|
419
422
|
return chart;
|
|
420
423
|
}
|
|
421
424
|
if (isDatasetRef(record.data) && !own(datasets, record.data.dataset)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import {nativeRunStyleEvidence, hasSourceRunStyles} from './source-run-styles.js';
|
|
1
2
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
3
|
import {decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
3
|
-
import {contentTopology, rebuildContent, validateTopology, listLineBreaks, validateListBreaks, validateListRuns, MAX_LIST_PATHS} from './content-topology.js';
|
|
4
|
+
import {contentTopology, rebuildContent, validateTopology, listLineBreaks, validateListBreaks, validateListRuns, listLineRoles, validateListRoles, MAX_LIST_PATHS} from './content-topology.js';
|
|
4
5
|
import {runColorRecord, validateRunColors} from './run-colors.js';
|
|
5
6
|
import {validateCatalogRecord} from '@openpresentation/opf';
|
|
6
7
|
import {parseReference, resolveReference, resolveSocialProfile} from '@openpresentation/opf/composition';
|
|
@@ -315,7 +316,12 @@ function styleSignature(tree) {
|
|
|
315
316
|
return canonical({faces: [...faces].sort(), colors: [...colors].sort()});
|
|
316
317
|
}
|
|
317
318
|
|
|
318
|
-
function
|
|
319
|
+
function nativeListIdentity(entries, path) {
|
|
320
|
+
const tree = parser.parse(dec.decode(entries[path]))?.['p:sld']?.['p:cSld']?.['p:spTree'];
|
|
321
|
+
return hash(canonical(array(tree?.['p:sp']).filter(node => /^OPF list /.test(node?.['p:nvSpPr']?.['p:cNvPr']?.name ?? '')).map(node => [node['p:nvSpPr']['p:cNvPr'].name, box(node?.['p:spPr']?.['a:xfrm'])])));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function nativeSlide(entries, path, includeRunStyles = true) {
|
|
319
325
|
const root = parser.parse(dec.decode(entries[path]))?.['p:sld'];
|
|
320
326
|
const cSld = root?.['p:cSld'], tree = cSld?.['p:spTree'];
|
|
321
327
|
const rels = relationships(entries, path);
|
|
@@ -327,6 +333,7 @@ function nativeSlide(entries, path) {
|
|
|
327
333
|
const background = backgroundObjects.length ? {bg: cSld?.['p:bg'] ?? null, objects: backgroundObjects} : cSld?.['p:bg'] ?? null;
|
|
328
334
|
return {
|
|
329
335
|
structure: hash(structure(tree)),
|
|
336
|
+
...(includeRunStyles ? {runStyles: nativeRunStyleEvidence(withoutExtensions(tree ?? null), hash)} : {}),
|
|
330
337
|
background: hash(canonical(withResolvedRelationships(withoutExtensions(background), rels, entries))),
|
|
331
338
|
style: hash(styleSignature(withoutExtensions(tree ?? null)))
|
|
332
339
|
};
|
|
@@ -345,7 +352,7 @@ const SOURCE_PREFIX = /^(asset:|data:|https?:|file:|\.\.?\/)/i;
|
|
|
345
352
|
// Metadata fields that hold content references (OPF 0.15: `id` or `name:id`; for audience and purpose a string that is not
|
|
346
353
|
// a reference is free text). `language` is an engine vocabulary (a BCP-47 tag), not a catalog reference.
|
|
347
354
|
const METADATA_REFERENCES = Object.freeze(['narrative', 'tone', 'purpose', 'audience']);
|
|
348
|
-
const LAYOUT_SCHEMA = 'https://openpresentation.org/schema/opf-layout/
|
|
355
|
+
const LAYOUT_SCHEMA = 'https://openpresentation.org/schema/opf-layout/v2';
|
|
349
356
|
const GROUP_NAME = /^[a-z][a-z0-9-]*$/;
|
|
350
357
|
|
|
351
358
|
function collectStrings(value, into = new Set()) {
|
|
@@ -545,6 +552,8 @@ export function recordContentTopology(provenance, slide, index, items) {
|
|
|
545
552
|
const omittedLists = [];
|
|
546
553
|
const lists = listLineBreaks(items, (path, reason) => omittedLists.push([path, reason]));
|
|
547
554
|
if (lists) provenance.slides[index].lists = lists;
|
|
555
|
+
const roles = listLineRoles(items, (path, reason) => omittedLists.push([path, reason]));
|
|
556
|
+
if (roles) provenance.slides[index].listRoles = roles;
|
|
548
557
|
if (omittedLists.length) provenance.slides[index].omittedLists = omittedLists;
|
|
549
558
|
}
|
|
550
559
|
|
|
@@ -709,7 +718,7 @@ function storable(entries, provenance) {
|
|
|
709
718
|
const result = {v: 1, slide: index};
|
|
710
719
|
if (record.omittedContent) omit(`slides.${index}.content`, record.omittedContent);
|
|
711
720
|
for (const [path, reason] of record.omittedLists ?? []) omit(path, reason);
|
|
712
|
-
for (const key of ['id', 'beat', ...SLIDE_STRUCTURE, ...SLIDE_METADATA, 'content', 'colors', 'lists', 'listRuns']) {
|
|
721
|
+
for (const key of ['id', 'beat', ...SLIDE_STRUCTURE, ...SLIDE_METADATA, 'content', 'colors', 'lists', 'listRuns', 'listRoles']) {
|
|
713
722
|
if (record[key] === undefined) continue;
|
|
714
723
|
const value = prepare(`slides.${index}.${key}`, record[key]);
|
|
715
724
|
if (value !== undefined) result[key] = value;
|
|
@@ -746,7 +755,7 @@ function storable(entries, provenance) {
|
|
|
746
755
|
}
|
|
747
756
|
for (const [index, record] of slides.entries()) {
|
|
748
757
|
while (tagChars(record) > budget) {
|
|
749
|
-
const candidates = [...['id', 'beat', ...SLIDE_STRUCTURE, ...SLIDE_METADATA, 'content', 'colors', 'lists', 'listRuns', 'layoutRecord'].filter(key => record[key] !== undefined).map(key => ({key, size: sizeOf(record[key])})),
|
|
758
|
+
const candidates = [...['id', 'beat', ...SLIDE_STRUCTURE, ...SLIDE_METADATA, 'content', 'colors', 'lists', 'listRuns', 'listRoles', 'layoutRecord'].filter(key => record[key] !== undefined).map(key => ({key, size: sizeOf(record[key])})),
|
|
750
759
|
...Object.keys(record.design ?? {}).map(key => ({key, design: true, size: sizeOf(record.design[key])}))];
|
|
751
760
|
if (!candidates.length) break;
|
|
752
761
|
const largest = candidates.sort((a, b) => b.size - a.size)[0];
|
|
@@ -796,7 +805,7 @@ export function attachDocumentProvenance(entries, provenance) {
|
|
|
796
805
|
|
|
797
806
|
for (const [index, path] of paths.entries()) {
|
|
798
807
|
const slideHashes = mediaHashes(prepared.slides[index], entries);
|
|
799
|
-
const record = toStoredShape({...prepared.slides[index], ...(Object.keys(slideHashes).length ? {mediaHash: slideHashes} : {}), native: nativeSlide(entries, path)}, SLIDE_SUPPLEMENT);
|
|
808
|
+
const record = toStoredShape({...prepared.slides[index], ...(Object.keys(slideHashes).length ? {mediaHash: slideHashes} : {}), native: {...nativeSlide(entries, path, hasSourceRunStyles(prepared.slides[index].content)), ...(prepared.slides[index].listRoles ? {listIdentity: nativeListIdentity(entries, path)} : {})}}, SLIDE_SUPPLEMENT);
|
|
800
809
|
const tag = `<p:tag name="${SLIDE_TAG}" val="${encodeTextTag(record)}"/>`;
|
|
801
810
|
let xml = dec.decode(entries[path]);
|
|
802
811
|
const slideRels = relsPath(path);
|
|
@@ -872,6 +881,7 @@ function validateDocument(stored) {
|
|
|
872
881
|
|
|
873
882
|
function validateSlide(stored) {
|
|
874
883
|
if (!object(stored) || stored.v !== 1 || !Number.isSafeInteger(stored.slide) || stored.slide < 0) throw Error('Unsupported slide provenance version.');
|
|
884
|
+
if (stored.native?.runStyles !== undefined && (!object(stored.native.runStyles) || Object.keys(stored.native.runStyles).length !== 3 || !['fontSize', 'fontFamily', 'color'].every(key => /^[a-f0-9]{14}$/.test(stored.native.runStyles[key])))) throw Error('Invalid native run style evidence.');
|
|
875
885
|
if (!object(stored.native) || typeof stored.native.structure !== 'string' || typeof stored.native.background !== 'string' || typeof stored.native.style !== 'string') throw Error('Missing slide native evidence.');
|
|
876
886
|
if (stored.design !== undefined && !object(stored.design)) throw Error('Invalid slide design record.');
|
|
877
887
|
for (const key of SLIDE_SUPPLEMENT.design) if (object(stored.design) && stored.design[key] !== undefined) throw Error(`Supplement field design.${key} stored in the legacy section.`);
|
|
@@ -886,6 +896,7 @@ function validateSlide(stored) {
|
|
|
886
896
|
if (value.colors !== undefined) validateRunColors(value.colors);
|
|
887
897
|
if (value.lists !== undefined) validateListBreaks(value.lists);
|
|
888
898
|
if (value.listRuns !== undefined) validateListRuns(value.listRuns);
|
|
899
|
+
if (value.listRoles !== undefined) { validateListRoles(value.listRoles); if (!/^[a-f0-9]{14}$/.test(value.native.listIdentity ?? '')) throw Error('Invalid native list identity.'); }
|
|
889
900
|
validateOmitted(value.omitted);
|
|
890
901
|
return value;
|
|
891
902
|
}
|
|
@@ -931,6 +942,8 @@ function pruneDangling(value, dangling, path, removed) {
|
|
|
931
942
|
* unreadable, or its arrangement changed since export (the lines are then no longer the exported ones);
|
|
932
943
|
* - `runs` (opf-pptx#212): the look export wrote on every run of each list, Map(list path -> {text, description?}), or null. It
|
|
933
944
|
* does not depend on the arrangement: import compares each run value with it, so an edited value is still observed.
|
|
945
|
+
* - `roles`: bounded line identities for lists with descriptions, only when native structure and named line positions/order
|
|
946
|
+
* still match. No words or style values are stored; those always come from the current native paragraphs.
|
|
934
947
|
* Any damage is reported later by restoreDocumentProvenance.
|
|
935
948
|
*/
|
|
936
949
|
export function slideListRecord(entries, path, root, rels) {
|
|
@@ -940,7 +953,8 @@ export function slideListRecord(entries, path, root, rels) {
|
|
|
940
953
|
const record = validateSlide(found.value);
|
|
941
954
|
const runs = record.listRuns === undefined ? null : new Map(record.listRuns);
|
|
942
955
|
const breaks = record.lists === undefined || record.native.structure !== nativeSlide(entries, path).structure ? null : new Map(record.lists.map(([list, lines]) => [list, new Map(lines)]));
|
|
943
|
-
|
|
956
|
+
const roles = record.listRoles === undefined || record.native.structure !== nativeSlide(entries, path).structure || record.native.listIdentity !== nativeListIdentity(entries, path) ? null : new Map(record.listRoles);
|
|
957
|
+
return {breaks, runs, roles};
|
|
944
958
|
} catch {
|
|
945
959
|
return {breaks: null, runs: null};
|
|
946
960
|
}
|
|
@@ -1025,7 +1039,7 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
1025
1039
|
return false;
|
|
1026
1040
|
}
|
|
1027
1041
|
let placedPaths;
|
|
1028
|
-
const result = rebuildContent(record.content, imported.slides[index]?.blocks ?? [], slides[index]?.contentBounds ?? [], paths => { placedPaths = paths; });
|
|
1042
|
+
const result = rebuildContent(record.content, imported.slides[index]?.blocks ?? [], slides[index]?.contentBounds ?? [], paths => { placedPaths = paths; }, Object.fromEntries(['fontSize', 'fontFamily', 'color'].map(key => [key, record.native.runStyles?.[key] !== undefined && record.native.runStyles[key] === observed.runStyles?.[key]])));
|
|
1029
1043
|
if (result.reason) {
|
|
1030
1044
|
report({code: 'content-structure-changed', path: `slides.${index}`, message: `The slide's content no longer matches its stored structure (${result.reason}), so the authored groups, regions and block ids of slides.${index} were not restored; the imported slide keeps its flat blocks.${typeNote}`});
|
|
1031
1045
|
return false;
|
|
@@ -1328,7 +1342,7 @@ export function layoutIntent(storedCatalogs, slideRecords, entries, group, repor
|
|
|
1328
1342
|
if (structureMatch && problem === 'source') report({code: 'invalid-document-provenance', path: `slides.${index}.layoutRecord`, message: `slides.${index}.layoutRecord belongs to catalogs.${stated.group}${stated.source ? ` (${stated.source})` : ''}, which the document declares with another source, so the slide's stored layout record was not restored.`});
|
|
1329
1343
|
const target = layout === undefined ? undefined : resolved.get(layout);
|
|
1330
1344
|
let restoreLayout = structureMatch;
|
|
1331
|
-
if (structureMatch && layout !== undefined) {
|
|
1345
|
+
if (structureMatch && layout !== undefined && layout !== 'auto') {
|
|
1332
1346
|
if (!target) {
|
|
1333
1347
|
restoreLayout = false;
|
|
1334
1348
|
report({code: 'unresolved-reference', path: `slides.${index}.layout`, message: `Layout '${layout}' resolves neither in the catalogs the PPTX stores nor in a registered catalog, so slides.${index}.layout was not restored; the imported slide keeps its observed arrangement.`});
|
package/dist/index.d.ts
CHANGED
|
@@ -89,7 +89,9 @@ export interface ToPptxOptions {
|
|
|
89
89
|
* clustered column chart as fallback) for the constructs desktop PowerPoint confirmed natively on 2026-09-30 (treemap,
|
|
90
90
|
* histogram, pareto, box-and-whisker, waterfall, funnel) and keeps `world` on the clustered column chart with
|
|
91
91
|
* `chart-data-adapted` (`chartex-fallback`) because the regionMap part, though accepted with `Requires="cx4"`, draws
|
|
92
|
-
* nothing until PowerPoint fetches map data online.
|
|
92
|
+
* nothing until PowerPoint fetches map data online. Multi-series Pareto also uses editable clustered columns with all series and this diagnostic;
|
|
93
|
+
* explicit native mode rejects it with unsupported-chart-data before returning bytes. Single-series Pareto stays native.
|
|
94
|
+
* "native" writes supported chartex parts, the map included (reports
|
|
93
95
|
* `chart-map-geodata`). "fallback" writes clustered columns only
|
|
94
96
|
* (`chartex-fallback`, `histogram-binned`), as releases before FF-22b did. Import of chartex parts is always on.
|
|
95
97
|
*/
|
|
@@ -166,7 +168,7 @@ export interface ToPptxOptions {
|
|
|
166
168
|
* against a document group's `catalogs.<group>.source`, and the first is the host default that bare ids use when the document
|
|
167
169
|
* omits `catalogs.default`. Never fetched. Every reference (slide layouts, themes, colour and font schemes) resolves in the
|
|
168
170
|
* document's embedded records first, then here; omitted or `[]`, only embedded records resolve. Register the gallery snapshot
|
|
169
|
-
* explicitly with `catalogs: [
|
|
171
|
+
* explicitly with `catalogs: [gallery]` from `@openpresentation/gallery`.
|
|
170
172
|
*/
|
|
171
173
|
catalogs?: readonly Catalog[];
|
|
172
174
|
}
|
|
@@ -177,9 +179,9 @@ export interface FromPptxOptions {
|
|
|
177
179
|
fallbackName?: string;
|
|
178
180
|
schema?: string;
|
|
179
181
|
/**
|
|
180
|
-
* The catalogs the host registered (core `Catalog[]`, as in ToPptxOptions; never fetched). Theme and colour-scheme recovery
|
|
182
|
+
* The catalogs the host registered (core `Catalog[]`, as in ToPptxOptions; never fetched). Omitted catalogs select the default from installed `@openpresentation/gallery` only for foreign layout mapping; explicit `[]` opts out. Theme and colour-scheme recovery requires explicitly registered catalogs and
|
|
181
183
|
* (FF-24) matches the package's theme against the first one, the host default: an exact colour-scheme match imports as its
|
|
182
|
-
* bare id and a corroborated theme name as `design.theme`.
|
|
184
|
+
* bare id and a corroborated theme name as `design.theme`. With omitted catalogs or explicit `catalogs: []` the colour scheme imports as an inline object and
|
|
183
185
|
* no theme id is recovered. A stored slide layout reference also resolves here.
|
|
184
186
|
*/
|
|
185
187
|
catalogs?: readonly Catalog[];
|
|
@@ -410,7 +412,8 @@ export interface PptxOpfLink {
|
|
|
410
412
|
* "block" fed a block (`path`, `blockType`); "title", "subtitle" and "tag" fed that slide field. Other roles name a shape the
|
|
411
413
|
* importer consumed without a block of its own: "furniture" (footer, date, number, section), "background" (the picture of an image background),
|
|
412
414
|
* "image-overlay" (the overlay shape of an image background or image block), "watermark", "logo",
|
|
413
|
-
* and the members of an OPF-tagged group ("code", "metric", "quote", "timeline", "media", "card-frame")
|
|
415
|
+
* and the members of an OPF-tagged group ("code", "metric", "quote", "timeline", "media", "card-frame"), and "prompt" (an empty
|
|
416
|
+
* placeholder: the prompt of a slide PowerPoint added from a layout).
|
|
414
417
|
*/
|
|
415
418
|
role: string;
|
|
416
419
|
/** Path in the presentation, for example "slides.2.blocks.1", "slides.0.title", or "slides.1.left" after a tagged round trip. */
|