@openpresentation/opf-pptx 0.12.1 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -264,7 +264,7 @@ LibreOffice is not a runtime dependency. When it is installed in CI or a local v
264
264
 
265
265
  Public npm package publication is handled by `.github/workflows/release.yml` through npm Trusted Publishing (GitHub Actions OIDC) with npm provenance; no npm token is stored. The owner authorized agents to prepare and publish npm releases whenever a release is required (2026-09-29). This authorization does not waive any gate.
266
266
 
267
- 1. Open a release-prep PR containing only the version bump, `CHANGELOG.md`, dependency ranges, lockfile and current-instruction docs. Publish in dependency order (core, then renderer, then PPTX, then editor): refresh this repo's lockfile only after the required `@openpresentation/opf` and `@openpresentation/opf-render` versions are on the registry (`npm install --package-lock-only`), then run `npm run test:packed` against them.
267
+ 1. Open a release-prep PR containing only the version bump, `CHANGELOG.md` (assembled from `changes/` with `node scripts/changelog-fragments.mjs assemble --version X.Y.Z`), dependency ranges, lockfile and current-instruction docs. Publish in dependency order (core, then renderer, then PPTX, then editor): refresh this repo's lockfile only after the required `@openpresentation/opf` and `@openpresentation/opf-render` versions are on the registry (`npm install --package-lock-only`), then run `npm run test:packed` against them.
268
268
  2. Merge after CI is green, then publish by pushing the git tag `opf-pptx-v<version>` (or `@openpresentation/opf-pptx@v<version>`) at the merge commit. The workflow verifies that the tag matches `package.json` and reruns audit, typecheck, validate, tests, packed and browser checks before `npm publish --access public --provenance`. A manual `workflow_dispatch` runs the same job without the tag check and is a fallback only.
269
269
  3. Verify with `npm view @openpresentation/opf-pptx@<version> version gitHead dist.attestations` and, from the core repo, `node scripts/test-pptx-publication.mjs <version> <release-commit> <this-checkout>`. Never republish an existing version.
270
270
 
@@ -81,3 +81,44 @@ export function writeChartCategoryHeading(entries,chartPart,heading){
81
81
  }
82
82
  entries[context.workbookPart]=zipSync(workbook);
83
83
  }
