node-pptx-templater 1.0.10 → 1.0.12
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 +192 -0
- package/package.json +2 -2
- package/src/core/PPTXTemplater.js +157 -0
- package/src/core/ValidationEngine.js +170 -0
- package/src/managers/ChartManager.js +1431 -7
- package/src/managers/ShapeManager.js +89 -0
- package/src/managers/charts/ChartCacheGenerator.js +148 -59
package/README.md
CHANGED
|
@@ -373,6 +373,68 @@ Repairs common chart corruption issues such as broken caches, missing embedded w
|
|
|
373
373
|
ppt.useSlide(1).repairCharts();
|
|
374
374
|
```
|
|
375
375
|
|
|
376
|
+
#### `getChartLabelPositions(chartId)`
|
|
377
|
+
Retrieves the exact coordinate positions of all data labels for a chart on the active slide. Calculates absolute layout limits in EMUs (English Metric Units).
|
|
378
|
+
|
|
379
|
+
* **Arguments**:
|
|
380
|
+
* `chartId` (`string`):
|
|
381
|
+
* **Returns**: `Promise<Array<{series: string, category: string, seriesIndex: number, categoryIndex: number, value: number, x: number, y: number, width: number, height: number` -
|
|
382
|
+
|
|
383
|
+
```javascript
|
|
384
|
+
const positions = await ppt.useSlide(1).getChartLabelPositions('SalesChart');
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### `getChartBarPositions(chartId)`
|
|
388
|
+
Retrieves the exact coordinate positions of all bars/columns for a chart on the active slide. Calculates absolute layout limits in EMUs (English Metric Units).
|
|
389
|
+
|
|
390
|
+
* **Arguments**:
|
|
391
|
+
* `chartId` (`string`):
|
|
392
|
+
* **Returns**: `Promise<Array<{series: string, category: string, seriesIndex: number, categoryIndex: number, value: number, x: number, y: number, width: number, height: number` -
|
|
393
|
+
|
|
394
|
+
```javascript
|
|
395
|
+
const bars = await ppt.useSlide(1).getChartBarPositions('SalesChart');
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### `addTextAtPosition(options)`
|
|
399
|
+
Adds a textbox shape at a specific EMU coordinate position on targeted slides. Supports custom font styling and alignment configuration.
|
|
400
|
+
|
|
401
|
+
* **Arguments**:
|
|
402
|
+
* `options` (`Object`):
|
|
403
|
+
* `options.text` (`string`):
|
|
404
|
+
* `options.x` (`number`):
|
|
405
|
+
* `options.y` (`number`):
|
|
406
|
+
* `[options.width=1200000]` (`number`):
|
|
407
|
+
* `[options.height=300000]` (`number`):
|
|
408
|
+
* `[options.style]` (`Object`):
|
|
409
|
+
* **Returns**: `this` - The chainable presentation engine instance.
|
|
410
|
+
|
|
411
|
+
```javascript
|
|
412
|
+
ppt.useSlide(1).addTextAtPosition({
|
|
413
|
+
text: 'Label',
|
|
414
|
+
x: 1000000,
|
|
415
|
+
y: 1000000
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### `addTextNearChartLabel(options)`
|
|
420
|
+
Dynamically places textboxes next to a chart's data labels with vertical collision avoidance. Textboxes are positioned either on the left or right of the chart area, vertically aligned with their corresponding label.
|
|
421
|
+
|
|
422
|
+
* **Arguments**:
|
|
423
|
+
* `options` (`Object`):
|
|
424
|
+
* `options.chart` (`string`):
|
|
425
|
+
* `options.text` (`string|Function`):
|
|
426
|
+
* `[options.position='left']` (`'left'|'right'`):
|
|
427
|
+
* `[options.style]` (`Object`):
|
|
428
|
+
* **Returns**: `this` - The chainable presentation engine instance.
|
|
429
|
+
|
|
430
|
+
```javascript
|
|
431
|
+
ppt.addTextNearChartLabel({
|
|
432
|
+
chart: 'SalesChart',
|
|
433
|
+
text: 'Series',
|
|
434
|
+
position: 'left'
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
376
438
|
#### `updateChartData(())`
|
|
377
439
|
Delegates core actions to slide element sub-managers.
|
|
378
440
|
|
|
@@ -446,6 +508,31 @@ const result = await ppt.useSlide(1).validateDataLabels('SalesChart', {
|
|
|
446
508
|
console.log(result.valid);
|
|
447
509
|
```
|
|
448
510
|
|
|
511
|
+
#### `validateChartLabels(())`
|
|
512
|
+
Delegates core actions to slide element sub-managers.
|
|
513
|
+
|
|
514
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
515
|
+
|
|
516
|
+
```javascript
|
|
517
|
+
const result = await ppt.useSlide(1).validateChartLabels('SalesChart', {
|
|
518
|
+
labels: ['High', 'Low']
|
|
519
|
+
});
|
|
520
|
+
console.log(result.valid);
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
#### `validateSeriesNameLabels(())`
|
|
524
|
+
Delegates core actions to slide element sub-managers.
|
|
525
|
+
|
|
526
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
527
|
+
|
|
528
|
+
```javascript
|
|
529
|
+
const result = await ppt.useSlide(1).validateSeriesNameLabels('SalesChart', {
|
|
530
|
+
enabled: true,
|
|
531
|
+
position: 'left'
|
|
532
|
+
});
|
|
533
|
+
console.log(result.valid);
|
|
534
|
+
```
|
|
535
|
+
|
|
449
536
|
#### `getCharts(())`
|
|
450
537
|
Delegates core actions to slide element sub-managers.
|
|
451
538
|
|
|
@@ -836,6 +923,38 @@ ppt.useSlide(1).getImages(());
|
|
|
836
923
|
|
|
837
924
|
### Shapes API
|
|
838
925
|
|
|
926
|
+
#### `updateShapePosition(shapeId, options = {})`
|
|
927
|
+
Updates the position and/or dimensions of an existing shape on targeted slides.
|
|
928
|
+
|
|
929
|
+
* **Arguments**:
|
|
930
|
+
* `shapeId` (`string`):
|
|
931
|
+
* `options` (`Object`):
|
|
932
|
+
* `[options.x]` (`number`):
|
|
933
|
+
* `[options.y]` (`number`):
|
|
934
|
+
* `[options.width]` (`number`):
|
|
935
|
+
* `[options.height]` (`number`):
|
|
936
|
+
* **Returns**: `this` - The chainable presentation engine instance.
|
|
937
|
+
|
|
938
|
+
```javascript
|
|
939
|
+
ppt.useSlide(1).updateShapePosition('TitleShape', { x: 1000000, y: 1500000 });
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
#### `updateTextBoxPosition(textBoxId, options = {})`
|
|
943
|
+
Updates the position and/or dimensions of an existing textbox on targeted slides.
|
|
944
|
+
|
|
945
|
+
* **Arguments**:
|
|
946
|
+
* `textBoxId` (`string`):
|
|
947
|
+
* `options` (`Object`):
|
|
948
|
+
* `[options.x]` (`number`):
|
|
949
|
+
* `[options.y]` (`number`):
|
|
950
|
+
* `[options.width]` (`number`):
|
|
951
|
+
* `[options.height]` (`number`):
|
|
952
|
+
* **Returns**: `this` - The chainable presentation engine instance.
|
|
953
|
+
|
|
954
|
+
```javascript
|
|
955
|
+
ppt.useSlide(1).updateTextBoxPosition('TextBox 2', { x: 1000000, y: 1500000 });
|
|
956
|
+
```
|
|
957
|
+
|
|
839
958
|
#### `updateShapeText(())`
|
|
840
959
|
Delegates core actions to slide element sub-managers.
|
|
841
960
|
|
|
@@ -1416,6 +1535,79 @@ ppt.useSlide(1).updateChart('RevenueChart', {
|
|
|
1416
1535
|
|
|
1417
1536
|
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
1537
|
|
|
1538
|
+
### 7. Label Style Inheritance & Series Names Inside Bars
|
|
1539
|
+
|
|
1540
|
+
#### Style Inheritance
|
|
1541
|
+
When custom labels (inline or via `updateDataLabels`) are generated, they inherit the styling properties (font family, font size, bold, italic, color, and alignment) defined in the template's `<c:txPr>` tag for the series. This ensures your custom labels match the branding and layout design from your PowerPoint template file.
|
|
1542
|
+
|
|
1543
|
+
#### Series Names Inside Bars (`showSeriesNameInBar`)
|
|
1544
|
+
For stacked bar charts, you can show the series name inside each segment (typically centered) to make them easily readable. To enable this, set `showSeriesNameInBar: true` in the chart options (globally) or on individual series:
|
|
1545
|
+
|
|
1546
|
+
```javascript
|
|
1547
|
+
ppt.useSlide(1).updateChart('RevenueChart', {
|
|
1548
|
+
showSeriesNameInBar: true, // Show series name labels globally
|
|
1549
|
+
categories: ['Q1', 'Q2', 'Q3', 'Q4'],
|
|
1550
|
+
series: [
|
|
1551
|
+
{ name: 'Product A', values: [100, 200, 300, 400] },
|
|
1552
|
+
{ name: 'Product B', values: [150, 250, 350, 450], showSeriesNameInBar: true } // Or per-series
|
|
1553
|
+
]
|
|
1554
|
+
});
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
#### Chart Labels Validation
|
|
1558
|
+
You can programmatically validate that the chart labels configured in a chart conform to your template structure:
|
|
1559
|
+
|
|
1560
|
+
```javascript
|
|
1561
|
+
const report = await ppt.useSlide(1).validateChartLabels('RevenueChart', {
|
|
1562
|
+
labels: ['Custom label 1', 'Custom label 2'],
|
|
1563
|
+
showSeriesNameInBar: true
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
if (!report.valid) {
|
|
1567
|
+
console.log('Errors:', report.errors);
|
|
1568
|
+
console.log('Warnings:', report.warnings);
|
|
1569
|
+
}
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
#### External Series Name Labels (`seriesNameLabels`)
|
|
1573
|
+
You can position the series names outside the chart area as separate text boxes (`<p:sp>`) aligned with each corresponding bar or stack. These external labels automatically inherit the styling (font family, font size, bold, italic, color) defined in the template's series properties.
|
|
1574
|
+
|
|
1575
|
+
Supported features:
|
|
1576
|
+
* **Position**: Can be set to `'left'` or `'right'` to align text boxes to the left or right of the chart.
|
|
1577
|
+
* **Auto-fit & Height Wrapping**: Automatically wraps text and calculates height if labels are longer than the available margin space.
|
|
1578
|
+
* **Collision Detection**: Prevent overlapping with slide bounds by shrinking the chart, and resolve vertical overlaps of labels by shifting them apart.
|
|
1579
|
+
* **Idempotency**: Safely clean up previous labels on multiple chart updates.
|
|
1580
|
+
|
|
1581
|
+
Example usage:
|
|
1582
|
+
```javascript
|
|
1583
|
+
ppt.useSlide(1).updateChart('RevenueChart', {
|
|
1584
|
+
categories: ['Q1', 'Q2', 'Q3', 'Q4'],
|
|
1585
|
+
series: [
|
|
1586
|
+
{ name: 'Product A', values: [100, 200, 300, 400] },
|
|
1587
|
+
{ name: 'Product B', values: [150, 250, 350, 450] }
|
|
1588
|
+
],
|
|
1589
|
+
seriesNameLabels: {
|
|
1590
|
+
enabled: true,
|
|
1591
|
+
position: 'left', // 'left' or 'right'
|
|
1592
|
+
autoFit: true // Automatically wrap and shrink layout if needed (default: true)
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
```
|
|
1596
|
+
|
|
1597
|
+
To validate your configuration and check for boundary or collision warning/errors:
|
|
1598
|
+
```javascript
|
|
1599
|
+
const report = await ppt.useSlide(1).validateSeriesNameLabels('RevenueChart', {
|
|
1600
|
+
enabled: true,
|
|
1601
|
+
position: 'left',
|
|
1602
|
+
autoFit: true
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
if (!report.valid) {
|
|
1606
|
+
console.error('Validation errors:', report.errors);
|
|
1607
|
+
console.warn('Validation warnings:', report.warnings);
|
|
1608
|
+
}
|
|
1609
|
+
```
|
|
1610
|
+
|
|
1419
1611
|
---
|
|
1420
1612
|
|
|
1421
1613
|
## 📋 Native Lists (Bullet & Numbered Lists)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
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
|
+
}
|
|
@@ -1371,6 +1371,34 @@ class PPTXTemplater {
|
|
|
1371
1371
|
return ValidationEngine.validateDataLabels(this, idx, chartId, options)
|
|
1372
1372
|
}
|
|
1373
1373
|
|
|
1374
|
+
async validateChartLabels(chartId, options = {}) {
|
|
1375
|
+
this.#assertLoaded()
|
|
1376
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1377
|
+
if (targetIndices.length === 0) return { valid: true, errors: [], warnings: [] }
|
|
1378
|
+
const idx = targetIndices[0]
|
|
1379
|
+
return this.#chartManager.validateChartLabels(
|
|
1380
|
+
idx,
|
|
1381
|
+
chartId,
|
|
1382
|
+
options,
|
|
1383
|
+
this.#slideManager,
|
|
1384
|
+
this.#relationshipManager
|
|
1385
|
+
)
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
async validateSeriesNameLabels(chartId, options = {}) {
|
|
1389
|
+
this.#assertLoaded()
|
|
1390
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1391
|
+
if (targetIndices.length === 0) return { valid: true, errors: [], warnings: [] }
|
|
1392
|
+
const idx = targetIndices[0]
|
|
1393
|
+
return this.#chartManager.validateSeriesNameLabels(
|
|
1394
|
+
idx,
|
|
1395
|
+
chartId,
|
|
1396
|
+
options,
|
|
1397
|
+
this.#slideManager,
|
|
1398
|
+
this.#relationshipManager
|
|
1399
|
+
)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1374
1402
|
getCharts() {
|
|
1375
1403
|
this.#assertLoaded()
|
|
1376
1404
|
const targetIndices = this.#getTargetSlideIndices()
|
|
@@ -1383,6 +1411,95 @@ class PPTXTemplater {
|
|
|
1383
1411
|
return charts
|
|
1384
1412
|
}
|
|
1385
1413
|
|
|
1414
|
+
/**
|
|
1415
|
+
* Retrieves the exact coordinate positions of all data labels for a chart on the active slide.
|
|
1416
|
+
* Calculates absolute layout limits in EMUs (English Metric Units).
|
|
1417
|
+
*
|
|
1418
|
+
* @param {string} chartId The unique chart name/id in the template slide.
|
|
1419
|
+
* @returns {Promise<Array<{series: string, category: string, seriesIndex: number, categoryIndex: number, value: number, x: number, y: number, width: number, height: number}>>} An array of data label geometry objects.
|
|
1420
|
+
*/
|
|
1421
|
+
async getChartLabelPositions(chartId) {
|
|
1422
|
+
this.#assertLoaded()
|
|
1423
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1424
|
+
if (targetIndices.length === 0) return []
|
|
1425
|
+
const idx = targetIndices[0]
|
|
1426
|
+
return this.#chartManager.getChartLabelPositions(
|
|
1427
|
+
idx,
|
|
1428
|
+
chartId,
|
|
1429
|
+
this.#slideManager,
|
|
1430
|
+
this.#relationshipManager
|
|
1431
|
+
)
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Retrieves the exact coordinate positions of all bars/columns for a chart on the active slide.
|
|
1436
|
+
* Calculates absolute layout limits in EMUs (English Metric Units).
|
|
1437
|
+
*
|
|
1438
|
+
* @param {string} chartId The unique chart name/id in the template slide.
|
|
1439
|
+
* @returns {Promise<Array<{series: string, category: string, seriesIndex: number, categoryIndex: number, value: number, x: number, y: number, width: number, height: number}>>} An array of bar geometry objects.
|
|
1440
|
+
*/
|
|
1441
|
+
async getChartBarPositions(chartId) {
|
|
1442
|
+
this.#assertLoaded()
|
|
1443
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1444
|
+
if (targetIndices.length === 0) return []
|
|
1445
|
+
const idx = targetIndices[0]
|
|
1446
|
+
return this.#chartManager.getChartBarPositions(
|
|
1447
|
+
idx,
|
|
1448
|
+
chartId,
|
|
1449
|
+
this.#slideManager,
|
|
1450
|
+
this.#relationshipManager
|
|
1451
|
+
)
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Adds a textbox shape at a specific EMU coordinate position on targeted slides.
|
|
1456
|
+
* Supports custom font styling and alignment configuration.
|
|
1457
|
+
*
|
|
1458
|
+
* @param {Object} options Textbox positioning and style configuration.
|
|
1459
|
+
* @param {string} options.text Text content to insert in the textbox.
|
|
1460
|
+
* @param {number} options.x Bounding box X offset coordinate (in EMUs).
|
|
1461
|
+
* @param {number} options.y Bounding box Y offset coordinate (in EMUs).
|
|
1462
|
+
* @param {number} [options.width=1200000] Bounding box width (in EMUs).
|
|
1463
|
+
* @param {number} [options.height=300000] Bounding box height (in EMUs).
|
|
1464
|
+
* @param {Object} [options.style] Font formatting properties (fontSize, fontFamily, color, bold, italic, align).
|
|
1465
|
+
* @returns {this} The chainable presentation engine instance.
|
|
1466
|
+
*/
|
|
1467
|
+
addTextAtPosition(options) {
|
|
1468
|
+
this.#assertLoaded()
|
|
1469
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1470
|
+
for (const idx of targetIndices) {
|
|
1471
|
+
const p = this.#chartManager.addTextAtPosition(idx, options, this.#slideManager)
|
|
1472
|
+
this.#zipManager.addPendingPromise(p)
|
|
1473
|
+
}
|
|
1474
|
+
return this
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Dynamically places textboxes next to a chart's data labels with vertical collision avoidance.
|
|
1479
|
+
* Textboxes are positioned either on the left or right of the chart area, vertically aligned with their corresponding label.
|
|
1480
|
+
*
|
|
1481
|
+
* @param {Object} options Text alignment, naming, and position configuration.
|
|
1482
|
+
* @param {string} options.chart The target chart name/id.
|
|
1483
|
+
* @param {string|Function} options.text Static label text or a callback function receiving `({ series, category, value })`.
|
|
1484
|
+
* @param {'left'|'right'} [options.position='left'] Alignment position relative to the chart boundaries.
|
|
1485
|
+
* @param {Object} [options.style] Text styling attributes (fontSize, fontFamily, color, bold, italic, align, autoFit).
|
|
1486
|
+
* @returns {this} The chainable presentation engine instance.
|
|
1487
|
+
*/
|
|
1488
|
+
addTextNearChartLabel(options) {
|
|
1489
|
+
this.#assertLoaded()
|
|
1490
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1491
|
+
for (const idx of targetIndices) {
|
|
1492
|
+
const p = this.#chartManager.addTextNearChartLabel(
|
|
1493
|
+
idx,
|
|
1494
|
+
options,
|
|
1495
|
+
this.#slideManager,
|
|
1496
|
+
this.#relationshipManager
|
|
1497
|
+
)
|
|
1498
|
+
this.#zipManager.addPendingPromise(p)
|
|
1499
|
+
}
|
|
1500
|
+
return this
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1386
1503
|
/**
|
|
1387
1504
|
* Updates shape text or list content by placeholder tag or shape name/ID.
|
|
1388
1505
|
* Supports bullet lists, numbered lists, nested lists, and custom styling.
|
|
@@ -1485,6 +1602,46 @@ class PPTXTemplater {
|
|
|
1485
1602
|
return this
|
|
1486
1603
|
}
|
|
1487
1604
|
|
|
1605
|
+
/**
|
|
1606
|
+
* Updates the position and/or dimensions of an existing shape on targeted slides.
|
|
1607
|
+
*
|
|
1608
|
+
* @param {string} shapeId The unique shape name/id in the template slide.
|
|
1609
|
+
* @param {Object} options Positioning and styling dimensions config.
|
|
1610
|
+
* @param {number} [options.x] Absolute X offset coordinate (in EMUs).
|
|
1611
|
+
* @param {number} [options.y] Absolute Y offset coordinate (in EMUs).
|
|
1612
|
+
* @param {number} [options.width] Bounding box width (in EMUs).
|
|
1613
|
+
* @param {number} [options.height] Bounding box height (in EMUs).
|
|
1614
|
+
* @returns {this} The chainable presentation engine instance.
|
|
1615
|
+
*/
|
|
1616
|
+
updateShapePosition(shapeId, options = {}) {
|
|
1617
|
+
this.#assertLoaded()
|
|
1618
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1619
|
+
for (const idx of targetIndices) {
|
|
1620
|
+
this.#shapeManager.updateShapePosition(idx, shapeId, options, this.#slideManager)
|
|
1621
|
+
}
|
|
1622
|
+
return this
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Updates the position and/or dimensions of an existing textbox on targeted slides.
|
|
1627
|
+
*
|
|
1628
|
+
* @param {string} textBoxId The unique textbox shape name/id in the template slide.
|
|
1629
|
+
* @param {Object} options Positioning and styling dimensions config.
|
|
1630
|
+
* @param {number} [options.x] Absolute X offset coordinate (in EMUs).
|
|
1631
|
+
* @param {number} [options.y] Absolute Y offset coordinate (in EMUs).
|
|
1632
|
+
* @param {number} [options.width] Bounding box width (in EMUs).
|
|
1633
|
+
* @param {number} [options.height] Bounding box height (in EMUs).
|
|
1634
|
+
* @returns {this} The chainable presentation engine instance.
|
|
1635
|
+
*/
|
|
1636
|
+
updateTextBoxPosition(textBoxId, options = {}) {
|
|
1637
|
+
this.#assertLoaded()
|
|
1638
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1639
|
+
for (const idx of targetIndices) {
|
|
1640
|
+
this.#shapeManager.updateTextBoxPosition(idx, textBoxId, options, this.#slideManager)
|
|
1641
|
+
}
|
|
1642
|
+
return this
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1488
1645
|
cloneShape(shapeId, newShapeId, options = {}) {
|
|
1489
1646
|
this.#assertLoaded()
|
|
1490
1647
|
const targetIndices = this.#getTargetSlideIndices()
|
|
@@ -497,6 +497,176 @@ class ValidationEngine {
|
|
|
497
497
|
warnings,
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Validates chart labels for stacked bar charts.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} xml - Chart XML string.
|
|
505
|
+
* @param {Object} options - Validation options.
|
|
506
|
+
* @returns {Object} report
|
|
507
|
+
*/
|
|
508
|
+
static validateChartLabels(xml, options = {}) {
|
|
509
|
+
const errors = []
|
|
510
|
+
const warnings = []
|
|
511
|
+
|
|
512
|
+
if (!xml) {
|
|
513
|
+
errors.push('Chart XML must be provided')
|
|
514
|
+
return { valid: false, errors, warnings }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Check chart type is stacked bar
|
|
518
|
+
const isBarChart = xml.includes('c:barChart')
|
|
519
|
+
const isStacked = xml.includes('val="stacked"') || xml.includes('val="percentStacked"')
|
|
520
|
+
if (!isBarChart || !isStacked) {
|
|
521
|
+
warnings.push(
|
|
522
|
+
'Chart is not a stacked bar chart (expected <c:barChart> with stacked grouping)'
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Verify label count consistency if options.labels is provided
|
|
527
|
+
let ptsCount = 0
|
|
528
|
+
const catMatch = /<c:cat>([\s\S]*?)<\/c:cat>/.exec(xml)
|
|
529
|
+
const valMatch = /<c:val>([\s\S]*?)<\/c:val>/.exec(xml)
|
|
530
|
+
const targetBlock = catMatch ? catMatch[1] : valMatch ? valMatch[1] : ''
|
|
531
|
+
const ptCountMatch = /<c:ptCount val="(\d+)"\/>/.exec(targetBlock)
|
|
532
|
+
if (ptCountMatch) {
|
|
533
|
+
ptsCount = parseInt(ptCountMatch[1], 10)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (options.labels) {
|
|
537
|
+
if (ptsCount > 0 && options.labels.length !== ptsCount) {
|
|
538
|
+
errors.push(
|
|
539
|
+
`Label count (${options.labels.length}) does not match chart data points count (${ptsCount})`
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Check series name availability (meaning if showSeriesNameInBar is requested, does the chart have series names?)
|
|
545
|
+
if (options.showSeriesNameInBar) {
|
|
546
|
+
const hasSeriesName = xml.includes('<c:tx>') || xml.includes('<c:f>')
|
|
547
|
+
if (!hasSeriesName) {
|
|
548
|
+
warnings.push('Series name might not be available or defined in the template')
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Check template style availability (dLbls, txPr, etc.)
|
|
553
|
+
const hasDLbls = xml.includes('<c:dLbls>')
|
|
554
|
+
const hasTxPr = xml.includes('<c:txPr>')
|
|
555
|
+
if (!hasDLbls) {
|
|
556
|
+
warnings.push('Template does not contain default data labels (<c:dLbls>)')
|
|
557
|
+
} else if (!hasTxPr) {
|
|
558
|
+
warnings.push('Template data labels do not have styling properties (<c:txPr>)')
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
valid: errors.length === 0,
|
|
563
|
+
errors,
|
|
564
|
+
warnings,
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Validates series name labels configuration.
|
|
570
|
+
*
|
|
571
|
+
* @param {string} xml - Chart XML string.
|
|
572
|
+
* @param {Object} xfrm - Chart graphic frame xfrm.
|
|
573
|
+
* @param {Object} options - Configuration options.
|
|
574
|
+
* @returns {Object} report
|
|
575
|
+
*/
|
|
576
|
+
static validateSeriesNameLabels(xml, xfrm, options = {}) {
|
|
577
|
+
const errors = []
|
|
578
|
+
const warnings = []
|
|
579
|
+
|
|
580
|
+
if (!options || !options.enabled) {
|
|
581
|
+
return { valid: true, errors, warnings }
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const { position } = options
|
|
585
|
+
const allowedPositions = ['left', 'right']
|
|
586
|
+
if (!position || !allowedPositions.includes(position)) {
|
|
587
|
+
errors.push(`Invalid position "${position}". Only "left" and "right" are supported.`)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (options.style) {
|
|
591
|
+
if (typeof options.style !== 'object' || Array.isArray(options.style)) {
|
|
592
|
+
errors.push('style must be a key-value object')
|
|
593
|
+
} else {
|
|
594
|
+
const { fontSize, bold, italic, color, fontFamily, align } = options.style
|
|
595
|
+
if (fontSize !== undefined && (typeof fontSize !== 'number' || fontSize <= 0)) {
|
|
596
|
+
errors.push('style.fontSize must be a positive number')
|
|
597
|
+
}
|
|
598
|
+
if (bold !== undefined && typeof bold !== 'boolean') {
|
|
599
|
+
errors.push('style.bold must be a boolean')
|
|
600
|
+
}
|
|
601
|
+
if (italic !== undefined && typeof italic !== 'boolean') {
|
|
602
|
+
errors.push('style.italic must be a boolean')
|
|
603
|
+
}
|
|
604
|
+
if (color !== undefined && typeof color !== 'string') {
|
|
605
|
+
errors.push('style.color must be a string')
|
|
606
|
+
} else if (color !== undefined) {
|
|
607
|
+
const cleanColor = color.replace('#', '').trim()
|
|
608
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(cleanColor)) {
|
|
609
|
+
errors.push(`style.color "${color}" is not a valid hex color (e.g. "#FF0000")`)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (fontFamily !== undefined && typeof fontFamily !== 'string') {
|
|
613
|
+
errors.push('style.fontFamily must be a string')
|
|
614
|
+
}
|
|
615
|
+
if (align !== undefined && !['left', 'right', 'center'].includes(align)) {
|
|
616
|
+
errors.push(
|
|
617
|
+
`Invalid style.align "${align}". Supported alignments are "left", "right", "center"`
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!xml) {
|
|
624
|
+
errors.push('Chart XML must be provided')
|
|
625
|
+
return { valid: false, errors, warnings }
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!xfrm) {
|
|
629
|
+
errors.push('Chart coordinates (xfrm) must be resolved')
|
|
630
|
+
return { valid: false, errors, warnings }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Check plot area layout
|
|
634
|
+
const plotAreaMatch = /<c:plotArea>([\s\S]*?)<\/c:plotArea>/.exec(xml)
|
|
635
|
+
if (!plotAreaMatch) {
|
|
636
|
+
errors.push('Plot area not detected in chart XML')
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Verify slide boundary collision
|
|
640
|
+
if (position === 'left') {
|
|
641
|
+
if (xfrm.left <= 0) {
|
|
642
|
+
errors.push(
|
|
643
|
+
`Chart left boundary (${xfrm.left}) is at or outside slide bounds, labels on the left cannot fit`
|
|
644
|
+
)
|
|
645
|
+
} else if (xfrm.left < 500000) {
|
|
646
|
+
warnings.push(
|
|
647
|
+
`Chart left boundary (${xfrm.left}) is very close to slide edge, labels on the left might clip`
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
} else if (position === 'right') {
|
|
651
|
+
const chartRight = xfrm.left + xfrm.width
|
|
652
|
+
const slideWidth = 12192000
|
|
653
|
+
if (chartRight >= slideWidth) {
|
|
654
|
+
errors.push(
|
|
655
|
+
`Chart right boundary (${chartRight}) is at or outside slide bounds, labels on the right cannot fit`
|
|
656
|
+
)
|
|
657
|
+
} else if (slideWidth - chartRight < 500000) {
|
|
658
|
+
warnings.push(
|
|
659
|
+
`Chart right boundary (${chartRight}) is very close to slide edge, labels on the right might clip`
|
|
660
|
+
)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
valid: errors.length === 0,
|
|
666
|
+
errors,
|
|
667
|
+
warnings,
|
|
668
|
+
}
|
|
669
|
+
}
|
|
500
670
|
}
|
|
501
671
|
|
|
502
672
|
module.exports = { ValidationEngine }
|