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.
- package/README.md +336 -281
- package/package.json +6 -6
- package/src/cli/commands/build.js +32 -31
- package/src/cli/commands/debug.js +25 -24
- package/src/cli/commands/extract.js +23 -21
- package/src/cli/commands/inspect.js +25 -23
- package/src/cli/commands/validate.js +19 -17
- package/src/cli/index.js +45 -43
- package/src/core/OutputWriter.js +81 -78
- package/src/core/PPTXTemplater.js +859 -274
- package/src/core/TemplateEngine.js +69 -71
- package/src/core/ValidationEngine.js +246 -0
- package/src/index.js +51 -15
- package/src/managers/ChartManager.js +197 -70
- package/src/managers/ContentTypesManager.js +51 -45
- package/src/managers/HyperlinkManager.js +148 -142
- package/src/managers/ImageManager.js +336 -0
- package/src/managers/MediaManager.js +64 -81
- package/src/managers/RelationshipManager.js +102 -96
- package/src/managers/ShapeManager.js +340 -0
- package/src/managers/SlideManager.js +410 -311
- package/src/managers/TableManager.js +981 -262
- package/src/managers/TextManager.js +197 -0
- package/src/managers/ZipManager.js +71 -69
- package/src/managers/charts/ChartCacheGenerator.js +77 -58
- package/src/managers/charts/ChartParser.js +11 -13
- package/src/managers/charts/ChartRelationshipManager.js +14 -10
- package/src/managers/charts/ChartWorkbookUpdater.js +61 -56
- package/src/parsers/XMLParser.js +50 -49
- package/src/templates/blankPptx.js +3 -1
- package/src/templates/slideTemplate.js +31 -32
- package/src/utils/contentTypesHelper.js +41 -53
- package/src/utils/errors.js +33 -23
- package/src/utils/idUtils.js +23 -15
- package/src/utils/logger.js +21 -15
- package/src/utils/relationshipUtils.js +28 -22
- package/src/utils/xmlUtils.js +37 -29
|
@@ -38,9 +38,6 @@
|
|
|
38
38
|
* </p:sp>
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import { generateUniqueId } from '../utils/idUtils.js';
|
|
42
|
-
import { PPTXError } from '../utils/errors.js';
|
|
43
|
-
|
|
44
41
|
/**
|
|
45
42
|
* OpenXML namespace declarations used in slide XML.
|
|
46
43
|
*/
|
|
@@ -48,7 +45,7 @@ const SLIDE_NAMESPACES = [
|
|
|
48
45
|
'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"',
|
|
49
46
|
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"',
|
|
50
47
|
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"',
|
|
51
|
-
].join(' ')
|
|
48
|
+
].join(' ')
|
|
52
49
|
|
|
53
50
|
/**
|
|
54
51
|
* EMU conversion constants.
|
|
@@ -60,7 +57,7 @@ const EMU = {
|
|
|
60
57
|
SLIDE_WIDTH: 9144000,
|
|
61
58
|
/** Slide height (7.5 inches = 6858000 EMU for 4:3, 5143500 for 16:9) */
|
|
62
59
|
SLIDE_HEIGHT: 5143500,
|
|
63
|
-
}
|
|
60
|
+
}
|
|
64
61
|
|
|
65
62
|
/**
|
|
66
63
|
* Builds the XML for a new slide.
|
|
@@ -69,29 +66,26 @@ const EMU = {
|
|
|
69
66
|
* @param {number} slideIndex - 1-based slide index (for unique IDs).
|
|
70
67
|
* @returns {string} Complete slide XML string.
|
|
71
68
|
*/
|
|
72
|
-
|
|
73
|
-
const { title = '', elements = [],
|
|
69
|
+
function buildNewSlideXml(options, _slideIndex) {
|
|
70
|
+
const { title = '', elements = [], _layout = 'blank', _rawXml } = options
|
|
74
71
|
|
|
75
72
|
// If raw XML is provided, use it directly (for clone/export operations)
|
|
76
|
-
if (_rawXml) return _rawXml
|
|
73
|
+
if (_rawXml) return _rawXml
|
|
77
74
|
|
|
78
|
-
const shapes = []
|
|
79
|
-
let shapeIdCounter = 1
|
|
75
|
+
const shapes = []
|
|
76
|
+
let shapeIdCounter = 1
|
|
80
77
|
|
|
81
78
|
// Add title shape if title is provided
|
|
82
79
|
if (title) {
|
|
83
|
-
shapes.push(buildTitleShape(title, shapeIdCounter++))
|
|
80
|
+
shapes.push(buildTitleShape(title, shapeIdCounter++))
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
// Add element shapes
|
|
87
84
|
for (const element of elements) {
|
|
88
|
-
shapes.push(buildElementShape(element, shapeIdCounter++))
|
|
85
|
+
shapes.push(buildElementShape(element, shapeIdCounter++))
|
|
89
86
|
}
|
|
90
87
|
|
|
91
|
-
const spTreeContent = [
|
|
92
|
-
buildGroupShapeProps(),
|
|
93
|
-
...shapes,
|
|
94
|
-
].join('\n ');
|
|
88
|
+
const spTreeContent = [buildGroupShapeProps(), ...shapes].join('\n ')
|
|
95
89
|
|
|
96
90
|
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
97
91
|
<p:sld ${SLIDE_NAMESPACES} show="1">
|
|
@@ -103,7 +97,7 @@ export function buildNewSlideXml(options, slideIndex) {
|
|
|
103
97
|
<p:clrMapOvr>
|
|
104
98
|
<a:masterClrMapping/>
|
|
105
99
|
</p:clrMapOvr>
|
|
106
|
-
</p:sld
|
|
100
|
+
</p:sld>`
|
|
107
101
|
}
|
|
108
102
|
|
|
109
103
|
/**
|
|
@@ -123,7 +117,7 @@ function buildGroupShapeProps() {
|
|
|
123
117
|
<a:chOff x="0" y="0"/>
|
|
124
118
|
<a:chExt cx="0" cy="0"/>
|
|
125
119
|
</a:xfrm>
|
|
126
|
-
</p:grpSpPr
|
|
120
|
+
</p:grpSpPr>`
|
|
127
121
|
}
|
|
128
122
|
|
|
129
123
|
/**
|
|
@@ -151,7 +145,7 @@ function buildTitleShape(title, shapeId) {
|
|
|
151
145
|
</a:r>
|
|
152
146
|
</a:p>
|
|
153
147
|
</p:txBody>
|
|
154
|
-
</p:sp
|
|
148
|
+
</p:sp>`
|
|
155
149
|
}
|
|
156
150
|
|
|
157
151
|
/**
|
|
@@ -164,13 +158,13 @@ function buildTitleShape(title, shapeId) {
|
|
|
164
158
|
function buildElementShape(element, shapeId) {
|
|
165
159
|
switch (element.type) {
|
|
166
160
|
case 'text':
|
|
167
|
-
return buildTextShape(element, shapeId)
|
|
161
|
+
return buildTextShape(element, shapeId)
|
|
168
162
|
case 'image':
|
|
169
|
-
return buildImagePlaceholder(element, shapeId)
|
|
163
|
+
return buildImagePlaceholder(element, shapeId)
|
|
170
164
|
case 'shape':
|
|
171
|
-
return buildBasicShape(element, shapeId)
|
|
165
|
+
return buildBasicShape(element, shapeId)
|
|
172
166
|
default:
|
|
173
|
-
return buildTextShape({ ...element, value: element.value || '' }, shapeId)
|
|
167
|
+
return buildTextShape({ ...element, value: element.value || '' }, shapeId)
|
|
174
168
|
}
|
|
175
169
|
}
|
|
176
170
|
|
|
@@ -192,10 +186,10 @@ function buildTextShape(element, shapeId) {
|
|
|
192
186
|
bold = false,
|
|
193
187
|
italic = false,
|
|
194
188
|
name = `TextBox ${shapeId}`,
|
|
195
|
-
} = element
|
|
189
|
+
} = element
|
|
196
190
|
|
|
197
|
-
const boldAttr = bold ? ' b="1"' : ''
|
|
198
|
-
const italicAttr = italic ? ' i="1"' : ''
|
|
191
|
+
const boldAttr = bold ? ' b="1"' : ''
|
|
192
|
+
const italicAttr = italic ? ' i="1"' : ''
|
|
199
193
|
|
|
200
194
|
return `<p:sp>
|
|
201
195
|
<p:nvSpPr>
|
|
@@ -223,7 +217,7 @@ function buildTextShape(element, shapeId) {
|
|
|
223
217
|
</a:r>
|
|
224
218
|
</a:p>
|
|
225
219
|
</p:txBody>
|
|
226
|
-
</p:sp
|
|
220
|
+
</p:sp>`
|
|
227
221
|
}
|
|
228
222
|
|
|
229
223
|
/**
|
|
@@ -242,7 +236,7 @@ function buildImagePlaceholder(element, shapeId) {
|
|
|
242
236
|
width = 3 * EMU.INCH,
|
|
243
237
|
height = 2 * EMU.INCH,
|
|
244
238
|
name = `Image ${shapeId}`,
|
|
245
|
-
} = element
|
|
239
|
+
} = element
|
|
246
240
|
|
|
247
241
|
return `<p:pic>
|
|
248
242
|
<p:nvPicPr>
|
|
@@ -261,7 +255,7 @@ function buildImagePlaceholder(element, shapeId) {
|
|
|
261
255
|
</a:xfrm>
|
|
262
256
|
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
|
263
257
|
</p:spPr>
|
|
264
|
-
</p:pic
|
|
258
|
+
</p:pic>`
|
|
265
259
|
}
|
|
266
260
|
|
|
267
261
|
/**
|
|
@@ -280,7 +274,7 @@ function buildBasicShape(element, shapeId) {
|
|
|
280
274
|
height = EMU.INCH,
|
|
281
275
|
fillColor = 'FFFFFF',
|
|
282
276
|
name = `Shape ${shapeId}`,
|
|
283
|
-
} = element
|
|
277
|
+
} = element
|
|
284
278
|
|
|
285
279
|
return `<p:sp>
|
|
286
280
|
<p:nvSpPr>
|
|
@@ -296,7 +290,7 @@ function buildBasicShape(element, shapeId) {
|
|
|
296
290
|
<a:prstGeom prst="${shape}"><a:avLst/></a:prstGeom>
|
|
297
291
|
<a:solidFill><a:srgbClr val="${fillColor}"/></a:solidFill>
|
|
298
292
|
</p:spPr>
|
|
299
|
-
</p:sp
|
|
293
|
+
</p:sp>`
|
|
300
294
|
}
|
|
301
295
|
|
|
302
296
|
/**
|
|
@@ -310,5 +304,10 @@ function escapeXml(str) {
|
|
|
310
304
|
.replace(/</g, '<')
|
|
311
305
|
.replace(/>/g, '>')
|
|
312
306
|
.replace(/"/g, '"')
|
|
313
|
-
.replace(/'/g, ''')
|
|
307
|
+
.replace(/'/g, ''')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
buildNewSlideXml,
|
|
312
|
+
EMU,
|
|
314
313
|
}
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
* </Types>
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const { createLogger } = require('./logger.js')
|
|
21
21
|
|
|
22
|
-
const logger = createLogger('ContentTypes')
|
|
22
|
+
const logger = createLogger('ContentTypes')
|
|
23
23
|
|
|
24
24
|
/** MIME type for PPTX slide parts. */
|
|
25
|
-
const SLIDE_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml'
|
|
25
|
+
const SLIDE_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml'
|
|
26
26
|
|
|
27
27
|
/** MIME type for chart parts. */
|
|
28
|
-
const CHART_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml'
|
|
28
|
+
const CHART_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml'
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Singleton helper for [Content_Types].xml manipulation.
|
|
@@ -38,11 +38,7 @@ class ContentTypesHelper {
|
|
|
38
38
|
* @param {string} slideFileName - e.g., 'slide5.xml'
|
|
39
39
|
*/
|
|
40
40
|
addSlideContentType(zipManager, slideFileName) {
|
|
41
|
-
this.#addOverride(
|
|
42
|
-
zipManager,
|
|
43
|
-
`/ppt/slides/${slideFileName}`,
|
|
44
|
-
SLIDE_CONTENT_TYPE
|
|
45
|
-
);
|
|
41
|
+
this.#addOverride(zipManager, `/ppt/slides/${slideFileName}`, SLIDE_CONTENT_TYPE)
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
/**
|
|
@@ -52,10 +48,7 @@ class ContentTypesHelper {
|
|
|
52
48
|
* @param {string} slideFileName - e.g., 'slide5.xml'
|
|
53
49
|
*/
|
|
54
50
|
removeSlideContentType(zipManager, slideFileName) {
|
|
55
|
-
this.#removeOverride(
|
|
56
|
-
zipManager,
|
|
57
|
-
`/ppt/slides/${slideFileName}`
|
|
58
|
-
);
|
|
51
|
+
this.#removeOverride(zipManager, `/ppt/slides/${slideFileName}`)
|
|
59
52
|
}
|
|
60
53
|
|
|
61
54
|
/**
|
|
@@ -65,11 +58,7 @@ class ContentTypesHelper {
|
|
|
65
58
|
* @param {string} chartFileName - e.g., 'chart3.xml'
|
|
66
59
|
*/
|
|
67
60
|
addChartContentType(zipManager, chartFileName) {
|
|
68
|
-
this.#addOverride(
|
|
69
|
-
zipManager,
|
|
70
|
-
`/ppt/charts/${chartFileName}`,
|
|
71
|
-
CHART_CONTENT_TYPE
|
|
72
|
-
);
|
|
61
|
+
this.#addOverride(zipManager, `/ppt/charts/${chartFileName}`, CHART_CONTENT_TYPE)
|
|
73
62
|
}
|
|
74
63
|
|
|
75
64
|
/**
|
|
@@ -81,24 +70,21 @@ class ContentTypesHelper {
|
|
|
81
70
|
*/
|
|
82
71
|
addMediaDefault(zipManager, extension, mimeType) {
|
|
83
72
|
this.#updateQueue = this.#updateQueue.then(async () => {
|
|
84
|
-
const xmlFile = zipManager.rawZip.file('[Content_Types].xml')
|
|
85
|
-
if (!xmlFile) return
|
|
73
|
+
const xmlFile = zipManager.rawZip.file('[Content_Types].xml')
|
|
74
|
+
if (!xmlFile) return
|
|
86
75
|
|
|
87
|
-
const content = await xmlFile.async('text')
|
|
88
|
-
const entry = `Extension="${extension}" ContentType="${mimeType}"
|
|
76
|
+
const content = await xmlFile.async('text')
|
|
77
|
+
const entry = `Extension="${extension}" ContentType="${mimeType}"`
|
|
89
78
|
if (!content.includes(entry)) {
|
|
90
|
-
const updated = content.replace(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
);
|
|
94
|
-
zipManager.writeFile('[Content_Types].xml', updated);
|
|
95
|
-
logger.debug(`Registered default content type for .${extension}`);
|
|
79
|
+
const updated = content.replace('</Types>', ` <Default ${entry}/>\n</Types>`)
|
|
80
|
+
zipManager.writeFile('[Content_Types].xml', updated)
|
|
81
|
+
logger.debug(`Registered default content type for .${extension}`)
|
|
96
82
|
}
|
|
97
|
-
})
|
|
98
|
-
zipManager.addPendingPromise(this.#updateQueue)
|
|
83
|
+
})
|
|
84
|
+
zipManager.addPendingPromise(this.#updateQueue)
|
|
99
85
|
}
|
|
100
86
|
|
|
101
|
-
#updateQueue = Promise.resolve()
|
|
87
|
+
#updateQueue = Promise.resolve()
|
|
102
88
|
|
|
103
89
|
/**
|
|
104
90
|
* Adds an Override entry to [Content_Types].xml.
|
|
@@ -106,21 +92,21 @@ class ContentTypesHelper {
|
|
|
106
92
|
*/
|
|
107
93
|
#addOverride(zipManager, partName, contentType) {
|
|
108
94
|
this.#updateQueue = this.#updateQueue.then(async () => {
|
|
109
|
-
const xmlFile = zipManager.rawZip.file('[Content_Types].xml')
|
|
95
|
+
const xmlFile = zipManager.rawZip.file('[Content_Types].xml')
|
|
110
96
|
if (!xmlFile) {
|
|
111
|
-
logger.warn('[Content_Types].xml not found')
|
|
112
|
-
return
|
|
97
|
+
logger.warn('[Content_Types].xml not found')
|
|
98
|
+
return
|
|
113
99
|
}
|
|
114
|
-
const content = await xmlFile.async('text')
|
|
115
|
-
const entry = `PartName="${partName}"
|
|
100
|
+
const content = await xmlFile.async('text')
|
|
101
|
+
const entry = `PartName="${partName}"`
|
|
116
102
|
if (!content.includes(entry)) {
|
|
117
|
-
const override = `<Override PartName="${partName}" ContentType="${contentType}"
|
|
118
|
-
const updated = content.replace('</Types>', ` ${override}\n</Types>`)
|
|
119
|
-
zipManager.writeFile('[Content_Types].xml', updated)
|
|
120
|
-
logger.debug(`Registered content type for ${partName}`)
|
|
103
|
+
const override = `<Override PartName="${partName}" ContentType="${contentType}"/>`
|
|
104
|
+
const updated = content.replace('</Types>', ` ${override}\n</Types>`)
|
|
105
|
+
zipManager.writeFile('[Content_Types].xml', updated)
|
|
106
|
+
logger.debug(`Registered content type for ${partName}`)
|
|
121
107
|
}
|
|
122
|
-
})
|
|
123
|
-
zipManager.addPendingPromise(this.#updateQueue)
|
|
108
|
+
})
|
|
109
|
+
zipManager.addPendingPromise(this.#updateQueue)
|
|
124
110
|
}
|
|
125
111
|
|
|
126
112
|
/**
|
|
@@ -129,21 +115,23 @@ class ContentTypesHelper {
|
|
|
129
115
|
*/
|
|
130
116
|
#removeOverride(zipManager, partName) {
|
|
131
117
|
this.#updateQueue = this.#updateQueue.then(async () => {
|
|
132
|
-
const xmlFile = zipManager.rawZip.file('[Content_Types].xml')
|
|
118
|
+
const xmlFile = zipManager.rawZip.file('[Content_Types].xml')
|
|
133
119
|
if (!xmlFile) {
|
|
134
|
-
logger.warn('[Content_Types].xml not found')
|
|
135
|
-
return
|
|
120
|
+
logger.warn('[Content_Types].xml not found')
|
|
121
|
+
return
|
|
136
122
|
}
|
|
137
|
-
const content = await xmlFile.async('text')
|
|
138
|
-
const regex = new RegExp(`<Override[^>]*PartName="${partName}"[^>]*/>\\s*`, 'g')
|
|
123
|
+
const content = await xmlFile.async('text')
|
|
124
|
+
const regex = new RegExp(`<Override[^>]*PartName="${partName}"[^>]*/>\\s*`, 'g')
|
|
139
125
|
if (regex.test(content)) {
|
|
140
|
-
const updated = content.replace(regex, '')
|
|
141
|
-
zipManager.writeFile('[Content_Types].xml', updated)
|
|
142
|
-
logger.debug(`Removed content type for ${partName}`)
|
|
126
|
+
const updated = content.replace(regex, '')
|
|
127
|
+
zipManager.writeFile('[Content_Types].xml', updated)
|
|
128
|
+
logger.debug(`Removed content type for ${partName}`)
|
|
143
129
|
}
|
|
144
|
-
})
|
|
145
|
-
zipManager.addPendingPromise(this.#updateQueue)
|
|
130
|
+
})
|
|
131
|
+
zipManager.addPendingPromise(this.#updateQueue)
|
|
146
132
|
}
|
|
147
133
|
}
|
|
148
134
|
|
|
149
|
-
|
|
135
|
+
module.exports = {
|
|
136
|
+
contentTypesHelper: new ContentTypesHelper(),
|
|
137
|
+
}
|
package/src/utils/errors.js
CHANGED
|
@@ -18,19 +18,19 @@
|
|
|
18
18
|
* @description Base error class for all node-pptx-templater errors.
|
|
19
19
|
* @extends Error
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
class PPTXError extends Error {
|
|
22
22
|
/**
|
|
23
23
|
* @param {string} message - Human-readable error description.
|
|
24
24
|
* @param {Error} [cause] - Original underlying error (for error chaining).
|
|
25
25
|
*/
|
|
26
26
|
constructor(message, cause) {
|
|
27
|
-
super(message)
|
|
28
|
-
this.name = 'PPTXError'
|
|
29
|
-
if (cause) this.cause = cause
|
|
27
|
+
super(message)
|
|
28
|
+
this.name = 'PPTXError'
|
|
29
|
+
if (cause) this.cause = cause
|
|
30
30
|
|
|
31
31
|
// Maintains proper stack trace for where error was thrown (V8 only)
|
|
32
32
|
if (Error.captureStackTrace) {
|
|
33
|
-
Error.captureStackTrace(this, this.constructor)
|
|
33
|
+
Error.captureStackTrace(this, this.constructor)
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -40,13 +40,13 @@ export class PPTXError extends Error {
|
|
|
40
40
|
* @description Thrown when a slide reference cannot be resolved.
|
|
41
41
|
* @extends PPTXError
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
class SlideNotFoundError extends PPTXError {
|
|
44
44
|
/**
|
|
45
45
|
* @param {string} message
|
|
46
46
|
*/
|
|
47
47
|
constructor(message) {
|
|
48
|
-
super(message)
|
|
49
|
-
this.name = 'SlideNotFoundError'
|
|
48
|
+
super(message)
|
|
49
|
+
this.name = 'SlideNotFoundError'
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -55,13 +55,13 @@ export class SlideNotFoundError extends PPTXError {
|
|
|
55
55
|
* @description Thrown when a chart cannot be located in a slide.
|
|
56
56
|
* @extends PPTXError
|
|
57
57
|
*/
|
|
58
|
-
|
|
58
|
+
class ChartNotFoundError extends PPTXError {
|
|
59
59
|
/**
|
|
60
60
|
* @param {string} message
|
|
61
61
|
*/
|
|
62
62
|
constructor(message) {
|
|
63
|
-
super(message)
|
|
64
|
-
this.name = 'ChartNotFoundError'
|
|
63
|
+
super(message)
|
|
64
|
+
this.name = 'ChartNotFoundError'
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -70,13 +70,13 @@ export class ChartNotFoundError extends PPTXError {
|
|
|
70
70
|
* @description Thrown when a table cannot be located in a slide.
|
|
71
71
|
* @extends PPTXError
|
|
72
72
|
*/
|
|
73
|
-
|
|
73
|
+
class TableNotFoundError extends PPTXError {
|
|
74
74
|
/**
|
|
75
75
|
* @param {string} message
|
|
76
76
|
*/
|
|
77
77
|
constructor(message) {
|
|
78
|
-
super(message)
|
|
79
|
-
this.name = 'TableNotFoundError'
|
|
78
|
+
super(message)
|
|
79
|
+
this.name = 'TableNotFoundError'
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -85,14 +85,14 @@ export class TableNotFoundError extends PPTXError {
|
|
|
85
85
|
* @description Thrown when XML parsing or building fails.
|
|
86
86
|
* @extends PPTXError
|
|
87
87
|
*/
|
|
88
|
-
|
|
88
|
+
class XMLParseError extends PPTXError {
|
|
89
89
|
/**
|
|
90
90
|
* @param {string} message
|
|
91
91
|
* @param {Error} [cause]
|
|
92
92
|
*/
|
|
93
93
|
constructor(message, cause) {
|
|
94
|
-
super(message, cause)
|
|
95
|
-
this.name = 'XMLParseError'
|
|
94
|
+
super(message, cause)
|
|
95
|
+
this.name = 'XMLParseError'
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -101,14 +101,14 @@ export class XMLParseError extends PPTXError {
|
|
|
101
101
|
* @description Thrown when a PPTX file has an invalid or unsupported structure.
|
|
102
102
|
* @extends PPTXError
|
|
103
103
|
*/
|
|
104
|
-
|
|
104
|
+
class InvalidTemplateError extends PPTXError {
|
|
105
105
|
/**
|
|
106
106
|
* @param {string} message
|
|
107
107
|
* @param {Error} [cause]
|
|
108
108
|
*/
|
|
109
109
|
constructor(message, cause) {
|
|
110
|
-
super(message, cause)
|
|
111
|
-
this.name = 'InvalidTemplateError'
|
|
110
|
+
super(message, cause)
|
|
111
|
+
this.name = 'InvalidTemplateError'
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -117,13 +117,23 @@ export class InvalidTemplateError extends PPTXError {
|
|
|
117
117
|
* @description Thrown when a media file cannot be embedded.
|
|
118
118
|
* @extends PPTXError
|
|
119
119
|
*/
|
|
120
|
-
|
|
120
|
+
class MediaEmbedError extends PPTXError {
|
|
121
121
|
/**
|
|
122
122
|
* @param {string} message
|
|
123
123
|
* @param {Error} [cause]
|
|
124
124
|
*/
|
|
125
125
|
constructor(message, cause) {
|
|
126
|
-
super(message, cause)
|
|
127
|
-
this.name = 'MediaEmbedError'
|
|
126
|
+
super(message, cause)
|
|
127
|
+
this.name = 'MediaEmbedError'
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
PPTXError,
|
|
133
|
+
SlideNotFoundError,
|
|
134
|
+
ChartNotFoundError,
|
|
135
|
+
TableNotFoundError,
|
|
136
|
+
XMLParseError,
|
|
137
|
+
InvalidTemplateError,
|
|
138
|
+
MediaEmbedError,
|
|
139
|
+
}
|
package/src/utils/idUtils.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Used for generating shape IDs, slide IDs, etc. in OpenXML.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const { randomBytes } = require('crypto')
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Generates a unique integer ID for use as a shape or slide ID.
|
|
@@ -12,9 +12,9 @@ import { randomBytes } from 'crypto';
|
|
|
12
12
|
* @param {number[]} [existingIds] - Array of existing IDs to avoid.
|
|
13
13
|
* @returns {number} Unique positive integer.
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 0
|
|
17
|
-
return maxId + 1
|
|
15
|
+
function generateUniqueId(existingIds = []) {
|
|
16
|
+
const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 0
|
|
17
|
+
return maxId + 1
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -23,21 +23,23 @@ export function generateUniqueId(existingIds = []) {
|
|
|
23
23
|
*
|
|
24
24
|
* @returns {string} UUID v4 string (e.g., '{A1B2C3D4-E5F6-...}')
|
|
25
25
|
*/
|
|
26
|
-
|
|
27
|
-
const bytes = randomBytes(16)
|
|
28
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
|
29
|
-
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
|
26
|
+
function generateGuid() {
|
|
27
|
+
const bytes = randomBytes(16)
|
|
28
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40 // Version 4
|
|
29
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80 // Variant RFC4122
|
|
30
30
|
|
|
31
|
-
const hex = bytes.toString('hex')
|
|
31
|
+
const hex = bytes.toString('hex')
|
|
32
32
|
const guid = [
|
|
33
33
|
hex.slice(0, 8),
|
|
34
34
|
hex.slice(8, 12),
|
|
35
35
|
hex.slice(12, 16),
|
|
36
36
|
hex.slice(16, 20),
|
|
37
37
|
hex.slice(20, 32),
|
|
38
|
-
]
|
|
38
|
+
]
|
|
39
|
+
.join('-')
|
|
40
|
+
.toUpperCase()
|
|
39
41
|
|
|
40
|
-
return `{${guid}}
|
|
42
|
+
return `{${guid}}`
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/**
|
|
@@ -47,8 +49,14 @@ export function generateGuid() {
|
|
|
47
49
|
* @param {string[]} [existingSlideIds] - Existing slide ID strings.
|
|
48
50
|
* @returns {string} New slide ID string.
|
|
49
51
|
*/
|
|
50
|
-
|
|
51
|
-
const existingNums = existingSlideIds.map(id => parseInt(id, 10)).filter(n => !isNaN(n))
|
|
52
|
-
const maxId = existingNums.length > 0 ? Math.max(...existingNums) : 255
|
|
53
|
-
return String(maxId + 1)
|
|
52
|
+
function generateSlideId(existingSlideIds = []) {
|
|
53
|
+
const existingNums = existingSlideIds.map(id => parseInt(id, 10)).filter(n => !isNaN(n))
|
|
54
|
+
const maxId = existingNums.length > 0 ? Math.max(...existingNums) : 255
|
|
55
|
+
return String(maxId + 1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
generateUniqueId,
|
|
60
|
+
generateGuid,
|
|
61
|
+
generateSlideId,
|
|
54
62
|
}
|
package/src/utils/logger.js
CHANGED
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
* PPTX_LOG_LEVEL=silent → suppress all logs
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 }
|
|
25
|
+
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 }
|
|
26
26
|
|
|
27
|
-
const currentLevel =
|
|
28
|
-
(process.env.PPTX_LOG_LEVEL || 'warn').toLowerCase()
|
|
29
|
-
] ?? LOG_LEVELS.warn;
|
|
27
|
+
const currentLevel =
|
|
28
|
+
LOG_LEVELS[(process.env.PPTX_LOG_LEVEL || 'warn').toLowerCase()] ?? LOG_LEVELS.warn
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
31
|
* ANSI color codes for terminal output.
|
|
@@ -38,14 +37,14 @@ const COLORS = {
|
|
|
38
37
|
green: '\x1b[32m',
|
|
39
38
|
yellow: '\x1b[33m',
|
|
40
39
|
red: '\x1b[31m',
|
|
41
|
-
}
|
|
40
|
+
}
|
|
42
41
|
|
|
43
42
|
/**
|
|
44
43
|
* Formats the current timestamp as HH:MM:SS.mmm
|
|
45
44
|
* @returns {string}
|
|
46
45
|
*/
|
|
47
46
|
function timestamp() {
|
|
48
|
-
return new Date().toISOString().substring(11, 23)
|
|
47
|
+
return new Date().toISOString().substring(11, 23)
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
/**
|
|
@@ -66,20 +65,23 @@ function timestamp() {
|
|
|
66
65
|
* const logger = createLogger('SlideManager');
|
|
67
66
|
* logger.info('Loaded 5 slides');
|
|
68
67
|
*/
|
|
69
|
-
|
|
70
|
-
const isTTY = process.stdout.isTTY
|
|
68
|
+
function createLogger(moduleName) {
|
|
69
|
+
const isTTY = process.stdout.isTTY
|
|
71
70
|
|
|
72
71
|
const log = (level, levelNum, color, message, ...args) => {
|
|
73
|
-
if (levelNum < currentLevel) return
|
|
72
|
+
if (levelNum < currentLevel) return
|
|
74
73
|
|
|
75
74
|
const prefix = isTTY
|
|
76
75
|
? `${COLORS.dim}${timestamp()}${COLORS.reset} ${color}[${level.toUpperCase().padEnd(5)}]${COLORS.reset} ${COLORS.cyan}[${moduleName}]${COLORS.reset}`
|
|
77
|
-
: `${timestamp()} [${level.toUpperCase().padEnd(5)}] [${moduleName}]
|
|
76
|
+
: `${timestamp()} [${level.toUpperCase().padEnd(5)}] [${moduleName}]`
|
|
78
77
|
|
|
79
|
-
const output =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
const output =
|
|
79
|
+
args.length > 0
|
|
80
|
+
? `${message} ${args.map(a => JSON.stringify(a, null, 0)).join(' ')}`
|
|
81
|
+
: message
|
|
82
|
+
const stream = level === 'error' ? process.stderr : process.stdout
|
|
83
|
+
stream.write(`${prefix} ${output}\n`)
|
|
84
|
+
}
|
|
83
85
|
|
|
84
86
|
return {
|
|
85
87
|
/**
|
|
@@ -109,5 +111,9 @@ export function createLogger(moduleName) {
|
|
|
109
111
|
* @param {...*} args
|
|
110
112
|
*/
|
|
111
113
|
error: (message, ...args) => log('error', LOG_LEVELS.error, COLORS.red, message, ...args),
|
|
112
|
-
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
createLogger,
|
|
113
119
|
}
|