84
+
85
+ // PptxGenJS 4.0.1 writes broken range metadata into chart workbooks (upstream gitbrent/PptxGenJS#1531): the
86
+ // category-chart branch (bar, line, area, pie, doughnut, radar, and the classic fallback of chartex charts) ends the
87
+ // table ref with a stray apostrophe (ref="A1:C7'"), the bubble branch takes the table's last row from its column
88
+ // count, and the category sheet dimension counts one label column (too narrow for multi-level categories). Keynote
89
+ // drops every chart whose embedded table ref does not parse (opf-pptx#162); Excel repairs the workbook when the chart
90
+ // data is opened. Each worksheet dimension and each table (and table autoFilter) ref that starts at the sheet's first
91
+ // data cell is set to the cell range the sheet actually holds (a correct range is left as it is).
92
+ // `workbook` maps the unzipped workbook part names to bytes and is changed in place.
93
+ const columnNumber=letters=>[...letters].reduce((value,letter)=>value*26+letter.charCodeAt(0)-64,0);
94
+ const columnName=number=>{let name='';for(let n=number;n>0;n=Math.floor((n-1)/26))name=String.fromCharCode(65+(n-1)%26)+name;return name;};
95
+ export function usedRange(sheetXml){
96
+ let minColumn=Infinity,minRow=Infinity,maxColumn=0,maxRow=0;
97
+ for(const [,letters,digits]of sheetXml.matchAll(/<c\b[^>]*?\br="([A-Z]{1,3})(\d{1,7})"/g)){
98
+ const column=columnNumber(letters),row=Number(digits);
99
+ minColumn=Math.min(minColumn,column);minRow=Math.min(minRow,row);maxColumn=Math.max(maxColumn,column);maxRow=Math.max(maxRow,row);
100
+ }
101
+ return maxRow?{start:`${columnName(minColumn)}${minRow}`,end:`${columnName(maxColumn)}${maxRow}`,ref:`${columnName(minColumn)}${minRow}:${columnName(maxColumn)}${maxRow}`,columns:maxColumn-minColumn+1}:undefined;
102
+ }
103
+ export function repairChartWorkbookRanges(workbook){
104
+ for(const sheetPart of Object.keys(workbook)){
105
+ if(!/^xl\/worksheets\/[^/]+\.xml$/.test(sheetPart))continue;
106
+ const sheet=decoder.decode(workbook[sheetPart]),range=usedRange(sheet);
107
+ if(!range)continue;
108
+ const fixedSheet=sheet.replace(/(<dimension\b[^>]*\bref=")[^"]*(")/,(_match,prefix,suffix)=>`${prefix}${range.ref}${suffix}`);
109
+ if(fixedSheet!==sheet)workbook[sheetPart]=encoder.encode(fixedSheet);
110
+ const slash=sheetPart.lastIndexOf('/');
111
+ const relationships=xml(workbook,`${sheetPart.slice(0,slash+1)}_rels/${sheetPart.slice(slash+1)}.rels`);
112
+ for(const rel of array(relationships?.Relationships?.Relationship)){
113
+ const ref=relationship(workbook,sheetPart,rel.Id);
114
+ if(!ref?.type?.endsWith('/table')||!workbook[ref.path])continue;
115
+ const table=decoder.decode(workbook[ref.path]);
116
+ // Only a table anchored at the sheet's first data cell (every PptxGenJS chart table) is resized.
117
+ const start=/<table\b[^>]*\bref="\$?([A-Z]+)\$?(\d+)/.exec(table);
118
+ if(!start||`${start[1]}${start[2]}`!==range.start)continue;
119
+ const fixed=table.replace(/(<(?:table|autoFilter)\b[^>]*\bref=")[^"]*(")/g,(_match,prefix,suffix)=>`${prefix}${range.ref}${suffix}`);
120
+ if(fixed!==table)workbook[ref.path]=encoder.encode(fixed);
121
+ }
122
+ }
123
+ return workbook;
124
+ }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {isFaceStyleSuffix} from './font-weights.js';
4
4
  import {importTableFrames} from './table-import.js';
5
5
  import {applyChartFonts, applyPitchFamilies, finalizeFontsUsed, fontPitchFamilies} from './package-fonts.js';
6
6
  import {giveNotesMastersOwnThemes} from './master-themes.js';
7
- import {readChartCategoryHeading,writeChartCategoryHeading} from './chart-workbook.js';
7
+ import {readChartCategoryHeading,writeChartCategoryHeading,repairChartWorkbookRanges} from './chart-workbook.js';
8
8
  import {CHARTEX_FALLBACK,resolveChartType,chartTypeFromNative,applyChartConstruct,NATIVE_CHART_ELEMENTS} from './chart-types.js';
9
9
  import {attachChartexParts,chartFromChartex,CHARTEX_GRAPHIC_DATA_URI} from './chartex.js';
10
10
  import {applyDataLabels,chartOptionsFromClassic,chartTargetFor,classicChartOptions,reportChartOptionDiagnostics,resolveChartOptionsFor} from './chart-options.js';
@@ -3623,7 +3623,8 @@ function normalizePartReferences(value, renameMaps) {
3623
3623
  }
3624
3624
 
3625
3625
  function normalizeNestedZip(bytes, context) {
3626
- const entries = unzipSync(bytes);
3626
+ // RR-17 (opf-pptx#162): valid table and dimension ranges, or Keynote drops the chart (see chart-workbook.js).
3627
+ const entries = repairChartWorkbookRanges(unzipSync(bytes));
3627
3628
  const output = {};
3628
3629
  for (const path of Object.keys(entries).sort()) {
3629
3630
  // A gap in the chart data is a blank workbook cell, not a numeric cell with an empty value.
@@ -90,6 +90,22 @@ function contentEastAsianFonts(presentation, deck, report) {
90
90
  }
91
91
  }
92
92
 
93
+ // RR-17 (RR-42 native run, 2026-10-02): Office's theme carries per-language script entries that core's script model does
94
+ // not name, because the language is written in a script core treats as Latin or as the plain complex-script slot:
95
+ // `Viet` (Vietnamese, `vi`) and `Uigh` (Uyghur, `ug`). PowerPoint applies the entry to runs in that language and lists its
96
+ // family in Presentation.Fonts, so the Office default (Viet: Times New Roman / Arial) named a font the deck never uses.
97
+ // The deck's own language entry names the family the deck uses for its text: the theme latin family (major latin in
98
+ // majorFont, minor latin in minorFont); for Uyghur, the complex-script family when the deck selects one. Entries for
99
+ // other languages stay as vendored.
100
+ const LANGUAGE_SCRIPT_SUPPLEMENTS = [[/^vi(?:-|$)/i, "Viet"], [/^ug(?:-|$)/i, "Uigh"]];
101
+ function languageScriptSupplement(deck) {
102
+ const match = LANGUAGE_SCRIPT_SUPPLEMENTS.find(([tag]) => tag.test(String(deck.lang ?? "")));
103
+ if (!match) return undefined;
104
+ const script = match[1];
105
+ const role = script === "Uigh" && deck.sources?.complexScript && deck.sources.complexScript !== "latin" ? "complexScript" : "latin";
106
+ return {script, heading: deck.heading?.[role], body: deck.body?.[role]};
107
+ }
108
+
93
109
  const escapeAttribute = value => String(value).replace(/[&<>"']/g, char => ({"&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&apos;"}[char]));
94
110
 
95
111
  /**
@@ -108,8 +124,10 @@ const escapeAttribute = value => String(value).replace(/[&<>"']/g, char => ({"&"
108
124
  * Japanese deck writes `ea` only.
109
125
  */
110
126
  export function themeScriptFonts(xml, plan) {
111
- const {heading, body, supplement} = plan.deck;
112
- for (const [tag, slots, family] of [["majorFont", heading, supplement?.heading], ["minorFont", body, supplement?.body]]) {
127
+ const {heading, body} = plan.deck;
128
+ // Core's supplement (the language's script entry, such as Arab for Uyghur) and the language's own entry (Viet, Uigh).
129
+ const supplements = [plan.deck.supplement, languageScriptSupplement(plan.deck)].filter((entry, index, all) => entry && all.findIndex(other => other?.script === entry.script) === index);
130
+ for (const [tag, slots, role] of [["majorFont", heading, "heading"], ["minorFont", body, "body"]]) {
113
131
  xml = xml.replace(new RegExp(`<a:${tag}>[\\s\\S]*?</a:${tag}>`), block => {
114
132
  for (const [element, slot] of SCRIPT_SLOTS) {
115
133
  // FF-05: the East Asian slot always names a font (a script font, a font for the East Asian text, else the latin
@@ -120,7 +138,9 @@ export function themeScriptFonts(xml, plan) {
120
138
  const face = escapeAttribute(content ?? slots[slot]);
121
139
  block = block.replace(new RegExp(`<a:${element}\\b[^>]*/>`), `<a:${element} typeface="${face}"/>`);
122
140
  }
123
- if (supplement && family) {
141
+ for (const supplement of supplements) {
142
+ const family = supplement[role];
143
+ if (!family) continue;
124
144
  const entry = `<a:font script="${supplement.script}" typeface="${escapeAttribute(family)}"/>`;
125
145
  const existing = new RegExp(`<a:font script="${supplement.script}" typeface="[^"]*"/>`);
126
146
  block = existing.test(block) ? block.replace(existing, entry) : block.replace(`</a:${tag}>`, `${entry}</a:${tag}>`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-pptx",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,8 +37,11 @@
37
37
  "sideEffects": false,
38
38
  "scripts": {
39
39
  "build": "node scripts/build.mjs",
40
- "typecheck": "node --check src/index.js && node --check src/annotation-export.js && node --check src/annotation-provenance.js && node --check test/annotations.mjs && node --check test/region-reading-order.mjs && node --check test/symbol-fonts.mjs && node --check test/math-emoji.mjs && node --check src/svg-image.js && node --check test/svg-image.mjs && node --check test/native-svg-decks.mjs && node --check src/chart-types.js && node --check src/chartex.js && node --check src/chart-options.js && node --check test/chart-options.mjs && node --check test/chartex.mjs && node --check test/chart-text-size.mjs && node --check test/chart-types.mjs && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check test/tag-colour.mjs && node --check test/table-theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/logo-provenance.js && node --check test/design-fields.mjs && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/single-series-colour.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check src/quote-provenance.js && node --check test/quote-provenance.mjs && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check src/content-topology.js && node --check src/sections.js && node --check test/document-provenance.mjs && node --check test/content-topology.mjs && node --check test/sections.mjs && node --check test/provenance-interop.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check src/native-furniture.js && node --check test/native-furniture.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/rtl-layout.mjs && node --check test/theme-script-slots.mjs && node --check src/master-themes.js && node --check test/presentation-order.mjs && node --check test/notes-master-theme.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/svg-image-degrade.mjs && node --check test/background-colorref.mjs && node --check test/import-fabrications.mjs && node --check src/run-colors.js && node --check test/hard-line-breaks.mjs && node --check test/import-details.mjs && node --check test/list-shape-names.mjs && node --check src/numbered-list.js && node --check test/numbered-lists.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs && node --check test/gallery-layout-alignment.mjs && node --check src/import-signals.js && node --check test/import-signals.mjs && node --check test/signals-deck.mjs && node --check test/rr-07-preview-polish.mjs && node --check scripts/rr07-native-set.mjs",
41
- "test": "npm run build && node test/annotations.mjs && node test/region-reading-order.mjs && node test/symbol-fonts.mjs && node test/math-emoji.mjs && node test/chart-types.mjs && node test/chartex.mjs && node test/chart-options.mjs && node test/chart-text-size.mjs && node test/zip-date.mjs && node test/background-fills.mjs && node test/rr-07-preview-polish.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/design-fields.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/content-topology.mjs && node test/sections.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/rtl-layout.mjs && node test/theme-script-slots.mjs && node test/presentation-order.mjs && node test/notes-master-theme.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/tag-colour.mjs && node test/table-theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/single-series-colour.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/svg-image-degrade.mjs && node test/svg-image.mjs && node test/background-colorref.mjs && node test/import-fabrications.mjs && node test/hard-line-breaks.mjs && node test/import-details.mjs && node test/list-shape-names.mjs && node test/numbered-lists.mjs && node test/shared-quote.mjs && node test/quote-provenance.mjs && node test/import-signals.mjs && npm run test:timeline && npm run test:determinism && node test/template-variables.mjs",
40
+ "typecheck": "node scripts/check-syntax.mjs src test scripts",
41
+ "test": "npm run build && node scripts/run-tests.mjs && npm run build:browser-check && npm run check:runners && npm run check:changes",
42
+ "test:contract": "npm run build && node scripts/run-tests.mjs --suite contract && npm run build:browser-check",
43
+ "check:runners": "node --test scripts/run-tests.test.mjs",
44
+ "check:changes": "node --test scripts/changelog-fragments.test.mjs && node scripts/changelog-fragments.mjs check",
42
45
  "validate": "node scripts/validate-package.mjs",
43
46
  "test:packed": "node test/packed-install.mjs && node test/provenance-interop.mjs",
44
47
  "test:compare-published": "node test/compare-published.mjs",