node-pptx-templater 1.0.10 → 1.0.11

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.
@@ -47,6 +47,76 @@ class ShapeManager {
47
47
  logger.debug(`Updated text for shape "${shapeId}" on slide ${slideIndex}`)
48
48
  }
49
49
 
50
+ /**
51
+ * Updates an existing shape's position and/or dimensions.
52
+ *
53
+ * @param {number} slideIndex
54
+ * @param {string} shapeId
55
+ * @param {Object} options Position and dimensions configuration.
56
+ * @param {number} [options.x] Absolute X offset coordinate (in EMUs).
57
+ * @param {number} [options.y] Absolute Y offset coordinate (in EMUs).
58
+ * @param {number} [options.width] Bounding box width (in EMUs).
59
+ * @param {number} [options.height] Bounding box height (in EMUs).
60
+ * @param {SlideManager} slideManager
61
+ */
62
+ updateShapePosition(slideIndex, shapeId, options = {}, slideManager) {
63
+ const slideXml = slideManager.getSlideXml(slideIndex)
64
+ const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
65
+ const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
66
+ const res = this.findShapeRecursive(spTree, shapeId)
67
+
68
+ if (!res) {
69
+ throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
70
+ }
71
+
72
+ const xfrm = res.shape['p:spPr']?.['a:xfrm']
73
+ if (xfrm) {
74
+ if (options.x !== undefined) {
75
+ if (!xfrm['a:off']) xfrm['a:off'] = {}
76
+ xfrm['a:off']['@_x'] = String(Math.round(options.x))
77
+ }
78
+ if (options.y !== undefined) {
79
+ if (!xfrm['a:off']) xfrm['a:off'] = {}
80
+ xfrm['a:off']['@_y'] = String(Math.round(options.y))
81
+ }
82
+ if (options.width !== undefined) {
83
+ if (!xfrm['a:ext']) xfrm['a:ext'] = {}
84
+ xfrm['a:ext']['@_cx'] = String(Math.round(options.width))
85
+ }
86
+ if (options.height !== undefined) {
87
+ if (!xfrm['a:ext']) xfrm['a:ext'] = {}
88
+ xfrm['a:ext']['@_cy'] = String(Math.round(options.height))
89
+ }
90
+ }
91
+
92
+ const decl = this.#xmlParser.extractDeclaration(slideXml)
93
+ slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
94
+ logger.debug(`Updated position/dimensions for shape "${shapeId}" on slide ${slideIndex}`)
95
+ }
96
+
97
+ /**
98
+ * Updates an existing textbox shape's position and/or dimensions.
99
+ *
100
+ * @param {number} slideIndex
101
+ * @param {string} textBoxId
102
+ * @param {Object} options Position and dimensions configuration.
103
+ * @param {number} [options.x] Absolute X offset coordinate (in EMUs).
104
+ * @param {number} [options.y] Absolute Y offset coordinate (in EMUs).
105
+ * @param {number} [options.width] Bounding box width (in EMUs).
106
+ * @param {number} [options.height] Bounding box height (in EMUs).
107
+ * @param {SlideManager} slideManager
108
+ */
109
+ updateTextBoxPosition(slideIndex, textBoxId, options = {}, slideManager) {
110
+ try {
111
+ this.updateShapePosition(slideIndex, textBoxId, options, slideManager)
112
+ } catch (err) {
113
+ if (err.message.includes('not found')) {
114
+ throw new PPTXError(`Textbox "${textBoxId}" not found in slide ${slideIndex}`)
115
+ }
116
+ throw err
117
+ }
118
+ }
119
+
50
120
  /**
51
121
  * Clones a shape and adds it with offsets.
52
122
  *
@@ -170,6 +170,58 @@ class ChartCacheGenerator {
170
170
  *
171
171
  * 3. If no <c:title> block exists at all: create a minimal one.
172
172
  */
