node-pptx-templater 1.0.8 → 1.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "High-performance, low-level PowerPoint (PPTX) OpenXML template engine for Node.js. Dynamically replace text, insert images, update charts (with Excel workbook data caching), and merge table cells without PowerPoint corruption or Repair Mode prompts.",
5
5
  "main": "./src/index.js",
6
6
  "type": "commonjs",
@@ -181,4 +181,4 @@
181
181
  "LICENSE",
182
182
  "CHANGELOG.md"
183
183
  ]
184
- }
184
+ }
@@ -154,17 +154,113 @@ class ChartCacheGenerator {
154
154
  }
155
155
 
156
156
  /**
157
- * Updates the chart title in chart XML.
157
+ * Updates the chart title text while fully preserving all existing styling.
158
+ *
159
+ * Three strategies in priority order:
160
+ *
161
+ * 1. If <c:tx><c:rich> already exists (title was previously set via PowerPoint or this
162
+ * library): ONLY replace <a:t> text values in-place, mapped by \n-split lines to
163
+ * existing runs in document order. This preserves alignment, bold/italic/underline,
164
+ * font sizes, paragraph structure, and any other per-run or per-paragraph properties.
165
+ *
166
+ * 2. If <c:title> exists but has no <c:tx> (title text comes from <c:txPr> default
167
+ * properties): Build a new <c:tx><c:rich> by extracting <a:bodyPr>, <a:pPr> (including
168
+ * algn), and <a:defRPr>→<a:rPr> from <c:txPr>. Supports multi-line via \n splitting
169
+ * into separate <a:p> paragraphs, each inheriting the same pPr and rPr.
170
+ *
171
+ * 3. If no <c:title> block exists at all: create a minimal one.
158
172
  */
159
173
  static updateTitle(xml, title) {
160
- const titleBlock = `<c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>${this.#escapeXml(title)}</a:t></a:r></a:p></c:rich></c:tx><c:layout/></c:title>`
161
- if (xml.includes('<c:title>')) {
162
- const fullTitlePattern = /(<c:title>[\s\S]*?<\/c:title>)/
163
- return xml.replace(fullTitlePattern, titleBlock)
164
- } else {
165
- const chartPattern = /(<c:chart>)/
166
- return xml.replace(chartPattern, `$1${titleBlock}`)
174
+ // Split by \n so callers can drive multi-paragraph titles
175
+ const titleLines = title.split('\n')
176
+
177
+ if (!xml.includes('<c:title>')) {
178
+ // Strategy 3 – no title block yet, create minimal
179
+ const escapedText = this.#escapeXml(titleLines[0])
180
+ const titleBlock = `<c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>${escapedText}</a:t></a:r></a:p></c:rich></c:tx><c:layout/></c:title>`
181
+ return xml.replace(/(<c:chart>)/, `$1${titleBlock}`)
167
182
  }
183
+
184
+ return xml.replace(/<c:title>([\s\S]*?)<\/c:title>/, (match, titleContent) => {
185
+ // ── Strategy 1 ──────────────────────────────────────────────────────────────
186
+ // Existing <c:tx><c:rich> is present: preserve every element, just swap text.
187
+ if (titleContent.includes('<c:tx>')) {
188
+ const txMatch = /<c:tx>([\s\S]*?)<\/c:tx>/.exec(titleContent)
189
+ if (txMatch && txMatch[1].includes('<c:rich>')) {
190
+ let lineIndex = 0
191
+ // Replace each <a:t>…</a:t> in document order with the next line.
192
+ // Any run that maps beyond the supplied lines gets an empty string.
193
+ const updatedTx = txMatch[1].replace(/<a:t>[^<]*<\/a:t>/g, () => {
194
+ const text = lineIndex < titleLines.length ? this.#escapeXml(titleLines[lineIndex]) : ''
195
+ lineIndex++
196
+ return `<a:t>${text}</a:t>`
197
+ })
198
+ const updatedContent = titleContent.replace(
199
+ /<c:tx>[\s\S]*?<\/c:tx>/,
200
+ `<c:tx>${updatedTx}</c:tx>`
201
+ )
202
+ return `<c:title>${updatedContent}</c:title>`
203
+ }
204
+ }
205
+
206
+ // ── Strategy 2 ──────────────────────────────────────────────────────────────
207
+ // 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
+ }
250
+
251
+ // Build one <a:p> per title line, each with the same pPr + rPr
252
+ const paragraphs = titleLines
253
+ .map(line => {
254
+ const escapedLine = this.#escapeXml(line)
255
+ return `<a:p>${pPrXml}<a:r>${rPr}<a:t>${escapedLine}</a:t></a:r></a:p>`
256
+ })
257
+ .join('')
258
+
259
+ const newTxBlock = `<c:tx><c:rich>${bodyPr}<a:lstStyle/>${paragraphs}</c:rich></c:tx>`
260
+
261
+ // Prepend <c:tx> before the first existing sibling (overlay, spPr, txPr, etc.)
262
+ return `<c:title>${newTxBlock}${titleContent}</c:title>`
263
+ })
168
264
  }
169
265
 
170
266
  static updateDataLabelsInXml(xml, seriesIndex, options, categories = [], seriesData = {}) {