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

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,263 @@ 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 fields = extractFieldsFromContent(replaced);
2591
+ const result = await generatePdfFromContent(replaced, {
2592
+ embedFormFields: fields.length > 0,
2593
+ fields
2594
+ });
2595
+ return { pdfBytes: result.pdfBytes };
2596
+ }
2597
+ async function generatePdfFromMarkdown(markdown, values) {
2598
+ const content = markdownToTiptap(markdown);
2599
+ return generatePdfFromTiptap(content, values);
2600
+ }
2601
+
2602
+ // src/utils/markdown-writer.ts
2603
+ function serializeFieldToken(attrs) {
2604
+ const parts = ["field"];
2605
+ if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
2606
+ if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
2607
+ if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
2608
+ if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
2609
+ if (attrs.required) parts.push(`required:true`);
2610
+ if (attrs.options) parts.push(`options:${attrs.options}`);
2611
+ if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
2612
+ if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
2613
+ if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
2614
+ if (attrs.multiline) parts.push(`multiline:true`);
2615
+ if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
2616
+ if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2617
+ return `{{${parts.join("|")}}}`;
2618
+ }
2619
+ function serializeVariableToken(attrs) {
2620
+ const parts = ["var"];
2621
+ if (attrs.varName) parts.push(`name:${attrs.varName}`);
2622
+ if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
2623
+ if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
2624
+ if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
2625
+ if (attrs.format) parts.push(`format:${attrs.format}`);
2626
+ return `{{${parts.join("|")}}}`;
2627
+ }
2628
+ function serializeInline(content) {
2629
+ if (!content) return "";
2630
+ let result = "";
2631
+ for (const node of content) {
2632
+ if (node.type === "fieldNode") {
2633
+ result += serializeFieldToken(node.attrs || {});
2634
+ continue;
2635
+ }
2636
+ if (node.type === "variableNode") {
2637
+ let token = serializeVariableToken(node.attrs || {});
2638
+ const varMarks = node.marks || [];
2639
+ const isBold = varMarks.some((m) => m.type === "bold");
2640
+ const isItalic = varMarks.some((m) => m.type === "italic");
2641
+ const isUnderline = varMarks.some((m) => m.type === "underline");
2642
+ if (isBold && isItalic) token = `***${token}***`;
2643
+ else if (isBold) token = `**${token}**`;
2644
+ else if (isItalic) token = `*${token}*`;
2645
+ if (isUnderline) token = `__${token}__`;
2646
+ result += token;
2647
+ continue;
2648
+ }
2649
+ if (node.type === "text") {
2650
+ let text = node.text || "";
2651
+ const marks = node.marks || [];
2652
+ for (const mark of marks) {
2653
+ if (mark.type === "bold") text = `**${text}**`;
2654
+ else if (mark.type === "italic") text = `*${text}*`;
2655
+ else if (mark.type === "underline") text = `__${text}__`;
2656
+ else if (mark.type === "code") text = `\`${text}\``;
2657
+ }
2658
+ result += text;
2659
+ }
2660
+ if (node.type === "hardBreak") {
2661
+ result += " \n";
2662
+ }
2663
+ }
2664
+ return result;
2665
+ }
2666
+ function serializeBlock(node) {
2667
+ switch (node.type) {
2668
+ case "heading": {
2669
+ const level = node.attrs?.level || 1;
2670
+ const prefix = "#".repeat(level);
2671
+ return `${prefix} ${serializeInline(node.content)}`;
2672
+ }
2673
+ case "paragraph": {
2674
+ const inline = serializeInline(node.content);
2675
+ return inline;
2676
+ }
2677
+ case "bulletList": {
2678
+ return (node.content || []).map((item) => {
2679
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2680
+ return `- ${inner}`;
2681
+ }).join("\n");
2682
+ }
2683
+ case "orderedList": {
2684
+ return (node.content || []).map((item, i) => {
2685
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2686
+ return `${i + 1}. ${inner}`;
2687
+ }).join("\n");
2688
+ }
2689
+ case "listItem": {
2690
+ return (node.content || []).map(serializeBlock).join("\n");
2691
+ }
2692
+ case "blockquote": {
2693
+ return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
2694
+ }
2695
+ case "codeBlock": {
2696
+ const text = serializeInline(node.content);
2697
+ return `\`\`\`
2698
+ ${text}
2699
+ \`\`\``;
2700
+ }
2701
+ case "horizontalRule": {
2702
+ return "---";
2703
+ }
2704
+ case "table": {
2705
+ const rows = node.content || [];
2706
+ const isBorderless = node.attrs?.borderless === true;
2707
+ const isSubtotals = node.attrs?.subtotals === true;
2708
+ const serializedRows = [];
2709
+ if (isBorderless && rows.length > 0) {
2710
+ const colCount = rows[0].content?.length || 0;
2711
+ const markerCell = isSubtotals ? "_" : "";
2712
+ const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
2713
+ const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
2714
+ serializedRows.push(markerRow);
2715
+ serializedRows.push(sep);
2716
+ }
2717
+ for (let ri = 0; ri < rows.length; ri++) {
2718
+ const row = rows[ri];
2719
+ const cells = (row.content || []).map((cell) => {
2720
+ const inner = (cell.content || []).map(serializeBlock).join("");
2721
+ return inner;
2722
+ });
2723
+ serializedRows.push(`| ${cells.join(" | ")} |`);
2724
+ if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
2725
+ const sep = cells.map(() => "------").join("|");
2726
+ serializedRows.push(`|${sep}|`);
2727
+ }
2728
+ }
2729
+ return serializedRows.join("\n");
2730
+ }
2731
+ case "tableRow": {
2732
+ const cells = (node.content || []).map((cell) => {
2733
+ const inner = (cell.content || []).map(serializeBlock).join("");
2734
+ return inner;
2735
+ });
2736
+ return `| ${cells.join(" | ")} |`;
2737
+ }
2738
+ case "tableHeader":
2739
+ case "tableCell": {
2740
+ return (node.content || []).map(serializeBlock).join("");
2741
+ }
2742
+ case "watermark": {
2743
+ const attrs = node.attrs || {};
2744
+ const parts = [];
2745
+ if (attrs.text) parts.push(`text:${attrs.text}`);
2746
+ if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
2747
+ if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
2748
+ return `:::watermark{${parts.join("|")}}`;
2749
+ }
2750
+ case "panel": {
2751
+ const attrs = node.attrs || {};
2752
+ const parts = [];
2753
+ if (attrs.title) parts.push(`title:${attrs.title}`);
2754
+ if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
2755
+ else if (attrs.border === "solid") parts.push(`border:solid`);
2756
+ if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
2757
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
2758
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2759
+ return `:::panel${attrStr}
2760
+ ${children}
2761
+ :::`;
2762
+ }
2763
+ case "repeatBlock": {
2764
+ const attrs = node.attrs || {};
2765
+ const parts = [];
2766
+ if (attrs.data) parts.push(`data:${attrs.data}`);
2767
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
2768
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2769
+ return `:::repeat${attrStr}
2770
+ ${children}
2771
+ :::`;
2772
+ }
2773
+ case "subtotalsBlock": {
2774
+ const children = (node.content || []).map(serializeBlock).join("\n");
2775
+ return `:::subtotals
2776
+ ${children}
2777
+ :::`;
2778
+ }
2779
+ case "columns": {
2780
+ const attrs = node.attrs || {};
2781
+ const splitVal = attrs.split || "50";
2782
+ const cols = (node.content || []).filter((c) => c.type === "column");
2783
+ const colBlocks = cols.map((col) => {
2784
+ const children = (col.content || []).map(serializeBlock).join("\n\n");
2785
+ const colAttrs = col.attrs || {};
2786
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
2787
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
2788
+ return `${colTag}
2789
+ ${children}
2790
+ :::`;
2791
+ });
2792
+ const padX = parseFloat(attrs.padX || "0") || 0;
2793
+ const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
2794
+ return `:::columns{${colsAttrs}}
2795
+ ${colBlocks.join("\n")}
2796
+ :::`;
2797
+ }
2798
+ case "column": {
2799
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2800
+ const colAttrs = node.attrs || {};
2801
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
2802
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
2803
+ return `${colTag}
2804
+ ${children}
2805
+ :::`;
2806
+ }
2807
+ default:
2808
+ return serializeInline(node.content);
2809
+ }
2810
+ }
2811
+ function tiptapToMarkdown(doc) {
2812
+ if (!doc.content) return "";
2813
+ return doc.content.map(serializeBlock).join("\n\n");
2814
+ }
2815
+
2816
+ // src/utils/pdf-preview.ts
2817
+ async function pdfToImages(pdfBytes, scale = 2) {
2818
+ let pdfjsLib;
2819
+ try {
2820
+ pdfjsLib = await import('pdfjs-dist');
2821
+ } catch (err) {
2822
+ throw new Error(formatError(
2823
+ "Failed to load the PDF preview library \u2014 ensure pdfjs-dist is installed and the worker is accessible",
2824
+ err
2825
+ ));
2826
+ }
2827
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
2828
+ pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/build/pdf.worker.mjs";
2829
+ }
2830
+ let pdf;
2831
+ try {
2832
+ const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
2833
+ pdf = await loadingTask.promise;
2834
+ } catch (err) {
2835
+ throw new Error(formatError("Failed to parse the generated PDF for preview", err));
2836
+ }
2837
+ const pages = [];
2838
+ const pageErrors = [];
2839
+ for (let i = 1; i <= pdf.numPages; i++) {
2840
+ try {
2841
+ const page = await pdf.getPage(i);
1599
2842
  const viewport = page.getViewport({ scale });
1600
2843
  const canvas = document.createElement("canvas");
1601
2844
  canvas.width = viewport.width;
@@ -1656,8 +2899,26 @@ function useDocumentGenerator(options = {}) {
1656
2899
  Placeholder__default.default.configure({
1657
2900
  placeholder: options.placeholder || "Start writing your document..."
1658
2901
  }),
2902
+ Table__default.default.extend({
2903
+ addAttributes() {
2904
+ return {
2905
+ ...this.parent?.(),
2906
+ borderless: { default: false },
2907
+ subtotals: { default: false }
2908
+ };
2909
+ }
2910
+ }).configure({ resizable: false }),
2911
+ TableRow__default.default,
2912
+ TableCell__default.default,
2913
+ TableHeader__default.default,
1659
2914
  FieldNode,
1660
- VariableNode
2915
+ VariableNode,
2916
+ PanelNode,
2917
+ ColumnsNode,
2918
+ ColumnNode,
2919
+ WatermarkNode,
2920
+ RepeatNode,
2921
+ SubtotalsNode
1661
2922
  ],
1662
2923
  content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
1663
2924
  onUpdate: ({ editor: ed }) => {
@@ -1761,10 +3022,9 @@ function useDocumentGenerator(options = {}) {
1761
3022
  async (values) => {
1762
3023
  if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
1763
3024
  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 };
3025
+ const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
3026
+ const pdfPages2 = await pdfToImages(pdfBytes2);
3027
+ return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
1768
3028
  },
1769
3029
  [editor]
1770
3030
  );
@@ -2918,15 +4178,47 @@ function FieldEditPopover({
2918
4178
  ] }) })
