@dev.smartpricing/message-composer-utils 3.1.2

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 (119) hide show
  1. package/dist/types/api/aiIntegration.js +7 -0
  2. package/dist/types/api/aiIntegration.js.map +1 -0
  3. package/dist/types/api/apiMessages.js +2 -0
  4. package/dist/types/api/apiMessages.js.map +1 -0
  5. package/dist/types/api/index.js +3 -0
  6. package/dist/types/api/index.js.map +1 -0
  7. package/dist/types/app.js +23 -0
  8. package/dist/types/app.js.map +1 -0
  9. package/dist/types/brand.js +31 -0
  10. package/dist/types/brand.js.map +1 -0
  11. package/dist/types/dynamic-values.js +5 -0
  12. package/dist/types/dynamic-values.js.map +1 -0
  13. package/dist/types/emailblocks.v1.defaults.js +83 -0
  14. package/dist/types/emailblocks.v1.defaults.js.map +1 -0
  15. package/dist/types/emailblocks.v1.js +339 -0
  16. package/dist/types/emailblocks.v1.js.map +1 -0
  17. package/dist/types/index.js +16 -0
  18. package/dist/types/index.js.map +1 -0
  19. package/dist/types/kafka-events.js +2 -0
  20. package/dist/types/kafka-events.js.map +1 -0
  21. package/dist/types/language.js +2 -0
  22. package/dist/types/language.js.map +1 -0
  23. package/dist/types/media.js +6 -0
  24. package/dist/types/media.js.map +1 -0
  25. package/dist/types/message.js +41 -0
  26. package/dist/types/message.js.map +1 -0
  27. package/dist/types/meta-template.js +2 -0
  28. package/dist/types/meta-template.js.map +1 -0
  29. package/dist/types/meta-webhook.js +2 -0
  30. package/dist/types/meta-webhook.js.map +1 -0
  31. package/dist/types/submission-job.js +2 -0
  32. package/dist/types/submission-job.js.map +1 -0
  33. package/dist/types/tag.js +2 -0
  34. package/dist/types/tag.js.map +1 -0
  35. package/dist/types/utils.js +2 -0
  36. package/dist/types/utils.js.map +1 -0
  37. package/dist/types/whatsappblocks.v1.js +24 -0
  38. package/dist/types/whatsappblocks.v1.js.map +1 -0
  39. package/dist/utils/array.js +26 -0
  40. package/dist/utils/array.js.map +1 -0
  41. package/dist/utils/async.js +22 -0
  42. package/dist/utils/async.js.map +1 -0
  43. package/dist/utils/date.js +37 -0
  44. package/dist/utils/date.js.map +1 -0
  45. package/dist/utils/index.js +34 -0
  46. package/dist/utils/index.js.map +1 -0
  47. package/dist/utils/intl.js +37 -0
  48. package/dist/utils/intl.js.map +1 -0
  49. package/dist/utils/meta-status.js +21 -0
  50. package/dist/utils/meta-status.js.map +1 -0
  51. package/dist/utils/number.js +24 -0
  52. package/dist/utils/number.js.map +1 -0
  53. package/dist/utils/object.js +87 -0
  54. package/dist/utils/object.js.map +1 -0
  55. package/dist/utils/string.js +22 -0
  56. package/dist/utils/string.js.map +1 -0
  57. package/dist/utils/templates/brand-cascade.js +116 -0
  58. package/dist/utils/templates/brand-cascade.js.map +1 -0
  59. package/dist/utils/templates/email-compiler.js +126 -0
  60. package/dist/utils/templates/email-compiler.js.map +1 -0
  61. package/dist/utils/templates/email-mjml.js +444 -0
  62. package/dist/utils/templates/email-mjml.js.map +1 -0
  63. package/dist/utils/templates/email.js +580 -0
  64. package/dist/utils/templates/email.js.map +1 -0
  65. package/dist/utils/templates/html-sanitizer.js +95 -0
  66. package/dist/utils/templates/html-sanitizer.js.map +1 -0
  67. package/dist/utils/templates/messages.js +33 -0
  68. package/dist/utils/templates/messages.js.map +1 -0
  69. package/dist/utils/templates/meta.js +246 -0
  70. package/dist/utils/templates/meta.js.map +1 -0
  71. package/dist/utils/templates/tiptap.js +167 -0
  72. package/dist/utils/templates/tiptap.js.map +1 -0
  73. package/dist/utils/templates/whatsapp-compiler.js +168 -0
  74. package/dist/utils/templates/whatsapp-compiler.js.map +1 -0
  75. package/dist/utils/templates/whatsapp.js +329 -0
  76. package/dist/utils/templates/whatsapp.js.map +1 -0
  77. package/dist/utils/validations.js +39 -0
  78. package/dist/utils/validations.js.map +1 -0
  79. package/package.json +43 -0
  80. package/src/types/api/aiIntegration.ts +59 -0
  81. package/src/types/api/apiMessages.ts +22 -0
  82. package/src/types/api/index.ts +2 -0
  83. package/src/types/app.ts +76 -0
  84. package/src/types/brand.ts +80 -0
  85. package/src/types/dynamic-values.ts +47 -0
  86. package/src/types/emailblocks.v1.defaults.ts +123 -0
  87. package/src/types/emailblocks.v1.ts +655 -0
  88. package/src/types/index.ts +15 -0
  89. package/src/types/kafka-events.ts +46 -0
  90. package/src/types/language.ts +9 -0
  91. package/src/types/media.ts +26 -0
  92. package/src/types/message.ts +635 -0
  93. package/src/types/meta-template.ts +50 -0
  94. package/src/types/meta-webhook.ts +32 -0
  95. package/src/types/submission-job.ts +30 -0
  96. package/src/types/tag.ts +8 -0
  97. package/src/types/utils.ts +14 -0
  98. package/src/types/whatsappblocks.v1.ts +102 -0
  99. package/src/utils/array.ts +33 -0
  100. package/src/utils/async.ts +30 -0
  101. package/src/utils/date.ts +39 -0
  102. package/src/utils/index.ts +44 -0
  103. package/src/utils/intl.ts +51 -0
  104. package/src/utils/meta-status.ts +24 -0
  105. package/src/utils/number.ts +36 -0
  106. package/src/utils/object.ts +101 -0
  107. package/src/utils/string.ts +24 -0
  108. package/src/utils/templates/brand-cascade.ts +156 -0
  109. package/src/utils/templates/email-compiler.ts +155 -0
  110. package/src/utils/templates/email-mjml.ts +602 -0
  111. package/src/utils/templates/email.ts +713 -0
  112. package/src/utils/templates/html-sanitizer.ts +121 -0
  113. package/src/utils/templates/messages.ts +44 -0
  114. package/src/utils/templates/meta.ts +336 -0
  115. package/src/utils/templates/tiptap.ts +247 -0
  116. package/src/utils/templates/whatsapp-compiler.ts +212 -0
  117. package/src/utils/templates/whatsapp.ts +408 -0
  118. package/src/utils/validations.ts +49 -0
  119. package/tsconfig.json +10 -0
