@carlonicora/nestjs-neo4jsonapi 1.73.0 → 1.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/blocknote/blocknote.module.d.ts +1 -0
- package/dist/core/blocknote/blocknote.module.d.ts.map +1 -1
- package/dist/core/blocknote/blocknote.module.js +4 -2
- package/dist/core/blocknote/blocknote.module.js.map +1 -1
- package/dist/core/blocknote/index.d.ts +1 -0
- package/dist/core/blocknote/index.d.ts.map +1 -1
- package/dist/core/blocknote/index.js +1 -0
- package/dist/core/blocknote/index.js.map +1 -1
- package/dist/core/blocknote/services/blocknote-to-docx.service.d.ts +55 -0
- package/dist/core/blocknote/services/blocknote-to-docx.service.d.ts.map +1 -0
- package/dist/core/blocknote/services/blocknote-to-docx.service.js +903 -0
- package/dist/core/blocknote/services/blocknote-to-docx.service.js.map +1 -0
- package/dist/core/document/document.module.d.ts +25 -0
- package/dist/core/document/document.module.d.ts.map +1 -0
- package/dist/core/document/document.module.js +46 -0
- package/dist/core/document/document.module.js.map +1 -0
- package/dist/core/document/index.d.ts +6 -0
- package/dist/core/document/index.d.ts.map +1 -0
- package/dist/core/document/index.js +22 -0
- package/dist/core/document/index.js.map +1 -0
- package/dist/core/document/services/abstract-document-generator.service.d.ts +90 -0
- package/dist/core/document/services/abstract-document-generator.service.d.ts.map +1 -0
- package/dist/core/document/services/abstract-document-generator.service.js +120 -0
- package/dist/core/document/services/abstract-document-generator.service.js.map +1 -0
- package/dist/core/document/services/docx-template.service.d.ts +36 -0
- package/dist/core/document/services/docx-template.service.d.ts.map +1 -0
- package/dist/core/document/services/docx-template.service.js +58 -0
- package/dist/core/document/services/docx-template.service.js.map +1 -0
- package/dist/core/document/utils/inject-draft-watermark.d.ts +24 -0
- package/dist/core/document/utils/inject-draft-watermark.d.ts.map +1 -0
- package/dist/core/document/utils/inject-draft-watermark.js +182 -0
- package/dist/core/document/utils/inject-draft-watermark.js.map +1 -0
- package/dist/core/document/utils/inject-xml.d.ts +15 -0
- package/dist/core/document/utils/inject-xml.d.ts.map +1 -0
- package/dist/core/document/utils/inject-xml.js +35 -0
- package/dist/core/document/utils/inject-xml.js.map +1 -0
- package/dist/core/pdf/index.d.ts +3 -0
- package/dist/core/pdf/index.d.ts.map +1 -0
- package/dist/core/pdf/index.js +19 -0
- package/dist/core/pdf/index.js.map +1 -0
- package/dist/core/pdf/pdf.module.d.ts +21 -0
- package/dist/core/pdf/pdf.module.d.ts.map +1 -0
- package/dist/core/pdf/pdf.module.js +39 -0
- package/dist/core/pdf/pdf.module.js.map +1 -0
- package/dist/core/pdf/services/docx-to-pdf.service.d.ts +28 -0
- package/dist/core/pdf/services/docx-to-pdf.service.d.ts.map +1 -0
- package/dist/core/pdf/services/docx-to-pdf.service.js +86 -0
- package/dist/core/pdf/services/docx-to-pdf.service.js.map +1 -0
- package/dist/foundations/stripe/__tests__/mocks/stripe.mock.d.ts +62 -62
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.BlockNoteToDocxService = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const docx_1 = require("docx");
|
|
15
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Image helpers (Node-compatible replacements for browser Image API)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/** Detects image MIME type from URL extension or Content-Type header. */
|
|
20
|
+
function detectImageType(url, contentType) {
|
|
21
|
+
if (contentType) {
|
|
22
|
+
if (contentType.includes("png"))
|
|
23
|
+
return "png";
|
|
24
|
+
if (contentType.includes("jpeg") || contentType.includes("jpg"))
|
|
25
|
+
return "jpg";
|
|
26
|
+
if (contentType.includes("gif"))
|
|
27
|
+
return "gif";
|
|
28
|
+
if (contentType.includes("webp"))
|
|
29
|
+
return "webp";
|
|
30
|
+
}
|
|
31
|
+
const urlLower = url.toLowerCase();
|
|
32
|
+
if (urlLower.endsWith(".png") || urlLower.includes(".png?"))
|
|
33
|
+
return "png";
|
|
34
|
+
if (urlLower.endsWith(".gif") || urlLower.includes(".gif?"))
|
|
35
|
+
return "gif";
|
|
36
|
+
if (urlLower.endsWith(".webp") || urlLower.includes(".webp?"))
|
|
37
|
+
return "webp";
|
|
38
|
+
return "jpg";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Calculates scaled dimensions to fit within max bounds while preserving
|
|
42
|
+
* aspect ratio. Never upscales (scale capped at 1).
|
|
43
|
+
*/
|
|
44
|
+
function calculateScaledDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
|
|
45
|
+
const scale = Math.min(maxWidth / originalWidth, maxHeight / originalHeight, 1);
|
|
46
|
+
return {
|
|
47
|
+
width: Math.round(originalWidth * scale),
|
|
48
|
+
height: Math.round(originalHeight * scale),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Uses `sharp` to read image dimensions from a Node.js Buffer.
|
|
53
|
+
* Falls back to (200, 50) defaults on failure.
|
|
54
|
+
*/
|
|
55
|
+
async function getImageDimensionsFromBuffer(buffer) {
|
|
56
|
+
try {
|
|
57
|
+
const metadata = await (0, sharp_1.default)(buffer).metadata();
|
|
58
|
+
return {
|
|
59
|
+
width: metadata.width ?? 200,
|
|
60
|
+
height: metadata.height ?? 50,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { width: 200, height: 50 };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Fetches a logo image from an absolute URL and returns it as a Buffer
|
|
69
|
+
* together with its type and scaled dimensions.
|
|
70
|
+
*
|
|
71
|
+
* Returns null on any network or processing error so that document
|
|
72
|
+
* generation can proceed without the logo.
|
|
73
|
+
*/
|
|
74
|
+
async function fetchLogoImage(logo) {
|
|
75
|
+
try {
|
|
76
|
+
// On the backend every logo URL must already be absolute.
|
|
77
|
+
const response = await fetch(logo);
|
|
78
|
+
if (!response.ok)
|
|
79
|
+
return null;
|
|
80
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
81
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
82
|
+
const contentType = response.headers.get("content-type") ?? undefined;
|
|
83
|
+
const type = detectImageType(logo, contentType);
|
|
84
|
+
const dimensions = await getImageDimensionsFromBuffer(buffer);
|
|
85
|
+
const scaled = calculateScaledDimensions(dimensions.width, dimensions.height, 200, 50);
|
|
86
|
+
return { buffer, type, ...scaled };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error("[BlockNoteToDocxService] Error fetching logo:", error);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// DOCX header / footer builders
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
async function createDocumentHeader(company) {
|
|
97
|
+
let logoData = null;
|
|
98
|
+
const logoUrl = company.logoUrl ?? company.logo;
|
|
99
|
+
if (logoUrl) {
|
|
100
|
+
logoData = await fetchLogoImage(logoUrl);
|
|
101
|
+
}
|
|
102
|
+
const leftColumnParagraphs = [];
|
|
103
|
+
if (logoData) {
|
|
104
|
+
try {
|
|
105
|
+
leftColumnParagraphs.push(new docx_1.Paragraph({
|
|
106
|
+
children: [
|
|
107
|
+
new docx_1.ImageRun({
|
|
108
|
+
data: logoData.buffer,
|
|
109
|
+
transformation: { width: logoData.width, height: logoData.height },
|
|
110
|
+
type: logoData.type,
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
113
|
+
spacing: { after: 80 },
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error("[BlockNoteToDocxService] Failed to embed logo in header:", error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
leftColumnParagraphs.push(new docx_1.Paragraph({
|
|
121
|
+
children: [new docx_1.TextRun({ text: company.name, bold: true, size: 24 })],
|
|
122
|
+
spacing: { after: 40 },
|
|
123
|
+
}));
|
|
124
|
+
const rightColumnParagraphs = [];
|
|
125
|
+
if (company.legal_address) {
|
|
126
|
+
rightColumnParagraphs.push(new docx_1.Paragraph({
|
|
127
|
+
children: [new docx_1.TextRun({ text: company.legal_address, size: 18 })],
|
|
128
|
+
alignment: docx_1.AlignmentType.RIGHT,
|
|
129
|
+
spacing: { after: 40 },
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
if (company.fiscal_data) {
|
|
133
|
+
try {
|
|
134
|
+
const fiscalData = JSON.parse(company.fiscal_data);
|
|
135
|
+
if (fiscalData.partita_iva) {
|
|
136
|
+
rightColumnParagraphs.push(new docx_1.Paragraph({
|
|
137
|
+
children: [
|
|
138
|
+
new docx_1.TextRun({ text: "P.IVA: ", bold: true, size: 18 }),
|
|
139
|
+
new docx_1.TextRun({ text: fiscalData.partita_iva, size: 18 }),
|
|
140
|
+
],
|
|
141
|
+
alignment: docx_1.AlignmentType.RIGHT,
|
|
142
|
+
spacing: { after: 40 },
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
if (fiscalData.codice_fiscale) {
|
|
146
|
+
rightColumnParagraphs.push(new docx_1.Paragraph({
|
|
147
|
+
children: [
|
|
148
|
+
new docx_1.TextRun({ text: "C.F.: ", bold: true, size: 18 }),
|
|
149
|
+
new docx_1.TextRun({ text: fiscalData.codice_fiscale, size: 18 }),
|
|
150
|
+
],
|
|
151
|
+
alignment: docx_1.AlignmentType.RIGHT,
|
|
152
|
+
spacing: { after: 40 },
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
rightColumnParagraphs.push(new docx_1.Paragraph({
|
|
158
|
+
children: [new docx_1.TextRun({ text: company.fiscal_data, size: 18 })],
|
|
159
|
+
alignment: docx_1.AlignmentType.RIGHT,
|
|
160
|
+
spacing: { after: 40 },
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const noBorder = { style: docx_1.BorderStyle.NONE, size: 0, color: "FFFFFF" };
|
|
165
|
+
const headerTable = new docx_1.Table({
|
|
166
|
+
width: { size: 100, type: docx_1.WidthType.PERCENTAGE },
|
|
167
|
+
borders: {
|
|
168
|
+
top: noBorder,
|
|
169
|
+
bottom: noBorder,
|
|
170
|
+
left: noBorder,
|
|
171
|
+
right: noBorder,
|
|
172
|
+
insideHorizontal: noBorder,
|
|
173
|
+
insideVertical: noBorder,
|
|
174
|
+
},
|
|
175
|
+
rows: [
|
|
176
|
+
new docx_1.TableRow({
|
|
177
|
+
children: [
|
|
178
|
+
new docx_1.TableCell({
|
|
179
|
+
children: leftColumnParagraphs,
|
|
180
|
+
width: { size: 40, type: docx_1.WidthType.PERCENTAGE },
|
|
181
|
+
verticalAlign: "top",
|
|
182
|
+
}),
|
|
183
|
+
new docx_1.TableCell({
|
|
184
|
+
children: rightColumnParagraphs,
|
|
185
|
+
width: { size: 60, type: docx_1.WidthType.PERCENTAGE },
|
|
186
|
+
verticalAlign: "top",
|
|
187
|
+
}),
|
|
188
|
+
],
|
|
189
|
+
}),
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
const separator = new docx_1.Paragraph({
|
|
193
|
+
border: {
|
|
194
|
+
bottom: { color: "000000", space: 1, style: docx_1.BorderStyle.SINGLE, size: 6 },
|
|
195
|
+
},
|
|
196
|
+
spacing: { after: 120 },
|
|
197
|
+
});
|
|
198
|
+
return new docx_1.Header({ children: [headerTable, separator] });
|
|
199
|
+
}
|
|
200
|
+
function createDocumentFooter(title) {
|
|
201
|
+
const separator = new docx_1.Paragraph({
|
|
202
|
+
border: {
|
|
203
|
+
top: { color: "000000", space: 1, style: docx_1.BorderStyle.SINGLE, size: 6 },
|
|
204
|
+
},
|
|
205
|
+
spacing: { before: 120 },
|
|
206
|
+
});
|
|
207
|
+
const noBorder = { style: docx_1.BorderStyle.NONE, size: 0, color: "FFFFFF" };
|
|
208
|
+
const footerTable = new docx_1.Table({
|
|
209
|
+
width: { size: 100, type: docx_1.WidthType.PERCENTAGE },
|
|
210
|
+
borders: {
|
|
211
|
+
top: noBorder,
|
|
212
|
+
bottom: noBorder,
|
|
213
|
+
left: noBorder,
|
|
214
|
+
right: noBorder,
|
|
215
|
+
insideHorizontal: noBorder,
|
|
216
|
+
insideVertical: noBorder,
|
|
217
|
+
},
|
|
218
|
+
rows: [
|
|
219
|
+
new docx_1.TableRow({
|
|
220
|
+
children: [
|
|
221
|
+
new docx_1.TableCell({
|
|
222
|
+
children: [
|
|
223
|
+
new docx_1.Paragraph({
|
|
224
|
+
children: [new docx_1.TextRun({ text: title, size: 18, italics: true })],
|
|
225
|
+
}),
|
|
226
|
+
],
|
|
227
|
+
width: { size: 50, type: docx_1.WidthType.PERCENTAGE },
|
|
228
|
+
verticalAlign: "center",
|
|
229
|
+
}),
|
|
230
|
+
new docx_1.TableCell({
|
|
231
|
+
children: [
|
|
232
|
+
new docx_1.Paragraph({
|
|
233
|
+
children: [
|
|
234
|
+
new docx_1.TextRun({ text: "Page ", size: 18 }),
|
|
235
|
+
new docx_1.TextRun({ children: [docx_1.PageNumber.CURRENT] }),
|
|
236
|
+
new docx_1.TextRun({ text: " of ", size: 18 }),
|
|
237
|
+
new docx_1.TextRun({ children: [docx_1.PageNumber.TOTAL_PAGES] }),
|
|
238
|
+
],
|
|
239
|
+
alignment: docx_1.AlignmentType.RIGHT,
|
|
240
|
+
}),
|
|
241
|
+
],
|
|
242
|
+
width: { size: 50, type: docx_1.WidthType.PERCENTAGE },
|
|
243
|
+
verticalAlign: "center",
|
|
244
|
+
}),
|
|
245
|
+
],
|
|
246
|
+
}),
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
return new docx_1.Footer({ children: [separator, footerTable] });
|
|
250
|
+
}
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Markdown parser (identical logic to export-markdown-to-docx.ts)
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
function parseMarkdown(markdown) {
|
|
255
|
+
const nodes = [];
|
|
256
|
+
const lines = markdown.split("\n");
|
|
257
|
+
let i = 0;
|
|
258
|
+
while (i < lines.length) {
|
|
259
|
+
const line = lines[i];
|
|
260
|
+
if (!line.trim()) {
|
|
261
|
+
i++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// Headings
|
|
265
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
266
|
+
if (headingMatch) {
|
|
267
|
+
nodes.push({ type: "heading", level: headingMatch[1].length, content: headingMatch[2].trim() });
|
|
268
|
+
i++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
// Blockquotes
|
|
272
|
+
if (line.startsWith("> ")) {
|
|
273
|
+
const quoteLines = [];
|
|
274
|
+
while (i < lines.length && lines[i].startsWith("> ")) {
|
|
275
|
+
quoteLines.push(lines[i].substring(2));
|
|
276
|
+
i++;
|
|
277
|
+
}
|
|
278
|
+
nodes.push({ type: "blockquote", content: quoteLines.join("\n") });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
// Code blocks
|
|
282
|
+
if (line.startsWith("```")) {
|
|
283
|
+
const codeLines = [];
|
|
284
|
+
i++;
|
|
285
|
+
while (i < lines.length && !lines[i].startsWith("```")) {
|
|
286
|
+
codeLines.push(lines[i]);
|
|
287
|
+
i++;
|
|
288
|
+
}
|
|
289
|
+
i++;
|
|
290
|
+
nodes.push({ type: "code", content: codeLines.join("\n") });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// Unordered lists
|
|
294
|
+
if (line.match(/^\s*[\-\*]\s+/)) {
|
|
295
|
+
const items = [];
|
|
296
|
+
while (i < lines.length && lines[i].match(/^\s*[\-\*]\s+/)) {
|
|
297
|
+
const leadingSpaces = lines[i].match(/^(\s*)/)?.[1].length ?? 0;
|
|
298
|
+
items.push({ text: lines[i].replace(/^\s*[\-\*]\s+/, "").trim(), level: Math.floor(leadingSpaces / 4) });
|
|
299
|
+
i++;
|
|
300
|
+
}
|
|
301
|
+
nodes.push({ type: "list", ordered: false, items });
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
// Ordered lists
|
|
305
|
+
if (line.match(/^\s*\d+\.\s+/)) {
|
|
306
|
+
const items = [];
|
|
307
|
+
while (i < lines.length && lines[i].match(/^\s*\d+\.\s+/)) {
|
|
308
|
+
const leadingSpaces = lines[i].match(/^(\s*)/)?.[1].length ?? 0;
|
|
309
|
+
items.push({ text: lines[i].replace(/^\s*\d+\.\s+/, "").trim(), level: Math.floor(leadingSpaces / 4) });
|
|
310
|
+
i++;
|
|
311
|
+
}
|
|
312
|
+
nodes.push({ type: "list", ordered: true, items });
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Tables
|
|
316
|
+
if (line.startsWith("|")) {
|
|
317
|
+
const rows = [];
|
|
318
|
+
while (i < lines.length && lines[i].startsWith("|")) {
|
|
319
|
+
if (!lines[i].match(/^\|[\s\-:]+\|/)) {
|
|
320
|
+
rows.push(lines[i]
|
|
321
|
+
.split("|")
|
|
322
|
+
.slice(1, -1)
|
|
323
|
+
.map((cell) => cell.trim()));
|
|
324
|
+
}
|
|
325
|
+
i++;
|
|
326
|
+
}
|
|
327
|
+
if (rows.length > 0)
|
|
328
|
+
nodes.push({ type: "table", rows });
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
// Paragraph
|
|
332
|
+
const paragraphLines = [line];
|
|
333
|
+
i++;
|
|
334
|
+
while (i < lines.length && lines[i].trim() && !lines[i].match(/^(#{1,6}|\||[\-\*]|\d+\.|\>|```)/)) {
|
|
335
|
+
paragraphLines.push(lines[i]);
|
|
336
|
+
i++;
|
|
337
|
+
}
|
|
338
|
+
nodes.push({ type: "paragraph", content: paragraphLines.join(" ") });
|
|
339
|
+
}
|
|
340
|
+
return nodes;
|
|
341
|
+
}
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// Inline formatting parser (identical logic to export-markdown-to-docx.ts)
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
function parseInlineFormatting(text) {
|
|
346
|
+
const elements = [];
|
|
347
|
+
let currentText = text;
|
|
348
|
+
while (currentText.length > 0) {
|
|
349
|
+
let foundMatch = null;
|
|
350
|
+
let earliestIndex = currentText.length;
|
|
351
|
+
// Bold + italic (***)
|
|
352
|
+
const boldItalicMatch = currentText.match(/\*\*\*([^*]+?)\*\*\*/);
|
|
353
|
+
if (boldItalicMatch?.index !== undefined && boldItalicMatch.index < earliestIndex) {
|
|
354
|
+
const before = boldItalicMatch.index > 0 ? currentText[boldItalicMatch.index - 1] : "";
|
|
355
|
+
const after = boldItalicMatch.index + boldItalicMatch[0].length < currentText.length
|
|
356
|
+
? currentText[boldItalicMatch.index + boldItalicMatch[0].length]
|
|
357
|
+
: "";
|
|
358
|
+
if (before !== "*" && after !== "*") {
|
|
359
|
+
earliestIndex = boldItalicMatch.index;
|
|
360
|
+
foundMatch = {
|
|
361
|
+
index: boldItalicMatch.index,
|
|
362
|
+
length: boldItalicMatch[0].length,
|
|
363
|
+
text: boldItalicMatch[1],
|
|
364
|
+
bold: true,
|
|
365
|
+
italics: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Bold (**)
|
|
370
|
+
if (!foundMatch || foundMatch.index > 0) {
|
|
371
|
+
const boldMatch = currentText.match(/\*\*([^*]+?)\*\*/);
|
|
372
|
+
if (boldMatch?.index !== undefined && boldMatch.index < earliestIndex) {
|
|
373
|
+
const before = boldMatch.index > 0 ? currentText[boldMatch.index - 1] : "";
|
|
374
|
+
const after = boldMatch.index + boldMatch[0].length < currentText.length
|
|
375
|
+
? currentText[boldMatch.index + boldMatch[0].length]
|
|
376
|
+
: "";
|
|
377
|
+
if (before !== "*" && after !== "*") {
|
|
378
|
+
earliestIndex = boldMatch.index;
|
|
379
|
+
foundMatch = {
|
|
380
|
+
index: boldMatch.index,
|
|
381
|
+
length: boldMatch[0].length,
|
|
382
|
+
text: boldMatch[1],
|
|
383
|
+
bold: true,
|
|
384
|
+
italics: false,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Italic (*)
|
|
390
|
+
if (!foundMatch || foundMatch.index > 0) {
|
|
391
|
+
const italicMatch = currentText.match(/\*([^*]+?)\*/);
|
|
392
|
+
if (italicMatch?.index !== undefined && italicMatch.index < earliestIndex) {
|
|
393
|
+
const before = italicMatch.index > 0 ? currentText[italicMatch.index - 1] : "";
|
|
394
|
+
const after = italicMatch.index + italicMatch[0].length < currentText.length
|
|
395
|
+
? currentText[italicMatch.index + italicMatch[0].length]
|
|
396
|
+
: "";
|
|
397
|
+
if (before !== "*" && after !== "*") {
|
|
398
|
+
earliestIndex = italicMatch.index;
|
|
399
|
+
foundMatch = {
|
|
400
|
+
index: italicMatch.index,
|
|
401
|
+
length: italicMatch[0].length,
|
|
402
|
+
text: italicMatch[1],
|
|
403
|
+
bold: false,
|
|
404
|
+
italics: true,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Inline code (`)
|
|
410
|
+
const codeMatch = currentText.match(/`(.+?)`/);
|
|
411
|
+
if (codeMatch?.index !== undefined && codeMatch.index < earliestIndex) {
|
|
412
|
+
earliestIndex = codeMatch.index;
|
|
413
|
+
foundMatch = { index: codeMatch.index, length: codeMatch[0].length, text: codeMatch[1], code: true };
|
|
414
|
+
}
|
|
415
|
+
// Links ([text](url))
|
|
416
|
+
const linkMatch = currentText.match(/\[(.+?)\]\((.+?)\)/);
|
|
417
|
+
if (linkMatch?.index !== undefined && linkMatch.index < earliestIndex) {
|
|
418
|
+
earliestIndex = linkMatch.index;
|
|
419
|
+
foundMatch = {
|
|
420
|
+
index: linkMatch.index,
|
|
421
|
+
length: linkMatch[0].length,
|
|
422
|
+
text: linkMatch[1],
|
|
423
|
+
url: linkMatch[2],
|
|
424
|
+
link: true,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// Underscore emphasis — CommonMark requires the underscore NOT to be
|
|
428
|
+
// flanked by an alphanumeric character on the inner side (i.e. intra-word
|
|
429
|
+
// underscores such as `primary_contact_name` are NEVER emphasis). The
|
|
430
|
+
// asterisk variants do not have this restriction.
|
|
431
|
+
const isWordChar = (c) => /\w/.test(c);
|
|
432
|
+
const isUnderscoreEmphasis = (m) => {
|
|
433
|
+
const startIdx = m.index;
|
|
434
|
+
const endIdx = startIdx + m[0].length;
|
|
435
|
+
const before = startIdx > 0 ? currentText[startIdx - 1] : "";
|
|
436
|
+
const after = endIdx < currentText.length ? currentText[endIdx] : "";
|
|
437
|
+
return !isWordChar(before) && !isWordChar(after);
|
|
438
|
+
};
|
|
439
|
+
// Underscore bold+italic (___)
|
|
440
|
+
const underscoreBoldItalicMatch = currentText.match(/___(.+?)___/);
|
|
441
|
+
if (underscoreBoldItalicMatch?.index !== undefined &&
|
|
442
|
+
underscoreBoldItalicMatch.index < earliestIndex &&
|
|
443
|
+
isUnderscoreEmphasis(underscoreBoldItalicMatch)) {
|
|
444
|
+
earliestIndex = underscoreBoldItalicMatch.index;
|
|
445
|
+
foundMatch = {
|
|
446
|
+
index: underscoreBoldItalicMatch.index,
|
|
447
|
+
length: underscoreBoldItalicMatch[0].length,
|
|
448
|
+
text: underscoreBoldItalicMatch[1],
|
|
449
|
+
bold: true,
|
|
450
|
+
italics: true,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
// Underscore bold (__)
|
|
454
|
+
const underscoreBoldMatch = currentText.match(/__(.+?)__/);
|
|
455
|
+
if (underscoreBoldMatch?.index !== undefined &&
|
|
456
|
+
underscoreBoldMatch.index < earliestIndex &&
|
|
457
|
+
isUnderscoreEmphasis(underscoreBoldMatch)) {
|
|
458
|
+
earliestIndex = underscoreBoldMatch.index;
|
|
459
|
+
foundMatch = {
|
|
460
|
+
index: underscoreBoldMatch.index,
|
|
461
|
+
length: underscoreBoldMatch[0].length,
|
|
462
|
+
text: underscoreBoldMatch[1],
|
|
463
|
+
bold: true,
|
|
464
|
+
italics: false,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
// Underscore italic (_)
|
|
468
|
+
const underscoreItalicMatch = currentText.match(/_(.+?)_/);
|
|
469
|
+
if (underscoreItalicMatch?.index !== undefined &&
|
|
470
|
+
underscoreItalicMatch.index < earliestIndex &&
|
|
471
|
+
isUnderscoreEmphasis(underscoreItalicMatch)) {
|
|
472
|
+
earliestIndex = underscoreItalicMatch.index;
|
|
473
|
+
foundMatch = {
|
|
474
|
+
index: underscoreItalicMatch.index,
|
|
475
|
+
length: underscoreItalicMatch[0].length,
|
|
476
|
+
text: underscoreItalicMatch[1],
|
|
477
|
+
bold: false,
|
|
478
|
+
italics: true,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (foundMatch) {
|
|
482
|
+
if (foundMatch.length === 0) {
|
|
483
|
+
elements.push(new docx_1.TextRun({ text: currentText }));
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
if (foundMatch.index > 0) {
|
|
487
|
+
elements.push(new docx_1.TextRun({ text: currentText.substring(0, foundMatch.index) }));
|
|
488
|
+
}
|
|
489
|
+
if (foundMatch.link && foundMatch.url) {
|
|
490
|
+
elements.push(new docx_1.ExternalHyperlink({
|
|
491
|
+
children: [new docx_1.TextRun({ text: foundMatch.text, style: "Hyperlink" })],
|
|
492
|
+
link: foundMatch.url,
|
|
493
|
+
}));
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
elements.push(new docx_1.TextRun({
|
|
497
|
+
text: foundMatch.text,
|
|
498
|
+
bold: foundMatch.bold,
|
|
499
|
+
italics: foundMatch.italics,
|
|
500
|
+
font: foundMatch.code ? "Courier New" : undefined,
|
|
501
|
+
size: foundMatch.code ? 20 : undefined,
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
currentText = currentText.substring(foundMatch.index + foundMatch.length);
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
if (currentText.length > 0)
|
|
508
|
+
elements.push(new docx_1.TextRun({ text: currentText }));
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (elements.length === 0)
|
|
513
|
+
elements.push(new docx_1.TextRun({ text }));
|
|
514
|
+
return elements;
|
|
515
|
+
}
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// Markdown node → DOCX elements converter
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
function nodeToDocxElements(node) {
|
|
520
|
+
switch (node.type) {
|
|
521
|
+
case "heading": {
|
|
522
|
+
const headingLevels = {
|
|
523
|
+
1: docx_1.HeadingLevel.HEADING_1,
|
|
524
|
+
2: docx_1.HeadingLevel.HEADING_2,
|
|
525
|
+
3: docx_1.HeadingLevel.HEADING_3,
|
|
526
|
+
4: docx_1.HeadingLevel.HEADING_4,
|
|
527
|
+
5: docx_1.HeadingLevel.HEADING_5,
|
|
528
|
+
6: docx_1.HeadingLevel.HEADING_6,
|
|
529
|
+
};
|
|
530
|
+
return [
|
|
531
|
+
new docx_1.Paragraph({
|
|
532
|
+
children: parseInlineFormatting(node.content ?? ""),
|
|
533
|
+
heading: headingLevels[node.level ?? 1],
|
|
534
|
+
spacing: { before: 240, after: 120 },
|
|
535
|
+
keepNext: true,
|
|
536
|
+
}),
|
|
537
|
+
];
|
|
538
|
+
}
|
|
539
|
+
case "paragraph": {
|
|
540
|
+
if (!node.content)
|
|
541
|
+
return [];
|
|
542
|
+
return [
|
|
543
|
+
new docx_1.Paragraph({
|
|
544
|
+
children: parseInlineFormatting(node.content),
|
|
545
|
+
spacing: { after: 120 },
|
|
546
|
+
}),
|
|
547
|
+
];
|
|
548
|
+
}
|
|
549
|
+
case "list": {
|
|
550
|
+
if (!node.items)
|
|
551
|
+
return [];
|
|
552
|
+
return node.items.map((item) => new docx_1.Paragraph({
|
|
553
|
+
children: parseInlineFormatting(item.text),
|
|
554
|
+
bullet: !node.ordered ? { level: item.level } : undefined,
|
|
555
|
+
numbering: node.ordered ? { reference: "default-numbering", level: item.level } : undefined,
|
|
556
|
+
spacing: { after: 80 },
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
case "blockquote": {
|
|
560
|
+
if (!node.content)
|
|
561
|
+
return [];
|
|
562
|
+
return [
|
|
563
|
+
new docx_1.Paragraph({
|
|
564
|
+
children: parseInlineFormatting(node.content),
|
|
565
|
+
border: { left: { color: "999999", space: 1, style: docx_1.BorderStyle.SINGLE, size: 8 } },
|
|
566
|
+
indent: { left: (0, docx_1.convertInchesToTwip)(0.5) },
|
|
567
|
+
spacing: { after: 120 },
|
|
568
|
+
shading: { fill: "F5F5F5" },
|
|
569
|
+
}),
|
|
570
|
+
];
|
|
571
|
+
}
|
|
572
|
+
case "code": {
|
|
573
|
+
if (!node.content)
|
|
574
|
+
return [];
|
|
575
|
+
return [
|
|
576
|
+
new docx_1.Paragraph({
|
|
577
|
+
children: [new docx_1.TextRun({ text: node.content, font: "Courier New", size: 20 })],
|
|
578
|
+
shading: { fill: "F5F5F5" },
|
|
579
|
+
spacing: { after: 120 },
|
|
580
|
+
}),
|
|
581
|
+
];
|
|
582
|
+
}
|
|
583
|
+
case "table": {
|
|
584
|
+
if (!node.rows || node.rows.length === 0)
|
|
585
|
+
return [];
|
|
586
|
+
const tableRows = node.rows.map((row, rowIndex) => new docx_1.TableRow({
|
|
587
|
+
children: row.map((cell) => new docx_1.TableCell({
|
|
588
|
+
children: [new docx_1.Paragraph({ children: parseInlineFormatting(cell) })],
|
|
589
|
+
shading: rowIndex === 0 ? { fill: "E7E6E6" } : undefined,
|
|
590
|
+
})),
|
|
591
|
+
}));
|
|
592
|
+
return [new docx_1.Table({ rows: tableRows, width: { size: 100, type: docx_1.WidthType.PERCENTAGE } })];
|
|
593
|
+
}
|
|
594
|
+
default:
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
// BlockNote → DOCX pipeline helpers
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
function extractInlineText(content) {
|
|
602
|
+
if (!content || !Array.isArray(content))
|
|
603
|
+
return "";
|
|
604
|
+
return content
|
|
605
|
+
.map((item) => {
|
|
606
|
+
if (typeof item === "string")
|
|
607
|
+
return item;
|
|
608
|
+
if (item.type === "text") {
|
|
609
|
+
let t = item.text ?? "";
|
|
610
|
+
if (item.styles?.bold)
|
|
611
|
+
t = `**${t}**`;
|
|
612
|
+
if (item.styles?.italic)
|
|
613
|
+
t = `*${t}*`;
|
|
614
|
+
if (item.styles?.code)
|
|
615
|
+
t = `\`${t}\``;
|
|
616
|
+
return t;
|
|
617
|
+
}
|
|
618
|
+
if (item.type === "link") {
|
|
619
|
+
return `[${extractInlineText(item.content ?? [])}](${item.href ?? ""})`;
|
|
620
|
+
}
|
|
621
|
+
if (item.type === "templateField") {
|
|
622
|
+
return `[[${item.props?.alias ?? item.props?.fieldId ?? "unknown"}]]`;
|
|
623
|
+
}
|
|
624
|
+
return item.text ?? "";
|
|
625
|
+
})
|
|
626
|
+
.join("");
|
|
627
|
+
}
|
|
628
|
+
function blockNodeToMarkdown(block, depth = 0) {
|
|
629
|
+
if (!block)
|
|
630
|
+
return "";
|
|
631
|
+
const indent = " ".repeat(depth);
|
|
632
|
+
const type = block.type ?? "";
|
|
633
|
+
const text = extractInlineText(block.content ?? []);
|
|
634
|
+
const children = block.children ?? [];
|
|
635
|
+
let md = "";
|
|
636
|
+
switch (type) {
|
|
637
|
+
case "heading": {
|
|
638
|
+
const level = block.props?.level ?? 1;
|
|
639
|
+
md = `${"#".repeat(level)} ${text}\n\n`;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case "heading1":
|
|
643
|
+
md = `# ${text}\n\n`;
|
|
644
|
+
break;
|
|
645
|
+
case "heading2":
|
|
646
|
+
md = `## ${text}\n\n`;
|
|
647
|
+
break;
|
|
648
|
+
case "heading3":
|
|
649
|
+
md = `### ${text}\n\n`;
|
|
650
|
+
break;
|
|
651
|
+
case "paragraph":
|
|
652
|
+
md = text ? `${indent}${text}\n\n` : "\n";
|
|
653
|
+
break;
|
|
654
|
+
case "bulletListItem":
|
|
655
|
+
case "listItem":
|
|
656
|
+
md = `${indent}- ${text}\n`;
|
|
657
|
+
break;
|
|
658
|
+
case "numberedListItem":
|
|
659
|
+
md = `${indent}1. ${text}\n`;
|
|
660
|
+
break;
|
|
661
|
+
case "checkListItem": {
|
|
662
|
+
const checked = block.props?.checked ? "x" : " ";
|
|
663
|
+
md = `${indent}- [${checked}] ${text}\n`;
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case "quote":
|
|
667
|
+
case "blockquote":
|
|
668
|
+
md = `> ${text}\n\n`;
|
|
669
|
+
break;
|
|
670
|
+
case "code":
|
|
671
|
+
md = `\`\`\`\n${text}\n\`\`\`\n\n`;
|
|
672
|
+
break;
|
|
673
|
+
case "table": {
|
|
674
|
+
if (block.content?.rows) {
|
|
675
|
+
const rows = block.content.rows;
|
|
676
|
+
for (let i = 0; i < rows.length; i++) {
|
|
677
|
+
const cells = rows[i].cells ?? [];
|
|
678
|
+
const rowText = cells.map((cell) => extractInlineText(cell)).join(" | ");
|
|
679
|
+
md += `| ${rowText} |\n`;
|
|
680
|
+
if (i === 0)
|
|
681
|
+
md += `| ${cells.map(() => "---").join(" | ")} |\n`;
|
|
682
|
+
}
|
|
683
|
+
md += "\n";
|
|
684
|
+
}
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
default:
|
|
688
|
+
if (text)
|
|
689
|
+
md = `${indent}${text}\n\n`;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
for (const child of children) {
|
|
693
|
+
md += blockNodeToMarkdown(child, depth + 1);
|
|
694
|
+
}
|
|
695
|
+
return md;
|
|
696
|
+
}
|
|
697
|
+
function blocksToMarkdown(blocks) {
|
|
698
|
+
if (!blocks || !Array.isArray(blocks))
|
|
699
|
+
return "";
|
|
700
|
+
return blocks.map((block) => blockNodeToMarkdown(block)).join("");
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Diagnostic helper: collect every templateField's fieldId/alias pair found
|
|
704
|
+
* in a tree of BlockNote blocks. Used only for one-shot debugging of the
|
|
705
|
+
* "placeholder didn't get replaced" class of bug.
|
|
706
|
+
*/
|
|
707
|
+
function collectTemplateFieldIds(blocks) {
|
|
708
|
+
const out = [];
|
|
709
|
+
const walk = (nodes) => {
|
|
710
|
+
for (const block of nodes ?? []) {
|
|
711
|
+
if (Array.isArray(block?.content)) {
|
|
712
|
+
for (const item of block.content) {
|
|
713
|
+
if (item?.type === "templateField") {
|
|
714
|
+
out.push({ fieldId: String(item.props?.fieldId ?? ""), alias: String(item.props?.alias ?? "") });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (Array.isArray(block?.children))
|
|
719
|
+
walk(block.children);
|
|
720
|
+
if (Array.isArray(block?.content?.rows)) {
|
|
721
|
+
for (const row of block.content.rows) {
|
|
722
|
+
for (const cell of row.cells ?? []) {
|
|
723
|
+
if (Array.isArray(cell)) {
|
|
724
|
+
for (const item of cell) {
|
|
725
|
+
if (item?.type === "templateField") {
|
|
726
|
+
out.push({ fieldId: String(item.props?.fieldId ?? ""), alias: String(item.props?.alias ?? "") });
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
walk(blocks);
|
|
736
|
+
return out;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Recursively walk BlockNote JSON and replace `templateField` inline nodes
|
|
740
|
+
* with plain text nodes containing the field value from fieldContext.
|
|
741
|
+
*/
|
|
742
|
+
function replaceFieldNodes(blocks, fieldContext) {
|
|
743
|
+
for (const block of blocks) {
|
|
744
|
+
if (block.content && Array.isArray(block.content)) {
|
|
745
|
+
for (let i = 0; i < block.content.length; i++) {
|
|
746
|
+
const item = block.content[i];
|
|
747
|
+
if (item.type === "templateField") {
|
|
748
|
+
const fieldId = item.props?.fieldId ?? item.props?.alias ?? "";
|
|
749
|
+
const value = fieldContext[fieldId] != null ? String(fieldContext[fieldId]) : `[[${fieldId}]]`;
|
|
750
|
+
block.content[i] = { type: "text", text: value, styles: {} };
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (block.children && Array.isArray(block.children)) {
|
|
755
|
+
replaceFieldNodes(block.children, fieldContext);
|
|
756
|
+
}
|
|
757
|
+
// Table content (rows > cells > inline content)
|
|
758
|
+
if (block.content?.rows && Array.isArray(block.content.rows)) {
|
|
759
|
+
for (const row of block.content.rows) {
|
|
760
|
+
for (const cell of row.cells ?? []) {
|
|
761
|
+
if (Array.isArray(cell)) {
|
|
762
|
+
for (let i = 0; i < cell.length; i++) {
|
|
763
|
+
if (cell[i].type === "templateField") {
|
|
764
|
+
const fieldId = cell[i].props?.fieldId ?? cell[i].props?.alias ?? "";
|
|
765
|
+
const value = fieldContext[fieldId] != null ? String(fieldContext[fieldId]) : `[[${fieldId}]]`;
|
|
766
|
+
cell[i] = { type: "text", text: value, styles: {} };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
// Service
|
|
777
|
+
// ---------------------------------------------------------------------------
|
|
778
|
+
/**
|
|
779
|
+
* Converts a BlockNote template (JSON-serialized blocks buffer) into a DOCX
|
|
780
|
+
* buffer by:
|
|
781
|
+
* 1. Parsing the BlockNote JSON.
|
|
782
|
+
* 2. Replacing `templateField` inline nodes with values from fieldContext.
|
|
783
|
+
* 3. Converting the resulting blocks to markdown.
|
|
784
|
+
* 4. Rendering markdown into a `docx` Document with company header/footer.
|
|
785
|
+
* 5. Returning the document as a Node.js Buffer.
|
|
786
|
+
*
|
|
787
|
+
* This service is the Node.js equivalent of the browser-side
|
|
788
|
+
* `generateDocxBlob` function in `apps/web/src/lib/export-markdown-to-docx.ts`.
|
|
789
|
+
*/
|
|
790
|
+
let BlockNoteToDocxService = class BlockNoteToDocxService {
|
|
791
|
+
/**
|
|
792
|
+
* Render a BlockNote template buffer against a field context and return a
|
|
793
|
+
* DOCX buffer.
|
|
794
|
+
*
|
|
795
|
+
* @param templateBuffer UTF-8 JSON-encoded BlockNote blocks array, OR
|
|
796
|
+
* a pre-rendered markdown string (UTF-8 encoded).
|
|
797
|
+
* When the buffer's content starts with `[` the
|
|
798
|
+
* service treats it as BlockNote JSON; otherwise it
|
|
799
|
+
* is treated as raw markdown.
|
|
800
|
+
* @param fieldContext Must include `title` (string), `company`
|
|
801
|
+
* (BlockNoteCompanyInfo), and any template field
|
|
802
|
+
* values whose keys match the `templateField`
|
|
803
|
+
* `fieldId` / `alias` props in the template blocks.
|
|
804
|
+
* Additional keys are ignored.
|
|
805
|
+
*/
|
|
806
|
+
async render(templateBuffer, fieldContext) {
|
|
807
|
+
const title = typeof fieldContext.title === "string" ? fieldContext.title : "Document";
|
|
808
|
+
const company = fieldContext.company ?? { name: "" };
|
|
809
|
+
const sectionsFromContext = fieldContext.sections;
|
|
810
|
+
// Determine markdown content
|
|
811
|
+
let markdownContent;
|
|
812
|
+
const templateStr = templateBuffer.toString("utf8").trimStart();
|
|
813
|
+
if (templateStr.startsWith("[")) {
|
|
814
|
+
// BlockNote JSON blocks
|
|
815
|
+
const blocks = JSON.parse(templateStr);
|
|
816
|
+
// Deep-clone to avoid mutating the parsed structure
|
|
817
|
+
const clonedBlocks = JSON.parse(JSON.stringify(blocks));
|
|
818
|
+
replaceFieldNodes(clonedBlocks, fieldContext);
|
|
819
|
+
markdownContent = blocksToMarkdown(clonedBlocks);
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
// Raw markdown — no field replacement for now (used when the template
|
|
823
|
+
// is already converted to markdown upstream)
|
|
824
|
+
markdownContent = templateStr;
|
|
825
|
+
}
|
|
826
|
+
// Build sections: if sections are provided via fieldContext use them,
|
|
827
|
+
// otherwise wrap the full markdown content in a single section.
|
|
828
|
+
const sections = sectionsFromContext ?? [{ title: "", content: markdownContent }];
|
|
829
|
+
return this._buildDocx({ title, sections, company });
|
|
830
|
+
}
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// Private: build the docx Document and serialize to Buffer
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
async _buildDocx(opts) {
|
|
835
|
+
const { title, sections, company } = opts;
|
|
836
|
+
if (!title)
|
|
837
|
+
throw new Error("BlockNoteToDocxService: title is required");
|
|
838
|
+
if (!sections || sections.length === 0)
|
|
839
|
+
throw new Error("BlockNoteToDocxService: at least one section is required");
|
|
840
|
+
const header = await createDocumentHeader(company);
|
|
841
|
+
const footer = createDocumentFooter(title);
|
|
842
|
+
const documentChildren = [];
|
|
843
|
+
documentChildren.push(new docx_1.Paragraph({
|
|
844
|
+
text: title,
|
|
845
|
+
heading: docx_1.HeadingLevel.HEADING_1,
|
|
846
|
+
spacing: { after: 240 },
|
|
847
|
+
keepNext: true,
|
|
848
|
+
}));
|
|
849
|
+
for (const section of sections) {
|
|
850
|
+
if (section.title) {
|
|
851
|
+
documentChildren.push(new docx_1.Paragraph({
|
|
852
|
+
text: section.title,
|
|
853
|
+
heading: docx_1.HeadingLevel.HEADING_2,
|
|
854
|
+
spacing: { before: 240, after: 120 },
|
|
855
|
+
keepNext: true,
|
|
856
|
+
}));
|
|
857
|
+
}
|
|
858
|
+
const contentNodes = parseMarkdown(section.content);
|
|
859
|
+
for (const node of contentNodes) {
|
|
860
|
+
documentChildren.push(...nodeToDocxElements(node));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const doc = new docx_1.Document({
|
|
864
|
+
sections: [
|
|
865
|
+
{
|
|
866
|
+
properties: {
|
|
867
|
+
page: {
|
|
868
|
+
margin: {
|
|
869
|
+
top: (0, docx_1.convertInchesToTwip)(1.5),
|
|
870
|
+
bottom: (0, docx_1.convertInchesToTwip)(1.5),
|
|
871
|
+
left: (0, docx_1.convertInchesToTwip)(1),
|
|
872
|
+
right: (0, docx_1.convertInchesToTwip)(1),
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
},
|
|
876
|
+
headers: { default: header },
|
|
877
|
+
footers: { default: footer },
|
|
878
|
+
children: documentChildren,
|
|
879
|
+
},
|
|
880
|
+
],
|
|
881
|
+
numbering: {
|
|
882
|
+
config: [
|
|
883
|
+
{
|
|
884
|
+
reference: "default-numbering",
|
|
885
|
+
levels: [
|
|
886
|
+
{ level: 0, format: "decimal", text: "%1.", alignment: docx_1.AlignmentType.LEFT },
|
|
887
|
+
{ level: 1, format: "decimal", text: "%2.", alignment: docx_1.AlignmentType.LEFT },
|
|
888
|
+
{ level: 2, format: "decimal", text: "%3.", alignment: docx_1.AlignmentType.LEFT },
|
|
889
|
+
{ level: 3, format: "decimal", text: "%4.", alignment: docx_1.AlignmentType.LEFT },
|
|
890
|
+
],
|
|
891
|
+
},
|
|
892
|
+
],
|
|
893
|
+
},
|
|
894
|
+
});
|
|
895
|
+
const result = await docx_1.Packer.toBuffer(doc);
|
|
896
|
+
return Buffer.from(result);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
exports.BlockNoteToDocxService = BlockNoteToDocxService;
|
|
900
|
+
exports.BlockNoteToDocxService = BlockNoteToDocxService = __decorate([
|
|
901
|
+
(0, common_1.Injectable)()
|
|
902
|
+
], BlockNoteToDocxService);
|
|
903
|
+
//# sourceMappingURL=blocknote-to-docx.service.js.map
|