@signiphi/pdf-compose 0.1.0-beta.2 → 0.1.0-beta.3

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
@@ -7,6 +7,10 @@ var StarterKit = require('@tiptap/starter-kit');
7
7
  var Underline = require('@tiptap/extension-underline');
8
8
  var TextAlign = require('@tiptap/extension-text-align');
9
9
  var Placeholder = require('@tiptap/extension-placeholder');
10
+ var Table = require('@tiptap/extension-table');
11
+ var TableRow = require('@tiptap/extension-table-row');
12
+ var TableCell = require('@tiptap/extension-table-cell');
13
+ var TableHeader = require('@tiptap/extension-table-header');
10
14
  var core = require('@tiptap/core');
11
15
  var clsx = require('clsx');
12
16
  var tailwindMerge = require('tailwind-merge');
@@ -45,13 +49,17 @@ var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
45
49
  var Underline__default = /*#__PURE__*/_interopDefault(Underline);
46
50
  var TextAlign__default = /*#__PURE__*/_interopDefault(TextAlign);
47
51
  var Placeholder__default = /*#__PURE__*/_interopDefault(Placeholder);
52
+ var Table__default = /*#__PURE__*/_interopDefault(Table);
53
+ var TableRow__default = /*#__PURE__*/_interopDefault(TableRow);
54
+ var TableCell__default = /*#__PURE__*/_interopDefault(TableCell);
55
+ var TableHeader__default = /*#__PURE__*/_interopDefault(TableHeader);
48
56
  var TooltipPrimitive__namespace = /*#__PURE__*/_interopNamespace(TooltipPrimitive);
49
57
  var PopoverPrimitive__namespace = /*#__PURE__*/_interopNamespace(PopoverPrimitive);
50
58
  var LabelPrimitive__namespace = /*#__PURE__*/_interopNamespace(LabelPrimitive);
51
59
 
52
60
  var __defProp = Object.defineProperty;
53
61
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
54
- var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
62
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
55
63
  function cn(...inputs) {
56
64
  return tailwindMerge.twMerge(clsx.clsx(inputs));
57
65
  }
@@ -243,7 +251,9 @@ var VariableNode = core.Node.create({
243
251
  return {
244
252
  varName: { default: "" },
245
253
  varLabel: { default: "" },
246
- varDefault: { default: "" }
254
+ varDefault: { default: "" },
255
+ format: { default: "" },
256
+ suppressZero: { default: "" }
247
257
  };
248
258
  },
249
259
  parseHTML() {
@@ -309,6 +319,169 @@ var VariableNode = core.Node.create({
309
319
  };
310
320
  }
311
321
  });
322
+ var PanelNode = core.Node.create({
323
+ name: "panel",
324
+ group: "block",
325
+ content: "block+",
326
+ defining: true,
327
+ addAttributes() {
328
+ return {
329
+ title: { default: "" },
330
+ border: { default: "solid" },
331
+ headerStyle: { default: "" }
332
+ };
333
+ },
334
+ parseHTML() {
335
+ return [{
336
+ tag: "div[data-panel-node]",
337
+ contentElement: "div.panel-content"
338
+ }];
339
+ },
340
+ renderHTML({ node, HTMLAttributes }) {
341
+ const { title, border, headerStyle } = node.attrs;
342
+ const classes = ["panel-node"];
343
+ if (border === "dashed") classes.push("panel-border-dashed");
344
+ else if (border === "none") classes.push("panel-border-none");
345
+ else classes.push("panel-border-solid");
346
+ if (headerStyle === "dark") classes.push("panel-dark");
347
+ const attrs = core.mergeAttributes(
348
+ { "data-panel-node": "", class: classes.join(" ") },
349
+ HTMLAttributes
350
+ );
351
+ if (title) {
352
+ return [
353
+ "div",
354
+ attrs,
355
+ ["div", { class: "panel-title" }, title],
356
+ ["div", { class: "panel-content" }, 0]
357
+ ];
358
+ }
359
+ return ["div", attrs, ["div", { class: "panel-content" }, 0]];
360
+ }
361
+ });
362
+ var ColumnsNode = core.Node.create({
363
+ name: "columns",
364
+ group: "block",
365
+ content: "column+",
366
+ defining: true,
367
+ addAttributes() {
368
+ return {
369
+ split: { default: "50" },
370
+ padX: { default: "0" }
371
+ };
372
+ },
373
+ parseHTML() {
374
+ return [{ tag: "div[data-columns-node]" }];
375
+ },
376
+ renderHTML({ node, HTMLAttributes }) {
377
+ const split = node.attrs.split || "50";
378
+ return ["div", core.mergeAttributes(
379
+ {
380
+ "data-columns-node": "",
381
+ class: "columns-node",
382
+ style: `--xpc-col-split: ${split}`
383
+ },
384
+ HTMLAttributes
385
+ ), 0];
386
+ }
387
+ });
388
+ var ColumnNode = core.Node.create({
389
+ name: "column",
390
+ group: "",
391
+ content: "block+",
392
+ defining: true,
393
+ addAttributes() {
394
+ return {
395
+ padTop: { default: 0 }
396
+ };
397
+ },
398
+ parseHTML() {
399
+ return [{ tag: "div[data-column-node]" }];
400
+ },
401
+ renderHTML({ HTMLAttributes }) {
402
+ return ["div", core.mergeAttributes(
403
+ { "data-column-node": "", class: "column-node" },
404
+ HTMLAttributes
405
+ ), 0];
406
+ }
407
+ });
408
+ function WatermarkNodeView({ node, selected }) {
409
+ const { text } = node.attrs;
410
+ return /* @__PURE__ */ jsxRuntime.jsx(react.NodeViewWrapper, { children: /* @__PURE__ */ jsxRuntime.jsxs(
411
+ "div",
412
+ {
413
+ "data-watermark-node": "",
414
+ className: cn(
415
+ "inline-flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm my-1",
416
+ "bg-blue-50 text-blue-600 border-blue-200",
417
+ selected && "ring-2 ring-primary ring-offset-1"
418
+ ),
419
+ children: [
420
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Droplets, { size: 14 }),
421
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
422
+ "Watermark: ",
423
+ text || "(empty)"
424
+ ] })
425
+ ]
426
+ }
427
+ ) });
428
+ }
429
+
430
+ // src/extensions/watermark-node.ts
431
+ var WatermarkNode = core.Node.create({
432
+ name: "watermark",
433
+ group: "block",
434
+ atom: true,
435
+ selectable: true,
436
+ draggable: false,
437
+ addAttributes() {
438
+ return {
439
+ text: { default: "" },
440
+ opacity: { default: "0.15" },
441
+ angle: { default: "-45" }
442
+ };
443
+ },
444
+ parseHTML() {
445
+ return [{ tag: "div[data-watermark-node]" }];
446
+ },
447
+ renderHTML({ HTMLAttributes }) {
448
+ return ["div", core.mergeAttributes({ "data-watermark-node": "" }, HTMLAttributes)];
449
+ },
450
+ addNodeView() {
451
+ return react.ReactNodeViewRenderer(WatermarkNodeView);
452
+ }
453
+ });
454
+ var RepeatNode = core.Node.create({
455
+ name: "repeatBlock",
456
+ group: "block",
457
+ content: "block+",
458
+ addAttributes() {
459
+ return {
460
+ data: { default: "" }
461
+ };
462
+ },
463
+ parseHTML() {
464
+ return [{ tag: "div[data-repeat-node]" }];
465
+ },
466
+ renderHTML({ HTMLAttributes }) {
467
+ return ["div", core.mergeAttributes({ "data-repeat-node": "" }, HTMLAttributes), 0];
468
+ }
469
+ });
470
+ var SubtotalsNode = core.Node.create({
471
+ name: "subtotalsBlock",
472
+ group: "block",
473
+ content: "block+",
474
+ defining: true,
475
+ parseHTML() {
476
+ return [{ tag: "div[data-subtotals-node]" }];
477
+ },
478
+ renderHTML({ HTMLAttributes }) {
479
+ return ["div", core.mergeAttributes(
480
+ { "data-subtotals-node": "", class: "subtotals-block" },
481
+ HTMLAttributes
482
+ ), 0];
483
+ }
484
+ });
312
485
 
313
486
  // src/types/index.ts
314
487
  var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
@@ -411,11 +584,24 @@ function extractVariablesFromContent(content) {
411
584
  }
412
585
  return Array.from(seen.values());
413
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
+ }
414
596
  function replaceVariablesInContent(content, values) {
415
597
  function walkNode(node) {
416
598
  if (node.type === "variableNode" && node.attrs) {
417
599
  const varName = node.attrs.varName;
418
- const replacement = values[varName] || node.attrs.varDefault || `[${node.attrs.varLabel || 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
+ }
419
605
  const textNode = { type: "text", text: replacement };
420
606
  if (node.marks && node.marks.length > 0) {
421
607
  textNode.marks = node.marks;
@@ -429,116 +615,143 @@ function replaceVariablesInContent(content, values) {
429
615
  }
430
616
  return walkNode(content);
431
617
  }
432
-
433
- // src/utils/markdown-writer.ts
434
- function serializeFieldToken(attrs) {
435
- const parts = ["field"];
436
- if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
437
- if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
438
- if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
439
- if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
440
- if (attrs.required) parts.push(`required:true`);
441
- if (attrs.options) parts.push(`options:${attrs.options}`);
442
- if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
443
- if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
444
- if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
445
- if (attrs.multiline) parts.push(`multiline:true`);
446
- if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
447
- if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
448
- return `{{${parts.join("|")}}}`;
618
+ function labelToVarName(label) {
619
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
449
620
  }
450
- function serializeVariableToken(attrs) {
451
- const parts = ["var"];
452
- if (attrs.varName) parts.push(`name:${attrs.varName}`);
453
- if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
454
- if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
455
- return `{{${parts.join("|")}}}`;
621
+ function isZeroLike(value) {
622
+ if (!value || value.trim() === "") return true;
623
+ const cleaned = value.replace(/[$,\s]/g, "");
624
+ return /^-?0+(\.0+)?$/.test(cleaned);
456
625
  }
457
- function serializeInline(content) {
458
- if (!content) return "";
459
- let result = "";
460
- for (const node of content) {
461
- if (node.type === "fieldNode") {
462
- result += serializeFieldToken(node.attrs || {});
463
- continue;
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);
464
632
  }
465
- if (node.type === "variableNode") {
466
- let token = serializeVariableToken(node.attrs || {});
467
- const varMarks = node.marks || [];
468
- const isBold = varMarks.some((m) => m.type === "bold");
469
- const isItalic = varMarks.some((m) => m.type === "italic");
470
- const isUnderline = varMarks.some((m) => m.type === "underline");
471
- if (isBold && isItalic) token = `***${token}***`;
472
- else if (isBold) token = `**${token}**`;
473
- else if (isItalic) token = `*${token}*`;
474
- if (isUnderline) token = `__${token}__`;
475
- result += token;
476
- continue;
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 };
477
648
  }
478
- if (node.type === "text") {
479
- let text = node.text || "";
480
- const marks = node.marks || [];
481
- for (const mark of marks) {
482
- if (mark.type === "bold") text = `**${text}**`;
483
- else if (mark.type === "italic") text = `*${text}*`;
484
- else if (mark.type === "underline") text = `__${text}__`;
485
- else if (mark.type === "code") text = `\`${text}\``;
486
- }
487
- result += text;
649
+ if (node.type === "paragraph" && hasSuppressedVar(node)) {
650
+ return null;
488
651
  }
489
- if (node.type === "hardBreak") {
490
- result += " \n";
652
+ if (node.content) {
653
+ const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
654
+ return { ...node, content: filtered };
491
655
  }
656
+ return node;
492
657
  }
493
- return result;
658
+ return walkNode(content) || content;
494
659
  }
495
- function serializeBlock(node) {
496
- switch (node.type) {
497
- case "heading": {
498
- const level = node.attrs?.level || 1;
499
- const prefix = "#".repeat(level);
500
- return `${prefix} ${serializeInline(node.content)}`;
501
- }
502
- case "paragraph": {
503
- const inline = serializeInline(node.content);
504
- return inline;
505
- }
506
- case "bulletList": {
507
- return (node.content || []).map((item) => {
508
- const inner = (item.content || []).map(serializeBlock).join("\n");
509
- return `- ${inner}`;
510
- }).join("\n");
511
- }
512
- case "orderedList": {
513
- return (node.content || []).map((item, i) => {
514
- const inner = (item.content || []).map(serializeBlock).join("\n");
515
- return `${i + 1}. ${inner}`;
516
- }).join("\n");
517
- }
518
- case "listItem": {
519
- return (node.content || []).map(serializeBlock).join("\n");
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 } };
520
668
  }
521
- case "blockquote": {
522
- return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
669
+ if (node.content) {
670
+ return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
523
671
  }
524
- case "codeBlock": {
525
- const text = serializeInline(node.content);
526
- return `\`\`\`
527
- ${text}
528
- \`\`\``;
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;
529
721
  }
530
- case "horizontalRule": {
531
- return "---";
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 };
532
730
  }
533
- default:
534
- return serializeInline(node.content);
731
+ return node;
535
732
  }
536
- }
537
- function tiptapToMarkdown(doc) {
538
- if (!doc.content) return "";
539
- return doc.content.map(serializeBlock).join("\n\n");
733
+ const result = walkNode(content);
734
+ return {
735
+ content: Array.isArray(result) ? { ...content, content: result } : result,
736
+ values: enrichedValues
737
+ };
540
738
  }
541
739
  var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
740
+ var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
741
+ var DIRECTIVE_CLOSE = /^:::$/;
742
+ function parseDirectiveAttrs(str) {
743
+ const attrs = {};
744
+ if (!str) return attrs;
745
+ const pairs = str.split("|");
746
+ for (const pair of pairs) {
747
+ const colonIdx = pair.indexOf(":");
748
+ if (colonIdx === -1) continue;
749
+ const key = pair.slice(0, colonIdx).trim();
750
+ const value = pair.slice(colonIdx + 1).trim();
751
+ if (key) attrs[key] = value;
752
+ }
753
+ return attrs;
754
+ }
542
755
  function injectMarks(token, marks) {
543
756
  const existingMatch = token.match(/\|_marks:([^}|]+)/);
544
757
  if (existingMatch) {
@@ -655,6 +868,12 @@ function parseVariableToken(tokenBody) {
655
868
  case "default":
656
869
  attrs.varDefault = value;
657
870
  break;
871
+ case "suppress":
872
+ attrs.suppressZero = value === "zero" ? "true" : "";
873
+ break;
874
+ case "format":
875
+ attrs.format = value;
876
+ break;
658
877
  case "_marks":
659
878
  attrs._marks = value;
660
879
  break;
@@ -696,45 +915,63 @@ function parseInline(text) {
696
915
  function parseFormattedText(text) {
697
916
  if (!text) return [];
698
917
  const nodes = [];
699
- const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
918
+ const parts = text.split(/(\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
700
919
  for (const part of parts) {
701
920
  if (!part) continue;
702
- if (part.startsWith("**") && part.endsWith("**")) {
703
- nodes.push({
704
- type: "text",
705
- text: part.slice(2, -2),
706
- marks: [{ type: "bold" }]
707
- });
921
+ if (part.startsWith("***") && part.endsWith("***")) {
922
+ const inner = part.slice(3, -3);
923
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }, { type: "italic" }] });
924
+ } else if (part.startsWith("**") && part.endsWith("**")) {
925
+ const inner = part.slice(2, -2);
926
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }] });
708
927
  } else if (part.startsWith("*") && part.endsWith("*")) {
709
- nodes.push({
710
- type: "text",
711
- text: part.slice(1, -1),
712
- marks: [{ type: "italic" }]
713
- });
928
+ const inner = part.slice(1, -1);
929
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "italic" }] });
714
930
  } else if (part.startsWith("__") && part.endsWith("__")) {
715
- nodes.push({
716
- type: "text",
717
- text: part.slice(2, -2),
718
- marks: [{ type: "underline" }]
719
- });
931
+ const inner = part.slice(2, -2);
932
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "underline" }] });
720
933
  } else if (part.startsWith("`") && part.endsWith("`")) {
721
- nodes.push({
722
- type: "text",
723
- text: part.slice(1, -1),
724
- marks: [{ type: "code" }]
725
- });
934
+ const inner = part.slice(1, -1);
935
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "code" }] });
726
936
  } else {
727
937
  nodes.push({ type: "text", text: part });
728
938
  }
729
939
  }
730
940
  return nodes;
731
941
  }
