@dev.smartpricing/message-composer-utils 4.2.7-maizzle.0 → 4.2.7

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.
@@ -1,7 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { brandDefaultEmailSettingsSchema, brandDefaultButtonSettingsSchema, } from "./brand.js";
3
3
  export const MESSAGE_RENDER_TYPES = ["whatsapp_v1", "email_v1"];
4
- export const EMAIL_RENDERERS = ["mjml", "maizzle"];
5
4
  export const MESSAGE_STATUS = [
6
5
  "pending",
7
6
  "draft",
@@ -37,6 +36,5 @@ export const previewMessageRequestSchema = z.object({
37
36
  })
38
37
  .passthrough(),
39
38
  brand: brandCascadeDataSchema.nullable().optional(),
40
- renderer: z.enum(EMAIL_RENDERERS).default("mjml").optional(),
41
39
  });
42
40
  //# sourceMappingURL=message.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/types/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,EACL,+BAA+B,EAC/B,gCAAgC,GACjC,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,aAAa,EAAE,UAAU,CAAU,CAAC;AAGzE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,SAAS,CAAU,CAAC;AAE5D,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,SAAS;IACT,OAAO;IACP,UAAU;IACV,UAAU;CACF,CAAC;AAmDX,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,WAAW,EAAE,SAAS,CAAU,CAAC;AAG1E,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAU,CAAC;AAiExE;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,sBAAsB,EAAE,+BAA+B;IACvD,uBAAuB,EAAE,gCAAgC;IACzD,eAAe,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC5D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAChC,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,OAAO,EAAE,CAAC;SACP,MAAM,CAAC;QACN,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;QAClC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;YACb,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;YACnB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAClD,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;SAC5C,CAAC;QACF,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;KACvD,CAAC;SACD,WAAW,EAAE;IAChB,KAAK,EAAE,sBAAsB,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACnD,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;CAC7D,CAAC,CAAC"}
1
+ {"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/types/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,EACL,+BAA+B,EAC/B,gCAAgC,GACjC,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,aAAa,EAAE,UAAU,CAAU,CAAC;AAEzE,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,SAAS;IACT,OAAO;IACP,UAAU;IACV,UAAU;CACF,CAAC;AAmDX,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,WAAW,EAAE,SAAS,CAAU,CAAC;AAG1E,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAU,CAAC;AA8DxE;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,sBAAsB,EAAE,+BAA+B;IACvD,uBAAuB,EAAE,gCAAgC;IACzD,eAAe,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC5D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAChC,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,OAAO,EAAE,CAAC;SACP,MAAM,CAAC;QACN,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;QAClC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;YACb,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;YACnB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAClD,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;SAC5C,CAAC;QACF,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;KACvD,CAAC;SACD,WAAW,EAAE;IAChB,KAAK,EAAE,sBAAsB,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACpD,CAAC,CAAC"}
@@ -14,7 +14,6 @@ export * from "./templates/tiptap.js";
14
14
  export * from "./templates/whatsapp-compiler.js";
15
15
  export * from "./templates/email-compiler.js";
16
16
  export * from "./templates/email-mjml.js";
17
- export * from "./templates/email-maizzle.js";
18
17
  export * from "./templates/brand-cascade.js";
19
18
  export * from "./templates/html-sanitizer.js";
20
19
  export * from "./validations.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kCAAkC,CAAC;AACjD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AAEjC,wDAAwD;AACxD,OAAO,KAAK,UAAU,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,UAAU,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,SAAS,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,WAAW,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,WAAW,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,WAAW,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,aAAa,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,gBAAgB,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,SAAS,MAAM,WAAW,CAAC;AAEvC,wDAAwD;AACxD,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,yBAAyB,EACzB,kBAAkB,IAAI,0BAA0B,GACjD,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kCAAkC,CAAC;AACjD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AAEjC,wDAAwD;AACxD,OAAO,KAAK,UAAU,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,UAAU,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,SAAS,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,WAAW,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,WAAW,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,WAAW,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,aAAa,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,gBAAgB,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,SAAS,MAAM,WAAW,CAAC;AAEvC,wDAAwD;AACxD,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,yBAAyB,EACzB,kBAAkB,IAAI,0BAA0B,GACjD,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.smartpricing/message-composer-utils",
