@signiphi/pdf-compose 0.1.0-beta.7 → 0.1.0-beta.9
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 +12 -5
- package/dist/components/GeneratePanel.d.ts.map +1 -1
- package/dist/extensions/variable-node.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +522 -206
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +522 -207
- package/dist/index.mjs.map +1 -1
- package/dist/styles/index.css +3 -0
- package/dist/styles/index.d.ts +1 -1
- package/dist/utils/markdown-parser.d.ts.map +1 -1
- package/dist/utils/markdown-validator.d.ts.map +1 -1
- package/dist/utils/markdown-writer.d.ts.map +1 -1
- package/dist/utils/pdf-generator.d.ts +50 -0
- package/dist/utils/pdf-generator.d.ts.map +1 -1
- package/dist/utils/template-pipeline.d.ts +21 -3
- package/dist/utils/template-pipeline.d.ts.map +1 -1
- package/dist/utils/variable-helpers.d.ts +28 -0
- package/dist/utils/variable-helpers.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/styles/index.d.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -253,7 +253,8 @@ var VariableNode = core.Node.create({
|
|
|
253
253
|
varLabel: { default: "" },
|
|
254
254
|
varDefault: { default: "" },
|
|
255
255
|
format: { default: "" },
|
|
256
|
-
suppressZero: { default: "" }
|
|
256
|
+
suppressZero: { default: "" },
|
|
257
|
+
block: { default: "" }
|
|
257
258
|
};
|
|
258
259
|
},
|
|
259
260
|
parseHTML() {
|
|
@@ -560,183 +561,8 @@ function extractFieldsFromContent(content) {
|
|
|
560
561
|
}
|
|
561
562
|
return fields;
|
|
562
563
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
function extractVariablesFromContent(content) {
|
|
566
|
-
const seen = /* @__PURE__ */ new Map();
|
|
567
|
-
function walk(node) {
|
|
568
|
-
if (node.type === "variableNode" && node.attrs) {
|
|
569
|
-
const varName = node.attrs.varName;
|
|
570
|
-
if (varName && !seen.has(varName)) {
|
|
571
|
-
seen.set(varName, {
|
|
572
|
-
varName,
|
|
573
|
-
varLabel: node.attrs.varLabel || varName,
|
|
574
|
-
varDefault: node.attrs.varDefault || ""
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
if (node.content) {
|
|
579
|
-
node.content.forEach(walk);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
if (content.content) {
|
|
583
|
-
content.content.forEach(walk);
|
|
584
|
-
}
|
|
585
|
-
return Array.from(seen.values());
|
|
586
|
-
}
|
|
587
|
-
function applyFormat(value, format) {
|
|
588
|
-
if (format === "phone") {
|
|
589
|
-
const digits = value.replace(/\D/g, "");
|
|
590
|
-
if (digits.length === 10) {
|
|
591
|
-
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return value;
|
|
595
|
-
}
|
|
596
|
-
function replaceVariablesInContent(content, values) {
|
|
597
|
-
function walkNode(node) {
|
|
598
|
-
if (node.type === "variableNode" && node.attrs) {
|
|
599
|
-
const varName = node.attrs.varName;
|
|
600
|
-
let replacement = values[varName] || node.attrs.varDefault || "";
|
|
601
|
-
const format = node.attrs.format;
|
|
602
|
-
if (format && replacement) {
|
|
603
|
-
replacement = applyFormat(replacement, format);
|
|
604
|
-
}
|
|
605
|
-
const textNode = { type: "text", text: replacement };
|
|
606
|
-
if (node.marks && node.marks.length > 0) {
|
|
607
|
-
textNode.marks = node.marks;
|
|
608
|
-
}
|
|
609
|
-
return textNode;
|
|
610
|
-
}
|
|
611
|
-
if (node.content) {
|
|
612
|
-
return { ...node, content: node.content.map(walkNode) };
|
|
613
|
-
}
|
|
614
|
-
return node;
|
|
615
|
-
}
|
|
616
|
-
return walkNode(content);
|
|
617
|
-
}
|
|
618
|
-
function labelToVarName(label) {
|
|
619
|
-
return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
620
|
-
}
|
|
621
|
-
function isZeroLike(value) {
|
|
622
|
-
if (!value || value.trim() === "") return true;
|
|
623
|
-
const cleaned = value.replace(/[$,\s]/g, "");
|
|
624
|
-
return /^-?0+(\.0+)?$/.test(cleaned);
|
|
625
|
-
}
|
|
626
|
-
function suppressZeroContent(content, values) {
|
|
627
|
-
function hasSuppressedVar(node) {
|
|
628
|
-
if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
|
|
629
|
-
const varName = node.attrs.varName;
|
|
630
|
-
const val = values[varName] || node.attrs.varDefault || "";
|
|
631
|
-
return isZeroLike(val);
|
|
632
|
-
}
|
|
633
|
-
return (node.content || []).some(hasSuppressedVar);
|
|
634
|
-
}
|
|
635
|
-
function walkNode(node) {
|
|
636
|
-
if (node.type === "table" && node.content) {
|
|
637
|
-
const filtered = node.content.filter((row) => {
|
|
638
|
-
if (row.type !== "tableRow") return true;
|
|
639
|
-
const isHeader = row.content?.[0]?.type === "tableHeader";
|
|
640
|
-
if (isHeader) return true;
|
|
641
|
-
return !hasSuppressedVar(row);
|
|
642
|
-
});
|
|
643
|
-
const bodyRows = filtered.filter(
|
|
644
|
-
(r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
|
|
645
|
-
);
|
|
646
|
-
if (bodyRows.length === 0) return null;
|
|
647
|
-
return { ...node, content: filtered };
|
|
648
|
-
}
|
|
649
|
-
if (node.type === "paragraph" && hasSuppressedVar(node)) {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
if (node.content) {
|
|
653
|
-
const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
|
|
654
|
-
return { ...node, content: filtered };
|
|
655
|
-
}
|
|
656
|
-
return node;
|
|
657
|
-
}
|
|
658
|
-
return walkNode(content) || content;
|
|
659
|
-
}
|
|
660
|
-
function expandRepeatContent(content, values) {
|
|
661
|
-
const enrichedValues = { ...values };
|
|
662
|
-
function cloneNode(node, dataKey, index) {
|
|
663
|
-
if (node.type === "variableNode" && node.attrs?.varName) {
|
|
664
|
-
const oldName = node.attrs.varName;
|
|
665
|
-
const bareName = oldName.replace(/^operation_/, "");
|
|
666
|
-
const newName = `${dataKey}_${index}_${bareName}`;
|
|
667
|
-
return { ...node, attrs: { ...node.attrs, varName: newName } };
|
|
668
|
-
}
|
|
669
|
-
if (node.content) {
|
|
670
|
-
return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
|
|
671
|
-
}
|
|
672
|
-
return { ...node };
|
|
673
|
-
}
|
|
674
|
-
function walkNode(node) {
|
|
675
|
-
if (node.type === "repeatBlock" && node.attrs?.data) {
|
|
676
|
-
const dataKey = node.attrs.data;
|
|
677
|
-
const arrayJson = values[dataKey];
|
|
678
|
-
if (!arrayJson) return [];
|
|
679
|
-
let items;
|
|
680
|
-
try {
|
|
681
|
-
items = JSON.parse(arrayJson);
|
|
682
|
-
} catch {
|
|
683
|
-
return [];
|
|
684
|
-
}
|
|
685
|
-
const sums = /* @__PURE__ */ new Map();
|
|
686
|
-
for (const item of items) {
|
|
687
|
-
for (const [key, val] of Object.entries(item)) {
|
|
688
|
-
const num = parseFloat(val);
|
|
689
|
-
if (!isNaN(num)) {
|
|
690
|
-
sums.set(key, (sums.get(key) || 0) + num);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
const woKeysByNorm = /* @__PURE__ */ new Map();
|
|
695
|
-
for (const k of Object.keys(enrichedValues)) {
|
|
696
|
-
if (k.startsWith("workorder_")) {
|
|
697
|
-
const norm = k.replace(/_/g, "");
|
|
698
|
-
woKeysByNorm.set(norm, k);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
for (const [field, total] of sums) {
|
|
702
|
-
const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
|
|
703
|
-
const directKey = `workorder_${field}`;
|
|
704
|
-
enrichedValues[directKey] = formatted;
|
|
705
|
-
const norm = `workorder${field}`.replace(/_/g, "");
|
|
706
|
-
const existing = woKeysByNorm.get(norm);
|
|
707
|
-
if (existing && existing !== directKey) {
|
|
708
|
-
enrichedValues[existing] = formatted;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
const expanded = [];
|
|
712
|
-
for (let i = 0; i < items.length; i++) {
|
|
713
|
-
const item = items[i];
|
|
714
|
-
for (const [key, val] of Object.entries(item)) {
|
|
715
|
-
enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
|
|
716
|
-
}
|
|
717
|
-
const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
|
|
718
|
-
expanded.push(...cloned);
|
|
719
|
-
}
|
|
720
|
-
return expanded;
|
|
721
|
-
}
|
|
722
|
-
if (node.content) {
|
|
723
|
-
const newContent = [];
|
|
724
|
-
for (const child of node.content) {
|
|
725
|
-
const result2 = walkNode(child);
|
|
726
|
-
if (Array.isArray(result2)) newContent.push(...result2);
|
|
727
|
-
else newContent.push(result2);
|
|
728
|
-
}
|
|
729
|
-
return { ...node, content: newContent };
|
|
730
|
-
}
|
|
731
|
-
return node;
|
|
732
|
-
}
|
|
733
|
-
const result = walkNode(content);
|
|
734
|
-
return {
|
|
735
|
-
content: Array.isArray(result) ? { ...content, content: result } : result,
|
|
736
|
-
values: enrichedValues
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
|
|
564
|
+
var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
|
|
565
|
+
var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
|
|
740
566
|
var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
|
|
741
567
|
var DIRECTIVE_CLOSE = /^:::$/;
|
|
742
568
|
function parseDirectiveAttrs(str) {
|
|
@@ -874,6 +700,9 @@ function parseVariableToken(tokenBody) {
|
|
|
874
700
|
case "format":
|
|
875
701
|
attrs.format = value;
|
|
876
702
|
break;
|
|
703
|
+
case "block":
|
|
704
|
+
attrs.block = value === "true" ? "true" : "";
|
|
705
|
+
break;
|
|
877
706
|
case "_marks":
|
|
878
707
|
attrs._marks = value;
|
|
879
708
|
break;
|
|
@@ -881,6 +710,44 @@ function parseVariableToken(tokenBody) {
|
|
|
881
710
|
}
|
|
882
711
|
return attrs;
|
|
883
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
|
+
}
|
|
884
751
|
function parseInline(text) {
|
|
885
752
|
text = preprocessMarkedTokens(text);
|
|
886
753
|
const nodes = [];
|
|
@@ -904,6 +771,8 @@ function parseInline(text) {
|
|
|
904
771
|
const node = { type: "variableNode", attrs };
|
|
905
772
|
if (marks.length > 0) node.marks = marks;
|
|
906
773
|
nodes.push(node);
|
|
774
|
+
} else if (match[1] === "image") {
|
|
775
|
+
nodes.push({ type: "imageNode", attrs: parseImageToken(match[2]) });
|
|
907
776
|
}
|
|
908
777
|
lastIndex = match.index + match[0].length;
|
|
909
778
|
}
|
|
@@ -1217,10 +1086,24 @@ function parseBlocks(lines, startIdx, stopCondition) {
|
|
|
1217
1086
|
i++;
|
|
1218
1087
|
continue;
|
|
1219
1088
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
content
|
|
1223
|
-
|
|
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
|
+
}
|
|
1224
1107
|
i++;
|
|
1225
1108
|
}
|
|
1226
1109
|
return { blocks: content, nextIdx: i };
|
|
@@ -1231,6 +1114,233 @@ function markdownToTiptap(markdown) {
|
|
|
1231
1114
|
return { type: "doc", content: blocks };
|
|
1232
1115
|
}
|
|
1233
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
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1234
1344
|
// src/utils/error-helpers.ts
|
|
1235
1345
|
function getErrorMessage(error) {
|
|
1236
1346
|
if (error instanceof Error) return error.message;
|
|
@@ -1371,6 +1481,34 @@ var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
|
|
|
1371
1481
|
var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
|
|
1372
1482
|
var LINE_HEIGHT_FACTOR = 1.4;
|
|
1373
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
|
+
}
|
|
1374
1512
|
var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
|
|
1375
1513
|
var FIELD_DISPLAY = {
|
|
1376
1514
|
["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
|
|
@@ -1402,6 +1540,53 @@ function getFieldDimensions(fieldType, attrs, font, fontSize) {
|
|
|
1402
1540
|
const height = textLineHeight + 4;
|
|
1403
1541
|
return { width, height };
|
|
1404
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
|
+
}
|
|
1405
1590
|
function ensureSpace(state, neededHeight) {
|
|
1406
1591
|
if (state.y + neededHeight > CONTENT_HEIGHT) {
|
|
1407
1592
|
const newPage = state.pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
|
@@ -1749,9 +1934,9 @@ function renderTable(state, node, region = DEFAULT_REGION) {
|
|
|
1749
1934
|
const cellPadding = borderless ? 2 : 4;
|
|
1750
1935
|
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
1751
1936
|
const rowPadding = cellPadding * 2;
|
|
1752
|
-
const borderColor =
|
|
1753
|
-
const headerBg =
|
|
1754
|
-
const headerTextColor =
|
|
1937
|
+
const borderColor = state.theme.tableBorder;
|
|
1938
|
+
const headerBg = state.theme.tableHeaderBg;
|
|
1939
|
+
const headerTextColor = state.theme.tableHeaderText;
|
|
1755
1940
|
const firstRow = tableRows[0];
|
|
1756
1941
|
const colCount = firstRow.content?.length || 0;
|
|
1757
1942
|
if (colCount === 0) return;
|
|
@@ -1999,7 +2184,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
|
|
|
1999
2184
|
start: { x: region.leftX, y: ruleY },
|
|
2000
2185
|
end: { x: region.leftX + region.width, y: ruleY },
|
|
2001
2186
|
thickness: 2.5,
|
|
2002
|
-
color:
|
|
2187
|
+
color: state.theme.rule
|
|
2003
2188
|
});
|
|
2004
2189
|
state.y += 4;
|
|
2005
2190
|
break;
|
|
@@ -2032,6 +2217,39 @@ function processBlock(state, node, region = DEFAULT_REGION) {
|
|
|
2032
2217
|
renderTable(state, node, region);
|
|
2033
2218
|
break;
|
|
2034
2219
|
}
|
|
2220
|
+
case "imageNode": {
|
|
2221
|
+
const src = node.attrs?.src || "";
|
|
2222
|
+
const image = src ? state.images.get(src) : void 0;
|
|
2223
|
+
if (!image) break;
|
|
2224
|
+
const natW = image.width;
|
|
2225
|
+
const natH = image.height;
|
|
2226
|
+
let w = parseFloat(node.attrs?.width || "") || 0;
|
|
2227
|
+
let h = parseFloat(node.attrs?.height || "") || 0;
|
|
2228
|
+
if (w && !h) h = natH / natW * w;
|
|
2229
|
+
else if (h && !w) w = natW / natH * h;
|
|
2230
|
+
else if (!w && !h) {
|
|
2231
|
+
w = natW;
|
|
2232
|
+
h = natH;
|
|
2233
|
+
}
|
|
2234
|
+
if (w > region.width) {
|
|
2235
|
+
h = h * (region.width / w);
|
|
2236
|
+
w = region.width;
|
|
2237
|
+
}
|
|
2238
|
+
const maxH = CONTENT_HEIGHT * 0.6;
|
|
2239
|
+
if (h > maxH) {
|
|
2240
|
+
w = w * (maxH / h);
|
|
2241
|
+
h = maxH;
|
|
2242
|
+
}
|
|
2243
|
+
ensureSpace(state, h + 4);
|
|
2244
|
+
const align = node.attrs?.align || "left";
|
|
2245
|
+
let x = region.leftX;
|
|
2246
|
+
if (align === "center") x = region.leftX + (region.width - w) / 2;
|
|
2247
|
+
else if (align === "right") x = region.leftX + region.width - w;
|
|
2248
|
+
const pdfY = PAGE_HEIGHT - MARGIN - state.y - h;
|
|
2249
|
+
state.currentPage.drawImage(image, { x, y: pdfY, width: w, height: h });
|
|
2250
|
+
state.y += h + 4;
|
|
2251
|
+
break;
|
|
2252
|
+
}
|
|
2035
2253
|
// ─── Layout directives ─────────────────────────────────────────
|
|
2036
2254
|
case "panel": {
|
|
2037
2255
|
const title = node.attrs?.title || "";
|
|
@@ -2068,8 +2286,8 @@ function processBlock(state, node, region = DEFAULT_REGION) {
|
|
|
2068
2286
|
y: titleBarY,
|
|
2069
2287
|
width: region.width,
|
|
2070
2288
|
height: titleHeight,
|
|
2071
|
-
color: isDarkHeader ?
|
|
2072
|
-
borderColor:
|
|
2289
|
+
color: isDarkHeader ? state.theme.panelDarkHeaderBg : pdfLib.rgb(0.93, 0.93, 0.93),
|
|
2290
|
+
borderColor: state.theme.panelBorder,
|
|
2073
2291
|
borderWidth: 0.75
|
|
2074
2292
|
});
|
|
2075
2293
|
startPage.drawText(title, {
|
|
@@ -2077,7 +2295,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
|
|
|
2077
2295
|
y: titleBarY + 4,
|
|
2078
2296
|
size: titleFontSize,
|
|
2079
2297
|
font: state.fonts.bold,
|
|
2080
|
-
color: isDarkHeader ?
|
|
2298
|
+
color: isDarkHeader ? state.theme.panelDarkHeaderText : pdfLib.rgb(0, 0, 0)
|
|
2081
2299
|
});
|
|
2082
2300
|
}
|
|
2083
2301
|
if (border !== "none") {
|
|
@@ -2090,7 +2308,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
|
|
|
2090
2308
|
y: borderY,
|
|
2091
2309
|
width: region.width,
|
|
2092
2310
|
height: panelHeight,
|
|
2093
|
-
borderColor:
|
|
2311
|
+
borderColor: state.theme.panelBorder,
|
|
2094
2312
|
borderWidth: 0.75,
|
|
2095
2313
|
borderDashArray
|
|
2096
2314
|
});
|
|
@@ -2102,7 +2320,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
|
|
|
2102
2320
|
y: startBorderBottom,
|
|
2103
2321
|
width: region.width,
|
|
2104
2322
|
height: startPanelHeight,
|
|
2105
|
-
borderColor:
|
|
2323
|
+
borderColor: state.theme.panelBorder,
|
|
2106
2324
|
borderWidth: 0.75,
|
|
2107
2325
|
borderDashArray
|
|
2108
2326
|
});
|
|
@@ -2114,7 +2332,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
|
|
|
2114
2332
|
y: contBorderY,
|
|
2115
2333
|
width: region.width,
|
|
2116
2334
|
height: contHeight,
|
|
2117
|
-
borderColor:
|
|
2335
|
+
borderColor: state.theme.panelBorder,
|
|
2118
2336
|
borderWidth: 0.75,
|
|
2119
2337
|
borderDashArray
|
|
2120
2338
|
});
|
|
@@ -2418,6 +2636,24 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
|
|
|
2418
2636
|
pdfLib.PDFString.of(field.label)
|
|
2419
2637
|
);
|
|
2420
2638
|
}
|
|
2639
|
+
const initialsFontSize = Math.min(
|
|
2640
|
+
72,
|
|
2641
|
+
Math.max(8, Math.round(position.height * 0.55))
|
|
2642
|
+
);
|
|
2643
|
+
try {
|
|
2644
|
+
initialsField.setFontSize(initialsFontSize);
|
|
2645
|
+
const acro = initialsField;
|
|
2646
|
+
const daRef = acro.acroField.dict.get(pdfLib.PDFName.of("DA"));
|
|
2647
|
+
if (daRef) {
|
|
2648
|
+
const daStr = daRef.value;
|
|
2649
|
+
if (typeof daStr === "string") {
|
|
2650
|
+
const newDa = daStr.replace(/\/Helv\b/, "/TiBo").replace(/\/HeBo\b/, "/TiBo");
|
|
2651
|
+
acro.acroField.dict.set(pdfLib.PDFName.of("DA"), pdfLib.PDFString.of(newDa));
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
} catch (fontError) {
|
|
2655
|
+
console.warn(`Failed to set initials font:`, fontError);
|
|
2656
|
+
}
|
|
2421
2657
|
initialsField.enableReadOnly();
|
|
2422
2658
|
if (field.required) initialsField.enableRequired();
|
|
2423
2659
|
break;
|
|
@@ -2528,7 +2764,8 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
|
|
|
2528
2764
|
return warnings;
|
|
2529
2765
|
}
|
|
2530
2766
|
async function generatePdfFromContent(content, options = {}) {
|
|
2531
|
-
const { drawFieldPlaceholders = false, embedFormFields = false, fields = [] } = options;
|
|
2767
|
+
const { drawFieldPlaceholders = false, embedFormFields = false, fields = [], header, footer } = options;
|
|
2768
|
+
const theme = resolveTheme(options.theme);
|
|
2532
2769
|
const pdfDoc = await pdfLib.PDFDocument.create();
|
|
2533
2770
|
const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
|
2534
2771
|
const fonts = {
|
|
@@ -2538,6 +2775,7 @@ async function generatePdfFromContent(content, options = {}) {
|
|
|
2538
2775
|
boldItalic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBoldOblique)
|
|
2539
2776
|
};
|
|
2540
2777
|
const watermarkNodes = collectWatermarkNodes(content);
|
|
2778
|
+
const { images, warnings: imageWarnings } = await preloadImages(pdfDoc, content);
|
|
2541
2779
|
const state = {
|
|
2542
2780
|
currentPage: firstPage,
|
|
2543
2781
|
pageIndex: 0,
|
|
@@ -2546,7 +2784,9 @@ async function generatePdfFromContent(content, options = {}) {
|
|
|
2546
2784
|
pdfDoc,
|
|
2547
2785
|
fieldPositions: /* @__PURE__ */ new Map(),
|
|
2548
2786
|
drawFieldPlaceholders,
|
|
2549
|
-
watermarkNodes
|
|
2787
|
+
watermarkNodes,
|
|
2788
|
+
theme,
|
|
2789
|
+
images
|
|
2550
2790
|
};
|
|
2551
2791
|
renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
|
|
2552
2792
|
if (content.content) {
|
|
@@ -2557,6 +2797,8 @@ async function generatePdfFromContent(content, options = {}) {
|
|
|
2557
2797
|
const allPages = pdfDoc.getPages();
|
|
2558
2798
|
const totalPages = allPages.length;
|
|
2559
2799
|
const pageNumFontSize = 9;
|
|
2800
|
+
const hfFontSize = 8;
|
|
2801
|
+
const hfColor = pdfLib.rgb(0.45, 0.45, 0.45);
|
|
2560
2802
|
for (let i = 0; i < totalPages; i++) {
|
|
2561
2803
|
const page = allPages[i];
|
|
2562
2804
|
const label = `Page ${i + 1} of ${totalPages}`;
|
|
@@ -2568,6 +2810,43 @@ async function generatePdfFromContent(content, options = {}) {
|
|
|
2568
2810
|
font: fonts.bold,
|
|
2569
2811
|
color: pdfLib.rgb(0, 0, 0)
|
|
2570
2812
|
});
|
|
2813
|
+
if (header && (header.left || header.right) && !(header.skipFirstPage && i === 0)) {
|
|
2814
|
+
const headerBaseY = PAGE_HEIGHT - MARGIN + 10;
|
|
2815
|
+
if (header.left) {
|
|
2816
|
+
page.drawText(header.left, {
|
|
2817
|
+
x: MARGIN,
|
|
2818
|
+
y: headerBaseY,
|
|
2819
|
+
size: pageNumFontSize,
|
|
2820
|
+
font: fonts.bold,
|
|
2821
|
+
color: theme.pageHeaderText
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
if (header.right) {
|
|
2825
|
+
const rightWidth = fonts.regular.widthOfTextAtSize(header.right, hfFontSize);
|
|
2826
|
+
page.drawText(header.right, {
|
|
2827
|
+
x: PAGE_WIDTH - MARGIN - rightWidth,
|
|
2828
|
+
y: headerBaseY,
|
|
2829
|
+
size: hfFontSize,
|
|
2830
|
+
font: fonts.regular,
|
|
2831
|
+
color: hfColor
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
page.drawLine({
|
|
2835
|
+
start: { x: MARGIN, y: headerBaseY - 6 },
|
|
2836
|
+
end: { x: PAGE_WIDTH - MARGIN, y: headerBaseY - 6 },
|
|
2837
|
+
thickness: 0.75,
|
|
2838
|
+
color: theme.pageHeaderRule
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
if (footer?.left) {
|
|
2842
|
+
page.drawText(footer.left, {
|
|
2843
|
+
x: MARGIN,
|
|
2844
|
+
y: MARGIN / 2 - pageNumFontSize / 2,
|
|
2845
|
+
size: hfFontSize,
|
|
2846
|
+
font: fonts.regular,
|
|
2847
|
+
color: hfColor
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2571
2850
|
}
|
|
2572
2851
|
let fieldWarnings;
|
|
2573
2852
|
if (embedFormFields && fields.length > 0) {
|
|
@@ -2578,25 +2857,31 @@ async function generatePdfFromContent(content, options = {}) {
|
|
|
2578
2857
|
return {
|
|
2579
2858
|
pdfBytes,
|
|
2580
2859
|
fieldPositions: state.fieldPositions,
|
|
2581
|
-
fieldWarnings
|
|
2860
|
+
fieldWarnings,
|
|
2861
|
+
imageWarnings: imageWarnings.length > 0 ? imageWarnings : void 0
|
|
2582
2862
|
};
|
|
2583
2863
|
}
|
|
2584
2864
|
|
|
2585
2865
|
// src/utils/template-pipeline.ts
|
|
2586
|
-
async function generatePdfFromTiptap(content, values) {
|
|
2866
|
+
async function generatePdfFromTiptap(content, values, options = {}) {
|
|
2587
2867
|
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
|
|
2588
|
-
const
|
|
2868
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
2869
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
2589
2870
|
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
2590
2871
|
const fields = extractFieldsFromContent(replaced);
|
|
2591
2872
|
const result = await generatePdfFromContent(replaced, {
|
|
2592
2873
|
embedFormFields: fields.length > 0,
|
|
2593
|
-
fields
|
|
2874
|
+
fields,
|
|
2875
|
+
theme: options.theme,
|
|
2876
|
+
header: options.header,
|
|
2877
|
+
footer: options.footer,
|
|
2878
|
+
drawFieldPlaceholders: options.drawFieldPlaceholders
|
|
2594
2879
|
});
|
|
2595
|
-
return { pdfBytes: result.pdfBytes };
|
|
2880
|
+
return { pdfBytes: result.pdfBytes, imageWarnings: result.imageWarnings };
|
|
2596
2881
|
}
|
|
2597
|
-
async function generatePdfFromMarkdown(markdown, values) {
|
|
2882
|
+
async function generatePdfFromMarkdown(markdown, values, options = {}) {
|
|
2598
2883
|
const content = markdownToTiptap(markdown);
|
|
2599
|
-
return generatePdfFromTiptap(content, values);
|
|
2884
|
+
return generatePdfFromTiptap(content, values, options);
|
|
2600
2885
|
}
|
|
2601
2886
|
|
|
2602
2887
|
// src/utils/markdown-writer.ts
|
|
@@ -2616,6 +2901,20 @@ function serializeFieldToken(attrs) {
|
|
|
2616
2901
|
if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
|
|
2617
2902
|
return `{{${parts.join("|")}}}`;
|
|
2618
2903
|
}
|
|
2904
|
+
function serializeImageNode(attrs) {
|
|
2905
|
+
const hasExtras = attrs.varName || attrs.width || attrs.height || attrs.align;
|
|
2906
|
+
if (!hasExtras && attrs.src) {
|
|
2907
|
+
return ``;
|
|
2908
|
+
}
|
|
2909
|
+
const parts = ["image"];
|
|
2910
|
+
if (attrs.varName) parts.push(`var:${attrs.varName}`);
|
|
2911
|
+
else if (attrs.src) parts.push(`src:${attrs.src}`);
|
|
2912
|
+
if (attrs.alt) parts.push(`alt:${attrs.alt}`);
|
|
2913
|
+
if (attrs.width) parts.push(`width:${attrs.width}`);
|
|
2914
|
+
if (attrs.height) parts.push(`height:${attrs.height}`);
|
|
2915
|
+
if (attrs.align) parts.push(`align:${attrs.align}`);
|
|
2916
|
+
return `{{${parts.join("|")}}}`;
|
|
2917
|
+
}
|
|
2619
2918
|
function serializeVariableToken(attrs) {
|
|
2620
2919
|
const parts = ["var"];
|
|
2621
2920
|
if (attrs.varName) parts.push(`name:${attrs.varName}`);
|
|
@@ -2623,6 +2922,7 @@ function serializeVariableToken(attrs) {
|
|
|
2623
2922
|
if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
|
|
2624
2923
|
if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
|
|
2625
2924
|
if (attrs.format) parts.push(`format:${attrs.format}`);
|
|
2925
|
+
if (attrs.block === "true") parts.push(`block:true`);
|
|
2626
2926
|
return `{{${parts.join("|")}}}`;
|
|
2627
2927
|
}
|
|
2628
2928
|
function serializeInline(content) {
|
|
@@ -2633,6 +2933,10 @@ function serializeInline(content) {
|
|
|
2633
2933
|
result += serializeFieldToken(node.attrs || {});
|
|
2634
2934
|
continue;
|
|
2635
2935
|
}
|
|
2936
|
+
if (node.type === "imageNode") {
|
|
2937
|
+
result += serializeImageNode(node.attrs || {});
|
|
2938
|
+
continue;
|
|
2939
|
+
}
|
|
2636
2940
|
if (node.type === "variableNode") {
|
|
2637
2941
|
let token = serializeVariableToken(node.attrs || {});
|
|
2638
2942
|
const varMarks = node.marks || [];
|
|
@@ -2739,6 +3043,9 @@ ${text}
|
|
|
2739
3043
|
case "tableCell": {
|
|
2740
3044
|
return (node.content || []).map(serializeBlock).join("");
|
|
2741
3045
|
}
|
|
3046
|
+
case "imageNode": {
|
|
3047
|
+
return serializeImageNode(node.attrs || {});
|
|
3048
|
+
}
|
|
2742
3049
|
case "watermark": {
|
|
2743
3050
|
const attrs = node.attrs || {};
|
|
2744
3051
|
const parts = [];
|
|
@@ -4802,8 +5109,8 @@ function validateMarkdown(markdown) {
|
|
|
4802
5109
|
errors.push(`Malformed token at line ${lineNum}: missing | delimiter`);
|
|
4803
5110
|
} else {
|
|
4804
5111
|
const tokenType = tokenContent.slice(0, pipeIdx);
|
|
4805
|
-
if (tokenType !== "field" && tokenType !== "var") {
|
|
4806
|
-
errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field" or "
|
|
5112
|
+
if (tokenType !== "field" && tokenType !== "var" && tokenType !== "image") {
|
|
5113
|
+
errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field", "var" or "image")`);
|
|
4807
5114
|
} else {
|
|
4808
5115
|
const body = tokenContent.slice(pipeIdx + 1);
|
|
4809
5116
|
const pairs = body.split("|");
|
|
@@ -4817,6 +5124,11 @@ function validateMarkdown(markdown) {
|
|
|
4817
5124
|
if (!hasName) {
|
|
4818
5125
|
errors.push(`Variable token at line ${lineNum} is missing required "name" attribute`);
|
|
4819
5126
|
}
|
|
5127
|
+
} else if (tokenType === "image") {
|
|
5128
|
+
const hasSource = pairs.some((p) => p.startsWith("src:") || p.startsWith("var:"));
|
|
5129
|
+
if (!hasSource) {
|
|
5130
|
+
errors.push(`Image token at line ${lineNum} needs a "src" or "var" attribute`);
|
|
5131
|
+
}
|
|
4820
5132
|
}
|
|
4821
5133
|
}
|
|
4822
5134
|
}
|
|
@@ -5158,7 +5470,8 @@ function GeneratePanel({
|
|
|
5158
5470
|
try {
|
|
5159
5471
|
const content = getActiveContent();
|
|
5160
5472
|
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
|
|
5161
|
-
const
|
|
5473
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
5474
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
5162
5475
|
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
5163
5476
|
const fields = extractFieldsFromContent(replaced);
|
|
5164
5477
|
const result = await generatePdfFromContent(replaced);
|
|
@@ -5181,7 +5494,8 @@ function GeneratePanel({
|
|
|
5181
5494
|
try {
|
|
5182
5495
|
const content = getActiveContent();
|
|
5183
5496
|
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
|
|
5184
|
-
const
|
|
5497
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
5498
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
5185
5499
|
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
5186
5500
|
const fields = extractFieldsFromContent(replaced);
|
|
5187
5501
|
const result = await generatePdfFromContent(replaced, {
|
|
@@ -5263,7 +5577,8 @@ function GeneratePanel({
|
|
|
5263
5577
|
for (let i = 0; i < bulkData.length; i++) {
|
|
5264
5578
|
const values = bulkData[i];
|
|
5265
5579
|
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
|
|
5266
|
-
const
|
|
5580
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
5581
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
5267
5582
|
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
5268
5583
|
const fields = extractFieldsFromContent(replaced);
|
|
5269
5584
|
const result = await generatePdfFromContent(replaced, {
|
|
@@ -7152,6 +7467,7 @@ exports.PreviewPanel = PreviewPanel;
|
|
|
7152
7467
|
exports.RepeatNode = RepeatNode;
|
|
7153
7468
|
exports.SubtotalsNode = SubtotalsNode;
|
|
7154
7469
|
exports.ThemeProvider = ThemeProvider;
|
|
7470
|
+
exports.expandBlockVariables = expandBlockVariables;
|
|
7155
7471
|
exports.expandRepeatContent = expandRepeatContent;
|
|
7156
7472
|
exports.extractVariablesFromContent = extractVariablesFromContent;
|
|
7157
7473
|
exports.generatePdfFromMarkdown = generatePdfFromMarkdown;
|