2919
4179
  ] });
2920
4180
  }
2921
- function labelToVarName(label) {
4181
+ var sizeClasses = {
4182
+ md: "px-3 py-1.5 text-xs font-medium",
4183
+ sm: "px-2 py-0.5 text-[10px] font-medium"
4184
+ };
4185
+ function ToggleGroup({
4186
+ value,
4187
+ onChange,
4188
+ options,
4189
+ size = "md",
4190
+ className
4191
+ }) {
4192
+ 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(
4193
+ "button",
4194
+ {
4195
+ className: cn(
4196
+ "rounded-md transition-colors",
4197
+ sizeClasses[size],
4198
+ value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
4199
+ ),
4200
+ onClick: () => onChange(opt.value),
4201
+ children: opt.label
4202
+ },
4203
+ opt.value
4204
+ )) });
4205
+ }
4206
+ function labelToVarName2(label) {
2922
4207
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2923
4208
  }
4209
+ function varNameToLabel(name) {
4210
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4211
+ }
2924
4212
  function VariableInsertPopover({
2925
4213
  open,
2926
4214
  onOpenChange,
2927
4215
  onInsert,
4216
+ predefinedVariables,
2928
4217
  children
2929
4218
  }) {
4219
+ const hasPredefined = !!predefinedVariables?.length;
4220
+ const [mode, setMode] = React12.useState(hasPredefined ? "existing" : "new");
4221
+ const [search, setSearch] = React12.useState("");
2930
4222
  const [varLabel, setVarLabel] = React12.useState("");
2931
4223
  const [varName, setVarName] = React12.useState("");
2932
4224
  const [varDefault, setVarDefault] = React12.useState("");
@@ -2936,7 +4228,9 @@ function VariableInsertPopover({
2936
4228
  setVarName("");
2937
4229
  setVarDefault("");
2938
4230
  setNameManuallyEdited(false);
2939
- }, []);
4231
+ setSearch("");
4232
+ setMode(hasPredefined ? "existing" : "new");
4233
+ }, [hasPredefined]);
2940
4234
  const handleOpenChange = React12.useCallback(
2941
4235
  (nextOpen) => {
2942
4236
  if (!nextOpen) reset();
@@ -2946,7 +4240,7 @@ function VariableInsertPopover({
2946
4240
  );
2947
4241
  React12.useEffect(() => {
2948
4242
  if (!nameManuallyEdited) {
2949
- setVarName(labelToVarName(varLabel));
4243
+ setVarName(labelToVarName2(varLabel));
2950
4244
  }
2951
4245
  }, [varLabel, nameManuallyEdited]);
2952
4246
  const handleInsert = React12.useCallback(() => {
@@ -2958,6 +4252,26 @@ function VariableInsertPopover({
2958
4252
  });
2959
4253
  handleOpenChange(false);
2960
4254
  }, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
4255
+ const handlePickPredefined = React12.useCallback(
4256
+ (pv) => {
4257
+ onInsert({
4258
+ varName: pv.varName,
4259
+ varLabel: pv.varLabel || varNameToLabel(pv.varName),
4260
+ varDefault: ""
4261
+ });
4262
+ handleOpenChange(false);
4263
+ },
4264
+ [onInsert, handleOpenChange]
4265
+ );
4266
+ const filteredVars = React12.useMemo(() => {
4267
+ if (!predefinedVariables?.length) return [];
4268
+ const q = search.toLowerCase();
4269
+ if (!q) return predefinedVariables;
4270
+ return predefinedVariables.filter((pv) => {
4271
+ const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
4272
+ return pv.varName.toLowerCase().includes(q) || label.includes(q);
4273
+ });
4274
+ }, [predefinedVariables, search]);
2961
4275
  return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
2962
4276
  /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children }),
2963
4277
  /* @__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 +4279,114 @@ function VariableInsertPopover({
2965
4279
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 14 }),
2966
4280
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Insert Variable" })
2967
4281
  ] }),
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" }),
4282
+ hasPredefined && /* @__PURE__ */ jsxRuntime.jsx(
4283
+ ToggleGroup,
4284
+ {
4285
+ value: mode,
4286
+ onChange: setMode,
4287
+ options: [
4288
+ { value: "existing", label: "Existing" },
4289
+ { value: "new", label: "New" }
4290
+ ],
4291
+ size: "sm"
4292
+ }
4293
+ ),
4294
+ mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
4295
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
4296
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
2971
4297
  /* @__PURE__ */ jsxRuntime.jsx(
2972
4298
  Input,
2973
4299
  {
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"
4300
+ value: search,
4301
+ onChange: (e) => setSearch(e.target.value),
4302
+ placeholder: "Search variables...",
4303
+ className: "h-8 text-xs pl-7"
2979
4304
  }
2980
4305
  )
2981
4306
  ] }),
