easy-template-x 6.0.0 → 6.1.0

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.
Files changed (39) hide show
  1. package/dist/cjs/easy-template-x.cjs +176 -96
  2. package/dist/es/easy-template-x.mjs +173 -95
  3. package/dist/types/errors/index.d.ts +4 -2
  4. package/dist/types/errors/internalArgumentMissingError.d.ts +5 -0
  5. package/dist/types/errors/internalError.d.ts +3 -0
  6. package/dist/types/errors/missingCloseDelimiterError.d.ts +2 -1
  7. package/dist/types/errors/missingStartDelimiterError.d.ts +2 -1
  8. package/dist/types/errors/tagOptionsParseError.d.ts +2 -1
  9. package/dist/types/errors/templateDataError.d.ts +3 -0
  10. package/dist/types/errors/templateSyntaxError.d.ts +3 -0
  11. package/dist/types/errors/unclosedTagError.d.ts +2 -1
  12. package/dist/types/errors/unknownContentTypeError.d.ts +2 -1
  13. package/dist/types/errors/unopenedTagError.d.ts +2 -1
  14. package/dist/types/plugins/chart/chartData.d.ts +5 -3
  15. package/dist/types/plugins/chart/chartDataValidation.d.ts +2 -0
  16. package/dist/types/plugins/loop/strategy/loopTableColumnsStrategy.d.ts +3 -1
  17. package/package.json +1 -1
  18. package/src/compilation/tagParser.ts +2 -2
  19. package/src/errors/index.ts +4 -2
  20. package/src/errors/{missingArgumentError.ts → internalArgumentMissingError.ts} +2 -2
  21. package/src/errors/internalError.ts +6 -0
  22. package/src/errors/missingCloseDelimiterError.ts +3 -1
  23. package/src/errors/missingStartDelimiterError.ts +3 -1
  24. package/src/errors/tagOptionsParseError.ts +3 -1
  25. package/src/errors/{argumentError.ts → templateDataError.ts} +1 -1
  26. package/src/errors/templateSyntaxError.ts +6 -0
  27. package/src/errors/unclosedTagError.ts +3 -1
  28. package/src/errors/unknownContentTypeError.ts +3 -1
  29. package/src/errors/unopenedTagError.ts +3 -1
  30. package/src/plugins/chart/chartData.ts +28 -3
  31. package/src/plugins/chart/chartDataValidation.ts +103 -0
  32. package/src/plugins/chart/chartPlugin.ts +2 -2
  33. package/src/plugins/chart/updateChart.ts +9 -45
  34. package/src/plugins/image/imagePlugin.ts +2 -2
  35. package/src/plugins/loop/strategy/loopTableColumnsStrategy.ts +30 -18
  36. package/src/xml/xml.ts +24 -18
  37. package/src/zip/jsZipHelper.ts +2 -2
  38. package/dist/types/errors/argumentError.d.ts +0 -3
  39. package/dist/types/errors/missingArgumentError.d.ts +0 -5
@@ -5,9 +5,16 @@ var xmldom = require('@xmldom/xmldom');
5
5
  var getProp = require('lodash.get');
6
6
  var JSON5 = require('json5');
7
7
 
8
- class ArgumentError extends Error {
8
+ class InternalError extends Error {
9
9
  constructor(message) {
10
- super(message);
10
+ super(`Internal error: ${message}`);
11
+ }
12
+ }
13
+
14
+ class InternalArgumentMissingError extends InternalError {
15
+ constructor(argName) {
16
+ super(`Argument '${argName}' is missing.`);
17
+ this.argName = argName;
11
18
  }
12
19
  }
13
20
 
@@ -25,28 +32,27 @@ class MaxXmlDepthError extends Error {
25
32
  }
26
33
  }
27
34
 
