@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.mjs CHANGED
@@ -219,7 +219,8 @@ var VariableNode = Node.create({
219
219
  varLabel: { default: "" },
220
220
  varDefault: { default: "" },
221
221
  format: { default: "" },
222
- suppressZero: { default: "" }
222
+ suppressZero: { default: "" },
223
+ block: { default: "" }
223
224
  };
224
225
  },
225
226
  parseHTML() {
@@ -526,183 +527,8 @@ function extractFieldsFromContent(content) {
526
527
  }
527
528
  return fields;
528
529
  }
529
-
530
- // src/utils/variable-helpers.ts
531
- function extractVariablesFromContent(content) {
532
- const seen = /* @__PURE__ */ new Map();
533
- function walk(node) {
534
- if (node.type === "variableNode" && node.attrs) {
535
- const varName = node.attrs.varName;
536
- if (varName && !seen.has(varName)) {
537
- seen.set(varName, {
538
- varName,
539
- varLabel: node.attrs.varLabel || varName,
540
- varDefault: node.attrs.varDefault || ""
541
- });
542
- }
543
- }
544
- if (node.content) {
545
- node.content.forEach(walk);
546
- }
547
- }
548
- if (content.content) {
549
- content.content.forEach(walk);
550
- }
551
- return Array.from(seen.values());
552
- }
553
- function applyFormat(value, format) {
554
- if (format === "phone") {
555
- const digits = value.replace(/\D/g, "");
556
- if (digits.length === 10) {
557
- return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
558
- }
559
- }
560
- return value;
561
- }
562
- function replaceVariablesInContent(content, values) {
563
- function walkNode(node) {
564
- if (node.type === "variableNode" && node.attrs) {
565
- const varName = node.attrs.varName;
566
- let replacement = values[varName] || node.attrs.varDefault || "";
567
- const format = node.attrs.format;
568
- if (format && replacement) {
569
- replacement = applyFormat(replacement, format);
570
- }
571
- const textNode = { type: "text", text: replacement };
572
- if (node.marks && node.marks.length > 0) {
573
- textNode.marks = node.marks;
574
- }
575
- return textNode;
576
- }
577
- if (node.content) {
578
- return { ...node, content: node.content.map(walkNode) };
579
- }
580
- return node;
581
- }
582
- return walkNode(content);
583
- }
584
- function labelToVarName(label) {
585
- return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
586
- }
587
- function isZeroLike(value) {
588
- if (!value || value.trim() === "") return true;
589
- const cleaned = value.replace(/[$,\s]/g, "");
590
- return /^-?0+(\.0+)?$/.test(cleaned);
591
- }
592
- function suppressZeroContent(content, values) {
593
- function hasSuppressedVar(node) {
594
- if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
595
- const varName = node.attrs.varName;
596
- const val = values[varName] || node.attrs.varDefault || "";
597
- return isZeroLike(val);
598
- }
599
- return (node.content || []).some(hasSuppressedVar);
600
- }
601
- function walkNode(node) {
602
- if (node.type === "table" && node.content) {
603
- const filtered = node.content.filter((row) => {
604
- if (row.type !== "tableRow") return true;
605
- const isHeader = row.content?.[0]?.type === "tableHeader";
606
- if (isHeader) return true;
607
- return !hasSuppressedVar(row);
608
- });
609
- const bodyRows = filtered.filter(
610
- (r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
611
- );
612
- if (bodyRows.length === 0) return null;
613
- return { ...node, content: filtered };
614
- }
615
- if (node.type === "paragraph" && hasSuppressedVar(node)) {
616
- return null;
617
- }
618
- if (node.content) {
619
- const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
620
- return { ...node, content: filtered };
621
- }
622
- return node;
623
- }
624
- return walkNode(content) || content;
625
- }
626
- function expandRepeatContent(content, values) {
627
- const enrichedValues = { ...values };
628
- function cloneNode(node, dataKey, index) {
629
- if (node.type === "variableNode" && node.attrs?.varName) {
630
- const oldName = node.attrs.varName;
631
- const bareName = oldName.replace(/^operation_/, "");
632
- const newName = `${dataKey}_${index}_${bareName}`;
633
- return { ...node, attrs: { ...node.attrs, varName: newName } };
634
- }
635
- if (node.content) {
636
- return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
637
- }
638
- return { ...node };
639
- }
640
- function walkNode(node) {
641
- if (node.type === "repeatBlock" && node.attrs?.data) {
642
- const dataKey = node.attrs.data;
643
- const arrayJson = values[dataKey];
644
- if (!arrayJson) return [];
645
- let items;
646
- try {
647
- items = JSON.parse(arrayJson);
648
- } catch {
649
- return [];
650
- }
651
- const sums = /* @__PURE__ */ new Map();
652
- for (const item of items) {
653
- for (const [key, val] of Object.entries(item)) {
654
- const num = parseFloat(val);
655
- if (!isNaN(num)) {
656
- sums.set(key, (sums.get(key) || 0) + num);
657
- }
658
- }
659
- }
660
- const woKeysByNorm = /* @__PURE__ */ new Map();
661
- for (const k of Object.keys(enrichedValues)) {
662
- if (k.startsWith("workorder_")) {
663
- const norm = k.replace(/_/g, "");
664
- woKeysByNorm.set(norm, k);
665
- }
666
- }
667
- for (const [field, total] of sums) {
668
- const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
669
- const directKey = `workorder_${field}`;
670
- enrichedValues[directKey] = formatted;
671
- const norm = `workorder${field}`.replace(/_/g, "");
672
- const existing = woKeysByNorm.get(norm);
673
- if (existing && existing !== directKey) {
674
- enrichedValues[existing] = formatted;
675
- }
676
- }
677
- const expanded = [];
678
- for (let i = 0; i < items.length; i++) {
679
- const item = items[i];
680
- for (const [key, val] of Object.entries(item)) {
681
- enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
682
- }
683
- const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
684
- expanded.push(...cloned);
685
- }
686
- return expanded;
687
- }
688
- if (node.content) {
689
- const newContent = [];
690
- for (const child of node.content) {
691
- const result2 = walkNode(child);
692
- if (Array.isArray(result2)) newContent.push(...result2);
693
- else newContent.push(result2);
694
- }
695
- return { ...node, content: newContent };
696
- }
697
- return node;
698
- }
699
- const result = walkNode(content);
700
- return {
701
- content: Array.isArray(result) ? { ...content, content: result } : result,
702
- values: enrichedValues
703
- };
704
- }
705
- var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
530
+ var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
531
+ var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
706
532
  var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
