node-pptx-templater 1.0.1 → 1.0.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.
Files changed (37) hide show
  1. package/README.md +336 -281
  2. package/package.json +6 -6
  3. package/src/cli/commands/build.js +32 -31
  4. package/src/cli/commands/debug.js +25 -24
  5. package/src/cli/commands/extract.js +23 -21
  6. package/src/cli/commands/inspect.js +25 -23
  7. package/src/cli/commands/validate.js +19 -17
  8. package/src/cli/index.js +45 -43
  9. package/src/core/OutputWriter.js +81 -78
  10. package/src/core/PPTXTemplater.js +859 -274
  11. package/src/core/TemplateEngine.js +69 -71
  12. package/src/core/ValidationEngine.js +246 -0
  13. package/src/index.js +51 -15
  14. package/src/managers/ChartManager.js +197 -70
  15. package/src/managers/ContentTypesManager.js +51 -45
  16. package/src/managers/HyperlinkManager.js +148 -142
  17. package/src/managers/ImageManager.js +336 -0
  18. package/src/managers/MediaManager.js +64 -81
  19. package/src/managers/RelationshipManager.js +102 -96
  20. package/src/managers/ShapeManager.js +340 -0
  21. package/src/managers/SlideManager.js +410 -311
  22. package/src/managers/TableManager.js +981 -262
  23. package/src/managers/TextManager.js +197 -0
  24. package/src/managers/ZipManager.js +71 -69
  25. package/src/managers/charts/ChartCacheGenerator.js +77 -58
  26. package/src/managers/charts/ChartParser.js +11 -13
  27. package/src/managers/charts/ChartRelationshipManager.js +14 -10
  28. package/src/managers/charts/ChartWorkbookUpdater.js +61 -56
  29. package/src/parsers/XMLParser.js +50 -49
  30. package/src/templates/blankPptx.js +3 -1
  31. package/src/templates/slideTemplate.js +31 -32
  32. package/src/utils/contentTypesHelper.js +41 -53
  33. package/src/utils/errors.js +33 -23
  34. package/src/utils/idUtils.js +23 -15
  35. package/src/utils/logger.js +21 -15
  36. package/src/utils/relationshipUtils.js +28 -22
  37. package/src/utils/xmlUtils.js +37 -29
@@ -35,17 +35,16 @@
35
35
  * - .../slideToSlide → slide → another slide (inter-slide link)
36
36
  */
37
37
 
38
- import { createLogger } from '../utils/logger.js';
39
- import { PPTXError } from '../utils/errors.js';
40
- import { generateRelationshipId } from '../utils/relationshipUtils.js';
38
+ const { createLogger } = require('../utils/logger.js')
39
+ const { generateRelationshipId } = require('../utils/relationshipUtils.js')
41
40
 
42
- const logger = createLogger('RelationshipManager');
41
+ const logger = createLogger('RelationshipManager')
43
42
 
44
43
  /**
45
44
  * OpenXML relationship type constants.
46
45
  * Using shortened forms; full URIs are in the OpenXML spec.
47
46
  */