2982
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2983
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4307
+ /* @__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) => {
4308
+ const label = pv.varLabel || varNameToLabel(pv.varName);
4309
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4310
+ "button",
4311
+ {
4312
+ 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",
4313
+ onClick: () => handlePickPredefined(pv),
4314
+ children: [
4315
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: label }),
4316
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
4317
+ ]
4318
+ },
4319
+ pv.varName
4320
+ );
4321
+ }) })
4322
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4323
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
4324
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4325
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4326
+ /* @__PURE__ */ jsxRuntime.jsx(
4327
+ Input,
4328
+ {
4329
+ id: "var-label",
4330
+ value: varLabel,
4331
+ onChange: (e) => setVarLabel(e.target.value),
4332
+ placeholder: "e.g. Company Name",
4333
+ className: "h-8 text-xs"
4334
+ }
4335
+ )
4336
+ ] }),
4337
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4338
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4339
+ /* @__PURE__ */ jsxRuntime.jsx(
4340
+ Input,
4341
+ {
4342
+ id: "var-name",
4343
+ value: varName,
4344
+ onChange: (e) => {
4345
+ setVarName(e.target.value);
4346
+ setNameManuallyEdited(true);
4347
+ },
4348
+ placeholder: "e.g. company_name",
4349
+ className: "h-8 text-xs font-mono"
4350
+ }
4351
+ ),
4352
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
4353
+ ] }),
4354
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4355
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
4356
+ /* @__PURE__ */ jsxRuntime.jsx(
4357
+ Input,
4358
+ {
4359
+ id: "var-default",
4360
+ value: varDefault,
4361
+ onChange: (e) => setVarDefault(e.target.value),
4362
+ placeholder: "Optional",
4363
+ className: "h-8 text-xs"
4364
+ }
4365
+ )
4366
+ ] })
4367
+ ] }),
4368
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
2984
4369
  /* @__PURE__ */ jsxRuntime.jsx(
2985
- Input,
4370
+ Button,
2986
4371
  {
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"
4372
+ size: "sm",
4373
+ className: "h-8 flex-1 text-xs",
4374
+ onClick: handleInsert,
4375
+ disabled: !varLabel.trim() || !varName.trim(),
4376
+ children: "Insert"
2995
4377
  }
2996
4378
  ),
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
4379
  /* @__PURE__ */ jsxRuntime.jsx(
3002
- Input,
4380
+ Button,
3003
4381
  {
3004
- id: "var-default",
3005
- value: varDefault,
3006
- onChange: (e) => setVarDefault(e.target.value),
3007
- placeholder: "Optional",
3008
- className: "h-8 text-xs"
4382
+ variant: "outline",
4383
+ size: "sm",
4384
+ className: "h-8 text-xs",
4385
+ onClick: () => handleOpenChange(false),
4386
+ children: "Cancel"
3009
4387
  }
3010
4388
  )
3011
4389
  ] })
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
4390
  ] })
3035
4391
  ] }) })
3036
4392
  ] });
@@ -3252,7 +4608,6 @@ function PreviewPanel({
3252
4608
  (f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
3253
4609
  );
3254
4610
  }, [positionedFields, currentPage, currentPageIndex]);
3255
- const showScrollbars = zoomLevel > 1;
3256
4611
  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
4612
  /* @__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
4613
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
@@ -3368,12 +4723,13 @@ function PreviewPanel({
3368
4723
  "div",
3369
4724
  {
3370
4725
  className: cn(
3371
- "border border-border rounded-lg bg-muted/30 shrink-0",
3372
- showScrollbars ? "overflow-auto" : "overflow-hidden"
4726
+ "border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
3373
4727
  ),
3374
4728
  style: {
3375
4729
  width: pageDisplaySize.viewportWidth,
3376
- height: pageDisplaySize.viewportHeight
4730
+ height: pageDisplaySize.viewportHeight,
4731
+ maxWidth: "100%",
4732
+ maxHeight: "100%"
3377
4733
  },
3378
4734
  children: /* @__PURE__ */ jsxRuntime.jsxs(
3379
4735
  "div",
@@ -3465,6 +4821,37 @@ function validateMarkdown(markdown) {
3465
4821
  searchFrom = closeIdx + 2;
3466
4822
  }
3467
4823
  }
4824
+ const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
4825
+ const DIRECTIVE_CLOSE_RE = /^:::$/;
4826
+ const directiveStack = [];
4827
+ for (let i = 0; i < lines.length; i++) {
4828
+ const trimmed = lines[i].trim();
4829
+ const lineNum = i + 1;
4830
+ const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
4831
+ if (openMatch) {
4832
+ const dtype = openMatch[1];
4833
+ if (dtype !== "watermark") {
4834
+ if (dtype === "col") {
4835
+ const parent = directiveStack[directiveStack.length - 1];
4836
+ if (!parent || parent.type !== "columns") {
4837
+ warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
4838
+ }
4839
+ }
4840
+ directiveStack.push({ type: dtype, line: lineNum });
4841
+ }
4842
+ continue;
4843
+ }
4844
+ if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
4845
+ if (directiveStack.length === 0) {
4846
+ warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
4847
+ } else {
4848
+ directiveStack.pop();
4849
+ }
4850
+ }
4851
+ }
4852
+ for (const open of directiveStack) {
4853
+ errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
4854
+ }
3468
4855
  let fields = [];
3469
4856
  let variables = [];
