@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.
Files changed (54) hide show
  1. package/dist/core/blocknote/blocknote.module.d.ts +1 -0
  2. package/dist/core/blocknote/blocknote.module.d.ts.map +1 -1
  3. package/dist/core/blocknote/blocknote.module.js +4 -2
  4. package/dist/core/blocknote/blocknote.module.js.map +1 -1
  5. package/dist/core/blocknote/index.d.ts +1 -0
  6. package/dist/core/blocknote/index.d.ts.map +1 -1
  7. package/dist/core/blocknote/index.js +1 -0
  8. package/dist/core/blocknote/index.js.map +1 -1
  9. package/dist/core/blocknote/services/blocknote-to-docx.service.d.ts +55 -0
  10. package/dist/core/blocknote/services/blocknote-to-docx.service.d.ts.map +1 -0
  11. package/dist/core/blocknote/services/blocknote-to-docx.service.js +903 -0
  12. package/dist/core/blocknote/services/blocknote-to-docx.service.js.map +1 -0
  13. package/dist/core/document/document.module.d.ts +25 -0
  14. package/dist/core/document/document.module.d.ts.map +1 -0
  15. package/dist/core/document/document.module.js +46 -0
  16. package/dist/core/document/document.module.js.map +1 -0
  17. package/dist/core/document/index.d.ts +6 -0
  18. package/dist/core/document/index.d.ts.map +1 -0
  19. package/dist/core/document/index.js +22 -0
  20. package/dist/core/document/index.js.map +1 -0
  21. package/dist/core/document/services/abstract-document-generator.service.d.ts +90 -0
  22. package/dist/core/document/services/abstract-document-generator.service.d.ts.map +1 -0
  23. package/dist/core/document/services/abstract-document-generator.service.js +120 -0
  24. package/dist/core/document/services/abstract-document-generator.service.js.map +1 -0
  25. package/dist/core/document/services/docx-template.service.d.ts +36 -0
  26. package/dist/core/document/services/docx-template.service.d.ts.map +1 -0
  27. package/dist/core/document/services/docx-template.service.js +58 -0
  28. package/dist/core/document/services/docx-template.service.js.map +1 -0
  29. package/dist/core/document/utils/inject-draft-watermark.d.ts +24 -0
  30. package/dist/core/document/utils/inject-draft-watermark.d.ts.map +1 -0
  31. package/dist/core/document/utils/inject-draft-watermark.js +182 -0
  32. package/dist/core/document/utils/inject-draft-watermark.js.map +1 -0
  33. package/dist/core/document/utils/inject-xml.d.ts +15 -0
  34. package/dist/core/document/utils/inject-xml.d.ts.map +1 -0
  35. package/dist/core/document/utils/inject-xml.js +35 -0
  36. package/dist/core/document/utils/inject-xml.js.map +1 -0
  37. package/dist/core/pdf/index.d.ts +3 -0
  38. package/dist/core/pdf/index.d.ts.map +1 -0
  39. package/dist/core/pdf/index.js +19 -0
  40. package/dist/core/pdf/index.js.map +1 -0
  41. package/dist/core/pdf/pdf.module.d.ts +21 -0
  42. package/dist/core/pdf/pdf.module.d.ts.map +1 -0
  43. package/dist/core/pdf/pdf.module.js +39 -0
  44. package/dist/core/pdf/pdf.module.js.map +1 -0
  45. package/dist/core/pdf/services/docx-to-pdf.service.d.ts +28 -0
  46. package/dist/core/pdf/services/docx-to-pdf.service.d.ts.map +1 -0
  47. package/dist/core/pdf/services/docx-to-pdf.service.js +86 -0
  48. package/dist/core/pdf/services/docx-to-pdf.service.js.map +1 -0
  49. package/dist/foundations/stripe/__tests__/mocks/stripe.mock.d.ts +62 -62
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -0
  53. package/dist/index.js.map +1 -1
  54. 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