node-pptx-templater 1.0.2 → 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.
- package/README.md +336 -281
- package/package.json +1 -1
- package/src/cli/commands/build.js +30 -31
- package/src/cli/commands/debug.js +23 -23
- package/src/cli/commands/extract.js +21 -21
- package/src/cli/commands/inspect.js +23 -23
- package/src/cli/commands/validate.js +17 -17
- package/src/cli/index.js +39 -36
- package/src/core/OutputWriter.js +79 -78
- package/src/core/PPTXTemplater.js +856 -273
- package/src/core/TemplateEngine.js +67 -71
- package/src/core/ValidationEngine.js +246 -0
- package/src/index.js +30 -17
- package/src/managers/ChartManager.js +195 -70
- package/src/managers/ContentTypesManager.js +49 -45
- package/src/managers/HyperlinkManager.js +146 -142
- package/src/managers/ImageManager.js +336 -0
- package/src/managers/MediaManager.js +62 -81
- package/src/managers/RelationshipManager.js +99 -95
- package/src/managers/ShapeManager.js +340 -0
- package/src/managers/SlideManager.js +408 -311
- package/src/managers/TableManager.js +979 -262
- package/src/managers/TextManager.js +197 -0
- package/src/managers/ZipManager.js +69 -69
- package/src/managers/charts/ChartCacheGenerator.js +75 -58
- package/src/managers/charts/ChartParser.js +9 -13
- package/src/managers/charts/ChartRelationshipManager.js +12 -10
- package/src/managers/charts/ChartWorkbookUpdater.js +59 -56
- package/src/parsers/XMLParser.js +47 -50
- package/src/templates/blankPptx.js +3 -2
- package/src/templates/slideTemplate.js +28 -34
- package/src/utils/contentTypesHelper.js +40 -54
- package/src/utils/errors.js +18 -18
- package/src/utils/idUtils.js +16 -14
- package/src/utils/logger.js +18 -16
- package/src/utils/relationshipUtils.js +19 -20
- package/src/utils/xmlUtils.js +26 -26
|
@@ -35,11 +35,10 @@
|
|
|
35
35
|
* - .../slideToSlide → slide → another slide (inter-slide link)
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
const { createLogger } = require('../utils/logger.js')
|
|
39
|
-
const {
|
|
40
|
-
const { generateRelationshipId } = require('../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.
|
|
@@ -55,11 +54,14 @@ 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:
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
@@ -75,13 +77,13 @@ class RelationshipManager {
|
|
|
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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ class RelationshipManager {
|
|
|
277
279
|
*/
|
|
278
280
|
resolveTarget(partPath, target) {
|
|
279
281
|
if (target.startsWith('http://') || target.startsWith('https://')) {
|
|
280
|
-
return target
|
|
282
|
+
return target // External URL — return as-is
|
|
281
283
|
}
|
|
282
284
|
|
|
283
|
-
const baseParts = partPath.split('/')
|
|
284
|
-
baseParts.pop()
|
|
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 @@ 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 @@ class RelationshipManager {
|
|
|
326
328
|
* @param {string} relsPath - Path of the .rels file.
|
|
327
329
|
* @param {string} partPath - For logging.
|
|
328
330
|
*/
|
|
329
|
-
#flushRels(relsPath,
|
|
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 @@ 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 @@ 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,30 +376,32 @@ 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(
|
|
388
|
-
|
|
389
|
-
|
|
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
|
}
|
|
402
406
|
|
|
403
|
-
module.exports = { REL_TYPES, RelationshipManager }
|
|
407
|
+
module.exports = { REL_TYPES, RelationshipManager }
|