48
- export const REL_TYPES = {
47
+ const REL_TYPES = {
49
48
  SLIDE: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide',
50
49
  SLIDE_LAYOUT: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout',
51
50
  SLIDE_MASTER: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster',
@@ -55,11 +54,14 @@ export const REL_TYPES = {
55
54
  NOTES_SLIDE: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide',
56
55
  THEME: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme',
57
56
  TABLE_STYLES: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles',
58
- PRESENTATION: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
59
- CORE_PROPERTIES: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
60
- EXTENDED_PROPERTIES: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
57
+ PRESENTATION:
58
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
59
+ CORE_PROPERTIES:
60
+ 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
61
+ EXTENDED_PROPERTIES:
62
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
61
63
  PACKAGE: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/package',
62
- };
64
+ }
63
65
 
64
66
  /**
65
67
  * @class RelationshipManager
@@ -70,18 +72,18 @@ export const REL_TYPES = {
70
72
  * - ppt/presentation.xml → ppt/_rels/presentation.xml.rels
71
73
  * - ppt/slides/slide1.xml → ppt/slides/_rels/slide1.xml.rels
72
74
  */
73
- export class RelationshipManager {
75
+ class RelationshipManager {
74
76
  /**
75
77
  * @private
76
78
  * @type {XMLParser}
77
79
  */
78
- #xmlParser;
80
+ #xmlParser
79
81
 
80
82
  /**
81
83
  * @private
82
84
  * @type {ZipManager}
83
85
  */
84
- #zipManager;
86
+ #zipManager
85
87
 
86
88
  /**
87
89
  * @private
@@ -89,13 +91,13 @@ export class RelationshipManager {
89
91
  * Maps zip path → parsed relationships array.
90
92
  * Key: relationship file path (e.g., 'ppt/_rels/presentation.xml.rels')
91
93
  */
92
- #relationships = new Map();
94
+ #relationships = new Map()
93
95
 
94
96
  /**
95
97
  * @param {XMLParser} xmlParser
96
98
  */
97
99
  constructor(xmlParser) {
98
- this.#xmlParser = xmlParser;
100
+ this.#xmlParser = xmlParser
99
101
  }
100
102
 
101
103
  /**
@@ -105,19 +107,19 @@ export class RelationshipManager {
105
107
  * @returns {Promise<void>}
106
108
  */
107
109
  async initialize(zipManager) {
108
- this.#zipManager = zipManager;
109
- const relFiles = zipManager.listFiles('').filter(f => f.endsWith('.rels'));
110
+ this.#zipManager = zipManager
111
+ const relFiles = zipManager.listFiles('').filter(f => f.endsWith('.rels'))
110
112
 
111
113
  await Promise.all(
112
114
  relFiles.map(async relsPath => {
113
- const content = await zipManager.readFile(relsPath);
115
+ const content = await zipManager.readFile(relsPath)
114
116
  if (content) {
115
- this.#relationships.set(relsPath, this.#parseRels(content, relsPath));
117
+ this.#relationships.set(relsPath, this.#parseRels(content, relsPath))
116
118
  }
117
119
  })
118
- );
120
+ )
119
121
 
120
- logger.debug(`Loaded ${this.#relationships.size} relationship files`);
122
+ logger.debug(`Loaded ${this.#relationships.size} relationship files`)
121
123
  }
122
124
 
123
125
  /**
@@ -131,10 +133,10 @@ export class RelationshipManager {
131
133
  * @returns {string} Path to the corresponding .rels file.
132
134
  */
133
135
  getRelsPath(partPath) {
134
- const lastSlash = partPath.lastIndexOf('/');
135
- const dir = lastSlash >= 0 ? partPath.substring(0, lastSlash) : '';
136
- const file = lastSlash >= 0 ? partPath.substring(lastSlash + 1) : partPath;
137
- return dir ? `${dir}/_rels/${file}.rels` : `_rels/${file}.rels`;
136
+ const lastSlash = partPath.lastIndexOf('/')
137
+ const dir = lastSlash >= 0 ? partPath.substring(0, lastSlash) : ''
138
+ const file = lastSlash >= 0 ? partPath.substring(lastSlash + 1) : partPath
139
+ return dir ? `${dir}/_rels/${file}.rels` : `_rels/${file}.rels`
138
140
  }
139
141
 
140
142
  /**
@@ -144,8 +146,8 @@ export class RelationshipManager {
144
146
  * @returns {Relationship[]} Array of relationships.
145
147
  */
146
148
  getRelationships(partPath) {
147
- const relsPath = this.getRelsPath(partPath);
148
- return this.#relationships.get(relsPath) || [];
149
+ const relsPath = this.getRelsPath(partPath)
150
+ return this.#relationships.get(relsPath) || []
149
151
  }
150
152
 
151
153
  /**
@@ -156,8 +158,8 @@ export class RelationshipManager {
156
158
  * @returns {Relationship|null}
157
159
  */
158
160
  getRelationshipById(partPath, rId) {
159
- const rels = this.getRelationships(partPath);
160
- return rels.find(r => r.id === rId) || null;
161
+ const rels = this.getRelationships(partPath)
162
+ return rels.find(r => r.id === rId) || null
161
163
  }
162
164
 
163
165
  /**
@@ -168,7 +170,7 @@ export class RelationshipManager {
168
170
  * @returns {Relationship[]}
169
171
  */
170
172
  getRelationshipsByType(partPath, type) {
171
- return this.getRelationships(partPath).filter(r => r.type === type);
173
+ return this.getRelationships(partPath).filter(r => r.type === type)
172
174
  }
173
175
 
174
176
  /**
@@ -182,23 +184,23 @@ export class RelationshipManager {
182
184
  * @returns {string} The assigned relationship ID (e.g., 'rId3').
183
185
  */
184
186
  addRelationship(partPath, type, target, targetMode) {
185
- const relsPath = this.getRelsPath(partPath);
187
+ const relsPath = this.getRelsPath(partPath)
186
188
 
187
189
  if (!this.#relationships.has(relsPath)) {
188
- this.#relationships.set(relsPath, []);
190
+ this.#relationships.set(relsPath, [])
189
191
  }
190
192
 
191
- const existing = this.#relationships.get(relsPath);
192
- const newId = generateRelationshipId(existing.map(r => r.id));
193
+ const existing = this.#relationships.get(relsPath)
194
+ const newId = generateRelationshipId(existing.map(r => r.id))
193
195
 
194
- const rel = { id: newId, type, target };
195
- if (targetMode) rel.targetMode = targetMode;
196
+ const rel = { id: newId, type, target }
197
+ if (targetMode) rel.targetMode = targetMode
196
198
 
197
- existing.push(rel);
198
- this.#flushRels(relsPath, partPath);
199
+ existing.push(rel)
200
+ this.#flushRels(relsPath, partPath)
199
201
 
200
- logger.debug(`Added relationship ${newId} (${type.split('/').pop()}) to ${partPath}`);
201
- return newId;
202
+ logger.debug(`Added relationship ${newId} (${type.split('/').pop()}) to ${partPath}`)
203
+ return newId
202
204
  }
203
205
 
204
206
  /**
@@ -208,11 +210,11 @@ export class RelationshipManager {
208
210
  * @param {string} rId - Relationship ID to remove.
209
211
  */
210
212
  removeRelationship(partPath, rId) {
211
- const relsPath = this.getRelsPath(partPath);
212
- const existing = this.#relationships.get(relsPath) || [];
213
- const filtered = existing.filter(r => r.id !== rId);
214
- this.#relationships.set(relsPath, filtered);
215
- this.#flushRels(relsPath, partPath);
213
+ const relsPath = this.getRelsPath(partPath)
214
+ const existing = this.#relationships.get(relsPath) || []
215
+ const filtered = existing.filter(r => r.id !== rId)
216
+ this.#relationships.set(relsPath, filtered)
217
+ this.#flushRels(relsPath, partPath)
216
218
  }
217
219
 
218
220
  /**
@@ -223,12 +225,12 @@ export class RelationshipManager {
223
225
  * @param {string} newTarget - New target value.
224
226
  */
225
227
  updateRelationshipTarget(partPath, rId, newTarget) {
226
- const relsPath = this.getRelsPath(partPath);
227
- const existing = this.#relationships.get(relsPath) || [];
228
- const rel = existing.find(r => r.id === rId);
228
+ const relsPath = this.getRelsPath(partPath)
229
+ const existing = this.#relationships.get(relsPath) || []
230
+ const rel = existing.find(r => r.id === rId)
229
231
  if (rel) {
230
- rel.target = newTarget;
231
- this.#flushRels(relsPath, partPath);
232
+ rel.target = newTarget
233
+ this.#flushRels(relsPath, partPath)
232
234
  }
233
235
  }
234
236
 
@@ -242,26 +244,26 @@ export class RelationshipManager {
242
244
  * @returns {Map<string, string>} Map of old rId → new rId for the cloned part.
243
245
  */
244
246
  copyRelationships(sourcePath, destPath, excludeTypes = []) {
245
- const sourceRels = this.getRelationships(sourcePath);
246
- const destRelsPath = this.getRelsPath(destPath);
247
- const idMap = new Map();
247
+ const sourceRels = this.getRelationships(sourcePath)
248
+ const destRelsPath = this.getRelsPath(destPath)
249
+ const idMap = new Map()
248
250
 
249
251
  if (!this.#relationships.has(destRelsPath)) {
250
- this.#relationships.set(destRelsPath, []);
252
+ this.#relationships.set(destRelsPath, [])
251
253
  }
252
254
 
253
- const destRels = this.#relationships.get(destRelsPath);
255
+ const destRels = this.#relationships.get(destRelsPath)
254
256
 
255
257
  for (const rel of sourceRels) {
256
- if (excludeTypes.includes(rel.type)) continue;
257
- const newId = generateRelationshipId(destRels.map(r => r.id));
258
- const newRel = { ...rel, id: newId };
259
- destRels.push(newRel);
260
- idMap.set(rel.id, newId);
258
+ if (excludeTypes.includes(rel.type)) continue
259
+ const newId = generateRelationshipId(destRels.map(r => r.id))
260
+ const newRel = { ...rel, id: newId }
261
+ destRels.push(newRel)
262
+ idMap.set(rel.id, newId)
261
263
  }
262
264
 
263
- this.#flushRels(destRelsPath, destPath);
264
- return idMap;
265
+ this.#flushRels(destRelsPath, destPath)
266
+ return idMap
265
267
  }
266
268
 
267
269
  /**
@@ -277,22 +279,22 @@ export class RelationshipManager {
277
279
  */
278
280
  resolveTarget(partPath, target) {
279
281
  if (target.startsWith('http://') || target.startsWith('https://')) {
280
- return target; // External URL — return as-is
282
+ return target // External URL — return as-is
281
283
  }
282
284
 
283
- const baseParts = partPath.split('/');
284
- baseParts.pop(); // Remove file name, keep directory
285
- const targetParts = target.split('/');
285
+ const baseParts = partPath.split('/')
286
+ baseParts.pop() // Remove file name, keep directory
287
+ const targetParts = target.split('/')
286
288
 
287
289
  for (const part of targetParts) {
288
290
  if (part === '..') {
289
- baseParts.pop();
291
+ baseParts.pop()
290
292
  } else if (part !== '.') {
291
- baseParts.push(part);
293
+ baseParts.push(part)
292
294
  }
293
295
  }
294
296
 
295
- return baseParts.join('/');
297
+ return baseParts.join('/')
296
298
  }
297
299
 
298
300
  /**
@@ -304,19 +306,19 @@ export class RelationshipManager {
304
306
  */
305
307
  #parseRels(xmlContent, relsPath) {
306
308
  try {
307
- const obj = this.#xmlParser.parse(xmlContent, relsPath);
308
- const relationships = obj?.Relationships?.Relationship || [];
309
- const relsArray = Array.isArray(relationships) ? relationships : [relationships];
309
+ const obj = this.#xmlParser.parse(xmlContent, relsPath)
310
+ const relationships = obj?.Relationships?.Relationship || []
311
+ const relsArray = Array.isArray(relationships) ? relationships : [relationships]
310
312
 
311
313
  return relsArray.map(rel => ({
312
314
  id: rel['@_Id'],
313
315
  type: rel['@_Type'],
314
316
  target: rel['@_Target'],
315
317
  targetMode: rel['@_TargetMode'] || null,
316
- }));
318
+ }))
317
319
  } catch (err) {
318
- logger.warn(`Failed to parse ${relsPath}: ${err.message}`);
319
- return [];
320
+ logger.warn(`Failed to parse ${relsPath}: ${err.message}`)
321
+ return []
320
322
  }