732
- function markdownToTiptap(markdown) {
733
- const lines = markdown.split("\n");
942
+ function splitTableCells(row) {
943
+ const cells = [];
944
+ let current = "";
945
+ let depth = 0;
946
+ for (let i = 0; i < row.length; i++) {
947
+ if (row[i] === "{" && row[i + 1] === "{") {
948
+ depth++;
949
+ current += "{{";
950
+ i++;
951
+ } else if (row[i] === "}" && row[i + 1] === "}") {
952
+ depth = Math.max(0, depth - 1);
953
+ current += "}}";
954
+ i++;
955
+ } else if (row[i] === "|" && depth === 0) {
956
+ cells.push(current.trim());
957
+ current = "";
958
+ } else {
959
+ current += row[i];
960
+ }
961
+ }
962
+ if (current.trim()) {
963
+ cells.push(current.trim());
964
+ }
965
+ return cells;
966
+ }
967
+ function parseBlocks(lines, startIdx, stopCondition) {
734
968
  const content = [];
735
- let i = 0;
969
+ let i = startIdx;
736
970
  while (i < lines.length) {
737
971
  const line = lines[i];
972
+ if (stopCondition && stopCondition(line)) {
973
+ break;
974
+ }
738
975
  if (line.trim() === "") {
739
976
  i++;
740
977
  continue;
@@ -808,41 +1045,218 @@ function markdownToTiptap(markdown) {
808
1045
  });
809
1046
  continue;
810
1047
  }
811
- content.push({
812
- type: "paragraph",
813
- content: parseInline(line)
814
- });
815
- i++;
816
- }
817
- return { type: "doc", content };
818
- }
819
-
820
- // src/utils/error-helpers.ts
821
- function getErrorMessage(error) {
822
- if (error instanceof Error) return error.message;
823
- if (typeof error === "string") return error;
824
- try {
825
- return JSON.stringify(error);
826
- } catch {
827
- return String(error);
828
- }
829
- }
830
- function formatError(context, error) {
831
- const message = getErrorMessage(error);
832
- return `${context}: ${message}`;
833
- }
834
- var CONTEXT_CHARS = 25;
835
- function extractPosition(error, inputLength) {
836
- const msg = error.message;
837
- const v8 = /position\s+(\d+)/i.exec(msg);
838
- if (v8) return Number(v8[1]);
839
- if (/unexpected end/i.test(msg) || /unterminated/i.test(msg)) {
840
- return inputLength > 0 ? inputLength - 1 : 0;
841
- }
842
- return null;
843
- }
844
- function stripNativePosition(msg) {
845
- const v8Stripped = msg.replace(/\s+in JSON at position\s+\d+(\s*\(line\s+\d+\s+column\s+\d+\))?/i, "");
1048
+ if (line.trimStart().startsWith("|") && line.trimEnd().endsWith("|")) {
1049
+ const tableLines = [];
1050
+ while (i < lines.length) {
1051
+ const tl = lines[i];
1052
+ if (tl.trimStart().startsWith("|") && tl.trimEnd().endsWith("|")) {
1053
+ tableLines.push(tl);
1054
+ i++;
1055
+ } else {
1056
+ break;
1057
+ }
1058
+ }
1059
+ const separatorRegex = /^[\s|]*-+[\s|:-]*$/;
1060
+ const dataLines = tableLines.filter(
1061
+ (tl) => !separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))
1062
+ );
1063
+ const firstDataCellsRaw = dataLines.length > 0 ? splitTableCells(dataLines[0]) : [];
1064
+ const isSubtotals = firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "_");
1065
+ const isBorderless = isSubtotals || firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "");
1066
+ const tableRows = [];
1067
+ let isFirstDataRow = true;
1068
+ for (const tl of tableLines) {
1069
+ if (separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))) continue;
1070
+ const cells = splitTableCells(tl).filter((c) => c !== "");
1071
+ if (isBorderless && isFirstDataRow) {
1072
+ isFirstDataRow = false;
1073
+ continue;
1074
+ }
1075
+ const cellType = isFirstDataRow && !isBorderless ? "tableHeader" : "tableCell";
1076
+ const rowContent = cells.map((cellText) => ({
1077
+ type: cellType,
1078
+ content: [{ type: "paragraph", content: cellText ? parseInline(cellText) : [] }]
1079
+ }));
1080
+ tableRows.push({ type: "tableRow", content: rowContent });
1081
+ isFirstDataRow = false;
1082
+ }
1083
+ if (tableRows.length > 0) {
1084
+ const tableNode = { type: "table", content: tableRows };
1085
+ if (isBorderless) {
1086
+ tableNode.attrs = { borderless: true, ...isSubtotals ? { subtotals: true } : {} };
1087
+ }
1088
+ content.push(tableNode);
1089
+ }
1090
+ continue;
1091
+ }
1092
+ const directiveMatch = line.trim().match(DIRECTIVE_OPEN);
1093
+ if (directiveMatch) {
1094
+ const directiveType = directiveMatch[1];
1095
+ const rawAttrs = directiveMatch[2] || "";
1096
+ const attrs = parseDirectiveAttrs(rawAttrs);
1097
+ i++;
1098
+ if (directiveType === "watermark") {
1099
+ content.push({
1100
+ type: "watermark",
1101
+ attrs: {
1102
+ text: attrs.text || "",
1103
+ opacity: attrs.opacity || "0.15",
1104
+ angle: attrs.angle || "-45"
1105
+ }
1106
+ });
1107
+ continue;
1108
+ }
1109
+ if (directiveType === "panel") {
1110
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1111
+ i = inner.nextIdx;
1112
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1113
+ i++;
1114
+ }
1115
+ content.push({
1116
+ type: "panel",
1117
+ attrs: {
1118
+ title: attrs.title || "",
1119
+ border: attrs.border || "solid",
1120
+ headerStyle: attrs.headerStyle || ""
1121
+ },
1122
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1123
+ });
1124
+ continue;
1125
+ }
1126
+ if (directiveType === "repeat") {
1127
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1128
+ i = inner.nextIdx;
1129
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1130
+ i++;
1131
+ }
1132
+ content.push({
1133
+ type: "repeatBlock",
1134
+ attrs: { data: attrs.data || "" },
1135
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1136
+ });
1137
+ continue;
1138
+ }
1139
+ if (directiveType === "subtotals") {
1140
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1141
+ i = inner.nextIdx;
1142
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1143
+ i++;
1144
+ }
1145
+ content.push({
1146
+ type: "subtotalsBlock",
1147
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1148
+ });
1149
+ continue;
1150
+ }
1151
+ if (directiveType === "columns") {
1152
+ const splitVal = attrs.split || "50";
1153
+ const columns = [];
1154
+ while (i < lines.length) {
1155
+ const colLine = lines[i].trim();
1156
+ if (DIRECTIVE_CLOSE.test(colLine)) {
1157
+ if (columns.length > 0) {
1158
+ i++;
1159
+ break;
1160
+ }
1161
+ i++;
1162
+ break;
1163
+ }
1164
+ const colMatch = colLine.match(/^:::col(?:\{([^}]*)\})?$/);
1165
+ if (colMatch) {
1166
+ i++;
1167
+ const colInner = parseBlocks(lines, i, (l) => {
1168
+ const trimmed = l.trim();
1169
+ return DIRECTIVE_CLOSE.test(trimmed) || /^:::col(?:\{[^}]*\})?$/.test(trimmed);
1170
+ });
1171
+ i = colInner.nextIdx;
1172
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1173
+ i++;
1174
+ }
1175
+ const colAttrsRaw = colMatch[1] || "";
1176
+ const colAttrs = parseDirectiveAttrs(colAttrsRaw);
1177
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
1178
+ columns.push({
1179
+ type: "column",
1180
+ attrs: padTop ? { padTop } : void 0,
1181
+ content: colInner.blocks.length > 0 ? colInner.blocks : [{ type: "paragraph" }]
1182
+ });
1183
+ continue;
1184
+ }
1185
+ if (colLine === "") {
1186
+ i++;
1187
+ continue;
1188
+ }
1189
+ break;
1190
+ }
1191
+ const columnsAttrs = { split: splitVal };
1192
+ if (attrs.padX) columnsAttrs.padX = attrs.padX;
1193
+ content.push({
1194
+ type: "columns",
1195
+ attrs: columnsAttrs,
1196
+ content: columns.length > 0 ? columns : [
1197
+ { type: "column", content: [{ type: "paragraph" }] },
1198
+ { type: "column", content: [{ type: "paragraph" }] }
1199
+ ]
1200
+ });
1201
+ continue;
1202
+ }
1203
+ if (directiveType === "col") {
1204
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1205
+ i = inner.nextIdx;
1206
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1207
+ i++;
1208
+ }
1209
+ for (const block of inner.blocks) {
1210
+ content.push(block);
1211
+ }
1212
+ continue;
1213
+ }
1214
+ continue;
1215
+ }
1216
+ if (DIRECTIVE_CLOSE.test(line.trim())) {
1217
+ i++;
1218
+ continue;
1219
+ }
1220
+ content.push({
1221
+ type: "paragraph",
1222
+ content: parseInline(line)
1223
+ });
1224
+ i++;
1225
+ }
1226
+ return { blocks: content, nextIdx: i };
1227
+ }
1228
+ function markdownToTiptap(markdown) {
1229
+ const lines = markdown.split("\n");
1230
+ const { blocks } = parseBlocks(lines, 0);
1231
+ return { type: "doc", content: blocks };
1232
+ }
1233
+
1234
+ // src/utils/error-helpers.ts
1235
+ function getErrorMessage(error) {
1236
+ if (error instanceof Error) return error.message;
1237
+ if (typeof error === "string") return error;
1238
+ try {
1239
+ return JSON.stringify(error);
1240
+ } catch {
1241
+ return String(error);
1242
+ }
1243
+ }
1244
+ function formatError(context, error) {
1245
+ const message = getErrorMessage(error);
1246
+ return `${context}: ${message}`;
1247
+ }
1248
+ var CONTEXT_CHARS = 25;
1249
+ function extractPosition(error, inputLength) {
1250
+ const msg = error.message;
1251
+ const v8 = /position\s+(\d+)/i.exec(msg);
1252
+ if (v8) return Number(v8[1]);
1253
+ if (/unexpected end/i.test(msg) || /unterminated/i.test(msg)) {
1254
+ return inputLength > 0 ? inputLength - 1 : 0;
1255
+ }
1256
+ return null;
1257
+ }
1258
+ function stripNativePosition(msg) {
1259
+ const v8Stripped = msg.replace(/\s+in JSON at position\s+\d+(\s*\(line\s+\d+\s+column\s+\d+\))?/i, "");
846
1260
  if (v8Stripped !== msg) return v8Stripped;
847
1261
  const ffStripped = msg.replace(/\s+at line\s+\d+\s+column\s+\d+\s+of the JSON data/i, "");
848
1262
  if (ffStripped !== msg) return ffStripped;
@@ -952,10 +1366,12 @@ function buildMetadataObject(fields, actualFieldNames) {
952
1366
  // src/utils/pdf-generator.ts
953
1367
  var PAGE_WIDTH = 595.28;
954
1368
  var PAGE_HEIGHT = 841.89;
955
- var MARGIN = 72;
1369
+ var MARGIN = 40;
956
1370
  var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
957
1371
  var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
958
1372
  var LINE_HEIGHT_FACTOR = 1.4;
1373
+ var BODY_FONT_SIZE = 10;
1374
+ var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
959
1375
  var FIELD_DISPLAY = {
960
1376
  ["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
961
1377
  ["signature" /* SIGNATURE */]: { label: "Signature", width: 200, height: 60 },
@@ -992,11 +1408,12 @@ function ensureSpace(state, neededHeight) {
992
1408
  state.currentPage = newPage;
993
1409
  state.pageIndex++;
994
1410
  state.y = 0;
1411
+ renderWatermarksOnPage(newPage, state.fonts, state.watermarkNodes);
995
1412
  }
996
1413
  }
997
- function drawText(state, text, font, fontSize, indent = 0) {
1414
+ function drawText(state, text, font, fontSize, indent = 0, region = DEFAULT_REGION) {
998
1415
  const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
999
- const maxWidth = CONTENT_WIDTH - indent;
1416
+ const maxWidth = region.width - indent;
1000
1417
  const words = text.split(/\s+/);
1001
1418
  let line = "";
1002
1419
  let totalAdvance = 0;
@@ -1007,7 +1424,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
1007
1424
  ensureSpace(state, lineHeight);
1008
1425
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1009
1426
  state.currentPage.drawText(line, {
1010
- x: MARGIN + indent,
1427
+ x: region.leftX + indent,
1011
1428
  y: pdfY,
1012
1429
  size: fontSize,
1013
1430
  font,
@@ -1024,7 +1441,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
1024
1441
  ensureSpace(state, lineHeight);
1025
1442
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1026
1443
  state.currentPage.drawText(line, {
1027
- x: MARGIN + indent,
1444
+ x: region.leftX + indent,
1028
1445
  y: pdfY,
1029
1446
  size: fontSize,
1030
1447
  font,
@@ -1068,10 +1485,10 @@ function collectInlineSegments(content, fonts, fontSize) {
1068
1485
  }
1069
1486
  return segments;
1070
1487
  }
1071
- function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1488
+ function layoutInlineSegments(state, segments, fontSize, indent = 0, region = DEFAULT_REGION) {
1072
1489
  const textLineHeight = fontSize * LINE_HEIGHT_FACTOR;
1073
- const maxX = MARGIN + CONTENT_WIDTH;
1074
- const startX = MARGIN + indent;
1490
+ const maxX = region.leftX + region.width;
1491
+ const startX = region.leftX + indent;
1075
1492
  let currentX = startX;
1076
1493
  let currentLineHeight = 0;
1077
1494
  let lineHasContent = false;
@@ -1176,13 +1593,316 @@ function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1176
1593
  }
1177
1594
  flushLine();
1178
1595
  }
1179
- function processInlineContent(state, content, fontSize, indent = 0) {
1596
+ function processInlineContent(state, content, fontSize, indent = 0, region = DEFAULT_REGION) {
1180
1597
  if (!content) return;
1181
1598
  const segments = collectInlineSegments(content, state.fonts, fontSize);
1182
1599
  if (segments.length === 0) return;
1183
- layoutInlineSegments(state, segments, fontSize, indent);
1600
+ layoutInlineSegments(state, segments, fontSize, indent, region);
1601
+ }
1602
+ function extractCellText(cell) {
1603
+ const parts = [];
1604
+ for (const child of cell.content || []) {
1605
+ if (child.type === "paragraph" || child.type === "heading") {
1606
+ for (const inline of child.content || []) {
1607
+ if (inline.type === "text") {
1608
+ parts.push(inline.text || "");
1609
+ } else if (inline.type === "variableNode") {
1610
+ const label = inline.attrs?.varLabel || inline.attrs?.varName || "Variable";
1611
+ parts.push(`[${label}]`);
1612
+ } else if (inline.type === "fieldNode") {
1613
+ const label = inline.attrs?.fieldLabel || inline.attrs?.fieldName || "Field";
1614
+ parts.push(`[ ${label} ]`);
1615
+ }
1616
+ }
1617
+ }
1618
+ }
1619
+ return parts.join("");
1620
+ }
1621
+ function isNumericText(text) {
1622
+ return /^\s*\$?\s*[\d,.]+\s*$/.test(text);
1623
+ }
1624
+ function measureCellHeight(cell, maxWidth, fonts, fontSize, isHeader) {
1625
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1626
+ const font = isHeader ? fonts.bold : fonts.regular;
1627
+ let totalHeight = 0;
1628
+ for (const child of cell.content || []) {
1629
+ if (child.type === "paragraph" || child.type === "heading") {
1630
+ const text = extractCellText({ content: [child] });
1631
+ if (!text) {
1632
+ totalHeight += lineHeight;
1633
+ continue;
1634
+ }
1635
+ const words = text.split(/\s+/);
1636
+ let line = "";
1637
+ let lineCount = 0;
1638
+ for (const word of words) {
1639
+ const testLine = line ? `${line} ${word}` : word;
1640
+ const testWidth = font.widthOfTextAtSize(testLine, fontSize);
1641
+ if (testWidth > maxWidth && line) {
1642
+ lineCount++;
1643
+ line = word;
1644
+ } else {
1645
+ line = testLine;
1646
+ }
1647
+ }
1648
+ if (line) lineCount++;
1649
+ totalHeight += lineCount * lineHeight;
1650
+ }
1651
+ }
1652
+ return Math.max(totalHeight, lineHeight);
1653
+ }
1654
+ function renderCellContent(state, cell, cellX, cellY, cellWidth, fonts, fontSize, isHeader, rightAlign, textColor = pdfLib.rgb(0, 0, 0)) {
1655
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1656
+ const cellPadding = 4;
1657
+ const maxTextWidth = cellWidth - cellPadding * 2;
1658
+ let textX = cellX + cellPadding;
1659
+ let textY = cellY;
1660
+ for (const child of cell.content || []) {
1661
+ if (child.type === "paragraph" || child.type === "heading") {
1662
+ const segments = collectInlineSegments(
1663
+ child.content || [],
1664
+ isHeader ? { regular: fonts.bold, bold: fonts.bold, italic: fonts.boldItalic, boldItalic: fonts.boldItalic } : fonts,
1665
+ fontSize
1666
+ );
1667
+ if (rightAlign && segments.length > 0) {
1668
+ const fullText = segments.filter((s) => s.kind === "text").map((s) => s.text).join("");
1669
+ const textWidth = (isHeader ? fonts.bold : fonts.regular).widthOfTextAtSize(fullText, fontSize);
1670
+ if (textWidth <= maxTextWidth) {
1671
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1672
+ state.currentPage.drawText(fullText, {
1673
+ x: cellX + cellWidth - cellPadding - textWidth,
1674
+ y: pdfY,
1675
+ size: fontSize,
1676
+ font: isHeader ? fonts.bold : fonts.regular,
1677
+ color: textColor
1678
+ });
1679
+ return;
1680
+ }
1681
+ }
1682
+ let currentX = textX;
1683
+ for (const seg of segments) {
1684
+ if (seg.kind === "break") {
1685
+ textY += lineHeight;
1686
+ currentX = textX;
1687
+ continue;
1688
+ }
1689
+ if (seg.kind === "text") {
1690
+ const words = seg.text.split(/(\s+)/);
1691
+ const spaceWidth = seg.font.widthOfTextAtSize(" ", fontSize);
1692
+ for (const token of words) {
1693
+ if (!token) continue;
1694
+ if (/^\s+$/.test(token)) {
1695
+ currentX += spaceWidth;
1696
+ continue;
1697
+ }
1698
+ const wordWidth = seg.font.widthOfTextAtSize(token, fontSize);
1699
+ if (currentX + wordWidth > textX + maxTextWidth && currentX > textX) {
1700
+ textY += lineHeight;
1701
+ currentX = textX;
1702
+ }
1703
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1704
+ state.currentPage.drawText(token, {
1705
+ x: currentX,
1706
+ y: pdfY,
1707
+ size: fontSize,
1708
+ font: seg.font,
1709
+ color: textColor
1710
+ });
1711
+ currentX += wordWidth;
1712
+ }
1713
+ } else if (seg.kind === "field") {
1714
+ const pdfY = PAGE_HEIGHT - textY - seg.height;
1715
+ if (state.drawFieldPlaceholders) {
1716
+ state.currentPage.drawRectangle({
1717
+ x: currentX,
1718
+ y: pdfY,
1719
+ width: seg.width,
1720
+ height: seg.height,
1721
+ borderColor: pdfLib.rgb(0.5, 0.5, 0.7),
1722
+ borderWidth: 0.5,
1723
+ color: pdfLib.rgb(0.95, 0.95, 1),
1724
+ borderDashArray: [4, 2]
1725
+ });
1726
+ }
1727
+ const fieldId = seg.attrs.fieldId;
1728
+ if (fieldId) {
1729
+ state.fieldPositions.set(fieldId, {
1730
+ x: currentX,
1731
+ y: pdfY,
1732
+ width: seg.width,
1733
+ height: seg.height,
1734
+ page: state.pageIndex + 1
1735
+ });
1736
+ }
1737
+ currentX += seg.width + 4;
1738
+ }
1739
+ }
1740
+ }
1741
+ }
1742
+ }
1743
+ function renderTable(state, node, region = DEFAULT_REGION) {
1744
+ const tableRows = node.content || [];
1745
+ if (tableRows.length === 0) return;
1746
+ const borderless = node.attrs?.borderless === true;
1747
+ const subtotals = node.attrs?.subtotals === true;
1748
+ const fontSize = 10;
1749
+ const cellPadding = borderless ? 2 : 4;
1750
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1751
+ 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);
1755
+ const firstRow = tableRows[0];
1756
+ const colCount = firstRow.content?.length || 0;
1757
+ if (colCount === 0) return;
1758
+ const colMaxWidths = new Array(colCount).fill(0);
1759
+ for (const row of tableRows) {
1760
+ const cells = row.content || [];
1761
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1762
+ const cell = cells[c];
1763
+ const isHeader = cell.type === "tableHeader";
1764
+ const text = extractCellText(cell);
1765
+ const font = isHeader ? state.fonts.bold : state.fonts.regular;
1766
+ const textWidth = font.widthOfTextAtSize(text, fontSize);
1767
+ colMaxWidths[c] = Math.max(colMaxWidths[c], textWidth + cellPadding * 2 + 4);
1768
+ }
1769
+ }
1770
+ const totalMeasured = colMaxWidths.reduce((a, b) => a + b, 0);
1771
+ const minColWidth = 40;
1772
+ let colWidths;
1773
+ if (totalMeasured <= region.width) {
1774
+ const scale = region.width / totalMeasured;
1775
+ colWidths = colMaxWidths.map((w) => Math.max(w * scale, minColWidth));
1776
+ } else {
1777
+ colWidths = colMaxWidths.map(
1778
+ (w) => Math.max(w / totalMeasured * region.width, minColWidth)
1779
+ );
1780
+ }
1781
+ const colSum = colWidths.reduce((a, b) => a + b, 0);
1782
+ if (colSum > 0) {
1783
+ const normFactor = region.width / colSum;
1784
+ colWidths = colWidths.map((w) => w * normFactor);
1785
+ }
1786
+ const headerRow = tableRows[0];
1787
+ const hasHeader = headerRow?.content?.[0]?.type === "tableHeader";
1788
+ function renderRow(row, isHeaderRow) {
1789
+ const cells = row.content || [];
1790
+ const isHeader = isHeaderRow && hasHeader;
1791
+ let rowHeight = lineHeight;
1792
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1793
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
1794
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, isHeader);
1795
+ rowHeight = Math.max(rowHeight, cellH);
1796
+ }
1797
+ rowHeight += rowPadding;
1798
+ ensureSpace(state, rowHeight);
1799
+ const rowTopY = state.y;
1800
+ let cellX = region.leftX;
1801
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1802
+ const cellW = colWidths[c];
1803
+ const cell = cells[c];
1804
+ const pdfCellTop = PAGE_HEIGHT - MARGIN - rowTopY;
1805
+ const pdfCellBottom = pdfCellTop - rowHeight;
1806
+ if (isHeader && !borderless) {
1807
+ state.currentPage.drawRectangle({
1808
+ x: cellX,
1809
+ y: pdfCellBottom,
1810
+ width: cellW,
1811
+ height: rowHeight,
1812
+ color: headerBg
1813
+ });
1814
+ }
1815
+ if (!borderless) {
1816
+ state.currentPage.drawRectangle({
1817
+ x: cellX,
1818
+ y: pdfCellBottom,
1819
+ width: cellW,
1820
+ height: rowHeight,
1821
+ borderColor,
1822
+ borderWidth: 0.5
1823
+ });
1824
+ }
1825
+ const cellText = extractCellText(cell);
1826
+ const rightAlign = !isHeader && isNumericText(cellText);
1827
+ const contentY = MARGIN + rowTopY + cellPadding;
1828
+ const effectiveHeader = isHeader && !borderless;
1829
+ renderCellContent(
1830
+ state,
1831
+ cell,
1832
+ cellX,
1833
+ contentY,
1834
+ cellW,
1835
+ state.fonts,
1836
+ fontSize,
1837
+ effectiveHeader,
1838
+ rightAlign,
1839
+ effectiveHeader ? headerTextColor : pdfLib.rgb(0, 0, 0)
1840
+ );
1841
+ cellX += cellW;
1842
+ }
1843
+ state.y += rowHeight;
1844
+ }
1845
+ if (hasHeader && tableRows.length > 0) {
1846
+ renderRow(tableRows[0], true);
1847
+ }
1848
+ let lastPageIndex = state.pageIndex;
1849
+ const startIdx = hasHeader ? 1 : 0;
1850
+ const bodyRowCount = tableRows.length - startIdx;
1851
+ for (let r = startIdx; r < tableRows.length; r++) {
1852
+ const cells = tableRows[r].content || [];
1853
+ let estRowHeight = lineHeight;
1854
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1855
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
1856
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, false);
1857
+ estRowHeight = Math.max(estRowHeight, cellH);
1858
+ }
1859
+ estRowHeight += rowPadding;
1860
+ if (state.y + estRowHeight > CONTENT_HEIGHT) {
1861
+ ensureSpace(state, estRowHeight + (hasHeader ? lineHeight + rowPadding : 0));
1862
+ if (state.pageIndex !== lastPageIndex && hasHeader) {
1863
+ renderRow(headerRow, true);
1864
+ lastPageIndex = state.pageIndex;
1865
+ }
1866
+ }
1867
+ const rowTopYBeforeRender = state.y;
1868
+ renderRow(tableRows[r], false);
1869
+ lastPageIndex = state.pageIndex;
1870
+ if (subtotals && colCount >= 2) {
1871
+ const bodyIdx = r - startIdx;
1872
+ const isFirstBody = bodyIdx === 0;
1873
+ const isLastBody = bodyIdx === bodyRowCount - 1;
1874
+ if (isFirstBody || isLastBody) {
1875
+ const valueStartX = region.leftX + colWidths[0];
1876
+ const valueEndX = region.leftX + colWidths.reduce((a, b) => a + b, 0);
1877
+ const pdfRowTopY = PAGE_HEIGHT - MARGIN - rowTopYBeforeRender;
1878
+ const overlineColor = pdfLib.rgb(0, 0, 0);
1879
+ if (isFirstBody) {
1880
+ state.currentPage.drawLine({
1881
+ start: { x: valueStartX, y: pdfRowTopY },
1882
+ end: { x: valueEndX, y: pdfRowTopY },
1883
+ thickness: 0.5,
1884
+ color: overlineColor
1885
+ });
1886
+ } else if (isLastBody) {
1887
+ state.currentPage.drawLine({
1888
+ start: { x: valueStartX, y: pdfRowTopY + 2 },
1889
+ end: { x: valueEndX, y: pdfRowTopY + 2 },
1890
+ thickness: 0.5,
1891
+ color: overlineColor
1892
+ });
1893
+ state.currentPage.drawLine({
1894
+ start: { x: valueStartX, y: pdfRowTopY },
1895
+ end: { x: valueEndX, y: pdfRowTopY },
1896
+ thickness: 0.5,
1897
+ color: overlineColor
1898
+ });
1899
+ }
1900
+ }
1901
+ }
1902
+ }
1903
+ state.y += 3;
1184
1904
  }
1185
- function processBlock(state, node) {
1905
+ function processBlock(state, node, region = DEFAULT_REGION) {
1186
1906
  switch (node.type) {
1187
1907
  case "heading": {
1188
1908
  const level = node.attrs?.level || 1;
@@ -1199,34 +1919,34 @@ function processBlock(state, node) {
1199
1919
  boldItalic: state.fonts.boldItalic
1200
1920
  };
1201
1921
  const segments = collectInlineSegments(node.content, headingFonts, fontSize);
1202
- layoutInlineSegments(state, segments, fontSize);
1922
+ layoutInlineSegments(state, segments, fontSize, 0, region);
1203
1923
  state.y += spacing / 2;
1204
1924
  break;
1205
1925
  }
1206
1926
  case "paragraph": {
1207
- const fontSize = 12;
1927
+ const fontSize = BODY_FONT_SIZE;
1208
1928
  if (!node.content || node.content.length === 0) {
1209
- state.y += fontSize * LINE_HEIGHT_FACTOR;
1929
+ state.y += fontSize * LINE_HEIGHT_FACTOR * 0.65;
1210
1930
  break;
1211
1931
  }
1212
- processInlineContent(state, node.content, fontSize);
1213
- state.y += 4;
1932
+ processInlineContent(state, node.content, fontSize, 0, region);
1933
+ state.y += 2;
1214
1934
  break;
1215
1935
  }
1216
1936
  case "bulletList": {
1217
1937
  for (const item of node.content || []) {
1218
1938
  for (const child of item.content || []) {
1219
- const fontSize = 12;
1939
+ const fontSize = BODY_FONT_SIZE;
1220
1940
  ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1221
1941
  const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1222
1942
  state.currentPage.drawText("\u2022", {
1223
- x: MARGIN + 8,
1943
+ x: region.leftX + 8,
1224
1944
  y: bulletPdfY,
1225
1945
  size: fontSize,
1226
1946
  font: state.fonts.regular,
1227
1947
  color: pdfLib.rgb(0, 0, 0)
1228
1948
  });
1229
- processInlineContent(state, child.content, fontSize, 24);
1949
+ processInlineContent(state, child.content, fontSize, 24, region);
1230
1950
  }
1231
1951
  }
1232
1952
  state.y += 4;
@@ -1236,17 +1956,17 @@ function processBlock(state, node) {
1236
1956
  let num = 1;
1237
1957
  for (const item of node.content || []) {
1238
1958
  for (const child of item.content || []) {
1239
- const fontSize = 12;
1959
+ const fontSize = BODY_FONT_SIZE;
1240
1960
  ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1241
1961
  const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1242
1962
  state.currentPage.drawText(`${num}.`, {
1243
- x: MARGIN + 4,
1963
+ x: region.leftX + 4,
1244
1964
  y: numPdfY,
1245
1965
  size: fontSize,
1246
1966
  font: state.fonts.regular,
1247
1967
  color: pdfLib.rgb(0, 0, 0)
1248
1968
  });
1249
- processInlineContent(state, child.content, fontSize, 24);
1969
+ processInlineContent(state, child.content, fontSize, 24, region);
1250
1970
  }
1251
1971
  num++;
1252
1972
  }
@@ -1256,10 +1976,10 @@ function processBlock(state, node) {
1256
1976
  case "blockquote": {
1257
1977
  const startY = state.y;
1258
1978
  for (const child of node.content || []) {
1259
- processInlineContent(state, child.content, 12, 16);
1979
+ processInlineContent(state, child.content, 12, 16, region);
1260
1980
  }
1261
1981
  const endY = state.y;
1262
- const barX = MARGIN + 4;
1982
+ const barX = region.leftX + 4;
1263
1983
  const barTop = PAGE_HEIGHT - MARGIN - startY;
1264
1984
  const barBottom = PAGE_HEIGHT - MARGIN - endY;
1265
1985
  state.currentPage.drawLine({
@@ -1272,16 +1992,16 @@ function processBlock(state, node) {
1272
1992
  break;
1273
1993
  }
1274
1994
  case "horizontalRule": {
1275
- ensureSpace(state, 16);
1276
- state.y += 8;
1995
+ ensureSpace(state, 10);
1996
+ state.y += 4;
1277
1997
  const ruleY = PAGE_HEIGHT - MARGIN - state.y;
1278
1998
  state.currentPage.drawLine({
1279
- start: { x: MARGIN, y: ruleY },
1280
- end: { x: PAGE_WIDTH - MARGIN, y: ruleY },
1281
- thickness: 1,
1282
- color: pdfLib.rgb(0.7, 0.7, 0.7)
1999
+ start: { x: region.leftX, y: ruleY },
2000
+ end: { x: region.leftX + region.width, y: ruleY },
2001
+ thickness: 2.5,
2002
+ color: pdfLib.rgb(0, 0, 0)
1283
2003
  });
1284
- state.y += 8;
2004
+ state.y += 4;
1285
2005
  break;
1286
2006
  }
1287
2007
  case "codeBlock": {
@@ -1293,9 +2013,9 @@ function processBlock(state, node) {
1293
2013
  ensureSpace(state, blockHeight);
1294
2014
  const boxY = PAGE_HEIGHT - MARGIN - state.y - blockHeight;
1295
2015
  state.currentPage.drawRectangle({
1296
- x: MARGIN,
2016
+ x: region.leftX,
1297
2017
  y: boxY,
1298
- width: CONTENT_WIDTH,
2018
+ width: region.width,
1299
2019
  height: blockHeight,
1300
2020
  color: pdfLib.rgb(0.95, 0.95, 0.95),
1301
2021
  borderColor: pdfLib.rgb(0.85, 0.85, 0.85),
@@ -1303,18 +2023,292 @@ function processBlock(state, node) {
1303
2023
  });
1304
2024
  state.y += 8;
1305
2025
  for (const line of lines) {
1306
- drawText(state, line || " ", state.fonts.regular, fontSize, 8);
2026
+ drawText(state, line || " ", state.fonts.regular, fontSize, 8, region);
1307
2027
  }
1308
2028
  state.y += 8;
1309
2029
  break;
1310
2030
  }
2031
+ case "table": {
2032
+ renderTable(state, node, region);
2033
+ break;
2034
+ }
2035
+ // ─── Layout directives ─────────────────────────────────────────
2036
+ case "panel": {
2037
+ const title = node.attrs?.title || "";
2038
+ const border = node.attrs?.border || "solid";
2039
+ const headerStyle = node.attrs?.headerStyle || "";
2040
+ const padding = 8;
2041
+ const titleFontSize = 10;
2042
+ const titleHeight = title ? titleFontSize * LINE_HEIGHT_FACTOR + 6 : 0;
2043
+ const panelStartY = state.y;
2044
+ const panelStartPage = state.pageIndex;
2045
+ if (title) {
2046
+ state.y += titleHeight;
2047
+ }
2048
+ const isHeaderOnly = border === "none";
2049
+ if (!isHeaderOnly) {
2050
+ state.y += padding;
2051
+ }
2052
+ const innerRegion = {
2053
+ leftX: region.leftX + padding,
2054
+ width: region.width - padding * 2
2055
+ };
2056
+ if (!isHeaderOnly) {
2057
+ for (const child of node.content || []) {
2058
+ processBlock(state, child, innerRegion);
2059
+ }
2060
+ state.y += padding;
2061
+ }
2062
+ const startPage = state.pdfDoc.getPages()[panelStartPage];
2063
+ if (title) {
2064
+ const titleBarY = PAGE_HEIGHT - MARGIN - panelStartY - titleHeight;
2065
+ const isDarkHeader = headerStyle === "dark";
2066
+ startPage.drawRectangle({
2067
+ x: region.leftX,
2068
+ y: titleBarY,
2069
+ width: region.width,
2070
+ 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),
2073
+ borderWidth: 0.75
2074
+ });
2075
+ startPage.drawText(title, {
2076
+ x: region.leftX + padding,
2077
+ y: titleBarY + 4,
2078
+ size: titleFontSize,
2079
+ font: state.fonts.bold,
2080
+ color: isDarkHeader ? pdfLib.rgb(1, 1, 1) : pdfLib.rgb(0, 0, 0)
2081
+ });
2082
+ }
2083
+ if (border !== "none") {
2084
+ const borderDashArray = border === "dashed" ? [4, 2] : void 0;
2085
+ if (panelStartPage === state.pageIndex) {
2086
+ const borderY = PAGE_HEIGHT - MARGIN - state.y;
2087
+ const panelHeight = state.y - panelStartY;
2088
+ startPage.drawRectangle({
2089
+ x: region.leftX,
2090
+ y: borderY,
2091
+ width: region.width,
2092
+ height: panelHeight,
2093
+ borderColor: pdfLib.rgb(0.6, 0.6, 0.6),
2094
+ borderWidth: 0.75,
2095
+ borderDashArray
2096
+ });
2097
+ } else {
2098
+ const startBorderBottom = MARGIN;
2099
+ const startPanelHeight = PAGE_HEIGHT - MARGIN - panelStartY - startBorderBottom;
2100
+ startPage.drawRectangle({
2101
+ x: region.leftX,
2102
+ y: startBorderBottom,
2103
+ width: region.width,
2104
+ height: startPanelHeight,
2105
+ borderColor: pdfLib.rgb(0.6, 0.6, 0.6),
2106
+ borderWidth: 0.75,
2107
+ borderDashArray
2108
+ });
2109
+ const contTop = PAGE_HEIGHT - MARGIN;
2110
+ const contBorderY = PAGE_HEIGHT - MARGIN - state.y;
2111
+ const contHeight = contTop - contBorderY;
2112
+ state.currentPage.drawRectangle({
2113
+ x: region.leftX,
2114
+ y: contBorderY,
2115
+ width: region.width,
2116
+ height: contHeight,
2117
+ borderColor: pdfLib.rgb(0.6, 0.6, 0.6),
2118
+ borderWidth: 0.75,
2119
+ borderDashArray
2120
+ });
2121
+ }
2122
+ }
2123
+ state.y += 4;
2124
+ break;
2125
+ }
2126
+ case "columns": {
2127
+ const split = parseInt(node.attrs?.split || "50", 10);
2128
+ const padX = parseFloat(node.attrs?.padX || "0") || 0;
2129
+ const columns = (node.content || []).filter((c) => c.type === "column");
2130
+ if (columns.length === 0) break;
2131
+ const colRegionLeftX = region.leftX + padX;
2132
+ const colRegionWidth = region.width - 2 * padX;
2133
+ const gap = 12;
2134
+ const columnsStartY = state.y;
2135
+ const columnsStartPage = state.pageIndex;
2136
+ let colWidths;
2137
+ if (columns.length === 2) {
2138
+ const firstWidth = (colRegionWidth - gap) * (split / 100);
2139
+ const secondWidth = colRegionWidth - gap - firstWidth;
2140
+ colWidths = [firstWidth, secondWidth];
2141
+ } else {
2142
+ const eachWidth = (colRegionWidth - gap * (columns.length - 1)) / columns.length;
2143
+ colWidths = columns.map(() => eachWidth);
2144
+ }
2145
+ let maxEndY = columnsStartY;
2146
+ let colX = colRegionLeftX;
2147
+ for (let ci = 0; ci < columns.length; ci++) {
2148
+ const col = columns[ci];
2149
+ const colWidth = colWidths[ci];
2150
+ const colRegion = { leftX: colX, width: colWidth };
2151
+ state.y = columnsStartY;
2152
+ state.pageIndex = columnsStartPage;
2153
+ state.currentPage = state.pdfDoc.getPages()[state.pageIndex];
2154
+ const padTop = parseFloat(col.attrs?.padTop || "0") || 0;
2155
+ if (padTop > 0) {
2156
+ state.y += padTop * BODY_FONT_SIZE * LINE_HEIGHT_FACTOR;
2157
+ }
2158
+ for (const child of col.content || []) {
2159
+ processBlock(state, child, colRegion);
2160
+ }
2161
+ maxEndY = Math.max(maxEndY, state.y);
2162
+ colX += colWidth + gap;
2163
+ }
2164
+ state.y = maxEndY;
2165
+ state.y += 4;
2166
+ break;
2167
+ }
2168
+ case "column": {
2169
+ for (const child of node.content || []) {
2170
+ processBlock(state, child, region);
2171
+ }
2172
+ break;
2173
+ }
2174
+ case "watermark": {
2175
+ break;
2176
+ }
2177
+ case "subtotalsBlock": {
2178
+ const stFontSize = BODY_FONT_SIZE;
2179
+ const stLineHeight = stFontSize * LINE_HEIGHT_FACTOR;
2180
+ const valueRightX = region.leftX + region.width;
2181
+ const rows = (node.content || []).filter(
2182
+ (c) => c.type === "paragraph" && c.content?.length
2183
+ );
2184
+ const parsed = [];
2185
+ for (const row of rows) {
2186
+ let label = "";
2187
+ let value = "";
2188
+ for (const seg of row.content || []) {
2189
+ if (seg.type === "text") {
2190
+ const isBold = seg.marks?.some((m) => m.type === "bold");
2191
+ if (isBold) {
2192
+ label += seg.text || "";
2193
+ } else {
2194
+ const t = (seg.text || "").trim();
2195
+ if (t) value += (value ? " " : "") + t;
2196
+ }
2197
+ } else if (seg.type === "variableNode") {
2198
+ const varLabel = seg.attrs?.varLabel || seg.attrs?.varName || "";
2199
+ const isBoldVar = seg.marks?.some((m) => m.type === "bold");
2200
+ if (isBoldVar) {
2201
+ label += varLabel;
2202
+ } else {
2203
+ value += (value ? " " : "") + `[${varLabel}]`;
2204
+ }
2205
+ }
2206
+ }
2207
+ parsed.push({ label: label.trim(), value: value.trim() });
2208
+ }
2209
+ let maxLabelWidth = 0;
2210
+ for (const { label } of parsed) {
2211
+ if (label) {
2212
+ const w = state.fonts.bold.widthOfTextAtSize(label, stFontSize);
2213
+ if (w > maxLabelWidth) maxLabelWidth = w;
2214
+ }
2215
+ }
2216
+ const gap = 12;
2217
+ const labelX = region.width > 300 ? Math.max(region.leftX + region.width - maxLabelWidth - gap - 80, region.leftX + region.width * 0.4) : region.leftX + 4;
2218
+ const valueColStartX = labelX + maxLabelWidth + gap;
2219
+ const isLastRowTotal = parsed.length > 1;
2220
+ for (let ri = 0; ri < parsed.length; ri++) {
2221
+ const isTotal = isLastRowTotal && ri === parsed.length - 1;
2222
+ if (isTotal) {
2223
+ state.y += stLineHeight * 0.6;
2224
+ }
2225
+ ensureSpace(state, stLineHeight + (isTotal ? 8 : 4));
2226
+ const { label, value } = parsed[ri];
2227
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - stFontSize;
2228
+ if (isTotal) {
2229
+ state.currentPage.drawLine({
2230
+ start: { x: valueColStartX, y: pdfY + stFontSize + 6 },
2231
+ end: { x: valueRightX, y: pdfY + stFontSize + 6 },
2232
+ thickness: 0.75,
2233
+ color: pdfLib.rgb(0, 0, 0)
2234
+ });
2235
+ state.currentPage.drawLine({
2236
+ start: { x: valueColStartX, y: pdfY + stFontSize + 3 },
2237
+ end: { x: valueRightX, y: pdfY + stFontSize + 3 },
2238
+ thickness: 0.75,
2239
+ color: pdfLib.rgb(0, 0, 0)
2240
+ });
2241
+ }
2242
+ if (label) {
2243
+ state.currentPage.drawText(label, {
2244
+ x: labelX,
2245
+ y: pdfY,
2246
+ size: stFontSize,
2247
+ font: state.fonts.bold,
2248
+ color: pdfLib.rgb(0, 0, 0)
2249
+ });
2250
+ }
2251
+ if (value) {
2252
+ const valueFont = state.fonts.bold;
2253
+ const valueWidth = valueFont.widthOfTextAtSize(value, stFontSize);
2254
+ state.currentPage.drawText(value, {
2255
+ x: valueRightX - valueWidth,
2256
+ y: pdfY,
2257
+ size: stFontSize,
2258
+ font: valueFont,
2259
+ color: pdfLib.rgb(0, 0, 0)
2260
+ });
2261
+ }
2262
+ state.y += stLineHeight;
2263
+ }
2264
+ state.y += 4;
2265
+ break;
2266
+ }
2267
+ case "repeatBlock":
2268
+ if (node.content) {
2269
+ for (const child of node.content) {
2270
+ processBlock(state, child, region);
2271
+ }
2272
+ }
2273
+ break;
1311
2274
  default:
1312
2275
  if (node.content) {
1313
- processInlineContent(state, node.content, 12);
2276
+ processInlineContent(state, node.content, 12, 0, region);
1314
2277
  }
1315
2278
  break;
1316
2279
  }
1317
2280
  }
2281
+ function renderWatermarksOnPage(page, fonts, watermarkNodes) {
2282
+ for (const wmNode of watermarkNodes) {
2283
+ const text = wmNode.attrs?.text || "";
2284
+ if (!text) continue;
2285
+ const opacity = parseFloat(wmNode.attrs?.opacity || "0.15");
2286
+ const angle = parseFloat(wmNode.attrs?.angle || "-45");
2287
+ const fontSize = 100;
2288
+ const { width, height } = page.getSize();
2289
+ const centerX = width / 2;
2290
+ const centerY = height / 2;
2291
+ const textWidth = fonts.bold.widthOfTextAtSize(text, fontSize);
2292
+ page.drawText(text, {
2293
+ x: centerX - textWidth / 2 * Math.cos(angle * Math.PI / 180),
2294
+ y: centerY - textWidth / 2 * Math.sin(angle * Math.PI / 180),
2295
+ size: fontSize,
2296
+ font: fonts.bold,
2297
+ color: pdfLib.rgb(0.5, 0.5, 0.5),
2298
+ opacity,
2299
+ rotate: pdfLib.degrees(angle)
2300
+ });
2301
+ }
2302
+ }
2303
+ function collectWatermarkNodes(content) {
2304
+ const nodes = [];
2305
+ for (const block of content.content || []) {
2306
+ if (block.type === "watermark") {
2307
+ nodes.push(block);
2308
+ }
2309
+ }
2310
+ return nodes;
2311
+ }
1318
2312
  function ensureFieldSuffix(fieldName, suffix) {
1319
2313
  const cleanName = fieldName.replace(/_signature$/i, "").replace(/_initials$/i, "").replace(/_date$/i, "");
1320
2314
  return `${cleanName}${suffix}`;
@@ -1543,6 +2537,7 @@ async function generatePdfFromContent(content, options = {}) {
1543
2537
  italic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaOblique),
1544
2538
  boldItalic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBoldOblique)
1545
2539
  };
2540
+ const watermarkNodes = collectWatermarkNodes(content);
1546
2541
  const state = {
1547
2542
  currentPage: firstPage,
1548
2543
  pageIndex: 0,
@@ -1550,13 +2545,30 @@ async function generatePdfFromContent(content, options = {}) {
1550
2545
  fonts,
1551
2546
  pdfDoc,
1552
2547
  fieldPositions: /* @__PURE__ */ new Map(),
1553
- drawFieldPlaceholders
2548
+ drawFieldPlaceholders,
2549
+ watermarkNodes
1554
2550
  };
2551
+ renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
1555
2552
  if (content.content) {
1556
2553
  for (const block of content.content) {
1557
2554
  processBlock(state, block);
1558
2555
  }
1559
2556
  }
2557
+ const allPages = pdfDoc.getPages();
2558
+ const totalPages = allPages.length;
2559
+ const pageNumFontSize = 9;
2560
+ for (let i = 0; i < totalPages; i++) {
2561
+ const page = allPages[i];
2562
+ const label = `Page ${i + 1} of ${totalPages}`;
2563
+ const labelWidth = fonts.bold.widthOfTextAtSize(label, pageNumFontSize);
2564
+ page.drawText(label, {
2565
+ x: PAGE_WIDTH - MARGIN - labelWidth,
2566
+ y: MARGIN / 2 - pageNumFontSize / 2,
2567
+ size: pageNumFontSize,
2568
+ font: fonts.bold,
2569
+ color: pdfLib.rgb(0, 0, 0)
2570
+ });
2571
+ }
1560
2572
  let fieldWarnings;
1561
2573
  if (embedFormFields && fields.length > 0) {
1562
2574
  const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
@@ -1570,32 +2582,259 @@ async function generatePdfFromContent(content, options = {}) {
1570
2582
  };
1571
2583
  }
1572
2584
 
1573
- // src/utils/pdf-preview.ts
1574
- async function pdfToImages(pdfBytes, scale = 2) {
1575
- let pdfjsLib;
1576
- try {
1577
- pdfjsLib = await import('pdfjs-dist');
1578
- } catch (err) {
1579
- throw new Error(formatError(
1580
- "Failed to load the PDF preview library \u2014 ensure pdfjs-dist is installed and the worker is accessible",
1581
- err
1582
- ));
1583
- }
1584
- if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
1585
- pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/build/pdf.worker.mjs";
1586
- }
1587
- let pdf;
1588
- try {
1589
- const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
1590
- pdf = await loadingTask.promise;
1591
- } catch (err) {
1592
- throw new Error(formatError("Failed to parse the generated PDF for preview", err));
1593
- }
1594
- const pages = [];
1595
- const pageErrors = [];
1596
- for (let i = 1; i <= pdf.numPages; i++) {
1597
- try {
1598
- const page = await pdf.getPage(i);
2585
+ // src/utils/template-pipeline.ts
2586
+ async function generatePdfFromTiptap(content, values) {
2587
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
2588
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
2589
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
2590
+ const result = await generatePdfFromContent(replaced);
2591
+ return { pdfBytes: result.pdfBytes };
2592
+ }
2593
+ async function generatePdfFromMarkdown(markdown, values) {
2594
+ const content = markdownToTiptap(markdown);
2595
+ return generatePdfFromTiptap(content, values);
2596
+ }
2597
+
2598
+ // src/utils/markdown-writer.ts
2599
+ function serializeFieldToken(attrs) {
2600
+ const parts = ["field"];
2601
+ if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
2602
+ if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
2603
+ if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
2604
+ if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
2605
+ if (attrs.required) parts.push(`required:true`);
2606
+ if (attrs.options) parts.push(`options:${attrs.options}`);
2607
+ if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
2608
+ if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
2609
+ if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
2610
+ if (attrs.multiline) parts.push(`multiline:true`);
2611
+ if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
2612
+ if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2613
+ return `{{${parts.join("|")}}}`;
2614
+ }
2615
+ function serializeVariableToken(attrs) {
2616
+ const parts = ["var"];
2617
+ if (attrs.varName) parts.push(`name:${attrs.varName}`);
2618
+ if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
2619
+ if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
2620
+ if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
2621
+ if (attrs.format) parts.push(`format:${attrs.format}`);
2622
+ return `{{${parts.join("|")}}}`;
2623
+ }
2624
+ function serializeInline(content) {
2625
+ if (!content) return "";
2626
+ let result = "";
2627
+ for (const node of content) {
2628
+ if (node.type === "fieldNode") {
2629
+ result += serializeFieldToken(node.attrs || {});
2630
+ continue;
2631
+ }
2632
+ if (node.type === "variableNode") {
2633
+ let token = serializeVariableToken(node.attrs || {});
2634
+ const varMarks = node.marks || [];
2635
+ const isBold = varMarks.some((m) => m.type === "bold");
2636
+ const isItalic = varMarks.some((m) => m.type === "italic");
2637
+ const isUnderline = varMarks.some((m) => m.type === "underline");
2638
+ if (isBold && isItalic) token = `***${token}***`;
2639
+ else if (isBold) token = `**${token}**`;
2640
+ else if (isItalic) token = `*${token}*`;
2641
+ if (isUnderline) token = `__${token}__`;
2642
+ result += token;
2643
+ continue;
2644
+ }
2645
+ if (node.type === "text") {
2646
+ let text = node.text || "";
2647
+ const marks = node.marks || [];
2648
+ for (const mark of marks) {
2649
+ if (mark.type === "bold") text = `**${text}**`;
2650
+ else if (mark.type === "italic") text = `*${text}*`;
2651
+ else if (mark.type === "underline") text = `__${text}__`;
2652
+ else if (mark.type === "code") text = `\`${text}\``;
2653
+ }
2654
+ result += text;
2655
+ }
2656
+ if (node.type === "hardBreak") {
2657
+ result += " \n";
2658
+ }
2659
+ }
2660
+ return result;
2661
+ }
2662
+ function serializeBlock(node) {
2663
+ switch (node.type) {
2664
+ case "heading": {
2665
+ const level = node.attrs?.level || 1;
2666
+ const prefix = "#".repeat(level);
2667
+ return `${prefix} ${serializeInline(node.content)}`;
2668
+ }
2669
+ case "paragraph": {
2670
+ const inline = serializeInline(node.content);
2671
+ return inline;
2672
+ }
2673
+ case "bulletList": {
2674
+ return (node.content || []).map((item) => {
2675
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2676
+ return `- ${inner}`;
2677
+ }).join("\n");
2678
+ }
2679
+ case "orderedList": {
2680
+ return (node.content || []).map((item, i) => {
2681
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2682
+ return `${i + 1}. ${inner}`;
2683
+ }).join("\n");
2684
+ }
2685
+ case "listItem": {
2686
+ return (node.content || []).map(serializeBlock).join("\n");
2687
+ }
2688
+ case "blockquote": {
2689
+ return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
2690
+ }
2691
+ case "codeBlock": {
2692
+ const text = serializeInline(node.content);
2693
+ return `\`\`\`
2694
+ ${text}
2695
+ \`\`\``;
2696
+ }
2697
+ case "horizontalRule": {
2698
+ return "---";
2699
+ }
2700
+ case "table": {
2701
+ const rows = node.content || [];
2702
+ const isBorderless = node.attrs?.borderless === true;
2703
+ const isSubtotals = node.attrs?.subtotals === true;
2704
+ const serializedRows = [];
2705
+ if (isBorderless && rows.length > 0) {
2706
+ const colCount = rows[0].content?.length || 0;
2707
+ const markerCell = isSubtotals ? "_" : "";
2708
+ const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
2709
+ const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
2710
+ serializedRows.push(markerRow);
2711
+ serializedRows.push(sep);
2712
+ }
2713
+ for (let ri = 0; ri < rows.length; ri++) {
2714
+ const row = rows[ri];
2715
+ const cells = (row.content || []).map((cell) => {
2716
+ const inner = (cell.content || []).map(serializeBlock).join("");
2717
+ return inner;
2718
+ });
2719
+ serializedRows.push(`| ${cells.join(" | ")} |`);
2720
+ if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
2721
+ const sep = cells.map(() => "------").join("|");
2722
+ serializedRows.push(`|${sep}|`);
2723
+ }
2724
+ }
2725
+ return serializedRows.join("\n");
2726
+ }
2727
+ case "tableRow": {
2728
+ const cells = (node.content || []).map((cell) => {
2729
+ const inner = (cell.content || []).map(serializeBlock).join("");
2730
+ return inner;
2731
+ });
2732
+ return `| ${cells.join(" | ")} |`;
2733
+ }
2734
+ case "tableHeader":
2735
+ case "tableCell": {
2736
+ return (node.content || []).map(serializeBlock).join("");
2737
+ }
2738
+ case "watermark": {
2739
+ const attrs = node.attrs || {};
2740
+ const parts = [];
2741
+ if (attrs.text) parts.push(`text:${attrs.text}`);
2742
+ if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
2743
+ if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
2744
+ return `:::watermark{${parts.join("|")}}`;
2745
+ }
2746
+ case "panel": {
2747
+ const attrs = node.attrs || {};
2748
+ const parts = [];
2749
+ if (attrs.title) parts.push(`title:${attrs.title}`);
2750
+ if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
2751
+ else if (attrs.border === "solid") parts.push(`border:solid`);
2752
+ if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
2753
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
2754
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2755
+ return `:::panel${attrStr}
2756
+ ${children}
2757
+ :::`;
2758
+ }
2759
+ case "repeatBlock": {
2760
+ const attrs = node.attrs || {};
2761
+ const parts = [];
2762
+ if (attrs.data) parts.push(`data:${attrs.data}`);
2763
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
2764
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2765
+ return `:::repeat${attrStr}
2766
+ ${children}
2767
+ :::`;
2768
+ }
2769
+ case "subtotalsBlock": {
2770
+ const children = (node.content || []).map(serializeBlock).join("\n");
2771
+ return `:::subtotals
2772
+ ${children}
2773
+ :::`;
2774
+ }
2775
+ case "columns": {
2776
+ const attrs = node.attrs || {};
2777
+ const splitVal = attrs.split || "50";
2778
+ const cols = (node.content || []).filter((c) => c.type === "column");
2779
+ const colBlocks = cols.map((col) => {
2780
+ const children = (col.content || []).map(serializeBlock).join("\n\n");
2781
+ const colAttrs = col.attrs || {};
2782
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
2783
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
2784
+ return `${colTag}
2785
+ ${children}
2786
+ :::`;
2787
+ });
2788
+ const padX = parseFloat(attrs.padX || "0") || 0;
2789
+ const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
2790
+ return `:::columns{${colsAttrs}}
2791
+ ${colBlocks.join("\n")}
2792
+ :::`;
2793
+ }
2794
+ case "column": {
2795
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2796
+ const colAttrs = node.attrs || {};
2797
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
2798
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
2799
+ return `${colTag}
2800
+ ${children}
2801
+ :::`;
2802
+ }
2803
+ default:
2804
+ return serializeInline(node.content);
2805
+ }
2806
+ }
2807
+ function tiptapToMarkdown(doc) {
2808
+ if (!doc.content) return "";
2809
+ return doc.content.map(serializeBlock).join("\n\n");
2810
+ }
2811
+
2812
+ // src/utils/pdf-preview.ts
2813
+ async function pdfToImages(pdfBytes, scale = 2) {
2814
+ let pdfjsLib;
2815
+ try {
2816
+ pdfjsLib = await import('pdfjs-dist');
2817
+ } catch (err) {
2818
+ throw new Error(formatError(
2819
+ "Failed to load the PDF preview library \u2014 ensure pdfjs-dist is installed and the worker is accessible",
2820
+ err
2821
+ ));
2822
+ }
2823
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
2824
+ pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/build/pdf.worker.mjs";
2825
+ }
2826
+ let pdf;
2827
+ try {
2828
+ const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
2829
+ pdf = await loadingTask.promise;
2830
+ } catch (err) {
2831
+ throw new Error(formatError("Failed to parse the generated PDF for preview", err));
2832
+ }
2833
+ const pages = [];
2834
+ const pageErrors = [];
2835
+ for (let i = 1; i <= pdf.numPages; i++) {
2836
+ try {
2837
+ const page = await pdf.getPage(i);
1599
2838
  const viewport = page.getViewport({ scale });
1600
2839
  const canvas = document.createElement("canvas");
1601
2840
  canvas.width = viewport.width;
@@ -1656,8 +2895,26 @@ function useDocumentGenerator(options = {}) {
1656
2895
  Placeholder__default.default.configure({
1657
2896
  placeholder: options.placeholder || "Start writing your document..."
1658
2897
  }),
2898
+ Table__default.default.extend({
2899
+ addAttributes() {
2900
+ return {
2901
+ ...this.parent?.(),
2902
+ borderless: { default: false },
2903
+ subtotals: { default: false }
2904
+ };
2905
+ }
2906
+ }).configure({ resizable: false }),
2907
+ TableRow__default.default,
2908
+ TableCell__default.default,
2909
+ TableHeader__default.default,
1659
2910
  FieldNode,
1660
- VariableNode
2911
+ VariableNode,
2912
+ PanelNode,
2913
+ ColumnsNode,
2914
+ ColumnNode,
2915
+ WatermarkNode,
2916
+ RepeatNode,
2917
+ SubtotalsNode
1661
2918
  ],
1662
2919
  content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
1663
2920
  onUpdate: ({ editor: ed }) => {
@@ -1761,10 +3018,9 @@ function useDocumentGenerator(options = {}) {
1761
3018
  async (values) => {
1762
3019
  if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
1763
3020
  const content = editor.getJSON();
1764
- const replacedContent = replaceVariablesInContent(content, values);
1765
- const result = await generatePdfFromContent(replacedContent);
1766
- const pages = await pdfToImages(result.pdfBytes);
1767
- return { pdfBytes: result.pdfBytes, pdfPages: pages };
3021
+ const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
3022
+ const pdfPages2 = await pdfToImages(pdfBytes2);
3023
+ return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
1768
3024
  },
1769
3025
  [editor]
1770
3026
  );
@@ -2918,15 +4174,47 @@ function FieldEditPopover({
2918
4174
  ] }) })
2919
4175
  ] });
2920
4176
  }
2921
- function labelToVarName(label) {
4177
+ var sizeClasses = {
4178
+ md: "px-3 py-1.5 text-xs font-medium",
4179
+ sm: "px-2 py-0.5 text-[10px] font-medium"
4180
+ };
4181
+ function ToggleGroup({
4182
+ value,
4183
+ onChange,
4184
+ options,
4185
+ size = "md",
4186
+ className
4187
+ }) {
4188
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
4189
+ "button",
4190
+ {
4191
+ className: cn(
4192
+ "rounded-md transition-colors",
4193
+ sizeClasses[size],
4194
+ value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
4195
+ ),
4196
+ onClick: () => onChange(opt.value),
4197
+ children: opt.label
4198
+ },
4199
+ opt.value
4200
+ )) });
4201
+ }
4202
+ function labelToVarName2(label) {
2922
4203
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2923
4204
  }
4205
+ function varNameToLabel(name) {
4206
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4207
+ }
2924
4208
  function VariableInsertPopover({
2925
4209
  open,
2926
4210
  onOpenChange,
2927
4211
  onInsert,
4212
+ predefinedVariables,
2928
4213
  children
2929
4214
  }) {
4215
+ const hasPredefined = !!predefinedVariables?.length;
4216
+ const [mode, setMode] = React12.useState(hasPredefined ? "existing" : "new");
4217
+ const [search, setSearch] = React12.useState("");
2930
4218
  const [varLabel, setVarLabel] = React12.useState("");
2931
4219
  const [varName, setVarName] = React12.useState("");
2932
4220
  const [varDefault, setVarDefault] = React12.useState("");
@@ -2936,7 +4224,9 @@ function VariableInsertPopover({
2936
4224
  setVarName("");
2937
4225
  setVarDefault("");
2938
4226
  setNameManuallyEdited(false);
2939
- }, []);
4227
+ setSearch("");
4228
+ setMode(hasPredefined ? "existing" : "new");
4229
+ }, [hasPredefined]);
2940
4230
  const handleOpenChange = React12.useCallback(
2941
4231
  (nextOpen) => {
2942
4232
  if (!nextOpen) reset();
@@ -2946,7 +4236,7 @@ function VariableInsertPopover({
2946
4236
  );
2947
4237
  React12.useEffect(() => {
2948
4238
  if (!nameManuallyEdited) {
2949
- setVarName(labelToVarName(varLabel));
4239
+ setVarName(labelToVarName2(varLabel));
2950
4240
  }
2951
4241
  }, [varLabel, nameManuallyEdited]);
2952
4242
  const handleInsert = React12.useCallback(() => {
@@ -2958,6 +4248,26 @@ function VariableInsertPopover({
2958
4248
  });
2959
4249
  handleOpenChange(false);
2960
4250
  }, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
4251
+ const handlePickPredefined = React12.useCallback(
4252
+ (pv) => {
4253
+ onInsert({
4254
+ varName: pv.varName,
4255
+ varLabel: pv.varLabel || varNameToLabel(pv.varName),
4256
+ varDefault: ""
4257
+ });
4258
+ handleOpenChange(false);
4259
+ },
4260
+ [onInsert, handleOpenChange]
4261
+ );
4262
+ const filteredVars = React12.useMemo(() => {
4263
+ if (!predefinedVariables?.length) return [];
4264
+ const q = search.toLowerCase();
4265
+ if (!q) return predefinedVariables;
4266
+ return predefinedVariables.filter((pv) => {
4267
+ const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
4268
+ return pv.varName.toLowerCase().includes(q) || label.includes(q);
4269
+ });
4270
+ }, [predefinedVariables, search]);
2961
4271
  return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
2962
4272
  /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children }),
2963
4273
  /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-72 p-0", align: "start", sideOffset: 8, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
@@ -2965,72 +4275,114 @@ function VariableInsertPopover({
2965
4275
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 14 }),
2966
4276
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Insert Variable" })
2967
4277
  ] }),
2968
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2969
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2970
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4278
+ hasPredefined && /* @__PURE__ */ jsxRuntime.jsx(
4279
+ ToggleGroup,
4280
+ {
4281
+ value: mode,
4282
+ onChange: setMode,
4283
+ options: [
4284
+ { value: "existing", label: "Existing" },
4285
+ { value: "new", label: "New" }
4286
+ ],
4287
+ size: "sm"
4288
+ }
4289
+ ),
4290
+ mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
4291
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
4292
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
2971
4293
  /* @__PURE__ */ jsxRuntime.jsx(
2972
4294
  Input,
2973
4295
  {
2974
- id: "var-label",
2975
- value: varLabel,
2976
- onChange: (e) => setVarLabel(e.target.value),
2977
- placeholder: "e.g. Company Name",
2978
- className: "h-8 text-xs"
4296
+ value: search,
4297
+ onChange: (e) => setSearch(e.target.value),
4298
+ placeholder: "Search variables...",
4299
+ className: "h-8 text-xs pl-7"
2979
4300
  }
2980
4301
  )
2981
4302
  ] }),
