@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.mjs CHANGED
@@ -1,17 +1,21 @@
1
1
  import * as React12 from 'react';
2
2
  import React12__default, { createContext, useCallback, useState, useRef, useEffect, useMemo, useContext } from 'react';
3
- import { AlertTriangle, RefreshCw, FileText, ChevronDown, Circle, CheckSquare, Calendar, Hash, PenTool, Type, Braces, ChevronLeft, ChevronRight, ZoomOut, ZoomIn, Maximize2, Loader2, Plus, PanelLeftOpen, CheckCircle2, FileDown, PanelLeftClose, Undo, Redo, Bold, Italic, Underline as Underline$1, Code, Heading1, Heading2, Heading3, List, ListOrdered, Quote, Minus, AlignLeft, AlignCenter, AlignRight, Trash2, X, PenLine, Upload, WrapText, Download } from 'lucide-react';
3
+ import { AlertTriangle, RefreshCw, FileText, ChevronDown, Circle, CheckSquare, Calendar, Hash, PenTool, Type, Braces, Droplets, ChevronLeft, ChevronRight, ZoomOut, ZoomIn, Maximize2, Loader2, Plus, Save, FileDown, CheckCircle2, PanelLeftOpen, PanelLeftClose, Undo, Redo, Bold, Italic, Underline as Underline$1, Code, Heading1, Heading2, Heading3, List, ListOrdered, Quote, Minus, AlignLeft, AlignCenter, AlignRight, Trash2, X, PenLine, Upload, Search, WrapText, Download } from 'lucide-react';
4
4
  import { ReactNodeViewRenderer, NodeViewWrapper, useEditor, EditorContent } from '@tiptap/react';
5
5
  import StarterKit from '@tiptap/starter-kit';
6
6
  import Underline from '@tiptap/extension-underline';
7
7
  import TextAlign from '@tiptap/extension-text-align';
8
8
  import Placeholder from '@tiptap/extension-placeholder';
9
+ import Table from '@tiptap/extension-table';
10
+ import TableRow from '@tiptap/extension-table-row';
11
+ import TableCell from '@tiptap/extension-table-cell';
12
+ import TableHeader from '@tiptap/extension-table-header';
9
13
  import { Node, mergeAttributes } from '@tiptap/core';
10
14
  import { clsx } from 'clsx';
11
15
  import { twMerge } from 'tailwind-merge';
12
16
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
17
  import { v4 } from 'uuid';
14
- import { PDFDocument, StandardFonts, rgb, PDFName as PDFName$1, PDFString as PDFString$1 } from 'pdf-lib';
18
+ import { PDFDocument, StandardFonts, rgb, degrees, PDFName as PDFName$1, PDFString as PDFString$1 } from 'pdf-lib';
15
19
  import { NodeSelection } from '@tiptap/pm/state';
16
20
  import { Slot } from '@radix-ui/react-slot';
17
21
  import { cva } from 'class-variance-authority';
@@ -21,7 +25,7 @@ import * as LabelPrimitive from '@radix-ui/react-label';
21
25
 
22
26
  var __defProp = Object.defineProperty;
23
27
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
24
- var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
28
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
25
29
  function cn(...inputs) {
26
30
  return twMerge(clsx(inputs));
27
31
  }
@@ -213,7 +217,9 @@ var VariableNode = Node.create({
213
217
  return {
214
218
  varName: { default: "" },
215
219
  varLabel: { default: "" },
216
- varDefault: { default: "" }
220
+ varDefault: { default: "" },
221
+ format: { default: "" },
222
+ suppressZero: { default: "" }
217
223
  };
218
224
  },
219
225
  parseHTML() {
@@ -279,6 +285,169 @@ var VariableNode = Node.create({
279
285
  };
280
286
  }
281
287
  });
288
+ var PanelNode = Node.create({
289
+ name: "panel",
290
+ group: "block",
291
+ content: "block+",
292
+ defining: true,
293
+ addAttributes() {
294
+ return {
295
+ title: { default: "" },
296
+ border: { default: "solid" },
297
+ headerStyle: { default: "" }
298
+ };
299
+ },
300
+ parseHTML() {
301
+ return [{
302
+ tag: "div[data-panel-node]",
303
+ contentElement: "div.panel-content"
304
+ }];
305
+ },
306
+ renderHTML({ node, HTMLAttributes }) {
307
+ const { title, border, headerStyle } = node.attrs;
308
+ const classes = ["panel-node"];
309
+ if (border === "dashed") classes.push("panel-border-dashed");
310
+ else if (border === "none") classes.push("panel-border-none");
311
+ else classes.push("panel-border-solid");
312
+ if (headerStyle === "dark") classes.push("panel-dark");
313
+ const attrs = mergeAttributes(
314
+ { "data-panel-node": "", class: classes.join(" ") },
315
+ HTMLAttributes
316
+ );
317
+ if (title) {
318
+ return [
319
+ "div",
320
+ attrs,
321
+ ["div", { class: "panel-title" }, title],
322
+ ["div", { class: "panel-content" }, 0]
323
+ ];
324
+ }
325
+ return ["div", attrs, ["div", { class: "panel-content" }, 0]];
326
+ }
327
+ });
328
+ var ColumnsNode = Node.create({
329
+ name: "columns",
330
+ group: "block",
331
+ content: "column+",
332
+ defining: true,
333
+ addAttributes() {
334
+ return {
335
+ split: { default: "50" },
336
+ padX: { default: "0" }
337
+ };
338
+ },
339
+ parseHTML() {
340
+ return [{ tag: "div[data-columns-node]" }];
341
+ },
342
+ renderHTML({ node, HTMLAttributes }) {
343
+ const split = node.attrs.split || "50";
344
+ return ["div", mergeAttributes(
345
+ {
346
+ "data-columns-node": "",
347
+ class: "columns-node",
348
+ style: `--xpc-col-split: ${split}`
349
+ },
350
+ HTMLAttributes
351
+ ), 0];
352
+ }
353
+ });
354
+ var ColumnNode = Node.create({
355
+ name: "column",
356
+ group: "",
357
+ content: "block+",
358
+ defining: true,
359
+ addAttributes() {
360
+ return {
361
+ padTop: { default: 0 }
362
+ };
363
+ },
364
+ parseHTML() {
365
+ return [{ tag: "div[data-column-node]" }];
366
+ },
367
+ renderHTML({ HTMLAttributes }) {
368
+ return ["div", mergeAttributes(
369
+ { "data-column-node": "", class: "column-node" },
370
+ HTMLAttributes
371
+ ), 0];
372
+ }
373
+ });
374
+ function WatermarkNodeView({ node, selected }) {
375
+ const { text } = node.attrs;
376
+ return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsxs(
377
+ "div",
378
+ {
379
+ "data-watermark-node": "",
380
+ className: cn(
381
+ "inline-flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm my-1",
382
+ "bg-blue-50 text-blue-600 border-blue-200",
383
+ selected && "ring-2 ring-primary ring-offset-1"
384
+ ),
385
+ children: [
386
+ /* @__PURE__ */ jsx(Droplets, { size: 14 }),
387
+ /* @__PURE__ */ jsxs("span", { children: [
388
+ "Watermark: ",
389
+ text || "(empty)"
390
+ ] })
391
+ ]
392
+ }
393
+ ) });
394
+ }
395
+
396
+ // src/extensions/watermark-node.ts
397
+ var WatermarkNode = Node.create({
398
+ name: "watermark",
399
+ group: "block",
400
+ atom: true,
401
+ selectable: true,
402
+ draggable: false,
403
+ addAttributes() {
404
+ return {
405
+ text: { default: "" },
406
+ opacity: { default: "0.15" },
407
+ angle: { default: "-45" }
408
+ };
409
+ },
410
+ parseHTML() {
411
+ return [{ tag: "div[data-watermark-node]" }];
412
+ },
413
+ renderHTML({ HTMLAttributes }) {
414
+ return ["div", mergeAttributes({ "data-watermark-node": "" }, HTMLAttributes)];
415
+ },
416
+ addNodeView() {
417
+ return ReactNodeViewRenderer(WatermarkNodeView);
418
+ }
419
+ });
420
+ var RepeatNode = Node.create({
421
+ name: "repeatBlock",
422
+ group: "block",
423
+ content: "block+",
424
+ addAttributes() {
425
+ return {
426
+ data: { default: "" }
427
+ };
428
+ },
429
+ parseHTML() {
430
+ return [{ tag: "div[data-repeat-node]" }];
431
+ },
432
+ renderHTML({ HTMLAttributes }) {
433
+ return ["div", mergeAttributes({ "data-repeat-node": "" }, HTMLAttributes), 0];
434
+ }
435
+ });
436
+ var SubtotalsNode = Node.create({
437
+ name: "subtotalsBlock",
438
+ group: "block",
439
+ content: "block+",
440
+ defining: true,
441
+ parseHTML() {
442
+ return [{ tag: "div[data-subtotals-node]" }];
443
+ },
444
+ renderHTML({ HTMLAttributes }) {
445
+ return ["div", mergeAttributes(
446
+ { "data-subtotals-node": "", class: "subtotals-block" },
447
+ HTMLAttributes
448
+ ), 0];
449
+ }
450
+ });
282
451
 
283
452
  // src/types/index.ts
284
453
  var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
@@ -381,11 +550,24 @@ function extractVariablesFromContent(content) {
381
550
  }
382
551
  return Array.from(seen.values());
383
552
  }
