@signiphi/pdf-compose 0.1.0-beta.1 → 0.1.0-beta.11
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 +265 -0
- package/dist/components/DocumentGenerator.d.ts +3 -0
- package/dist/components/DocumentGenerator.d.ts.map +1 -0
- package/dist/components/EditorPanel.d.ts +15 -0
- package/dist/components/EditorPanel.d.ts.map +1 -0
- package/dist/components/EditorToolbar.d.ts +11 -0
- package/dist/components/EditorToolbar.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +18 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/FieldEditPopover.d.ts +12 -0
- package/dist/components/FieldEditPopover.d.ts.map +1 -0
- package/dist/components/FieldInsertPopover.d.ts +12 -0
- package/dist/components/FieldInsertPopover.d.ts.map +1 -0
- package/dist/components/FieldNodeView.d.ts +3 -0
- package/dist/components/FieldNodeView.d.ts.map +1 -0
- package/dist/components/FieldOverlay.d.ts +9 -0
- package/dist/components/FieldOverlay.d.ts.map +1 -0
- package/dist/components/GeneratePanel.d.ts +15 -0
- package/dist/components/GeneratePanel.d.ts.map +1 -0
- package/dist/components/ImageNodeView.d.ts +11 -0
- package/dist/components/ImageNodeView.d.ts.map +1 -0
- package/dist/components/PreviewPanel.d.ts +12 -0
- package/dist/components/PreviewPanel.d.ts.map +1 -0
- package/dist/components/VariableEditPopover.d.ts +12 -0
- package/dist/components/VariableEditPopover.d.ts.map +1 -0
- package/dist/components/VariableInsertPopover.d.ts +13 -0
- package/dist/components/VariableInsertPopover.d.ts.map +1 -0
- package/dist/components/VariableNodeView.d.ts +3 -0
- package/dist/components/VariableNodeView.d.ts.map +1 -0
- package/dist/components/WatermarkNodeView.d.ts +3 -0
- package/dist/components/WatermarkNodeView.d.ts.map +1 -0
- package/dist/contexts/ThemeContext.d.ts +19 -0
- package/dist/contexts/ThemeContext.d.ts.map +1 -0
- package/dist/extensions/columns-node.d.ts +4 -0
- package/dist/extensions/columns-node.d.ts.map +1 -0
- package/dist/extensions/field-node.d.ts +31 -0
- package/dist/extensions/field-node.d.ts.map +1 -0
- package/dist/extensions/image-node.d.ts +10 -0
- package/dist/extensions/image-node.d.ts.map +1 -0
- package/dist/extensions/panel-node.d.ts +3 -0
- package/dist/extensions/panel-node.d.ts.map +1 -0
- package/dist/extensions/repeat-node.d.ts +3 -0
- package/dist/extensions/repeat-node.d.ts.map +1 -0
- package/dist/extensions/subtotals-node.d.ts +3 -0
- package/dist/extensions/subtotals-node.d.ts.map +1 -0
- package/dist/extensions/variable-node.d.ts +18 -0
- package/dist/extensions/variable-node.d.ts.map +1 -0
- package/dist/extensions/watermark-node.d.ts +3 -0
- package/dist/extensions/watermark-node.d.ts.map +1 -0
- package/dist/hooks/useDocumentGenerator.d.ts +46 -0
- package/dist/hooks/useDocumentGenerator.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3108 -509
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3094 -512
- package/dist/index.mjs.map +1 -1
- package/dist/lib/ui/button.d.ts +12 -0
- package/dist/lib/ui/button.d.ts.map +1 -0
- package/dist/lib/ui/index.d.ts +10 -0
- package/dist/lib/ui/index.d.ts.map +1 -0
- package/dist/lib/ui/input.d.ts +6 -0
- package/dist/lib/ui/input.d.ts.map +1 -0
- package/dist/lib/ui/label.d.ts +8 -0
- package/dist/lib/ui/label.d.ts.map +1 -0
- package/dist/lib/ui/popover.d.ts +8 -0
- package/dist/lib/ui/popover.d.ts.map +1 -0
- package/dist/lib/ui/select.d.ts +14 -0
- package/dist/lib/ui/select.d.ts.map +1 -0
- package/dist/lib/ui/toggle-group.d.ts +13 -0
- package/dist/lib/ui/toggle-group.d.ts.map +1 -0
- package/dist/lib/ui/tooltip.d.ts +8 -0
- package/dist/lib/ui/tooltip.d.ts.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/styles/index.css +374 -194
- package/dist/styles/index.d.ts +1 -0
- package/dist/types/index.d.ts +204 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/error-helpers.d.ts +27 -0
- package/dist/utils/error-helpers.d.ts.map +1 -0
- package/dist/utils/field-helpers.d.ts +31 -0
- package/dist/utils/field-helpers.d.ts.map +1 -0
- package/dist/utils/markdown-parser.d.ts +6 -0
- package/dist/utils/markdown-parser.d.ts.map +1 -0
- package/dist/utils/markdown-validator.d.ts +13 -0
- package/dist/utils/markdown-validator.d.ts.map +1 -0
- package/dist/utils/markdown-writer.d.ts +6 -0
- package/dist/utils/markdown-writer.d.ts.map +1 -0
- package/dist/utils/pdf-generator.d.ts +77 -0
- package/dist/utils/pdf-generator.d.ts.map +1 -0
- package/dist/utils/pdf-metadata.d.ts +58 -0
- package/dist/utils/pdf-metadata.d.ts.map +1 -0
- package/dist/utils/pdf-preview.d.ts +7 -0
- package/dist/utils/pdf-preview.d.ts.map +1 -0
- package/dist/utils/template-pipeline.d.ts +39 -0
- package/dist/utils/template-pipeline.d.ts.map +1 -0
- package/dist/utils/theme-helpers.d.ts +21 -0
- package/dist/utils/theme-helpers.d.ts.map +1 -0
- package/dist/utils/variable-helpers.d.ts +74 -0
- package/dist/utils/variable-helpers.d.ts.map +1 -0
- package/dist/utils/xml-template-parser.d.ts +9 -0
- package/dist/utils/xml-template-parser.d.ts.map +1 -0
- package/package.json +16 -6
- package/src/styles/index.css +140 -68
- package/src/styles/index.d.ts +1 -0
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,
|
|
3
|
+
import { AlertTriangle, RefreshCw, FileText, ChevronDown, Circle, CheckSquare, Calendar, Hash, PenTool, Type, Braces, Droplets, Image, 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,10 @@ 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: "" },
|
|
223
|
+
block: { default: "" }
|
|
217
224
|
};
|
|
218
225
|
},
|
|
219
226
|
parseHTML() {
|
|
@@ -279,6 +286,235 @@ var VariableNode = Node.create({
|
|
|
279
286
|
};
|
|
280
287
|
}
|
|
281
288
|
});
|
|
289
|
+
var PanelNode = Node.create({
|
|
290
|
+
name: "panel",
|
|
291
|
+
group: "block",
|
|
292
|
+
content: "block+",
|
|
293
|
+
defining: true,
|
|
294
|
+
addAttributes() {
|
|
295
|
+
return {
|
|
296
|
+
title: { default: "" },
|
|
297
|
+
border: { default: "solid" },
|
|
298
|
+
headerStyle: { default: "" }
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
parseHTML() {
|
|
302
|
+
return [{
|
|
303
|
+
tag: "div[data-panel-node]",
|
|
304
|
+
contentElement: "div.panel-content"
|
|
305
|
+
}];
|
|
306
|
+
},
|
|
307
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
308
|
+
const { title, border, headerStyle } = node.attrs;
|
|
309
|
+
const classes = ["panel-node"];
|
|
310
|
+
if (border === "dashed") classes.push("panel-border-dashed");
|
|
311
|
+
else if (border === "none") classes.push("panel-border-none");
|
|
312
|
+
else classes.push("panel-border-solid");
|
|
313
|
+
if (headerStyle === "dark") classes.push("panel-dark");
|
|
314
|
+
const attrs = mergeAttributes(
|
|
315
|
+
{ "data-panel-node": "", class: classes.join(" ") },
|
|
316
|
+
HTMLAttributes
|
|
317
|
+
);
|
|
318
|
+
if (title) {
|
|
319
|
+
return [
|
|
320
|
+
"div",
|
|
321
|
+
attrs,
|
|
322
|
+
["div", { class: "panel-title" }, title],
|
|
323
|
+
["div", { class: "panel-content" }, 0]
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
return ["div", attrs, ["div", { class: "panel-content" }, 0]];
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
var ColumnsNode = Node.create({
|
|
330
|
+
name: "columns",
|
|
331
|
+
group: "block",
|
|
332
|
+
content: "column+",
|
|
333
|
+
defining: true,
|
|
334
|
+
addAttributes() {
|
|
335
|
+
return {
|
|
336
|
+
split: { default: "50" },
|
|
337
|
+
padX: { default: "0" }
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
parseHTML() {
|
|
341
|
+
return [{ tag: "div[data-columns-node]" }];
|
|
342
|
+
},
|
|
343
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
344
|
+
const split = node.attrs.split || "50";
|
|
345
|
+
return ["div", mergeAttributes(
|
|
346
|
+
{
|
|
347
|
+
"data-columns-node": "",
|
|
348
|
+
class: "columns-node",
|
|
349
|
+
style: `--xpc-col-split: ${split}`
|
|
350
|
+
},
|
|
351
|
+
HTMLAttributes
|
|
352
|
+
), 0];
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
var ColumnNode = Node.create({
|
|
356
|
+
name: "column",
|
|
357
|
+
group: "",
|
|
358
|
+
content: "block+",
|
|
359
|
+
defining: true,
|
|
360
|
+
addAttributes() {
|
|
361
|
+
return {
|
|
362
|
+
padTop: { default: 0 }
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
parseHTML() {
|
|
366
|
+
return [{ tag: "div[data-column-node]" }];
|
|
367
|
+
},
|
|
368
|
+
renderHTML({ HTMLAttributes }) {
|
|
369
|
+
return ["div", mergeAttributes(
|
|
370
|
+
{ "data-column-node": "", class: "column-node" },
|
|
371
|
+
HTMLAttributes
|
|
372
|
+
), 0];
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
function WatermarkNodeView({ node, selected }) {
|
|
376
|
+
const { text } = node.attrs;
|
|
377
|
+
return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsxs(
|
|
378
|
+
"div",
|
|
379
|
+
{
|
|
380
|
+
"data-watermark-node": "",
|
|
381
|
+
className: cn(
|
|
382
|
+
"inline-flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm my-1",
|
|
383
|
+
"bg-blue-50 text-blue-600 border-blue-200",
|
|
384
|
+
selected && "ring-2 ring-primary ring-offset-1"
|
|
385
|
+
),
|
|
386
|
+
children: [
|
|
387
|
+
/* @__PURE__ */ jsx(Droplets, { size: 14 }),
|
|
388
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
389
|
+
"Watermark: ",
|
|
390
|
+
text || "(empty)"
|
|
391
|
+
] })
|
|
392
|
+
]
|
|
393
|
+
}
|
|
394
|
+
) });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/extensions/watermark-node.ts
|
|
398
|
+
var WatermarkNode = Node.create({
|
|
399
|
+
name: "watermark",
|
|
400
|
+
group: "block",
|
|
401
|
+
atom: true,
|
|
402
|
+
selectable: true,
|
|
403
|
+
draggable: false,
|
|
404
|
+
addAttributes() {
|
|
405
|
+
return {
|
|
406
|
+
text: { default: "" },
|
|
407
|
+
opacity: { default: "0.15" },
|
|
408
|
+
angle: { default: "-45" }
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
parseHTML() {
|
|
412
|
+
return [{ tag: "div[data-watermark-node]" }];
|
|
413
|
+
},
|
|
414
|
+
renderHTML({ HTMLAttributes }) {
|
|
415
|
+
return ["div", mergeAttributes({ "data-watermark-node": "" }, HTMLAttributes)];
|
|
416
|
+
},
|
|
417
|
+
addNodeView() {
|
|
418
|
+
return ReactNodeViewRenderer(WatermarkNodeView);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
function ImageNodeView({ node, selected }) {
|
|
422
|
+
const { src, varName, alt, width, height, align } = node.attrs;
|
|
423
|
+
const hasRenderableSrc = !!src && /^(https?:|data:)/i.test(src);
|
|
424
|
+
const wrapperJustify = align === "center" ? "justify-center" : align === "right" ? "justify-end" : "justify-start";
|
|
425
|
+
const toCssSize = (v) => /^\d+(\.\d+)?$/.test(v) ? `${v}px` : v;
|
|
426
|
+
const sizeStyle = {};
|
|
427
|
+
if (width) sizeStyle.width = toCssSize(width);
|
|
428
|
+
if (height) sizeStyle.height = toCssSize(height);
|
|
429
|
+
return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsx("div", { className: cn("flex my-1", wrapperJustify), "data-image-node": "", children: hasRenderableSrc ? /* @__PURE__ */ jsx(
|
|
430
|
+
"img",
|
|
431
|
+
{
|
|
432
|
+
src,
|
|
433
|
+
alt: alt || varName || "",
|
|
434
|
+
style: sizeStyle,
|
|
435
|
+
className: cn(
|
|
436
|
+
"max-w-full rounded-sm object-contain",
|
|
437
|
+
selected && "ring-2 ring-primary ring-offset-1"
|
|
438
|
+
),
|
|
439
|
+
draggable: false
|
|
440
|
+
}
|
|
441
|
+
) : /* @__PURE__ */ jsxs(
|
|
442
|
+
"div",
|
|
443
|
+
{
|
|
444
|
+
"data-image-placeholder": "",
|
|
445
|
+
className: cn(
|
|
446
|
+
"inline-flex items-center gap-2 px-3 py-2 rounded-md border border-dashed text-sm",
|
|
447
|
+
"bg-slate-50 text-slate-500 border-slate-300",
|
|
448
|
+
selected && "ring-2 ring-primary ring-offset-1"
|
|
449
|
+
),
|
|
450
|
+
style: height ? { minHeight: toCssSize(height) } : void 0,
|
|
451
|
+
title: varName ? `Image variable: ${varName}` : "Image",
|
|
452
|
+
children: [
|
|
453
|
+
/* @__PURE__ */ jsx(Image, { size: 16 }),
|
|
454
|
+
/* @__PURE__ */ jsx("span", { children: varName ? `Image: ${varName}` : alt || "Image" })
|
|
455
|
+
]
|
|
456
|
+
}
|
|
457
|
+
) }) });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/extensions/image-node.ts
|
|
461
|
+
var ImageNode = Node.create({
|
|
462
|
+
name: "imageNode",
|
|
463
|
+
group: "block",
|
|
464
|
+
atom: true,
|
|
465
|
+
selectable: true,
|
|
466
|
+
draggable: false,
|
|
467
|
+
addAttributes() {
|
|
468
|
+
return {
|
|
469
|
+
src: { default: "" },
|
|
470
|
+
varName: { default: "" },
|
|
471
|
+
alt: { default: "" },
|
|
472
|
+
width: { default: "" },
|
|
473
|
+
height: { default: "" },
|
|
474
|
+
align: { default: "" }
|
|
475
|
+
};
|
|
476
|
+
},
|
|
477
|
+
parseHTML() {
|
|
478
|
+
return [{ tag: "div[data-image-node]" }];
|
|
479
|
+
},
|
|
480
|
+
renderHTML({ HTMLAttributes }) {
|
|
481
|
+
return ["div", mergeAttributes({ "data-image-node": "" }, HTMLAttributes)];
|
|
482
|
+
},
|
|
483
|
+
addNodeView() {
|
|
484
|
+
return ReactNodeViewRenderer(ImageNodeView);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
var RepeatNode = Node.create({
|
|
488
|
+
name: "repeatBlock",
|
|
489
|
+
group: "block",
|
|
490
|
+
content: "block+",
|
|
491
|
+
addAttributes() {
|
|
492
|
+
return {
|
|
493
|
+
data: { default: "" }
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
parseHTML() {
|
|
497
|
+
return [{ tag: "div[data-repeat-node]" }];
|
|
498
|
+
},
|
|
499
|
+
renderHTML({ HTMLAttributes }) {
|
|
500
|
+
return ["div", mergeAttributes({ "data-repeat-node": "" }, HTMLAttributes), 0];
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
var SubtotalsNode = Node.create({
|
|
504
|
+
name: "subtotalsBlock",
|
|
505
|
+
group: "block",
|
|
506
|
+
content: "block+",
|
|
507
|
+
defining: true,
|
|
508
|
+
parseHTML() {
|
|
509
|
+
return [{ tag: "div[data-subtotals-node]" }];
|
|
510
|
+
},
|
|
511
|
+
renderHTML({ HTMLAttributes }) {
|
|
512
|
+
return ["div", mergeAttributes(
|
|
513
|
+
{ "data-subtotals-node": "", class: "subtotals-block" },
|
|
514
|
+
HTMLAttributes
|
|
515
|
+
), 0];
|
|
516
|
+
}
|
|
517
|
+
});
|
|
282
518
|
|
|
283
519
|
// src/types/index.ts
|
|
284
520
|
var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
|
|
@@ -357,158 +593,23 @@ function extractFieldsFromContent(content) {
|
|
|
357
593
|
}
|
|
358
594
|
return fields;
|
|
359
595
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
if (node.content) {
|
|
376
|
-
node.content.forEach(walk);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
if (content.content) {
|
|
380
|
-
content.content.forEach(walk);
|
|
381
|
-
}
|
|
382
|
-
return Array.from(seen.values());
|
|
383
|
-
}
|
|
384
|
-
function replaceVariablesInContent(content, values) {
|
|
385
|
-
function walkNode(node) {
|
|
386
|
-
if (node.type === "variableNode" && node.attrs) {
|
|
387
|
-
const varName = node.attrs.varName;
|
|
388
|
-
const replacement = values[varName] || node.attrs.varDefault || `[${node.attrs.varLabel || varName}]`;
|
|
389
|
-
const textNode = { type: "text", text: replacement };
|
|
390
|
-
if (node.marks && node.marks.length > 0) {
|
|
391
|
-
textNode.marks = node.marks;
|
|
392
|
-
}
|
|
393
|
-
return textNode;
|
|
394
|
-
}
|
|
395
|
-
if (node.content) {
|
|
396
|
-
return { ...node, content: node.content.map(walkNode) };
|
|
397
|
-
}
|
|
398
|
-
return node;
|
|
399
|
-
}
|
|
400
|
-
return walkNode(content);
|
|
401
|
-
}
|
|
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("|")}}}`;
|
|
419
|
-
}
|
|
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("|")}}}`;
|
|
426
|
-
}
|
|
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;
|
|
434
|
-
}
|
|
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;
|
|
447
|
-
}
|
|
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;
|
|
458
|
-
}
|
|
459
|
-
if (node.type === "hardBreak") {
|
|
460
|
-
result += " \n";
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
return result;
|
|
464
|
-
}
|
|
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");
|
|
490
|
-
}
|
|
491
|
-
case "blockquote": {
|
|
492
|
-
return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
|
|
493
|
-
}
|
|
494
|
-
case "codeBlock": {
|
|
495
|
-
const text = serializeInline(node.content);
|
|
496
|
-
return `\`\`\`
|
|
497
|
-
${text}
|
|
498
|
-
\`\`\``;
|
|
499
|
-
}
|
|
500
|
-
case "horizontalRule": {
|
|
501
|
-
return "---";
|
|
502
|
-
}
|
|
503
|
-
default:
|
|
504
|
-
return serializeInline(node.content);
|
|
596
|
+
var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
|
|
597
|
+
var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
|
|
598
|
+
var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
|
|
599
|
+
var DIRECTIVE_CLOSE = /^:::$/;
|
|
600
|
+
function parseDirectiveAttrs(str) {
|
|
601
|
+
const attrs = {};
|
|
602
|
+
if (!str) return attrs;
|
|
603
|
+
const pairs = str.split("|");
|
|
604
|
+
for (const pair of pairs) {
|
|
605
|
+
const colonIdx = pair.indexOf(":");
|
|
606
|
+
if (colonIdx === -1) continue;
|
|
607
|
+
const key = pair.slice(0, colonIdx).trim();
|
|
608
|
+
const value = pair.slice(colonIdx + 1).trim();
|
|
609
|
+
if (key) attrs[key] = value;
|
|
505
610
|
}
|
|
611
|
+
return attrs;
|
|
506
612
|
}
|
|
507
|
-
function tiptapToMarkdown(doc) {
|
|
508
|
-
if (!doc.content) return "";
|
|
509
|
-
return doc.content.map(serializeBlock).join("\n\n");
|
|
510
|
-
}
|
|
511
|
-
var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
|
|
512
613
|
function injectMarks(token, marks) {
|
|
513
614
|
const existingMatch = token.match(/\|_marks:([^}|]+)/);
|
|
514
615
|
if (existingMatch) {
|
|
@@ -625,6 +726,15 @@ function parseVariableToken(tokenBody) {
|
|
|
625
726
|
case "default":
|
|
626
727
|
attrs.varDefault = value;
|
|
627
728
|
break;
|
|
729
|
+
case "suppress":
|
|
730
|
+
attrs.suppressZero = value === "zero" ? "true" : "";
|
|
731
|
+
break;
|
|
732
|
+
case "format":
|
|
733
|
+
attrs.format = value;
|
|
734
|
+
break;
|
|
735
|
+
case "block":
|
|
736
|
+
attrs.block = value === "true" ? "true" : "";
|
|
737
|
+
break;
|
|
628
738
|
case "_marks":
|
|
629
739
|
attrs._marks = value;
|
|
630
740
|
break;
|
|
@@ -632,6 +742,44 @@ function parseVariableToken(tokenBody) {
|
|
|
632
742
|
}
|
|
633
743
|
return attrs;
|
|
634
744
|
}
|
|
745
|
+
function parseImageToken(tokenBody) {
|
|
746
|
+
const attrs = {
|
|
747
|
+
src: "",
|
|
748
|
+
varName: "",
|
|
749
|
+
alt: "",
|
|
750
|
+
width: "",
|
|
751
|
+
height: "",
|
|
752
|
+
align: ""
|
|
753
|
+
};
|
|
754
|
+
const pairs = tokenBody.split("|");
|
|
755
|
+
for (const pair of pairs) {
|
|
756
|
+
const colonIdx = pair.indexOf(":");
|
|
757
|
+
if (colonIdx === -1) continue;
|
|
758
|
+
const key = pair.slice(0, colonIdx).trim();
|
|
759
|
+
const value = pair.slice(colonIdx + 1).trim();
|
|
760
|
+
switch (key) {
|
|
761
|
+
case "src":
|
|
762
|
+
attrs.src = value;
|
|
763
|
+
break;
|
|
764
|
+
case "var":
|
|
765
|
+
attrs.varName = value;
|
|
766
|
+
break;
|
|
767
|
+
case "alt":
|
|
768
|
+
attrs.alt = value;
|
|
769
|
+
break;
|
|
770
|
+
case "width":
|
|
771
|
+
attrs.width = value;
|
|
772
|
+
break;
|
|
773
|
+
case "height":
|
|
774
|
+
attrs.height = value;
|
|
775
|
+
break;
|
|
776
|
+
case "align":
|
|
777
|
+
attrs.align = value;
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return attrs;
|
|
782
|
+
}
|
|
635
783
|
function parseInline(text) {
|
|
636
784
|
text = preprocessMarkedTokens(text);
|
|
637
785
|
const nodes = [];
|
|
@@ -655,6 +803,8 @@ function parseInline(text) {
|
|
|
655
803
|
const node = { type: "variableNode", attrs };
|
|
656
804
|
if (marks.length > 0) node.marks = marks;
|
|
657
805
|
nodes.push(node);
|
|
806
|
+
} else if (match[1] === "image") {
|
|
807
|
+
nodes.push({ type: "imageNode", attrs: parseImageToken(match[2]) });
|
|
658
808
|
}
|
|
659
809
|
lastIndex = match.index + match[0].length;
|
|
660
810
|
}
|
|
@@ -666,45 +816,63 @@ function parseInline(text) {
|
|
|
666
816
|
function parseFormattedText(text) {
|
|
667
817
|
if (!text) return [];
|
|
668
818
|
const nodes = [];
|
|
669
|
-
const parts = text.split(/(
|
|
819
|
+
const parts = text.split(/(\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|__[^_]+__|`[^`]+`)/);
|
|
670
820
|
for (const part of parts) {
|
|
671
821
|
if (!part) continue;
|
|
672
|
-
if (part.startsWith("
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
});
|
|
822
|
+
if (part.startsWith("***") && part.endsWith("***")) {
|
|
823
|
+
const inner = part.slice(3, -3);
|
|
824
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }, { type: "italic" }] });
|
|
825
|
+
} else if (part.startsWith("**") && part.endsWith("**")) {
|
|
826
|
+
const inner = part.slice(2, -2);
|
|
827
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "bold" }] });
|
|
678
828
|
} else if (part.startsWith("*") && part.endsWith("*")) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
text: part.slice(1, -1),
|
|
682
|
-
marks: [{ type: "italic" }]
|
|
683
|
-
});
|
|
829
|
+
const inner = part.slice(1, -1);
|
|
830
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "italic" }] });
|
|
684
831
|
} else if (part.startsWith("__") && part.endsWith("__")) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
text: part.slice(2, -2),
|
|
688
|
-
marks: [{ type: "underline" }]
|
|
689
|
-
});
|
|
832
|
+
const inner = part.slice(2, -2);
|
|
833
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "underline" }] });
|
|
690
834
|
} else if (part.startsWith("`") && part.endsWith("`")) {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
text: part.slice(1, -1),
|
|
694
|
-
marks: [{ type: "code" }]
|
|
695
|
-
});
|
|
835
|
+
const inner = part.slice(1, -1);
|
|
836
|
+
if (inner) nodes.push({ type: "text", text: inner, marks: [{ type: "code" }] });
|
|
696
837
|
} else {
|
|
697
838
|
nodes.push({ type: "text", text: part });
|
|
698
839
|
}
|
|
699
840
|
}
|
|
700
841
|
return nodes;
|
|
701
842
|
}
|
|
702
|
-
function
|
|
703
|
-
const
|
|
843
|
+
function splitTableCells(row) {
|
|
844
|
+
const cells = [];
|
|
845
|
+
let current = "";
|
|
846
|
+
let depth = 0;
|
|
847
|
+
for (let i = 0; i < row.length; i++) {
|
|
848
|
+
if (row[i] === "{" && row[i + 1] === "{") {
|
|
849
|
+
depth++;
|
|
850
|
+
current += "{{";
|
|
851
|
+
i++;
|
|
852
|
+
} else if (row[i] === "}" && row[i + 1] === "}") {
|
|
853
|
+
depth = Math.max(0, depth - 1);
|
|
854
|
+
current += "}}";
|
|
855
|
+
i++;
|
|
856
|
+
} else if (row[i] === "|" && depth === 0) {
|
|
857
|
+
cells.push(current.trim());
|
|
858
|
+
current = "";
|
|
859
|
+
} else {
|
|
860
|
+
current += row[i];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (current.trim()) {
|
|
864
|
+
cells.push(current.trim());
|
|
865
|
+
}
|
|
866
|
+
return cells;
|
|
867
|
+
}
|
|
868
|
+
function parseBlocks(lines, startIdx, stopCondition) {
|
|
704
869
|
const content = [];
|
|
705
|
-
let i =
|
|
870
|
+
let i = startIdx;
|
|
706
871
|
while (i < lines.length) {
|
|
707
872
|
const line = lines[i];
|
|
873
|
+
if (stopCondition && stopCondition(line)) {
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
708
876
|
if (line.trim() === "") {
|
|
709
877
|
i++;
|
|
710
878
|
continue;
|
|
@@ -778,13 +946,443 @@ function markdownToTiptap(markdown) {
|
|
|
778
946
|
});
|
|
779
947
|
continue;
|
|
780
948
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
949
|
+
if (line.trimStart().startsWith("|") && line.trimEnd().endsWith("|")) {
|
|
950
|
+
const tableLines = [];
|
|
951
|
+
while (i < lines.length) {
|
|
952
|
+
const tl = lines[i];
|
|
953
|
+
if (tl.trimStart().startsWith("|") && tl.trimEnd().endsWith("|")) {
|
|
954
|
+
tableLines.push(tl);
|
|
955
|
+
i++;
|
|
956
|
+
} else {
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const separatorRegex = /^[\s|]*-+[\s|:-]*$/;
|
|
961
|
+
const dataLines = tableLines.filter(
|
|
962
|
+
(tl) => !separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))
|
|
963
|
+
);
|
|
964
|
+
const firstDataCellsRaw = dataLines.length > 0 ? splitTableCells(dataLines[0]) : [];
|
|
965
|
+
const isSubtotals = firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "_");
|
|
966
|
+
const isBorderless = isSubtotals || firstDataCellsRaw.length >= 2 && firstDataCellsRaw.every((c) => c.trim() === "");
|
|
967
|
+
const tableRows = [];
|
|
968
|
+
let isFirstDataRow = true;
|
|
969
|
+
for (const tl of tableLines) {
|
|
970
|
+
if (separatorRegex.test(tl.replace(/^\s*\|/, "").replace(/\|\s*$/, ""))) continue;
|
|
971
|
+
const cells = splitTableCells(tl).filter((c) => c !== "");
|
|
972
|
+
if (isBorderless && isFirstDataRow) {
|
|
973
|
+
isFirstDataRow = false;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const cellType = isFirstDataRow && !isBorderless ? "tableHeader" : "tableCell";
|
|
977
|
+
const rowContent = cells.map((cellText) => ({
|
|
978
|
+
type: cellType,
|
|
979
|
+
content: [{ type: "paragraph", content: cellText ? parseInline(cellText) : [] }]
|
|
980
|
+
}));
|
|
981
|
+
tableRows.push({ type: "tableRow", content: rowContent });
|
|
982
|
+
isFirstDataRow = false;
|
|
983
|
+
}
|
|
984
|
+
if (tableRows.length > 0) {
|
|
985
|
+
const tableNode = { type: "table", content: tableRows };
|
|
986
|
+
if (isBorderless) {
|
|
987
|
+
tableNode.attrs = { borderless: true, ...isSubtotals ? { subtotals: true } : {} };
|
|
988
|
+
}
|
|
989
|
+
content.push(tableNode);
|
|
990
|
+
}
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const directiveMatch = line.trim().match(DIRECTIVE_OPEN);
|
|
994
|
+
if (directiveMatch) {
|
|
995
|
+
const directiveType = directiveMatch[1];
|
|
996
|
+
const rawAttrs = directiveMatch[2] || "";
|
|
997
|
+
const attrs = parseDirectiveAttrs(rawAttrs);
|
|
998
|
+
i++;
|
|
999
|
+
if (directiveType === "watermark") {
|
|
1000
|
+
content.push({
|
|
1001
|
+
type: "watermark",
|
|
1002
|
+
attrs: {
|
|
1003
|
+
text: attrs.text || "",
|
|
1004
|
+
opacity: attrs.opacity || "0.15",
|
|
1005
|
+
angle: attrs.angle || "-45"
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (directiveType === "panel") {
|
|
1011
|
+
const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
|
|
1012
|
+
i = inner.nextIdx;
|
|
1013
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
1014
|
+
i++;
|
|
1015
|
+
}
|
|
1016
|
+
content.push({
|
|
1017
|
+
type: "panel",
|
|
1018
|
+
attrs: {
|
|
1019
|
+
title: attrs.title || "",
|
|
1020
|
+
border: attrs.border || "solid",
|
|
1021
|
+
headerStyle: attrs.headerStyle || ""
|
|
1022
|
+
},
|
|
1023
|
+
content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
|
|
1024
|
+
});
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (directiveType === "repeat") {
|
|
1028
|
+
const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
|
|
1029
|
+
i = inner.nextIdx;
|
|
1030
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
1031
|
+
i++;
|
|
1032
|
+
}
|
|
1033
|
+
content.push({
|
|
1034
|
+
type: "repeatBlock",
|
|
1035
|
+
attrs: { data: attrs.data || "" },
|
|
1036
|
+
content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
|
|
1037
|
+
});
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (directiveType === "subtotals") {
|
|
1041
|
+
const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
|
|
1042
|
+
i = inner.nextIdx;
|
|
1043
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
1044
|
+
i++;
|
|
1045
|
+
}
|
|
1046
|
+
content.push({
|
|
1047
|
+
type: "subtotalsBlock",
|
|
1048
|
+
content: inner.blocks.length > 0 ? inner.blocks : [{ type: "paragraph" }]
|
|
1049
|
+
});
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (directiveType === "columns") {
|
|
1053
|
+
const splitVal = attrs.split || "50";
|
|
1054
|
+
const columns = [];
|
|
1055
|
+
while (i < lines.length) {
|
|
1056
|
+
const colLine = lines[i].trim();
|
|
1057
|
+
if (DIRECTIVE_CLOSE.test(colLine)) {
|
|
1058
|
+
if (columns.length > 0) {
|
|
1059
|
+
i++;
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
i++;
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
const colMatch = colLine.match(/^:::col(?:\{([^}]*)\})?$/);
|
|
1066
|
+
if (colMatch) {
|
|
1067
|
+
i++;
|
|
1068
|
+
const colInner = parseBlocks(lines, i, (l) => {
|
|
1069
|
+
const trimmed = l.trim();
|
|
1070
|
+
return DIRECTIVE_CLOSE.test(trimmed) || /^:::col(?:\{[^}]*\})?$/.test(trimmed);
|
|
1071
|
+
});
|
|
1072
|
+
i = colInner.nextIdx;
|
|
1073
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
1074
|
+
i++;
|
|
1075
|
+
}
|
|
1076
|
+
const colAttrsRaw = colMatch[1] || "";
|
|
1077
|
+
const colAttrs = parseDirectiveAttrs(colAttrsRaw);
|
|
1078
|
+
const padTop = parseFloat(colAttrs.padTop || "0") || 0;
|
|
1079
|
+
columns.push({
|
|
1080
|
+
type: "column",
|
|
1081
|
+
attrs: padTop ? { padTop } : void 0,
|
|
1082
|
+
content: colInner.blocks.length > 0 ? colInner.blocks : [{ type: "paragraph" }]
|
|
1083
|
+
});
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (colLine === "") {
|
|
1087
|
+
i++;
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
const columnsAttrs = { split: splitVal };
|
|
1093
|
+
if (attrs.padX) columnsAttrs.padX = attrs.padX;
|
|
1094
|
+
content.push({
|
|
1095
|
+
type: "columns",
|
|
1096
|
+
attrs: columnsAttrs,
|
|
1097
|
+
content: columns.length > 0 ? columns : [
|
|
1098
|
+
{ type: "column", content: [{ type: "paragraph" }] },
|
|
1099
|
+
{ type: "column", content: [{ type: "paragraph" }] }
|
|
1100
|
+
]
|
|
1101
|
+
});
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (directiveType === "col") {
|
|
1105
|
+
const inner = parseBlocks(lines, i, (l) => DIRECTIVE_CLOSE.test(l.trim()));
|
|
1106
|
+
i = inner.nextIdx;
|
|
1107
|
+
if (i < lines.length && DIRECTIVE_CLOSE.test(lines[i].trim())) {
|
|
1108
|
+
i++;
|
|
1109
|
+
}
|
|
1110
|
+
for (const block of inner.blocks) {
|
|
1111
|
+
content.push(block);
|
|
1112
|
+
}
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (DIRECTIVE_CLOSE.test(line.trim())) {
|
|
1118
|
+
i++;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const imageMatch = line.trim().match(BLOCK_IMAGE_REGEX);
|
|
1122
|
+
if (imageMatch) {
|
|
1123
|
+
content.push({
|
|
1124
|
+
type: "imageNode",
|
|
1125
|
+
attrs: { src: imageMatch[2], varName: "", alt: imageMatch[1], width: "", height: "", align: "" }
|
|
1126
|
+
});
|
|
1127
|
+
i++;
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const inlineNodes = parseInline(line);
|
|
1131
|
+
if (inlineNodes.some((n) => n.type === "imageNode")) {
|
|
1132
|
+
let run = [];
|
|
1133
|
+
const flushRun = () => {
|
|
1134
|
+
if (run.some((n) => !(n.type === "text" && !(n.text || "").trim()))) {
|
|
1135
|
+
content.push({ type: "paragraph", content: run });
|
|
1136
|
+
}
|
|
1137
|
+
run = [];
|
|
1138
|
+
};
|
|
1139
|
+
for (const n of inlineNodes) {
|
|
1140
|
+
if (n.type === "imageNode") {
|
|
1141
|
+
flushRun();
|
|
1142
|
+
content.push(n);
|
|
1143
|
+
} else {
|
|
1144
|
+
run.push(n);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
flushRun();
|
|
1148
|
+
} else {
|
|
1149
|
+
content.push({ type: "paragraph", content: inlineNodes });
|
|
1150
|
+
}
|
|
785
1151
|
i++;
|
|
786
1152
|
}
|
|
787
|
-
return {
|
|
1153
|
+
return { blocks: content, nextIdx: i };
|
|
1154
|
+
}
|
|
1155
|
+
function markdownToTiptap(markdown) {
|
|
1156
|
+
const lines = markdown.split("\n");
|
|
1157
|
+
const { blocks } = parseBlocks(lines, 0);
|
|
1158
|
+
return { type: "doc", content: blocks };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/utils/variable-helpers.ts
|
|
1162
|
+
function extractVariablesFromContent(content) {
|
|
1163
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1164
|
+
function walk(node) {
|
|
1165
|
+
if (node.type === "variableNode" && node.attrs) {
|
|
1166
|
+
const varName = node.attrs.varName;
|
|
1167
|
+
if (varName && !seen.has(varName)) {
|
|
1168
|
+
seen.set(varName, {
|
|
1169
|
+
varName,
|
|
1170
|
+
varLabel: node.attrs.varLabel || varName,
|
|
1171
|
+
varDefault: node.attrs.varDefault || ""
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (node.content) {
|
|
1176
|
+
node.content.forEach(walk);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (content.content) {
|
|
1180
|
+
content.content.forEach(walk);
|
|
1181
|
+
}
|
|
1182
|
+
return Array.from(seen.values());
|
|
1183
|
+
}
|
|
1184
|
+
function applyFormat(value, format) {
|
|
1185
|
+
if (format === "phone") {
|
|
1186
|
+
const digits = value.replace(/\D/g, "");
|
|
1187
|
+
if (digits.length === 10) {
|
|
1188
|
+
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)} - ${digits.slice(6)}`;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return value;
|
|
1192
|
+
}
|
|
1193
|
+
function replaceVariablesInContent(content, values) {
|
|
1194
|
+
function walkNode(node) {
|
|
1195
|
+
if (node.type === "variableNode" && node.attrs) {
|
|
1196
|
+
const varName = node.attrs.varName;
|
|
1197
|
+
let replacement = values[varName] || node.attrs.varDefault || "";
|
|
1198
|
+
const format = node.attrs.format;
|
|
1199
|
+
if (format && replacement) {
|
|
1200
|
+
replacement = applyFormat(replacement, format);
|
|
1201
|
+
}
|
|
1202
|
+
const textNode = { type: "text", text: replacement };
|
|
1203
|
+
if (node.marks && node.marks.length > 0) {
|
|
1204
|
+
textNode.marks = node.marks;
|
|
1205
|
+
}
|
|
1206
|
+
return textNode;
|
|
1207
|
+
}
|
|
1208
|
+
if (node.type === "imageNode" && node.attrs?.varName) {
|
|
1209
|
+
const varName = node.attrs.varName;
|
|
1210
|
+
const resolved = values[varName] || node.attrs.src || "";
|
|
1211
|
+
return { ...node, attrs: { ...node.attrs, src: resolved } };
|
|
1212
|
+
}
|
|
1213
|
+
if (node.content) {
|
|
1214
|
+
return { ...node, content: node.content.map(walkNode) };
|
|
1215
|
+
}
|
|
1216
|
+
return node;
|
|
1217
|
+
}
|
|
1218
|
+
return walkNode(content);
|
|
1219
|
+
}
|
|
1220
|
+
function expandBlockVariables(content, values) {
|
|
1221
|
+
function soleVariableOf(node) {
|
|
1222
|
+
if (node.type !== "paragraph" || !node.content) return null;
|
|
1223
|
+
const meaningful = node.content.filter(
|
|
1224
|
+
(c) => !(c.type === "text" && !(c.text || "").trim())
|
|
1225
|
+
);
|
|
1226
|
+
if (meaningful.length === 1 && meaningful[0].type === "variableNode") {
|
|
1227
|
+
return meaningful[0];
|
|
1228
|
+
}
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
function stripUnsafeNodes(node) {
|
|
1232
|
+
if (!node.content) return node;
|
|
1233
|
+
return {
|
|
1234
|
+
...node,
|
|
1235
|
+
content: node.content.filter((c) => c.type !== "fieldNode" && c.type !== "watermark").map(stripUnsafeNodes)
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function isCellSafe(blocks) {
|
|
1239
|
+
return blocks.every((b) => b.type === "paragraph" || b.type === "heading");
|
|
1240
|
+
}
|
|
1241
|
+
function walkNode(node, inCell) {
|
|
1242
|
+
if (!node.content) return node;
|
|
1243
|
+
const childInCell = inCell || node.type === "tableCell" || node.type === "tableHeader";
|
|
1244
|
+
const newContent = [];
|
|
1245
|
+
for (const child of node.content) {
|
|
1246
|
+
const varNode = soleVariableOf(child);
|
|
1247
|
+
if (varNode?.attrs) {
|
|
1248
|
+
const varName = varNode.attrs.varName;
|
|
1249
|
+
const value = values[varName] || varNode.attrs.varDefault || "";
|
|
1250
|
+
const isBlock = varNode.attrs.block === "true" || value.includes("\n");
|
|
1251
|
+
if (isBlock) {
|
|
1252
|
+
const parsed = stripUnsafeNodes(markdownToTiptap(value));
|
|
1253
|
+
const blocks = parsed.content || [];
|
|
1254
|
+
if (!childInCell || isCellSafe(blocks)) {
|
|
1255
|
+
newContent.push(...blocks);
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
newContent.push(walkNode(child, childInCell));
|
|
1261
|
+
}
|
|
1262
|
+
return { ...node, content: newContent };
|
|
1263
|
+
}
|
|
1264
|
+
return walkNode(content, false);
|
|
1265
|
+
}
|
|
1266
|
+
function labelToVarName(label) {
|
|
1267
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
1268
|
+
}
|
|
1269
|
+
function isZeroLike(value) {
|
|
1270
|
+
if (!value || value.trim() === "") return true;
|
|
1271
|
+
const cleaned = value.replace(/[$,\s]/g, "");
|
|
1272
|
+
return /^-?0+(\.0+)?$/.test(cleaned);
|
|
1273
|
+
}
|
|
1274
|
+
function suppressZeroContent(content, values) {
|
|
1275
|
+
function hasSuppressedVar(node) {
|
|
1276
|
+
if (node.type === "variableNode" && node.attrs?.suppressZero === "true") {
|
|
1277
|
+
const varName = node.attrs.varName;
|
|
1278
|
+
const val = values[varName] || node.attrs.varDefault || "";
|
|
1279
|
+
return isZeroLike(val);
|
|
1280
|
+
}
|
|
1281
|
+
return (node.content || []).some(hasSuppressedVar);
|
|
1282
|
+
}
|
|
1283
|
+
function walkNode(node) {
|
|
1284
|
+
if (node.type === "table" && node.content) {
|
|
1285
|
+
const filtered = node.content.filter((row) => {
|
|
1286
|
+
if (row.type !== "tableRow") return true;
|
|
1287
|
+
const isHeader = row.content?.[0]?.type === "tableHeader";
|
|
1288
|
+
if (isHeader) return true;
|
|
1289
|
+
return !hasSuppressedVar(row);
|
|
1290
|
+
});
|
|
1291
|
+
const bodyRows = filtered.filter(
|
|
1292
|
+
(r) => r.type === "tableRow" && r.content?.[0]?.type !== "tableHeader"
|
|
1293
|
+
);
|
|
1294
|
+
if (bodyRows.length === 0) return null;
|
|
1295
|
+
return { ...node, content: filtered };
|
|
1296
|
+
}
|
|
1297
|
+
if (node.type === "paragraph" && hasSuppressedVar(node)) {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
if (node.content) {
|
|
1301
|
+
const filtered = node.content.map((child) => walkNode(child)).filter((c) => c !== null);
|
|
1302
|
+
return { ...node, content: filtered };
|
|
1303
|
+
}
|
|
1304
|
+
return node;
|
|
1305
|
+
}
|
|
1306
|
+
return walkNode(content) || content;
|
|
1307
|
+
}
|
|
1308
|
+
function expandRepeatContent(content, values) {
|
|
1309
|
+
const enrichedValues = { ...values };
|
|
1310
|
+
function cloneNode(node, dataKey, index) {
|
|
1311
|
+
if (node.type === "variableNode" && node.attrs?.varName) {
|
|
1312
|
+
const oldName = node.attrs.varName;
|
|
1313
|
+
const bareName = oldName.replace(/^operation_/, "");
|
|
1314
|
+
const newName = `${dataKey}_${index}_${bareName}`;
|
|
1315
|
+
return { ...node, attrs: { ...node.attrs, varName: newName } };
|
|
1316
|
+
}
|
|
1317
|
+
if (node.content) {
|
|
1318
|
+
return { ...node, content: node.content.map((n) => cloneNode(n, dataKey, index)) };
|
|
1319
|
+
}
|
|
1320
|
+
return { ...node };
|
|
1321
|
+
}
|
|
1322
|
+
function walkNode(node) {
|
|
1323
|
+
if (node.type === "repeatBlock" && node.attrs?.data) {
|
|
1324
|
+
const dataKey = node.attrs.data;
|
|
1325
|
+
const arrayJson = values[dataKey];
|
|
1326
|
+
if (!arrayJson) return [];
|
|
1327
|
+
let items;
|
|
1328
|
+
try {
|
|
1329
|
+
items = JSON.parse(arrayJson);
|
|
1330
|
+
} catch {
|
|
1331
|
+
return [];
|
|
1332
|
+
}
|
|
1333
|
+
const sums = /* @__PURE__ */ new Map();
|
|
1334
|
+
for (const item of items) {
|
|
1335
|
+
for (const [key, val] of Object.entries(item)) {
|
|
1336
|
+
const num = parseFloat(val);
|
|
1337
|
+
if (!isNaN(num)) {
|
|
1338
|
+
sums.set(key, (sums.get(key) || 0) + num);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const woKeysByNorm = /* @__PURE__ */ new Map();
|
|
1343
|
+
for (const k of Object.keys(enrichedValues)) {
|
|
1344
|
+
if (k.startsWith("workorder_")) {
|
|
1345
|
+
const norm = k.replace(/_/g, "");
|
|
1346
|
+
woKeysByNorm.set(norm, k);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
for (const [field, total] of sums) {
|
|
1350
|
+
const formatted = total % 1 === 0 ? total.toFixed(2) : total.toFixed(2);
|
|
1351
|
+
const directKey = `workorder_${field}`;
|
|
1352
|
+
enrichedValues[directKey] = formatted;
|
|
1353
|
+
const norm = `workorder${field}`.replace(/_/g, "");
|
|
1354
|
+
const existing = woKeysByNorm.get(norm);
|
|
1355
|
+
if (existing && existing !== directKey) {
|
|
1356
|
+
enrichedValues[existing] = formatted;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const expanded = [];
|
|
1360
|
+
for (let i = 0; i < items.length; i++) {
|
|
1361
|
+
const item = items[i];
|
|
1362
|
+
for (const [key, val] of Object.entries(item)) {
|
|
1363
|
+
enrichedValues[`${dataKey}_${i}_${key}`] = String(val);
|
|
1364
|
+
}
|
|
1365
|
+
const cloned = (node.content || []).map((n) => cloneNode(n, dataKey, i));
|
|
1366
|
+
expanded.push(...cloned);
|
|
1367
|
+
}
|
|
1368
|
+
return expanded;
|
|
1369
|
+
}
|
|
1370
|
+
if (node.content) {
|
|
1371
|
+
const newContent = [];
|
|
1372
|
+
for (const child of node.content) {
|
|
1373
|
+
const result2 = walkNode(child);
|
|
1374
|
+
if (Array.isArray(result2)) newContent.push(...result2);
|
|
1375
|
+
else newContent.push(result2);
|
|
1376
|
+
}
|
|
1377
|
+
return { ...node, content: newContent };
|
|
1378
|
+
}
|
|
1379
|
+
return node;
|
|
1380
|
+
}
|
|
1381
|
+
const result = walkNode(content);
|
|
1382
|
+
return {
|
|
1383
|
+
content: Array.isArray(result) ? { ...content, content: result } : result,
|
|
1384
|
+
values: enrichedValues
|
|
1385
|
+
};
|
|
788
1386
|
}
|
|
789
1387
|
|
|
790
1388
|
// src/utils/error-helpers.ts
|
|
@@ -922,10 +1520,40 @@ function buildMetadataObject(fields, actualFieldNames) {
|
|
|
922
1520
|
// src/utils/pdf-generator.ts
|
|
923
1521
|
var PAGE_WIDTH = 595.28;
|
|
924
1522
|
var PAGE_HEIGHT = 841.89;
|
|
925
|
-
var MARGIN =
|
|
1523
|
+
var MARGIN = 40;
|
|
926
1524
|
var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
|
|
927
1525
|
var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
|
|
928
1526
|
var LINE_HEIGHT_FACTOR = 1.4;
|
|
1527
|
+
var BODY_FONT_SIZE = 10;
|
|
1528
|
+
function hexToRgbColor(hex) {
|
|
1529
|
+
if (!hex) return null;
|
|
1530
|
+
const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
|
|
1531
|
+
if (!m) return null;
|
|
1532
|
+
const n = parseInt(m[1], 16);
|
|
1533
|
+
return rgb((n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255);
|
|
1534
|
+
}
|
|
1535
|
+
function contrastTextColor(fill) {
|
|
1536
|
+
const c = fill;
|
|
1537
|
+
const luminance = 0.299 * c.red + 0.587 * c.green + 0.114 * c.blue;
|
|
1538
|
+
return luminance > 0.6 ? rgb(0, 0, 0) : rgb(1, 1, 1);
|
|
1539
|
+
}
|
|
1540
|
+
function resolveTheme(theme) {
|
|
1541
|
+
const primary = hexToRgbColor(theme?.primaryColor);
|
|
1542
|
+
const accent = hexToRgbColor(theme?.accentColor);
|
|
1543
|
+
const border = hexToRgbColor(theme?.borderColor);
|
|
1544
|
+
return {
|
|
1545
|
+
tableHeaderBg: primary ?? rgb(0.3, 0.3, 0.3),
|
|
1546
|
+
tableHeaderText: primary ? contrastTextColor(primary) : rgb(1, 1, 1),
|
|
1547
|
+
tableBorder: border ?? rgb(0.75, 0.75, 0.75),
|
|
1548
|
+
panelDarkHeaderBg: primary ?? rgb(0.25, 0.25, 0.25),
|
|
1549
|
+
panelDarkHeaderText: primary ? contrastTextColor(primary) : rgb(1, 1, 1),
|
|
1550
|
+
panelBorder: border ?? rgb(0.6, 0.6, 0.6),
|
|
1551
|
+
rule: primary ?? rgb(0, 0, 0),
|
|
1552
|
+
pageHeaderText: primary ?? rgb(0, 0, 0),
|
|
1553
|
+
pageHeaderRule: accent ?? primary ?? rgb(0.75, 0.75, 0.75)
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
|
|
929
1557
|
var FIELD_DISPLAY = {
|
|
930
1558
|
["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
|
|
931
1559
|
["signature" /* SIGNATURE */]: { label: "Signature", width: 200, height: 60 },
|
|
@@ -956,17 +1584,65 @@ function getFieldDimensions(fieldType, attrs, font, fontSize) {
|
|
|
956
1584
|
const height = textLineHeight + 4;
|
|
957
1585
|
return { width, height };
|
|
958
1586
|
}
|
|
1587
|
+
async function loadImageBytes(src) {
|
|
1588
|
+
if (src.startsWith("data:")) {
|
|
1589
|
+
const comma = src.indexOf(",");
|
|
1590
|
+
if (comma === -1) throw new Error("malformed data URL");
|
|
1591
|
+
const meta = src.slice(0, comma);
|
|
1592
|
+
if (!/;base64$/i.test(meta)) throw new Error("data URL must be base64-encoded");
|
|
1593
|
+
const bin = atob(src.slice(comma + 1));
|
|
1594
|
+
const bytes = new Uint8Array(bin.length);
|
|
1595
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1596
|
+
return bytes;
|
|
1597
|
+
}
|
|
1598
|
+
const res = await fetch(src);
|
|
1599
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1600
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
1601
|
+
}
|
|
1602
|
+
function describeImageSrc(src) {
|
|
1603
|
+
return src.length > 80 ? `${src.slice(0, 77)}...` : src;
|
|
1604
|
+
}
|
|
1605
|
+
async function preloadImages(pdfDoc, content) {
|
|
1606
|
+
const srcs = /* @__PURE__ */ new Set();
|
|
1607
|
+
(function walk(node) {
|
|
1608
|
+
if (node.type === "imageNode") {
|
|
1609
|
+
const src = node.attrs?.src || "";
|
|
1610
|
+
if (src) srcs.add(src);
|
|
1611
|
+
}
|
|
1612
|
+
for (const child of node.content || []) walk(child);
|
|
1613
|
+
})(content);
|
|
1614
|
+
const images = /* @__PURE__ */ new Map();
|
|
1615
|
+
const warnings = [];
|
|
1616
|
+
for (const src of srcs) {
|
|
1617
|
+
try {
|
|
1618
|
+
const bytes = await loadImageBytes(src);
|
|
1619
|
+
const isPng = bytes.length > 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
1620
|
+
const isJpeg = bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216;
|
|
1621
|
+
if (isPng) {
|
|
1622
|
+
images.set(src, await pdfDoc.embedPng(bytes));
|
|
1623
|
+
} else if (isJpeg) {
|
|
1624
|
+
images.set(src, await pdfDoc.embedJpg(bytes));
|
|
1625
|
+
} else {
|
|
1626
|
+
warnings.push(`Image skipped (unsupported format \u2014 only PNG/JPEG embed): ${describeImageSrc(src)}`);
|
|
1627
|
+
}
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
warnings.push(`Image skipped (${getErrorMessage(err)}): ${describeImageSrc(src)}`);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return { images, warnings };
|
|
1633
|
+
}
|
|
959
1634
|
function ensureSpace(state, neededHeight) {
|
|
960
1635
|
if (state.y + neededHeight > CONTENT_HEIGHT) {
|
|
961
1636
|
const newPage = state.pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
|
962
1637
|
state.currentPage = newPage;
|
|
963
1638
|
state.pageIndex++;
|
|
964
1639
|
state.y = 0;
|
|
1640
|
+
renderWatermarksOnPage(newPage, state.fonts, state.watermarkNodes);
|
|
965
1641
|
}
|
|
966
1642
|
}
|
|
967
|
-
function drawText(state, text, font, fontSize, indent = 0) {
|
|
1643
|
+
function drawText(state, text, font, fontSize, indent = 0, region = DEFAULT_REGION) {
|
|
968
1644
|
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
969
|
-
const maxWidth =
|
|
1645
|
+
const maxWidth = region.width - indent;
|
|
970
1646
|
const words = text.split(/\s+/);
|
|
971
1647
|
let line = "";
|
|
972
1648
|
let totalAdvance = 0;
|
|
@@ -977,7 +1653,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
|
|
|
977
1653
|
ensureSpace(state, lineHeight);
|
|
978
1654
|
const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
|
|
979
1655
|
state.currentPage.drawText(line, {
|
|
980
|
-
x:
|
|
1656
|
+
x: region.leftX + indent,
|
|
981
1657
|
y: pdfY,
|
|
982
1658
|
size: fontSize,
|
|
983
1659
|
font,
|
|
@@ -994,7 +1670,7 @@ function drawText(state, text, font, fontSize, indent = 0) {
|
|
|
994
1670
|
ensureSpace(state, lineHeight);
|
|
995
1671
|
const pdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
|
|
996
1672
|
state.currentPage.drawText(line, {
|
|
997
|
-
x:
|
|
1673
|
+
x: region.leftX + indent,
|
|
998
1674
|
y: pdfY,
|
|
999
1675
|
size: fontSize,
|
|
1000
1676
|
font,
|
|
@@ -1038,10 +1714,10 @@ function collectInlineSegments(content, fonts, fontSize) {
|
|
|
1038
1714
|
}
|
|
1039
1715
|
return segments;
|
|
1040
1716
|
}
|
|
1041
|
-
function layoutInlineSegments(state, segments, fontSize, indent = 0) {
|
|
1717
|
+
function layoutInlineSegments(state, segments, fontSize, indent = 0, region = DEFAULT_REGION) {
|
|
1042
1718
|
const textLineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
1043
|
-
const maxX =
|
|
1044
|
-
const startX =
|
|
1719
|
+
const maxX = region.leftX + region.width;
|
|
1720
|
+
const startX = region.leftX + indent;
|
|
1045
1721
|
let currentX = startX;
|
|
1046
1722
|
let currentLineHeight = 0;
|
|
1047
1723
|
let lineHasContent = false;
|
|
@@ -1146,13 +1822,318 @@ function layoutInlineSegments(state, segments, fontSize, indent = 0) {
|
|
|
1146
1822
|
}
|
|
1147
1823
|
flushLine();
|
|
1148
1824
|
}
|
|
1149
|
-
function processInlineContent(state, content, fontSize, indent = 0) {
|
|
1825
|
+
function processInlineContent(state, content, fontSize, indent = 0, region = DEFAULT_REGION) {
|
|
1150
1826
|
if (!content) return;
|
|
1151
1827
|
const segments = collectInlineSegments(content, state.fonts, fontSize);
|
|
1152
1828
|
if (segments.length === 0) return;
|
|
1153
|
-
layoutInlineSegments(state, segments, fontSize, indent);
|
|
1829
|
+
layoutInlineSegments(state, segments, fontSize, indent, region);
|
|
1830
|
+
}
|
|
1831
|
+
function extractCellText(cell) {
|
|
1832
|
+
const parts = [];
|
|
1833
|
+
for (const child of cell.content || []) {
|
|
1834
|
+
if (child.type === "paragraph" || child.type === "heading") {
|
|
1835
|
+
for (const inline of child.content || []) {
|
|
1836
|
+
if (inline.type === "text") {
|
|
1837
|
+
parts.push(inline.text || "");
|
|
1838
|
+
} else if (inline.type === "variableNode") {
|
|
1839
|
+
const label = inline.attrs?.varLabel || inline.attrs?.varName || "Variable";
|
|
1840
|
+
parts.push(`[${label}]`);
|
|
1841
|
+
} else if (inline.type === "fieldNode") {
|
|
1842
|
+
const label = inline.attrs?.fieldLabel || inline.attrs?.fieldName || "Field";
|
|
1843
|
+
parts.push(`[ ${label} ]`);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return parts.join("");
|
|
1154
1849
|
}
|
|
1155
|
-
function
|
|
1850
|
+
function isNumericText(text) {
|
|
1851
|
+
return /^\s*\$?\s*[\d,.]+\s*$/.test(text);
|
|
1852
|
+
}
|
|
1853
|
+
function measureCellHeight(cell, maxWidth, fonts, fontSize, isHeader) {
|
|
1854
|
+
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
1855
|
+
const font = isHeader ? fonts.bold : fonts.regular;
|
|
1856
|
+
let totalHeight = 0;
|
|
1857
|
+
for (const child of cell.content || []) {
|
|
1858
|
+
if (child.type === "paragraph" || child.type === "heading") {
|
|
1859
|
+
const text = extractCellText({ content: [child] });
|
|
1860
|
+
if (!text) {
|
|
1861
|
+
totalHeight += lineHeight;
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
const words = text.split(/\s+/);
|
|
1865
|
+
let line = "";
|
|
1866
|
+
let lineCount = 0;
|
|
1867
|
+
for (const word of words) {
|
|
1868
|
+
const testLine = line ? `${line} ${word}` : word;
|
|
1869
|
+
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
|
|
1870
|
+
if (testWidth > maxWidth && line) {
|
|
1871
|
+
lineCount++;
|
|
1872
|
+
line = word;
|
|
1873
|
+
} else {
|
|
1874
|
+
line = testLine;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (line) lineCount++;
|
|
1878
|
+
totalHeight += lineCount * lineHeight;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return Math.max(totalHeight, lineHeight);
|
|
1882
|
+
}
|
|
1883
|
+
function renderCellContent(state, cell, cellX, cellY, cellWidth, fonts, fontSize, isHeader, rightAlign, textColor = rgb(0, 0, 0)) {
|
|
1884
|
+
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
1885
|
+
const cellPadding = 4;
|
|
1886
|
+
const maxTextWidth = cellWidth - cellPadding * 2;
|
|
1887
|
+
let textX = cellX + cellPadding;
|
|
1888
|
+
let textY = cellY;
|
|
1889
|
+
for (const child of cell.content || []) {
|
|
1890
|
+
if (child.type === "paragraph" || child.type === "heading") {
|
|
1891
|
+
const segments = collectInlineSegments(
|
|
1892
|
+
child.content || [],
|
|
1893
|
+
isHeader ? { regular: fonts.bold, bold: fonts.bold, italic: fonts.boldItalic, boldItalic: fonts.boldItalic } : fonts,
|
|
1894
|
+
fontSize
|
|
1895
|
+
);
|
|
1896
|
+
if (rightAlign && segments.length > 0) {
|
|
1897
|
+
const fullText = segments.filter((s) => s.kind === "text").map((s) => s.text).join("");
|
|
1898
|
+
const textWidth = (isHeader ? fonts.bold : fonts.regular).widthOfTextAtSize(fullText, fontSize);
|
|
1899
|
+
if (textWidth <= maxTextWidth) {
|
|
1900
|
+
const pdfY = PAGE_HEIGHT - textY - fontSize;
|
|
1901
|
+
state.currentPage.drawText(fullText, {
|
|
1902
|
+
x: cellX + cellWidth - cellPadding - textWidth,
|
|
1903
|
+
y: pdfY,
|
|
1904
|
+
size: fontSize,
|
|
1905
|
+
font: isHeader ? fonts.bold : fonts.regular,
|
|
1906
|
+
color: textColor
|
|
1907
|
+
});
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
let currentX = textX;
|
|
1912
|
+
for (const seg of segments) {
|
|
1913
|
+
if (seg.kind === "break") {
|
|
1914
|
+
textY += lineHeight;
|
|
1915
|
+
currentX = textX;
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1918
|
+
if (seg.kind === "text") {
|
|
1919
|
+
const words = seg.text.split(/(\s+)/);
|
|
1920
|
+
const spaceWidth = seg.font.widthOfTextAtSize(" ", fontSize);
|
|
1921
|
+
for (const token of words) {
|
|
1922
|
+
if (!token) continue;
|
|
1923
|
+
if (/^\s+$/.test(token)) {
|
|
1924
|
+
currentX += spaceWidth;
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
const wordWidth = seg.font.widthOfTextAtSize(token, fontSize);
|
|
1928
|
+
if (currentX + wordWidth > textX + maxTextWidth && currentX > textX) {
|
|
1929
|
+
textY += lineHeight;
|
|
1930
|
+
currentX = textX;
|
|
1931
|
+
}
|
|
1932
|
+
const pdfY = PAGE_HEIGHT - textY - fontSize;
|
|
1933
|
+
state.currentPage.drawText(token, {
|
|
1934
|
+
x: currentX,
|
|
1935
|
+
y: pdfY,
|
|
1936
|
+
size: fontSize,
|
|
1937
|
+
font: seg.font,
|
|
1938
|
+
color: textColor
|
|
1939
|
+
});
|
|
1940
|
+
currentX += wordWidth;
|
|
1941
|
+
}
|
|
1942
|
+
} else if (seg.kind === "field") {
|
|
1943
|
+
const pdfY = PAGE_HEIGHT - textY - seg.height;
|
|
1944
|
+
if (state.drawFieldPlaceholders) {
|
|
1945
|
+
state.currentPage.drawRectangle({
|
|
1946
|
+
x: currentX,
|
|
1947
|
+
y: pdfY,
|
|
1948
|
+
width: seg.width,
|
|
1949
|
+
height: seg.height,
|
|
1950
|
+
borderColor: rgb(0.5, 0.5, 0.7),
|
|
1951
|
+
borderWidth: 0.5,
|
|
1952
|
+
color: rgb(0.95, 0.95, 1),
|
|
1953
|
+
borderDashArray: [4, 2]
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
const fieldId = seg.attrs.fieldId;
|
|
1957
|
+
if (fieldId) {
|
|
1958
|
+
state.fieldPositions.set(fieldId, {
|
|
1959
|
+
x: currentX,
|
|
1960
|
+
y: pdfY,
|
|
1961
|
+
width: seg.width,
|
|
1962
|
+
height: seg.height,
|
|
1963
|
+
page: state.pageIndex + 1
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
currentX += seg.width + 4;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
function renderTable(state, node, region = DEFAULT_REGION) {
|
|
1973
|
+
const tableRows = node.content || [];
|
|
1974
|
+
if (tableRows.length === 0) return;
|
|
1975
|
+
const borderless = node.attrs?.borderless === true;
|
|
1976
|
+
const subtotals = node.attrs?.subtotals === true;
|
|
1977
|
+
const fontSize = 10;
|
|
1978
|
+
const cellPadding = borderless ? 2 : 4;
|
|
1979
|
+
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
1980
|
+
const rowPadding = cellPadding * 2;
|
|
1981
|
+
const borderColor = state.theme.tableBorder;
|
|
1982
|
+
const headerBg = state.theme.tableHeaderBg;
|
|
1983
|
+
const headerTextColor = state.theme.tableHeaderText;
|
|
1984
|
+
let colCount = 0;
|
|
1985
|
+
for (const row of tableRows) {
|
|
1986
|
+
colCount = Math.max(colCount, row.content?.length || 0);
|
|
1987
|
+
}
|
|
1988
|
+
if (colCount === 0) return;
|
|
1989
|
+
const colMaxWidths = new Array(colCount).fill(0);
|
|
1990
|
+
for (const row of tableRows) {
|
|
1991
|
+
const cells = row.content || [];
|
|
1992
|
+
for (let c = 0; c < cells.length && c < colCount; c++) {
|
|
1993
|
+
const cell = cells[c];
|
|
1994
|
+
const isHeader = cell.type === "tableHeader";
|
|
1995
|
+
const text = extractCellText(cell);
|
|
1996
|
+
const font = isHeader ? state.fonts.bold : state.fonts.regular;
|
|
1997
|
+
const textWidth = font.widthOfTextAtSize(text, fontSize);
|
|
1998
|
+
colMaxWidths[c] = Math.max(colMaxWidths[c], textWidth + cellPadding * 2 + 4);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
const totalMeasured = colMaxWidths.reduce((a, b) => a + b, 0);
|
|
2002
|
+
const minColWidth = 40;
|
|
2003
|
+
let colWidths;
|
|
2004
|
+
if (totalMeasured <= region.width) {
|
|
2005
|
+
const scale = region.width / totalMeasured;
|
|
2006
|
+
colWidths = colMaxWidths.map((w) => Math.max(w * scale, minColWidth));
|
|
2007
|
+
} else {
|
|
2008
|
+
colWidths = colMaxWidths.map(
|
|
2009
|
+
(w) => Math.max(w / totalMeasured * region.width, minColWidth)
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
const colSum = colWidths.reduce((a, b) => a + b, 0);
|
|
2013
|
+
if (colSum > 0) {
|
|
2014
|
+
const normFactor = region.width / colSum;
|
|
2015
|
+
colWidths = colWidths.map((w) => w * normFactor);
|
|
2016
|
+
}
|
|
2017
|
+
const headerRow = tableRows[0];
|
|
2018
|
+
const hasHeader = headerRow?.content?.[0]?.type === "tableHeader";
|
|
2019
|
+
function renderRow(row, isHeaderRow) {
|
|
2020
|
+
const cells = row.content || [];
|
|
2021
|
+
const isHeader = isHeaderRow && hasHeader;
|
|
2022
|
+
let rowHeight = lineHeight;
|
|
2023
|
+
for (let c = 0; c < cells.length && c < colCount; c++) {
|
|
2024
|
+
const cellContentWidth = colWidths[c] - cellPadding * 2;
|
|
2025
|
+
const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, isHeader);
|
|
2026
|
+
rowHeight = Math.max(rowHeight, cellH);
|
|
2027
|
+
}
|
|
2028
|
+
rowHeight += rowPadding;
|
|
2029
|
+
ensureSpace(state, rowHeight);
|
|
2030
|
+
const rowTopY = state.y;
|
|
2031
|
+
let cellX = region.leftX;
|
|
2032
|
+
for (let c = 0; c < cells.length && c < colCount; c++) {
|
|
2033
|
+
const cellW = colWidths[c];
|
|
2034
|
+
const cell = cells[c];
|
|
2035
|
+
const pdfCellTop = PAGE_HEIGHT - MARGIN - rowTopY;
|
|
2036
|
+
const pdfCellBottom = pdfCellTop - rowHeight;
|
|
2037
|
+
if (isHeader && !borderless) {
|
|
2038
|
+
state.currentPage.drawRectangle({
|
|
2039
|
+
x: cellX,
|
|
2040
|
+
y: pdfCellBottom,
|
|
2041
|
+
width: cellW,
|
|
2042
|
+
height: rowHeight,
|
|
2043
|
+
color: headerBg
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
if (!borderless) {
|
|
2047
|
+
state.currentPage.drawRectangle({
|
|
2048
|
+
x: cellX,
|
|
2049
|
+
y: pdfCellBottom,
|
|
2050
|
+
width: cellW,
|
|
2051
|
+
height: rowHeight,
|
|
2052
|
+
borderColor,
|
|
2053
|
+
borderWidth: 0.5
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
const cellText = extractCellText(cell);
|
|
2057
|
+
const rightAlign = !isHeader && isNumericText(cellText);
|
|
2058
|
+
const contentY = MARGIN + rowTopY + cellPadding;
|
|
2059
|
+
const effectiveHeader = isHeader && !borderless;
|
|
2060
|
+
renderCellContent(
|
|
2061
|
+
state,
|
|
2062
|
+
cell,
|
|
2063
|
+
cellX,
|
|
2064
|
+
contentY,
|
|
2065
|
+
cellW,
|
|
2066
|
+
state.fonts,
|
|
2067
|
+
fontSize,
|
|
2068
|
+
effectiveHeader,
|
|
2069
|
+
rightAlign,
|
|
2070
|
+
effectiveHeader ? headerTextColor : rgb(0, 0, 0)
|
|
2071
|
+
);
|
|
2072
|
+
cellX += cellW;
|
|
2073
|
+
}
|
|
2074
|
+
state.y += rowHeight;
|
|
2075
|
+
}
|
|
2076
|
+
if (hasHeader && tableRows.length > 0) {
|
|
2077
|
+
renderRow(tableRows[0], true);
|
|
2078
|
+
}
|
|
2079
|
+
let lastPageIndex = state.pageIndex;
|
|
2080
|
+
const startIdx = hasHeader ? 1 : 0;
|
|
2081
|
+
const bodyRowCount = tableRows.length - startIdx;
|
|
2082
|
+
for (let r = startIdx; r < tableRows.length; r++) {
|
|
2083
|
+
const cells = tableRows[r].content || [];
|
|
2084
|
+
let estRowHeight = lineHeight;
|
|
2085
|
+
for (let c = 0; c < cells.length && c < colCount; c++) {
|
|
2086
|
+
const cellContentWidth = colWidths[c] - cellPadding * 2;
|
|
2087
|
+
const cellH = measureCellHeight(cells[c], cellContentWidth, state.fonts, fontSize, false);
|
|
2088
|
+
estRowHeight = Math.max(estRowHeight, cellH);
|
|
2089
|
+
}
|
|
2090
|
+
estRowHeight += rowPadding;
|
|
2091
|
+
if (state.y + estRowHeight > CONTENT_HEIGHT) {
|
|
2092
|
+
ensureSpace(state, estRowHeight + (hasHeader ? lineHeight + rowPadding : 0));
|
|
2093
|
+
if (state.pageIndex !== lastPageIndex && hasHeader) {
|
|
2094
|
+
renderRow(headerRow, true);
|
|
2095
|
+
lastPageIndex = state.pageIndex;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
const rowTopYBeforeRender = state.y;
|
|
2099
|
+
renderRow(tableRows[r], false);
|
|
2100
|
+
lastPageIndex = state.pageIndex;
|
|
2101
|
+
if (subtotals && colCount >= 2) {
|
|
2102
|
+
const bodyIdx = r - startIdx;
|
|
2103
|
+
const isFirstBody = bodyIdx === 0;
|
|
2104
|
+
const isLastBody = bodyIdx === bodyRowCount - 1;
|
|
2105
|
+
if (isFirstBody || isLastBody) {
|
|
2106
|
+
const valueStartX = region.leftX + colWidths[0];
|
|
2107
|
+
const valueEndX = region.leftX + colWidths.reduce((a, b) => a + b, 0);
|
|
2108
|
+
const pdfRowTopY = PAGE_HEIGHT - MARGIN - rowTopYBeforeRender;
|
|
2109
|
+
const overlineColor = rgb(0, 0, 0);
|
|
2110
|
+
if (isFirstBody) {
|
|
2111
|
+
state.currentPage.drawLine({
|
|
2112
|
+
start: { x: valueStartX, y: pdfRowTopY },
|
|
2113
|
+
end: { x: valueEndX, y: pdfRowTopY },
|
|
2114
|
+
thickness: 0.5,
|
|
2115
|
+
color: overlineColor
|
|
2116
|
+
});
|
|
2117
|
+
} else if (isLastBody) {
|
|
2118
|
+
state.currentPage.drawLine({
|
|
2119
|
+
start: { x: valueStartX, y: pdfRowTopY + 2 },
|
|
2120
|
+
end: { x: valueEndX, y: pdfRowTopY + 2 },
|
|
2121
|
+
thickness: 0.5,
|
|
2122
|
+
color: overlineColor
|
|
2123
|
+
});
|
|
2124
|
+
state.currentPage.drawLine({
|
|
2125
|
+
start: { x: valueStartX, y: pdfRowTopY },
|
|
2126
|
+
end: { x: valueEndX, y: pdfRowTopY },
|
|
2127
|
+
thickness: 0.5,
|
|
2128
|
+
color: overlineColor
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
state.y += 3;
|
|
2135
|
+
}
|
|
2136
|
+
function processBlock(state, node, region = DEFAULT_REGION) {
|
|
1156
2137
|
switch (node.type) {
|
|
1157
2138
|
case "heading": {
|
|
1158
2139
|
const level = node.attrs?.level || 1;
|
|
@@ -1169,34 +2150,34 @@ function processBlock(state, node) {
|
|
|
1169
2150
|
boldItalic: state.fonts.boldItalic
|
|
1170
2151
|
};
|
|
1171
2152
|
const segments = collectInlineSegments(node.content, headingFonts, fontSize);
|
|
1172
|
-
layoutInlineSegments(state, segments, fontSize);
|
|
2153
|
+
layoutInlineSegments(state, segments, fontSize, 0, region);
|
|
1173
2154
|
state.y += spacing / 2;
|
|
1174
2155
|
break;
|
|
1175
2156
|
}
|
|
1176
2157
|
case "paragraph": {
|
|
1177
|
-
const fontSize =
|
|
2158
|
+
const fontSize = BODY_FONT_SIZE;
|
|
1178
2159
|
if (!node.content || node.content.length === 0) {
|
|
1179
|
-
state.y += fontSize * LINE_HEIGHT_FACTOR;
|
|
2160
|
+
state.y += fontSize * LINE_HEIGHT_FACTOR * 0.65;
|
|
1180
2161
|
break;
|
|
1181
2162
|
}
|
|
1182
|
-
processInlineContent(state, node.content, fontSize);
|
|
1183
|
-
state.y +=
|
|
2163
|
+
processInlineContent(state, node.content, fontSize, 0, region);
|
|
2164
|
+
state.y += 2;
|
|
1184
2165
|
break;
|
|
1185
2166
|
}
|
|
1186
2167
|
case "bulletList": {
|
|
1187
2168
|
for (const item of node.content || []) {
|
|
1188
2169
|
for (const child of item.content || []) {
|
|
1189
|
-
const fontSize =
|
|
2170
|
+
const fontSize = BODY_FONT_SIZE;
|
|
1190
2171
|
ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
|
|
1191
2172
|
const bulletPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
|
|
1192
2173
|
state.currentPage.drawText("\u2022", {
|
|
1193
|
-
x:
|
|
2174
|
+
x: region.leftX + 8,
|
|
1194
2175
|
y: bulletPdfY,
|
|
1195
2176
|
size: fontSize,
|
|
1196
2177
|
font: state.fonts.regular,
|
|
1197
2178
|
color: rgb(0, 0, 0)
|
|
1198
2179
|
});
|
|
1199
|
-
processInlineContent(state, child.content, fontSize, 24);
|
|
2180
|
+
processInlineContent(state, child.content, fontSize, 24, region);
|
|
1200
2181
|
}
|
|
1201
2182
|
}
|
|
1202
2183
|
state.y += 4;
|
|
@@ -1206,17 +2187,17 @@ function processBlock(state, node) {
|
|
|
1206
2187
|
let num = 1;
|
|
1207
2188
|
for (const item of node.content || []) {
|
|
1208
2189
|
for (const child of item.content || []) {
|
|
1209
|
-
const fontSize =
|
|
2190
|
+
const fontSize = BODY_FONT_SIZE;
|
|
1210
2191
|
ensureSpace(state, fontSize * LINE_HEIGHT_FACTOR);
|
|
1211
2192
|
const numPdfY = PAGE_HEIGHT - MARGIN - state.y - fontSize;
|
|
1212
2193
|
state.currentPage.drawText(`${num}.`, {
|
|
1213
|
-
x:
|
|
2194
|
+
x: region.leftX + 4,
|
|
1214
2195
|
y: numPdfY,
|
|
1215
2196
|
size: fontSize,
|
|
1216
2197
|
font: state.fonts.regular,
|
|
1217
2198
|
color: rgb(0, 0, 0)
|
|
1218
2199
|
});
|
|
1219
|
-
processInlineContent(state, child.content, fontSize, 24);
|
|
2200
|
+
processInlineContent(state, child.content, fontSize, 24, region);
|
|
1220
2201
|
}
|
|
1221
2202
|
num++;
|
|
1222
2203
|
}
|
|
@@ -1226,10 +2207,10 @@ function processBlock(state, node) {
|
|
|
1226
2207
|
case "blockquote": {
|
|
1227
2208
|
const startY = state.y;
|
|
1228
2209
|
for (const child of node.content || []) {
|
|
1229
|
-
processInlineContent(state, child.content, 12, 16);
|
|
2210
|
+
processInlineContent(state, child.content, 12, 16, region);
|
|
1230
2211
|
}
|
|
1231
2212
|
const endY = state.y;
|
|
1232
|
-
const barX =
|
|
2213
|
+
const barX = region.leftX + 4;
|
|
1233
2214
|
const barTop = PAGE_HEIGHT - MARGIN - startY;
|
|
1234
2215
|
const barBottom = PAGE_HEIGHT - MARGIN - endY;
|
|
1235
2216
|
state.currentPage.drawLine({
|
|
@@ -1242,16 +2223,16 @@ function processBlock(state, node) {
|
|
|
1242
2223
|
break;
|
|
1243
2224
|
}
|
|
1244
2225
|
case "horizontalRule": {
|
|
1245
|
-
ensureSpace(state,
|
|
1246
|
-
state.y +=
|
|
2226
|
+
ensureSpace(state, 10);
|
|
2227
|
+
state.y += 4;
|
|
1247
2228
|
const ruleY = PAGE_HEIGHT - MARGIN - state.y;
|
|
1248
2229
|
state.currentPage.drawLine({
|
|
1249
|
-
start: { x:
|
|
1250
|
-
end: { x:
|
|
1251
|
-
thickness:
|
|
1252
|
-
color:
|
|
2230
|
+
start: { x: region.leftX, y: ruleY },
|
|
2231
|
+
end: { x: region.leftX + region.width, y: ruleY },
|
|
2232
|
+
thickness: 2.5,
|
|
2233
|
+
color: state.theme.rule
|
|
1253
2234
|
});
|
|
1254
|
-
state.y +=
|
|
2235
|
+
state.y += 4;
|
|
1255
2236
|
break;
|
|
1256
2237
|
}
|
|
1257
2238
|
case "codeBlock": {
|
|
@@ -1263,9 +2244,9 @@ function processBlock(state, node) {
|
|
|
1263
2244
|
ensureSpace(state, blockHeight);
|
|
1264
2245
|
const boxY = PAGE_HEIGHT - MARGIN - state.y - blockHeight;
|
|
1265
2246
|
state.currentPage.drawRectangle({
|
|
1266
|
-
x:
|
|
2247
|
+
x: region.leftX,
|
|
1267
2248
|
y: boxY,
|
|
1268
|
-
width:
|
|
2249
|
+
width: region.width,
|
|
1269
2250
|
height: blockHeight,
|
|
1270
2251
|
color: rgb(0.95, 0.95, 0.95),
|
|
1271
2252
|
borderColor: rgb(0.85, 0.85, 0.85),
|
|
@@ -1273,18 +2254,325 @@ function processBlock(state, node) {
|
|
|
1273
2254
|
});
|
|
1274
2255
|
state.y += 8;
|
|
1275
2256
|
for (const line of lines) {
|
|
1276
|
-
drawText(state, line || " ", state.fonts.regular, fontSize, 8);
|
|
2257
|
+
drawText(state, line || " ", state.fonts.regular, fontSize, 8, region);
|
|
1277
2258
|
}
|
|
1278
2259
|
state.y += 8;
|
|
1279
2260
|
break;
|
|
1280
2261
|
}
|
|
2262
|
+
case "table": {
|
|
2263
|
+
renderTable(state, node, region);
|
|
2264
|
+
break;
|
|
2265
|
+
}
|
|
2266
|
+
case "imageNode": {
|
|
2267
|
+
const src = node.attrs?.src || "";
|
|
2268
|
+
const image = src ? state.images.get(src) : void 0;
|
|
2269
|
+
if (!image) break;
|
|
2270
|
+
const natW = image.width;
|
|
2271
|
+
const natH = image.height;
|
|
2272
|
+
let w = parseFloat(node.attrs?.width || "") || 0;
|
|
2273
|
+
let h = parseFloat(node.attrs?.height || "") || 0;
|
|
2274
|
+
if (w && !h) h = natH / natW * w;
|
|
2275
|
+
else if (h && !w) w = natW / natH * h;
|
|
2276
|
+
else if (!w && !h) {
|
|
2277
|
+
w = natW;
|
|
2278
|
+
h = natH;
|
|
2279
|
+
}
|
|
2280
|
+
if (w > region.width) {
|
|
2281
|
+
h = h * (region.width / w);
|
|
2282
|
+
w = region.width;
|
|
2283
|
+
}
|
|
2284
|
+
const maxH = CONTENT_HEIGHT * 0.6;
|
|
2285
|
+
if (h > maxH) {
|
|
2286
|
+
w = w * (maxH / h);
|
|
2287
|
+
h = maxH;
|
|
2288
|
+
}
|
|
2289
|
+
ensureSpace(state, h + 4);
|
|
2290
|
+
const align = node.attrs?.align || "left";
|
|
2291
|
+
let x = region.leftX;
|
|
2292
|
+
if (align === "center") x = region.leftX + (region.width - w) / 2;
|
|
2293
|
+
else if (align === "right") x = region.leftX + region.width - w;
|
|
2294
|
+
const pdfY = PAGE_HEIGHT - MARGIN - state.y - h;
|
|
2295
|
+
state.currentPage.drawImage(image, { x, y: pdfY, width: w, height: h });
|
|
2296
|
+
state.y += h + 4;
|
|
2297
|
+
break;
|
|
2298
|
+
}
|
|
2299
|
+
// ─── Layout directives ─────────────────────────────────────────
|
|
2300
|
+
case "panel": {
|
|
2301
|
+
const title = node.attrs?.title || "";
|
|
2302
|
+
const border = node.attrs?.border || "solid";
|
|
2303
|
+
const headerStyle = node.attrs?.headerStyle || "";
|
|
2304
|
+
const padding = 8;
|
|
2305
|
+
const titleFontSize = 10;
|
|
2306
|
+
const titleHeight = title ? titleFontSize * LINE_HEIGHT_FACTOR + 6 : 0;
|
|
2307
|
+
const panelStartY = state.y;
|
|
2308
|
+
const panelStartPage = state.pageIndex;
|
|
2309
|
+
if (title) {
|
|
2310
|
+
state.y += titleHeight;
|
|
2311
|
+
}
|
|
2312
|
+
const isHeaderOnly = border === "none";
|
|
2313
|
+
if (!isHeaderOnly) {
|
|
2314
|
+
state.y += padding;
|
|
2315
|
+
}
|
|
2316
|
+
const innerRegion = {
|
|
2317
|
+
leftX: region.leftX + padding,
|
|
2318
|
+
width: region.width - padding * 2
|
|
2319
|
+
};
|
|
2320
|
+
if (!isHeaderOnly) {
|
|
2321
|
+
for (const child of node.content || []) {
|
|
2322
|
+
processBlock(state, child, innerRegion);
|
|
2323
|
+
}
|
|
2324
|
+
state.y += padding;
|
|
2325
|
+
}
|
|
2326
|
+
const startPage = state.pdfDoc.getPages()[panelStartPage];
|
|
2327
|
+
if (title) {
|
|
2328
|
+
const titleBarY = PAGE_HEIGHT - MARGIN - panelStartY - titleHeight;
|
|
2329
|
+
const isDarkHeader = headerStyle === "dark";
|
|
2330
|
+
startPage.drawRectangle({
|
|
2331
|
+
x: region.leftX,
|
|
2332
|
+
y: titleBarY,
|
|
2333
|
+
width: region.width,
|
|
2334
|
+
height: titleHeight,
|
|
2335
|
+
color: isDarkHeader ? state.theme.panelDarkHeaderBg : rgb(0.93, 0.93, 0.93),
|
|
2336
|
+
borderColor: state.theme.panelBorder,
|
|
2337
|
+
borderWidth: 0.75
|
|
2338
|
+
});
|
|
2339
|
+
startPage.drawText(title, {
|
|
2340
|
+
x: region.leftX + padding,
|
|
2341
|
+
y: titleBarY + 4,
|
|
2342
|
+
size: titleFontSize,
|
|
2343
|
+
font: state.fonts.bold,
|
|
2344
|
+
color: isDarkHeader ? state.theme.panelDarkHeaderText : rgb(0, 0, 0)
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
if (border !== "none") {
|
|
2348
|
+
const borderDashArray = border === "dashed" ? [4, 2] : void 0;
|
|
2349
|
+
if (panelStartPage === state.pageIndex) {
|
|
2350
|
+
const borderY = PAGE_HEIGHT - MARGIN - state.y;
|
|
2351
|
+
const panelHeight = state.y - panelStartY;
|
|
2352
|
+
startPage.drawRectangle({
|
|
2353
|
+
x: region.leftX,
|
|
2354
|
+
y: borderY,
|
|
2355
|
+
width: region.width,
|
|
2356
|
+
height: panelHeight,
|
|
2357
|
+
borderColor: state.theme.panelBorder,
|
|
2358
|
+
borderWidth: 0.75,
|
|
2359
|
+
borderDashArray
|
|
2360
|
+
});
|
|
2361
|
+
} else {
|
|
2362
|
+
const startBorderBottom = MARGIN;
|
|
2363
|
+
const startPanelHeight = PAGE_HEIGHT - MARGIN - panelStartY - startBorderBottom;
|
|
2364
|
+
startPage.drawRectangle({
|
|
2365
|
+
x: region.leftX,
|
|
2366
|
+
y: startBorderBottom,
|
|
2367
|
+
width: region.width,
|
|
2368
|
+
height: startPanelHeight,
|
|
2369
|
+
borderColor: state.theme.panelBorder,
|
|
2370
|
+
borderWidth: 0.75,
|
|
2371
|
+
borderDashArray
|
|
2372
|
+
});
|
|
2373
|
+
const contTop = PAGE_HEIGHT - MARGIN;
|
|
2374
|
+
const contBorderY = PAGE_HEIGHT - MARGIN - state.y;
|
|
2375
|
+
const contHeight = contTop - contBorderY;
|
|
2376
|
+
state.currentPage.drawRectangle({
|
|
2377
|
+
x: region.leftX,
|
|
2378
|
+
y: contBorderY,
|
|
2379
|
+
width: region.width,
|
|
2380
|
+
height: contHeight,
|
|
2381
|
+
borderColor: state.theme.panelBorder,
|
|
2382
|
+
borderWidth: 0.75,
|
|
2383
|
+
borderDashArray
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
state.y += 4;
|
|
2388
|
+
break;
|
|
2389
|
+
}
|
|
2390
|
+
case "columns": {
|
|
2391
|
+
const split = parseInt(node.attrs?.split || "50", 10);
|
|
2392
|
+
const padX = parseFloat(node.attrs?.padX || "0") || 0;
|
|
2393
|
+
const columns = (node.content || []).filter((c) => c.type === "column");
|
|
2394
|
+
if (columns.length === 0) break;
|
|
2395
|
+
const colRegionLeftX = region.leftX + padX;
|
|
2396
|
+
const colRegionWidth = region.width - 2 * padX;
|
|
2397
|
+
const gap = 12;
|
|
2398
|
+
const columnsStartY = state.y;
|
|
2399
|
+
const columnsStartPage = state.pageIndex;
|
|
2400
|
+
let colWidths;
|
|
2401
|
+
if (columns.length === 2) {
|
|
2402
|
+
const firstWidth = (colRegionWidth - gap) * (split / 100);
|
|
2403
|
+
const secondWidth = colRegionWidth - gap - firstWidth;
|
|
2404
|
+
colWidths = [firstWidth, secondWidth];
|
|
2405
|
+
} else {
|
|
2406
|
+
const eachWidth = (colRegionWidth - gap * (columns.length - 1)) / columns.length;
|
|
2407
|
+
colWidths = columns.map(() => eachWidth);
|
|
2408
|
+
}
|
|
2409
|
+
let maxEndY = columnsStartY;
|
|
2410
|
+
let colX = colRegionLeftX;
|
|
2411
|
+
for (let ci = 0; ci < columns.length; ci++) {
|
|
2412
|
+
const col = columns[ci];
|
|
2413
|
+
const colWidth = colWidths[ci];
|
|
2414
|
+
const colRegion = { leftX: colX, width: colWidth };
|
|
2415
|
+
state.y = columnsStartY;
|
|
2416
|
+
state.pageIndex = columnsStartPage;
|
|
2417
|
+
state.currentPage = state.pdfDoc.getPages()[state.pageIndex];
|
|
2418
|
+
const padTop = parseFloat(col.attrs?.padTop || "0") || 0;
|
|
2419
|
+
if (padTop > 0) {
|
|
2420
|
+
state.y += padTop * BODY_FONT_SIZE * LINE_HEIGHT_FACTOR;
|
|
2421
|
+
}
|
|
2422
|
+
for (const child of col.content || []) {
|
|
2423
|
+
processBlock(state, child, colRegion);
|
|
2424
|
+
}
|
|
2425
|
+
maxEndY = Math.max(maxEndY, state.y);
|
|
2426
|
+
colX += colWidth + gap;
|
|
2427
|
+
}
|
|
2428
|
+
state.y = maxEndY;
|
|
2429
|
+
state.y += 4;
|
|
2430
|
+
break;
|
|
2431
|
+
}
|
|
2432
|
+
case "column": {
|
|
2433
|
+
for (const child of node.content || []) {
|
|
2434
|
+
processBlock(state, child, region);
|
|
2435
|
+
}
|
|
2436
|
+
break;
|
|
2437
|
+
}
|
|
2438
|
+
case "watermark": {
|
|
2439
|
+
break;
|
|
2440
|
+
}
|
|
2441
|
+
case "subtotalsBlock": {
|
|
2442
|
+
const stFontSize = BODY_FONT_SIZE;
|
|
2443
|
+
const stLineHeight = stFontSize * LINE_HEIGHT_FACTOR;
|
|
2444
|
+
const valueRightX = region.leftX + region.width;
|
|
2445
|
+
const rows = (node.content || []).filter(
|
|
2446
|
+
(c) => c.type === "paragraph" && c.content?.length
|
|
2447
|
+
);
|
|
2448
|
+
const parsed = [];
|
|
2449
|
+
for (const row of rows) {
|
|
2450
|
+
let label = "";
|
|
2451
|
+
let value = "";
|
|
2452
|
+
for (const seg of row.content || []) {
|
|
2453
|
+
if (seg.type === "text") {
|
|
2454
|
+
const isBold = seg.marks?.some((m) => m.type === "bold");
|
|
2455
|
+
if (isBold) {
|
|
2456
|
+
label += seg.text || "";
|
|
2457
|
+
} else {
|
|
2458
|
+
const t = (seg.text || "").trim();
|
|
2459
|
+
if (t) value += (value ? " " : "") + t;
|
|
2460
|
+
}
|
|
2461
|
+
} else if (seg.type === "variableNode") {
|
|
2462
|
+
const varLabel = seg.attrs?.varLabel || seg.attrs?.varName || "";
|
|
2463
|
+
const isBoldVar = seg.marks?.some((m) => m.type === "bold");
|
|
2464
|
+
if (isBoldVar) {
|
|
2465
|
+
label += varLabel;
|
|
2466
|
+
} else {
|
|
2467
|
+
value += (value ? " " : "") + `[${varLabel}]`;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
parsed.push({ label: label.trim(), value: value.trim() });
|
|
2472
|
+
}
|
|
2473
|
+
let maxLabelWidth = 0;
|
|
2474
|
+
for (const { label } of parsed) {
|
|
2475
|
+
if (label) {
|
|
2476
|
+
const w = state.fonts.bold.widthOfTextAtSize(label, stFontSize);
|
|
2477
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const gap = 12;
|
|
2481
|
+
const labelX = region.width > 300 ? Math.max(region.leftX + region.width - maxLabelWidth - gap - 80, region.leftX + region.width * 0.4) : region.leftX + 4;
|
|
2482
|
+
const valueColStartX = labelX + maxLabelWidth + gap;
|
|
2483
|
+
const isLastRowTotal = parsed.length > 1;
|
|
2484
|
+
for (let ri = 0; ri < parsed.length; ri++) {
|
|
2485
|
+
const isTotal = isLastRowTotal && ri === parsed.length - 1;
|
|
2486
|
+
if (isTotal) {
|
|
2487
|
+
state.y += stLineHeight * 0.6;
|
|
2488
|
+
}
|
|
2489
|
+
ensureSpace(state, stLineHeight + (isTotal ? 8 : 4));
|
|
2490
|
+
const { label, value } = parsed[ri];
|
|
2491
|
+
const pdfY = PAGE_HEIGHT - MARGIN - state.y - stFontSize;
|
|
2492
|
+
if (isTotal) {
|
|
2493
|
+
state.currentPage.drawLine({
|
|
2494
|
+
start: { x: valueColStartX, y: pdfY + stFontSize + 6 },
|
|
2495
|
+
end: { x: valueRightX, y: pdfY + stFontSize + 6 },
|
|
2496
|
+
thickness: 0.75,
|
|
2497
|
+
color: rgb(0, 0, 0)
|
|
2498
|
+
});
|
|
2499
|
+
state.currentPage.drawLine({
|
|
2500
|
+
start: { x: valueColStartX, y: pdfY + stFontSize + 3 },
|
|
2501
|
+
end: { x: valueRightX, y: pdfY + stFontSize + 3 },
|
|
2502
|
+
thickness: 0.75,
|
|
2503
|
+
color: rgb(0, 0, 0)
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
if (label) {
|
|
2507
|
+
state.currentPage.drawText(label, {
|
|
2508
|
+
x: labelX,
|
|
2509
|
+
y: pdfY,
|
|
2510
|
+
size: stFontSize,
|
|
2511
|
+
font: state.fonts.bold,
|
|
2512
|
+
color: rgb(0, 0, 0)
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
if (value) {
|
|
2516
|
+
const valueFont = state.fonts.bold;
|
|
2517
|
+
const valueWidth = valueFont.widthOfTextAtSize(value, stFontSize);
|
|
2518
|
+
state.currentPage.drawText(value, {
|
|
2519
|
+
x: valueRightX - valueWidth,
|
|
2520
|
+
y: pdfY,
|
|
2521
|
+
size: stFontSize,
|
|
2522
|
+
font: valueFont,
|
|
2523
|
+
color: rgb(0, 0, 0)
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
state.y += stLineHeight;
|
|
2527
|
+
}
|
|
2528
|
+
state.y += 4;
|
|
2529
|
+
break;
|
|
2530
|
+
}
|
|
2531
|
+
case "repeatBlock":
|
|
2532
|
+
if (node.content) {
|
|
2533
|
+
for (const child of node.content) {
|
|
2534
|
+
processBlock(state, child, region);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
break;
|
|
1281
2538
|
default:
|
|
1282
2539
|
if (node.content) {
|
|
1283
|
-
processInlineContent(state, node.content, 12);
|
|
2540
|
+
processInlineContent(state, node.content, 12, 0, region);
|
|
1284
2541
|
}
|
|
1285
2542
|
break;
|
|
1286
2543
|
}
|
|
1287
2544
|
}
|
|
2545
|
+
function renderWatermarksOnPage(page, fonts, watermarkNodes) {
|
|
2546
|
+
for (const wmNode of watermarkNodes) {
|
|
2547
|
+
const text = wmNode.attrs?.text || "";
|
|
2548
|
+
if (!text) continue;
|
|
2549
|
+
const opacity = parseFloat(wmNode.attrs?.opacity || "0.15");
|
|
2550
|
+
const angle = parseFloat(wmNode.attrs?.angle || "-45");
|
|
2551
|
+
const fontSize = 100;
|
|
2552
|
+
const { width, height } = page.getSize();
|
|
2553
|
+
const centerX = width / 2;
|
|
2554
|
+
const centerY = height / 2;
|
|
2555
|
+
const textWidth = fonts.bold.widthOfTextAtSize(text, fontSize);
|
|
2556
|
+
page.drawText(text, {
|
|
2557
|
+
x: centerX - textWidth / 2 * Math.cos(angle * Math.PI / 180),
|
|
2558
|
+
y: centerY - textWidth / 2 * Math.sin(angle * Math.PI / 180),
|
|
2559
|
+
size: fontSize,
|
|
2560
|
+
font: fonts.bold,
|
|
2561
|
+
color: rgb(0.5, 0.5, 0.5),
|
|
2562
|
+
opacity,
|
|
2563
|
+
rotate: degrees(angle)
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
function collectWatermarkNodes(content) {
|
|
2568
|
+
const nodes = [];
|
|
2569
|
+
for (const block of content.content || []) {
|
|
2570
|
+
if (block.type === "watermark") {
|
|
2571
|
+
nodes.push(block);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
return nodes;
|
|
2575
|
+
}
|
|
1288
2576
|
function ensureFieldSuffix(fieldName, suffix) {
|
|
1289
2577
|
const cleanName = fieldName.replace(/_signature$/i, "").replace(/_initials$/i, "").replace(/_date$/i, "");
|
|
1290
2578
|
return `${cleanName}${suffix}`;
|
|
@@ -1394,6 +2682,24 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
|
|
|
1394
2682
|
PDFString$1.of(field.label)
|
|
1395
2683
|
);
|
|
1396
2684
|
}
|
|
2685
|
+
const initialsFontSize = Math.min(
|
|
2686
|
+
72,
|
|
2687
|
+
Math.max(8, Math.round(position.height * 0.55))
|
|
2688
|
+
);
|
|
2689
|
+
try {
|
|
2690
|
+
initialsField.setFontSize(initialsFontSize);
|
|
2691
|
+
const acro = initialsField;
|
|
2692
|
+
const daRef = acro.acroField.dict.get(PDFName$1.of("DA"));
|
|
2693
|
+
if (daRef) {
|
|
2694
|
+
const daStr = daRef.value;
|
|
2695
|
+
if (typeof daStr === "string") {
|
|
2696
|
+
const newDa = daStr.replace(/\/Helv\b/, "/TiBo").replace(/\/HeBo\b/, "/TiBo");
|
|
2697
|
+
acro.acroField.dict.set(PDFName$1.of("DA"), PDFString$1.of(newDa));
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
} catch (fontError) {
|
|
2701
|
+
console.warn(`Failed to set initials font:`, fontError);
|
|
2702
|
+
}
|
|
1397
2703
|
initialsField.enableReadOnly();
|
|
1398
2704
|
if (field.required) initialsField.enableRequired();
|
|
1399
2705
|
break;
|
|
@@ -1497,47 +2803,367 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
|
|
|
1497
2803
|
} catch (err) {
|
|
1498
2804
|
warnings.push(`Failed to create ${field.type} field "${field.name}": ${getErrorMessage(err)}`);
|
|
1499
2805
|
}
|
|
1500
|
-
}
|
|
1501
|
-
initPdfMetadata({ PDFName: PDFName$1, PDFString: PDFString$1 });
|
|
1502
|
-
const metadata = buildMetadataObject(fields, actualFieldNames);
|
|
1503
|
-
setSigniphiMetadata(pdfDoc, metadata);
|
|
1504
|
-
return warnings;
|
|
1505
|
-
}
|
|
1506
|
-
async function generatePdfFromContent(content, options = {}) {
|
|
1507
|
-
const { drawFieldPlaceholders = false, embedFormFields = false, fields = [] } = options;
|
|
1508
|
-
const
|
|
1509
|
-
const
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
2806
|
+
}
|
|
2807
|
+
initPdfMetadata({ PDFName: PDFName$1, PDFString: PDFString$1 });
|
|
2808
|
+
const metadata = buildMetadataObject(fields, actualFieldNames);
|
|
2809
|
+
setSigniphiMetadata(pdfDoc, metadata);
|
|
2810
|
+
return warnings;
|
|
2811
|
+
}
|
|
2812
|
+
async function generatePdfFromContent(content, options = {}) {
|
|
2813
|
+
const { drawFieldPlaceholders = false, embedFormFields = false, fields = [], header, footer } = options;
|
|
2814
|
+
const theme = resolveTheme(options.theme);
|
|
2815
|
+
const pdfDoc = await PDFDocument.create();
|
|
2816
|
+
const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
|
2817
|
+
const fonts = {
|
|
2818
|
+
regular: await pdfDoc.embedFont(StandardFonts.Helvetica),
|
|
2819
|
+
bold: await pdfDoc.embedFont(StandardFonts.HelveticaBold),
|
|
2820
|
+
italic: await pdfDoc.embedFont(StandardFonts.HelveticaOblique),
|
|
2821
|
+
boldItalic: await pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique)
|
|
2822
|
+
};
|
|
2823
|
+
const watermarkNodes = collectWatermarkNodes(content);
|
|
2824
|
+
const { images, warnings: imageWarnings } = await preloadImages(pdfDoc, content);
|
|
2825
|
+
const state = {
|
|
2826
|
+
currentPage: firstPage,
|
|
2827
|
+
pageIndex: 0,
|
|
2828
|
+
y: 0,
|
|
2829
|
+
fonts,
|
|
2830
|
+
pdfDoc,
|
|
2831
|
+
fieldPositions: /* @__PURE__ */ new Map(),
|
|
2832
|
+
drawFieldPlaceholders,
|
|
2833
|
+
watermarkNodes,
|
|
2834
|
+
theme,
|
|
2835
|
+
images
|
|
2836
|
+
};
|
|
2837
|
+
renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
|
|
2838
|
+
if (content.content) {
|
|
2839
|
+
for (const block of content.content) {
|
|
2840
|
+
processBlock(state, block);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
const allPages = pdfDoc.getPages();
|
|
2844
|
+
const totalPages = allPages.length;
|
|
2845
|
+
const pageNumFontSize = 9;
|
|
2846
|
+
const hfFontSize = 8;
|
|
2847
|
+
const hfColor = rgb(0.45, 0.45, 0.45);
|
|
2848
|
+
for (let i = 0; i < totalPages; i++) {
|
|
2849
|
+
const page = allPages[i];
|
|
2850
|
+
const label = `Page ${i + 1} of ${totalPages}`;
|
|
2851
|
+
const labelWidth = fonts.bold.widthOfTextAtSize(label, pageNumFontSize);
|
|
2852
|
+
page.drawText(label, {
|
|
2853
|
+
x: PAGE_WIDTH - MARGIN - labelWidth,
|
|
2854
|
+
y: MARGIN / 2 - pageNumFontSize / 2,
|
|
2855
|
+
size: pageNumFontSize,
|
|
2856
|
+
font: fonts.bold,
|
|
2857
|
+
color: rgb(0, 0, 0)
|
|
2858
|
+
});
|
|
2859
|
+
if (header && (header.left || header.right) && !(header.skipFirstPage && i === 0)) {
|
|
2860
|
+
const headerBaseY = PAGE_HEIGHT - MARGIN + 10;
|
|
2861
|
+
if (header.left) {
|
|
2862
|
+
page.drawText(header.left, {
|
|
2863
|
+
x: MARGIN,
|
|
2864
|
+
y: headerBaseY,
|
|
2865
|
+
size: pageNumFontSize,
|
|
2866
|
+
font: fonts.bold,
|
|
2867
|
+
color: theme.pageHeaderText
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
if (header.right) {
|
|
2871
|
+
const rightWidth = fonts.regular.widthOfTextAtSize(header.right, hfFontSize);
|
|
2872
|
+
page.drawText(header.right, {
|
|
2873
|
+
x: PAGE_WIDTH - MARGIN - rightWidth,
|
|
2874
|
+
y: headerBaseY,
|
|
2875
|
+
size: hfFontSize,
|
|
2876
|
+
font: fonts.regular,
|
|
2877
|
+
color: hfColor
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
page.drawLine({
|
|
2881
|
+
start: { x: MARGIN, y: headerBaseY - 6 },
|
|
2882
|
+
end: { x: PAGE_WIDTH - MARGIN, y: headerBaseY - 6 },
|
|
2883
|
+
thickness: 0.75,
|
|
2884
|
+
color: theme.pageHeaderRule
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
if (footer?.left) {
|
|
2888
|
+
page.drawText(footer.left, {
|
|
2889
|
+
x: MARGIN,
|
|
2890
|
+
y: MARGIN / 2 - pageNumFontSize / 2,
|
|
2891
|
+
size: hfFontSize,
|
|
2892
|
+
font: fonts.regular,
|
|
2893
|
+
color: hfColor
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
let fieldWarnings;
|
|
2898
|
+
if (embedFormFields && fields.length > 0) {
|
|
2899
|
+
const warnings = await addFormFields(pdfDoc, state.fieldPositions, fields);
|
|
2900
|
+
if (warnings.length > 0) fieldWarnings = warnings;
|
|
2901
|
+
}
|
|
2902
|
+
const pdfBytes = new Uint8Array(await pdfDoc.save({ useObjectStreams: false }));
|
|
2903
|
+
return {
|
|
2904
|
+
pdfBytes,
|
|
2905
|
+
fieldPositions: state.fieldPositions,
|
|
2906
|
+
fieldWarnings,
|
|
2907
|
+
imageWarnings: imageWarnings.length > 0 ? imageWarnings : void 0
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
// src/utils/template-pipeline.ts
|
|
2912
|
+
async function generatePdfFromTiptap(content, values, options = {}) {
|
|
2913
|
+
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
|
|
2914
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
2915
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
2916
|
+
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
2917
|
+
const fields = extractFieldsFromContent(replaced);
|
|
2918
|
+
const result = await generatePdfFromContent(replaced, {
|
|
2919
|
+
embedFormFields: fields.length > 0,
|
|
2920
|
+
fields,
|
|
2921
|
+
theme: options.theme,
|
|
2922
|
+
header: options.header,
|
|
2923
|
+
footer: options.footer,
|
|
2924
|
+
drawFieldPlaceholders: options.drawFieldPlaceholders
|
|
2925
|
+
});
|
|
2926
|
+
return { pdfBytes: result.pdfBytes, imageWarnings: result.imageWarnings };
|
|
2927
|
+
}
|
|
2928
|
+
async function generatePdfFromMarkdown(markdown, values, options = {}) {
|
|
2929
|
+
const content = markdownToTiptap(markdown);
|
|
2930
|
+
return generatePdfFromTiptap(content, values, options);
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// src/utils/markdown-writer.ts
|
|
2934
|
+
function serializeFieldToken(attrs) {
|
|
2935
|
+
const parts = ["field"];
|
|
2936
|
+
if (attrs.fieldType) parts.push(`type:${attrs.fieldType}`);
|
|
2937
|
+
if (attrs.fieldName) parts.push(`name:${attrs.fieldName}`);
|
|
2938
|
+
if (attrs.fieldLabel) parts.push(`label:${attrs.fieldLabel}`);
|
|
2939
|
+
if (attrs.fieldId) parts.push(`id:${attrs.fieldId}`);
|
|
2940
|
+
if (attrs.required) parts.push(`required:true`);
|
|
2941
|
+
if (attrs.options) parts.push(`options:${attrs.options}`);
|
|
2942
|
+
if (attrs.fontSize) parts.push(`fontSize:${attrs.fontSize}`);
|
|
2943
|
+
if (attrs.placeholder) parts.push(`placeholder:${attrs.placeholder}`);
|
|
2944
|
+
if (attrs.defaultValue) parts.push(`defaultValue:${attrs.defaultValue}`);
|
|
2945
|
+
if (attrs.multiline) parts.push(`multiline:true`);
|
|
2946
|
+
if (attrs.maxLength) parts.push(`maxLength:${attrs.maxLength}`);
|
|
2947
|
+
if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
|
|
2948
|
+
return `{{${parts.join("|")}}}`;
|
|
2949
|
+
}
|
|
2950
|
+
function serializeImageNode(attrs) {
|
|
2951
|
+
const hasExtras = attrs.varName || attrs.width || attrs.height || attrs.align;
|
|
2952
|
+
if (!hasExtras && attrs.src) {
|
|
2953
|
+
return ``;
|
|
2954
|
+
}
|
|
2955
|
+
const parts = ["image"];
|
|
2956
|
+
if (attrs.varName) parts.push(`var:${attrs.varName}`);
|
|
2957
|
+
else if (attrs.src) parts.push(`src:${attrs.src}`);
|
|
2958
|
+
if (attrs.alt) parts.push(`alt:${attrs.alt}`);
|
|
2959
|
+
if (attrs.width) parts.push(`width:${attrs.width}`);
|
|
2960
|
+
if (attrs.height) parts.push(`height:${attrs.height}`);
|
|
2961
|
+
if (attrs.align) parts.push(`align:${attrs.align}`);
|
|
2962
|
+
return `{{${parts.join("|")}}}`;
|
|
2963
|
+
}
|
|
2964
|
+
function serializeVariableToken(attrs) {
|
|
2965
|
+
const parts = ["var"];
|
|
2966
|
+
if (attrs.varName) parts.push(`name:${attrs.varName}`);
|
|
2967
|
+
if (attrs.varLabel) parts.push(`label:${attrs.varLabel}`);
|
|
2968
|
+
if (attrs.varDefault) parts.push(`default:${attrs.varDefault}`);
|
|
2969
|
+
if (attrs.suppressZero === "true") parts.push(`suppress:zero`);
|
|
2970
|
+
if (attrs.format) parts.push(`format:${attrs.format}`);
|
|
2971
|
+
if (attrs.block === "true") parts.push(`block:true`);
|
|
2972
|
+
return `{{${parts.join("|")}}}`;
|
|
2973
|
+
}
|
|
2974
|
+
function serializeInline(content) {
|
|
2975
|
+
if (!content) return "";
|
|
2976
|
+
let result = "";
|
|
2977
|
+
for (const node of content) {
|
|
2978
|
+
if (node.type === "fieldNode") {
|
|
2979
|
+
result += serializeFieldToken(node.attrs || {});
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
if (node.type === "imageNode") {
|
|
2983
|
+
result += serializeImageNode(node.attrs || {});
|
|
2984
|
+
continue;
|
|
2985
|
+
}
|
|
2986
|
+
if (node.type === "variableNode") {
|
|
2987
|
+
let token = serializeVariableToken(node.attrs || {});
|
|
2988
|
+
const varMarks = node.marks || [];
|
|
2989
|
+
const isBold = varMarks.some((m) => m.type === "bold");
|
|
2990
|
+
const isItalic = varMarks.some((m) => m.type === "italic");
|
|
2991
|
+
const isUnderline = varMarks.some((m) => m.type === "underline");
|
|
2992
|
+
if (isBold && isItalic) token = `***${token}***`;
|
|
2993
|
+
else if (isBold) token = `**${token}**`;
|
|
2994
|
+
else if (isItalic) token = `*${token}*`;
|
|
2995
|
+
if (isUnderline) token = `__${token}__`;
|
|
2996
|
+
result += token;
|
|
2997
|
+
continue;
|
|
2998
|
+
}
|
|
2999
|
+
if (node.type === "text") {
|
|
3000
|
+
let text = node.text || "";
|
|
3001
|
+
const marks = node.marks || [];
|
|
3002
|
+
for (const mark of marks) {
|
|
3003
|
+
if (mark.type === "bold") text = `**${text}**`;
|
|
3004
|
+
else if (mark.type === "italic") text = `*${text}*`;
|
|
3005
|
+
else if (mark.type === "underline") text = `__${text}__`;
|
|
3006
|
+
else if (mark.type === "code") text = `\`${text}\``;
|
|
3007
|
+
}
|
|
3008
|
+
result += text;
|
|
3009
|
+
}
|
|
3010
|
+
if (node.type === "hardBreak") {
|
|
3011
|
+
result += " \n";
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
return result;
|
|
3015
|
+
}
|
|
3016
|
+
function serializeBlock(node) {
|
|
3017
|
+
switch (node.type) {
|
|
3018
|
+
case "heading": {
|
|
3019
|
+
const level = node.attrs?.level || 1;
|
|
3020
|
+
const prefix = "#".repeat(level);
|
|
3021
|
+
return `${prefix} ${serializeInline(node.content)}`;
|
|
3022
|
+
}
|
|
3023
|
+
case "paragraph": {
|
|
3024
|
+
const inline = serializeInline(node.content);
|
|
3025
|
+
return inline;
|
|
3026
|
+
}
|
|
3027
|
+
case "bulletList": {
|
|
3028
|
+
return (node.content || []).map((item) => {
|
|
3029
|
+
const inner = (item.content || []).map(serializeBlock).join("\n");
|
|
3030
|
+
return `- ${inner}`;
|
|
3031
|
+
}).join("\n");
|
|
3032
|
+
}
|
|
3033
|
+
case "orderedList": {
|
|
3034
|
+
return (node.content || []).map((item, i) => {
|
|
3035
|
+
const inner = (item.content || []).map(serializeBlock).join("\n");
|
|
3036
|
+
return `${i + 1}. ${inner}`;
|
|
3037
|
+
}).join("\n");
|
|
3038
|
+
}
|
|
3039
|
+
case "listItem": {
|
|
3040
|
+
return (node.content || []).map(serializeBlock).join("\n");
|
|
3041
|
+
}
|
|
3042
|
+
case "blockquote": {
|
|
3043
|
+
return (node.content || []).map(serializeBlock).map((line) => `> ${line}`).join("\n");
|
|
3044
|
+
}
|
|
3045
|
+
case "codeBlock": {
|
|
3046
|
+
const text = serializeInline(node.content);
|
|
3047
|
+
return `\`\`\`
|
|
3048
|
+
${text}
|
|
3049
|
+
\`\`\``;
|
|
3050
|
+
}
|
|
3051
|
+
case "horizontalRule": {
|
|
3052
|
+
return "---";
|
|
3053
|
+
}
|
|
3054
|
+
case "table": {
|
|
3055
|
+
const rows = node.content || [];
|
|
3056
|
+
const isBorderless = node.attrs?.borderless === true;
|
|
3057
|
+
const isSubtotals = node.attrs?.subtotals === true;
|
|
3058
|
+
const serializedRows = [];
|
|
3059
|
+
if (isBorderless && rows.length > 0) {
|
|
3060
|
+
const colCount = rows[0].content?.length || 0;
|
|
3061
|
+
const markerCell = isSubtotals ? "_" : "";
|
|
3062
|
+
const markerRow = "| " + new Array(colCount).fill(markerCell).join(" | ") + " |";
|
|
3063
|
+
const sep = "|" + new Array(colCount).fill("---").join("|") + "|";
|
|
3064
|
+
serializedRows.push(markerRow);
|
|
3065
|
+
serializedRows.push(sep);
|
|
3066
|
+
}
|
|
3067
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
3068
|
+
const row = rows[ri];
|
|
3069
|
+
const cells = (row.content || []).map((cell) => {
|
|
3070
|
+
const inner = (cell.content || []).map(serializeBlock).join("");
|
|
3071
|
+
return inner;
|
|
3072
|
+
});
|
|
3073
|
+
serializedRows.push(`| ${cells.join(" | ")} |`);
|
|
3074
|
+
if (!isBorderless && ri === 0 && row.content?.[0]?.type === "tableHeader") {
|
|
3075
|
+
const sep = cells.map(() => "------").join("|");
|
|
3076
|
+
serializedRows.push(`|${sep}|`);
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
return serializedRows.join("\n");
|
|
3080
|
+
}
|
|
3081
|
+
case "tableRow": {
|
|
3082
|
+
const cells = (node.content || []).map((cell) => {
|
|
3083
|
+
const inner = (cell.content || []).map(serializeBlock).join("");
|
|
3084
|
+
return inner;
|
|
3085
|
+
});
|
|
3086
|
+
return `| ${cells.join(" | ")} |`;
|
|
3087
|
+
}
|
|
3088
|
+
case "tableHeader":
|
|
3089
|
+
case "tableCell": {
|
|
3090
|
+
return (node.content || []).map(serializeBlock).join("");
|
|
3091
|
+
}
|
|
3092
|
+
case "imageNode": {
|
|
3093
|
+
return serializeImageNode(node.attrs || {});
|
|
3094
|
+
}
|
|
3095
|
+
case "watermark": {
|
|
3096
|
+
const attrs = node.attrs || {};
|
|
3097
|
+
const parts = [];
|
|
3098
|
+
if (attrs.text) parts.push(`text:${attrs.text}`);
|
|
3099
|
+
if (attrs.opacity && attrs.opacity !== "0.15") parts.push(`opacity:${attrs.opacity}`);
|
|
3100
|
+
if (attrs.angle && attrs.angle !== "-45") parts.push(`angle:${attrs.angle}`);
|
|
3101
|
+
return `:::watermark{${parts.join("|")}}`;
|
|
3102
|
+
}
|
|
3103
|
+
case "panel": {
|
|
3104
|
+
const attrs = node.attrs || {};
|
|
3105
|
+
const parts = [];
|
|
3106
|
+
if (attrs.title) parts.push(`title:${attrs.title}`);
|
|
3107
|
+
if (attrs.border && attrs.border !== "solid") parts.push(`border:${attrs.border}`);
|
|
3108
|
+
else if (attrs.border === "solid") parts.push(`border:solid`);
|
|
3109
|
+
if (attrs.headerStyle) parts.push(`headerStyle:${attrs.headerStyle}`);
|
|
3110
|
+
const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
|
|
3111
|
+
const children = (node.content || []).map(serializeBlock).join("\n\n");
|
|
3112
|
+
return `:::panel${attrStr}
|
|
3113
|
+
${children}
|
|
3114
|
+
:::`;
|
|
3115
|
+
}
|
|
3116
|
+
case "repeatBlock": {
|
|
3117
|
+
const attrs = node.attrs || {};
|
|
3118
|
+
const parts = [];
|
|
3119
|
+
if (attrs.data) parts.push(`data:${attrs.data}`);
|
|
3120
|
+
const attrStr = parts.length > 0 ? `{${parts.join("|")}}` : "";
|
|
3121
|
+
const children = (node.content || []).map(serializeBlock).join("\n\n");
|
|
3122
|
+
return `:::repeat${attrStr}
|
|
3123
|
+
${children}
|
|
3124
|
+
:::`;
|
|
3125
|
+
}
|
|
3126
|
+
case "subtotalsBlock": {
|
|
3127
|
+
const children = (node.content || []).map(serializeBlock).join("\n");
|
|
3128
|
+
return `:::subtotals
|
|
3129
|
+
${children}
|
|
3130
|
+
:::`;
|
|
3131
|
+
}
|
|
3132
|
+
case "columns": {
|
|
3133
|
+
const attrs = node.attrs || {};
|
|
3134
|
+
const splitVal = attrs.split || "50";
|
|
3135
|
+
const cols = (node.content || []).filter((c) => c.type === "column");
|
|
3136
|
+
const colBlocks = cols.map((col) => {
|
|
3137
|
+
const children = (col.content || []).map(serializeBlock).join("\n\n");
|
|
3138
|
+
const colAttrs = col.attrs || {};
|
|
3139
|
+
const padTop = parseFloat(colAttrs.padTop || "0") || 0;
|
|
3140
|
+
const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
|
|
3141
|
+
return `${colTag}
|
|
3142
|
+
${children}
|
|
3143
|
+
:::`;
|
|
3144
|
+
});
|
|
3145
|
+
const padX = parseFloat(attrs.padX || "0") || 0;
|
|
3146
|
+
const colsAttrs = padX ? `split:${splitVal}|padX:${padX}` : `split:${splitVal}`;
|
|
3147
|
+
return `:::columns{${colsAttrs}}
|
|
3148
|
+
${colBlocks.join("\n")}
|
|
3149
|
+
:::`;
|
|
3150
|
+
}
|
|
3151
|
+
case "column": {
|
|
3152
|
+
const children = (node.content || []).map(serializeBlock).join("\n\n");
|
|
3153
|
+
const colAttrs = node.attrs || {};
|
|
3154
|
+
const padTop = parseFloat(colAttrs.padTop || "0") || 0;
|
|
3155
|
+
const colTag = padTop ? `:::col{padTop:${padTop}}` : ":::col";
|
|
3156
|
+
return `${colTag}
|
|
3157
|
+
${children}
|
|
3158
|
+
:::`;
|
|
1528
3159
|
}
|
|
3160
|
+
default:
|
|
3161
|
+
return serializeInline(node.content);
|
|
1529
3162
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
}
|
|
1535
|
-
const pdfBytes = await pdfDoc.save({ useObjectStreams: false });
|
|
1536
|
-
return {
|
|
1537
|
-
pdfBytes,
|
|
1538
|
-
fieldPositions: state.fieldPositions,
|
|
1539
|
-
fieldWarnings
|
|
1540
|
-
};
|
|
3163
|
+
}
|
|
3164
|
+
function tiptapToMarkdown(doc) {
|
|
3165
|
+
if (!doc.content) return "";
|
|
3166
|
+
return doc.content.map(serializeBlock).join("\n\n");
|
|
1541
3167
|
}
|
|
1542
3168
|
|
|
1543
3169
|
// src/utils/pdf-preview.ts
|
|
@@ -1626,8 +3252,27 @@ function useDocumentGenerator(options = {}) {
|
|
|
1626
3252
|
Placeholder.configure({
|
|
1627
3253
|
placeholder: options.placeholder || "Start writing your document..."
|
|
1628
3254
|
}),
|
|
3255
|
+
Table.extend({
|
|
3256
|
+
addAttributes() {
|
|
3257
|
+
return {
|
|
3258
|
+
...this.parent?.(),
|
|
3259
|
+
borderless: { default: false },
|
|
3260
|
+
subtotals: { default: false }
|
|
3261
|
+
};
|
|
3262
|
+
}
|
|
3263
|
+
}).configure({ resizable: false }),
|
|
3264
|
+
TableRow,
|
|
3265
|
+
TableCell,
|
|
3266
|
+
TableHeader,
|
|
1629
3267
|
FieldNode,
|
|
1630
|
-
VariableNode
|
|
3268
|
+
VariableNode,
|
|
3269
|
+
PanelNode,
|
|
3270
|
+
ColumnsNode,
|
|
3271
|
+
ColumnNode,
|
|
3272
|
+
WatermarkNode,
|
|
3273
|
+
ImageNode,
|
|
3274
|
+
RepeatNode,
|
|
3275
|
+
SubtotalsNode
|
|
1631
3276
|
],
|
|
1632
3277
|
content: initialContent || { type: "doc", content: [{ type: "paragraph" }] },
|
|
1633
3278
|
onUpdate: ({ editor: ed }) => {
|
|
@@ -1731,10 +3376,9 @@ function useDocumentGenerator(options = {}) {
|
|
|
1731
3376
|
async (values) => {
|
|
1732
3377
|
if (!editor) throw new Error("Editor is not initialized yet \u2014 wait for the editor to load before generating");
|
|
1733
3378
|
const content = editor.getJSON();
|
|
1734
|
-
const
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
return { pdfBytes: result.pdfBytes, pdfPages: pages };
|
|
3379
|
+
const { pdfBytes: pdfBytes2 } = await generatePdfFromTiptap(content, values);
|
|
3380
|
+
const pdfPages2 = await pdfToImages(pdfBytes2);
|
|
3381
|
+
return { pdfBytes: pdfBytes2, pdfPages: pdfPages2 };
|
|
1738
3382
|
},
|
|
1739
3383
|
[editor]
|
|
1740
3384
|
);
|
|
@@ -2888,15 +4532,47 @@ function FieldEditPopover({
|
|
|
2888
4532
|
] }) })
|
|
2889
4533
|
] });
|
|
2890
4534
|
}
|
|
2891
|
-
|
|
4535
|
+
var sizeClasses = {
|
|
4536
|
+
md: "px-3 py-1.5 text-xs font-medium",
|
|
4537
|
+
sm: "px-2 py-0.5 text-[10px] font-medium"
|
|
4538
|
+
};
|
|
4539
|
+
function ToggleGroup({
|
|
4540
|
+
value,
|
|
4541
|
+
onChange,
|
|
4542
|
+
options,
|
|
4543
|
+
size = "md",
|
|
4544
|
+
className
|
|
4545
|
+
}) {
|
|
4546
|
+
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(
|
|
4547
|
+
"button",
|
|
4548
|
+
{
|
|
4549
|
+
className: cn(
|
|
4550
|
+
"rounded-md transition-colors",
|
|
4551
|
+
sizeClasses[size],
|
|
4552
|
+
value === opt.value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
|
4553
|
+
),
|
|
4554
|
+
onClick: () => onChange(opt.value),
|
|
4555
|
+
children: opt.label
|
|
4556
|
+
},
|
|
4557
|
+
opt.value
|
|
4558
|
+
)) });
|
|
4559
|
+
}
|
|
4560
|
+
function labelToVarName2(label) {
|
|
2892
4561
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
2893
4562
|
}
|
|
4563
|
+
function varNameToLabel(name) {
|
|
4564
|
+
return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
4565
|
+
}
|
|
2894
4566
|
function VariableInsertPopover({
|
|
2895
4567
|
open,
|
|
2896
4568
|
onOpenChange,
|
|
2897
4569
|
onInsert,
|
|
4570
|
+
predefinedVariables,
|
|
2898
4571
|
children
|
|
2899
4572
|
}) {
|
|
4573
|
+
const hasPredefined = !!predefinedVariables?.length;
|
|
4574
|
+
const [mode, setMode] = useState(hasPredefined ? "existing" : "new");
|
|
4575
|
+
const [search, setSearch] = useState("");
|
|
2900
4576
|
const [varLabel, setVarLabel] = useState("");
|
|
2901
4577
|
const [varName, setVarName] = useState("");
|
|
2902
4578
|
const [varDefault, setVarDefault] = useState("");
|
|
@@ -2906,7 +4582,9 @@ function VariableInsertPopover({
|
|
|
2906
4582
|
setVarName("");
|
|
2907
4583
|
setVarDefault("");
|
|
2908
4584
|
setNameManuallyEdited(false);
|
|
2909
|
-
|
|
4585
|
+
setSearch("");
|
|
4586
|
+
setMode(hasPredefined ? "existing" : "new");
|
|
4587
|
+
}, [hasPredefined]);
|
|
2910
4588
|
const handleOpenChange = useCallback(
|
|
2911
4589
|
(nextOpen) => {
|
|
2912
4590
|
if (!nextOpen) reset();
|
|
@@ -2916,7 +4594,7 @@ function VariableInsertPopover({
|
|
|
2916
4594
|
);
|
|
2917
4595
|
useEffect(() => {
|
|
2918
4596
|
if (!nameManuallyEdited) {
|
|
2919
|
-
setVarName(
|
|
4597
|
+
setVarName(labelToVarName2(varLabel));
|
|
2920
4598
|
}
|
|
2921
4599
|
}, [varLabel, nameManuallyEdited]);
|
|
2922
4600
|
const handleInsert = useCallback(() => {
|
|
@@ -2924,10 +4602,32 @@ function VariableInsertPopover({
|
|
|
2924
4602
|
onInsert({
|
|
2925
4603
|
varName: varName.trim(),
|
|
2926
4604
|
varLabel: varLabel.trim(),
|
|
2927
|
-
varDefault: varDefault.trim()
|
|
4605
|
+
varDefault: varDefault.trim(),
|
|
4606
|
+
format: ""
|
|
2928
4607
|
});
|
|
2929
4608
|
handleOpenChange(false);
|
|
2930
4609
|
}, [varLabel, varName, varDefault, onInsert, handleOpenChange]);
|
|
4610
|
+
const handlePickPredefined = useCallback(
|
|
4611
|
+
(pv) => {
|
|
4612
|
+
onInsert({
|
|
4613
|
+
varName: pv.varName,
|
|
4614
|
+
varLabel: pv.varLabel || varNameToLabel(pv.varName),
|
|
4615
|
+
varDefault: "",
|
|
4616
|
+
format: ""
|
|
4617
|
+
});
|
|
4618
|
+
handleOpenChange(false);
|
|
4619
|
+
},
|
|
4620
|
+
[onInsert, handleOpenChange]
|
|
4621
|
+
);
|
|
4622
|
+
const filteredVars = useMemo(() => {
|
|
4623
|
+
if (!predefinedVariables?.length) return [];
|
|
4624
|
+
const q = search.toLowerCase();
|
|
4625
|
+
if (!q) return predefinedVariables;
|
|
4626
|
+
return predefinedVariables.filter((pv) => {
|
|
4627
|
+
const label = (pv.varLabel || varNameToLabel(pv.varName)).toLowerCase();
|
|
4628
|
+
return pv.varName.toLowerCase().includes(q) || label.includes(q);
|
|
4629
|
+
});
|
|
4630
|
+
}, [predefinedVariables, search]);
|
|
2931
4631
|
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
|
|
2932
4632
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children }),
|
|
2933
4633
|
/* @__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 +4635,114 @@ function VariableInsertPopover({
|
|
|
2935
4635
|
/* @__PURE__ */ jsx(Braces, { size: 14 }),
|
|
2936
4636
|
/* @__PURE__ */ jsx("span", { children: "Insert Variable" })
|
|
2937
4637
|
] }),
|
|
2938
|
-
/* @__PURE__ */
|
|
2939
|
-
|
|
2940
|
-
|
|
4638
|
+
hasPredefined && /* @__PURE__ */ jsx(
|
|
4639
|
+
ToggleGroup,
|
|
4640
|
+
{
|
|
4641
|
+
value: mode,
|
|
4642
|
+
onChange: setMode,
|
|
4643
|
+
options: [
|
|
4644
|
+
{ value: "existing", label: "Existing" },
|
|
4645
|
+
{ value: "new", label: "New" }
|
|
4646
|
+
],
|
|
4647
|
+
size: "sm"
|
|
4648
|
+
}
|
|
4649
|
+
),
|
|
4650
|
+
mode === "existing" && hasPredefined ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
4651
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
4652
|
+
/* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" }),
|
|
2941
4653
|
/* @__PURE__ */ jsx(
|
|
2942
4654
|
Input,
|
|
2943
4655
|
{
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
className: "h-8 text-xs"
|
|
4656
|
+
value: search,
|
|
4657
|
+
onChange: (e) => setSearch(e.target.value),
|
|
4658
|
+
placeholder: "Search variables...",
|
|
4659
|
+
className: "h-8 text-xs pl-7"
|
|
2949
4660
|
}
|
|
2950
4661
|
)
|
|
2951
4662
|
] }),
|
|
2952
|
-
/* @__PURE__ */
|
|
2953
|
-
|
|
4663
|
+
/* @__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) => {
|
|
4664
|
+
const label = pv.varLabel || varNameToLabel(pv.varName);
|
|
4665
|
+
return /* @__PURE__ */ jsxs(
|
|
4666
|
+
"button",
|
|
4667
|
+
{
|
|
4668
|
+
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",
|
|
4669
|
+
onClick: () => handlePickPredefined(pv),
|
|
4670
|
+
children: [
|
|
4671
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: label }),
|
|
4672
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: pv.varName })
|
|
4673
|
+
]
|
|
4674
|
+
},
|
|
4675
|
+
pv.varName
|
|
4676
|
+
);
|
|
4677
|
+
}) })
|
|
4678
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4679
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
4680
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
4681
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "var-label", className: "text-xs", children: "Label" }),
|
|
4682
|
+
/* @__PURE__ */ jsx(
|
|
4683
|
+
Input,
|
|
4684
|
+
{
|
|
4685
|
+
id: "var-label",
|
|
4686
|
+
value: varLabel,
|
|
4687
|
+
onChange: (e) => setVarLabel(e.target.value),
|
|
4688
|
+
placeholder: "e.g. Company Name",
|
|
4689
|
+
className: "h-8 text-xs"
|
|
4690
|
+
}
|
|
4691
|
+
)
|
|
4692
|
+
] }),
|
|
4693
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
4694
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "var-name", className: "text-xs", children: "Name (identifier)" }),
|
|
4695
|
+
/* @__PURE__ */ jsx(
|
|
4696
|
+
Input,
|
|
4697
|
+
{
|
|
4698
|
+
id: "var-name",
|
|
4699
|
+
value: varName,
|
|
4700
|
+
onChange: (e) => {
|
|
4701
|
+
setVarName(e.target.value);
|
|
4702
|
+
setNameManuallyEdited(true);
|
|
4703
|
+
},
|
|
4704
|
+
placeholder: "e.g. company_name",
|
|
4705
|
+
className: "h-8 text-xs font-mono"
|
|
4706
|
+
}
|
|
4707
|
+
),
|
|
4708
|
+
/* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: "Used as key in data objects" })
|
|
4709
|
+
] }),
|
|
4710
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
4711
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "var-default", className: "text-xs", children: "Default Value" }),
|
|
4712
|
+
/* @__PURE__ */ jsx(
|
|
4713
|
+
Input,
|
|
4714
|
+
{
|
|
4715
|
+
id: "var-default",
|
|
4716
|
+
value: varDefault,
|
|
4717
|
+
onChange: (e) => setVarDefault(e.target.value),
|
|
4718
|
+
placeholder: "Optional",
|
|
4719
|
+
className: "h-8 text-xs"
|
|
4720
|
+
}
|
|
4721
|
+
)
|
|
4722
|
+
] })
|
|
4723
|
+
] }),
|
|
4724
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
|
|
2954
4725
|
/* @__PURE__ */ jsx(
|
|
2955
|
-
|
|
4726
|
+
Button,
|
|
2956
4727
|
{
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
},
|
|
2963
|
-
placeholder: "e.g. company_name",
|
|
2964
|
-
className: "h-8 text-xs font-mono"
|
|
4728
|
+
size: "sm",
|
|
4729
|
+
className: "h-8 flex-1 text-xs",
|
|
4730
|
+
onClick: handleInsert,
|
|
4731
|
+
disabled: !varLabel.trim() || !varName.trim(),
|
|
4732
|
+
children: "Insert"
|
|
2965
4733
|
}
|
|
2966
4734
|
),
|
|
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
4735
|
/* @__PURE__ */ jsx(
|
|
2972
|
-
|
|
4736
|
+
Button,
|
|
2973
4737
|
{
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
4738
|
+
variant: "outline",
|
|
4739
|
+
size: "sm",
|
|
4740
|
+
className: "h-8 text-xs",
|
|
4741
|
+
onClick: () => handleOpenChange(false),
|
|
4742
|
+
children: "Cancel"
|
|
2979
4743
|
}
|
|
2980
4744
|
)
|
|
2981
4745
|
] })
|
|
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
4746
|
] })
|
|
3005
4747
|
] }) })
|
|
3006
4748
|
] });
|
|
@@ -3222,7 +4964,6 @@ function PreviewPanel({
|
|
|
3222
4964
|
(f) => f.position.page === currentPageIndex + 1 && f.position.width > 0
|
|
3223
4965
|
);
|
|
3224
4966
|
}, [positionedFields, currentPage, currentPageIndex]);
|
|
3225
|
-
const showScrollbars = zoomLevel > 1;
|
|
3226
4967
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col h-full border border-border rounded-lg overflow-hidden bg-muted/20", className), children: [
|
|
3227
4968
|
/* @__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
4969
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
@@ -3338,12 +5079,13 @@ function PreviewPanel({
|
|
|
3338
5079
|
"div",
|
|
3339
5080
|
{
|
|
3340
5081
|
className: cn(
|
|
3341
|
-
"border border-border rounded-lg bg-muted/30 shrink-0"
|
|
3342
|
-
showScrollbars ? "overflow-auto" : "overflow-hidden"
|
|
5082
|
+
"border border-border rounded-lg bg-muted/30 shrink-0 overflow-auto scrollbar-hidden"
|
|
3343
5083
|
),
|
|
3344
5084
|
style: {
|
|
3345
5085
|
width: pageDisplaySize.viewportWidth,
|
|
3346
|
-
height: pageDisplaySize.viewportHeight
|
|
5086
|
+
height: pageDisplaySize.viewportHeight,
|
|
5087
|
+
maxWidth: "100%",
|
|
5088
|
+
maxHeight: "100%"
|
|
3347
5089
|
},
|
|
3348
5090
|
children: /* @__PURE__ */ jsxs(
|
|
3349
5091
|
"div",
|
|
@@ -3414,8 +5156,8 @@ function validateMarkdown(markdown) {
|
|
|
3414
5156
|
errors.push(`Malformed token at line ${lineNum}: missing | delimiter`);
|
|
3415
5157
|
} else {
|
|
3416
5158
|
const tokenType = tokenContent.slice(0, pipeIdx);
|
|
3417
|
-
if (tokenType !== "field" && tokenType !== "var") {
|
|
3418
|
-
errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field" or "
|
|
5159
|
+
if (tokenType !== "field" && tokenType !== "var" && tokenType !== "image") {
|
|
5160
|
+
errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field", "var" or "image")`);
|
|
3419
5161
|
} else {
|
|
3420
5162
|
const body = tokenContent.slice(pipeIdx + 1);
|
|
3421
5163
|
const pairs = body.split("|");
|
|
@@ -3429,12 +5171,48 @@ function validateMarkdown(markdown) {
|
|
|
3429
5171
|
if (!hasName) {
|
|
3430
5172
|
errors.push(`Variable token at line ${lineNum} is missing required "name" attribute`);
|
|
3431
5173
|
}
|
|
5174
|
+
} else if (tokenType === "image") {
|
|
5175
|
+
const hasSource = pairs.some((p) => p.startsWith("src:") || p.startsWith("var:"));
|
|
5176
|
+
if (!hasSource) {
|
|
5177
|
+
errors.push(`Image token at line ${lineNum} needs a "src" or "var" attribute`);
|
|
5178
|
+
}
|
|
3432
5179
|
}
|
|
3433
5180
|
}
|
|
3434
5181
|
}
|
|
3435
5182
|
searchFrom = closeIdx + 2;
|
|
3436
5183
|
}
|
|
3437
5184
|
}
|
|
5185
|
+
const DIRECTIVE_OPEN_RE = /^:::(panel|columns|col|watermark)(?:\{[^}]*\})?$/;
|
|
5186
|
+
const DIRECTIVE_CLOSE_RE = /^:::$/;
|
|
5187
|
+
const directiveStack = [];
|
|
5188
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5189
|
+
const trimmed = lines[i].trim();
|
|
5190
|
+
const lineNum = i + 1;
|
|
5191
|
+
const openMatch = trimmed.match(DIRECTIVE_OPEN_RE);
|
|
5192
|
+
if (openMatch) {
|
|
5193
|
+
const dtype = openMatch[1];
|
|
5194
|
+
if (dtype !== "watermark") {
|
|
5195
|
+
if (dtype === "col") {
|
|
5196
|
+
const parent = directiveStack[directiveStack.length - 1];
|
|
5197
|
+
if (!parent || parent.type !== "columns") {
|
|
5198
|
+
warnings.push(`:::col at line ${lineNum} appears outside :::columns`);
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
directiveStack.push({ type: dtype, line: lineNum });
|
|
5202
|
+
}
|
|
5203
|
+
continue;
|
|
5204
|
+
}
|
|
5205
|
+
if (DIRECTIVE_CLOSE_RE.test(trimmed)) {
|
|
5206
|
+
if (directiveStack.length === 0) {
|
|
5207
|
+
warnings.push(`Stray ::: close at line ${lineNum} with no matching open directive`);
|
|
5208
|
+
} else {
|
|
5209
|
+
directiveStack.pop();
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
for (const open of directiveStack) {
|
|
5214
|
+
errors.push(`Unclosed :::${open.type} directive opened at line ${open.line}`);
|
|
5215
|
+
}
|
|
3438
5216
|
let fields = [];
|
|
3439
5217
|
let variables = [];
|
|
3440
5218
|
try {
|
|
@@ -3468,31 +5246,6 @@ function validateMarkdown(markdown) {
|
|
|
3468
5246
|
fields
|
|
3469
5247
|
};
|
|
3470
5248
|
}
|
|
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
5249
|
function parseCsv(text) {
|
|
3497
5250
|
const lines = text.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
3498
5251
|
if (lines.length < 2) return [];
|
|
@@ -3542,7 +5295,10 @@ function GeneratePanel({
|
|
|
3542
5295
|
editorContent,
|
|
3543
5296
|
editorVariables,
|
|
3544
5297
|
onGeneratePdf,
|
|
3545
|
-
initialBulkData
|
|
5298
|
+
initialBulkData,
|
|
5299
|
+
initialVariableValues,
|
|
5300
|
+
exportFileName,
|
|
5301
|
+
templateMode
|
|
3546
5302
|
}) {
|
|
3547
5303
|
const [templateSource, setTemplateSource] = useState("editor");
|
|
3548
5304
|
const [importedMarkdown, setImportedMarkdown] = useState(null);
|
|
@@ -3550,6 +5306,7 @@ function GeneratePanel({
|
|
|
3550
5306
|
const [validationResult, setValidationResult] = useState(null);
|
|
3551
5307
|
const [mode, setMode] = useState("single");
|
|
3552
5308
|
const [variableValues, setVariableValues] = useState({});
|
|
5309
|
+
const [variableSearch, setVariableSearch] = useState("");
|
|
3553
5310
|
const [bulkInputFormat, setBulkInputFormat] = useState("json");
|
|
3554
5311
|
const [bulkInput, setBulkInput] = useState("");
|
|
3555
5312
|
const [bulkData, setBulkData] = useState(null);
|
|
@@ -3576,11 +5333,22 @@ function GeneratePanel({
|
|
|
3576
5333
|
const col = pos - textBefore.lastIndexOf("\n");
|
|
3577
5334
|
setJsonCursor({ line, col });
|
|
3578
5335
|
}, []);
|
|
5336
|
+
const lockedVarNames = React12__default.useMemo(
|
|
5337
|
+
() => templateMode && initialVariableValues ? new Set(Object.keys(initialVariableValues)) : /* @__PURE__ */ new Set(),
|
|
5338
|
+
[templateMode, initialVariableValues]
|
|
5339
|
+
);
|
|
3579
5340
|
const activeVariables = templateSource === "imported" && validationResult?.valid ? validationResult.variables : editorVariables;
|
|
3580
5341
|
const hasVariables = activeVariables.length > 0;
|
|
3581
5342
|
const hasEditorContent = editorMarkdown.trim().length > 0;
|
|
3582
5343
|
const hasImportedContent = templateSource === "imported" && importedMarkdown != null;
|
|
3583
5344
|
const hasContent = hasImportedContent || hasEditorContent;
|
|
5345
|
+
const filteredVariables = React12__default.useMemo(() => {
|
|
5346
|
+
if (!variableSearch.trim()) return activeVariables;
|
|
5347
|
+
const q = variableSearch.toLowerCase();
|
|
5348
|
+
return activeVariables.filter(
|
|
5349
|
+
(v) => v.varName.toLowerCase().includes(q) || (v.varLabel || "").toLowerCase().includes(q)
|
|
5350
|
+
);
|
|
5351
|
+
}, [activeVariables, variableSearch]);
|
|
3584
5352
|
const getActiveContent = useCallback(() => {
|
|
3585
5353
|
if (templateSource === "imported" && importedMarkdown) {
|
|
3586
5354
|
return markdownToTiptap(importedMarkdown);
|
|
@@ -3748,7 +5516,10 @@ function GeneratePanel({
|
|
|
3748
5516
|
setPreviewError(null);
|
|
3749
5517
|
try {
|
|
3750
5518
|
const content = getActiveContent();
|
|
3751
|
-
const
|
|
5519
|
+
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
|
|
5520
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
5521
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
5522
|
+
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
3752
5523
|
const fields = extractFieldsFromContent(replaced);
|
|
3753
5524
|
const result = await generatePdfFromContent(replaced);
|
|
3754
5525
|
const pages = await pdfToImages(result.pdfBytes);
|
|
@@ -3769,7 +5540,10 @@ function GeneratePanel({
|
|
|
3769
5540
|
setExportSuccess(null);
|
|
3770
5541
|
try {
|
|
3771
5542
|
const content = getActiveContent();
|
|
3772
|
-
const
|
|
5543
|
+
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, variableValues);
|
|
5544
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
5545
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
5546
|
+
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
3773
5547
|
const fields = extractFieldsFromContent(replaced);
|
|
3774
5548
|
const result = await generatePdfFromContent(replaced, {
|
|
3775
5549
|
drawFieldPlaceholders: false,
|
|
@@ -3777,7 +5551,7 @@ function GeneratePanel({
|
|
|
3777
5551
|
fields
|
|
3778
5552
|
});
|
|
3779
5553
|
const blob = new Blob([result.pdfBytes], { type: "application/pdf" });
|
|
3780
|
-
const fileName = importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
|
|
5554
|
+
const fileName = exportFileName ? exportFileName.endsWith(".pdf") ? exportFileName : `${exportFileName}.pdf` : importedFileName ? importedFileName.replace(".md", ".pdf") : "document.pdf";
|
|
3781
5555
|
if (onGeneratePdf) {
|
|
3782
5556
|
onGeneratePdf(blob, fileName);
|
|
3783
5557
|
} else {
|
|
@@ -3849,7 +5623,10 @@ function GeneratePanel({
|
|
|
3849
5623
|
const allWarnings = [];
|
|
3850
5624
|
for (let i = 0; i < bulkData.length; i++) {
|
|
3851
5625
|
const values = bulkData[i];
|
|
3852
|
-
const
|
|
5626
|
+
const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
|
|
5627
|
+
const blockExpanded = expandBlockVariables(expanded, enrichedValues);
|
|
5628
|
+
const suppressed = suppressZeroContent(blockExpanded, enrichedValues);
|
|
5629
|
+
const replaced = replaceVariablesInContent(suppressed, enrichedValues);
|
|
3853
5630
|
const fields = extractFieldsFromContent(replaced);
|
|
3854
5631
|
const result = await generatePdfFromContent(replaced, {
|
|
3855
5632
|
drawFieldPlaceholders: false,
|
|
@@ -3904,13 +5681,15 @@ ${allWarnings.join("\n")}`);
|
|
|
3904
5681
|
}, [mode, bulkInputFormat, bulkInput, initialBulkJson]);
|
|
3905
5682
|
React12__default.useEffect(() => {
|
|
3906
5683
|
if (templateSource === "editor") {
|
|
3907
|
-
const defaults = {};
|
|
5684
|
+
const defaults = initialVariableValues ? { ...initialVariableValues } : {};
|
|
3908
5685
|
for (const v of editorVariables) {
|
|
3909
|
-
defaults[v.varName]
|
|
5686
|
+
if (!defaults[v.varName]) {
|
|
5687
|
+
defaults[v.varName] = variableValues[v.varName] || v.varDefault || "";
|
|
5688
|
+
}
|
|
3910
5689
|
}
|
|
3911
5690
|
setVariableValues(defaults);
|
|
3912
5691
|
}
|
|
3913
|
-
}, [editorVariables, templateSource]);
|
|
5692
|
+
}, [editorVariables, templateSource, initialVariableValues]);
|
|
3914
5693
|
useEffect(() => {
|
|
3915
5694
|
setPreviewFresh(false);
|
|
3916
5695
|
}, [variableValues, templateSource, importedMarkdown, editorMarkdown, editorContent]);
|
|
@@ -3922,8 +5701,8 @@ ${allWarnings.join("\n")}`);
|
|
|
3922
5701
|
}, [exportSuccess]);
|
|
3923
5702
|
return /* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 grid grid-cols-[1fr_1px_1fr]", children: [
|
|
3924
5703
|
/* @__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: [
|
|
5704
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hidden", children: [
|
|
5705
|
+
!templateMode && /* @__PURE__ */ jsxs("div", { children: [
|
|
3927
5706
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
3928
5707
|
/* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Template" }),
|
|
3929
5708
|
templateSource === "imported" && /* @__PURE__ */ jsxs(
|
|
@@ -4016,10 +5795,10 @@ ${allWarnings.join("\n")}`);
|
|
|
4016
5795
|
!hasContent && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
|
|
4017
5796
|
/* @__PURE__ */ jsx(Braces, { size: 32, className: "mb-2 opacity-50" }),
|
|
4018
5797
|
/* @__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." })
|
|
5798
|
+
/* @__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
5799
|
] }),
|
|
4021
5800
|
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(
|
|
5801
|
+
!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
5802
|
ToggleGroup,
|
|
4024
5803
|
{
|
|
4025
5804
|
value: mode,
|
|
@@ -4031,20 +5810,53 @@ ${allWarnings.join("\n")}`);
|
|
|
4031
5810
|
}
|
|
4032
5811
|
) }),
|
|
4033
5812
|
mode === "single" && /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
|
|
4034
|
-
/* @__PURE__ */
|
|
4035
|
-
|
|
4036
|
-
/* @__PURE__ */ jsx(
|
|
5813
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
5814
|
+
/* @__PURE__ */ jsx("h3", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Variables" }),
|
|
5815
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: activeVariables.length })
|
|
5816
|
+
] }),
|
|
5817
|
+
activeVariables.length > 5 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
5818
|
+
/* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" }),
|
|
4037
5819
|
/* @__PURE__ */ jsx(
|
|
4038
5820
|
Input,
|
|
4039
5821
|
{
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
5822
|
+
value: variableSearch,
|
|
5823
|
+
onChange: (e) => setVariableSearch(e.target.value),
|
|
5824
|
+
placeholder: "Search variables...",
|
|
5825
|
+
className: "h-8 text-xs pl-8"
|
|
5826
|
+
}
|
|
5827
|
+
),
|
|
5828
|
+
variableSearch && /* @__PURE__ */ jsx(
|
|
5829
|
+
"button",
|
|
5830
|
+
{
|
|
5831
|
+
type: "button",
|
|
5832
|
+
onClick: () => setVariableSearch(""),
|
|
5833
|
+
className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
|
|
5834
|
+
children: /* @__PURE__ */ jsx(X, { size: 12 })
|
|
4045
5835
|
}
|
|
4046
5836
|
)
|
|
4047
|
-
] },
|
|
5837
|
+
] }),
|
|
5838
|
+
filteredVariables.length === 0 && variableSearch.trim() && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground py-2 text-center", children: [
|
|
5839
|
+
"No variables match \u201C",
|
|
5840
|
+
variableSearch,
|
|
5841
|
+
"\u201D"
|
|
5842
|
+
] }),
|
|
5843
|
+
filteredVariables.map((v) => {
|
|
5844
|
+
const isLocked = lockedVarNames.has(v.varName);
|
|
5845
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
5846
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: `gen-${v.varName}`, className: "text-xs", children: v.varLabel || v.varName }),
|
|
5847
|
+
/* @__PURE__ */ jsx(
|
|
5848
|
+
Input,
|
|
5849
|
+
{
|
|
5850
|
+
id: `gen-${v.varName}`,
|
|
5851
|
+
value: variableValues[v.varName] || "",
|
|
5852
|
+
onChange: (e) => updateVariableValue(v.varName, e.target.value),
|
|
5853
|
+
placeholder: v.varDefault || `Enter ${v.varLabel || v.varName}`,
|
|
5854
|
+
disabled: isLocked,
|
|
5855
|
+
className: cn("h-8 text-xs", isLocked && "opacity-60 cursor-not-allowed")
|
|
5856
|
+
}
|
|
5857
|
+
)
|
|
5858
|
+
] }, v.varName);
|
|
5859
|
+
})
|
|
4048
5860
|
] }),
|
|
4049
5861
|
mode === "bulk" && /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-3", children: [
|
|
4050
5862
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
@@ -4233,56 +6045,60 @@ ${allWarnings.join("\n")}`);
|
|
|
4233
6045
|
] }),
|
|
4234
6046
|
!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
6047
|
] }),
|
|
4236
|
-
|
|
4237
|
-
/* @__PURE__ */
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
6048
|
+
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: [
|
|
6049
|
+
mode === "single" || !hasVariables ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6050
|
+
/* @__PURE__ */ jsxs(
|
|
6051
|
+
Button,
|
|
6052
|
+
{
|
|
6053
|
+
variant: "secondary",
|
|
6054
|
+
onClick: handlePreview,
|
|
6055
|
+
disabled: isGenerating,
|
|
6056
|
+
className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
|
|
6057
|
+
children: [
|
|
6058
|
+
isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
|
|
6059
|
+
isGenerating ? "Generating..." : "Generate PDF"
|
|
6060
|
+
]
|
|
6061
|
+
}
|
|
6062
|
+
),
|
|
6063
|
+
/* @__PURE__ */ jsxs(
|
|
6064
|
+
Button,
|
|
6065
|
+
{
|
|
6066
|
+
onClick: handleExportSingle,
|
|
6067
|
+
disabled: !previewFresh || isExporting,
|
|
6068
|
+
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",
|
|
6069
|
+
children: [
|
|
6070
|
+
isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
|
|
6071
|
+
isExporting ? "Exporting..." : "Export PDF"
|
|
6072
|
+
]
|
|
6073
|
+
}
|
|
6074
|
+
)
|
|
6075
|
+
] }) : /* @__PURE__ */ jsxs(
|
|
4263
6076
|
Button,
|
|
4264
6077
|
{
|
|
4265
|
-
onClick:
|
|
4266
|
-
disabled: !
|
|
4267
|
-
className: "h-10 px-4 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground shadow-
|
|
6078
|
+
onClick: handleBulkGenerate,
|
|
6079
|
+
disabled: isExporting || !bulkData || bulkData.length === 0,
|
|
6080
|
+
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
6081
|
children: [
|
|
4269
6082
|
isExporting ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Download, { size: 16 }),
|
|
4270
|
-
isExporting ? "
|
|
6083
|
+
isExporting ? "Generating..." : `Generate All (${bulkData?.length || 0} PDFs)`
|
|
4271
6084
|
]
|
|
4272
6085
|
}
|
|
4273
|
-
)
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
]
|
|
4284
|
-
|
|
4285
|
-
|
|
6086
|
+
),
|
|
6087
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
|
|
6088
|
+
previewError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
6089
|
+
/* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6090
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: previewError })
|
|
6091
|
+
] }),
|
|
6092
|
+
exportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
6093
|
+
/* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6094
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: exportError })
|
|
6095
|
+
] }),
|
|
6096
|
+
exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
|
|
6097
|
+
/* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
|
|
6098
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: exportSuccess })
|
|
6099
|
+
] })
|
|
6100
|
+
] })
|
|
6101
|
+
] })
|
|
4286
6102
|
] }),
|
|
4287
6103
|
/* @__PURE__ */ jsx("div", { className: "bg-border" }),
|
|
4288
6104
|
/* @__PURE__ */ jsx("div", { className: "flex flex-col min-h-0 min-w-0", children: /* @__PURE__ */ jsx(
|
|
@@ -4460,7 +6276,13 @@ function DocumentGeneratorInner({
|
|
|
4460
6276
|
showToolbar = true,
|
|
4461
6277
|
showGenerateTab = true,
|
|
4462
6278
|
onGeneratePdf,
|
|
4463
|
-
initialBulkData
|
|
6279
|
+
initialBulkData,
|
|
6280
|
+
defaultTab = "editor",
|
|
6281
|
+
initialVariableValues,
|
|
6282
|
+
predefinedVariables,
|
|
6283
|
+
exportFileName,
|
|
6284
|
+
templateMode,
|
|
6285
|
+
onSaveTemplate
|
|
4464
6286
|
}) {
|
|
4465
6287
|
const {
|
|
4466
6288
|
editor,
|
|
@@ -4487,7 +6309,7 @@ function DocumentGeneratorInner({
|
|
|
4487
6309
|
placeholder,
|
|
4488
6310
|
onChange
|
|
4489
6311
|
});
|
|
4490
|
-
const [activeTab, setActiveTab] = useState(
|
|
6312
|
+
const [activeTab, setActiveTab] = useState(defaultTab);
|
|
4491
6313
|
const [insertPopoverOpen, setInsertPopoverOpen] = useState(false);
|
|
4492
6314
|
const [insertVarPopoverOpen, setInsertVarPopoverOpen] = useState(false);
|
|
4493
6315
|
const [editFieldId, setEditFieldId] = useState(null);
|
|
@@ -4499,6 +6321,7 @@ function DocumentGeneratorInner({
|
|
|
4499
6321
|
const editorWrapperRef = useRef(null);
|
|
4500
6322
|
const [editorCollapsed, setEditorCollapsed] = useState(false);
|
|
4501
6323
|
const [mdExportError, setMdExportError] = useState(null);
|
|
6324
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
4502
6325
|
const [exportSuccess, setExportSuccess] = useState(null);
|
|
4503
6326
|
useEffect(() => {
|
|
4504
6327
|
const handler = (e) => {
|
|
@@ -4569,7 +6392,28 @@ function DocumentGeneratorInner({
|
|
|
4569
6392
|
onExport(result);
|
|
4570
6393
|
setExportSuccess("Document exported successfully");
|
|
4571
6394
|
}
|
|
4572
|
-
}, [exportDocument, onExport]);
|
|
6395
|
+
}, [exportDocument, onExport]);
|
|
6396
|
+
const handleSaveTemplate = useCallback(async () => {
|
|
6397
|
+
const md = exportMarkdown();
|
|
6398
|
+
if (!md.trim()) {
|
|
6399
|
+
setMdExportError("Editor content is empty");
|
|
6400
|
+
return;
|
|
6401
|
+
}
|
|
6402
|
+
const validation = validateMarkdown(md);
|
|
6403
|
+
if (!validation.valid) {
|
|
6404
|
+
setMdExportError(validation.errors.join("; "));
|
|
6405
|
+
return;
|
|
6406
|
+
}
|
|
6407
|
+
setMdExportError(null);
|
|
6408
|
+
setIsSaving(true);
|
|
6409
|
+
try {
|
|
6410
|
+
await onSaveTemplate?.(md);
|
|
6411
|
+
setExportSuccess("Template saved");
|
|
6412
|
+
} catch {
|
|
6413
|
+
} finally {
|
|
6414
|
+
setIsSaving(false);
|
|
6415
|
+
}
|
|
6416
|
+
}, [exportMarkdown, onSaveTemplate]);
|
|
4573
6417
|
const handleExportMarkdown = useCallback(() => {
|
|
4574
6418
|
const md = exportMarkdown();
|
|
4575
6419
|
if (!md.trim()) {
|
|
@@ -4586,10 +6430,10 @@ function DocumentGeneratorInner({
|
|
|
4586
6430
|
const url = URL.createObjectURL(blob);
|
|
4587
6431
|
const a = document.createElement("a");
|
|
4588
6432
|
a.href = url;
|
|
4589
|
-
a.download = "template.md";
|
|
6433
|
+
a.download = exportFileName ? `${exportFileName}.md` : "template.md";
|
|
4590
6434
|
a.click();
|
|
4591
6435
|
URL.revokeObjectURL(url);
|
|
4592
|
-
}, [exportMarkdown]);
|
|
6436
|
+
}, [exportMarkdown, exportFileName]);
|
|
4593
6437
|
useEffect(() => {
|
|
4594
6438
|
if (mdExportError) setMdExportError(null);
|
|
4595
6439
|
}, [markdown]);
|
|
@@ -4625,6 +6469,7 @@ function DocumentGeneratorInner({
|
|
|
4625
6469
|
open: insertVarPopoverOpen,
|
|
4626
6470
|
onOpenChange: setInsertVarPopoverOpen,
|
|
4627
6471
|
onInsert: handleInsertVariable,
|
|
6472
|
+
predefinedVariables,
|
|
4628
6473
|
children: /* @__PURE__ */ jsxs(
|
|
4629
6474
|
Button,
|
|
4630
6475
|
{
|
|
@@ -4665,118 +6510,141 @@ function DocumentGeneratorInner({
|
|
|
4665
6510
|
] })
|
|
4666
6511
|
] })
|
|
4667
6512
|
] }),
|
|
4668
|
-
/* @__PURE__ */
|
|
6513
|
+
/* @__PURE__ */ jsx("div", { className: cn(
|
|
4669
6514
|
"flex flex-col flex-1 min-h-0",
|
|
4670
6515
|
activeTab !== "editor" && "hidden"
|
|
6516
|
+
), children: /* @__PURE__ */ jsxs("div", { className: cn(
|
|
6517
|
+
"flex-1 min-h-0",
|
|
6518
|
+
showPreview && !editorCollapsed ? "grid grid-cols-[1fr_1px_1fr]" : "flex flex-col"
|
|
4671
6519
|
), children: [
|
|
4672
|
-
/* @__PURE__ */ jsxs("div", { className:
|
|
4673
|
-
"
|
|
4674
|
-
|
|
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,
|
|
6520
|
+
!editorCollapsed && /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-0 min-w-0", children: [
|
|
6521
|
+
showToolbar && !readOnly && /* @__PURE__ */ jsx("div", { className: "border-b border-border", children: /* @__PURE__ */ jsx(
|
|
6522
|
+
EditorToolbar,
|
|
4697
6523
|
{
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
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"
|
|
6524
|
+
editor,
|
|
6525
|
+
insertFieldButton,
|
|
6526
|
+
insertVariableButton,
|
|
6527
|
+
onCollapse: showPreview ? () => setEditorCollapsed(true) : void 0
|
|
4712
6528
|
}
|
|
4713
|
-
)
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
/* @__PURE__ */ jsxs(
|
|
4717
|
-
Button,
|
|
6529
|
+
) }),
|
|
6530
|
+
/* @__PURE__ */ jsx("div", { ref: editorWrapperRef, className: "flex-1 overflow-y-auto min-h-0 scrollbar-hidden", children: /* @__PURE__ */ jsx(
|
|
6531
|
+
EditorContent,
|
|
4718
6532
|
{
|
|
4719
|
-
|
|
4720
|
-
|
|
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
|
-
]
|
|
6533
|
+
editor,
|
|
6534
|
+
className: "prose prose-sm max-w-none p-4 focus-within:outline-none"
|
|
4727
6535
|
}
|
|
4728
|
-
),
|
|
4729
|
-
/* @__PURE__ */ jsxs("div", { className: "flex
|
|
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: [
|
|
6536
|
+
) }),
|
|
6537
|
+
/* @__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
6538
|
/* @__PURE__ */ jsxs(
|
|
4754
6539
|
Button,
|
|
4755
6540
|
{
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
6541
|
+
variant: "secondary",
|
|
6542
|
+
onClick: generatePdf,
|
|
6543
|
+
disabled: isGenerating || !editor,
|
|
6544
|
+
className: "h-10 px-4 font-semibold shadow-sm hover:shadow-md transition-all duration-200 text-sm",
|
|
4759
6545
|
children: [
|
|
4760
|
-
/* @__PURE__ */ jsx(
|
|
4761
|
-
"
|
|
6546
|
+
isGenerating ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { size: 16 }),
|
|
6547
|
+
isGenerating ? "Generating..." : "Generate PDF"
|
|
4762
6548
|
]
|
|
4763
6549
|
}
|
|
4764
6550
|
),
|
|
4765
|
-
/* @__PURE__ */ jsxs(
|
|
6551
|
+
onSaveTemplate ? /* @__PURE__ */ jsxs(
|
|
4766
6552
|
Button,
|
|
4767
6553
|
{
|
|
4768
|
-
onClick:
|
|
4769
|
-
disabled:
|
|
6554
|
+
onClick: handleSaveTemplate,
|
|
6555
|
+
disabled: isSaving || !editor || !markdown.trim(),
|
|
4770
6556
|
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
6557
|
children: [
|
|
4772
|
-
/* @__PURE__ */ jsx(
|
|
4773
|
-
exportButtonText
|
|
6558
|
+
isSaving ? /* @__PURE__ */ jsx(Loader2, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsx(Save, { size: 16 }),
|
|
6559
|
+
isSaving ? "Saving..." : exportButtonText || "Save Template"
|
|
4774
6560
|
]
|
|
4775
6561
|
}
|
|
4776
|
-
)
|
|
6562
|
+
) : /* @__PURE__ */ jsxs(Popover, { children: [
|
|
6563
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
6564
|
+
Button,
|
|
6565
|
+
{
|
|
6566
|
+
disabled: !editor || !markdown.trim() && pdfPages.length === 0,
|
|
6567
|
+
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",
|
|
6568
|
+
children: [
|
|
6569
|
+
/* @__PURE__ */ jsx(FileDown, { size: 16 }),
|
|
6570
|
+
"Export",
|
|
6571
|
+
/* @__PURE__ */ jsx(ChevronDown, { size: 14 })
|
|
6572
|
+
]
|
|
6573
|
+
}
|
|
6574
|
+
) }),
|
|
6575
|
+
/* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-48 p-1", children: [
|
|
6576
|
+
/* @__PURE__ */ jsxs(
|
|
6577
|
+
"button",
|
|
6578
|
+
{
|
|
6579
|
+
onClick: handleExportMarkdown,
|
|
6580
|
+
disabled: !editor || !markdown.trim(),
|
|
6581
|
+
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",
|
|
6582
|
+
children: [
|
|
6583
|
+
/* @__PURE__ */ jsx(FileText, { size: 16 }),
|
|
6584
|
+
"Export as MD"
|
|
6585
|
+
]
|
|
6586
|
+
}
|
|
6587
|
+
),
|
|
6588
|
+
/* @__PURE__ */ jsxs(
|
|
6589
|
+
"button",
|
|
6590
|
+
{
|
|
6591
|
+
onClick: handleExport,
|
|
6592
|
+
disabled: isGenerating || !editor || pdfPages.length === 0,
|
|
6593
|
+
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",
|
|
6594
|
+
children: [
|
|
6595
|
+
/* @__PURE__ */ jsx(FileDown, { size: 16 }),
|
|
6596
|
+
"Export as PDF"
|
|
6597
|
+
]
|
|
6598
|
+
}
|
|
6599
|
+
)
|
|
6600
|
+
] })
|
|
6601
|
+
] }),
|
|
6602
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 ml-auto", children: [
|
|
6603
|
+
mdExportError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
6604
|
+
/* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6605
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: mdExportError })
|
|
6606
|
+
] }),
|
|
6607
|
+
generationError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive max-w-[300px]", children: [
|
|
6608
|
+
/* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6609
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: generationError })
|
|
6610
|
+
] }),
|
|
6611
|
+
fieldWarnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-orange-600 max-w-[400px]", children: [
|
|
6612
|
+
/* @__PURE__ */ jsx(AlertTriangle, { size: 14, className: "shrink-0" }),
|
|
6613
|
+
/* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
6614
|
+
fieldWarnings.length,
|
|
6615
|
+
" field",
|
|
6616
|
+
fieldWarnings.length !== 1 ? "s" : "",
|
|
6617
|
+
" had warnings during generation"
|
|
6618
|
+
] })
|
|
6619
|
+
] }),
|
|
6620
|
+
exportSuccess && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-emerald-600 max-w-[300px]", children: [
|
|
6621
|
+
/* @__PURE__ */ jsx(CheckCircle2, { size: 14, className: "shrink-0" }),
|
|
6622
|
+
/* @__PURE__ */ jsx("span", { children: exportSuccess })
|
|
6623
|
+
] })
|
|
6624
|
+
] })
|
|
4777
6625
|
] })
|
|
4778
|
-
] })
|
|
4779
|
-
|
|
6626
|
+
] }),
|
|
6627
|
+
showPreview && !editorCollapsed && /* @__PURE__ */ jsx("div", { className: "bg-border" }),
|
|
6628
|
+
showPreview && /* @__PURE__ */ jsx(
|
|
6629
|
+
PreviewPanel,
|
|
6630
|
+
{
|
|
6631
|
+
pages: pdfPages,
|
|
6632
|
+
isGenerating,
|
|
6633
|
+
positionedFields,
|
|
6634
|
+
headerLeft: editorCollapsed ? /* @__PURE__ */ jsx(
|
|
6635
|
+
Button,
|
|
6636
|
+
{
|
|
6637
|
+
variant: "ghost",
|
|
6638
|
+
size: "sm",
|
|
6639
|
+
className: "h-8 w-8 p-0",
|
|
6640
|
+
onClick: () => setEditorCollapsed(false),
|
|
6641
|
+
children: /* @__PURE__ */ jsx(PanelLeftOpen, { size: 14 })
|
|
6642
|
+
}
|
|
6643
|
+
) : void 0,
|
|
6644
|
+
className: "flex-1 border-0 rounded-none"
|
|
6645
|
+
}
|
|
6646
|
+
)
|
|
6647
|
+
] }) }),
|
|
4780
6648
|
showGenerateTab && activeTab === "generate" && /* @__PURE__ */ jsx(
|
|
4781
6649
|
GeneratePanel,
|
|
4782
6650
|
{
|
|
@@ -4784,7 +6652,10 @@ function DocumentGeneratorInner({
|
|
|
4784
6652
|
editorContent: editor?.getJSON(),
|
|
4785
6653
|
editorVariables: variables,
|
|
4786
6654
|
onGeneratePdf,
|
|
4787
|
-
initialBulkData
|
|
6655
|
+
initialBulkData,
|
|
6656
|
+
initialVariableValues,
|
|
6657
|
+
exportFileName,
|
|
6658
|
+
templateMode
|
|
4788
6659
|
}
|
|
4789
6660
|
),
|
|
4790
6661
|
/* @__PURE__ */ jsx(
|
|
@@ -4925,6 +6796,717 @@ function EditorPanel({
|
|
|
4925
6796
|
] });
|
|
4926
6797
|
}
|
|
4927
6798
|
|
|
4928
|
-
|
|
6799
|
+
// src/utils/xml-template-parser.ts
|
|
6800
|
+
function extractFieldRefs(expr) {
|
|
6801
|
+
const refs = [];
|
|
6802
|
+
const re = /\[([^\]]+)\](?:\.\[([^\]]+)\])?/g;
|
|
6803
|
+
let m;
|
|
6804
|
+
while ((m = re.exec(expr)) !== null) {
|
|
6805
|
+
if (m[2]) {
|
|
6806
|
+
refs.push(`${m[1]}.${m[2]}`);
|
|
6807
|
+
} else {
|
|
6808
|
+
refs.push(m[1]);
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
return refs;
|
|
6812
|
+
}
|
|
6813
|
+
function fieldRefToVarName(ref, bandContext) {
|
|
6814
|
+
if (ref.includes(".")) {
|
|
6815
|
+
return labelToVarName(ref.replace(/\./g, "_"));
|
|
6816
|
+
}
|
|
6817
|
+
if (bandContext) {
|
|
6818
|
+
return labelToVarName(bandContext + "_" + ref);
|
|
6819
|
+
}
|
|
6820
|
+
return labelToVarName(ref);
|
|
6821
|
+
}
|
|
6822
|
+
function fieldRefToLabel(ref) {
|
|
6823
|
+
const parts = ref.split(".");
|
|
6824
|
+
const raw = parts[parts.length - 1];
|
|
6825
|
+
return raw.replace(/_/g, " ");
|
|
6826
|
+
}
|
|
6827
|
+
function resolveExpression(expr, registry, siblingLabel, bandContext) {
|
|
6828
|
+
const trimmed = expr.trim();
|
|
6829
|
+
const suppressZero = /<>\s*0/.test(trimmed);
|
|
6830
|
+
const szOpts = suppressZero ? { suppressZero: true } : void 0;
|
|
6831
|
+
const aggMatch = trimmed.match(
|
|
6832
|
+
/^\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)$/i
|
|
6833
|
+
);
|
|
6834
|
+
if (aggMatch) {
|
|
6835
|
+
const fieldName = aggMatch[2];
|
|
6836
|
+
const varName = labelToVarName("workorder_" + fieldName);
|
|
6837
|
+
const label = fieldName.replace(/_/g, " ");
|
|
6838
|
+
return registry.register(varName, label, szOpts);
|
|
6839
|
+
}
|
|
6840
|
+
const concatSingle = trimmed.match(
|
|
6841
|
+
/^concat\(\s*'[^']*'\s*,\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
|
|
6842
|
+
);
|
|
6843
|
+
if (concatSingle) {
|
|
6844
|
+
const ref = concatSingle[2] ? `${concatSingle[1]}.${concatSingle[2]}` : concatSingle[1];
|
|
6845
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6846
|
+
const label = fieldRefToLabel(ref);
|
|
6847
|
+
return registry.register(varName, label, szOpts);
|
|
6848
|
+
}
|
|
6849
|
+
const concatMulti = trimmed.match(
|
|
6850
|
+
/^concat\(/i
|
|
6851
|
+
);
|
|
6852
|
+
if (concatMulti) {
|
|
6853
|
+
const refs = extractFieldRefs(trimmed);
|
|
6854
|
+
if (refs.length > 0) {
|
|
6855
|
+
const uniqueRefs = [...new Set(refs)];
|
|
6856
|
+
const tokens = uniqueRefs.map((ref) => {
|
|
6857
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6858
|
+
const label = fieldRefToLabel(ref);
|
|
6859
|
+
return registry.register(varName, label, szOpts);
|
|
6860
|
+
});
|
|
6861
|
+
return tokens.join(" ");
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
const toLongMatch = trimmed.match(
|
|
6865
|
+
/^ToLong\(\s*\[([^\]]+)\](?:\.\[([^\]]+)\])?\s*\)$/i
|
|
6866
|
+
);
|
|
6867
|
+
if (toLongMatch) {
|
|
6868
|
+
const ref = toLongMatch[2] ? `${toLongMatch[1]}.${toLongMatch[2]}` : toLongMatch[1];
|
|
6869
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6870
|
+
const label = fieldRefToLabel(ref);
|
|
6871
|
+
return registry.register(varName, label, szOpts);
|
|
6872
|
+
}
|
|
6873
|
+
const fmtMatch = trimmed.match(
|
|
6874
|
+
/^FormatString\(\s*'[^']*'\s*,\s*(.+)\s*\)$/i
|
|
6875
|
+
);
|
|
6876
|
+
if (fmtMatch) {
|
|
6877
|
+
return resolveExpression(fmtMatch[1], registry, siblingLabel, bandContext);
|
|
6878
|
+
}
|
|
6879
|
+
const replaceMatch = trimmed.match(
|
|
6880
|
+
/^Replace\(\s*\[([^\]]+)\]/i
|
|
6881
|
+
);
|
|
6882
|
+
if (replaceMatch) {
|
|
6883
|
+
const ref = replaceMatch[1];
|
|
6884
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6885
|
+
const label = fieldRefToLabel(ref);
|
|
6886
|
+
return registry.register(varName, label, szOpts);
|
|
6887
|
+
}
|
|
6888
|
+
const iifLabelMatch = trimmed.match(
|
|
6889
|
+
/^iif\s*\(.+?,\s*'([^']+)'\s*,\s*\?\s*\)$/i
|
|
6890
|
+
);
|
|
6891
|
+
if (iifLabelMatch) {
|
|
6892
|
+
return iifLabelMatch[1];
|
|
6893
|
+
}
|
|
6894
|
+
const iifLabelElse = trimmed.match(
|
|
6895
|
+
/^iif\s*\(.+?,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)$/i
|
|
6896
|
+
);
|
|
6897
|
+
if (iifLabelElse) {
|
|
6898
|
+
return iifLabelElse[1];
|
|
6899
|
+
}
|
|
6900
|
+
const iifEmptyElse = trimmed.match(
|
|
6901
|
+
/^[Ii]if\s*\(.+?,\s*'([^']+)'\s*,\s*''\s*\)$/i
|
|
6902
|
+
);
|
|
6903
|
+
if (iifEmptyElse) {
|
|
6904
|
+
return iifEmptyElse[1];
|
|
6905
|
+
}
|
|
6906
|
+
const nestedIifStaticLabel = trimmed.match(
|
|
6907
|
+
/^iif\s*\(.*,\s*[Ii]if\s*\([^,]+,\s*'([^']+)'\s*,/i
|
|
6908
|
+
);
|
|
6909
|
+
if (nestedIifStaticLabel) {
|
|
6910
|
+
return nestedIifStaticLabel[1];
|
|
6911
|
+
}
|
|
6912
|
+
const iifValueMatch = trimmed.match(
|
|
6913
|
+
/^iif\s*\(/i
|
|
6914
|
+
);
|
|
6915
|
+
if (iifValueMatch) {
|
|
6916
|
+
const refs = extractFieldRefs(trimmed);
|
|
6917
|
+
if (refs.length > 0) {
|
|
6918
|
+
const uniqueRefs = [...new Set(refs)];
|
|
6919
|
+
const innerAggMatch = trimmed.match(
|
|
6920
|
+
/\[([^\]]+)\]\.sum\(\[([^\]]+)\]\)/i
|
|
6921
|
+
);
|
|
6922
|
+
if (innerAggMatch) {
|
|
6923
|
+
const fieldName = innerAggMatch[2];
|
|
6924
|
+
const varName2 = labelToVarName("workorder_" + fieldName);
|
|
6925
|
+
const label2 = fieldName.replace(/_/g, " ");
|
|
6926
|
+
return registry.register(varName2, label2, szOpts);
|
|
6927
|
+
}
|
|
6928
|
+
const ref = uniqueRefs[uniqueRefs.length - 1];
|
|
6929
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6930
|
+
const label = fieldRefToLabel(ref);
|
|
6931
|
+
return registry.register(varName, label, szOpts);
|
|
6932
|
+
}
|
|
6933
|
+
const staticMatch = trimmed.match(/'([^']+)'/);
|
|
6934
|
+
if (staticMatch) return staticMatch[1];
|
|
6935
|
+
return "";
|
|
6936
|
+
}
|
|
6937
|
+
const arithmeticRefs = extractFieldRefs(trimmed);
|
|
6938
|
+
if (arithmeticRefs.length > 1 && /[+\-*/]/.test(trimmed)) {
|
|
6939
|
+
if (siblingLabel) {
|
|
6940
|
+
const cleanLabel = siblingLabel.replace(/[:\s]+$/, "");
|
|
6941
|
+
const varName = labelToVarName(
|
|
6942
|
+
(bandContext ? bandContext + "_" : "") + cleanLabel.replace(/\s+/g, "_")
|
|
6943
|
+
);
|
|
6944
|
+
return registry.register(varName, cleanLabel, szOpts);
|
|
6945
|
+
}
|
|
6946
|
+
const uniqueRefs = [...new Set(arithmeticRefs)];
|
|
6947
|
+
const tokens = uniqueRefs.map((ref) => {
|
|
6948
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6949
|
+
const label = fieldRefToLabel(ref);
|
|
6950
|
+
return registry.register(varName, label, szOpts);
|
|
6951
|
+
});
|
|
6952
|
+
return tokens.join(" ");
|
|
6953
|
+
}
|
|
6954
|
+
const simpleMatch = trimmed.match(
|
|
6955
|
+
/^\[([^\]]+)\](?:\.\[([^\]]+)\])?$/
|
|
6956
|
+
);
|
|
6957
|
+
if (simpleMatch) {
|
|
6958
|
+
const ref = simpleMatch[2] ? `${simpleMatch[1]}.${simpleMatch[2]}` : simpleMatch[1];
|
|
6959
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6960
|
+
const label = fieldRefToLabel(ref);
|
|
6961
|
+
return registry.register(varName, label, szOpts);
|
|
6962
|
+
}
|
|
6963
|
+
if (arithmeticRefs.length >= 1) {
|
|
6964
|
+
const ref = arithmeticRefs[arithmeticRefs.length - 1];
|
|
6965
|
+
const varName = fieldRefToVarName(ref, bandContext);
|
|
6966
|
+
const label = fieldRefToLabel(ref);
|
|
6967
|
+
return registry.register(varName, label, szOpts);
|
|
6968
|
+
}
|
|
6969
|
+
return "";
|
|
6970
|
+
}
|
|
6971
|
+
var VariableRegistry = class {
|
|
6972
|
+
constructor() {
|
|
6973
|
+
__publicField(this, "map", /* @__PURE__ */ new Map());
|
|
6974
|
+
}
|
|
6975
|
+
register(varName, varLabel, options) {
|
|
6976
|
+
if (!this.map.has(varName)) {
|
|
6977
|
+
this.map.set(varName, { varName, varLabel, varDefault: "" });
|
|
6978
|
+
}
|
|
6979
|
+
const suppress = options?.suppressZero ? "|suppress:zero" : "";
|
|
6980
|
+
return `{{var|name:${varName}|label:${varLabel}${suppress}}}`;
|
|
6981
|
+
}
|
|
6982
|
+
getAll() {
|
|
6983
|
+
return Array.from(this.map.values());
|
|
6984
|
+
}
|
|
6985
|
+
};
|
|
6986
|
+
function parseLocationFloat(s) {
|
|
6987
|
+
if (!s) return { x: 0, y: 0 };
|
|
6988
|
+
const parts = s.split(",");
|
|
6989
|
+
return {
|
|
6990
|
+
x: parseFloat(parts[0]) || 0,
|
|
6991
|
+
y: parseFloat(parts[1]) || 0
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
function parseSizeF(s) {
|
|
6995
|
+
if (!s) return { w: 0, h: 0 };
|
|
6996
|
+
const parts = s.split(",");
|
|
6997
|
+
return {
|
|
6998
|
+
w: parseFloat(parts[0]) || 0,
|
|
6999
|
+
h: parseFloat(parts[1]) || 0
|
|
7000
|
+
};
|
|
7001
|
+
}
|
|
7002
|
+
function extractControls(bandEl) {
|
|
7003
|
+
const controlsEl = bandEl.querySelector(":scope > Controls");
|
|
7004
|
+
if (!controlsEl) return [];
|
|
7005
|
+
const items = [];
|
|
7006
|
+
for (const child of Array.from(controlsEl.children)) {
|
|
7007
|
+
items.push(extractControl(child));
|
|
7008
|
+
}
|
|
7009
|
+
return items;
|
|
7010
|
+
}
|
|
7011
|
+
function extractControl(el) {
|
|
7012
|
+
const ctrlType = el.getAttribute("ControlType") || "";
|
|
7013
|
+
const name = el.getAttribute("Name") || "";
|
|
7014
|
+
const text = el.getAttribute("Text") || "";
|
|
7015
|
+
const loc = parseLocationFloat(el.getAttribute("LocationFloat"));
|
|
7016
|
+
const size = parseSizeF(el.getAttribute("SizeF"));
|
|
7017
|
+
const formatString = el.getAttribute("TextFormatString") || null;
|
|
7018
|
+
let expression = null;
|
|
7019
|
+
const bindingsEl = el.querySelector(":scope > ExpressionBindings");
|
|
7020
|
+
if (bindingsEl) {
|
|
7021
|
+
for (const item of Array.from(bindingsEl.children)) {
|
|
7022
|
+
if (item.getAttribute("PropertyName") === "Text" && item.getAttribute("EventName") === "BeforePrint") {
|
|
7023
|
+
expression = item.getAttribute("Expression");
|
|
7024
|
+
break;
|
|
7025
|
+
}
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
const children = [];
|
|
7029
|
+
if (ctrlType === "XRPanel") {
|
|
7030
|
+
const panelControls = extractControls(el);
|
|
7031
|
+
children.push(...panelControls);
|
|
7032
|
+
}
|
|
7033
|
+
return {
|
|
7034
|
+
name,
|
|
7035
|
+
type: ctrlType,
|
|
7036
|
+
text,
|
|
7037
|
+
x: loc.x,
|
|
7038
|
+
y: loc.y,
|
|
7039
|
+
width: size.w,
|
|
7040
|
+
expression,
|
|
7041
|
+
formatString,
|
|
7042
|
+
children
|
|
7043
|
+
};
|
|
7044
|
+
}
|
|
7045
|
+
function groupByRow(controls, threshold = 8) {
|
|
7046
|
+
const sorted = [...controls].sort((a, b) => a.y - b.y);
|
|
7047
|
+
const rows = [];
|
|
7048
|
+
for (const ctrl of sorted) {
|
|
7049
|
+
const lastRow = rows[rows.length - 1];
|
|
7050
|
+
if (lastRow && Math.abs(ctrl.y - lastRow.y) < threshold) {
|
|
7051
|
+
lastRow.controls.push(ctrl);
|
|
7052
|
+
} else {
|
|
7053
|
+
rows.push({ y: ctrl.y, controls: [ctrl] });
|
|
7054
|
+
}
|
|
7055
|
+
}
|
|
7056
|
+
for (const row of rows) {
|
|
7057
|
+
row.controls.sort((a, b) => a.x - b.x);
|
|
7058
|
+
}
|
|
7059
|
+
return rows;
|
|
7060
|
+
}
|
|
7061
|
+
function isStaticLabel(ctrl) {
|
|
7062
|
+
return ctrl.type === "XRLabel" && !ctrl.expression;
|
|
7063
|
+
}
|
|
7064
|
+
function isBound(ctrl) {
|
|
7065
|
+
return ctrl.type === "XRLabel" && !!ctrl.expression;
|
|
7066
|
+
}
|
|
7067
|
+
function computeSplitPercent(leftControls, rightControls, contentWidth) {
|
|
7068
|
+
const leftExtent = leftControls.reduce(
|
|
7069
|
+
(max, c) => Math.max(max, c.x + c.width),
|
|
7070
|
+
0
|
|
7071
|
+
);
|
|
7072
|
+
const rightMinX = rightControls.reduce(
|
|
7073
|
+
(min, c) => Math.min(min, c.x),
|
|
7074
|
+
contentWidth
|
|
7075
|
+
);
|
|
7076
|
+
const rightMaxExtent = rightControls.reduce(
|
|
7077
|
+
(max, c) => Math.max(max, c.x + c.width),
|
|
7078
|
+
0
|
|
7079
|
+
);
|
|
7080
|
+
const rightExtent = rightMaxExtent - rightMinX;
|
|
7081
|
+
if (leftExtent + rightExtent === 0) return 50;
|
|
7082
|
+
return Math.round(leftExtent / (leftExtent + rightExtent) * 100);
|
|
7083
|
+
}
|
|
7084
|
+
function renderTopMarginBand(controls, registry, contentWidth, calculatedFieldNames) {
|
|
7085
|
+
const lines = [];
|
|
7086
|
+
const leftControls = controls.filter((c) => c.x < 437);
|
|
7087
|
+
const rightControls = controls.filter((c) => c.x >= 437);
|
|
7088
|
+
const leftEntries = [];
|
|
7089
|
+
if (leftControls.length > 0) {
|
|
7090
|
+
const leftRows = groupByRow(leftControls);
|
|
7091
|
+
for (const row of leftRows) {
|
|
7092
|
+
const parts = [];
|
|
7093
|
+
for (const ctrl of row.controls) {
|
|
7094
|
+
if (isBound(ctrl)) {
|
|
7095
|
+
let resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
|
|
7096
|
+
if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && resolved.startsWith("{{var|")) {
|
|
7097
|
+
resolved = resolved.replace("}}", "|format:phone}}");
|
|
7098
|
+
}
|
|
7099
|
+
if (resolved) parts.push(resolved);
|
|
7100
|
+
} else if (isStaticLabel(ctrl)) {
|
|
7101
|
+
const text = ctrl.text.trim();
|
|
7102
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7103
|
+
parts.push(text);
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
}
|
|
7107
|
+
if (parts.length > 0) {
|
|
7108
|
+
leftEntries.push(parts.join(" "));
|
|
7109
|
+
}
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
7112
|
+
const rightEntries = [];
|
|
7113
|
+
if (rightControls.length > 0) {
|
|
7114
|
+
const rightRows = groupByRow(rightControls);
|
|
7115
|
+
for (const row of rightRows) {
|
|
7116
|
+
let label = "";
|
|
7117
|
+
let value = "";
|
|
7118
|
+
for (const ctrl of row.controls) {
|
|
7119
|
+
if (isStaticLabel(ctrl)) {
|
|
7120
|
+
const text = ctrl.text.trim();
|
|
7121
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7122
|
+
label = text;
|
|
7123
|
+
}
|
|
7124
|
+
} else if (isBound(ctrl)) {
|
|
7125
|
+
value = resolveExpression(ctrl.expression, registry, label, "workorder");
|
|
7126
|
+
if (ctrl.expression) {
|
|
7127
|
+
const refs = extractFieldRefs(ctrl.expression);
|
|
7128
|
+
if (refs.some((ref) => calculatedFieldNames.has(ref)) && value.startsWith("{{var|")) {
|
|
7129
|
+
value = value.replace("}}", "|suppress:zero}}");
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
|
|
7133
|
+
value = value.replace("}}", "|format:phone}}");
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
if (label || value) {
|
|
7138
|
+
rightEntries.push({ label, value, y: row.y });
|
|
7139
|
+
}
|
|
7140
|
+
}
|
|
7141
|
+
}
|
|
7142
|
+
if (leftEntries.length > 0 || rightEntries.length > 0) {
|
|
7143
|
+
const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
|
|
7144
|
+
lines.push(`:::columns{split:${outerSplit}}`);
|
|
7145
|
+
const leftMinY = leftControls.length > 0 ? Math.min(...leftControls.map((c) => c.y)) : 0;
|
|
7146
|
+
const rightMinY = rightControls.length > 0 ? Math.min(...rightControls.map((c) => c.y)) : 0;
|
|
7147
|
+
const xmlLineHeight = 17;
|
|
7148
|
+
const leftPadTop = leftMinY > rightMinY ? Math.round((leftMinY - rightMinY) / xmlLineHeight) : 0;
|
|
7149
|
+
const rightPadTop = rightMinY > leftMinY ? Math.round((rightMinY - leftMinY) / xmlLineHeight) : 0;
|
|
7150
|
+
const leftColTag = leftPadTop ? `:::col{padTop:${leftPadTop}}` : ":::col";
|
|
7151
|
+
lines.push(leftColTag);
|
|
7152
|
+
for (const entry of leftEntries) {
|
|
7153
|
+
lines.push(entry);
|
|
7154
|
+
}
|
|
7155
|
+
lines.push(":::");
|
|
7156
|
+
const rightColTag = rightPadTop ? `:::col{padTop:${rightPadTop}}` : ":::col";
|
|
7157
|
+
lines.push(rightColTag);
|
|
7158
|
+
if (rightEntries.length > 0) {
|
|
7159
|
+
lines.push("| | |");
|
|
7160
|
+
lines.push("|---|---|");
|
|
7161
|
+
let prevY = -Infinity;
|
|
7162
|
+
for (const entry of rightEntries) {
|
|
7163
|
+
if (prevY > -Infinity && entry.y - prevY > 25) {
|
|
7164
|
+
lines.push("| | |");
|
|
7165
|
+
}
|
|
7166
|
+
const label = entry.label || "";
|
|
7167
|
+
const colon = entry.label ? ":" : "";
|
|
7168
|
+
const value = entry.value ? ` ${entry.value}` : "";
|
|
7169
|
+
lines.push(`| ${label} | ${colon}${value} |`);
|
|
7170
|
+
prevY = entry.y;
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
lines.push(":::");
|
|
7174
|
+
lines.push(":::");
|
|
7175
|
+
lines.push("");
|
|
7176
|
+
}
|
|
7177
|
+
return lines.join("\n");
|
|
7178
|
+
}
|
|
7179
|
+
function renderReportHeaderBand(controls, registry, contentWidth) {
|
|
7180
|
+
const lines = [];
|
|
7181
|
+
const leftControls = controls.filter((c) => c.x < 437);
|
|
7182
|
+
const rightControls = controls.filter((c) => c.x >= 437);
|
|
7183
|
+
const leftEntries = [];
|
|
7184
|
+
if (leftControls.length > 0) {
|
|
7185
|
+
const leftRows = groupByRow(leftControls);
|
|
7186
|
+
for (const row of leftRows) {
|
|
7187
|
+
const parts = [];
|
|
7188
|
+
for (const ctrl of row.controls) {
|
|
7189
|
+
if (isBound(ctrl)) {
|
|
7190
|
+
const resolved = resolveExpression(ctrl.expression, registry, void 0, "workorder");
|
|
7191
|
+
if (resolved) parts.push(resolved);
|
|
7192
|
+
} else if (isStaticLabel(ctrl)) {
|
|
7193
|
+
const text = ctrl.text.trim();
|
|
7194
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7195
|
+
parts.push(text);
|
|
7196
|
+
}
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
if (parts.length > 0) {
|
|
7200
|
+
leftEntries.push(parts.join(" "));
|
|
7201
|
+
}
|
|
7202
|
+
}
|
|
7203
|
+
}
|
|
7204
|
+
const rightEntries = [];
|
|
7205
|
+
if (rightControls.length > 0) {
|
|
7206
|
+
const rightRows = groupByRow(rightControls);
|
|
7207
|
+
for (const row of rightRows) {
|
|
7208
|
+
let label = "";
|
|
7209
|
+
let value = "";
|
|
7210
|
+
for (const ctrl of row.controls) {
|
|
7211
|
+
if (isStaticLabel(ctrl)) {
|
|
7212
|
+
const text = ctrl.text.trim();
|
|
7213
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7214
|
+
label = text;
|
|
7215
|
+
}
|
|
7216
|
+
} else if (isBound(ctrl)) {
|
|
7217
|
+
value = resolveExpression(ctrl.expression, registry, void 0, "workorder");
|
|
7218
|
+
if (ctrl.formatString && /\(###\)\s*###\s*-\s*####/.test(ctrl.formatString) && value.startsWith("{{var|")) {
|
|
7219
|
+
value = value.replace("}}", "|format:phone}}");
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
}
|
|
7223
|
+
if (label || value) {
|
|
7224
|
+
rightEntries.push({ label, value });
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
if (leftEntries.length > 0 || rightEntries.length > 0) {
|
|
7229
|
+
const outerSplit = computeSplitPercent(leftControls, rightControls, contentWidth);
|
|
7230
|
+
lines.push(`:::columns{split:${outerSplit}}`);
|
|
7231
|
+
lines.push(":::col");
|
|
7232
|
+
for (const entry of leftEntries) {
|
|
7233
|
+
lines.push(entry);
|
|
7234
|
+
}
|
|
7235
|
+
lines.push(":::");
|
|
7236
|
+
lines.push(":::col");
|
|
7237
|
+
lines.push("| | |");
|
|
7238
|
+
lines.push("|---|---|");
|
|
7239
|
+
for (const entry of rightEntries) {
|
|
7240
|
+
const label = entry.label || "";
|
|
7241
|
+
const value = entry.value ? ` ${entry.value}` : "";
|
|
7242
|
+
lines.push(`| ${label} |${value} |`);
|
|
7243
|
+
}
|
|
7244
|
+
lines.push(":::");
|
|
7245
|
+
lines.push(":::");
|
|
7246
|
+
lines.push("");
|
|
7247
|
+
}
|
|
7248
|
+
return lines.join("\n");
|
|
7249
|
+
}
|
|
7250
|
+
function renderOperationsSection(pageHeaderControls, detailControls, groupFooterControls, registry, contentWidth) {
|
|
7251
|
+
const lines = [];
|
|
7252
|
+
const headerTexts = [];
|
|
7253
|
+
for (const ctrl of [...pageHeaderControls].sort((a, b) => a.x - b.x)) {
|
|
7254
|
+
if (isStaticLabel(ctrl)) {
|
|
7255
|
+
const text = ctrl.text.trim();
|
|
7256
|
+
if (text) headerTexts.push(text);
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
const titleText = headerTexts.join(" ");
|
|
7260
|
+
lines.push(`:::panel{title:${titleText}|headerStyle:dark|border:none}`);
|
|
7261
|
+
lines.push(":::");
|
|
7262
|
+
lines.push(":::repeat{data:operations}");
|
|
7263
|
+
const sortedDetail = [...detailControls].sort((a, b) => a.x - b.x);
|
|
7264
|
+
const detailParts = [];
|
|
7265
|
+
for (const ctrl of sortedDetail) {
|
|
7266
|
+
if (isBound(ctrl)) {
|
|
7267
|
+
const resolved = resolveExpression(ctrl.expression, registry, void 0, "operation");
|
|
7268
|
+
if (resolved) detailParts.push(resolved);
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7271
|
+
if (detailParts.length > 0) {
|
|
7272
|
+
if (detailParts.length >= 2) {
|
|
7273
|
+
lines.push(":::columns{split:12}");
|
|
7274
|
+
lines.push(":::col");
|
|
7275
|
+
lines.push(`**${detailParts[0]}**`);
|
|
7276
|
+
lines.push(":::");
|
|
7277
|
+
lines.push(":::col");
|
|
7278
|
+
lines.push(`**${detailParts.slice(1).join(" ")}**`);
|
|
7279
|
+
lines.push(":::");
|
|
7280
|
+
lines.push(":::");
|
|
7281
|
+
} else {
|
|
7282
|
+
lines.push(`**${detailParts[0]}**`);
|
|
7283
|
+
}
|
|
7284
|
+
lines.push("");
|
|
7285
|
+
}
|
|
7286
|
+
if (groupFooterControls.length > 0) {
|
|
7287
|
+
const subtotalsMarkdown = renderGroupFooterBand(groupFooterControls, registry);
|
|
7288
|
+
if (subtotalsMarkdown) lines.push(subtotalsMarkdown);
|
|
7289
|
+
}
|
|
7290
|
+
lines.push("---");
|
|
7291
|
+
lines.push(":::");
|
|
7292
|
+
lines.push("");
|
|
7293
|
+
return lines.join("\n");
|
|
7294
|
+
}
|
|
7295
|
+
function renderGroupFooterBand(controls, registry, _contentWidth) {
|
|
7296
|
+
const rows = groupByRow(controls);
|
|
7297
|
+
const entries = [];
|
|
7298
|
+
for (const row of rows) {
|
|
7299
|
+
const labels = row.controls.filter((c) => c.type === "XRLabel");
|
|
7300
|
+
if (labels.length === 0) continue;
|
|
7301
|
+
let label = "";
|
|
7302
|
+
let value = "";
|
|
7303
|
+
const sorted = [...labels].sort((a, b) => a.x - b.x);
|
|
7304
|
+
for (const ctrl of sorted) {
|
|
7305
|
+
if (isStaticLabel(ctrl)) {
|
|
7306
|
+
const text = ctrl.text.trim();
|
|
7307
|
+
if (text && !text.match(/^label\d+$/)) {
|
|
7308
|
+
label = text;
|
|
7309
|
+
}
|
|
7310
|
+
} else if (isBound(ctrl)) {
|
|
7311
|
+
const resolved = resolveExpression(ctrl.expression, registry, label, "operation");
|
|
7312
|
+
if (resolved) {
|
|
7313
|
+
if (!resolved.startsWith("{{var|")) {
|
|
7314
|
+
if (!label) label = resolved;
|
|
7315
|
+
} else {
|
|
7316
|
+
value = resolved;
|
|
7317
|
+
}
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
if (label && value) {
|
|
7322
|
+
entries.push({ label, value });
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
if (entries.length === 0) return "";
|
|
7326
|
+
const lines = [];
|
|
7327
|
+
lines.push(":::subtotals");
|
|
7328
|
+
for (const entry of entries) {
|
|
7329
|
+
lines.push(`**${entry.label}** ${entry.value}`);
|
|
7330
|
+
}
|
|
7331
|
+
lines.push(":::");
|
|
7332
|
+
lines.push("");
|
|
7333
|
+
return lines.join("\n");
|
|
7334
|
+
}
|
|
7335
|
+
function renderReportFooterBand(controls, registry) {
|
|
7336
|
+
const lines = [];
|
|
7337
|
+
const panel = controls.find((c) => c.type === "XRPanel");
|
|
7338
|
+
const thankYouLabel = controls.find(
|
|
7339
|
+
(c) => c.type === "XRLabel" && c.text.includes("Thank You")
|
|
7340
|
+
);
|
|
7341
|
+
const priceRows = [];
|
|
7342
|
+
if (panel && panel.children.length > 0) {
|
|
7343
|
+
const rows = groupByRow(panel.children, 15);
|
|
7344
|
+
for (const row of rows) {
|
|
7345
|
+
const labels = row.controls.filter((c) => c.type === "XRLabel");
|
|
7346
|
+
if (labels.length === 0) continue;
|
|
7347
|
+
let label = "";
|
|
7348
|
+
let value = "";
|
|
7349
|
+
const sorted = [...labels].sort((a, b) => a.x - b.x);
|
|
7350
|
+
for (const ctrl of sorted) {
|
|
7351
|
+
if (isStaticLabel(ctrl) && ctrl.text.includes("Estimate Price Summary")) {
|
|
7352
|
+
continue;
|
|
7353
|
+
}
|
|
7354
|
+
const isLabelPos = ctrl.x < 150;
|
|
7355
|
+
if (isStaticLabel(ctrl)) {
|
|
7356
|
+
const text = ctrl.text.trim();
|
|
7357
|
+
if (isLabelPos && text && !text.match(/^label\d+$/) && text !== "0.00") {
|
|
7358
|
+
label = text;
|
|
7359
|
+
}
|
|
7360
|
+
} else if (isBound(ctrl)) {
|
|
7361
|
+
const resolved = resolveExpression(ctrl.expression, registry, label || void 0, "workorder");
|
|
7362
|
+
if (resolved) {
|
|
7363
|
+
if (isLabelPos) {
|
|
7364
|
+
if (!label) label = resolved;
|
|
7365
|
+
} else {
|
|
7366
|
+
if (!value) value = resolved;
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
}
|
|
7371
|
+
if (label && value) {
|
|
7372
|
+
priceRows.push({ label, value });
|
|
7373
|
+
} else if (value) {
|
|
7374
|
+
priceRows.push({ label: "", value });
|
|
7375
|
+
}
|
|
7376
|
+
}
|
|
7377
|
+
}
|
|
7378
|
+
lines.push(":::columns{split:40|padX:20}");
|
|
7379
|
+
lines.push(":::col{padTop:1}");
|
|
7380
|
+
if (thankYouLabel) {
|
|
7381
|
+
lines.push(`## ***${thankYouLabel.text.trim()}***`);
|
|
7382
|
+
}
|
|
7383
|
+
lines.push(":::");
|
|
7384
|
+
lines.push(":::col{padTop:1}");
|
|
7385
|
+
lines.push(":::panel{title:Estimate Price Summary|border:solid|headerStyle:dark}");
|
|
7386
|
+
if (priceRows.length > 0) {
|
|
7387
|
+
lines.push(":::subtotals");
|
|
7388
|
+
for (const row of priceRows) {
|
|
7389
|
+
const label = row.label ? `**${row.label}**` : "";
|
|
7390
|
+
lines.push(`${label} ${row.value}`);
|
|
7391
|
+
}
|
|
7392
|
+
lines.push(":::");
|
|
7393
|
+
}
|
|
7394
|
+
lines.push(":::");
|
|
7395
|
+
lines.push(":::");
|
|
7396
|
+
lines.push(":::");
|
|
7397
|
+
lines.push("");
|
|
7398
|
+
return lines.join("\n");
|
|
7399
|
+
}
|
|
7400
|
+
function parseXmlTemplate(xmlString) {
|
|
7401
|
+
const warnings = [];
|
|
7402
|
+
const registry = new VariableRegistry();
|
|
7403
|
+
const parser = new DOMParser();
|
|
7404
|
+
const doc = parser.parseFromString(xmlString, "text/xml");
|
|
7405
|
+
const parseError = doc.querySelector("parsererror");
|
|
7406
|
+
if (parseError) {
|
|
7407
|
+
return {
|
|
7408
|
+
markdown: "",
|
|
7409
|
+
variables: [],
|
|
7410
|
+
reportName: "",
|
|
7411
|
+
warnings: ["XML parse error: " + (parseError.textContent || "Unknown error")]
|
|
7412
|
+
};
|
|
7413
|
+
}
|
|
7414
|
+
const root = doc.documentElement;
|
|
7415
|
+
const reportName = root.getAttribute("DisplayName") || root.getAttribute("Name") || "Report";
|
|
7416
|
+
const pageWidth = parseInt(root.getAttribute("PageWidth") || "850", 10);
|
|
7417
|
+
const marginsStr = root.getAttribute("Margins") || "50,50,250,50";
|
|
7418
|
+
const marginParts = marginsStr.split(",").map(Number);
|
|
7419
|
+
const leftMargin = marginParts[0] || 50;
|
|
7420
|
+
const rightMargin = marginParts[1] || 50;
|
|
7421
|
+
const contentWidth = pageWidth - leftMargin - rightMargin;
|
|
7422
|
+
const calculatedFieldNames = /* @__PURE__ */ new Set();
|
|
7423
|
+
const calcFieldsEl = root.querySelector(":scope > CalculatedFields");
|
|
7424
|
+
if (calcFieldsEl) {
|
|
7425
|
+
for (const item of Array.from(calcFieldsEl.children)) {
|
|
7426
|
+
const name = item.getAttribute("Name");
|
|
7427
|
+
if (name) calculatedFieldNames.add(name);
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7430
|
+
const markdownSections = [];
|
|
7431
|
+
const watermarkEl = root.querySelector("Watermarks > Item1");
|
|
7432
|
+
if (watermarkEl) {
|
|
7433
|
+
const wmText = watermarkEl.getAttribute("Text");
|
|
7434
|
+
if (wmText) {
|
|
7435
|
+
markdownSections.push(`:::watermark{text:${wmText}|opacity:0.15|angle:45}`);
|
|
7436
|
+
markdownSections.push("");
|
|
7437
|
+
}
|
|
7438
|
+
}
|
|
7439
|
+
const bandsEl = root.querySelector(":scope > Bands");
|
|
7440
|
+
if (!bandsEl) {
|
|
7441
|
+
warnings.push("No <Bands> element found in the XML");
|
|
7442
|
+
return {
|
|
7443
|
+
markdown: "",
|
|
7444
|
+
variables: registry.getAll(),
|
|
7445
|
+
reportName,
|
|
7446
|
+
warnings
|
|
7447
|
+
};
|
|
7448
|
+
}
|
|
7449
|
+
let topMarginControls = [];
|
|
7450
|
+
let reportHeaderControls = [];
|
|
7451
|
+
let pageHeaderControls = [];
|
|
7452
|
+
let detailControls = [];
|
|
7453
|
+
let groupFooterControls = [];
|
|
7454
|
+
let reportFooterControls = [];
|
|
7455
|
+
for (const bandItem of Array.from(bandsEl.children)) {
|
|
7456
|
+
const ctrlType = bandItem.getAttribute("ControlType") || "";
|
|
7457
|
+
if (ctrlType === "TopMarginBand") {
|
|
7458
|
+
topMarginControls = extractControls(bandItem);
|
|
7459
|
+
} else if (ctrlType === "ReportHeaderBand") {
|
|
7460
|
+
reportHeaderControls = extractControls(bandItem);
|
|
7461
|
+
} else if (ctrlType === "PageHeaderBand") {
|
|
7462
|
+
pageHeaderControls = extractControls(bandItem);
|
|
7463
|
+
} else if (ctrlType === "DetailReportBand") {
|
|
7464
|
+
const nestedBands = bandItem.querySelector(":scope > Bands");
|
|
7465
|
+
if (nestedBands) {
|
|
7466
|
+
for (const nested of Array.from(nestedBands.children)) {
|
|
7467
|
+
const nestedType = nested.getAttribute("ControlType") || "";
|
|
7468
|
+
if (nestedType === "DetailBand") {
|
|
7469
|
+
detailControls = extractControls(nested);
|
|
7470
|
+
} else if (nestedType === "GroupFooterBand") {
|
|
7471
|
+
groupFooterControls = extractControls(nested);
|
|
7472
|
+
}
|
|
7473
|
+
}
|
|
7474
|
+
}
|
|
7475
|
+
} else if (ctrlType === "ReportFooterBand") {
|
|
7476
|
+
reportFooterControls = extractControls(bandItem);
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
if (topMarginControls.length > 0) {
|
|
7480
|
+
markdownSections.push(renderTopMarginBand(topMarginControls, registry, contentWidth, calculatedFieldNames));
|
|
7481
|
+
}
|
|
7482
|
+
if (reportHeaderControls.length > 0) {
|
|
7483
|
+
markdownSections.push(
|
|
7484
|
+
renderReportHeaderBand(reportHeaderControls, registry, contentWidth)
|
|
7485
|
+
);
|
|
7486
|
+
}
|
|
7487
|
+
if (pageHeaderControls.length > 0 || detailControls.length > 0) {
|
|
7488
|
+
markdownSections.push(
|
|
7489
|
+
renderOperationsSection(
|
|
7490
|
+
pageHeaderControls,
|
|
7491
|
+
detailControls,
|
|
7492
|
+
groupFooterControls,
|
|
7493
|
+
registry)
|
|
7494
|
+
);
|
|
7495
|
+
}
|
|
7496
|
+
if (reportFooterControls.length > 0) {
|
|
7497
|
+
markdownSections.push(
|
|
7498
|
+
renderReportFooterBand(reportFooterControls, registry)
|
|
7499
|
+
);
|
|
7500
|
+
}
|
|
7501
|
+
const markdown = markdownSections.join("\n").replace(/\n{4,}/g, "\n\n\n");
|
|
7502
|
+
return {
|
|
7503
|
+
markdown,
|
|
7504
|
+
variables: registry.getAll(),
|
|
7505
|
+
reportName,
|
|
7506
|
+
warnings
|
|
7507
|
+
};
|
|
7508
|
+
}
|
|
7509
|
+
|
|
7510
|
+
export { DocumentGenerator, EditorPanel, FormFieldType, ImageNode, PreviewPanel, RepeatNode, SubtotalsNode, ThemeProvider, expandBlockVariables, expandRepeatContent, extractVariablesFromContent, generatePdfFromMarkdown, generatePdfFromTiptap, isZeroLike, markdownToTiptap, parseXmlTemplate, suppressZeroContent, tiptapToMarkdown, useDocumentGenerator, useTheme };
|
|
4929
7511
|
//# sourceMappingURL=index.mjs.map
|
|
4930
7512
|
//# sourceMappingURL=index.mjs.map
|