node-pptx-templater 1.0.9 → 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,58 +154,113 @@ class ChartCacheGenerator {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* Updates the chart title text
|
|
158
|
-
* styling (spPr, txPr, overlay, layout) from the template.
|
|
157
|
+
* Updates the chart title text while fully preserving all existing styling.
|
|
159
158
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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.
|
|
164
172
|
*/
|
|
165
173
|
static updateTitle(xml, title) {
|
|
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}`)
|
|
182
|
+
}
|
|
167
183
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
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>`
|
|
176
201
|
)
|
|
177
|
-
|
|
202
|
+
return `<c:title>${updatedContent}</c:title>`
|
|
178
203
|
}
|
|
204
|
+
}
|
|
179
205
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Convert <a:defRPr ...> to <a:rPr ...> (same attributes, different tag name)
|
|
186
|
-
rPr = defRPrMatch[1]
|
|
187
|
-
.replace(/^<a:defRPr/, '<a:rPr')
|
|
188
|
-
.replace(/<\/a:defRPr>$/, '</a:rPr>')
|
|
189
|
-
}
|
|
190
|
-
}
|
|
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
|
|
191
211
|
|
|
192
|
-
|
|
212
|
+
const txPrMatch = /<c:txPr>([\s\S]*?)<\/c:txPr>/.exec(titleContent)
|
|
213
|
+
if (txPrMatch) {
|
|
214
|
+
const txPrContent = txPrMatch[1]
|
|
193
215
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
244
|
} else {
|
|
199
|
-
//
|
|
200
|
-
|
|
245
|
+
// Self-closing <a:pPr .../> (no children)
|
|
246
|
+
const scPPrMatch = /(<a:pPr[^>]*\/>)/.exec(txPrContent)
|
|
247
|
+
if (scPPrMatch) pPrXml = scPPrMatch[1]
|
|
201
248
|
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
})
|
|
209
264
|
}
|
|
210
265
|
|
|
211
266
|
static updateDataLabelsInXml(xml, seriesIndex, options, categories = [], seriesData = {}) {
|