553
+ function applyFormat(value, format) {
554
+ if (format === "phone") {
555
+ const digits = value.replace(/\D/g, "");
556
+ if (digits.length === 10) {
557
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
558
+ }
559
+ }
560
+ return value;
561
+ }
384
562
  function replaceVariablesInContent(content, values) {
385
563
  function walkNode(node) {
386
564
  if (node.type === "variableNode" && node.attrs) {
387
565
  const varName = node.attrs.varName;
388
- const replacement = values[varName] || node.attrs.varDefault || `[${node.attrs.varLabel || varName}]`;
566
+ let replacement = values[varName] || node.attrs.varDefault || "";
567
+ const format = node.attrs.format;
568
+ if (format && replacement) {
569
+ replacement = applyFormat(replacement, format);
570
+ }
389
571
  const textNode = { type: "text", text: replacement };
390
572
  if (node.marks && node.marks.length > 0) {
391
573
  textNode.marks = node.marks;
@@ -399,116 +581,143 @@ function replaceVariablesInContent(content, values) {
399
581
  }
400
582
  return walkNode(content);
401
583
  }
402
-
403
- // src/utils/markdown-writer.ts
404
- function serializeFieldToken(attrs) {
405
- const parts = ["field"];
406
- if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
407
- if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
408
- if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
409
- if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
410
- if (attrs.required) parts.push(`required:true`);
411
- if (attrs.options) parts.push(`options:${attrs.options}`);
412
- if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
413
- if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
414
- if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
415
- if (attrs.multiline) parts.push(`multiline:true`);
416
- if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
417
- if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
418
- return `{{${parts.join("|")}}}`;
584
+ function labelToVarName(label) {
585
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
419
586
  }
420
- function serializeVariableToken(attrs) {
421
- const parts = ["var"];
422
- if (attrs.varName) parts.push(`name:${attrs.varName}`);
423
- if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
424
- if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
425
- return `{{${parts.join("|")}}}`;
587
+ function isZeroLike(value) {
588
+ if (!value || value.trim() === "") return true;
589
+ const cleaned = value.replace(/[$,\s]/g, "");
590
+ return /^-?0+(\.0+)?$/.test(cleaned);
426
591
  }
427
- function serializeInline(content) {
428
- if (!content) return "";
429
- let result = "";
430
- for (const node of content) {
431
- if (node.type === "fieldNode") {
432
- result += serializeFieldToken(node.attrs || {});
433
- continue;
592
+ function suppressZeroContent(content, values) {
593
+ function hasSuppressedVar(node) {
594
+ if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
595
+ const varName = node.attrs.varName;
596
+ const val = values[varName] || node.attrs.varDefault || "";
597
+ return isZeroLike(val);
434
598
  }
435
- if (node.type === "variableNode") {
436
- let token = serializeVariableToken(node.attrs || {});
437
- const varMarks = node.marks || [];
438
- const isBold = varMarks.some((m) => m.type === "bold");
439
- const isItalic = varMarks.some((m) => m.type === "italic");
440
- const isUnderline = varMarks.some((m) => m.type === "underline");
441
- if (isBold && isItalic) token = `***${token}***`;
442
- else if (isBold) token = `**${token}**`;
443
- else if (isItalic) token = `*${token}*`;
444
- if (isUnderline) token = `__${token}__`;
445
- result += token;
446
- continue;
599
+ return (node.content || []).some(hasSuppressedVar);
600
+ }
601
+ function walkNode(node) {
602
+ if (node.type === "table" && node.content) {
603
+ const filtered = node.content.filter((row) => {
604
+ if (row.type !== "tableRow") return true;
605
+ const isHeader = row.content?.[0]?.type === "tableHeader";
606
+ if (isHeader) return true;
607
+ return !hasSuppressedVar(row);
608
+ });
609
+ const bodyRows = filtered.filter(
610
+ (r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
611
+ );
612
+ if (bodyRows.length === 0) return null;
613
+ return { ...node, content: filtered };
447
614
  }
448
- if (node.type === "text") {
449
- let text = node.text || "";
450
- const marks = node.marks || [];
451
- for (const mark of marks) {
452
- if (mark.type === "bold") text = `**${text}**`;
453
- else if (mark.type === "italic") text = `*${text}*`;
454
- else if (mark.type === "underline") text = `__${text}__`;
455
- else if (mark.type === "code") text = `\`${text}\``;
456
- }
457
- result += text;
615
+ if (node.type === "paragraph" && hasSuppressedVar(node)) {
616
+ return null;
458
617
  }
459
- if (node.type === "hardBreak") {
460
- result += " \n";
618
+ if (node.content) {
619
+ const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
620
+ return { ...node, content: filtered };
461
621
  }
622
+ return node;
462
623
  }
463
- return result;
624
+ return walkNode(content) || content;
464
625
  }
465
- function serializeBlock(node) {
466
- switch (node.type) {
467
- case "heading": {
468
- const level = node.attrs?.level || 1;
469
- const prefix = "#".repeat(level);
470
- return `${prefix} ${serializeInline(node.content)}`;
471
- }
472
- case "paragraph": {
473
- const inline = serializeInline(node.content);
474
- return inline;
475
- }
476
- case "bulletList": {
477
- return (node.content || []).map((item) => {
478
- const inner = (item.content || []).map(serializeBlock).join("\n");
479
- return `- ${inner}`;
480
- }).join("\n");
481
- }
482
- case "orderedList": {
483
- return (node.content || []).map((item, i) => {
484
- const inner = (item.content || []).map(serializeBlock).join("\n");
485
- return `${i + 1}. ${inner}`;
486
- }).join("\n");
487
- }
488
- case "listItem": {
489
- return (node.content || []).map(serializeBlock).join("\n");
626
+ function expandRepeatContent(content, values) {
627
+ const enrichedValues = { ...values };
628
+ function cloneNode(node, dataKey, index) {
629
+ if (node.type === "variableNode" && node.attrs?.varName) {
630
+ const oldName = node.attrs.varName;
631
+ const bareName = oldName.replace(/^operation_/, "");
632
+ const newName = `${dataKey}_${index}_${bareName}`;
633
+ return { ...node, attrs: { ...node.attrs, varName: newName } };
490
634
  }
491
- case "blockquote": {
492
- return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
635
+ if (node.content) {
636
+ return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
493
637
  }
494
- case "codeBlock": {
495
- const text = serializeInline(node.content);
496
- return `\`\`\`
497
- ${text}
498
- \`\`\``;
638
+ return { ...node };
639
+ }
640
+ function walkNode(node) {
641
+ if (node.type === "repeatBlock" && node.attrs?.data) {
642
+ const dataKey = node.attrs.data;
643
+ const arrayJson = values[dataKey];
644
+ if (!arrayJson) return [];
645
+ let items;
646
+ try {
647
+ items = JSON.parse(arrayJson);
648
+ } catch {
649
+ return [];
650
+ }
651
+ const sums = /* @__PURE__ */ new Map();
652
+ for (const item of items) {
653
+ for (const [key, val] of Object.entries(item)) {
654
+ const num = parseFloat(val);
655
+ if (!isNaN(num)) {
656
+ sums.set(key, (sums.get(key) || 0) + num);
657
+ }
658
+ }
659
+ }
660
+ const woKeysByNorm = /* @__PURE__ */ new Map();
661
+ for (const k of Object.keys(enrichedValues)) {
662
+ if (k.startsWith("workorder_")) {
663
+ const norm = k.replace(/_/g, "");
664
+ woKeysByNorm.set(norm, k);
665
+ }
666
+ }
667
+ for (const [field, total] of sums) {
668
+ const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
669
+ const directKey = `workorder_${field}`;
670
+ enrichedValues[directKey] = formatted;
671
+ const norm = `workorder${field}`.replace(/_/g, "");
672
+ const existing = woKeysByNorm.get(norm);
673
+ if (existing && existing !== directKey) {
674
+ enrichedValues[existing] = formatted;
675
+ }
676
+ }
677
+ const expanded = [];
678
+ for (let i = 0; i < items.length; i++) {
679
+ const item = items[i];
680
+ for (const [key, val] of Object.entries(item)) {
681
+ enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
682
+ }
683
+ const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
684
+ expanded.push(...cloned);
685
+ }
686
+ return expanded;
499
687
  }
500
- case "horizontalRule": {
501
- return "---";
688
+ if (node.content) {
689
+ const newContent = [];
690
+ for (const child of node.content) {
691
+ const result2 = walkNode(child);
692
+ if (Array.isArray(result2)) newContent.push(...result2);
693
+ else newContent.push(result2);
694
+ }
695
+ return { ...node, content: newContent };
502
696
  }
503
- default:
504
- return serializeInline(node.content);
697
+ return node;
505
698
  }
506
- }
507
- function tiptapToMarkdown(doc) {
508
- if (!doc.content) return "";
509
- return doc.content.map(serializeBlock).join("\n\n");
699
+ const result = walkNode(content);
700
+ return {
701
+ content: Array.isArray(result) ? { ...content, content: result } : result,
702
+ values: enrichedValues
703
+ };
510
704
  }
511
705
  var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
706
+ var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
707
+ var DIRECTIVE_CLOSE = /^:::$/;
708
+ function parseDirectiveAttrs(str) {
709
+ const attrs = {};
710
+ if (!str) return attrs;
711
+ const pairs = str.split("|");
712
+ for (const pair of pairs) {
713
+ const colonIdx = pair.indexOf(":");
714
+ if (colonIdx === -1) continue;
715
+ const key = pair.slice(0, colonIdx).trim();
716
+ const value = pair.slice(colonIdx + 1).trim();
717
+ if (key) attrs[key] = value;
718
+ }
719
+ return attrs;
720
+ }
512
721
  function injectMarks(token, marks) {
513
722
  const existingMatch = token.match(/\|_marks:([^}|]+)/);
514
723
  if (existingMatch) {
@@ -625,6 +834,12 @@ function parseVariableToken(tokenBody) {
625
834
  case "default":
626
835
  attrs.varDefault = value;
627
836
  break;
837
+ case "suppress":
838
+ attrs.suppressZero = value === "zero" ? "true" : "";
839
+ break;
840
+ case "format":
841
+ attrs.format = value;
842
+ break;
628
843
  case "_marks":
629
844
  attrs._marks = value;
630
845
  break;
@@ -666,45 +881,63 @@ function parseInline(text) {
666
881
  function parseFormattedText(text) {
667
882
  if (!text) return [];
668
883
  const nodes = [];
669
- const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
884
+ const parts = text.split(/(\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
670
885
  for (const part of parts) {
671
886
  if (!part) continue;
672
- if (part.startsWith("**") && part.endsWith("**")) {
673
- nodes.push({
674
- type: "text",
675
- text: part.slice(2, -2),
676
- marks: [{ type: "bold" }]
677
- });
887
+ if (part.startsWith("***") && part.endsWith("***")) {
888
+ const inner = part.slice(3, -3);
889
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }, { type: "italic" }] });
890
+ } else if (part.startsWith("**") && part.endsWith("**")) {
891
+ const inner = part.slice(2, -2);
892
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }] });
678
893
  } else if (part.startsWith("*") && part.endsWith("*")) {
679
- nodes.push({
680
- type: "text",
681
- text: part.slice(1, -1),
682
- marks: [{ type: "italic" }]
683
- });
894
+ const inner = part.slice(1, -1);
895
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "italic" }] });
684
896
  } else if (part.startsWith("__") && part.endsWith("__")) {
685
- nodes.push({
686
- type: "text",
687
- text: part.slice(2, -2),
688
- marks: [{ type: "underline" }]
689
- });
897
+ const inner = part.slice(2, -2);
898
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "underline" }] });
690
899
  } else if (part.startsWith("`") && part.endsWith("`")) {
691
- nodes.push({
692
- type: "text",
693
- text: part.slice(1, -1),
694
- marks: [{ type: "code" }]
695
- });
900
+ const inner = part.slice(1, -1);
901
+ if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "code" }] });
696
902
  } else {
697
903
  nodes.push({ type: "text", text: part });
698
904
  }
699
905
  }
700
906
  return nodes;
701
907
  }
702
- function markdownToTiptap(markdown) {
703
- const lines = markdown.split("\n");
908
+ function splitTableCells(row) {
909
+ const cells = [];
910
+ let current = "";
911
+ let depth = 0;
912
+ for (let i = 0; i < row.length; i++) {
913
+ if (row[i] === "{" && row[i + 1] === "{") {
914
+ depth++;
915
+ current += "{{";
916
+ i++;
917
+ } else if (row[i] === "}" && row[i + 1] === "}") {
918
+ depth = Math.max(0, depth - 1);
919
+ current += "}}";
920
+ i++;
921
+ } else if (row[i] === "|" && depth === 0) {
922
+ cells.push(current.trim());
923
+ current = "";
924
+ } else {
925
+ current += row[i];
926
+ }
927
+ }
928
+ if (current.trim()) {
929
+ cells.push(current.trim());
930
+ }
931
+ return cells;
932
+ }
933
+ function parseBlocks(lines, startIdx, stopCondition) {
704
934
  const content = [];
705
- let i = 0;
935
+ let i = startIdx;
706
936
  while (i < lines.length) {
707
937
  const line = lines[i];
938
+ if (stopCondition && stopCondition(line)) {
939
+ break;
940
+ }
708
941
  if (line.trim() === "") {
709
942
  i++;
710
943
  continue;
@@ -778,41 +1011,218 @@ function markdownToTiptap(markdown) {
778
1011
  });
779
1012
  continue;
780
1013
  }
781
- content.push({
782
- type: "paragraph",
783
- content: parseInline(line)
784
- });
785
- i++;
786
- }
787
- return { type: "doc", content };
788
- }
789
-
790
- // src/utils/error-helpers.ts
791
- function getErrorMessage(error) {
792
- if (error instanceof Error) return error.message;
793
- if (typeof error === "string") return error;
794
- try {
795
- return JSON.stringify(error);
796
- } catch {
797
- return String(error);
798
- }
799
- }
800
- function formatError(context, error) {
801
- const message = getErrorMessage(error);
802
- return `${context}: ${message}`;
803
- }
804
- var CONTEXT_CHARS = 25;
805
- function extractPosition(error, inputLength) {
806
- const msg = error.message;
807
- const v8 = /position\s+(\d+)/i.exec(msg);
808
- if (v8) return Number(v8[1]);
809
- if (/unexpected end/i.test(msg) || /unterminated/i.test(msg)) {
810
- return inputLength > 0 ? inputLength - 1 : 0;
811
- }
812
- return null;
813
- }
814
- function stripNativePosition(msg) {
815
- const v8Stripped = msg.replace(/\s+in JSON at position\s+\d+(\s*\(line\s+\d+\s+column\s+\d+\))?/i, "");
1014
+ if (line.trimStart().startsWith("|") && line.trimEnd().endsWith("|")) {
1015
+ const tableLines = [];
1016
+ while (i < lines.length) {
1017
+ const tl = lines[i];
1018
+ if (tl.trimStart().startsWith("|") && tl.trimEnd().endsWith("|")) {
1019
+ tableLines.push(tl);
1020
+ i++;
1021
+ } else {
1022
+ break;
1023
+ }
1024
+ }
1025
+ const separatorRegex = /^[\s|]*-+[\s|:-]*$/;
1026
+ const dataLines = tableLines.filter(
1027
+ (tl) => !separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))
1028
+ );
1029
+ const firstDataCellsRaw = dataLines.length > 0 ? splitTableCells(dataLines[0]) : [];
1030
+ const isSubtotals = firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "_");
1031
+ const isBorderless = isSubtotals || firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "");
1032
+ const tableRows = [];
1033
+ let isFirstDataRow = true;
1034
+ for (const tl of tableLines) {
1035
+ if (separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))) continue;
1036
+ const cells = splitTableCells(tl).filter((c) => c !== "");
1037
+ if (isBorderless && isFirstDataRow) {
1038
+ isFirstDataRow = false;
1039
+ continue;
1040
+ }
1041
+ const cellType = isFirstDataRow && !isBorderless ? "tableHeader" : "tableCell";
1042
+ const rowContent = cells.map((cellText) => ({
1043
+ type: cellType,
1044
+ content: [{ type: "paragraph", content: cellText ? parseInline(cellText) : [] }]
1045
+ }));
1046
+ tableRows.push({ type: "tableRow", content: rowContent });
1047
+ isFirstDataRow = false;
1048
+ }
1049
+ if (tableRows.length > 0) {
1050
+ const tableNode = { type: "table", content: tableRows };
1051
+ if (isBorderless) {
1052
+ tableNode.attrs = { borderless: true, ...isSubtotals ? { subtotals: true } : {} };
1053
+ }
1054
+ content.push(tableNode);
1055
+ }
1056
+ continue;
1057
+ }
1058
+ const directiveMatch = line.trim().match(DIRECTIVE_OPEN);
1059
+ if (directiveMatch) {
1060
+ const directiveType = directiveMatch[1];
1061
+ const rawAttrs = directiveMatch[2] || "";
1062
+ const attrs = parseDirectiveAttrs(rawAttrs);
1063
+ i++;
1064
+ if (directiveType === "watermark") {
1065
+ content.push({
1066
+ type: "watermark",
1067
+ attrs: {
1068
+ text: attrs.text || "",
1069
+ opacity: attrs.opacity || "0.15",
1070
+ angle: attrs.angle || "-45"
1071
+ }
1072
+ });
1073
+ continue;
1074
+ }
1075
+ if (directiveType === "panel") {
1076
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1077
+ i = inner.nextIdx;
1078
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1079
+ i++;
1080
+ }
1081
+ content.push({
1082
+ type: "panel",
1083
+ attrs: {
1084
+ title: attrs.title || "",
1085
+ border: attrs.border || "solid",
1086
+ headerStyle: attrs.headerStyle || ""
1087
+ },
1088
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1089
+ });
1090
+ continue;
1091
+ }
1092
+ if (directiveType === "repeat") {
1093
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1094
+ i = inner.nextIdx;
1095
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1096
+ i++;
1097
+ }
1098
+ content.push({
1099
+ type: "repeatBlock",
1100
+ attrs: { data: attrs.data || "" },
1101
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1102
+ });
1103
+ continue;
1104
+ }
1105
+ if (directiveType === "subtotals") {
1106
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1107
+ i = inner.nextIdx;
1108
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1109
+ i++;
1110
+ }
1111
+ content.push({
1112
+ type: "subtotalsBlock",
1113
+ content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
1114
+ });
1115
+ continue;
1116
+ }
1117
+ if (directiveType === "columns") {
1118
+ const splitVal = attrs.split || "50";
1119
+ const columns = [];
1120
+ while (i < lines.length) {
1121
+ const colLine = lines[i].trim();
1122
+ if (DIRECTIVE_CLOSE.test(colLine)) {
1123
+ if (columns.length > 0) {
1124
+ i++;
1125
+ break;
1126
+ }
1127
+ i++;
1128
+ break;
1129
+ }
1130
+ const colMatch = colLine.match(/^:::col(?:\{([^}]*)\})?$/);
1131
+ if (colMatch) {
1132
+ i++;
1133
+ const colInner = parseBlocks(lines, i, (l) => {
1134
+ const trimmed = l.trim();
1135
+ return DIRECTIVE_CLOSE.test(trimmed) || /^:::col(?:\{[^}]*\})?$/.test(trimmed);
1136
+ });
1137
+ i = colInner.nextIdx;
1138
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1139
+ i++;
1140
+ }
1141
+ const colAttrsRaw = colMatch[1] || "";
1142
+ const colAttrs = parseDirectiveAttrs(colAttrsRaw);
1143
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
1144
+ columns.push({
1145
+ type: "column",
1146
+ attrs: padTop ? { padTop } : void 0,
1147
+ content: colInner.blocks.length > 0 ? colInner.blocks : [{ type: "paragraph" }]
1148
+ });
1149
+ continue;
1150
+ }
1151
+ if (colLine === "") {
1152
+ i++;
1153
+ continue;
1154
+ }
1155
+ break;
1156
+ }
1157
+ const columnsAttrs = { split: splitVal };
1158
+ if (attrs.padX) columnsAttrs.padX = attrs.padX;
1159
+ content.push({
1160
+ type: "columns",
1161
+ attrs: columnsAttrs,
1162
+ content: columns.length > 0 ? columns : [
1163
+ { type: "column", content: [{ type: "paragraph" }] },
1164
+ { type: "column", content: [{ type: "paragraph" }] }
1165
+ ]
1166
+ });
1167
+ continue;
1168
+ }
1169
+ if (directiveType === "col") {
1170
+ const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
1171
+ i = inner.nextIdx;
1172
+ if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
1173
+ i++;
1174
+ }
1175
+ for (const block of inner.blocks) {
1176
+ content.push(block);
1177
+ }
1178
+ continue;
1179
+ }
1180
+ continue;
1181
+ }
1182
+ if (DIRECTIVE_CLOSE.test(line.trim())) {
1183
+ i++;
1184
+ continue;
1185
+ }
1186
+ content.push({
1187
+ type: "paragraph",
1188
+ content: parseInline(line)
1189
+ });
1190
+ i++;
1191
+ }
1192
+ return { blocks: content, nextIdx: i };
1193
+ }
1194
+ function markdownToTiptap(markdown) {
1195
+ const lines = markdown.split("\n");
1196
+ const { blocks } = parseBlocks(lines, 0);
1197
+ return { type: "doc", content: blocks };
1198
+ }
1199
+
1200
+ // src/utils/error-helpers.ts
1201
+ function getErrorMessage(error) {
1202
+ if (error instanceof Error) return error.message;
1203
+ if (typeof error === "string") return error;
1204
+ try {
1205
+ return JSON.stringify(error);
1206
+ } catch {
1207
+ return String(error);
1208
+ }
1209
+ }
1210
+ function formatError(context, error) {
1211
+ const message = getErrorMessage(error);
1212
+ return `${context}: ${message}`;
1213
+ }
1214
+ var CONTEXT_CHARS = 25;
1215
+ function extractPosition(error, inputLength) {
1216
+ const msg = error.message;
1217
+ const v8 = /position\s+(\d+)/i.exec(msg);
1218
+ if (v8) return Number(v8[1]);
1219
+ if (/unexpected end/i.test(msg) || /unterminated/i.test(msg)) {
1220
+ return inputLength > 0 ? inputLength - 1 : 0;
1221
+ }
1222
+ return null;
1223
+ }
1224
+ function stripNativePosition(msg) {
1225
+ const v8Stripped = msg.replace(/\s+in JSON at position\s+\d+(\s*\(line\s+\d+\s+column\s+\d+\))?/i, "");
816
1226
  if (v8Stripped !== msg) return v8Stripped;
817
1227
  const ffStripped = msg.replace(/\s+at line\s+\d+\s+column\s+\d+\s+of the JSON data/i, "");
818
1228
  if (ffStripped !== msg) return ffStripped;
@@ -922,10 +1332,12 @@ function buildMetadataObject(fields, actualFieldNames) {
922
1332
  // src/utils/pdf-generator.ts
923
1333
  var PAGE_WIDTH = 595.28;
924
1334
  var PAGE_HEIGHT = 841.89;
925
- var MARGIN = 72;
1335
+ var MARGIN = 40;
926
1336
  var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
927
1337
  var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
928
1338
  var LINE_HEIGHT_FACTOR = 1.4;
1339
+ var BODY_FONT_SIZE = 10;
1340
+ var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
929
1341
  var FIELD_DISPLAY = {
930
1342
  ["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
931
1343
  ["signature" /* SIGNATURE */]: { label: "Signature", width: 200, height: 60 },
@@ -962,11 +1374,12 @@ function ensureSpace(state, neededHeight) {
962
1374
  state.currentPage = newPage;
963
1375
  state.pageIndex++;
964
1376
  state.y = 0;
1377
+ renderWatermarksOnPage(newPage, state.fonts, state.watermarkNodes);
965
1378
  }
966
1379
  }
967
- function drawText(state, text, font, fontSize, indent = 0) {
1380
+ function drawText(state, text, font, fontSize, indent = 0, region = DEFAULT_REGION) {
968
1381
  const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
969
- const maxWidth = CONTENT_WIDTH - indent;
1382
+ const maxWidth = region.width - indent;
970
1383
  const words = text.split(/\s+/);
971
1384
  let line = "";
972
1385
  let totalAdvance = 0;
@@ -977,7 +1390,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
977
1390
  ensureSpace(state, lineHeight);
978
1391
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
979
1392
  state.currentPage.drawText(line, {
980
- x: MARGIN + indent,
1393
+ x: region.leftX + indent,
981
1394
  y: pdfY,
982
1395
  size: fontSize,
983
1396
  font,
@@ -994,7 +1407,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
994
1407
  ensureSpace(state, lineHeight);
995
1408
  const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
996
1409
  state.currentPage.drawText(line, {
997
- x: MARGIN + indent,
1410
+ x: region.leftX + indent,
998
1411
  y: pdfY,
999
1412
  size: fontSize,
1000
1413
  font,
@@ -1038,10 +1451,10 @@ function collectInlineSegments(content, fonts, fontSize) {
1038
1451
  }
1039
1452
  return segments;
1040
1453
  }
1041
- function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1454
+ function layoutInlineSegments(state, segments, fontSize, indent = 0, region = DEFAULT_REGION) {
1042
1455
  const textLineHeight = fontSize * LINE_HEIGHT_FACTOR;
1043
- const maxX = MARGIN + CONTENT_WIDTH;
1044
- const startX = MARGIN + indent;
1456
+ const maxX = region.leftX + region.width;
1457
+ const startX = region.leftX + indent;
1045
1458
  let currentX = startX;
1046
1459
  let currentLineHeight = 0;
1047
1460
  let lineHasContent = false;
@@ -1146,13 +1559,316 @@ function layoutInlineSegments(state, segments, fontSize, indent = 0) {
1146
1559
  }
1147
1560
  flushLine();
1148
1561
  }
1149
- function processInlineContent(state, content, fontSize, indent = 0) {
1562
+ function processInlineContent(state, content, fontSize, indent = 0, region = DEFAULT_REGION) {
1150
1563
  if (!content) return;
1151
1564
  const segments = collectInlineSegments(content, state.fonts, fontSize);
1152
1565
  if (segments.length === 0) return;
1153
- layoutInlineSegments(state, segments, fontSize, indent);
1566
+ layoutInlineSegments(state, segments, fontSize, indent, region);
1567
+ }
1568
+ function extractCellText(cell) {
1569
+ const parts = [];
1570
+ for (const child of cell.content || []) {
1571
+ if (child.type === "paragraph" || child.type === "heading") {
1572
+ for (const inline of child.content || []) {
1573
+ if (inline.type === "text") {
1574
+ parts.push(inline.text || "");
1575
+ } else if (inline.type === "variableNode") {
1576
+ const label = inline.attrs?.varLabel || inline.attrs?.varName || "Variable";
1577
+ parts.push(`[${label}]`);
1578
+ } else if (inline.type === "fieldNode") {
1579
+ const label = inline.attrs?.fieldLabel || inline.attrs?.fieldName || "Field";
1580
+ parts.push(`[ ${label} ]`);
1581
+ }
1582
+ }
1583
+ }
1584
+ }
1585
+ return parts.join("");
1586
+ }
1587
+ function isNumericText(text) {
1588
+ return /^\s*\$?\s*[\d,.]+\s*$/.test(text);
1589
+ }
1590
+ function measureCellHeight(cell, maxWidth, fonts, fontSize, isHeader) {
1591
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1592
+ const font = isHeader ? fonts.bold : fonts.regular;
1593
+ let totalHeight = 0;
1594
+ for (const child of cell.content || []) {
1595
+ if (child.type === "paragraph" || child.type === "heading") {
1596
+ const text = extractCellText({ content: [child] });
1597
+ if (!text) {
1598
+ totalHeight += lineHeight;
1599
+ continue;
1600
+ }
1601
+ const words = text.split(/\s+/);
1602
+ let line = "";
1603
+ let lineCount = 0;
1604
+ for (const word of words) {
1605
+ const testLine = line ? `${line} ${word}` : word;
1606
+ const testWidth = font.widthOfTextAtSize(testLine, fontSize);
1607
+ if (testWidth > maxWidth && line) {
1608
+ lineCount++;
1609
+ line = word;
1610
+ } else {
1611
+ line = testLine;
1612
+ }
1613
+ }
1614
+ if (line) lineCount++;
1615
+ totalHeight += lineCount * lineHeight;
1616
+ }
1617
+ }
1618
+ return Math.max(totalHeight, lineHeight);
1619
+ }
1620
+ function renderCellContent(state, cell, cellX, cellY, cellWidth, fonts, fontSize, isHeader, rightAlign, textColor = rgb(0, 0, 0)) {
1621
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1622
+ const cellPadding = 4;
1623
+ const maxTextWidth = cellWidth - cellPadding * 2;
1624
+ let textX = cellX + cellPadding;
1625
+ let textY = cellY;
1626
+ for (const child of cell.content || []) {
1627
+ if (child.type === "paragraph" || child.type === "heading") {
1628
+ const segments = collectInlineSegments(
1629
+ child.content || [],
1630
+ isHeader ? { regular: fonts.bold, bold: fonts.bold, italic: fonts.boldItalic, boldItalic: fonts.boldItalic } : fonts,
1631
+ fontSize
1632
+ );
1633
+ if (rightAlign && segments.length > 0) {
1634
+ const fullText = segments.filter((s) => s.kind === "text").map((s) => s.text).join("");
1635
+ const textWidth = (isHeader ? fonts.bold : fonts.regular).widthOfTextAtSize(fullText, fontSize);
1636
+ if (textWidth <= maxTextWidth) {
1637
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1638
+ state.currentPage.drawText(fullText, {
1639
+ x: cellX + cellWidth - cellPadding - textWidth,
1640
+ y: pdfY,
1641
+ size: fontSize,
1642
+ font: isHeader ? fonts.bold : fonts.regular,
1643
+ color: textColor
1644
+ });
1645
+ return;
1646
+ }
1647
+ }
1648
+ let currentX = textX;
1649
+ for (const seg of segments) {
1650
+ if (seg.kind === "break") {
1651
+ textY += lineHeight;
1652
+ currentX = textX;
1653
+ continue;
1654
+ }
1655
+ if (seg.kind === "text") {
1656
+ const words = seg.text.split(/(\s+)/);
1657
+ const spaceWidth = seg.font.widthOfTextAtSize(" ", fontSize);
1658
+ for (const token of words) {
1659
+ if (!token) continue;
1660
+ if (/^\s+$/.test(token)) {
1661
+ currentX += spaceWidth;
1662
+ continue;
1663
+ }
1664
+ const wordWidth = seg.font.widthOfTextAtSize(token, fontSize);
1665
+ if (currentX + wordWidth > textX + maxTextWidth && currentX > textX) {
1666
+ textY += lineHeight;
1667
+ currentX = textX;
1668
+ }
1669
+ const pdfY = PAGE_HEIGHT - textY - fontSize;
1670
+ state.currentPage.drawText(token, {
1671
+ x: currentX,
1672
+ y: pdfY,
1673
+ size: fontSize,
1674
+ font: seg.font,
1675
+ color: textColor
1676
+ });
1677
+ currentX += wordWidth;
1678
+ }
1679
+ } else if (seg.kind === "field") {
1680
+ const pdfY = PAGE_HEIGHT - textY - seg.height;
1681
+ if (state.drawFieldPlaceholders) {
1682
+ state.currentPage.drawRectangle({
1683
+ x: currentX,
1684
+ y: pdfY,
1685
+ width: seg.width,
1686
+ height: seg.height,
1687
+ borderColor: rgb(0.5, 0.5, 0.7),
1688
+ borderWidth: 0.5,
1689
+ color: rgb(0.95, 0.95, 1),
1690
+ borderDashArray: [4, 2]
1691
+ });
1692
+ }
1693
+ const fieldId = seg.attrs.fieldId;
1694
+ if (fieldId) {
1695
+ state.fieldPositions.set(fieldId, {
1696
+ x: currentX,
1697
+ y: pdfY,
1698
+ width: seg.width,
1699
+ height: seg.height,
1700
+ page: state.pageIndex + 1
1701
+ });
1702
+ }
1703
+ currentX += seg.width + 4;
1704
+ }
1705
+ }
1706
+ }
1707
+ }
1708
+ }
1709
+ function renderTable(state, node, region = DEFAULT_REGION) {
1710
+ const tableRows = node.content || [];
1711
+ if (tableRows.length === 0) return;
1712
+ const borderless = node.attrs?.borderless === true;
1713
+ const subtotals = node.attrs?.subtotals === true;
1714
+ const fontSize = 10;
1715
+ const cellPadding = borderless ? 2 : 4;
1716
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1717
+ const rowPadding = cellPadding * 2;
1718
+ const borderColor = rgb(0.75, 0.75, 0.75);
1719
+ const headerBg = rgb(0.3, 0.3, 0.3);
1720
+ const headerTextColor = rgb(1, 1, 1);
1721
+ const firstRow = tableRows[0];
1722
+ const colCount = firstRow.content?.length || 0;
1723
+ if (colCount === 0) return;
1724
+ const colMaxWidths = new Array(colCount).fill(0);
1725
+ for (const row of tableRows) {
1726
+ const cells = row.content || [];
1727
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1728
+ const cell = cells[c];
1729
+ const isHeader = cell.type === "tableHeader";
1730
+ const text = extractCellText(cell);
1731
+ const font = isHeader ? state.fonts.bold : state.fonts.regular;
1732
+ const textWidth = font.widthOfTextAtSize(text, fontSize);
1733
+ colMaxWidths[c] = Math.max(colMaxWidths[c], textWidth + cellPadding * 2 + 4);
1734
+ }
1735
+ }
1736
+ const totalMeasured = colMaxWidths.reduce((a, b) => a + b, 0);
1737
+ const minColWidth = 40;
1738
+ let colWidths;
1739
+ if (totalMeasured <= region.width) {
1740
+ const scale = region.width / totalMeasured;
1741
+ colWidths = colMaxWidths.map((w) => Math.max(w * scale, minColWidth));
1742
+ } else {
1743
+ colWidths = colMaxWidths.map(
1744
+ (w) => Math.max(w / totalMeasured * region.width, minColWidth)
1745
+ );
1746
+ }
1747
+ const colSum = colWidths.reduce((a, b) => a + b, 0);
1748
+ if (colSum > 0) {
1749
+ const normFactor = region.width / colSum;
1750
+ colWidths = colWidths.map((w) => w * normFactor);
1751
+ }
1752
+ const headerRow = tableRows[0];
1753
+ const hasHeader = headerRow?.content?.[0]?.type === "tableHeader";
1754
+ function renderRow(row, isHeaderRow) {
1755
+ const cells = row.content || [];
1756
+ const isHeader = isHeaderRow && hasHeader;
1757
+ let rowHeight = lineHeight;
1758
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1759
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
1760
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, isHeader);
1761
+ rowHeight = Math.max(rowHeight, cellH);
1762
+ }
1763
+ rowHeight += rowPadding;
1764
+ ensureSpace(state, rowHeight);
1765
+ const rowTopY = state.y;
1766
+ let cellX = region.leftX;
1767
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1768
+ const cellW = colWidths[c];
1769
+ const cell = cells[c];
1770
+ const pdfCellTop = PAGE_HEIGHT - MARGIN - rowTopY;
1771
+ const pdfCellBottom = pdfCellTop - rowHeight;
1772
+ if (isHeader && !borderless) {
1773
+ state.currentPage.drawRectangle({
1774
+ x: cellX,
1775
+ y: pdfCellBottom,
1776
+ width: cellW,
1777
+ height: rowHeight,
1778
+ color: headerBg
1779
+ });
1780
+ }
1781
+ if (!borderless) {
1782
+ state.currentPage.drawRectangle({
1783
+ x: cellX,
1784
+ y: pdfCellBottom,
1785
+ width: cellW,
1786
+ height: rowHeight,
1787
+ borderColor,
1788
+ borderWidth: 0.5
1789
+ });
1790
+ }
1791
+ const cellText = extractCellText(cell);
1792
+ const rightAlign = !isHeader && isNumericText(cellText);
1793
+ const contentY = MARGIN + rowTopY + cellPadding;
1794
+ const effectiveHeader = isHeader && !borderless;
1795
+ renderCellContent(
1796
+ state,
1797
+ cell,
1798
+ cellX,
1799
+ contentY,
1800
+ cellW,
1801
+ state.fonts,
1802
+ fontSize,
1803
+ effectiveHeader,
1804
+ rightAlign,
1805
+ effectiveHeader ? headerTextColor : rgb(0, 0, 0)
1806
+ );
1807
+ cellX += cellW;
1808
+ }
1809
+ state.y += rowHeight;
1810
+ }
1811
+ if (hasHeader && tableRows.length > 0) {
1812
+ renderRow(tableRows[0], true);
1813
+ }
1814
+ let lastPageIndex = state.pageIndex;
1815
+ const startIdx = hasHeader ? 1 : 0;
1816
+ const bodyRowCount = tableRows.length - startIdx;
1817
+ for (let r = startIdx; r < tableRows.length; r++) {
1818
+ const cells = tableRows[r].content || [];
1819
+ let estRowHeight = lineHeight;
1820
+ for (let c = 0; c < cells.length && c < colCount; c++) {
1821
+ const cellContentWidth = colWidths[c] - cellPadding * 2;
1822
+ const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, false);
1823
+ estRowHeight = Math.max(estRowHeight, cellH);
1824
+ }
1825
+ estRowHeight += rowPadding;
1826
+ if (state.y + estRowHeight > CONTENT_HEIGHT) {
1827
+ ensureSpace(state, estRowHeight + (hasHeader ? lineHeight + rowPadding : 0));
1828
+ if (state.pageIndex !== lastPageIndex && hasHeader) {
1829
+ renderRow(headerRow, true);
1830
+ lastPageIndex = state.pageIndex;
1831
+ }
1832
+ }
1833
+ const rowTopYBeforeRender = state.y;
1834
+ renderRow(tableRows[r], false);
1835
+ lastPageIndex = state.pageIndex;
1836
+ if (subtotals && colCount >= 2) {
1837
+ const bodyIdx = r - startIdx;
1838
+ const isFirstBody = bodyIdx === 0;
1839
+ const isLastBody = bodyIdx === bodyRowCount - 1;
1840
+ if (isFirstBody || isLastBody) {
1841
+ const valueStartX = region.leftX + colWidths[0];
1842
+ const valueEndX = region.leftX + colWidths.reduce((a, b) => a + b, 0);
1843
+ const pdfRowTopY = PAGE_HEIGHT - MARGIN - rowTopYBeforeRender;
1844
+ const overlineColor = rgb(0, 0, 0);
1845
+ if (isFirstBody) {
1846
+ state.currentPage.drawLine({
1847
+ start: { x: valueStartX, y: pdfRowTopY },
1848
+ end: { x: valueEndX, y: pdfRowTopY },
1849
+ thickness: 0.5,
1850
+ color: overlineColor
1851
+ });
1852
+ } else if (isLastBody) {
1853
+ state.currentPage.drawLine({
1854
+ start: { x: valueStartX, y: pdfRowTopY + 2 },
1855
+ end: { x: valueEndX, y: pdfRowTopY + 2 },
1856
+ thickness: 0.5,
1857
+ color: overlineColor
1858
+ });
1859
+ state.currentPage.drawLine({
1860
+ start: { x: valueStartX, y: pdfRowTopY },
1861
+ end: { x: valueEndX, y: pdfRowTopY },
1862
+ thickness: 0.5,
1863
+ color: overlineColor
1864
+ });
1865
+ }
1866
+ }
1867
+ }
1868
+ }
1869
+ state.y += 3;
1154
1870
  }
1155
- function processBlock(state, node) {
1871
+ function processBlock(state, node, region = DEFAULT_REGION) {
1156
1872
  switch (node.type) {
1157
1873
  case "heading": {
1158
1874
  const level = node.attrs?.level || 1;
@@ -1169,34 +1885,34 @@ function processBlock(state, node) {
1169
1885
  boldItalic: state.fonts.boldItalic
1170
1886
  };
1171
1887
  const segments = collectInlineSegments(node.content, headingFonts, fontSize);
1172
- layoutInlineSegments(state, segments, fontSize);
1888
+ layoutInlineSegments(state, segments, fontSize, 0, region);
1173
1889
  state.y += spacing / 2;
1174
1890
  break;
1175
1891
  }
1176
1892
  case "paragraph": {
1177
- const fontSize = 12;
1893
+ const fontSize = BODY_FONT_SIZE;
1178
1894
  if (!node.content || node.content.length === 0) {
1179
- state.y += fontSize * LINE_HEIGHT_FACTOR;
1895
+ state.y += fontSize * LINE_HEIGHT_FACTOR * 0.65;
1180
1896
  break;
1181
1897
  }
1182
- processInlineContent(state, node.content, fontSize);
1183
- state.y += 4;
1898
+ processInlineContent(state, node.content, fontSize, 0, region);
1899
+ state.y += 2;
1184
1900
  break;
1185
1901
  }
1186
1902
  case "bulletList": {
1187
1903
  for (const item of node.content || []) {
1188
1904
  for (const child of item.content || []) {
1189
- const fontSize = 12;
1905
+ const fontSize = BODY_FONT_SIZE;
1190
1906
  ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1191
1907
  const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1192
1908
  state.currentPage.drawText("\u2022", {
1193
- x: MARGIN + 8,
1909
+ x: region.leftX + 8,
1194
1910
  y: bulletPdfY,
1195
1911
  size: fontSize,
1196
1912
  font: state.fonts.regular,
1197
1913
  color: rgb(0, 0, 0)
1198
1914
  });
1199
- processInlineContent(state, child.content, fontSize, 24);
1915
+ processInlineContent(state, child.content, fontSize, 24, region);
1200
1916
  }
1201
1917
  }
1202
1918
  state.y += 4;
@@ -1206,17 +1922,17 @@ function processBlock(state, node) {
1206
1922
  let num = 1;
1207
1923
  for (const item of node.content || []) {
1208
1924
  for (const child of item.content || []) {
1209
- const fontSize = 12;
1925
+ const fontSize = BODY_FONT_SIZE;
1210
1926
  ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
1211
1927
  const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
1212
1928
  state.currentPage.drawText(`${num}.`, {
1213
- x: MARGIN + 4,
1929
+ x: region.leftX + 4,
1214
1930
  y: numPdfY,
1215
1931
  size: fontSize,
1216
1932
  font: state.fonts.regular,
1217
1933
  color: rgb(0, 0, 0)
1218
1934
  });
1219
- processInlineContent(state, child.content, fontSize, 24);
1935
+ processInlineContent(state, child.content, fontSize, 24, region);
1220
1936
  }
1221
1937
  num++;
1222
1938
  }
@@ -1226,10 +1942,10 @@ function processBlock(state, node) {
1226
1942
  case "blockquote": {
1227
1943
  const startY = state.y;
1228
1944
  for (const child of node.content || []) {
1229
- processInlineContent(state, child.content, 12, 16);
1945
+ processInlineContent(state, child.content, 12, 16, region);
1230
1946
  }
1231
1947
  const endY = state.y;
1232
- const barX = MARGIN + 4;
1948
+ const barX = region.leftX + 4;
1233
1949
  const barTop = PAGE_HEIGHT - MARGIN - startY;
1234
1950
  const barBottom = PAGE_HEIGHT - MARGIN - endY;
1235
1951
  state.currentPage.drawLine({
@@ -1242,16 +1958,16 @@ function processBlock(state, node) {
1242
1958
  break;
1243
1959
  }
1244
1960
  case "horizontalRule": {
1245
- ensureSpace(state, 16);
1246
- state.y += 8;
1961
+ ensureSpace(state, 10);
1962
+ state.y += 4;
1247
1963
  const ruleY = PAGE_HEIGHT - MARGIN - state.y;
1248
1964
  state.currentPage.drawLine({
1249
- start: { x: MARGIN, y: ruleY },
1250
- end: { x: PAGE_WIDTH - MARGIN, y: ruleY },
1251
- thickness: 1,
1252
- color: rgb(0.7, 0.7, 0.7)
1965
+ start: { x: region.leftX, y: ruleY },
1966
+ end: { x: region.leftX + region.width, y: ruleY },
1967
+ thickness: 2.5,
1968
+ color: rgb(0, 0, 0)
1253
1969
  });
1254
- state.y += 8;
1970
+ state.y += 4;
1255
1971
  break;
1256
1972
  }
1257
1973
  case "codeBlock": {
@@ -1263,9 +1979,9 @@ function processBlock(state, node) {
1263
1979
  ensureSpace(state, blockHeight);
1264
1980
  const boxY = PAGE_HEIGHT - MARGIN - state.y - blockHeight;
1265
1981
  state.currentPage.drawRectangle({
1266
- x: MARGIN,
1982
+ x: region.leftX,
1267
1983
  y: boxY,
1268
- width: CONTENT_WIDTH,
1984
+ width: region.width,
1269
1985
  height: blockHeight,
1270
1986
  color: rgb(0.95, 0.95, 0.95),
1271
1987
  borderColor: rgb(0.85, 0.85, 0.85),
@@ -1273,18 +1989,292 @@ function processBlock(state, node) {
1273
1989
  });
1274
1990
  state.y += 8;
1275
1991
  for (const line of lines) {
1276
- drawText(state, line || " ", state.fonts.regular, fontSize, 8);
1992
+ drawText(state, line || " ", state.fonts.regular, fontSize, 8, region);
1277
1993
  }
1278
1994
  state.y += 8;
1279
1995
  break;
1280
1996
  }
1997
+ case "table": {
1998
+ renderTable(state, node, region);
1999
+ break;
2000
+ }
2001
+ // ─── Layout directives ─────────────────────────────────────────
2002
+ case "panel": {
2003
+ const title = node.attrs?.title || "";
2004
+ const border = node.attrs?.border || "solid";
2005
+ const headerStyle = node.attrs?.headerStyle || "";
2006
+ const padding = 8;
2007
+ const titleFontSize = 10;
2008
+ const titleHeight = title ? titleFontSize * LINE_HEIGHT_FACTOR + 6 : 0;
2009
+ const panelStartY = state.y;
2010
+ const panelStartPage = state.pageIndex;
2011
+ if (title) {
2012
+ state.y += titleHeight;
2013
+ }
2014
+ const isHeaderOnly = border === "none";
2015
+ if (!isHeaderOnly) {
2016
+ state.y += padding;
2017
+ }
2018
+ const innerRegion = {
2019
+ leftX: region.leftX + padding,
2020
+ width: region.width - padding * 2
2021
+ };
2022
+ if (!isHeaderOnly) {
2023
+ for (const child of node.content || []) {
2024
+ processBlock(state, child, innerRegion);
2025
+ }
2026
+ state.y += padding;
2027
+ }
2028
+ const startPage = state.pdfDoc.getPages()[panelStartPage];
2029
+ if (title) {
2030
+ const titleBarY = PAGE_HEIGHT - MARGIN - panelStartY - titleHeight;
2031
+ const isDarkHeader = headerStyle === "dark";
2032
+ startPage.drawRectangle({
2033
+ x: region.leftX,
2034
+ y: titleBarY,
2035
+ width: region.width,
2036
+ height: titleHeight,
2037
+ color: isDarkHeader ? rgb(0.25, 0.25, 0.25) : rgb(0.93, 0.93, 0.93),
2038
+ borderColor: rgb(0.6, 0.6, 0.6),
2039
+ borderWidth: 0.75
2040
+ });
2041
+ startPage.drawText(title, {
2042
+ x: region.leftX + padding,
2043
+ y: titleBarY + 4,
2044
+ size: titleFontSize,
2045
+ font: state.fonts.bold,
2046
+ color: isDarkHeader ? rgb(1, 1, 1) : rgb(0, 0, 0)
2047
+ });
2048
+ }
2049
+ if (border !== "none") {
2050
+ const borderDashArray = border === "dashed" ? [4, 2] : void 0;
2051
+ if (panelStartPage === state.pageIndex) {
2052
+ const borderY = PAGE_HEIGHT - MARGIN - state.y;
2053
+ const panelHeight = state.y - panelStartY;
2054
+ startPage.drawRectangle({
2055
+ x: region.leftX,
2056
+ y: borderY,
2057
+ width: region.width,
2058
+ height: panelHeight,
2059
+ borderColor: rgb(0.6, 0.6, 0.6),
2060
+ borderWidth: 0.75,
2061
+ borderDashArray
2062
+ });
2063
+ } else {
2064
+ const startBorderBottom = MARGIN;
2065
+ const startPanelHeight = PAGE_HEIGHT - MARGIN - panelStartY - startBorderBottom;
2066
+ startPage.drawRectangle({
2067
+ x: region.leftX,
2068
+ y: startBorderBottom,
2069
+ width: region.width,
2070
+ height: startPanelHeight,
2071
+ borderColor: rgb(0.6, 0.6, 0.6),
2072
+ borderWidth: 0.75,
2073
+ borderDashArray
2074
+ });
2075
+ const contTop = PAGE_HEIGHT - MARGIN;
2076
+ const contBorderY = PAGE_HEIGHT - MARGIN - state.y;
2077
+ const contHeight = contTop - contBorderY;
2078
+ state.currentPage.drawRectangle({
2079
+ x: region.leftX,
2080
+ y: contBorderY,
2081
+ width: region.width,
2082
+ height: contHeight,
2083
+ borderColor: rgb(0.6, 0.6, 0.6),
2084
+ borderWidth: 0.75,
2085
+ borderDashArray
2086
+ });
2087
+ }
2088
+ }
2089
+ state.y += 4;
2090
+ break;
2091
+ }
2092
+ case "columns": {
2093
+ const split = parseInt(node.attrs?.split || "50", 10);
2094
+ const padX = parseFloat(node.attrs?.padX || "0") || 0;
2095
+ const columns = (node.content || []).filter((c) => c.type === "column");
2096
+ if (columns.length === 0) break;
2097
+ const colRegionLeftX = region.leftX + padX;
2098
+ const colRegionWidth = region.width - 2 * padX;
2099
+ const gap = 12;
2100
+ const columnsStartY = state.y;
2101
+ const columnsStartPage = state.pageIndex;
2102
+ let colWidths;
2103
+ if (columns.length === 2) {
2104
+ const firstWidth = (colRegionWidth - gap) * (split / 100);
2105
+ const secondWidth = colRegionWidth - gap - firstWidth;
2106
+ colWidths = [firstWidth, secondWidth];
2107
+ } else {
2108
+ const eachWidth = (colRegionWidth - gap * (columns.length - 1)) / columns.length;
2109
+ colWidths = columns.map(() => eachWidth);
2110
+ }
2111
+ let maxEndY = columnsStartY;
2112
+ let colX = colRegionLeftX;
2113
+ for (let ci = 0; ci < columns.length; ci++) {
2114
+ const col = columns[ci];
2115
+ const colWidth = colWidths[ci];
2116
+ const colRegion = { leftX: colX, width: colWidth };
2117
+ state.y = columnsStartY;
2118
+ state.pageIndex = columnsStartPage;
2119
+ state.currentPage = state.pdfDoc.getPages()[state.pageIndex];
2120
+ const padTop = parseFloat(col.attrs?.padTop || "0") || 0;
2121
+ if (padTop > 0) {
2122
+ state.y += padTop * BODY_FONT_SIZE * LINE_HEIGHT_FACTOR;
2123
+ }
2124
+ for (const child of col.content || []) {
2125
+ processBlock(state, child, colRegion);
2126
+ }
2127
+ maxEndY = Math.max(maxEndY, state.y);
2128
+ colX += colWidth + gap;
2129
+ }
2130
+ state.y = maxEndY;
2131
+ state.y += 4;
2132
+ break;
2133
+ }
2134
+ case "column": {
2135
+ for (const child of node.content || []) {
2136
+ processBlock(state, child, region);
2137
+ }
2138
+ break;
2139
+ }
2140
+ case "watermark": {
2141
+ break;
2142
+ }
2143
+ case "subtotalsBlock": {
2144
+ const stFontSize = BODY_FONT_SIZE;
2145
+ const stLineHeight = stFontSize * LINE_HEIGHT_FACTOR;
2146
+ const valueRightX = region.leftX + region.width;
2147
+ const rows = (node.content || []).filter(
2148
+ (c) => c.type === "paragraph" && c.content?.length
2149
+ );
2150
+ const parsed = [];
2151
+ for (const row of rows) {
2152
+ let label = "";
2153
+ let value = "";
2154
+ for (const seg of row.content || []) {
2155
+ if (seg.type === "text") {
2156
+ const isBold = seg.marks?.some((m) => m.type === "bold");
2157
+ if (isBold) {
2158
+ label += seg.text || "";
2159
+ } else {
2160
+ const t = (seg.text || "").trim();
2161
+ if (t) value += (value ? " " : "") + t;
2162
+ }
2163
+ } else if (seg.type === "variableNode") {
2164
+ const varLabel = seg.attrs?.varLabel || seg.attrs?.varName || "";
2165
+ const isBoldVar = seg.marks?.some((m) => m.type === "bold");
2166
+ if (isBoldVar) {
2167
+ label += varLabel;
2168
+ } else {
2169
+ value += (value ? " " : "") + `[${varLabel}]`;
2170
+ }
2171
+ }
2172
+ }
2173
+ parsed.push({ label: label.trim(), value: value.trim() });
2174
+ }
2175
+ let maxLabelWidth = 0;
2176
+ for (const { label } of parsed) {
2177
+ if (label) {
2178
+ const w = state.fonts.bold.widthOfTextAtSize(label, stFontSize);
2179
+ if (w > maxLabelWidth) maxLabelWidth = w;
2180
+ }
2181
+ }
2182
+ const gap = 12;
2183
+ const labelX = region.width > 300 ? Math.max(region.leftX + region.width - maxLabelWidth - gap - 80, region.leftX + region.width * 0.4) : region.leftX + 4;
2184
+ const valueColStartX = labelX + maxLabelWidth + gap;
2185
+ const isLastRowTotal = parsed.length > 1;
2186
+ for (let ri = 0; ri < parsed.length; ri++) {
2187
+ const isTotal = isLastRowTotal && ri === parsed.length - 1;
2188
+ if (isTotal) {
2189
+ state.y += stLineHeight * 0.6;
2190
+ }
2191
+ ensureSpace(state, stLineHeight + (isTotal ? 8 : 4));
2192
+ const { label, value } = parsed[ri];
2193
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - stFontSize;
2194
+ if (isTotal) {
2195
+ state.currentPage.drawLine({
2196
+ start: { x: valueColStartX, y: pdfY + stFontSize + 6 },
2197
+ end: { x: valueRightX, y: pdfY + stFontSize + 6 },
2198
+ thickness: 0.75,
2199
+ color: rgb(0, 0, 0)
2200
+ });
2201
+ state.currentPage.drawLine({
2202
+ start: { x: valueColStartX, y: pdfY + stFontSize + 3 },
2203
+ end: { x: valueRightX, y: pdfY + stFontSize + 3 },
2204
+ thickness: 0.75,
2205
+ color: rgb(0, 0, 0)
2206
+ });
2207
+ }
2208
+ if (label) {
2209
+ state.currentPage.drawText(label, {
2210
+ x: labelX,
2211
+ y: pdfY,
2212
+ size: stFontSize,
2213
+ font: state.fonts.bold,
2214
+ color: rgb(0, 0, 0)
2215
+ });
2216
+ }
2217
+ if (value) {
2218
+ const valueFont = state.fonts.bold;
2219
+ const valueWidth = valueFont.widthOfTextAtSize(value, stFontSize);
2220
+ state.currentPage.drawText(value, {
2221
+ x: valueRightX - valueWidth,
2222
+ y: pdfY,
2223
+ size: stFontSize,
2224
+ font: valueFont,
2225
+ color: rgb(0, 0, 0)
2226
+ });
2227
+ }
2228
+ state.y += stLineHeight;
2229
+ }
2230
+ state.y += 4;
2231
+ break;
2232
+ }
2233
+ case "repeatBlock":
2234
+ if (node.content) {
2235
+ for (const child of node.content) {
2236
+ processBlock(state, child, region);
2237
+ }
2238
+ }
2239
+ break;
1281
2240
  default:
1282
2241
  if (node.content) {
1283
- processInlineContent(state, node.content, 12);
2242
+ processInlineContent(state, node.content, 12, 0, region);
1284
2243
  }
1285
2244
  break;
1286
2245
  }
1287
2246
  }
2247
+ function renderWatermarksOnPage(page, fonts, watermarkNodes) {
2248
+ for (const wmNode of watermarkNodes) {
2249
+ const text = wmNode.attrs?.text || "";
2250
+ if (!text) continue;
2251
+ const opacity = parseFloat(wmNode.attrs?.opacity || "0.15");
2252
+ const angle = parseFloat(wmNode.attrs?.angle || "-45");
2253
+ const fontSize = 100;
2254
+ const { width, height } = page.getSize();
2255
+ const centerX = width / 2;
2256
+ const centerY = height / 2;
2257
+ const textWidth = fonts.bold.widthOfTextAtSize(text, fontSize);
2258
+ page.drawText(text, {
2259
+ x: centerX - textWidth / 2 * Math.cos(angle * Math.PI / 180),
2260
+ y: centerY - textWidth / 2 * Math.sin(angle * Math.PI / 180),
2261
+ size: fontSize,
2262
+ font: fonts.bold,
2263
+ color: rgb(0.5, 0.5, 0.5),
2264
+ opacity,
2265
+ rotate: degrees(angle)
2266
+ });
2267
+ }
2268
+ }
2269
+ function collectWatermarkNodes(content) {
2270
+ const nodes = [];
2271
+ for (const block of content.content || []) {
2272
+ if (block.type === "watermark") {
2273
+ nodes.push(block);
2274
+ }
2275
+ }
2276
+ return nodes;
2277
+ }
1288
2278
  function ensureFieldSuffix(fieldName, suffix) {
1289
2279
  const cleanName = fieldName.replace(/_signature$/i, "").replace(/_initials$/i, "").replace(/_date$/i, "");
1290
2280
  return `${cleanName}${suffix}`;
@@ -1513,6 +2503,7 @@ async function generatePdfFromContent(content, options = {}) {
1513
2503
  italic: await pdfDoc.embedFont(StandardFonts.HelveticaOblique),
1514
2504
  boldItalic: await pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique)
1515
2505
  };
2506
+ const watermarkNodes = collectWatermarkNodes(content);
1516
2507
  const state = {
1517
2508
  currentPage: firstPage,
1518
2509
  pageIndex: 0,
@@ -1520,13 +2511,30 @@ async function generatePdfFromContent(content, options = {}) {
1520
2511
  fonts,
1521
2512
  pdfDoc,
1522
2513
  fieldPositions: /* @__PURE__ */ new Map(),
1523
- drawFieldPlaceholders
2514
+ drawFieldPlaceholders,
2515
+ watermarkNodes
1524
2516
  };
2517
+ renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
1525
2518
  if (content.content) {
1526
2519
  for (const block of content.content) {
1527
2520
  processBlock(state, block);
1528
2521
  }
1529
2522
  }
2523
+ const allPages = pdfDoc.getPages();
2524
+ const totalPages = allPages.length;
2525
+ const pageNumFontSize = 9;
2526
+ for (let i = 0; i < totalPages; i++) {
2527
+ const page = allPages[i];
2528
+ const label = `Page ${i + 1} of ${totalPages}`;
2529
+ const labelWidth = fonts.bold.widthOfTextAtSize(label, pageNumFontSize);
2530
+ page.drawText(label, {
2531
+ x: PAGE_WIDTH - MARGIN - labelWidth,
2532
+ y: MARGIN / 2 - pageNumFontSize / 2,
2533
+ size: pageNumFontSize,
2534
+ font: fonts.bold,
2535
+ color: rgb(0, 0, 0)
2536
+ });
2537
+ }
1530
2538
  let fieldWarnings;
1531
2539
  if (embedFormFields && fields.length > 0) {
1532
2540
  const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
@@ -1540,32 +2548,263 @@ async function generatePdfFromContent(content, options = {}) {
1540
2548
  };
1541
2549
  }
1542
2550
 
1543
- // src/utils/pdf-preview.ts
1544
- async function pdfToImages(pdfBytes, scale = 2) {
1545
- let pdfjsLib;
1546
- try {
1547
- pdfjsLib = await import('pdfjs-dist');
1548
- } catch (err) {
1549
- throw new Error(formatError(
1550
- "Failed to load the PDF preview library \u2014 ensure pdfjs-dist is installed and the worker is accessible",
1551
- err
1552
- ));
1553
- }
1554
- if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
1555
- pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/build/pdf.worker.mjs";
1556
- }
1557
- let pdf;
1558
- try {
1559
- const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
1560
- pdf = await loadingTask.promise;
1561
- } catch (err) {
1562
- throw new Error(formatError("Failed to parse the generated PDF for preview", err));
1563
- }
1564
- const pages = [];
1565
- const pageErrors = [];
1566
- for (let i = 1; i <= pdf.numPages; i++) {
1567
- try {
1568
- const page = await pdf.getPage(i);
2551
+ // src/utils/template-pipeline.ts
2552
+ async function generatePdfFromTiptap(content, values) {
2553
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
2554
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
2555
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
2556
+ const fields = extractFieldsFromContent(replaced);
2557
+ const result = await generatePdfFromContent(replaced, {
2558
+ embedFormFields: fields.length > 0,
2559
+ fields
2560
+ });
2561
+ return { pdfBytes: result.pdfBytes };
2562
+ }
2563
+ async function generatePdfFromMarkdown(markdown, values) {
2564
+ const content = markdownToTiptap(markdown);
2565
+ return generatePdfFromTiptap(content, values);
2566
+ }
2567
+
2568
+ // src/utils/markdown-writer.ts
2569
+ function serializeFieldToken(attrs) {
2570
+ const parts = ["field"];
2571
+ if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
2572
+ if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
2573
+ if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
2574
+ if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
2575
+ if (attrs.required) parts.push(`required:true`);
2576
+ if (attrs.options) parts.push(`options:${attrs.options}`);
2577
+ if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
2578
+ if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
2579
+ if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
2580
+ if (attrs.multiline) parts.push(`multiline:true`);
2581
+ if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
2582
+ if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2583
+ return `{{${parts.join("|")}}}`;
2584
+ }
2585
+ function serializeVariableToken(attrs) {
2586
+ const parts = ["var"];
2587
+ if (attrs.varName) parts.push(`name:${attrs.varName}`);
2588
+ if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
2589
+ if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
2590
+ if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
2591
+ if (attrs.format) parts.push(`format:${attrs.format}`);
2592
+ return `{{${parts.join("|")}}}`;
2593
+ }
2594
+ function serializeInline(content) {
2595
+ if (!content) return "";
2596
+ let result = "";
2597
+ for (const node of content) {
2598
+ if (node.type === "fieldNode") {
2599
+ result += serializeFieldToken(node.attrs || {});
2600
+ continue;
2601
+ }
2602
+ if (node.type === "variableNode") {
2603
+ let token = serializeVariableToken(node.attrs || {});
2604
+ const varMarks = node.marks || [];
2605
+ const isBold = varMarks.some((m) => m.type === "bold");
2606
+ const isItalic = varMarks.some((m) => m.type === "italic");
2607
+ const isUnderline = varMarks.some((m) => m.type === "underline");
2608
+ if (isBold && isItalic) token = `***${token}***`;
2609
+ else if (isBold) token = `**${token}**`;
2610
+ else if (isItalic) token = `*${token}*`;
2611
+ if (isUnderline) token = `__${token}__`;
2612
+ result += token;
2613
+ continue;
2614
+ }
2615
+ if (node.type === "text") {
2616
+ let text = node.text || "";
2617
+ const marks = node.marks || [];
2618
+ for (const mark of marks) {
2619
+ if (mark.type === "bold") text = `**${text}**`;
2620
+ else if (mark.type === "italic") text = `*${text}*`;
2621
+ else if (mark.type === "underline") text = `__${text}__`;
2622
+ else if (mark.type === "code") text = `\`${text}\``;
2623
+ }
2624
+ result += text;
2625
+ }
2626
+ if (node.type === "hardBreak") {
2627
+ result += " \n";
2628
+ }
2629
+ }
2630
+ return result;
2631
+ }
2632
+ function serializeBlock(node) {
2633
+ switch (node.type) {
2634
+ case "heading": {
2635
+ const level = node.attrs?.level || 1;
2636
+ const prefix = "#".repeat(level);
2637
+ return `${prefix} ${serializeInline(node.content)}`;
2638
+ }
2639
+ case "paragraph": {
2640
+ const inline = serializeInline(node.content);
2641
+ return inline;
2642
+ }
2643
+ case "bulletList": {
2644
+ return (node.content || []).map((item) => {
2645
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2646
+ return `- ${inner}`;
2647
+ }).join("\n");
2648
+ }
2649
+ case "orderedList": {
2650
+ return (node.content || []).map((item, i) => {
2651
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2652
+ return `${i + 1}. ${inner}`;
2653
+ }).join("\n");
2654
+ }
2655
+ case "listItem": {
2656
+ return (node.content || []).map(serializeBlock).join("\n");
2657
+ }
2658
+ case "blockquote": {
2659
+ return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
2660
+ }
2661
+ case "codeBlock": {
2662
+ const text = serializeInline(node.content);
2663
+ return `\`\`\`
2664
+ ${text}
2665
+ \`\`\``;
2666
+ }
2667
+ case "horizontalRule": {
2668
+ return "---";
2669
+ }
2670
+ case "table": {
2671
+ const rows = node.content || [];
2672
+ const isBorderless = node.attrs?.borderless === true;
2673
+ const isSubtotals = node.attrs?.subtotals === true;
2674
+ const serializedRows = [];
2675
+ if (isBorderless && rows.length > 0) {
2676
+ const colCount = rows[0].content?.length || 0;
2677
+ const markerCell = isSubtotals ? "_" : "";
2678
+ const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
2679
+ const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
2680
+ serializedRows.push(markerRow);
2681
+ serializedRows.push(sep);
2682
+ }
2683
+ for (let ri = 0; ri < rows.length; ri++) {
2684
+ const row = rows[ri];
2685
+ const cells = (row.content || []).map((cell) => {
2686
+ const inner = (cell.content || []).map(serializeBlock).join("");
2687
+ return inner;
2688
+ });
2689
+ serializedRows.push(`| ${cells.join(" | ")} |`);
2690
+ if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
2691
+ const sep = cells.map(() => "------").join("|");
2692
+ serializedRows.push(`|${sep}|`);
2693
+ }
2694
+ }
2695
+ return serializedRows.join("\n");
2696
+ }
2697
+ case "tableRow": {
2698
+ const cells = (node.content || []).map((cell) => {
2699
+ const inner = (cell.content || []).map(serializeBlock).join("");
2700
+ return inner;
2701
+ });
2702
+ return `| ${cells.join(" | ")} |`;
2703
+ }
2704
+ case "tableHeader":
2705
+ case "tableCell": {
2706
+ return (node.content || []).map(serializeBlock).join("");
2707
+ }
2708
+ case "watermark": {
2709
+ const attrs = node.attrs || {};
2710
+ const parts = [];
2711
+ if (attrs.text) parts.push(`text:${attrs.text}`);
2712
+ if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
2713
+ if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
2714
+ return `:::watermark{${parts.join("|")}}`;
2715
+ }
2716
+ case "panel": {
2717
+ const attrs = node.attrs || {};
2718
+ const parts = [];
2719
+ if (attrs.title) parts.push(`title:${attrs.title}`);
2720
+ if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
2721
+ else if (attrs.border === "solid") parts.push(`border:solid`);
2722
+ if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
2723
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
2724
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2725
+ return `:::panel${attrStr}
2726
+ ${children}
2727
+ :::`;
2728
+ }
2729
+ case "repeatBlock": {
2730
+ const attrs = node.attrs || {};
2731
+ const parts = [];
2732
+ if (attrs.data) parts.push(`data:${attrs.data}`);
2733
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
2734
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2735
+ return `:::repeat${attrStr}
2736
+ ${children}
2737
+ :::`;
2738
+ }
2739
+ case "subtotalsBlock": {
2740
+ const children = (node.content || []).map(serializeBlock).join("\n");
2741
+ return `:::subtotals
2742
+ ${children}
2743
+ :::`;
2744
+ }
2745
+ case "columns": {
2746
+ const attrs = node.attrs || {};
2747
+ const splitVal = attrs.split || "50";
2748
+ const cols = (node.content || []).filter((c) => c.type === "column");
2749
+ const colBlocks = cols.map((col) => {
2750
+ const children = (col.content || []).map(serializeBlock).join("\n\n");
2751
+ const colAttrs = col.attrs || {};
2752
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
2753
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
2754
+ return `${colTag}
2755
+ ${children}
2756
+ :::`;
2757
+ });
2758
+ const padX = parseFloat(attrs.padX || "0") || 0;
2759
+ const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
2760
+ return `:::columns{${colsAttrs}}
2761
+ ${colBlocks.join("\n")}
2762
+ :::`;
2763
+ }
2764
+ case "column": {
2765
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2766
+ const colAttrs = node.attrs || {};
2767
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
2768
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
2769
+ return `${colTag}
2770
+ ${children}
2771
+ :::`;
2772
+ }
2773
+ default:
2774
+ return serializeInline(node.content);
2775
+ }
2776
+ }
2777
+ function tiptapToMarkdown(doc) {
2778
+ if (!doc.content) return "";
2779
+ return doc.content.map(serializeBlock).join("\n\n");
2780
+ }
2781
+
2782
+ // src/utils/pdf-preview.ts
2783
+ async function pdfToImages(pdfBytes, scale = 2) {
2784
+ let pdfjsLib;
2785
+ try {
2786
+ pdfjsLib = await import('pdfjs-dist');
2787
+ } catch (err) {
2788
+ throw new Error(formatError(
2789
+ "Failed to load the PDF preview library \u2014 ensure pdfjs-dist is installed and the worker is accessible",
2790
+ err
2791
+ ));
2792
+ }
2793
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
2794
+ pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/build/pdf.worker.mjs";
2795
+ }
2796
+ let pdf;
2797
+ try {
2798
+ const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
2799
+ pdf = await loadingTask.promise;
2800
+ } catch (err) {
2801
+ throw new Error(formatError("Failed to parse the generated PDF for preview", err));
2802
+ }
2803
+ const pages = [];
2804
+ const pageErrors = [];
2805
+ for (let i = 1; i <= pdf.numPages; i++) {
2806
+ try {
2807
+ const page = await pdf.getPage(i);
1569
2808
  const viewport = page.getViewport({ scale });
1570
2809
  const canvas = document.createElement("canvas");
1571
2810
  canvas.width = viewport.width;
@@ -1626,8 +2865,26 @@ function useDocumentGenerator(options = {}) {
1626
2865
  Placeholder.configure({
1627
2866
  placeholder: options.placeholder || "Start writing your document..."
1628
2867
  }),
2868
+ Table.extend({
2869
+ addAttributes() {
2870
+ return {
2871
+ ...this.parent?.(),
2872
+ borderless: { default: false },
2873
+ subtotals: { default: false }
2874
+ };
2875
+ }
2876
+ }).configure({ resizable: false }),
2877
+ TableRow,
2878
+ TableCell,
2879
+ TableHeader,
1629
2880
  FieldNode,
1630
- VariableNode
2881
+ VariableNode,
2882
+ PanelNode,
2883
+ ColumnsNode,
2884
+ ColumnNode,
2885
+ WatermarkNode,
2886
+ RepeatNode,
2887
+ SubtotalsNode
1631
2888
  ],
1632
2889
  content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
1633
2890
  onUpdate: ({ editor: ed }) => {
@@ -1731,10 +2988,9 @@ function useDocumentGenerator(options = {}) {
1731
2988
  async (values) => {
1732
2989
  if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
1733
2990
  const content = editor.getJSON();
1734
- const replacedContent = replaceVariablesInContent(content, values);
1735
- const result = await generatePdfFromContent(replacedContent);
1736
- const pages = await pdfToImages(result.pdfBytes);
1737
- return { pdfBytes: result.pdfBytes, pdfPages: pages };
2991
+ const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
2992
+ const pdfPages2 = await pdfToImages(pdfBytes2);
2993
+ return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
1738
2994
  },
1739
2995
  [editor]
1740
2996
  );
@@ -2888,15 +4144,47 @@ function FieldEditPopover({
2888
4144
  ] }) })
2889
4145
  ] });
2890
4146
  }
2891
- function labelToVarName(label) {
4147
+ var sizeClasses = {
4148
+ md: "px-3 py-1.5 text-xs font-medium",
4149
+ sm: "px-2 py-0.5 text-[10px] font-medium"
4150
+ };
4151
+ function ToggleGroup({
4152
+ value,
4153
+ onChange,
4154
+ options,
4155
+ size = "md",
4156
+ className
4157
+ }) {
4158
+ return /* @__PURE__ */ jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsx(
4159
+ "button",
4160
+ {
4161
+ className: cn(
4162
+ "rounded-md transition-colors",
4163
+ sizeClasses[size],
4164
+ value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
4165
+ ),
4166
+ onClick: () => onChange(opt.value),
4167
+ children: opt.label
4168
+ },
4169
+ opt.value
4170
+ )) });
4171
+ }
4172
+ function labelToVarName2(label) {
2892
4173
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2893
4174
  }
4175
+ function varNameToLabel(name) {
4176
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4177
+ }
2894
4178
  function VariableInsertPopover({
2895
4179
  open,
2896
4180
  onOpenChange,
2897
4181
  onInsert,
4182
+ predefinedVariables,
2898
4183
  children
2899
4184
  }) {
4185
+ const hasPredefined = !!predefinedVariables?.length;
4186
+ const [mode, setMode] = useState(hasPredefined ? "existing" : "new");
4187
+ const [search, setSearch] = useState("");
2900
4188
  const [varLabel, setVarLabel] = useState("");
2901
4189
  const [varName, setVarName] = useState("");
2902
4190
  const [varDefault, setVarDefault] = useState("");
@@ -2906,7 +4194,9 @@ function VariableInsertPopover({
2906
4194
  setVarName("");
2907
4195
  setVarDefault("");
2908
4196
  setNameManuallyEdited(false);
2909
- }, []);
4197
+ setSearch("");
4198
+ setMode(hasPredefined ? "existing" : "new");
4199
+ }, [hasPredefined]);
2910
4200
  const handleOpenChange = useCallback(
2911
4201
  (nextOpen) => {
2912
4202
  if (!nextOpen) reset();
@@ -2916,7 +4206,7 @@ function VariableInsertPopover({
2916
4206
  );
2917
4207
  useEffect(() => {
2918
4208
  if (!nameManuallyEdited) {
2919
- setVarName(labelToVarName(varLabel));
4209
+ setVarName(labelToVarName2(varLabel));
2920
4210
  }
2921
4211
  }, [varLabel, nameManuallyEdited]);
2922
4212
  const handleInsert = useCallback(() => {
@@ -2928,6 +4218,26 @@ function VariableInsertPopover({
2928
4218
  });
2929
4219
  handleOpenChange(false);
2930
4220
  }, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
4221
+ const handlePickPredefined = useCallback(
4222
+ (pv) => {
4223
+ onInsert({
4224
+ varName: pv.varName,
4225
+ varLabel: pv.varLabel || varNameToLabel(pv.varName),
4226
+ varDefault: ""
4227
+ });
4228
+ handleOpenChange(false);
4229
+ },
4230
+ [onInsert, handleOpenChange]
4231
+ );
4232
+ const filteredVars = useMemo(() => {
4233
+ if (!predefinedVariables?.length) return [];
4234
+ const q = search.toLowerCase();
4235
+ if (!q) return predefinedVariables;
4236
+ return predefinedVariables.filter((pv) => {
4237
+ const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
4238
+ return pv.varName.toLowerCase().includes(q) || label.includes(q);
4239
+ });
4240
+ }, [predefinedVariables, search]);
2931
4241
  return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
2932
4242
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children }),
2933
4243
  /* @__PURE__ */ jsx(PopoverContent, { className: "w-72 p-0", align: "start", sideOffset: 8, children: /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
@@ -2935,72 +4245,114 @@ function VariableInsertPopover({
2935
4245
  /* @__PURE__ */ jsx(Braces, { size: 14 }),
2936
4246
  /* @__PURE__ */ jsx("span", { children: "Insert Variable" })
2937
4247
  ] }),
2938
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2939
- /* @__PURE__ */ jsxs("div", { children: [
2940
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4248
+ hasPredefined && /* @__PURE__ */ jsx(
4249
+ ToggleGroup,
4250
+ {
4251
+ value: mode,
4252
+ onChange: setMode,
4253
+ options: [
4254
+ { value: "existing", label: "Existing" },
4255
+ { value: "new", label: "New" }
4256
+ ],
4257
+ size: "sm"
4258
+ }
4259
+ ),
4260
+ mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4261
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
4262
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
2941
4263
  /* @__PURE__ */ jsx(
2942
4264
  Input,
2943
4265
  {
2944
- id: "var-label",
2945
- value: varLabel,
2946
- onChange: (e) => setVarLabel(e.target.value),
2947
- placeholder: "e.g. Company Name",
2948
- className: "h-8 text-xs"
4266
+ value: search,
4267
+ onChange: (e) => setSearch(e.target.value),
4268
+ placeholder: "Search variables...",
4269
+ className: "h-8 text-xs pl-7"
2949
4270
  }
2950
4271
  )
2951
4272
  ] }),
2952
- /* @__PURE__ */ jsxs("div", { children: [
2953
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4273
+ /* @__PURE__ */ jsx("div", { className: "max-h-48 overflow-y-auto -mx-1", children: filteredVars.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground px-2 py-3 text-center", children: "No variables found" }) : filteredVars.map((pv) => {
4274
+ const label = pv.varLabel || varNameToLabel(pv.varName);
4275
+ return /* @__PURE__ */ jsxs(
4276
+ "button",
4277
+ {
4278
+ 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",
4279
+ onClick: () => handlePickPredefined(pv),
4280
+ children: [
4281
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: label }),
4282
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
4283
+ ]
4284
+ },
4285
+ pv.varName
4286
+ );
4287
+ }) })
4288
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4289
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4290
+ /* @__PURE__ */ jsxs("div", { children: [
4291
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4292
+ /* @__PURE__ */ jsx(
4293
+ Input,
4294
+ {
4295
+ id: "var-label",
4296
+ value: varLabel,
4297
+ onChange: (e) => setVarLabel(e.target.value),
4298
+ placeholder: "e.g. Company Name",
4299
+ className: "h-8 text-xs"
4300
+ }
4301
+ )
4302
+ ] }),
4303
+ /* @__PURE__ */ jsxs("div", { children: [
4304
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4305
+ /* @__PURE__ */ jsx(
4306
+ Input,
4307
+ {
4308
+ id: "var-name",
4309
+ value: varName,
4310
+ onChange: (e) => {
4311
+ setVarName(e.target.value);
4312
+ setNameManuallyEdited(true);
4313
+ },
4314
+ placeholder: "e.g. company_name",
4315
+ className: "h-8 text-xs font-mono"
4316
+ }
4317
+ ),
4318
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
4319
+ ] }),
4320
+ /* @__PURE__ */ jsxs("div", { children: [
4321
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
4322
+ /* @__PURE__ */ jsx(
4323
+ Input,
4324
+ {
4325
+ id: "var-default",
4326
+ value: varDefault,
4327
+ onChange: (e) => setVarDefault(e.target.value),
4328
+ placeholder: "Optional",
4329
+ className: "h-8 text-xs"
4330
+ }
4331
+ )
4332
+ ] })
4333
+ ] }),
4334
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
2954
4335
  /* @__PURE__ */ jsx(
2955
- Input,
4336
+ Button,
2956
4337
  {
2957
- id: "var-name",
2958
- value: varName,
2959
- onChange: (e) => {
2960
- setVarName(e.target.value);
2961
- setNameManuallyEdited(true);
2962
- },
2963
- placeholder: "e.g. company_name",
2964
- className: "h-8 text-xs font-mono"
4338
+ size: "sm",
4339
+ className: "h-8 flex-1 text-xs",
4340
+ onClick: handleInsert,
4341
+ disabled: !varLabel.trim() || !varName.trim(),
4342
+ children: "Insert"
2965
4343
  }
2966
4344
  ),
2967
- /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
2968
- ] }),
2969
- /* @__PURE__ */ jsxs("div", { children: [
2970
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
2971
4345
  /* @__PURE__ */ jsx(
2972
- Input,
4346
+ Button,
2973
4347
  {
2974
- id: "var-default",
2975
- value: varDefault,
2976
- onChange: (e) => setVarDefault(e.target.value),
2977
- placeholder: "Optional",
2978
- className: "h-8 text-xs"
4348
+ variant: "outline",
4349
+ size: "sm",
4350
+ className: "h-8 text-xs",
4351
+ onClick: () => handleOpenChange(false),
4352
+ children: "Cancel"
2979
4353
  }
2980
4354
  )
2981
4355
  ] })
2982
- ] }),
2983
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
2984
- /* @__PURE__ */ jsx(
2985
- Button,
2986
- {
2987
- size: "sm",
2988
- className: "h-8 flex-1 text-xs",
2989
- onClick: handleInsert,
2990
- disabled: !varLabel.trim() || !varName.trim(),
2991
- children: "Insert"
2992
- }
2993
- ),
2994
- /* @__PURE__ */ jsx(
2995
- Button,
2996
- {
2997
- variant: "outline",
2998
- size: "sm",
2999
- className: "h-8 text-xs",
3000
- onClick: () => handleOpenChange(false),
3001
- children: "Cancel"
3002
- }
3003
- )
3004
4356
  ] })