3
- "version": "4.2.7-maizzle.0",
3
+ "version": "4.2.7",
4
4
  "description": "Shared types and utilities for the message composer",
5
5
  "type": "module",
6
6
  "exports": {
@@ -15,9 +15,6 @@ import {
15
15
 
16
16
  export const MESSAGE_RENDER_TYPES = ["whatsapp_v1", "email_v1"] as const;
17
17
  export type RenderType = (typeof MESSAGE_RENDER_TYPES)[number];
18
-
19
- export const EMAIL_RENDERERS = ["mjml", "maizzle"] as const;
20
- export type EmailRenderer = (typeof EMAIL_RENDERERS)[number];
21
18
  export const MESSAGE_STATUS = [
22
19
  "pending",
23
20
  "draft",
@@ -137,9 +134,6 @@ export interface CompileMessageRequest {
137
134
 
138
135
  /** When true, skips "first available" fallback and returns 404 if neither language nor fallbackLanguage found */
139
136
  strict_language?: boolean;
140
-
141
- /** Email renderer to use for compilation (default: "mjml") */
142
- renderer?: EmailRenderer;
143
137
  }
144
138
 
145
139
  /**
@@ -170,7 +164,6 @@ export const previewMessageRequestSchema = z.object({
170
164
  })
171
165
  .passthrough(),
172
166
  brand: brandCascadeDataSchema.nullable().optional(),
173
- renderer: z.enum(EMAIL_RENDERERS).default("mjml").optional(),
174
167
  });
175
168
 
176
169
  export type PreviewMessageRequest = z.infer<typeof previewMessageRequestSchema>;
@@ -221,9 +214,6 @@ export interface TestEmailRequest {
221
214
 
222
215
  /** Sender email address — falls back to TEST_EMAIL_FROM env var */
223
216
  from?: string;
224
-
225
- /** Email renderer to use for compilation (default: "mjml") */
226
- renderer?: EmailRenderer;
227
217
  }
228
218
 
229
219
  /**
@@ -338,7 +328,6 @@ export interface CompileEmailResponse {
338
328
  language: string;
339
329
  subject: string;
340
330
  html: string;
341
- plaintext?: string;
342
331
  parameters_used: string[];
343
332
  missingParameters?: string[];
344
333
  }
@@ -14,7 +14,6 @@ export * from "./templates/tiptap.js";
14
14
  export * from "./templates/whatsapp-compiler.js";
15
15
  export * from "./templates/email-compiler.js";
16
16
  export * from "./templates/email-mjml.js";
17
- export * from "./templates/email-maizzle.js";
18
17
  export * from "./templates/brand-cascade.js";
19
18
  export * from "./templates/html-sanitizer.js";
20
19
  export * from "./validations.js";
@@ -1,581 +0,0 @@
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
- import { generateHTML } from "@tiptap/html";
25
- import type { JSONContent } from "@tiptap/core";
26
- import { getTipTapExtensions, sanitizeContent } from "./tiptap.js";
27
- import { generateHtml } from "./messages.js";
28
-
29
- // Default internal horizontal padding for text blocks (heading, paragraph, footer, unsubscribe_link)
30
- const TEXT_HORIZONTAL_PADDING = 25;
31
-
32
- /**
33
- * Converts TipTap JSONContent to HTML for email rendering
34
- */
35
- function generateEmailHtml(content: JSONContent | undefined): string {
36
- if (!content) return "";
37
- const extensions = getTipTapExtensions({ preset: "email" });
38
- const html = generateHTML(sanitizeContent(content, extensions), extensions);
39
- // Preserve empty paragraphs as spacing (TipTap outputs <p></p> which collapses)
40
- return html.replace(/<p><\/p>/g, "<p><br></p>");
41
- }
42
-
43
- /**
44
- * Escapes special characters for HTML attributes
45
- */
46
- function escapeHtmlAttribute(value: string | undefined): string {
47
- if (value === undefined || value === null) {
48
- return "";
49
- }
50
- return value
51
- .replace(/&/g, "&amp;")
52
- .replace(/</g, "&lt;")
53
- .replace(/>/g, "&gt;")
54
- .replace(/"/g, "&quot;")
55
- .replace(/'/g, "&apos;");
56
- }
57
-
58
- /**
59
- * Wraps HTML content in v-pre to prevent Vue from parsing {{ }} placeholders.
60
- * Used for TipTap-generated HTML that may contain {{param}} template variables.
61
- */
62
- function _wrapVPre(html: string): string {
63
- return `<div v-pre>${html}</div>`;
64
- }
65
-
66
- /**
67
- * Build a bg class from a color, treating #FFFFFF00 as transparent (no class).
68
- */
69
- function bgClass(color: string | undefined): string {
70
- if (!color || color === "#FFFFFF00" || color === "transparent") return "";
71
- return ` bg-[${color}]`;
72
- }
73
-
74
- /**
75
- * Translates a spacer block to Maizzle
76
- */
77
- function translateSpacerBlock(
78
- block: SpacerBlock,
79
- settings: EmailGlobalSettings,
80
- ): string {
81
- const hPad = settings.horizontalSpacing ?? 0;
82
-
83
- return `<Spacer class="h-[${block.settings.height}px] px-[${hPad}px]${bgClass(block.settings.backgroundColor)}" />`;
84
- }
85
-
86
- /**
87
- * Translates a divider block to Maizzle.
88
- * Maizzle's <Hr> is a <div role="separator"> styled with bg-* for color.
89
- * For dotted/dashed styles we use a raw div with border-top.
90
- */
91
- function translateDividerBlock(
92
- block: DividerBlock,
93
- settings: EmailGlobalSettings,
94
- ): string {
95
- const hPad = settings.horizontalSpacing ?? 0;
96
- const bg = bgClass(block.settings.backgroundColor);
97
- const color = escapeHtmlAttribute(block.settings.color);
98
-
99
- if (block.settings.style === "solid") {
100
- return `<Hr class="bg-[${color}] h-[${block.settings.thickness}px] my-0 mx-auto max-w-[${block.settings.width}%] px-[${hPad}px]${bg}" />`;
101
- }
102
-
103
- // dotted/dashed — Hr can't express border styles, use raw div
104
- return `<div role="separator" class="my-0 mx-auto max-w-[${block.settings.width}%] px-[${hPad}px]${bg}" style="border-top: ${block.settings.thickness}px ${block.settings.style} ${color};"></div>`;
105
- }
106
-
107
- /**
108
- * Translates a heading block to Maizzle.
109
- *
110
- * Uses <div> instead of Maizzle's <Text> (which renders <p>) because the inner
111
- * content contains block-level elements (<h1>–<h6>, <div>, <ul>, etc.).
112
- * Nesting block elements inside <p> is invalid HTML — browsers auto-close the
113
- * <p>, orphaning all styles (background, color, font, padding).
114
- * Maizzle's CSS pipeline (inlining, purging, shorthand) works on all elements,
115
- * not just Maizzle components, so <div> gets identical treatment.
116
- */
117
- function translateHeadingBlock(
118
- block: HeadingBlock,
119
- settings: EmailGlobalSettings,
120
- ): string {
121
- const html = generateEmailHtml(block.settings.content);
122
- const textColor = block.settings.textColor ?? settings.textColor ?? "#1F2937";
123
- const fontFamily =
124
- block.settings.fontFamily ?? settings.fontFamily ?? "Arial";
125
- const hPad = TEXT_HORIZONTAL_PADDING + (settings.horizontalSpacing ?? 0);
126
-
127
- return `<div class="overflow-hidden font-[${escapeHtmlAttribute(fontFamily)}] text-[${block.settings.fontSize}px] text-[${escapeHtmlAttribute(textColor)}] px-[${hPad}px]${bgClass(block.settings.backgroundColor)}" v-pre>
128
- <${block.settings.tag} style="margin: 0; font-size: ${block.settings.fontSize}px">${html}</${block.settings.tag}>
129
- </div>`;
130
- }
131
-
132
- /**
133
- * Translates a paragraph block to Maizzle
134
- * See translateHeadingBlock for why <div> is used instead of <Text>.
135
- */
136
- function translateParagraphBlock(
137
- block: ParagraphBlock,
138
- settings: EmailGlobalSettings,
139
- ): string {
140
- const html = generateEmailHtml(block.settings.content);
141
- const textColor = block.settings.textColor ?? settings.textColor ?? "#1F2937";
142
- const fontFamily =
143
- block.settings.fontFamily ?? settings.fontFamily ?? "Arial";
144
- const hPad = TEXT_HORIZONTAL_PADDING + (settings.horizontalSpacing ?? 0);
145
-
146
- return `<div class="overflow-hidden font-[${escapeHtmlAttribute(fontFamily)}] text-[${block.settings.fontSize}px] text-[${escapeHtmlAttribute(textColor)}] px-[${hPad}px]${bgClass(block.settings.backgroundColor)}" v-pre>
147
- <${block.settings.tag} style="margin: 0; font-size: ${block.settings.fontSize}px">${html}</${block.settings.tag}>
148
- </div>`;
149
- }
150
-
151
- /**
152
- * Translates an image block to Maizzle
153
- */
154
- function translateImageBlock(
155
- block: ImageBlock,
156
- settings: EmailGlobalSettings,
157
- ): string {
158
- const hPad = settings.horizontalSpacing ?? 0;
159
- const src = escapeHtmlAttribute(block.settings.src);
160
- const hrefAttr = block.settings.href
161
- ? ` href="${escapeHtmlAttribute(block.settings.href)}"`
162
- : "";
163
-
164
- return `<Img src="${src}" alt="" width="600"${hrefAttr} class="w-full px-[${hPad}px]" />`;
165
- }
166
-
167
- /**
168
- * Translates a button block to Maizzle
169
- */
170
- function translateButtonBlock(
171
- block: ButtonBlock,
172
- settings: EmailGlobalSettings,
173
- ): string {
174
- const text = generateHtml(block.settings.content!, "email_v1");
175
-
176
- const borderRadius =
177
- block.settings.shape === "square"
178
- ? "0px"
179
- : block.settings.shape === "rounded"
180
- ? "4px"
181
- : "50px";
182
-
183
- const widthClass =
184
- block.settings.width === "full_width" ? " w-full box-border" : "";
185
-
186
- let href = block.settings.url;
187
- if (block.settings.linkType === "email" && !href.startsWith("mailto:")) {
188
- href = `mailto:${href}`;
189
- } else if (block.settings.linkType === "phone" && !href.startsWith("tel:")) {
190
- href = `tel:${href}`;
191
- }
192
-
193
- const textColor =
194
- block.settings.textColor ?? settings.button?.textColor ?? "#FFFFFF";
195
- const backgroundColor =
196
- block.settings.backgroundColor ??
197
- settings.button?.backgroundColor ??
198
- "#000000";
199
- const fontFamily =
200
- block.settings.fontFamily ?? settings.fontFamily ?? "Arial";
201
-
202
- const alignAttr =
203
- block.settings.width === "full_width" ? "center" : block.settings.alignment;
204
-
205
- const btn = `<Button href="${escapeHtmlAttribute(href)}" align="${alignAttr}" class="no-underline bg-[${escapeHtmlAttribute(backgroundColor)}] text-[${escapeHtmlAttribute(textColor)}] rounded-[${borderRadius}] font-[${escapeHtmlAttribute(fontFamily)}] text-[${block.settings.fontSize}px]${widthClass}">
206
- <span v-pre>${text}</span>
207
- </Button>`;
208
-
209
- const bg = bgClass(block.settings.backgroundBlock);
210
- if (bg) {
211
- return `<div class="${bg.trim()}">${btn}</div>`;
212
- }
213
- return btn;
214
- }
215
-
216
- /**
217
- * Translates a content block inside a grid column to Maizzle
218
- */
219
- function translateContentBlockToMaizzle(
220
- block: GridAllowedBlock,
221
- settings: EmailGlobalSettings,
222
- ): string {
223
- switch (block.type) {
224
- case "discount":
225
- return translateDiscountBlock(block, settings);
226
- case "heading":
227
- return translateHeadingBlock(block, settings);
228
- case "paragraph":
229
- return translateParagraphBlock(block, settings);
230
- case "image":
231
- return translateImageBlock(block, settings);
232
- case "button":
233
- return translateButtonBlock(block, settings);
234
- default:
235
- return "";
236
- }
237
- }
238
-
239
- /**
240
- * Extracts the "container background color" from a grid-allowed block.
241
- * Mirrors MJML's container-background-color which fills the parent column cell.
242
- */
243
- function getBlockContainerBg(block: GridAllowedBlock): string | undefined {
244
- switch (block.type) {
245
- case "heading":
246
- case "paragraph":
247
- return block.settings.backgroundColor;
248
- case "discount":
249
- case "button":
250
- return block.settings.backgroundBlock;
251
- default:
252
- return undefined;
253
- }
254
- }
255
-
256
- /**
257
- * Translates a grid block to Maizzle (multi-column layout).
258
- * Columns stack to full-width rows on mobile via max-sm:min-w-full.
259
- * Lifts first block's container-bg to Column so it fills the full cell height.
260
- *
261
- * TODO: equal-height columns — add `table-cell` back on <Column> once we have
262
- * a mobile-safe approach (table-cell prevents stacking on small viewports).
263
- */
264
- function translateGridBlock(
265
- block: GridBlock,
266
- settings: EmailGlobalSettings,
267
- ): string {
268
- const cols = block.settings.columns;
269
- const widthClass = `w-${cols === 3 ? "1/3" : cols === 4 ? "1/4" : "1/2"}`;
270
-
271
- const columns = block.settings.columnData
272
- .map((col) => {
273
- const content = col.blocks
274
- .map((b) => translateContentBlockToMaizzle(b, settings))
275
- .join("\n ");
276
- const colBg =
277
- col.blocks.length > 0
278
- ? bgClass(getBlockContainerBg(col.blocks[0]))
279
- : "";
280
-
281
- return `
282
- <Column class="${widthClass} max-sm:min-w-full${colBg}">
283
- ${content}
284
- </Column>`;
285
- })
286
- .join("");
287
-
288
- return `<Row class="${bgClass(block.settings.backgroundColor).trim()}">
289
- ${columns}
290
- </Row>`;
291
- }
292
-
293
- /**
294
- * Translates a raw HTML block to Maizzle
295
- */
296
- function translateRawHtmlBlock(block: RawHtmlBlock): string {
297
- return `<Raw>${block.settings.html}</Raw>`;
298
- }
299
-
300
- /**
301
- * Translates a discount block to Maizzle
302
- */
303
- function translateDiscountBlock(
304
- block: DiscountBlock,
305
- settings: EmailGlobalSettings,
306
- ): string {
307
- const html = generateEmailHtml(block.settings.content);
308
-
309
- const borderRadius =
310
- block.settings.style === "square"
311
- ? "0px"
312
- : block.settings.style === "rounded"
313
- ? "4px"
314
- : "50px";
315
-
316
- const widthClass =
317
- block.settings.width === "full_width" ? " w-full" : " inline-block";
318
-
319
- const textColor = block.settings.textColor ?? settings.textColor ?? "#1F2937";
320
- const fontFamily =
321
- block.settings.fontFamily ?? settings.fontFamily ?? "Arial";
322
- const hPad = settings.horizontalSpacing ?? 0;
323
-
324
- return `<div class="overflow-hidden font-[${escapeHtmlAttribute(fontFamily)}] text-[${block.settings.fontSize}px] text-[${escapeHtmlAttribute(textColor)}] text-center px-[${hPad}px]${bgClass(block.settings.backgroundBlock)}" v-pre>
325
- <${block.settings.tag} class="rounded-[${borderRadius}] bg-[${escapeHtmlAttribute(block.settings.backgroundComponent)}] px-2 py-3 ${widthClass} box-border" style="margin: 0; font-size: ${block.settings.fontSize}px;">${html}</${block.settings.tag}>
326
- </div>`;
327
- }
328
-
329
- /**
330
- * Translates a header block to Maizzle.
331
- * When a backgroundColor is set, wraps <Img> in a <div> so the bg fills the
332
- * full container width (mimics MJML's container-background-color on the section cell).
333
- */
334
- function translateHeaderBlock(
335
- block: HeaderBlock,
336
- settings: EmailGlobalSettings,
337
- ): string {
338
- const hPad = settings.horizontalSpacing ?? 0;
339
-
340
- const alignClass =
341
- block.settings.alignment === "center"
342
- ? " block mx-auto"
343
- : block.settings.alignment === "right"
344
- ? " block ml-auto"
345
- : "";
346
-
347
- const bg = bgClass(block.settings.backgroundColor);
348
- const img = `<Img src="${escapeHtmlAttribute(block.settings.src)}" alt="${escapeHtmlAttribute(block.settings.alt)}" width="${block.settings.width}" class="${alignClass.trim()}" />`;
349
-
350
- if (bg) {
351
- return `<div class="px-[${hPad}px]${bg}">${img}</div>`;
352
- }
353
-
354
- return `<Img src="${escapeHtmlAttribute(block.settings.src)}" alt="${escapeHtmlAttribute(block.settings.alt)}" width="${block.settings.width}" class="px-[${hPad}px]${alignClass}" />`;
355
- }
356
-
357
- /**
358
- * Translates a footer block to Maizzle
359
- */
360
- function translateFooterBlock(
361
- block: FooterBlock,
362
- settings: EmailGlobalSettings,
363
- ): string {
364
- const html = generateEmailHtml(block.settings.content);
365
- const textColor = block.settings.textColor ?? settings.textColor ?? "#1F2937";
366
- const fontFamily =
367
- block.settings.fontFamily ?? settings.fontFamily ?? "Arial";
368
- const hPad = TEXT_HORIZONTAL_PADDING + (settings.horizontalSpacing ?? 0);
369
-
370
- return `<div class="overflow-hidden font-[${escapeHtmlAttribute(fontFamily)}] text-[${block.settings.fontSize}px] text-[${escapeHtmlAttribute(textColor)}] px-[${hPad}px]${bgClass(block.settings.backgroundColor)} leading-lg" v-pre>
371
- ${html}
372
- </div>`;
373
- }
374
-
375
- /**
376
- * Translates an unsubscribe link block to Maizzle
377
- * See translateHeadingBlock for why <div> is used instead of <Text>.
378
- */
379
- function translateUnsubscribeLinkBlock(
380
- block: UnsubscribeLinkBlock,
381
- settings: EmailGlobalSettings,
382
- ): string {
383
- const html = generateEmailHtml(block.settings.content);
384
- const textColor = block.settings.textColor ?? settings.textColor ?? "#1F2937";
385
- const fontFamily =
386
- block.settings.fontFamily ?? settings.fontFamily ?? "Arial";
387
- const hPad = TEXT_HORIZONTAL_PADDING + (settings.horizontalSpacing ?? 0);
388
-
389
- return `<div class="overflow-hidden font-[${escapeHtmlAttribute(fontFamily)}] text-[${block.settings.fontSize}px] text-[${escapeHtmlAttribute(textColor)}] text-${block.settings.alignment} px-[${hPad}px]${bgClass(block.settings.backgroundColor)}" v-pre>
390
- ${html}
391
- </div>`;
392
- }
393
-
394
- /**
395
- * Translates a socials block to Maizzle
396
- */
397
- function translateSocialsBlock(
398
- block: SocialsBlock,
399
- settings: EmailGlobalSettings,
400
- ): string {
401
- const hPad = settings.horizontalSpacing ?? 0;
402
-
403
- const enabledSocials = SOCIAL_PLATFORMS.filter(
404
- (platform: SocialPlatform) => block.settings.socials[platform].enabled,
405
- );
406
-
407
- const socialIconsHtml = enabledSocials
408
- .filter((platform: SocialPlatform) => {
409
- const url = block.settings.socials[platform].url;
410
- return url && url.trim() !== "";
411
- })
412
- .map((platform: SocialPlatform) => {
413
- const url = escapeHtmlAttribute(block.settings.socials[platform].url);
414
- const icon = SOCIAL_ICONS[platform];
415
- return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="inline-block mx-1"><img src="${icon}" alt="${platform}" width="24" height="24" class="border-0 inline-block" /></a>`;
416
- })
417
- .join("");
418
-
419
- return `<Text class="m-0 text-center px-[${hPad}px]${bgClass(block.settings.backgroundColor)}">
420
- ${socialIconsHtml}
421
- </Text>`;
422
- }
423
-
424
- /**
425
- * Translates a top products block to Maizzle
426
- * Products are pre-fetched during compilation enrichment
427
- */
428
- function translateTopProductsBlock(
429
- block: TopProductsBlock,
430
- settings: EmailGlobalSettings,
431
- ): string {
432
- const products = block._products;
433
- if (!products || products.length === 0) return "";
434
-
435
- const fontFamily = settings.fontFamily || "Arial";
436
- const displayProducts = products.slice(0, block.settings.maxProducts);
437
-
438
- const productCards = displayProducts
439
- .map((product) => {
440
- const parsed = parseFloat(product.price);
441
- const showPrice =
442
- product.price &&
443
- product.price !== "0" &&
444
- !isNaN(parsed) &&
445
- parsed !== 0;
446
- const priceHtml = showPrice
447
- ? `\n <Text class="text-[10px] leading-[16px] text-[#86837C] font-[${escapeHtmlAttribute(fontFamily)}]">${escapeHtmlAttribute(product.price)}</Text>`
448
- : "";
449
-
450
- return `<Row class="mb-2 bg-white rounded-xl border-2 border-[#F1F0EF]">
451
- <Column class="w-1/4 table-cell p-1 align-top">
452
- <Link href="${escapeHtmlAttribute(product.link)}">
453
- <Img src="${escapeHtmlAttribute(product.image)}" alt="${escapeHtmlAttribute(product.name)}" class="rounded w-full" />
454
- </Link>
455
- </Column>
456
- <Column class="w-3/4 table-cell px-2 py-3 align-top">
457
- <Text class="text-xs leading-[18px] font-medium text-[#292826] font-[${escapeHtmlAttribute(fontFamily)}]">${escapeHtmlAttribute(product.name)}</Text>${priceHtml}
458
- </Column>
459
- </Row>`;
460
- })
461
- .join("\n ");
462
-
463
- return `<div class="p-6 bg-[${escapeHtmlAttribute(block.settings.backgroundColor)}]">
464
- ${productCards}
465
- </div>`;
466
- }
467
-
468
- /**
469
- * Translates a block to a section fragment.
470
- * Adds vertical spacing between blocks via a Spacer when not the first block.
471
- */
472
- function translateBlockToSection(
473
- block: EmailBlock,
474
- settings: EmailGlobalSettings,
475
- isFirst: boolean,
476
- ): string {
477
- const { verticalSpacing = 0 } = settings;
478
- const spacer =
479
- !isFirst && verticalSpacing > 0
480
- ? `<Spacer class="h-[${verticalSpacing}px]" />\n `
481
- : "";
482
-
483
- let content = "";
484
- switch (block.type) {
485
- case "spacer":
486
- content = translateSpacerBlock(block, settings);
487
- break;
488
- case "divider":
489
- content = translateDividerBlock(block, settings);
490
- break;
491
- case "heading":
492
- content = translateHeadingBlock(block, settings);
493
- break;
494
- case "paragraph":
495
- content = translateParagraphBlock(block, settings);
496
- break;
497
- case "image":
498
- content = translateImageBlock(block, settings);
499
- break;
500
- case "button":
501
- content = translateButtonBlock(block, settings);
502
- break;
503
- case "discount":
504
- content = translateDiscountBlock(block, settings);
505
- break;
506
- case "header":
507
- content = translateHeaderBlock(block, settings);
508
- break;
509
- case "raw_html":
510
- return `${spacer}${translateRawHtmlBlock(block)}`;
511
- case "grid":
512
- return `${spacer}${translateGridBlock(block, settings)}`;
513
- case "footer":
514
- content = translateFooterBlock(block, settings);
515
- break;
516
- case "unsubscribe_link":
517
- content = translateUnsubscribeLinkBlock(block, settings);
518
- break;
519
- case "socials":
520
- content = translateSocialsBlock(block, settings);
521
- break;
522
- case "top_products":
523
- return `${spacer}${translateTopProductsBlock(block, settings)}`;
524
- default:
525
- return "";
526
- }
527
-
528
- return `${spacer}${content}`;
529
- }
530
-
531
- /**
532
- * Translates an EmailMessage to a Maizzle Vue SFC string.
533
- *
534
- * The output is a raw Vue Single File Component string ready to be
535
- * compiled by Maizzle's render() API. User-generated content containing
536
- * {{param}} placeholders is wrapped in v-pre to prevent Vue evaluation.
537
- * Parameters are injected post-render via string replacement.
538
- *
539
- * @param message - The email message to translate
540
- * @returns Vue SFC string ready for Maizzle render()
541
- */
542
- export function translateEmailToMaizzle(message: EmailMessage): string {
543
- const { blocks, settings } = message.body;
544
-
545
- const sections = blocks
546
- .map((block, index) =>
547
- translateBlockToSection(block, settings, index === 0),
548
- )
549
- .join("\n ");
550
-
551
- const contentBackgroundColor = settings.contentBackgroundColor || "#FFFFFF";
552
- const backgroundColor = settings.backgroundColor || "#F3F4F6";
553
- const contentWidth = settings.contentWidth || 600;
554
-
555
- const preheader = message.metadata.preheader
556
- ? `\n <Preheader v-pre>${escapeHtmlAttribute(message.metadata.preheader)}</Preheader>`
557
- : "";
558
-
559
- const fontFamily = settings.fontFamily || "Arial";
560
- const textColor = settings.textColor || "#1F2937";
561
-
562
- return `<script setup>
563
- usePlaintext()
564
- </script>
565
-
566
- <template>
567
- <Layout body-class="font-[${escapeHtmlAttribute(fontFamily)}] text-[${escapeHtmlAttribute(textColor)}] bg-[${escapeHtmlAttribute(backgroundColor)}]">
568
- <Head>
569
- <style embed>
570
- p { margin: 0; line-height: 1.5em }
571
- h1, h2, h3, h4, h5, h6 { font-weight: normal; }
572
- .tiptap-emoji img { height: 1em !important; width: 1em !important; vertical-align: middle !important; }
573
- </style>
574
- </Head>${preheader}
575
- <Container class="max-w-[${contentWidth}px] bg-[${escapeHtmlAttribute(contentBackgroundColor)}]">
576
- ${sections}
577
- </Container>
578
- </Layout>
579
- </template>
580
- `;
581
- }