2982
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2983
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4303
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-48 overflow-y-auto -mx-1", children: filteredVars.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground px-2 py-3 text-center", children: "No variables found" }) : filteredVars.map((pv) => {
4304
+ const label = pv.varLabel || varNameToLabel(pv.varName);
4305
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4306
+ "button",
4307
+ {
4308
+ className: "w-full text-left px-2 py-1.5 rounded-md text-xs hover:bg-accent transition-colors flex flex-col gap-0.5",
4309
+ onClick: () => handlePickPredefined(pv),
4310
+ children: [
4311
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: label }),
4312
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
4313
+ ]
4314
+ },
4315
+ pv.varName
4316
+ );
4317
+ }) })
4318
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4319
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
4320
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4321
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4322
+ /* @__PURE__ */ jsxRuntime.jsx(
4323
+ Input,
4324
+ {
4325
+ id: "var-label",
4326
+ value: varLabel,
4327
+ onChange: (e) => setVarLabel(e.target.value),
4328
+ placeholder: "e.g. Company Name",
4329
+ className: "h-8 text-xs"
4330
+ }
4331
+ )
4332
+ ] }),
4333
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4334
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4335
+ /* @__PURE__ */ jsxRuntime.jsx(
4336
+ Input,
4337
+ {
4338
+ id: "var-name",
4339
+ value: varName,
4340
+ onChange: (e) => {
4341
+ setVarName(e.target.value);
4342
+ setNameManuallyEdited(true);
4343
+ },
4344
+ placeholder: "e.g. company_name",
4345
+ className: "h-8 text-xs font-mono"
4346
+ }
4347
+ ),
4348
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
4349
+ ] }),
4350
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4351
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
4352
+ /* @__PURE__ */ jsxRuntime.jsx(
4353
+ Input,
4354
+ {
4355
+ id: "var-default",
4356
+ value: varDefault,
4357
+ onChange: (e) => setVarDefault(e.target.value),
4358
+ placeholder: "Optional",
4359
+ className: "h-8 text-xs"
4360
+ }
4361
+ )
4362
+ ] })
4363
+ ] }),
4364
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
2984
4365
  /* @__PURE__ */ jsxRuntime.jsx(
2985
- Input,
4366
+ Button,
2986
4367
  {
2987
- id: "var-name",
2988
- value: varName,
2989
- onChange: (e) => {
2990
- setVarName(e.target.value);
2991
- setNameManuallyEdited(true);
2992
- },
2993
- placeholder: "e.g. company_name",
2994
- className: "h-8 text-xs font-mono"
4368
+ size: "sm",
4369
+ className: "h-8 flex-1 text-xs",
4370
+ onClick: handleInsert,
4371
+ disabled: !varLabel.trim() || !varName.trim(),
4372
+ children: "Insert"
2995
4373
  }
2996
4374
  ),
