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.
|
|
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
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
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 = {}) {
|