h17-sspdf 0.1.3 → 0.1.4

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 (40) hide show
  1. package/README.md +21 -11
  2. package/cli.js +31 -7
  3. package/core/pdf-core.js +210 -3
  4. package/core/plugin-chart.js +40 -12
  5. package/core/plugin-registry.js +1 -1
  6. package/core/render-document.js +172 -2
  7. package/core/shapes.js +1 -1
  8. package/core/validate.js +13 -1
  9. package/fonts/crimson-text.js +1 -1
  10. package/fonts/fira-code.js +1 -1
  11. package/fonts/ibm-plex-sans.js +1 -1
  12. package/fonts/inter.js +1 -1
  13. package/fonts/jetbrains-mono.js +1 -1
  14. package/fonts/lato.js +1 -1
  15. package/fonts/libre-baskerville.js +1 -1
  16. package/fonts/lora.js +1 -1
  17. package/fonts/merriweather.js +1 -1
  18. package/fonts/montserrat.js +1 -1
  19. package/fonts/nunito.js +1 -1
  20. package/fonts/open-sans.js +1 -1
  21. package/fonts/oswald.js +1 -1
  22. package/fonts/playfair-display.js +1 -1
  23. package/fonts/pt-sans.js +1 -1
  24. package/fonts/raleway.js +1 -1
  25. package/fonts/roboto.js +1 -1
  26. package/fonts/source-code-pro.js +1 -1
  27. package/fonts/source-serif-4.js +1 -1
  28. package/fonts/work-sans.js +1 -1
  29. package/index.js +1 -1
  30. package/package.json +4 -15
  31. package/vendor/chart.js/LICENSE +9 -0
  32. package/vendor/chart.js/chart.umd.js +14 -0
  33. package/vendor/chartjs-node-canvas/LICENSE +21 -0
  34. package/vendor/chartjs-node-canvas/backgroundColourPlugin.js +21 -0
  35. package/vendor/chartjs-node-canvas/chartJSNodeCanvas.js +129 -0
  36. package/vendor/chartjs-node-canvas/chartJSNodeCanvasBase.js +90 -0
  37. package/vendor/chartjs-node-canvas/freshRequire.js +20 -0
  38. package/vendor/chartjs-node-canvas/index.js +12 -0
  39. package/vendor/jspdf/LICENSE +22 -0
  40. package/vendor/jspdf/jspdf.node.js +32458 -0
package/README.md CHANGED
@@ -22,7 +22,7 @@ npm install h17-sspdf
22
22
 
23
23
  ## The problem it solves
24
24
 
25
- Generating PDFs imperatively means tracking the cursor yourself. Every element you place shifts everything below it. Line wrapping, page breaks, font resets all manual.
25
+ Generating PDFs imperatively means tracking the cursor yourself. Every element you place shifts everything below it. Line wrapping, page breaks, font resets, all manual.
26
26
 
27
27
  This engine inverts that. You describe *what* to render and *how it looks*. The cursor, the math, the page breaks happen automatically.
28
28
 
@@ -34,7 +34,7 @@ Every operation has a `type` and a `label`. The label maps to a style in the the
34
34
  operation → label → theme style → layout → cursor advance → next operation
35
35
  ```
36
36
 
37
- Page breaks happen automatically when content reaches the bottom margin. Style resets after every operation nothing leaks.
37
+ Page breaks happen automatically when content reaches the bottom margin. Style resets after every operation, nothing leaks.
38
38
 
39
39
  ## Quick start
40
40
 
@@ -203,7 +203,7 @@ A repeating pattern of `row` + `text` pairs. The row carries the label/value, th
203
203
  }
204
204
  ```
205
205
 
206
- `xMm` and `maxWidthMm` indent it from the body column the indentation is in the source, not the theme.
206
+ `xMm` and `maxWidthMm` indent it from the body column, the indentation is in the source, not the theme.
207
207
 
208
208
  #### Hidden text (ATS / search metadata)
209
209
 
@@ -273,8 +273,8 @@ Any operation accepts `xMm` and `maxWidthMm` to override the theme margins for t
273
273
 
274
274
  ### Page break control
275
275
 
276
- - `keepWithNext: N` keep this operation on the same page as the next N operations
277
- - `block` with `keepTogether: true` all children stay on the same page
276
+ - `keepWithNext: N` - keep this operation on the same page as the next N operations
277
+ - `block` with `keepTogether: true` - all children stay on the same page
278
278
 
