@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/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
- // src/utils/variable-helpers.ts
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
- content.push({
1221
- type: "paragraph",
1222
- content: parseInline(line)
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 = pdfLib.rgb(0.75, 0.75, 0.75);
1753
- const headerBg = pdfLib.rgb(0.3, 0.3, 0.3);
1754
- const headerTextColor = pdfLib.rgb(1, 1, 1);
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: pdfLib.rgb(0, 0, 0)
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 ? pdfLib.rgb(0.25, 0.25, 0.25) : pdfLib.rgb(0.93, 0.93, 0.93),
2072
- borderColor: pdfLib.rgb(0.6, 0.6, 0.6),
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 ? pdfLib.rgb(1, 1, 1) : pdfLib.rgb(0, 0, 0)
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: pdfLib.rgb(0.6, 0.6, 0.6),
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: pdfLib.rgb(0.6, 0.6, 0.6),
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: pdfLib.rgb(0.6, 0.6, 0.6),
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 suppressed = suppressZeroContent(expanded, enrichedValues);
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 `![${attrs.alt || ""}](${attrs.src})`;
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 "var")`);
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 suppressed = suppressZeroContent(expanded, enrichedValues);
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 suppressed = suppressZeroContent(expanded, enrichedValues);
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 suppressed = suppressZeroContent(expanded, enrichedValues);
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;