node-pptx-templater 1.0.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +415 -0
  4. package/package.json +83 -0
  5. package/src/cli/commands/build.js +79 -0
  6. package/src/cli/commands/debug.js +46 -0
  7. package/src/cli/commands/extract.js +42 -0
  8. package/src/cli/commands/inspect.js +39 -0
  9. package/src/cli/commands/validate.js +36 -0
  10. package/src/cli/index.js +132 -0
  11. package/src/core/OutputWriter.js +181 -0
  12. package/src/core/PPTXTemplater.js +961 -0
  13. package/src/core/TemplateEngine.js +321 -0
  14. package/src/index.js +43 -0
  15. package/src/managers/ChartManager.js +317 -0
  16. package/src/managers/ContentTypesManager.js +160 -0
  17. package/src/managers/HyperlinkManager.js +451 -0
  18. package/src/managers/MediaManager.js +307 -0
  19. package/src/managers/RelationshipManager.js +401 -0
  20. package/src/managers/SlideManager.js +950 -0
  21. package/src/managers/TableManager.js +416 -0
  22. package/src/managers/ZipManager.js +298 -0
  23. package/src/managers/charts/ChartCacheGenerator.js +156 -0
  24. package/src/managers/charts/ChartParser.js +43 -0
  25. package/src/managers/charts/ChartRelationshipManager.js +33 -0
  26. package/src/managers/charts/ChartWorkbookUpdater.js +130 -0
  27. package/src/parsers/XMLParser.js +291 -0
  28. package/src/templates/blankPptx.js +1 -0
  29. package/src/templates/slideTemplate.js +314 -0
  30. package/src/utils/contentTypesHelper.js +149 -0
  31. package/src/utils/errors.js +129 -0
  32. package/src/utils/idUtils.js +54 -0
  33. package/src/utils/logger.js +113 -0
  34. package/src/utils/relationshipUtils.js +89 -0
  35. package/src/utils/xmlUtils.js +115 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @fileoverview `extract` CLI command — extracts XML parts from a PPTX.