3005
4357
  ] }) })
3006
4358
  ] });
@@ -3222,7 +4574,6 @@ function PreviewPanel({
3222
4574
  (f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
3223
4575
  );
3224
4576
  }, [positionedFields, currentPage, currentPageIndex]);
3225
- const showScrollbars = zoomLevel > 1;
3226
4577
  return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col h-full border border-border rounded-lg overflow-hidden bg-muted/20", className), children: [
3227
4578
  /* @__PURE__ */ jsx("div", { className: "border-b border-border px-2 py-1.5 flex-shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
3228
4579
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -3338,12 +4689,13 @@ function PreviewPanel({
3338
4689
  "div",
3339
4690
  {
3340
4691
  className: cn(
3341
- "border border-border rounded-lg bg-muted/30 shrink-0",
3342
- showScrollbars ? "overflow-auto" : "overflow-hidden"
4692
+ "border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
3343
4693
  ),
3344
4694
  style: {
3345
4695
  width: pageDisplaySize.viewportWidth,
3346
- height: pageDisplaySize.viewportHeight
4696
+ height: pageDisplaySize.viewportHeight,
4697
+ maxWidth: "100%",
4698
+ maxHeight: "100%"
3347
4699
  },
3348
4700
  children: /* @__PURE__ */ jsxs(
3349
4701
  "div",
@@ -3435,6 +4787,37 @@ function validateMarkdown(markdown) {
3435
4787
  searchFrom = closeIdx + 2;
3436
4788
  }
3437
4789
  }
4790
+ const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
4791
+ const DIRECTIVE_CLOSE_RE = /^:::$/;
4792
+ const directiveStack = [];
4793
+ for (let i = 0; i < lines.length; i++) {
4794
+ const trimmed = lines[i].trim();
4795
+ const lineNum = i + 1;
4796
+ const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
4797
+ if (openMatch) {
4798
+ const dtype = openMatch[1];
4799
+ if (dtype !== "watermark") {
4800
+ if (dtype === "col") {
4801
+ const parent = directiveStack[directiveStack.length - 1];
4802
+ if (!parent || parent.type !== "columns") {
4803
+ warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
4804
+ }
4805
+ }
4806
+ directiveStack.push({ type: dtype, line: lineNum });
4807
+ }
4808
+ continue;
4809
+ }
4810
+ if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
4811
+ if (directiveStack.length === 0) {
4812
+ warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
4813
+ } else {
4814
+ directiveStack.pop();
4815
+ }
4816
+ }
4817
+ }
4818
+ for (const open of directiveStack) {
4819
+ errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
4820
+ }
3438
4821
  let fields = [];
3439
4822
  let variables = [];
3440
4823
  try {
@@ -3468,31 +4851,6 @@ function validateMarkdown(markdown) {
3468
4851
  fields
3469
4852
  };
3470
4853
  }
3471
- var sizeClasses = {
3472
- md: "px-3 py-1.5 text-xs font-medium",
3473
- sm: "px-2 py-0.5 text-[10px] font-medium"
3474
- };
3475
- function ToggleGroup({
3476
- value,
3477
- onChange,
3478
- options,
3479
- size = "md",
3480
- className
3481
- }) {
3482
- return /* @__PURE__ */ jsx("div", { className: cn("inline-flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg", className), children: options.map((opt) => /* @__PURE__ */ jsx(
3483
- "button",
3484
- {
3485
- className: cn(
3486
- "rounded-md transition-colors",
3487
- sizeClasses[size],
3488
- value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
3489
- ),
3490
- onClick: () => onChange(opt.value),
3491
- children: opt.label
3492
- },
3493
- opt.value
3494
- )) });
3495
- }
3496
4854
  function parseCsv(text) {
3497
4855
  const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
3498
4856
  if (lines.length < 2) return [];
@@ -3542,7 +4900,10 @@ function GeneratePanel({
3542
4900
  editorContent,
3543
4901
  editorVariables,
3544
4902
  onGeneratePdf,
3545
- initialBulkData
4903
+ initialBulkData,
4904
+ initialVariableValues,
4905
+ exportFileName,
4906
+ templateMode
3546
4907
  }) {
3547
4908
  const [templateSource, setTemplateSource] = useState("editor");
3548
4909
  const [importedMarkdown, setImportedMarkdown] = useState(null);
@@ -3550,6 +4911,7 @@ function GeneratePanel({
3550
4911
  const [validationResult, setValidationResult] = useState(null);
3551
4912
  const [mode, setMode] = useState("single");
3552
4913
  const [variableValues, setVariableValues] = useState({});
4914
+ const [variableSearch, setVariableSearch] = useState("");
3553
4915
  const [bulkInputFormat, setBulkInputFormat] = useState("json");
3554
4916
  const [bulkInput, setBulkInput] = useState("");
3555
4917
  const [bulkData, setBulkData] = useState(null);
@@ -3576,11 +4938,22 @@ function GeneratePanel({
3576
4938
  const col = pos - textBefore.lastIndexOf("\n");
3577
4939
  setJsonCursor({ line, col });
3578
4940
  }, []);
4941
+ const lockedVarNames = React12__default.useMemo(
4942
+ () => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
4943
+ [templateMode, initialVariableValues]
4944
+ );
3579
4945
  const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
3580
4946
  const hasVariables = activeVariables.length > 0;
3581
4947
  const hasEditorContent = editorMarkdown.trim().length > 0;
3582
4948
  const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
3583
4949
  const hasContent = hasImportedContent || hasEditorContent;
4950
+ const filteredVariables = React12__default.useMemo(() => {
4951
+ if (!variableSearch.trim()) return activeVariables;
4952
+ const q = variableSearch.toLowerCase();
4953
+ return activeVariables.filter(
4954
+ (v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
4955
+ );
4956
+ }, [activeVariables, variableSearch]);
3584
4957
  const getActiveContent = useCallback(() => {
3585
4958
  if (templateSource === "imported" && importedMarkdown) {
3586
4959
  return markdownToTiptap(importedMarkdown);
@@ -3748,7 +5121,9 @@ function GeneratePanel({
3748
5121
  setPreviewError(null);
3749
5122
  try {
3750
5123
  const content = getActiveContent();
3751
- const replaced = replaceVariablesInContent(content, variableValues);
5124
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5125
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5126
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3752
5127
  const fields = extractFieldsFromContent(replaced);
3753
5128
  const result = await generatePdfFromContent(replaced);
3754
5129
  const pages = await pdfToImages(result.pdfBytes);
@@ -3769,7 +5144,9 @@ function GeneratePanel({
3769
5144
  setExportSuccess(null);
3770
5145
  try {
3771
5146
  const content = getActiveContent();
3772
- const replaced = replaceVariablesInContent(content, variableValues);
5147
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5148
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5149
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3773
5150
  const fields = extractFieldsFromContent(replaced);
3774
5151
  const result = await generatePdfFromContent(replaced, {
3775
5152
  drawFieldPlaceholders: false,
@@ -3777,7 +5154,7 @@ function GeneratePanel({
3777
5154
  fields
3778
5155
  });
3779
5156
  const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
3780
- const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
5157
+ const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
3781
5158
  if (onGeneratePdf) {
3782
5159
  onGeneratePdf(blob, fileName);
3783
5160
  } else {
@@ -3849,7 +5226,9 @@ function GeneratePanel({
3849
5226
  const allWarnings = [];
3850
5227
  for (let i = 0; i < bulkData.length; i++) {
3851
5228
  const values = bulkData[i];
3852
- const replaced = replaceVariablesInContent(content, values);
5229
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5230
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5231
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3853
5232
  const fields = extractFieldsFromContent(replaced);
3854
5233
  const result = await generatePdfFromContent(replaced, {
3855
5234
  drawFieldPlaceholders: false,
@@ -3904,13 +5283,15 @@ ${allWarnings.join("\n")}`);
3904
5283
  }, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
3905
5284
  React12__default.useEffect(() => {
3906
5285
  if (templateSource === "editor") {
3907
- const defaults = {};
5286
+ const defaults = initialVariableValues ? { ...initialVariableValues } : {};
3908
5287
  for (const v of editorVariables) {
3909
- defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5288
+ if (!defaults[v.varName]) {
5289
+ defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5290
+ }
3910
5291
  }
3911
5292
  setVariableValues(defaults);
3912
5293
  }
3913
- }, [editorVariables, templateSource]);
5294
+ }, [editorVariables, templateSource, initialVariableValues]);
3914
5295
  useEffect(() => {
3915
5296
  setPreviewFresh(false);
3916
5297
  }, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
@@ -3922,8 +5303,8 @@ ${allWarnings.join("\n")}`);
3922
5303
  }, [exportSuccess]);
3923
5304
  return /* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
3924
5305
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
3925
- /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
3926
- /* @__PURE__ */ jsxs("div", { children: [
5306
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
5307
+ !templateMode && /* @__PURE__ */ jsxs("div", { children: [
3927
5308
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
3928
5309
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Template" }),
3929
5310
  templateSource === "imported" && /* @__PURE__ */ jsxs(
@@ -4016,10 +5397,10 @@ ${allWarnings.join("\n")}`);
4016
5397
  !hasContent && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
4017
5398
  /* @__PURE__ */ jsx(Braces, { size: 32, className: "mb-2 opacity-50" }),
4018
5399
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "No template content" }),
4019
- /* @__PURE__ */ jsx("p", { className: "text-xs mt-1", children: "Import a .md template above or add content in the Editor tab." })
5400
+ /* @__PURE__ */ 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." })
4020
5401
  ] }),
4021
5402
  hasVariables && hasContent && /* @__PURE__ */ jsxs("div", { className: "border border-border rounded-lg overflow-hidden", children: [
4022
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsx(
5403
+ !templateMode && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/30 border-b border-border", children: /* @__PURE__ */ jsx(
4023
5404
  ToggleGroup,
4024
5405
  {
4025
5406
  value: mode,
@@ -4031,20 +5412,53 @@ ${allWarnings.join("\n")}`);
4031
5412
  }
4032
5413
  ) }),
4033
5414
  mode === "single" && /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
4034
- /* @__PURE__ */ jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
4035
- activeVariables.map((v) => /* @__PURE__ */ jsxs("div", { children: [
4036
- /* @__PURE__ */ jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5415
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
5416
+ /* @__PURE__ */ jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
5417
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
5418
+ ] }),
5419
+ activeVariables.length > 5 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
5420
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
4037
5421
  /* @__PURE__ */ jsx(
4038
5422
  Input,
4039
5423
  {
4040
- id: `gen-${v.varName}`,
4041
- value: variableValues[v.varName] || "",
4042
- onChange: (e) => updateVariableValue(v.varName, e.target.value),
4043
- placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
4044
- className: "h-8 text-xs"
5424
+ value: variableSearch,
5425
+ onChange: (e) => setVariableSearch(e.target.value),
5426
+ placeholder: "Search variables...",
5427
+ className: "h-8 text-xs pl-8"
5428
+ }
5429
+ ),
5430
+ variableSearch && /* @__PURE__ */ jsx(
5431
+ "button",
5432
+ {
5433
+ type: "button",
5434
+ onClick: () => setVariableSearch(""),
5435
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
5436
+ children: /* @__PURE__ */ jsx(X, { size: 12 })
4045
5437
  }
4046
5438
  )
4047
- ] }, v.varName))
5439
+ ] }),
5440
+ filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
5441
+ "No variables match \u201C",
5442
+ variableSearch,
5443
+ "\u201D"
5444
+ ] }),
5445
+ filteredVariables.map((v) => {
5446
+ const isLocked = lockedVarNames.has(v.varName);
5447
+ return /* @__PURE__ */ jsxs("div", { children: [
5448
+ /* @__PURE__ */ jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5449
+ /* @__PURE__ */ jsx(
5450
+ Input,
5451
+ {
5452
+ id: `gen-${v.varName}`,
5453
+ value: variableValues[v.varName] || "",
5454
+ onChange: (e) => updateVariableValue(v.varName, e.target.value),
5455
+ placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
5456
+ disabled: isLocked,
5457
+ className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
5458
+ }
5459
+ )
5460
+ ] }, v.varName);
5461
+ })
4048
5462
  ] }),
4049
5463
  mode === "bulk" && /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
4050
5464
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
@@ -4233,56 +5647,60 @@ ${allWarnings.join("\n")}`);
4233
5647
  ] }),
4234
5648
  !hasVariables && hasContent && /* @__PURE__ */ jsx("div", { className: "p-3 rounded-md bg-muted/30 border border-border", children: /* @__PURE__ */ 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.' }) })
4235
5649
  ] }),
4236
- previewError && /* @__PURE__ */ jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
4237
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4238
- /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: previewError })
4239
- ] }) }) }),
4240
- exportError && /* @__PURE__ */ jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-2 rounded-md bg-destructive/10 border border-destructive/20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
4241
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "text-destructive shrink-0 mt-0.5" }),
4242
- /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: exportError })
4243
- ] }) }) }),
4244
- exportSuccess && /* @__PURE__ */ jsx("div", { className: "px-4 pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-2 rounded-md bg-emerald-500/10 border border-emerald-500/20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
4245
- /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "text-emerald-600 shrink-0" }),
4246
- /* @__PURE__ */ jsx("p", { className: "text-xs text-emerald-700", children: exportSuccess })
4247
- ] }) }) }),
4248
- hasContent && /* @__PURE__ */ 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__ */ jsxs(Fragment, { children: [
4249
- /* @__PURE__ */ jsxs(
4250
- Button,
4251
- {
4252
- variant: "secondary",
4253
- onClick: handlePreview,
4254
- disabled: isGenerating,
4255
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4256
- children: [
4257
- isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
4258
- isGenerating ? "Generating..." : "Generate PDF"
4259
- ]
4260
- }
4261
- ),
4262
- /* @__PURE__ */ jsxs(
5650
+ hasContent && /* @__PURE__ */ 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: [
5651
+ mode === "single" || !hasVariables ? /* @__PURE__ */ jsxs(Fragment, { children: [
5652
+ /* @__PURE__ */ jsxs(
5653
+ Button,
5654
+ {
5655
+ variant: "secondary",
5656
+ onClick: handlePreview,
5657
+ disabled: isGenerating,
5658
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
5659
+ children: [
5660
+ isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
5661
+ isGenerating ? "Generating..." : "Generate PDF"
5662
+ ]
5663
+ }
5664
+ ),
5665
+ /* @__PURE__ */ jsxs(
5666
+ Button,
5667
+ {
5668
+ onClick: handleExportSingle,
5669
+ disabled: !previewFresh || isExporting,
5670
+ 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",
5671
+ children: [
5672
+ isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
5673
+ isExporting ? "Exporting..." : "Export PDF"
5674
+ ]
5675
+ }
5676
+ )
5677
+ ] }) : /* @__PURE__ */ jsxs(
4263
5678
  Button,
4264
5679
  {
4265
- onClick: handleExportSingle,
4266
- disabled: !previewFresh || isExporting,
4267
- 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",
5680
+ onClick: handleBulkGenerate,
5681
+ disabled: isExporting || !bulkData || bulkData.length === 0,
5682
+ 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",
4268
5683
  children: [
4269
5684
  isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
4270
- isExporting ? "Exporting..." : "Export PDF"
5685
+ isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4271
5686
  ]
4272
5687
  }
4273
- )
4274
- ] }) : /* @__PURE__ */ jsxs(
4275
- Button,
4276
- {
4277
- onClick: handleBulkGenerate,
4278
- disabled: isExporting || !bulkData || bulkData.length === 0,
4279
- 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",
4280
- children: [
4281
- isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
4282
- isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4283
- ]
4284
- }
4285
- ) })
5688
+ ),
5689
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
5690
+ previewError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
5691
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
5692
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: previewError })
5693
+ ] }),
5694
+ exportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
5695
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
5696
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exportError })
5697
+ ] }),
5698
+ exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
5699
+ /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
5700
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exportSuccess })
5701
+ ] })
5702
+ ] })
5703
+ ] })
4286
5704
  ] }),
4287
5705
  /* @__PURE__ */ jsx("div", { className: "bg-border" }),
4288
5706
  /* @__PURE__ */ jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsx(
@@ -4460,7 +5878,13 @@ function DocumentGeneratorInner({
4460
5878
  showToolbar = true,
4461
5879
  showGenerateTab = true,
4462
5880
  onGeneratePdf,
4463
- initialBulkData
5881
+ initialBulkData,
5882
+ defaultTab = "editor",
5883
+ initialVariableValues,
5884
+ predefinedVariables,
5885
+ exportFileName,
5886
+ templateMode,
5887
+ onSaveTemplate
4464
5888
  }) {
4465
5889
  const {
4466
5890
  editor,
@@ -4487,7 +5911,7 @@ function DocumentGeneratorInner({
4487
5911
  placeholder,
4488
5912
  onChange
4489
5913
  });
4490
- const [activeTab, setActiveTab] = useState("editor");
5914
+ const [activeTab, setActiveTab] = useState(defaultTab);
4491
5915
  const [insertPopoverOpen, setInsertPopoverOpen] = useState(false);
4492
5916
  const [insertVarPopoverOpen, setInsertVarPopoverOpen] = useState(false);
4493
5917
  const [editFieldId, setEditFieldId] = useState(null);
@@ -4570,6 +5994,21 @@ function DocumentGeneratorInner({
4570
5994
  setExportSuccess("Document exported successfully");
4571
5995
  }
4572
5996
  }, [exportDocument, onExport]);
5997
+ const handleSaveTemplate = useCallback(() => {
5998
+ const md = exportMarkdown();
5999
+ if (!md.trim()) {
6000
+ setMdExportError("Editor content is empty");
6001
+ return;
6002
+ }
6003
+ const validation = validateMarkdown(md);
6004
+ if (!validation.valid) {
6005
+ setMdExportError(validation.errors.join("; "));
6006
+ return;
6007
+ }
6008
+ setMdExportError(null);
6009
+ onSaveTemplate?.(md);
6010
+ setExportSuccess("Template saved");
6011
+ }, [exportMarkdown, onSaveTemplate]);
4573
6012
  const handleExportMarkdown = useCallback(() => {
4574
6013
  const md = exportMarkdown();
4575
6014
  if (!md.trim()) {
@@ -4586,10 +6025,10 @@ function DocumentGeneratorInner({
4586
6025
  const url = URL.createObjectURL(blob);
4587
6026
  const a = document.createElement("a");
4588
6027
  a.href = url;
4589
- a.download = "template.md";
6028
+ a.download = exportFileName ? `${exportFileName}.md` : "template.md";
4590
6029
  a.click();
4591
6030
  URL.revokeObjectURL(url);
4592
- }, [exportMarkdown]);
6031
+ }, [exportMarkdown, exportFileName]);
4593
6032
  useEffect(() => {
4594
6033
  if (mdExportError) setMdExportError(null);
4595
6034
  }, [markdown]);
@@ -4625,6 +6064,7 @@ function DocumentGeneratorInner({
4625
6064
  open: insertVarPopoverOpen,
4626
6065
  onOpenChange: setInsertVarPopoverOpen,
4627
6066
  onInsert: handleInsertVariable,
6067
+ predefinedVariables,
4628
6068
  children: /* @__PURE__ */ jsxs(
4629
6069
  Button,
4630
6070
  {
@@ -4665,118 +6105,141 @@ function DocumentGeneratorInner({
4665
6105
  ] })
4666
6106
  ] })
4667
6107
  ] }),
4668
- /* @__PURE__ */ jsxs("div", { className: cn(
6108
+ /* @__PURE__ */ jsx("div", { className: cn(
4669
6109
  "flex flex-col flex-1 min-h-0",
4670
6110
  activeTab !== "editor" && "hidden"
6111
+ ), children: /* @__PURE__ */ jsxs("div", { className: cn(
6112
+ "flex-1 min-h-0",
6113
+ showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4671
6114
  ), children: [
4672
- /* @__PURE__ */ jsxs("div", { className: cn(
4673
- "flex-1 min-h-0",
4674
- showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4675
- ), children: [
4676
- !editorCollapsed && /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
4677
- showToolbar && !readOnly && /* @__PURE__ */ jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsx(
4678
- EditorToolbar,
4679
- {
4680
- editor,
4681
- insertFieldButton,
4682
- insertVariableButton,
4683
- onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4684
- }
4685
- ) }),
4686
- /* @__PURE__ */ jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0", children: /* @__PURE__ */ jsx(
4687
- EditorContent,
4688
- {
4689
- editor,
4690
- className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4691
- }
4692
- ) })
4693
- ] }),
4694
- showPreview && !editorCollapsed && /* @__PURE__ */ jsx("div", { className: "bg-border" }),
4695
- showPreview && /* @__PURE__ */ jsx(
4696
- PreviewPanel,
6115
+ !editorCollapsed && /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
6116
+ showToolbar && !readOnly && /* @__PURE__ */ jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsx(
6117
+ EditorToolbar,
4697
6118
  {
4698
- pages: pdfPages,
4699
- isGenerating,
4700
- positionedFields,
4701
- headerLeft: editorCollapsed ? /* @__PURE__ */ jsx(
4702
- Button,
4703
- {
4704
- variant: "ghost",
4705
- size: "sm",
4706
- className: "h-8 w-8 p-0",
4707
- onClick: () => setEditorCollapsed(false),
4708
- children: /* @__PURE__ */ jsx(PanelLeftOpen, { size: 14 })
4709
- }
4710
- ) : void 0,
4711
- className: "flex-1 border-0 rounded-none"
6119
+ editor,
6120
+ insertFieldButton,
6121
+ insertVariableButton,
6122
+ onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4712
6123
  }
4713
- )
4714
- ] }),
4715
- /* @__PURE__ */ 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: [
4716
- /* @__PURE__ */ jsxs(
4717
- Button,
6124
+ ) }),
6125
+ /* @__PURE__ */ jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsx(
6126
+ EditorContent,
4718
6127
  {
4719
- variant: "secondary",
4720
- onClick: generatePdf,
4721
- disabled: isGenerating || !editor,
4722
- className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4723
- children: [
4724
- isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
4725
- isGenerating ? "Generating..." : "Generate PDF"
4726
- ]
6128
+ editor,
6129
+ className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4727
6130
  }
4728
- ),
4729
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
4730
- mdExportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4731
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
4732
- /* @__PURE__ */ jsx("span", { className: "truncate", children: mdExportError })
4733
- ] }),
4734
- generationError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
4735
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
4736
- /* @__PURE__ */ jsx("span", { className: "truncate", children: generationError })
4737
- ] }),
4738
- fieldWarnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
4739
- /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
4740
- /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
4741
- fieldWarnings.length,
4742
- " field",
4743
- fieldWarnings.length !== 1 ? "s" : "",
4744
- " had warnings during generation"
4745
- ] })
4746
- ] }),
4747
- exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
4748
- /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
4749
- /* @__PURE__ */ jsx("span", { children: exportSuccess })
4750
- ] })
4751
- ] }),
4752
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6131
+ ) }),
6132
+ /* @__PURE__ */ 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: [
4753
6133
  /* @__PURE__ */ jsxs(
4754
6134
  Button,
4755
6135
  {
4756
- onClick: handleExportMarkdown,
4757
- disabled: !editor || !markdown.trim(),
4758
- 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",
6136
+ variant: "secondary",
6137
+ onClick: generatePdf,
6138
+ disabled: isGenerating || !editor,
6139
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4759
6140
  children: [
4760
- /* @__PURE__ */ jsx(FileText, { size: 16 }),
4761
- "Export MD"
6141
+ isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
6142
+ isGenerating ? "Generating..." : "Generate PDF"
4762
6143
  ]
4763
6144
  }
4764
6145
  ),
4765
- /* @__PURE__ */ jsxs(
6146
+ onSaveTemplate ? /* @__PURE__ */ jsxs(
4766
6147
  Button,
4767
6148
  {
4768
- onClick: handleExport,
4769
- disabled: isGenerating || !editor || pdfPages.length === 0,
6149
+ onClick: handleSaveTemplate,
6150
+ disabled: !editor || !markdown.trim(),
4770
6151
  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",
4771
6152
  children: [
4772
- /* @__PURE__ */ jsx(FileDown, { size: 16 }),
4773
- exportButtonText
6153
+ /* @__PURE__ */ jsx(Save, { size: 16 }),
6154
+ exportButtonText || "Save Template"
4774
6155
  ]
4775
6156
  }
4776
- )
6157
+ ) : /* @__PURE__ */ jsxs(Popover, { children: [
6158
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
6159
+ Button,
6160
+ {
6161
+ disabled: !editor || !markdown.trim() && pdfPages.length === 0,
6162
+ 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",
6163
+ children: [
6164
+ /* @__PURE__ */ jsx(FileDown, { size: 16 }),
6165
+ "Export",
6166
+ /* @__PURE__ */ jsx(ChevronDown, { size: 14 })
6167
+ ]
6168
+ }
6169
+ ) }),
6170
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
6171
+ /* @__PURE__ */ jsxs(
6172
+ "button",
6173
+ {
6174
+ onClick: handleExportMarkdown,
6175
+ disabled: !editor || !markdown.trim(),
6176
+ 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",
6177
+ children: [
6178
+ /* @__PURE__ */ jsx(FileText, { size: 16 }),
6179
+ "Export as MD"
6180
+ ]
6181
+ }
6182
+ ),
6183
+ /* @__PURE__ */ jsxs(
6184
+ "button",
6185
+ {
6186
+ onClick: handleExport,
6187
+ disabled: isGenerating || !editor || pdfPages.length === 0,
6188
+ 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",
6189
+ children: [
6190
+ /* @__PURE__ */ jsx(FileDown, { size: 16 }),
6191
+ "Export as PDF"
6192
+ ]
6193
+ }
6194
+ )
6195
+ ] })
6196
+ ] }),
6197
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6198
+ mdExportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6199
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6200
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: mdExportError })
6201
+ ] }),
6202
+ generationError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6203
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6204
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: generationError })
6205
+ ] }),
6206
+ fieldWarnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
6207
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6208
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
6209
+ fieldWarnings.length,
6210
+ " field",
6211
+ fieldWarnings.length !== 1 ? "s" : "",
6212
+ " had warnings during generation"
6213
+ ] })
6214
+ ] }),
6215
+ exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6216
+ /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
6217
+ /* @__PURE__ */ jsx("span", { children: exportSuccess })
6218
+ ] })
6219
+ ] })
4777
6220
  ] })
4778
- ] })
4779
- ] }),
6221
+ ] }),
6222
+ showPreview && !editorCollapsed && /* @__PURE__ */ jsx("div", { className: "bg-border" }),
6223
+ showPreview && /* @__PURE__ */ jsx(
6224
+ PreviewPanel,
6225
+ {
6226
+ pages: pdfPages,
6227
+ isGenerating,
6228
+ positionedFields,
6229
+ headerLeft: editorCollapsed ? /* @__PURE__ */ jsx(
6230
+ Button,
6231
+ {
6232
+ variant: "ghost",
6233
+ size: "sm",
6234
+ className: "h-8 w-8 p-0",
6235
+ onClick: () => setEditorCollapsed(false),
6236
+ children: /* @__PURE__ */ jsx(PanelLeftOpen, { size: 14 })
6237
+ }
6238
+ ) : void 0,
6239
+ className: "flex-1 border-0 rounded-none"
6240
+ }
6241
+ )
6242
+ ] }) }),
4780
6243
  showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsx(
4781
6244
  GeneratePanel,
4782
6245
  {
@@ -4784,7 +6247,10 @@ function DocumentGeneratorInner({
4784
6247
  editorContent: editor?.getJSON(),
4785
6248
  editorVariables: variables,
4786
6249
  onGeneratePdf,
4787
- initialBulkData
6250
+ initialBulkData,
6251
+ initialVariableValues,
6252
+ exportFileName,
6253
+ templateMode
4788
6254
  }
4789
6255
  ),
4790
6256
  /* @__PURE__ */ jsx(
@@ -4925,6 +6391,717 @@ function EditorPanel({
4925
6391
  ] });
4926
6392
  }
4927
6393
 
4928
- export { DocumentGenerator, EditorPanel, FormFieldType, PreviewPanel, ThemeProvider, useDocumentGenerator, useTheme };
6394
+ // src/utils/xml-template-parser.ts
6395
+ function extractFieldRefs(expr) {
6396
+ const refs = [];
6397
+ const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
6398
+ let m;
6399
+ while ((m = re.exec(expr)) !== null) {
6400
+ if (m[2]) {
6401
+ refs.push(`${m[1]}.${m[2]}`);
6402
+ } else {
6403
+ refs.push(m[1]);
6404
+ }
6405
+ }
6406
+ return refs;
6407
+ }
6408
+ function fieldRefToVarName(ref, bandContext) {
6409
+ if (ref.includes(".")) {
6410
+ return labelToVarName(ref.replace(/\./g, "_"));
6411
+ }
6412
+ if (bandContext) {
6413
+ return labelToVarName(bandContext + "_" + ref);
6414
+ }
6415
+ return labelToVarName(ref);
6416
+ }
6417
+ function fieldRefToLabel(ref) {
6418
+ const parts = ref.split(".");
6419
+ const raw = parts[parts.length - 1];
6420
+ return raw.replace(/_/g, " ");
6421
+ }
6422
+ function resolveExpression(expr, registry, siblingLabel, bandContext) {
6423
+ const trimmed = expr.trim();
6424
+ const suppressZero = /<>\s*0/.test(trimmed);
6425
+ const szOpts = suppressZero ? { suppressZero: true } : void 0;
6426
+ const aggMatch = trimmed.match(
6427
+ /^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
6428
+ );
6429
+ if (aggMatch) {
6430
+ const fieldName = aggMatch[2];
6431
+ const varName = labelToVarName("workorder_" + fieldName);
6432
+ const label = fieldName.replace(/_/g, " ");
6433
+ return registry.register(varName, label, szOpts);
6434
+ }
6435
+ const concatSingle = trimmed.match(
6436
+ /^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6437
+ );
6438
+ if (concatSingle) {
6439
+ const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
6440
+ const varName = fieldRefToVarName(ref, bandContext);
6441
+ const label = fieldRefToLabel(ref);
6442
+ return registry.register(varName, label, szOpts);
6443
+ }
6444
+ const concatMulti = trimmed.match(
6445
+ /^concat\(/i
6446
+ );
6447
+ if (concatMulti) {
6448
+ const refs = extractFieldRefs(trimmed);
6449
+ if (refs.length > 0) {
6450
+ const uniqueRefs = [...new Set(refs)];
6451
+ const tokens = uniqueRefs.map((ref) => {
6452
+ const varName = fieldRefToVarName(ref, bandContext);
6453
+ const label = fieldRefToLabel(ref);
6454
+ return registry.register(varName, label, szOpts);
6455
+ });
6456
+ return tokens.join(" ");
6457
+ }
6458
+ }
6459
+ const toLongMatch = trimmed.match(
6460
+ /^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6461
+ );
6462
+ if (toLongMatch) {
6463
+ const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
6464
+ const varName = fieldRefToVarName(ref, bandContext);
6465
+ const label = fieldRefToLabel(ref);
6466
+ return registry.register(varName, label, szOpts);
6467
+ }
6468
+ const fmtMatch = trimmed.match(
6469
+ /^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
6470
+ );
6471
+ if (fmtMatch) {
6472
+ return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
6473
+ }
6474
+ const replaceMatch = trimmed.match(
6475
+ /^Replace\(\s*\[([^\]]+)\]/i
6476
+ );
6477
+ if (replaceMatch) {
6478
+ const ref = replaceMatch[1];
6479
+ const varName = fieldRefToVarName(ref, bandContext);
6480
+ const label = fieldRefToLabel(ref);
6481
+ return registry.register(varName, label, szOpts);
6482
+ }
6483
+ const iifLabelMatch = trimmed.match(
6484
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
6485
+ );
6486
+ if (iifLabelMatch) {
6487
+ return iifLabelMatch[1];
6488
+ }
6489
+ const iifLabelElse = trimmed.match(
6490
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
6491
+ );
6492
+ if (iifLabelElse) {
6493
+ return iifLabelElse[1];
6494
+ }
6495
+ const iifEmptyElse = trimmed.match(
6496
+ /^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
6497
+ );
6498
+ if (iifEmptyElse) {
6499
+ return iifEmptyElse[1];
6500
+ }
6501
+ const nestedIifStaticLabel = trimmed.match(
6502
+ /^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
6503
+ );
6504
+ if (nestedIifStaticLabel) {
6505
+ return nestedIifStaticLabel[1];
6506
+ }
6507
+ const iifValueMatch = trimmed.match(
6508
+ /^iif\s*\(/i
6509
+ );
6510
+ if (iifValueMatch) {
6511
+ const refs = extractFieldRefs(trimmed);
6512
+ if (refs.length > 0) {
6513
+ const uniqueRefs = [...new Set(refs)];
6514
+ const innerAggMatch = trimmed.match(
6515
+ /\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
6516
+ );
6517
+ if (innerAggMatch) {
6518
+ const fieldName = innerAggMatch[2];
6519
+ const varName2 = labelToVarName("workorder_" + fieldName);
6520
+ const label2 = fieldName.replace(/_/g, " ");
6521
+ return registry.register(varName2, label2, szOpts);
6522
+ }
6523
+ const ref = uniqueRefs[uniqueRefs.length - 1];
6524
+ const varName = fieldRefToVarName(ref, bandContext);
6525
+ const label = fieldRefToLabel(ref);
6526
+ return registry.register(varName, label, szOpts);
6527
+ }
6528
+ const staticMatch = trimmed.match(/'([^']+)'/);
6529
+ if (staticMatch) return staticMatch[1];
6530
+ return "";
6531
+ }
6532
+ const arithmeticRefs = extractFieldRefs(trimmed);
6533
+ if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
6534
+ if (siblingLabel) {
6535
+ const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
6536
+ const varName = labelToVarName(
6537
+ (bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
6538
+ );
6539
+ return registry.register(varName, cleanLabel, szOpts);
6540
+ }
6541
+ const uniqueRefs = [...new Set(arithmeticRefs)];
6542
+ const tokens = uniqueRefs.map((ref) => {
6543
+ const varName = fieldRefToVarName(ref, bandContext);
6544
+ const label = fieldRefToLabel(ref);
6545
+ return registry.register(varName, label, szOpts);
6546
+ });
6547
+ return tokens.join(" ");
6548
+ }
6549
+ const simpleMatch = trimmed.match(
6550
+ /^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
6551
+ );
6552
+ if (simpleMatch) {
6553
+ const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
6554
+ const varName = fieldRefToVarName(ref, bandContext);
6555
+ const label = fieldRefToLabel(ref);
6556
+ return registry.register(varName, label, szOpts);
6557
+ }
6558
+ if (arithmeticRefs.length >= 1) {
6559
+ const ref = arithmeticRefs[arithmeticRefs.length - 1];
6560
+ const varName = fieldRefToVarName(ref, bandContext);
6561
+ const label = fieldRefToLabel(ref);
6562
+ return registry.register(varName, label, szOpts);
6563
+ }
6564
+ return "";
6565
+ }
6566
+ var VariableRegistry = class {
6567
+ constructor() {
6568
+ __publicField(this, "map", /* @__PURE__ */ new Map());
6569
+ }
6570
+ register(varName, varLabel, options) {
6571
+ if (!this.map.has(varName)) {
6572
+ this.map.set(varName, { varName, varLabel, varDefault: "" });
6573
+ }
6574
+ const suppress = options?.suppressZero ? "|suppress:zero" : "";
6575
+ return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
6576
+ }
6577
+ getAll() {
6578
+ return Array.from(this.map.values());
6579
+ }
6580
+ };
6581
+ function parseLocationFloat(s) {
6582
+ if (!s) return { x: 0, y: 0 };
6583
+ const parts = s.split(",");
6584
+ return {
6585
+ x: parseFloat(parts[0]) || 0,
6586
+ y: parseFloat(parts[1]) || 0
6587
+ };
6588
+ }
6589
+ function parseSizeF(s) {
6590
+ if (!s) return { w: 0, h: 0 };
6591
+ const parts = s.split(",");
6592
+ return {
6593
+ w: parseFloat(parts[0]) || 0,
6594
+ h: parseFloat(parts[1]) || 0
6595
+ };
6596
+ }
6597
+ function extractControls(bandEl) {
6598
+ const controlsEl = bandEl.querySelector(":scope > Controls");
6599
+ if (!controlsEl) return [];
6600
+ const items = [];
6601
+ for (const child of Array.from(controlsEl.children)) {
6602
+ items.push(extractControl(child));
6603
+ }
6604
+ return items;
6605
+ }
6606
+ function extractControl(el) {
6607
+ const ctrlType = el.getAttribute("ControlType") || "";
6608
+ const name = el.getAttribute("Name") || "";
6609
+ const text = el.getAttribute("Text") || "";
6610
+ const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
6611
+ const size = parseSizeF(el.getAttribute("SizeF"));
6612
+ const formatString = el.getAttribute("TextFormatString") || null;
6613
+ let expression = null;
6614
+ const bindingsEl = el.querySelector(":scope > ExpressionBindings");
6615
+ if (bindingsEl) {
6616
+ for (const item of Array.from(bindingsEl.children)) {
6617
+ if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
6618
+ expression = item.getAttribute("Expression");
6619
+ break;
6620
+ }
6621
+ }
6622
+ }
6623
+ const children = [];
6624
+ if (ctrlType === "XRPanel") {
6625
+ const panelControls = extractControls(el);
6626
+ children.push(...panelControls);
6627
+ }
6628
+ return {
6629
+ name,
6630
+ type: ctrlType,
6631
+ text,
6632
+ x: loc.x,
6633
+ y: loc.y,
6634
+ width: size.w,
6635
+ expression,
6636
+ formatString,
6637
+ children
6638
+ };
6639
+ }
6640
+ function groupByRow(controls, threshold = 8) {
6641
+ const sorted = [...controls].sort((a, b) => a.y - b.y);
6642
+ const rows = [];
6643
+ for (const ctrl of sorted) {
6644
+ const lastRow = rows[rows.length - 1];
6645
+ if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
6646
+ lastRow.controls.push(ctrl);
6647
+ } else {
6648
+ rows.push({ y: ctrl.y, controls: [ctrl] });
6649
+ }
6650
+ }
6651
+ for (const row of rows) {
6652
+ row.controls.sort((a, b) => a.x - b.x);
6653
+ }
6654
+ return rows;
6655
+ }
6656
+ function isStaticLabel(ctrl) {
6657
+ return ctrl.type === "XRLabel" && !ctrl.expression;
6658
+ }
6659
+ function isBound(ctrl) {
6660
+ return ctrl.type === "XRLabel" && !!ctrl.expression;
6661
+ }
6662
+ function computeSplitPercent(leftControls, rightControls, contentWidth) {
6663
+ const leftExtent = leftControls.reduce(
6664
+ (max, c) => Math.max(max, c.x + c.width),
6665
+ 0
6666
+ );
6667
+ const rightMinX = rightControls.reduce(
6668
+ (min, c) => Math.min(min, c.x),
6669
+ contentWidth
6670
+ );
6671
+ const rightMaxExtent = rightControls.reduce(
6672
+ (max, c) => Math.max(max, c.x + c.width),
6673
+ 0
6674
+ );
6675
+ const rightExtent = rightMaxExtent - rightMinX;
6676
+ if (leftExtent + rightExtent === 0) return 50;
6677
+ return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
6678
+ }
6679
+ function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
6680
+ const lines = [];
6681
+ const leftControls = controls.filter((c) => c.x < 437);
6682
+ const rightControls = controls.filter((c) => c.x >= 437);
6683
+ const leftEntries = [];
6684
+ if (leftControls.length > 0) {
6685
+ const leftRows = groupByRow(leftControls);
6686
+ for (const row of leftRows) {
6687
+ const parts = [];
6688
+ for (const ctrl of row.controls) {
6689
+ if (isBound(ctrl)) {
6690
+ let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6691
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
6692
+ resolved = resolved.replace("}}", "|format:phone}}");
6693
+ }
6694
+ if (resolved) parts.push(resolved);
6695
+ } else if (isStaticLabel(ctrl)) {
6696
+ const text = ctrl.text.trim();
6697
+ if (text && !text.match(/^label\d+$/)) {
6698
+ parts.push(text);
6699
+ }
6700
+ }
6701
+ }
6702
+ if (parts.length > 0) {
6703
+ leftEntries.push(parts.join(" "));
6704
+ }
6705
+ }
6706
+ }
6707
+ const rightEntries = [];
6708
+ if (rightControls.length > 0) {
6709
+ const rightRows = groupByRow(rightControls);
6710
+ for (const row of rightRows) {
6711
+ let label = "";
6712
+ let value = "";
6713
+ for (const ctrl of row.controls) {
6714
+ if (isStaticLabel(ctrl)) {
6715
+ const text = ctrl.text.trim();
6716
+ if (text && !text.match(/^label\d+$/)) {
6717
+ label = text;
6718
+ }
6719
+ } else if (isBound(ctrl)) {
6720
+ value = resolveExpression(ctrl.expression, registry, label, "workorder");
6721
+ if (ctrl.expression) {
6722
+ const refs = extractFieldRefs(ctrl.expression);
6723
+ if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
6724
+ value = value.replace("}}", "|suppress:zero}}");
6725
+ }
6726
+ }
6727
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
6728
+ value = value.replace("}}", "|format:phone}}");
6729
+ }
6730
+ }
6731
+ }
6732
+ if (label || value) {
6733
+ rightEntries.push({ label, value, y: row.y });
6734
+ }
6735
+ }
6736
+ }
6737
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
6738
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
6739
+ lines.push(`:::columns{split:${outerSplit}}`);
6740
+ const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
6741
+ const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
6742
+ const xmlLineHeight = 17;
6743
+ const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
6744
+ const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
6745
+ const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
6746
+ lines.push(leftColTag);
6747
+ for (const entry of leftEntries) {
6748
+ lines.push(entry);
6749
+ }
6750
+ lines.push(":::");
6751
+ const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
6752
+ lines.push(rightColTag);
6753
+ if (rightEntries.length > 0) {
6754
+ lines.push("| | |");
6755
+ lines.push("|---|---|");
6756
+ let prevY = -Infinity;
6757
+ for (const entry of rightEntries) {
6758
+ if (prevY > -Infinity && entry.y - prevY > 25) {
6759
+ lines.push("| | |");
6760
+ }
6761
+ const label = entry.label || "";
6762
+ const colon = entry.label ? ":" : "";
6763
+ const value = entry.value ? ` ${entry.value}` : "";
6764
+ lines.push(`| ${label} | ${colon}${value} |`);
6765
+ prevY = entry.y;
6766
+ }
6767
+ }
6768
+ lines.push(":::");
6769
+ lines.push(":::");
6770
+ lines.push("");
6771
+ }
6772
+ return lines.join("\n");
6773
+ }
6774
+ function renderReportHeaderBand(controls, registry, contentWidth) {
6775
+ const lines = [];
6776
+ const leftControls = controls.filter((c) => c.x < 437);
6777
+ const rightControls = controls.filter((c) => c.x >= 437);
6778
+ const leftEntries = [];
6779
+ if (leftControls.length > 0) {
6780
+ const leftRows = groupByRow(leftControls);
6781
+ for (const row of leftRows) {
6782
+ const parts = [];
6783
+ for (const ctrl of row.controls) {
6784
+ if (isBound(ctrl)) {
6785
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6786
+ if (resolved) parts.push(resolved);
6787
+ } else if (isStaticLabel(ctrl)) {
6788
+ const text = ctrl.text.trim();
6789
+ if (text && !text.match(/^label\d+$/)) {
6790
+ parts.push(text);
6791
+ }
6792
+ }
6793
+ }
6794
+ if (parts.length > 0) {
6795
+ leftEntries.push(parts.join(" "));
6796
+ }
6797
+ }
6798
+ }
6799
+ const rightEntries = [];
6800
+ if (rightControls.length > 0) {
6801
+ const rightRows = groupByRow(rightControls);
6802
+ for (const row of rightRows) {
6803
+ let label = "";
6804
+ let value = "";
6805
+ for (const ctrl of row.controls) {
6806
+ if (isStaticLabel(ctrl)) {
6807
+ const text = ctrl.text.trim();
6808
+ if (text && !text.match(/^label\d+$/)) {
6809
+ label = text;
6810
+ }
6811
+ } else if (isBound(ctrl)) {
6812
+ value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6813
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
6814
+ value = value.replace("}}", "|format:phone}}");
6815
+ }
6816
+ }
6817
+ }
6818
+ if (label || value) {
6819
+ rightEntries.push({ label, value });
6820
+ }
6821
+ }
6822
+ }
6823
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
6824
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
6825
+ lines.push(`:::columns{split:${outerSplit}}`);
6826
+ lines.push(":::col");
6827
+ for (const entry of leftEntries) {
6828
+ lines.push(entry);
6829
+ }
6830
+ lines.push(":::");
6831
+ lines.push(":::col");
6832
+ lines.push("| | |");
6833
+ lines.push("|---|---|");
6834
+ for (const entry of rightEntries) {
6835
+ const label = entry.label || "";
6836
+ const value = entry.value ? ` ${entry.value}` : "";
6837
+ lines.push(`| ${label} |${value} |`);
6838
+ }
6839
+ lines.push(":::");
6840
+ lines.push(":::");
6841
+ lines.push("");
6842
+ }
6843
+ return lines.join("\n");
6844
+ }
6845
+ function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
6846
+ const lines = [];
6847
+ const headerTexts = [];
6848
+ for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
6849
+ if (isStaticLabel(ctrl)) {
6850
+ const text = ctrl.text.trim();
6851
+ if (text) headerTexts.push(text);
6852
+ }
6853
+ }
6854
+ const titleText = headerTexts.join(" ");
6855
+ lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
6856
+ lines.push(":::");
6857
+ lines.push(":::repeat{data:operations}");
6858
+ const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
6859
+ const detailParts = [];
6860
+ for (const ctrl of sortedDetail) {
6861
+ if (isBound(ctrl)) {
6862
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
6863
+ if (resolved) detailParts.push(resolved);
6864
+ }
6865
+ }
6866
+ if (detailParts.length > 0) {
6867
+ if (detailParts.length >= 2) {
6868
+ lines.push(":::columns{split:12}");
6869
+ lines.push(":::col");
6870
+ lines.push(`**${detailParts[0]}**`);
6871
+ lines.push(":::");
6872
+ lines.push(":::col");
6873
+ lines.push(`**${detailParts.slice(1).join(" ")}**`);
6874
+ lines.push(":::");
6875
+ lines.push(":::");
6876
+ } else {
6877
+ lines.push(`**${detailParts[0]}**`);
6878
+ }
6879
+ lines.push("");
6880
+ }
6881
+ if (groupFooterControls.length > 0) {
6882
+ const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
6883
+ if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
6884
+ }
6885
+ lines.push("---");
6886
+ lines.push(":::");
6887
+ lines.push("");
6888
+ return lines.join("\n");
6889
+ }
6890
+ function renderGroupFooterBand(controls, registry, contentWidth) {
6891
+ const rows = groupByRow(controls);
6892
+ const entries = [];
6893
+ for (const row of rows) {
6894
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
6895
+ if (labels.length === 0) continue;
6896
+ let label = "";
6897
+ let value = "";
6898
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
6899
+ for (const ctrl of sorted) {
6900
+ if (isStaticLabel(ctrl)) {
6901
+ const text = ctrl.text.trim();
6902
+ if (text && !text.match(/^label\d+$/)) {
6903
+ label = text;
6904
+ }
6905
+ } else if (isBound(ctrl)) {
6906
+ const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
6907
+ if (resolved) {
6908
+ if (!resolved.startsWith("{{var|")) {
6909
+ if (!label) label = resolved;
6910
+ } else {
6911
+ value = resolved;
6912
+ }
6913
+ }
6914
+ }
6915
+ }
6916
+ if (label && value) {
6917
+ entries.push({ label, value });
6918
+ }
6919
+ }
6920
+ if (entries.length === 0) return "";
6921
+ const lines = [];
6922
+ lines.push(":::subtotals");
6923
+ for (const entry of entries) {
6924
+ lines.push(`**${entry.label}** ${entry.value}`);
6925
+ }
6926
+ lines.push(":::");
6927
+ lines.push("");
6928
+ return lines.join("\n");
6929
+ }
6930
+ function renderReportFooterBand(controls, registry) {
6931
+ const lines = [];
6932
+ const panel = controls.find((c) => c.type === "XRPanel");
6933
+ const thankYouLabel = controls.find(
6934
+ (c) => c.type === "XRLabel" && c.text.includes("Thank You")
6935
+ );
6936
+ const priceRows = [];
6937
+ if (panel && panel.children.length > 0) {
6938
+ const rows = groupByRow(panel.children, 15);
6939
+ for (const row of rows) {
6940
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
6941
+ if (labels.length === 0) continue;
6942
+ let label = "";
6943
+ let value = "";
6944
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
6945
+ for (const ctrl of sorted) {
6946
+ if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
6947
+ continue;
6948
+ }
6949
+ const isLabelPos = ctrl.x < 150;
6950
+ if (isStaticLabel(ctrl)) {
6951
+ const text = ctrl.text.trim();
6952
+ if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
6953
+ label = text;
6954
+ }
6955
+ } else if (isBound(ctrl)) {
6956
+ const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
6957
+ if (resolved) {
6958
+ if (isLabelPos) {
6959
+ if (!label) label = resolved;
6960
+ } else {
6961
+ if (!value) value = resolved;
6962
+ }
6963
+ }
6964
+ }
6965
+ }
6966
+ if (label && value) {
6967
+ priceRows.push({ label, value });
6968
+ } else if (value) {
6969
+ priceRows.push({ label: "", value });
6970
+ }
6971
+ }
6972
+ }
6973
+ lines.push(":::columns{split:40|padX:20}");
6974
+ lines.push(":::col{padTop:1}");
6975
+ if (thankYouLabel) {
6976
+ lines.push(`## ***${thankYouLabel.text.trim()}***`);
6977
+ }
6978
+ lines.push(":::");
6979
+ lines.push(":::col{padTop:1}");
6980
+ lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
6981
+ if (priceRows.length > 0) {
6982
+ lines.push(":::subtotals");
6983
+ for (const row of priceRows) {
6984
+ const label = row.label ? `**${row.label}**` : "";
6985
+ lines.push(`${label} ${row.value}`);
6986
+ }
6987
+ lines.push(":::");
6988
+ }
6989
+ lines.push(":::");
6990
+ lines.push(":::");
6991
+ lines.push(":::");
6992
+ lines.push("");
6993
+ return lines.join("\n");
6994
+ }
6995
+ function parseXmlTemplate(xmlString) {
6996
+ const warnings = [];
6997
+ const registry = new VariableRegistry();
6998
+ const parser = new DOMParser();
6999
+ const doc = parser.parseFromString(xmlString, "text/xml");
7000
+ const parseError = doc.querySelector("parsererror");
7001
+ if (parseError) {
7002
+ return {
7003
+ markdown: "",
7004
+ variables: [],
7005
+ reportName: "",
7006
+ warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
7007
+ };
7008
+ }
7009
+ const root = doc.documentElement;
7010
+ const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
7011
+ const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
7012
+ const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
7013
+ const marginParts = marginsStr.split(",").map(Number);
7014
+ const leftMargin = marginParts[0] || 50;
7015
+ const rightMargin = marginParts[1] || 50;
7016
+ const contentWidth = pageWidth - leftMargin - rightMargin;
7017
+ const calculatedFieldNames = /* @__PURE__ */ new Set();
7018
+ const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
7019
+ if (calcFieldsEl) {
7020
+ for (const item of Array.from(calcFieldsEl.children)) {
7021
+ const name = item.getAttribute("Name");
7022
+ if (name) calculatedFieldNames.add(name);
7023
+ }
7024
+ }
7025
+ const markdownSections = [];
7026
+ const watermarkEl = root.querySelector("Watermarks > Item1");
7027
+ if (watermarkEl) {
7028
+ const wmText = watermarkEl.getAttribute("Text");
7029
+ if (wmText) {
7030
+ markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
7031
+ markdownSections.push("");
7032
+ }
7033
+ }
7034
+ const bandsEl = root.querySelector(":scope > Bands");
7035
+ if (!bandsEl) {
7036
+ warnings.push("No <Bands> element found in the XML");
7037
+ return {
7038
+ markdown: "",
7039
+ variables: registry.getAll(),
7040
+ reportName,
7041
+ warnings
7042
+ };
7043
+ }
7044
+ let topMarginControls = [];
7045
+ let reportHeaderControls = [];
7046
+ let pageHeaderControls = [];
7047
+ let detailControls = [];
7048
+ let groupFooterControls = [];
7049
+ let reportFooterControls = [];
7050
+ for (const bandItem of Array.from(bandsEl.children)) {
7051
+ const ctrlType = bandItem.getAttribute("ControlType") || "";
7052
+ if (ctrlType === "TopMarginBand") {
7053
+ topMarginControls = extractControls(bandItem);
7054
+ } else if (ctrlType === "ReportHeaderBand") {
7055
+ reportHeaderControls = extractControls(bandItem);
7056
+ } else if (ctrlType === "PageHeaderBand") {
7057
+ pageHeaderControls = extractControls(bandItem);
7058
+ } else if (ctrlType === "DetailReportBand") {
7059
+ const nestedBands = bandItem.querySelector(":scope > Bands");
7060
+ if (nestedBands) {
7061
+ for (const nested of Array.from(nestedBands.children)) {
7062
+ const nestedType = nested.getAttribute("ControlType") || "";
7063
+ if (nestedType === "DetailBand") {
7064
+ detailControls = extractControls(nested);
7065
+ } else if (nestedType === "GroupFooterBand") {
7066
+ groupFooterControls = extractControls(nested);
7067
+ }
7068
+ }
7069
+ }
7070
+ } else if (ctrlType === "ReportFooterBand") {
7071
+ reportFooterControls = extractControls(bandItem);
7072
+ }
7073
+ }
7074
+ if (topMarginControls.length > 0) {
7075
+ markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
7076
+ }
7077
+ if (reportHeaderControls.length > 0) {
7078
+ markdownSections.push(
7079
+ renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
7080
+ );
7081
+ }
7082
+ if (pageHeaderControls.length > 0 || detailControls.length > 0) {
7083
+ markdownSections.push(
7084
+ renderOperationsSection(
7085
+ pageHeaderControls,
7086
+ detailControls,
7087
+ groupFooterControls,
7088
+ registry)
7089
+ );
7090
+ }
7091
+ if (reportFooterControls.length > 0) {
7092
+ markdownSections.push(
7093
+ renderReportFooterBand(reportFooterControls, registry)
7094
+ );
7095
+ }
7096
+ const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
7097
+ return {
7098
+ markdown,
7099
+ variables: registry.getAll(),
7100
+ reportName,
7101
+ warnings
7102
+ };
7103
+ }
7104
+
7105
+ export { DocumentGenerator, EditorPanel, FormFieldType, PreviewPanel, RepeatNode, SubtotalsNode, ThemeProvider, expandRepeatContent, extractVariablesFromContent, generatePdfFromMarkdown, generatePdfFromTiptap, isZeroLike, markdownToTiptap, parseXmlTemplate, suppressZeroContent, tiptapToMarkdown, useDocumentGenerator, useTheme };
4929
7106
  //# sourceMappingURL=index.mjs.map
4930
7107
  //# sourceMappingURL=index.mjs.map