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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,259 @@ 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 result = await generatePdfFromContent(replaced);
2557
+ return { pdfBytes: result.pdfBytes };
2558
+ }
2559
+ async function generatePdfFromMarkdown(markdown, values) {
2560
+ const content = markdownToTiptap(markdown);
2561
+ return generatePdfFromTiptap(content, values);
2562
+ }
2563
+
2564
+ // src/utils/markdown-writer.ts
2565
+ function serializeFieldToken(attrs) {
2566
+ const parts = ["field"];
2567
+ if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
2568
+ if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
2569
+ if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
2570
+ if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
2571
+ if (attrs.required) parts.push(`required:true`);
2572
+ if (attrs.options) parts.push(`options:${attrs.options}`);
2573
+ if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
2574
+ if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
2575
+ if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
2576
+ if (attrs.multiline) parts.push(`multiline:true`);
2577
+ if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
2578
+ if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2579
+ return `{{${parts.join("|")}}}`;
2580
+ }
2581
+ function serializeVariableToken(attrs) {
2582
+ const parts = ["var"];
2583
+ if (attrs.varName) parts.push(`name:${attrs.varName}`);
2584
+ if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
2585
+ if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
2586
+ if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
2587
+ if (attrs.format) parts.push(`format:${attrs.format}`);
2588
+ return `{{${parts.join("|")}}}`;
2589
+ }
2590
+ function serializeInline(content) {
2591
+ if (!content) return "";
2592
+ let result = "";
2593
+ for (const node of content) {
2594
+ if (node.type === "fieldNode") {
2595
+ result += serializeFieldToken(node.attrs || {});
2596
+ continue;
2597
+ }
2598
+ if (node.type === "variableNode") {
2599
+ let token = serializeVariableToken(node.attrs || {});
2600
+ const varMarks = node.marks || [];
2601
+ const isBold = varMarks.some((m) => m.type === "bold");
2602
+ const isItalic = varMarks.some((m) => m.type === "italic");
2603
+ const isUnderline = varMarks.some((m) => m.type === "underline");
2604
+ if (isBold && isItalic) token = `***${token}***`;
2605
+ else if (isBold) token = `**${token}**`;
2606
+ else if (isItalic) token = `*${token}*`;
2607
+ if (isUnderline) token = `__${token}__`;
2608
+ result += token;
2609
+ continue;
2610
+ }
2611
+ if (node.type === "text") {
2612
+ let text = node.text || "";
2613
+ const marks = node.marks || [];
2614
+ for (const mark of marks) {
2615
+ if (mark.type === "bold") text = `**${text}**`;
2616
+ else if (mark.type === "italic") text = `*${text}*`;
2617
+ else if (mark.type === "underline") text = `__${text}__`;
2618
+ else if (mark.type === "code") text = `\`${text}\``;
2619
+ }
2620
+ result += text;
2621
+ }
2622
+ if (node.type === "hardBreak") {
2623
+ result += " \n";
2624
+ }
2625
+ }
2626
+ return result;
2627
+ }
2628
+ function serializeBlock(node) {
2629
+ switch (node.type) {
2630
+ case "heading": {
2631
+ const level = node.attrs?.level || 1;
2632
+ const prefix = "#".repeat(level);
2633
+ return `${prefix} ${serializeInline(node.content)}`;
2634
+ }
2635
+ case "paragraph": {
2636
+ const inline = serializeInline(node.content);
2637
+ return inline;
2638
+ }
2639
+ case "bulletList": {
2640
+ return (node.content || []).map((item) => {
2641
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2642
+ return `- ${inner}`;
2643
+ }).join("\n");
2644
+ }
2645
+ case "orderedList": {
2646
+ return (node.content || []).map((item, i) => {
2647
+ const inner = (item.content || []).map(serializeBlock).join("\n");
2648
+ return `${i + 1}. ${inner}`;
2649
+ }).join("\n");
2650
+ }
2651
+ case "listItem": {
2652
+ return (node.content || []).map(serializeBlock).join("\n");
2653
+ }
2654
+ case "blockquote": {
2655
+ return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
2656
+ }
2657
+ case "codeBlock": {
2658
+ const text = serializeInline(node.content);
2659
+ return `\`\`\`
2660
+ ${text}
2661
+ \`\`\``;
2662
+ }
2663
+ case "horizontalRule": {
2664
+ return "---";
2665
+ }
2666
+ case "table": {
2667
+ const rows = node.content || [];
2668
+ const isBorderless = node.attrs?.borderless === true;
2669
+ const isSubtotals = node.attrs?.subtotals === true;
2670
+ const serializedRows = [];
2671
+ if (isBorderless && rows.length > 0) {
2672
+ const colCount = rows[0].content?.length || 0;
2673
+ const markerCell = isSubtotals ? "_" : "";
2674
+ const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
2675
+ const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
2676
+ serializedRows.push(markerRow);
2677
+ serializedRows.push(sep);
2678
+ }
2679
+ for (let ri = 0; ri < rows.length; ri++) {
2680
+ const row = rows[ri];
2681
+ const cells = (row.content || []).map((cell) => {
2682
+ const inner = (cell.content || []).map(serializeBlock).join("");
2683
+ return inner;
2684
+ });
2685
+ serializedRows.push(`| ${cells.join(" | ")} |`);
2686
+ if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
2687
+ const sep = cells.map(() => "------").join("|");
2688
+ serializedRows.push(`|${sep}|`);
2689
+ }
2690
+ }
2691
+ return serializedRows.join("\n");
2692
+ }
2693
+ case "tableRow": {
2694
+ const cells = (node.content || []).map((cell) => {
2695
+ const inner = (cell.content || []).map(serializeBlock).join("");
2696
+ return inner;
2697
+ });
2698
+ return `| ${cells.join(" | ")} |`;
2699
+ }
2700
+ case "tableHeader":
2701
+ case "tableCell": {
2702
+ return (node.content || []).map(serializeBlock).join("");
2703
+ }
2704
+ case "watermark": {
2705
+ const attrs = node.attrs || {};
2706
+ const parts = [];
2707
+ if (attrs.text) parts.push(`text:${attrs.text}`);
2708
+ if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
2709
+ if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
2710
+ return `:::watermark{${parts.join("|")}}`;
2711
+ }
2712
+ case "panel": {
2713
+ const attrs = node.attrs || {};
2714
+ const parts = [];
2715
+ if (attrs.title) parts.push(`title:${attrs.title}`);
2716
+ if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
2717
+ else if (attrs.border === "solid") parts.push(`border:solid`);
2718
+ if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
2719
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
2720
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2721
+ return `:::panel${attrStr}
2722
+ ${children}
2723
+ :::`;
2724
+ }
2725
+ case "repeatBlock": {
2726
+ const attrs = node.attrs || {};
2727
+ const parts = [];
2728
+ if (attrs.data) parts.push(`data:${attrs.data}`);
2729
+ const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
2730
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2731
+ return `:::repeat${attrStr}
2732
+ ${children}
2733
+ :::`;
2734
+ }
2735
+ case "subtotalsBlock": {
2736
+ const children = (node.content || []).map(serializeBlock).join("\n");
2737
+ return `:::subtotals
2738
+ ${children}
2739
+ :::`;
2740
+ }
2741
+ case "columns": {
2742
+ const attrs = node.attrs || {};
2743
+ const splitVal = attrs.split || "50";
2744
+ const cols = (node.content || []).filter((c) => c.type === "column");
2745
+ const colBlocks = cols.map((col) => {
2746
+ const children = (col.content || []).map(serializeBlock).join("\n\n");
2747
+ const colAttrs = col.attrs || {};
2748
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
2749
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
2750
+ return `${colTag}
2751
+ ${children}
2752
+ :::`;
2753
+ });
2754
+ const padX = parseFloat(attrs.padX || "0") || 0;
2755
+ const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
2756
+ return `:::columns{${colsAttrs}}
2757
+ ${colBlocks.join("\n")}
2758
+ :::`;
2759
+ }
2760
+ case "column": {
2761
+ const children = (node.content || []).map(serializeBlock).join("\n\n");
2762
+ const colAttrs = node.attrs || {};
2763
+ const padTop = parseFloat(colAttrs.padTop || "0") || 0;
2764
+ const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
2765
+ return `${colTag}
2766
+ ${children}
2767
+ :::`;
2768
+ }
2769
+ default:
2770
+ return serializeInline(node.content);
2771
+ }
2772
+ }
2773
+ function tiptapToMarkdown(doc) {
2774
+ if (!doc.content) return "";
2775
+ return doc.content.map(serializeBlock).join("\n\n");
2776
+ }
2777
+
2778
+ // src/utils/pdf-preview.ts
2779
+ async function pdfToImages(pdfBytes, scale = 2) {
2780
+ let pdfjsLib;
2781
+ try {
2782
+ pdfjsLib = await import('pdfjs-dist');
2783
+ } catch (err) {
2784
+ throw new Error(formatError(
2785
+ "Failed to load the PDF preview library \u2014 ensure pdfjs-dist is installed and the worker is accessible",
2786
+ err
2787
+ ));
2788
+ }
2789
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
2790
+ pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/build/pdf.worker.mjs";
2791
+ }
2792
+ let pdf;
2793
+ try {
2794
+ const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
2795
+ pdf = await loadingTask.promise;
2796
+ } catch (err) {
2797
+ throw new Error(formatError("Failed to parse the generated PDF for preview", err));
2798
+ }
2799
+ const pages = [];
2800
+ const pageErrors = [];
2801
+ for (let i = 1; i <= pdf.numPages; i++) {
2802
+ try {
2803
+ const page = await pdf.getPage(i);
1569
2804
  const viewport = page.getViewport({ scale });
1570
2805
  const canvas = document.createElement("canvas");
1571
2806
  canvas.width = viewport.width;
@@ -1626,8 +2861,26 @@ function useDocumentGenerator(options = {}) {
1626
2861
  Placeholder.configure({
1627
2862
  placeholder: options.placeholder || "Start writing your document..."
1628
2863
  }),
2864
+ Table.extend({
2865
+ addAttributes() {
2866
+ return {
2867
+ ...this.parent?.(),
2868
+ borderless: { default: false },
2869
+ subtotals: { default: false }
2870
+ };
2871
+ }
2872
+ }).configure({ resizable: false }),
2873
+ TableRow,
2874
+ TableCell,
2875
+ TableHeader,
1629
2876
  FieldNode,
1630
- VariableNode
2877
+ VariableNode,
2878
+ PanelNode,
2879
+ ColumnsNode,
2880
+ ColumnNode,
2881
+ WatermarkNode,
2882
+ RepeatNode,
2883
+ SubtotalsNode
1631
2884
  ],
1632
2885
  content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
1633
2886
  onUpdate: ({ editor: ed }) => {
@@ -1731,10 +2984,9 @@ function useDocumentGenerator(options = {}) {
1731
2984
  async (values) => {
1732
2985
  if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
1733
2986
  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 };
2987
+ const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
2988
+ const pdfPages2 = await pdfToImages(pdfBytes2);
2989
+ return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
1738
2990
  },
1739
2991
  [editor]
1740
2992
  );
@@ -2888,15 +4140,47 @@ function FieldEditPopover({
2888
4140
  ] }) })