707
533
  var DIRECTIVE_CLOSE = /^:::$/;
708
534
  function parseDirectiveAttrs(str) {
@@ -840,6 +666,9 @@ function parseVariableToken(tokenBody) {
840
666
  case "format":
841
667
  attrs.format = value;
842
668
  break;
669
+ case "block":
670
+ attrs.block = value === "true" ? "true" : "";
671
+ break;
843
672
  case "_marks":
844
673
  attrs._marks = value;
845
674
  break;
@@ -847,6 +676,44 @@ function parseVariableToken(tokenBody) {
847
676
  }
848
677
  return attrs;
849
678
  }
679
+ function parseImageToken(tokenBody) {
680
+ const attrs = {
681
+ src: "",
682
+ varName: "",
683
+ alt: "",
684
+ width: "",
685
+ height: "",
686
+ align: ""
687
+ };
688
+ const pairs = tokenBody.split("|");
689
+ for (const pair of pairs) {
690
+ const colonIdx = pair.indexOf(":");
691
+ if (colonIdx === -1) continue;
692
+ const key = pair.slice(0, colonIdx).trim();
693
+ const value = pair.slice(colonIdx + 1).trim();
694
+ switch (key) {
695
+ case "src":
696
+ attrs.src = value;
697
+ break;
698
+ case "var":
699
+ attrs.varName = value;
700
+ break;
701
+ case "alt":
702
+ attrs.alt = value;
703
+ break;
704
+ case "width":
705
+ attrs.width = value;
706
+ break;
707
+ case "height":
708
+ attrs.height = value;
709
+ break;
710
+ case "align":
711
+ attrs.align = value;
712
+ break;
713
+ }
714
+ }
715
+ return attrs;
716
+ }
850
717
  function parseInline(text) {
851
718
  text = preprocessMarkedTokens(text);
852
719
  const nodes = [];
@@ -870,6 +737,8 @@ function parseInline(text) {
870
737
  const node = { type: "variableNode", attrs };
871
738
  if (marks.length > 0) node.marks = marks;
872
739
  nodes.push(node);
740
+ } else if (match[1] === "image") {
741
+ nodes.push({ type: "imageNode", attrs: parseImageToken(match[2]) });
873
742
  }
874
743
  lastIndex = match.index + match[0].length;
875
744
  }
@@ -1183,10 +1052,24 @@ function parseBlocks(lines, startIdx, stopCondition) {
1183
1052
  i++;
1184
1053
  continue;
1185
1054
  }
1186
- content.push({
1187
- type: "paragraph",
1188
- content: parseInline(line)
1189
- });
1055
+ const imageMatch = line.trim().match(BLOCK_IMAGE_REGEX);
1056
+ if (imageMatch) {
1057
+ content.push({
1058
+ type: "imageNode",
1059
+ attrs: { src: imageMatch[2], varName: "", alt: imageMatch[1], width: "", height: "", align: "" }
1060
+ });
1061
+ i++;
1062
+ continue;
1063
+ }
1064
+ const inlineNodes = parseInline(line);
1065
+ const nonWhitespace = inlineNodes.filter(
1066
+ (n) => !(n.type === "text" && !(n.text || "").trim())
1067
+ );
1068
+ if (nonWhitespace.length === 1 && nonWhitespace[0].type === "imageNode") {
1069
+ content.push(nonWhitespace[0]);
1070
+ } else {
1071
+ content.push({ type: "paragraph", content: inlineNodes });
1072
+ }
1190
1073
  i++;
1191
1074
  }
1192
1075
  return { blocks: content, nextIdx: i };
@@ -1197,6 +1080,233 @@ function markdownToTiptap(markdown) {
1197
1080
  return { type: "doc", content: blocks };
1198
1081
  }
1199
1082
 