2997
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
2998
- ] }),
2999
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3000
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
3001
4375
  /* @__PURE__ */ jsxRuntime.jsx(
3002
- Input,
4376
+ Button,
3003
4377
  {
3004
- id: "var-default",
3005
- value: varDefault,
3006
- onChange: (e) => setVarDefault(e.target.value),
3007
- placeholder: "Optional",
3008
- className: "h-8 text-xs"
4378
+ variant: "outline",
4379
+ size: "sm",
4380
+ className: "h-8 text-xs",
4381
+ onClick: () => handleOpenChange(false),
4382
+ children: "Cancel"
3009
4383
  }
3010
4384
  )
3011
4385
  ] })
3012
- ] }),
3013
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
3014
- /* @__PURE__ */ jsxRuntime.jsx(
3015
- Button,
3016
- {
3017
- size: "sm",
3018
- className: "h-8 flex-1 text-xs",
3019
- onClick: handleInsert,
3020
- disabled: !varLabel.trim() || !varName.trim(),
3021
- children: "Insert"
3022
- }
3023
- ),
3024
- /* @__PURE__ */ jsxRuntime.jsx(
3025
- Button,
3026
- {
3027
- variant: "outline",
3028
- size: "sm",
3029
- className: "h-8 text-xs",
3030
- onClick: () => handleOpenChange(false),
3031
- children: "Cancel"
3032
- }
3033
- )
3034
4386
  ] })