3470
4857
  try {
@@ -3498,31 +4885,6 @@ function validateMarkdown(markdown) {
3498
4885
  fields
3499
4886
  };
3500
4887
  }
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
4888
  function parseCsv(text) {
3527
4889
  const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
3528
4890
  if (lines.length < 2) return [];
@@ -3572,7 +4934,10 @@ function GeneratePanel({
3572
4934
  editorContent,
3573
4935
  editorVariables,
3574
4936
  onGeneratePdf,
3575
- initialBulkData
4937
+ initialBulkData,
4938
+ initialVariableValues,
4939
+ exportFileName,
4940
+ templateMode
3576
4941
  }) {
3577
4942
  const [templateSource, setTemplateSource] = React12.useState("editor");
3578
4943
  const [importedMarkdown, setImportedMarkdown] = React12.useState(null);
@@ -3580,6 +4945,7 @@ function GeneratePanel({
3580
4945
  const [validationResult, setValidationResult] = React12.useState(null);
3581
4946
  const [mode, setMode] = React12.useState("single");
3582
4947
  const [variableValues, setVariableValues] = React12.useState({});
4948
+ const [variableSearch, setVariableSearch] = React12.useState("");
3583
4949
  const [bulkInputFormat, setBulkInputFormat] = React12.useState("json");
3584
4950
  const [bulkInput, setBulkInput] = React12.useState("");
3585
4951
  const [bulkData, setBulkData] = React12.useState(null);
@@ -3606,11 +4972,22 @@ function GeneratePanel({
3606
4972
  const col = pos - textBefore.lastIndexOf("\n");
3607
4973
  setJsonCursor({ line, col });
3608
4974
  }, []);
4975
+ const lockedVarNames = React12__namespace.default.useMemo(
4976
+ () => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
4977
+ [templateMode, initialVariableValues]
4978
+ );
3609
4979
  const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
3610
4980
  const hasVariables = activeVariables.length > 0;
3611
4981
  const hasEditorContent = editorMarkdown.trim().length > 0;
3612
4982
  const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
3613
4983
  const hasContent = hasImportedContent || hasEditorContent;
4984
+ const filteredVariables = React12__namespace.default.useMemo(() => {
4985
+ if (!variableSearch.trim()) return activeVariables;
4986
+ const q = variableSearch.toLowerCase();
4987
+ return activeVariables.filter(
4988
+ (v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
4989
+ );
4990
+ }, [activeVariables, variableSearch]);
3614
4991
  const getActiveContent = React12.useCallback(() => {
3615
4992
  if (templateSource === "imported" && importedMarkdown) {
3616
4993
  return markdownToTiptap(importedMarkdown);
@@ -3778,7 +5155,9 @@ function GeneratePanel({
3778
5155
  setPreviewError(null);
3779
5156
  try {
3780
5157
  const content = getActiveContent();
3781
- const replaced = replaceVariablesInContent(content, variableValues);
5158
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5159
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5160
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3782
5161
  const fields = extractFieldsFromContent(replaced);
3783
5162
  const result = await generatePdfFromContent(replaced);
3784
5163
  const pages = await pdfToImages(result.pdfBytes);
@@ -3799,7 +5178,9 @@ function GeneratePanel({
3799
5178
  setExportSuccess(null);
3800
5179
  try {
3801
5180
  const content = getActiveContent();
3802
- const replaced = replaceVariablesInContent(content, variableValues);
5181
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5182
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5183
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3803
5184
  const fields = extractFieldsFromContent(replaced);
3804
5185
  const result = await generatePdfFromContent(replaced, {
3805
5186
  drawFieldPlaceholders: false,
@@ -3807,7 +5188,7 @@ function GeneratePanel({
3807
5188
  fields
3808
5189
  });
3809
5190
  const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
3810
- const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
5191
+ const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
3811
5192
  if (onGeneratePdf) {
3812
5193
  onGeneratePdf(blob, fileName);
3813
5194
  } else {
@@ -3879,7 +5260,9 @@ function GeneratePanel({
3879
5260
  const allWarnings = [];
3880
5261
  for (let i = 0; i < bulkData.length; i++) {
3881
5262
  const values = bulkData[i];
3882
- const replaced = replaceVariablesInContent(content, values);
5263
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5264
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5265
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3883
5266
  const fields = extractFieldsFromContent(replaced);
3884
5267
  const result = await generatePdfFromContent(replaced, {
3885
5268
  drawFieldPlaceholders: false,
@@ -3934,13 +5317,15 @@ ${allWarnings.join("\n")}`);
3934
5317
  }, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
3935
5318
  React12__namespace.default.useEffect(() => {
3936
5319
  if (templateSource === "editor") {
3937
- const defaults = {};
5320
+ const defaults = initialVariableValues ? { ...initialVariableValues } : {};
3938
5321
  for (const v of editorVariables) {
3939
- defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5322
+ if (!defaults[v.varName]) {
5323
+ defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5324
+ }
3940
5325
  }
3941
5326
  setVariableValues(defaults);
3942
5327
  }
3943
- }, [editorVariables, templateSource]);
5328
+ }, [editorVariables, templateSource, initialVariableValues]);
3944
5329
  React12.useEffect(() => {
3945
5330
  setPreviewFresh(false);
3946
5331
  }, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
@@ -3952,8 +5337,8 @@ ${allWarnings.join("\n")}`);
3952
5337
  }, [exportSuccess]);
3953
5338
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
3954
5339
  /* @__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: [
5340
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
5341
+ !templateMode && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3957
5342
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
3958
5343
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium", children: "Template" }),
3959
5344
  templateSource === "imported" && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4046,10 +5431,10 @@ ${allWarnings.join("\n")}`);
4046
5431
  !hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
4047
5432
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Braces, { size: 32, className: "mb-2 opacity-50" }),
4048
5433
  /* @__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." })
5434
+ /* @__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
5435
  ] }),
4051
5436
  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(
5437
+ !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
5438
  ToggleGroup,
4054
5439
  {
4055
5440
  value: mode,
@@ -4061,20 +5446,53 @@ ${allWarnings.join("\n")}`);
4061
5446
  }
4062
5447
  ) }),
4063
5448
  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 }),
5449
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
5450
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
5451
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
5452
+ ] }),
5453
+ activeVariables.length > 5 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
5454
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
4067
5455
  /* @__PURE__ */ jsxRuntime.jsx(
4068
5456
  Input,
4069
5457
  {
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"
5458
+ value: variableSearch,
5459
+ onChange: (e) => setVariableSearch(e.target.value),
5460
+ placeholder: "Search variables...",
5461
+ className: "h-8 text-xs pl-8"
5462
+ }
5463
+ ),
5464
+ variableSearch && /* @__PURE__ */ jsxRuntime.jsx(
5465
+ "button",
5466
+ {
5467
+ type: "button",
5468
+ onClick: () => setVariableSearch(""),
5469
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
5470
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 12 })
4075
5471
  }
4076
5472
  )
4077
- ] }, v.varName))
5473
+ ] }),
5474
+ filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
5475
+ "No variables match \u201C",
5476
+ variableSearch,
5477
+ "\u201D"
5478
+ ] }),
5479
+ filteredVariables.map((v) => {
5480
+ const isLocked = lockedVarNames.has(v.varName);
5481
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5482
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5483
+ /* @__PURE__ */ jsxRuntime.jsx(
5484
+ Input,
5485
+ {
5486
+ id: `gen-${v.varName}`,
5487
+ value: variableValues[v.varName] || "",
5488
+ onChange: (e) => updateVariableValue(v.varName, e.target.value),
5489
+ placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
5490
+ disabled: isLocked,
5491
+ className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
5492
+ }
5493
+ )
5494
+ ] }, v.varName);
5495
+ })
4078
5496
  ] }),
4079
5497
  mode === "bulk" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 space-y-3", children: [
4080
5498
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
@@ -4263,56 +5681,60 @@ ${allWarnings.join("\n")}`);
4263
5681
  ] }),
4264
5682
  !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
5683
  ] }),
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(
5684
+ 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: [
5685
+ mode === "single" || !hasVariables ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5686
+ /* @__PURE__ */ jsxRuntime.jsxs(
5687
+ Button,
5688
+ {
5689
+ variant: "secondary",
5690
+ onClick: handlePreview,
5691
+ disabled: isGenerating,
5692
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
5693
+ children: [
5694
+ isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
5695
+ isGenerating ? "Generating..." : "Generate PDF"
5696
+ ]
5697
+ }
5698
+ ),
5699
+ /* @__PURE__ */ jsxRuntime.jsxs(
5700
+ Button,
5701
+ {
5702
+ onClick: handleExportSingle,
5703
+ disabled: !previewFresh || isExporting,
5704
+ 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",
5705
+ children: [
5706
+ isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
5707
+ isExporting ? "Exporting..." : "Export PDF"
5708
+ ]
5709
+ }
5710
+ )
5711
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
4293
5712
  Button,
4294
5713
  {
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",
5714
+ onClick: handleBulkGenerate,
5715
+ disabled: isExporting || !bulkData || bulkData.length === 0,
5716
+ 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
5717
  children: [
4299
5718
  isExporting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
4300
- isExporting ? "Exporting..." : "Export PDF"
5719
+ isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4301
5720
  ]
4302
5721
  }
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
- ) })
5722
+ ),
5723
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
5724
+ previewError && /* @__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: previewError })
5727
+ ] }),
5728
+ exportError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
5729
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
5730
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportError })
5731
+ ] }),
5732
+ exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
5733
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
5734
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: exportSuccess })
5735
+ ] })
5736
+ ] })
5737
+ ] })
4316
5738
  ] }),
4317
5739
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
4318
5740
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -4490,7 +5912,13 @@ function DocumentGeneratorInner({
4490
5912
  showToolbar = true,
4491
5913
  showGenerateTab = true,
4492
5914
  onGeneratePdf,
4493
- initialBulkData
5915
+ initialBulkData,
5916
+ defaultTab = "editor",
5917
+ initialVariableValues,
5918
+ predefinedVariables,
5919
+ exportFileName,
5920
+ templateMode,
5921
+ onSaveTemplate
4494
5922
  }) {
4495
5923
  const {
4496
5924
  editor,
@@ -4517,7 +5945,7 @@ function DocumentGeneratorInner({
4517
5945
  placeholder,
4518
5946
  onChange
4519
5947
  });
4520
- const [activeTab, setActiveTab] = React12.useState("editor");
5948
+ const [activeTab, setActiveTab] = React12.useState(defaultTab);
4521
5949
  const [insertPopoverOpen, setInsertPopoverOpen] = React12.useState(false);
4522
5950
  const [insertVarPopoverOpen, setInsertVarPopoverOpen] = React12.useState(false);
4523
5951
  const [editFieldId, setEditFieldId] = React12.useState(null);
@@ -4600,6 +6028,21 @@ function DocumentGeneratorInner({
4600
6028
  setExportSuccess("Document exported successfully");
4601
6029
  }
4602
6030
  }, [exportDocument, onExport]);
6031
+ const handleSaveTemplate = React12.useCallback(() => {
6032
+ const md = exportMarkdown();
6033
+ if (!md.trim()) {
6034
+ setMdExportError("Editor content is empty");
6035
+ return;
6036
+ }
6037
+ const validation = validateMarkdown(md);
6038
+ if (!validation.valid) {
6039
+ setMdExportError(validation.errors.join("; "));
6040
+ return;
6041
+ }
6042
+ setMdExportError(null);
6043
+ onSaveTemplate?.(md);
6044
+ setExportSuccess("Template saved");
6045
+ }, [exportMarkdown, onSaveTemplate]);
4603
6046
  const handleExportMarkdown = React12.useCallback(() => {
4604
6047
  const md = exportMarkdown();
4605
6048
  if (!md.trim()) {
@@ -4616,10 +6059,10 @@ function DocumentGeneratorInner({
4616
6059
  const url = URL.createObjectURL(blob);
4617
6060
  const a = document.createElement("a");
4618
6061
  a.href = url;
4619
- a.download = "template.md";
6062
+ a.download = exportFileName ? `${exportFileName}.md` : "template.md";
4620
6063
  a.click();
4621
6064
  URL.revokeObjectURL(url);
4622
- }, [exportMarkdown]);
6065
+ }, [exportMarkdown, exportFileName]);
4623
6066
  React12.useEffect(() => {
4624
6067
  if (mdExportError) setMdExportError(null);
4625
6068
  }, [markdown]);
@@ -4655,6 +6098,7 @@ function DocumentGeneratorInner({
4655
6098
  open: insertVarPopoverOpen,
4656
6099
  onOpenChange: setInsertVarPopoverOpen,
4657
6100
  onInsert: handleInsertVariable,
6101
+ predefinedVariables,
4658
6102
  children: /* @__PURE__ */ jsxRuntime.jsxs(
4659
6103
  Button,
4660
6104
  {
@@ -4695,118 +6139,141 @@ function DocumentGeneratorInner({
4695
6139
  ] })
4696
6140
  ] })
4697
6141
  ] }),
4698
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
6142
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
4699
6143
  "flex flex-col flex-1 min-h-0",
4700
6144
  activeTab !== "editor" && "hidden"
6145
+ ), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
6146
+ "flex-1 min-h-0",
6147
+ showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4701
6148
  ), 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,
6149
+ !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
6150
+ showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
6151
+ EditorToolbar,
4727
6152
  {
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"
6153
+ editor,
6154
+ insertFieldButton,
6155
+ insertVariableButton,
6156
+ onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4742
6157
  }
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,
6158
+ ) }),
6159
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
6160
+ react.EditorContent,
4748
6161
  {
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
- ]
6162
+ editor,
6163
+ className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4757
6164
  }
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: [
6165
+ ) }),
6166
+ /* @__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
6167
  /* @__PURE__ */ jsxRuntime.jsxs(
4784
6168
  Button,
4785
6169
  {
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",
6170
+ variant: "secondary",
6171
+ onClick: generatePdf,
6172
+ disabled: isGenerating || !editor,
6173
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4789
6174
  children: [
4790
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 16 }),
4791
- "Export MD"
6175
+ isGenerating ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 16 }),
6176
+ isGenerating ? "Generating..." : "Generate PDF"
4792
6177
  ]
4793
6178
  }
4794
6179
  ),
4795
- /* @__PURE__ */ jsxRuntime.jsxs(
6180
+ onSaveTemplate ? /* @__PURE__ */ jsxRuntime.jsxs(
4796
6181
  Button,
4797
6182
  {
4798
- onClick: handleExport,
4799
- disabled: isGenerating || !editor || pdfPages.length === 0,
6183
+ onClick: handleSaveTemplate,
6184
+ disabled: !editor || !markdown.trim(),
4800
6185
  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
6186
  children: [
4802
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
4803
- exportButtonText
6187
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 16 }),
6188
+ exportButtonText || "Save Template"
4804
6189
  ]
4805
6190
  }
4806
- )
6191
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(Popover, { children: [
6192
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
6193
+ Button,
6194
+ {
6195
+ disabled: !editor || !markdown.trim() && pdfPages.length === 0,
6196
+ 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",
6197
+ children: [
6198
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
6199
+ "Export",
6200
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { size: 14 })
6201
+ ]
6202
+ }
6203
+ ) }),
6204
+ /* @__PURE__ */ jsxRuntime.jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
6205
+ /* @__PURE__ */ jsxRuntime.jsxs(
6206
+ "button",
6207
+ {
6208
+ onClick: handleExportMarkdown,
6209
+ disabled: !editor || !markdown.trim(),
6210
+ 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",
6211
+ children: [
6212
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 16 }),
6213
+ "Export as MD"
6214
+ ]
6215
+ }
6216
+ ),
6217
+ /* @__PURE__ */ jsxRuntime.jsxs(
6218
+ "button",
6219
+ {
6220
+ onClick: handleExport,
6221
+ disabled: isGenerating || !editor || pdfPages.length === 0,
6222
+ 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",
6223
+ children: [
6224
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileDown, { size: 16 }),
6225
+ "Export as PDF"
6226
+ ]
6227
+ }
6228
+ )
6229
+ ] })
6230
+ ] }),
6231
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6232
+ mdExportError && /* @__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: mdExportError })
6235
+ ] }),
6236
+ generationError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6237
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6238
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: generationError })
6239
+ ] }),
6240
+ fieldWarnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
6241
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { size: 14, className: "shrink-0" }),
6242
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
6243
+ fieldWarnings.length,
6244
+ " field",
6245
+ fieldWarnings.length !== 1 ? "s" : "",
6246
+ " had warnings during generation"
6247
+ ] })
6248
+ ] }),
6249
+ exportSuccess && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6250
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { size: 14, className: "shrink-0" }),
6251
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: exportSuccess })
6252
+ ] })
6253
+ ] })
4807
6254
  ] })
4808
- ] })
4809
- ] }),
6255
+ ] }),
6256
+ showPreview && !editorCollapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-border" }),
6257
+ showPreview && /* @__PURE__ */ jsxRuntime.jsx(
6258
+ PreviewPanel,
6259
+ {
6260
+ pages: pdfPages,
6261
+ isGenerating,
6262
+ positionedFields,
6263
+ headerLeft: editorCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(
6264
+ Button,
6265
+ {
6266
+ variant: "ghost",
6267
+ size: "sm",
6268
+ className: "h-8 w-8 p-0",
6269
+ onClick: () => setEditorCollapsed(false),
6270
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftOpen, { size: 14 })
6271
+ }
6272
+ ) : void 0,
6273
+ className: "flex-1 border-0 rounded-none"
6274
+ }
6275
+ )
6276
+ ] }) }),
4810
6277
  showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsxRuntime.jsx(
4811
6278
  GeneratePanel,
4812
6279
  {
@@ -4814,7 +6281,10 @@ function DocumentGeneratorInner({
4814
6281
  editorContent: editor?.getJSON(),
4815
6282
  editorVariables: variables,
4816
6283
  onGeneratePdf,
4817
- initialBulkData
6284
+ initialBulkData,
6285
+ initialVariableValues,
6286
+ exportFileName,
6287
+ templateMode
4818
6288
  }
4819
6289
  ),
4820
6290
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4955,11 +6425,733 @@ function EditorPanel({
4955
6425
  ] });
4956
6426
  }
4957
6427
 
6428
+ // src/utils/xml-template-parser.ts
6429
+ function extractFieldRefs(expr) {
6430
+ const refs = [];
6431
+ const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
6432
+ let m;
6433
+ while ((m = re.exec(expr)) !== null) {
6434
+ if (m[2]) {
6435
+ refs.push(`${m[1]}.${m[2]}`);
6436
+ } else {
6437
+ refs.push(m[1]);
6438
+ }
6439
+ }
6440
+ return refs;
6441
+ }
6442
+ function fieldRefToVarName(ref, bandContext) {
6443
+ if (ref.includes(".")) {
6444
+ return labelToVarName(ref.replace(/\./g, "_"));
6445
+ }
6446
+ if (bandContext) {
6447
+ return labelToVarName(bandContext + "_" + ref);
6448
+ }
6449
+ return labelToVarName(ref);
6450
+ }
6451
+ function fieldRefToLabel(ref) {
6452
+ const parts = ref.split(".");
6453
+ const raw = parts[parts.length - 1];
6454
+ return raw.replace(/_/g, " ");
6455
+ }
6456
+ function resolveExpression(expr, registry, siblingLabel, bandContext) {
6457
+ const trimmed = expr.trim();
6458
+ const suppressZero = /<>\s*0/.test(trimmed);
6459
+ const szOpts = suppressZero ? { suppressZero: true } : void 0;
6460
+ const aggMatch = trimmed.match(
6461
+ /^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
6462
+ );
6463
+ if (aggMatch) {
6464
+ const fieldName = aggMatch[2];
6465
+ const varName = labelToVarName("workorder_" + fieldName);
6466
+ const label = fieldName.replace(/_/g, " ");
6467
+ return registry.register(varName, label, szOpts);
6468
+ }
6469
+ const concatSingle = trimmed.match(
6470
+ /^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6471
+ );
6472
+ if (concatSingle) {
6473
+ const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
6474
+ const varName = fieldRefToVarName(ref, bandContext);
6475
+ const label = fieldRefToLabel(ref);
6476
+ return registry.register(varName, label, szOpts);
6477
+ }
6478
+ const concatMulti = trimmed.match(
6479
+ /^concat\(/i
6480
+ );
6481
+ if (concatMulti) {
6482
+ const refs = extractFieldRefs(trimmed);
6483
+ if (refs.length > 0) {
6484
+ const uniqueRefs = [...new Set(refs)];
6485
+ const tokens = uniqueRefs.map((ref) => {
6486
+ const varName = fieldRefToVarName(ref, bandContext);
6487
+ const label = fieldRefToLabel(ref);
6488
+ return registry.register(varName, label, szOpts);
6489
+ });
6490
+ return tokens.join(" ");
6491
+ }
6492
+ }
6493
+ const toLongMatch = trimmed.match(
6494
+ /^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6495
+ );
6496
+ if (toLongMatch) {
6497
+ const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
6498
+ const varName = fieldRefToVarName(ref, bandContext);
6499
+ const label = fieldRefToLabel(ref);
6500
+ return registry.register(varName, label, szOpts);
6501
+ }
6502
+ const fmtMatch = trimmed.match(
6503
+ /^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
6504
+ );
6505
+ if (fmtMatch) {
6506
+ return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
6507
+ }
6508
+ const replaceMatch = trimmed.match(
6509
+ /^Replace\(\s*\[([^\]]+)\]/i
6510
+ );
6511
+ if (replaceMatch) {
6512
+ const ref = replaceMatch[1];
6513
+ const varName = fieldRefToVarName(ref, bandContext);
6514
+ const label = fieldRefToLabel(ref);
6515
+ return registry.register(varName, label, szOpts);
6516
+ }
6517
+ const iifLabelMatch = trimmed.match(
6518
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
6519
+ );
6520
+ if (iifLabelMatch) {
6521
+ return iifLabelMatch[1];
6522
+ }
6523
+ const iifLabelElse = trimmed.match(
6524
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
6525
+ );
6526
+ if (iifLabelElse) {
6527
+ return iifLabelElse[1];
6528
+ }
6529
+ const iifEmptyElse = trimmed.match(
6530
+ /^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
6531
+ );
6532
+ if (iifEmptyElse) {
6533
+ return iifEmptyElse[1];
6534
+ }
6535
+ const nestedIifStaticLabel = trimmed.match(
6536
+ /^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
6537
+ );
6538
+ if (nestedIifStaticLabel) {
6539
+ return nestedIifStaticLabel[1];
6540
+ }
6541
+ const iifValueMatch = trimmed.match(
6542
+ /^iif\s*\(/i
6543
+ );
6544
+ if (iifValueMatch) {
6545
+ const refs = extractFieldRefs(trimmed);
6546
+ if (refs.length > 0) {
6547
+ const uniqueRefs = [...new Set(refs)];
6548
+ const innerAggMatch = trimmed.match(
6549
+ /\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
6550
+ );
6551
+ if (innerAggMatch) {
6552
+ const fieldName = innerAggMatch[2];
6553
+ const varName2 = labelToVarName("workorder_" + fieldName);
6554
+ const label2 = fieldName.replace(/_/g, " ");
6555
+ return registry.register(varName2, label2, szOpts);
6556
+ }
6557
+ const ref = uniqueRefs[uniqueRefs.length - 1];
6558
+ const varName = fieldRefToVarName(ref, bandContext);
6559
+ const label = fieldRefToLabel(ref);
6560
+ return registry.register(varName, label, szOpts);
6561
+ }
6562
+ const staticMatch = trimmed.match(/'([^']+)'/);
6563
+ if (staticMatch) return staticMatch[1];
6564
+ return "";
6565
+ }
6566
+ const arithmeticRefs = extractFieldRefs(trimmed);
6567
+ if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
6568
+ if (siblingLabel) {
6569
+ const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
6570
+ const varName = labelToVarName(
6571
+ (bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
6572
+ );
6573
+ return registry.register(varName, cleanLabel, szOpts);
6574
+ }
6575
+ const uniqueRefs = [...new Set(arithmeticRefs)];
6576
+ const tokens = uniqueRefs.map((ref) => {
6577
+ const varName = fieldRefToVarName(ref, bandContext);
6578
+ const label = fieldRefToLabel(ref);
6579
+ return registry.register(varName, label, szOpts);
6580
+ });
6581
+ return tokens.join(" ");
6582
+ }
6583
+ const simpleMatch = trimmed.match(
6584
+ /^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
6585
+ );
6586
+ if (simpleMatch) {
6587
+ const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
6588
+ const varName = fieldRefToVarName(ref, bandContext);
6589
+ const label = fieldRefToLabel(ref);
6590
+ return registry.register(varName, label, szOpts);
6591
+ }
6592
+ if (arithmeticRefs.length >= 1) {
6593
+ const ref = arithmeticRefs[arithmeticRefs.length - 1];
6594
+ const varName = fieldRefToVarName(ref, bandContext);
6595
+ const label = fieldRefToLabel(ref);
6596
+ return registry.register(varName, label, szOpts);
6597
+ }
6598
+ return "";
6599
+ }
6600
+ var VariableRegistry = class {
6601
+ constructor() {
6602
+ __publicField(this, "map", /* @__PURE__ */ new Map());
6603
+ }
6604
+ register(varName, varLabel, options) {
6605
+ if (!this.map.has(varName)) {
6606
+ this.map.set(varName, { varName, varLabel, varDefault: "" });
6607
+ }
6608
+ const suppress = options?.suppressZero ? "|suppress:zero" : "";
6609
+ return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
6610
+ }
6611
+ getAll() {
6612
+ return Array.from(this.map.values());
6613
+ }
6614
+ };
6615
+ function parseLocationFloat(s) {
6616
+ if (!s) return { x: 0, y: 0 };
6617
+ const parts = s.split(",");
6618
+ return {
6619
+ x: parseFloat(parts[0]) || 0,
6620
+ y: parseFloat(parts[1]) || 0
6621
+ };
6622
+ }
6623
+ function parseSizeF(s) {
6624
+ if (!s) return { w: 0, h: 0 };
6625
+ const parts = s.split(",");
6626
+ return {
6627
+ w: parseFloat(parts[0]) || 0,
6628
+ h: parseFloat(parts[1]) || 0
6629
+ };
6630
+ }
6631
+ function extractControls(bandEl) {
6632
+ const controlsEl = bandEl.querySelector(":scope > Controls");
6633
+ if (!controlsEl) return [];
6634
+ const items = [];
6635
+ for (const child of Array.from(controlsEl.children)) {
6636
+ items.push(extractControl(child));
6637
+ }
6638
+ return items;
6639
+ }
6640
+ function extractControl(el) {
6641
+ const ctrlType = el.getAttribute("ControlType") || "";
6642
+ const name = el.getAttribute("Name") || "";
6643
+ const text = el.getAttribute("Text") || "";
6644
+ const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
6645
+ const size = parseSizeF(el.getAttribute("SizeF"));
6646
+ const formatString = el.getAttribute("TextFormatString") || null;
6647
+ let expression = null;
6648
+ const bindingsEl = el.querySelector(":scope > ExpressionBindings");
6649
+ if (bindingsEl) {
6650
+ for (const item of Array.from(bindingsEl.children)) {
6651
+ if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
6652
+ expression = item.getAttribute("Expression");
6653
+ break;
6654
+ }
6655
+ }
6656
+ }
6657
+ const children = [];
6658
+ if (ctrlType === "XRPanel") {
6659
+ const panelControls = extractControls(el);
6660
+ children.push(...panelControls);
6661
+ }
6662
+ return {
6663
+ name,
6664
+ type: ctrlType,
6665
+ text,
6666
+ x: loc.x,
6667
+ y: loc.y,
6668
+ width: size.w,
6669
+ expression,
6670
+ formatString,
6671
+ children
6672
+ };
6673
+ }
6674
+ function groupByRow(controls, threshold = 8) {
6675
+ const sorted = [...controls].sort((a, b) => a.y - b.y);
6676
+ const rows = [];
6677
+ for (const ctrl of sorted) {
6678
+ const lastRow = rows[rows.length - 1];
6679
+ if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
6680
+ lastRow.controls.push(ctrl);
6681
+ } else {
6682
+ rows.push({ y: ctrl.y, controls: [ctrl] });
6683
+ }
6684
+ }
6685
+ for (const row of rows) {
6686
+ row.controls.sort((a, b) => a.x - b.x);
6687
+ }
6688
+ return rows;
6689
+ }
6690
+ function isStaticLabel(ctrl) {
6691
+ return ctrl.type === "XRLabel" && !ctrl.expression;
6692
+ }
6693
+ function isBound(ctrl) {
6694
+ return ctrl.type === "XRLabel" && !!ctrl.expression;
6695
+ }
6696
+ function computeSplitPercent(leftControls, rightControls, contentWidth) {
6697
+ const leftExtent = leftControls.reduce(
6698
+ (max, c) => Math.max(max, c.x + c.width),
6699
+ 0
6700
+ );
6701
+ const rightMinX = rightControls.reduce(
6702
+ (min, c) => Math.min(min, c.x),
6703
+ contentWidth
6704
+ );
6705
+ const rightMaxExtent = rightControls.reduce(
6706
+ (max, c) => Math.max(max, c.x + c.width),
6707
+ 0
6708
+ );
6709
+ const rightExtent = rightMaxExtent - rightMinX;
6710
+ if (leftExtent + rightExtent === 0) return 50;
6711
+ return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
6712
+ }
6713
+ function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
6714
+ const lines = [];
6715
+ const leftControls = controls.filter((c) => c.x < 437);
6716
+ const rightControls = controls.filter((c) => c.x >= 437);
6717
+ const leftEntries = [];
6718
+ if (leftControls.length > 0) {
6719
+ const leftRows = groupByRow(leftControls);
6720
+ for (const row of leftRows) {
6721
+ const parts = [];
6722
+ for (const ctrl of row.controls) {
6723
+ if (isBound(ctrl)) {
6724
+ let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6725
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
6726
+ resolved = resolved.replace("}}", "|format:phone}}");
6727
+ }
6728
+ if (resolved) parts.push(resolved);
6729
+ } else if (isStaticLabel(ctrl)) {
6730
+ const text = ctrl.text.trim();
6731
+ if (text && !text.match(/^label\d+$/)) {
6732
+ parts.push(text);
6733
+ }
6734
+ }
6735
+ }
6736
+ if (parts.length > 0) {
6737
+ leftEntries.push(parts.join(" "));
6738
+ }
6739
+ }
6740
+ }
6741
+ const rightEntries = [];
6742
+ if (rightControls.length > 0) {
6743
+ const rightRows = groupByRow(rightControls);
6744
+ for (const row of rightRows) {
6745
+ let label = "";
6746
+ let value = "";
6747
+ for (const ctrl of row.controls) {
6748
+ if (isStaticLabel(ctrl)) {
6749
+ const text = ctrl.text.trim();
6750
+ if (text && !text.match(/^label\d+$/)) {
6751
+ label = text;
6752
+ }
6753
+ } else if (isBound(ctrl)) {
6754
+ value = resolveExpression(ctrl.expression, registry, label, "workorder");
6755
+ if (ctrl.expression) {
6756
+ const refs = extractFieldRefs(ctrl.expression);
6757
+ if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
6758
+ value = value.replace("}}", "|suppress:zero}}");
6759
+ }
6760
+ }
6761
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
6762
+ value = value.replace("}}", "|format:phone}}");
6763
+ }
6764
+ }
6765
+ }
6766
+ if (label || value) {
6767
+ rightEntries.push({ label, value, y: row.y });
6768
+ }
6769
+ }
6770
+ }
6771
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
6772
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
6773
+ lines.push(`:::columns{split:${outerSplit}}`);
6774
+ const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
6775
+ const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
6776
+ const xmlLineHeight = 17;
6777
+ const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
6778
+ const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
6779
+ const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
6780
+ lines.push(leftColTag);
6781
+ for (const entry of leftEntries) {
6782
+ lines.push(entry);
6783
+ }
6784
+ lines.push(":::");
6785
+ const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
6786
+ lines.push(rightColTag);
6787
+ if (rightEntries.length > 0) {
6788
+ lines.push("| | |");
6789
+ lines.push("|---|---|");
6790
+ let prevY = -Infinity;
6791
+ for (const entry of rightEntries) {
6792
+ if (prevY > -Infinity && entry.y - prevY > 25) {
6793
+ lines.push("| | |");
6794
+ }
6795
+ const label = entry.label || "";
6796
+ const colon = entry.label ? ":" : "";
6797
+ const value = entry.value ? ` ${entry.value}` : "";
6798
+ lines.push(`| ${label} | ${colon}${value} |`);
6799
+ prevY = entry.y;
6800
+ }
6801
+ }
6802
+ lines.push(":::");
6803
+ lines.push(":::");
6804
+ lines.push("");
6805
+ }
6806
+ return lines.join("\n");
6807
+ }
6808
+ function renderReportHeaderBand(controls, registry, contentWidth) {
6809
+ const lines = [];
6810
+ const leftControls = controls.filter((c) => c.x < 437);
6811
+ const rightControls = controls.filter((c) => c.x >= 437);
6812
+ const leftEntries = [];
6813
+ if (leftControls.length > 0) {
6814
+ const leftRows = groupByRow(leftControls);
6815
+ for (const row of leftRows) {
6816
+ const parts = [];
6817
+ for (const ctrl of row.controls) {
6818
+ if (isBound(ctrl)) {
6819
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6820
+ if (resolved) parts.push(resolved);
6821
+ } else if (isStaticLabel(ctrl)) {
6822
+ const text = ctrl.text.trim();
6823
+ if (text && !text.match(/^label\d+$/)) {
6824
+ parts.push(text);
6825
+ }
6826
+ }
6827
+ }
6828
+ if (parts.length > 0) {
6829
+ leftEntries.push(parts.join(" "));
6830
+ }
6831
+ }
6832
+ }
6833
+ const rightEntries = [];
6834
+ if (rightControls.length > 0) {
6835
+ const rightRows = groupByRow(rightControls);
6836
+ for (const row of rightRows) {
6837
+ let label = "";
6838
+ let value = "";
6839
+ for (const ctrl of row.controls) {
6840
+ if (isStaticLabel(ctrl)) {
6841
+ const text = ctrl.text.trim();
6842
+ if (text && !text.match(/^label\d+$/)) {
6843
+ label = text;
6844
+ }
6845
+ } else if (isBound(ctrl)) {
6846
+ value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6847
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
6848
+ value = value.replace("}}", "|format:phone}}");
6849
+ }
6850
+ }
6851
+ }
6852
+ if (label || value) {
6853
+ rightEntries.push({ label, value });
6854
+ }
6855
+ }
6856
+ }
6857
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
6858
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
6859
+ lines.push(`:::columns{split:${outerSplit}}`);
6860
+ lines.push(":::col");
6861
+ for (const entry of leftEntries) {
6862
+ lines.push(entry);
6863
+ }
6864
+ lines.push(":::");
6865
+ lines.push(":::col");
6866
+ lines.push("| | |");
6867
+ lines.push("|---|---|");
6868
+ for (const entry of rightEntries) {
6869
+ const label = entry.label || "";
6870
+ const value = entry.value ? ` ${entry.value}` : "";
6871
+ lines.push(`| ${label} |${value} |`);
6872
+ }
6873
+ lines.push(":::");
6874
+ lines.push(":::");
6875
+ lines.push("");
6876
+ }
6877
+ return lines.join("\n");
6878
+ }
6879
+ function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
6880
+ const lines = [];
6881
+ const headerTexts = [];
6882
+ for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
6883
+ if (isStaticLabel(ctrl)) {
6884
+ const text = ctrl.text.trim();
6885
+ if (text) headerTexts.push(text);
6886
+ }
6887
+ }
6888
+ const titleText = headerTexts.join(" ");
6889
+ lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
6890
+ lines.push(":::");
6891
+ lines.push(":::repeat{data:operations}");
6892
+ const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
6893
+ const detailParts = [];
6894
+ for (const ctrl of sortedDetail) {
6895
+ if (isBound(ctrl)) {
6896
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
6897
+ if (resolved) detailParts.push(resolved);
6898
+ }
6899
+ }
6900
+ if (detailParts.length > 0) {
6901
+ if (detailParts.length >= 2) {
6902
+ lines.push(":::columns{split:12}");
6903
+ lines.push(":::col");
6904
+ lines.push(`**${detailParts[0]}**`);
6905
+ lines.push(":::");
6906
+ lines.push(":::col");
6907
+ lines.push(`**${detailParts.slice(1).join(" ")}**`);
6908
+ lines.push(":::");
6909
+ lines.push(":::");
6910
+ } else {
6911
+ lines.push(`**${detailParts[0]}**`);
6912
+ }
6913
+ lines.push("");
6914
+ }
6915
+ if (groupFooterControls.length > 0) {
6916
+ const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
6917
+ if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
6918
+ }
6919
+ lines.push("---");
6920
+ lines.push(":::");
6921
+ lines.push("");
6922
+ return lines.join("\n");
6923
+ }
6924
+ function renderGroupFooterBand(controls, registry, contentWidth) {
6925
+ const rows = groupByRow(controls);
6926
+ const entries = [];
6927
+ for (const row of rows) {
6928
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
6929
+ if (labels.length === 0) continue;
6930
+ let label = "";
6931
+ let value = "";
6932
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
6933
+ for (const ctrl of sorted) {
6934
+ if (isStaticLabel(ctrl)) {
6935
+ const text = ctrl.text.trim();
6936
+ if (text && !text.match(/^label\d+$/)) {
6937
+ label = text;
6938
+ }
6939
+ } else if (isBound(ctrl)) {
6940
+ const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
6941
+ if (resolved) {
6942
+ if (!resolved.startsWith("{{var|")) {
6943
+ if (!label) label = resolved;
6944
+ } else {
6945
+ value = resolved;
6946
+ }
6947
+ }
6948
+ }
6949
+ }
6950
+ if (label && value) {
6951
+ entries.push({ label, value });
6952
+ }
6953
+ }
6954
+ if (entries.length === 0) return "";
6955
+ const lines = [];
6956
+ lines.push(":::subtotals");
6957
+ for (const entry of entries) {
6958
+ lines.push(`**${entry.label}** ${entry.value}`);
6959
+ }
6960
+ lines.push(":::");
6961
+ lines.push("");
6962
+ return lines.join("\n");
6963
+ }
6964
+ function renderReportFooterBand(controls, registry) {
6965
+ const lines = [];
6966
+ const panel = controls.find((c) => c.type === "XRPanel");
6967
+ const thankYouLabel = controls.find(
6968
+ (c) => c.type === "XRLabel" && c.text.includes("Thank You")
6969
+ );
6970
+ const priceRows = [];
6971
+ if (panel && panel.children.length > 0) {
6972
+ const rows = groupByRow(panel.children, 15);
6973
+ for (const row of rows) {
6974
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
6975
+ if (labels.length === 0) continue;
6976
+ let label = "";
6977
+ let value = "";
6978
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
6979
+ for (const ctrl of sorted) {
6980
+ if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
6981
+ continue;
6982
+ }
6983
+ const isLabelPos = ctrl.x < 150;
6984
+ if (isStaticLabel(ctrl)) {
6985
+ const text = ctrl.text.trim();
6986
+ if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
6987
+ label = text;
6988
+ }
6989
+ } else if (isBound(ctrl)) {
6990
+ const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
6991
+ if (resolved) {
6992
+ if (isLabelPos) {
6993
+ if (!label) label = resolved;
6994
+ } else {
6995
+ if (!value) value = resolved;
6996
+ }
6997
+ }
6998
+ }
6999
+ }
7000
+ if (label && value) {
7001
+ priceRows.push({ label, value });
7002
+ } else if (value) {
7003
+ priceRows.push({ label: "", value });
7004
+ }
7005
+ }
7006
+ }
7007
+ lines.push(":::columns{split:40|padX:20}");
7008
+ lines.push(":::col{padTop:1}");
7009
+ if (thankYouLabel) {
7010
+ lines.push(`## ***${thankYouLabel.text.trim()}***`);
7011
+ }
7012
+ lines.push(":::");
7013
+ lines.push(":::col{padTop:1}");
7014
+ lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
7015
+ if (priceRows.length > 0) {
7016
+ lines.push(":::subtotals");
7017
+ for (const row of priceRows) {
7018
+ const label = row.label ? `**${row.label}**` : "";
7019
+ lines.push(`${label} ${row.value}`);
7020
+ }
7021
+ lines.push(":::");
7022
+ }
7023
+ lines.push(":::");
7024
+ lines.push(":::");
7025
+ lines.push(":::");
7026
+ lines.push("");
7027
+ return lines.join("\n");
7028
+ }
7029
+ function parseXmlTemplate(xmlString) {
7030
+ const warnings = [];
7031
+ const registry = new VariableRegistry();
7032
+ const parser = new DOMParser();
7033
+ const doc = parser.parseFromString(xmlString, "text/xml");
7034
+ const parseError = doc.querySelector("parsererror");
7035
+ if (parseError) {
7036
+ return {
7037
+ markdown: "",
7038
+ variables: [],
7039
+ reportName: "",
7040
+ warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
7041
+ };
7042
+ }
7043
+ const root = doc.documentElement;
7044
+ const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
7045
+ const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
7046
+ const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
7047
+ const marginParts = marginsStr.split(",").map(Number);
7048
+ const leftMargin = marginParts[0] || 50;
7049
+ const rightMargin = marginParts[1] || 50;
7050
+ const contentWidth = pageWidth - leftMargin - rightMargin;
7051
+ const calculatedFieldNames = /* @__PURE__ */ new Set();
7052
+ const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
7053
+ if (calcFieldsEl) {
7054
+ for (const item of Array.from(calcFieldsEl.children)) {
7055
+ const name = item.getAttribute("Name");
7056
+ if (name) calculatedFieldNames.add(name);
7057
+ }
7058
+ }
7059
+ const markdownSections = [];
7060
+ const watermarkEl = root.querySelector("Watermarks > Item1");
7061
+ if (watermarkEl) {
7062
+ const wmText = watermarkEl.getAttribute("Text");
7063
+ if (wmText) {
7064
+ markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
7065
+ markdownSections.push("");
7066
+ }
7067
+ }
7068
+ const bandsEl = root.querySelector(":scope > Bands");
7069
+ if (!bandsEl) {
7070
+ warnings.push("No <Bands> element found in the XML");
7071
+ return {
7072
+ markdown: "",
7073
+ variables: registry.getAll(),
7074
+ reportName,
7075
+ warnings
7076
+ };
7077
+ }
7078
+ let topMarginControls = [];
7079
+ let reportHeaderControls = [];
7080
+ let pageHeaderControls = [];
7081
+ let detailControls = [];
7082
+ let groupFooterControls = [];
7083
+ let reportFooterControls = [];
7084
+ for (const bandItem of Array.from(bandsEl.children)) {
7085
+ const ctrlType = bandItem.getAttribute("ControlType") || "";
7086
+ if (ctrlType === "TopMarginBand") {
7087
+ topMarginControls = extractControls(bandItem);
7088
+ } else if (ctrlType === "ReportHeaderBand") {
7089
+ reportHeaderControls = extractControls(bandItem);
7090
+ } else if (ctrlType === "PageHeaderBand") {
7091
+ pageHeaderControls = extractControls(bandItem);
7092
+ } else if (ctrlType === "DetailReportBand") {
7093
+ const nestedBands = bandItem.querySelector(":scope > Bands");
7094
+ if (nestedBands) {
7095
+ for (const nested of Array.from(nestedBands.children)) {
7096
+ const nestedType = nested.getAttribute("ControlType") || "";
7097
+ if (nestedType === "DetailBand") {
7098
+ detailControls = extractControls(nested);
7099
+ } else if (nestedType === "GroupFooterBand") {
7100
+ groupFooterControls = extractControls(nested);
7101
+ }
7102
+ }
7103
+ }
7104
+ } else if (ctrlType === "ReportFooterBand") {
7105
+ reportFooterControls = extractControls(bandItem);
7106
+ }
7107
+ }
7108
+ if (topMarginControls.length > 0) {
7109
+ markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
7110
+ }
7111
+ if (reportHeaderControls.length > 0) {
7112
+ markdownSections.push(
7113
+ renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
7114
+ );
7115
+ }
7116
+ if (pageHeaderControls.length > 0 || detailControls.length > 0) {
7117
+ markdownSections.push(
7118
+ renderOperationsSection(
7119
+ pageHeaderControls,
7120
+ detailControls,
7121
+ groupFooterControls,
7122
+ registry)
7123
+ );
7124
+ }
7125
+ if (reportFooterControls.length > 0) {
7126
+ markdownSections.push(
7127
+ renderReportFooterBand(reportFooterControls, registry)
7128
+ );
7129
+ }
7130
+ const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
7131
+ return {
7132
+ markdown,
7133
+ variables: registry.getAll(),
7134
+ reportName,
7135
+ warnings
7136
+ };
7137
+ }
7138
+
4958
7139
  exports.DocumentGenerator = DocumentGenerator;
4959
7140
  exports.EditorPanel = EditorPanel;
4960
7141
  exports.FormFieldType = FormFieldType;
4961
7142
  exports.PreviewPanel = PreviewPanel;
7143
+ exports.RepeatNode = RepeatNode;
7144
+ exports.SubtotalsNode = SubtotalsNode;
4962
7145
  exports.ThemeProvider = ThemeProvider;
7146
+ exports.expandRepeatContent = expandRepeatContent;
7147
+ exports.extractVariablesFromContent = extractVariablesFromContent;
7148
+ exports.generatePdfFromMarkdown = generatePdfFromMarkdown;
7149
+ exports.generatePdfFromTiptap = generatePdfFromTiptap;
7150
+ exports.isZeroLike = isZeroLike;
7151
+ exports.markdownToTiptap = markdownToTiptap;
7152
+ exports.parseXmlTemplate = parseXmlTemplate;
7153
+ exports.suppressZeroContent = suppressZeroContent;
7154
+ exports.tiptapToMarkdown = tiptapToMarkdown;
4963
7155
  exports.useDocumentGenerator = useDocumentGenerator;
4964
7156
  exports.useTheme = useTheme;
4965
7157
  //# sourceMappingURL=index.js.map