@signiphi/pdf-compose 0.1.0-beta.8 → 0.1.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,14 +112,21 @@ Fields and variables are embedded as inline tokens in the markdown content:
112
112
  {{var|name:company_name|label:Company Name|default:Acme Corp}}
113
113
  ```
114
114
 
115
- | Attribute | Description |
116
- | --------- | --------------------- |
117
- | `name` | Variable identifier |
118
- | `label` | Display label |
119
- | `default` | Default value |
115
+ | Attribute | Description |
116
+ | ---------- | --------------------- |
117
+ | `name` | Variable identifier |
118
+ | `label` | Display label |
119
+ | `default` | Default value |
120
+ | `suppress` | `zero` — remove the containing row/paragraph when the value is zero-like |
121
+ | `format` | Named value format (e.g. `phone`) |
122
+ | `block` | `true` — parse the value as extended markdown and splice it in as block content (headings, tables, lists) |
120
123
 
121
124
  Variables are replaced with actual values at PDF generation time. In bulk mode, each row of data produces a separate PDF.
122
125
 
126
+ #### Block variables
127
+
128
+ A variable that is the **sole content of its paragraph** can expand into full block content: the resolved value is parsed as extended markdown and the resulting blocks (headings, tables, lists, panels) replace the paragraph. This happens when the token carries `block:true`, or automatically when the resolved value contains a newline. An empty block value removes the paragraph entirely, so optional sections collapse without leaving a gap. Variables mixed inline with other text always substitute as plain text.
129
+
123
130
  ## Advanced Usage
124
131
 
125
132
  ### Using the Hook Directly
@@ -1 +1 @@
1
- {"version":3,"file":"GeneratePanel.d.ts","sourceRoot":"","sources":["../../src/components/GeneratePanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,yBAAyB,EAA0E,MAAM,UAAU,CAAC;AAelI,UAAU,kBAAkB;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,eAAe,EAAE,yBAAyB,EAAE,CAAC;IAC7C,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC3C,qBAAqB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AA4DD,wBAAgB,aAAa,CAAC,EAC5B,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,YAAY,GACb,EAAE,kBAAkB,2CA6+BpB"}
1
+ {"version":3,"file":"GeneratePanel.d.ts","sourceRoot":"","sources":["../../src/components/GeneratePanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,yBAAyB,EAA0E,MAAM,UAAU,CAAC;AAelI,UAAU,kBAAkB;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,eAAe,EAAE,yBAAyB,EAAE,CAAC;IAC7C,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC3C,qBAAqB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AA4DD,wBAAgB,aAAa,CAAC,EAC5B,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,YAAY,GACb,EAAE,kBAAkB,2CAg/BpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"variable-node.d.ts","sourceRoot":"","sources":["../../src/extensions/variable-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAIrD,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,YAAY,EAAE;YACZ,cAAc,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,UAAU,CAAC;YAC9D,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,sBAAsB,CAAC,KAAK,UAAU,CAAC;YACxF,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,CAAC;SACjD,CAAC;KACH;CACF;AAED,eAAO,MAAM,YAAY,gBAgGvB,CAAC"}
1
+ {"version":3,"file":"variable-node.d.ts","sourceRoot":"","sources":["../../src/extensions/variable-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAIrD,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,YAAY,EAAE;YACZ,cAAc,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,UAAU,CAAC;YAC9D,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,sBAAsB,CAAC,KAAK,UAAU,CAAC;YACxF,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,CAAC;SACjD,CAAC;KACH;CACF;AAED,eAAO,MAAM,YAAY,gBAiGvB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { PreviewPanel } from './components/PreviewPanel';
4
4
  export { useDocumentGenerator } from './hooks/useDocumentGenerator';
5
5
  export type { Acknowledgement, DocumentGeneratorField, DocumentGeneratorVariable, DocumentTemplate, VariableValues, GeneratedDocument, DocumentGeneratorExportResult, FormFieldForExport, DocumentGeneratorProps, PredefinedVariable, ThemeMode, ThemeConfig, PdfPage, } from './types';
6
6
  export { FormFieldType } from './types';
7
- export { suppressZeroContent, isZeroLike, expandRepeatContent, extractVariablesFromContent } from './utils/variable-helpers';
7
+ export { suppressZeroContent, isZeroLike, expandRepeatContent, expandBlockVariables, extractVariablesFromContent } from './utils/variable-helpers';
8
8
  export { generatePdfFromMarkdown, generatePdfFromTiptap } from './utils/template-pipeline';
9
9
  export type { HeadlessPdfResult, HeadlessPdfOptions } from './utils/template-pipeline';
10
10
  export type { PdfTheme, PdfPageHeader, PdfPageFooter } from './utils/pdf-generator';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGnE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,yBAAyB,EACzB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,6BAA6B,EAC7B,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,OAAO,GACR,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAG7H,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACvF,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAG1E,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGnE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,yBAAyB,EACzB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,6BAA6B,EAC7B,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,OAAO,GACR,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAGnJ,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACvF,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAG1E,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -253,7 +253,8 @@ var VariableNode = core.Node.create({
253
253
  varLabel: { default: "" },
254
254
  varDefault: { default: "" },
255
255
  format: { default: "" },
256
- suppressZero: { default: "" }
256
+ suppressZero: { default: "" },
257
+ block: { default: "" }
257
258
  };
258
259
  },
259
260
  parseHTML() {
@@ -560,187 +561,6 @@ function extractFieldsFromContent(content) {
560
561
  }
561
562
  return fields;
562
563
  }
563
-
564
- // src/utils/variable-helpers.ts
565
- function extractVariablesFromContent(content) {
566
- const seen = /* @__PURE__ */ new Map();
567
- function walk(node) {
568
- if (node.type === "variableNode" && node.attrs) {
569
- const varName = node.attrs.varName;
570
- if (varName && !seen.has(varName)) {
571
- seen.set(varName, {
572
- varName,
573
- varLabel: node.attrs.varLabel || varName,
574
- varDefault: node.attrs.varDefault || ""
575
- });
576
- }
577
- }
578
- if (node.content) {
579
- node.content.forEach(walk);
580
- }
581
- }
582
- if (content.content) {
583
- content.content.forEach(walk);
584
- }
585
- return Array.from(seen.values());
586
- }
587
- function applyFormat(value, format) {
588
- if (format === "phone") {
589
- const digits = value.replace(/\D/g, "");
590
- if (digits.length === 10) {
591
- return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
592
- }
593
- }
594
- return value;
595
- }
596
- function replaceVariablesInContent(content, values) {
597
- function walkNode(node) {
598
- if (node.type === "variableNode" && node.attrs) {
599
- const varName = node.attrs.varName;
600
- let replacement = values[varName] || node.attrs.varDefault || "";
601
- const format = node.attrs.format;
602
- if (format && replacement) {
603
- replacement = applyFormat(replacement, format);
604
- }
605
- const textNode = { type: "text", text: replacement };
606
- if (node.marks && node.marks.length > 0) {
607
- textNode.marks = node.marks;
608
- }
609
- return textNode;
610
- }
611
- if (node.type === "imageNode" && node.attrs?.varName) {
612
- const varName = node.attrs.varName;
613
- const resolved = values[varName] || node.attrs.src || "";
614
- return { ...node, attrs: { ...node.attrs, src: resolved } };
615
- }
616
- if (node.content) {
617
- return { ...node, content: node.content.map(walkNode) };
618
- }
619
- return node;
620
- }
621
- return walkNode(content);
622
- }
623
- function labelToVarName(label) {
624
- return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
625
- }
626
- function isZeroLike(value) {
627
- if (!value || value.trim() === "") return true;
628
- const cleaned = value.replace(/[$,\s]/g, "");
629
- return /^-?0+(\.0+)?$/.test(cleaned);
630
- }
631
- function suppressZeroContent(content, values) {
632
- function hasSuppressedVar(node) {
633
- if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
634
- const varName = node.attrs.varName;
635
- const val = values[varName] || node.attrs.varDefault || "";
636
- return isZeroLike(val);
637
- }
638
- return (node.content || []).some(hasSuppressedVar);
639
- }
640
- function walkNode(node) {
641
- if (node.type === "table" && node.content) {
642
- const filtered = node.content.filter((row) => {
643
- if (row.type !== "tableRow") return true;
644
- const isHeader = row.content?.[0]?.type === "tableHeader";
645
- if (isHeader) return true;
646
- return !hasSuppressedVar(row);
647
- });
648
- const bodyRows = filtered.filter(
649
- (r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
650
- );
651
- if (bodyRows.length === 0) return null;
652
- return { ...node, content: filtered };
653
- }
654
- if (node.type === "paragraph" && hasSuppressedVar(node)) {
655
- return null;
656
- }
657
- if (node.content) {
658
- const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
659
- return { ...node, content: filtered };
660
- }
661
- return node;
662
- }
663
- return walkNode(content) || content;
664
- }
665
- function expandRepeatContent(content, values) {
666
- const enrichedValues = { ...values };
667
- function cloneNode(node, dataKey, index) {
668
- if (node.type === "variableNode" && node.attrs?.varName) {
669
- const oldName = node.attrs.varName;
670
- const bareName = oldName.replace(/^operation_/, "");
671
- const newName = `${dataKey}_${index}_${bareName}`;
672
- return { ...node, attrs: { ...node.attrs, varName: newName } };
673
- }
674
- if (node.content) {
675
- return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
676
- }
677
- return { ...node };
678
- }
679
- function walkNode(node) {
680
- if (node.type === "repeatBlock" && node.attrs?.data) {
681
- const dataKey = node.attrs.data;
682
- const arrayJson = values[dataKey];
683
- if (!arrayJson) return [];
684
- let items;
685
- try {
686
- items = JSON.parse(arrayJson);
687
- } catch {
688
- return [];
689
- }
690
- const sums = /* @__PURE__ */ new Map();
691
- for (const item of items) {
692
- for (const [key, val] of Object.entries(item)) {
693
- const num = parseFloat(val);
694
- if (!isNaN(num)) {
695
- sums.set(key, (sums.get(key) || 0) + num);
696
- }
697
- }
698
- }
699
- const woKeysByNorm = /* @__PURE__ */ new Map();
700
- for (const k of Object.keys(enrichedValues)) {
701
- if (k.startsWith("workorder_")) {
702
- const norm = k.replace(/_/g, "");
703
- woKeysByNorm.set(norm, k);
704
- }
705
- }
706
- for (const [field, total] of sums) {
707
- const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
708
- const directKey = `workorder_${field}`;
709
- enrichedValues[directKey] = formatted;
710
- const norm = `workorder${field}`.replace(/_/g, "");
711
- const existing = woKeysByNorm.get(norm);
712
- if (existing && existing !== directKey) {
713
- enrichedValues[existing] = formatted;
714
- }
715
- }
716
- const expanded = [];
717
- for (let i = 0; i < items.length; i++) {
718
- const item = items[i];
719
- for (const [key, val] of Object.entries(item)) {
720
- enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
721
- }
722
- const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
723
- expanded.push(...cloned);
724
- }
725
- return expanded;
726
- }
727
- if (node.content) {
728
- const newContent = [];
729
- for (const child of node.content) {
730
- const result2 = walkNode(child);
731
- if (Array.isArray(result2)) newContent.push(...result2);
732
- else newContent.push(result2);
733
- }
734
- return { ...node, content: newContent };
735
- }
736
- return node;
737
- }
738
- const result = walkNode(content);
739
- return {
740
- content: Array.isArray(result) ? { ...content, content: result } : result,
741
- values: enrichedValues
742
- };
743
- }
744
564
  var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
745
565
  var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
746
566
  var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
@@ -880,6 +700,9 @@ function parseVariableToken(tokenBody) {
880
700
  case "format":
881
701
  attrs.format = value;
882
702
  break;
703
+ case "block":
704
+ attrs.block = value === "true" ? "true" : "";
705
+ break;
883
706
  case "_marks":
884
707
  attrs._marks = value;
885
708
  break;
@@ -1291,6 +1114,233 @@ function markdownToTiptap(markdown) {
1291
1114
  return { type: "doc", content: blocks };
1292
1115
  }
1293
1116
 
1117
+ // src/utils/variable-helpers.ts
1118
+ function extractVariablesFromContent(content) {
1119
+ const seen = /* @__PURE__ */ new Map();
1120
+ function walk(node) {
1121
+ if (node.type === "variableNode" && node.attrs) {
1122
+ const varName = node.attrs.varName;
1123
+ if (varName && !seen.has(varName)) {
1124
+ seen.set(varName, {
1125
+ varName,
1126
+ varLabel: node.attrs.varLabel || varName,
1127
+ varDefault: node.attrs.varDefault || ""
1128
+ });
1129
+ }
1130
+ }
1131
+ if (node.content) {
1132
+ node.content.forEach(walk);
1133
+ }
1134
+ }
1135
+ if (content.content) {
1136
+ content.content.forEach(walk);
1137
+ }
1138
+ return Array.from(seen.values());
1139
+ }
1140
+ function applyFormat(value, format) {
1141
+ if (format === "phone") {
1142
+ const digits = value.replace(/\D/g, "");
1143
+ if (digits.length === 10) {
1144
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
1145
+ }
1146
+ }
1147
+ return value;
1148
+ }
1149
+ function replaceVariablesInContent(content, values) {
1150
+ function walkNode(node) {
1151
+ if (node.type === "variableNode" && node.attrs) {
1152
+ const varName = node.attrs.varName;
1153
+ let replacement = values[varName] || node.attrs.varDefault || "";
1154
+ const format = node.attrs.format;
1155
+ if (format && replacement) {
1156
+ replacement = applyFormat(replacement, format);
1157
+ }
1158
+ const textNode = { type: "text", text: replacement };
1159
+ if (node.marks && node.marks.length > 0) {
1160
+ textNode.marks = node.marks;
1161
+ }
1162
+ return textNode;
1163
+ }
1164
+ if (node.type === "imageNode" && node.attrs?.varName) {
1165
+ const varName = node.attrs.varName;
1166
+ const resolved = values[varName] || node.attrs.src || "";
1167
+ return { ...node, attrs: { ...node.attrs, src: resolved } };
1168
+ }
1169
+ if (node.content) {
1170
+ return { ...node, content: node.content.map(walkNode) };
1171
+ }
1172
+ return node;
1173
+ }
1174
+ return walkNode(content);
1175
+ }
1176
+ function expandBlockVariables(content, values) {
1177
+ function soleVariableOf(node) {
1178
+ if (node.type !== "paragraph" || !node.content) return null;
1179
+ const meaningful = node.content.filter(
1180
+ (c) => !(c.type === "text" && !(c.text || "").trim())
1181
+ );
1182
+ if (meaningful.length === 1 && meaningful[0].type === "variableNode") {
1183
+ return meaningful[0];
1184
+ }
1185
+ return null;
1186
+ }
1187
+ function stripUnsafeNodes(node) {
1188
+ if (!node.content) return node;
1189
+ return {
1190
+ ...node,
1191
+ content: node.content.filter((c) => c.type !== "fieldNode" && c.type !== "watermark").map(stripUnsafeNodes)
1192
+ };
1193
+ }
1194
+ function isCellSafe(blocks) {
1195
+ return blocks.every((b) => b.type === "paragraph" || b.type === "heading");
1196
+ }
1197
+ function walkNode(node, inCell) {
1198
+ if (!node.content) return node;
1199
+ const childInCell = inCell || node.type === "tableCell" || node.type === "tableHeader";
1200
+ const newContent = [];
1201
+ for (const child of node.content) {
1202
+ const varNode = soleVariableOf(child);
1203
+ if (varNode?.attrs) {
1204
+ const varName = varNode.attrs.varName;
1205
+ const value = values[varName] || varNode.attrs.varDefault || "";
1206
+ const isBlock = varNode.attrs.block === "true" || value.includes("\n");
1207
+ if (isBlock) {
1208
+ const parsed = stripUnsafeNodes(markdownToTiptap(value));
1209
+ const blocks = parsed.content || [];
1210
+ if (!childInCell || isCellSafe(blocks)) {
1211
+ newContent.push(...blocks);
1212
+ continue;
1213
+ }
1214
+ }
1215
+ }
1216
+ newContent.push(walkNode(child, childInCell));
1217
+ }
1218
+ return { ...node, content: newContent };
1219
+ }
1220
+ return walkNode(content, false);
1221
+ }
1222
+ function labelToVarName(label) {
1223
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1224
+ }
1225
+ function isZeroLike(value) {
1226
+ if (!value || value.trim() === "") return true;
1227
+ const cleaned = value.replace(/[$,\s]/g, "");
1228
+ return /^-?0+(\.0+)?$/.test(cleaned);
1229
+ }
1230
+ function suppressZeroContent(content, values) {
1231
+ function hasSuppressedVar(node) {
1232
+ if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
1233
+ const varName = node.attrs.varName;
1234
+ const val = values[varName] || node.attrs.varDefault || "";
1235
+ return isZeroLike(val);
1236
+ }
1237
+ return (node.content || []).some(hasSuppressedVar);
1238
+ }
1239
+ function walkNode(node) {
1240
+ if (node.type === "table" && node.content) {
1241
+ const filtered = node.content.filter((row) => {
1242
+ if (row.type !== "tableRow") return true;
1243
+ const isHeader = row.content?.[0]?.type === "tableHeader";
1244
+ if (isHeader) return true;
1245
+ return !hasSuppressedVar(row);
1246
+ });
1247
+ const bodyRows = filtered.filter(
1248
+ (r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
1249
+ );
1250
+ if (bodyRows.length === 0) return null;
1251
+ return { ...node, content: filtered };
1252
+ }
1253
+ if (node.type === "paragraph" && hasSuppressedVar(node)) {
1254
+ return null;
1255
+ }
1256
+ if (node.content) {
1257
+ const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
1258
+ return { ...node, content: filtered };
1259
+ }
1260
+ return node;
1261
+ }
1262
+ return walkNode(content) || content;
1263
+ }
1264
+ function expandRepeatContent(content, values) {
1265
+ const enrichedValues = { ...values };
1266
+ function cloneNode(node, dataKey, index) {
1267
+ if (node.type === "variableNode" && node.attrs?.varName) {
1268
+ const oldName = node.attrs.varName;
1269
+ const bareName = oldName.replace(/^operation_/, "");
1270
+ const newName = `${dataKey}_${index}_${bareName}`;
1271
+ return { ...node, attrs: { ...node.attrs, varName: newName } };
1272
+ }
1273
+ if (node.content) {
1274
+ return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
1275
+ }
1276
+ return { ...node };
1277
+ }
1278
+ function walkNode(node) {
1279
+ if (node.type === "repeatBlock" && node.attrs?.data) {
1280
+ const dataKey = node.attrs.data;
1281
+ const arrayJson = values[dataKey];
1282
+ if (!arrayJson) return [];
1283
+ let items;
1284
+ try {
1285
+ items = JSON.parse(arrayJson);
1286
+ } catch {
1287
+ return [];
1288
+ }
1289
+ const sums = /* @__PURE__ */ new Map();
1290
+ for (const item of items) {
1291
+ for (const [key, val] of Object.entries(item)) {
1292
+ const num = parseFloat(val);
1293
+ if (!isNaN(num)) {
1294
+ sums.set(key, (sums.get(key) || 0) + num);
1295
+ }
1296
+ }
1297
+ }
1298
+ const woKeysByNorm = /* @__PURE__ */ new Map();
1299
+ for (const k of Object.keys(enrichedValues)) {
1300
+ if (k.startsWith("workorder_")) {
1301
+ const norm = k.replace(/_/g, "");
1302
+ woKeysByNorm.set(norm, k);
1303
+ }
1304
+ }
1305
+ for (const [field, total] of sums) {
1306
+ const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
1307
+ const directKey = `workorder_${field}`;
1308
+ enrichedValues[directKey] = formatted;
1309
+ const norm = `workorder${field}`.replace(/_/g, "");
1310
+ const existing = woKeysByNorm.get(norm);
1311
+ if (existing && existing !== directKey) {
1312
+ enrichedValues[existing] = formatted;
1313
+ }
1314
+ }
1315
+ const expanded = [];
1316
+ for (let i = 0; i < items.length; i++) {
1317
+ const item = items[i];
1318
+ for (const [key, val] of Object.entries(item)) {
1319
+ enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
1320
+ }
1321
+ const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
1322
+ expanded.push(...cloned);
1323
+ }
1324
+ return expanded;
1325
+ }
1326
+ if (node.content) {
1327
+ const newContent = [];
1328
+ for (const child of node.content) {
1329
+ const result2 = walkNode(child);
1330
+ if (Array.isArray(result2)) newContent.push(...result2);
1331
+ else newContent.push(result2);
1332
+ }
1333
+ return { ...node, content: newContent };
1334
+ }
1335
+ return node;
1336
+ }
1337
+ const result = walkNode(content);
1338
+ return {
1339
+ content: Array.isArray(result) ? { ...content, content: result } : result,
1340
+ values: enrichedValues
1341
+ };
1342
+ }
1343
+
1294
1344
  // src/utils/error-helpers.ts
1295
1345
  function getErrorMessage(error) {
1296
1346
  if (error instanceof Error) return error.message;
@@ -2815,7 +2865,8 @@ async function generatePdfFromContent(content, options = {}) {
2815
2865
  // src/utils/template-pipeline.ts
2816
2866
  async function generatePdfFromTiptap(content, values, options = {}) {
2817
2867
  const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
2818
- const suppressed = suppressZeroContent(expanded, enrichedValues);
2868
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
2869
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
2819
2870
  const replaced = replaceVariablesInContent(suppressed, enrichedValues);
2820
2871
  const fields = extractFieldsFromContent(replaced);
2821
2872
  const result = await generatePdfFromContent(replaced, {
@@ -2871,6 +2922,7 @@ function serializeVariableToken(attrs) {
2871
2922
  if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
2872
2923
  if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
2873
2924
  if (attrs.format) parts.push(`format:${attrs.format}`);
2925
+ if (attrs.block === "true") parts.push(`block:true`);
2874
2926
  return `{{${parts.join("|")}}}`;
2875
2927
  }
2876
2928
  function serializeInline(content) {
@@ -5418,7 +5470,8 @@ function GeneratePanel({
5418
5470
  try {
5419
5471
  const content = getActiveContent();
5420
5472
  const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5421
- const suppressed = suppressZeroContent(expanded, enrichedValues);
5473
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5474
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5422
5475
  const replaced = replaceVariablesInContent(suppressed, enrichedValues);
5423
5476
  const fields = extractFieldsFromContent(replaced);
5424
5477
  const result = await generatePdfFromContent(replaced);
@@ -5441,7 +5494,8 @@ function GeneratePanel({
5441
5494
  try {
5442
5495
  const content = getActiveContent();
5443
5496
  const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
5444
- const suppressed = suppressZeroContent(expanded, enrichedValues);
5497
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5498
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5445
5499
  const replaced = replaceVariablesInContent(suppressed, enrichedValues);
5446
5500
  const fields = extractFieldsFromContent(replaced);
5447
5501
  const result = await generatePdfFromContent(replaced, {
@@ -5523,7 +5577,8 @@ function GeneratePanel({
5523
5577
  for (let i = 0; i < bulkData.length; i++) {
5524
5578
  const values = bulkData[i];
5525
5579
  const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
5526
- const suppressed = suppressZeroContent(expanded, enrichedValues);
5580
+ const blockExpanded = expandBlockVariables(expanded, enrichedValues);
5581
+ const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
5527
5582
  const replaced = replaceVariablesInContent(suppressed, enrichedValues);
5528
5583
  const fields = extractFieldsFromContent(replaced);
5529
5584
  const result = await generatePdfFromContent(replaced, {
@@ -7412,6 +7467,7 @@ exports.PreviewPanel = PreviewPanel;
7412
7467
  exports.RepeatNode = RepeatNode;
7413
7468
  exports.SubtotalsNode = SubtotalsNode;
7414
7469
  exports.ThemeProvider = ThemeProvider;
7470
+ exports.expandBlockVariables = expandBlockVariables;
7415
7471
  exports.expandRepeatContent = expandRepeatContent;
7416
7472
  exports.extractVariablesFromContent = extractVariablesFromContent;
7417
7473
  exports.generatePdfFromMarkdown = generatePdfFromMarkdown;