1083
+ // src/utils/variable-helpers.ts
1084
+ function extractVariablesFromContent(content) {
1085
+ const seen = /* @__PURE__ */ new Map();
1086
+ function walk(node) {
1087
+ if (node.type === "variableNode" && node.attrs) {
1088
+ const varName = node.attrs.varName;
1089
+ if (varName && !seen.has(varName)) {
1090
+ seen.set(varName, {
1091
+ varName,
1092
+ varLabel: node.attrs.varLabel || varName,
1093
+ varDefault: node.attrs.varDefault || ""
1094
+ });
1095
+ }
1096
+ }
1097
+ if (node.content) {
1098
+ node.content.forEach(walk);
1099
+ }
1100
+ }
1101
+ if (content.content) {
1102
+ content.content.forEach(walk);
1103
+ }
1104
+ return Array.from(seen.values());
1105
+ }
1106
+ function applyFormat(value, format) {
1107
+ if (format === "phone") {
1108
+ const digits = value.replace(/\D/g, "");
1109
+ if (digits.length === 10) {
1110
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
1111
+ }
1112
+ }
1113
+ return value;
1114
+ }
1115
+ function replaceVariablesInContent(content, values) {
1116
+ function walkNode(node) {
1117
+ if (node.type === "variableNode" && node.attrs) {
1118
+ const varName = node.attrs.varName;
1119
+ let replacement = values[varName] || node.attrs.varDefault || "";
1120
+ const format = node.attrs.format;
1121
+ if (format && replacement) {
1122
+ replacement = applyFormat(replacement, format);
1123
+ }
1124
+ const textNode = { type: "text", text: replacement };
1125
+ if (node.marks && node.marks.length > 0) {
1126
+ textNode.marks = node.marks;
1127
+ }
1128
+ return textNode;
1129
+ }
1130
+ if (node.type === "imageNode" && node.attrs?.varName) {
1131
+ const varName = node.attrs.varName;
1132
+ const resolved = values[varName] || node.attrs.src || "";
1133
+ return { ...node, attrs: { ...node.attrs, src: resolved } };
1134
+ }
1135
+ if (node.content) {
1136
+ return { ...node, content: node.content.map(walkNode) };
1137
+ }
1138
+ return node;
1139
+ }
1140
+ return walkNode(content);
1141
+ }
1142
+ function expandBlockVariables(content, values) {
1143
+ function soleVariableOf(node) {
1144
+ if (node.type !== "paragraph" || !node.content) return null;
1145
+ const meaningful = node.content.filter(
1146
+ (c) => !(c.type === "text" && !(c.text || "").trim())
1147
+ );
1148
+ if (meaningful.length === 1 && meaningful[0].type === "variableNode") {
1149
+ return meaningful[0];
1150
+ }
1151
+ return null;
1152
+ }
1153
+ function stripUnsafeNodes(node) {
1154
+ if (!node.content) return node;
1155
+ return {
1156
+ ...node,
1157
+ content: node.content.filter((c) => c.type !== "fieldNode" && c.type !== "watermark").map(stripUnsafeNodes)
1158
+ };
1159
+ }
1160
+ function isCellSafe(blocks) {
1161
+ return blocks.every((b) => b.type === "paragraph" || b.type === "heading");
1162
+ }
1163
+ function walkNode(node, inCell) {
1164
+ if (!node.content) return node;
1165
+ const childInCell = inCell || node.type === "tableCell" || node.type === "tableHeader";
1166
+ const newContent = [];
1167
+ for (const child of node.content) {
1168
+ const varNode = soleVariableOf(child);
1169
+ if (varNode?.attrs) {
1170
+ const varName = varNode.attrs.varName;
1171
+ const value = values[varName] || varNode.attrs.varDefault || "";
1172
+ const isBlock = varNode.attrs.block === "true" || value.includes("\n");
1173
+ if (isBlock) {
1174
+ const parsed = stripUnsafeNodes(markdownToTiptap(value));
1175
+ const blocks = parsed.content || [];
1176
+ if (!childInCell || isCellSafe(blocks)) {
1177
+ newContent.push(...blocks);
1178
+ continue;
1179
+ }
1180
+ }
1181
+ }
1182
+ newContent.push(walkNode(child, childInCell));
1183
+ }
1184
+ return { ...node, content: newContent };
1185
+ }
1186
+ return walkNode(content, false);
1187
+ }
1188
+ function labelToVarName(label) {
1189
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1190
+ }
1191
+ function isZeroLike(value) {
1192
+ if (!value || value.trim() === "") return true;
1193
+ const cleaned = value.replace(/[$,\s]/g, "");
1194
+ return /^-?0+(\.0+)?$/.test(cleaned);
1195
+ }
1196
+ function suppressZeroContent(content, values) {
1197
+ function hasSuppressedVar(node) {
1198
+ if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
1199
+ const varName = node.attrs.varName;
1200
+ const val = values[varName] || node.attrs.varDefault || "";
1201
+ return isZeroLike(val);
1202
+ }
1203
+ return (node.content || []).some(hasSuppressedVar);
1204
+ }
1205
+ function walkNode(node) {
1206
+ if (node.type === "table" && node.content) {
1207
+ const filtered = node.content.filter((row) => {
1208
+ if (row.type !== "tableRow") return true;
1209
+ const isHeader = row.content?.[0]?.type === "tableHeader";
1210
+ if (isHeader) return true;
1211
+ return !hasSuppressedVar(row);
1212
+ });
1213
+ const bodyRows = filtered.filter(
1214
+ (r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
1215
+ );
1216
+ if (bodyRows.length === 0) return null;
1217
+ return { ...node, content: filtered };
1218
+ }
1219
+ if (node.type === "paragraph" && hasSuppressedVar(node)) {
1220
+ return null;
1221
+ }
1222
+ if (node.content) {
1223
+ const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
1224
+ return { ...node, content: filtered };
1225
+ }
1226
+ return node;
1227
+ }
1228
+ return walkNode(content) || content;
1229
+ }
1230
+ function expandRepeatContent(content, values) {
1231
+ const enrichedValues = { ...values };
1232
+ function cloneNode(node, dataKey, index) {
1233
+ if (node.type === "variableNode" && node.attrs?.varName) {
1234
+ const oldName = node.attrs.varName;
1235
+ const bareName = oldName.replace(/^operation_/, "");
1236
+ const newName = `${dataKey}_${index}_${bareName}`;
1237
+ return { ...node, attrs: { ...node.attrs, varName: newName } };
1238
+ }
1239
+ if (node.content) {
1240
+ return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
1241
+ }
1242
+ return { ...node };
1243
+ }
1244
+ function walkNode(node) {
1245
+ if (node.type === "repeatBlock" && node.attrs?.data) {
1246
+ const dataKey = node.attrs.data;
1247
+ const arrayJson = values[dataKey];
1248
+ if (!arrayJson) return [];
1249
+ let items;
1250
+ try {
1251
+ items = JSON.parse(arrayJson);
1252
+ } catch {
1253
+ return [];
1254
+ }
1255
+ const sums = /* @__PURE__ */ new Map();
1256
+ for (const item of items) {
1257
+ for (const [key, val] of Object.entries(item)) {
1258
+ const num = parseFloat(val);
1259
+ if (!isNaN(num)) {
1260
+ sums.set(key, (sums.get(key) || 0) + num);
1261
+ }
1262
+ }
1263
+ }
1264
+ const woKeysByNorm = /* @__PURE__ */ new Map();
1265
+ for (const k of Object.keys(enrichedValues)) {
1266
+ if (k.startsWith("workorder_")) {
1267
+ const norm = k.replace(/_/g, "");
1268
+ woKeysByNorm.set(norm, k);
1269
+ }
1270
+ }
1271
+ for (const [field, total] of sums) {
1272
+ const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
1273
+ const directKey = `workorder_${field}`;
1274
+ enrichedValues[directKey] = formatted;
1275
+ const norm = `workorder${field}`.replace(/_/g, "");
1276
+ const existing = woKeysByNorm.get(norm);
1277
+ if (existing && existing !== directKey) {
1278
+ enrichedValues[existing] = formatted;
1279
+ }
1280
+ }
1281
+ const expanded = [];
1282
+ for (let i = 0; i < items.length; i++) {
1283
+ const item = items[i];
1284
+ for (const [key, val] of Object.entries(item)) {
1285
+ enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
1286
+ }
1287
+ const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
1288
+ expanded.push(...cloned);
1289
+ }
1290
+ return expanded;
1291
+ }
1292
+ if (node.content) {
1293
+ const newContent = [];
1294
+ for (const child of node.content) {
1295
+ const result2 = walkNode(child);
1296
+ if (Array.isArray(result2)) newContent.push(...result2);
1297
+ else newContent.push(result2);
1298
+ }
1299
+ return { ...node, content: newContent };
1300
+ }
1301
+ return node;
1302
+ }
1303
+ const result = walkNode(content);
1304
+ return {
1305
+ content: Array.isArray(result) ? { ...content, content: result } : result,
1306
+ values: enrichedValues
1307
+ };
1308
+ }
1309
+
1200
1310
  // src/utils/error-helpers.ts