28
- class MissingArgumentError extends ArgumentError {
29
- constructor(argName) {
30
- super(`Argument '${argName}' is missing.`);
31
- this.argName = argName;
35
+ class TemplateSyntaxError extends Error {
36
+ constructor(message) {
37
+ super(message);
32
38
  }
33
39
  }
34
40
 
35
- class MissingCloseDelimiterError extends Error {
41
+ class MissingCloseDelimiterError extends TemplateSyntaxError {
36
42
  constructor(openDelimiterText) {
37
43
  super(`Close delimiter is missing from '${openDelimiterText}'.`);
38
44
  this.openDelimiterText = openDelimiterText;
39
45
  }
40
46
  }
41
47
 
42
- class MissingStartDelimiterError extends Error {
48
+ class MissingStartDelimiterError extends TemplateSyntaxError {
43
49
  constructor(closeDelimiterText) {
44
50
  super(`Open delimiter is missing from '${closeDelimiterText}'.`);
45
51
  this.closeDelimiterText = closeDelimiterText;
46
52
  }
47
53
  }
48
54
 
49
- class TagOptionsParseError extends Error {
55
+ class TagOptionsParseError extends TemplateSyntaxError {
50
56
  constructor(tagRawText, parseError) {
51
57
  super(`Failed to parse tag options of '${tagRawText}': ${parseError.message}.`);
52
58
  this.tagRawText = tagRawText;
@@ -54,7 +60,13 @@ class TagOptionsParseError extends Error {
54
60
  }
55
61
  }
56
62
 
57
- class UnclosedTagError extends Error {
63
+ class TemplateDataError extends Error {
64
+ constructor(message) {
65
+ super(message);
66
+ }
67
+ }
68
+
69
+ class UnclosedTagError extends TemplateSyntaxError {
58
70
  constructor(tagName) {
59
71
  super(`Tag '${tagName}' is never closed.`);
60
72
  this.tagName = tagName;
@@ -67,7 +79,7 @@ class UnidentifiedFileTypeError extends Error {
67
79
  }
68
80
  }
69
81
 
70
- class UnknownContentTypeError extends Error {
82
+ class UnknownContentTypeError extends TemplateDataError {
71
83
  constructor(contentType, tagRawText, path) {
72
84
  super(`Content type '${contentType}' does not have a registered plugin to handle it.`);
73
85
  this.contentType = contentType;
@@ -76,7 +88,7 @@ class UnknownContentTypeError extends Error {
76
88
  }
77
89
  }
78
90
 
79
- class UnopenedTagError extends Error {
91
+ class UnopenedTagError extends TemplateSyntaxError {
80
92
  constructor(tagName) {
81
93
  super(`Tag '${tagName}' is closed but was never opened.`);
82
94
  this.tagName = tagName;
@@ -395,7 +407,7 @@ function normalizeDoubleQuotes(text) {
395
407
 
396
408
  class JsZipHelper {
397
409
  static toJsZipOutputType(binaryOrType) {
398
- if (!binaryOrType) throw new MissingArgumentError("binaryOrType");
410
+ if (!binaryOrType) throw new InternalArgumentMissingError("binaryOrType");
399
411
  let binaryType;
400
412
  if (typeof binaryOrType === 'function') {
401
413
  binaryType = binaryOrType;
@@ -493,13 +505,19 @@ class Parser {
493
505
  * handles xml namespaces more forgivingly (required mainly by the
494
506
  * RawXmlPlugin).
495
507
  */
496
- static parser = new xmldom.DOMParser();
508
+ static parser = new xmldom.DOMParser({
509
+ errorHandler: {
510
+ // Ignore xmldom warnings. They are often incorrect since we are
511
+ // parsing OOXML, not HTML.
512
+ warning: () => {}
513
+ }
514
+ });
497
515
  parse(str) {
498
516
  const doc = this.domParse(str);
499
517
  return xml.create.fromDomNode(doc.documentElement);
500
518
  }
501
519
  domParse(str) {
502
- if (str === null || str === undefined) throw new MissingArgumentError("str");
520
+ if (str === null || str === undefined) throw new InternalArgumentMissingError("str");
503
521
  return Parser.parser.parseFromString(str, "text/xml");
504
522
  }
505
523
 
@@ -509,7 +527,7 @@ class Parser {
509
527
  * https://stackoverflow.com/questions/7918868/how-to-escape-xml-entities-in-javascript
510
528
  */
511
529
  encodeValue(str) {
512
- if (str === null || str === undefined) throw new MissingArgumentError("str");
530
+ if (str === null || str === undefined) throw new InternalArgumentMissingError("str");
513
531
  if (typeof str !== 'string') throw new TypeError(`Expected a string, got '${str.constructor.name}'.`);
514
532
  return str.replace(/[<>&'"]/g, c => {
515
533
  switch (c) {
@@ -595,7 +613,7 @@ class Create {
595
613
  };
596
614
  }
597
615
  cloneNode(node, deep) {
598
- if (!node) throw new MissingArgumentError("node");
616
+ if (!node) throw new InternalArgumentMissingError("node");
599
617
  if (!deep) {
600
618
  const clone = Object.assign({}, node);
601
619
  clone.parentNode = null;
@@ -748,8 +766,8 @@ let Query$1 = class Query {
748
766
  * Returns all siblings between 'firstNode' and 'lastNode' inclusive.
749
767
  */
750
768
  siblingsInRange(firstNode, lastNode) {
751
- if (!firstNode) throw new MissingArgumentError("firstNode");
752
- if (!lastNode) throw new MissingArgumentError("lastNode");
769
+ if (!firstNode) throw new InternalArgumentMissingError("firstNode");
770
+ if (!lastNode) throw new InternalArgumentMissingError("lastNode");
753
771
  const range = [];
754
772
  let curNode = firstNode;
755
773
  while (curNode && curNode !== lastNode) {
@@ -769,8 +787,8 @@ let Modify$1 = class Modify {
769
787
  * already know the relevant index.
770
788
  */
771
789
  insertBefore(newNode, referenceNode) {
772
- if (!newNode) throw new MissingArgumentError("newNode");
773
- if (!referenceNode) throw new MissingArgumentError("referenceNode");
790
+ if (!newNode) throw new InternalArgumentMissingError("newNode");
791
+ if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
774
792
  if (!referenceNode.parentNode) throw new Error(`'${"referenceNode"}' has no parent`);
775
793
  const childNodes = referenceNode.parentNode.childNodes;
776
794
  const beforeNodeIndex = childNodes.indexOf(referenceNode);
@@ -784,17 +802,17 @@ let Modify$1 = class Modify {
784
802
  * already know the relevant index.
785
803
  */
786
804
  insertAfter(newNode, referenceNode) {
787
- if (!newNode) throw new MissingArgumentError("newNode");
788
- if (!referenceNode) throw new MissingArgumentError("referenceNode");
805
+ if (!newNode) throw new InternalArgumentMissingError("newNode");
806
+ if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
789
807
  if (!referenceNode.parentNode) throw new Error(`'${"referenceNode"}' has no parent`);
790
808
  const childNodes = referenceNode.parentNode.childNodes;
791
809
  const referenceNodeIndex = childNodes.indexOf(referenceNode);
792
810
  xml.modify.insertChild(referenceNode.parentNode, newNode, referenceNodeIndex + 1);
793
811
  }
794
812
  insertChild(parent, child, childIndex) {
795
- if (!parent) throw new MissingArgumentError("parent");
813
+ if (!parent) throw new InternalArgumentMissingError("parent");
796
814
  if (xml.query.isTextNode(parent)) throw new Error('Appending children to text nodes is forbidden');
797
- if (!child) throw new MissingArgumentError("child");
815
+ if (!child) throw new InternalArgumentMissingError("child");
798
816
  if (!parent.childNodes) parent.childNodes = [];
799
817
 
800
818
  // revert to append
@@ -817,9 +835,9 @@ let Modify$1 = class Modify {
817
835
  parent.childNodes.splice(childIndex, 0, child);
818
836
  }
819
837
  appendChild(parent, child) {
820
- if (!parent) throw new MissingArgumentError("parent");
838
+ if (!parent) throw new InternalArgumentMissingError("parent");
821
839
  if (xml.query.isTextNode(parent)) throw new Error('Appending children to text nodes is forbidden');
822
- if (!child) throw new MissingArgumentError("child");
840
+ if (!child) throw new InternalArgumentMissingError("child");
823
841
  if (!parent.childNodes) parent.childNodes = [];
824
842
 
825
843
  // update references
@@ -840,7 +858,7 @@ let Modify$1 = class Modify {
840
858
  * * **Note**: It is more efficient to call removeChild(parent, childIndex).
841
859
  */
842
860
  remove(node) {
843
- if (!node) throw new MissingArgumentError("node");
861
+ if (!node) throw new InternalArgumentMissingError("node");
844
862
  if (!node.parentNode) throw new Error('Node has no parent');
845
863
  xml.modify.removeChild(node.parentNode, node);
846
864
  }
@@ -856,8 +874,8 @@ let Modify$1 = class Modify {
856
874
  */
857
875
 
858
876
  removeChild(parent, childOrIndex) {
859
- if (!parent) throw new MissingArgumentError("parent");
860
- if (childOrIndex === null || childOrIndex === undefined) throw new MissingArgumentError("childOrIndex");
877
+ if (!parent) throw new InternalArgumentMissingError("parent");
878
+ if (childOrIndex === null || childOrIndex === undefined) throw new InternalArgumentMissingError("childOrIndex");
861
879
  if (!parent.childNodes || !parent.childNodes.length) throw new Error('Parent node has node children');
862
880
 
863
881
  // get child index
@@ -2328,7 +2346,7 @@ const TagDisposition = Object.freeze({
2328
2346
 
2329
2347
  class TagParser {
2330
2348
  constructor(delimiters) {
2331
- if (!delimiters) throw new MissingArgumentError("delimiters");
2349
+ if (!delimiters) throw new InternalArgumentMissingError("delimiters");
2332
2350
  this.delimiters = delimiters;
2333
2351
  const tagOptionsRegex = `${Regex.escape(delimiters.tagOptionsStart)}(?<tagOptions>.*?)${Regex.escape(delimiters.tagOptionsEnd)}`;
2334
2352
  this.tagRegex = new RegExp(`^${Regex.escape(delimiters.tagStart)}(?<tagName>.*?)(${tagOptionsRegex})?${Regex.escape(delimiters.tagEnd)}`, 'm');
@@ -2644,7 +2662,7 @@ class ImagePlugin extends TemplatePlugin {
2644
2662
  return '';
2645
2663
  }
2646
2664
  if (transparencyPercent < 0 || transparencyPercent > 100) {
2647
- throw new ArgumentError(`Transparency percent must be between 0 and 100, but was ${transparencyPercent}.`);
2665
+ throw new TemplateDataError(`Transparency percent must be between 0 and 100, but was ${transparencyPercent}.`);
2648
2666
  }
2649
2667
  const alpha = Math.round((100 - transparencyPercent) * 1000);
2650
2668
  return `<a:alphaModFix amt="${alpha}" />`;
@@ -2865,9 +2883,9 @@ class LoopTableColumnsStrategy {
2865
2883
  if (!openRow) return false;
2866
2884
  const closeRow = officeMarkup.query.containingTableRowNode(closeCell);
2867
2885
  if (!closeRow) return false;
2868
- const openColumnIndex = openRow.childNodes?.findIndex(child => child === openCell);
2886
+ const openColumnIndex = this.getColumnIndex(openRow, openCell);
2869
2887
  if (openColumnIndex === -1) return false;
2870
- const closeColumnIndex = closeRow.childNodes?.findIndex(child => child === closeCell);
2888
+ const closeColumnIndex = this.getColumnIndex(closeRow, closeCell);
2871
2889
  if (closeColumnIndex === -1) return false;
2872
2890
 
2873
2891
  // If the tags are in different columns, assume it's a table rows loop (iterate rows, not columns).
@@ -2879,8 +2897,8 @@ class LoopTableColumnsStrategy {
2879
2897
  const lastCell = officeMarkup.query.containingTableCellNode(closeTag.xmlTextNode);
2880
2898
  const firstRow = officeMarkup.query.containingTableRowNode(firstCell);
2881
2899
  const lastRow = officeMarkup.query.containingTableRowNode(lastCell);
2882
- const firstColumnIndex = firstRow.childNodes?.findIndex(child => child === firstCell);
2883
- const lastColumnIndex = lastRow.childNodes?.findIndex(child => child === lastCell);
2900
+ const firstColumnIndex = this.getColumnIndex(firstRow, firstCell);
2901
+ const lastColumnIndex = this.getColumnIndex(lastRow, lastCell);
2884
2902
  const table = officeMarkup.query.containingTableNode(firstCell);
2885
2903
 
2886
2904
  // Remove the loop tags
@@ -2899,16 +2917,16 @@ class LoopTableColumnsStrategy {
2899
2917
  mergeBack(columnsWrapperGroups, firstCell, lastCell) {
2900
2918
  const table = officeMarkup.query.containingTableNode(firstCell);
2901
2919
  const firstRow = officeMarkup.query.containingTableRowNode(firstCell);
2902
- const firstColumnIndex = firstRow.childNodes?.findIndex(child => child === firstCell);
2920
+ const firstColumnIndex = this.getColumnIndex(firstRow, firstCell);
2903
2921
  const lastRow = officeMarkup.query.containingTableRowNode(lastCell);
2904
- const lastColumnIndex = lastRow.childNodes?.findIndex(child => child === lastCell);
2905
- let index = firstColumnIndex + 1;
2922
+ const lastColumnIndex = this.getColumnIndex(lastRow, lastCell);
2923
+ let index = firstColumnIndex;
2906
2924
  for (const colWrapperGroup of columnsWrapperGroups) {
2907
2925
  if (colWrapperGroup.length !== 1) {
2908
2926
  throw new Error('Expected a single synthetic table as the columns wrapper.');
2909
2927
  }
2910
2928
  const colWrapper = colWrapperGroup[0];
2911
- this.insertColumnAtIndex(table, colWrapper, index);
2929
+ this.insertColumnAfterIndex(table, colWrapper, index);
2912
2930
  index++;
2913
2931
  }
2914
2932
 
@@ -2925,20 +2943,20 @@ class LoopTableColumnsStrategy {
2925
2943
  // For each row in the original table
2926
2944
  const rows = table.childNodes?.filter(node => node.nodeName === 'w:tr') || [];
2927
2945
  for (const row of rows) {
2928
- const newRow = xml.create.cloneNode(row, false);
2946
+ const syntheticRow = xml.create.cloneNode(row, false);
2929
2947
  const cells = row.childNodes?.filter(node => node.nodeName === 'w:tc') || [];
2930
2948
 
2931
2949
  // Copy only the cells within our column range
2932
2950
  for (let i = firstColumnIndex; i <= lastColumnIndex; i++) {
2933
2951
  if (cells[i]) {
2934
- xml.modify.appendChild(newRow, xml.create.cloneNode(cells[i], true));
2952
+ xml.modify.appendChild(syntheticRow, xml.create.cloneNode(cells[i], true));
2935
2953
  }
2936
2954
  }
2937
- xml.modify.appendChild(syntheticTable, newRow);
2955
+ xml.modify.appendChild(syntheticTable, syntheticRow);
2938
2956
  }
2939
2957
  return syntheticTable;
2940
2958
  }
2941
- insertColumnAtIndex(table, column, index) {
2959
+ insertColumnAfterIndex(table, column, index) {
2942
2960
  // Get all rows from both tables
2943
2961
  const sourceRows = column.childNodes?.filter(node => node.nodeName === 'w:tr') || [];
2944
2962
  const targetRows = table.childNodes?.filter(node => node.nodeName === 'w:tr') || [];
@@ -2956,21 +2974,31 @@ class LoopTableColumnsStrategy {
2956
2974
  if (!sourceCell) {
2957
2975
  throw new Error(`Cell not found in synthetic source table row ${i}.`);
2958
2976
  }
2959
- xml.modify.insertChild(targetRow, xml.create.cloneNode(sourceCell, true), index);
2977
+ const targetCell = this.getColumnByIndex(targetRow, index);
2978
+ const newCell = xml.create.cloneNode(sourceCell, true);
2979
+ if (targetCell) {
2980
+ xml.modify.insertAfter(newCell, targetCell);
2981
+ } else {
2982
+ xml.modify.appendChild(targetRow, newCell);
2983
+ }
2960
2984
  }
2961
2985
  }
2962
2986
  removeColumn(table, index) {
2963
2987
  const rows = table.childNodes?.filter(node => node.nodeName === 'w:tr') || [];
2964
2988
  for (const row of rows) {
2965
- if (!row.childNodes) {
2966
- continue;
2967
- }
2968
- if (row.childNodes.length <= index) {
2989
+ const cell = this.getColumnByIndex(row, index);
2990
+ if (!cell) {
2969
2991
  continue;
2970
2992
  }
2971
- xml.modify.remove(row.childNodes[index]);
2993
+ xml.modify.remove(cell);
2972
2994
  }
2973
2995
  }
2996
+ getColumnIndex(row, cell) {
2997
+ return row.childNodes?.filter(child => child.nodeName === 'w:tc')?.findIndex(child => child === cell);
2998
+ }
2999
+ getColumnByIndex(row, index) {
3000
+ return row.childNodes?.filter(child => child.nodeName === 'w:tc')?.[index];
3001
+ }
2974
3002
  }
2975
3003
 
2976
3004
  class LoopTableRowsStrategy {
@@ -3210,13 +3238,10 @@ const chartTypes = Object.freeze({
3210
3238
  barChart: "c:barChart",
3211
3239
  line3DChart: "c:line3DChart",
3212
3240
  lineChart: "c:lineChart",
3213
- stockChart: "c:stockChart",
3214
3241
  doughnutChart: "c:doughnutChart",
3215
3242
  ofPieChart: "c:ofPieChart",
3216
3243
  pie3DChart: "c:pie3DChart",
3217
3244
  pieChart: "c:pieChart",
3218
- surface3DChart: "c:surface3DChart",
3219
- surfaceChart: "c:surfaceChart",
3220
3245
  scatterChart: "c:scatterChart",
3221
3246
  bubbleChart: "c:bubbleChart"
3222
3247
  });
@@ -3257,6 +3282,19 @@ const formatIds = Object.freeze({
3257
3282
  // Functions
3258
3283
  //
3259
3284
 
3285
+ function chartFriendlyName(chartType) {
3286
+ const name = chartType.replace("c:", "").replace("Chart", "");
3287
+ return name.charAt(0).toUpperCase() + name.slice(1);
3288
+ }
3289
+ function isStandardChartType(chartType) {
3290
+ return chartType === chartTypes.area3DChart || chartType === chartTypes.areaChart || chartType === chartTypes.bar3DChart || chartType === chartTypes.barChart || chartType === chartTypes.line3DChart || chartType === chartTypes.lineChart || chartType === chartTypes.doughnutChart || chartType === chartTypes.ofPieChart || chartType === chartTypes.pie3DChart || chartType === chartTypes.pieChart;
3291
+ }
3292
+ function isScatterChartType(chartType) {
3293
+ return chartType === chartTypes.scatterChart;
3294
+ }
3295
+ function isBubbleChartType(chartType) {
3296
+ return chartType === chartTypes.bubbleChart;
3297
+ }
3260
3298
  function isStandardChartData(chartData) {
3261
3299
  return "categories" in chartData;
3262
3300
  }
@@ -3311,6 +3349,80 @@ function bubbleSizeValues(xValues, series) {
3311
3349
  return xValues.map(x => sizeValuesMap[x]);
3312
3350
  }
3313
3351
 
3352
+ function validateChartData(chartType, chartData) {
3353
+ if (isStandardChartType(chartType)) {
3354
+ validateStandardChartData(chartData, chartType);
3355
+ return;
3356
+ }
3357
+ if (isScatterChartType(chartType)) {
3358
+ validateScatterChartData(chartData, chartType);
3359
+ return;
3360
+ }
3361
+ if (isBubbleChartType(chartType)) {
3362
+ validateScatterChartData(chartData, chartType);
3363
+ validateBubbleChartData(chartData, chartType);
3364
+ return;
3365
+ }
3366
+ throw new TemplateDataError("Invalid chart data: " + JSON.stringify(chartData));
3367
+ }
3368
+ function validateStandardChartData(chartData, chartType) {
3369
+ if (!chartData.categories) {
3370
+ throw new TemplateDataError(`${chartFriendlyName(chartType)} chart must have categories.`);
3371
+ }
3372
+ if (!chartData.categories.names) {
3373
+ throw new TemplateDataError(`${chartFriendlyName(chartType)} chart categories must have a "names" field.`);
3374
+ }
3375
+ for (const ser of chartData.series) {
3376
+ if (!ser.values) {
3377
+ throw new TemplateDataError(`${chartFriendlyName(chartType)} chart series must have a "values" field.`);
3378
+ }
3379
+
3380
+ // Check if the series values and category names have the same length (same number of x and y values)
3381
+ if (ser.values.length != chartData.categories.names.length) {
3382
+ throw new TemplateDataError(`${chartFriendlyName(chartType)} chart series values and category names must have the same length.`);
3383
+ }
3384
+
3385
+ // Verify series values are numbers
3386
+ for (const val of ser.values) {
3387
+ if (val === null || val === undefined) {
3388
+ continue;
3389
+ }
3390
+ if (typeof val === "number") {
3391
+ continue;
3392
+ }
3393
+ throw new TemplateDataError(`${chartFriendlyName(chartType)} chart series values must be numbers.`);
3394
+ }
3395
+ }
3396
+ }
3397
+ function validateScatterChartData(chartData, chartType) {
3398
+ if (!chartData.series) {
3399
+ throw new TemplateDataError(`${chartFriendlyName(chartType)} chart must have series.`);
3400
+ }
3401
+ for (const ser of chartData.series) {
3402
+ if (!ser.values) {
3403
+ throw new TemplateDataError(`${chartFriendlyName(chartType)} chart series must have a "values" field.`);
3404
+ }
3405
+ for (const val of ser.values) {
3406
+ // Verify series values are valid point objects
3407
+ if (typeof val === "object" && "x" in val && "y" in val) {
3408
+ continue;
3409
+ }
3410
+ throw new TemplateDataError(`${chartFriendlyName(chartType)} chart series values must have x and y properties.`);
3411
+ }
3412
+ }
3413
+ }
3414
+ function validateBubbleChartData(chartData, chartType) {
3415
+ for (const ser of chartData.series) {
3416
+ for (const val of ser.values) {
3417
+ // Verify series points have a "size" property (x and y are checked in validateScatterChartData)
3418
+ if (typeof val === "object" && "size" in val) {
3419
+ continue;
3420
+ }
3421
+ throw new TemplateDataError(`${chartFriendlyName(chartType)} chart series values must have a "size" property.`);
3422
+ }
3423
+ }
3424
+ }
3425
+
3314
3426
  // Based on: https://github.com/OpenXmlDev/Open-Xml-PowerTools/blob/vNext/OpenXmlPowerTools/ChartUpdater.cs
3315
3427
 
3316
3428
  const space = " ";
@@ -3325,9 +3437,6 @@ async function updateChart(chartPart, chartData) {
3325
3437
  chartData.series[i].name = seriesName(ser.name, i);
3326
3438
  }
3327
3439
 
3328
- // Input validation
3329
- validateChartData(chartData);
3330
-
3331
3440
  // Get the chart node
3332
3441
  const root = await chartPart.xmlRoot();
3333
3442
  if (root.nodeName !== "c:chartSpace") {
@@ -3345,8 +3454,12 @@ async function updateChart(chartPart, chartData) {
3345
3454
  if (!chartNode) {
3346
3455
  const plotAreaChildren = plotAreaNode.childNodes?.map(child => `<${child.nodeName}>`);
3347
3456
  const supportedChartTypes = Object.values(chartTypes).join(", ");
3348
- throw new Error(`Unsupported chart type. Plot area children: ${plotAreaChildren?.join(", ")}. Supported chart types: ${supportedChartTypes}`);
3457
+ throw new TemplateSyntaxError(`Unsupported chart type. Plot area children: ${plotAreaChildren?.join(", ")}. Supported chart types: ${supportedChartTypes}`);
3349
3458
  }
3459
+ const chartType = chartNode.nodeName;
3460
+
3461
+ // Input validation
3462
+ validateChartData(chartType, chartData);
3350
3463
 
3351
3464
  // Read the first series
3352
3465
  const firstSeries = readFirstSeries(chartNode, chartData);
@@ -3357,41 +3470,6 @@ async function updateChart(chartPart, chartData) {
3357
3470
  // Update inline series
3358
3471
  updateInlineSeries(chartNode, firstSeries, chartData);
3359
3472
  }
3360
- function validateChartData(chartData) {
3361
- if (isStandardChartData(chartData)) {
3362
- for (const ser of chartData.series) {
3363
- // Check if the series values and category names have the same length (same number of x and y values)
3364
- if (ser.values.length != chartData.categories.names.length) {
3365
- throw new ArgumentError("Invalid chart data. Series values and category names must have the same length.");
3366
- }
3367
-
3368
- // Verify series values are numbers
3369
- for (const val of ser.values) {
3370
- if (val === null || val === undefined) {
3371
- continue;
3372
- }
3373
- if (typeof val === "number") {
3374
- continue;
3375
- }
3376
- throw new ArgumentError("Invalid chart data. Series values must be numbers.");
3377
- }
3378
- }
3379
- return;
3380
- }
3381
- if (isScatterChartData(chartData)) {
3382
- for (const ser of chartData.series) {
3383
- for (const val of ser.values) {
3384
- // Verify series values are valid point objects
3385
- if (typeof val === "object" && "x" in val && "y" in val) {
3386
- continue;
3387
- }
3388
- throw new ArgumentError("Invalid scatter chart data. Series values must contain x and y properties.");
3389
- }
3390
- }
3391
- return;
3392
- }
3393
- throw new ArgumentError("Invalid chart data: " + JSON.stringify(chartData));
3394
- }
3395
3473
 
3396
3474
  //
3397
3475
  // Read the first series
@@ -4155,7 +4233,7 @@ class ChartPlugin extends TemplatePlugin {
4155
4233
  async simpleTagReplacements(tag, data, context) {
4156
4234
  const chartNode = xml.query.findParentByName(tag.xmlTextNode, "c:chart");
4157
4235
  if (!chartNode) {
4158
- throw new ArgumentError("Chart tag not placed in chart title");
4236
+ throw new TemplateSyntaxError("Chart tag not placed in chart title");
4159
4237
  }
4160
4238
  const content = data.getScopeData();
4161
4239
  if (!content) {
@@ -4400,7 +4478,7 @@ class TemplateHandler {
4400
4478
  /**
4401
4479
  * Version number of the `easy-template-x` library.
4402
4480
  */
4403
- version = "6.0.0" ;
4481
+ version = "6.1.0" ;
4404
4482
  constructor(options) {
4405
4483
  this.options = new TemplateHandlerOptions(options);
4406
4484
 
@@ -4538,7 +4616,6 @@ class TemplateHandler {
4538
4616
  }
4539
4617
  }
4540
4618
 
4541
- exports.ArgumentError = ArgumentError;
4542
4619
  exports.Base64 = Base64;
4543
4620
  exports.Binary = Binary;
4544
4621
  exports.COMMENT_NODE_NAME = COMMENT_NODE_NAME;
@@ -4546,6 +4623,8 @@ exports.DelimiterSearcher = DelimiterSearcher;
4546
4623
  exports.Delimiters = Delimiters;
4547
4624
  exports.Docx = Docx;
4548
4625
  exports.ImagePlugin = ImagePlugin;
4626
+ exports.InternalArgumentMissingError = InternalArgumentMissingError;
4627
+ exports.InternalError = InternalError;
4549
4628
  exports.LOOP_CONTENT_TYPE = LOOP_CONTENT_TYPE;
4550
4629
  exports.LinkPlugin = LinkPlugin;
4551
4630
  exports.LoopPlugin = LoopPlugin;
@@ -4553,7 +4632,6 @@ exports.MalformedFileError = MalformedFileError;
4553
4632
  exports.MaxXmlDepthError = MaxXmlDepthError;
4554
4633
  exports.MimeType = MimeType;
4555
4634
  exports.MimeTypeHelper = MimeTypeHelper;
4556
- exports.MissingArgumentError = MissingArgumentError;
4557
4635
  exports.MissingCloseDelimiterError = MissingCloseDelimiterError;
4558
4636
  exports.MissingStartDelimiterError = MissingStartDelimiterError;
4559
4637
  exports.OfficeMarkup = OfficeMarkup;
@@ -4573,10 +4651,12 @@ exports.TagDisposition = TagDisposition;
4573
4651
  exports.TagOptionsParseError = TagOptionsParseError;
4574
4652
  exports.TagParser = TagParser;
4575
4653
  exports.TemplateCompiler = TemplateCompiler;
4654
+ exports.TemplateDataError = TemplateDataError;
4576
4655
  exports.TemplateExtension = TemplateExtension;
4577
4656
  exports.TemplateHandler = TemplateHandler;
4578
4657
  exports.TemplateHandlerOptions = TemplateHandlerOptions;
4579
4658
  exports.TemplatePlugin = TemplatePlugin;
4659
+ exports.TemplateSyntaxError = TemplateSyntaxError;
4580
4660
  exports.TextPlugin = TextPlugin;
4581
4661
  exports.UnclosedTagError = UnclosedTagError;
4582
4662
  exports.UnidentifiedFileTypeError = UnidentifiedFileTypeError;