2889
4141
  ] });
2890
4142
  }
2891
- function labelToVarName(label) {
4143
+ var sizeClasses = {
4144
+ md: "px-3 py-1.5 text-xs font-medium",
4145
+ sm: "px-2 py-0.5 text-[10px] font-medium"
4146
+ };
4147
+ function ToggleGroup({
4148
+ value,
4149
+ onChange,
4150
+ options,
4151
+ size = "md",
4152
+ className
4153
+ }) {
4154
+ 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(
4155
+ "button",
4156
+ {
4157
+ className: cn(
4158
+ "rounded-md transition-colors",
4159
+ sizeClasses[size],
4160
+ value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
4161
+ ),
4162
+ onClick: () => onChange(opt.value),
4163
+ children: opt.label
4164
+ },
4165
+ opt.value
4166
+ )) });
4167
+ }
4168
+ function labelToVarName2(label) {
2892
4169
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
2893
4170
  }
4171
+ function varNameToLabel(name) {
4172
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4173
+ }
2894
4174
  function VariableInsertPopover({
2895
4175
  open,
2896
4176
  onOpenChange,
2897
4177
  onInsert,
4178
+ predefinedVariables,
2898
4179
  children
2899
4180
  }) {
4181
+ const hasPredefined = !!predefinedVariables?.length;
4182
+ const [mode, setMode] = useState(hasPredefined ? "existing" : "new");
4183
+ const [search, setSearch] = useState("");
2900
4184
  const [varLabel, setVarLabel] = useState("");
2901
4185
  const [varName, setVarName] = useState("");
2902
4186
  const [varDefault, setVarDefault] = useState("");
@@ -2906,7 +4190,9 @@ function VariableInsertPopover({
2906
4190
  setVarName("");
2907
4191
  setVarDefault("");
2908
4192
  setNameManuallyEdited(false);
2909
- }, []);
4193
+ setSearch("");
4194
+ setMode(hasPredefined ? "existing" : "new");
4195
+ }, [hasPredefined]);
2910
4196
  const handleOpenChange = useCallback(
2911
4197
  (nextOpen) => {
2912
4198
  if (!nextOpen) reset();
@@ -2916,7 +4202,7 @@ function VariableInsertPopover({
2916
4202
  );
2917
4203
  useEffect(() => {
2918
4204
  if (!nameManuallyEdited) {
2919
- setVarName(labelToVarName(varLabel));
4205
+ setVarName(labelToVarName2(varLabel));
2920
4206
  }
2921
4207
  }, [varLabel, nameManuallyEdited]);
2922
4208
  const handleInsert = useCallback(() => {
@@ -2928,6 +4214,26 @@ function VariableInsertPopover({
2928
4214
  });
2929
4215
  handleOpenChange(false);
2930
4216
  }, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
4217
+ const handlePickPredefined = useCallback(
4218
+ (pv) => {
4219
+ onInsert({
4220
+ varName: pv.varName,
4221
+ varLabel: pv.varLabel || varNameToLabel(pv.varName),
4222
+ varDefault: ""
4223
+ });
4224
+ handleOpenChange(false);
4225
+ },
4226
+ [onInsert, handleOpenChange]
4227
+ );
4228
+ const filteredVars = useMemo(() => {
4229
+ if (!predefinedVariables?.length) return [];
4230
+ const q = search.toLowerCase();
4231
+ if (!q) return predefinedVariables;
4232
+ return predefinedVariables.filter((pv) => {
4233
+ const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
4234
+ return pv.varName.toLowerCase().includes(q) || label.includes(q);
4235
+ });
4236
+ }, [predefinedVariables, search]);
2931
4237
  return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
2932
4238
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children }),
2933
4239
  /* @__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 +4241,114 @@ function VariableInsertPopover({
2935
4241
  /* @__PURE__ */ jsx(Braces, { size: 14 }),
2936
4242
  /* @__PURE__ */ jsx("span", { children: "Insert Variable" })
2937
4243
  ] }),
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" }),
4244
+ hasPredefined && /* @__PURE__ */ jsx(
4245
+ ToggleGroup,
4246
+ {
4247
+ value: mode,
4248
+ onChange: setMode,
4249
+ options: [
4250
+ { value: "existing", label: "Existing" },
4251
+ { value: "new", label: "New" }
4252
+ ],
4253
+ size: "sm"
4254
+ }
4255
+ ),
4256
+ mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4257
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
4258
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
2941
4259
  /* @__PURE__ */ jsx(
2942
4260
  Input,
2943
4261
  {
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"
4262
+ value: search,
4263
+ onChange: (e) => setSearch(e.target.value),
4264
+ placeholder: "Search variables...",
4265
+ className: "h-8 text-xs pl-7"
2949
4266
  }
2950
4267
  )
2951
4268
  ] }),
