h17-sspdf 1.0.2 → 1.2.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 +36 -0
- package/README.md +4 -3
- package/cli.js +1 -1
- package/core/pdf-core.js +27 -0
- package/core/plugin-chart.js +61 -24
- package/core/plugin-registry.js +1 -0
- package/core/render-document.js +85 -0
- package/core/validate.js +15 -1
- package/examples/output/article.pdf +0 -0
- package/examples/output/board-brief.pdf +0 -0
- package/examples/output/certificate.pdf +0 -0
- package/examples/output/event-program.pdf +0 -0
- package/examples/output/financial-report.pdf +3612 -0
- package/examples/output/generic-document.pdf +0 -0
- package/examples/output/invoice-table.pdf +0 -0
- package/examples/output/invoice.pdf +0 -0
- package/examples/output/magazine.pdf +0 -0
- package/examples/output/newspaper-frontpage.pdf +0 -0
- package/examples/output/presentation.pdf +7994 -22
- package/examples/output/styled-invoice.pdf +0 -0
- package/examples/output/two-column.pdf +0 -0
- package/examples/sources/source-newspaper-frontpage.json +1 -1
- package/examples/sources/source-newspaper-hugopalma-arc.json +1 -1
- package/examples/sources/source-sspdf-story.json +11 -11
- package/examples/sources/source-two-column.json +68 -0
- package/examples/themes/theme-two-column.js +105 -0
- package/package.json +8 -2
package/DOCUMENTATION.md
CHANGED
|
@@ -240,6 +240,7 @@ Shared layout defaults read by operation handlers.
|
|
|
240
240
|
layout: {
|
|
241
241
|
bulletIndentMm: 4.5, // text indent after bullet marker (default: 4)
|
|
242
242
|
chartAlign: "center", // chart horizontal alignment: "left" (default) or "center"
|
|
243
|
+
columnGutterMm: 5, // default gutter between columns (required if columns ops omit gutterMm)
|
|
243
244
|
}
|
|
244
245
|
```
|
|
245
246
|
|
|
@@ -645,6 +646,39 @@ Identical to `block` but defaults to `keepTogether: false`. Use sections to grou
|
|
|
645
646
|
|
|
646
647
|
Alias for `block`.
|
|
647
648
|
|
|
649
|
+
#### `columns`
|
|
650
|
+
|
|
651
|
+
Renders two independent operation lists side by side. Each column gets half the content width minus half the gutter. The cursor advances to the bottom of the taller column, so subsequent full-width operations continue from the correct position.
|
|
652
|
+
|
|
653
|
+
```json
|
|
654
|
+
{
|
|
655
|
+
"type": "columns",
|
|
656
|
+
"gutterMm": 5,
|
|
657
|
+
"column1": [
|
|
658
|
+
{ "type": "text", "label": "doc.heading", "text": "Left Column" },
|
|
659
|
+
{ "type": "text", "label": "doc.body", "text": "Content on the left." }
|
|
660
|
+
],
|
|
661
|
+
"column2": [
|
|
662
|
+
{ "type": "text", "label": "doc.heading", "text": "Right Column" },
|
|
663
|
+
{ "type": "text", "label": "doc.body", "text": "Content on the right." }
|
|
664
|
+
]
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
| Field | Required | Type | Description |
|
|
669
|
+
|---|---|---|---|
|
|
670
|
+
| `column1` | yes | array | Operations for the left column |
|
|
671
|
+
| `column2` | yes | array | Operations for the right column |
|
|
672
|
+
| `gutterMm` | no* | number | Gap between columns in mm. *Required if `theme.layout.columnGutterMm` is not set |
|
|
673
|
+
|
|
674
|
+
**Gutter resolution:** `gutterMm` on the operation takes precedence. If omitted, `theme.layout.columnGutterMm` is used. If neither is defined, the engine throws.
|
|
675
|
+
|
|
676
|
+
**Column width:** `(contentWidth - gutterMm) / 2`. Both columns are always equal width.
|
|
677
|
+
|
|
678
|
+
**Cursor after:** `max(col1EndY, col2EndY)` - the bottom of the taller column.
|
|
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).
|
|
681
|
+
|
|
648
682
|
### Inferred text operations
|
|
649
683
|
|
|
650
684
|
If a node has `label` and `text` (or `value`) but no `type`, it's treated as text:
|
|
@@ -675,6 +709,7 @@ The engine positions content using a cursor that starts at `marginTopMm` and mov
|
|
|
675
709
|
| `table` | marginTop + headerRowHeight + sum(dataRowHeights) + marginBottom |
|
|
676
710
|
| `image` | marginTop + paddingTop + imageHeightMm + captionHeight + paddingBottom + marginBottom |
|
|
677
711
|
| `block` | sum of children deltas (+ spaceAfter if defined) |
|
|
712
|
+
| `columns` | max(col1Height, col2Height) |
|
|
678
713
|
|
|
679
714
|
Where:
|
|
680
715
|
- `lineHeightMm = (fontSize × 25.4/72) × lineHeight` (font points → mm × multiplier)
|
|
@@ -1350,6 +1385,7 @@ This applies to all color properties: `color`, `backgroundColor`, `borderColor`,
|
|
|
1350
1385
|
| `section` | `content` | `label`, `keepTogether` |
|
|
1351
1386
|
| `quote` | `label`, `text` | `attribution`, `attributionLabel` |
|
|
1352
1387
|
| `hiddenText` | `label`, `text` | - |
|
|
1388
|
+
| `columns` | `column1`, `column2` | `gutterMm` (falls back to `theme.layout.columnGutterMm`) |
|
|
1353
1389
|
|
|
1354
1390
|
Every operation also accepts `xMm` and `maxWidthMm` to override the theme margins for that operation only.
|
|
1355
1391
|
|
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
|
|
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": "
|
|
202
|
+
"attribution": "- Elena Ward, public records modernization lead",
|
|
203
203
|
"xMm": 22,
|
|
204
204
|
"maxWidthMm": 166
|
|
205
205
|
}
|
|
@@ -270,6 +270,7 @@ labels: {
|
|
|
270
270
|
| `quote` | Blockquote with attribution | `label`, `text`, `attribution` |
|
|
271
271
|
| `block` | Group children, optional background + border | `children`, `keepTogether` |
|
|
272
272
|
| `section` | Logical group, allows breaks inside | `content` |
|
|
273
|
+
| `columns` | Two-column side-by-side layout | `column1`, `column2`, `gutterMm` |
|
|
273
274
|
|
|
274
275
|
### Position overrides
|
|
275
276
|
|
|
@@ -473,7 +474,7 @@ Claude Code skills for generating PDFs and themes are available in the `skills/`
|
|
|
473
474
|
- Single-line `row` cells, no multi-line column pairs
|
|
474
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
|
|
475
476
|
- Charts require the `canvas` npm package (native C++ addon) for server-side rendering; everything else is zero native dependencies
|
|
476
|
-
- 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
|
|
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
478
|
|
|
478
479
|
---
|
|
479
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) {
|
package/core/pdf-core.js
CHANGED
|
@@ -278,6 +278,33 @@ class PDFCore {
|
|
|
278
278
|
this.cursorY = Number(y) || this.cursorY;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Narrow the content area to a column band. Saves current margins and cursor.
|
|
283
|
+
* @param {number} left Absolute left x in mm
|
|
284
|
+
* @param {number} right Absolute right x in mm
|
|
285
|
+
*/
|
|
286
|
+
_enterColumnBounds(left, right) {
|
|
287
|
+
if (!this._colMarginStack) this._colMarginStack = [];
|
|
288
|
+
this._colMarginStack.push({ left: this.marginLeftMm, right: this.marginRightMm });
|
|
289
|
+
this.marginLeftMm = left;
|
|
290
|
+
this.marginRightMm = this.pageWidth - right;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Restore margins saved by _enterColumnBounds. Returns the cursor position
|
|
295
|
+
* reached at end of column so the caller can track the tallest column.
|
|
296
|
+
* @param {number} savedCursorY The cursor to restore after saving column end Y
|
|
297
|
+
* @returns {number} Cursor Y at end of column
|
|
298
|
+
*/
|
|
299
|
+
_exitColumnBounds(savedCursorY) {
|
|
300
|
+
const endY = this.cursorY;
|
|
301
|
+
const saved = this._colMarginStack.pop();
|
|
302
|
+
this.marginLeftMm = saved.left;
|
|
303
|
+
this.marginRightMm = saved.right;
|
|
304
|
+
this.cursorY = savedCursorY;
|
|
305
|
+
return endY;
|
|
306
|
+
}
|
|
307
|
+
|
|
281
308
|
/**
|
|
282
309
|
* Apply a text style to jsPDF.
|
|
283
310
|
* @param {object} style
|
package/core/plugin-chart.js
CHANGED
|
@@ -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(
|
|
81
|
-
if (!
|
|
82
|
-
if (Array.isArray(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
136
|
+
out[key] = resolveCallbackTemplates(value[key]);
|
|
94
137
|
}
|
|
95
138
|
}
|
|
96
|
-
return
|
|
139
|
+
return out;
|
|
97
140
|
}
|
|
98
141
|
|
|
99
142
|
async function preRender(operation) {
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
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
|
|
155
|
+
data: cloneChartValue(operation.data || { labels: [], datasets: [] }),
|
|
119
156
|
options,
|
|
120
157
|
});
|
|
121
158
|
}
|
package/core/plugin-registry.js
CHANGED
package/core/render-document.js
CHANGED
|
@@ -284,6 +284,14 @@ function normalizeNode(node, path) {
|
|
|
284
284
|
}];
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
if (node.type === "columns") {
|
|
288
|
+
return [{
|
|
289
|
+
...node,
|
|
290
|
+
column1: Array.isArray(node.column1) ? normalizeNodes(node.column1, `${path}.column1`) : [],
|
|
291
|
+
column2: Array.isArray(node.column2) ? normalizeNodes(node.column2, `${path}.column2`) : [],
|
|
292
|
+
}];
|
|
293
|
+
}
|
|
294
|
+
|
|
287
295
|
if (isOperationType(node.type)) {
|
|
288
296
|
return expandOperationNode(node, path);
|
|
289
297
|
}
|
|
@@ -344,6 +352,7 @@ function isOperationType(type) {
|
|
|
344
352
|
|| type === "hiddenText"
|
|
345
353
|
|| type === "table"
|
|
346
354
|
|| type === "image"
|
|
355
|
+
|| type === "columns"
|
|
347
356
|
|| hasPlugin(type);
|
|
348
357
|
}
|
|
349
358
|
|
|
@@ -873,6 +882,39 @@ function executeOperation(ctx) {
|
|
|
873
882
|
return;
|
|
874
883
|
}
|
|
875
884
|
|
|
885
|
+
if (operation.type === "columns") {
|
|
886
|
+
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
887
|
+
const contentWidth = bounds.right - bounds.left;
|
|
888
|
+
const gutterMm = resolveColumnsGutter(operation, theme, index);
|
|
889
|
+
const colWidth = (contentWidth - gutterMm) / 2;
|
|
890
|
+
const col1Left = bounds.left;
|
|
891
|
+
const col1Right = bounds.left + colWidth;
|
|
892
|
+
const col2Left = col1Right + gutterMm;
|
|
893
|
+
const col2Right = bounds.right;
|
|
894
|
+
const startY = core.getCursorY();
|
|
895
|
+
|
|
896
|
+
core._enterColumnBounds(col1Left, col1Right);
|
|
897
|
+
executeOperations({
|
|
898
|
+
core, theme,
|
|
899
|
+
operations: Array.isArray(operation.column1) ? operation.column1 : [],
|
|
900
|
+
indexPrefix: `${index}.col1.`,
|
|
901
|
+
templateMode, templateBypassMargins,
|
|
902
|
+
});
|
|
903
|
+
const col1EndY = core._exitColumnBounds(startY);
|
|
904
|
+
|
|
905
|
+
core._enterColumnBounds(col2Left, col2Right);
|
|
906
|
+
executeOperations({
|
|
907
|
+
core, theme,
|
|
908
|
+
operations: Array.isArray(operation.column2) ? operation.column2 : [],
|
|
909
|
+
indexPrefix: `${index}.col2.`,
|
|
910
|
+
templateMode, templateBypassMargins,
|
|
911
|
+
});
|
|
912
|
+
const col2EndY = core._exitColumnBounds(startY);
|
|
913
|
+
|
|
914
|
+
core.setCursorY(Math.max(col1EndY, col2EndY));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
876
918
|
const plugin = getPlugin(operation.type);
|
|
877
919
|
if (plugin) {
|
|
878
920
|
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
@@ -1120,6 +1162,35 @@ function estimateOperationHeight(ctx) {
|
|
|
1120
1162
|
return margins.top + padding.top + imgHeightMm + captionHeight + padding.bottom + margins.bottom;
|
|
1121
1163
|
}
|
|
1122
1164
|
|
|
1165
|
+
if (operation.type === "columns") {
|
|
1166
|
+
const contentWidth = core.pageWidth - core.marginLeftMm - core.marginRightMm;
|
|
1167
|
+
const gutterMm = resolveColumnsGutter(operation, theme, index);
|
|
1168
|
+
const colWidth = (contentWidth - gutterMm) / 2;
|
|
1169
|
+
|
|
1170
|
+
const savedMarginLeft = core.marginLeftMm;
|
|
1171
|
+
const savedMarginRight = core.marginRightMm;
|
|
1172
|
+
|
|
1173
|
+
core.marginLeftMm = savedMarginLeft;
|
|
1174
|
+
core.marginRightMm = core.pageWidth - (savedMarginLeft + colWidth);
|
|
1175
|
+
const col1H = estimateOperationsHeight({
|
|
1176
|
+
core, theme,
|
|
1177
|
+
operations: Array.isArray(operation.column1) ? operation.column1 : [],
|
|
1178
|
+
indexPrefix: `${index}.col1.`,
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
core.marginLeftMm = savedMarginLeft + colWidth + gutterMm;
|
|
1182
|
+
core.marginRightMm = core.pageWidth - (savedMarginLeft + colWidth + gutterMm + colWidth);
|
|
1183
|
+
const col2H = estimateOperationsHeight({
|
|
1184
|
+
core, theme,
|
|
1185
|
+
operations: Array.isArray(operation.column2) ? operation.column2 : [],
|
|
1186
|
+
indexPrefix: `${index}.col2.`,
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
core.marginLeftMm = savedMarginLeft;
|
|
1190
|
+
core.marginRightMm = savedMarginRight;
|
|
1191
|
+
return Math.max(col1H, col2H);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1123
1194
|
const plugin = getPlugin(operation.type);
|
|
1124
1195
|
if (plugin) {
|
|
1125
1196
|
if (typeof plugin.estimateHeight === "function") {
|
|
@@ -1172,6 +1243,20 @@ function applyPageTokens(value, core) {
|
|
|
1172
1243
|
return String(value).replace(/\{\{page\}\}/g, String(page));
|
|
1173
1244
|
}
|
|
1174
1245
|
|
|
1246
|
+
function resolveColumnsGutter(operation, theme, index) {
|
|
1247
|
+
if (operation.gutterMm !== undefined) {
|
|
1248
|
+
return Number(operation.gutterMm);
|
|
1249
|
+
}
|
|
1250
|
+
const themeGutter = (theme.layout || {}).columnGutterMm;
|
|
1251
|
+
if (themeGutter !== undefined) {
|
|
1252
|
+
return Number(themeGutter);
|
|
1253
|
+
}
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
`columns operation at index ${index} has no gutterMm. ` +
|
|
1256
|
+
`Set gutterMm on the operation or columnGutterMm in theme.layout.`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1175
1260
|
function getHorizontalBounds(core, bypassMargins) {
|
|
1176
1261
|
if (bypassMargins) {
|
|
1177
1262
|
return { left: 0, right: core.pageWidth };
|
package/core/validate.js
CHANGED
|
@@ -2,7 +2,7 @@ const { hasPlugin } = require("./plugin-registry");
|
|
|
2
2
|
|
|
3
3
|
const OPERATION_TYPES = new Set([
|
|
4
4
|
"text", "row", "bullet", "divider", "spacer", "hiddenText",
|
|
5
|
-
"block", "group", "section", "quote", "table", "image",
|
|
5
|
+
"block", "group", "section", "quote", "table", "image", "columns",
|
|
6
6
|
]);
|
|
7
7
|
|
|
8
8
|
function validateSource(source) {
|
|
@@ -108,6 +108,18 @@ function validateOperation(op, path) {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
if (type === "columns") {
|
|
112
|
+
if (!Array.isArray(op.column1)) {
|
|
113
|
+
throw new Error(`${path}: columns requires column1 array`);
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(op.column2)) {
|
|
116
|
+
throw new Error(`${path}: columns requires column2 array`);
|
|
117
|
+
}
|
|
118
|
+
op.column1.forEach((child, i) => validateOperation(child, `${path}.column1[${i}]`));
|
|
119
|
+
op.column2.forEach((child, i) => validateOperation(child, `${path}.column2[${i}]`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
if (hasPlugin(type)) {
|
|
112
124
|
return;
|
|
113
125
|
}
|
|
@@ -147,6 +159,8 @@ function collectLabels(node, missing, themeLabels) {
|
|
|
147
159
|
if (Array.isArray(ops)) {
|
|
148
160
|
ops.forEach((n) => collectLabels(n, missing, themeLabels));
|
|
149
161
|
}
|
|
162
|
+
if (Array.isArray(node.column1)) node.column1.forEach((n) => collectLabels(n, missing, themeLabels));
|
|
163
|
+
if (Array.isArray(node.column2)) node.column2.forEach((n) => collectLabels(n, missing, themeLabels));
|
|
150
164
|
|
|
151
165
|
if (node.label && !themeLabels.has(node.label)) missing.push(node.label);
|
|
152
166
|
if (node.leftLabel && !themeLabels.has(node.leftLabel)) missing.push(node.leftLabel);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|