279
279
  ---
280
280
 
@@ -344,10 +344,12 @@ Then use `fontFamily: 'Inter'` in any label.
344
344
 
345
345
  Renders any Chart.js configuration to a PNG and embeds it in the PDF.
346
346
 
347
- ### Install peer dependencies
347
+ ### Requirements
348
+
349
+ The chart plugin requires the `canvas` npm package (native C++ addon). Chart.js and chartjs-node-canvas are vendored and ship with the engine.
348
350
 
349
351
  ```bash
350
- npm install chart.js chartjs-node-canvas
352
+ npm install canvas
351
353
  ```
352
354
 
353
355
  ### Register
@@ -385,7 +387,7 @@ registerPlugin('chart', plugins.chart);
385
387
  }
386
388
  ```
387
389
 
388
- `data` and `options` are passed directly to Chart.js the plugin does not abstract the Chart.js API. `canvasWidth`/`canvasHeight` control render resolution (default 1600×800). `widthMm`/`heightMm` control the slot size in the PDF.
390
+ `data` and `options` are passed directly to Chart.js, the plugin does not abstract the Chart.js API. `canvasWidth`/`canvasHeight` control render resolution (default 1600×800). `widthMm`/`heightMm` control the slot size in the PDF.
389
391
 
390
392
  ---
391
393
 
@@ -406,12 +408,20 @@ npx sspdf -s source.json -t theme.js -o output.pdf
406
408
  ## Constraints
407
409
 
408
410
  - A4 only
409
- - Single-line `row` cells no multi-line column pairs
411
+ - Single-line `row` cells, no multi-line column pairs
410
412
  - No `{{pages}}` total page count token
411
- - The `chart` plugin requires Node.js (`chartjs-node-canvas` is server-side only) the core engine works in the browser if you use the jsPDF UMD build
413
+ - The `canvas` npm package (native C++ addon) is the only runtime dependency, required for server-side chart rendering.
412
414
 
413
415
  ---
414
416
 
415
417
  Hugo Palma, 2026
416
418
 
417
- Built on [jsPDF](https://github.com/parallax/jsPDF).
419
+ ## Third-party
420
+
421
+ This project vendors the following MIT-licensed libraries:
422
+
423
+ - [jsPDF](https://github.com/parallax/jsPDF) - PDF generation. Copyright (c) 2010-2025 James Hall, yWorks GmbH.
424
+ - [Chart.js](https://github.com/chartjs/Chart.js) - Chart rendering. Copyright (c) 2014-2024 Chart.js Contributors.
425
+ - [chartjs-node-canvas](https://github.com/SeanSobey/ChartjsNodeCanvas) - Server-side Chart.js rendering. Copyright (c) 2018 Sean Sobey.
426
+
427
+ Full license texts are in `vendor/*/LICENSE`.
package/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
- const { renderDocument } = require("./core/render-document");
5
+ const { renderDocument, registerPlugin, plugins } = require("./index");
6
6
 
7
7
  const EXAMPLES_THEMES_DIR = path.join(__dirname, "examples", "themes");
8
8
 
@@ -84,7 +84,7 @@ function readSource(sourcePath) {
84
84
  function printHelp() {
85
85
  const themes = listBuiltInThemes();
86
86
  console.log(`
87
- sspdf CLI Render a source JSON + theme into a PDF.
87
+ sspdf CLI - Render a source JSON + theme into a PDF.
88
88
 
89
89
  Usage:
90
90
  node cli.js --source <file.json> --theme <theme> --output <file.pdf>
@@ -105,7 +105,25 @@ Examples:
105
105
  `.trim());
106
106
  }
107
107
 
108
- function main() {
108
+ function collectChartOps(obj) {
109
+ const charts = [];
110
+ if (!obj || typeof obj !== "object") return charts;
111
+ if (Array.isArray(obj)) {
112
+ obj.forEach((item) => charts.push(...collectChartOps(item)));
113
+ } else {
114
+ if (obj.type === "chart") charts.push(obj);
115
+ for (const key of ["operations", "sections", "content", "items", "children"]) {
116
+ if (Array.isArray(obj[key])) charts.push(...collectChartOps(obj[key]));
117
+ }
118
+ if (obj.pageTemplates) {
119
+ if (Array.isArray(obj.pageTemplates.header)) charts.push(...collectChartOps(obj.pageTemplates.header));
120
+ if (Array.isArray(obj.pageTemplates.footer)) charts.push(...collectChartOps(obj.pageTemplates.footer));
121
+ }
122
+ }
123
+ return charts;
124
+ }
125
+
126
+ async function main() {
109
127
  const args = parseArgs(process.argv.slice(2));
110
128
  if (args.help) {
111
129
  printHelp();
@@ -121,15 +139,21 @@ function main() {
121
139
 
122
140
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
123
141
 
142
+ const chartOps = collectChartOps(source);
143
+ if (chartOps.length > 0) {
144
+ registerPlugin("chart", plugins.chart);
145
+ for (const op of chartOps) {
146
+ await plugins.chart.preRender(op);
147
+ }
148
+ }
149
+
124
150
  const result = renderDocument({ source, theme, outputPath });
125
151
 
126
152
  console.log(`[OK] ${result.operationsCount} operations rendered`);
127
153
  console.log(`[OK] ${outputPath}`);
128
154
  }
129
155
 
130
- try {
131
- main();
132
- } catch (err) {
156
+ main().catch((err) => {
133
157
  console.error(`[ERROR] ${err.message}`);
134
158
  process.exit(1);
135
- }
159
+ });
package/core/pdf-core.js CHANGED
@@ -1,8 +1,8 @@
1
1
  const fs = require("fs");
2
- const { jsPDF } = require("jspdf");
2
+ const { jsPDF } = require("../vendor/jspdf/jspdf.node");
3
3
  const { pxToMm, ptToMm, resolveLineHeightMm } = require("./units");
4
4
 
5
- // Style math helpers shared between core rendering and height estimation.
5
+ // Style math helpers, shared between core rendering and height estimation.
6
6
 
7
7
  function getMarginTopMm(style) {
8
8
  if (style.marginTopMm !== undefined) {
@@ -130,7 +130,7 @@ class PDFCore {
130
130
  subject: meta.subject || "",
131
131
  author: meta.author || "Hugo Palma",
132
132
  keywords: meta.keywords || "",
133
- creator: "SuperSimplePDF (github.com/hugopalma17/sspdf) built on jsPDF",
133
+ creator: "SuperSimplePDF (github.com/hugopalma17/sspdf) - built on jsPDF",
134
134
  });
135
135
 
136
136
  this.paintBackground();
@@ -628,6 +628,213 @@ class PDFCore {
628
628
  return { y: drawY, endY };
629
629
  }
630
630
 
631
+ /**
632
+ * Draw a table with header row, data rows, per-edge borders, and alt row shading.
633
+ * All values derived from style properties, no hardcoded constants.
634
+ *
635
+ * @param {object} payload
636
+ * @param {Array<{widthMm: number, align: string}>} payload.columns Resolved column definitions
637
+ * @param {string[]|null} payload.headers Header cell texts, or null for no header row
638
+ * @param {string[][]} payload.rows Data rows, each row is an array of cell strings
639
+ * @param {object} payload.cellStyle Resolved style for data cells
640
+ * @param {object|null} payload.headerStyle Resolved style for header cells
641
+ * @param {number} [payload.x] Left edge of table
642
+ * @param {number} [payload.maxWidth] Total table width
643
+ * @param {boolean} [payload.allowPageBreak]
644
+ * @returns {{ y: number, endY: number, rowCount: number }}
645
+ */
646
+ drawTable(payload) {
647
+ const columns = payload.columns;
648
+ const headers = payload.headers || null;
649
+ const rows = payload.rows || [];
650
+ const cellStyle = payload.cellStyle || {};
651
+ const headerStyle = payload.headerStyle || null;
652
+ const x = payload.x !== undefined ? payload.x : this.marginLeftMm;
653
+ const allowPageBreak = payload.allowPageBreak !== false;
654
+
655
+ const marginTopMm = getMarginTopMm(cellStyle);
656
+ const marginBottomMm = getMarginBottomMm(cellStyle);
657
+
658
+ const cellPad = Number(cellStyle.cellPaddingMm) || 0;
659
+ const headerPad = headerStyle
660
+ ? (Number(headerStyle.cellPaddingMm) || 0)
661
+ : cellPad;
662
+
663
+ // Column x positions
664
+ const colX = [];
665
+ let cx = x;
666
+ for (let c = 0; c < columns.length; c++) {
667
+ colX.push(cx);
668
+ cx += columns[c].widthMm;
669
+ }
670
+
671
+ // Margin before table
672
+ if (allowPageBreak) {
673
+ const firstRowHeight = headers
674
+ ? this._measureTableRowHeight(headers, columns, colX, headerStyle, headerPad)
675
+ : this._measureTableRowHeight(rows[0] || [], columns, colX, cellStyle, cellPad);
676
+ this.ensureSpace(marginTopMm + firstRowHeight);
677
+ }
678
+ const tableStartY = this.cursorY + marginTopMm;
679
+ this.cursorY = tableStartY;
680
+
681
+ // Draw header
682
+ if (headerStyle && headers) {
683
+ this._drawTableRow({
684
+ rowData: headers,
685
+ columns, colX,
686
+ style: headerStyle,
687
+ cellPadding: headerPad,
688
+ bgColor: this._resolveColor(headerStyle.backgroundColor),
689
+ });
690
+ }
691
+
692
+ // Draw data rows
693
+ for (let r = 0; r < rows.length; r++) {
694
+ const rowData = rows[r];
695
+ const rowHeight = this._measureTableRowHeight(rowData, columns, colX, cellStyle, cellPad);
696
+
697
+ // Page break check - re-draw header after break
698
+ if (allowPageBreak && this.cursorY + rowHeight > this.contentBottomY) {
699
+ this.addPage();
700
+ if (headerStyle && headers) {
701
+ this._drawTableRow({
702
+ rowData: headers,
703
+ columns, colX,
704
+ style: headerStyle,
705
+ cellPadding: headerPad,
706
+ bgColor: this._resolveColor(headerStyle.backgroundColor),
707
+ });
708
+ }
709
+ }
710
+
711
+ // Determine row background
712
+ const baseBg = this._resolveColor(cellStyle.backgroundColor);
713
+ const altBg = cellStyle.altRowColor && Array.isArray(cellStyle.altRowColor) && cellStyle.altRowColor.length === 3
714
+ ? cellStyle.altRowColor
715
+ : null;
716
+ const bgColor = (altBg && r % 2 === 1) ? altBg : baseBg;
717
+
718
+ this._drawTableRow({
719
+ rowData,
720
+ columns, colX,
721
+ style: cellStyle,
722
+ cellPadding: cellPad,
723
+ bgColor,
724
+ });
725
+ }
726
+
727
+ const endY = this.cursorY;
728
+ this.cursorY = endY + marginBottomMm;
729
+
730
+ this.lastDrawnBounds = {
731
+ topY: tableStartY,
732
+ bottomY: endY,
733
+ leftX: x,
734
+ rightX: colX.length > 0 ? colX[colX.length - 1] + columns[columns.length - 1].widthMm : x,
735
+ };
736
+
737
+ return { y: tableStartY, endY, rowCount: rows.length };
738
+ }
739
+
740
+ /**
741
+ * Measure the height of a single table row.
742
+ * @private
743
+ */
744
+ _measureTableRowHeight(rowData, columns, colX, style, cellPadding) {
745
+ const lineHeightMm = style.lineHeightMm || resolveLineHeightMm(Number(style.fontSize), style.lineHeight);
746
+ let maxLines = 1;
747
+ for (let c = 0; c < columns.length; c++) {
748
+ const innerWidth = columns[c].widthMm - (cellPadding * 2);
749
+ if (innerWidth <= 0) continue;
750
+ const text = String((rowData && rowData[c]) || "");
751
+ const lines = this.measureWrappedLines(text, innerWidth, style);
752
+ if (lines.length > maxLines) maxLines = lines.length;
753
+ }
754
+ return cellPadding + (maxLines * lineHeightMm) + cellPadding;
755
+ }
756
+
757
+ /**
758
+ * Draw a single table row (background, text, borders).
759
+ * @private
760
+ */
761
+ _drawTableRow(payload) {
762
+ const { rowData, columns, colX, style, cellPadding, bgColor } = payload;
763
+ const rowY = this.cursorY;
764
+ const lineHeightMm = style.lineHeightMm || resolveLineHeightMm(Number(style.fontSize), style.lineHeight);
765
+
766
+ // Measure row height
767
+ let maxLines = 1;
768
+ const cellLines = [];
769
+ for (let c = 0; c < columns.length; c++) {
770
+ const innerWidth = columns[c].widthMm - (cellPadding * 2);
771
+ const text = String((rowData && rowData[c]) || "");
772
+ const lines = innerWidth > 0 ? this.measureWrappedLines(text, innerWidth, style) : [text];
773
+ cellLines.push(lines);
774
+ if (lines.length > maxLines) maxLines = lines.length;
775
+ }
776
+ const rowHeight = cellPadding + (maxLines * lineHeightMm) + cellPadding;
777
+
778
+ // Draw cell backgrounds
779
+ if (bgColor) {
780
+ this.doc.setFillColor(...bgColor);
781
+ for (let c = 0; c < columns.length; c++) {
782
+ this.doc.rect(colX[c], rowY, columns[c].widthMm, rowHeight, "F");
783
+ }
784
+ }
785
+
786
+ // Draw cell text
787
+ this.applyTextStyle(style);
788
+ const baselineOffset = this._getBaselineOffsetMm(Number(style.fontSize));
789
+ this.doc.setLineHeightFactor(Number(style.lineHeight));
790
+ for (let c = 0; c < columns.length; c++) {
791
+ const innerWidth = columns[c].widthMm - (cellPadding * 2);
792
+ if (innerWidth <= 0) continue;
793
+ const textX = colX[c] + cellPadding;
794
+ const textY = rowY + cellPadding + baselineOffset;
795
+ const align = columns[c].align || "left";
796
+ this._drawTextLines(cellLines[c], {
797
+ x: textX,
798
+ y: textY,
799
+ maxWidth: innerWidth,
800
+ align,
801
+ lineHeightMm,
802
+ });
803
+ }
804
+
805
+ // Draw cell borders
806
+ for (let c = 0; c < columns.length; c++) {
807
+ this._drawTableCellBorders(style, colX[c], rowY, columns[c].widthMm, rowHeight);
808
+ }
809
+
810
+ this.applyDefaultRenderState();
811
+ this.cursorY = rowY + rowHeight;
812
+ return rowHeight;
813
+ }
814
+
815
+ /**
816
+ * Draw per-edge borders for a single table cell.
817
+ * @private
818
+ */
819
+ _drawTableCellBorders(style, cellX, cellY, cellWidth, cellHeight) {
820
+ const baseBorderColor = this._resolveColor(style.borderColor, [200, 200, 200]);
821
+ const edges = [
822
+ { widthProp: "borderTopMm", colorProp: "borderTopColor", x1: cellX, y1: cellY, x2: cellX + cellWidth, y2: cellY },
823
+ { widthProp: "borderBottomMm", colorProp: "borderBottomColor", x1: cellX, y1: cellY + cellHeight, x2: cellX + cellWidth, y2: cellY + cellHeight },
824
+ { widthProp: "borderLeftMm", colorProp: "borderLeftColor", x1: cellX, y1: cellY, x2: cellX, y2: cellY + cellHeight },
825
+ { widthProp: "borderRightMm", colorProp: "borderRightColor", x1: cellX + cellWidth, y1: cellY, x2: cellX + cellWidth, y2: cellY + cellHeight },
826
+ ];
827
+
828
+ for (const edge of edges) {
829
+ const width = Number(style[edge.widthProp]) || 0;
830
+ if (width <= 0) continue;
831
+ const color = this._resolveColor(style[edge.colorProp], baseBorderColor);
832
+ this.doc.setDrawColor(...color);
833
+ this.doc.setLineWidth(width);
834
+ this.doc.line(edge.x1, edge.y1, edge.x2, edge.y2);
835
+ }
836
+ }
837
+
631
838
  /**
632
839
  * Draw visually hidden text (ATS tags, keywords) without moving cursor.
633
840
  * @param {object} payload
@@ -6,8 +6,8 @@
6
6
  * Renders any Chart.js configuration server-side via chartjs-node-canvas
7
7
  * and embeds the result as a PNG image in the PDF.
8
8
  *
9
- * Requires peer dependencies:
10
- * npm install chart.js chartjs-node-canvas
9
+ * Requires the `canvas` npm package (native C++ addon):
10
+ * npm install canvas
11
11
  *
12
12
  * Registration:
13
13
  * const { registerPlugin, plugins } = require('h17-sspdf');
@@ -45,7 +45,7 @@
45
45
  * - canvasWidth/canvasHeight control render resolution (default 1600x800).
46
46
  * Higher values = sharper chart. widthMm/heightMm control the PDF slot size.
47
47
  * - responsive: false and animation: false are injected automatically.
48
- * - Pass any valid Chart.js config in data and options the plugin does not
48
+ * - Pass any valid Chart.js config in data and options, the plugin does not
49
49
  * modify or abstract it.
50
50
  */
