@openpresentation/opf-pptx 0.12.4 → 0.13.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.
@@ -9,7 +9,8 @@ const SLIDE_MASTER = /^ppt\/slideMasters\/slideMaster\d+\.xml$/;
9
9
  const relsOf = path => path.replace(/([^/]+)$/, '_rels/$1.rels');
10
10
 
11
11
  /**
12
- * FF-05 (font-fidelity-everywhere). PptxGenJS 4.0.1 points the notes master at the slide master's theme part
12
+ * FF-05 (font-fidelity-everywhere). PptxGenJS 4.0.1 points the notes master at the slide master's theme part (and
13
+ * vendor-compat.js points pptxgenjs-plus's notes master back there, replacing its default Office notes theme)
13
14
  * (`ppt/theme/theme1.xml`). Every master owns its theme part in a PowerPoint package (theme1 for the slide master,
14
15
  * theme2 for the notes master, theme3 for a handout master), and PowerPoint refuses to read a package whose notes
15
16
  * master it recognises when that master shares the slide theme ("The file or directory is corrupted and unreadable",
@@ -47,3 +48,86 @@ export function giveNotesMastersOwnThemes(output) {
47
48
  }
48
49
  if (overrides) output['[Content_Types].xml'] = [encoder.encode(text(types[0]).replace('</Types>', `${overrides}</Types>`)), types[1]];
49
50
  }
51
+
52
+ const MASTER_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml';
53
+ const LAYOUT_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml';
54
+ const REL_MASTER = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster';
55
+ const RELATIONSHIP = /<Relationship\b[^>]*\/>/g;
56
+ const attribute = (xml, name) => new RegExp(`\\s${name}="([^"]*)"`).exec(xml)?.[1];
57
+ const partNumber = (output, pattern) => Math.max(0, ...Object.keys(output).map(path => Number(pattern.exec(path)?.[1] ?? 0)));
58
+ const read = (output, path) => text(output[path][0]);
59
+ const write = (output, path, xml, options) => { output[path] = [encoder.encode(xml), options]; };
60
+
61
+ /**
62
+ * opf-pptx#168 (FF-05). Slide runs name no East Asian / complex-script font (stripRunScriptFonts: PowerPoint lists an
63
+ * explicit run `ea`/`cs` as an empty-name font), so a slide's script fonts reach PowerPoint only through the theme of
64
+ * its slide master. `themes` and `assignment` come from planSlideThemes (script-fonts.js): theme 0 is the presentation
65
+ * theme of slide master 1, and each further theme gets its own slide master, a copy of master 1 with a copy of its
66
+ * layout, so the slides that select those script fonts are drawn with them while every other part stays as it was.
67
+ *
68
+ * Runs after giveNotesMastersOwnThemes (the notes master keeps its copy of the presentation theme) and before the
69
+ * fonts-used list and the document provenance, which read the finished parts. With one theme nothing changes, so a
70
+ * deck with a single script profile is byte-identical. New parts are numbered after the existing ones: slide masters
71
+ * and layouts, themes (`1_<name>`, `2_<name>`, ... as PowerPoint names a second master's theme), master and layout ids
72
+ * after the largest id in use, presentation relationship ids after the largest `rIdN`.
73
+ */
74
+ export function giveSlidesScriptMasters(output, {themes, assignment}) {
75
+ if (themes.length < 2) return;
76
+ const presentation = 'ppt/presentation.xml', presentationRels = relsOf(presentation);
77
+ const master = 'ppt/slideMasters/slideMaster1.xml', masterRels = relsOf(master);
78
+ const masterRelsXml = read(output, masterRels);
79
+ const layoutTarget = [...masterRelsXml.matchAll(RELATIONSHIP)].map(([node]) => node).filter(node => attribute(node, 'Type')?.endsWith('/slideLayout'));
80
+ if (layoutTarget.length !== 1) throw new Error('Slide master 1 must have exactly one layout to copy for a script profile.');
81
+ const layout = `ppt/slideLayouts/${attribute(layoutTarget[0], 'Target').replace(/^\.\.\/slideLayouts\//, '')}`, layoutRels = relsOf(layout);
82
+ const theme = `ppt/theme/${THEME_TARGET.exec(masterRelsXml)?.[1]}`;
83
+ if (!output[layout] || !output[layoutRels] || !output[theme]) throw new Error('Slide master 1, its layout and its theme must exist.');
84
+ const options = output[master][1];
85
+ let presentationXml = read(output, presentation), presentationRelsXml = read(output, presentationRels), types = read(output, '[Content_Types].xml');
86
+ const ids = [...presentationXml.matchAll(/<p:sldMasterId\b[^>]*\bid="(\d+)"/g), ...Object.keys(output).filter(path => SLIDE_MASTER.test(path))
87
+ .flatMap(path => [...read(output, path).matchAll(/<p:sldLayoutId\b[^>]*\bid="(\d+)"/g)])].map(match => Number(match[1]));
88
+ let nextId = Math.max(2147483647, ...ids) + 1;
89
+ let nextRel = Math.max(0, ...[...presentationRelsXml.matchAll(/\bId="rId(\d+)"/g)].map(match => Number(match[1]))) + 1;
90
+ let nextMaster = partNumber(output, /^ppt\/slideMasters\/slideMaster(\d+)\.xml$/) + 1;
91
+ let nextLayout = partNumber(output, /^ppt\/slideLayouts\/slideLayout(\d+)\.xml$/) + 1;
92
+ let nextTheme = partNumber(output, /^ppt\/theme\/theme(\d+)\.xml$/) + 1;
93
+ const layoutFor = [layout];
94
+ let masterIds = '', overrides = '';
95
+ themes.forEach((themeXml, index) => {
96
+ if (index === 0) return;
97
+ const masterName = `slideMaster${nextMaster++}.xml`, layoutName = `slideLayout${nextLayout++}.xml`, themeName = `theme${nextTheme++}.xml`;
98
+ const masterId = nextId++, layoutId = nextId++, relId = `rId${nextRel++}`;
99
+ write(output, `ppt/theme/${themeName}`, themeXml.replace(/(<a:theme\b[^>]*\bname=")([^"]*)"/, (match, open, name) => `${open}${index}_${name}"`), output[theme][1]);
100
+ write(output, `ppt/slideMasters/${masterName}`, read(output, master).replace(/<p:sldLayoutIdLst>[\s\S]*?<\/p:sldLayoutIdLst>/,
101
+ list => list.replace(/(<p:sldLayoutId\b[^>]*\bid=")\d+"/, (match, open) => `${open}${layoutId}"`)), options);
102
+ write(output, `ppt/slideMasters/_rels/${masterName}.rels`, masterRelsXml.replace(RELATIONSHIP, node => {
103
+ const type = attribute(node, 'Type') ?? '';
104
+ if (type.endsWith('/slideLayout')) return node.replace(/\sTarget="[^"]*"/, ` Target="../slideLayouts/${layoutName}"`);
105
+ if (type.endsWith('/theme')) return node.replace(/\sTarget="[^"]*"/, ` Target="../theme/${themeName}"`);
106
+ return node;
107
+ }), output[masterRels][1]);
108
+ write(output, `ppt/slideLayouts/${layoutName}`, read(output, layout), output[layout][1]);
109
+ write(output, `ppt/slideLayouts/_rels/${layoutName}.rels`, read(output, layoutRels).replace(RELATIONSHIP, node =>
110
+ attribute(node, 'Type') === REL_MASTER ? node.replace(/\sTarget="[^"]*"/, ` Target="../slideMasters/${masterName}"`) : node), output[layoutRels][1]);
111
+ masterIds += `<p:sldMasterId id="${masterId}" r:id="${relId}"/>`;
112
+ presentationRelsXml = presentationRelsXml.replace('</Relationships>', `<Relationship Id="${relId}" Type="${REL_MASTER}" Target="slideMasters/${masterName}"/></Relationships>`);
113
+ overrides += `<Override PartName="/ppt/slideMasters/${masterName}" ContentType="${MASTER_TYPE}"/><Override PartName="/ppt/slideLayouts/${layoutName}" ContentType="${LAYOUT_TYPE}"/><Override PartName="/ppt/theme/${themeName}" ContentType="${THEME_TYPE}"/>`;
114
+ layoutFor.push(`ppt/slideLayouts/${layoutName}`);
115
+ });
116
+ if (!presentationXml.includes('</p:sldMasterIdLst>')) throw new Error('The presentation has no slide master list.');
117
+ presentationXml = presentationXml.replace('</p:sldMasterIdLst>', `${masterIds}</p:sldMasterIdLst>`);
118
+ write(output, presentation, presentationXml, output[presentation][1]);
119
+ write(output, presentationRels, presentationRelsXml, output[presentationRels][1]);
120
+ write(output, '[Content_Types].xml', types.replace('</Types>', `${overrides}</Types>`), output['[Content_Types].xml'][1]);
121
+ // Each slide's layout relationship points at the layout of its theme's master.
122
+ const slides = [...presentationXml.matchAll(/<p:sldId\b[^>]*\br:id="([^"]+)"/g)].map(match => {
123
+ const node = [...presentationRelsXml.matchAll(RELATIONSHIP)].map(([rel]) => rel).find(rel => attribute(rel, 'Id') === match[1]);
124
+ return `ppt/${attribute(node ?? '', 'Target')}`;
125
+ });
126
+ assignment.forEach((index, slideIndex) => {
127
+ if (!index) return;
128
+ const slide = slides[slideIndex], rels = slide && relsOf(slide);
129
+ if (!rels || !output[rels]) throw new Error(`Slide ${slideIndex + 1} has no relationships to point at its slide layout.`);
130
+ const target = layoutFor[index].replace(/^ppt\/slideLayouts\//, '../slideLayouts/');
131
+ write(output, rels, read(output, rels).replace(RELATIONSHIP, node => attribute(node, 'Type')?.endsWith('/slideLayout') ? node.replace(/\sTarget="[^"]*"/, ` Target="${target}"`) : node), output[rels][1]);
132
+ });
133
+ }
@@ -57,7 +57,104 @@ export function planScriptFonts(presentation, report) {
57
57
  report?.({code: "paragraph-direction-unavailable", path: "language",
58
58
  message: "The installed @openpresentation/opf has no paragraphDirection, so right-to-left paragraphs are not marked; the preview and export must share that rule. Use a core release that exports it."});
59
59
  }
60
- return {deck, slides, lang: deck.lang, rtl, contentEastAsian: contentEastAsianFonts(presentation, deck, report)};
60
+ // opf-pptx#168: which slides have notes (a presentation has one notes master; see reportPerSlideNotesScriptFonts).
61
+ const notes = (presentation.slides ?? []).map(slide => typeof slide?.notes === "string" ? slide.notes.trim() !== "" : Boolean(slide?.notes));
62
+ return {deck, slides, notes, report, lang: deck.lang, rtl, contentEastAsian: contentEastAsianFonts(presentation, deck, report)};
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Per-slide script profiles (opf-pptx#168)
67
+
68
+ const FONT_GROUPS = [["majorFont", "heading"], ["minorFont", "body"]];
69
+
70
+ /**
71
+ * The theme font slots a slide's resolved script fonts select: for the major (heading) and minor (body) font group,
72
+ * the `ea` / `cs` family when a script font is selected for that slot (its source is not `latin`), and core's
73
+ * per-script supplement entry. Values are escaped as in the theme XML. A slot with nothing selected is no requirement:
74
+ * the slide's script text then follows whatever the theme carries, as before.
75
+ */
76
+ export function slideScriptSelections(resolved) {
77
+ const wanted = [];
78
+ for (const [tag, role] of FONT_GROUPS) {
79
+ for (const [element, slot] of SCRIPT_SLOTS) {
80
+ if (resolved.sources?.[slot] !== "latin" && resolved[role]?.[slot]) wanted.push({tag, element, value: escapeAttribute(resolved[role][slot])});
81
+ }
82
+ if (resolved.supplement?.script && resolved.supplement[role]) wanted.push({tag, script: resolved.supplement.script, value: escapeAttribute(resolved.supplement[role])});
83
+ }
84
+ return wanted;
85
+ }
86
+
87
+ /** The ea/cs typefaces and per-script entries of a theme part, as written: {majorFont: {ea, cs, scripts}, minorFont: ...}. */
88
+ export function themeFontSlots(xml) {
89
+ const slots = {};
90
+ for (const [tag] of FONT_GROUPS) {
91
+ const block = new RegExp(`<a:${tag}>[\\s\\S]*?</a:${tag}>`).exec(xml)?.[0] ?? "";
92
+ const face = element => new RegExp(`<a:${element}\\b[^>]*?\\btypeface="([^"]*)"`).exec(block)?.[1];
93
+ slots[tag] = {ea: face("ea"), cs: face("cs"), scripts: Object.fromEntries([...block.matchAll(/<a:font script="([^"]*)" typeface="([^"]*)"\/>/g)].map(match => [match[1], match[2]]))};
94
+ }
95
+ return slots;
96
+ }
97
+
98
+ /** The selections a theme does not carry. */
99
+ export function unmetSelections(wanted, slots) {
100
+ return wanted.filter(item => (item.script ? slots[item.tag]?.scripts[item.script] : slots[item.tag]?.[item.element]) !== item.value);
101
+ }
102
+
103
+ /** A theme part with the given selections written into its major/minor `ea`/`cs` slots and per-script entries. */
104
+ export function themeWithSelections(xml, wanted) {
105
+ for (const [tag] of FONT_GROUPS) {
106
+ xml = xml.replace(new RegExp(`<a:${tag}>[\\s\\S]*?</a:${tag}>`), block => {
107
+ for (const item of wanted.filter(entry => entry.tag === tag)) {
108
+ if (item.element) {
109
+ block = block.replace(new RegExp(`<a:${item.element}\\b[^>]*/>`), () => `<a:${item.element} typeface="${item.value}"/>`);
110
+ continue;
111
+ }
112
+ const entry = `<a:font script="${item.script}" typeface="${item.value}"/>`;
113
+ const existing = new RegExp(`<a:font script="${item.script}" typeface="[^"]*"/>`);
114
+ block = existing.test(block) ? block.replace(existing, () => entry) : block.replace(`</a:${tag}>`, () => `${entry}</a:${tag}>`);
115
+ }
116
+ return block;
117
+ });
118
+ }
119
+ return xml;
120
+ }
121
+
122
+ /**
123
+ * opf-pptx#168: which theme each slide needs. Theme 0 is the finished presentation theme (`themeXml`, built from slide 1).
124
+ * Each slide uses the first theme that carries every script font it selects (slide 1 always uses theme 0); a slide that
125
+ * none carries gets a new theme: the presentation theme with that slide's selections written in. Slides that select no
126
+ * script font, or the same ones, share a theme, so a deck with one script profile keeps one theme. Returns
127
+ * `{themes: [xml], assignment: [theme index per slide]}`; deterministic in slide order.
128
+ */
129
+ export function planSlideThemes(plan, themeXml) {
130
+ const themes = [{xml: themeXml, slots: themeFontSlots(themeXml)}];
131
+ const assignment = plan.slides.map((resolved, index) => {
132
+ if (index === 0) return 0;
133
+ const wanted = slideScriptSelections(resolved);
134
+ const found = themes.findIndex(theme => !unmetSelections(wanted, theme.slots).length);
135
+ if (found >= 0) return found;
136
+ const xml = themeWithSelections(themeXml, wanted);
137
+ themes.push({xml, slots: themeFontSlots(xml)});
138
+ return themes.length - 1;
139
+ });
140
+ return {themes: themes.map(theme => theme.xml), assignment};
141
+ }
142
+
143
+ /**
144
+ * A presentation has one notes master, whose theme is a copy of the presentation theme (FF-05). The notes of a slide
145
+ * that uses another theme (planSlideThemes) therefore draw their East Asian / complex-script text in the presentation
146
+ * theme's script fonts; that is reported for every such slide that has notes.
147
+ */
148
+ export function reportPerSlideNotesScriptFonts(plan, {themes, assignment}) {
149
+ if (!plan?.report) return;
150
+ const deckSlots = themeFontSlots(themes[0]);
151
+ assignment.forEach((theme, index) => {
152
+ if (!theme || !plan.notes?.[index]) return;
153
+ const unmet = unmetSelections(slideScriptSelections(plan.slides[index]), deckSlots);
154
+ const families = [...new Set(unmet.map(item => item.value))].join(", ");
155
+ plan.report({code: "script-font-notes-not-exported", path: `slides.${index}.notes`,
156
+ message: `Slide ${index + 1} has its own script fonts (${families}) through its own slide master, but a presentation has one notes master, so PowerPoint draws this slide's notes in the presentation theme's East Asian/complex-script fonts.`});
157
+ });
61
158
  }
62
159
 
63
160
  // FF-05: East Asian characters in a deck whose own language has no East Asian font (an English deck with Japanese
@@ -331,7 +428,7 @@ export function matchCatalogLanguage(lang, catalogs) {
331
428
  * dominant run tag itself. Nothing is reported here: languageDiagnostics()
332
429
  * reports against the final imported document, after FF-32 provenance.
333
430
  */
334
- export function observeLanguage({slides, theme, catalogs}) {
431
+ export function observeLanguage({slides, theme, themePath, slideThemes, catalogs}) {
335
432
  const counts = new Map();
336
433
  let rtlParagraphs = 0;
337
434
  for (const xml of slides) {
@@ -345,7 +442,7 @@ export function observeLanguage({slides, theme, catalogs}) {
345
442
  const ranked = [...counts].sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0));
346
443
  const lang = ranked[0]?.[0];
347
444
  const match = lang === undefined ? null : matchCatalogLanguage(lang, catalogs);
348
- return {lang, language: lang === undefined ? undefined : match?.language ?? lang, match, ranked, rtlParagraphs, theme};
445
+ return {lang, language: lang === undefined ? undefined : match?.language ?? lang, match, ranked, rtlParagraphs, theme, themePath, slideThemes};
349
446
  }
350
447
 
351
448
  /**
@@ -404,19 +501,43 @@ export function languageDiagnostics(imported, observed, report) {
404
501
  if (rtlParagraphs && !rtl) {
405
502
  report({code: "rtl-language-mismatch", path: "language", message: `${rtlParagraphs} right-to-left paragraph(s) do not match the left-to-right language ${typeof imported.language === "string" ? imported.language : lang}; paragraph direction is not imported separately.`});
406
503
  }
407
- if (theme && resolved) {
408
- for (const [tag, role] of [["majorFont", "heading"], ["minorFont", "body"]]) {
409
- const block = new RegExp(`<a:${tag}>[\\s\\S]*?</a:${tag}>`).exec(theme)?.[0];
410
- if (!block) continue;
411
- const latin = attribute(/<a:latin\b[^>]*\/>/.exec(block)?.[0] ?? "", "typeface");
412
- for (const [element, slot] of SCRIPT_SLOTS) {
413
- const face = attribute(new RegExp(`<a:${element}\\b[^>]*/>`).exec(block)?.[0] ?? "", "typeface");
414
- if (!face || face.startsWith("+") || face === latin || face === escapeAttribute(resolved[role][slot])) continue;
415
- report({code: "script-font-not-imported", path: "design.fontScheme",
416
- message: `Theme ${tag} ${slot} font "${face}" differs from both its latin font and what the imported document resolves, and is not represented in the imported OPF.`});
504
+ // The presentation theme carries the first slide's script fonts (planScriptFonts), so it is compared with that slide.
505
+ let themeResolved = resolved;
506
+ if (resolver && imported.slides?.length) { try { themeResolved = resolver(imported, {slideIndex: 0}); } catch { themeResolved = resolved; } }
507
+ if (theme && themeResolved) {
508
+ for (const {tag, slot, face} of themeScriptMismatches(theme, themeResolved)) {
509
+ report({code: "script-font-not-imported", path: "design.fontScheme",
510
+ message: `Theme ${tag} ${slot} font "${face}" differs from both its latin font and what the imported document resolves, and is not represented in the imported OPF.`});
511
+ }
512
+ }
513
+ // opf-pptx#168: a slide on another slide master carries its own script fonts in that master's theme.
514
+ if (resolver && Array.isArray(observed.slideThemes)) {
515
+ observed.slideThemes.forEach((slideTheme, index) => {
516
+ if (!slideTheme || slideTheme.path === observed.themePath) return;
517
+ let slideResolved;
518
+ try { slideResolved = resolver(imported, {slideIndex: index}); } catch { return; }
519
+ for (const {tag, slot, face} of themeScriptMismatches(slideTheme.xml, slideResolved)) {
520
+ report({code: "script-font-not-imported", path: `slides.${index}.design.fontScheme`,
521
+ message: `Slide ${index + 1}'s master theme ${tag} ${slot} font "${face}" differs from both its latin font and what the imported slide resolves, and is not represented in the imported OPF.`});
417
522
  }
523
+ });
524
+ }
525
+ }
526
+
527
+ /** Theme ea/cs faces that neither repeat the latin font nor match the resolved slot: [{tag, slot, face}]. */
528
+ function themeScriptMismatches(theme, resolved) {
529
+ const mismatches = [];
530
+ for (const [tag, role] of FONT_GROUPS) {
531
+ const block = new RegExp(`<a:${tag}>[\\s\\S]*?</a:${tag}>`).exec(theme)?.[0];
532
+ if (!block) continue;
533
+ const latin = attribute(/<a:latin\b[^>]*\/>/.exec(block)?.[0] ?? "", "typeface");
534
+ for (const [element, slot] of SCRIPT_SLOTS) {
535
+ const face = attribute(new RegExp(`<a:${element}\\b[^>]*/>`).exec(block)?.[0] ?? "", "typeface");
536
+ if (!face || face.startsWith("+") || face === latin || face === escapeAttribute(resolved[role][slot])) continue;
537
+ mismatches.push({tag, slot, face});
418
538
  }
419
539
  }
540
+ return mismatches;
420
541
  }
421
542
 
422
543
  /**
@@ -26,7 +26,8 @@ const CONTENT_VALUES = Object.freeze({
26
26
  accent4: 'accent4', accent5: 'accent5', accent6: 'accent6',
27
27
  hyperlink: 'hlink', followedHyperlink: 'folHlink',
28
28
  });
29
- // PptxGenJS 4.0.1 createColorElement accepts only these scheme values.
29
+ // PptxGenJS 4.0.1 createColorElement accepts only these scheme values (pptxgenjs-plus accepts more; the set is kept
30
+ // so exported colours stay as they were).
30
31
  const VENDOR_VALUES = new Set(['tx1', 'tx2', 'bg1', 'bg2', 'accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6']);
31
32
 
32
33
  // Role names resolve through core resolveColorRef with the exporter's resolved
@@ -20,7 +20,7 @@ const THEME_SLOT = {lt: 'latin', ea: 'ea', cs: 'cs'};
20
20
  const FIXED_PITCH = 1;
21
21
 
22
22
  // Per-script theme fonts (`<a:font script="…">`) written by the vendored
23
- // PptxGenJS 4.0.1 theme. They are Office's defaults for scripts the deck
23
+ // PptxGenJS theme (4.0.1 and pptxgenjs-plus 4.3.4). They are Office's defaults for scripts the deck
24
24
  // declares no font for. PowerPoint uses one only for text in that script, and
25
25
  // it does not list them in "Fonts Used". The check allows exactly these
26
26
  // script/typeface pairs in the presentation theme; any other supplement fails
@@ -36,6 +36,9 @@ export const THEME_SCRIPT_SUPPLEMENTS = Object.freeze({
36
36
  export function inventoryPptxTypefaces(input, options = {}) {
37
37
  const typefaces = [];
38
38
  const themes = {};
39
+ // Every theme part's fonts, and the theme each part of the presentation package resolves its references against.
40
+ const themeParts = {};
41
+ let partTheme = () => undefined;
39
42
  let fontsUsed = null;
40
43
  const visit = (entries, prefix) => {
41
44
  const packageThemes = [];
@@ -53,20 +56,23 @@ export function inventoryPptxTypefaces(input, options = {}) {
53
56
  if (/^xl\/.*\.xml$/.test(path)) readSpreadsheetFonts(xml, part, typefaces);
54
57
  if (!prefix && path === 'docProps/app.xml') fontsUsed = readFontsUsed(xml);
55
58
  }
56
- // Theme references resolve against the package theme. The exporter writes
57
- // one theme per package; with several, the first (theme1) is used.
59
+ // Theme references resolve against the package theme (theme1). In the presentation package a part on another slide
60
+ // master (opf-pptx#168: one master per script profile) or the notes master resolves against that master's theme.
58
61
  const theme = packageThemes[0];
59
62
  if (theme) themes[prefix] = themeFonts(typefaces, theme);
63
+ for (const part of packageThemes) themeParts[part] = themeFonts(typefaces, part);
64
+ if (!prefix) partTheme = masterTheme(entries);
60
65
  };
61
66
  visit(toEntries(input), '');
62
67
  for (const entry of typefaces) {
63
68
  const reference = THEME_REFERENCE.exec(entry.typeface);
64
69
  if (!reference) continue;
65
70
  const prefix = entry.part.includes('!/') ? entry.part.slice(0, entry.part.lastIndexOf('!/') + 2) : '';
66
- const resolved = themes[prefix]?.[reference[1] === 'mj' ? 'major' : 'minor']?.[THEME_SLOT[reference[2]]];
71
+ const own = prefix ? undefined : partTheme(entry.part);
72
+ const resolved = (own && themeParts[own] ? themeParts[own] : themes[prefix])?.[reference[1] === 'mj' ? 'major' : 'minor']?.[THEME_SLOT[reference[2]]];
67
73
  entry.resolved = resolved ?? null;
68
74
  }
69
- return {typefaces, themes, fontsUsed};
75
+ return {typefaces, themes, themeParts, fontsUsed};
70
76
  }
71
77
 
72
78
  // Fonts a package uses, as PowerPoint lists them under "Fonts Used": the
@@ -99,8 +105,9 @@ export function checkPptxTypefaces(input, options = {}) {
99
105
  // FF-49: options.themeScripts names the East Asian / complex-script families the author selected for the
100
106
  // presentation theme, {major: {ea, cs}, minor: {ea, cs}}. A selected slot must carry exactly that family (an
101
107
  // empty or different one is a violation); a slot with nothing selected may stay empty, as in Office's own themes.
108
+ // options.themeScriptsByPart overrides it for the theme parts it names (opf-pptx#168: a slide master per script profile).
102
109
  const themeSlot = entry => entry.theme && !entry.part.includes('!/') && (entry.element === 'ea' || entry.element === 'cs')
103
- ? options.themeScripts?.[entry.theme]?.[entry.element] || '' : '';
110
+ ? (options.themeScriptsByPart?.[entry.part] ?? options.themeScripts)?.[entry.theme]?.[entry.element] || '' : '';
104
111
  const pitches = new Map();
105
112
  for (const entry of inventory.typefaces) {
106
113
  const wanted = themeSlot(entry);
@@ -179,6 +186,43 @@ function readTypefaces(xml, part, isTheme, output) {
179
186
  }
180
187
  }
181
188
 
189
+ // The theme part a slide, layout, master, notes slide or notes master uses: its own theme relationship, else its
190
+ // layout's, master's or notes master's. Undefined for other parts (charts, the presentation part).
191
+ function masterTheme(entries) {
192
+ const targets = new Map();
193
+ const related = path => {
194
+ if (!targets.has(path)) {
195
+ const rels = entries[path.replace(/([^/]+)$/, '_rels/$1.rels')];
196
+ const list = rels ? [...decoder.decode(rels).matchAll(/<Relationship\b[^>]*\/>/g)].map(([node]) => ({
197
+ type: /\sType="[^"]*\/([^"/]+)"/.exec(node)?.[1], target: /\sTarget="([^"]*)"/.exec(node)?.[1], external: /\sTargetMode="External"/.test(node)
198
+ })).filter(rel => rel.type && rel.target && !rel.external).map(rel => ({type: rel.type, path: resolvePart(path, rel.target)})) : [];
199
+ targets.set(path, list);
200
+ }
201
+ return targets.get(path);
202
+ };
203
+ return part => {
204
+ let path = part;
205
+ for (let depth = 0; depth < 4 && path; depth++) {
206
+ if (!/^ppt\/(?:slides|slideLayouts|slideMasters|notesSlides|notesMasters)\/[^/]+\.xml$/.test(path)) return undefined;
207
+ const rels = related(path);
208
+ const theme = rels.find(rel => rel.type === 'theme')?.path;
209
+ if (theme) return theme;
210
+ path = rels.find(rel => ['slideLayout', 'slideMaster', 'notesMaster'].includes(rel.type))?.path;
211
+ }
212
+ return undefined;
213
+ };
214
+ }
215
+
216
+ function resolvePart(source, target) {
217
+ if (target.startsWith('/')) return target.slice(1);
218
+ const parts = source.split('/').slice(0, -1);
219
+ for (const piece of target.split('/')) {
220
+ if (piece === '..') parts.pop();
221
+ else if (piece !== '.' && piece) parts.push(piece);
222
+ }
223
+ return parts.join('/');
224
+ }
225
+
182
226
  function readSpreadsheetFonts(xml, part, output) {
183
227
  for (const match of xml.matchAll(/<(?:[\w.-]+:)?(name|rFont)\b[^<>]*?\bval="([^"]*)"[^<>]*>/g)) {
184
228
  output.push({part, kind: 'spreadsheetml', element: match[1], typeface: unescapeXml(match[2])});
@@ -0,0 +1,45 @@
1
+ // RR-17 (opf-pptx#162): opf-pptx exports through pptxgenjs-plus 4.3.4 (vendor/pptxgenjs, UPSTREAM.json), the maintained
2
+ // PptxGenJS fork. A few of its output changes are not correctness fixes for opf-pptx and would change what PowerPoint shows
3
+ // or what the exporter's own contracts guarantee; this module puts those parts back to the PptxGenJS 4.0.1 output that the
4
+ // native PowerPoint, Keynote and parity evidence was gathered on. It runs on the raw package, before any other
5
+ // normalization. Every rule names the upstream change it undoes; docs/pptxgenjs-plus-migration.md lists all output changes.
6
+ const decoder = new TextDecoder(), encoder = new TextEncoder();
7
+ const text = bytes => decoder.decode(bytes);
8
+
9
+ // 4.0.1 notes master (makeXmlNotesMaster, byte for byte). pptxgenjs-plus 2d3e872 ("emit a minimal notes master") drops
10
+ // its header, date, slide image, body, footer and slide number placeholders, which sets the notes page layout in
11
+ // PowerPoint (Notes Page view, printing). The repair it cites came from the notes master sharing the slide theme, which
12
+ // opf-pptx already fixes (FF-05, master-themes.js), and PowerPoint opened this master in every native check.
13
+ const LEGACY_NOTES_MASTER = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>${"\r\n"}<p:notesMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr><p:sp><p:nvSpPr><p:cNvPr id="2" name="Header Placeholder 1"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="hdr" sz="quarter"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="2971800" cy="458788"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/><a:lstStyle><a:lvl1pPr algn="l"><a:defRPr sz="1200"/></a:lvl1pPr></a:lstStyle><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="3" name="Date Placeholder 2"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="dt" idx="1"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="3884613" y="0"/><a:ext cx="2971800" cy="458788"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/><a:lstStyle><a:lvl1pPr algn="r"><a:defRPr sz="1200"/></a:lvl1pPr></a:lstStyle><a:p><a:fld id="{5282F153-3F37-0F45-9E97-73ACFA13230C}" type="datetimeFigureOut"><a:rPr lang="en-US"/><a:t>7/23/19</a:t></a:fld><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="4" name="Slide Image Placeholder 3"/><p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg" idx="2"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="685800" y="1143000"/><a:ext cx="5486400" cy="3086100"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln w="12700"><a:solidFill><a:prstClr val="black"/></a:solidFill></a:ln></p:spPr><p:txBody><a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="ctr"/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="5" name="Notes Placeholder 4"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" sz="quarter" idx="3"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="685800" y="4400550"/><a:ext cx="5486400" cy="3600450"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/><a:lstStyle/><a:p><a:pPr lvl="0"/><a:r><a:rPr lang="en-US"/><a:t>Click to edit Master text styles</a:t></a:r></a:p><a:p><a:pPr lvl="1"/><a:r><a:rPr lang="en-US"/><a:t>Second level</a:t></a:r></a:p><a:p><a:pPr lvl="2"/><a:r><a:rPr lang="en-US"/><a:t>Third level</a:t></a:r></a:p><a:p><a:pPr lvl="3"/><a:r><a:rPr lang="en-US"/><a:t>Fourth level</a:t></a:r></a:p><a:p><a:pPr lvl="4"/><a:r><a:rPr lang="en-US"/><a:t>Fifth level</a:t></a:r></a:p></p:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="6" name="Footer Placeholder 5"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="ftr" sz="quarter" idx="4"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="0" y="8685213"/><a:ext cx="2971800" cy="458787"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="b"/><a:lstStyle><a:lvl1pPr algn="l"><a:defRPr sz="1200"/></a:lvl1pPr></a:lstStyle><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="7" name="Slide Number Placeholder 6"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldNum" sz="quarter" idx="5"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="3884613" y="8685213"/><a:ext cx="2971800" cy="458787"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="b"/><a:lstStyle><a:lvl1pPr algn="r"><a:defRPr sz="1200"/></a:lvl1pPr></a:lstStyle><a:p><a:fld id="{CE5E9CC1-C706-0F49-92D6-E571CC5EEA8F}" type="slidenum"><a:rPr lang="en-US"/><a:t>‹#›</a:t></a:fld><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp></p:spTree><p:extLst><p:ext uri="{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}"><p14:creationId xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="1024086991"/></p:ext></p:extLst></p:cSld><p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/><p:notesStyle><a:lvl1pPr marL="0" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl1pPr><a:lvl2pPr marL="457200" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl2pPr><a:lvl3pPr marL="914400" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl3pPr><a:lvl4pPr marL="1371600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl4pPr><a:lvl5pPr marL="1828800" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl5pPr><a:lvl6pPr marL="2286000" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl6pPr><a:lvl7pPr marL="2743200" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl7pPr><a:lvl8pPr marL="3200400" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl8pPr><a:lvl9pPr marL="3657600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl9pPr></p:notesStyle></p:notesMaster>`;
14
+
15
+ // pptxgenjs-plus genXmlTextBody writes `<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang=".."/></a:p></p:txBody>`
16
+ // into every shape without text ("Shapes without text still require a <p:txBody>"); 4.0.1 wrote none. CT_Shape's txBody
17
+ // is optional (ECMA-376 Part 1, 19.3.1.43, minOccurs 0) and PowerPoint opened text-less shapes in every native check;
18
+ // an empty body would make every card frame, connector and marker an (empty) text shape for editors and importers.
19
+ const EMPTY_SHAPE_TEXT = /<p:txBody><a:bodyPr\/><a:lstStyle\/><a:p><a:endParaRPr lang="[^"]*"\/><\/a:p><\/p:txBody>(?=<\/p:sp>)/g;
20
+
21
+ /** Rewrites the raw pptxgenjs-plus entries in place (part name -> bytes). */
22
+ export function legacyVendorOutput(entries) {
23
+ for (const part of Object.keys(entries)) {
24
+ if (/^ppt\/slides\/slide\d+\.xml$/.test(part)) {
25
+ const xml = text(entries[part]), output = xml.replace(EMPTY_SHAPE_TEXT, '');
26
+ if (output !== xml) entries[part] = encoder.encode(output);
27
+ }
28
+ }
29
+ if (entries['ppt/notesMasters/notesMaster1.xml']) entries['ppt/notesMasters/notesMaster1.xml'] = encoder.encode(LEGACY_NOTES_MASTER);
30
+ // pptxgenjs-plus 7c92136 ("spec-valid notes theme2") gives the notes master its own default Office theme (Office colours,
31
+ // Office font scheme). 4.0.1 pointed the notes master at theme1, and opf-pptx then gives it a copy of the finished deck
32
+ // theme (FF-05, giveNotesMastersOwnThemes), so PowerPoint lists only the deck's fonts. Point it back at theme1 and drop
33
+ // the vendor theme; the FF-05 copy replaces it after every theme rewrite.
34
+ const rels = 'ppt/notesMasters/_rels/notesMaster1.xml.rels';
35
+ if (entries[rels] && entries['ppt/theme/theme2.xml']) {
36
+ const relsXml = text(entries[rels]);
37
+ if (relsXml.includes('Target="../theme/theme2.xml"')) {
38
+ entries[rels] = encoder.encode(relsXml.replace('Target="../theme/theme2.xml"', 'Target="../theme/theme1.xml"'));
39
+ delete entries['ppt/theme/theme2.xml'];
40
+ const types = text(entries['[Content_Types].xml']);
41
+ entries['[Content_Types].xml'] = encoder.encode(types.replace(/<Override PartName="\/ppt\/theme\/theme2\.xml"[^>]*\/>/, ''));
42
+ }
43
+ }
44
+ return entries;
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-pptx",
3
- "version": "0.12.4",
3
+ "version": "0.13.0",
4
4
  "description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,14 +64,16 @@
64
64
  "test:determinism": "node test/determinism.mjs"
65
65
  },
66
66
  "dependencies": {
67
- "@openpresentation/opf": "^0.12.2",
67
+ "@node-projects/jszip": "4.3.0",
68
+ "@openpresentation/opf": "^0.13.0",
69
+ "@rgrove/parse-xml": "4.2.3",
68
70
  "fast-xml-parser": "^5.8.0",
69
71
  "fflate": "^0.8.3",
70
- "jszip": "3.10.2",
72
+ "pako": "3.0.2",
71
73
  "sharp": "0.35.4"
72
74
  },
73
75
  "peerDependencies": {
74
- "@openpresentation/opf-render": "^0.12.2"
76
+ "@openpresentation/opf-render": "^0.13.0"
75
77
  },
76
78
  "peerDependenciesMeta": {
77
79
  "@openpresentation/opf-render": {
@@ -80,7 +82,7 @@
80
82
  },
81
83
  "devDependencies": {
82
84
  "@expo-google-fonts/carlito": "0.4.1",
83
- "@openpresentation/opf-render": "0.12.2",
85
+ "@openpresentation/opf-render": "0.13.0",
84
86
  "esbuild": "0.28.2",
85
87
  "playwright": "1.63.0"
86
88
  },
@@ -1,21 +1,22 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2015-2022 Brent Ely
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2022 Brent Ely
4
+ Copyright (c) 2026 lofcz
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -1,19 +1,24 @@
1
1
  {
2
- "name": "pptxgenjs",
3
- "version": "4.0.1",
2
+ "name": "pptxgenjs-plus",
3
+ "version": "4.3.4",
4
4
  "license": "MIT",
5
- "tarball": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
6
- "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==",
5
+ "tarball": "https://registry.npmjs.org/pptxgenjs-plus/-/pptxgenjs-plus-4.3.4.tgz",
6
+ "integrity": "sha512-E4mu3Guf2O5CIzy6nS7tlAv55RyhPWinpsj2MdOM5QZ3XukkSne4oipE1HONTfaVLeJG4rkr2lOk6KhwbU/4mQ==",
7
+ "provenance": {
8
+ "repository": "https://github.com/lofcz/pptxgenjs-plus",
9
+ "ref": "refs/heads/next",
10
+ "commit": "ecfa5086a7a35dc8cc0faa502a61794a577554d5"
11
+ },
7
12
  "files": {
8
13
  "pptxgen.es.js": {
9
14
  "source": "package/dist/pptxgen.es.js",
10
- "bytes": 516931,
11
- "sha256": "05844c5625e2cda3b449eb967c2246dd57ca57341886a7c28eeebca263b29bd4"
15
+ "bytes": 1230609,
16
+ "sha256": "71f89c1bea86cee3c89f9ec948da99ef1f7dacf38fab33a21b6857ca6fb20dd1"
12
17
  },
13
18
  "LICENSE": {
14
19
  "source": "package/LICENSE",
15
- "bytes": 1081,
16
- "sha256": "7a2bfe96150786ed1908b8e63f98ebab88875c1e79e28faff6649e0f11f77e52"
20
+ "bytes": 1128,
21
+ "sha256": "09b419524813383278d9353e387b75abec06c184336e5c48974a377804b0239a"
17
22
  }
18
23
  }
19
24
  }