@@ -0,0 +1,602 @@
1
+ import type {
2
+ EmailMessage,
3
+ EmailBlock,
4
+ EmailGlobalSettings,
5
+ SpacerBlock,
6
+ DividerBlock,
7
+ HeadingBlock,
8
+ ParagraphBlock,
9
+ ImageBlock,
10
+ ButtonBlock,
11
+ GridBlock,
12
+ RawHtmlBlock,
13
+ DiscountBlock,
14
+ HeaderBlock,
15
+ FooterBlock,
16
+ UnsubscribeLinkBlock,
17
+ SocialsBlock,
18
+ TopProductsBlock,
19
+ GridAllowedBlock,
20
+ SocialPlatform,
21
+ } from "../../types/index.js";
22
+ import { SOCIAL_ICONS, SOCIAL_PLATFORMS } from "../../types/index.js";
23
+
24
+ // Default internal horizontal padding for text blocks (heading, paragraph, footer, unsubscribe_link)
25
+ const TEXT_HORIZONTAL_PADDING = 25;
26
+
27
+ import { generateHTML } from "@tiptap/html";
28
+ import type { JSONContent } from "@tiptap/core";
29
+ import { getTipTapExtensions } from "./tiptap.js";
30
+ import { generateText } from "./messages.js";
31
+
32
+ /**
33
+ * Converts TipTap JSONContent to HTML for email rendering
34
+ */
35
+ function generateEmailHtml(content: JSONContent | undefined): string {
36
+ if (!content) return "";
37
+ return generateHTML(content, getTipTapExtensions({ preset: "email" }));
38
+ }
39
+
40
+ /**
41
+ * Escapes special characters for XML/MJML attributes
42
+ */
43
+ function escapeXmlAttribute(value: string | undefined): string {
44
+ if (value === undefined || value === null) {
45
+ return "";
46
+ }
47
+ return value
48
+ .replace(/&/g, "&")
49
+ .replace(/</g, "&lt;")
50
+ .replace(/>/g, "&gt;")
51
+ .replace(/"/g, "&quot;")
52
+ .replace(/'/g, "&apos;");
53
+ }
54
+
55
+ /**
56
+ * Translates a spacer block to MJML
57
+ */
58
+ function translateSpacerBlock(
59
+ block: SpacerBlock,
60
+ settings: EmailGlobalSettings,
61
+ ): string {
62
+ const bgColor =
63
+ block.settings.backgroundColor === "#FFFFFF00"
64
+ ? ""
65
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundColor)}"`;
66
+ const hPad = settings.horizontalSpacing ?? 0;
67
+
68
+ return `<mj-spacer height="${block.settings.height}px" padding="0px ${hPad}px"${bgColor} />`;
69
+ }
70
+
71
+ /**
72
+ * Translates a divider block to MJML
73
+ */
74
+ function translateDividerBlock(
75
+ block: DividerBlock,
76
+ settings: EmailGlobalSettings,
77
+ ): string {
78
+ const bgColor =
79
+ block.settings.backgroundColor === "#FFFFFF00"
80
+ ? ""
81
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundColor)}"`;
82
+ const hPad = settings.horizontalSpacing ?? 0;
83
+
84
+ return `<mj-divider
85
+ border-style="${block.settings.style}"
86
+ border-width="${block.settings.thickness}px"
87
+ width="${block.settings.width}%"
88
+ border-color="${escapeXmlAttribute(block.settings.color)}"
89
+ padding="0px ${hPad}px"${bgColor} />`;
90
+ }
91
+
92
+ /**
93
+ * Translates a heading block to MJML
94
+ */
95
+ function translateHeadingBlock(
96
+ block: HeadingBlock,
97
+ settings: EmailGlobalSettings,
98
+ ): string {
99
+ const html = generateEmailHtml(block.settings.content);
100
+ const bgColor =
101
+ block.settings.backgroundColor === "#FFFFFF00"
102
+ ? ""
103
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundColor)}"`;
104
+
105
+ const textColor = block.settings.textColor ?? settings.textColor;
106
+ const fontFamily = block.settings.fontFamily ?? settings.fontFamily;
107
+ const hPad = TEXT_HORIZONTAL_PADDING + (settings.horizontalSpacing ?? 0);
108
+
109
+ return `<mj-text
110
+ font-family="${escapeXmlAttribute(fontFamily)}"
111
+ font-size="${block.settings.fontSize}px"
112
+ color="${escapeXmlAttribute(textColor)}"
113
+ padding="0px ${hPad}px"${bgColor}>
114
+ <${block.settings.tag} style="font-size: ${block.settings.fontSize}px">${html}</${block.settings.tag}>
115
+ </mj-text>`;
116
+ }
117
+
118
+ /**
119
+ * Translates a paragraph block to MJML
120
+ */
121
+ function translateParagraphBlock(
122
+ block: ParagraphBlock,
123
+ settings: EmailGlobalSettings,
124
+ ): string {
125
+ const html = generateEmailHtml(block.settings.content);
126
+
127
+ const bgColor =
128
+ block.settings.backgroundColor === "#FFFFFF00"
129
+ ? ""
130
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundColor)}"`;
131
+
132
+ const textColor = block.settings.textColor ?? settings.textColor;
133
+ const fontFamily = block.settings.fontFamily ?? settings.fontFamily;
134
+ const hPad = TEXT_HORIZONTAL_PADDING + (settings.horizontalSpacing ?? 0);
135
+
136
+ return `<mj-text
137
+ font-family="${escapeXmlAttribute(fontFamily)}"
138
+ font-size="${block.settings.fontSize}px"
139
+ color="${escapeXmlAttribute(textColor)}"
140
+ padding="0px ${hPad}px"${bgColor}>
141
+ <${block.settings.tag} style="font-size: ${block.settings.fontSize}px">${html}</${block.settings.tag}>
142
+ </mj-text>`;
143
+ }
144
+
145
+ /**
146
+ * Translates an image block to MJML
147
+ */
148
+ function translateImageBlock(
149
+ block: ImageBlock,
150
+ settings: EmailGlobalSettings,
151
+ ): string {
152
+ const href = block.settings.href
153
+ ? ` href="${escapeXmlAttribute(block.settings.href)}"`
154
+ : "";
155
+ const hPad = settings.horizontalSpacing ?? 0;
156
+
157
+ return `<mj-image src="${escapeXmlAttribute(block.settings.src)}"${href} padding="0px ${hPad}px" />`;
158
+ }
159
+
160
+ /**
161
+ * Translates a button block to MJML
162
+ */
163
+ function translateButtonBlock(
164
+ block: ButtonBlock,
165
+ settings: EmailGlobalSettings,
166
+ ): string {
167
+ const html = generateText(block.settings.content!, "email_v1");
168
+
169
+ const borderRadius =
170
+ block.settings.shape === "square"
171
+ ? "0px"
172
+ : block.settings.shape === "rounded"
173
+ ? "4px"
174
+ : "50px";
175
+
176
+ const widthAttr =
177
+ block.settings.width === "full_width" ? ' width="100%"' : "";
178
+
179
+ const bgBlock =
180
+ block.settings.backgroundBlock === "#FFFFFF00"
181
+ ? ""
182
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundBlock)}"`;
183
+
184
+ // Prepend mailto: or tel: based on linkType
185
+ let href = block.settings.url;
186
+ if (block.settings.linkType === "email" && !href.startsWith("mailto:")) {
187
+ href = `mailto:${href}`;
188
+ } else if (block.settings.linkType === "phone" && !href.startsWith("tel:")) {
189
+ href = `tel:${href}`;
190
+ }
191
+
192
+ const textColor =
193
+ block.settings.textColor ?? settings.button?.textColor ?? "#FFFFFF";
194
+ const backgroundColor =
195
+ block.settings.backgroundColor ??
196
+ settings.button?.backgroundColor ??
197
+ "#000000";
198
+
199
+ const fontFamily = block.settings.fontFamily ?? settings.fontFamily;
200
+
201
+ const hPad = settings.horizontalSpacing ?? 0;
202
+
203
+ return `<mj-button
204
+ href="${escapeXmlAttribute(href)}"
205
+ font-family="${escapeXmlAttribute(fontFamily)}"
206
+ font-size="${block.settings.fontSize}px"
207
+ color="${escapeXmlAttribute(textColor)}"
208
+ background-color="${escapeXmlAttribute(backgroundColor)}"
209
+ border-radius="${borderRadius}"${widthAttr}
210
+ align="${block.settings.alignment}"
211
+ padding="0px ${hPad}px"${bgBlock}>
212
+ ${html}
213
+ </mj-button>`;
214
+ }
215
+
216
+ /**
217
+ * Translates a grid block to MJML (multi-column section)
218
+ */
219
+ function translateGridBlock(
220
+ block: GridBlock,
221
+ settings: EmailGlobalSettings,
222
+ sectionPadding: string,
223
+ ): string {
224
+ const columnWidth = `${100 / block.settings.columns}%`;
225
+
226
+ const columns = block.settings.columnData
227
+ .map(
228
+ (col) => `
229
+ <mj-column width="${columnWidth}">
230
+ ${col.blocks.map((b) => translateContentBlockToMjml(b, settings)).join("\n ")}
231
+ </mj-column>`,
232
+ )
233
+ .join("");
234
+
235
+ const bgColor =
236
+ block.settings.backgroundColor === "#FFFFFF00"
237
+ ? ""
238
+ : ` background-color="${escapeXmlAttribute(block.settings.backgroundColor)}"`;
239
+
240
+ return `<mj-section padding="${sectionPadding}"${bgColor}>
241
+ ${columns}
242
+ </mj-section>`;
243
+ }
244
+
245
+ /**
246
+ * Translates a raw HTML block to MJML
247
+ */
248
+ function translateRawHtmlBlock(block: RawHtmlBlock): string {
249
+ return `<mj-raw>${block.settings.html}</mj-raw>`;
250
+ }
251
+
252
+ /**
253
+ * Translates a discount block to MJML
254
+ */
255
+ function translateDiscountBlock(
256
+ block: DiscountBlock,
257
+ settings: EmailGlobalSettings,
258
+ ): string {
259
+ const html = generateEmailHtml(block.settings.content);
260
+
261
+ const borderRadius =
262
+ block.settings.style === "square"
263
+ ? "0px"
264
+ : block.settings.style === "rounded"
265
+ ? "4px"
266
+ : "50px";
267
+
268
+ const widthAttr =
269
+ block.settings.width === "full_width" ? " width: 100%;" : "";
270
+
271
+ const bgBlock =
272
+ block.settings.backgroundBlock === "#FFFFFF00"
273
+ ? ""
274
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundBlock)}"`;
275
+
276
+ const textColor = block.settings.textColor ?? settings.textColor;
277
+ const fontFamily = block.settings.fontFamily ?? settings.fontFamily;
278
+
279
+ const hPad = settings.horizontalSpacing ?? 0;
280
+
281
+ return `<mj-text
282
+ font-family="${escapeXmlAttribute(fontFamily)}"
283
+ font-size="${block.settings.fontSize}px"
284
+ color="${escapeXmlAttribute(textColor)}"
285
+ align="center"
286
+ padding="0px ${hPad}px"${bgBlock}>
287
+ <${block.settings.tag}
288
+ style="
289
+ font-size: ${block.settings.fontSize}px;
290
+ border-radius: ${borderRadius};
291
+ padding: 12px 0px;
292
+ background-color: ${escapeXmlAttribute(block.settings.backgroundComponent)};
293
+ text-align: center; ${widthAttr}"
294
+ >${html}</${block.settings.tag}>
295
+ </mj-text>`;
296
+ }
297
+
298
+ /**
299
+ * Translates a header block to MJML
300
+ */
301
+ function translateHeaderBlock(
302
+ block: HeaderBlock,
303
+ settings: EmailGlobalSettings,
304
+ ): string {
305
+ const bgColor =
306
+ block.settings.backgroundColor === "#FFFFFF00"
307
+ ? ""
308
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundColor)}"`;
309
+ const hPad = settings.horizontalSpacing ?? 0;
310
+
311
+ return `<mj-image
312
+ src="${escapeXmlAttribute(block.settings.src)}"
313
+ alt="${escapeXmlAttribute(block.settings.alt)}"
314
+ width="${block.settings.width}px"
315
+ align="${block.settings.alignment}"
316
+ padding="0px ${hPad}px"${bgColor} />`;
317
+ }
318
+
319
+ /**
320
+ * Translates a footer block to MJML
321
+ */
322
+ function translateFooterBlock(
323
+ block: FooterBlock,
324
+ settings: EmailGlobalSettings,
325
+ ): string {
326
+ const html = generateEmailHtml(block.settings.content);
327
+
328
+ const bgColor =
329
+ block.settings.backgroundColor === "#FFFFFF00"
330
+ ? ""
331
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundColor)}"`;
332
+
333
+ const textColor = block.settings.textColor ?? settings.textColor;
334
+ const fontFamily = block.settings.fontFamily ?? settings.fontFamily;
335
+
336
+ const hPad = TEXT_HORIZONTAL_PADDING + (settings.horizontalSpacing ?? 0);
337
+
338
+ return `<mj-text
339
+ font-family="${escapeXmlAttribute(fontFamily)}"
340
+ font-size="${block.settings.fontSize}px"
341
+ color="${escapeXmlAttribute(textColor)}"
342
+ padding="0px ${hPad}px"${bgColor}>
343
+ ${html}
344
+ </mj-text>`;
345
+ }
346
+
347
+ /**
348
+ * Translates an unsubscribe link block to MJML
349
+ */
350
+ function translateUnsubscribeLinkBlock(
351
+ block: UnsubscribeLinkBlock,
352
+ settings: EmailGlobalSettings,
353
+ ): string {
354
+ const html = generateEmailHtml(block.settings.content);
355
+
356
+ const bgColor =
357
+ block.settings.backgroundColor === "#FFFFFF00"
358
+ ? ""
359
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundColor)}"`;
360
+
361
+ const textColor = block.settings.textColor ?? settings.textColor;
362
+ const fontFamily = block.settings.fontFamily ?? settings.fontFamily;
363
+
364
+ const hPad = TEXT_HORIZONTAL_PADDING + (settings.horizontalSpacing ?? 0);
365
+
366
+ return `<mj-text
367
+ font-family="${escapeXmlAttribute(fontFamily)}"
368
+ font-size="${block.settings.fontSize}px"
369
+ color="${escapeXmlAttribute(textColor)}"
370
+ align="${block.settings.alignment}"
371
+ padding="0px ${hPad}px"${bgColor}>
372
+ ${html}
373
+ </mj-text>`;
374
+ }
375
+
376
+ /**
377
+ * Translates a socials block to MJML
378
+ */
379
+ function translateSocialsBlock(
380
+ block: SocialsBlock,
381
+ settings: EmailGlobalSettings,
382
+ ): string {
383
+ const bgColor =
384
+ block.settings.backgroundColor === "#FFFFFF00"
385
+ ? ""
386
+ : ` container-background-color="${escapeXmlAttribute(block.settings.backgroundColor)}"`;
387
+
388
+ const enabledSocials = SOCIAL_PLATFORMS.filter(
389
+ (platform: SocialPlatform) => block.settings.socials[platform].enabled,
390
+ );
391
+
392
+ const socialIconsHtml = enabledSocials
393
+ .filter((platform: SocialPlatform) => {
394
+ const url = block.settings.socials[platform].url;
395
+ return url && url.trim() !== "";
396
+ })
397
+ .map((platform: SocialPlatform) => {
398
+ const url = escapeXmlAttribute(block.settings.socials[platform].url);
399
+ const icon = SOCIAL_ICONS[platform];
400
+ return `<a href="${url}" target="_blank" rel="noopener noreferrer" style="display:inline-block;margin:0 4px;"><img src="${icon}" alt="${platform}" width="24" height="24" style="border:0;display:inline-block;" /></a>`;
401
+ })
402
+ .join("");
403
+
404
+ const hPad = settings.horizontalSpacing ?? 0;
405
+
406
+ return `<mj-text align="center" padding="0px ${hPad}px"${bgColor}>
407
+ ${socialIconsHtml}
408
+ </mj-text>`;
409
+ }
410
+
411
+ /**
412
+ * Translates a top products block to MJML
413
+ * Products are pre-fetched during compilation enrichment
414
+ */
415
+ function translateTopProductsBlock(
416
+ block: TopProductsBlock,
417
+ settings: EmailGlobalSettings,
418
+ ): string {
419
+ const products = block._products;
420
+ if (!products || products.length === 0) return "";
421
+
422
+ const fontFamily = settings.fontFamily || "Arial";
423
+ const displayProducts = products.slice(0, block.settings.maxProducts);
424
+
425
+ const productSections = displayProducts
426
+ .map((product) => {
427
+ const parsed = parseFloat(product.price);
428
+ const showPrice =
429
+ product.price &&
430
+ product.price !== "0" &&
431
+ !isNaN(parsed) &&
432
+ parsed !== 0;
433
+ const priceHtml = showPrice
434
+ ? `<mj-text align="left" font-family="${escapeXmlAttribute(fontFamily)}" font-size="10px" line-height="16px" color="#86837C" padding="0px">${escapeXmlAttribute(product.price)}</mj-text>`
435
+ : "";
436
+
437
+ return `<mj-section background-color="#FFFFFF" padding="4px" border-radius="12px" border="2px solid #F1F0EF">
438
+ <mj-group>
439
+ <mj-column width="30%">
440
+ <mj-image padding="0px" border-radius="4px" src="${escapeXmlAttribute(product.image)}" alt="${escapeXmlAttribute(product.name)}" href="${escapeXmlAttribute(product.link)}" />
441
+ </mj-column>
442
+ <mj-column width="70%" padding="8px">
443
+ <mj-text padding="0px" align="left" font-family="${escapeXmlAttribute(fontFamily)}" font-size="12px" line-height="18px" font-weight="500" color="#292826" height="55px">${escapeXmlAttribute(product.name)}</mj-text>
444
+ ${priceHtml}
445
+ </mj-column>
446
+ </mj-group>
447
+ </mj-section>`;
448
+ })
449
+ .join("\n");
450
+
451
+ return `<mj-wrapper padding="24px" background-color="${escapeXmlAttribute(block.settings.backgroundColor)}">
452
+ ${productSections}
453
+ </mj-wrapper>`;
454
+ }
455
+
456
+ /**
457
+ * Translates a single content block to MJML (non-grid blocks only)
458
+ * These are components that go inside mj-column
459
+ */
460
+ function translateContentBlockToMjml(
461
+ block: GridAllowedBlock,
462
+ settings: EmailGlobalSettings,
463
+ ): string {
464
+ switch (block.type) {
465
+ case "discount":
466
+ return translateDiscountBlock(block, settings);
467
+ case "heading":
468
+ return translateHeadingBlock(block, settings);
469
+ case "paragraph":
470
+ return translateParagraphBlock(block, settings);
471
+ case "image":
472
+ return translateImageBlock(block, settings);
473
+ case "button":
474
+ return translateButtonBlock(block, settings);
475
+ default:
476
+ return "";
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Translates a block to MJML section(s)
482
+ * Grid blocks become multi-column sections
483
+ * Other blocks are wrapped in single-column sections
484
+ */
485
+ function translateBlockToSection(
486
+ block: EmailBlock,
487
+ settings: EmailGlobalSettings,
488
+ isFirst: boolean,
489
+ ): string {
490
+ const { verticalSpacing = 0 } = settings;
491
+ const paddingTop = isFirst ? 0 : verticalSpacing;
492
+ const sectionPadding = `${paddingTop}px 0px 0px 0px`;
493
+
494
+ // Wrap single blocks in a section with one column
495
+ let content = "";
496
+ switch (block.type) {
497
+ case "spacer":
498
+ content = translateSpacerBlock(block, settings);
499
+ break;
500
+ case "divider":
501
+ content = translateDividerBlock(block, settings);
502
+ break;
503
+ case "heading":
504
+ content = translateHeadingBlock(block, settings);
505
+ break;
506
+ case "paragraph":
507
+ content = translateParagraphBlock(block, settings);
508
+ break;
509
+ case "image":
510
+ content = translateImageBlock(block, settings);
511
+ break;
512
+ case "button":
513
+ content = translateButtonBlock(block, settings);
514
+ break;
515
+ case "discount":
516
+ content = translateDiscountBlock(block, settings);
517
+ break;
518
+ case "header":
519
+ content = translateHeaderBlock(block, settings);
520
+ break;
521
+ case "raw_html":
522
+ return translateRawHtmlBlock(block);
523
+ case "grid":
524
+ return translateGridBlock(block, settings, sectionPadding);
525
+ case "footer":
526
+ content = translateFooterBlock(block, settings);
527
+ break;
528
+ case "unsubscribe_link":
529
+ content = translateUnsubscribeLinkBlock(block, settings);
530
+ break;
531
+ case "socials":
532
+ content = translateSocialsBlock(block, settings);
533
+ break;
534
+ case "top_products":
535
+ return translateTopProductsBlock(block, settings);
536
+ default:
537
+ return "";
538
+ }
539
+
540
+ return `<mj-section padding="${sectionPadding}">
541
+ <mj-column>
542
+ ${content}
543
+ </mj-column>
544
+ </mj-section>`;
545
+ }
546
+
547
+ /**
548
+ * Generates MJML head section with global settings
549
+ */
550
+ function generateMjmlHead(settings: EmailGlobalSettings): string {
551
+ const fontFamily = settings.fontFamily || "Arial";
552
+ const textColor = settings.textColor || "#1F2937";
553
+
554
+ return `<mj-head>
555
+ <mj-attributes>
556
+ <mj-text font-family="${escapeXmlAttribute(fontFamily)}" color="${escapeXmlAttribute(textColor)}" padding="0" />
557
+ <mj-all font-family="${escapeXmlAttribute(fontFamily)}" />
558
+ <mj-wrapper padding="0" />
559
+ <mj-section padding="0" />
560
+ <mj-image padding="0" />
561
+ <mj-button padding="0" />
562
+ </mj-attributes>
563
+ <mj-style>
564
+ h1, h2, h3, h4, h5, h6 {
565
+ font-weight: normal;
566
+ }
567
+ .tiptap-emoji img {
568
+ height: 1em !important;
569
+ width: 1em !important;
570
+ vertical-align: middle !important;
571
+ }
572
+ </mj-style>
573
+ </mj-head>`;
574
+ }
575
+
576
+ /**
577
+ * Translates an EmailMessage to MJML format
578
+ *
579
+ * @param message - The email message to translate
580
+ * @returns MJML string ready for compilation
581
+ */
582
+ export function translateEmailToMjml(message: EmailMessage): string {
583
+ const { blocks, settings } = message.body;
584
+
585
+ const sections = blocks
586
+ .map((block, index) =>
587
+ translateBlockToSection(block, settings, index === 0),
588
+ )
589
+ .join("\n ");
590
+
591
+ // Apply defaults for potentially undefined settings
592
+ const contentBackgroundColor = settings.contentBackgroundColor || "#FFFFFF";
593
+ const _backgroundColor = settings.backgroundColor || "#F3F4F6";
594
+ const contentWidth = settings.contentWidth || 600;
595
+ return `<mjml>
596
+ ${generateMjmlHead(settings)}
597
+ <mj-body background-color="${escapeXmlAttribute(contentBackgroundColor)}" width="${contentWidth}px">
598
+
599
+ ${sections}
600
+ </mj-body>
601
+ </mjml>`;
602
+ }