321
323
  }
322
324
 
@@ -326,11 +328,11 @@ export class RelationshipManager {
326
328
  * @param {string} relsPath - Path of the .rels file.
327
329
  * @param {string} partPath - For logging.
328
330
  */
329
- #flushRels(relsPath, partPath) {
330
- const rels = this.#relationships.get(relsPath) || [];
331
- const xml = this.#buildRelsXml(rels);
331
+ #flushRels(relsPath, _partPath) {
332
+ const rels = this.#relationships.get(relsPath) || []
333
+ const xml = this.#buildRelsXml(rels)
332
334
  if (this.#zipManager) {
333
- this.#zipManager.writeFile(relsPath, xml);
335
+ this.#zipManager.writeFile(relsPath, xml)
334
336
  }
335
337
  }
336
338
 
@@ -342,16 +344,16 @@ export class RelationshipManager {
342
344
  */
343
345
  #buildRelsXml(rels) {
344
346
  const lines = rels.map(rel => {
345
- const targetMode = rel.targetMode ? ` TargetMode="${rel.targetMode}"` : '';
346
- return ` <Relationship Id="${rel.id}" Type="${rel.type}" Target="${rel.target}"${targetMode}/>`;
347
- });
347
+ const targetMode = rel.targetMode ? ` TargetMode="${rel.targetMode}"` : ''
348
+ return ` <Relationship Id="${rel.id}" Type="${rel.type}" Target="${rel.target}"${targetMode}/>`
349
+ })
348
350
 