51
51
 
@@ -54,10 +54,10 @@ let _ChartJSNodeCanvas = null;
54
54
  function getCanvas() {
55
55
  if (!_ChartJSNodeCanvas) {
56
56
  try {
57
- _ChartJSNodeCanvas = require('chartjs-node-canvas').ChartJSNodeCanvas;
57
+ _ChartJSNodeCanvas = require('../vendor/chartjs-node-canvas').ChartJSNodeCanvas;
58
58
  } catch {
59
59
  throw new Error(
60
- 'chart plugin requires chartjs-node-canvas run: npm install chart.js chartjs-node-canvas'
60
+ 'chart plugin requires the canvas package - run: npm install canvas'
61
61
  );
62
62
  }
63
63
  }
@@ -70,6 +70,32 @@ function getCanvas() {
70
70
  * @param {object} operation - the chart operation object (mutated in place)
71
71
  * @returns {Promise<void>}
72
72
  */
73
+ /**
74
+ * Walk an options object and convert callback template strings like "{{v}}%"
75
+ * into real functions. This lets JSON sources define simple tick formatters
76
+ * without requiring executable code in the source file.
77
+ *
78
+ * Supported token: {{v}} - replaced with the callback's first argument (value).
79
+ */
80
+ function resolveCallbackTemplates(obj) {
81
+ if (!obj || typeof obj !== 'object') return obj;
82
+ if (Array.isArray(obj)) {
83
+ for (let i = 0; i < obj.length; i++) {
84
+ obj[i] = resolveCallbackTemplates(obj[i]);
85
+ }
86
+ return obj;
87
+ }
88
+ for (const key of Object.keys(obj)) {
89
+ if (key === 'callback' && typeof obj[key] === 'string' && obj[key].includes('{{v}}')) {
90
+ const template = obj[key];
91
+ obj[key] = function (value) { return template.replace(/\{\{v\}\}/g, String(value)); };
92
+ } else {
93
+ obj[key] = resolveCallbackTemplates(obj[key]);
94
+ }
95
+ }
96
+ return obj;
97
+ }
98
+
73
99
  async function preRender(operation) {
74
100
  const ChartJSNodeCanvas = getCanvas();
75
101
  const canvasW = operation.canvasWidth || 1600;
@@ -81,14 +107,16 @@ async function preRender(operation) {
81
107
  backgroundColour: 'transparent',
82
108
  });
83
109
 
110
+ const options = resolveCallbackTemplates({
111
+ ...(operation.options || {}),
112
+ responsive: false,
113
+ animation: false,
114
+ });
115
+
84
116
  operation._buf = await canvas.renderToBuffer({
85
117
  type: operation.chartType || 'bar',
86
118
  data: operation.data || { labels: [], datasets: [] },
87
- options: {
88
- ...(operation.options || {}),
89
- responsive: false,
90
- animation: false,
91
- },
119
+ options,
92
120
  });
93
121
  }
94
122
 
@@ -100,7 +128,7 @@ module.exports = {
100
128
 
101
129
  if (!operation._buf) {
102
130
  throw new Error(
103
- 'chart plugin: operation._buf is missing call plugin.preRender(operation) before renderDocument()'
131
+ 'chart plugin: operation._buf is missing - call plugin.preRender(operation) before renderDocument()'
104
132
  );
105
133
  }
106
134
 
@@ -120,7 +148,7 @@ module.exports = {
120
148
  throw new Error('chart operation requires chartType (e.g. "bar", "line", "doughnut")');
121
149
  }
122
150
  if (!operation.data) {
123
- throw new Error('chart operation requires data pass a Chart.js data config object');
151
+ throw new Error('chart operation requires data - pass a Chart.js data config object');
124
152
  }
125
153
  },
126
154
  };
@@ -2,7 +2,7 @@ const _plugins = new Map();
2
2
 
3
3
  const BUILT_IN_TYPES = new Set([
4
4
  "text", "row", "bullet", "divider", "spacer", "hiddenText",
5
- "block", "section", "group", "quote",
5
+ "block", "section", "group", "quote", "table",
6
6
  ]);
7
7
 
8
8
  function registerPlugin(type, handler) {