173
+ /**
174
+ * Extracts parts of <c:txPr> block (bodyPr, pPr, and converts defRPr to rPr)
175
+ * to use when creating custom rich text components.
176
+ *
177
+ * @param {string} txPrXml - The <c:txPr> XML string or block content.
178
+ * @returns {Object} { bodyPr, pPrXml, rPrXml }
179
+ */
180
+ static extractTxPrParts(txPrXml) {
181
+ let bodyPr = '<a:bodyPr/>'
182
+ let pPrXml = ''
183
+ let rPrXml = ''
184
+
185
+ if (!txPrXml) {
186
+ return { bodyPr, pPrXml, rPrXml }
187
+ }
188
+
189
+ let txPrContent = txPrXml
190
+ const txPrInnerMatch = /<c:txPr>([\s\S]*?)<\/c:txPr>/.exec(txPrXml)
191
+ if (txPrInnerMatch) {
192
+ txPrContent = txPrInnerMatch[1]
193
+ }
194
+
195
+ // Extract <a:bodyPr>
196
+ const bodyPrMatch = /(<a:bodyPr[^>]*\/>|<a:bodyPr[^>]*>[\s\S]*?<\/a:bodyPr>)/.exec(txPrContent)
197
+ if (bodyPrMatch) bodyPr = bodyPrMatch[1]
198
+
199
+ // Extract <a:defRPr> → convert to <a:rPr> for the run
200
+ const defRPrMatch = /(<a:defRPr[\s\S]*?<\/a:defRPr>|<a:defRPr[^>]*\/>)/.exec(txPrContent)
201
+ if (defRPrMatch) {
202
+ rPrXml = defRPrMatch[1].replace(/^<a:defRPr/, '<a:rPr').replace(/<\/a:defRPr>$/, '</a:rPr>')
203
+ }
204
+
205
+ // Extract <a:pPr> (keeps algn, indent, etc.) but strip <a:defRPr> from it
206
+ const pPrBlockMatch = /(<a:pPr[^>]*>)([\s\S]*?)(<\/a:pPr>)/.exec(txPrContent)
207
+ if (pPrBlockMatch) {
208
+ const innerContent = pPrBlockMatch[2]
209
+ .replace(/<a:defRPr(?:[^>]*\/>|[\s\S]*?<\/a:defRPr>)/g, '')
210
+ .trim()
211
+ const attrs = pPrBlockMatch[1].slice(7, -1).trim()
212
+ if (attrs || innerContent) {
213
+ pPrXml = innerContent
214
+ ? `${pPrBlockMatch[1]}${innerContent}${pPrBlockMatch[3]}`
215
+ : `<a:pPr ${attrs}/>`
216
+ }
217
+ } else {
218
+ const scPPrMatch = /(<a:pPr[^>]*\/>)/.exec(txPrContent)
219
+ if (scPPrMatch) pPrXml = scPPrMatch[1]
220
+ }
221
+
222
+ return { bodyPr, pPrXml, rPrXml }
223
+ }
224
+
173
225
  static updateTitle(xml, title) {
174
226
  // Split by \n so callers can drive multi-paragraph titles
175
227
  const titleLines = title.split('\n')
@@ -205,48 +257,7 @@ class ChartCacheGenerator {
205
257
 
206
258
  // ── Strategy 2 ──────────────────────────────────────────────────────────────
207
259
  // No <c:tx> yet – build one from <c:txPr> styles.
208
- let bodyPr = '<a:bodyPr/>'
209
- let pPrXml = '' // paragraph properties (alignment etc.) without defRPr
210
- let rPr = '' // run properties from defRPr
211
-
212
- const txPrMatch = /<c:txPr>([\s\S]*?)<\/c:txPr>/.exec(titleContent)
213
- if (txPrMatch) {
214
- const txPrContent = txPrMatch[1]
215
-
216
- // Extract <a:bodyPr>
217
- const bodyPrMatch = /(<a:bodyPr[^>]*\/>|<a:bodyPr[^>]*>[\s\S]*?<\/a:bodyPr>)/.exec(
218
- txPrContent
219
- )
220
- if (bodyPrMatch) bodyPr = bodyPrMatch[1]
221
-
222
- // Extract <a:defRPr> → convert to <a:rPr> for the run
223
- const defRPrMatch = /(<a:defRPr[\s\S]*?<\/a:defRPr>|<a:defRPr[^>]*\/>)/.exec(txPrContent)
224
- if (defRPrMatch) {
225
- rPr = defRPrMatch[1].replace(/^<a:defRPr/, '<a:rPr').replace(/<\/a:defRPr>$/, '</a:rPr>')
226
- }
227
-
228
- // Extract <a:pPr> (keeps algn, indent, etc.) but strip <a:defRPr> from it
229
- // since that is now expressed as <a:rPr> in the run.
230
- // We must handle both <a:defRPr .../> (self-closing) and
231
- // <a:defRPr ...>...</a:defRPr> (element with children).
232
- const pPrBlockMatch = /(<a:pPr[^>]*>)([\s\S]*?)(<\/a:pPr>)/.exec(txPrContent)
233
- if (pPrBlockMatch) {
234
- const innerContent = pPrBlockMatch[2]
235
- .replace(/<a:defRPr(?:[^>]*\/>|[\s\S]*?<\/a:defRPr>)/g, '')
236
- .trim()
237
- // Only emit pPr tag if it has attributes or remaining child content
238
- const attrs = pPrBlockMatch[1].slice(7, -1).trim() // strip '<a:pPr' and '>'
239
- if (attrs || innerContent) {
240
- pPrXml = innerContent
241
- ? `${pPrBlockMatch[1]}${innerContent}${pPrBlockMatch[3]}`
242
- : `<a:pPr ${attrs}/>`
243
- }
244
- } else {
245
- // Self-closing <a:pPr .../> (no children)
246
- const scPPrMatch = /(<a:pPr[^>]*\/>)/.exec(txPrContent)
247
- if (scPPrMatch) pPrXml = scPPrMatch[1]
248
- }
249
- }
260
+ const { bodyPr, pPrXml, rPrXml: rPr } = this.extractTxPrParts(titleContent)
250
261
 
251
262
  // Build one <a:p> per title line, each with the same pPr + rPr
252
263
  const paragraphs = titleLines
@@ -388,7 +399,15 @@ class ChartCacheGenerator {
388
399
  existingSpPr = '',
389
400
  existingDLblSpPrs = {}
390
401
  ) {
391
- const { labels, labelsFromCells, template, position, labelStyle, labelMap } = options
402
+ const {
403
+ labels,
404
+ labelsFromCells,
405
+ template,
406
+ position,
407
+ labelStyle,
408
+ labelMap,
409
+ showSeriesNameInBar,
410
+ } = options
392
411
 
393
412
  let xml = '<c:dLbls>'
394
413
 
@@ -403,7 +422,10 @@ class ChartCacheGenerator {
403
422
  top: 't',
404
423
  bottom: 'b',
405
424
  }
406
- const openxmlPos = position ? posMap[position] : null
425
+ let openxmlPos = position ? posMap[position] : null
426
+ if (!openxmlPos && showSeriesNameInBar) {
427
+ openxmlPos = 'ctr'
428
+ }
407
429
 
408
430
  const values = seriesData.values || []
409
431
  const sumValues = values.reduce((sum, v) => sum + (Number(v) || 0), 0)
@@ -485,15 +507,29 @@ class ChartCacheGenerator {
485
507
  xml += `</c:strRef>`
486
508
  xml += `</c:tx>`
487
509
  } else {
510
+ let bodyPr = '<a:bodyPr/>'
511
+ let pPrXml = ''
512
+ let rPrXml = ''
513
+ if (existingTxPr) {
514
+ const extracted = this.extractTxPrParts(existingTxPr)
515
+ bodyPr = extracted.bodyPr
516
+ pPrXml = extracted.pPrXml
517
+ rPrXml = extracted.rPrXml
518
+ }
519
+
520
+ const labelLines = String(textContent).split('\n')
521
+ const paragraphs = labelLines
522
+ .map(line => {
523
+ const escapedLine = this.#escapeXml(line)
524
+ return `<a:p>${pPrXml}<a:r>${rPrXml}<a:t>${escapedLine}</a:t></a:r></a:p>`
525
+ })
526
+ .join('')
527
+
488
528
  xml += `<c:tx>`
489
529
  xml += `<c:rich>`
490
- xml += `<a:bodyPr/>`
530
+ xml += bodyPr
491
531
  xml += `<a:lstStyle/>`
492
- xml += `<a:p>`
493
- xml += `<a:r>`
494
- xml += `<a:t>${this.#escapeXml(textContent)}</a:t>`
495
- xml += `</a:r>`
496
- xml += `</a:p>`
532
+ xml += paragraphs
497
533
  xml += `</c:rich>`
498
534
  xml += `</c:tx>`
499
535
  }
@@ -571,7 +607,9 @@ class ChartCacheGenerator {
571
607
 
572
608
  // showVal
573
609
  const defaultShowVal = hasCustomLabels ? '0' : '1'
574
- if (existingShowTags['showVal'] && !hasCustomLabels) {
610
+ if (showSeriesNameInBar) {
611
+ xml += `<c:showVal val="0"/>`
612
+ } else if (existingShowTags['showVal'] && !hasCustomLabels) {
575
613
  xml += existingShowTags['showVal']
576
614
  } else {
577
615
  xml += `<c:showVal val="${defaultShowVal}"/>`
@@ -585,7 +623,9 @@ class ChartCacheGenerator {
585
623
  }
586
624
 
587
625
  // showSerName
588
- if (existingShowTags['showSerName']) {
626
+ if (showSeriesNameInBar) {
627
+ xml += `<c:showSerName val="1"/>`
628
+ } else if (existingShowTags['showSerName']) {
589
629
  xml += existingShowTags['showSerName']
590
630
  } else {
591
631
  xml += `<c:showSerName val="0"/>`