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
@@ -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
- export function buildNewSlideXml(options, slideIndex) {
73
- const { title = '', elements = [], layout = 'blank', _rawXml } = options;
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, '&lt;')
311
305
  .replace(/>/g, '&gt;')
312
306
  .replace(/"/g, '&quot;')
313
- .replace(/'/g, '&apos;');
307
+ .replace(/'/g, '&apos;')
308
+ }
309
+
310
+ module.exports = {
311
+ buildNewSlideXml,
312
+ EMU,
314
313
  }
@@ -17,15 +17,15 @@
17
17
  * </Types>
18
18
  */
19
19
 
20
- import { createLogger } from './logger.js';
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
- '</Types>',
92
- ` <Default ${entry}/>\n</Types>`
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
- export const contentTypesHelper = new ContentTypesHelper();
135
+ module.exports = {
136
+ contentTypesHelper: new ContentTypesHelper(),
137
+ }
@@ -18,19 +18,19 @@
18
18
  * @description Base error class for all node-pptx-templater errors.
19
19
  * @extends Error
20
20
  */
21
- export class PPTXError extends Error {
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
- export class SlideNotFoundError extends PPTXError {
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
- export class ChartNotFoundError extends PPTXError {
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
- export class TableNotFoundError extends PPTXError {
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
- export class XMLParseError extends PPTXError {
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
- export class InvalidTemplateError extends PPTXError {
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
- export class MediaEmbedError extends PPTXError {
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
+ }
@@ -3,7 +3,7 @@
3
3
  * Used for generating shape IDs, slide IDs, etc. in OpenXML.
4
4
  */
5
5
 
6
- import { randomBytes } from 'crypto';
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
- export function generateUniqueId(existingIds = []) {
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
- export 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
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
- ].join('-').toUpperCase();
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
- export function generateSlideId(existingSlideIds = []) {
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
  }
@@ -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 = LOG_LEVELS[
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
- export function createLogger(moduleName) {
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 = args.length > 0 ? `${message} ${args.map(a => JSON.stringify(a, null, 0)).join(' ')}` : message;
80
- const stream = level === 'error' ? process.stderr : process.stdout;
81
- stream.write(`${prefix} ${output}\n`);
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
  }