@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.
@@ -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 (FA-23, OPF 0.15, unreleased; requires core 0.15): the package ships and reads no catalog data of its own. `toPptx(presentation, { catalogs })` and `fromPptx(bytes, { catalogs })` take core's registered catalogs (`Catalog[]`: `{ source, layouts?, themes?, colorSchemes?, fontSchemes?, ... }`, records keyed by id, never fetched), the same option opf-render and core's `resolveSlideContext` take. Every reference (slide `layout`, `design.theme`, `colorScheme`, `fontScheme`, a language object's font schemes) resolves through core: the document's embedded `catalogs.<group>` records first, then the registered catalog whose `source` the group names; the first registered catalog is the host default for bare ids, and `"default": false` closes that fallback. Register the gallery snapshot explicitly: `import { defaultCatalog } from "@openpresentation/opf/catalog"; toPptx(deck, { catalogs: [defaultCatalog] })`. A reference that resolves nowhere is reported once per path as core's `unresolved-reference` (kind, reference, path, group, source, fallback); the slide composes automatically and the theme and schemes draw with core's engine defaults (`ENGINE_DEFAULT_THEME`, `ENGINE_DEFAULT_COLOR_SCHEME`, `ENGINE_DEFAULT_FONT_SCHEME`; the theme part's colour scheme is then named `OpenPresentation`). `strictReferences: true` (default: the value of `strictAssets`) makes such a reference fail the export with `OPFPptxError` code `unresolved-reference`, its `path` and core's `diagnostics`. Removed with no alias: the per-source host record option, the `{kind: records[]}` form of `catalogs` (now refused with core's `OPFCatalogsOptionError`, code `invalid-catalogs`), catalog language ids, and catalog social-platform records (core formats socials from its `SOCIAL_PLATFORMS` vocabulary).
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`; nothing is bundled): the bare ids it imports resolve in a document that declares no catalogs. 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 no catalog registered, 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.
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.
@@ -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
- return box ? {field, box: [box.x, box.y, box.width, box.height].map(round1), ...lines} : {field};
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
  };
@@ -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
- /** The record of a chart that uses an RR-54 data field, or undefined (the frame then carries no tag). */
215
- export function chartDataRecord(chart, datasets) {
216
- if (!chartUsesDataFields(chart) && !object(chart?.highlight)) return undefined;
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 nativeSlide(entries, path) {
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/v1';
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
- return {breaks, runs};
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. "native" writes every chartex part, the map included (reports
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: [defaultCatalog]` from `@openpresentation/opf/catalog`.
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`. Without catalogs the colour scheme imports as an inline object and
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. */