2952
- /* @__PURE__ */ jsxs("div", { children: [
2953
- /* @__PURE__ */ jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4269
+ /* @__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) => {
4270
+ const label = pv.varLabel || varNameToLabel(pv.varName);
4271
+ return /* @__PURE__ */ jsxs(
4272
+ "button",
4273
+ {
4274
+ 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",
4275
+ onClick: () => handlePickPredefined(pv),
4276
+ children: [
4277
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: label }),
4278
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
4279
+ ]
4280
+ },
4281
+ pv.varName
4282
+ );
4283
+ }) })
4284
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4285
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4286
+ /* @__PURE__ */ jsxs("div", { children: [
4287
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
4288
+ /* @__PURE__ */ jsx(
4289
+ Input,
4290
+ {
4291
+ id: "var-label",
4292
+ value: varLabel,
4293
+ onChange: (e) => setVarLabel(e.target.value),
4294
+ placeholder: "e.g. Company Name",
4295
+ className: "h-8 text-xs"
4296
+ }
4297
+ )
4298
+ ] }),
4299
+ /* @__PURE__ */ jsxs("div", { children: [
4300
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
4301
+ /* @__PURE__ */ jsx(
4302
+ Input,
4303
+ {
4304
+ id: "var-name",
4305
+ value: varName,
4306
+ onChange: (e) => {
4307
+ setVarName(e.target.value);
4308
+ setNameManuallyEdited(true);
4309
+ },
4310
+ placeholder: "e.g. company_name",
4311
+ className: "h-8 text-xs font-mono"
4312
+ }
4313
+ ),
4314
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
4315
+ ] }),
4316
+ /* @__PURE__ */ jsxs("div", { children: [
4317
+ /* @__PURE__ */ jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
4318
+ /* @__PURE__ */ jsx(
4319
+ Input,
4320
+ {
4321
+ id: "var-default",
4322
+ value: varDefault,
4323
+ onChange: (e) => setVarDefault(e.target.value),
4324
+ placeholder: "Optional",
4325
+ className: "h-8 text-xs"
4326
+ }
4327
+ )
4328
+ ] })
4329
+ ] }),
4330
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
2954
4331
  /* @__PURE__ */ jsx(
2955
- Input,
4332
+ Button,
2956
4333
  {
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"
4334
+ size: "sm",
4335
+ className: "h-8 flex-1 text-xs",
4336
+ onClick: handleInsert,
4337
+ disabled: !varLabel.trim() || !varName.trim(),
4338
+ children: "Insert"
2965
4339
  }
2966
4340
  ),
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
4341
  /* @__PURE__ */ jsx(
2972
- Input,
4342
+ Button,
2973
4343
  {
2974
- id: "var-default",
2975
- value: varDefault,
2976
- onChange: (e) => setVarDefault(e.target.value),
2977
- placeholder: "Optional",
2978
- className: "h-8 text-xs"
4344
+ variant: "outline",
4345
+ size: "sm",
4346
+ className: "h-8 text-xs",
4347
+ onClick: () => handleOpenChange(false),
4348
+ children: "Cancel"
2979
4349
  }
2980
4350
  )
2981
4351
  ] })
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
4352
  ] })
3005
4353
  ] }) })
3006
4354
  ] });
@@ -3222,7 +4570,6 @@ function PreviewPanel({
3222
4570
  (f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
3223
4571
  );
3224
4572
  }, [positionedFields, currentPage, currentPageIndex]);
3225
- const showScrollbars = zoomLevel > 1;
3226
4573
  return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col h-full border border-border rounded-lg overflow-hidden bg-muted/20", className), children: [
3227
4574
  /* @__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
4575
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -3338,12 +4685,13 @@ function PreviewPanel({
3338
4685
  "div",
3339
4686
  {
3340
4687
  className: cn(
3341
- "border border-border rounded-lg bg-muted/30 shrink-0",
3342
- showScrollbars ? "overflow-auto" : "overflow-hidden"
4688
+ "border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
3343
4689
  ),
3344
4690
  style: {
3345
4691
  width: pageDisplaySize.viewportWidth,
3346
- height: pageDisplaySize.viewportHeight
4692
+ height: pageDisplaySize.viewportHeight,
4693
+ maxWidth: "100%",
4694
+ maxHeight: "100%"
3347
4695
  },
3348
4696
  children: /* @__PURE__ */ jsxs(
3349
4697
  "div",
@@ -3435,6 +4783,37 @@ function validateMarkdown(markdown) {
3435
4783
  searchFrom = closeIdx + 2;
3436
4784
  }
3437
4785
  }
4786
+ const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
4787
+ const DIRECTIVE_CLOSE_RE = /^:::$/;
4788
+ const directiveStack = [];
4789
+ for (let i = 0; i < lines.length; i++) {
4790
+ const trimmed = lines[i].trim();
4791
+ const lineNum = i + 1;
4792
+ const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
4793
+ if (openMatch) {
4794
+ const dtype = openMatch[1];
4795
+ if (dtype !== "watermark") {
4796
+ if (dtype === "col") {
4797
+ const parent = directiveStack[directiveStack.length - 1];
4798
+ if (!parent || parent.type !== "columns") {
4799
+ warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
4800
+ }
4801
+ }
4802
+ directiveStack.push({ type: dtype, line: lineNum });
4803
+ }
4804
+ continue;
4805
+ }
4806
+ if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
4807
+ if (directiveStack.length === 0) {
4808
+ warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
4809
+ } else {
4810
+ directiveStack.pop();
4811
+ }
4812
+ }
4813
+ }
4814
+ for (const open of directiveStack) {
4815
+ errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
4816
+ }
3438
4817
  let fields = [];
3439
4818
  let variables = [];
3440
4819
  try {
@@ -3468,31 +4847,6 @@ function validateMarkdown(markdown) {
3468
4847
  fields
3469
4848
  };
3470
4849
  }
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
4850
  function parseCsv(text) {
3497
4851
  const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
3498
4852
  if (lines.length < 2) return [];
@@ -3542,7 +4896,10 @@ function GeneratePanel({
3542
4896
  editorContent,
3543
4897
  editorVariables,
3544
4898
  onGeneratePdf,
3545
- initialBulkData
4899
+ initialBulkData,
4900
+ initialVariableValues,
4901
+ exportFileName,
4902
+ templateMode
3546
4903
  }) {
3547
4904
  const [templateSource, setTemplateSource] = useState("editor");
3548
4905
  const [importedMarkdown, setImportedMarkdown] = useState(null);
@@ -3550,6 +4907,7 @@ function GeneratePanel({
3550
4907
  const [validationResult, setValidationResult] = useState(null);
3551
4908
  const [mode, setMode] = useState("single");
3552
4909
  const [variableValues, setVariableValues] = useState({});
4910
+ const [variableSearch, setVariableSearch] = useState("");
3553
4911
  const [bulkInputFormat, setBulkInputFormat] = useState("json");
3554
4912
  const [bulkInput, setBulkInput] = useState("");
3555
4913
  const [bulkData, setBulkData] = useState(null);
@@ -3576,11 +4934,22 @@ function GeneratePanel({
3576
4934
  const col = pos - textBefore.lastIndexOf("\n");
3577
4935
  setJsonCursor({ line, col });
3578
4936
  }, []);
4937
+ const lockedVarNames = React12__default.useMemo(
4938
+ () => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
4939
+ [templateMode, initialVariableValues]
4940
+ );
3579
4941
  const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
3580
4942
  const hasVariables = activeVariables.length > 0;
3581
4943
  const hasEditorContent = editorMarkdown.trim().length > 0;
3582
4944
  const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
3583
4945
  const hasContent = hasImportedContent || hasEditorContent;
4946
+ const filteredVariables = React12__default.useMemo(() => {
4947
+ if (!variableSearch.trim()) return activeVariables;
4948
+ const q = variableSearch.toLowerCase();
4949
+ return activeVariables.filter(
4950
+ (v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
4951
+ );
4952
+ }, [activeVariables, variableSearch]);
3584
4953
  const getActiveContent = useCallback(() => {
3585
4954
  if (templateSource === "imported" && importedMarkdown) {
3586
4955
  return markdownToTiptap(importedMarkdown);
@@ -3748,7 +5117,9 @@ function GeneratePanel({
3748
5117
  setPreviewError(null);
3749
5118
  try {
3750
5119
  const content = getActiveContent();
3751
- const replaced = replaceVariablesInContent(content, variableValues);
5120
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5121
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5122
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3752
5123
  const fields = extractFieldsFromContent(replaced);
3753
5124
  const result = await generatePdfFromContent(replaced);
3754
5125
  const pages = await pdfToImages(result.pdfBytes);
@@ -3769,7 +5140,9 @@ function GeneratePanel({
3769
5140
  setExportSuccess(null);
3770
5141
  try {
3771
5142
  const content = getActiveContent();
3772
- const replaced = replaceVariablesInContent(content, variableValues);
5143
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5144
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5145
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3773
5146
  const fields = extractFieldsFromContent(replaced);
3774
5147
  const result = await generatePdfFromContent(replaced, {
3775
5148
  drawFieldPlaceholders: false,
@@ -3777,7 +5150,7 @@ function GeneratePanel({
3777
5150
  fields
3778
5151
  });
3779
5152
  const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
3780
- const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
5153
+ const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
3781
5154
  if (onGeneratePdf) {
3782
5155
  onGeneratePdf(blob, fileName);
3783
5156
  } else {
@@ -3849,7 +5222,9 @@ function GeneratePanel({
3849
5222
  const allWarnings = [];
3850
5223
  for (let i = 0; i < bulkData.length; i++) {
3851
5224
  const values = bulkData[i];
3852
- const replaced = replaceVariablesInContent(content, values);
5225
+ const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5226
+ const suppressed = suppressZeroContent(expanded, enrichedValues);
5227
+ const replaced = replaceVariablesInContent(suppressed, enrichedValues);
3853
5228
  const fields = extractFieldsFromContent(replaced);
3854
5229
  const result = await generatePdfFromContent(replaced, {
3855
5230
  drawFieldPlaceholders: false,
@@ -3904,13 +5279,15 @@ ${allWarnings.join("\n")}`);
3904
5279
  }, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
3905
5280
  React12__default.useEffect(() => {
3906
5281
  if (templateSource === "editor") {
3907
- const defaults = {};
5282
+ const defaults = initialVariableValues ? { ...initialVariableValues } : {};
3908
5283
  for (const v of editorVariables) {
3909
- defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5284
+ if (!defaults[v.varName]) {
5285
+ defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
5286
+ }
3910
5287
  }
3911
5288
  setVariableValues(defaults);
3912
5289
  }
3913
- }, [editorVariables, templateSource]);
5290
+ }, [editorVariables, templateSource, initialVariableValues]);
3914
5291
  useEffect(() => {
3915
5292
  setPreviewFresh(false);
3916
5293
  }, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
@@ -3922,8 +5299,8 @@ ${allWarnings.join("\n")}`);
3922
5299
  }, [exportSuccess]);
3923
5300
  return /* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
3924
5301
  /* @__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: [
5302
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
5303
+ !templateMode && /* @__PURE__ */ jsxs("div", { children: [
3927
5304
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
3928
5305
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Template" }),
3929
5306
  templateSource === "imported" && /* @__PURE__ */ jsxs(
@@ -4016,10 +5393,10 @@ ${allWarnings.join("\n")}`);
4016
5393
  !hasContent && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
4017
5394
  /* @__PURE__ */ jsx(Braces, { size: 32, className: "mb-2 opacity-50" }),
4018
5395
  /* @__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." })
5396
+ /* @__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
5397
  ] }),
4021
5398
  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(
5399
+ !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
5400
  ToggleGroup,
4024
5401
  {
4025
5402
  value: mode,
@@ -4031,20 +5408,53 @@ ${allWarnings.join("\n")}`);
4031
5408
  }
4032
5409
  ) }),
4033
5410
  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 }),
5411
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
5412
+ /* @__PURE__ */ jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
5413
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
5414
+ ] }),
5415
+ activeVariables.length > 5 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
5416
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
4037
5417
  /* @__PURE__ */ jsx(
4038
5418
  Input,
4039
5419
  {
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"
5420
+ value: variableSearch,
5421
+ onChange: (e) => setVariableSearch(e.target.value),
5422
+ placeholder: "Search variables...",
5423
+ className: "h-8 text-xs pl-8"
5424
+ }
5425
+ ),
5426
+ variableSearch && /* @__PURE__ */ jsx(
5427
+ "button",
5428
+ {
5429
+ type: "button",
5430
+ onClick: () => setVariableSearch(""),
5431
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
5432
+ children: /* @__PURE__ */ jsx(X, { size: 12 })
4045
5433
  }
4046
5434
  )
4047
- ] }, v.varName))
5435
+ ] }),
5436
+ filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
5437
+ "No variables match \u201C",
5438
+ variableSearch,
5439
+ "\u201D"
5440
+ ] }),
5441
+ filteredVariables.map((v) => {
5442
+ const isLocked = lockedVarNames.has(v.varName);
5443
+ return /* @__PURE__ */ jsxs("div", { children: [
5444
+ /* @__PURE__ */ jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
5445
+ /* @__PURE__ */ jsx(
5446
+ Input,
5447
+ {
5448
+ id: `gen-${v.varName}`,
5449
+ value: variableValues[v.varName] || "",
5450
+ onChange: (e) => updateVariableValue(v.varName, e.target.value),
5451
+ placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
5452
+ disabled: isLocked,
5453
+ className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
5454
+ }
5455
+ )
5456
+ ] }, v.varName);
5457
+ })
4048
5458
  ] }),