3035
4387
  ] }) })
3036
4388
  ] });
@@ -3252,7 +4604,6 @@ function PreviewPanel({
3252
4604
  (f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
3253
4605
  );
3254
4606
  }, [positionedFields, currentPage, currentPageIndex]);
3255
- const showScrollbars = zoomLevel > 1;
3256
4607
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col h-full border border-border rounded-lg overflow-hidden bg-muted/20", className), children: [
3257
4608
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border px-2 py-1.5 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
3258
4609
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
@@ -3368,12 +4719,13 @@ function PreviewPanel({
3368
4719
  "div",
3369
4720
  {
3370
4721
  className: cn(
3371
- "border border-border rounded-lg bg-muted/30 shrink-0",
3372
- showScrollbars ? "overflow-auto" : "overflow-hidden"
4722
+ "border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
3373
4723
  ),
3374
4724
  style: {
3375
4725
  width: pageDisplaySize.viewportWidth,
3376
- height: pageDisplaySize.viewportHeight
4726
+ height: pageDisplaySize.viewportHeight,
4727
+ maxWidth: "100%",
4728
+ maxHeight: "100%"
3377
4729
  },
3378
4730
  children: /* @__PURE__ */ jsxRuntime.jsxs(
3379
4731
  "div",
@@ -3465,6 +4817,37 @@ function validateMarkdown(markdown) {
3465
4817
  searchFrom = closeIdx + 2;
3466
4818
  }
3467
4819
  }
4820
+ const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
4821
+ const DIRECTIVE_CLOSE_RE = /^:::$/;
4822
+ const directiveStack = [];
4823
+ for (let i = 0; i < lines.length; i++) {
4824
+ const trimmed = lines[i].trim();
4825
+ const lineNum = i + 1;
4826
+ const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
4827
+ if (openMatch) {
4828
+ const dtype = openMatch[1];
4829
+ if (dtype !== "watermark") {
4830
+ if (dtype === "col") {
4831
+ const parent = directiveStack[directiveStack.length - 1];
4832
+ if (!parent || parent.type !== "columns") {
4833
+ warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
4834
+ }
4835
+ }
4836
+ directiveStack.push({ type: dtype, line: lineNum });
4837
+ }
4838
+ continue;
4839
+ }
4840
+ if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
4841
+ if (directiveStack.length === 0) {
4842
+ warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
4843
+ } else {
4844
+ directiveStack.pop();
4845
+ }
4846
+ }
4847
+ }
4848
+ for (const open of directiveStack) {
4849
+ errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
4850
+ }
3468
4851
  let fields = [];
3469
4852
  let variables = [];
3470
4853
  try {
@@ -3498,31 +4881,6 @@ function validateMarkdown(markdown) {
3498
4881
  fields
3499
4882
  };
3500
4883
  }
3501
- var sizeClasses = {
3502
- md: "px-3 py-1.5 text-xs font-medium",
3503
- sm: "px-2 py-0.5 text-[10px] font-medium"
3504
- };
3505
- function ToggleGroup({
3506
- value,
3507
- onChange,
3508
- options,
3509
- size = "md",
3510
- className
3511
- }) {
3512
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
3513
- "button",
3514
- {
3515
- className: cn(
3516
- "rounded-md transition-colors",
3517
- sizeClasses[size],
3518
- value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
3519
- ),
3520
- onClick: () => onChange(opt.value),
3521
- children: opt.label
3522
- },
3523
- opt.value
3524
- )) });
3525
- }
3526
4884
  function parseCsv(text) {
3527
4885
  const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
3528
4886
  if (lines.length < 2) return [];
@@ -3572,7 +4930,10 @@ function GeneratePanel({
3572
4930
  editorContent,
3573
4931
  editorVariables,
3574
4932
  onGeneratePdf,
3575
- initialBulkData
4933
+ initialBulkData,
4934
+ initialVariableValues,
4935
+ exportFileName,
4936
+ templateMode
3576
4937
  }) {
3577
4938
  const [templateSource, setTemplateSource] = React12.useState("editor");
3578
4939
  const [importedMarkdown, setImportedMarkdown] = React12.useState(null);
@@ -3580,6 +4941,7 @@ function GeneratePanel({
3580
4941
  const [validationResult, setValidationResult] = React12.useState(null);
3581
4942
  const [mode, setMode] = React12.useState("single");
3582
4943
  const [variableValues, setVariableValues] = React12.useState({});
4944
+ const [variableSearch, setVariableSearch] = React12.useState("");
3583
4945
  const [bulkInputFormat, setBulkInputFormat] = React12.useState("json");
3584
4946
  const [bulkInput, setBulkInput] = React12.useState("");
3585
4947
  const [bulkData, setBulkData] = React12.useState(null);
@@ -3606,11 +4968,22 @@ function GeneratePanel({
3606
4968
  const col = pos - textBefore.lastIndexOf("\n");
3607
4969
  setJsonCursor({ line, col });
3608
4970
  }, []);
4971
+ const lockedVarNames = React12__namespace.default.useMemo(
4972
+ () => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
4973
+ [templateMode, initialVariableValues]
4974
+ );
3609
4975
  const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
3610
4976
  const hasVariables = activeVariables.length > 0;
3611
4977
  const hasEditorContent = editorMarkdown.trim().length > 0;
3612
4978
  const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
3613
4979
  const hasContent = hasImportedContent || hasEditorContent;
4980
+ const filteredVariables = React12__namespace.default.useMemo(() => {
4981
+ if (!variableSearch.trim()) return activeVariables;
4982
+ const q = variableSearch.toLowerCase();
4983
+ return activeVariables.filter(
4984
+ (v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
4985
+ );
4986
+ }, [activeVariables, variableSearch]);
3614
4987
  const getActiveContent = React12.useCallback(() => {
3615
4988
  if (templateSource === "imported" && importedMarkdown) {
3616
4989
  return markdownToTiptap(importedMarkdown);
@@ -3778,7 +5151,9 @@ function GeneratePanel({
3778
5151
  setPreviewError(null);
3779
5152
  try {
3780
5153
  const content = getActiveContent();
3781
- const replaced = replaceVariablesInContent(content, variableValues);
5154
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5155
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5156
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3782
5157
  const fields = extractFieldsFromContent(replaced);
3783
5158
  const result = await generatePdfFromContent(replaced);
3784
5159
  const pages = await pdfToImages(result.pdfBytes);
@@ -3799,7 +5174,9 @@ function GeneratePanel({
3799
5174
  setExportSuccess(null);
3800
5175
  try {
3801
5176
  const content = getActiveContent();
3802
- const replaced = replaceVariablesInContent(content, variableValues);
5177
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5178
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5179
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3803
5180
  const fields = extractFieldsFromContent(replaced);
3804
5181
  const result = await generatePdfFromContent(replaced, {
3805
5182
  drawFieldPlaceholders: false,
@@ -3807,7 +5184,7 @@ function GeneratePanel({
3807
5184
  fields
3808
5185
  });
3809
5186
  const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
3810
- const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
5187
+ const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
3811
5188
  if (onGeneratePdf) {
3812
5189
  onGeneratePdf(blob, fileName);
3813
5190
  } else {
@@ -3879,7 +5256,9 @@ function GeneratePanel({
3879
5256
  const allWarnings = [];
3880
5257
  for (let i = 0; i < bulkData.length; i++) {
3881
5258
  const values = bulkData[i];
3882
- const replaced = replaceVariablesInContent(content, values);
5259
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5260
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5261
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3883
5262
  const fields = extractFieldsFromContent(replaced);
3884
5263
  const result = await generatePdfFromContent(replaced, {
3885
5264
  drawFieldPlaceholders: false,
@@ -3934,13 +5313,15 @@ ${allWarnings.join("\n")}`);
3934
5313
  }, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
3935
5314
  React12__namespace.default.useEffect(() => {
3936
5315
  if (templateSource === "editor") {
3937
- const defaults = {};
5316
+ const defaults = initialVariableValues ? { ...initialVariableValues } : {};
3938
5317
  for (const v of editorVariables) {
3939
- defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5318
+ if (!defaults[v.varName]) {
5319
+ defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5320
+ }
3940
5321
  }
3941
5322
  setVariableValues(defaults);
3942
5323
  }
3943
- }, [editorVariables, templateSource]);
5324
+ }, [editorVariables, templateSource, initialVariableValues]);
3944
5325
  React12.useEffect(() => {
3945
5326
  setPreviewFresh(false);
3946
5327
  }, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
@@ -3952,8 +5333,8 @@ ${allWarnings.join("\n")}`);
3952
5333
  }, [exportSuccess]);
3953
5334
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
3954
5335
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
3955
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
3956
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5336
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
5337
+ !templateMode && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3957
5338
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
3958
5339
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium", children: "Template" }),
3959
5340
  templateSource === "imported" && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4046,10 +5427,10 @@ ${allWarnings.join("\n")}`);
4046
5427
  !hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
4047
5428
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 32, className: "mb-2 opacity-50" }),
4048
5429
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: "No template content" }),
4049
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs mt-1", children: "Import a .md template above or add content in the Editor tab." })
5430
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs mt-1", children: templateMode ? "No template content was provided." : "Import a .md template above or add content in the Editor tab." })
4050
5431
  ] }),
4051
5432
  hasVariables && hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border border-border rounded-lg overflow-hidden", children: [
4052
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
5433
+ !templateMode && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
4053
5434
  ToggleGroup,
4054
5435
  {
4055
5436
  value: mode,
@@ -4061,20 +5442,53 @@ ${allWarnings.join("\n")}`);
4061
5442
  }
4062
5443
  ) }),
4063
5444
  mode === "single" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
4064
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
4065
- activeVariables.map((v) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4066
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5445
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
5446
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
5447
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
5448
+ ] }),
5449
+ activeVariables.length > 5 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
5450
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
4067
5451
  /* @__PURE__ */ jsxRuntime.jsx(
4068
5452
  Input,
4069
5453
  {
4070
- id: `gen-${v.varName}`,
4071
- value: variableValues[v.varName] || "",
4072
- onChange: (e) => updateVariableValue(v.varName, e.target.value),
4073
- placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
4074
- className: "h-8 text-xs"
5454
+ value: variableSearch,
5455
+ onChange: (e) => setVariableSearch(e.target.value),
5456
+ placeholder: "Search variables...",
5457
+ className: "h-8 text-xs pl-8"
5458
+ }
5459
+ ),
5460
+ variableSearch && /* @__PURE__ */ jsxRuntime.jsx(
5461
+ "button",
5462
+ {
5463
+ type: "button",
5464
+ onClick: () => setVariableSearch(""),
5465
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
5466
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 12 })
4075
5467
  }
4076
5468
  )
4077
- ] }, v.varName))
5469
+ ] }),
5470
+ filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
5471
+ "No variables match \u201C",
5472
+ variableSearch,
5473
+ "\u201D"
5474
+ ] }),
5475
+ filteredVariables.map((v) => {
5476
+ const isLocked = lockedVarNames.has(v.varName);
5477
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5478
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5479
+ /* @__PURE__ */ jsxRuntime.jsx(
5480
+ Input,
5481
+ {
5482
+ id: `gen-${v.varName}`,
5483
+ value: variableValues[v.varName] || "",
5484
+ onChange: (e) => updateVariableValue(v.varName, e.target.value),
5485
+ placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
5486
+ disabled: isLocked,
5487
+ className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
5488
+ }
5489
+ )
5490
+ ] }, v.varName);
5491
+ })
4078
5492
  ] }),
