node-pptx-templater 1.0.6 → 1.0.8
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/README.md +258 -2
- package/package.json +1 -1
- package/src/core/PPTXTemplater.js +92 -1
- package/src/core/TemplateEngine.js +126 -0
- package/src/core/ValidationEngine.js +179 -0
- package/src/managers/ChartManager.js +383 -21
- package/src/managers/TextManager.js +271 -0
- package/src/managers/charts/ChartCacheGenerator.js +427 -1
- package/src/managers/charts/ChartWorkbookUpdater.js +204 -33
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ You design your layouts, tables, fonts, brand styles, charts, and animations ins
|
|
|
35
35
|
* 🛡️ **Hardened XML Security**: Fully immune to Billion Laughs (XML bombs), XXE (XML External Entity Injection), and parser crashes due to oversized expansions.
|
|
36
36
|
* 🧩 **Text Run Fragmentation Healing**: Solves the split-tag issue. PowerPoint splits `{{placeholders}}` into fragmented `<a:r>` nodes; our parser unifies and replaces them while keeping original formatting intact.
|
|
37
37
|
* 📊 **Excel Cache Synchronized Charting**: Supports Bar, Line, Pie, Doughnut, Area, and Scatter charts. Not only updates the visual XML coordinates but also synchronizes data points inside the underlying embedded Excel spreadsheets (`ppt/embeddings/`) to bypass PowerPoint's "Update Data" warnings.
|
|
38
|
+
* 🏷️ **Chart Data Labels & Value From Cells**: Configure custom arrays, dynamic category maps, rich templates, positions (insideEnd, bestFit, center), and custom styles (fonts, colors, weight) and serialize them directly to the underlying Excel backing sheet.
|
|
38
39
|
* 📋 **DrawingML Table Cell Merging**: Easily configure horizontal spans (`gridSpan`/`hMerge`), vertical spans (`rowSpan`/`vMerge`), and rectangular blocks. Injects unique `rowId` hashes to maintain relationship integrity.
|
|
39
40
|
* 🥞 **Z-Order Layer Stacking**: Reorder shapes, images, charts, and tables programmatically. Simulates PowerPoint's "Bring to Front" and "Send to Back" commands directly in the slide's `<p:spTree>`.
|
|
40
41
|
* 🎛️ **Slide Management**: Duplicate, delete, reorder slides, or import slides from external decks with automatic media and theme deduplication.
|
|
@@ -80,6 +81,15 @@ async function generateReport() {
|
|
|
80
81
|
]
|
|
81
82
|
});
|
|
82
83
|
|
|
84
|
+
// 3b. Configure custom chart data labels with templates and styling
|
|
85
|
+
ppt.useSlide(2)
|
|
86
|
+
.updateDataLabels('revenue-chart', {
|
|
87
|
+
series: 0,
|
|
88
|
+
template: '{category}: {value}',
|
|
89
|
+
position: 'insideEnd',
|
|
90
|
+
labelStyle: { fontFamily: 'Arial', fontSize: 10, bold: true }
|
|
91
|
+
});
|
|
92
|
+
|
|
83
93
|
// 4. Update table structure with cell merges and colors on Slide 3
|
|
84
94
|
ppt.useSlide(3)
|
|
85
95
|
.updateTable('performance-table', [
|
|
@@ -330,7 +340,7 @@ ppt.useSlide(1).getTables(());
|
|
|
330
340
|
### Charts API
|
|
331
341
|
|
|
332
342
|
#### `updateChart(chartId, data)`
|
|
333
|
-
Updates chart data in the selected slide(s). Finds charts by their name/ID and updates categories, series, and values. Preserves original chart styles, themes, and formatting.
|
|
343
|
+
Updates chart data in the selected slide(s). Finds charts by their name/ID and updates categories, series, and values. Preserves original chart styles, themes, and formatting. Supports inline custom data labels by passing objects in the format `{ data: number, label: string }` instead of numbers.
|
|
334
344
|
|
|
335
345
|
* **Arguments**:
|
|
336
346
|
* `chartId` (`string`): Chart name or relationship ID.
|
|
@@ -338,7 +348,7 @@ Updates chart data in the selected slide(s). Finds charts by their name/ID and u
|
|
|
338
348
|
* `data.categories` (`string[]`): Category labels (X-axis).
|
|
339
349
|
* `data.series` (`SeriesData[]`): Data series array.
|
|
340
350
|
* `data.series[].name` (`string`): Series name.
|
|
341
|
-
* `data.series[].values` (`number[]`): Data values.
|
|
351
|
+
* `data.series[].values` (`number[]|object[]`): Data values (numbers or label objects).
|
|
342
352
|
* **Returns**: `PPTXTemplater` - this (chainable)
|
|
343
353
|
|
|
344
354
|
```javascript
|
|
@@ -402,6 +412,40 @@ Delegates core actions to slide element sub-managers.
|
|
|
402
412
|
ppt.useSlide(1).updateChartCategories(());
|
|
403
413
|
```
|
|
404
414
|
|
|
415
|
+
#### `updateDataLabels(())`
|
|
416
|
+
Delegates core actions to slide element sub-managers.
|
|
417
|
+
|
|
418
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
419
|
+
|
|
420
|
+
```javascript
|
|
421
|
+
ppt.useSlide(1).updateDataLabels('SalesChart', {
|
|
422
|
+
series: 0,
|
|
423
|
+
labels: ['Excellent', 'Good', 'Poor']
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### `getDataLabels(())`
|
|
428
|
+
Delegates core actions to slide element sub-managers.
|
|
429
|
+
|
|
430
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
431
|
+
|
|
432
|
+
```javascript
|
|
433
|
+
const labels = await ppt.useSlide(1).getDataLabels('SalesChart', { series: 0 });
|
|
434
|
+
console.log(labels); // [{ point: 0, value: 'Excellent' }, ...]
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### `validateDataLabels(())`
|
|
438
|
+
Delegates core actions to slide element sub-managers.
|
|
439
|
+
|
|
440
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
441
|
+
|
|
442
|
+
```javascript
|
|
443
|
+
const result = await ppt.useSlide(1).validateDataLabels('SalesChart', {
|
|
444
|
+
labels: ['High', 'Low']
|
|
445
|
+
});
|
|
446
|
+
console.log(result.valid);
|
|
447
|
+
```
|
|
448
|
+
|
|
405
449
|
#### `getCharts(())`
|
|
406
450
|
Delegates core actions to slide element sub-managers.
|
|
407
451
|
|
|
@@ -674,6 +718,44 @@ Adds a special navigation link (next, previous, first, last slide) to a shape or
|
|
|
674
718
|
ppt.useSlide(1).addShapeNavigationLink(options);
|
|
675
719
|
```
|
|
676
720
|
|
|
721
|
+
#### `updateText(tag, data)`
|
|
722
|
+
Updates shape text or list content by placeholder tag or shape name/ID. Supports bullet lists, numbered lists, nested lists, and custom styling.
|
|
723
|
+
|
|
724
|
+
* **Arguments**:
|
|
725
|
+
* `tag` (`string`): Placeholder tag (e.g. '{{name}}' or 'name') or shape name/ID.
|
|
726
|
+
* `data` (`string|Object`): String value or list configuration object.
|
|
727
|
+
* **Returns**: `PPTXTemplater` - this (chainable)
|
|
728
|
+
|
|
729
|
+
```javascript
|
|
730
|
+
ppt.useSlide(1).updateText('Features', {
|
|
731
|
+
list: ['Point A', 'Point B', 'Point C']
|
|
732
|
+
});
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
#### `getList(tag)`
|
|
736
|
+
Retrieves list items from a shape or text box by name or placeholder tag.
|
|
737
|
+
|
|
738
|
+
* **Arguments**:
|
|
739
|
+
* `tag` (`string`): Shape name/ID or placeholder tag.
|
|
740
|
+
* **Returns**: `Array` - Nested list structure of items.
|
|
741
|
+
|
|
742
|
+
```javascript
|
|
743
|
+
const items = ppt.useSlide(1).getList('Features');
|
|
744
|
+
console.log(items); // ['A', { text: 'B', children: [...] }]
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
#### `validateList(data)`
|
|
748
|
+
Validates a list structure and values.
|
|
749
|
+
|
|
750
|
+
* **Arguments**:
|
|
751
|
+
* `data` (`Object|Array`): List config object or array of items.
|
|
752
|
+
* **Returns**: `Object` - Report containing validation result.
|
|
753
|
+
|
|
754
|
+
```javascript
|
|
755
|
+
const result = ppt.validateList(['Valid string', 'Another item']);
|
|
756
|
+
console.log(result.valid);
|
|
757
|
+
```
|
|
758
|
+
|
|
677
759
|
#### `replaceTextByTag(())`
|
|
678
760
|
Delegates core actions to slide element sub-managers.
|
|
679
761
|
|
|
@@ -1249,6 +1331,180 @@ ppt.useSlide(1).create(());
|
|
|
1249
1331
|
|
|
1250
1332
|
---
|
|
1251
1333
|
|
|
1334
|
+
## 📊 Chart Data Labels & Value From Cells
|
|
1335
|
+
|
|
1336
|
+
PPTXForge supports advanced, PowerPoint-compatible chart data labels. You can customize label data sources, templates, positioning, and visual styles.
|
|
1337
|
+
|
|
1338
|
+
### 1. Value From Cells (Excel Synchronization)
|
|
1339
|
+
Pull dynamic data labels directly from worksheet range cells inside the backing Excel spreadsheet. This is Excel's native "Value From Cells" feature, reconstructed programmatically inside the `.pptx` XML and `.xlsx` worksheets.
|
|
1340
|
+
|
|
1341
|
+
```javascript
|
|
1342
|
+
ppt.useSlide(1).updateDataLabels('SalesChart', {
|
|
1343
|
+
series: 0,
|
|
1344
|
+
labelsFromCells: 'Sheet1!D2:D5'
|
|
1345
|
+
});
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
### 2. Literal Arrays
|
|
1349
|
+
Override data labels for specific points in a series with a static list of strings:
|
|
1350
|
+
|
|
1351
|
+
```javascript
|
|
1352
|
+
ppt.useSlide(1).updateDataLabels('SalesChart', {
|
|
1353
|
+
series: 0,
|
|
1354
|
+
labels: ['Top Performance', 'Met Target', 'Action Required', 'At Risk']
|
|
1355
|
+
});
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
### 3. Dynamic Label Templates
|
|
1359
|
+
Combine values, category names, percentages, and custom label strings to format annotations:
|
|
1360
|
+
|
|
1361
|
+
```javascript
|
|
1362
|
+
ppt.useSlide(1).updateDataLabels('SalesChart', {
|
|
1363
|
+
series: 0,
|
|
1364
|
+
template: '{category}: {value} ({percentage}%)'
|
|
1365
|
+
});
|
|
1366
|
+
```
|
|
1367
|
+
* **Variables**: `{category}`, `{value}`, `{percentage}`, `{series}`, `{customLabel}`
|
|
1368
|
+
|
|
1369
|
+
### 4. Label Positions
|
|
1370
|
+
Set label alignment to any of PowerPoint's standard values:
|
|
1371
|
+
|
|
1372
|
+
```javascript
|
|
1373
|
+
ppt.useSlide(1).updateDataLabels('SalesChart', {
|
|
1374
|
+
series: 0,
|
|
1375
|
+
position: 'insideEnd' // 'center', 'insideEnd', 'insideBase', 'outsideEnd', 'bestFit', 'left', 'right', 'top', 'bottom'
|
|
1376
|
+
});
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
### 5. Custom Label Styling
|
|
1380
|
+
Format your data labels using custom fonts, sizes, colors, and weight properties:
|
|
1381
|
+
|
|
1382
|
+
```javascript
|
|
1383
|
+
ppt.useSlide(1).updateDataLabels('SalesChart', {
|
|
1384
|
+
series: 0,
|
|
1385
|
+
labels: ['High', 'Medium', 'Low'],
|
|
1386
|
+
labelStyle: {
|
|
1387
|
+
fontFamily: 'Century Gothic',
|
|
1388
|
+
fontSize: 12,
|
|
1389
|
+
color: '#0055A5',
|
|
1390
|
+
bold: true,
|
|
1391
|
+
italic: true,
|
|
1392
|
+
underline: true
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
### 6. Inline Custom Data Labels (`updateChart`)
|
|
1398
|
+
You can define custom data labels inline within the standard `updateChart()` series `values` array using objects in the format `{ data: number, label: string }`. This avoids calling separate styling or update methods:
|
|
1399
|
+
|
|
1400
|
+
```javascript
|
|
1401
|
+
ppt.useSlide(1).updateChart('RevenueChart', {
|
|
1402
|
+
categories: ['Q1', 'Q2', 'Q3', 'Q4'],
|
|
1403
|
+
series: [
|
|
1404
|
+
{
|
|
1405
|
+
name: 'Product A',
|
|
1406
|
+
values: [
|
|
1407
|
+
{ data: 145, label: 'Q1: 145 (Low)' },
|
|
1408
|
+
{ data: 210, label: 'Q2: 210 (Med)' },
|
|
1409
|
+
{ data: 190, label: 'Q3: 190 (Med)' },
|
|
1410
|
+
{ data: 250, label: 'Q4: 250 (High)' }
|
|
1411
|
+
]
|
|
1412
|
+
}
|
|
1413
|
+
]
|
|
1414
|
+
});
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
To preserve PowerPoint integrity, the engine ensures that if one value contains a label, all values in that series must have labels, and the label properties must be string values.
|
|
1418
|
+
|
|
1419
|
+
---
|
|
1420
|
+
|
|
1421
|
+
## 📋 Native Lists (Bullet & Numbered Lists)
|
|
1422
|
+
|
|
1423
|
+
PPTXForge supports native PowerPoint bullet lists and numbered lists across text placeholders, shapes, text boxes, table cells, and grouped shapes. When generating lists, the engine preserves run styles, custom bullet characters, indentation, and color overlays, generating valid OpenXML/DrawingML without repair alerts.
|
|
1424
|
+
|
|
1425
|
+
### 1. Basic Bullet List
|
|
1426
|
+
Update a shape or text placeholder to be a bullet list:
|
|
1427
|
+
|
|
1428
|
+
```javascript
|
|
1429
|
+
ppt.useSlide(1).updateText('Features', {
|
|
1430
|
+
list: [
|
|
1431
|
+
'Fast PPTX generation',
|
|
1432
|
+
'OpenXML based',
|
|
1433
|
+
'Chart updates',
|
|
1434
|
+
'Table updates'
|
|
1435
|
+
]
|
|
1436
|
+
});
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
### 2. Numbered / Ordered List
|
|
1440
|
+
Use the `ordered` flag to convert the list to a numbered format:
|
|
1441
|
+
|
|
1442
|
+
```javascript
|
|
1443
|
+
ppt.useSlide(1).updateText('Steps', {
|
|
1444
|
+
ordered: true,
|
|
1445
|
+
list: [
|
|
1446
|
+
'Import template',
|
|
1447
|
+
'Update data',
|
|
1448
|
+
'Generate PPTX'
|
|
1449
|
+
]
|
|
1450
|
+
});
|
|
1451
|
+
```
|
|
1452
|
+
* Custom numbering systems can be specified with `style.numberType` (e.g., `arabicPeriod`, `alphaLcParen`, `romanUcPeriod`).
|
|
1453
|
+
|
|
1454
|
+
### 3. Nested / Multi-Level Lists
|
|
1455
|
+
Construct hierarchy by passing objects containing a `text` string and a `children` array:
|
|
1456
|
+
|
|
1457
|
+
```javascript
|
|
1458
|
+
ppt.useSlide(1).updateText('Requirements', {
|
|
1459
|
+
list: [
|
|
1460
|
+
'Frontend',
|
|
1461
|
+
{
|
|
1462
|
+
text: 'Backend Development',
|
|
1463
|
+
children: [
|
|
1464
|
+
'Node.js',
|
|
1465
|
+
{
|
|
1466
|
+
text: 'Databases',
|
|
1467
|
+
children: ['MS SQL', 'PostgreSQL']
|
|
1468
|
+
}
|
|
1469
|
+
]
|
|
1470
|
+
}
|
|
1471
|
+
]
|
|
1472
|
+
});
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
### 4. Custom List Styling
|
|
1476
|
+
Customize bullet characters, colors, sizes, and font properties:
|
|
1477
|
+
|
|
1478
|
+
```javascript
|
|
1479
|
+
ppt.useSlide(1).updateText('KPIs', {
|
|
1480
|
+
list: ['Revenue Up', 'Margins Normal'],
|
|
1481
|
+
style: {
|
|
1482
|
+
fontFamily: 'Arial',
|
|
1483
|
+
fontSize: 18,
|
|
1484
|
+
color: '#0055AA',
|
|
1485
|
+
bulletColor: '#FF5500',
|
|
1486
|
+
bulletChar: '✦',
|
|
1487
|
+
bulletSize: 120 // Percentage relative to text (e.g. 120%)
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
### 5. Table Cell Lists
|
|
1493
|
+
You can also generate list hierarchies directly inside a cell of a DrawingML table:
|
|
1494
|
+
|
|
1495
|
+
```javascript
|
|
1496
|
+
ppt.useSlide(3).updateTable('sales-table', [
|
|
1497
|
+
['Category', 'Performance details'],
|
|
1498
|
+
['North Region', '{{CellPlaceholder}}']
|
|
1499
|
+
]);
|
|
1500
|
+
|
|
1501
|
+
ppt.updateText('CellPlaceholder', {
|
|
1502
|
+
list: ['Table Bullet 1', 'Table Bullet 2']
|
|
1503
|
+
});
|
|
1504
|
+
```
|
|
1505
|
+
|
|
1506
|
+
---
|
|
1507
|
+
|
|
1252
1508
|
## 🔒 Advanced XML Security
|
|
1253
1509
|
|
|
1254
1510
|
PPTXForge uses a secure XML parser that defends your application servers against common XML vulnerabilities.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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",
|
|
@@ -359,20 +359,34 @@ class PPTXTemplater {
|
|
|
359
359
|
* Updates chart data in the selected slide(s).
|
|
360
360
|
* Finds charts by their name/ID and updates categories, series, and values.
|
|
361
361
|
* Preserves original chart styles, themes, and formatting.
|
|
362
|
+
* Supports inline custom data labels by passing objects in the format `{ data: number, label: string }` instead of numbers.
|
|
362
363
|
*
|
|
363
364
|
* @param {string} chartId - Chart name or relationship ID.
|
|
364
365
|
* @param {ChartData} data - New chart data.
|
|
365
366
|
* @param {string[]} data.categories - Category labels (X-axis).
|
|
366
367
|
* @param {SeriesData[]} data.series - Data series array.
|
|
367
368
|
* @param {string} data.series[].name - Series name.
|
|
368
|
-
* @param {number[]} data.series[].values - Data values.
|
|
369
|
+
* @param {number[]|object[]} data.series[].values - Data values (numbers or label objects).
|
|
369
370
|
* @returns {PPTXTemplater} this (chainable)
|
|
370
371
|
*
|
|
371
372
|
* @example
|
|
373
|
+
* // Simple numeric values:
|
|
372
374
|
* ppt.updateChart('sales-chart', {
|
|
373
375
|
* categories: ['Jan', 'Feb', 'Mar'],
|
|
374
376
|
* series: [{ name: 'Revenue', values: [120, 150, 180] }]
|
|
375
377
|
* });
|
|
378
|
+
*
|
|
379
|
+
* // Custom inline data labels:
|
|
380
|
+
* ppt.updateChart('sales-chart', {
|
|
381
|
+
* categories: ['Q1', 'Q2'],
|
|
382
|
+
* series: [{
|
|
383
|
+
* name: 'Revenue',
|
|
384
|
+
* values: [
|
|
385
|
+
* { data: 120, label: '120 (40%)' },
|
|
386
|
+
* { data: 180, label: '180 (60%)' }
|
|
387
|
+
* ]
|
|
388
|
+
* }]
|
|
389
|
+
* });
|
|
376
390
|
*/
|
|
377
391
|
updateChart(chartId, data) {
|
|
378
392
|
this.#assertLoaded()
|
|
@@ -1320,6 +1334,43 @@ class PPTXTemplater {
|
|
|
1320
1334
|
return this
|
|
1321
1335
|
}
|
|
1322
1336
|
|
|
1337
|
+
updateDataLabels(chartId, options) {
|
|
1338
|
+
this.#assertLoaded()
|
|
1339
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1340
|
+
for (const idx of targetIndices) {
|
|
1341
|
+
this.#chartManager.updateDataLabels(
|
|
1342
|
+
idx,
|
|
1343
|
+
chartId,
|
|
1344
|
+
options,
|
|
1345
|
+
this.#slideManager,
|
|
1346
|
+
this.#relationshipManager
|
|
1347
|
+
)
|
|
1348
|
+
}
|
|
1349
|
+
return this
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
async getDataLabels(chartId, options = {}) {
|
|
1353
|
+
this.#assertLoaded()
|
|
1354
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1355
|
+
if (targetIndices.length === 0) return []
|
|
1356
|
+
const idx = targetIndices[0]
|
|
1357
|
+
return this.#chartManager.getDataLabels(
|
|
1358
|
+
idx,
|
|
1359
|
+
chartId,
|
|
1360
|
+
options,
|
|
1361
|
+
this.#slideManager,
|
|
1362
|
+
this.#relationshipManager
|
|
1363
|
+
)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async validateDataLabels(chartId, options = {}) {
|
|
1367
|
+
this.#assertLoaded()
|
|
1368
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1369
|
+
if (targetIndices.length === 0) return { valid: true, errors: [] }
|
|
1370
|
+
const idx = targetIndices[0]
|
|
1371
|
+
return ValidationEngine.validateDataLabels(this, idx, chartId, options)
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1323
1374
|
getCharts() {
|
|
1324
1375
|
this.#assertLoaded()
|
|
1325
1376
|
const targetIndices = this.#getTargetSlideIndices()
|
|
@@ -1332,6 +1383,46 @@ class PPTXTemplater {
|
|
|
1332
1383
|
return charts
|
|
1333
1384
|
}
|
|
1334
1385
|
|
|
1386
|
+
/**
|
|
1387
|
+
* Updates shape text or list content by placeholder tag or shape name/ID.
|
|
1388
|
+
* Supports bullet lists, numbered lists, nested lists, and custom styling.
|
|
1389
|
+
*
|
|
1390
|
+
* @param {string} tag - Placeholder tag (e.g. '{{name}}' or 'name') or shape name/ID.
|
|
1391
|
+
* @param {string|Object} data - String value or list configuration object.
|
|
1392
|
+
* @returns {PPTXTemplater} this (chainable)
|
|
1393
|
+
*/
|
|
1394
|
+
updateText(tag, data) {
|
|
1395
|
+
this.#assertLoaded()
|
|
1396
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1397
|
+
for (const idx of targetIndices) {
|
|
1398
|
+
this.#textManager.updateText(idx, tag, data, this.#slideManager, this.#templateEngine)
|
|
1399
|
+
}
|
|
1400
|
+
return this
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Retrieves list items from a shape or text box by name or placeholder tag.
|
|
1405
|
+
*
|
|
1406
|
+
* @param {string} tag - Shape name/ID or placeholder tag.
|
|
1407
|
+
* @returns {Array} Nested list structure of items.
|
|
1408
|
+
*/
|
|
1409
|
+
getList(tag) {
|
|
1410
|
+
this.#assertLoaded()
|
|
1411
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1412
|
+
const idx = targetIndices.length > 0 ? targetIndices[0] : 1
|
|
1413
|
+
return this.#textManager.getList(idx, tag, this.#slideManager)
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* Validates a list structure and values.
|
|
1418
|
+
*
|
|
1419
|
+
* @param {Object|Array} data - List config object or array of items.
|
|
1420
|
+
* @returns {Object} Report containing validation result.
|
|
1421
|
+
*/
|
|
1422
|
+
validateList(data) {
|
|
1423
|
+
return ValidationEngine.validateList(data)
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1335
1426
|
// === Text Features ===
|
|
1336
1427
|
replaceTextByTag(tag, value, options = {}) {
|
|
1337
1428
|
this.#assertLoaded()
|
|
@@ -80,6 +80,13 @@ class TemplateEngine {
|
|
|
80
80
|
|
|
81
81
|
// Step 2: Simple direct replacement for any remaining unfragmented placeholders
|
|
82
82
|
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
83
|
+
if (
|
|
84
|
+
value &&
|
|
85
|
+
typeof value === 'object' &&
|
|
86
|
+
(Array.isArray(value) || value.list !== undefined)
|
|
87
|
+
) {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
83
90
|
const escaped = this.#escapeXml(String(value))
|
|
84
91
|
const placeholderEscaped = this.#escapeXml(placeholder)
|
|
85
92
|
|
|
@@ -146,15 +153,35 @@ class TemplateEngine {
|
|
|
146
153
|
|
|
147
154
|
// Check if any placeholder appears in the combined text
|
|
148
155
|
let hasPlaceholder = false
|
|
156
|
+
let matchedPlaceholder = null
|
|
149
157
|
for (const placeholder of Object.keys(replacements)) {
|
|
150
158
|
if (combinedText.includes(placeholder)) {
|
|
151
159
|
hasPlaceholder = true
|
|
160
|
+
matchedPlaceholder = placeholder
|
|
152
161
|
break
|
|
153
162
|
}
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
if (!hasPlaceholder) return paragraphXml
|
|
157
166
|
|
|
167
|
+
const replacementValue = replacements[matchedPlaceholder]
|
|
168
|
+
const isList =
|
|
169
|
+
replacementValue &&
|
|
170
|
+
(Array.isArray(replacementValue) ||
|
|
171
|
+
(typeof replacementValue === 'object' && replacementValue.list !== undefined))
|
|
172
|
+
|
|
173
|
+
if (isList) {
|
|
174
|
+
const listConfig = Array.isArray(replacementValue)
|
|
175
|
+
? { list: replacementValue }
|
|
176
|
+
: replacementValue
|
|
177
|
+
const { ValidationEngine } = require('./ValidationEngine.js')
|
|
178
|
+
const validation = ValidationEngine.validateList(listConfig)
|
|
179
|
+
if (!validation.valid) {
|
|
180
|
+
throw new Error(`List validation failed: ${validation.errors.join(', ')}`)
|
|
181
|
+
}
|
|
182
|
+
return this.generateListParagraphs(paragraphXml, runs[0], listConfig)
|
|
183
|
+
}
|
|
184
|
+
|
|
158
185
|
// Perform replacement on combined text
|
|
159
186
|
let replacedText = combinedText
|
|
160
187
|
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
@@ -285,6 +312,105 @@ class TemplateEngine {
|
|
|
285
312
|
return Array.from(placeholders)
|
|
286
313
|
}
|
|
287
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Generates a block of list paragraph XML elements from a template paragraph,
|
|
317
|
+
* a baseline run, and list options.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} paragraphXml
|
|
320
|
+
* @param {Object} firstRun - Run XML info.
|
|
321
|
+
* @param {Object} listConfig - List styling and items.
|
|
322
|
+
* @returns {string} XML string of multiple paragraphs.
|
|
323
|
+
*/
|
|
324
|
+
generateListParagraphs(paragraphXml, firstRun, listConfig) {
|
|
325
|
+
const list = listConfig.list || []
|
|
326
|
+
const ordered = !!listConfig.ordered
|
|
327
|
+
const style = listConfig.style || {}
|
|
328
|
+
|
|
329
|
+
const flattenList = (items, currentLvl = 0) => {
|
|
330
|
+
let flat = []
|
|
331
|
+
for (const item of items) {
|
|
332
|
+
if (typeof item === 'string') {
|
|
333
|
+
flat.push({ text: item, lvl: currentLvl })
|
|
334
|
+
} else if (typeof item === 'object' && item !== null) {
|
|
335
|
+
const text = item.text || ''
|
|
336
|
+
flat.push({ text, lvl: currentLvl })
|
|
337
|
+
if (Array.isArray(item.children)) {
|
|
338
|
+
flat = flat.concat(flattenList(item.children, currentLvl + 1))
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return flat
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const flatItems = flattenList(list)
|
|
346
|
+
|
|
347
|
+
const rPrMatch = /(<a:rPr>[\s\S]*?<\/a:rPr>)/.exec(firstRun.xml)
|
|
348
|
+
let baseRPr = rPrMatch ? rPrMatch[1] : '<a:rPr/>'
|
|
349
|
+
|
|
350
|
+
if (style.fontSize) {
|
|
351
|
+
const szVal = Math.round(style.fontSize * 100)
|
|
352
|
+
if (/sz="\d+"/.test(baseRPr)) {
|
|
353
|
+
baseRPr = baseRPr.replace(/sz="\d+"/, `sz="${szVal}"`)
|
|
354
|
+
} else {
|
|
355
|
+
baseRPr = baseRPr.replace('<a:rPr', `<a:rPr sz="${szVal}"`)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (style.color) {
|
|
359
|
+
const cleanColor = style.color.replace('#', '')
|
|
360
|
+
const newFill = `<a:solidFill><a:srgbClr val="${cleanColor}"/></a:solidFill>`
|
|
361
|
+
if (/<a:solidFill>[\s\S]*?<\/a:solidFill>/.test(baseRPr)) {
|
|
362
|
+
baseRPr = baseRPr.replace(/<a:solidFill>[\s\S]*?<\/a:solidFill>/, newFill)
|
|
363
|
+
} else {
|
|
364
|
+
if (baseRPr.endsWith('/>')) {
|
|
365
|
+
baseRPr = baseRPr.replace('/>', `>${newFill}</a:rPr>`)
|
|
366
|
+
} else {
|
|
367
|
+
baseRPr = baseRPr.replace('</a:rPr>', `${newFill}</a:rPr>`)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (style.fontFamily) {
|
|
372
|
+
const latinXml = `<a:latin typeface="${style.fontFamily}"/><a:cs typeface="${style.fontFamily}"/>`
|
|
373
|
+
baseRPr = baseRPr.replace(/<a:latin\s+[^>]*\/>/g, '').replace(/<a:cs\s+[^>]*\/>/g, '')
|
|
374
|
+
if (baseRPr.endsWith('/>')) {
|
|
375
|
+
baseRPr = baseRPr.replace('/>', `>${latinXml}</a:rPr>`)
|
|
376
|
+
} else {
|
|
377
|
+
baseRPr = baseRPr.replace('</a:rPr>', `${latinXml}</a:rPr>`)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let paragraphsXml = ''
|
|
382
|
+
for (const item of flatItems) {
|
|
383
|
+
const lvl = item.lvl
|
|
384
|
+
const marL = style.marL !== undefined ? style.marL : 381000 + lvl * 457200
|
|
385
|
+
const indent = style.indent !== undefined ? style.indent : -228600
|
|
386
|
+
|
|
387
|
+
let pPr = `<a:pPr lvl="${lvl}" marL="${marL}" indent="${indent}">`
|
|
388
|
+
if (ordered) {
|
|
389
|
+
const numType = style.numberType || 'arabicPeriod'
|
|
390
|
+
pPr += `<a:buAutoNum type="${numType}"/>`
|
|
391
|
+
} else {
|
|
392
|
+
const bulletChar = style.bulletChar || '•'
|
|
393
|
+
pPr += `<a:buChar char="${bulletChar}"/>`
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (style.bulletColor) {
|
|
397
|
+
const cleanBClr = style.bulletColor.replace('#', '')
|
|
398
|
+
pPr += `<a:buClr><a:srgbClr val="${cleanBClr}"/></a:buClr>`
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (style.bulletSize) {
|
|
402
|
+
pPr += `<a:buSzPct val="${Math.round(style.bulletSize * 1000)}"/>`
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
pPr += `</a:pPr>`
|
|
406
|
+
|
|
407
|
+
const runXml = `<a:r>${baseRPr}<a:t>${this.#escapeXml(item.text)}</a:t></a:r>`
|
|
408
|
+
paragraphsXml += `<a:p>${pPr}${runXml}</a:p>`
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return paragraphsXml
|
|
412
|
+
}
|
|
413
|
+
|
|
288
414
|
/**
|
|
289
415
|
* Escapes XML special characters.
|
|
290
416
|
* @private
|