3
+ */
4
+ import chalk from 'chalk';
5
+ import { resolve } from 'path';
6
+ import { writeFileSync } from 'fs';
7
+ import { PPTXTemplater } from '../../index.js';
8
+
9
+ export async function extractCommand(filePath, opts) {
10
+ try {
11
+ const ppt = await PPTXTemplater.load(resolve(filePath));
12
+
13
+ if (opts.slide) {
14
+ const slideNum = parseInt(opts.slide, 10);
15
+ // Access internal zip via the engine's buffer
16
+ const buffer = await ppt.toBuffer();
17
+ const JSZip = (await import('jszip')).default;
18
+ const zip = await JSZip.loadAsync(buffer);
19
+ const slideFile = zip.file(`ppt/slides/slide${slideNum}.xml`);
20
+
21
+ if (!slideFile) {
22
+ console.error(chalk.red(`Slide ${slideNum} not found`));
23
+ process.exit(1);
24
+ }
25
+
26
+ const xml = await slideFile.async('text');
27
+
28
+ if (opts.out) {
29
+ writeFileSync(resolve(opts.out), xml, 'utf-8');
30
+ console.log(chalk.green(`✓ Extracted slide ${slideNum} to ${opts.out}`));
31
+ } else {
32
+ console.log(xml);
33
+ }
34
+ } else {
35
+ console.log(chalk.yellow('Specify --slide <number> to extract'));
36
+ process.exit(1);
37
+ }
38
+ } catch (err) {
39
+ console.error(chalk.red(`Extract failed: ${err.message}`));
40
+ process.exit(1);
41
+ }
42
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @fileoverview `inspect` CLI command — detailed PPTX structure inspection.
3
+ */
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { resolve } from 'path';
7
+ import { PPTXTemplater } from '../../index.js';
8
+
9
+ export async function inspectCommand(filePath, opts) {
10
+ const showAll = opts.all;
11
+ const spinner = ora(`Inspecting: ${filePath}`).start();
12
+
13
+ try {
14
+ const ppt = await PPTXTemplater.load(resolve(filePath));
15
+ const info = ppt.getInfo();
16
+ spinner.stop();
17
+
18
+ console.log(chalk.bold.cyan('\n═══ PPTX Inspection Report ═══\n'));
19
+ console.log(chalk.bold('General:'));
20
+ console.log(` Title: ${info.title || chalk.dim('(none)')}`);
21
+ console.log(` Author: ${info.author || chalk.dim('(none)')}`);
22
+ console.log(` Created: ${info.created || chalk.dim('(unknown)')}`);
23
+ console.log(` Slides: ${chalk.cyan(info.slideCount)}`);
24
+ console.log(` Media: ${chalk.cyan(info.mediaCount)} files`);
25
+
26
+ if (opts.slides || showAll) {
27
+ console.log(chalk.bold('\nSlides:'));
28
+ for (const slide of info.slides) {
29
+ const tags = slide.tags.length > 0 ? chalk.dim(` [${slide.tags.join(', ')}]`) : '';
30
+ console.log(` ${chalk.cyan(slide.index.toString().padStart(2))}. ${slide.zipPath}${tags}`);
31
+ }
32
+ }
33
+
34
+ console.log('');
35
+ } catch (err) {
36
+ spinner.fail(chalk.red(`Inspect failed: ${err.message}`));
37
+ process.exit(1);
38
+ }
39
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @fileoverview `validate` CLI command.
3
+ */
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { resolve } from 'path';
7
+ import { PPTXTemplater } from '../../index.js';
8
+
9
+ export async function validateCommand(filePath, opts) {
10
+ const spinner = ora(`Validating: ${filePath}`).start();
11
+ try {
12
+ const ppt = await PPTXTemplater.load(resolve(filePath));
13
+ const result = ppt.validate();
14
+ spinner.stop();
15
+
16
+ if (result.valid && result.warnings.length === 0) {
17
+ console.log(chalk.green(`\n✓ Valid PPTX (${ppt.slideCount} slides)\n`));
18
+ } else {
19
+ if (result.errors.length > 0) {
20
+ console.log(chalk.red(`\n✗ Validation errors (${result.errors.length}):\n`));
21
+ result.errors.forEach(e => console.log(chalk.red(` • ${e}`)));
22
+ }
23
+ if (result.warnings.length > 0) {
24
+ console.log(chalk.yellow(`\n⚠ Warnings (${result.warnings.length}):\n`));
25
+ result.warnings.forEach(w => console.log(chalk.yellow(` • ${w}`)));
26
+ }
27
+ }
28
+
29
+ if (!result.valid || (opts.strict && result.warnings.length > 0)) {
30
+ process.exit(1);
31
+ }
32
+ } catch (err) {
33
+ spinner.fail(chalk.red(`Validation failed: ${err.message}`));
34
+ process.exit(1);
35
+ }
36
+ }
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview CLI entry point for node-pptx-templater.
4
+ *
5
+ * Provides command-line access to the template engine's core features.
6
+ *
7
+ * Usage:
8
+ * node-pptx-templater build template.pptx output.pptx [options]
9
+ * node-pptx-templater validate template.pptx
10
+ * node-pptx-templater inspect template.pptx
11
+ * node-pptx-templater extract template.pptx --slide 1 --out ./slide1.xml
12
+ * node-pptx-templater debug template.pptx
13
+ *
14
+ * Install globally:
15
+ * npm install -g node-pptx-templater
16
+ *
17
+ * Then run:
18
+ * node-pptx-templater --help
19
+ */
20
+
21
+ import { Command } from 'commander';
22
+ import chalk from 'chalk';
23
+ import ora from 'ora';
24
+ import { readFileSync } from 'fs';
25
+ import { resolve, dirname } from 'path';
26
+ import { fileURLToPath } from 'url';
27
+ import { PPTXTemplater } from '../index.js';
28
+ import { buildCommand } from './commands/build.js';
29
+ import { validateCommand } from './commands/validate.js';
30
+ import { inspectCommand } from './commands/inspect.js';
31
+ import { extractCommand } from './commands/extract.js';
32
+ import { debugCommand } from './commands/debug.js';
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+
36
+ // Read version from package.json
37
+ const pkg = JSON.parse(
38
+ readFileSync(resolve(__dirname, '../../package.json'), 'utf-8')
39
+ );
40
+
41
+ /**
42
+ * CLI banner displayed on startup.
43
+ */
44
+ function printBanner() {
45
+ console.log(chalk.bold.cyan(`
46
+ ╔═══════════════════════════════════════════════╗
47
+ ║ node-pptx-templater v${pkg.version.padEnd(17)}║
48
+ ║ Low-level OpenXML PowerPoint template engine ║
49
+ ╚═══════════════════════════════════════════════╝
50
+ `));
51
+ }
52
+
53
+ const program = new Command();
54
+
55
+ program
56
+ .name('node-pptx-templater')
57
+ .description('Low-level PowerPoint OpenXML template engine for Node.js')
58
+ .version(pkg.version, '-v, --version', 'Display version number')
59
+ .addHelpText('before', chalk.bold.cyan('\nnode-pptx-templater — PowerPoint XML manipulation engine\n'));
60
+
61
+ // ─── build command ─────────────────────────────────────────────────────────
62
+ program
63
+ .command('build <template> <output>')
64
+ .description('Build a PPTX from a template with data injected from a JSON file')
65
+ .option('-d, --data <file>', 'JSON file with template data (text replacements, etc.)')
66
+ .option('-s, --slide <numbers>', 'Comma-separated slide numbers to include (e.g., 1,3,5)')
67
+ .option('--no-banner', 'Suppress the banner')
68
+ .action(async (template, output, opts) => {
69
+ if (!opts.noBanner) printBanner();
70
+ await buildCommand(template, output, opts);
71
+ });
72
+
73
+ // ─── validate command ───────────────────────────────────────────────────────
74
+ program
75
+ .command('validate <file>')
76
+ .description('Validate the structure of a PPTX file')
77
+ .option('--strict', 'Exit with error code on warnings too')
78
+ .action(async (file, opts) => {
79
+ printBanner();
80
+ await validateCommand(file, opts);
81
+ });
82
+
83
+ // ─── inspect command ────────────────────────────────────────────────────────
84
+ program
85
+ .command('inspect <file>')
86
+ .description('Inspect the internal structure of a PPTX file')
87
+ .option('--slides', 'Show slide details')
88
+ .option('--charts', 'Show chart details')
89
+ .option('--tables', 'Show table details')
90
+ .option('--media', 'Show embedded media files')
91
+ .option('--rels', 'Show relationship tree')
92
+ .option('--all', 'Show everything')
93
+ .action(async (file, opts) => {
94
+ printBanner();
95
+ await inspectCommand(file, opts);
96
+ });
97
+
98
+ // ─── extract command ────────────────────────────────────────────────────────
99
+ program
100
+ .command('extract <file>')
101
+ .description('Extract specific parts from a PPTX file')
102
+ .option('-s, --slide <number>', 'Slide number to extract XML from')
103
+ .option('-o, --out <path>', 'Output file path (default: stdout)')
104
+ .option('--chart <name>', 'Extract chart XML by name')
105
+ .option('--rels', 'Extract relationship files')
106
+ .action(async (file, opts) => {
107
+ await extractCommand(file, opts);
108
+ });
109
+
110
+ // ─── debug command ──────────────────────────────────────────────────────────
111
+ program
112
+ .command('debug <file>')
113
+ .description('Debug a potentially corrupted PPTX structure')
114
+ .option('--fix', 'Attempt automatic repairs')
115
+ .option('-o, --out <path>', 'Output repaired file path')
116
+ .action(async (file, opts) => {
117
+ printBanner();
118
+ await debugCommand(file, opts);
119
+ });
120
+
121
+ // ─── Global error handling ──────────────────────────────────────────────────
122
+ program.exitOverride();
123
+
124
+ try {
125
+ await program.parseAsync(process.argv);
126
+ } catch (err) {
127
+ if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') {
128
+ process.exit(0);
129
+ }
130
+ console.error(chalk.red(`\n✗ Error: ${err.message}\n`));
131
+ process.exit(1);
132
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @fileoverview OutputWriter - Handles PPTX serialization and output.
3
+ *
4
+ * Responsibilities:
5
+ * 1. Flush all pending changes from all managers into the ZipManager
6
+ * 2. Ensure [Content_Types].xml is up to date
7
+ * 3. Generate the final ZIP archive
8
+ * 4. Write to file, buffer, or stream
9
+ */
10
+
11
+ import fsExtra from 'fs-extra';
12
+ const { writeFile, ensureDir } = fsExtra;
13
+ import path from 'path';
14
+ import { XMLParser } from '../parsers/XMLParser.js';
15
+ import { createLogger } from '../utils/logger.js';
16
+ import { PPTXError } from '../utils/errors.js';
17
+ import { Readable } from 'stream';
18
+
19
+ const logger = createLogger('OutputWriter');
20
+
21
+ /**
22
+ * @class OutputWriter
23
+ * @description Serializes the modified PPTX to various output formats.
24
+ */
25
+ export class OutputWriter {
26
+ /** @private @type {ZipManager} */
27
+ #zipManager;
28
+ /** @private @type {ContentTypesManager} */
29
+ #contentTypesManager;
30
+
31
+ /**
32
+ * @param {ZipManager} zipManager
33
+ * @param {ContentTypesManager} contentTypesManager
34
+ */
35
+ constructor(zipManager, contentTypesManager) {
36
+ this.#zipManager = zipManager;
37
+ this.#contentTypesManager = contentTypesManager;
38
+ }
39
+
40
+ /**
41
+ * Writes the final PPTX to a file on disk.
42
+ * Creates parent directories if needed.
43
+ *
44
+ * @param {string} filePath - Output file path.
45
+ * @param {SlideManager} slideManager
46
+ * @param {ZipManager} zipManager
47
+ * @returns {Promise<void>}
48
+ */
49
+ async saveToFile(filePath, slideManager, zipManager) {
50
+ try {
51
+ const buffer = await this.toBuffer(slideManager, zipManager);
52
+ const dir = path.dirname(filePath);
53
+ await ensureDir(dir);
54
+ await writeFile(filePath, buffer);
55
+ logger.info(`Saved to ${filePath} (${(buffer.length / 1024).toFixed(1)} KB)`);
56
+ } catch (err) {
57
+ if (err instanceof PPTXError) throw err;
58
+ throw new PPTXError(`Failed to save file to ${filePath}: ${err.message}`, err);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Returns the PPTX as a Node.js Buffer.
64
+ *
65
+ * @param {SlideManager} slideManager
66
+ * @param {ZipManager} zipManager
67
+ * @returns {Promise<Buffer>}
68
+ */
69
+ async toBuffer(slideManager, zipManager) {
70
+ // Ensure all slides are flushed to the ZIP
71
+ await this.#flushAllSlides(slideManager, zipManager);
72
+
73
+ // Flush Content Types safely
74
+ this.#contentTypesManager.flush(zipManager);
75
+
76
+ // Wait for any queued asynchronous writes (like content types, media hashing)
77
+ await zipManager.waitForPendingWrites();
78
+
79
+ const buffer = await zipManager.toBuffer();
80
+ logger.debug(`Generated buffer: ${(buffer.length / 1024).toFixed(1)} KB`);
81
+ return buffer;
82
+ }
83
+
84
+ /**
85
+ * Returns the PPTX as a readable Node.js stream.
86
+ *
87
+ * @param {SlideManager} slideManager
88
+ * @param {ZipManager} zipManager
89
+ * @returns {Promise<Readable>}
90
+ */
91
+ async toStream(slideManager, zipManager) {
92
+ await this.#flushAllSlides(slideManager, zipManager);
93
+
94
+ // Flush Content Types safely
95
+ this.#contentTypesManager.flush(zipManager);
96
+
97
+ await zipManager.waitForPendingWrites();
98
+ const nodeStream = await zipManager.toStream();
99
+ return nodeStream;
100
+ }
101
+
102
+ /**
103
+ * Ensures all dirty slide XML is committed to the ZipManager.
104
+ * This is called before any output operation.
105
+ *
106
+ * @private
107
+ * @param {SlideManager} slideManager
108
+ * @param {ZipManager} zipManager
109
+ * @returns {Promise<void>}
110
+ */
111
+ async #flushAllSlides(slideManager, zipManager) {
112
+ // SlideManager already writes to zipManager via setSlideXml,
113
+ // so this is mostly a no-op with a validation step.
114
+ const info = slideManager.getAllSlideInfo();
115
+
116
+ for (const slide of info) {
117
+ if (!zipManager.hasFile(slide.zipPath)) {
118
+ logger.warn(`Slide file missing in ZIP: ${slide.zipPath}`);
119
+ }
120
+ }
121
+
122
+ // Update the slide count and titles in docProps/app.xml to prevent repair mode issues
123
+ if (zipManager.hasFile('docProps/app.xml')) {
124
+ zipManager.addPendingPromise(
125
+ zipManager.rawZip.file('docProps/app.xml').async('text').then(content => {
126
+ const parser = new XMLParser();
127
+ const appObj = parser.parse(content, 'app.xml');
128
+ const properties = appObj.Properties;
129
+
130
+ if (properties) {
131
+ // 1. Update Slides count
132
+ properties.Slides = info.length;
133
+
134
+ // 2. Find old slide titles count and update HeadingPairs
135
+ let oldSlideTitlesCount = 0;
136
+ const variants = properties.HeadingPairs?.['vt:vector']?.['vt:variant'];
137
+ if (Array.isArray(variants)) {
138
+ for (let i = 0; i < variants.length; i++) {
139
+ if (variants[i]['vt:lpstr'] === 'Slide Titles') {
140
+ const countVar = variants[i + 1];
141
+ if (countVar) {
142
+ oldSlideTitlesCount = parseInt(countVar['vt:i4'], 10) || 0;
143
+ countVar['vt:i4'] = info.length;
144
+ }
145
+ break;
146
+ }
147
+ }
148
+ }
149
+
150
+ // 3. Update TitlesOfParts
151
+ const titlesVector = properties.TitlesOfParts?.['vt:vector'];
152
+ if (titlesVector) {
153
+ let lpstrs = titlesVector['vt:lpstr'];
154
+ if (lpstrs) {
155
+ if (!Array.isArray(lpstrs)) lpstrs = [lpstrs];
156
+
157
+ // Remove the old slide titles (which are at the end)
158
+ if (oldSlideTitlesCount > 0 && lpstrs.length >= oldSlideTitlesCount) {
159
+ lpstrs = lpstrs.slice(0, lpstrs.length - oldSlideTitlesCount);
160
+ }
161
+
162
+ // Append new slide titles
163
+ const newSlideTitles = info.map(slide => slide.title || `Slide ${slide.index}`);
164
+ lpstrs.push(...newSlideTitles);
165
+
166
+ titlesVector['vt:lpstr'] = lpstrs;
167
+ titlesVector['@_size'] = String(lpstrs.length);
168
+ }
169
+ }
170
+
171
+ const declaration = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
172
+ const updatedXml = parser.build(appObj, declaration);
173
+ zipManager.writeFile('docProps/app.xml', updatedXml);
174
+ }
175
+ })
176
+ );
177
+ }
178
+
179
+ logger.debug(`Flushed ${info.length} slide(s) to ZIP`);
180
+ }
181
+ }