h17-sspdf 1.1.0 → 1.3.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.
package/DOCUMENTATION.md CHANGED
@@ -675,9 +675,9 @@ Renders two independent operation lists side by side. Each column gets half the
675
675
 
676
676
  **Column width:** `(contentWidth - gutterMm) / 2`. Both columns are always equal width.
677
677
 
678
- **Cursor after:** `max(col1EndY, col2EndY)` the bottom of the taller column.
678
+ **Cursor after:** `max(col1EndY, col2EndY)` - the bottom of the taller column.
679
679
 
680
- Any operation type that works at the top level works inside columns. Tables, images, bullets, nested blocks all work. Columns inside columns are supported (the margin stack is re-entrant).
680
+ Any operation type that works at the top level works inside columns. Tables, images, bullets, nested blocks all work. Columns inside columns are supported (the margin stack is re-entrant).
681
681
 
682
682
  ### Inferred text operations
683
683
 
package/README.md CHANGED
@@ -20,7 +20,7 @@ Source JSON + Theme = PDF
20
20
  npm install h17-sspdf
21
21
  ```
22
22
 
23
- Requires **Node.js 18 or newer**. The engine vendors a single self-contained UMD build of jsPDF (which bundles fflate, fast-png, iobuffer internally) plus Chart.js and chartjs-node-canvas. The only install-time dependency is [`canvas`](https://www.npmjs.com/package/canvas) (native C++ addon, used by the chart plugin) — no transitive dependency tree beyond that.
23
+ Requires **Node.js 18 or newer**. The engine vendors a single self-contained UMD build of jsPDF (which bundles fflate, fast-png, iobuffer internally) plus Chart.js and chartjs-node-canvas. The base install has no runtime dependencies. The optional chart plugin uses [`canvas`](https://www.npmjs.com/package/canvas) as an optional peer dependency.
24
24
 
25
25
  ## The problem it solves
26
26
 
@@ -199,7 +199,7 @@ A repeating pattern of `row` + `text` pairs. The row carries the label/value, th
199
199
  "type": "quote",
200
200
  "label": "news.pullquote",
201
201
  "text": "When the format becomes a system instead of a template, agencies stop re-solving the same layout problem every week.",
202
- "attribution": " Elena Ward, public records modernization lead",
202
+ "attribution": "- Elena Ward, public records modernization lead",
203
203
  "xMm": 22,
204
204
  "maxWidthMm": 166
205
205
  }
@@ -474,7 +474,7 @@ Claude Code skills for generating PDFs and themes are available in the `skills/`
474
474
  - Single-line `row` cells, no multi-line column pairs
475
475
  - `{{page}}` gives the current page number; `{{pages}}` (total page count) is not supported because keep-together rules make the final page count unpredictable until the last operation is laid out
476
476
  - Charts require the `canvas` npm package (native C++ addon) for server-side rendering; everything else is zero native dependencies
477
- - A `jspdf.umd.js` build is vendored for client-side/browser use. It bundles all dependencies internally but requires wiring up your own entry point `pdf-core.js` uses the Node build by default
477
+ - A `jspdf.umd.js` build is vendored for client-side/browser use. It bundles all dependencies internally but requires wiring up your own entry point; `pdf-core.js` uses the Node build by default
478
478
 
479
479
  ---
480
480
 
package/cli.js CHANGED
@@ -155,7 +155,7 @@ function collectChartOps(obj) {
155
155
  obj.forEach((item) => charts.push(...collectChartOps(item)));
156
156
  } else {
157
157
  if (obj.type === "chart") charts.push(obj);
158
- for (const key of ["operations", "sections", "content", "items", "children"]) {
158
+ for (const key of ["operations", "sections", "content", "items", "children", "column1", "column2"]) {
159
159
  if (Array.isArray(obj[key])) charts.push(...collectChartOps(obj[key]));
160
160
  }
161
161
  if (obj.pageTemplates) {
@@ -50,6 +50,10 @@
50
50
  */
51
51
 
52
52
  let _ChartJSNodeCanvas = null;
53
+ const _rendererCache = new Map();
54
+ const MAX_RENDERER_CACHE_SIZE = 8;
55
+ const DEFAULT_CANVAS_WIDTH = 1600;
56
+ const DEFAULT_CANVAS_HEIGHT = 800;
53
57
 
54
58
  function getCanvas() {
55
59
  if (!_ChartJSNodeCanvas) {
@@ -64,6 +68,46 @@ function getCanvas() {
64
68
  return _ChartJSNodeCanvas;
65
69
  }
66
70
 
71
+ function getRenderer(width, height) {
72
+ const ChartJSNodeCanvas = getCanvas();
73
+ const key = `${width}x${height}`;
74
+ if (_rendererCache.has(key)) {
75
+ return _rendererCache.get(key);
76
+ }
77
+ if (_rendererCache.size >= MAX_RENDERER_CACHE_SIZE) {
78
+ const oldestKey = _rendererCache.keys().next().value;
79
+ _rendererCache.delete(oldestKey);
80
+ }
81
+ const renderer = new ChartJSNodeCanvas({
82
+ width,
83
+ height,
84
+ backgroundColour: 'transparent',
85
+ });
86
+ _rendererCache.set(key, renderer);
87
+ return renderer;
88
+ }
89
+
90
+ function resolvePositiveNumber(value, fallback, name) {
91
+ if (value === undefined || value === null || value === '') {
92
+ return fallback;
93
+ }
94
+ const numberValue = Number(value);
95
+ if (!Number.isFinite(numberValue) || numberValue <= 0) {
96
+ throw new Error(`chart plugin: ${name} must be a positive number`);
97
+ }
98
+ return numberValue;
99
+ }
100
+
101
+ function cloneChartValue(value) {
102
+ if (!value || typeof value !== 'object') return value;
103
+ if (Array.isArray(value)) return value.map(cloneChartValue);
104
+ const out = {};
105
+ for (const key of Object.keys(value)) {
106
+ out[key] = cloneChartValue(value[key]);
107
+ }
108
+ return out;
109
+ }
110
+
67
111
  /**
68
112
  * Pre-render the chart to a PNG buffer and cache it on the operation.
69
113
  * Call this before renderDocument() for any source containing chart operations.
@@ -77,35 +121,28 @@ function getCanvas() {
77
121
  *
78
122
  * Supported token: {{v}} - replaced with the callback's first argument (value).
79
123
  */
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)); };
124
+ function resolveCallbackTemplates(value) {
125
+ if (!value || typeof value !== 'object') return value;
126
+ if (Array.isArray(value)) return value.map(resolveCallbackTemplates);
127
+
128
+ const out = {};
129
+ for (const key of Object.keys(value)) {
130
+ if (key === 'callback' && typeof value[key] === 'string' && value[key].includes('{{v}}')) {
131
+ const template = value[key];
132
+ out[key] = function (callbackValue) {
133
+ return template.replace(/\{\{v\}\}/g, String(callbackValue));
134
+ };
92
135
  } else {
93
- obj[key] = resolveCallbackTemplates(obj[key]);
136
+ out[key] = resolveCallbackTemplates(value[key]);
94
137
  }
95
138
  }
96
- return obj;
139
+ return out;
97
140
  }
98
141
 
99
142
  async function preRender(operation) {
100
- const ChartJSNodeCanvas = getCanvas();
101
- const canvasW = operation.canvasWidth || 1600;
102
- const canvasH = operation.canvasHeight || 800;
103
-
104
- const canvas = new ChartJSNodeCanvas({
105
- width: canvasW,
106
- height: canvasH,
107
- backgroundColour: 'transparent',
108
- });
143
+ const canvasW = resolvePositiveNumber(operation.canvasWidth, DEFAULT_CANVAS_WIDTH, 'canvasWidth');
144
+ const canvasH = resolvePositiveNumber(operation.canvasHeight, DEFAULT_CANVAS_HEIGHT, 'canvasHeight');
145
+ const canvas = getRenderer(canvasW, canvasH);
109
146
 
110
147
  const options = resolveCallbackTemplates({
111
148
  ...(operation.options || {}),
@@ -115,7 +152,7 @@ async function preRender(operation) {
115
152
 
116
153
  operation._buf = await canvas.renderToBuffer({
117
154
  type: operation.chartType || 'bar',
118
- data: operation.data || { labels: [], datasets: [] },
155
+ data: cloneChartValue(operation.data || { labels: [], datasets: [] }),
119
156
  options,
120
157
  });
121
158
  }
@@ -3,6 +3,7 @@ const _plugins = new Map();
3
3
  const BUILT_IN_TYPES = new Set([
4
4
  "text", "row", "bullet", "divider", "spacer", "hiddenText",
5
5
  "block", "section", "group", "quote", "table", "image",
6
+ "columns", "pageBreak",
6
7
  ]);
7
8
 
8
9
  function registerPlugin(type, handler) {
Binary file
Binary file
Binary file