349
351
  return [
350
352
  '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
351
353
  '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
352
354
  ...lines,
353
355
  '</Relationships>',
354
- ].join('\n');
356
+ ].join('\n')
355
357
  }
356
358
 
357
359
  /**
@@ -362,8 +364,8 @@ export class RelationshipManager {
362
364
  */
363
365
  flushAll(zipManager) {
364
366
  for (const [relsPath, rels] of this.#relationships) {
365
- const xml = this.#buildRelsXml(rels);
366
- zipManager.writeFile(relsPath, xml);
367
+ const xml = this.#buildRelsXml(rels)
368
+ zipManager.writeFile(relsPath, xml)
367
369
  }
368
370
  }
369
371
 
@@ -374,28 +376,32 @@ export class RelationshipManager {
374
376
  * @param {ZipManager} zipManager
375
377
  */
376
378
  removeOrphanRelationships(zipManager) {
377
- let removedCount = 0;
379
+ let removedCount = 0
378
380
  for (const [relsPath, rels] of this.#relationships.entries()) {
379
381
  // Determine the base part path from the rels path
380
382
  // e.g. ppt/slides/_rels/slide1.xml.rels -> ppt/slides/slide1.xml
381
- const partPath = relsPath.replace('_rels/', '').replace('.rels', '');
383
+ const partPath = relsPath.replace('_rels/', '').replace('.rels', '')
382
384
 
383
385
  const filtered = rels.filter(rel => {
384
- if (rel.targetMode === 'External') return true;
385
- const targetPath = this.resolveTarget(partPath, rel.target);
386
+ if (rel.targetMode === 'External') return true
387
+ const targetPath = this.resolveTarget(partPath, rel.target)
386
388
  if (!zipManager.hasFile(targetPath)) {
387
- logger.warn(`Removing orphan relationship ${rel.id} pointing to missing target: ${targetPath}`);
388
- removedCount++;
389
- return false;
389
+ logger.warn(
390
+ `Removing orphan relationship ${rel.id} pointing to missing target: ${targetPath}`
391
+ )
392
+ removedCount++
393
+ return false
390
394
  }
391
- return true;
392
- });
395
+ return true
396
+ })
393
397
 
394
398
  if (filtered.length !== rels.length) {
395
- this.#relationships.set(relsPath, filtered);
396
- this.#flushRels(relsPath, partPath);
399
+ this.#relationships.set(relsPath, filtered)
400
+ this.#flushRels(relsPath, partPath)
397
401
  }
398
402
  }
399
- logger.debug(`Removed ${removedCount} orphan relationship(s).`);
403
+ logger.debug(`Removed ${removedCount} orphan relationship(s).`)
400
404
  }
401
405
  }
406
+
407
+ module.exports = { REL_TYPES, RelationshipManager }