1201
1311
  function getErrorMessage(error) {
1202
1312
  if (error instanceof Error) return error.message;
@@ -1337,6 +1447,34 @@ var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
1337
1447
  var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
1338
1448
  var LINE_HEIGHT_FACTOR = 1.4;
1339
1449
  var BODY_FONT_SIZE = 10;
1450
+ function hexToRgbColor(hex) {
1451
+ if (!hex) return null;
1452
+ const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
1453
+ if (!m) return null;
1454
+ const n = parseInt(m[1], 16);
1455
+ return rgb((n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255);
1456
+ }
1457
+ function contrastTextColor(fill) {
1458
+ const c = fill;
1459
+ const luminance = 0.299 * c.red + 0.587 * c.green + 0.114 * c.blue;
1460
+ return luminance > 0.6 ? rgb(0, 0, 0) : rgb(1, 1, 1);
1461
+ }
1462
+ function resolveTheme(theme) {
1463
+ const primary = hexToRgbColor(theme?.primaryColor);
1464
+ const accent = hexToRgbColor(theme?.accentColor);
1465
+ const border = hexToRgbColor(theme?.borderColor);
1466
+ return {
1467
+ tableHeaderBg: primary ?? rgb(0.3, 0.3, 0.3),
1468
+ tableHeaderText: primary ? contrastTextColor(primary) : rgb(1, 1, 1),
1469
+ tableBorder: border ?? rgb(0.75, 0.75, 0.75),
1470
+ panelDarkHeaderBg: primary ?? rgb(0.25, 0.25, 0.25),
1471
+ panelDarkHeaderText: primary ? contrastTextColor(primary) : rgb(1, 1, 1),
1472
+ panelBorder: border ?? rgb(0.6, 0.6, 0.6),
1473
+ rule: primary ?? rgb(0, 0, 0),
1474
+ pageHeaderText: primary ?? rgb(0, 0, 0),
1475
+ pageHeaderRule: accent ?? primary ?? rgb(0.75, 0.75, 0.75)
1476
+ };
1477
+ }
1340
1478
  var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
1341
1479
  var FIELD_DISPLAY = {
1342
1480
  ["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
@@ -1368,6 +1506,53 @@ function getFieldDimensions(fieldType, attrs, font, fontSize) {
1368
1506
  const height = textLineHeight + 4;
1369
1507
  return { width, height };
1370
1508
  }
1509
+ async function loadImageBytes(src) {
1510
+ if (src.startsWith("data:")) {
1511
+ const comma = src.indexOf(",");
1512
+ if (comma === -1) throw new Error("malformed data URL");
1513
+ const meta = src.slice(0, comma);
1514
+ if (!/;base64$/i.test(meta)) throw new Error("data URL must be base64-encoded");
1515
+ const bin = atob(src.slice(comma + 1));
1516
+ const bytes = new Uint8Array(bin.length);
1517
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1518
+ return bytes;
1519
+ }
1520
+ const res = await fetch(src);
1521
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1522
+ return new Uint8Array(await res.arrayBuffer());
1523
+ }
1524
+ function describeImageSrc(src) {
1525
+ return src.length > 80 ? `${src.slice(0, 77)}...` : src;
1526
+ }
1527
+ async function preloadImages(pdfDoc, content) {
1528
+ const srcs = /* @__PURE__ */ new Set();
1529
+ (function walk(node) {
1530
+ if (node.type === "imageNode") {
1531
+ const src = node.attrs?.src || "";
1532
+ if (src) srcs.add(src);
1533
+ }
1534
+ for (const child of node.content || []) walk(child);
1535
+ })(content);
1536
+ const images = /* @__PURE__ */ new Map();
1537
+ const warnings = [];
1538
+ for (const src of srcs) {
1539
+ try {
1540
+ const bytes = await loadImageBytes(src);
1541
+ const isPng = bytes.length > 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
1542
+ const isJpeg = bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216;
1543
+ if (isPng) {
1544
+ images.set(src, await pdfDoc.embedPng(bytes));
1545
+ } else if (isJpeg) {
1546
+ images.set(src, await pdfDoc.embedJpg(bytes));
1547
+ } else {
1548
+ warnings.push(`Image skipped (unsupported format \u2014 only PNG/JPEG embed): ${describeImageSrc(src)}`);
1549
+ }
1550
+ } catch (err) {
1551
+ warnings.push(`Image skipped (${getErrorMessage(err)}): ${describeImageSrc(src)}`);
1552
+ }
1553
+ }
1554
+ return { images, warnings };
1555
+ }
1371
1556
  function ensureSpace(state, neededHeight) {
1372
1557
  if (state.y + neededHeight > CONTENT_HEIGHT) {
1373
1558
  const newPage = state.pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
@@ -1715,9 +1900,9 @@ function renderTable(state, node, region = DEFAULT_REGION) {
1715
1900
  const cellPadding = borderless ? 2 : 4;
1716
1901
  const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1717
1902
  const rowPadding = cellPadding * 2;
1718
- const borderColor = rgb(0.75, 0.75, 0.75);
1719
- const headerBg = rgb(0.3, 0.3, 0.3);
1720
- const headerTextColor = rgb(1, 1, 1);
1903
+ const borderColor = state.theme.tableBorder;
1904
+ const headerBg = state.theme.tableHeaderBg;
1905
+ const headerTextColor = state.theme.tableHeaderText;
1721
1906
  const firstRow = tableRows[0];
1722
1907
  const colCount = firstRow.content?.length || 0;
1723
1908
  if (colCount === 0) return;
@@ -1965,7 +2150,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
1965
2150
  start: { x: region.leftX, y: ruleY },
1966
2151
  end: { x: region.leftX + region.width, y: ruleY },
1967
2152
  thickness: 2.5,
1968
- color: rgb(0, 0, 0)
2153
+ color: state.theme.rule
1969
2154
  });
1970
2155
  state.y += 4;
1971
2156
  break;
@@ -1998,6 +2183,39 @@ function processBlock(state, node, region = DEFAULT_REGION) {
1998
2183
  renderTable(state, node, region);
1999
2184
  break;
2000
2185
  }
2186
+ case "imageNode": {
2187
+ const src = node.attrs?.src || "";
2188
+ const image = src ? state.images.get(src) : void 0;
2189
+ if (!image) break;
2190
+ const natW = image.width;
2191
+ const natH = image.height;
2192
+ let w = parseFloat(node.attrs?.width || "") || 0;
2193
+ let h = parseFloat(node.attrs?.height || "") || 0;
2194
+ if (w && !h) h = natH / natW * w;
2195
+ else if (h && !w) w = natW / natH * h;
2196
+ else if (!w && !h) {
2197
+ w = natW;
2198
+ h = natH;
2199
+ }
2200
+ if (w > region.width) {
2201
+ h = h * (region.width / w);
2202
+ w = region.width;
2203
+ }
2204
+ const maxH = CONTENT_HEIGHT * 0.6;
2205
+ if (h > maxH) {
2206
+ w = w * (maxH / h);
2207
+ h = maxH;
2208
+ }
2209
+ ensureSpace(state, h + 4);
2210
+ const align = node.attrs?.align || "left";
2211
+ let x = region.leftX;
2212
+ if (align === "center") x = region.leftX + (region.width - w) / 2;
2213
+ else if (align === "right") x = region.leftX + region.width - w;
2214
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - h;
2215
+ state.currentPage.drawImage(image, { x, y: pdfY, width: w, height: h });
2216
+ state.y += h + 4;
2217
+ break;
2218
+ }
2001
2219
  // ─── Layout directives ─────────────────────────────────────────
2002
2220
  case "panel": {
2003
2221
  const title = node.attrs?.title || "";
@@ -2034,8 +2252,8 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2034
2252
  y: titleBarY,
2035
2253
  width: region.width,
2036
2254
  height: titleHeight,
2037
- color: isDarkHeader ? rgb(0.25, 0.25, 0.25) : rgb(0.93, 0.93, 0.93),
2038
- borderColor: rgb(0.6, 0.6, 0.6),
2255
+ color: isDarkHeader ? state.theme.panelDarkHeaderBg : rgb(0.93, 0.93, 0.93),
2256
+ borderColor: state.theme.panelBorder,
2039
2257
  borderWidth: 0.75
2040
2258
  });
2041
2259
  startPage.drawText(title, {
@@ -2043,7 +2261,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2043
2261
  y: titleBarY + 4,
2044
2262
  size: titleFontSize,
2045
2263
  font: state.fonts.bold,
2046
- color: isDarkHeader ? rgb(1, 1, 1) : rgb(0, 0, 0)
2264
+ color: isDarkHeader ? state.theme.panelDarkHeaderText : rgb(0, 0, 0)
2047
2265
  });
2048
2266
  }
2049
2267
  if (border !== "none") {
@@ -2056,7 +2274,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2056
2274
  y: borderY,
2057
2275
  width: region.width,
2058
2276
  height: panelHeight,
2059
- borderColor: rgb(0.6, 0.6, 0.6),
2277
+ borderColor: state.theme.panelBorder,
2060
2278
  borderWidth: 0.75,
2061
2279
  borderDashArray
2062
2280
  });
@@ -2068,7 +2286,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2068
2286
  y: startBorderBottom,
2069
2287
  width: region.width,
2070
2288
  height: startPanelHeight,
2071
- borderColor: rgb(0.6, 0.6, 0.6),
2289
+ borderColor: state.theme.panelBorder,
2072
2290
  borderWidth: 0.75,
2073
2291
  borderDashArray
2074
2292
  });
