h17-sspdf 0.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.
- package/LICENSE +201 -0
- package/README.md +409 -0
- package/cli.js +135 -0
- package/core/font-registry.js +47 -0
- package/core/pdf-core.js +853 -0
- package/core/plugin-chart.js +109 -0
- package/core/plugin-registry.js +46 -0
- package/core/render-document.js +931 -0
- package/core/shapes.js +354 -0
- package/core/units.js +39 -0
- package/core/validate.js +151 -0
- package/fonts/crimson-text.js +13 -0
- package/fonts/fira-code.js +9 -0
- package/fonts/ibm-plex-sans.js +13 -0
- package/fonts/inter.js +9 -0
- package/fonts/jetbrains-mono.js +13 -0
- package/fonts/lato.js +13 -0
- package/fonts/libre-baskerville.js +11 -0
- package/fonts/lora.js +13 -0
- package/fonts/merriweather.js +13 -0
- package/fonts/montserrat.js +13 -0
- package/fonts/nunito.js +13 -0
- package/fonts/open-sans.js +13 -0
- package/fonts/oswald.js +9 -0
- package/fonts/playfair-display.js +13 -0
- package/fonts/pt-sans.js +13 -0
- package/fonts/raleway.js +13 -0
- package/fonts/roboto.js +13 -0
- package/fonts/source-code-pro.js +13 -0
- package/fonts/source-serif-4.js +13 -0
- package/fonts/work-sans.js +13 -0
- package/index.js +24 -0
- package/package.json +62 -0
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
const { PDFCore, getStyleMarginsMm, getTextPaddingMm, applyTextTransform } = require("./pdf-core");
|
|
2
|
+
const { pxToMm, resolveLineHeightMm } = require("./units");
|
|
3
|
+
const { registerThemeFonts } = require("./font-registry");
|
|
4
|
+
const { getPlugin, hasPlugin } = require("./plugin-registry");
|
|
5
|
+
const { validateSource, validateTheme, validateSourceAgainstTheme } = require("./validate");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render a document by executing labeled operations.
|
|
9
|
+
*
|
|
10
|
+
* pageTemplates format (inside source):
|
|
11
|
+
* {
|
|
12
|
+
* header: operations[],
|
|
13
|
+
* footer: operations[],
|
|
14
|
+
* headerHeightMm: number, // reserves body space
|
|
15
|
+
* footerHeightMm: number, // reserves body space
|
|
16
|
+
* headerBypassMargins: true // default true
|
|
17
|
+
* footerBypassMargins: true // default true
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* @param {object} input
|
|
21
|
+
* @param {object} input.source Source-of-truth JSON
|
|
22
|
+
* @param {object} input.theme Theme with `labels` style map
|
|
23
|
+
* @param {string} [input.outputPath]
|
|
24
|
+
* @returns {{ buffer: Buffer, operationsCount: number, core: PDFCore }}
|
|
25
|
+
*/
|
|
26
|
+
function renderDocument(input) {
|
|
27
|
+
if (!input || typeof input !== "object") {
|
|
28
|
+
throw new Error("renderDocument: input is required");
|
|
29
|
+
}
|
|
30
|
+
if (!input.source) {
|
|
31
|
+
throw new Error("renderDocument: source is required");
|
|
32
|
+
}
|
|
33
|
+
if (!input.theme) {
|
|
34
|
+
throw new Error("renderDocument: theme is required");
|
|
35
|
+
}
|
|
36
|
+
if (input.validate) {
|
|
37
|
+
validateSource(input.source);
|
|
38
|
+
validateTheme(input.theme);
|
|
39
|
+
validateSourceAgainstTheme(input.source, input.theme);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const built = normalizeSourceModel(input.source);
|
|
43
|
+
const runtimeTheme = buildRuntimeTheme(input.theme, built.pageTemplates);
|
|
44
|
+
|
|
45
|
+
const core = new PDFCore(runtimeTheme);
|
|
46
|
+
registerThemeFonts(core, runtimeTheme);
|
|
47
|
+
|
|
48
|
+
installPageTemplates(core, runtimeTheme, built.pageTemplates);
|
|
49
|
+
|
|
50
|
+
executeOperations({
|
|
51
|
+
core,
|
|
52
|
+
theme: runtimeTheme,
|
|
53
|
+
operations: built.operations,
|
|
54
|
+
indexPrefix: "",
|
|
55
|
+
templateMode: false,
|
|
56
|
+
templateBypassMargins: false,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const buffer = core.toBuffer();
|
|
60
|
+
if (input.outputPath) {
|
|
61
|
+
core.saveToFile(input.outputPath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
buffer,
|
|
66
|
+
operationsCount: countLeafOperations(built.operations),
|
|
67
|
+
core,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Normalize direct JSON contract into operations.
|
|
73
|
+
* Accepted root forms:
|
|
74
|
+
* - operations[]
|
|
75
|
+
* - { operations, pageTemplates? }
|
|
76
|
+
* - { content|items|sections|children, pageTemplates? } wrappers
|
|
77
|
+
*
|
|
78
|
+
* Node wrappers can nest.
|
|
79
|
+
* - `section` wrappers are normalized as parent `block` operations.
|
|
80
|
+
* - other wrappers are flattened while preserving order.
|
|
81
|
+
* A wrapper node is any object with content/items/sections/children array.
|
|
82
|
+
*
|
|
83
|
+
* "group" is accepted as alias for "block".
|
|
84
|
+
*
|
|
85
|
+
* @param {object|Array<object>} source
|
|
86
|
+
* @returns {{ operations: Array<object>, pageTemplates: object|null }}
|
|
87
|
+
*/
|
|
88
|
+
function normalizeSourceModel(source) {
|
|
89
|
+
if (Array.isArray(source)) {
|
|
90
|
+
return { operations: normalizeNodes(source, "source"), pageTemplates: null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!source || typeof source !== "object") {
|
|
94
|
+
throw new Error("renderDocument: source must be an object or operation array");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (Array.isArray(source.operations)) {
|
|
98
|
+
return {
|
|
99
|
+
operations: normalizeNodes(source.operations, "source.operations"),
|
|
100
|
+
pageTemplates: source.pageTemplates || null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const rootChildren = getNodeChildren(source);
|
|
105
|
+
if (rootChildren) {
|
|
106
|
+
return {
|
|
107
|
+
operations: normalizeNodes(rootChildren, "source"),
|
|
108
|
+
pageTemplates: source.pageTemplates || null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new Error(
|
|
113
|
+
"renderDocument: source must provide operations[] or wrapper arrays (content/items/sections/children)"
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildRuntimeTheme(theme, pageTemplates) {
|
|
118
|
+
const page = Object.assign({}, theme.page || {});
|
|
119
|
+
const hasHeader = pageTemplates && Array.isArray(pageTemplates.header) && pageTemplates.header.length > 0;
|
|
120
|
+
const hasFooter = pageTemplates && Array.isArray(pageTemplates.footer) && pageTemplates.footer.length > 0;
|
|
121
|
+
|
|
122
|
+
if (pageTemplates && pageTemplates.headerHeightMm !== undefined) {
|
|
123
|
+
page.headerHeightMm = Number(pageTemplates.headerHeightMm) || 0;
|
|
124
|
+
} else if (hasHeader && page.headerHeightMm === undefined) {
|
|
125
|
+
page.headerHeightMm = 12;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (pageTemplates && pageTemplates.footerHeightMm !== undefined) {
|
|
129
|
+
page.footerHeightMm = Number(pageTemplates.footerHeightMm) || 0;
|
|
130
|
+
} else if (hasFooter && page.footerHeightMm === undefined) {
|
|
131
|
+
page.footerHeightMm = 10;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return Object.assign({}, theme, { page });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function installPageTemplates(core, theme, pageTemplates) {
|
|
138
|
+
if (!pageTemplates || typeof pageTemplates !== "object") {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const headerOps = Array.isArray(pageTemplates.header) ? pageTemplates.header : [];
|
|
143
|
+
const footerOps = Array.isArray(pageTemplates.footer) ? pageTemplates.footer : [];
|
|
144
|
+
if (headerOps.length === 0 && footerOps.length === 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const headerBypassMargins = pageTemplates.headerBypassMargins !== false;
|
|
149
|
+
const footerBypassMargins = pageTemplates.footerBypassMargins !== false;
|
|
150
|
+
|
|
151
|
+
const renderTemplatesForCurrentPage = () => {
|
|
152
|
+
if (headerOps.length > 0) {
|
|
153
|
+
const startY = pageTemplates.headerStartMm !== undefined
|
|
154
|
+
? Number(pageTemplates.headerStartMm) || 0
|
|
155
|
+
: 0;
|
|
156
|
+
renderTemplateRegion(core, theme, headerOps, startY, "header", headerBypassMargins);
|
|
157
|
+
}
|
|
158
|
+
if (footerOps.length > 0) {
|
|
159
|
+
const defaultFooterStart = core.pageHeight - (core.footerHeightMm || 0);
|
|
160
|
+
const startY = pageTemplates.footerStartMm !== undefined
|
|
161
|
+
? Number(pageTemplates.footerStartMm) || 0
|
|
162
|
+
: defaultFooterStart;
|
|
163
|
+
renderTemplateRegion(core, theme, footerOps, startY, "footer", footerBypassMargins);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const originalAddPage = core.addPage.bind(core);
|
|
168
|
+
core.addPage = () => {
|
|
169
|
+
originalAddPage();
|
|
170
|
+
renderTemplatesForCurrentPage();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
renderTemplatesForCurrentPage();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderTemplateRegion(core, theme, operations, startY, regionName, bypassMargins) {
|
|
177
|
+
const savedY = core.getCursorY();
|
|
178
|
+
core.setCursorY(startY);
|
|
179
|
+
|
|
180
|
+
executeOperations({
|
|
181
|
+
core,
|
|
182
|
+
theme,
|
|
183
|
+
operations,
|
|
184
|
+
indexPrefix: `template:${regionName}.`,
|
|
185
|
+
templateMode: true,
|
|
186
|
+
templateBypassMargins: bypassMargins,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
core.setCursorY(savedY);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function executeOperations(ctx) {
|
|
193
|
+
const { core, theme, operations, indexPrefix, templateMode, templateBypassMargins, insideContainer } = ctx;
|
|
194
|
+
if (!Array.isArray(operations)) {
|
|
195
|
+
throw new Error("Operations must be an array");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (let i = 0; i < operations.length; i += 1) {
|
|
199
|
+
const operation = operations[i];
|
|
200
|
+
const index = `${indexPrefix}${i}`;
|
|
201
|
+
|
|
202
|
+
if (!templateMode) {
|
|
203
|
+
const keepCount = normalizeKeepWithNext(operation && operation.keepWithNext);
|
|
204
|
+
if (keepCount > 0) {
|
|
205
|
+
const grouped = operations.slice(i, i + 1 + keepCount);
|
|
206
|
+
const groupedHeight = estimateOperationsHeight({
|
|
207
|
+
core,
|
|
208
|
+
theme,
|
|
209
|
+
operations: grouped,
|
|
210
|
+
indexPrefix: `${index}.keep.`,
|
|
211
|
+
});
|
|
212
|
+
core.ensureSpace(groupedHeight);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
core.withDocumentState(() => {
|
|
217
|
+
executeOperation({
|
|
218
|
+
core,
|
|
219
|
+
theme,
|
|
220
|
+
operation,
|
|
221
|
+
index,
|
|
222
|
+
templateMode,
|
|
223
|
+
templateBypassMargins,
|
|
224
|
+
insideContainer,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalizeNodes(nodes, path) {
|
|
231
|
+
if (!Array.isArray(nodes)) {
|
|
232
|
+
throw new Error(`${path} must be an array`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const operations = [];
|
|
236
|
+
nodes.forEach((node, i) => {
|
|
237
|
+
const nodePath = `${path}[${i}]`;
|
|
238
|
+
operations.push(...normalizeNode(node, nodePath));
|
|
239
|
+
});
|
|
240
|
+
return operations;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeNode(node, path) {
|
|
244
|
+
if (!node || typeof node !== "object") {
|
|
245
|
+
throw new Error(`${path} must be an object`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (node.type === "quote") {
|
|
249
|
+
if (!node.label) {
|
|
250
|
+
throw new Error(`${path} type "quote" requires label`);
|
|
251
|
+
}
|
|
252
|
+
const quoteText = node.content !== undefined ? node.content : node.text;
|
|
253
|
+
if (quoteText === undefined) {
|
|
254
|
+
throw new Error(`${path} type "quote" requires content or text`);
|
|
255
|
+
}
|
|
256
|
+
const attrLabel = node.attributionLabel || (node.label + ".attribution");
|
|
257
|
+
const attrText = node.author !== undefined ? node.author : node.attribution;
|
|
258
|
+
const children = [
|
|
259
|
+
{ type: "text", label: node.label, text: quoteText, xMm: node.xMm, maxWidthMm: node.maxWidthMm },
|
|
260
|
+
];
|
|
261
|
+
if (attrText) {
|
|
262
|
+
children.push({ type: "text", label: attrLabel, text: attrText, xMm: node.xMm, maxWidthMm: node.maxWidthMm });
|
|
263
|
+
}
|
|
264
|
+
return [{
|
|
265
|
+
type: "block",
|
|
266
|
+
label: node.label,
|
|
267
|
+
keepTogether: true,
|
|
268
|
+
xMm: node.xMm,
|
|
269
|
+
maxWidthMm: node.maxWidthMm,
|
|
270
|
+
children,
|
|
271
|
+
}];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (isOperationType(node.type)) {
|
|
275
|
+
return expandOperationNode(node, path);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const children = getNodeChildren(node);
|
|
279
|
+
if (children) {
|
|
280
|
+
if (isParentContainerType(node.type)) {
|
|
281
|
+
const childrenOps = normalizeNodes(children, `${path}.children`);
|
|
282
|
+
return [toParentBlock(node, childrenOps)];
|
|
283
|
+
}
|
|
284
|
+
return normalizeNodes(children, `${path}.children`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (node.type === "group") {
|
|
288
|
+
throw new Error(`${path} type "group" requires children/content/items/sections array`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (node.type === "block") {
|
|
292
|
+
throw new Error(`${path} type "block" requires children/content/items/sections array`);
|
|
293
|
+
}
|
|
294
|
+
if (node.type === "section") {
|
|
295
|
+
throw new Error(`${path} type "section" requires children/content/items/sections array`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (node.label && (node.text !== undefined || node.value !== undefined)) {
|
|
299
|
+
const raw = node.text !== undefined ? node.text : node.value;
|
|
300
|
+
if (Array.isArray(raw)) {
|
|
301
|
+
return raw
|
|
302
|
+
.map((item) => String(item || "").trim())
|
|
303
|
+
.filter(Boolean)
|
|
304
|
+
.map((text) => ({
|
|
305
|
+
type: "text",
|
|
306
|
+
label: node.label,
|
|
307
|
+
text,
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
return [
|
|
311
|
+
{
|
|
312
|
+
type: "text",
|
|
313
|
+
label: node.label,
|
|
314
|
+
text: raw,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
throw new Error(
|
|
320
|
+
`${path} is not a valid operation node. Provide operation.type or wrapper children arrays`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isOperationType(type) {
|
|
325
|
+
return type === "text"
|
|
326
|
+
|| type === "row"
|
|
327
|
+
|| type === "bullet"
|
|
328
|
+
|| type === "divider"
|
|
329
|
+
|| type === "spacer"
|
|
330
|
+
|| type === "hiddenText"
|
|
331
|
+
|| hasPlugin(type);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function expandOperationNode(node, path) {
|
|
335
|
+
if (node.type === "bullet") {
|
|
336
|
+
const bulletList = getBulletArray(node);
|
|
337
|
+
if (bulletList) {
|
|
338
|
+
return bulletList
|
|
339
|
+
.map((item) => String(item || "").trim())
|
|
340
|
+
.filter(Boolean)
|
|
341
|
+
.map((text) => ({
|
|
342
|
+
type: "bullet",
|
|
343
|
+
label: node.label,
|
|
344
|
+
markerLabel: node.markerLabel,
|
|
345
|
+
marker: node.marker,
|
|
346
|
+
xMm: node.xMm,
|
|
347
|
+
textIndentMm: node.textIndentMm,
|
|
348
|
+
maxWidthMm: node.maxWidthMm,
|
|
349
|
+
keepWithNext: node.keepWithNext,
|
|
350
|
+
text,
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
return [node];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (node.type === "text" && Array.isArray(node.text)) {
|
|
357
|
+
return node.text
|
|
358
|
+
.map((item) => String(item || "").trim())
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
.map((text) => ({
|
|
361
|
+
type: "text",
|
|
362
|
+
label: node.label,
|
|
363
|
+
xMm: node.xMm,
|
|
364
|
+
maxWidthMm: node.maxWidthMm,
|
|
365
|
+
align: node.align,
|
|
366
|
+
wrap: node.wrap,
|
|
367
|
+
advance: node.advance,
|
|
368
|
+
keepWithNext: node.keepWithNext,
|
|
369
|
+
text,
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if ((node.type === "hiddenText") && Array.isArray(node.text)) {
|
|
374
|
+
return [
|
|
375
|
+
Object.assign({}, node, {
|
|
376
|
+
text: node.text.map((item) => String(item || "")).join(" "),
|
|
377
|
+
}),
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return [node];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function getBulletArray(node) {
|
|
385
|
+
if (Array.isArray(node.text)) {
|
|
386
|
+
return node.text;
|
|
387
|
+
}
|
|
388
|
+
if (Array.isArray(node.items)) {
|
|
389
|
+
return node.items;
|
|
390
|
+
}
|
|
391
|
+
if (Array.isArray(node.bullets)) {
|
|
392
|
+
return node.bullets;
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function isParentContainerType(type) {
|
|
398
|
+
return type === "block" || type === "group" || type === "section";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function toParentBlock(node, childrenOps) {
|
|
402
|
+
const out = {};
|
|
403
|
+
Object.keys(node).forEach((key) => {
|
|
404
|
+
if (key === "children" || key === "content" || key === "items" || key === "sections") {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
out[key] = node[key];
|
|
408
|
+
});
|
|
409
|
+
out.type = "block";
|
|
410
|
+
out.children = childrenOps;
|
|
411
|
+
|
|
412
|
+
// `section` defines a parent boundary but should not force keepTogether unless explicit.
|
|
413
|
+
if (node.type === "section" && out.keepTogether === undefined) {
|
|
414
|
+
out.keepTogether = false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function getNodeChildren(node) {
|
|
421
|
+
if (!node || typeof node !== "object") {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
if (Array.isArray(node.children)) {
|
|
425
|
+
return node.children;
|
|
426
|
+
}
|
|
427
|
+
if (Array.isArray(node.content)) {
|
|
428
|
+
return node.content;
|
|
429
|
+
}
|
|
430
|
+
if (Array.isArray(node.items)) {
|
|
431
|
+
return node.items;
|
|
432
|
+
}
|
|
433
|
+
if (Array.isArray(node.sections)) {
|
|
434
|
+
return node.sections;
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Execute one operation.
|
|
441
|
+
* Supported types:
|
|
442
|
+
* - text
|
|
443
|
+
* - row
|
|
444
|
+
* - bullet
|
|
445
|
+
* - divider
|
|
446
|
+
* - spacer
|
|
447
|
+
* - hiddenText
|
|
448
|
+
* - block
|
|
449
|
+
*
|
|
450
|
+
* block format:
|
|
451
|
+
* {
|
|
452
|
+
* type: "block",
|
|
453
|
+
* keepTogether: true,
|
|
454
|
+
* children: [ ...operations ],
|
|
455
|
+
* spaceAfterMm: 2
|
|
456
|
+
* }
|
|
457
|
+
*
|
|
458
|
+
* @param {object} ctx
|
|
459
|
+
*/
|
|
460
|
+
function executeOperation(ctx) {
|
|
461
|
+
const { core, theme, operation, index, templateMode, templateBypassMargins, insideContainer } = ctx;
|
|
462
|
+
if (!operation || !operation.type) {
|
|
463
|
+
throw new Error(`Invalid operation at index ${index}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (operation.type === "block") {
|
|
467
|
+
const children = Array.isArray(operation.children) ? operation.children : null;
|
|
468
|
+
if (!children) {
|
|
469
|
+
throw new Error(`Block operation at index ${index} must define children[]`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const containerStyle = operation.label
|
|
473
|
+
? resolveLabelStyle(theme, operation.label, operation, index, "label", true)
|
|
474
|
+
: null;
|
|
475
|
+
const hasContainer = containerStyle && (
|
|
476
|
+
Array.isArray(containerStyle.backgroundColor)
|
|
477
|
+
|| (Number(containerStyle.borderWidthMm) > 0)
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const childrenHeight = estimateOperationsHeight({
|
|
481
|
+
core,
|
|
482
|
+
theme,
|
|
483
|
+
operations: children,
|
|
484
|
+
indexPrefix: `${index}.block.`,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (!templateMode && (hasContainer || operation.keepTogether !== false)) {
|
|
488
|
+
core.ensureSpace(childrenHeight);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (hasContainer) {
|
|
492
|
+
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
493
|
+
const x = operation.xMm !== undefined ? operation.xMm : bounds.left;
|
|
494
|
+
const width = operation.maxWidthMm !== undefined ? operation.maxWidthMm : (bounds.right - x);
|
|
495
|
+
const containerY = core.getCursorY();
|
|
496
|
+
|
|
497
|
+
core._drawTextContainer({ style: containerStyle, x, y: containerY, width, height: childrenHeight });
|
|
498
|
+
core._drawTextLeftBorder({ style: containerStyle, x, y: containerY, lineHeightMm: 0, blockHeight: childrenHeight });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
executeOperations({
|
|
502
|
+
core,
|
|
503
|
+
theme,
|
|
504
|
+
operations: children,
|
|
505
|
+
indexPrefix: `${index}.`,
|
|
506
|
+
templateMode,
|
|
507
|
+
templateBypassMargins,
|
|
508
|
+
insideContainer: hasContainer || insideContainer,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (operation.spaceAfterMm !== undefined) {
|
|
512
|
+
core.moveDown(Number(operation.spaceAfterMm) || 0);
|
|
513
|
+
} else if (operation.spaceAfterPx !== undefined) {
|
|
514
|
+
core.moveDown(pxToMm(operation.spaceAfterPx));
|
|
515
|
+
} else if (operation.spaceAfterLabel) {
|
|
516
|
+
const style = resolveLabelStyle(theme, operation.spaceAfterLabel, operation, index, "spaceAfterLabel");
|
|
517
|
+
moveFromSpacerStyle(core, style, index);
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (operation.type === "text") {
|
|
523
|
+
const rawStyle = resolveLabelStyle(theme, operation.label, operation, index);
|
|
524
|
+
const style = insideContainer ? stripContainerProps(rawStyle) : rawStyle;
|
|
525
|
+
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
526
|
+
const defaultX = bounds.left;
|
|
527
|
+
const x = operation.xMm !== undefined ? operation.xMm : defaultX;
|
|
528
|
+
validatePointInsideBounds("xMm", x, bounds, index, templateBypassMargins);
|
|
529
|
+
const maxWidth = operation.maxWidthMm !== undefined
|
|
530
|
+
? operation.maxWidthMm
|
|
531
|
+
: bounds.right - x;
|
|
532
|
+
if (maxWidth <= 0) {
|
|
533
|
+
throw new Error(`Operation ${index} (${operation.type}) has non-positive width`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
core.drawText({
|
|
537
|
+
text: applyPageTokens(operation.text, core),
|
|
538
|
+
style,
|
|
539
|
+
x,
|
|
540
|
+
maxWidth,
|
|
541
|
+
align: operation.align,
|
|
542
|
+
wrap: operation.wrap,
|
|
543
|
+
advance: operation.advance,
|
|
544
|
+
allowPageBreak: !templateMode,
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (operation.type === "row") {
|
|
550
|
+
const leftStyle = resolveLabelStyle(theme, operation.leftLabel, operation, index, "leftLabel");
|
|
551
|
+
const rightStyle = resolveLabelStyle(theme, operation.rightLabel, operation, index, "rightLabel");
|
|
552
|
+
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
553
|
+
const defaultLeft = bounds.left;
|
|
554
|
+
const defaultRight = bounds.right;
|
|
555
|
+
const leftPadding = getTextPaddingMm(leftStyle);
|
|
556
|
+
const rightPadding = getTextPaddingMm(rightStyle);
|
|
557
|
+
const xLeft = (operation.xLeftMm !== undefined ? operation.xLeftMm : defaultLeft) + leftPadding.left;
|
|
558
|
+
const xRight = (operation.xRightMm !== undefined ? operation.xRightMm : defaultRight) - rightPadding.right;
|
|
559
|
+
validatePointInsideBounds("xLeftMm", xLeft, bounds, index, templateBypassMargins);
|
|
560
|
+
validatePointInsideBounds("xRightMm", xRight, bounds, index, templateBypassMargins);
|
|
561
|
+
if (xRight < xLeft) {
|
|
562
|
+
throw new Error(`Operation ${index} (${operation.type}) has xRightMm < xLeftMm`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
core.drawRow({
|
|
566
|
+
leftText: applyPageTokens(operation.leftText, core),
|
|
567
|
+
rightText: applyPageTokens(operation.rightText, core),
|
|
568
|
+
leftStyle,
|
|
569
|
+
rightStyle,
|
|
570
|
+
xLeft,
|
|
571
|
+
xRight,
|
|
572
|
+
allowPageBreak: !templateMode,
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (operation.type === "bullet") {
|
|
578
|
+
const textStyle = resolveLabelStyle(theme, operation.label, operation, index);
|
|
579
|
+
const markerStyle = resolveLabelStyle(
|
|
580
|
+
theme,
|
|
581
|
+
operation.markerLabel || "bullet.marker",
|
|
582
|
+
operation,
|
|
583
|
+
index,
|
|
584
|
+
"markerLabel",
|
|
585
|
+
true
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
589
|
+
const defaultX = bounds.left;
|
|
590
|
+
const x = operation.xMm !== undefined ? operation.xMm : defaultX;
|
|
591
|
+
validatePointInsideBounds("xMm", x, bounds, index, templateBypassMargins);
|
|
592
|
+
const textIndentMm = operation.textIndentMm !== undefined
|
|
593
|
+
? operation.textIndentMm
|
|
594
|
+
: (theme.layout && Number(theme.layout.bulletIndentMm)) || 4;
|
|
595
|
+
const rightBoundary = bounds.right;
|
|
596
|
+
const maxWidth = operation.maxWidthMm !== undefined
|
|
597
|
+
? operation.maxWidthMm
|
|
598
|
+
: rightBoundary - (x + textIndentMm);
|
|
599
|
+
if (maxWidth <= 0) {
|
|
600
|
+
throw new Error(`Operation ${index} (${operation.type}) has non-positive width`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
core.drawBullet({
|
|
604
|
+
text: applyPageTokens(operation.text, core),
|
|
605
|
+
textStyle,
|
|
606
|
+
markerStyle: markerStyle || {},
|
|
607
|
+
marker: operation.marker,
|
|
608
|
+
x,
|
|
609
|
+
textIndentMm,
|
|
610
|
+
maxWidth,
|
|
611
|
+
allowPageBreak: !templateMode,
|
|
612
|
+
});
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (operation.type === "divider") {
|
|
617
|
+
const style = resolveLabelStyle(theme, operation.label, operation, index);
|
|
618
|
+
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
619
|
+
const defaultX1 = bounds.left;
|
|
620
|
+
const defaultX2 = bounds.right;
|
|
621
|
+
const x1 = operation.x1Mm !== undefined ? operation.x1Mm : defaultX1;
|
|
622
|
+
const x2 = operation.x2Mm !== undefined ? operation.x2Mm : defaultX2;
|
|
623
|
+
validatePointInsideBounds("x1Mm", x1, bounds, index, templateBypassMargins);
|
|
624
|
+
validatePointInsideBounds("x2Mm", x2, bounds, index, templateBypassMargins);
|
|
625
|
+
if (x2 < x1) {
|
|
626
|
+
throw new Error(`Operation ${index} (${operation.type}) has x2Mm < x1Mm`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
core.drawDivider({
|
|
630
|
+
style,
|
|
631
|
+
x1,
|
|
632
|
+
x2,
|
|
633
|
+
allowPageBreak: !templateMode,
|
|
634
|
+
});
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (operation.type === "spacer") {
|
|
639
|
+
if (operation.mm !== undefined) {
|
|
640
|
+
core.moveDown(Number(operation.mm) || 0);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (operation.px !== undefined) {
|
|
644
|
+
core.moveDown(pxToMm(operation.px));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (operation.label) {
|
|
648
|
+
const style = resolveLabelStyle(theme, operation.label, operation, index);
|
|
649
|
+
moveFromSpacerStyle(core, style, index);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
throw new Error(`Spacer operation at index ${index} must provide mm, px, or label with spaceMm/spacePx`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (operation.type === "hiddenText") {
|
|
656
|
+
const style = resolveLabelStyle(theme, operation.label, operation, index);
|
|
657
|
+
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
658
|
+
const x = operation.xMm !== undefined
|
|
659
|
+
? operation.xMm
|
|
660
|
+
: bounds.left;
|
|
661
|
+
core.drawHiddenText({
|
|
662
|
+
text: applyPageTokens(operation.text, core),
|
|
663
|
+
style,
|
|
664
|
+
x,
|
|
665
|
+
});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const plugin = getPlugin(operation.type);
|
|
670
|
+
if (plugin) {
|
|
671
|
+
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
672
|
+
if (typeof plugin.validate === "function") {
|
|
673
|
+
plugin.validate(operation);
|
|
674
|
+
}
|
|
675
|
+
plugin.render({ core, operation, theme, bounds, index, templateMode });
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
throw new Error(`Unsupported operation type "${operation.type}" at index ${index}`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function estimateOperationsHeight(ctx) {
|
|
683
|
+
const { core, theme, operations, indexPrefix, maxTextLines } = ctx;
|
|
684
|
+
let total = 0;
|
|
685
|
+
operations.forEach((operation, idx) => {
|
|
686
|
+
total += estimateOperationHeight({
|
|
687
|
+
core,
|
|
688
|
+
theme,
|
|
689
|
+
operation,
|
|
690
|
+
index: `${indexPrefix}${idx}`,
|
|
691
|
+
maxTextLines,
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
return total;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function estimateOperationHeight(ctx) {
|
|
698
|
+
const { core, theme, operation, index } = ctx;
|
|
699
|
+
if (!operation || !operation.type) {
|
|
700
|
+
throw new Error(`Invalid operation at index ${index}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (operation.type === "block") {
|
|
704
|
+
const children = Array.isArray(operation.children) ? operation.children : null;
|
|
705
|
+
if (!children) {
|
|
706
|
+
throw new Error(`Block operation at index ${index} must define children[]`);
|
|
707
|
+
}
|
|
708
|
+
let total = estimateOperationsHeight({
|
|
709
|
+
core,
|
|
710
|
+
theme,
|
|
711
|
+
operations: children,
|
|
712
|
+
indexPrefix: `${index}.block.`,
|
|
713
|
+
});
|
|
714
|
+
if (operation.spaceAfterMm !== undefined) {
|
|
715
|
+
total += Number(operation.spaceAfterMm) || 0;
|
|
716
|
+
} else if (operation.spaceAfterPx !== undefined) {
|
|
717
|
+
total += pxToMm(operation.spaceAfterPx);
|
|
718
|
+
} else if (operation.spaceAfterLabel) {
|
|
719
|
+
const style = resolveLabelStyle(theme, operation.spaceAfterLabel, operation, index, "spaceAfterLabel");
|
|
720
|
+
total += estimateSpacerFromStyle(style, index);
|
|
721
|
+
}
|
|
722
|
+
return total;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (operation.type === "text") {
|
|
726
|
+
const style = resolveLabelStyle(theme, operation.label, operation, index);
|
|
727
|
+
const lineHeightMm = style.lineHeightMm || resolveLineHeightMm(Number(style.fontSize) || 10, style.lineHeight);
|
|
728
|
+
const margins = getStyleMarginsMm(style);
|
|
729
|
+
const padding = getTextPaddingMm(style);
|
|
730
|
+
|
|
731
|
+
let lineCount;
|
|
732
|
+
if (ctx.maxTextLines > 0) {
|
|
733
|
+
lineCount = ctx.maxTextLines;
|
|
734
|
+
} else {
|
|
735
|
+
const x = operation.xMm !== undefined ? operation.xMm : core.marginLeftMm;
|
|
736
|
+
const maxWidth = operation.maxWidthMm !== undefined
|
|
737
|
+
? operation.maxWidthMm
|
|
738
|
+
: core.pageWidth - core.marginRightMm - x;
|
|
739
|
+
const innerWidth = maxWidth - padding.left - padding.right;
|
|
740
|
+
if (innerWidth <= 0) {
|
|
741
|
+
throw new Error(`Operation ${index} (${operation.type}) has non-positive inner width after padding`);
|
|
742
|
+
}
|
|
743
|
+
const text = applyTextTransform(
|
|
744
|
+
String(operation.text !== undefined ? operation.text : ""),
|
|
745
|
+
style.textTransform
|
|
746
|
+
);
|
|
747
|
+
const lines = operation.wrap === false
|
|
748
|
+
? [text]
|
|
749
|
+
: core.measureWrappedLines(text, innerWidth, style);
|
|
750
|
+
lineCount = Math.max(lines.length, 1);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return margins.top + padding.top + (lineCount * lineHeightMm) + padding.bottom + margins.bottom;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (operation.type === "row") {
|
|
757
|
+
const leftStyle = resolveLabelStyle(theme, operation.leftLabel, operation, index, "leftLabel");
|
|
758
|
+
const rightStyle = resolveLabelStyle(theme, operation.rightLabel, operation, index, "rightLabel");
|
|
759
|
+
const leftMargins = getStyleMarginsMm(leftStyle);
|
|
760
|
+
const rightMargins = getStyleMarginsMm(rightStyle);
|
|
761
|
+
const top = Math.max(leftMargins.top, rightMargins.top);
|
|
762
|
+
const bottom = Math.max(leftMargins.bottom, rightMargins.bottom);
|
|
763
|
+
const leftHeight = resolveLineHeightMm(Number(leftStyle.fontSize) || 10, leftStyle.lineHeight);
|
|
764
|
+
const rightHeight = resolveLineHeightMm(Number(rightStyle.fontSize) || 10, rightStyle.lineHeight);
|
|
765
|
+
return top + Math.max(leftHeight, rightHeight) + bottom;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (operation.type === "bullet") {
|
|
769
|
+
const style = resolveLabelStyle(theme, operation.label, operation, index);
|
|
770
|
+
const x = operation.xMm !== undefined ? operation.xMm : core.marginLeftMm;
|
|
771
|
+
const textIndentMm = operation.textIndentMm !== undefined
|
|
772
|
+
? operation.textIndentMm
|
|
773
|
+
: (theme.layout && Number(theme.layout.bulletIndentMm)) || 4;
|
|
774
|
+
const maxWidth = operation.maxWidthMm !== undefined
|
|
775
|
+
? operation.maxWidthMm
|
|
776
|
+
: core.pageWidth - core.marginRightMm - (x + textIndentMm);
|
|
777
|
+
const text = applyTextTransform(
|
|
778
|
+
String(operation.text !== undefined ? operation.text : ""),
|
|
779
|
+
style.textTransform
|
|
780
|
+
);
|
|
781
|
+
const lines = core.measureWrappedLines(text, maxWidth, style);
|
|
782
|
+
const lineCount = Math.max(lines.length, 1);
|
|
783
|
+
const lineHeightMm = style.lineHeightMm || resolveLineHeightMm(Number(style.fontSize) || 10, style.lineHeight);
|
|
784
|
+
const margins = getStyleMarginsMm(style);
|
|
785
|
+
return margins.top + (lineCount * lineHeightMm) + margins.bottom;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (operation.type === "divider") {
|
|
789
|
+
const style = resolveLabelStyle(theme, operation.label, operation, index);
|
|
790
|
+
const margins = getStyleMarginsMm(style);
|
|
791
|
+
return margins.top + (Number(style.lineWidth) || 0.3) + margins.bottom;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (operation.type === "spacer") {
|
|
795
|
+
if (operation.mm !== undefined) {
|
|
796
|
+
return Number(operation.mm) || 0;
|
|
797
|
+
}
|
|
798
|
+
if (operation.px !== undefined) {
|
|
799
|
+
return pxToMm(operation.px);
|
|
800
|
+
}
|
|
801
|
+
if (operation.label) {
|
|
802
|
+
const style = resolveLabelStyle(theme, operation.label, operation, index);
|
|
803
|
+
return estimateSpacerFromStyle(style, index);
|
|
804
|
+
}
|
|
805
|
+
throw new Error(`Spacer operation at index ${index} must provide mm, px, or label with spaceMm/spacePx`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (operation.type === "hiddenText") {
|
|
809
|
+
return 0;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const plugin = getPlugin(operation.type);
|
|
813
|
+
if (plugin) {
|
|
814
|
+
if (typeof plugin.estimateHeight === "function") {
|
|
815
|
+
return plugin.estimateHeight({ core, operation, theme, index });
|
|
816
|
+
}
|
|
817
|
+
return 20;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
throw new Error(`Unsupported operation type "${operation.type}" at index ${index}`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function moveFromSpacerStyle(core, style, index) {
|
|
824
|
+
if (style.spaceMm !== undefined) {
|
|
825
|
+
core.moveDown(Number(style.spaceMm) || 0);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (style.spacePx !== undefined) {
|
|
829
|
+
core.moveDown(pxToMm(style.spacePx));
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
throw new Error(`Spacer style at index ${index} must contain spaceMm or spacePx`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function estimateSpacerFromStyle(style, index) {
|
|
836
|
+
if (style.spaceMm !== undefined) {
|
|
837
|
+
return Number(style.spaceMm) || 0;
|
|
838
|
+
}
|
|
839
|
+
if (style.spacePx !== undefined) {
|
|
840
|
+
return pxToMm(style.spacePx);
|
|
841
|
+
}
|
|
842
|
+
throw new Error(`Spacer style at index ${index} must contain spaceMm or spacePx`);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function normalizeKeepWithNext(value) {
|
|
846
|
+
if (value === true) {
|
|
847
|
+
return 1;
|
|
848
|
+
}
|
|
849
|
+
const n = Number(value);
|
|
850
|
+
if (Number.isInteger(n) && n > 0) {
|
|
851
|
+
return n;
|
|
852
|
+
}
|
|
853
|
+
return 0;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function applyPageTokens(value, core) {
|
|
857
|
+
if (value === undefined || value === null) {
|
|
858
|
+
return "";
|
|
859
|
+
}
|
|
860
|
+
const page = core.doc.getNumberOfPages();
|
|
861
|
+
return String(value).replace(/\{\{page\}\}/g, String(page));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function getHorizontalBounds(core, bypassMargins) {
|
|
865
|
+
if (bypassMargins) {
|
|
866
|
+
return { left: 0, right: core.pageWidth };
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
left: core.marginLeftMm,
|
|
870
|
+
right: core.pageWidth - core.marginRightMm,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function validatePointInsideBounds(fieldName, value, bounds, index, bypassMargins) {
|
|
875
|
+
if (bypassMargins) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (value < bounds.left || value > bounds.right) {
|
|
879
|
+
throw new Error(
|
|
880
|
+
`Operation ${index} has ${fieldName}=${value} outside content bounds (${bounds.left}..${bounds.right})`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function countLeafOperations(operations) {
|
|
886
|
+
let count = 0;
|
|
887
|
+
operations.forEach((operation) => {
|
|
888
|
+
if (operation && operation.type === "block" && Array.isArray(operation.children)) {
|
|
889
|
+
count += countLeafOperations(operation.children);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
count += 1;
|
|
893
|
+
});
|
|
894
|
+
return count;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function resolveLabelStyle(theme, label, operation, index, fieldName, optional) {
|
|
898
|
+
if (!label) {
|
|
899
|
+
if (optional) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
const keyName = fieldName || "label";
|
|
903
|
+
throw new Error(`Operation ${index} (${operation.type}) is missing ${keyName}`);
|
|
904
|
+
}
|
|
905
|
+
if (!theme.labels || !theme.labels[label]) {
|
|
906
|
+
if (optional) {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
throw new Error(`No theme style found for label "${label}" (operation ${index}, type ${operation.type})`);
|
|
910
|
+
}
|
|
911
|
+
return theme.labels[label];
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Strip background/border properties from a style so drawText
|
|
916
|
+
* renders text only, without drawing its own container rect.
|
|
917
|
+
*/
|
|
918
|
+
function stripContainerProps(style) {
|
|
919
|
+
const s = Object.assign({}, style);
|
|
920
|
+
delete s.backgroundColor;
|
|
921
|
+
delete s.borderWidthMm;
|
|
922
|
+
delete s.borderWidth;
|
|
923
|
+
delete s.borderColor;
|
|
924
|
+
delete s.borderRadiusMm;
|
|
925
|
+
delete s.leftBorder;
|
|
926
|
+
return s;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
module.exports = {
|
|
930
|
+
renderDocument,
|
|
931
|
+
};
|