4049
5459
  mode === "bulk" && /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
4050
5460
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
@@ -4233,56 +5643,60 @@ ${allWarnings.join("\n")}`);
4233
5643
  ] }),
4234
5644
  !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
5645
  ] }),
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(
5646
+ 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: [
5647
+ mode === "single" || !hasVariables ? /* @__PURE__ */ jsxs(Fragment, { children: [
5648
+ /* @__PURE__ */ jsxs(
5649
+ Button,
5650
+ {
5651
+ variant: "secondary",
5652
+ onClick: handlePreview,
5653
+ disabled: isGenerating,
5654
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
5655
+ children: [
5656
+ isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
5657
+ isGenerating ? "Generating..." : "Generate PDF"
5658
+ ]
5659
+ }
5660
+ ),
5661
+ /* @__PURE__ */ jsxs(
5662
+ Button,
5663
+ {
5664
+ onClick: handleExportSingle,
5665
+ disabled: !previewFresh || isExporting,
5666
+ 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",
5667
+ children: [
5668
+ isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
5669
+ isExporting ? "Exporting..." : "Export PDF"
5670
+ ]
5671
+ }
5672
+ )
5673
+ ] }) : /* @__PURE__ */ jsxs(
4263
5674
  Button,
4264
5675
  {
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",
5676
+ onClick: handleBulkGenerate,
5677
+ disabled: isExporting || !bulkData || bulkData.length === 0,
5678
+ 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
5679
  children: [
4269
5680
  isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
4270
- isExporting ? "Exporting..." : "Export PDF"
5681
+ isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
4271
5682
  ]
4272
5683
  }
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
- ) })
5684
+ ),
5685
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
5686
+ previewError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
5687
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
5688
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: previewError })
5689
+ ] }),
5690
+ exportError && /* @__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: exportError })
5693
+ ] }),
5694
+ exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
5695
+ /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
5696
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: exportSuccess })
5697
+ ] })
5698
+ ] })
5699
+ ] })
4286
5700
  ] }),
4287
5701
  /* @__PURE__ */ jsx("div", { className: "bg-border" }),
4288
5702
  /* @__PURE__ */ jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsx(
@@ -4460,7 +5874,13 @@ function DocumentGeneratorInner({
4460
5874
  showToolbar = true,
4461
5875
  showGenerateTab = true,
4462
5876
  onGeneratePdf,
4463
- initialBulkData
5877
+ initialBulkData,
5878
+ defaultTab = "editor",
5879
+ initialVariableValues,
5880
+ predefinedVariables,
5881
+ exportFileName,
5882
+ templateMode,
5883
+ onSaveTemplate
4464
5884
  }) {
4465
5885
  const {
4466
5886
  editor,
@@ -4487,7 +5907,7 @@ function DocumentGeneratorInner({
4487
5907
  placeholder,
4488
5908
  onChange
4489
5909
  });
4490
- const [activeTab, setActiveTab] = useState("editor");
5910
+ const [activeTab, setActiveTab] = useState(defaultTab);
4491
5911
  const [insertPopoverOpen, setInsertPopoverOpen] = useState(false);
4492
5912
  const [insertVarPopoverOpen, setInsertVarPopoverOpen] = useState(false);
4493
5913
  const [editFieldId, setEditFieldId] = useState(null);
@@ -4570,6 +5990,21 @@ function DocumentGeneratorInner({
4570
5990
  setExportSuccess("Document exported successfully");
4571
5991
  }
4572
5992
  }, [exportDocument, onExport]);
5993
+ const handleSaveTemplate = useCallback(() => {
5994
+ const md = exportMarkdown();
5995
+ if (!md.trim()) {
5996
+ setMdExportError("Editor content is empty");
5997
+ return;
5998
+ }
5999
+ const validation = validateMarkdown(md);
6000
+ if (!validation.valid) {
6001
+ setMdExportError(validation.errors.join("; "));
6002
+ return;
6003
+ }
6004
+ setMdExportError(null);
6005
+ onSaveTemplate?.(md);
6006
+ setExportSuccess("Template saved");
6007
+ }, [exportMarkdown, onSaveTemplate]);
4573
6008
  const handleExportMarkdown = useCallback(() => {
4574
6009
  const md = exportMarkdown();
4575
6010
  if (!md.trim()) {
@@ -4586,10 +6021,10 @@ function DocumentGeneratorInner({
4586
6021
  const url = URL.createObjectURL(blob);
4587
6022
  const a = document.createElement("a");
4588
6023
  a.href = url;
4589
- a.download = "template.md";
6024
+ a.download = exportFileName ? `${exportFileName}.md` : "template.md";
4590
6025
  a.click();
4591
6026
  URL.revokeObjectURL(url);
4592
- }, [exportMarkdown]);
6027
+ }, [exportMarkdown, exportFileName]);
4593
6028
  useEffect(() => {
4594
6029
  if (mdExportError) setMdExportError(null);
4595
6030
  }, [markdown]);
@@ -4625,6 +6060,7 @@ function DocumentGeneratorInner({
4625
6060
  open: insertVarPopoverOpen,
4626
6061
  onOpenChange: setInsertVarPopoverOpen,
4627
6062
  onInsert: handleInsertVariable,
6063
+ predefinedVariables,
4628
6064
  children: /* @__PURE__ */ jsxs(
4629
6065
  Button,
4630
6066
  {
@@ -4665,118 +6101,141 @@ function DocumentGeneratorInner({
4665
6101
  ] })
4666
6102
  ] })
4667
6103
  ] }),
4668
- /* @__PURE__ */ jsxs("div", { className: cn(
6104
+ /* @__PURE__ */ jsx("div", { className: cn(
4669
6105
  "flex flex-col flex-1 min-h-0",
4670
6106
  activeTab !== "editor" && "hidden"
6107
+ ), children: /* @__PURE__ */ jsxs("div", { className: cn(
6108
+ "flex-1 min-h-0",
6109
+ showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
4671
6110
  ), 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,
6111
+ !editorCollapsed && /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
6112
+ showToolbar && !readOnly && /* @__PURE__ */ jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsx(
6113
+ EditorToolbar,
4697
6114
  {
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"
6115
+ editor,
6116
+ insertFieldButton,
6117
+ insertVariableButton,
6118
+ onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
4712
6119
  }
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,
6120
+ ) }),
6121
+ /* @__PURE__ */ jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsx(
6122
+ EditorContent,
4718
6123
  {
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
- ]
6124
+ editor,
6125
+ className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
4727
6126
  }
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: [
6127
+ ) }),
6128
+ /* @__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
6129
  /* @__PURE__ */ jsxs(
4754
6130
  Button,
4755
6131
  {
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",
6132
+ variant: "secondary",
6133
+ onClick: generatePdf,
6134
+ disabled: isGenerating || !editor,
6135
+ className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
4759
6136
  children: [
4760
- /* @__PURE__ */ jsx(FileText, { size: 16 }),
4761
- "Export MD"
6137
+ isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
6138
+ isGenerating ? "Generating..." : "Generate PDF"
4762
6139
  ]
4763
6140
  }
4764
6141
  ),
4765
- /* @__PURE__ */ jsxs(
6142
+ onSaveTemplate ? /* @__PURE__ */ jsxs(
4766
6143
  Button,
4767
6144
  {
4768
- onClick: handleExport,
4769
- disabled: isGenerating || !editor || pdfPages.length === 0,
6145
+ onClick: handleSaveTemplate,
6146
+ disabled: !editor || !markdown.trim(),
4770
6147
  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
6148
  children: [
4772
- /* @__PURE__ */ jsx(FileDown, { size: 16 }),
4773
- exportButtonText
6149
+ /* @__PURE__ */ jsx(Save, { size: 16 }),
6150
+ exportButtonText || "Save Template"
4774
6151
  ]
4775
6152
  }
4776
- )
6153
+ ) : /* @__PURE__ */ jsxs(Popover, { children: [
6154
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
6155
+ Button,
6156
+ {
6157
+ disabled: !editor || !markdown.trim() && pdfPages.length === 0,
6158
+ 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",
6159
+ children: [
6160
+ /* @__PURE__ */ jsx(FileDown, { size: 16 }),
6161
+ "Export",
6162
+ /* @__PURE__ */ jsx(ChevronDown, { size: 14 })
6163
+ ]
6164
+ }
6165
+ ) }),
6166
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
6167
+ /* @__PURE__ */ jsxs(
6168
+ "button",
6169
+ {
6170
+ onClick: handleExportMarkdown,
6171
+ disabled: !editor || !markdown.trim(),
6172
+ 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",
6173
+ children: [
6174
+ /* @__PURE__ */ jsx(FileText, { size: 16 }),
6175
+ "Export as MD"
6176
+ ]
6177
+ }
6178
+ ),
6179
+ /* @__PURE__ */ jsxs(
6180
+ "button",
6181
+ {
6182
+ onClick: handleExport,
6183
+ disabled: isGenerating || !editor || pdfPages.length === 0,
6184
+ 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",
6185
+ children: [
6186
+ /* @__PURE__ */ jsx(FileDown, { size: 16 }),
6187
+ "Export as PDF"
6188
+ ]
6189
+ }
6190
+ )
6191
+ ] })
6192
+ ] }),
6193
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
6194
+ mdExportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
6195
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6196
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: mdExportError })
6197
+ ] }),
6198
+ generationError && /* @__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: generationError })
6201
+ ] }),
6202
+ fieldWarnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
6203
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
6204
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
6205
+ fieldWarnings.length,
6206
+ " field",
6207
+ fieldWarnings.length !== 1 ? "s" : "",
6208
+ " had warnings during generation"
6209
+ ] })
6210
+ ] }),
6211
+ exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
6212
+ /* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
6213
+ /* @__PURE__ */ jsx("span", { children: exportSuccess })
6214
+ ] })
6215
+ ] })
4777
6216
  ] })
4778
- ] })
4779
- ] }),
6217
+ ] }),
6218
+ showPreview && !editorCollapsed && /* @__PURE__ */ jsx("div", { className: "bg-border" }),
6219
+ showPreview && /* @__PURE__ */ jsx(
6220
+ PreviewPanel,
6221
+ {
6222
+ pages: pdfPages,
6223
+ isGenerating,
6224
+ positionedFields,
6225
+ headerLeft: editorCollapsed ? /* @__PURE__ */ jsx(
6226
+ Button,
6227
+ {
6228
+ variant: "ghost",
6229
+ size: "sm",
6230
+ className: "h-8 w-8 p-0",
6231
+ onClick: () => setEditorCollapsed(false),
6232
+ children: /* @__PURE__ */ jsx(PanelLeftOpen, { size: 14 })
6233
+ }
6234
+ ) : void 0,
6235
+ className: "flex-1 border-0 rounded-none"
6236
+ }
6237
+ )
6238
+ ] }) }),
4780
6239
  showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsx(
4781
6240
  GeneratePanel,
4782
6241
  {
@@ -4784,7 +6243,10 @@ function DocumentGeneratorInner({
4784
6243
  editorContent: editor?.getJSON(),
4785
6244
  editorVariables: variables,
4786
6245
  onGeneratePdf,
4787
- initialBulkData
6246
+ initialBulkData,
6247
+ initialVariableValues,
6248
+ exportFileName,
6249
+ templateMode
4788
6250
  }
4789
6251
  ),
4790
6252
  /* @__PURE__ */ jsx(
@@ -4925,6 +6387,717 @@ function EditorPanel({
4925
6387
  ] });
4926
6388
  }
4927
6389
 
4928
- export { DocumentGenerator, EditorPanel, FormFieldType, PreviewPanel, ThemeProvider, useDocumentGenerator, useTheme };
6390
+ // src/utils/xml-template-parser.ts
6391
+ function extractFieldRefs(expr) {
6392
+ const refs = [];
6393
+ const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
6394
+ let m;
6395
+ while ((m = re.exec(expr)) !== null) {
6396
+ if (m[2]) {
6397
+ refs.push(`${m[1]}.${m[2]}`);
6398
+ } else {
6399
+ refs.push(m[1]);
6400
+ }
6401
+ }
6402
+ return refs;
6403
+ }
6404
+ function fieldRefToVarName(ref, bandContext) {
6405
+ if (ref.includes(".")) {
6406
+ return labelToVarName(ref.replace(/\./g, "_"));
6407
+ }
6408
+ if (bandContext) {
6409
+ return labelToVarName(bandContext + "_" + ref);
6410
+ }
6411
+ return labelToVarName(ref);
6412
+ }
6413
+ function fieldRefToLabel(ref) {
6414
+ const parts = ref.split(".");
6415
+ const raw = parts[parts.length - 1];
6416
+ return raw.replace(/_/g, " ");
6417
+ }
6418
+ function resolveExpression(expr, registry, siblingLabel, bandContext) {
6419
+ const trimmed = expr.trim();
6420
+ const suppressZero = /<>\s*0/.test(trimmed);
6421
+ const szOpts = suppressZero ? { suppressZero: true } : void 0;
6422
+ const aggMatch = trimmed.match(
6423
+ /^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
6424
+ );
6425
+ if (aggMatch) {
6426
+ const fieldName = aggMatch[2];
6427
+ const varName = labelToVarName("workorder_" + fieldName);
6428
+ const label = fieldName.replace(/_/g, " ");
6429
+ return registry.register(varName, label, szOpts);
6430
+ }
6431
+ const concatSingle = trimmed.match(
6432
+ /^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6433
+ );
6434
+ if (concatSingle) {
6435
+ const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
6436
+ const varName = fieldRefToVarName(ref, bandContext);
6437
+ const label = fieldRefToLabel(ref);
6438
+ return registry.register(varName, label, szOpts);
6439
+ }
6440
+ const concatMulti = trimmed.match(
6441
+ /^concat\(/i
6442
+ );
6443
+ if (concatMulti) {
6444
+ const refs = extractFieldRefs(trimmed);
6445
+ if (refs.length > 0) {
6446
+ const uniqueRefs = [...new Set(refs)];
6447
+ const tokens = uniqueRefs.map((ref) => {
6448
+ const varName = fieldRefToVarName(ref, bandContext);
6449
+ const label = fieldRefToLabel(ref);
6450
+ return registry.register(varName, label, szOpts);
6451
+ });
6452
+ return tokens.join(" ");
6453
+ }
6454
+ }
6455
+ const toLongMatch = trimmed.match(
6456
+ /^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
6457
+ );
6458
+ if (toLongMatch) {
6459
+ const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
6460
+ const varName = fieldRefToVarName(ref, bandContext);
6461
+ const label = fieldRefToLabel(ref);
6462
+ return registry.register(varName, label, szOpts);
6463
+ }
6464
+ const fmtMatch = trimmed.match(
6465
+ /^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
6466
+ );
6467
+ if (fmtMatch) {
6468
+ return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
6469
+ }
6470
+ const replaceMatch = trimmed.match(
6471
+ /^Replace\(\s*\[([^\]]+)\]/i
6472
+ );
6473
+ if (replaceMatch) {
6474
+ const ref = replaceMatch[1];
6475
+ const varName = fieldRefToVarName(ref, bandContext);
6476
+ const label = fieldRefToLabel(ref);
6477
+ return registry.register(varName, label, szOpts);
6478
+ }
6479
+ const iifLabelMatch = trimmed.match(
6480
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
6481
+ );
6482
+ if (iifLabelMatch) {
6483
+ return iifLabelMatch[1];
6484
+ }
6485
+ const iifLabelElse = trimmed.match(
6486
+ /^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
6487
+ );
6488
+ if (iifLabelElse) {
6489
+ return iifLabelElse[1];
6490
+ }
6491
+ const iifEmptyElse = trimmed.match(
6492
+ /^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
6493
+ );
6494
+ if (iifEmptyElse) {
6495
+ return iifEmptyElse[1];
6496
+ }
6497
+ const nestedIifStaticLabel = trimmed.match(
6498
+ /^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
6499
+ );
6500
+ if (nestedIifStaticLabel) {
6501
+ return nestedIifStaticLabel[1];
6502
+ }
6503
+ const iifValueMatch = trimmed.match(
6504
+ /^iif\s*\(/i
6505
+ );
6506
+ if (iifValueMatch) {
6507
+ const refs = extractFieldRefs(trimmed);
6508
+ if (refs.length > 0) {
6509
+ const uniqueRefs = [...new Set(refs)];
6510
+ const innerAggMatch = trimmed.match(
6511
+ /\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
6512
+ );
6513
+ if (innerAggMatch) {
6514
+ const fieldName = innerAggMatch[2];
6515
+ const varName2 = labelToVarName("workorder_" + fieldName);
6516
+ const label2 = fieldName.replace(/_/g, " ");
6517
+ return registry.register(varName2, label2, szOpts);
6518
+ }
6519
+ const ref = uniqueRefs[uniqueRefs.length - 1];
6520
+ const varName = fieldRefToVarName(ref, bandContext);
6521
+ const label = fieldRefToLabel(ref);
6522
+ return registry.register(varName, label, szOpts);
6523
+ }
6524
+ const staticMatch = trimmed.match(/'([^']+)'/);
6525
+ if (staticMatch) return staticMatch[1];
6526
+ return "";
6527
+ }
6528
+ const arithmeticRefs = extractFieldRefs(trimmed);
6529
+ if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
6530
+ if (siblingLabel) {
6531
+ const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
6532
+ const varName = labelToVarName(
6533
+ (bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
6534
+ );
6535
+ return registry.register(varName, cleanLabel, szOpts);
6536
+ }
6537
+ const uniqueRefs = [...new Set(arithmeticRefs)];
6538
+ const tokens = uniqueRefs.map((ref) => {
6539
+ const varName = fieldRefToVarName(ref, bandContext);
6540
+ const label = fieldRefToLabel(ref);
6541
+ return registry.register(varName, label, szOpts);
6542
+ });
6543
+ return tokens.join(" ");
6544
+ }
6545
+ const simpleMatch = trimmed.match(
6546
+ /^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
6547
+ );
6548
+ if (simpleMatch) {
6549
+ const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
6550
+ const varName = fieldRefToVarName(ref, bandContext);
6551
+ const label = fieldRefToLabel(ref);
6552
+ return registry.register(varName, label, szOpts);
6553
+ }
6554
+ if (arithmeticRefs.length >= 1) {
6555
+ const ref = arithmeticRefs[arithmeticRefs.length - 1];
6556
+ const varName = fieldRefToVarName(ref, bandContext);
6557
+ const label = fieldRefToLabel(ref);
6558
+ return registry.register(varName, label, szOpts);
6559
+ }
6560
+ return "";
6561
+ }
6562
+ var VariableRegistry = class {
6563
+ constructor() {
6564
+ __publicField(this, "map", /* @__PURE__ */ new Map());
6565
+ }
6566
+ register(varName, varLabel, options) {
6567
+ if (!this.map.has(varName)) {
6568
+ this.map.set(varName, { varName, varLabel, varDefault: "" });
6569
+ }
6570
+ const suppress = options?.suppressZero ? "|suppress:zero" : "";
6571
+ return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
6572
+ }
6573
+ getAll() {
6574
+ return Array.from(this.map.values());
6575
+ }
6576
+ };
6577
+ function parseLocationFloat(s) {
6578
+ if (!s) return { x: 0, y: 0 };
6579
+ const parts = s.split(",");
6580
+ return {
6581
+ x: parseFloat(parts[0]) || 0,
6582
+ y: parseFloat(parts[1]) || 0
6583
+ };
6584
+ }
6585
+ function parseSizeF(s) {
6586
+ if (!s) return { w: 0, h: 0 };
6587
+ const parts = s.split(",");
6588
+ return {
6589
+ w: parseFloat(parts[0]) || 0,
6590
+ h: parseFloat(parts[1]) || 0
6591
+ };
6592
+ }
6593
+ function extractControls(bandEl) {
6594
+ const controlsEl = bandEl.querySelector(":scope > Controls");
6595
+ if (!controlsEl) return [];
6596
+ const items = [];
6597
+ for (const child of Array.from(controlsEl.children)) {
6598
+ items.push(extractControl(child));
6599
+ }
6600
+ return items;
6601
+ }
6602
+ function extractControl(el) {
6603
+ const ctrlType = el.getAttribute("ControlType") || "";
6604
+ const name = el.getAttribute("Name") || "";
6605
+ const text = el.getAttribute("Text") || "";
6606
+ const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
6607
+ const size = parseSizeF(el.getAttribute("SizeF"));
6608
+ const formatString = el.getAttribute("TextFormatString") || null;
6609
+ let expression = null;
6610
+ const bindingsEl = el.querySelector(":scope > ExpressionBindings");
6611
+ if (bindingsEl) {
6612
+ for (const item of Array.from(bindingsEl.children)) {
6613
+ if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
6614
+ expression = item.getAttribute("Expression");
6615
+ break;
6616
+ }
6617
+ }
6618
+ }
6619
+ const children = [];
6620
+ if (ctrlType === "XRPanel") {
6621
+ const panelControls = extractControls(el);
6622
+ children.push(...panelControls);
6623
+ }
6624
+ return {
6625
+ name,
6626
+ type: ctrlType,
6627
+ text,
6628
+ x: loc.x,
6629
+ y: loc.y,
6630
+ width: size.w,
6631
+ expression,
6632
+ formatString,
6633
+ children
6634
+ };
6635
+ }
6636
+ function groupByRow(controls, threshold = 8) {
6637
+ const sorted = [...controls].sort((a, b) => a.y - b.y);
6638
+ const rows = [];
6639
+ for (const ctrl of sorted) {
6640
+ const lastRow = rows[rows.length - 1];
6641
+ if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
6642
+ lastRow.controls.push(ctrl);
6643
+ } else {
6644
+ rows.push({ y: ctrl.y, controls: [ctrl] });
6645
+ }
6646
+ }
6647
+ for (const row of rows) {
6648
+ row.controls.sort((a, b) => a.x - b.x);
6649
+ }
6650
+ return rows;
6651
+ }
6652
+ function isStaticLabel(ctrl) {
6653
+ return ctrl.type === "XRLabel" && !ctrl.expression;
6654
+ }
6655
+ function isBound(ctrl) {
6656
+ return ctrl.type === "XRLabel" && !!ctrl.expression;
6657
+ }
6658
+ function computeSplitPercent(leftControls, rightControls, contentWidth) {
6659
+ const leftExtent = leftControls.reduce(
6660
+ (max, c) => Math.max(max, c.x + c.width),
6661
+ 0
6662
+ );
6663
+ const rightMinX = rightControls.reduce(
6664
+ (min, c) => Math.min(min, c.x),
6665
+ contentWidth
6666
+ );
6667
+ const rightMaxExtent = rightControls.reduce(
6668
+ (max, c) => Math.max(max, c.x + c.width),
6669
+ 0
6670
+ );
6671
+ const rightExtent = rightMaxExtent - rightMinX;
6672
+ if (leftExtent + rightExtent === 0) return 50;
6673
+ return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
6674
+ }
6675
+ function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
6676
+ const lines = [];
6677
+ const leftControls = controls.filter((c) => c.x < 437);
6678
+ const rightControls = controls.filter((c) => c.x >= 437);
6679
+ const leftEntries = [];
6680
+ if (leftControls.length > 0) {
6681
+ const leftRows = groupByRow(leftControls);
6682
+ for (const row of leftRows) {
6683
+ const parts = [];
6684
+ for (const ctrl of row.controls) {
6685
+ if (isBound(ctrl)) {
6686
+ let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6687
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
6688
+ resolved = resolved.replace("}}", "|format:phone}}");
6689
+ }
6690
+ if (resolved) parts.push(resolved);
6691
+ } else if (isStaticLabel(ctrl)) {
6692
+ const text = ctrl.text.trim();
6693
+ if (text && !text.match(/^label\d+$/)) {
6694
+ parts.push(text);
6695
+ }
6696
+ }
6697
+ }
6698
+ if (parts.length > 0) {
6699
+ leftEntries.push(parts.join(" "));
6700
+ }
6701
+ }
6702
+ }
6703
+ const rightEntries = [];
6704
+ if (rightControls.length > 0) {
6705
+ const rightRows = groupByRow(rightControls);
6706
+ for (const row of rightRows) {
6707
+ let label = "";
6708
+ let value = "";
6709
+ for (const ctrl of row.controls) {
6710
+ if (isStaticLabel(ctrl)) {
6711
+ const text = ctrl.text.trim();
6712
+ if (text && !text.match(/^label\d+$/)) {
6713
+ label = text;
6714
+ }
6715
+ } else if (isBound(ctrl)) {
6716
+ value = resolveExpression(ctrl.expression, registry, label, "workorder");
6717
+ if (ctrl.expression) {
6718
+ const refs = extractFieldRefs(ctrl.expression);
6719
+ if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
6720
+ value = value.replace("}}", "|suppress:zero}}");
6721
+ }
6722
+ }
6723
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
6724
+ value = value.replace("}}", "|format:phone}}");
6725
+ }
6726
+ }
6727
+ }
6728
+ if (label || value) {
6729
+ rightEntries.push({ label, value, y: row.y });
6730
+ }
6731
+ }
6732
+ }
6733
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
6734
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
6735
+ lines.push(`:::columns{split:${outerSplit}}`);
6736
+ const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
6737
+ const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
6738
+ const xmlLineHeight = 17;
6739
+ const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
6740
+ const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
6741
+ const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
6742
+ lines.push(leftColTag);
6743
+ for (const entry of leftEntries) {
6744
+ lines.push(entry);
6745
+ }
6746
+ lines.push(":::");
6747
+ const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
6748
+ lines.push(rightColTag);
6749
+ if (rightEntries.length > 0) {
6750
+ lines.push("| | |");
6751
+ lines.push("|---|---|");
6752
+ let prevY = -Infinity;
6753
+ for (const entry of rightEntries) {
6754
+ if (prevY > -Infinity && entry.y - prevY > 25) {
6755
+ lines.push("| | |");
6756
+ }
6757
+ const label = entry.label || "";
6758
+ const colon = entry.label ? ":" : "";
6759
+ const value = entry.value ? ` ${entry.value}` : "";
6760
+ lines.push(`| ${label} | ${colon}${value} |`);
6761
+ prevY = entry.y;
6762
+ }
6763
+ }
6764
+ lines.push(":::");
6765
+ lines.push(":::");
6766
+ lines.push("");
6767
+ }
6768
+ return lines.join("\n");
6769
+ }
6770
+ function renderReportHeaderBand(controls, registry, contentWidth) {
6771
+ const lines = [];
6772
+ const leftControls = controls.filter((c) => c.x < 437);
6773
+ const rightControls = controls.filter((c) => c.x >= 437);
6774
+ const leftEntries = [];
6775
+ if (leftControls.length > 0) {
6776
+ const leftRows = groupByRow(leftControls);
6777
+ for (const row of leftRows) {
6778
+ const parts = [];
6779
+ for (const ctrl of row.controls) {
6780
+ if (isBound(ctrl)) {
6781
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6782
+ if (resolved) parts.push(resolved);
6783
+ } else if (isStaticLabel(ctrl)) {
6784
+ const text = ctrl.text.trim();
6785
+ if (text && !text.match(/^label\d+$/)) {
6786
+ parts.push(text);
6787
+ }
6788
+ }
6789
+ }
6790
+ if (parts.length > 0) {
6791
+ leftEntries.push(parts.join(" "));
6792
+ }
6793
+ }
6794
+ }
6795
+ const rightEntries = [];
6796
+ if (rightControls.length > 0) {
6797
+ const rightRows = groupByRow(rightControls);
6798
+ for (const row of rightRows) {
6799
+ let label = "";
6800
+ let value = "";
6801
+ for (const ctrl of row.controls) {
6802
+ if (isStaticLabel(ctrl)) {
6803
+ const text = ctrl.text.trim();
6804
+ if (text && !text.match(/^label\d+$/)) {
6805
+ label = text;
6806
+ }
6807
+ } else if (isBound(ctrl)) {
6808
+ value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
6809
+ if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
6810
+ value = value.replace("}}", "|format:phone}}");
6811
+ }
6812
+ }
6813
+ }
6814
+ if (label || value) {
6815
+ rightEntries.push({ label, value });
6816
+ }
6817
+ }
6818
+ }
6819
+ if (leftEntries.length > 0 || rightEntries.length > 0) {
6820
+ const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
6821
+ lines.push(`:::columns{split:${outerSplit}}`);
6822
+ lines.push(":::col");
6823
+ for (const entry of leftEntries) {
6824
+ lines.push(entry);
6825
+ }
6826
+ lines.push(":::");
6827
+ lines.push(":::col");
6828
+ lines.push("| | |");
6829
+ lines.push("|---|---|");
6830
+ for (const entry of rightEntries) {
6831
+ const label = entry.label || "";
6832
+ const value = entry.value ? ` ${entry.value}` : "";
6833
+ lines.push(`| ${label} |${value} |`);
6834
+ }
6835
+ lines.push(":::");
6836
+ lines.push(":::");
6837
+ lines.push("");
6838
+ }
6839
+ return lines.join("\n");
6840
+ }
6841
+ function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
6842
+ const lines = [];
6843
+ const headerTexts = [];
6844
+ for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
6845
+ if (isStaticLabel(ctrl)) {
6846
+ const text = ctrl.text.trim();
6847
+ if (text) headerTexts.push(text);
6848
+ }
6849
+ }
6850
+ const titleText = headerTexts.join(" ");
6851
+ lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
6852
+ lines.push(":::");
6853
+ lines.push(":::repeat{data:operations}");
6854
+ const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
6855
+ const detailParts = [];
6856
+ for (const ctrl of sortedDetail) {
6857
+ if (isBound(ctrl)) {
6858
+ const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
6859
+ if (resolved) detailParts.push(resolved);
6860
+ }
6861
+ }
6862
+ if (detailParts.length > 0) {
6863
+ if (detailParts.length >= 2) {
6864
+ lines.push(":::columns{split:12}");
6865
+ lines.push(":::col");
6866
+ lines.push(`**${detailParts[0]}**`);
6867
+ lines.push(":::");
6868
+ lines.push(":::col");
6869
+ lines.push(`**${detailParts.slice(1).join(" ")}**`);
6870
+ lines.push(":::");
6871
+ lines.push(":::");
6872
+ } else {
6873
+ lines.push(`**${detailParts[0]}**`);
6874
+ }
6875
+ lines.push("");
6876
+ }
6877
+ if (groupFooterControls.length > 0) {
6878
+ const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
6879
+ if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
6880
+ }
6881
+ lines.push("---");
6882
+ lines.push(":::");
6883
+ lines.push("");
6884
+ return lines.join("\n");
6885
+ }
6886
+ function renderGroupFooterBand(controls, registry, contentWidth) {
6887
+ const rows = groupByRow(controls);
6888
+ const entries = [];
6889
+ for (const row of rows) {
6890
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
6891
+ if (labels.length === 0) continue;
6892
+ let label = "";
6893
+ let value = "";
6894
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
6895
+ for (const ctrl of sorted) {
6896
+ if (isStaticLabel(ctrl)) {
6897
+ const text = ctrl.text.trim();
6898
+ if (text && !text.match(/^label\d+$/)) {
6899
+ label = text;
6900
+ }
6901
+ } else if (isBound(ctrl)) {
6902
+ const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
6903
+ if (resolved) {
6904
+ if (!resolved.startsWith("{{var|")) {
6905
+ if (!label) label = resolved;
6906
+ } else {
6907
+ value = resolved;
6908
+ }
6909
+ }
6910
+ }
6911
+ }
6912
+ if (label && value) {
6913
+ entries.push({ label, value });
6914
+ }
6915
+ }
6916
+ if (entries.length === 0) return "";
6917
+ const lines = [];
6918
+ lines.push(":::subtotals");
6919
+ for (const entry of entries) {
6920
+ lines.push(`**${entry.label}** ${entry.value}`);
6921
+ }
6922
+ lines.push(":::");
6923
+ lines.push("");
6924
+ return lines.join("\n");
6925
+ }
6926
+ function renderReportFooterBand(controls, registry) {
6927
+ const lines = [];
6928
+ const panel = controls.find((c) => c.type === "XRPanel");
6929
+ const thankYouLabel = controls.find(
6930
+ (c) => c.type === "XRLabel" && c.text.includes("Thank You")
6931
+ );
6932
+ const priceRows = [];
6933
+ if (panel && panel.children.length > 0) {
6934
+ const rows = groupByRow(panel.children, 15);
6935
+ for (const row of rows) {
6936
+ const labels = row.controls.filter((c) => c.type === "XRLabel");
6937
+ if (labels.length === 0) continue;
6938
+ let label = "";
6939
+ let value = "";
6940
+ const sorted = [...labels].sort((a, b) => a.x - b.x);
6941
+ for (const ctrl of sorted) {
6942
+ if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
6943
+ continue;
6944
+ }
6945
+ const isLabelPos = ctrl.x < 150;
6946
+ if (isStaticLabel(ctrl)) {
6947
+ const text = ctrl.text.trim();
6948
+ if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
6949
+ label = text;
6950
+ }
6951
+ } else if (isBound(ctrl)) {
6952
+ const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
6953
+ if (resolved) {
6954
+ if (isLabelPos) {
6955
+ if (!label) label = resolved;
6956
+ } else {
6957
+ if (!value) value = resolved;
6958
+ }
6959
+ }
6960
+ }
6961
+ }
6962
+ if (label && value) {
6963
+ priceRows.push({ label, value });
6964
+ } else if (value) {
6965
+ priceRows.push({ label: "", value });
6966
+ }
6967
+ }
6968
+ }
6969
+ lines.push(":::columns{split:40|padX:20}");
6970
+ lines.push(":::col{padTop:1}");
6971
+ if (thankYouLabel) {
6972
+ lines.push(`## ***${thankYouLabel.text.trim()}***`);
6973
+ }
6974
+ lines.push(":::");
6975
+ lines.push(":::col{padTop:1}");
6976
+ lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
6977
+ if (priceRows.length > 0) {
6978
+ lines.push(":::subtotals");
6979
+ for (const row of priceRows) {
6980
+ const label = row.label ? `**${row.label}**` : "";
6981
+ lines.push(`${label} ${row.value}`);
6982
+ }
6983
+ lines.push(":::");
6984
+ }
6985
+ lines.push(":::");
6986
+ lines.push(":::");
6987
+ lines.push(":::");
6988
+ lines.push("");
6989
+ return lines.join("\n");
6990
+ }
6991
+ function parseXmlTemplate(xmlString) {
6992
+ const warnings = [];
6993
+ const registry = new VariableRegistry();
6994
+ const parser = new DOMParser();
6995
+ const doc = parser.parseFromString(xmlString, "text/xml");
6996
+ const parseError = doc.querySelector("parsererror");
6997
+ if (parseError) {
6998
+ return {
6999
+ markdown: "",
7000
+ variables: [],
7001
+ reportName: "",
7002
+ warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
7003
+ };
7004
+ }
7005
+ const root = doc.documentElement;
7006
+ const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
7007
+ const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
7008
+ const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
7009
+ const marginParts = marginsStr.split(",").map(Number);
7010
+ const leftMargin = marginParts[0] || 50;
7011
+ const rightMargin = marginParts[1] || 50;
7012
+ const contentWidth = pageWidth - leftMargin - rightMargin;
7013
+ const calculatedFieldNames = /* @__PURE__ */ new Set();
7014
+ const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
7015
+ if (calcFieldsEl) {
7016
+ for (const item of Array.from(calcFieldsEl.children)) {
7017
+ const name = item.getAttribute("Name");
7018
+ if (name) calculatedFieldNames.add(name);
7019
+ }
7020
+ }
7021
+ const markdownSections = [];
7022
+ const watermarkEl = root.querySelector("Watermarks > Item1");
7023
+ if (watermarkEl) {
7024
+ const wmText = watermarkEl.getAttribute("Text");
7025
+ if (wmText) {
7026
+ markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
7027
+ markdownSections.push("");
7028
+ }
7029
+ }
7030
+ const bandsEl = root.querySelector(":scope > Bands");
7031
+ if (!bandsEl) {
7032
+ warnings.push("No <Bands> element found in the XML");
7033
+ return {
7034
+ markdown: "",
7035
+ variables: registry.getAll(),
7036
+ reportName,
7037
+ warnings
7038
+ };
7039
+ }
7040
+ let topMarginControls = [];
7041
+ let reportHeaderControls = [];
7042
+ let pageHeaderControls = [];
7043
+ let detailControls = [];
7044
+ let groupFooterControls = [];
7045
+ let reportFooterControls = [];
7046
+ for (const bandItem of Array.from(bandsEl.children)) {
7047
+ const ctrlType = bandItem.getAttribute("ControlType") || "";
7048
+ if (ctrlType === "TopMarginBand") {
7049
+ topMarginControls = extractControls(bandItem);
7050
+ } else if (ctrlType === "ReportHeaderBand") {
7051
+ reportHeaderControls = extractControls(bandItem);
7052
+ } else if (ctrlType === "PageHeaderBand") {
7053
+ pageHeaderControls = extractControls(bandItem);
7054
+ } else if (ctrlType === "DetailReportBand") {
7055
+ const nestedBands = bandItem.querySelector(":scope > Bands");
7056
+ if (nestedBands) {
7057
+ for (const nested of Array.from(nestedBands.children)) {
7058
+ const nestedType = nested.getAttribute("ControlType") || "";
7059
+ if (nestedType === "DetailBand") {
7060
+ detailControls = extractControls(nested);
7061
+ } else if (nestedType === "GroupFooterBand") {
7062
+ groupFooterControls = extractControls(nested);
7063
+ }
7064
+ }
7065
+ }
7066
+ } else if (ctrlType === "ReportFooterBand") {
7067
+ reportFooterControls = extractControls(bandItem);
7068
+ }
7069
+ }
7070
+ if (topMarginControls.length > 0) {
7071
+ markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
7072
+ }
7073
+ if (reportHeaderControls.length > 0) {
7074
+ markdownSections.push(
7075
+ renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
7076
+ );
7077
+ }
7078
+ if (pageHeaderControls.length > 0 || detailControls.length > 0) {
7079
+ markdownSections.push(
7080
+ renderOperationsSection(
7081
+ pageHeaderControls,
7082
+ detailControls,
7083
+ groupFooterControls,
7084
+ registry)
7085
+ );
7086
+ }
7087
+ if (reportFooterControls.length > 0) {
7088
+ markdownSections.push(
7089
+ renderReportFooterBand(reportFooterControls, registry)
7090
+ );
7091
+ }
7092
+ const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
7093
+ return {
7094
+ markdown,
7095
+ variables: registry.getAll(),
7096
+ reportName,
7097
+ warnings
7098
+ };
7099
+ }
7100
+
7101
+ export { DocumentGenerator, EditorPanel, FormFieldType, PreviewPanel, RepeatNode, SubtotalsNode, ThemeProvider, expandRepeatContent, extractVariablesFromContent, generatePdfFromMarkdown, generatePdfFromTiptap, isZeroLike, markdownToTiptap, parseXmlTemplate, suppressZeroContent, tiptapToMarkdown, useDocumentGenerator, useTheme };
4929
7102
  //# sourceMappingURL=index.mjs.map
4930
7103
  //# sourceMappingURL=index.mjs.map