@@ -2080,7 +2298,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2080
2298
  y: contBorderY,
2081
2299
  width: region.width,
2082
2300
  height: contHeight,
2083
- borderColor: rgb(0.6, 0.6, 0.6),
2301
+ borderColor: state.theme.panelBorder,
2084
2302
  borderWidth: 0.75,
2085
2303
  borderDashArray
2086
2304
  });
@@ -2384,6 +2602,24 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
2384
2602
  PDFString$1.of(field.label)
2385
2603
  );
2386
2604
  }
2605
+ const initialsFontSize = Math.min(
2606
+ 72,
2607
+ Math.max(8, Math.round(position.height * 0.55))
2608
+ );
2609
+ try {
2610
+ initialsField.setFontSize(initialsFontSize);
2611
+ const acro = initialsField;
2612
+ const daRef = acro.acroField.dict.get(PDFName$1.of("DA"));
2613
+ if (daRef) {
2614
+ const daStr = daRef.value;
2615
+ if (typeof daStr === "string") {
2616
+ const newDa = daStr.replace(/\/Helv\b/, "/TiBo").replace(/\/HeBo\b/, "/TiBo");
2617
+ acro.acroField.dict.set(PDFName$1.of("DA"), PDFString$1.of(newDa));
2618
+ }
2619
+ }
2620
+ } catch (fontError) {
2621
+ console.warn(`Failed to set initials font:`, fontError);
2622
+ }
2387
2623
  initialsField.enableReadOnly();
