@signiphi/pdf-compose 0.1.0-beta.0 → 0.1.0-beta.10
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/README.md +265 -0
- package/dist/components/DocumentGenerator.d.ts +3 -0
- package/dist/components/DocumentGenerator.d.ts.map +1 -0
- package/dist/components/EditorPanel.d.ts +15 -0
- package/dist/components/EditorPanel.d.ts.map +1 -0
- package/dist/components/EditorToolbar.d.ts +11 -0
- package/dist/components/EditorToolbar.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +18 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/FieldEditPopover.d.ts +12 -0
- package/dist/components/FieldEditPopover.d.ts.map +1 -0
- package/dist/components/FieldInsertPopover.d.ts +12 -0
- package/dist/components/FieldInsertPopover.d.ts.map +1 -0
- package/dist/components/FieldNodeView.d.ts +3 -0
- package/dist/components/FieldNodeView.d.ts.map +1 -0
- package/dist/components/FieldOverlay.d.ts +9 -0
- package/dist/components/FieldOverlay.d.ts.map +1 -0
- package/dist/components/GeneratePanel.d.ts +15 -0
- package/dist/components/GeneratePanel.d.ts.map +1 -0
- package/dist/components/PreviewPanel.d.ts +12 -0
- package/dist/components/PreviewPanel.d.ts.map +1 -0
- package/dist/components/VariableEditPopover.d.ts +12 -0
- package/dist/components/VariableEditPopover.d.ts.map +1 -0
- package/dist/components/VariableInsertPopover.d.ts +13 -0
- package/dist/components/VariableInsertPopover.d.ts.map +1 -0
- package/dist/components/VariableNodeView.d.ts +3 -0
- package/dist/components/VariableNodeView.d.ts.map +1 -0
- package/dist/components/WatermarkNodeView.d.ts +3 -0
- package/dist/components/WatermarkNodeView.d.ts.map +1 -0
- package/dist/contexts/ThemeContext.d.ts +19 -0
- package/dist/contexts/ThemeContext.d.ts.map +1 -0
- package/dist/extensions/columns-node.d.ts +4 -0
- package/dist/extensions/columns-node.d.ts.map +1 -0
- package/dist/extensions/field-node.d.ts +31 -0
- package/dist/extensions/field-node.d.ts.map +1 -0
- package/dist/extensions/panel-node.d.ts +3 -0
- package/dist/extensions/panel-node.d.ts.map +1 -0
- package/dist/extensions/repeat-node.d.ts +3 -0
- package/dist/extensions/repeat-node.d.ts.map +1 -0
- package/dist/extensions/subtotals-node.d.ts +3 -0
- package/dist/extensions/subtotals-node.d.ts.map +1 -0
- package/dist/extensions/variable-node.d.ts +18 -0
- package/dist/extensions/variable-node.d.ts.map +1 -0
- package/dist/extensions/watermark-node.d.ts +3 -0
- package/dist/extensions/watermark-node.d.ts.map +1 -0
- package/dist/hooks/useDocumentGenerator.d.ts +46 -0
- package/dist/hooks/useDocumentGenerator.d.ts.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3083 -564
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3070 -567
- package/dist/index.mjs.map +1 -1
- package/dist/lib/ui/button.d.ts +12 -0
- package/dist/lib/ui/button.d.ts.map +1 -0
- package/dist/lib/ui/index.d.ts +10 -0
- package/dist/lib/ui/index.d.ts.map +1 -0
- package/dist/lib/ui/input.d.ts +6 -0
- package/dist/lib/ui/input.d.ts.map +1 -0
- package/dist/lib/ui/label.d.ts +8 -0
- package/dist/lib/ui/label.d.ts.map +1 -0
- package/dist/lib/ui/popover.d.ts +8 -0
- package/dist/lib/ui/popover.d.ts.map +1 -0
- package/dist/lib/ui/select.d.ts +14 -0
- package/dist/lib/ui/select.d.ts.map +1 -0
- package/dist/lib/ui/toggle-group.d.ts +13 -0
- package/dist/lib/ui/toggle-group.d.ts.map +1 -0
- package/dist/lib/ui/tooltip.d.ts +8 -0
- package/dist/lib/ui/tooltip.d.ts.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/styles/index.css +349 -194
- package/dist/styles/index.d.ts +1 -0
- package/dist/types/index.d.ts +204 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/error-helpers.d.ts +27 -0
- package/dist/utils/error-helpers.d.ts.map +1 -0
- package/dist/utils/field-helpers.d.ts +31 -0
- package/dist/utils/field-helpers.d.ts.map +1 -0
- package/dist/utils/markdown-parser.d.ts +6 -0
- package/dist/utils/markdown-parser.d.ts.map +1 -0
- package/dist/utils/markdown-validator.d.ts +13 -0
- package/dist/utils/markdown-validator.d.ts.map +1 -0
- package/dist/utils/markdown-writer.d.ts +6 -0
- package/dist/utils/markdown-writer.d.ts.map +1 -0
- package/dist/utils/pdf-generator.d.ts +77 -0
- package/dist/utils/pdf-generator.d.ts.map +1 -0
- package/dist/utils/pdf-metadata.d.ts +58 -0
- package/dist/utils/pdf-metadata.d.ts.map +1 -0
- package/dist/utils/pdf-preview.d.ts +7 -0
- package/dist/utils/pdf-preview.d.ts.map +1 -0
- package/dist/utils/template-pipeline.d.ts +39 -0
- package/dist/utils/template-pipeline.d.ts.map +1 -0
- package/dist/utils/theme-helpers.d.ts +21 -0
- package/dist/utils/theme-helpers.d.ts.map +1 -0
- package/dist/utils/variable-helpers.d.ts +74 -0
- package/dist/utils/variable-helpers.d.ts.map +1 -0
- package/dist/utils/xml-template-parser.d.ts +9 -0
- package/dist/utils/xml-template-parser.d.ts.map +1 -0
- package/package.json +16 -6
- package/src/styles/index.css +140 -68
- package/src/styles/index.d.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -7,6 +7,10 @@ var StarterKit = require('@tiptap/starter-kit');
|
|
|
7
7
|
var Underline = require('@tiptap/extension-underline');
|
|
8
8
|
var TextAlign = require('@tiptap/extension-text-align');
|
|
9
9
|
var Placeholder = require('@tiptap/extension-placeholder');
|
|
10
|
+
var Table = require('@tiptap/extension-table');
|
|
11
|
+
var TableRow = require('@tiptap/extension-table-row');
|
|
12
|
+
var TableCell = require('@tiptap/extension-table-cell');
|
|
13
|
+
var TableHeader = require('@tiptap/extension-table-header');
|
|
10
14
|
var core = require('@tiptap/core');
|
|
11
15
|
var clsx = require('clsx');
|
|
12
16
|
var tailwindMerge = require('tailwind-merge');
|
|
@@ -45,13 +49,17 @@ var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
|
|
|
45
49
|
var Underline__default = /*#__PURE__*/_interopDefault(Underline);
|
|
46
50
|
var TextAlign__default = /*#__PURE__*/_interopDefault(TextAlign);
|
|
47
51
|
var Placeholder__default = /*#__PURE__*/_interopDefault(Placeholder);
|
|
52
|
+
var Table__default = /*#__PURE__*/_interopDefault(Table);
|
|
53
|
+
var TableRow__default = /*#__PURE__*/_interopDefault(TableRow);
|
|
54
|
+
var TableCell__default = /*#__PURE__*/_interopDefault(TableCell);
|
|
55
|
+
var TableHeader__default = /*#__PURE__*/_interopDefault(TableHeader);
|
|
48
56
|
var TooltipPrimitive__namespace = /*#__PURE__*/_interopNamespace(TooltipPrimitive);
|
|
49
57
|
var PopoverPrimitive__namespace = /*#__PURE__*/_interopNamespace(PopoverPrimitive);
|
|
50
58
|
var LabelPrimitive__namespace = /*#__PURE__*/_interopNamespace(LabelPrimitive);
|
|
51
59
|
|
|
52
60
|
var __defProp = Object.defineProperty;
|
|
53
61
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
54
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
|
|
62
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
55
63
|
function cn(...inputs) {
|
|
56
64
|
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
57
65
|
}
|
|
@@ -243,7 +251,10 @@ var VariableNode = core.Node.create({
|
|
|
243
251
|
return {
|
|
244
252
|
varName: { default: "" },
|
|
245
253
|
varLabel: { default: "" },
|
|
246
|
-
varDefault: { default: "" }
|
|
254
|
+
varDefault: { default: "" },
|
|
255
|
+
format: { default: "" },
|
|
256
|
+
suppressZero: { default: "" },
|
|
257
|
+
block: { default: "" }
|
|
247
258
|
};
|
|
248
259
|
},
|
|
249
260
|
parseHTML() {
|
|
@@ -309,6 +320,169 @@ var VariableNode = core.Node.create({
|
|
|
309
320
|
};
|
|
310
321
|
}
|
|
311
322
|
});
|
|
323
|
+
var PanelNode = core.Node.create({
|
|
324
|
+
name: "panel",
|
|
325
|
+
group: "block",
|
|
326
|
+
content: "block+",
|
|
327
|
+
defining: true,
|
|
328
|
+
addAttributes() {
|
|
329
|
+
return {
|
|
330
|
+
title: { default: "" },
|
|
331
|
+
border: { default: "solid" },
|
|
332
|
+
headerStyle: { default: "" }
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
parseHTML() {
|
|
336
|
+
return [{
|
|
337
|
+
tag: "div[data-panel-node]",
|
|
338
|
+
contentElement: "div.panel-content"
|
|
339
|
+
}];
|
|
340
|
+
},
|
|
341
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
342
|
+
const { title, border, headerStyle } = node.attrs;
|
|
343
|
+
const classes = ["panel-node"];
|
|
344
|
+
if (border === "dashed") classes.push("panel-border-dashed");
|
|
345
|
+
else if (border === "none") classes.push("panel-border-none");
|
|
346
|
+
else classes.push("panel-border-solid");
|
|
347
|
+
if (headerStyle === "dark") classes.push("panel-dark");
|
|
348
|
+
const attrs = core.mergeAttributes(
|
|
349
|
+
{ "data-panel-node": "", class: classes.join(" ") },
|
|
350
|
+
HTMLAttributes
|
|
351
|
+
);
|
|
352
|
+
if (title) {
|
|
353
|
+
return [
|
|
354
|
+
"div",
|
|
355
|
+
attrs,
|
|
356
|
+
["div", { class: "panel-title" }, title],
|
|
357
|
+
["div", { class: "panel-content" }, 0]
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
return ["div", attrs, ["div", { class: "panel-content" }, 0]];
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
var ColumnsNode = core.Node.create({
|
|
364
|
+
name: "columns",
|
|
365
|
+
group: "block",
|
|
366
|
+
content: "column+",
|
|
367
|
+
defining: true,
|
|
368
|
+
addAttributes() {
|
|
369
|
+
return {
|
|
370
|
+
split: { default: "50" },
|
|
371
|
+
padX: { default: "0" }
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
parseHTML() {
|
|
375
|
+
return [{ tag: "div[data-columns-node]" }];
|
|
376
|
+
},
|
|
377
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
378
|
+
const split = node.attrs.split || "50";
|
|
379
|
+
return ["div", core.mergeAttributes(
|
|
380
|
+
{
|
|
381
|
+
"data-columns-node": "",
|
|
382
|
+
class: "columns-node",
|
|
383
|
+
style: `--xpc-col-split: ${split}`
|
|
384
|
+
},
|
|
385
|
+
HTMLAttributes
|
|
386
|
+
), 0];
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
var ColumnNode = core.Node.create({
|
|
390
|
+
name: "column",
|
|
391
|
+
group: "",
|
|
392
|
+
content: "block+",
|
|
393
|
+
defining: true,
|
|
394
|
+
addAttributes() {
|
|
395
|
+
return {
|
|
396
|
+
padTop: { default: 0 }
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
parseHTML() {
|
|
400
|
+
return [{ tag: "div[data-column-node]" }];
|
|
401
|
+
},
|
|
402
|
+
renderHTML({ HTMLAttributes }) {
|
|
403
|
+
return ["div", core.mergeAttributes(
|
|
404
|
+
{ "data-column-node": "", class: "column-node" },
|
|
405
|
+
HTMLAttributes
|
|
406
|
+
), 0];
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
function WatermarkNodeView({ node, selected }) {
|
|
410
|
+
const { text } = node.attrs;
|
|
411
|
+
return /* @__PURE__ */ jsxRuntime.jsx(react.NodeViewWrapper, { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
412
|
+
"div",
|
|
413
|
+
{
|
|
414
|
+
"data-watermark-node": "",
|
|
415
|
+
className: cn(
|
|
416
|
+
"inline-flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm my-1",
|
|
417
|
+
"bg-blue-50 text-blue-600 border-blue-200",
|
|
418
|
+
selected && "ring-2 ring-primary ring-offset-1"
|
|
419
|
+
),
|
|
420
|
+
children: [
|
|
421
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Droplets, { size: 14 }),
|
|
422
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
423
|
+
"Watermark: ",
|
|
424
|
+
text || "(empty)"
|
|
425
|
+
] })
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
) });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/extensions/watermark-node.ts
|
|
432
|
+
var WatermarkNode = core.Node.create({
|
|
433
|
+
name: "watermark",
|
|
434
|
+
group: "block",
|
|
435
|
+
atom: true,
|
|
436
|
+
selectable: true,
|
|
437
|
+
draggable: false,
|
|
438
|
+
addAttributes() {
|
|
439
|
+
return {
|
|
440
|
+
text: { default: "" },
|
|
441
|
+
opacity: { default: "0.15" },
|
|
442
|
+
angle: { default: "-45" }
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
parseHTML() {
|
|
446
|
+
return [{ tag: "div[data-watermark-node]" }];
|
|
447
|
+
},
|
|
448
|
+
renderHTML({ HTMLAttributes }) {
|
|
449
|
+
return ["div", core.mergeAttributes({ "data-watermark-node": "" }, HTMLAttributes)];
|
|
450
|
+
},
|
|
451
|
+
addNodeView() {
|
|
452
|
+
return react.ReactNodeViewRenderer(WatermarkNodeView);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
var RepeatNode = core.Node.create({
|
|
456
|
+
name: "repeatBlock",
|
|
457
|
+
group: "block",
|
|
458
|
+
content: "block+",
|
|
459
|
+
addAttributes() {
|
|
460
|
+
return {
|
|
461
|
+
data: { default: "" }
|
|
462
|
+
};
|
|
463
|
+
},
|
|
464
|
+
parseHTML() {
|
|
465
|
+
return [{ tag: "div[data-repeat-node]" }];
|
|
466
|
+
},
|
|
467
|
+
renderHTML({ HTMLAttributes }) {
|
|
468
|
+
return ["div", core.mergeAttributes({ "data-repeat-node": "" }, HTMLAttributes), 0];
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
var SubtotalsNode = core.Node.create({
|
|
472
|
+
name: "subtotalsBlock",
|
|
473
|
+
group: "block",
|
|
474
|
+
content: "block+",
|
|
475
|
+
defining: true,
|
|
476
|
+
parseHTML() {
|
|
477
|
+
return [{ tag: "div[data-subtotals-node]" }];
|
|
478
|
+
},
|
|
479
|
+
renderHTML({ HTMLAttributes }) {
|
|
480
|
+
return ["div", core.mergeAttributes(
|
|
481
|
+
{ "data-subtotals-node": "", class: "subtotals-block" },
|
|
482
|
+
HTMLAttributes
|
|
483
|
+
), 0];
|
|
484
|
+
}
|
|
485
|
+
});
|
|
312
486
|
|
|
313
487
|
// src/types/index.ts
|
|
314
488
|
var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
|
|
@@ -387,158 +561,23 @@ function extractFieldsFromContent(content) {
|
|
|
387
561
|
}
|
|
388
562
|
return fields;
|
|
389
563
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
if (node.content) {
|
|
406
|
-
node.content.forEach(walk);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (content.content) {
|
|
410
|
-
content.content.forEach(walk);
|
|
411
|
-
}
|
|
412
|
-
return Array.from(seen.values());
|
|
413
|
-
}
|
|
414
|
-
function replaceVariablesInContent(content, values) {
|
|
415
|
-
function walkNode(node) {
|
|
416
|
-
if (node.type === "variableNode" && node.attrs) {
|
|
417
|
-
const varName = node.attrs.varName;
|
|
418
|
-
const replacement = values[varName] || node.attrs.varDefault || `[${node.attrs.varLabel || varName}]`;
|
|
419
|
-
const textNode = { type: "text", text: replacement };
|
|
420
|
-
if (node.marks && node.marks.length > 0) {
|
|
421
|
-
textNode.marks = node.marks;
|
|
422
|
-
}
|
|
423
|
-
return textNode;
|
|
424
|
-
}
|
|
425
|
-
if (node.content) {
|
|
426
|
-
return { ...node, content: node.content.map(walkNode) };
|
|
427
|
-
}
|
|
428
|
-
return node;
|
|
429
|
-
}
|
|
430
|
-
return walkNode(content);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// src/utils/markdown-writer.ts
|
|
434
|
-
function serializeFieldToken(attrs) {
|
|
435
|
-
const parts = ["field"];
|
|
436
|
-
if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
|
|
437
|
-
if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
|
|
438
|
-
if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
|
|
439
|
-
if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
|
|
440
|
-
if (attrs.required) parts.push(`required:true`);
|
|
441
|
-
if (attrs.options) parts.push(`options:${attrs.options}`);
|
|
442
|
-
if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
|
|
443
|
-
if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
|
|
444
|
-
if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
|
|
445
|
-
if (attrs.multiline) parts.push(`multiline:true`);
|
|
446
|
-
if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
|
|
447
|
-
if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
|
|
448
|
-
return `{{${parts.join("|")}}}`;
|
|
449
|
-
}
|
|
450
|
-
function serializeVariableToken(attrs) {
|
|
451
|
-
const parts = ["var"];
|
|
452
|
-
if (attrs.varName) parts.push(`name:${attrs.varName}`);
|
|
453
|
-
if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
|
|
454
|
-
if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
|
|
455
|
-
return `{{${parts.join("|")}}}`;
|
|
456
|
-
}
|
|
457
|
-
function serializeInline(content) {
|
|
458
|
-
if (!content) return "";
|
|
459
|
-
let result = "";
|
|
460
|
-
for (const node of content) {
|
|
461
|
-
if (node.type === "fieldNode") {
|
|
462
|
-
result += serializeFieldToken(node.attrs || {});
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
if (node.type === "variableNode") {
|
|
466
|
-
let token = serializeVariableToken(node.attrs || {});
|
|
467
|
-
const varMarks = node.marks || [];
|
|
468
|
-
const isBold = varMarks.some((m) => m.type === "bold");
|
|
469
|
-
const isItalic = varMarks.some((m) => m.type === "italic");
|
|
470
|
-
const isUnderline = varMarks.some((m) => m.type === "underline");
|
|
471
|
-
if (isBold && isItalic) token = `***${token}***`;
|
|
472
|
-
else if (isBold) token = `**${token}**`;
|
|
473
|
-
else if (isItalic) token = `*${token}*`;
|
|
474
|
-
if (isUnderline) token = `__${token}__`;
|
|
475
|
-
result += token;
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
if (node.type === "text") {
|
|
479
|
-
let text = node.text || "";
|
|
480
|
-
const marks = node.marks || [];
|
|
481
|
-
for (const mark of marks) {
|
|
482
|
-
if (mark.type === "bold") text = `**${text}**`;
|
|
483
|
-
else if (mark.type === "italic") text = `*${text}*`;
|
|
484
|
-
else if (mark.type === "underline") text = `__${text}__`;
|
|
485
|
-
else if (mark.type === "code") text = `\`${text}\``;
|
|
486
|
-
}
|
|
487
|
-
result += text;
|
|
488
|
-
}
|
|
489
|
-
if (node.type === "hardBreak") {
|
|
490
|
-
result += " \n";
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
return result;
|
|
494
|
-
}
|
|
495
|
-
function serializeBlock(node) {
|
|
496
|
-
switch (node.type) {
|
|
497
|
-
case "heading": {
|
|
498
|
-
const level = node.attrs?.level || 1;
|
|
499
|
-
const prefix = "#".repeat(level);
|
|
500
|
-
return `${prefix} ${serializeInline(node.content)}`;
|
|
501
|
-
}
|
|
502
|
-
case "paragraph": {
|
|
503
|
-
const inline = serializeInline(node.content);
|
|
504
|
-
return inline;
|
|
505
|
-
}
|
|
506
|
-
case "bulletList": {
|
|
507
|
-
return (node.content || []).map((item) => {
|
|
508
|
-
const inner = (item.content || []).map(serializeBlock).join("\n");
|
|
509
|
-
return `- ${inner}`;
|
|
510
|
-
}).join("\n");
|
|
511
|
-
}
|
|
512
|
-
case "orderedList": {
|
|
513
|
-
return (node.content || []).map((item, i) => {
|
|
514
|
-
const inner = (item.content || []).map(serializeBlock).join("\n");
|
|
515
|
-
return `${i + 1}. ${inner}`;
|
|
516
|
-
}).join("\n");
|
|
517
|
-
}
|
|
518
|
-
case "listItem": {
|
|
519
|
-
return (node.content || []).map(serializeBlock).join("\n");
|
|
520
|
-
}
|
|
521
|
-
case "blockquote": {
|
|
522
|
-
return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
|
|
523
|
-
}
|
|
524
|
-
case "codeBlock": {
|
|
525
|
-
const text = serializeInline(node.content);
|
|
526
|
-
return `\`\`\`
|
|
527
|
-
${text}
|
|
528
|
-
\`\`\``;
|
|
529
|
-
}
|
|
530
|
-
case "horizontalRule": {
|
|
531
|
-
return "---";
|
|
532
|
-
}
|
|
533
|
-
default:
|
|
534
|
-
return serializeInline(node.content);
|
|
564
|
+
var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
|
|
565
|
+
var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
|
|
566
|
+
var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
|
|
567
|
+
var DIRECTIVE_CLOSE = /^:::$/;
|
|
568
|
+
function parseDirectiveAttrs(str) {
|
|
569
|
+
const attrs = {};
|
|
570
|
+
if (!str) return attrs;
|
|
571
|
+
const pairs = str.split("|");
|
|
572
|
+
for (const pair of pairs) {
|
|
573
|
+
const colonIdx = pair.indexOf(":");
|
|
574
|
+
if (colonIdx === -1) continue;
|
|
575
|
+
const key = pair.slice(0, colonIdx).trim();
|
|
576
|
+
const value = pair.slice(colonIdx + 1).trim();
|
|
577
|
+
if (key) attrs[key] = value;
|
|
535
578
|
}
|
|
579
|
+
return attrs;
|
|
536
580
|
}
|
|
537
|
-
function tiptapToMarkdown(doc) {
|
|
538
|
-
if (!doc.content) return "";
|
|
539
|
-
return doc.content.map(serializeBlock).join("\n\n");
|
|
540
|
-
}
|
|
541
|
-
var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
|
|
542
581
|
function injectMarks(token, marks) {
|
|
543
582
|
const existingMatch = token.match(/\|_marks:([^}|]+)/);
|
|
544
583
|
if (existingMatch) {
|
|
@@ -655,6 +694,15 @@ function parseVariableToken(tokenBody) {
|
|
|
655
694
|
case "default":
|
|
656
695
|
attrs.varDefault = value;
|
|
657
696
|
break;
|
|
697
|
+
case "suppress":
|
|
698
|
+
attrs.suppressZero = value === "zero" ? "true" : "";
|
|
699
|
+
break;
|
|
700
|
+
case "format":
|
|
701
|
+
attrs.format = value;
|
|
702
|
+
break;
|
|
703
|
+
case "block":
|
|
704
|
+
attrs.block = value === "true" ? "true" : "";
|
|
705
|
+
break;
|
|
658
706
|
case "_marks":
|
|
659
707
|
attrs._marks = value;
|
|
660
708
|
break;
|
|
@@ -662,6 +710,44 @@ function parseVariableToken(tokenBody) {
|
|
|
662
710
|
}
|
|
663
711
|
return attrs;
|
|
664
712
|
}
|
|
713
|
+
function parseImageToken(tokenBody) {
|
|
714
|
+
const attrs = {
|
|
715
|
+
src: "",
|
|
716
|
+
varName: "",
|
|
717
|
+
alt: "",
|
|
718
|
+
width: "",
|
|
719
|
+
height: "",
|
|
720
|
+
align: ""
|
|
721
|
+
};
|
|
722
|
+
const pairs = tokenBody.split("|");
|
|
723
|
+
for (const pair of pairs) {
|
|
724
|
+
const colonIdx = pair.indexOf(":");
|
|
725
|
+
if (colonIdx === -1) continue;
|
|
726
|
+
const key = pair.slice(0, colonIdx).trim();
|
|
727
|
+
const value = pair.slice(colonIdx + 1).trim();
|
|
728
|
+
switch (key) {
|
|
729
|
+
case "src":
|
|
730
|
+
attrs.src = value;
|
|
731
|
+
break;
|
|
732
|
+
case "var":
|
|
733
|
+
attrs.varName = value;
|
|
734
|
+
break;
|
|
735
|
+
case "alt":
|
|
736
|
+
attrs.alt = value;
|
|
737
|
+
break;
|
|
738
|
+
case "width":
|
|
739
|
+
attrs.width = value;
|
|
740
|
+
break;
|
|
741
|
+
case "height":
|
|
742
|
+
attrs.height = value;
|
|
743
|
+
break;
|
|
744
|
+
case "align":
|
|
745
|
+
attrs.align = value;
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return attrs;
|
|
750
|
+
}
|
|
665
751
|
function parseInline(text) {
|
|
666
752
|
text = preprocessMarkedTokens(text);
|
|
667
753
|
const nodes = [];
|
|
@@ -685,6 +771,8 @@ function parseInline(text) {
|
|
|
685
771
|
const node = { type: "variableNode", attrs };
|
|
686
772
|
if (marks.length > 0) node.marks = marks;
|
|
687
773
|
nodes.push(node);
|
|
774
|
+
} else if (match[1] === "image") {
|
|
775
|
+
nodes.push({ type: "imageNode", attrs: parseImageToken(match[2]) });
|
|
688
776
|
}
|
|
689
777
|
lastIndex = match.index + match[0].length;
|
|
690
778
|
}
|
|
@@ -696,45 +784,63 @@ function parseInline(text) {
|
|
|
696
784
|
function parseFormattedText(text) {
|
|
697
785
|
if (!text) return [];
|
|
698
786
|
const nodes = [];
|
|
699
|
-
const parts = text.split(/(
|
|
787
|
+
const parts = text.split(/(\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
|
|
700
788
|
for (const part of parts) {
|
|
701
789
|
if (!part) continue;
|
|
702
|
-
if (part.startsWith("
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
});
|
|
790
|
+
if (part.startsWith("***") && part.endsWith("***")) {
|
|
791
|
+
const inner = part.slice(3, -3);
|
|
792
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }, { type: "italic" }] });
|
|
793
|
+
} else if (part.startsWith("**") && part.endsWith("**")) {
|
|
794
|
+
const inner = part.slice(2, -2);
|
|
795
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }] });
|
|
708
796
|
} else if (part.startsWith("*") && part.endsWith("*")) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
text: part.slice(1, -1),
|
|
712
|
-
marks: [{ type: "italic" }]
|
|
713
|
-
});
|
|
797
|
+
const inner = part.slice(1, -1);
|
|
798
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "italic" }] });
|
|
714
799
|
} else if (part.startsWith("__") && part.endsWith("__")) {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
text: part.slice(2, -2),
|
|
718
|
-
marks: [{ type: "underline" }]
|
|
719
|
-
});
|
|
800
|
+
const inner = part.slice(2, -2);
|
|
801
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "underline" }] });
|
|
720
802
|
} else if (part.startsWith("`") && part.endsWith("`")) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
text: part.slice(1, -1),
|
|
724
|
-
marks: [{ type: "code" }]
|
|
725
|
-
});
|
|
803
|
+
const inner = part.slice(1, -1);
|
|
804
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "code" }] });
|
|
726
805
|
} else {
|
|
727
806
|
nodes.push({ type: "text", text: part });
|
|
728
807
|
}
|
|
729
808
|
}
|
|
730
809
|
return nodes;
|
|
731
810
|
}
|
|
732
|
-
function
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
let
|
|
736
|
-
|
|
737
|
-
|
|
811
|
+
function splitTableCells(row) {
|
|
812
|
+
const cells = [];
|
|
813
|
+
let current = "";
|
|
814
|
+
let depth = 0;
|
|
815
|
+
for (let i = 0; i < row.length; i++) {
|
|
816
|
+
if (row[i] === "{" && row[i + 1] === "{") {
|
|
817
|
+
depth++;
|
|
818
|
+
current += "{{";
|
|
819
|
+
i++;
|
|
820
|
+
} else if (row[i] === "}" && row[i + 1] === "}") {
|
|
821
|
+
depth = Math.max(0, depth - 1);
|
|
822
|
+
current += "}}";
|
|
823
|
+
i++;
|
|
824
|
+
} else if (row[i] === "|" && depth === 0) {
|
|
825
|
+
cells.push(current.trim());
|
|
826
|
+
current = "";
|
|
827
|
+
} else {
|
|
828
|
+
current += row[i];
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (current.trim()) {
|
|
832
|
+
cells.push(current.trim());
|
|
833
|
+
}
|
|
834
|
+
return cells;
|
|
835
|
+
}
|
|
836
|
+
function parseBlocks(lines, startIdx, stopCondition) {
|
|
837
|
+
const content = [];
|
|
838
|
+
let i = startIdx;
|
|
839
|
+
while (i < lines.length) {
|
|
840
|
+
const line = lines[i];
|
|
841
|
+
if (stopCondition && stopCondition(line)) {
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
738
844
|
if (line.trim() === "") {
|
|
739
845
|
i++;
|
|
740
846
|
continue;
|
|
@@ -808,13 +914,431 @@ function markdownToTiptap(markdown) {
|
|
|
808
914
|
});
|
|
809
915
|
continue;
|
|
810
916
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
917
|
+
if (line.trimStart().startsWith("|") && line.trimEnd().endsWith("|")) {
|
|
918
|
+
const tableLines = [];
|
|
919
|
+
while (i < lines.length) {
|
|
920
|
+
const tl = lines[i];
|
|
921
|
+
if (tl.trimStart().startsWith("|") && tl.trimEnd().endsWith("|")) {
|
|
922
|
+
tableLines.push(tl);
|
|
923
|
+
i++;
|
|
924
|
+
} else {
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const separatorRegex = /^[\s|]*-+[\s|:-]*$/;
|
|
929
|
+
const dataLines = tableLines.filter(
|
|
930
|
+
(tl) => !separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))
|
|
931
|
+
);
|
|
932
|
+
const firstDataCellsRaw = dataLines.length > 0 ? splitTableCells(dataLines[0]) : [];
|
|
933
|
+
const isSubtotals = firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "_");
|
|
934
|
+
const isBorderless = isSubtotals || firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "");
|
|
935
|
+
const tableRows = [];
|
|
936
|
+
let isFirstDataRow = true;
|
|
937
|
+
for (const tl of tableLines) {
|
|
938
|
+
if (separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))) continue;
|
|
939
|
+
const cells = splitTableCells(tl).filter((c) => c !== "");
|
|
940
|
+
if (isBorderless && isFirstDataRow) {
|
|
941
|
+
isFirstDataRow = false;
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const cellType = isFirstDataRow && !isBorderless ? "tableHeader" : "tableCell";
|
|
945
|
+
const rowContent = cells.map((cellText) => ({
|
|
946
|
+
type: cellType,
|
|
947
|
+
content: [{ type: "paragraph", content: cellText ? parseInline(cellText) : [] }]
|
|
948
|
+
}));
|
|
949
|
+
tableRows.push({ type: "tableRow", content: rowContent });
|
|
950
|
+
isFirstDataRow = false;
|
|
951
|
+
}
|
|
952
|
+
if (tableRows.length > 0) {
|
|
953
|
+
const tableNode = { type: "table", content: tableRows };
|
|
954
|
+
if (isBorderless) {
|
|
955
|
+
tableNode.attrs = { borderless: true, ...isSubtotals ? { subtotals: true } : {} };
|
|
956
|
+
}
|
|
957
|
+
content.push(tableNode);
|
|
958
|
+
}
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const directiveMatch = line.trim().match(DIRECTIVE_OPEN);
|
|
962
|
+
if (directiveMatch) {
|
|
963
|
+
const directiveType = directiveMatch[1];
|
|
964
|
+
const rawAttrs = directiveMatch[2] || "";
|
|
965
|
+
const attrs = parseDirectiveAttrs(rawAttrs);
|
|
966
|
+
i++;
|
|
967
|
+
if (directiveType === "watermark") {
|
|
968
|
+
content.push({
|
|
969
|
+
type: "watermark",
|
|
970
|
+
attrs: {
|
|
971
|
+
text: attrs.text || "",
|
|
972
|
+
opacity: attrs.opacity || "0.15",
|
|
973
|
+
angle: attrs.angle || "-45"
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (directiveType === "panel") {
|
|
979
|
+
const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
|
|
980
|
+
i = inner.nextIdx;
|
|
981
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
982
|
+
i++;
|
|
983
|
+
}
|
|
984
|
+
content.push({
|
|
985
|
+
type: "panel",
|
|
986
|
+
attrs: {
|
|
987
|
+
title: attrs.title || "",
|
|
988
|
+
border: attrs.border || "solid",
|
|
989
|
+
headerStyle: attrs.headerStyle || ""
|
|
990
|
+
},
|
|
991
|
+
content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
|
|
992
|
+
});
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
if (directiveType === "repeat") {
|
|
996
|
+
const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
|
|
997
|
+
i = inner.nextIdx;
|
|
998
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
999
|
+
i++;
|
|
1000
|
+
}
|
|
1001
|
+
content.push({
|
|
1002
|
+
type: "repeatBlock",
|
|
1003
|
+
attrs: { data: attrs.data || "" },
|
|
1004
|
+
content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
|
|
1005
|
+
});
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (directiveType === "subtotals") {
|
|
1009
|
+
const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
|
|
1010
|
+
i = inner.nextIdx;
|
|
1011
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
1012
|
+
i++;
|
|
1013
|
+
}
|
|
1014
|
+
content.push({
|
|
1015
|
+
type: "subtotalsBlock",
|
|
1016
|
+
content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
|
|
1017
|
+
});
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (directiveType === "columns") {
|
|
1021
|
+
const splitVal = attrs.split || "50";
|
|
1022
|
+
const columns = [];
|
|
1023
|
+
while (i < lines.length) {
|
|
1024
|
+
const colLine = lines[i].trim();
|
|
1025
|
+
if (DIRECTIVE_CLOSE.test(colLine)) {
|
|
1026
|
+
if (columns.length > 0) {
|
|
1027
|
+
i++;
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
i++;
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
const colMatch = colLine.match(/^:::col(?:\{([^}]*)\})?$/);
|
|
1034
|
+
if (colMatch) {
|
|
1035
|
+
i++;
|
|
1036
|
+
const colInner = parseBlocks(lines, i, (l) => {
|
|
1037
|
+
const trimmed = l.trim();
|
|
1038
|
+
return DIRECTIVE_CLOSE.test(trimmed) || /^:::col(?:\{[^}]*\})?$/.test(trimmed);
|
|
1039
|
+
});
|
|
1040
|
+
i = colInner.nextIdx;
|
|
1041
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
1042
|
+
i++;
|
|
1043
|
+
}
|
|
1044
|
+
const colAttrsRaw = colMatch[1] || "";
|
|
1045
|
+
const colAttrs = parseDirectiveAttrs(colAttrsRaw);
|
|
1046
|
+
const padTop = parseFloat(colAttrs.padTop || "0") || 0;
|
|
1047
|
+
columns.push({
|
|
1048
|
+
type: "column",
|
|
1049
|
+
attrs: padTop ? { padTop } : void 0,
|
|
1050
|
+
content: colInner.blocks.length > 0 ? colInner.blocks : [{ type: "paragraph" }]
|
|
1051
|
+
});
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
if (colLine === "") {
|
|
1055
|
+
i++;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
const columnsAttrs = { split: splitVal };
|
|
1061
|
+
if (attrs.padX) columnsAttrs.padX = attrs.padX;
|
|
1062
|
+
content.push({
|
|
1063
|
+
type: "columns",
|
|
1064
|
+
attrs: columnsAttrs,
|
|
1065
|
+
content: columns.length > 0 ? columns : [
|
|
1066
|
+
{ type: "column", content: [{ type: "paragraph" }] },
|
|
1067
|
+
{ type: "column", content: [{ type: "paragraph" }] }
|
|
1068
|
+
]
|
|
1069
|
+
});
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (directiveType === "col") {
|
|
1073
|
+
const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
|
|
1074
|
+
i = inner.nextIdx;
|
|
1075
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
1076
|
+
i++;
|
|
1077
|
+
}
|
|
1078
|
+
for (const block of inner.blocks) {
|
|
1079
|
+
content.push(block);
|
|
1080
|
+
}
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
if (DIRECTIVE_CLOSE.test(line.trim())) {
|
|
1086
|
+
i++;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const imageMatch = line.trim().match(BLOCK_IMAGE_REGEX);
|
|
1090
|
+
if (imageMatch) {
|
|
1091
|
+
content.push({
|
|
1092
|
+
type: "imageNode",
|
|
1093
|
+
attrs: { src: imageMatch[2], varName: "", alt: imageMatch[1], width: "", height: "", align: "" }
|
|
1094
|
+
});
|
|
1095
|
+
i++;
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
const inlineNodes = parseInline(line);
|
|
1099
|
+
const nonWhitespace = inlineNodes.filter(
|
|
1100
|
+
(n) => !(n.type === "text" && !(n.text || "").trim())
|
|
1101
|
+
);
|
|
1102
|
+
if (nonWhitespace.length === 1 && nonWhitespace[0].type === "imageNode") {
|
|
1103
|
+
content.push(nonWhitespace[0]);
|
|
1104
|
+
} else {
|
|
1105
|
+
content.push({ type: "paragraph", content: inlineNodes });
|
|
1106
|
+
}
|
|
815
1107
|
i++;
|
|
816
1108
|
}
|
|
817
|
-
return {
|
|
1109
|
+
return { blocks: content, nextIdx: i };
|
|
1110
|
+
}
|
|
1111
|
+
function markdownToTiptap(markdown) {
|
|
1112
|
+
const lines = markdown.split("\n");
|
|
1113
|
+
const { blocks } = parseBlocks(lines, 0);
|
|
1114
|
+
return { type: "doc", content: blocks };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/utils/variable-helpers.ts
|
|
1118
|
+
function extractVariablesFromContent(content) {
|
|
1119
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1120
|
+
function walk(node) {
|
|
1121
|
+
if (node.type === "variableNode" && node.attrs) {
|
|
1122
|
+
const varName = node.attrs.varName;
|
|
1123
|
+
if (varName && !seen.has(varName)) {
|
|
1124
|
+
seen.set(varName, {
|
|
1125
|
+
varName,
|
|
1126
|
+
varLabel: node.attrs.varLabel || varName,
|
|
1127
|
+
varDefault: node.attrs.varDefault || ""
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if (node.content) {
|
|
1132
|
+
node.content.forEach(walk);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (content.content) {
|
|
1136
|
+
content.content.forEach(walk);
|
|
1137
|
+
}
|
|
1138
|
+
return Array.from(seen.values());
|
|
1139
|
+
}
|
|
1140
|
+
function applyFormat(value, format) {
|
|
1141
|
+
if (format === "phone") {
|
|
1142
|
+
const digits = value.replace(/\D/g, "");
|
|
1143
|
+
if (digits.length === 10) {
|
|
1144
|
+
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return value;
|
|
1148
|
+
}
|
|
1149
|
+
function replaceVariablesInContent(content, values) {
|
|
1150
|
+
function walkNode(node) {
|
|
1151
|
+
if (node.type === "variableNode" && node.attrs) {
|
|
1152
|
+
const varName = node.attrs.varName;
|
|
1153
|
+
let replacement = values[varName] || node.attrs.varDefault || "";
|
|
1154
|
+
const format = node.attrs.format;
|
|
1155
|
+
if (format && replacement) {
|
|
1156
|
+
replacement = applyFormat(replacement, format);
|
|
1157
|
+
}
|
|
1158
|
+
const textNode = { type: "text", text: replacement };
|
|
1159
|
+
if (node.marks && node.marks.length > 0) {
|
|
1160
|
+
textNode.marks = node.marks;
|
|
1161
|
+
}
|
|
1162
|
+
return textNode;
|
|
1163
|
+
}
|
|
1164
|
+
if (node.type === "imageNode" && node.attrs?.varName) {
|
|
1165
|
+
const varName = node.attrs.varName;
|
|
1166
|
+
const resolved = values[varName] || node.attrs.src || "";
|
|
1167
|
+
return { ...node, attrs: { ...node.attrs, src: resolved } };
|
|
1168
|
+
}
|
|
1169
|
+
if (node.content) {
|
|
1170
|
+
return { ...node, content: node.content.map(walkNode) };
|
|
1171
|
+
}
|
|
1172
|
+
return node;
|
|
1173
|
+
}
|
|
1174
|
+
return walkNode(content);
|
|
1175
|
+
}
|
|
1176
|
+
function expandBlockVariables(content, values) {
|
|
1177
|
+
function soleVariableOf(node) {
|
|
1178
|
+
if (node.type !== "paragraph" || !node.content) return null;
|
|
1179
|
+
const meaningful = node.content.filter(
|
|
1180
|
+
(c) => !(c.type === "text" && !(c.text || "").trim())
|
|
1181
|
+
);
|
|
1182
|
+
if (meaningful.length === 1 && meaningful[0].type === "variableNode") {
|
|
1183
|
+
return meaningful[0];
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
function stripUnsafeNodes(node) {
|
|
1188
|
+
if (!node.content) return node;
|
|
1189
|
+
return {
|
|
1190
|
+
...node,
|
|
1191
|
+
content: node.content.filter((c) => c.type !== "fieldNode" && c.type !== "watermark").map(stripUnsafeNodes)
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function isCellSafe(blocks) {
|
|
1195
|
+
return blocks.every((b) => b.type === "paragraph" || b.type === "heading");
|
|
1196
|
+
}
|
|
1197
|
+
function walkNode(node, inCell) {
|
|
1198
|
+
if (!node.content) return node;
|
|
1199
|
+
const childInCell = inCell || node.type === "tableCell" || node.type === "tableHeader";
|
|
1200
|
+
const newContent = [];
|
|
1201
|
+
for (const child of node.content) {
|
|
1202
|
+
const varNode = soleVariableOf(child);
|
|
1203
|
+
if (varNode?.attrs) {
|
|
1204
|
+
const varName = varNode.attrs.varName;
|
|
1205
|
+
const value = values[varName] || varNode.attrs.varDefault || "";
|
|
1206
|
+
const isBlock = varNode.attrs.block === "true" || value.includes("\n");
|
|
1207
|
+
if (isBlock) {
|
|
1208
|
+
const parsed = stripUnsafeNodes(markdownToTiptap(value));
|
|
1209
|
+
const blocks = parsed.content || [];
|
|
1210
|
+
if (!childInCell || isCellSafe(blocks)) {
|
|
1211
|
+
newContent.push(...blocks);
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
newContent.push(walkNode(child, childInCell));
|
|
1217
|
+
}
|
|
1218
|
+
return { ...node, content: newContent };
|
|
1219
|
+
}
|
|
1220
|
+
return walkNode(content, false);
|
|
1221
|
+
}
|
|
1222
|
+
function labelToVarName(label) {
|
|
1223
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
1224
|
+
}
|
|
1225
|
+
function isZeroLike(value) {
|
|
1226
|
+
if (!value || value.trim() === "") return true;
|
|
1227
|
+
const cleaned = value.replace(/[$,\s]/g, "");
|
|
1228
|
+
return /^-?0+(\.0+)?$/.test(cleaned);
|
|
1229
|
+
}
|
|
1230
|
+
function suppressZeroContent(content, values) {
|
|
1231
|
+
function hasSuppressedVar(node) {
|
|
1232
|
+
if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
|
|
1233
|
+
const varName = node.attrs.varName;
|
|
1234
|
+
const val = values[varName] || node.attrs.varDefault || "";
|
|
1235
|
+
return isZeroLike(val);
|
|
1236
|
+
}
|
|
1237
|
+
return (node.content || []).some(hasSuppressedVar);
|
|
1238
|
+
}
|
|
1239
|
+
function walkNode(node) {
|
|
1240
|
+
if (node.type === "table" && node.content) {
|
|
1241
|
+
const filtered = node.content.filter((row) => {
|
|
1242
|
+
if (row.type !== "tableRow") return true;
|
|
1243
|
+
const isHeader = row.content?.[0]?.type === "tableHeader";
|
|
1244
|
+
if (isHeader) return true;
|
|
1245
|
+
return !hasSuppressedVar(row);
|
|
1246
|
+
});
|
|
1247
|
+
const bodyRows = filtered.filter(
|
|
1248
|
+
(r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
|
|
1249
|
+
);
|
|
1250
|
+
if (bodyRows.length === 0) return null;
|
|
1251
|
+
return { ...node, content: filtered };
|
|
1252
|
+
}
|
|
1253
|
+
if (node.type === "paragraph" && hasSuppressedVar(node)) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
if (node.content) {
|
|
1257
|
+
const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
|
|
1258
|
+
return { ...node, content: filtered };
|
|
1259
|
+
}
|
|
1260
|
+
return node;
|
|
1261
|
+
}
|
|
1262
|
+
return walkNode(content) || content;
|
|
1263
|
+
}
|
|
1264
|
+
function expandRepeatContent(content, values) {
|
|
1265
|
+
const enrichedValues = { ...values };
|
|
1266
|
+
function cloneNode(node, dataKey, index) {
|
|
1267
|
+
if (node.type === "variableNode" && node.attrs?.varName) {
|
|
1268
|
+
const oldName = node.attrs.varName;
|
|
1269
|
+
const bareName = oldName.replace(/^operation_/, "");
|
|
1270
|
+
const newName = `${dataKey}_${index}_${bareName}`;
|
|
1271
|
+
return { ...node, attrs: { ...node.attrs, varName: newName } };
|
|
1272
|
+
}
|
|
1273
|
+
if (node.content) {
|
|
1274
|
+
return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
|
|
1275
|
+
}
|
|
1276
|
+
return { ...node };
|
|
1277
|
+
}
|
|
1278
|
+
function walkNode(node) {
|
|
1279
|
+
if (node.type === "repeatBlock" && node.attrs?.data) {
|
|
1280
|
+
const dataKey = node.attrs.data;
|
|
1281
|
+
const arrayJson = values[dataKey];
|
|
1282
|
+
if (!arrayJson) return [];
|
|
1283
|
+
let items;
|
|
1284
|
+
try {
|
|
1285
|
+
items = JSON.parse(arrayJson);
|
|
1286
|
+
} catch {
|
|
1287
|
+
return [];
|
|
1288
|
+
}
|
|
1289
|
+
const sums = /* @__PURE__ */ new Map();
|
|
1290
|
+
for (const item of items) {
|
|
1291
|
+
for (const [key, val] of Object.entries(item)) {
|
|
1292
|
+
const num = parseFloat(val);
|
|
1293
|
+
if (!isNaN(num)) {
|
|
1294
|
+
sums.set(key, (sums.get(key) || 0) + num);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const woKeysByNorm = /* @__PURE__ */ new Map();
|
|
1299
|
+
for (const k of Object.keys(enrichedValues)) {
|
|
1300
|
+
if (k.startsWith("workorder_")) {
|
|
1301
|
+
const norm = k.replace(/_/g, "");
|
|
1302
|
+
woKeysByNorm.set(norm, k);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
for (const [field, total] of sums) {
|
|
1306
|
+
const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
|
|
1307
|
+
const directKey = `workorder_${field}`;
|
|
1308
|
+
enrichedValues[directKey] = formatted;
|
|
1309
|
+
const norm = `workorder${field}`.replace(/_/g, "");
|
|
1310
|
+
const existing = woKeysByNorm.get(norm);
|
|
1311
|
+
if (existing && existing !== directKey) {
|
|
1312
|
+
enrichedValues[existing] = formatted;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const expanded = [];
|
|
1316
|
+
for (let i = 0; i < items.length; i++) {
|
|
1317
|
+
const item = items[i];
|
|
1318
|
+
for (const [key, val] of Object.entries(item)) {
|
|
1319
|
+
enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
|
|
1320
|
+
}
|
|
1321
|
+
const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
|
|
1322
|
+
expanded.push(...cloned);
|
|
1323
|
+
}
|
|
1324
|
+
return expanded;
|
|
1325
|
+
}
|
|
1326
|
+
if (node.content) {
|
|
1327
|
+
const newContent = [];
|
|
1328
|
+
for (const child of node.content) {
|
|
1329
|
+
const result2 = walkNode(child);
|
|
1330
|
+
if (Array.isArray(result2)) newContent.push(...result2);
|
|
1331
|
+
else newContent.push(result2);
|
|
1332
|
+
}
|
|
1333
|
+
return { ...node, content: newContent };
|
|
1334
|
+
}
|
|
1335
|
+
return node;
|
|
1336
|
+
}
|
|
1337
|
+
const result = walkNode(content);
|
|
1338
|
+
return {
|
|
1339
|
+
content: Array.isArray(result) ? { ...content, content: result } : result,
|
|
1340
|
+
values: enrichedValues
|
|
1341
|
+
};
|
|
818
1342
|
}
|
|
819
1343
|
|
|
820
1344
|
// src/utils/error-helpers.ts
|
|
@@ -952,10 +1476,40 @@ function buildMetadataObject(fields, actualFieldNames) {
|
|
|
952
1476
|
// src/utils/pdf-generator.ts
|
|
953
1477
|
var PAGE_WIDTH = 595.28;
|
|
954
1478
|
var PAGE_HEIGHT = 841.89;
|
|
955
|
-
var MARGIN =
|
|
1479
|
+
var MARGIN = 40;
|
|
956
1480
|
var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
|
|
957
1481
|
var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
|
|
958
1482
|
var LINE_HEIGHT_FACTOR = 1.4;
|
|
1483
|
+
var BODY_FONT_SIZE = 10;
|
|
1484
|
+
function hexToRgbColor(hex) {
|
|
1485
|
+
if (!hex) return null;
|
|
1486
|
+
const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
|
|
1487
|
+
if (!m) return null;
|
|
1488
|
+
const n = parseInt(m[1], 16);
|
|
1489
|
+
return pdfLib.rgb((n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255);
|
|
1490
|
+
}
|
|
1491
|
+
function contrastTextColor(fill) {
|
|
1492
|
+
const c = fill;
|
|
1493
|
+
const luminance = 0.299 * c.red + 0.587 * c.green + 0.114 * c.blue;
|
|
1494
|
+
return luminance > 0.6 ? pdfLib.rgb(0, 0, 0) : pdfLib.rgb(1, 1, 1);
|
|
1495
|
+
}
|
|
1496
|
+
function resolveTheme(theme) {
|
|
1497
|
+
const primary = hexToRgbColor(theme?.primaryColor);
|
|
1498
|
+
const accent = hexToRgbColor(theme?.accentColor);
|
|
1499
|
+
const border = hexToRgbColor(theme?.borderColor);
|
|
1500
|
+
return {
|
|
1501
|
+
tableHeaderBg: primary ?? pdfLib.rgb(0.3, 0.3, 0.3),
|
|
1502
|
+
tableHeaderText: primary ? contrastTextColor(primary) : pdfLib.rgb(1, 1, 1),
|
|
1503
|
+
tableBorder: border ?? pdfLib.rgb(0.75, 0.75, 0.75),
|
|
1504
|
+
panelDarkHeaderBg: primary ?? pdfLib.rgb(0.25, 0.25, 0.25),
|
|
1505
|
+
panelDarkHeaderText: primary ? contrastTextColor(primary) : pdfLib.rgb(1, 1, 1),
|
|
1506
|
+
panelBorder: border ?? pdfLib.rgb(0.6, 0.6, 0.6),
|
|
1507
|
+
rule: primary ?? pdfLib.rgb(0, 0, 0),
|
|
1508
|
+
pageHeaderText: primary ?? pdfLib.rgb(0, 0, 0),
|
|
1509
|
+
pageHeaderRule: accent ?? primary ?? pdfLib.rgb(0.75, 0.75, 0.75)
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
|
|
959
1513
|
var FIELD_DISPLAY = {
|
|
960
1514
|
["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
|
|
961
1515
|
["signature" /* SIGNATURE */]: { label: "Signature", width: 200, height: 60 },
|
|
@@ -986,17 +1540,65 @@ function getFieldDimensions(fieldType, attrs, font, fontSize) {
|
|
|
986
1540
|
const height = textLineHeight + 4;
|
|
987
1541
|
return { width, height };
|
|
988
1542
|
}
|
|
1543
|
+
async function loadImageBytes(src) {
|
|
1544
|
+
if (src.startsWith("data:")) {
|
|
1545
|
+
const comma = src.indexOf(",");
|
|
1546
|
+
if (comma === -1) throw new Error("malformed data URL");
|
|
1547
|
+
const meta = src.slice(0, comma);
|
|
1548
|
+
if (!/;base64$/i.test(meta)) throw new Error("data URL must be base64-encoded");
|
|
1549
|
+
const bin = atob(src.slice(comma + 1));
|
|
1550
|
+
const bytes = new Uint8Array(bin.length);
|
|
1551
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1552
|
+
return bytes;
|
|
1553
|
+
}
|
|
1554
|
+
const res = await fetch(src);
|
|
1555
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1556
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
1557
|
+
}
|
|
1558
|
+
function describeImageSrc(src) {
|
|
1559
|
+
return src.length > 80 ? `${src.slice(0, 77)}...` : src;
|
|
1560
|
+
}
|
|
1561
|
+
async function preloadImages(pdfDoc, content) {
|
|
1562
|
+
const srcs = /* @__PURE__ */ new Set();
|
|
1563
|
+
(function walk(node) {
|
|
1564
|
+
if (node.type === "imageNode") {
|
|
1565
|
+
const src = node.attrs?.src || "";
|
|
1566
|
+
if (src) srcs.add(src);
|
|
1567
|
+
}
|
|
1568
|
+
for (const child of node.content || []) walk(child);
|
|
1569
|
+
})(content);
|
|
1570
|
+
const images = /* @__PURE__ */ new Map();
|
|
1571
|
+
const warnings = [];
|
|
1572
|
+
for (const src of srcs) {
|
|
1573
|
+
try {
|
|
1574
|
+
const bytes = await loadImageBytes(src);
|
|
1575
|
+
const isPng = bytes.length > 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
1576
|
+
const isJpeg = bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216;
|
|
1577
|
+
if (isPng) {
|
|
1578
|
+
images.set(src, await pdfDoc.embedPng(bytes));
|
|
1579
|
+
} else if (isJpeg) {
|
|
1580
|
+
images.set(src, await pdfDoc.embedJpg(bytes));
|
|
1581
|
+
} else {
|
|
1582
|
+
warnings.push(`Image skipped (unsupported format \u2014 only PNG/JPEG embed): ${describeImageSrc(src)}`);
|
|
1583
|
+
}
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
warnings.push(`Image skipped (${getErrorMessage(err)}): ${describeImageSrc(src)}`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return { images, warnings };
|
|
1589
|
+
}
|
|
989
1590
|
function ensureSpace(state, neededHeight) {
|
|
990
1591
|
if (state.y + neededHeight > CONTENT_HEIGHT) {
|
|
991
1592
|
const newPage = state.pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
|
992
1593
|
state.currentPage = newPage;
|
|
993
1594
|
state.pageIndex++;
|
|
994
1595
|
state.y = 0;
|
|
1596
|
+
renderWatermarksOnPage(newPage, state.fonts, state.watermarkNodes);
|
|
995
1597
|
}
|
|
996
1598
|
}
|
|
997
|
-
function drawText(state, text, font, fontSize, indent = 0) {
|
|
1599
|
+
function drawText(state, text, font, fontSize, indent = 0, region = DEFAULT_REGION) {
|
|
998
1600
|
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
999
|
-
const maxWidth =
|
|
1601
|
+
const maxWidth = region.width - indent;
|
|
1000
1602
|
const words = text.split(/\s+/);
|
|
1001
1603
|
let line = "";
|
|
1002
1604
|
let totalAdvance = 0;
|
|
@@ -1007,7 +1609,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
|
|
|
1007
1609
|
ensureSpace(state, lineHeight);
|
|
1008
1610
|
const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
|
|
1009
1611
|
state.currentPage.drawText(line, {
|
|
1010
|
-
x:
|
|
1612
|
+
x: region.leftX + indent,
|
|
1011
1613
|
y: pdfY,
|
|
1012
1614
|
size: fontSize,
|
|
1013
1615
|
font,
|
|
@@ -1024,7 +1626,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
|
|
|
1024
1626
|
ensureSpace(state, lineHeight);
|
|
1025
1627
|
const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
|
|
1026
1628
|
state.currentPage.drawText(line, {
|
|
1027
|
-
x:
|
|
1629
|
+
x: region.leftX + indent,
|
|
1028
1630
|
y: pdfY,
|
|
1029
1631
|
size: fontSize,
|
|
1030
1632
|
font,
|
|
@@ -1068,10 +1670,10 @@ function collectInlineSegments(content, fonts, fontSize) {
|
|
|
1068
1670
|
}
|
|
1069
1671
|
return segments;
|
|
1070
1672
|
}
|
|
1071
|
-
function layoutInlineSegments(state, segments, fontSize, indent = 0) {
|
|
1673
|
+
function layoutInlineSegments(state, segments, fontSize, indent = 0, region = DEFAULT_REGION) {
|
|
1072
1674
|
const textLineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
1073
|
-
const maxX =
|
|
1074
|
-
const startX =
|
|
1675
|
+
const maxX = region.leftX + region.width;
|
|
1676
|
+
const startX = region.leftX + indent;
|
|
1075
1677
|
let currentX = startX;
|
|
1076
1678
|
let currentLineHeight = 0;
|
|
1077
1679
|
let lineHasContent = false;
|
|
@@ -1176,90 +1778,395 @@ function layoutInlineSegments(state, segments, fontSize, indent = 0) {
|
|
|
1176
1778
|
}
|
|
1177
1779
|
flushLine();
|
|
1178
1780
|
}
|
|
1179
|
-
function processInlineContent(state, content, fontSize, indent = 0) {
|
|
1781
|
+
function processInlineContent(state, content, fontSize, indent = 0, region = DEFAULT_REGION) {
|
|
1180
1782
|
if (!content) return;
|
|
1181
1783
|
const segments = collectInlineSegments(content, state.fonts, fontSize);
|
|
1182
1784
|
if (segments.length === 0) return;
|
|
1183
|
-
layoutInlineSegments(state, segments, fontSize, indent);
|
|
1785
|
+
layoutInlineSegments(state, segments, fontSize, indent, region);
|
|
1184
1786
|
}
|
|
1185
|
-
function
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
boldItalic: state.fonts.boldItalic
|
|
1200
|
-
};
|
|
1201
|
-
const segments = collectInlineSegments(node.content, headingFonts, fontSize);
|
|
1202
|
-
layoutInlineSegments(state, segments, fontSize);
|
|
1203
|
-
state.y += spacing / 2;
|
|
1204
|
-
break;
|
|
1205
|
-
}
|
|
1206
|
-
case "paragraph": {
|
|
1207
|
-
const fontSize = 12;
|
|
1208
|
-
if (!node.content || node.content.length === 0) {
|
|
1209
|
-
state.y += fontSize * LINE_HEIGHT_FACTOR;
|
|
1210
|
-
break;
|
|
1787
|
+
function extractCellText(cell) {
|
|
1788
|
+
const parts = [];
|
|
1789
|
+
for (const child of cell.content || []) {
|
|
1790
|
+
if (child.type === "paragraph" || child.type === "heading") {
|
|
1791
|
+
for (const inline of child.content || []) {
|
|
1792
|
+
if (inline.type === "text") {
|
|
1793
|
+
parts.push(inline.text || "");
|
|
1794
|
+
} else if (inline.type === "variableNode") {
|
|
1795
|
+
const label = inline.attrs?.varLabel || inline.attrs?.varName || "Variable";
|
|
1796
|
+
parts.push(`[${label}]`);
|
|
1797
|
+
} else if (inline.type === "fieldNode") {
|
|
1798
|
+
const label = inline.attrs?.fieldLabel || inline.attrs?.fieldName || "Field";
|
|
1799
|
+
parts.push(`[ ${label} ]`);
|
|
1800
|
+
}
|
|
1211
1801
|
}
|
|
1212
|
-
processInlineContent(state, node.content, fontSize);
|
|
1213
|
-
state.y += 4;
|
|
1214
|
-
break;
|
|
1215
1802
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1803
|
+
}
|
|
1804
|
+
return parts.join("");
|
|
1805
|
+
}
|
|
1806
|
+
function isNumericText(text) {
|
|
1807
|
+
return /^\s*\$?\s*[\d,.]+\s*$/.test(text);
|
|
1808
|
+
}
|
|
1809
|
+
function measureCellHeight(cell, maxWidth, fonts, fontSize, isHeader) {
|
|
1810
|
+
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
1811
|
+
const font = isHeader ? fonts.bold : fonts.regular;
|
|
1812
|
+
let totalHeight = 0;
|
|
1813
|
+
for (const child of cell.content || []) {
|
|
1814
|
+
if (child.type === "paragraph" || child.type === "heading") {
|
|
1815
|
+
const text = extractCellText({ content: [child] });
|
|
1816
|
+
if (!text) {
|
|
1817
|
+
totalHeight += lineHeight;
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
const words = text.split(/\s+/);
|
|
1821
|
+
let line = "";
|
|
1822
|
+
let lineCount = 0;
|
|
1823
|
+
for (const word of words) {
|
|
1824
|
+
const testLine = line ? `${line} ${word}` : word;
|
|
1825
|
+
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
|
|
1826
|
+
if (testWidth > maxWidth && line) {
|
|
1827
|
+
lineCount++;
|
|
1828
|
+
line = word;
|
|
1829
|
+
} else {
|
|
1830
|
+
line = testLine;
|
|
1230
1831
|
}
|
|
1231
1832
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1833
|
+
if (line) lineCount++;
|
|
1834
|
+
totalHeight += lineCount * lineHeight;
|
|
1234
1835
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1836
|
+
}
|
|
1837
|
+
return Math.max(totalHeight, lineHeight);
|
|
1838
|
+
}
|
|
1839
|
+
function renderCellContent(state, cell, cellX, cellY, cellWidth, fonts, fontSize, isHeader, rightAlign, textColor = pdfLib.rgb(0, 0, 0)) {
|
|
1840
|
+
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
1841
|
+
const cellPadding = 4;
|
|
1842
|
+
const maxTextWidth = cellWidth - cellPadding * 2;
|
|
1843
|
+
let textX = cellX + cellPadding;
|
|
1844
|
+
let textY = cellY;
|
|
1845
|
+
for (const child of cell.content || []) {
|
|
1846
|
+
if (child.type === "paragraph" || child.type === "heading") {
|
|
1847
|
+
const segments = collectInlineSegments(
|
|
1848
|
+
child.content || [],
|
|
1849
|
+
isHeader ? { regular: fonts.bold, bold: fonts.bold, italic: fonts.boldItalic, boldItalic: fonts.boldItalic } : fonts,
|
|
1850
|
+
fontSize
|
|
1851
|
+
);
|
|
1852
|
+
if (rightAlign && segments.length > 0) {
|
|
1853
|
+
const fullText = segments.filter((s) => s.kind === "text").map((s) => s.text).join("");
|
|
1854
|
+
const textWidth = (isHeader ? fonts.bold : fonts.regular).widthOfTextAtSize(fullText, fontSize);
|
|
1855
|
+
if (textWidth <= maxTextWidth) {
|
|
1856
|
+
const pdfY = PAGE_HEIGHT - textY - fontSize;
|
|
1857
|
+
state.currentPage.drawText(fullText, {
|
|
1858
|
+
x: cellX + cellWidth - cellPadding - textWidth,
|
|
1859
|
+
y: pdfY,
|
|
1245
1860
|
size: fontSize,
|
|
1246
|
-
font:
|
|
1247
|
-
color:
|
|
1861
|
+
font: isHeader ? fonts.bold : fonts.regular,
|
|
1862
|
+
color: textColor
|
|
1248
1863
|
});
|
|
1249
|
-
|
|
1864
|
+
return;
|
|
1250
1865
|
}
|
|
1251
|
-
num++;
|
|
1252
1866
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1867
|
+
let currentX = textX;
|
|
1868
|
+
for (const seg of segments) {
|
|
1869
|
+
if (seg.kind === "break") {
|
|
1870
|
+
textY += lineHeight;
|
|
1871
|
+
currentX = textX;
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
if (seg.kind === "text") {
|
|
1875
|
+
const words = seg.text.split(/(\s+)/);
|
|
1876
|
+
const spaceWidth = seg.font.widthOfTextAtSize(" ", fontSize);
|
|
1877
|
+
for (const token of words) {
|
|
1878
|
+
if (!token) continue;
|
|
1879
|
+
if (/^\s+$/.test(token)) {
|
|
1880
|
+
currentX += spaceWidth;
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
const wordWidth = seg.font.widthOfTextAtSize(token, fontSize);
|
|
1884
|
+
if (currentX + wordWidth > textX + maxTextWidth && currentX > textX) {
|
|
1885
|
+
textY += lineHeight;
|
|
1886
|
+
currentX = textX;
|
|
1887
|
+
}
|
|
1888
|
+
const pdfY = PAGE_HEIGHT - textY - fontSize;
|
|
1889
|
+
state.currentPage.drawText(token, {
|
|
1890
|
+
x: currentX,
|
|
1891
|
+
y: pdfY,
|
|
1892
|
+
size: fontSize,
|
|
1893
|
+
font: seg.font,
|
|
1894
|
+
color: textColor
|
|
1895
|
+
});
|
|
1896
|
+
currentX += wordWidth;
|
|
1897
|
+
}
|
|
1898
|
+
} else if (seg.kind === "field") {
|
|
1899
|
+
const pdfY = PAGE_HEIGHT - textY - seg.height;
|
|
1900
|
+
if (state.drawFieldPlaceholders) {
|
|
1901
|
+
state.currentPage.drawRectangle({
|
|
1902
|
+
x: currentX,
|
|
1903
|
+
y: pdfY,
|
|
1904
|
+
width: seg.width,
|
|
1905
|
+
height: seg.height,
|
|
1906
|
+
borderColor: pdfLib.rgb(0.5, 0.5, 0.7),
|
|
1907
|
+
borderWidth: 0.5,
|
|
1908
|
+
color: pdfLib.rgb(0.95, 0.95, 1),
|
|
1909
|
+
borderDashArray: [4, 2]
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
const fieldId = seg.attrs.fieldId;
|
|
1913
|
+
if (fieldId) {
|
|
1914
|
+
state.fieldPositions.set(fieldId, {
|
|
1915
|
+
x: currentX,
|
|
1916
|
+
y: pdfY,
|
|
1917
|
+
width: seg.width,
|
|
1918
|
+
height: seg.height,
|
|
1919
|
+
page: state.pageIndex + 1
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
currentX += seg.width + 4;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function renderTable(state, node, region = DEFAULT_REGION) {
|
|
1929
|
+
const tableRows = node.content || [];
|
|
1930
|
+
if (tableRows.length === 0) return;
|
|
1931
|
+
const borderless = node.attrs?.borderless === true;
|
|
1932
|
+
const subtotals = node.attrs?.subtotals === true;
|
|
1933
|
+
const fontSize = 10;
|
|
1934
|
+
const cellPadding = borderless ? 2 : 4;
|
|
1935
|
+
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
1936
|
+
const rowPadding = cellPadding * 2;
|
|
1937
|
+
const borderColor = state.theme.tableBorder;
|
|
1938
|
+
const headerBg = state.theme.tableHeaderBg;
|
|
1939
|
+
const headerTextColor = state.theme.tableHeaderText;
|
|
1940
|
+
let colCount = 0;
|
|
1941
|
+
for (const row of tableRows) {
|
|
1942
|
+
colCount = Math.max(colCount, row.content?.length || 0);
|
|
1943
|
+
}
|
|
1944
|
+
if (colCount === 0) return;
|
|
1945
|
+
const colMaxWidths = new Array(colCount).fill(0);
|
|
1946
|
+
for (const row of tableRows) {
|
|
1947
|
+
const cells = row.content || [];
|
|
1948
|
+
for (let c = 0; c < cells.length && c < colCount; c++) {
|
|
1949
|
+
const cell = cells[c];
|
|
1950
|
+
const isHeader = cell.type === "tableHeader";
|
|
1951
|
+
const text = extractCellText(cell);
|
|
1952
|
+
const font = isHeader ? state.fonts.bold : state.fonts.regular;
|
|
1953
|
+
const textWidth = font.widthOfTextAtSize(text, fontSize);
|
|
1954
|
+
colMaxWidths[c] = Math.max(colMaxWidths[c], textWidth + cellPadding * 2 + 4);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
const totalMeasured = colMaxWidths.reduce((a, b) => a + b, 0);
|
|
1958
|
+
const minColWidth = 40;
|
|
1959
|
+
let colWidths;
|
|
1960
|
+
if (totalMeasured <= region.width) {
|
|
1961
|
+
const scale = region.width / totalMeasured;
|
|
1962
|
+
colWidths = colMaxWidths.map((w) => Math.max(w * scale, minColWidth));
|
|
1963
|
+
} else {
|
|
1964
|
+
colWidths = colMaxWidths.map(
|
|
1965
|
+
(w) => Math.max(w / totalMeasured * region.width, minColWidth)
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
const colSum = colWidths.reduce((a, b) => a + b, 0);
|
|
1969
|
+
if (colSum > 0) {
|
|
1970
|
+
const normFactor = region.width / colSum;
|
|
1971
|
+
colWidths = colWidths.map((w) => w * normFactor);
|
|
1972
|
+
}
|
|
1973
|
+
const headerRow = tableRows[0];
|
|
1974
|
+
const hasHeader = headerRow?.content?.[0]?.type === "tableHeader";
|
|
1975
|
+
function renderRow(row, isHeaderRow) {
|
|
1976
|
+
const cells = row.content || [];
|
|
1977
|
+
const isHeader = isHeaderRow && hasHeader;
|
|
1978
|
+
let rowHeight = lineHeight;
|
|
1979
|
+
for (let c = 0; c < cells.length && c < colCount; c++) {
|
|
1980
|
+
const cellContentWidth = colWidths[c] - cellPadding * 2;
|
|
1981
|
+
const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, isHeader);
|
|
1982
|
+
rowHeight = Math.max(rowHeight, cellH);
|
|
1983
|
+
}
|
|
1984
|
+
rowHeight += rowPadding;
|
|
1985
|
+
ensureSpace(state, rowHeight);
|
|
1986
|
+
const rowTopY = state.y;
|
|
1987
|
+
let cellX = region.leftX;
|
|
1988
|
+
for (let c = 0; c < cells.length && c < colCount; c++) {
|
|
1989
|
+
const cellW = colWidths[c];
|
|
1990
|
+
const cell = cells[c];
|
|
1991
|
+
const pdfCellTop = PAGE_HEIGHT - MARGIN - rowTopY;
|
|
1992
|
+
const pdfCellBottom = pdfCellTop - rowHeight;
|
|
1993
|
+
if (isHeader && !borderless) {
|
|
1994
|
+
state.currentPage.drawRectangle({
|
|
1995
|
+
x: cellX,
|
|
1996
|
+
y: pdfCellBottom,
|
|
1997
|
+
width: cellW,
|
|
1998
|
+
height: rowHeight,
|
|
1999
|
+
color: headerBg
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
if (!borderless) {
|
|
2003
|
+
state.currentPage.drawRectangle({
|
|
2004
|
+
x: cellX,
|
|
2005
|
+
y: pdfCellBottom,
|
|
2006
|
+
width: cellW,
|
|
2007
|
+
height: rowHeight,
|
|
2008
|
+
borderColor,
|
|
2009
|
+
borderWidth: 0.5
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
const cellText = extractCellText(cell);
|
|
2013
|
+
const rightAlign = !isHeader && isNumericText(cellText);
|
|
2014
|
+
const contentY = MARGIN + rowTopY + cellPadding;
|
|
2015
|
+
const effectiveHeader = isHeader && !borderless;
|
|
2016
|
+
renderCellContent(
|
|
2017
|
+
state,
|
|
2018
|
+
cell,
|
|
2019
|
+
cellX,
|
|
2020
|
+
contentY,
|
|
2021
|
+
cellW,
|
|
2022
|
+
state.fonts,
|
|
2023
|
+
fontSize,
|
|
2024
|
+
effectiveHeader,
|
|
2025
|
+
rightAlign,
|
|
2026
|
+
effectiveHeader ? headerTextColor : pdfLib.rgb(0, 0, 0)
|
|
2027
|
+
);
|
|
2028
|
+
cellX += cellW;
|
|
2029
|
+
}
|
|
2030
|
+
state.y += rowHeight;
|
|
2031
|
+
}
|
|
2032
|
+
if (hasHeader && tableRows.length > 0) {
|
|
2033
|
+
renderRow(tableRows[0], true);
|
|
2034
|
+
}
|
|
2035
|
+
let lastPageIndex = state.pageIndex;
|
|
2036
|
+
const startIdx = hasHeader ? 1 : 0;
|
|
2037
|
+
const bodyRowCount = tableRows.length - startIdx;
|
|
2038
|
+
for (let r = startIdx; r < tableRows.length; r++) {
|
|
2039
|
+
const cells = tableRows[r].content || [];
|
|
2040
|
+
let estRowHeight = lineHeight;
|
|
2041
|
+
for (let c = 0; c < cells.length && c < colCount; c++) {
|
|
2042
|
+
const cellContentWidth = colWidths[c] - cellPadding * 2;
|
|
2043
|
+
const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, false);
|
|
2044
|
+
estRowHeight = Math.max(estRowHeight, cellH);
|
|
2045
|
+
}
|
|
2046
|
+
estRowHeight += rowPadding;
|
|
2047
|
+
if (state.y + estRowHeight > CONTENT_HEIGHT) {
|
|
2048
|
+
ensureSpace(state, estRowHeight + (hasHeader ? lineHeight + rowPadding : 0));
|
|
2049
|
+
if (state.pageIndex !== lastPageIndex && hasHeader) {
|
|
2050
|
+
renderRow(headerRow, true);
|
|
2051
|
+
lastPageIndex = state.pageIndex;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
const rowTopYBeforeRender = state.y;
|
|
2055
|
+
renderRow(tableRows[r], false);
|
|
2056
|
+
lastPageIndex = state.pageIndex;
|
|
2057
|
+
if (subtotals && colCount >= 2) {
|
|
2058
|
+
const bodyIdx = r - startIdx;
|
|
2059
|
+
const isFirstBody = bodyIdx === 0;
|
|
2060
|
+
const isLastBody = bodyIdx === bodyRowCount - 1;
|
|
2061
|
+
if (isFirstBody || isLastBody) {
|
|
2062
|
+
const valueStartX = region.leftX + colWidths[0];
|
|
2063
|
+
const valueEndX = region.leftX + colWidths.reduce((a, b) => a + b, 0);
|
|
2064
|
+
const pdfRowTopY = PAGE_HEIGHT - MARGIN - rowTopYBeforeRender;
|
|
2065
|
+
const overlineColor = pdfLib.rgb(0, 0, 0);
|
|
2066
|
+
if (isFirstBody) {
|
|
2067
|
+
state.currentPage.drawLine({
|
|
2068
|
+
start: { x: valueStartX, y: pdfRowTopY },
|
|
2069
|
+
end: { x: valueEndX, y: pdfRowTopY },
|
|
2070
|
+
thickness: 0.5,
|
|
2071
|
+
color: overlineColor
|
|
2072
|
+
});
|
|
2073
|
+
} else if (isLastBody) {
|
|
2074
|
+
state.currentPage.drawLine({
|
|
2075
|
+
start: { x: valueStartX, y: pdfRowTopY + 2 },
|
|
2076
|
+
end: { x: valueEndX, y: pdfRowTopY + 2 },
|
|
2077
|
+
thickness: 0.5,
|
|
2078
|
+
color: overlineColor
|
|
2079
|
+
});
|
|
2080
|
+
state.currentPage.drawLine({
|
|
2081
|
+
start: { x: valueStartX, y: pdfRowTopY },
|
|
2082
|
+
end: { x: valueEndX, y: pdfRowTopY },
|
|
2083
|
+
thickness: 0.5,
|
|
2084
|
+
color: overlineColor
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
state.y += 3;
|
|
2091
|
+
}
|
|
2092
|
+
function processBlock(state, node, region = DEFAULT_REGION) {
|
|
2093
|
+
switch (node.type) {
|
|
2094
|
+
case "heading": {
|
|
2095
|
+
const level = node.attrs?.level || 1;
|
|
2096
|
+
const fontSizes = { 1: 24, 2: 20, 3: 16 };
|
|
2097
|
+
const fontSize = fontSizes[level] || 16;
|
|
2098
|
+
const spacing = fontSize * 0.6;
|
|
2099
|
+
ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR + spacing);
|
|
2100
|
+
state.y += spacing / 2;
|
|
2101
|
+
if (!node.content) break;
|
|
2102
|
+
const headingFonts = {
|
|
2103
|
+
regular: state.fonts.bold,
|
|
2104
|
+
bold: state.fonts.bold,
|
|
2105
|
+
italic: state.fonts.boldItalic,
|
|
2106
|
+
boldItalic: state.fonts.boldItalic
|
|
2107
|
+
};
|
|
2108
|
+
const segments = collectInlineSegments(node.content, headingFonts, fontSize);
|
|
2109
|
+
layoutInlineSegments(state, segments, fontSize, 0, region);
|
|
2110
|
+
state.y += spacing / 2;
|
|
2111
|
+
break;
|
|
2112
|
+
}
|
|
2113
|
+
case "paragraph": {
|
|
2114
|
+
const fontSize = BODY_FONT_SIZE;
|
|
2115
|
+
if (!node.content || node.content.length === 0) {
|
|
2116
|
+
state.y += fontSize * LINE_HEIGHT_FACTOR * 0.65;
|
|
2117
|
+
break;
|
|
2118
|
+
}
|
|
2119
|
+
processInlineContent(state, node.content, fontSize, 0, region);
|
|
2120
|
+
state.y += 2;
|
|
2121
|
+
break;
|
|
2122
|
+
}
|
|
2123
|
+
case "bulletList": {
|
|
2124
|
+
for (const item of node.content || []) {
|
|
2125
|
+
for (const child of item.content || []) {
|
|
2126
|
+
const fontSize = BODY_FONT_SIZE;
|
|
2127
|
+
ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
|
|
2128
|
+
const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
|
|
2129
|
+
state.currentPage.drawText("\u2022", {
|
|
2130
|
+
x: region.leftX + 8,
|
|
2131
|
+
y: bulletPdfY,
|
|
2132
|
+
size: fontSize,
|
|
2133
|
+
font: state.fonts.regular,
|
|
2134
|
+
color: pdfLib.rgb(0, 0, 0)
|
|
2135
|
+
});
|
|
2136
|
+
processInlineContent(state, child.content, fontSize, 24, region);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
state.y += 4;
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
case "orderedList": {
|
|
2143
|
+
let num = 1;
|
|
2144
|
+
for (const item of node.content || []) {
|
|
2145
|
+
for (const child of item.content || []) {
|
|
2146
|
+
const fontSize = BODY_FONT_SIZE;
|
|
2147
|
+
ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
|
|
2148
|
+
const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
|
|
2149
|
+
state.currentPage.drawText(`${num}.`, {
|
|
2150
|
+
x: region.leftX + 4,
|
|
2151
|
+
y: numPdfY,
|
|
2152
|
+
size: fontSize,
|
|
2153
|
+
font: state.fonts.regular,
|
|
2154
|
+
color: pdfLib.rgb(0, 0, 0)
|
|
2155
|
+
});
|
|
2156
|
+
processInlineContent(state, child.content, fontSize, 24, region);
|
|
2157
|
+
}
|
|
2158
|
+
num++;
|
|
2159
|
+
}
|
|
2160
|
+
state.y += 4;
|
|
2161
|
+
break;
|
|
2162
|
+
}
|
|
1256
2163
|
case "blockquote": {
|
|
1257
2164
|
const startY = state.y;
|
|
1258
2165
|
for (const child of node.content || []) {
|
|
1259
|
-
processInlineContent(state, child.content, 12, 16);
|
|
2166
|
+
processInlineContent(state, child.content, 12, 16, region);
|
|
1260
2167
|
}
|
|
1261
2168
|
const endY = state.y;
|
|
1262
|
-
const barX =
|
|
2169
|
+
const barX = region.leftX + 4;
|
|
1263
2170
|
const barTop = PAGE_HEIGHT - MARGIN - startY;
|
|
1264
2171
|
const barBottom = PAGE_HEIGHT - MARGIN - endY;
|
|
1265
2172
|
state.currentPage.drawLine({
|
|
@@ -1272,16 +2179,16 @@ function processBlock(state, node) {
|
|
|
1272
2179
|
break;
|
|
1273
2180
|
}
|
|
1274
2181
|
case "horizontalRule": {
|
|
1275
|
-
ensureSpace(state,
|
|
1276
|
-
state.y +=
|
|
2182
|
+
ensureSpace(state, 10);
|
|
2183
|
+
state.y += 4;
|
|
1277
2184
|
const ruleY = PAGE_HEIGHT - MARGIN - state.y;
|
|
1278
2185
|
state.currentPage.drawLine({
|
|
1279
|
-
start: { x:
|
|
1280
|
-
end: { x:
|
|
1281
|
-
thickness:
|
|
1282
|
-
color:
|
|
2186
|
+
start: { x: region.leftX, y: ruleY },
|
|
2187
|
+
end: { x: region.leftX + region.width, y: ruleY },
|
|
2188
|
+
thickness: 2.5,
|
|
2189
|
+
color: state.theme.rule
|
|
1283
2190
|
});
|
|
1284
|
-
state.y +=
|
|
2191
|
+
state.y += 4;
|
|
1285
2192
|
break;
|
|
1286
2193
|
}
|
|
1287
2194
|
case "codeBlock": {
|
|
@@ -1293,9 +2200,9 @@ function processBlock(state, node) {
|
|
|
1293
2200
|
ensureSpace(state, blockHeight);
|
|
1294
2201
|
const boxY = PAGE_HEIGHT - MARGIN - state.y - blockHeight;
|
|
1295
2202
|
state.currentPage.drawRectangle({
|
|
1296
|
-
x:
|
|
2203
|
+
x: region.leftX,
|
|
1297
2204
|
y: boxY,
|
|
1298
|
-
width:
|
|
2205
|
+
width: region.width,
|
|
1299
2206
|
height: blockHeight,
|
|
1300
2207
|
color: pdfLib.rgb(0.95, 0.95, 0.95),
|
|
1301
2208
|
borderColor: pdfLib.rgb(0.85, 0.85, 0.85),
|
|
@@ -1303,18 +2210,325 @@ function processBlock(state, node) {
|
|
|
1303
2210
|
});
|
|
1304
2211
|
state.y += 8;
|
|
1305
2212
|
for (const line of lines) {
|
|
1306
|
-
drawText(state, line || " ", state.fonts.regular, fontSize, 8);
|
|
2213
|
+
drawText(state, line || " ", state.fonts.regular, fontSize, 8, region);
|
|
1307
2214
|
}
|
|
1308
2215
|
state.y += 8;
|
|
1309
2216
|
break;
|
|
1310
2217
|
}
|
|
2218
|
+
case "table": {
|
|
2219
|
+
renderTable(state, node, region);
|
|
2220
|
+
break;
|
|
2221
|
+
}
|
|
2222
|
+
case "imageNode": {
|
|
2223
|
+
const src = node.attrs?.src || "";
|
|
2224
|
+
const image = src ? state.images.get(src) : void 0;
|
|
2225
|
+
if (!image) break;
|
|
2226
|
+
const natW = image.width;
|
|
2227
|
+
const natH = image.height;
|
|
2228
|
+
let w = parseFloat(node.attrs?.width || "") || 0;
|
|
2229
|
+
let h = parseFloat(node.attrs?.height || "") || 0;
|
|
2230
|
+
if (w && !h) h = natH / natW * w;
|
|
2231
|
+
else if (h && !w) w = natW / natH * h;
|
|
2232
|
+
else if (!w && !h) {
|
|
2233
|
+
w = natW;
|
|
2234
|
+
h = natH;
|
|
2235
|
+
}
|
|
2236
|
+
if (w > region.width) {
|
|
2237
|
+
h = h * (region.width / w);
|
|
2238
|
+
w = region.width;
|
|
2239
|
+
}
|
|
2240
|
+
const maxH = CONTENT_HEIGHT * 0.6;
|
|
2241
|
+
if (h > maxH) {
|
|
2242
|
+
w = w * (maxH / h);
|
|
2243
|
+
h = maxH;
|
|
2244
|
+
}
|
|
2245
|
+
ensureSpace(state, h + 4);
|
|
2246
|
+
const align = node.attrs?.align || "left";
|
|
2247
|
+
let x = region.leftX;
|
|
2248
|
+
if (align === "center") x = region.leftX + (region.width - w) / 2;
|
|
2249
|
+
else if (align === "right") x = region.leftX + region.width - w;
|
|
2250
|
+
const pdfY = PAGE_HEIGHT - MARGIN - state.y - h;
|
|
2251
|
+
state.currentPage.drawImage(image, { x, y: pdfY, width: w, height: h });
|
|
2252
|
+
state.y += h + 4;
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
// ─── Layout directives ─────────────────────────────────────────
|
|
2256
|
+
case "panel": {
|
|
2257
|
+
const title = node.attrs?.title || "";
|
|
2258
|
+
const border = node.attrs?.border || "solid";
|
|
2259
|
+
const headerStyle = node.attrs?.headerStyle || "";
|
|
2260
|
+
const padding = 8;
|
|
2261
|
+
const titleFontSize = 10;
|
|
2262
|
+
const titleHeight = title ? titleFontSize * LINE_HEIGHT_FACTOR + 6 : 0;
|
|
2263
|
+
const panelStartY = state.y;
|
|
2264
|
+
const panelStartPage = state.pageIndex;
|
|
2265
|
+
if (title) {
|
|
2266
|
+
state.y += titleHeight;
|
|
2267
|
+
}
|
|
2268
|
+
const isHeaderOnly = border === "none";
|
|
2269
|
+
if (!isHeaderOnly) {
|
|
2270
|
+
state.y += padding;
|
|
2271
|
+
}
|
|
2272
|
+
const innerRegion = {
|
|
2273
|
+
leftX: region.leftX + padding,
|
|
2274
|
+
width: region.width - padding * 2
|
|
2275
|
+
};
|
|
2276
|
+
if (!isHeaderOnly) {
|
|
2277
|
+
for (const child of node.content || []) {
|
|
2278
|
+
processBlock(state, child, innerRegion);
|
|
2279
|
+
}
|
|
2280
|
+
state.y += padding;
|
|
2281
|
+
}
|
|
2282
|
+
const startPage = state.pdfDoc.getPages()[panelStartPage];
|
|
2283
|
+
if (title) {
|
|
2284
|
+
const titleBarY = PAGE_HEIGHT - MARGIN - panelStartY - titleHeight;
|
|
2285
|
+
const isDarkHeader = headerStyle === "dark";
|
|
2286
|
+
startPage.drawRectangle({
|
|
2287
|
+
x: region.leftX,
|
|
2288
|
+
y: titleBarY,
|
|
2289
|
+
width: region.width,
|
|
2290
|
+
height: titleHeight,
|
|
2291
|
+
color: isDarkHeader ? state.theme.panelDarkHeaderBg : pdfLib.rgb(0.93, 0.93, 0.93),
|
|
2292
|
+
borderColor: state.theme.panelBorder,
|
|
2293
|
+
borderWidth: 0.75
|
|
2294
|
+
});
|
|
2295
|
+
startPage.drawText(title, {
|
|
2296
|
+
x: region.leftX + padding,
|
|
2297
|
+
y: titleBarY + 4,
|
|
2298
|
+
size: titleFontSize,
|
|
2299
|
+
font: state.fonts.bold,
|
|
2300
|
+
color: isDarkHeader ? state.theme.panelDarkHeaderText : pdfLib.rgb(0, 0, 0)
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
if (border !== "none") {
|
|
2304
|
+
const borderDashArray = border === "dashed" ? [4, 2] : void 0;
|
|
2305
|
+
if (panelStartPage === state.pageIndex) {
|
|
2306
|
+
const borderY = PAGE_HEIGHT - MARGIN - state.y;
|
|
2307
|
+
const panelHeight = state.y - panelStartY;
|
|
2308
|
+
startPage.drawRectangle({
|
|
2309
|
+
x: region.leftX,
|
|
2310
|
+
y: borderY,
|
|
2311
|
+
width: region.width,
|
|
2312
|
+
height: panelHeight,
|
|
2313
|
+
borderColor: state.theme.panelBorder,
|
|
2314
|
+
borderWidth: 0.75,
|
|
2315
|
+
borderDashArray
|
|
2316
|
+
});
|
|
2317
|
+
} else {
|
|
2318
|
+
const startBorderBottom = MARGIN;
|
|
2319
|
+
const startPanelHeight = PAGE_HEIGHT - MARGIN - panelStartY - startBorderBottom;
|
|
2320
|
+
startPage.drawRectangle({
|
|
2321
|
+
x: region.leftX,
|
|
2322
|
+
y: startBorderBottom,
|
|
2323
|
+
width: region.width,
|
|
2324
|
+
height: startPanelHeight,
|
|
2325
|
+
borderColor: state.theme.panelBorder,
|
|
2326
|
+
borderWidth: 0.75,
|
|
2327
|
+
borderDashArray
|
|
2328
|
+
});
|
|
2329
|
+
const contTop = PAGE_HEIGHT - MARGIN;
|
|
2330
|
+
const contBorderY = PAGE_HEIGHT - MARGIN - state.y;
|
|
2331
|
+
const contHeight = contTop - contBorderY;
|
|
2332
|
+
state.currentPage.drawRectangle({
|
|
2333
|
+
x: region.leftX,
|
|
2334
|
+
y: contBorderY,
|
|
2335
|
+
width: region.width,
|
|
2336
|
+
height: contHeight,
|
|
2337
|
+
borderColor: state.theme.panelBorder,
|
|
2338
|
+
borderWidth: 0.75,
|
|
2339
|
+
borderDashArray
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
state.y += 4;
|
|
2344
|
+
break;
|
|
2345
|
+
}
|
|
2346
|
+
case "columns": {
|
|
2347
|
+
const split = parseInt(node.attrs?.split || "50", 10);
|
|
2348
|
+
const padX = parseFloat(node.attrs?.padX || "0") || 0;
|
|
2349
|
+
const columns = (node.content || []).filter((c) => c.type === "column");
|
|
2350
|
+
if (columns.length === 0) break;
|
|
2351
|
+
const colRegionLeftX = region.leftX + padX;
|
|
2352
|
+
const colRegionWidth = region.width - 2 * padX;
|
|
2353
|
+
const gap = 12;
|
|
2354
|
+
const columnsStartY = state.y;
|
|
2355
|
+
const columnsStartPage = state.pageIndex;
|
|
2356
|
+
let colWidths;
|
|
2357
|
+
if (columns.length === 2) {
|
|
2358
|
+
const firstWidth = (colRegionWidth - gap) * (split / 100);
|
|
2359
|
+
const secondWidth = colRegionWidth - gap - firstWidth;
|
|
2360
|
+
colWidths = [firstWidth, secondWidth];
|
|
2361
|
+
} else {
|
|
2362
|
+
const eachWidth = (colRegionWidth - gap * (columns.length - 1)) / columns.length;
|
|
2363
|
+
colWidths = columns.map(() => eachWidth);
|
|
2364
|
+
}
|
|
2365
|
+
let maxEndY = columnsStartY;
|
|
2366
|
+
let colX = colRegionLeftX;
|
|
2367
|
+
for (let ci = 0; ci < columns.length; ci++) {
|
|
2368
|
+
const col = columns[ci];
|
|
2369
|
+
const colWidth = colWidths[ci];
|
|
2370
|
+
const colRegion = { leftX: colX, width: colWidth };
|
|
2371
|
+
state.y = columnsStartY;
|
|
2372
|
+
state.pageIndex = columnsStartPage;
|
|
2373
|
+
state.currentPage = state.pdfDoc.getPages()[state.pageIndex];
|
|
2374
|
+
const padTop = parseFloat(col.attrs?.padTop || "0") || 0;
|
|
2375
|
+
if (padTop > 0) {
|
|
2376
|
+
state.y += padTop * BODY_FONT_SIZE * LINE_HEIGHT_FACTOR;
|
|
2377
|
+
}
|
|
2378
|
+
for (const child of col.content || []) {
|
|
2379
|
+
processBlock(state, child, colRegion);
|
|
2380
|
+
}
|
|
2381
|
+
maxEndY = Math.max(maxEndY, state.y);
|
|
2382
|
+
colX += colWidth + gap;
|
|
2383
|
+
}
|
|
2384
|
+
state.y = maxEndY;
|
|
2385
|
+
state.y += 4;
|
|
2386
|
+
break;
|
|
2387
|
+
}
|
|
2388
|
+
case "column": {
|
|
2389
|
+
for (const child of node.content || []) {
|
|
2390
|
+
processBlock(state, child, region);
|
|
2391
|
+
}
|
|
2392
|
+
break;
|
|
2393
|
+
}
|
|
2394
|
+
case "watermark": {
|
|
2395
|
+
break;
|
|
2396
|
+
}
|
|
2397
|
+
case "subtotalsBlock": {
|
|
2398
|
+
const stFontSize = BODY_FONT_SIZE;
|
|
2399
|
+
const stLineHeight = stFontSize * LINE_HEIGHT_FACTOR;
|
|
2400
|
+
const valueRightX = region.leftX + region.width;
|
|
2401
|
+
const rows = (node.content || []).filter(
|
|
2402
|
+
(c) => c.type === "paragraph" && c.content?.length
|
|
2403
|
+
);
|
|
2404
|
+
const parsed = [];
|
|
2405
|
+
for (const row of rows) {
|
|
2406
|
+
let label = "";
|
|
2407
|
+
let value = "";
|
|
2408
|
+
for (const seg of row.content || []) {
|
|
2409
|
+
if (seg.type === "text") {
|
|
2410
|
+
const isBold = seg.marks?.some((m) => m.type === "bold");
|
|
2411
|
+
if (isBold) {
|
|
2412
|
+
label += seg.text || "";
|
|
2413
|
+
} else {
|
|
2414
|
+
const t = (seg.text || "").trim();
|
|
2415
|
+
if (t) value += (value ? " " : "") + t;
|
|
2416
|
+
}
|
|
2417
|
+
} else if (seg.type === "variableNode") {
|
|
2418
|
+
const varLabel = seg.attrs?.varLabel || seg.attrs?.varName || "";
|
|
2419
|
+
const isBoldVar = seg.marks?.some((m) => m.type === "bold");
|
|
2420
|
+
if (isBoldVar) {
|
|
2421
|
+
label += varLabel;
|
|
2422
|
+
} else {
|
|
2423
|
+
value += (value ? " " : "") + `[${varLabel}]`;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
parsed.push({ label: label.trim(), value: value.trim() });
|
|
2428
|
+
}
|
|
2429
|
+
let maxLabelWidth = 0;
|
|
2430
|
+
for (const { label } of parsed) {
|
|
2431
|
+
if (label) {
|
|
2432
|
+
const w = state.fonts.bold.widthOfTextAtSize(label, stFontSize);
|
|
2433
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
const gap = 12;
|
|
2437
|
+
const labelX = region.width > 300 ? Math.max(region.leftX + region.width - maxLabelWidth - gap - 80, region.leftX + region.width * 0.4) : region.leftX + 4;
|
|
2438
|
+
const valueColStartX = labelX + maxLabelWidth + gap;
|
|
2439
|
+
const isLastRowTotal = parsed.length > 1;
|
|
2440
|
+
for (let ri = 0; ri < parsed.length; ri++) {
|
|
2441
|
+
const isTotal = isLastRowTotal && ri === parsed.length - 1;
|
|
2442
|
+
if (isTotal) {
|
|
2443
|
+
state.y += stLineHeight * 0.6;
|
|
2444
|
+
}
|
|
2445
|
+
ensureSpace(state, stLineHeight + (isTotal ? 8 : 4));
|
|
2446
|
+
const { label, value } = parsed[ri];
|
|
2447
|
+
const pdfY = PAGE_HEIGHT - MARGIN - state.y - stFontSize;
|
|
2448
|
+
if (isTotal) {
|
|
2449
|
+
state.currentPage.drawLine({
|
|
2450
|
+
start: { x: valueColStartX, y: pdfY + stFontSize + 6 },
|
|
2451
|
+
end: { x: valueRightX, y: pdfY + stFontSize + 6 },
|
|
2452
|
+
thickness: 0.75,
|
|
2453
|
+
color: pdfLib.rgb(0, 0, 0)
|
|
2454
|
+
});
|
|
2455
|
+
state.currentPage.drawLine({
|
|
2456
|
+
start: { x: valueColStartX, y: pdfY + stFontSize + 3 },
|
|
2457
|
+
end: { x: valueRightX, y: pdfY + stFontSize + 3 },
|
|
2458
|
+
thickness: 0.75,
|
|
2459
|
+
color: pdfLib.rgb(0, 0, 0)
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
if (label) {
|
|
2463
|
+
state.currentPage.drawText(label, {
|
|
2464
|
+
x: labelX,
|
|
2465
|
+
y: pdfY,
|
|
2466
|
+
size: stFontSize,
|
|
2467
|
+
font: state.fonts.bold,
|
|
2468
|
+
color: pdfLib.rgb(0, 0, 0)
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
if (value) {
|
|
2472
|
+
const valueFont = state.fonts.bold;
|
|
2473
|
+
const valueWidth = valueFont.widthOfTextAtSize(value, stFontSize);
|
|
2474
|
+
state.currentPage.drawText(value, {
|
|
2475
|
+
x: valueRightX - valueWidth,
|
|
2476
|
+
y: pdfY,
|
|
2477
|
+
size: stFontSize,
|
|
2478
|
+
font: valueFont,
|
|
2479
|
+
color: pdfLib.rgb(0, 0, 0)
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
state.y += stLineHeight;
|
|
2483
|
+
}
|
|
2484
|
+
state.y += 4;
|
|
2485
|
+
break;
|
|
2486
|
+
}
|
|
2487
|
+
case "repeatBlock":
|
|
2488
|
+
if (node.content) {
|
|
2489
|
+
for (const child of node.content) {
|
|
2490
|
+
processBlock(state, child, region);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
break;
|
|
1311
2494
|
default:
|
|
1312
2495
|
if (node.content) {
|
|
1313
|
-
processInlineContent(state, node.content, 12);
|
|
2496
|
+
processInlineContent(state, node.content, 12, 0, region);
|
|
1314
2497
|
}
|
|
1315
2498
|
break;
|
|
1316
2499
|
}
|
|
1317
2500
|
}
|
|
2501
|
+
function renderWatermarksOnPage(page, fonts, watermarkNodes) {
|
|
2502
|
+
for (const wmNode of watermarkNodes) {
|
|
2503
|
+
const text = wmNode.attrs?.text || "";
|
|
2504
|
+
if (!text) continue;
|
|
2505
|
+
const opacity = parseFloat(wmNode.attrs?.opacity || "0.15");
|
|
2506
|
+
const angle = parseFloat(wmNode.attrs?.angle || "-45");
|
|
2507
|
+
const fontSize = 100;
|
|
2508
|
+
const { width, height } = page.getSize();
|
|
2509
|
+
const centerX = width / 2;
|
|
2510
|
+
const centerY = height / 2;
|
|
2511
|
+
const textWidth = fonts.bold.widthOfTextAtSize(text, fontSize);
|
|
2512
|
+
page.drawText(text, {
|
|
2513
|
+
x: centerX - textWidth / 2 * Math.cos(angle * Math.PI / 180),
|
|
2514
|
+
y: centerY - textWidth / 2 * Math.sin(angle * Math.PI / 180),
|
|
2515
|
+
size: fontSize,
|
|
2516
|
+
font: fonts.bold,
|
|
2517
|
+
color: pdfLib.rgb(0.5, 0.5, 0.5),
|
|
2518
|
+
opacity,
|
|
2519
|
+
rotate: pdfLib.degrees(angle)
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
function collectWatermarkNodes(content) {
|
|
2524
|
+
const nodes = [];
|
|
2525
|
+
for (const block of content.content || []) {
|
|
2526
|
+
if (block.type === "watermark") {
|
|
2527
|
+
nodes.push(block);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
return nodes;
|
|
2531
|
+
}
|
|
1318
2532
|
function ensureFieldSuffix(fieldName, suffix) {
|
|
1319
2533
|
const cleanName = fieldName.replace(/_signature$/i, "").replace(/_initials$/i, "").replace(/_date$/i, "");
|
|
1320
2534
|
return `${cleanName}${suffix}`;
|
|
@@ -1424,6 +2638,24 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
|
|
|
1424
2638
|
pdfLib.PDFString.of(field.label)
|
|
1425
2639
|
);
|
|
1426
2640
|
}
|
|
2641
|
+
const initialsFontSize = Math.min(
|
|
2642
|
+
72,
|
|
2643
|
+
Math.max(8, Math.round(position.height * 0.55))
|
|
2644
|
+
);
|
|
2645
|
+
try {
|
|
2646
|
+
initialsField.setFontSize(initialsFontSize);
|
|
2647
|
+
const acro = initialsField;
|
|
2648
|
+
const daRef = acro.acroField.dict.get(pdfLib.PDFName.of("DA"));
|
|
2649
|
+
if (daRef) {
|
|
2650
|
+
const daStr = daRef.value;
|
|
2651
|
+
if (typeof daStr === "string") {
|
|
2652
|
+
const newDa = daStr.replace(/\/Helv\b/, "/TiBo").replace(/\/HeBo\b/, "/TiBo");
|
|
2653
|
+
acro.acroField.dict.set(pdfLib.PDFName.of("DA"), pdfLib.PDFString.of(newDa));
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
} catch (fontError) {
|
|
2657
|
+
console.warn(`Failed to set initials font:`, fontError);
|
|
2658
|
+
}
|
|
1427
2659
|
initialsField.enableReadOnly();
|
|
1428
2660
|
if (field.required) initialsField.enableRequired();
|
|
1429
2661
|
break;
|
|
@@ -1527,47 +2759,367 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
|
|
|
1527
2759
|
} catch (err) {
|
|
1528
2760
|
warnings.push(`Failed to create ${field.type} field "${field.name}": ${getErrorMessage(err)}`);
|
|
1529
2761
|
}
|
|
1530
|
-
}
|
|
1531
|
-
initPdfMetadata({ PDFName: pdfLib.PDFName, PDFString: pdfLib.PDFString });
|
|
1532
|
-
const metadata = buildMetadataObject(fields, actualFieldNames);
|
|
1533
|
-
setSigniphiMetadata(pdfDoc, metadata);
|
|
1534
|
-
return warnings;
|
|
1535
|
-
}
|
|
1536
|
-
async function generatePdfFromContent(content, options = {}) {
|
|
1537
|
-
const { drawFieldPlaceholders = false, embedFormFields = false, fields = [] } = options;
|
|
1538
|
-
const
|
|
1539
|
-
const
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
2762
|
+
}
|
|
2763
|
+
initPdfMetadata({ PDFName: pdfLib.PDFName, PDFString: pdfLib.PDFString });
|
|
2764
|
+
const metadata = buildMetadataObject(fields, actualFieldNames);
|
|
2765
|
+
setSigniphiMetadata(pdfDoc, metadata);
|
|
2766
|
+
return warnings;
|
|
2767
|
+
}
|
|
2768
|
+
async function generatePdfFromContent(content, options = {}) {
|
|
2769
|
+
const { drawFieldPlaceholders = false, embedFormFields = false, fields = [], header, footer } = options;
|
|
2770
|
+
const theme = resolveTheme(options.theme);
|
|
2771
|
+
const pdfDoc = await pdfLib.PDFDocument.create();
|
|
2772
|
+
const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
|
2773
|
+
const fonts = {
|
|
2774
|
+
regular: await pdfDoc.embedFont(pdfLib.StandardFonts.Helvetica),
|
|
2775
|
+
bold: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBold),
|
|
2776
|
+
italic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaOblique),
|
|
2777
|
+
boldItalic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBoldOblique)
|
|
2778
|
+
};
|
|
2779
|
+
const watermarkNodes = collectWatermarkNodes(content);
|
|
2780
|
+
const { images, warnings: imageWarnings } = await preloadImages(pdfDoc, content);
|
|
2781
|
+
const state = {
|
|
2782
|
+
currentPage: firstPage,
|
|
2783
|
+
pageIndex: 0,
|
|
2784
|
+
y: 0,
|
|
2785
|
+
fonts,
|
|
2786
|
+
pdfDoc,
|
|
2787
|
+
fieldPositions: /* @__PURE__ */ new Map(),
|
|
2788
|
+
drawFieldPlaceholders,
|
|
2789
|
+
watermarkNodes,
|
|
2790
|
+
theme,
|
|
2791
|
+
images
|
|
2792
|
+
};
|
|
2793
|
+
renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
|
|
2794
|
+
if (content.content) {
|
|
2795
|
+
for (const block of content.content) {
|
|
2796
|
+
processBlock(state, block);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
const allPages = pdfDoc.getPages();
|
|
2800
|
+
const totalPages = allPages.length;
|
|
2801
|
+
const pageNumFontSize = 9;
|
|
2802
|
+
const hfFontSize = 8;
|
|
2803
|
+
const hfColor = pdfLib.rgb(0.45, 0.45, 0.45);
|
|
2804
|
+
for (let i = 0; i < totalPages; i++) {
|
|
2805
|
+
const page = allPages[i];
|
|
2806
|
+
const label = `Page ${i + 1} of ${totalPages}`;
|
|
2807
|
+
const labelWidth = fonts.bold.widthOfTextAtSize(label, pageNumFontSize);
|
|
2808
|
+
page.drawText(label, {
|
|
2809
|
+
x: PAGE_WIDTH - MARGIN - labelWidth,
|
|
2810
|
+
y: MARGIN / 2 - pageNumFontSize / 2,
|
|
2811
|
+
size: pageNumFontSize,
|
|
2812
|
+
font: fonts.bold,
|
|
2813
|
+
color: pdfLib.rgb(0, 0, 0)
|
|
2814
|
+
});
|
|
2815
|
+
if (header && (header.left || header.right) && !(header.skipFirstPage && i === 0)) {
|
|
2816
|
+
const headerBaseY = PAGE_HEIGHT - MARGIN + 10;
|
|
2817
|
+
if (header.left) {
|
|
2818
|
+
page.drawText(header.left, {
|
|
2819
|
+
x: MARGIN,
|
|
2820
|
+
y: headerBaseY,
|
|
2821
|
+
size: pageNumFontSize,
|
|
2822
|
+
font: fonts.bold,
|
|
2823
|
+
color: theme.pageHeaderText
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
if (header.right) {
|
|
2827
|
+
const rightWidth = fonts.regular.widthOfTextAtSize(header.right, hfFontSize);
|
|
2828
|
+
page.drawText(header.right, {
|
|
2829
|
+
x: PAGE_WIDTH - MARGIN - rightWidth,
|
|
2830
|
+
y: headerBaseY,
|
|
2831
|
+
size: hfFontSize,
|
|
2832
|
+
font: fonts.regular,
|
|
2833
|
+
color: hfColor
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
page.drawLine({
|
|
2837
|
+
start: { x: MARGIN, y: headerBaseY - 6 },
|
|
2838
|
+
end: { x: PAGE_WIDTH - MARGIN, y: headerBaseY - 6 },
|
|
2839
|
+
thickness: 0.75,
|
|
2840
|
+
color: theme.pageHeaderRule
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
if (footer?.left) {
|
|
2844
|
+
page.drawText(footer.left, {
|
|
2845
|
+
x: MARGIN,
|
|
2846
|
+
y: MARGIN / 2 - pageNumFontSize / 2,
|
|
2847
|
+
size: hfFontSize,
|
|
2848
|
+
font: fonts.regular,
|
|
2849
|
+
color: hfColor
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
let fieldWarnings;
|
|
2854
|
+
if (embedFormFields && fields.length > 0) {
|
|
2855
|
+
const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
|
|
2856
|
+
if (warnings.length > 0) fieldWarnings = warnings;
|
|
2857
|
+
}
|
|
2858
|
+
const pdfBytes = new Uint8Array(await pdfDoc.save({ useObjectStreams: false }));
|
|
2859
|
+
return {
|
|
2860
|
+
pdfBytes,
|
|
2861
|
+
fieldPositions: state.fieldPositions,
|
|
2862
|
+
fieldWarnings,
|
|
2863
|
+
imageWarnings: imageWarnings.length > 0 ? imageWarnings : void 0
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// src/utils/template-pipeline.ts
|
|
2868
|
+
async function generatePdfFromTiptap(content, values, options = {}) {
|
|
2869
|
+
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
|
|
2870
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
2871
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
2872
|
+
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
2873
|
+
const fields = extractFieldsFromContent(replaced);
|
|
2874
|
+
const result = await generatePdfFromContent(replaced, {
|
|
2875
|
+
embedFormFields: fields.length > 0,
|
|
2876
|
+
fields,
|
|
2877
|
+
theme: options.theme,
|
|
2878
|
+
header: options.header,
|
|
2879
|
+
footer: options.footer,
|
|
2880
|
+
drawFieldPlaceholders: options.drawFieldPlaceholders
|
|
2881
|
+
});
|
|
2882
|
+
return { pdfBytes: result.pdfBytes, imageWarnings: result.imageWarnings };
|
|
2883
|
+
}
|
|
2884
|
+
async function generatePdfFromMarkdown(markdown, values, options = {}) {
|
|
2885
|
+
const content = markdownToTiptap(markdown);
|
|
2886
|
+
return generatePdfFromTiptap(content, values, options);
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// src/utils/markdown-writer.ts
|
|
2890
|
+
function serializeFieldToken(attrs) {
|
|
2891
|
+
const parts = ["field"];
|
|
2892
|
+
if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
|
|
2893
|
+
if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
|
|
2894
|
+
if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
|
|
2895
|
+
if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
|
|
2896
|
+
if (attrs.required) parts.push(`required:true`);
|
|
2897
|
+
if (attrs.options) parts.push(`options:${attrs.options}`);
|
|
2898
|
+
if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
|
|
2899
|
+
if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
|
|
2900
|
+
if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
|
|
2901
|
+
if (attrs.multiline) parts.push(`multiline:true`);
|
|
2902
|
+
if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
|
|
2903
|
+
if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
|
|
2904
|
+
return `{{${parts.join("|")}}}`;
|
|
2905
|
+
}
|
|
2906
|
+
function serializeImageNode(attrs) {
|
|
2907
|
+
const hasExtras = attrs.varName || attrs.width || attrs.height || attrs.align;
|
|
2908
|
+
if (!hasExtras && attrs.src) {
|
|
2909
|
+
return ``;
|
|
2910
|
+
}
|
|
2911
|
+
const parts = ["image"];
|
|
2912
|
+
if (attrs.varName) parts.push(`var:${attrs.varName}`);
|
|
2913
|
+
else if (attrs.src) parts.push(`src:${attrs.src}`);
|
|
2914
|
+
if (attrs.alt) parts.push(`alt:${attrs.alt}`);
|
|
2915
|
+
if (attrs.width) parts.push(`width:${attrs.width}`);
|
|
2916
|
+
if (attrs.height) parts.push(`height:${attrs.height}`);
|
|
2917
|
+
if (attrs.align) parts.push(`align:${attrs.align}`);
|
|
2918
|
+
return `{{${parts.join("|")}}}`;
|
|
2919
|
+
}
|
|
2920
|
+
function serializeVariableToken(attrs) {
|
|
2921
|
+
const parts = ["var"];
|
|
2922
|
+
if (attrs.varName) parts.push(`name:${attrs.varName}`);
|
|
2923
|
+
if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
|
|
2924
|
+
if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
|
|
2925
|
+
if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
|
|
2926
|
+
if (attrs.format) parts.push(`format:${attrs.format}`);
|
|
2927
|
+
if (attrs.block === "true") parts.push(`block:true`);
|
|
2928
|
+
return `{{${parts.join("|")}}}`;
|
|
2929
|
+
}
|
|
2930
|
+
function serializeInline(content) {
|
|
2931
|
+
if (!content) return "";
|
|
2932
|
+
let result = "";
|
|
2933
|
+
for (const node of content) {
|
|
2934
|
+
if (node.type === "fieldNode") {
|
|
2935
|
+
result += serializeFieldToken(node.attrs || {});
|
|
2936
|
+
continue;
|
|
2937
|
+
}
|
|
2938
|
+
if (node.type === "imageNode") {
|
|
2939
|
+
result += serializeImageNode(node.attrs || {});
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
if (node.type === "variableNode") {
|
|
2943
|
+
let token = serializeVariableToken(node.attrs || {});
|
|
2944
|
+
const varMarks = node.marks || [];
|
|
2945
|
+
const isBold = varMarks.some((m) => m.type === "bold");
|
|
2946
|
+
const isItalic = varMarks.some((m) => m.type === "italic");
|
|
2947
|
+
const isUnderline = varMarks.some((m) => m.type === "underline");
|
|
2948
|
+
if (isBold && isItalic) token = `***${token}***`;
|
|
2949
|
+
else if (isBold) token = `**${token}**`;
|
|
2950
|
+
else if (isItalic) token = `*${token}*`;
|
|
2951
|
+
if (isUnderline) token = `__${token}__`;
|
|
2952
|
+
result += token;
|
|
2953
|
+
continue;
|
|
2954
|
+
}
|
|
2955
|
+
if (node.type === "text") {
|
|
2956
|
+
let text = node.text || "";
|
|
2957
|
+
const marks = node.marks || [];
|
|
2958
|
+
for (const mark of marks) {
|
|
2959
|
+
if (mark.type === "bold") text = `**${text}**`;
|
|
2960
|
+
else if (mark.type === "italic") text = `*${text}*`;
|
|
2961
|
+
else if (mark.type === "underline") text = `__${text}__`;
|
|
2962
|
+
else if (mark.type === "code") text = `\`${text}\``;
|
|
2963
|
+
}
|
|
2964
|
+
result += text;
|
|
2965
|
+
}
|
|
2966
|
+
if (node.type === "hardBreak") {
|
|
2967
|
+
result += " \n";
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
return result;
|
|
2971
|
+
}
|
|
2972
|
+
function serializeBlock(node) {
|
|
2973
|
+
switch (node.type) {
|
|
2974
|
+
case "heading": {
|
|
2975
|
+
const level = node.attrs?.level || 1;
|
|
2976
|
+
const prefix = "#".repeat(level);
|
|
2977
|
+
return `${prefix} ${serializeInline(node.content)}`;
|
|
2978
|
+
}
|
|
2979
|
+
case "paragraph": {
|
|
2980
|
+
const inline = serializeInline(node.content);
|
|
2981
|
+
return inline;
|
|
2982
|
+
}
|
|
2983
|
+
case "bulletList": {
|
|
2984
|
+
return (node.content || []).map((item) => {
|
|
2985
|
+
const inner = (item.content || []).map(serializeBlock).join("\n");
|
|
2986
|
+
return `- ${inner}`;
|
|
2987
|
+
}).join("\n");
|
|
2988
|
+
}
|
|
2989
|
+
case "orderedList": {
|
|
2990
|
+
return (node.content || []).map((item, i) => {
|
|
2991
|
+
const inner = (item.content || []).map(serializeBlock).join("\n");
|
|
2992
|
+
return `${i + 1}. ${inner}`;
|
|
2993
|
+
}).join("\n");
|
|
2994
|
+
}
|
|
2995
|
+
case "listItem": {
|
|
2996
|
+
return (node.content || []).map(serializeBlock).join("\n");
|
|
2997
|
+
}
|
|
2998
|
+
case "blockquote": {
|
|
2999
|
+
return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
|
|
3000
|
+
}
|
|
3001
|
+
case "codeBlock": {
|
|
3002
|
+
const text = serializeInline(node.content);
|
|
3003
|
+
return `\`\`\`
|
|
3004
|
+
${text}
|
|
3005
|
+
\`\`\``;
|
|
3006
|
+
}
|
|
3007
|
+
case "horizontalRule": {
|
|
3008
|
+
return "---";
|
|
3009
|
+
}
|
|
3010
|
+
case "table": {
|
|
3011
|
+
const rows = node.content || [];
|
|
3012
|
+
const isBorderless = node.attrs?.borderless === true;
|
|
3013
|
+
const isSubtotals = node.attrs?.subtotals === true;
|
|
3014
|
+
const serializedRows = [];
|
|
3015
|
+
if (isBorderless && rows.length > 0) {
|
|
3016
|
+
const colCount = rows[0].content?.length || 0;
|
|
3017
|
+
const markerCell = isSubtotals ? "_" : "";
|
|
3018
|
+
const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
|
|
3019
|
+
const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
|
|
3020
|
+
serializedRows.push(markerRow);
|
|
3021
|
+
serializedRows.push(sep);
|
|
3022
|
+
}
|
|
3023
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
3024
|
+
const row = rows[ri];
|
|
3025
|
+
const cells = (row.content || []).map((cell) => {
|
|
3026
|
+
const inner = (cell.content || []).map(serializeBlock).join("");
|
|
3027
|
+
return inner;
|
|
3028
|
+
});
|
|
3029
|
+
serializedRows.push(`| ${cells.join(" | ")} |`);
|
|
3030
|
+
if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
|
|
3031
|
+
const sep = cells.map(() => "------").join("|");
|
|
3032
|
+
serializedRows.push(`|${sep}|`);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return serializedRows.join("\n");
|
|
3036
|
+
}
|
|
3037
|
+
case "tableRow": {
|
|
3038
|
+
const cells = (node.content || []).map((cell) => {
|
|
3039
|
+
const inner = (cell.content || []).map(serializeBlock).join("");
|
|
3040
|
+
return inner;
|
|
3041
|
+
});
|
|
3042
|
+
return `| ${cells.join(" | ")} |`;
|
|
3043
|
+
}
|
|
3044
|
+
case "tableHeader":
|
|
3045
|
+
case "tableCell": {
|
|
3046
|
+
return (node.content || []).map(serializeBlock).join("");
|
|
3047
|
+
}
|
|
3048
|
+
case "imageNode": {
|
|
3049
|
+
return serializeImageNode(node.attrs || {});
|
|
3050
|
+
}
|
|
3051
|
+
case "watermark": {
|
|
3052
|
+
const attrs = node.attrs || {};
|
|
3053
|
+
const parts = [];
|
|
3054
|
+
if (attrs.text) parts.push(`text:${attrs.text}`);
|
|
3055
|
+
if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
|
|
3056
|
+
if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
|
|
3057
|
+
return `:::watermark{${parts.join("|")}}`;
|
|
3058
|
+
}
|
|
3059
|
+
case "panel": {
|
|
3060
|
+
const attrs = node.attrs || {};
|
|
3061
|
+
const parts = [];
|
|
3062
|
+
if (attrs.title) parts.push(`title:${attrs.title}`);
|
|
3063
|
+
if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
|
|
3064
|
+
else if (attrs.border === "solid") parts.push(`border:solid`);
|
|
3065
|
+
if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
|
|
3066
|
+
const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
|
|
3067
|
+
const children = (node.content || []).map(serializeBlock).join("\n\n");
|
|
3068
|
+
return `:::panel${attrStr}
|
|
3069
|
+
${children}
|
|
3070
|
+
:::`;
|
|
3071
|
+
}
|
|
3072
|
+
case "repeatBlock": {
|
|
3073
|
+
const attrs = node.attrs || {};
|
|
3074
|
+
const parts = [];
|
|
3075
|
+
if (attrs.data) parts.push(`data:${attrs.data}`);
|
|
3076
|
+
const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
|
|
3077
|
+
const children = (node.content || []).map(serializeBlock).join("\n\n");
|
|
3078
|
+
return `:::repeat${attrStr}
|
|
3079
|
+
${children}
|
|
3080
|
+
:::`;
|
|
3081
|
+
}
|
|
3082
|
+
case "subtotalsBlock": {
|
|
3083
|
+
const children = (node.content || []).map(serializeBlock).join("\n");
|
|
3084
|
+
return `:::subtotals
|
|
3085
|
+
${children}
|
|
3086
|
+
:::`;
|
|
3087
|
+
}
|
|
3088
|
+
case "columns": {
|
|
3089
|
+
const attrs = node.attrs || {};
|
|
3090
|
+
const splitVal = attrs.split || "50";
|
|
3091
|
+
const cols = (node.content || []).filter((c) => c.type === "column");
|
|
3092
|
+
const colBlocks = cols.map((col) => {
|
|
3093
|
+
const children = (col.content || []).map(serializeBlock).join("\n\n");
|
|
3094
|
+
const colAttrs = col.attrs || {};
|
|
3095
|
+
const padTop = parseFloat(colAttrs.padTop || "0") || 0;
|
|
3096
|
+
const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
|
|
3097
|
+
return `${colTag}
|
|
3098
|
+
${children}
|
|
3099
|
+
:::`;
|
|
3100
|
+
});
|
|
3101
|
+
const padX = parseFloat(attrs.padX || "0") || 0;
|
|
3102
|
+
const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
|
|
3103
|
+
return `:::columns{${colsAttrs}}
|
|
3104
|
+
${colBlocks.join("\n")}
|
|
3105
|
+
:::`;
|
|
3106
|
+
}
|
|
3107
|
+
case "column": {
|
|
3108
|
+
const children = (node.content || []).map(serializeBlock).join("\n\n");
|
|
3109
|
+
const colAttrs = node.attrs || {};
|
|
3110
|
+
const padTop = parseFloat(colAttrs.padTop || "0") || 0;
|
|
3111
|
+
const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
|
|
3112
|
+
return `${colTag}
|
|
3113
|
+
${children}
|
|
3114
|
+
:::`;
|
|
1558
3115
|
}
|
|
3116
|
+
default:
|
|
3117
|
+
return serializeInline(node.content);
|
|
1559
3118
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
}
|
|
1565
|
-
const pdfBytes = await pdfDoc.save({ useObjectStreams: false });
|
|
1566
|
-
return {
|
|
1567
|
-
pdfBytes,
|
|
1568
|
-
fieldPositions: state.fieldPositions,
|
|
1569
|
-
fieldWarnings
|
|
1570
|
-
};
|
|
3119
|
+
}
|
|
3120
|
+
function tiptapToMarkdown(doc) {
|
|
3121
|
+
if (!doc.content) return "";
|
|
3122
|
+
return doc.content.map(serializeBlock).join("\n\n");
|
|
1571
3123
|
}
|
|
1572
3124
|
|
|
1573
3125
|
// src/utils/pdf-preview.ts
|
|
@@ -1656,8 +3208,26 @@ function useDocumentGenerator(options = {}) {
|
|
|
1656
3208
|
Placeholder__default.default.configure({
|
|
1657
3209
|
placeholder: options.placeholder || "Start writing your document..."
|
|
1658
3210
|
}),
|
|
3211
|
+
Table__default.default.extend({
|
|
3212
|
+
addAttributes() {
|
|
3213
|
+
return {
|
|
3214
|
+
...this.parent?.(),
|
|
3215
|
+
borderless: { default: false },
|
|
3216
|
+
subtotals: { default: false }
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
}).configure({ resizable: false }),
|
|
3220
|
+
TableRow__default.default,
|
|
3221
|
+
TableCell__default.default,
|
|
3222
|
+
TableHeader__default.default,
|
|
1659
3223
|
FieldNode,
|
|
1660
|
-
VariableNode
|
|
3224
|
+
VariableNode,
|
|
3225
|
+
PanelNode,
|
|
3226
|
+
ColumnsNode,
|
|
3227
|
+
ColumnNode,
|
|
3228
|
+
WatermarkNode,
|
|
3229
|
+
RepeatNode,
|
|
3230
|
+
SubtotalsNode
|
|
1661
3231
|
],
|
|
1662
3232
|
content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
|
|
1663
3233
|
onUpdate: ({ editor: ed }) => {
|
|
@@ -1761,10 +3331,9 @@ function useDocumentGenerator(options = {}) {
|
|
|
1761
3331
|
async (values) => {
|
|
1762
3332
|
if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
|
|
1763
3333
|
const content = editor.getJSON();
|
|
1764
|
-
const
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
return { pdfBytes: result.pdfBytes, pdfPages: pages };
|
|
3334
|
+
const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
|
|
3335
|
+
const pdfPages2 = await pdfToImages(pdfBytes2);
|
|
3336
|
+
return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
|
|
1768
3337
|
},
|
|
1769
3338
|
[editor]
|
|
1770
3339
|
);
|
|
@@ -2918,15 +4487,47 @@ function FieldEditPopover({
|
|
|
2918
4487
|
] }) })
|
|
2919
4488
|
] });
|
|
2920
4489
|
}
|
|
2921
|
-
|
|
4490
|
+
var sizeClasses = {
|
|
4491
|
+
md: "px-3 py-1.5 text-xs font-medium",
|
|
4492
|
+
sm: "px-2 py-0.5 text-[10px] font-medium"
|
|
4493
|
+
};
|
|
4494
|
+
function ToggleGroup({
|
|
4495
|
+
value,
|
|
4496
|
+
onChange,
|
|
4497
|
+
options,
|
|
4498
|
+
size = "md",
|
|
4499
|
+
className
|
|
4500
|
+
}) {
|
|
4501
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4502
|
+
"button",
|
|
4503
|
+
{
|
|
4504
|
+
className: cn(
|
|
4505
|
+
"rounded-md transition-colors",
|
|
4506
|
+
sizeClasses[size],
|
|
4507
|
+
value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
|
4508
|
+
),
|
|
4509
|
+
onClick: () => onChange(opt.value),
|
|
4510
|
+
children: opt.label
|
|
4511
|
+
},
|
|
4512
|
+
opt.value
|
|
4513
|
+
)) });
|
|
4514
|
+
}
|
|
4515
|
+
function labelToVarName2(label) {
|
|
2922
4516
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
2923
4517
|
}
|
|
4518
|
+
function varNameToLabel(name) {
|
|
4519
|
+
return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
4520
|
+
}
|
|
2924
4521
|
function VariableInsertPopover({
|
|
2925
4522
|
open,
|
|
2926
4523
|
onOpenChange,
|
|
2927
4524
|
onInsert,
|
|
4525
|
+
predefinedVariables,
|
|
2928
4526
|
children
|
|
2929
4527
|
}) {
|
|
4528
|
+
const hasPredefined = !!predefinedVariables?.length;
|
|
4529
|
+
const [mode, setMode] = React12.useState(hasPredefined ? "existing" : "new");
|
|
4530
|
+
const [search, setSearch] = React12.useState("");
|
|
2930
4531
|
const [varLabel, setVarLabel] = React12.useState("");
|
|
2931
4532
|
const [varName, setVarName] = React12.useState("");
|
|
2932
4533
|
const [varDefault, setVarDefault] = React12.useState("");
|
|
@@ -2936,7 +4537,9 @@ function VariableInsertPopover({
|
|
|
2936
4537
|
setVarName("");
|
|
2937
4538
|
setVarDefault("");
|
|
2938
4539
|
setNameManuallyEdited(false);
|
|
2939
|
-
|
|
4540
|
+
setSearch("");
|
|
4541
|
+
setMode(hasPredefined ? "existing" : "new");
|
|
4542
|
+
}, [hasPredefined]);
|
|
2940
4543
|
const handleOpenChange = React12.useCallback(
|
|
2941
4544
|
(nextOpen) => {
|
|
2942
4545
|
if (!nextOpen) reset();
|
|
@@ -2946,7 +4549,7 @@ function VariableInsertPopover({
|
|
|
2946
4549
|
);
|
|
2947
4550
|
React12.useEffect(() => {
|
|
2948
4551
|
if (!nameManuallyEdited) {
|
|
2949
|
-
setVarName(
|
|
4552
|
+
setVarName(labelToVarName2(varLabel));
|
|
2950
4553
|
}
|
|
2951
4554
|
}, [varLabel, nameManuallyEdited]);
|
|
2952
4555
|
const handleInsert = React12.useCallback(() => {
|
|
@@ -2954,10 +4557,32 @@ function VariableInsertPopover({
|
|
|
2954
4557
|
onInsert({
|
|
2955
4558
|
varName: varName.trim(),
|
|
2956
4559
|
varLabel: varLabel.trim(),
|
|
2957
|
-
varDefault: varDefault.trim()
|
|
4560
|
+
varDefault: varDefault.trim(),
|
|
4561
|
+
format: ""
|
|
2958
4562
|
});
|
|
2959
4563
|
handleOpenChange(false);
|
|
2960
4564
|
}, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
|
|
4565
|
+
const handlePickPredefined = React12.useCallback(
|
|
4566
|
+
(pv) => {
|
|
4567
|
+
onInsert({
|
|
4568
|
+
varName: pv.varName,
|
|
4569
|
+
varLabel: pv.varLabel || varNameToLabel(pv.varName),
|
|
4570
|
+
varDefault: "",
|
|
4571
|
+
format: ""
|
|
4572
|
+
});
|
|
4573
|
+
handleOpenChange(false);
|
|
4574
|
+
},
|
|
4575
|
+
[onInsert, handleOpenChange]
|
|
4576
|
+
);
|
|
4577
|
+
const filteredVars = React12.useMemo(() => {
|
|
4578
|
+
if (!predefinedVariables?.length) return [];
|
|
4579
|
+
const q = search.toLowerCase();
|
|
4580
|
+
if (!q) return predefinedVariables;
|
|
4581
|
+
return predefinedVariables.filter((pv) => {
|
|
4582
|
+
const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
|
|
4583
|
+
return pv.varName.toLowerCase().includes(q) || label.includes(q);
|
|
4584
|
+
});
|
|
4585
|
+
}, [predefinedVariables, search]);
|
|
2961
4586
|
return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
|
|
2962
4587
|
/* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children }),
|
|
2963
4588
|
/* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-72 p-0", align: "start", sideOffset: 8, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
|
|
@@ -2965,72 +4590,114 @@ function VariableInsertPopover({
|
|
|
2965
4590
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 14 }),
|
|
2966
4591
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Insert Variable" })
|
|
2967
4592
|
] }),
|
|
2968
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2969
|
-
|
|
2970
|
-
|
|
4593
|
+
hasPredefined && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4594
|
+
ToggleGroup,
|
|
4595
|
+
{
|
|
4596
|
+
value: mode,
|
|
4597
|
+
onChange: setMode,
|
|
4598
|
+
options: [
|
|
4599
|
+
{ value: "existing", label: "Existing" },
|
|
4600
|
+
{ value: "new", label: "New" }
|
|
4601
|
+
],
|
|
4602
|
+
size: "sm"
|
|
4603
|
+
}
|
|
4604
|
+
),
|
|
4605
|
+
mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
4606
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
4607
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
|
|
2971
4608
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2972
4609
|
Input,
|
|
2973
4610
|
{
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
className: "h-8 text-xs"
|
|
4611
|
+
value: search,
|
|
4612
|
+
onChange: (e) => setSearch(e.target.value),
|
|
4613
|
+
placeholder: "Search variables...",
|
|
4614
|
+
className: "h-8 text-xs pl-7"
|
|
2979
4615
|
}
|
|
2980
4616
|
)
|
|
2981
4617
|
] }),
|
|
2982
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2983
|
-
|
|
4618
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-48 overflow-y-auto -mx-1", children: filteredVars.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground px-2 py-3 text-center", children: "No variables found" }) : filteredVars.map((pv) => {
|
|
4619
|
+
const label = pv.varLabel || varNameToLabel(pv.varName);
|
|
4620
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4621
|
+
"button",
|
|
4622
|
+
{
|
|
4623
|
+
className: "w-full text-left px-2 py-1.5 rounded-md text-xs hover:bg-accent transition-colors flex flex-col gap-0.5",
|
|
4624
|
+
onClick: () => handlePickPredefined(pv),
|
|
4625
|
+
children: [
|
|
4626
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: label }),
|
|
4627
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
|
|
4628
|
+
]
|
|
4629
|
+
},
|
|
4630
|
+
pv.varName
|
|
4631
|
+
);
|
|
4632
|
+
}) })
|
|
4633
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4634
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
4635
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4636
|
+
/* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
|
|
4637
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4638
|
+
Input,
|
|
4639
|
+
{
|
|
4640
|
+
id: "var-label",
|
|
4641
|
+
value: varLabel,
|
|
4642
|
+
onChange: (e) => setVarLabel(e.target.value),
|
|
4643
|
+
placeholder: "e.g. Company Name",
|
|
4644
|
+
className: "h-8 text-xs"
|
|
4645
|
+
}
|
|
4646
|
+
)
|
|
4647
|
+
] }),
|
|
4648
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4649
|
+
/* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
|
|
4650
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4651
|
+
Input,
|
|
4652
|
+
{
|
|
4653
|
+
id: "var-name",
|
|
4654
|
+
value: varName,
|
|
4655
|
+
onChange: (e) => {
|
|
4656
|
+
setVarName(e.target.value);
|
|
4657
|
+
setNameManuallyEdited(true);
|
|
4658
|
+
},
|
|
4659
|
+
placeholder: "e.g. company_name",
|
|
4660
|
+
className: "h-8 text-xs font-mono"
|
|
4661
|
+
}
|
|
4662
|
+
),
|
|
4663
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
|
|
4664
|
+
] }),
|
|
4665
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4666
|
+
/* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
|
|
4667
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4668
|
+
Input,
|
|
4669
|
+
{
|
|
4670
|
+
id: "var-default",
|
|
4671
|
+
value: varDefault,
|
|
4672
|
+
onChange: (e) => setVarDefault(e.target.value),
|
|
4673
|
+
placeholder: "Optional",
|
|
4674
|
+
className: "h-8 text-xs"
|
|
4675
|
+
}
|
|
4676
|
+
)
|
|
4677
|
+
] })
|
|
4678
|
+
] }),
|
|
4679
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
|
|
2984
4680
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2985
|
-
|
|
4681
|
+
Button,
|
|
2986
4682
|
{
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
},
|
|
2993
|
-
placeholder: "e.g. company_name",
|
|
2994
|
-
className: "h-8 text-xs font-mono"
|
|
4683
|
+
size: "sm",
|
|
4684
|
+
className: "h-8 flex-1 text-xs",
|
|
4685
|
+
onClick: handleInsert,
|
|
4686
|
+
disabled: !varLabel.trim() || !varName.trim(),
|
|
4687
|
+
children: "Insert"
|
|
2995
4688
|
}
|
|
2996
4689
|
),
|
|
2997
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
|
|
2998
|
-
] }),
|
|
2999
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3000
|
-
/* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
|
|
3001
4690
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3002
|
-
|
|
4691
|
+
Button,
|
|
3003
4692
|
{
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
4693
|
+
variant: "outline",
|
|
4694
|
+
size: "sm",
|
|
4695
|
+
className: "h-8 text-xs",
|
|
4696
|
+
onClick: () => handleOpenChange(false),
|
|
4697
|
+
children: "Cancel"
|
|
3009
4698
|
}
|
|
3010
4699
|
)
|
|
3011
4700
|
] })
|
|
3012
|
-
] }),
|
|
3013
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
|
|
3014
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3015
|
-
Button,
|
|
3016
|
-
{
|
|
3017
|
-
size: "sm",
|
|
3018
|
-
className: "h-8 flex-1 text-xs",
|
|
3019
|
-
onClick: handleInsert,
|
|
3020
|
-
disabled: !varLabel.trim() || !varName.trim(),
|
|
3021
|
-
children: "Insert"
|
|
3022
|
-
}
|
|
3023
|
-
),
|
|
3024
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3025
|
-
Button,
|
|
3026
|
-
{
|
|
3027
|
-
variant: "outline",
|
|
3028
|
-
size: "sm",
|
|
3029
|
-
className: "h-8 text-xs",
|
|
3030
|
-
onClick: () => handleOpenChange(false),
|
|
3031
|
-
children: "Cancel"
|
|
3032
|
-
}
|
|
3033
|
-
)
|
|
3034
4701
|
] })
|
|
3035
4702
|
] }) })
|
|
3036
4703
|
] });
|
|
@@ -3252,7 +4919,6 @@ function PreviewPanel({
|
|
|
3252
4919
|
(f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
|
|
3253
4920
|
);
|
|
3254
4921
|
}, [positionedFields, currentPage, currentPageIndex]);
|
|
3255
|
-
const showScrollbars = zoomLevel > 1;
|
|
3256
4922
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col h-full border border-border rounded-lg overflow-hidden bg-muted/20", className), children: [
|
|
3257
4923
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border px-2 py-1.5 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
|
|
3258
4924
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
@@ -3368,12 +5034,13 @@ function PreviewPanel({
|
|
|
3368
5034
|
"div",
|
|
3369
5035
|
{
|
|
3370
5036
|
className: cn(
|
|
3371
|
-
"border border-border rounded-lg bg-muted/30 shrink-0"
|
|
3372
|
-
showScrollbars ? "overflow-auto" : "overflow-hidden"
|
|
5037
|
+
"border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
|
|
3373
5038
|
),
|
|
3374
5039
|
style: {
|
|
3375
5040
|
width: pageDisplaySize.viewportWidth,
|
|
3376
|
-
height: pageDisplaySize.viewportHeight
|
|
5041
|
+
height: pageDisplaySize.viewportHeight,
|
|
5042
|
+
maxWidth: "100%",
|
|
5043
|
+
maxHeight: "100%"
|
|
3377
5044
|
},
|
|
3378
5045
|
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3379
5046
|
"div",
|
|
@@ -3444,8 +5111,8 @@ function validateMarkdown(markdown) {
|
|
|
3444
5111
|
errors.push(`Malformed token at line ${lineNum}: missing | delimiter`);
|
|
3445
5112
|
} else {
|
|
3446
5113
|
const tokenType = tokenContent.slice(0, pipeIdx);
|
|
3447
|
-
if (tokenType !== "field" && tokenType !== "var") {
|
|
3448
|
-
errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field" or "
|
|
5114
|
+
if (tokenType !== "field" && tokenType !== "var" && tokenType !== "image") {
|
|
5115
|
+
errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field", "var" or "image")`);
|
|
3449
5116
|
} else {
|
|
3450
5117
|
const body = tokenContent.slice(pipeIdx + 1);
|
|
3451
5118
|
const pairs = body.split("|");
|
|
@@ -3459,12 +5126,48 @@ function validateMarkdown(markdown) {
|
|
|
3459
5126
|
if (!hasName) {
|
|
3460
5127
|
errors.push(`Variable token at line ${lineNum} is missing required "name" attribute`);
|
|
3461
5128
|
}
|
|
5129
|
+
} else if (tokenType === "image") {
|
|
5130
|
+
const hasSource = pairs.some((p) => p.startsWith("src:") || p.startsWith("var:"));
|
|
5131
|
+
if (!hasSource) {
|
|
5132
|
+
errors.push(`Image token at line ${lineNum} needs a "src" or "var" attribute`);
|
|
5133
|
+
}
|
|
3462
5134
|
}
|
|
3463
5135
|
}
|
|
3464
5136
|
}
|
|
3465
5137
|
searchFrom = closeIdx + 2;
|
|
3466
5138
|
}
|
|
3467
5139
|
}
|
|
5140
|
+
const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
|
|
5141
|
+
const DIRECTIVE_CLOSE_RE = /^:::$/;
|
|
5142
|
+
const directiveStack = [];
|
|
5143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5144
|
+
const trimmed = lines[i].trim();
|
|
5145
|
+
const lineNum = i + 1;
|
|
5146
|
+
const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
|
|
5147
|
+
if (openMatch) {
|
|
5148
|
+
const dtype = openMatch[1];
|
|
5149
|
+
if (dtype !== "watermark") {
|
|
5150
|
+
if (dtype === "col") {
|
|
5151
|
+
const parent = directiveStack[directiveStack.length - 1];
|
|
5152
|
+
if (!parent || parent.type !== "columns") {
|
|
5153
|
+
warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
directiveStack.push({ type: dtype, line: lineNum });
|
|
5157
|
+
}
|
|
5158
|
+
continue;
|
|
5159
|
+
}
|
|
5160
|
+
if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
|
|
5161
|
+
if (directiveStack.length === 0) {
|
|
5162
|
+
warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
|
|
5163
|
+
} else {
|
|
5164
|
+
directiveStack.pop();
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5168
|
+
for (const open of directiveStack) {
|
|
5169
|
+
errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
|
|
5170
|
+
}
|
|
3468
5171
|
let fields = [];
|
|
3469
5172
|
let variables = [];
|
|
3470
5173
|
try {
|
|
@@ -3498,31 +5201,6 @@ function validateMarkdown(markdown) {
|
|
|
3498
5201
|
fields
|
|
3499
5202
|
};
|
|
3500
5203
|
}
|
|
3501
|
-
var sizeClasses = {
|
|
3502
|
-
md: "px-3 py-1.5 text-xs font-medium",
|
|
3503
|
-
sm: "px-2 py-0.5 text-[10px] font-medium"
|
|
3504
|
-
};
|
|
3505
|
-
function ToggleGroup({
|
|
3506
|
-
value,
|
|
3507
|
-
onChange,
|
|
3508
|
-
options,
|
|
3509
|
-
size = "md",
|
|
3510
|
-
className
|
|
3511
|
-
}) {
|
|
3512
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3513
|
-
"button",
|
|
3514
|
-
{
|
|
3515
|
-
className: cn(
|
|
3516
|
-
"rounded-md transition-colors",
|
|
3517
|
-
sizeClasses[size],
|
|
3518
|
-
value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
|
3519
|
-
),
|
|
3520
|
-
onClick: () => onChange(opt.value),
|
|
3521
|
-
children: opt.label
|
|
3522
|
-
},
|
|
3523
|
-
opt.value
|
|
3524
|
-
)) });
|
|
3525
|
-
}
|
|
3526
5204
|
function parseCsv(text) {
|
|
3527
5205
|
const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
3528
5206
|
if (lines.length < 2) return [];
|
|
@@ -3572,7 +5250,10 @@ function GeneratePanel({
|
|
|
3572
5250
|
editorContent,
|
|
3573
5251
|
editorVariables,
|
|
3574
5252
|
onGeneratePdf,
|
|
3575
|
-
initialBulkData
|
|
5253
|
+
initialBulkData,
|
|
5254
|
+
initialVariableValues,
|
|
5255
|
+
exportFileName,
|
|
5256
|
+
templateMode
|
|
3576
5257
|
}) {
|
|
3577
5258
|
const [templateSource, setTemplateSource] = React12.useState("editor");
|
|
3578
5259
|
const [importedMarkdown, setImportedMarkdown] = React12.useState(null);
|
|
@@ -3580,6 +5261,7 @@ function GeneratePanel({
|
|
|
3580
5261
|
const [validationResult, setValidationResult] = React12.useState(null);
|
|
3581
5262
|
const [mode, setMode] = React12.useState("single");
|
|
3582
5263
|
const [variableValues, setVariableValues] = React12.useState({});
|
|
5264
|
+
const [variableSearch, setVariableSearch] = React12.useState("");
|
|
3583
5265
|
const [bulkInputFormat, setBulkInputFormat] = React12.useState("json");
|
|
3584
5266
|
const [bulkInput, setBulkInput] = React12.useState("");
|
|
3585
5267
|
const [bulkData, setBulkData] = React12.useState(null);
|
|
@@ -3606,11 +5288,22 @@ function GeneratePanel({
|
|
|
3606
5288
|
const col = pos - textBefore.lastIndexOf("\n");
|
|
3607
5289
|
setJsonCursor({ line, col });
|
|
3608
5290
|
}, []);
|
|
5291
|
+
const lockedVarNames = React12__namespace.default.useMemo(
|
|
5292
|
+
() => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
|
|
5293
|
+
[templateMode, initialVariableValues]
|
|
5294
|
+
);
|
|
3609
5295
|
const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
|
|
3610
5296
|
const hasVariables = activeVariables.length > 0;
|
|
3611
5297
|
const hasEditorContent = editorMarkdown.trim().length > 0;
|
|
3612
5298
|
const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
|
|
3613
5299
|
const hasContent = hasImportedContent || hasEditorContent;
|
|
5300
|
+
const filteredVariables = React12__namespace.default.useMemo(() => {
|
|
5301
|
+
if (!variableSearch.trim()) return activeVariables;
|
|
5302
|
+
const q = variableSearch.toLowerCase();
|
|
5303
|
+
return activeVariables.filter(
|
|
5304
|
+
(v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
|
|
5305
|
+
);
|
|
5306
|
+
}, [activeVariables, variableSearch]);
|
|
3614
5307
|
const getActiveContent = React12.useCallback(() => {
|
|
3615
5308
|
if (templateSource === "imported" && importedMarkdown) {
|
|
3616
5309
|
return markdownToTiptap(importedMarkdown);
|
|
@@ -3778,7 +5471,10 @@ function GeneratePanel({
|
|
|
3778
5471
|
setPreviewError(null);
|
|
3779
5472
|
try {
|
|
3780
5473
|
const content = getActiveContent();
|
|
3781
|
-
const
|
|
5474
|
+
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
|
|
5475
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
5476
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
5477
|
+
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
3782
5478
|
const fields = extractFieldsFromContent(replaced);
|
|
3783
5479
|
const result = await generatePdfFromContent(replaced);
|
|
3784
5480
|
const pages = await pdfToImages(result.pdfBytes);
|
|
@@ -3799,7 +5495,10 @@ function GeneratePanel({
|
|
|
3799
5495
|
setExportSuccess(null);
|
|
3800
5496
|
try {
|
|
3801
5497
|
const content = getActiveContent();
|
|
3802
|
-
const
|
|
5498
|
+
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
|
|
5499
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
5500
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
5501
|
+
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
3803
5502
|
const fields = extractFieldsFromContent(replaced);
|
|
3804
5503
|
const result = await generatePdfFromContent(replaced, {
|
|
3805
5504
|
drawFieldPlaceholders: false,
|
|
@@ -3807,7 +5506,7 @@ function GeneratePanel({
|
|
|
3807
5506
|
fields
|
|
3808
5507
|
});
|
|
3809
5508
|
const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
|
|
3810
|
-
const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
|
|
5509
|
+
const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
|
|
3811
5510
|
if (onGeneratePdf) {
|
|
3812
5511
|
onGeneratePdf(blob, fileName);
|
|
3813
5512
|
} else {
|
|
@@ -3879,7 +5578,10 @@ function GeneratePanel({
|
|
|
3879
5578
|
const allWarnings = [];
|
|
3880
5579
|
for (let i = 0; i < bulkData.length; i++) {
|
|
3881
5580
|
const values = bulkData[i];
|
|
3882
|
-
const
|
|
5581
|
+
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
|
|
5582
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
5583
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
5584
|
+
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
3883
5585
|
const fields = extractFieldsFromContent(replaced);
|
|
3884
5586
|
const result = await generatePdfFromContent(replaced, {
|
|
3885
5587
|
drawFieldPlaceholders: false,
|
|
@@ -3934,13 +5636,15 @@ ${allWarnings.join("\n")}`);
|
|
|
3934
5636
|
}, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
|
|
3935
5637
|
React12__namespace.default.useEffect(() => {
|
|
3936
5638
|
if (templateSource === "editor") {
|
|
3937
|
-
const defaults = {};
|
|
5639
|
+
const defaults = initialVariableValues ? { ...initialVariableValues } : {};
|
|
3938
5640
|
for (const v of editorVariables) {
|
|
3939
|
-
defaults[v.varName]
|
|
5641
|
+
if (!defaults[v.varName]) {
|
|
5642
|
+
defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
|
|
5643
|
+
}
|
|
3940
5644
|
}
|
|
3941
5645
|
setVariableValues(defaults);
|
|
3942
5646
|
}
|
|
3943
|
-
}, [editorVariables, templateSource]);
|
|
5647
|
+
}, [editorVariables, templateSource, initialVariableValues]);
|
|
3944
5648
|
React12.useEffect(() => {
|
|
3945
5649
|
setPreviewFresh(false);
|
|
3946
5650
|
}, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
|
|
@@ -3952,8 +5656,8 @@ ${allWarnings.join("\n")}`);
|
|
|
3952
5656
|
}, [exportSuccess]);
|
|
3953
5657
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
|
|
3954
5658
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
|
|
3955
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
|
|
3956
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
5659
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
|
|
5660
|
+
!templateMode && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3957
5661
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
3958
5662
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium", children: "Template" }),
|
|
3959
5663
|
templateSource === "imported" && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -4046,10 +5750,10 @@ ${allWarnings.join("\n")}`);
|
|
|
4046
5750
|
!hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
|
|
4047
5751
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 32, className: "mb-2 opacity-50" }),
|
|
4048
5752
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: "No template content" }),
|
|
4049
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs mt-1", children: "Import a .md template above or add content in the Editor tab." })
|
|
5753
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs mt-1", children: templateMode ? "No template content was provided." : "Import a .md template above or add content in the Editor tab." })
|
|
4050
5754
|
] }),
|
|
4051
5755
|
hasVariables && hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border border-border rounded-lg overflow-hidden", children: [
|
|
4052
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5756
|
+
!templateMode && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4053
5757
|
ToggleGroup,
|
|
4054
5758
|
{
|
|
4055
5759
|
value: mode,
|
|
@@ -4061,20 +5765,53 @@ ${allWarnings.join("\n")}`);
|
|
|
4061
5765
|
}
|
|
4062
5766
|
) }),
|
|
4063
5767
|
mode === "single" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
|
|
4064
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4065
|
-
|
|
4066
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5768
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
5769
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
|
|
5770
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
|
|
5771
|
+
] }),
|
|
5772
|
+
activeVariables.length > 5 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
5773
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
|
|
4067
5774
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4068
5775
|
Input,
|
|
4069
5776
|
{
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
5777
|
+
value: variableSearch,
|
|
5778
|
+
onChange: (e) => setVariableSearch(e.target.value),
|
|
5779
|
+
placeholder: "Search variables...",
|
|
5780
|
+
className: "h-8 text-xs pl-8"
|
|
5781
|
+
}
|
|
5782
|
+
),
|
|
5783
|
+
variableSearch && /* @__PURE__ */ jsxRuntime.jsx(
|
|
5784
|
+
"button",
|
|
5785
|
+
{
|
|
5786
|
+
type: "button",
|
|
5787
|
+
onClick: () => setVariableSearch(""),
|
|
5788
|
+
className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
|
|
5789
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 12 })
|
|
4075
5790
|
}
|
|
4076
5791
|
)
|
|
4077
|
-
] },
|
|
5792
|
+
] }),
|
|
5793
|
+
filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
|
|
5794
|
+
"No variables match \u201C",
|
|
5795
|
+
variableSearch,
|
|
5796
|
+
"\u201D"
|
|
5797
|
+
] }),
|
|
5798
|
+
filteredVariables.map((v) => {
|
|
5799
|
+
const isLocked = lockedVarNames.has(v.varName);
|
|
5800
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
5801
|
+
/* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
|
|
5802
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5803
|
+
Input,
|
|
5804
|
+
{
|
|
5805
|
+
id: `gen-${v.varName}`,
|
|
5806
|
+
value: variableValues[v.varName] || "",
|
|
5807
|
+
onChange: (e) => updateVariableValue(v.varName, e.target.value),
|
|
5808
|
+
placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
|
|
5809
|
+
disabled: isLocked,
|
|
5810
|
+
className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
|
|
5811
|
+
}
|
|
5812
|
+
)
|
|
5813
|
+
] }, v.varName);
|
|
5814
|
+
})
|
|
4078
5815
|
] }),
|
|
4079
5816
|
mode === "bulk" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
|
|
4080
5817
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
@@ -4263,56 +6000,60 @@ ${allWarnings.join("\n")}`);
|
|
|
4263
6000
|
] }),
|
|
4264
6001
|
!hasVariables && hasContent && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-3 rounded-md bg-muted/30 border border-border", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: 'This template has no variables. You can still preview and export the PDF as-is, or add variables in the Editor tab using the "Insert Variable" button.' }) })
|
|
4265
6002
|
] }),
|
|
4266
|
-
|
|
4267
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
6003
|
+
hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-border px-4 py-3 flex items-center gap-2 bg-gradient-to-b from-background to-muted/20", children: [
|
|
6004
|
+
mode === "single" || !hasVariables ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
6005
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
6006
|
+
Button,
|
|
6007
|
+
{
|
|
6008
|
+
variant: "secondary",
|
|
6009
|
+
onClick: handlePreview,
|
|
6010
|
+
disabled: isGenerating,
|
|
6011
|
+
className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
|
|
6012
|
+
children: [
|
|
6013
|
+
isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
|
|
6014
|
+
isGenerating ? "Generating..." : "Generate PDF"
|
|
6015
|
+
]
|
|
6016
|
+
}
|
|
6017
|
+
),
|
|
6018
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
6019
|
+
Button,
|
|
6020
|
+
{
|
|
6021
|
+
onClick: handleExportSingle,
|
|
6022
|
+
disabled: !previewFresh || isExporting,
|
|
6023
|
+
className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
|
|
6024
|
+
children: [
|
|
6025
|
+
isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
|
|
6026
|
+
isExporting ? "Exporting..." : "Export PDF"
|
|
6027
|
+
]
|
|
6028
|
+
}
|
|
6029
|
+
)
|
|
6030
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4293
6031
|
Button,
|
|
4294
6032
|
{
|
|
4295
|
-
onClick:
|
|
4296
|
-
disabled: !
|
|
4297
|
-
className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-
|
|
6033
|
+
onClick: handleBulkGenerate,
|
|
6034
|
+
disabled: isExporting || !bulkData || bulkData.length === 0,
|
|
6035
|
+
className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transition-all duration-200 text-sm",
|
|
4298
6036
|
children: [
|
|
4299
6037
|
isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
|
|
4300
|
-
isExporting ? "
|
|
6038
|
+
isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
|
|
4301
6039
|
]
|
|
4302
6040
|
}
|
|
4303
|
-
)
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
]
|
|
4314
|
-
|
|
4315
|
-
|
|
6041
|
+
),
|
|
6042
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
|
|
6043
|
+
previewError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
6044
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6045
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: previewError })
|
|
6046
|
+
] }),
|
|
6047
|
+
exportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
6048
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6049
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportError })
|
|
6050
|
+
] }),
|
|
6051
|
+
exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
|
|
6052
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
|
|
6053
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportSuccess })
|
|
6054
|
+
] })
|
|
6055
|
+
] })
|
|
6056
|
+
] })
|
|
4316
6057
|
] }),
|
|
4317
6058
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
|
|
4318
6059
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4490,7 +6231,13 @@ function DocumentGeneratorInner({
|
|
|
4490
6231
|
showToolbar = true,
|
|
4491
6232
|
showGenerateTab = true,
|
|
4492
6233
|
onGeneratePdf,
|
|
4493
|
-
initialBulkData
|
|
6234
|
+
initialBulkData,
|
|
6235
|
+
defaultTab = "editor",
|
|
6236
|
+
initialVariableValues,
|
|
6237
|
+
predefinedVariables,
|
|
6238
|
+
exportFileName,
|
|
6239
|
+
templateMode,
|
|
6240
|
+
onSaveTemplate
|
|
4494
6241
|
}) {
|
|
4495
6242
|
const {
|
|
4496
6243
|
editor,
|
|
@@ -4517,7 +6264,7 @@ function DocumentGeneratorInner({
|
|
|
4517
6264
|
placeholder,
|
|
4518
6265
|
onChange
|
|
4519
6266
|
});
|
|
4520
|
-
const [activeTab, setActiveTab] = React12.useState(
|
|
6267
|
+
const [activeTab, setActiveTab] = React12.useState(defaultTab);
|
|
4521
6268
|
const [insertPopoverOpen, setInsertPopoverOpen] = React12.useState(false);
|
|
4522
6269
|
const [insertVarPopoverOpen, setInsertVarPopoverOpen] = React12.useState(false);
|
|
4523
6270
|
const [editFieldId, setEditFieldId] = React12.useState(null);
|
|
@@ -4529,6 +6276,7 @@ function DocumentGeneratorInner({
|
|
|
4529
6276
|
const editorWrapperRef = React12.useRef(null);
|
|
4530
6277
|
const [editorCollapsed, setEditorCollapsed] = React12.useState(false);
|
|
4531
6278
|
const [mdExportError, setMdExportError] = React12.useState(null);
|
|
6279
|
+
const [isSaving, setIsSaving] = React12.useState(false);
|
|
4532
6280
|
const [exportSuccess, setExportSuccess] = React12.useState(null);
|
|
4533
6281
|
React12.useEffect(() => {
|
|
4534
6282
|
const handler = (e) => {
|
|
@@ -4599,7 +6347,28 @@ function DocumentGeneratorInner({
|
|
|
4599
6347
|
onExport(result);
|
|
4600
6348
|
setExportSuccess("Document exported successfully");
|
|
4601
6349
|
}
|
|
4602
|
-
}, [exportDocument, onExport]);
|
|
6350
|
+
}, [exportDocument, onExport]);
|
|
6351
|
+
const handleSaveTemplate = React12.useCallback(async () => {
|
|
6352
|
+
const md = exportMarkdown();
|
|
6353
|
+
if (!md.trim()) {
|
|
6354
|
+
setMdExportError("Editor content is empty");
|
|
6355
|
+
return;
|
|
6356
|
+
}
|
|
6357
|
+
const validation = validateMarkdown(md);
|
|
6358
|
+
if (!validation.valid) {
|
|
6359
|
+
setMdExportError(validation.errors.join("; "));
|
|
6360
|
+
return;
|
|
6361
|
+
}
|
|
6362
|
+
setMdExportError(null);
|
|
6363
|
+
setIsSaving(true);
|
|
6364
|
+
try {
|
|
6365
|
+
await onSaveTemplate?.(md);
|
|
6366
|
+
setExportSuccess("Template saved");
|
|
6367
|
+
} catch {
|
|
6368
|
+
} finally {
|
|
6369
|
+
setIsSaving(false);
|
|
6370
|
+
}
|
|
6371
|
+
}, [exportMarkdown, onSaveTemplate]);
|
|
4603
6372
|
const handleExportMarkdown = React12.useCallback(() => {
|
|
4604
6373
|
const md = exportMarkdown();
|
|
4605
6374
|
if (!md.trim()) {
|
|
@@ -4616,10 +6385,10 @@ function DocumentGeneratorInner({
|
|
|
4616
6385
|
const url = URL.createObjectURL(blob);
|
|
4617
6386
|
const a = document.createElement("a");
|
|
4618
6387
|
a.href = url;
|
|
4619
|
-
a.download = "template.md";
|
|
6388
|
+
a.download = exportFileName ? `${exportFileName}.md` : "template.md";
|
|
4620
6389
|
a.click();
|
|
4621
6390
|
URL.revokeObjectURL(url);
|
|
4622
|
-
}, [exportMarkdown]);
|
|
6391
|
+
}, [exportMarkdown, exportFileName]);
|
|
4623
6392
|
React12.useEffect(() => {
|
|
4624
6393
|
if (mdExportError) setMdExportError(null);
|
|
4625
6394
|
}, [markdown]);
|
|
@@ -4655,6 +6424,7 @@ function DocumentGeneratorInner({
|
|
|
4655
6424
|
open: insertVarPopoverOpen,
|
|
4656
6425
|
onOpenChange: setInsertVarPopoverOpen,
|
|
4657
6426
|
onInsert: handleInsertVariable,
|
|
6427
|
+
predefinedVariables,
|
|
4658
6428
|
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4659
6429
|
Button,
|
|
4660
6430
|
{
|
|
@@ -4695,118 +6465,141 @@ function DocumentGeneratorInner({
|
|
|
4695
6465
|
] })
|
|
4696
6466
|
] })
|
|
4697
6467
|
] }),
|
|
4698
|
-
/* @__PURE__ */ jsxRuntime.
|
|
6468
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
|
|
4699
6469
|
"flex flex-col flex-1 min-h-0",
|
|
4700
6470
|
activeTab !== "editor" && "hidden"
|
|
6471
|
+
), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
|
|
6472
|
+
"flex-1 min-h-0",
|
|
6473
|
+
showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
|
|
4701
6474
|
), children: [
|
|
4702
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className:
|
|
4703
|
-
"
|
|
4704
|
-
|
|
4705
|
-
), children: [
|
|
4706
|
-
!editorCollapsed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
|
|
4707
|
-
showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4708
|
-
EditorToolbar,
|
|
4709
|
-
{
|
|
4710
|
-
editor,
|
|
4711
|
-
insertFieldButton,
|
|
4712
|
-
insertVariableButton,
|
|
4713
|
-
onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
|
|
4714
|
-
}
|
|
4715
|
-
) }),
|
|
4716
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4717
|
-
react.EditorContent,
|
|
4718
|
-
{
|
|
4719
|
-
editor,
|
|
4720
|
-
className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
|
|
4721
|
-
}
|
|
4722
|
-
) })
|
|
4723
|
-
] }),
|
|
4724
|
-
showPreview && !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
|
|
4725
|
-
showPreview && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4726
|
-
PreviewPanel,
|
|
6475
|
+
!editorCollapsed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
|
|
6476
|
+
showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6477
|
+
EditorToolbar,
|
|
4727
6478
|
{
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
Button,
|
|
4733
|
-
{
|
|
4734
|
-
variant: "ghost",
|
|
4735
|
-
size: "sm",
|
|
4736
|
-
className: "h-8 w-8 p-0",
|
|
4737
|
-
onClick: () => setEditorCollapsed(false),
|
|
4738
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftOpen, { size: 14 })
|
|
4739
|
-
}
|
|
4740
|
-
) : void 0,
|
|
4741
|
-
className: "flex-1 border-0 rounded-none"
|
|
6479
|
+
editor,
|
|
6480
|
+
insertFieldButton,
|
|
6481
|
+
insertVariableButton,
|
|
6482
|
+
onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
|
|
4742
6483
|
}
|
|
4743
|
-
)
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4747
|
-
Button,
|
|
6484
|
+
) }),
|
|
6485
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6486
|
+
react.EditorContent,
|
|
4748
6487
|
{
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
disabled: isGenerating || !editor,
|
|
4752
|
-
className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
|
|
4753
|
-
children: [
|
|
4754
|
-
isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
|
|
4755
|
-
isGenerating ? "Generating..." : "Generate PDF"
|
|
4756
|
-
]
|
|
6488
|
+
editor,
|
|
6489
|
+
className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
|
|
4757
6490
|
}
|
|
4758
|
-
),
|
|
4759
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex
|
|
4760
|
-
mdExportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
4761
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
4762
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: mdExportError })
|
|
4763
|
-
] }),
|
|
4764
|
-
generationError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
4765
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
4766
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: generationError })
|
|
4767
|
-
] }),
|
|
4768
|
-
fieldWarnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
|
|
4769
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
4770
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
|
|
4771
|
-
fieldWarnings.length,
|
|
4772
|
-
" field",
|
|
4773
|
-
fieldWarnings.length !== 1 ? "s" : "",
|
|
4774
|
-
" had warnings during generation"
|
|
4775
|
-
] })
|
|
4776
|
-
] }),
|
|
4777
|
-
exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
|
|
4778
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
|
|
4779
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: exportSuccess })
|
|
4780
|
-
] })
|
|
4781
|
-
] }),
|
|
4782
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
6491
|
+
) }),
|
|
6492
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-4 py-3 border-t border-border bg-gradient-to-b from-background to-muted/20", children: [
|
|
4783
6493
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4784
6494
|
Button,
|
|
4785
6495
|
{
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
6496
|
+
variant: "secondary",
|
|
6497
|
+
onClick: generatePdf,
|
|
6498
|
+
disabled: isGenerating || !editor,
|
|
6499
|
+
className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
|
|
4789
6500
|
children: [
|
|
4790
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.
|
|
4791
|
-
"
|
|
6501
|
+
isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
|
|
6502
|
+
isGenerating ? "Generating..." : "Generate PDF"
|
|
4792
6503
|
]
|
|
4793
6504
|
}
|
|
4794
6505
|
),
|
|
4795
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
6506
|
+
onSaveTemplate ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4796
6507
|
Button,
|
|
4797
6508
|
{
|
|
4798
|
-
onClick:
|
|
4799
|
-
disabled:
|
|
6509
|
+
onClick: handleSaveTemplate,
|
|
6510
|
+
disabled: isSaving || !editor || !markdown.trim(),
|
|
4800
6511
|
className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
|
|
4801
6512
|
children: [
|
|
4802
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.
|
|
4803
|
-
exportButtonText
|
|
6513
|
+
isSaving ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 16 }),
|
|
6514
|
+
isSaving ? "Saving..." : exportButtonText || "Save Template"
|
|
4804
6515
|
]
|
|
4805
6516
|
}
|
|
4806
|
-
)
|
|
6517
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(Popover, { children: [
|
|
6518
|
+
/* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6519
|
+
Button,
|
|
6520
|
+
{
|
|
6521
|
+
disabled: !editor || !markdown.trim() && pdfPages.length === 0,
|
|
6522
|
+
className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
|
|
6523
|
+
children: [
|
|
6524
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
|
|
6525
|
+
"Export",
|
|
6526
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { size: 14 })
|
|
6527
|
+
]
|
|
6528
|
+
}
|
|
6529
|
+
) }),
|
|
6530
|
+
/* @__PURE__ */ jsxRuntime.jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
|
|
6531
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
6532
|
+
"button",
|
|
6533
|
+
{
|
|
6534
|
+
onClick: handleExportMarkdown,
|
|
6535
|
+
disabled: !editor || !markdown.trim(),
|
|
6536
|
+
className: "flex w-full items-center gap-2 rounded-sm px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
|
|
6537
|
+
children: [
|
|
6538
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 16 }),
|
|
6539
|
+
"Export as MD"
|
|
6540
|
+
]
|
|
6541
|
+
}
|
|
6542
|
+
),
|
|
6543
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
6544
|
+
"button",
|
|
6545
|
+
{
|
|
6546
|
+
onClick: handleExport,
|
|
6547
|
+
disabled: isGenerating || !editor || pdfPages.length === 0,
|
|
6548
|
+
className: "flex w-full items-center gap-2 rounded-sm px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
|
|
6549
|
+
children: [
|
|
6550
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
|
|
6551
|
+
"Export as PDF"
|
|
6552
|
+
]
|
|
6553
|
+
}
|
|
6554
|
+
)
|
|
6555
|
+
] })
|
|
6556
|
+
] }),
|
|
6557
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
|
|
6558
|
+
mdExportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
6559
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6560
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: mdExportError })
|
|
6561
|
+
] }),
|
|
6562
|
+
generationError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
6563
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6564
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: generationError })
|
|
6565
|
+
] }),
|
|
6566
|
+
fieldWarnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
|
|
6567
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6568
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
|
|
6569
|
+
fieldWarnings.length,
|
|
6570
|
+
" field",
|
|
6571
|
+
fieldWarnings.length !== 1 ? "s" : "",
|
|
6572
|
+
" had warnings during generation"
|
|
6573
|
+
] })
|
|
6574
|
+
] }),
|
|
6575
|
+
exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
|
|
6576
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
|
|
6577
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: exportSuccess })
|
|
6578
|
+
] })
|
|
6579
|
+
] })
|
|
4807
6580
|
] })
|
|
4808
|
-
] })
|
|
4809
|
-
|
|
6581
|
+
] }),
|
|
6582
|
+
showPreview && !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
|
|
6583
|
+
showPreview && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6584
|
+
PreviewPanel,
|
|
6585
|
+
{
|
|
6586
|
+
pages: pdfPages,
|
|
6587
|
+
isGenerating,
|
|
6588
|
+
positionedFields,
|
|
6589
|
+
headerLeft: editorCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
6590
|
+
Button,
|
|
6591
|
+
{
|
|
6592
|
+
variant: "ghost",
|
|
6593
|
+
size: "sm",
|
|
6594
|
+
className: "h-8 w-8 p-0",
|
|
6595
|
+
onClick: () => setEditorCollapsed(false),
|
|
6596
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftOpen, { size: 14 })
|
|
6597
|
+
}
|
|
6598
|
+
) : void 0,
|
|
6599
|
+
className: "flex-1 border-0 rounded-none"
|
|
6600
|
+
}
|
|
6601
|
+
)
|
|
6602
|
+
] }) }),
|
|
4810
6603
|
showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4811
6604
|
GeneratePanel,
|
|
4812
6605
|
{
|
|
@@ -4814,7 +6607,10 @@ function DocumentGeneratorInner({
|
|
|
4814
6607
|
editorContent: editor?.getJSON(),
|
|
4815
6608
|
editorVariables: variables,
|
|
4816
6609
|
onGeneratePdf,
|
|
4817
|
-
initialBulkData
|
|
6610
|
+
initialBulkData,
|
|
6611
|
+
initialVariableValues,
|
|
6612
|
+
exportFileName,
|
|
6613
|
+
templateMode
|
|
4818
6614
|
}
|
|
4819
6615
|
),
|
|
4820
6616
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4827,7 +6623,7 @@ function DocumentGeneratorInner({
|
|
|
4827
6623
|
onDelete: deleteField,
|
|
4828
6624
|
onClose: handleCloseFieldEditPopover
|
|
4829
6625
|
},
|
|
4830
|
-
editFieldId
|
|
6626
|
+
editFieldId ?? "field-edit"
|
|
4831
6627
|
),
|
|
4832
6628
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4833
6629
|
VariableEditPopover,
|
|
@@ -4839,7 +6635,7 @@ function DocumentGeneratorInner({
|
|
|
4839
6635
|
onDelete: deleteVariable,
|
|
4840
6636
|
onClose: handleCloseVarEditPopover
|
|
4841
6637
|
},
|
|
4842
|
-
editVarName
|
|
6638
|
+
editVarName ?? "var-edit"
|
|
4843
6639
|
)
|
|
4844
6640
|
] });
|
|
4845
6641
|
}
|
|
@@ -4955,11 +6751,734 @@ function EditorPanel({
|
|
|
4955
6751
|
] });
|
|
4956
6752
|
}
|
|
4957
6753
|
|
|
6754
|
+
// src/utils/xml-template-parser.ts
|
|
6755
|
+
function extractFieldRefs(expr) {
|
|
6756
|
+
const refs = [];
|
|
6757
|
+
const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
|
|
6758
|
+
let m;
|
|
6759
|
+
while ((m = re.exec(expr)) !== null) {
|
|
6760
|
+
if (m[2]) {
|
|
6761
|
+
refs.push(`${m[1]}.${m[2]}`);
|
|
6762
|
+
} else {
|
|
6763
|
+
refs.push(m[1]);
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
return refs;
|
|
6767
|
+
}
|
|
6768
|
+
function fieldRefToVarName(ref, bandContext) {
|
|
6769
|
+
if (ref.includes(".")) {
|
|
6770
|
+
return labelToVarName(ref.replace(/\./g, "_"));
|
|
6771
|
+
}
|
|
6772
|
+
if (bandContext) {
|
|
6773
|
+
return labelToVarName(bandContext + "_" + ref);
|
|
6774
|
+
}
|
|
6775
|
+
return labelToVarName(ref);
|
|
6776
|
+
}
|
|
6777
|
+
function fieldRefToLabel(ref) {
|
|
6778
|
+
const parts = ref.split(".");
|
|
6779
|
+
const raw = parts[parts.length - 1];
|
|
6780
|
+
return raw.replace(/_/g, " ");
|
|
6781
|
+
}
|
|
6782
|
+
function resolveExpression(expr, registry, siblingLabel, bandContext) {
|
|
6783
|
+
const trimmed = expr.trim();
|
|
6784
|
+
const suppressZero = /<>\s*0/.test(trimmed);
|
|
6785
|
+
const szOpts = suppressZero ? { suppressZero: true } : void 0;
|
|
6786
|
+
const aggMatch = trimmed.match(
|
|
6787
|
+
/^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
|
|
6788
|
+
);
|
|
6789
|
+
if (aggMatch) {
|
|
6790
|
+
const fieldName = aggMatch[2];
|
|
6791
|
+
const varName = labelToVarName("workorder_" + fieldName);
|
|
6792
|
+
const label = fieldName.replace(/_/g, " ");
|
|
6793
|
+
return registry.register(varName, label, szOpts);
|
|
6794
|
+
}
|
|
6795
|
+
const concatSingle = trimmed.match(
|
|
6796
|
+
/^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
|
|
6797
|
+
);
|
|
6798
|
+
if (concatSingle) {
|
|
6799
|
+
const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
|
|
6800
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6801
|
+
const label = fieldRefToLabel(ref);
|
|
6802
|
+
return registry.register(varName, label, szOpts);
|
|
6803
|
+
}
|
|
6804
|
+
const concatMulti = trimmed.match(
|
|
6805
|
+
/^concat\(/i
|
|
6806
|
+
);
|
|
6807
|
+
if (concatMulti) {
|
|
6808
|
+
const refs = extractFieldRefs(trimmed);
|
|
6809
|
+
if (refs.length > 0) {
|
|
6810
|
+
const uniqueRefs = [...new Set(refs)];
|
|
6811
|
+
const tokens = uniqueRefs.map((ref) => {
|
|
6812
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6813
|
+
const label = fieldRefToLabel(ref);
|
|
6814
|
+
return registry.register(varName, label, szOpts);
|
|
6815
|
+
});
|
|
6816
|
+
return tokens.join(" ");
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
const toLongMatch = trimmed.match(
|
|
6820
|
+
/^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
|
|
6821
|
+
);
|
|
6822
|
+
if (toLongMatch) {
|
|
6823
|
+
const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
|
|
6824
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6825
|
+
const label = fieldRefToLabel(ref);
|
|
6826
|
+
return registry.register(varName, label, szOpts);
|
|
6827
|
+
}
|
|
6828
|
+
const fmtMatch = trimmed.match(
|
|
6829
|
+
/^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
|
|
6830
|
+
);
|
|
6831
|
+
if (fmtMatch) {
|
|
6832
|
+
return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
|
|
6833
|
+
}
|
|
6834
|
+
const replaceMatch = trimmed.match(
|
|
6835
|
+
/^Replace\(\s*\[([^\]]+)\]/i
|
|
6836
|
+
);
|
|
6837
|
+
if (replaceMatch) {
|
|
6838
|
+
const ref = replaceMatch[1];
|
|
6839
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6840
|
+
const label = fieldRefToLabel(ref);
|
|
6841
|
+
return registry.register(varName, label, szOpts);
|
|
6842
|
+
}
|
|
6843
|
+
const iifLabelMatch = trimmed.match(
|
|
6844
|
+
/^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
|
|
6845
|
+
);
|
|
6846
|
+
if (iifLabelMatch) {
|
|
6847
|
+
return iifLabelMatch[1];
|
|
6848
|
+
}
|
|
6849
|
+
const iifLabelElse = trimmed.match(
|
|
6850
|
+
/^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
|
|
6851
|
+
);
|
|
6852
|
+
if (iifLabelElse) {
|
|
6853
|
+
return iifLabelElse[1];
|
|
6854
|
+
}
|
|
6855
|
+
const iifEmptyElse = trimmed.match(
|
|
6856
|
+
/^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
|
|
6857
|
+
);
|
|
6858
|
+
if (iifEmptyElse) {
|
|
6859
|
+
return iifEmptyElse[1];
|
|
6860
|
+
}
|
|
6861
|
+
const nestedIifStaticLabel = trimmed.match(
|
|
6862
|
+
/^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
|
|
6863
|
+
);
|
|
6864
|
+
if (nestedIifStaticLabel) {
|
|
6865
|
+
return nestedIifStaticLabel[1];
|
|
6866
|
+
}
|
|
6867
|
+
const iifValueMatch = trimmed.match(
|
|
6868
|
+
/^iif\s*\(/i
|
|
6869
|
+
);
|
|
6870
|
+
if (iifValueMatch) {
|
|
6871
|
+
const refs = extractFieldRefs(trimmed);
|
|
6872
|
+
if (refs.length > 0) {
|
|
6873
|
+
const uniqueRefs = [...new Set(refs)];
|
|
6874
|
+
const innerAggMatch = trimmed.match(
|
|
6875
|
+
/\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
|
|
6876
|
+
);
|
|
6877
|
+
if (innerAggMatch) {
|
|
6878
|
+
const fieldName = innerAggMatch[2];
|
|
6879
|
+
const varName2 = labelToVarName("workorder_" + fieldName);
|
|
6880
|
+
const label2 = fieldName.replace(/_/g, " ");
|
|
6881
|
+
return registry.register(varName2, label2, szOpts);
|
|
6882
|
+
}
|
|
6883
|
+
const ref = uniqueRefs[uniqueRefs.length - 1];
|
|
6884
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6885
|
+
const label = fieldRefToLabel(ref);
|
|
6886
|
+
return registry.register(varName, label, szOpts);
|
|
6887
|
+
}
|
|
6888
|
+
const staticMatch = trimmed.match(/'([^']+)'/);
|
|
6889
|
+
if (staticMatch) return staticMatch[1];
|
|
6890
|
+
return "";
|
|
6891
|
+
}
|
|
6892
|
+
const arithmeticRefs = extractFieldRefs(trimmed);
|
|
6893
|
+
if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
|
|
6894
|
+
if (siblingLabel) {
|
|
6895
|
+
const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
|
|
6896
|
+
const varName = labelToVarName(
|
|
6897
|
+
(bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
|
|
6898
|
+
);
|
|
6899
|
+
return registry.register(varName, cleanLabel, szOpts);
|
|
6900
|
+
}
|
|
6901
|
+
const uniqueRefs = [...new Set(arithmeticRefs)];
|
|
6902
|
+
const tokens = uniqueRefs.map((ref) => {
|
|
6903
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6904
|
+
const label = fieldRefToLabel(ref);
|
|
6905
|
+
return registry.register(varName, label, szOpts);
|
|
6906
|
+
});
|
|
6907
|
+
return tokens.join(" ");
|
|
6908
|
+
}
|
|
6909
|
+
const simpleMatch = trimmed.match(
|
|
6910
|
+
/^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
|
|
6911
|
+
);
|
|
6912
|
+
if (simpleMatch) {
|
|
6913
|
+
const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
|
|
6914
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6915
|
+
const label = fieldRefToLabel(ref);
|
|
6916
|
+
return registry.register(varName, label, szOpts);
|
|
6917
|
+
}
|
|
6918
|
+
if (arithmeticRefs.length >= 1) {
|
|
6919
|
+
const ref = arithmeticRefs[arithmeticRefs.length - 1];
|
|
6920
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6921
|
+
const label = fieldRefToLabel(ref);
|
|
6922
|
+
return registry.register(varName, label, szOpts);
|
|
6923
|
+
}
|
|
6924
|
+
return "";
|
|
6925
|
+
}
|
|
6926
|
+
var VariableRegistry = class {
|
|
6927
|
+
constructor() {
|
|
6928
|
+
__publicField(this, "map", /* @__PURE__ */ new Map());
|
|
6929
|
+
}
|
|
6930
|
+
register(varName, varLabel, options) {
|
|
6931
|
+
if (!this.map.has(varName)) {
|
|
6932
|
+
this.map.set(varName, { varName, varLabel, varDefault: "" });
|
|
6933
|
+
}
|
|
6934
|
+
const suppress = options?.suppressZero ? "|suppress:zero" : "";
|
|
6935
|
+
return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
|
|
6936
|
+
}
|
|
6937
|
+
getAll() {
|
|
6938
|
+
return Array.from(this.map.values());
|
|
6939
|
+
}
|
|
6940
|
+
};
|
|
6941
|
+
function parseLocationFloat(s) {
|
|
6942
|
+
if (!s) return { x: 0, y: 0 };
|
|
6943
|
+
const parts = s.split(",");
|
|
6944
|
+
return {
|
|
6945
|
+
x: parseFloat(parts[0]) || 0,
|
|
6946
|
+
y: parseFloat(parts[1]) || 0
|
|
6947
|
+
};
|
|
6948
|
+
}
|
|
6949
|
+
function parseSizeF(s) {
|
|
6950
|
+
if (!s) return { w: 0, h: 0 };
|
|
6951
|
+
const parts = s.split(",");
|
|
6952
|
+
return {
|
|
6953
|
+
w: parseFloat(parts[0]) || 0,
|
|
6954
|
+
h: parseFloat(parts[1]) || 0
|
|
6955
|
+
};
|
|
6956
|
+
}
|
|
6957
|
+
function extractControls(bandEl) {
|
|
6958
|
+
const controlsEl = bandEl.querySelector(":scope > Controls");
|
|
6959
|
+
if (!controlsEl) return [];
|
|
6960
|
+
const items = [];
|
|
6961
|
+
for (const child of Array.from(controlsEl.children)) {
|
|
6962
|
+
items.push(extractControl(child));
|
|
6963
|
+
}
|
|
6964
|
+
return items;
|
|
6965
|
+
}
|
|
6966
|
+
function extractControl(el) {
|
|
6967
|
+
const ctrlType = el.getAttribute("ControlType") || "";
|
|
6968
|
+
const name = el.getAttribute("Name") || "";
|
|
6969
|
+
const text = el.getAttribute("Text") || "";
|
|
6970
|
+
const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
|
|
6971
|
+
const size = parseSizeF(el.getAttribute("SizeF"));
|
|
6972
|
+
const formatString = el.getAttribute("TextFormatString") || null;
|
|
6973
|
+
let expression = null;
|
|
6974
|
+
const bindingsEl = el.querySelector(":scope > ExpressionBindings");
|
|
6975
|
+
if (bindingsEl) {
|
|
6976
|
+
for (const item of Array.from(bindingsEl.children)) {
|
|
6977
|
+
if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
|
|
6978
|
+
expression = item.getAttribute("Expression");
|
|
6979
|
+
break;
|
|
6980
|
+
}
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
const children = [];
|
|
6984
|
+
if (ctrlType === "XRPanel") {
|
|
6985
|
+
const panelControls = extractControls(el);
|
|
6986
|
+
children.push(...panelControls);
|
|
6987
|
+
}
|
|
6988
|
+
return {
|
|
6989
|
+
name,
|
|
6990
|
+
type: ctrlType,
|
|
6991
|
+
text,
|
|
6992
|
+
x: loc.x,
|
|
6993
|
+
y: loc.y,
|
|
6994
|
+
width: size.w,
|
|
6995
|
+
expression,
|
|
6996
|
+
formatString,
|
|
6997
|
+
children
|
|
6998
|
+
};
|
|
6999
|
+
}
|
|
7000
|
+
function groupByRow(controls, threshold = 8) {
|
|
7001
|
+
const sorted = [...controls].sort((a, b) => a.y - b.y);
|
|
7002
|
+
const rows = [];
|
|
7003
|
+
for (const ctrl of sorted) {
|
|
7004
|
+
const lastRow = rows[rows.length - 1];
|
|
7005
|
+
if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
|
|
7006
|
+
lastRow.controls.push(ctrl);
|
|
7007
|
+
} else {
|
|
7008
|
+
rows.push({ y: ctrl.y, controls: [ctrl] });
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
for (const row of rows) {
|
|
7012
|
+
row.controls.sort((a, b) => a.x - b.x);
|
|
7013
|
+
}
|
|
7014
|
+
return rows;
|
|
7015
|
+
}
|
|
7016
|
+
function isStaticLabel(ctrl) {
|
|
7017
|
+
return ctrl.type === "XRLabel" && !ctrl.expression;
|
|
7018
|
+
}
|
|
7019
|
+
function isBound(ctrl) {
|
|
7020
|
+
return ctrl.type === "XRLabel" && !!ctrl.expression;
|
|
7021
|
+
}
|
|
7022
|
+
function computeSplitPercent(leftControls, rightControls, contentWidth) {
|
|
7023
|
+
const leftExtent = leftControls.reduce(
|
|
7024
|
+
(max, c) => Math.max(max, c.x + c.width),
|
|
7025
|
+
0
|
|
7026
|
+
);
|
|
7027
|
+
const rightMinX = rightControls.reduce(
|
|
7028
|
+
(min, c) => Math.min(min, c.x),
|
|
7029
|
+
contentWidth
|
|
7030
|
+
);
|
|
7031
|
+
const rightMaxExtent = rightControls.reduce(
|
|
7032
|
+
(max, c) => Math.max(max, c.x + c.width),
|
|
7033
|
+
0
|
|
7034
|
+
);
|
|
7035
|
+
const rightExtent = rightMaxExtent - rightMinX;
|
|
7036
|
+
if (leftExtent + rightExtent === 0) return 50;
|
|
7037
|
+
return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
|
|
7038
|
+
}
|
|
7039
|
+
function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
|
|
7040
|
+
const lines = [];
|
|
7041
|
+
const leftControls = controls.filter((c) => c.x < 437);
|
|
7042
|
+
const rightControls = controls.filter((c) => c.x >= 437);
|
|
7043
|
+
const leftEntries = [];
|
|
7044
|
+
if (leftControls.length > 0) {
|
|
7045
|
+
const leftRows = groupByRow(leftControls);
|
|
7046
|
+
for (const row of leftRows) {
|
|
7047
|
+
const parts = [];
|
|
7048
|
+
for (const ctrl of row.controls) {
|
|
7049
|
+
if (isBound(ctrl)) {
|
|
7050
|
+
let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
|
|
7051
|
+
if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
|
|
7052
|
+
resolved = resolved.replace("}}", "|format:phone}}");
|
|
7053
|
+
}
|
|
7054
|
+
if (resolved) parts.push(resolved);
|
|
7055
|
+
} else if (isStaticLabel(ctrl)) {
|
|
7056
|
+
const text = ctrl.text.trim();
|
|
7057
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7058
|
+
parts.push(text);
|
|
7059
|
+
}
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
if (parts.length > 0) {
|
|
7063
|
+
leftEntries.push(parts.join(" "));
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
}
|
|
7067
|
+
const rightEntries = [];
|
|
7068
|
+
if (rightControls.length > 0) {
|
|
7069
|
+
const rightRows = groupByRow(rightControls);
|
|
7070
|
+
for (const row of rightRows) {
|
|
7071
|
+
let label = "";
|
|
7072
|
+
let value = "";
|
|
7073
|
+
for (const ctrl of row.controls) {
|
|
7074
|
+
if (isStaticLabel(ctrl)) {
|
|
7075
|
+
const text = ctrl.text.trim();
|
|
7076
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7077
|
+
label = text;
|
|
7078
|
+
}
|
|
7079
|
+
} else if (isBound(ctrl)) {
|
|
7080
|
+
value = resolveExpression(ctrl.expression, registry, label, "workorder");
|
|
7081
|
+
if (ctrl.expression) {
|
|
7082
|
+
const refs = extractFieldRefs(ctrl.expression);
|
|
7083
|
+
if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
|
|
7084
|
+
value = value.replace("}}", "|suppress:zero}}");
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
7087
|
+
if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
|
|
7088
|
+
value = value.replace("}}", "|format:phone}}");
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
if (label || value) {
|
|
7093
|
+
rightEntries.push({ label, value, y: row.y });
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
}
|
|
7097
|
+
if (leftEntries.length > 0 || rightEntries.length > 0) {
|
|
7098
|
+
const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
|
|
7099
|
+
lines.push(`:::columns{split:${outerSplit}}`);
|
|
7100
|
+
const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
|
|
7101
|
+
const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
|
|
7102
|
+
const xmlLineHeight = 17;
|
|
7103
|
+
const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
|
|
7104
|
+
const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
|
|
7105
|
+
const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
|
|
7106
|
+
lines.push(leftColTag);
|
|
7107
|
+
for (const entry of leftEntries) {
|
|
7108
|
+
lines.push(entry);
|
|
7109
|
+
}
|
|
7110
|
+
lines.push(":::");
|
|
7111
|
+
const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
|
|
7112
|
+
lines.push(rightColTag);
|
|
7113
|
+
if (rightEntries.length > 0) {
|
|
7114
|
+
lines.push("| | |");
|
|
7115
|
+
lines.push("|---|---|");
|
|
7116
|
+
let prevY = -Infinity;
|
|
7117
|
+
for (const entry of rightEntries) {
|
|
7118
|
+
if (prevY > -Infinity && entry.y - prevY > 25) {
|
|
7119
|
+
lines.push("| | |");
|
|
7120
|
+
}
|
|
7121
|
+
const label = entry.label || "";
|
|
7122
|
+
const colon = entry.label ? ":" : "";
|
|
7123
|
+
const value = entry.value ? ` ${entry.value}` : "";
|
|
7124
|
+
lines.push(`| ${label} | ${colon}${value} |`);
|
|
7125
|
+
prevY = entry.y;
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
7128
|
+
lines.push(":::");
|
|
7129
|
+
lines.push(":::");
|
|
7130
|
+
lines.push("");
|
|
7131
|
+
}
|
|
7132
|
+
return lines.join("\n");
|
|
7133
|
+
}
|
|
7134
|
+
function renderReportHeaderBand(controls, registry, contentWidth) {
|
|
7135
|
+
const lines = [];
|
|
7136
|
+
const leftControls = controls.filter((c) => c.x < 437);
|
|
7137
|
+
const rightControls = controls.filter((c) => c.x >= 437);
|
|
7138
|
+
const leftEntries = [];
|
|
7139
|
+
if (leftControls.length > 0) {
|
|
7140
|
+
const leftRows = groupByRow(leftControls);
|
|
7141
|
+
for (const row of leftRows) {
|
|
7142
|
+
const parts = [];
|
|
7143
|
+
for (const ctrl of row.controls) {
|
|
7144
|
+
if (isBound(ctrl)) {
|
|
7145
|
+
const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
|
|
7146
|
+
if (resolved) parts.push(resolved);
|
|
7147
|
+
} else if (isStaticLabel(ctrl)) {
|
|
7148
|
+
const text = ctrl.text.trim();
|
|
7149
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7150
|
+
parts.push(text);
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
if (parts.length > 0) {
|
|
7155
|
+
leftEntries.push(parts.join(" "));
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
7158
|
+
}
|
|
7159
|
+
const rightEntries = [];
|
|
7160
|
+
if (rightControls.length > 0) {
|
|
7161
|
+
const rightRows = groupByRow(rightControls);
|
|
7162
|
+
for (const row of rightRows) {
|
|
7163
|
+
let label = "";
|
|
7164
|
+
let value = "";
|
|
7165
|
+
for (const ctrl of row.controls) {
|
|
7166
|
+
if (isStaticLabel(ctrl)) {
|
|
7167
|
+
const text = ctrl.text.trim();
|
|
7168
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7169
|
+
label = text;
|
|
7170
|
+
}
|
|
7171
|
+
} else if (isBound(ctrl)) {
|
|
7172
|
+
value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
|
|
7173
|
+
if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
|
|
7174
|
+
value = value.replace("}}", "|format:phone}}");
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
}
|
|
7178
|
+
if (label || value) {
|
|
7179
|
+
rightEntries.push({ label, value });
|
|
7180
|
+
}
|
|
7181
|
+
}
|
|
7182
|
+
}
|
|
7183
|
+
if (leftEntries.length > 0 || rightEntries.length > 0) {
|
|
7184
|
+
const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
|
|
7185
|
+
lines.push(`:::columns{split:${outerSplit}}`);
|
|
7186
|
+
lines.push(":::col");
|
|
7187
|
+
for (const entry of leftEntries) {
|
|
7188
|
+
lines.push(entry);
|
|
7189
|
+
}
|
|
7190
|
+
lines.push(":::");
|
|
7191
|
+
lines.push(":::col");
|
|
7192
|
+
lines.push("| | |");
|
|
7193
|
+
lines.push("|---|---|");
|
|
7194
|
+
for (const entry of rightEntries) {
|
|
7195
|
+
const label = entry.label || "";
|
|
7196
|
+
const value = entry.value ? ` ${entry.value}` : "";
|
|
7197
|
+
lines.push(`| ${label} |${value} |`);
|
|
7198
|
+
}
|
|
7199
|
+
lines.push(":::");
|
|
7200
|
+
lines.push(":::");
|
|
7201
|
+
lines.push("");
|
|
7202
|
+
}
|
|
7203
|
+
return lines.join("\n");
|
|
7204
|
+
}
|
|
7205
|
+
function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
|
|
7206
|
+
const lines = [];
|
|
7207
|
+
const headerTexts = [];
|
|
7208
|
+
for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
|
|
7209
|
+
if (isStaticLabel(ctrl)) {
|
|
7210
|
+
const text = ctrl.text.trim();
|
|
7211
|
+
if (text) headerTexts.push(text);
|
|
7212
|
+
}
|
|
7213
|
+
}
|
|
7214
|
+
const titleText = headerTexts.join(" ");
|
|
7215
|
+
lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
|
|
7216
|
+
lines.push(":::");
|
|
7217
|
+
lines.push(":::repeat{data:operations}");
|
|
7218
|
+
const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
|
|
7219
|
+
const detailParts = [];
|
|
7220
|
+
for (const ctrl of sortedDetail) {
|
|
7221
|
+
if (isBound(ctrl)) {
|
|
7222
|
+
const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
|
|
7223
|
+
if (resolved) detailParts.push(resolved);
|
|
7224
|
+
}
|
|
7225
|
+
}
|
|
7226
|
+
if (detailParts.length > 0) {
|
|
7227
|
+
if (detailParts.length >= 2) {
|
|
7228
|
+
lines.push(":::columns{split:12}");
|
|
7229
|
+
lines.push(":::col");
|
|
7230
|
+
lines.push(`**${detailParts[0]}**`);
|
|
7231
|
+
lines.push(":::");
|
|
7232
|
+
lines.push(":::col");
|
|
7233
|
+
lines.push(`**${detailParts.slice(1).join(" ")}**`);
|
|
7234
|
+
lines.push(":::");
|
|
7235
|
+
lines.push(":::");
|
|
7236
|
+
} else {
|
|
7237
|
+
lines.push(`**${detailParts[0]}**`);
|
|
7238
|
+
}
|
|
7239
|
+
lines.push("");
|
|
7240
|
+
}
|
|
7241
|
+
if (groupFooterControls.length > 0) {
|
|
7242
|
+
const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
|
|
7243
|
+
if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
|
|
7244
|
+
}
|
|
7245
|
+
lines.push("---");
|
|
7246
|
+
lines.push(":::");
|
|
7247
|
+
lines.push("");
|
|
7248
|
+
return lines.join("\n");
|
|
7249
|
+
}
|
|
7250
|
+
function renderGroupFooterBand(controls, registry, _contentWidth) {
|
|
7251
|
+
const rows = groupByRow(controls);
|
|
7252
|
+
const entries = [];
|
|
7253
|
+
for (const row of rows) {
|
|
7254
|
+
const labels = row.controls.filter((c) => c.type === "XRLabel");
|
|
7255
|
+
if (labels.length === 0) continue;
|
|
7256
|
+
let label = "";
|
|
7257
|
+
let value = "";
|
|
7258
|
+
const sorted = [...labels].sort((a, b) => a.x - b.x);
|
|
7259
|
+
for (const ctrl of sorted) {
|
|
7260
|
+
if (isStaticLabel(ctrl)) {
|
|
7261
|
+
const text = ctrl.text.trim();
|
|
7262
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7263
|
+
label = text;
|
|
7264
|
+
}
|
|
7265
|
+
} else if (isBound(ctrl)) {
|
|
7266
|
+
const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
|
|
7267
|
+
if (resolved) {
|
|
7268
|
+
if (!resolved.startsWith("{{var|")) {
|
|
7269
|
+
if (!label) label = resolved;
|
|
7270
|
+
} else {
|
|
7271
|
+
value = resolved;
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
7274
|
+
}
|
|
7275
|
+
}
|
|
7276
|
+
if (label && value) {
|
|
7277
|
+
entries.push({ label, value });
|
|
7278
|
+
}
|
|
7279
|
+
}
|
|
7280
|
+
if (entries.length === 0) return "";
|
|
7281
|
+
const lines = [];
|
|
7282
|
+
lines.push(":::subtotals");
|
|
7283
|
+
for (const entry of entries) {
|
|
7284
|
+
lines.push(`**${entry.label}** ${entry.value}`);
|
|
7285
|
+
}
|
|
7286
|
+
lines.push(":::");
|
|
7287
|
+
lines.push("");
|
|
7288
|
+
return lines.join("\n");
|
|
7289
|
+
}
|
|
7290
|
+
function renderReportFooterBand(controls, registry) {
|
|
7291
|
+
const lines = [];
|
|
7292
|
+
const panel = controls.find((c) => c.type === "XRPanel");
|
|
7293
|
+
const thankYouLabel = controls.find(
|
|
7294
|
+
(c) => c.type === "XRLabel" && c.text.includes("Thank You")
|
|
7295
|
+
);
|
|
7296
|
+
const priceRows = [];
|
|
7297
|
+
if (panel && panel.children.length > 0) {
|
|
7298
|
+
const rows = groupByRow(panel.children, 15);
|
|
7299
|
+
for (const row of rows) {
|
|
7300
|
+
const labels = row.controls.filter((c) => c.type === "XRLabel");
|
|
7301
|
+
if (labels.length === 0) continue;
|
|
7302
|
+
let label = "";
|
|
7303
|
+
let value = "";
|
|
7304
|
+
const sorted = [...labels].sort((a, b) => a.x - b.x);
|
|
7305
|
+
for (const ctrl of sorted) {
|
|
7306
|
+
if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
|
|
7307
|
+
continue;
|
|
7308
|
+
}
|
|
7309
|
+
const isLabelPos = ctrl.x < 150;
|
|
7310
|
+
if (isStaticLabel(ctrl)) {
|
|
7311
|
+
const text = ctrl.text.trim();
|
|
7312
|
+
if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
|
|
7313
|
+
label = text;
|
|
7314
|
+
}
|
|
7315
|
+
} else if (isBound(ctrl)) {
|
|
7316
|
+
const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
|
|
7317
|
+
if (resolved) {
|
|
7318
|
+
if (isLabelPos) {
|
|
7319
|
+
if (!label) label = resolved;
|
|
7320
|
+
} else {
|
|
7321
|
+
if (!value) value = resolved;
|
|
7322
|
+
}
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
}
|
|
7326
|
+
if (label && value) {
|
|
7327
|
+
priceRows.push({ label, value });
|
|
7328
|
+
} else if (value) {
|
|
7329
|
+
priceRows.push({ label: "", value });
|
|
7330
|
+
}
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7333
|
+
lines.push(":::columns{split:40|padX:20}");
|
|
7334
|
+
lines.push(":::col{padTop:1}");
|
|
7335
|
+
if (thankYouLabel) {
|
|
7336
|
+
lines.push(`## ***${thankYouLabel.text.trim()}***`);
|
|
7337
|
+
}
|
|
7338
|
+
lines.push(":::");
|
|
7339
|
+
lines.push(":::col{padTop:1}");
|
|
7340
|
+
lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
|
|
7341
|
+
if (priceRows.length > 0) {
|
|
7342
|
+
lines.push(":::subtotals");
|
|
7343
|
+
for (const row of priceRows) {
|
|
7344
|
+
const label = row.label ? `**${row.label}**` : "";
|
|
7345
|
+
lines.push(`${label} ${row.value}`);
|
|
7346
|
+
}
|
|
7347
|
+
lines.push(":::");
|
|
7348
|
+
}
|
|
7349
|
+
lines.push(":::");
|
|
7350
|
+
lines.push(":::");
|
|
7351
|
+
lines.push(":::");
|
|
7352
|
+
lines.push("");
|
|
7353
|
+
return lines.join("\n");
|
|
7354
|
+
}
|
|
7355
|
+
function parseXmlTemplate(xmlString) {
|
|
7356
|
+
const warnings = [];
|
|
7357
|
+
const registry = new VariableRegistry();
|
|
7358
|
+
const parser = new DOMParser();
|
|
7359
|
+
const doc = parser.parseFromString(xmlString, "text/xml");
|
|
7360
|
+
const parseError = doc.querySelector("parsererror");
|
|
7361
|
+
if (parseError) {
|
|
7362
|
+
return {
|
|
7363
|
+
markdown: "",
|
|
7364
|
+
variables: [],
|
|
7365
|
+
reportName: "",
|
|
7366
|
+
warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
|
|
7367
|
+
};
|
|
7368
|
+
}
|
|
7369
|
+
const root = doc.documentElement;
|
|
7370
|
+
const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
|
|
7371
|
+
const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
|
|
7372
|
+
const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
|
|
7373
|
+
const marginParts = marginsStr.split(",").map(Number);
|
|
7374
|
+
const leftMargin = marginParts[0] || 50;
|
|
7375
|
+
const rightMargin = marginParts[1] || 50;
|
|
7376
|
+
const contentWidth = pageWidth - leftMargin - rightMargin;
|
|
7377
|
+
const calculatedFieldNames = /* @__PURE__ */ new Set();
|
|
7378
|
+
const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
|
|
7379
|
+
if (calcFieldsEl) {
|
|
7380
|
+
for (const item of Array.from(calcFieldsEl.children)) {
|
|
7381
|
+
const name = item.getAttribute("Name");
|
|
7382
|
+
if (name) calculatedFieldNames.add(name);
|
|
7383
|
+
}
|
|
7384
|
+
}
|
|
7385
|
+
const markdownSections = [];
|
|
7386
|
+
const watermarkEl = root.querySelector("Watermarks > Item1");
|
|
7387
|
+
if (watermarkEl) {
|
|
7388
|
+
const wmText = watermarkEl.getAttribute("Text");
|
|
7389
|
+
if (wmText) {
|
|
7390
|
+
markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
|
|
7391
|
+
markdownSections.push("");
|
|
7392
|
+
}
|
|
7393
|
+
}
|
|
7394
|
+
const bandsEl = root.querySelector(":scope > Bands");
|
|
7395
|
+
if (!bandsEl) {
|
|
7396
|
+
warnings.push("No <Bands> element found in the XML");
|
|
7397
|
+
return {
|
|
7398
|
+
markdown: "",
|
|
7399
|
+
variables: registry.getAll(),
|
|
7400
|
+
reportName,
|
|
7401
|
+
warnings
|
|
7402
|
+
};
|
|
7403
|
+
}
|
|
7404
|
+
let topMarginControls = [];
|
|
7405
|
+
let reportHeaderControls = [];
|
|
7406
|
+
let pageHeaderControls = [];
|
|
7407
|
+
let detailControls = [];
|
|
7408
|
+
let groupFooterControls = [];
|
|
7409
|
+
let reportFooterControls = [];
|
|
7410
|
+
for (const bandItem of Array.from(bandsEl.children)) {
|
|
7411
|
+
const ctrlType = bandItem.getAttribute("ControlType") || "";
|
|
7412
|
+
if (ctrlType === "TopMarginBand") {
|
|
7413
|
+
topMarginControls = extractControls(bandItem);
|
|
7414
|
+
} else if (ctrlType === "ReportHeaderBand") {
|
|
7415
|
+
reportHeaderControls = extractControls(bandItem);
|
|
7416
|
+
} else if (ctrlType === "PageHeaderBand") {
|
|
7417
|
+
pageHeaderControls = extractControls(bandItem);
|
|
7418
|
+
} else if (ctrlType === "DetailReportBand") {
|
|
7419
|
+
const nestedBands = bandItem.querySelector(":scope > Bands");
|
|
7420
|
+
if (nestedBands) {
|
|
7421
|
+
for (const nested of Array.from(nestedBands.children)) {
|
|
7422
|
+
const nestedType = nested.getAttribute("ControlType") || "";
|
|
7423
|
+
if (nestedType === "DetailBand") {
|
|
7424
|
+
detailControls = extractControls(nested);
|
|
7425
|
+
} else if (nestedType === "GroupFooterBand") {
|
|
7426
|
+
groupFooterControls = extractControls(nested);
|
|
7427
|
+
}
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7430
|
+
} else if (ctrlType === "ReportFooterBand") {
|
|
7431
|
+
reportFooterControls = extractControls(bandItem);
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
if (topMarginControls.length > 0) {
|
|
7435
|
+
markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
|
|
7436
|
+
}
|
|
7437
|
+
if (reportHeaderControls.length > 0) {
|
|
7438
|
+
markdownSections.push(
|
|
7439
|
+
renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
|
|
7440
|
+
);
|
|
7441
|
+
}
|
|
7442
|
+
if (pageHeaderControls.length > 0 || detailControls.length > 0) {
|
|
7443
|
+
markdownSections.push(
|
|
7444
|
+
renderOperationsSection(
|
|
7445
|
+
pageHeaderControls,
|
|
7446
|
+
detailControls,
|
|
7447
|
+
groupFooterControls,
|
|
7448
|
+
registry)
|
|
7449
|
+
);
|
|
7450
|
+
}
|
|
7451
|
+
if (reportFooterControls.length > 0) {
|
|
7452
|
+
markdownSections.push(
|
|
7453
|
+
renderReportFooterBand(reportFooterControls, registry)
|
|
7454
|
+
);
|
|
7455
|
+
}
|
|
7456
|
+
const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
|
|
7457
|
+
return {
|
|
7458
|
+
markdown,
|
|
7459
|
+
variables: registry.getAll(),
|
|
7460
|
+
reportName,
|
|
7461
|
+
warnings
|
|
7462
|
+
};
|
|
7463
|
+
}
|
|
7464
|
+
|
|
4958
7465
|
exports.DocumentGenerator = DocumentGenerator;
|
|
4959
7466
|
exports.EditorPanel = EditorPanel;
|
|
4960
7467
|
exports.FormFieldType = FormFieldType;
|
|
4961
7468
|
exports.PreviewPanel = PreviewPanel;
|
|
7469
|
+
exports.RepeatNode = RepeatNode;
|
|
7470
|
+
exports.SubtotalsNode = SubtotalsNode;
|
|
4962
7471
|
exports.ThemeProvider = ThemeProvider;
|
|
7472
|
+
exports.expandBlockVariables = expandBlockVariables;
|
|
7473
|
+
exports.expandRepeatContent = expandRepeatContent;
|
|
7474
|
+
exports.extractVariablesFromContent = extractVariablesFromContent;
|
|
7475
|
+
exports.generatePdfFromMarkdown = generatePdfFromMarkdown;
|
|
7476
|
+
exports.generatePdfFromTiptap = generatePdfFromTiptap;
|
|
7477
|
+
exports.isZeroLike = isZeroLike;
|
|
7478
|
+
exports.markdownToTiptap = markdownToTiptap;
|
|
7479
|
+
exports.parseXmlTemplate = parseXmlTemplate;
|
|
7480
|
+
exports.suppressZeroContent = suppressZeroContent;
|
|
7481
|
+
exports.tiptapToMarkdown = tiptapToMarkdown;
|
|
4963
7482
|
exports.useDocumentGenerator = useDocumentGenerator;
|
|
4964
7483
|
exports.useTheme = useTheme;
|
|
4965
7484
|
//# sourceMappingURL=index.js.map
|