4079
5493
  mode === "bulk" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
4080
5494
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
@@ -4263,56 +5677,60 @@ ${allWarnings.join("\n")}`);
4263
5677
  ] }),
4264
5678
  !hasVariables && hasContent && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-3 rounded-md bg-muted/30 border border-border", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: 'This template has no variables. You can still preview and export the PDF as-is, or add variables in the Editor tab using the "Insert Variable" button.' }) })
4265
5679
  ] }),
4266
- previewError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-1.5", children: [
4267
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4268
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive", children: previewError })
4269
- ] }) }) }),
4270
- exportError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-1.5", children: [
4271
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4272
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive", children: exportError })
4273
- ] }) }) }),
4274
- exportSuccess && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 rounded-md bg-emerald-500/10 border border-emerald-500/20", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
4275
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "text-emerald-600 shrink-0" }),
4276
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-emerald-700", children: exportSuccess })
4277
- ] }) }) }),
4278
- hasContent && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-border px-4 py-3 flex items-center gap-2 bg-gradient-to-b from-background to-muted/20", children: mode === "single" || !hasVariables ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4279
- /* @__PURE__ */ jsxRuntime.jsxs(
4280
- Button,
4281
- {
4282
- variant: "secondary",
4283
- onClick: handlePreview,
4284
- disabled: isGenerating,
4285
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4286
- children: [
4287
- isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
4288
- isGenerating ? "Generating..." : "Generate PDF"
4289
- ]
4290
- }
4291
- ),
4292
- /* @__PURE__ */ jsxRuntime.jsxs(
5680
+ hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-border px-4 py-3 flex items-center gap-2 bg-gradient-to-b from-background to-muted/20", children: [
5681
+ mode === "single" || !hasVariables ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5682
+ /* @__PURE__ */ jsxRuntime.jsxs(
5683
+ Button,
5684
+ {
5685
+ variant: "secondary",
5686
+ onClick: handlePreview,
5687
+ disabled: isGenerating,
5688
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
5689
+ children: [
5690
+ isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
5691
+ isGenerating ? "Generating..." : "Generate PDF"
5692
+ ]
5693
+ }
5694
+ ),
5695
+ /* @__PURE__ */ jsxRuntime.jsxs(
5696
+ Button,
5697
+ {
5698
+ onClick: handleExportSingle,
5699
+ disabled: !previewFresh || isExporting,
5700
+ className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
5701
+ children: [
5702
+ isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
5703
+ isExporting ? "Exporting..." : "Export PDF"
5704
+ ]
5705
+ }
5706
+ )
5707
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
4293
5708
  Button,
4294
5709
  {
4295
- onClick: handleExportSingle,
4296
- disabled: !previewFresh || isExporting,
4297
- className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
5710
+ onClick: handleBulkGenerate,
5711
+ disabled: isExporting || !bulkData || bulkData.length === 0,
5712
+ className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transition-all duration-200 text-sm",
4298
5713
  children: [
4299
5714
  isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
4300
- isExporting ? "Exporting..." : "Export PDF"
5715
+ isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4301
5716
  ]
4302
5717
  }
4303
- )
4304
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
4305
- Button,
4306
- {
4307
- onClick: handleBulkGenerate,
4308
- disabled: isExporting || !bulkData || bulkData.length === 0,
4309
- className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transition-all duration-200 text-sm",
4310
- children: [
4311
- isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
4312
- isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4313
- ]
4314
- }
4315
- ) })
5718
+ ),
5719
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
5720
+ previewError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
5721
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
5722
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: previewError })
5723
+ ] }),
5724
+ exportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
5725
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
5726
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportError })
5727
+ ] }),
5728
+ exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
5729
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
5730
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportSuccess })
5731
+ ] })
5732
+ ] })
5733
+ ] })
4316
5734
  ] }),
4317
5735
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
4318
5736
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -4490,7 +5908,13 @@ function DocumentGeneratorInner({
4490
5908
  showToolbar = true,
4491
5909
  showGenerateTab = true,
4492
5910
  onGeneratePdf,
4493
- initialBulkData
5911
+ initialBulkData,
5912
+ defaultTab = "editor",
5913
+ initialVariableValues,
5914
+ predefinedVariables,
5915
+ exportFileName,
5916
+ templateMode,
5917
+ onSaveTemplate
4494
5918
  }) {
4495
5919
  const {
4496
5920
  editor,
@@ -4517,7 +5941,7 @@ function DocumentGeneratorInner({
4517
5941
  placeholder,
4518
5942
  onChange
4519
5943
  });
4520
- const [activeTab, setActiveTab] = React12.useState("editor");
5944
+ const [activeTab, setActiveTab] = React12.useState(defaultTab);
4521
5945
  const [insertPopoverOpen, setInsertPopoverOpen] = React12.useState(false);
4522
5946
  const [insertVarPopoverOpen, setInsertVarPopoverOpen] = React12.useState(false);
4523
5947
  const [editFieldId, setEditFieldId] = React12.useState(null);
@@ -4600,6 +6024,21 @@ function DocumentGeneratorInner({
4600
6024
  setExportSuccess("Document exported successfully");
4601
6025
  }
4602
6026
  }, [exportDocument, onExport]);
6027
+ const handleSaveTemplate = React12.useCallback(() => {
6028
+ const md = exportMarkdown();
6029
+ if (!md.trim()) {
6030
+ setMdExportError("Editor content is empty");
6031
+ return;
6032
+ }
6033
+ const validation = validateMarkdown(md);
6034
+ if (!validation.valid) {
6035
+ setMdExportError(validation.errors.join("; "));
6036
+ return;
6037
+ }
6038
+ setMdExportError(null);
6039
+ onSaveTemplate?.(md);
6040
+ setExportSuccess("Template saved");
6041
+ }, [exportMarkdown, onSaveTemplate]);
4603
6042
  const handleExportMarkdown = React12.useCallback(() => {
4604
6043
  const md = exportMarkdown();
4605
6044
  if (!md.trim()) {
@@ -4616,10 +6055,10 @@ function DocumentGeneratorInner({
4616
6055
  const url = URL.createObjectURL(blob);
4617
6056
  const a = document.createElement("a");
4618
6057
  a.href = url;
4619
- a.download = "template.md";
6058
+ a.download = exportFileName ? `${exportFileName}.md` : "template.md";
4620
6059
  a.click();
4621
6060
  URL.revokeObjectURL(url);
4622
- }, [exportMarkdown]);
6061
+ }, [exportMarkdown, exportFileName]);
4623
6062
  React12.useEffect(() => {
4624
6063
  if (mdExportError) setMdExportError(null);
4625
6064
  }, [markdown]);
@@ -4655,6 +6094,7 @@ function DocumentGeneratorInner({
4655
6094
  open: insertVarPopoverOpen,
4656
6095
  onOpenChange: setInsertVarPopoverOpen,
4657
6096
  onInsert: handleInsertVariable,
6097
+ predefinedVariables,
4658
6098
  children: /* @__PURE__ */ jsxRuntime.jsxs(
4659
6099
  Button,
4660
6100
  {
@@ -4695,118 +6135,141 @@ function DocumentGeneratorInner({
4695
6135
  ] })
4696
6136
  ] })
4697
6137
  ] }),
4698
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
6138
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
4699
6139
  "flex flex-col flex-1 min-h-0",
4700
6140
  activeTab !== "editor" && "hidden"
6141
+ ), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
6142
+ "flex-1 min-h-0",
6143
+ showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4701
6144
  ), children: [
4702
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
4703
- "flex-1 min-h-0",
4704
- showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4705
- ), children: [
4706
- !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
4707
- showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
4708
- EditorToolbar,
4709
- {
4710
- editor,
4711
- insertFieldButton,
4712
- insertVariableButton,
4713
- onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4714
- }
4715
- ) }),
4716
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0", children: /* @__PURE__ */ jsxRuntime.jsx(
4717
- react.EditorContent,
4718
- {
4719
- editor,
4720
- className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4721
- }
4722
- ) })
4723
- ] }),
4724
- showPreview && !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
4725
- showPreview && /* @__PURE__ */ jsxRuntime.jsx(
4726
- PreviewPanel,
6145
+ !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
6146
+ showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
6147
+ EditorToolbar,
4727
6148
  {
4728
- pages: pdfPages,
4729
- isGenerating,
4730
- positionedFields,
4731
- headerLeft: editorCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(
4732
- Button,
4733
- {
4734
- variant: "ghost",
4735
- size: "sm",
4736
- className: "h-8 w-8 p-0",
4737
- onClick: () => setEditorCollapsed(false),
4738
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftOpen, { size: 14 })
4739
- }
4740
- ) : void 0,
4741
- className: "flex-1 border-0 rounded-none"
6149
+ editor,
6150
+ insertFieldButton,
6151
+ insertVariableButton,
6152
+ onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4742
6153
  }
4743
- )
4744
- ] }),
4745
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-t border-border bg-gradient-to-b from-background to-muted/20", children: [
4746
- /* @__PURE__ */ jsxRuntime.jsxs(
4747
- Button,
6154
+ ) }),
6155
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
6156
+ react.EditorContent,
4748
6157
  {
4749
- variant: "secondary",
4750
- onClick: generatePdf,
4751
- disabled: isGenerating || !editor,
4752
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4753
- children: [
4754
- isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
4755
- isGenerating ? "Generating..." : "Generate PDF"
4756
- ]
6158
+ editor,
6159
+ className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4757
6160
  }
4758
- ),
4759
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [
4760
- mdExportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4761
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
4762
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: mdExportError })
4763
- ] }),
4764
- generationError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4765
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
4766
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: generationError })
4767
- ] }),
4768
- fieldWarnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
4769
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
4770
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
4771
- fieldWarnings.length,
4772
- " field",
4773
- fieldWarnings.length !== 1 ? "s" : "",
4774
- " had warnings during generation"
4775
- ] })
4776
- ] }),
4777
- exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
4778
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
4779
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: exportSuccess })
4780
- ] })
4781
- ] }),
4782
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
6161
+ ) }),
6162
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-4 py-3 border-t border-border bg-gradient-to-b from-background to-muted/20", children: [
4783
6163
  /* @__PURE__ */ jsxRuntime.jsxs(
4784
6164
  Button,
4785
6165
  {
4786
- onClick: handleExportMarkdown,
4787
- disabled: !editor || !markdown.trim(),
4788
- className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6166
+ variant: "secondary",
6167
+ onClick: generatePdf,
6168
+ disabled: isGenerating || !editor,
6169
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4789
6170
  children: [
4790
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 16 }),
4791
- "Export MD"
6171
+ isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
6172
+ isGenerating ? "Generating..." : "Generate PDF"
4792
6173
  ]
4793
6174
  }
4794
6175
  ),
4795
- /* @__PURE__ */ jsxRuntime.jsxs(
6176
+ onSaveTemplate ? /* @__PURE__ */ jsxRuntime.jsxs(
4796
6177
  Button,
4797
6178
  {
4798
- onClick: handleExport,
4799
- disabled: isGenerating || !editor || pdfPages.length === 0,
6179
+ onClick: handleSaveTemplate,
6180
+ disabled: !editor || !markdown.trim(),
4800
6181
  className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4801
6182
  children: [
4802
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
4803
- exportButtonText
6183
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 16 }),
6184
+ exportButtonText || "Save Template"
4804
6185
  ]
4805
6186
  }
4806
- )
6187
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(Popover, { children: [
6188
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
6189
+ Button,
6190
+ {
6191
+ disabled: !editor || !markdown.trim() && pdfPages.length === 0,
6192
+ className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200 text-sm",
6193
+ children: [
6194
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
6195
+ "Export",
6196
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { size: 14 })
6197
+ ]
6198
+ }
6199
+ ) }),
6200
+ /* @__PURE__ */ jsxRuntime.jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
6201
+ /* @__PURE__ */ jsxRuntime.jsxs(
6202
+ "button",
6203
+ {
6204
+ onClick: handleExportMarkdown,
6205
+ disabled: !editor || !markdown.trim(),
6206
+ className: "flex w-full items-center gap-2 rounded-sm px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
6207
+ children: [
6208
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 16 }),
6209
+ "Export as MD"
6210
+ ]
6211
+ }
6212
+ ),
6213
+ /* @__PURE__ */ jsxRuntime.jsxs(
6214
+ "button",
6215
+ {
6216
+ onClick: handleExport,
6217
+ disabled: isGenerating || !editor || pdfPages.length === 0,
6218
+ className: "flex w-full items-center gap-2 rounded-sm px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
6219
+ children: [
6220
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
6221
+ "Export as PDF"
6222
+ ]
6223
+ }
6224
+ )
6225
+ ] })
6226
+ ] }),
6227
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6228
+ mdExportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6229
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6230
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: mdExportError })
6231
+ ] }),
6232
+ generationError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6233
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6234
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: generationError })
6235
+ ] }),
6236
+ fieldWarnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
6237
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6238
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
6239
+ fieldWarnings.length,
6240
+ " field",
6241
+ fieldWarnings.length !== 1 ? "s" : "",
6242
+ " had warnings during generation"
6243
+ ] })
6244
+ ] }),
6245
+ exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6246
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
6247
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: exportSuccess })
6248
+ ] })
6249
+ ] })
4807
6250
  ] })
4808
- ] })
4809
- ] }),
6251
+ ] }),
6252
+ showPreview && !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
6253
+ showPreview && /* @__PURE__ */ jsxRuntime.jsx(
6254
+ PreviewPanel,
6255
+ {
6256
+ pages: pdfPages,
6257
+ isGenerating,
6258
+ positionedFields,
6259
+ headerLeft: editorCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(
6260
+ Button,
6261
+ {
6262
+ variant: "ghost",
6263
+ size: "sm",
6264
+ className: "h-8 w-8 p-0",
6265
+ onClick: () => setEditorCollapsed(false),
6266
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftOpen, { size: 14 })
6267
+ }
6268
+ ) : void 0,
6269
+ className: "flex-1 border-0 rounded-none"
6270
+ }
6271
+ )
6272
+ ] }) }),
4810
6273
  showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsxRuntime.jsx(
4811
6274
  GeneratePanel,
4812
6275
  {
@@ -4814,7 +6277,10 @@ function DocumentGeneratorInner({
4814
6277
  editorContent: editor?.getJSON(),
4815
6278
  editorVariables: variables,
4816
6279
  onGeneratePdf,
4817
- initialBulkData
6280
+ initialBulkData,
6281
+ initialVariableValues,
6282
+ exportFileName,
6283
+ templateMode
4818
6284
  }
4819
6285
  ),
4820
6286
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4955,11 +6421,733 @@ function EditorPanel({
4955
6421
  ] });
4956
6422
  }
4957
6423
 
6424
+ // src/utils/xml-template-parser.ts
6425
+ function extractFieldRefs(expr) {
6426
+ const refs = [];
6427
+ const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
6428
+ let m;
6429
+ while ((m = re.exec(expr)) !== null) {
6430
+ if (m[2]) {
6431
+ refs.push(`${m[1]}.${m[2]}`);
6432
+ } else {
6433
+ refs.push(m[1]);
6434
+ }
6435
+ }
6436
+ return refs;
6437
+ }
6438
+ function fieldRefToVarName(ref, bandContext) {
6439
+ if (ref.includes(".")) {
6440
+ return labelToVarName(ref.replace(/\./g, "_"));
6441
+ }
6442
+ if (bandContext) {
6443
+ return labelToVarName(bandContext + "_" + ref);
6444
+ }
6445
+ return labelToVarName(ref);
6446
+ }
6447
+ function fieldRefToLabel(ref) {
6448
+ const parts = ref.split(".");
6449
+ const raw = parts[parts.length - 1];
6450
+ return raw.replace(/_/g, " ");
6451
+ }
6452
+ function resolveExpression(expr, registry, siblingLabel, bandContext) {
6453
+ const trimmed = expr.trim();
6454
+ const suppressZero = /<>\s*0/.test(trimmed);
6455
+ const szOpts = suppressZero ? { suppressZero: true } : void 0;
6456
+ const aggMatch = trimmed.match(
6457
+ /^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
6458
+ );
6459
+ if (aggMatch) {
6460
+ const fieldName = aggMatch[2];
6461
+ const varName = labelToVarName("workorder_" + fieldName);
6462
+ const label = fieldName.replace(/_/g, " ");
6463
+ return registry.register(varName, label, szOpts);
6464
+ }
6465
+ const concatSingle = trimmed.match(
6466
+ /^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6467
+ );
6468
+ if (concatSingle) {
6469
+ const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
6470
+ const varName = fieldRefToVarName(ref, bandContext);
6471
+ const label = fieldRefToLabel(ref);
6472
+ return registry.register(varName, label, szOpts);
6473
+ }
6474
+ const concatMulti = trimmed.match(
6475
+ /^concat\(/i
6476
+ );
6477
+ if (concatMulti) {
6478
+ const refs = extractFieldRefs(trimmed);
6479
+ if (refs.length > 0) {
6480
+ const uniqueRefs = [...new Set(refs)];
6481
+ const tokens = uniqueRefs.map((ref) => {
6482
+ const varName = fieldRefToVarName(ref, bandContext);
6483
+ const label = fieldRefToLabel(ref);
6484
+ return registry.register(varName, label, szOpts);
6485
+ });
6486
+ return tokens.join(" ");
6487
+ }
6488
+ }
6489
+ const toLongMatch = trimmed.match(
6490
+ /^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6491
+ );
6492
+ if (toLongMatch) {
6493
+ const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
6494
+ const varName = fieldRefToVarName(ref, bandContext);
6495
+ const label = fieldRefToLabel(ref);
6496
+ return registry.register(varName, label, szOpts);
6497
+ }
6498
+ const fmtMatch = trimmed.match(
6499
+ /^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
6500
+ );
6501
+ if (fmtMatch) {
6502
+ return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
6503
+ }
6504
+ const replaceMatch = trimmed.match(
6505
+ /^Replace\(\s*\[([^\]]+)\]/i
6506
+ );
6507
+ if (replaceMatch) {
6508
+ const ref = replaceMatch[1];
6509
+ const varName = fieldRefToVarName(ref, bandContext);
6510
+ const label = fieldRefToLabel(ref);
6511
+ return registry.register(varName, label, szOpts);
6512
+ }
6513
+ const iifLabelMatch = trimmed.match(
6514
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
6515
+ );
6516
+ if (iifLabelMatch) {
6517
+ return iifLabelMatch[1];
6518
+ }
6519
+ const iifLabelElse = trimmed.match(
6520
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
6521
+ );
6522
+ if (iifLabelElse) {
6523
+ return iifLabelElse[1];
6524
+ }
6525
+ const iifEmptyElse = trimmed.match(
6526
+ /^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
6527
+ );
6528
+ if (iifEmptyElse) {
6529
+ return iifEmptyElse[1];
6530
+ }
6531
+ const nestedIifStaticLabel = trimmed.match(
6532
+ /^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
6533
+ );
6534
+ if (nestedIifStaticLabel) {
6535
+ return nestedIifStaticLabel[1];
6536
+ }
6537
+ const iifValueMatch = trimmed.match(
6538
+ /^iif\s*\(/i
6539
+ );
6540
+ if (iifValueMatch) {
6541
+ const refs = extractFieldRefs(trimmed);
6542
+ if (refs.length > 0) {
6543
+ const uniqueRefs = [...new Set(refs)];
6544
+ const innerAggMatch = trimmed.match(
6545
+ /\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
6546
+ );
6547
+ if (innerAggMatch) {
6548
+ const fieldName = innerAggMatch[2];
6549
+ const varName2 = labelToVarName("workorder_" + fieldName);
6550
+ const label2 = fieldName.replace(/_/g, " ");
6551
+ return registry.register(varName2, label2, szOpts);
6552
+ }
6553
+ const ref = uniqueRefs[uniqueRefs.length - 1];
6554
+ const varName = fieldRefToVarName(ref, bandContext);
6555
+ const label = fieldRefToLabel(ref);
6556
+ return registry.register(varName, label, szOpts);
6557
+ }
6558
+ const staticMatch = trimmed.match(/'([^']+)'/);
6559
+ if (staticMatch) return staticMatch[1];
6560
+ return "";
6561
+ }
6562
+ const arithmeticRefs = extractFieldRefs(trimmed);
6563
+ if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
6564
+ if (siblingLabel) {
6565
+ const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
6566
+ const varName = labelToVarName(
6567
+ (bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
6568
+ );
6569
+ return registry.register(varName, cleanLabel, szOpts);
6570
+ }
6571
+ const uniqueRefs = [...new Set(arithmeticRefs)];
6572
+ const tokens = uniqueRefs.map((ref) => {
6573
+ const varName = fieldRefToVarName(ref, bandContext);
6574
+ const label = fieldRefToLabel(ref);
6575
+ return registry.register(varName, label, szOpts);
6576
+ });
6577
+ return tokens.join(" ");
6578
+ }
6579
+ const simpleMatch = trimmed.match(
6580
+ /^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
6581
+ );
6582
+ if (simpleMatch) {
6583
+ const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
6584
+ const varName = fieldRefToVarName(ref, bandContext);
6585
+ const label = fieldRefToLabel(ref);
6586
+ return registry.register(varName, label, szOpts);
6587
+ }
6588
+ if (arithmeticRefs.length >= 1) {
6589
+ const ref = arithmeticRefs[arithmeticRefs.length - 1];
6590
+ const varName = fieldRefToVarName(ref, bandContext);
6591
+ const label = fieldRefToLabel(ref);
6592
+ return registry.register(varName, label, szOpts);
6593
+ }
6594
+ return "";
6595
+ }
6596
+ var VariableRegistry = class {
6597
+ constructor() {
6598
+ __publicField(this, "map", /* @__PURE__ */ new Map());
6599
+ }
6600
+ register(varName, varLabel, options) {
6601
+ if (!this.map.has(varName)) {
6602
+ this.map.set(varName, { varName, varLabel, varDefault: "" });
6603
+ }
6604
+ const suppress = options?.suppressZero ? "|suppress:zero" : "";
6605
+ return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
6606
+ }
6607
+ getAll() {
6608
+ return Array.from(this.map.values());
6609
+ }
6610
+ };
6611
+ function parseLocationFloat(s) {
6612
+ if (!s) return { x: 0, y: 0 };
6613
+ const parts = s.split(",");
6614
+ return {
6615
+ x: parseFloat(parts[0]) || 0,
6616
+ y: parseFloat(parts[1]) || 0
6617
+ };
6618
+ }
6619
+ function parseSizeF(s) {
6620
+ if (!s) return { w: 0, h: 0 };
6621
+ const parts = s.split(",");
6622
+ return {
6623
+ w: parseFloat(parts[0]) || 0,
6624
+ h: parseFloat(parts[1]) || 0
6625
+ };
6626
+ }
6627
+ function extractControls(bandEl) {
6628
+ const controlsEl = bandEl.querySelector(":scope > Controls");
6629
+ if (!controlsEl) return [];
6630
+ const items = [];
6631
+ for (const child of Array.from(controlsEl.children)) {
6632
+ items.push(extractControl(child));
6633
+ }
6634
+ return items;
6635
+ }
6636
+ function extractControl(el) {
6637
+ const ctrlType = el.getAttribute("ControlType") || "";
6638
+ const name = el.getAttribute("Name") || "";
6639
+ const text = el.getAttribute("Text") || "";
6640
+ const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
6641
+ const size = parseSizeF(el.getAttribute("SizeF"));
6642
+ const formatString = el.getAttribute("TextFormatString") || null;
6643
+ let expression = null;
6644
+ const bindingsEl = el.querySelector(":scope > ExpressionBindings");
6645
+ if (bindingsEl) {
6646
+ for (const item of Array.from(bindingsEl.children)) {
6647
+ if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
6648
+ expression = item.getAttribute("Expression");
6649
+ break;
6650
+ }
6651
+ }
6652
+ }
6653
+ const children = [];
6654
+ if (ctrlType === "XRPanel") {
6655
+ const panelControls = extractControls(el);
6656
+ children.push(...panelControls);
6657
+ }
6658
+ return {
6659
+ name,
6660
+ type: ctrlType,
6661
+ text,
6662
+ x: loc.x,
6663
+ y: loc.y,
6664
+ width: size.w,
6665
+ expression,
6666
+ formatString,
6667
+ children
6668
+ };
6669
+ }
6670
+ function groupByRow(controls, threshold = 8) {
6671
+ const sorted = [...controls].sort((a, b) => a.y - b.y);
6672
+ const rows = [];
6673
+ for (const ctrl of sorted) {
6674
+ const lastRow = rows[rows.length - 1];
6675
+ if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
6676
+ lastRow.controls.push(ctrl);
6677
+ } else {
6678
+ rows.push({ y: ctrl.y, controls: [ctrl] });
6679
+ }
6680
+ }
6681
+ for (const row of rows) {
6682
+ row.controls.sort((a, b) => a.x - b.x);
6683
+ }
6684
+ return rows;
6685
+ }
6686
+ function isStaticLabel(ctrl) {
6687
+ return ctrl.type === "XRLabel" && !ctrl.expression;
6688
+ }
6689
+ function isBound(ctrl) {
6690
+ return ctrl.type === "XRLabel" && !!ctrl.expression;
6691
+ }
6692
+ function computeSplitPercent(leftControls, rightControls, contentWidth) {
6693
+ const leftExtent = leftControls.reduce(
6694
+ (max, c) => Math.max(max, c.x + c.width),
6695
+ 0
6696
+ );
6697
+ const rightMinX = rightControls.reduce(
6698
+ (min, c) => Math.min(min, c.x),
6699
+ contentWidth
6700
+ );
6701
+ const rightMaxExtent = rightControls.reduce(
6702
+ (max, c) => Math.max(max, c.x + c.width),
6703
+ 0
6704
+ );
6705
+ const rightExtent = rightMaxExtent - rightMinX;
6706
+ if (leftExtent + rightExtent === 0) return 50;
6707
+ return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
6708
+ }
6709
+ function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
6710
+ const lines = [];
6711
+ const leftControls = controls.filter((c) => c.x < 437);
6712
+ const rightControls = controls.filter((c) => c.x >= 437);
6713
+ const leftEntries = [];
6714
+ if (leftControls.length > 0) {
6715
+ const leftRows = groupByRow(leftControls);
6716
+ for (const row of leftRows) {
6717
+ const parts = [];
6718
+ for (const ctrl of row.controls) {
6719
+ if (isBound(ctrl)) {
6720
+ let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6721
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
6722
+ resolved = resolved.replace("}}", "|format:phone}}");
6723
+ }
6724
+ if (resolved) parts.push(resolved);
6725
+ } else if (isStaticLabel(ctrl)) {
6726
+ const text = ctrl.text.trim();
6727
+ if (text && !text.match(/^label\d+$/)) {
6728
+ parts.push(text);
6729
+ }
6730
+ }
6731
+ }
6732
+ if (parts.length > 0) {
6733
+ leftEntries.push(parts.join(" "));
6734
+ }
6735
+ }
6736
+ }
6737
+ const rightEntries = [];
6738
+ if (rightControls.length > 0) {
6739
+ const rightRows = groupByRow(rightControls);
6740
+ for (const row of rightRows) {
6741
+ let label = "";
6742
+ let value = "";
6743
+ for (const ctrl of row.controls) {
6744
+ if (isStaticLabel(ctrl)) {
6745
+ const text = ctrl.text.trim();
6746
+ if (text && !text.match(/^label\d+$/)) {
6747
+ label = text;
6748
+ }
6749
+ } else if (isBound(ctrl)) {
6750
+ value = resolveExpression(ctrl.expression, registry, label, "workorder");
6751
+ if (ctrl.expression) {
6752
+ const refs = extractFieldRefs(ctrl.expression);
6753
+ if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
6754
+ value = value.replace("}}", "|suppress:zero}}");
6755
+ }
6756
+ }
6757
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
6758
+ value = value.replace("}}", "|format:phone}}");
6759
+ }
6760
+ }
6761
+ }
6762
+ if (label || value) {
6763
+ rightEntries.push({ label, value, y: row.y });
6764
+ }
6765
+ }
6766
+ }
6767
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
6768
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
6769
+ lines.push(`:::columns{split:${outerSplit}}`);
6770
+ const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
6771
+ const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
6772
+ const xmlLineHeight = 17;
6773
+ const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
6774
+ const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
6775
+ const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
6776
+ lines.push(leftColTag);
6777
+ for (const entry of leftEntries) {
6778
+ lines.push(entry);
6779
+ }
6780
+ lines.push(":::");
6781
+ const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
6782
+ lines.push(rightColTag);
6783
+ if (rightEntries.length > 0) {
6784
+ lines.push("| | |");
6785
+ lines.push("|---|---|");
6786
+ let prevY = -Infinity;
6787
+ for (const entry of rightEntries) {
6788
+ if (prevY > -Infinity && entry.y - prevY > 25) {
6789
+ lines.push("| | |");
6790
+ }
6791
+ const label = entry.label || "";
6792
+ const colon = entry.label ? ":" : "";
6793
+ const value = entry.value ? ` ${entry.value}` : "";
6794
+ lines.push(`| ${label} | ${colon}${value} |`);
6795
+ prevY = entry.y;
6796
+ }
6797
+ }
6798
+ lines.push(":::");
6799
+ lines.push(":::");
6800
+ lines.push("");
6801
+ }
6802
+ return lines.join("\n");
6803
+ }
6804
+ function renderReportHeaderBand(controls, registry, contentWidth) {
6805
+ const lines = [];
6806
+ const leftControls = controls.filter((c) => c.x < 437);
6807
+ const rightControls = controls.filter((c) => c.x >= 437);
6808
+ const leftEntries = [];
6809
+ if (leftControls.length > 0) {
6810
+ const leftRows = groupByRow(leftControls);
6811
+ for (const row of leftRows) {
6812
+ const parts = [];
6813
+ for (const ctrl of row.controls) {
6814
+ if (isBound(ctrl)) {
6815
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6816
+ if (resolved) parts.push(resolved);
6817
+ } else if (isStaticLabel(ctrl)) {
6818
+ const text = ctrl.text.trim();
6819
+ if (text && !text.match(/^label\d+$/)) {
6820
+ parts.push(text);
6821
+ }
6822
+ }
6823
+ }
6824
+ if (parts.length > 0) {
6825
+ leftEntries.push(parts.join(" "));
6826
+ }
6827
+ }
6828
+ }
6829
+ const rightEntries = [];
6830
+ if (rightControls.length > 0) {
6831
+ const rightRows = groupByRow(rightControls);
6832
+ for (const row of rightRows) {
6833
+ let label = "";
6834
+ let value = "";
6835
+ for (const ctrl of row.controls) {
6836
+ if (isStaticLabel(ctrl)) {
6837
+ const text = ctrl.text.trim();
6838
+ if (text && !text.match(/^label\d+$/)) {
6839
+ label = text;
6840
+ }
6841
+ } else if (isBound(ctrl)) {
6842
+ value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6843
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
6844
+ value = value.replace("}}", "|format:phone}}");
6845
+ }
6846
+ }
6847
+ }
6848
+ if (label || value) {
6849
+ rightEntries.push({ label, value });
6850
+ }
6851
+ }
6852
+ }
6853
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
6854
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
6855
+ lines.push(`:::columns{split:${outerSplit}}`);
6856
+ lines.push(":::col");
6857
+ for (const entry of leftEntries) {
6858
+ lines.push(entry);
6859
+ }
6860
+ lines.push(":::");
6861
+ lines.push(":::col");
6862
+ lines.push("| | |");
6863
+ lines.push("|---|---|");
6864
+ for (const entry of rightEntries) {
6865
+ const label = entry.label || "";
6866
+ const value = entry.value ? ` ${entry.value}` : "";
6867
+ lines.push(`| ${label} |${value} |`);
6868
+ }
6869
+ lines.push(":::");
6870
+ lines.push(":::");
6871
+ lines.push("");
6872
+ }
6873
+ return lines.join("\n");
6874
+ }
6875
+ function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
6876
+ const lines = [];
6877
+ const headerTexts = [];
6878
+ for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
6879
+ if (isStaticLabel(ctrl)) {
6880
+ const text = ctrl.text.trim();
6881
+ if (text) headerTexts.push(text);
6882
+ }
6883
+ }
6884
+ const titleText = headerTexts.join(" ");
6885
+ lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
6886
+ lines.push(":::");
6887
+ lines.push(":::repeat{data:operations}");
6888
+ const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
6889
+ const detailParts = [];
6890
+ for (const ctrl of sortedDetail) {
6891
+ if (isBound(ctrl)) {
6892
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
6893
+ if (resolved) detailParts.push(resolved);
6894
+ }
6895
+ }
6896
+ if (detailParts.length > 0) {
6897
+ if (detailParts.length >= 2) {
6898
+ lines.push(":::columns{split:12}");
6899
+ lines.push(":::col");
6900
+ lines.push(`**${detailParts[0]}**`);
6901
+ lines.push(":::");
6902
+ lines.push(":::col");
6903
+ lines.push(`**${detailParts.slice(1).join(" ")}**`);
6904
+ lines.push(":::");
6905
+ lines.push(":::");
6906
+ } else {
6907
+ lines.push(`**${detailParts[0]}**`);
6908
+ }
6909
+ lines.push("");
6910
+ }
6911
+ if (groupFooterControls.length > 0) {
6912
+ const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
6913
+ if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
6914
+ }
6915
+ lines.push("---");
6916
+ lines.push(":::");
6917
+ lines.push("");
6918
+ return lines.join("\n");
6919
+ }
6920
+ function renderGroupFooterBand(controls, registry, contentWidth) {
6921
+ const rows = groupByRow(controls);
6922
+ const entries = [];
6923
+ for (const row of rows) {
6924
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
6925
+ if (labels.length === 0) continue;
6926
+ let label = "";
6927
+ let value = "";
6928
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
6929
+ for (const ctrl of sorted) {
6930
+ if (isStaticLabel(ctrl)) {
6931
+ const text = ctrl.text.trim();
6932
+ if (text && !text.match(/^label\d+$/)) {
6933
+ label = text;
6934
+ }
6935
+ } else if (isBound(ctrl)) {
6936
+ const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
6937
+ if (resolved) {
6938
+ if (!resolved.startsWith("{{var|")) {
6939
+ if (!label) label = resolved;
6940
+ } else {
6941
+ value = resolved;
6942
+ }
6943
+ }
6944
+ }
6945
+ }
6946
+ if (label && value) {
6947
+ entries.push({ label, value });
6948
+ }
6949
+ }
6950
+ if (entries.length === 0) return "";
6951
+ const lines = [];
6952
+ lines.push(":::subtotals");
6953
+ for (const entry of entries) {
6954
+ lines.push(`**${entry.label}** ${entry.value}`);
6955
+ }
6956
+ lines.push(":::");
6957
+ lines.push("");
6958
+ return lines.join("\n");
6959
+ }
6960
+ function renderReportFooterBand(controls, registry) {
6961
+ const lines = [];
6962
+ const panel = controls.find((c) => c.type === "XRPanel");
6963
+ const thankYouLabel = controls.find(
6964
+ (c) => c.type === "XRLabel" && c.text.includes("Thank You")
6965
+ );
6966
+ const priceRows = [];
6967
+ if (panel && panel.children.length > 0) {
6968
+ const rows = groupByRow(panel.children, 15);
6969
+ for (const row of rows) {
6970
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
6971
+ if (labels.length === 0) continue;
6972
+ let label = "";
6973
+ let value = "";
6974
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
6975
+ for (const ctrl of sorted) {
6976
+ if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
6977
+ continue;
6978
+ }
6979
+ const isLabelPos = ctrl.x < 150;
6980
+ if (isStaticLabel(ctrl)) {
6981
+ const text = ctrl.text.trim();
6982
+ if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
6983
+ label = text;
6984
+ }
6985
+ } else if (isBound(ctrl)) {
6986
+ const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
6987
+ if (resolved) {
6988
+ if (isLabelPos) {
6989
+ if (!label) label = resolved;
6990
+ } else {
6991
+ if (!value) value = resolved;
6992
+ }
6993
+ }
6994
+ }
6995
+ }
6996
+ if (label && value) {
6997
+ priceRows.push({ label, value });
6998
+ } else if (value) {
6999
+ priceRows.push({ label: "", value });
7000
+ }
7001
+ }
7002
+ }
7003
+ lines.push(":::columns{split:40|padX:20}");
7004
+ lines.push(":::col{padTop:1}");
7005
+ if (thankYouLabel) {
7006
+ lines.push(`## ***${thankYouLabel.text.trim()}***`);
7007
+ }
7008
+ lines.push(":::");
7009
+ lines.push(":::col{padTop:1}");
7010
+ lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
7011
+ if (priceRows.length > 0) {
7012
+ lines.push(":::subtotals");
7013
+ for (const row of priceRows) {
7014
+ const label = row.label ? `**${row.label}**` : "";
7015
+ lines.push(`${label} ${row.value}`);
7016
+ }
7017
+ lines.push(":::");
7018
+ }
7019
+ lines.push(":::");
7020
+ lines.push(":::");
7021
+ lines.push(":::");
7022
+ lines.push("");
7023
+ return lines.join("\n");
7024
+ }
7025
+ function parseXmlTemplate(xmlString) {
7026
+ const warnings = [];
7027
+ const registry = new VariableRegistry();
7028
+ const parser = new DOMParser();
7029
+ const doc = parser.parseFromString(xmlString, "text/xml");
7030
+ const parseError = doc.querySelector("parsererror");
7031
+ if (parseError) {
7032
+ return {
7033
+ markdown: "",
7034
+ variables: [],
7035
+ reportName: "",
7036
+ warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
7037
+ };
7038
+ }
7039
+ const root = doc.documentElement;
7040
+ const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
7041
+ const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
7042
+ const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
7043
+ const marginParts = marginsStr.split(",").map(Number);
7044
+ const leftMargin = marginParts[0] || 50;
7045
+ const rightMargin = marginParts[1] || 50;
7046
+ const contentWidth = pageWidth - leftMargin - rightMargin;
7047
+ const calculatedFieldNames = /* @__PURE__ */ new Set();
7048
+ const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
7049
+ if (calcFieldsEl) {
7050
+ for (const item of Array.from(calcFieldsEl.children)) {
7051
+ const name = item.getAttribute("Name");
7052
+ if (name) calculatedFieldNames.add(name);
7053
+ }
7054
+ }
7055
+ const markdownSections = [];
7056
+ const watermarkEl = root.querySelector("Watermarks > Item1");
7057
+ if (watermarkEl) {
7058
+ const wmText = watermarkEl.getAttribute("Text");
7059
+ if (wmText) {
7060
+ markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
7061
+ markdownSections.push("");
7062
+ }
7063
+ }
7064
+ const bandsEl = root.querySelector(":scope > Bands");
7065
+ if (!bandsEl) {
7066
+ warnings.push("No <Bands> element found in the XML");
7067
+ return {
7068
+ markdown: "",
7069
+ variables: registry.getAll(),
7070
+ reportName,
7071
+ warnings
7072
+ };
7073
+ }
7074
+ let topMarginControls = [];
7075
+ let reportHeaderControls = [];
7076
+ let pageHeaderControls = [];
7077
+ let detailControls = [];
7078
+ let groupFooterControls = [];
7079
+ let reportFooterControls = [];
7080
+ for (const bandItem of Array.from(bandsEl.children)) {
7081
+ const ctrlType = bandItem.getAttribute("ControlType") || "";
7082
+ if (ctrlType === "TopMarginBand") {
7083
+ topMarginControls = extractControls(bandItem);
7084
+ } else if (ctrlType === "ReportHeaderBand") {
7085
+ reportHeaderControls = extractControls(bandItem);
7086
+ } else if (ctrlType === "PageHeaderBand") {
7087
+ pageHeaderControls = extractControls(bandItem);
7088
+ } else if (ctrlType === "DetailReportBand") {
7089
+ const nestedBands = bandItem.querySelector(":scope > Bands");
7090
+ if (nestedBands) {
7091
+ for (const nested of Array.from(nestedBands.children)) {
7092
+ const nestedType = nested.getAttribute("ControlType") || "";
7093
+ if (nestedType === "DetailBand") {
7094
+ detailControls = extractControls(nested);
7095
+ } else if (nestedType === "GroupFooterBand") {
7096
+ groupFooterControls = extractControls(nested);
7097
+ }
7098
+ }
7099
+ }
7100
+ } else if (ctrlType === "ReportFooterBand") {
7101
+ reportFooterControls = extractControls(bandItem);
7102
+ }
7103
+ }
7104
+ if (topMarginControls.length > 0) {
7105
+ markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
7106
+ }
7107
+ if (reportHeaderControls.length > 0) {
7108
+ markdownSections.push(
7109
+ renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
7110
+ );
7111
+ }
7112
+ if (pageHeaderControls.length > 0 || detailControls.length > 0) {
7113
+ markdownSections.push(
7114
+ renderOperationsSection(
7115
+ pageHeaderControls,
7116
+ detailControls,
7117
+ groupFooterControls,
7118
+ registry)
7119
+ );
7120
+ }
7121
+ if (reportFooterControls.length > 0) {
7122
+ markdownSections.push(
7123
+ renderReportFooterBand(reportFooterControls, registry)
7124
+ );
7125
+ }
7126
+ const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
7127
+ return {
7128
+ markdown,
7129
+ variables: registry.getAll(),
7130
+ reportName,
7131
+ warnings
7132
+ };
7133
+ }
7134
+
4958
7135
  exports.DocumentGenerator = DocumentGenerator;
4959
7136
  exports.EditorPanel = EditorPanel;
4960
7137
  exports.FormFieldType = FormFieldType;
4961
7138
  exports.PreviewPanel = PreviewPanel;
7139
+ exports.RepeatNode = RepeatNode;
7140
+ exports.SubtotalsNode = SubtotalsNode;
4962
7141
  exports.ThemeProvider = ThemeProvider;
7142
+ exports.expandRepeatContent = expandRepeatContent;
7143
+ exports.extractVariablesFromContent = extractVariablesFromContent;
7144
+ exports.generatePdfFromMarkdown = generatePdfFromMarkdown;
7145
+ exports.generatePdfFromTiptap = generatePdfFromTiptap;
7146
+ exports.isZeroLike = isZeroLike;
7147
+ exports.markdownToTiptap = markdownToTiptap;
7148
+ exports.parseXmlTemplate = parseXmlTemplate;
7149
+ exports.suppressZeroContent = suppressZeroContent;
7150
+ exports.tiptapToMarkdown = tiptapToMarkdown;
4963
7151
  exports.useDocumentGenerator = useDocumentGenerator;
4964
7152
  exports.useTheme = useTheme;
4965
7153
  //# sourceMappingURL=index.js.map