2388
2624
  if (field.required) initialsField.enableRequired();
2389
2625
  break;
@@ -2494,7 +2730,8 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
2494
2730
  return warnings;
2495
2731
  }
2496
2732
  async function generatePdfFromContent(content, options = {}) {
2497
- const { drawFieldPlaceholders = false, embedFormFields = false, fields = [] } = options;
2733
+ const { drawFieldPlaceholders = false, embedFormFields = false, fields = [], header, footer } = options;
2734
+ const theme = resolveTheme(options.theme);
2498
2735
  const pdfDoc = await PDFDocument.create();
2499
2736
  const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
2500
2737
  const fonts = {
@@ -2504,6 +2741,7 @@ async function generatePdfFromContent(content, options = {}) {
2504
2741
  boldItalic: await pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique)
2505
2742
  };
2506
2743
  const watermarkNodes = collectWatermarkNodes(content);
2744
+ const { images, warnings: imageWarnings } = await preloadImages(pdfDoc, content);
2507
2745
  const state = {
2508
2746
  currentPage: firstPage,
2509
2747
  pageIndex: 0,
@@ -2512,7 +2750,9 @@ async function generatePdfFromContent(content, options = {}) {
2512
2750
  pdfDoc,
2513
2751
  fieldPositions: /* @__PURE__ */ new Map(),
2514
2752
  drawFieldPlaceholders,
2515
- watermarkNodes
2753
+ watermarkNodes,
2754
+ theme,
2755
+ images
2516
2756
  };
2517
2757
  renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
2518
2758
  if (content.content) {
@@ -2523,6 +2763,8 @@ async function generatePdfFromContent(content, options = {}) {
2523
2763
  const allPages = pdfDoc.getPages();
2524
2764
  const totalPages = allPages.length;
2525
2765
  const pageNumFontSize = 9;
2766
+ const hfFontSize = 8;
2767
+ const hfColor = rgb(0.45, 0.45, 0.45);
2526
2768
  for (let i = 0; i < totalPages; i++) {
2527
2769
  const page = allPages[i];
2528
2770
  const label = `Page ${i + 1} of ${totalPages}`;
@@ -2534,6 +2776,43 @@ async function generatePdfFromContent(content, options = {}) {
2534
2776
  font: fonts.bold,
2535
2777
  color: rgb(0, 0, 0)
2536
2778
  });
2779
+ if (header && (header.left || header.right) && !(header.skipFirstPage && i === 0)) {
2780
+ const headerBaseY = PAGE_HEIGHT - MARGIN + 10;
2781
+ if (header.left) {
2782
+ page.drawText(header.left, {
2783
+ x: MARGIN,
2784
+ y: headerBaseY,
2785
+ size: pageNumFontSize,
2786
+ font: fonts.bold,
2787
+ color: theme.pageHeaderText
2788
+ });
2789
+ }
2790
+ if (header.right) {
2791
+ const rightWidth = fonts.regular.widthOfTextAtSize(header.right, hfFontSize);
2792
+ page.drawText(header.right, {
2793
+ x: PAGE_WIDTH - MARGIN - rightWidth,
2794
+ y: headerBaseY,
2795
+ size: hfFontSize,
2796
+ font: fonts.regular,
2797
+ color: hfColor
2798
+ });
2799
+ }
2800
+ page.drawLine({
2801
+ start: { x: MARGIN, y: headerBaseY - 6 },
2802
+ end: { x: PAGE_WIDTH - MARGIN, y: headerBaseY - 6 },
2803
+ thickness: 0.75,
2804
+ color: theme.pageHeaderRule
2805
+ });
2806
+ }
2807
+ if (footer?.left) {
2808
+ page.drawText(footer.left, {
2809
+ x: MARGIN,
2810
+ y: MARGIN / 2 - pageNumFontSize / 2,
2811
+ size: hfFontSize,
2812
+ font: fonts.regular,
2813
+ color: hfColor
2814
+ });
2815
+ }
2537
2816
  }
2538
2817
  let fieldWarnings;
2539
2818
  if (embedFormFields && fields.length > 0) {
@@ -2544,25 +2823,31 @@ async function generatePdfFromContent(content, options = {}) {
2544
2823
  return {
2545
2824
  pdfBytes,
2546
2825
  fieldPositions: state.fieldPositions,
2547
- fieldWarnings
2826
+ fieldWarnings,
2827
+ imageWarnings: imageWarnings.length > 0 ? imageWarnings : void 0
2548
2828
  };
2549
2829
  }
2550
2830
 
2551
2831
  // src/utils/template-pipeline.ts
2552
- async function generatePdfFromTiptap(content, values) {
2832
+ async function generatePdfFromTiptap(content, values, options = {}) {
2553
2833
  const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
2554
- const suppressed = suppressZeroContent(expanded, enrichedValues);
2834
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
2835
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
2555
2836
  const replaced = replaceVariablesInContent(suppressed, enrichedValues);
2556
2837
  const fields = extractFieldsFromContent(replaced);
2557
2838
  const result = await generatePdfFromContent(replaced, {
2558
2839
  embedFormFields: fields.length > 0,
2559
- fields
2840
+ fields,
2841
+ theme: options.theme,
2842
+ header: options.header,
2843
+ footer: options.footer,
2844
+ drawFieldPlaceholders: options.drawFieldPlaceholders
2560
2845
  });
2561
- return { pdfBytes: result.pdfBytes };
2846
+ return { pdfBytes: result.pdfBytes, imageWarnings: result.imageWarnings };
2562
2847
  }
2563
- async function generatePdfFromMarkdown(markdown, values) {
2848
+ async function generatePdfFromMarkdown(markdown, values, options = {}) {
2564
2849
  const content = markdownToTiptap(markdown);
2565
- return generatePdfFromTiptap(content, values);
2850
+ return generatePdfFromTiptap(content, values, options);
2566
2851
  }
2567
2852
 
2568
2853
  // src/utils/markdown-writer.ts
@@ -2582,6 +2867,20 @@ function serializeFieldToken(attrs) {
2582
2867
  if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2583
2868
  return `{{${parts.join("|")}}}`;
2584
2869
  }
2870
+ function serializeImageNode(attrs) {
2871
+ const hasExtras = attrs.varName || attrs.width || attrs.height || attrs.align;
2872
+ if (!hasExtras && attrs.src) {
2873
+ return `![${attrs.alt || ""}](${attrs.src})`;
2874
+ }
2875
+ const parts = ["image"];
2876
+ if (attrs.varName) parts.push(`var:${attrs.varName}`);
2877
+ else if (attrs.src) parts.push(`src:${attrs.src}`);
2878
+ if (attrs.alt) parts.push(`alt:${attrs.alt}`);
2879
+ if (attrs.width) parts.push(`width:${attrs.width}`);
2880
+ if (attrs.height) parts.push(`height:${attrs.height}`);
2881
+ if (attrs.align) parts.push(`align:${attrs.align}`);
2882
+ return `{{${parts.join("|")}}}`;
2883
+ }
2585
2884
  function serializeVariableToken(attrs) {
2586
2885
  const parts = ["var"];
2587
2886
  if (attrs.varName) parts.push(`name:${attrs.varName}`);
@@ -2589,6 +2888,7 @@ function serializeVariableToken(attrs) {
2589
2888
  if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
2590
2889
  if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
2591
2890
  if (attrs.format) parts.push(`format:${attrs.format}`);
2891
+ if (attrs.block === "true") parts.push(`block:true`);
2592
2892
  return `{{${parts.join("|")}}}`;
2593
2893
  }
2594
2894
  function serializeInline(content) {
@@ -2599,6 +2899,10 @@ function serializeInline(content) {
2599
2899
  result += serializeFieldToken(node.attrs || {});
2600
2900
  continue;
2601
2901
  }
2902
+ if (node.type === "imageNode") {
2903
+ result += serializeImageNode(node.attrs || {});
2904
+ continue;
2905
+ }
2602
2906
  if (node.type === "variableNode") {
2603
2907
  let token = serializeVariableToken(node.attrs || {});
2604
2908
  const varMarks = node.marks || [];
@@ -2705,6 +3009,9 @@ ${text}
2705
3009
  case "tableCell": {
2706
3010
  return (node.content || []).map(serializeBlock).join("");
2707
3011
  }
3012
+ case "imageNode": {
3013
+ return serializeImageNode(node.attrs || {});
3014
+ }
2708
3015
  case "watermark": {
2709
3016
  const attrs = node.attrs || {};
2710
3017
  const parts = [];
@@ -4768,8 +5075,8 @@ function validateMarkdown(markdown) {
4768
5075
  errors.push(`Malformed token at line ${lineNum}: missing | delimiter`);
4769
5076
  } else {
4770
5077
  const tokenType = tokenContent.slice(0, pipeIdx);
4771
- if (tokenType !== "field" && tokenType !== "var") {
4772
- errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field" or "var")`);
5078
+ if (tokenType !== "field" && tokenType !== "var" && tokenType !== "image") {
5079
+ errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field", "var" or "image")`);
4773
5080
  } else {
4774
5081
  const body = tokenContent.slice(pipeIdx + 1);
4775
5082
  const pairs = body.split("|");
@@ -4783,6 +5090,11 @@ function validateMarkdown(markdown) {
4783
5090
  if (!hasName) {
4784
5091
  errors.push(`Variable token at line ${lineNum} is missing required "name" attribute`);
4785
5092
  }
5093
+ } else if (tokenType === "image") {
5094
+ const hasSource = pairs.some((p) => p.startsWith("src:") || p.startsWith("var:"));
5095
+ if (!hasSource) {
5096
+ errors.push(`Image token at line ${lineNum} needs a "src" or "var" attribute`);
5097
+ }
4786
5098
  }
4787
5099
  }
4788
5100
  }
@@ -5124,7 +5436,8 @@ function GeneratePanel({
5124
5436
  try {
5125
5437
  const content = getActiveContent();
5126
5438
  const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5127
- const suppressed = suppressZeroContent(expanded, enrichedValues);
5439
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5440
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5128
5441
  const replaced = replaceVariablesInContent(suppressed, enrichedValues);
5129
5442
  const fields = extractFieldsFromContent(replaced);
5130
5443
  const result = await generatePdfFromContent(replaced);
@@ -5147,7 +5460,8 @@ function GeneratePanel({
5147
5460
  try {
5148
5461
  const content = getActiveContent();
5149
5462
  const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5150
- const suppressed = suppressZeroContent(expanded, enrichedValues);
5463
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5464
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5151
5465
  const replaced = replaceVariablesInContent(suppressed, enrichedValues);
5152
5466
  const fields = extractFieldsFromContent(replaced);
5153
5467
  const result = await generatePdfFromContent(replaced, {
@@ -5229,7 +5543,8 @@ function GeneratePanel({
5229
5543
  for (let i = 0; i < bulkData.length; i++) {
5230
5544
  const values = bulkData[i];
5231
5545
  const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5232
- const suppressed = suppressZeroContent(expanded, enrichedValues);
5546
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5547
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5233
5548
  const replaced = replaceVariablesInContent(suppressed, enrichedValues);
5234
5549
  const fields = extractFieldsFromContent(replaced);
5235
5550
  const result = await generatePdfFromContent(replaced, {
@@ -7111,6 +7426,6 @@ function parseXmlTemplate(xmlString) {
7111
7426
  };
7112
7427
  }
7113
7428
 
7114
- export { DocumentGenerator, EditorPanel, FormFieldType, PreviewPanel, RepeatNode, SubtotalsNode, ThemeProvider, expandRepeatContent, extractVariablesFromContent, generatePdfFromMarkdown, generatePdfFromTiptap, isZeroLike, markdownToTiptap, parseXmlTemplate, suppressZeroContent, tiptapToMarkdown, useDocumentGenerator, useTheme };
7429
+ export { DocumentGenerator, EditorPanel, FormFieldType, PreviewPanel, RepeatNode, SubtotalsNode, ThemeProvider, expandBlockVariables, expandRepeatContent, extractVariablesFromContent, generatePdfFromMarkdown, generatePdfFromTiptap, isZeroLike, markdownToTiptap, parseXmlTemplate, suppressZeroContent, tiptapToMarkdown, useDocumentGenerator, useTheme };
7115
7430
  //# sourceMappingURL=index.mjs.map
7116
7431
  //# sourceMappingURL=index.mjs.map