@marlinjai/email-editor-core 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35,8 +35,11 @@ __export(index_exports, {
35
35
  ColumnModel: () => ColumnModel,
36
36
  ColumnSchema: () => ColumnSchema,
37
37
  CustomFontSchema: () => CustomFontSchema,
38
+ DEFAULT_MJML_ATTRIBUTES: () => DEFAULT_MJML_ATTRIBUTES,
38
39
  DEFAULT_ON_CHANGE_DEBOUNCE_MS: () => DEFAULT_ON_CHANGE_DEBOUNCE_MS,
39
40
  DividerBlockSchema: () => DividerBlockSchema,
41
+ EDITOR_DATA_ATTRIBUTE: () => EDITOR_DATA_ATTRIBUTE,
42
+ EDITOR_MARKER: () => EDITOR_MARKER,
40
43
  EditorUIStore: () => EditorUIStore,
41
44
  EmailTemplateSchema: () => EmailTemplateSchema,
42
45
  EmailTemplateSchemaV1_0: () => EmailTemplateSchemaV1_0,
@@ -51,6 +54,7 @@ __export(index_exports, {
51
54
  ImageBlockSchema: () => ImageBlockSchema,
52
55
  LEAF_BLOCK_TYPES: () => LEAF_BLOCK_TYPES,
53
56
  MJML_WRAPPER_DEFAULT_PADDING: () => MJML_WRAPPER_DEFAULT_PADDING,
57
+ MjmlBuilder: () => MjmlBuilder,
54
58
  MjmlHeadSchema: () => MjmlHeadSchema,
55
59
  NavbarBlockSchema: () => NavbarBlockSchema,
56
60
  NavbarLinkModel: () => NavbarLinkModel,
@@ -78,7 +82,9 @@ __export(index_exports, {
78
82
  TopLevelItemSchema: () => TopLevelItemSchema,
79
83
  WrapperModel: () => WrapperModel,
80
84
  WrapperSchema: () => WrapperSchema,
85
+ addPx: () => addPx,
81
86
  allSections: () => allSections,
87
+ base64Utf8: () => base64Utf8,
82
88
  buildGradientCSS: () => buildGradientCSS,
83
89
  cloneSectionSnapshot: () => cloneSectionSnapshot,
84
90
  createBlockRegistry: () => createBlockRegistry,
@@ -93,12 +99,17 @@ __export(index_exports, {
93
99
  createTemplate: () => createTemplate,
94
100
  createTemplateWithDefaultSection: () => createTemplateWithDefaultSection,
95
101
  createWrapper: () => createWrapper,
102
+ encodeEditorData: () => encodeEditorData,
96
103
  isLeafBlockType: () => isLeafBlockType,
97
104
  isTemplateMigrationError: () => isTemplateMigrationError,
98
105
  isWrapper: () => isWrapper,
99
106
  isWrapperInstance: () => isWrapperInstance,
100
107
  migrateTemplate: () => migrateTemplate,
101
108
  migrateV1_0ToV1_1: () => migrateV1_0ToV1_1,
109
+ mjmlOptions: () => mjmlOptions,
110
+ paddingWithGap: () => paddingWithGap,
111
+ stripEditorMarkup: () => stripEditorMarkup,
112
+ textInlineStyles: () => textInlineStyles,
102
113
  validateTemplate: () => validateTemplate,
103
114
  withTemplateId: () => withTemplateId
104
115
  });
@@ -2605,8 +2616,7 @@ var EditorUIStore = import_mobx_state_tree7.types.model("EditorUI", {
2605
2616
  previewDevice: import_mobx_state_tree7.types.optional(
2606
2617
  import_mobx_state_tree7.types.enumeration(["desktop", "mobile"]),
2607
2618
  "desktop"
2608
- ),
2609
- zoomLevel: import_mobx_state_tree7.types.optional(import_mobx_state_tree7.types.number, 1)
2619
+ )
2610
2620
  }).volatile(() => ({
2611
2621
  // === Transient State (not persisted) ===
2612
2622
  hoverBlockId: void 0,
@@ -2863,30 +2873,6 @@ var EditorUIStore = import_mobx_state_tree7.types.model("EditorUI", {
2863
2873
  */
2864
2874
  togglePreviewDevice() {
2865
2875
  self.previewDevice = self.previewDevice === "desktop" ? "mobile" : "desktop";
2866
- },
2867
- /**
2868
- * Set zoom level
2869
- */
2870
- setZoomLevel(zoom) {
2871
- self.zoomLevel = Math.max(0.25, Math.min(2, zoom));
2872
- },
2873
- /**
2874
- * Zoom in
2875
- */
2876
- zoomIn() {
2877
- self.zoomLevel = Math.min(2, self.zoomLevel + 0.1);
2878
- },
2879
- /**
2880
- * Zoom out
2881
- */
2882
- zoomOut() {
2883
- self.zoomLevel = Math.max(0.25, self.zoomLevel - 0.1);
2884
- },
2885
- /**
2886
- * Reset zoom to 100%
2887
- */
2888
- resetZoom() {
2889
- self.zoomLevel = 1;
2890
2876
  }
2891
2877
  })).views((self) => ({
2892
2878
  /**
@@ -2977,12 +2963,6 @@ var EditorUIStore = import_mobx_state_tree7.types.model("EditorUI", {
2977
2963
  */
2978
2964
  get isDesktopPreview() {
2979
2965
  return self.previewDevice === "desktop";
2980
- },
2981
- /**
2982
- * Get zoom percentage
2983
- */
2984
- get zoomPercentage() {
2985
- return Math.round(self.zoomLevel * 100);
2986
2966
  }
2987
2967
  }));
2988
2968
 
@@ -3216,6 +3196,860 @@ function createEmptyStore() {
3216
3196
  template: createTemplate()
3217
3197
  });
3218
3198
  }
3199
+
3200
+ // src/compiler/blockToRawHtml.ts
3201
+ function escapeAttr(s) {
3202
+ return s.replace(/"/g, "&quot;").replace(/</g, "&lt;");
3203
+ }
3204
+ function spacingToCss(p) {
3205
+ if (!p) return "";
3206
+ const t = p.top ?? "0", r = p.right ?? "0", b = p.bottom ?? "0", l = p.left ?? "0";
3207
+ return `padding:${t} ${r} ${b} ${l};`;
3208
+ }
3209
+ function identity(b) {
3210
+ return `class="el-${b.type} el-${b.id}"`;
3211
+ }
3212
+ function textToHtml(b) {
3213
+ const styles = [];
3214
+ if (b.color) styles.push(`color:${b.color}`);
3215
+ if (b.fontSize) styles.push(`font-size:${b.fontSize}`);
3216
+ if (b.fontFamily) styles.push(`font-family:${b.fontFamily}`);
3217
+ if (b.lineHeight) styles.push(`line-height:${b.lineHeight}`);
3218
+ if (b.align) styles.push(`text-align:${b.align}`);
3219
+ styles.push("margin:0");
3220
+ const padding = spacingToCss(b.padding);
3221
+ return `<div ${identity(b)} style="${styles.join(";")};${padding}">${b.content}</div>`;
3222
+ }
3223
+ function imageToHtml(b) {
3224
+ const align = b.align ?? "center";
3225
+ const wrap = `${identity(b)} style="text-align:${align};${spacingToCss(b.padding)}"`;
3226
+ const widthAttr = b.width ? ` width="${escapeAttr(b.width)}"` : "";
3227
+ const heightAttr = b.height ? ` height="${escapeAttr(b.height)}"` : "";
3228
+ const radius = b.borderRadius ? `border-radius:${b.borderRadius};` : "";
3229
+ const img = `<img src="${escapeAttr(b.src)}" alt="${escapeAttr(b.alt ?? "")}"${widthAttr}${heightAttr} style="display:block;max-width:100%;${radius}border:0;outline:none;text-decoration:none;" />`;
3230
+ const inner = b.href ? `<a href="${escapeAttr(b.href)}" target="_blank" rel="noopener" style="text-decoration:none;">${img}</a>` : img;
3231
+ return `<div ${wrap}>${inner}</div>`;
3232
+ }
3233
+ function buttonToHtml(b) {
3234
+ const align = b.align ?? "center";
3235
+ const bg = b.backgroundColor ?? "#000000";
3236
+ const fg = b.color ?? "#ffffff";
3237
+ const radius = b.borderRadius ?? "3px";
3238
+ const innerPadding = b.innerPadding ?? "10px 25px";
3239
+ const border = b.border ? `border:${b.border};` : "";
3240
+ return `<div ${identity(b)} style="text-align:${align};${spacingToCss(b.padding)}">
3241
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="border-collapse:separate;line-height:100%;display:inline-block;">
3242
+ <tr>
3243
+ <td align="center" valign="middle" role="presentation" style="background-color:${bg};border-radius:${radius};${border}padding:${innerPadding};">
3244
+ <a href="${escapeAttr(b.href)}" target="_blank" rel="noopener" style="display:inline-block;color:${fg};font-family:inherit;font-size:14px;font-weight:600;line-height:120%;text-decoration:none;text-transform:none;">${escapeAttr(b.label)}</a>
3245
+ </td>
3246
+ </tr>
3247
+ </table>
3248
+ </div>`;
3249
+ }
3250
+ function dividerToHtml(b) {
3251
+ const color = b.borderColor ?? "#cccccc";
3252
+ const w = b.borderWidth ?? "1px";
3253
+ const style = b.borderStyle ?? "solid";
3254
+ const width = b.width ?? "100%";
3255
+ return `<div ${identity(b)} style="${spacingToCss(b.padding)}">
3256
+ <div style="border-top:${w} ${style} ${color};width:${width};font-size:1px;line-height:1px;">&nbsp;</div>
3257
+ </div>`;
3258
+ }
3259
+ function spacerToHtml(b) {
3260
+ return `<div ${identity(b)} style="height:${b.height};line-height:${b.height};font-size:1px;">&nbsp;</div>`;
3261
+ }
3262
+ function socialToHtml(b) {
3263
+ const items = (b.links ?? []).map((link) => {
3264
+ const icon = link.icon ?? "";
3265
+ return `<a href="${escapeAttr(link.url)}" target="_blank" rel="noopener" style="display:inline-block;margin:0 4px;">
3266
+ <img src="${escapeAttr(icon)}" alt="${escapeAttr(link.platform)}" width="24" height="24" style="display:inline-block;border:0;" />
3267
+ </a>`;
3268
+ }).join("");
3269
+ return `<div ${identity(b)} style="text-align:center;">${items}</div>`;
3270
+ }
3271
+ function blockToRawHtml(block) {
3272
+ if (!isLeafBlockType(block.type)) {
3273
+ throw new Error(`blockToRawHtml only handles leaf blocks; got "${block.type}"`);
3274
+ }
3275
+ switch (block.type) {
3276
+ case "text" /* TEXT */:
3277
+ return textToHtml(block);
3278
+ case "image" /* IMAGE */:
3279
+ return imageToHtml(block);
3280
+ case "button" /* BUTTON */:
3281
+ return buttonToHtml(block);
3282
+ case "divider" /* DIVIDER */:
3283
+ return dividerToHtml(block);
3284
+ case "spacer" /* SPACER */:
3285
+ return spacerToHtml(block);
3286
+ case "social" /* SOCIAL */:
3287
+ return socialToHtml(block);
3288
+ default:
3289
+ throw new Error(`Unhandled leaf block type: ${block.type}`);
3290
+ }
3291
+ }
3292
+
3293
+ // src/compiler/MjmlBuilder.ts
3294
+ var DEFAULT_MJML_ATTRIBUTES = `
3295
+ <mj-all font-family="Georgia, serif" />
3296
+ <mj-text font-size="14px" line-height="1.6" />
3297
+ `;
3298
+ var EDITOR_DATA_ATTRIBUTE = {
3299
+ social: "data-ee-social",
3300
+ subColumns: "data-ee-subcols"
3301
+ };
3302
+ function base64Utf8(text) {
3303
+ const bytes = new TextEncoder().encode(text);
3304
+ let binary = "";
3305
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
3306
+ return btoa(binary);
3307
+ }
3308
+ var EDITOR_MARKER = { begin: "<!--ee:begin-->", end: "<!--ee:end-->" };
3309
+ function stripEditorMarkup(markup) {
3310
+ return markup.replace(/<!--ee:begin-->[\s\S]*?<!--ee:end-->/g, "").replace(/<!--\/?ee:raw:[^>]*-->/g, "");
3311
+ }
3312
+ function rawMarkers(id, html, editor) {
3313
+ return editor ? `<!--ee:raw:${id}-->${html}<!--/ee:raw:${id}-->` : html;
3314
+ }
3315
+ function textInlineStyles(block) {
3316
+ const styles = [];
3317
+ if (block.align) styles.push(`text-align:${block.align}`);
3318
+ if (block.color) styles.push(`color:${block.color}`);
3319
+ if (block.fontSize) styles.push(`font-size:${block.fontSize}`);
3320
+ if (block.fontFamily) styles.push(`font-family:${block.fontFamily}`);
3321
+ if (block.lineHeight) styles.push(`line-height:${block.lineHeight}`);
3322
+ return styles;
3323
+ }
3324
+ function encodeEditorData(value) {
3325
+ return base64Utf8(JSON.stringify(value));
3326
+ }
3327
+ function finishAttributes(attrs, classes, extra) {
3328
+ const out = [...attrs];
3329
+ const taken = new Set(attrs.map((a) => a.slice(0, a.indexOf("="))));
3330
+ const allClasses = [...classes];
3331
+ if (extra) {
3332
+ for (const [name, value] of Object.entries(extra)) {
3333
+ if (name === "css-class") {
3334
+ for (const c of value.replace(/"/g, "&quot;").split(/\s+/)) if (c && !allClasses.includes(c)) allClasses.push(c);
3335
+ continue;
3336
+ }
3337
+ if (taken.has(name)) continue;
3338
+ out.push(`${name}="${value.replace(/"/g, "&quot;")}"`);
3339
+ }
3340
+ }
3341
+ if (allClasses.length > 0) out.push(`css-class="${allClasses.join(" ")}"`);
3342
+ return out.join(" ");
3343
+ }
3344
+ function quoted(value) {
3345
+ return value.replace(/"/g, "&quot;");
3346
+ }
3347
+ function spacingToString(spacing) {
3348
+ if (!spacing) return "";
3349
+ const { top, right, bottom, left } = spacing;
3350
+ if (!top && !right && !bottom && !left) return "";
3351
+ return [top || "0", right || "0", bottom || "0", left || "0"].join(" ");
3352
+ }
3353
+ var MJML_SECTION_PADDING = { top: "20px", right: "0", bottom: "20px", left: "0" };
3354
+ function addPx(a, b) {
3355
+ const px = (v) => v === "0" ? 0 : /^-?\d+(\.\d+)?px$/.test(v) ? Number.parseFloat(v) : NaN;
3356
+ const sum = px(a) + px(b);
3357
+ if (Number.isNaN(sum)) return void 0;
3358
+ return `${Number.isInteger(sum) ? sum : Number(sum.toFixed(2))}px`;
3359
+ }
3360
+ function paddingWithGap(spacing, gap) {
3361
+ const base = spacing ?? MJML_SECTION_PADDING;
3362
+ const top = addPx(base.top || "0", gap);
3363
+ return top === void 0 ? base : { ...base, top };
3364
+ }
3365
+ function mjmlOptions(options = {}) {
3366
+ return {
3367
+ // Never resolve <mj-include>: its path is read from the server's disk,
3368
+ // and a stored document (a Raw block's html) must not reach local files.
3369
+ ignoreIncludes: true,
3370
+ validationLevel: "soft",
3371
+ minify: false,
3372
+ ...options.webFonts === false ? { fonts: {} } : {}
3373
+ };
3374
+ }
3375
+ var MjmlBuilder = class {
3376
+ constructor() {
3377
+ /**
3378
+ * Set true while emitting a column with sub-columns. Causes generateHead
3379
+ * to inject the responsive media query exactly once per template.
3380
+ */
3381
+ this.needsSubColumnStyles = false;
3382
+ /** Set for the duration of one build in editor mode; see {@link BuildOptions}. */
3383
+ this.editor = false;
3384
+ /**
3385
+ * Platform brand colors for social icons
3386
+ */
3387
+ this.SOCIAL_COLORS = {
3388
+ facebook: "#1877F2",
3389
+ twitter: "#000000",
3390
+ instagram: "#E4405F",
3391
+ linkedin: "#0A66C2",
3392
+ youtube: "#FF0000",
3393
+ pinterest: "#BD081C",
3394
+ github: "#181717"
3395
+ };
3396
+ }
3397
+ /** `fragment` fenced as an editor addition, or nothing outside editor mode. */
3398
+ ee(fragment) {
3399
+ return this.editor ? `${EDITOR_MARKER.begin}${fragment}${EDITOR_MARKER.end}` : "";
3400
+ }
3401
+ /** The label of a ghost: what the node is, so the person can find and unhide it. */
3402
+ ghostText(cssClasses, label) {
3403
+ return `<mj-text css-class="ee-ghost ${cssClasses.join(" ")}" padding="8px 25px" font-size="12px" line-height="1.4" color="#6b7280" container-background-color="#f3f4f6"><span style="display:block;border:1px dashed #9ca3af;padding:6px 10px;">${label} (hidden)</span></mj-text>`;
3404
+ }
3405
+ /**
3406
+ * The MJML markup for a document, without compiling it to HTML.
3407
+ */
3408
+ toMJML(template, options = {}) {
3409
+ this.needsSubColumnStyles = false;
3410
+ this.editor = options.editor === true;
3411
+ try {
3412
+ return this.templateToMJML(template);
3413
+ } finally {
3414
+ this.editor = false;
3415
+ }
3416
+ }
3417
+ /**
3418
+ * Convert template to MJML markup
3419
+ */
3420
+ templateToMJML(template) {
3421
+ const { metadata, sections } = template;
3422
+ const body = sections.map((item) => isWrapper(item) ? this.wrapperToMJML(item) : this.sectionToMJML(item)).join("\n");
3423
+ const gradientCSS = this.collectGradientStyles(sections);
3424
+ const head = this.generateHead(metadata, gradientCSS);
3425
+ const bodyAttributes = finishAttributes([], [], metadata.mjmlHead?.bodyAttributes);
3426
+ return `
3427
+ <mjml>
3428
+ ${head}
3429
+ <mj-body${bodyAttributes ? ` ${bodyAttributes}` : ""}>
3430
+ ${body}
3431
+ </mj-body>
3432
+ </mjml>
3433
+ `.trim();
3434
+ }
3435
+ /**
3436
+ * Generate MJML head section with all head components
3437
+ * Supports: mj-title, mj-preview, mj-font, mj-breakpoint, mj-style
3438
+ */
3439
+ generateHead(metadata, gradientCSS) {
3440
+ const parts = ["<mj-head>"];
3441
+ if (metadata.title) {
3442
+ parts.push(`<mj-title>${metadata.title}</mj-title>`);
3443
+ }
3444
+ if (metadata.previewText || metadata.subject) {
3445
+ parts.push(`<mj-preview>${metadata.previewText || metadata.subject || ""}</mj-preview>`);
3446
+ }
3447
+ if (metadata.fonts && metadata.fonts.length > 0) {
3448
+ for (const font of metadata.fonts) {
3449
+ parts.push(`<mj-font name="${font.name}" href="${font.href}" />`);
3450
+ }
3451
+ }
3452
+ if (metadata.breakpoint) {
3453
+ parts.push(`<mj-breakpoint width="${metadata.breakpoint}" />`);
3454
+ }
3455
+ const mjmlHead = metadata.mjmlHead;
3456
+ const attributes = mjmlHead?.attributes ?? DEFAULT_MJML_ATTRIBUTES;
3457
+ if (attributes.trim()) parts.push(`<mj-attributes>${attributes}</mj-attributes>`);
3458
+ if (mjmlHead?.headRaw) parts.push(mjmlHead.headRaw);
3459
+ if (metadata.customCSS) {
3460
+ parts.push(`<mj-style>${metadata.customCSS}</mj-style>`);
3461
+ }
3462
+ if (metadata.inlineCSS) {
3463
+ parts.push(`<mj-style inline="inline">${metadata.inlineCSS}</mj-style>`);
3464
+ }
3465
+ if (gradientCSS) {
3466
+ parts.push(`<mj-style>${gradientCSS}</mj-style>`);
3467
+ }
3468
+ if (this.needsSubColumnStyles) {
3469
+ parts.push(`<mj-style>
3470
+ @media only screen and (max-width:480px) {
3471
+ table.ee-sub-cols td.ee-sub-col {
3472
+ display: block !important;
3473
+ width: 100% !important;
3474
+ padding-bottom: 12px !important;
3475
+ }
3476
+ table.ee-sub-cols td.ee-sub-col:last-child {
3477
+ padding-bottom: 0 !important;
3478
+ }
3479
+ }
3480
+ </mj-style>`);
3481
+ }
3482
+ parts.push("</mj-head>");
3483
+ return parts.join("\n");
3484
+ }
3485
+ /**
3486
+ * Collect background-image CSS rules for all wrappers, sections and columns that have gradients.
3487
+ * Returns a string of CSS rules to inject as a single mj-style block.
3488
+ */
3489
+ collectGradientStyles(items) {
3490
+ const rules = [];
3491
+ for (const item of items) {
3492
+ if (isWrapper(item) && item.backgroundGradient) {
3493
+ const css = buildGradientCSS(item.backgroundGradient);
3494
+ if (css) rules.push(`.el-grad-${item.id} { background-image: ${css}; }`);
3495
+ }
3496
+ }
3497
+ for (const section of allSections(items)) {
3498
+ if (section.backgroundGradient) {
3499
+ const css = buildGradientCSS(section.backgroundGradient);
3500
+ if (css) {
3501
+ rules.push(`.el-grad-${section.id} { background-image: ${css}; }`);
3502
+ }
3503
+ }
3504
+ for (const column of section.columns) {
3505
+ if (column.backgroundGradient) {
3506
+ const css = buildGradientCSS(column.backgroundGradient);
3507
+ if (css) {
3508
+ rules.push(`.el-grad-${column.id} { background-image: ${css}; }`);
3509
+ }
3510
+ }
3511
+ }
3512
+ }
3513
+ return rules.join("\n");
3514
+ }
3515
+ /**
3516
+ * Convert a wrapper to an `<mj-wrapper>` around its sections. The wrapper's
3517
+ * attributes are MJML's own for `mj-wrapper`; the sections inside follow
3518
+ * MJML's rules there (a full-width section inside a full-width wrapper
3519
+ * renders at standard width, which the inspector explains).
3520
+ */
3521
+ wrapperToMJML(wrapper) {
3522
+ if (wrapper.hidden) {
3523
+ return this.ee(`<mj-wrapper css-class="ee-ghost el-wrapper el-${wrapper.id}" padding="0"><mj-section padding="0"><mj-column>${this.ghostText([], "Container")}</mj-column></mj-section></mj-wrapper>`);
3524
+ }
3525
+ const attrs = [];
3526
+ const cssClasses = ["el-wrapper", `el-${wrapper.id}`];
3527
+ this.pushBackground(attrs, cssClasses, wrapper);
3528
+ if (wrapper.border) attrs.push(`border="${quoted(wrapper.border)}"`);
3529
+ if (wrapper.borderTop) attrs.push(`border-top="${quoted(wrapper.borderTop)}"`);
3530
+ if (wrapper.borderRight) attrs.push(`border-right="${quoted(wrapper.borderRight)}"`);
3531
+ if (wrapper.borderBottom) attrs.push(`border-bottom="${quoted(wrapper.borderBottom)}"`);
3532
+ if (wrapper.borderLeft) attrs.push(`border-left="${quoted(wrapper.borderLeft)}"`);
3533
+ if (wrapper.borderRadius) attrs.push(`border-radius="${quoted(wrapper.borderRadius)}"`);
3534
+ if (wrapper.fullWidth) attrs.push('full-width="full-width"');
3535
+ const padding = spacingToString(wrapper.padding);
3536
+ if (padding) attrs.push(`padding="${quoted(padding)}"`);
3537
+ if (wrapper.gap) attrs.push(`gap="${quoted(wrapper.gap)}"`);
3538
+ if (wrapper.textAlign) attrs.push(`text-align="${quoted(wrapper.textAlign)}"`);
3539
+ for (const c of (wrapper.cssClass ?? "").split(/\s+/)) if (c && !cssClasses.includes(quoted(c))) cssClasses.push(quoted(c));
3540
+ let first = true;
3541
+ const sections = wrapper.sections.map((section) => {
3542
+ if (section.hidden) return this.sectionToMJML(section);
3543
+ const out = this.sectionToMJML(section, first ? void 0 : wrapper.gap);
3544
+ first = false;
3545
+ return out;
3546
+ }).filter(Boolean).join("\n");
3547
+ return `
3548
+ <mj-wrapper ${finishAttributes(attrs, cssClasses, wrapper.extraAttributes)}>
3549
+ ${sections}
3550
+ </mj-wrapper>
3551
+ `.trim();
3552
+ }
3553
+ /** Background attributes shared by sections and wrappers: a gradient (with its Outlook fallback colour), or colour and image. */
3554
+ pushBackground(attrs, cssClasses, node) {
3555
+ if (node.backgroundGradient) {
3556
+ const fallbackColor = node.backgroundGradient.stops[0]?.color;
3557
+ if (fallbackColor) attrs.push(`background-color="${quoted(fallbackColor)}"`);
3558
+ cssClasses.push(`el-grad-${node.id}`);
3559
+ return;
3560
+ }
3561
+ if (node.backgroundColor) attrs.push(`background-color="${quoted(node.backgroundColor)}"`);
3562
+ if (node.backgroundImage) attrs.push(`background-url="${quoted(node.backgroundImage)}"`);
3563
+ if (node.backgroundPosition) attrs.push(`background-position="${quoted(node.backgroundPosition)}"`);
3564
+ if (node.backgroundRepeat) attrs.push(`background-repeat="${quoted(node.backgroundRepeat)}"`);
3565
+ if (node.backgroundSize) attrs.push(`background-size="${quoted(node.backgroundSize)}"`);
3566
+ }
3567
+ /**
3568
+ * Convert section to MJML
3569
+ * Supports background images, full-width, and mj-group for non-stacking columns
3570
+ */
3571
+ sectionToMJML(section, gapAbove) {
3572
+ if (section.hidden) {
3573
+ return this.ee(`<mj-section css-class="ee-ghost el-section el-${section.id}" padding="0"><mj-column>${this.ghostText([], "Section")}</mj-column></mj-section>`);
3574
+ }
3575
+ if (section.bodyRaw) {
3576
+ const blocks = section.columns.flatMap((c) => c.blocks).filter((b) => !b.hidden);
3577
+ if (blocks.every((b) => b.type === "raw") && section.columns.every((c) => !c.subColumns?.length)) {
3578
+ return blocks.map((b) => `<mj-raw>${rawMarkers(b.id, b.html, this.editor)}</mj-raw>`).join("\n");
3579
+ }
3580
+ }
3581
+ const attrs = [];
3582
+ const cssClasses = ["el-section", `el-${section.id}`];
3583
+ this.pushBackground(attrs, cssClasses, section);
3584
+ if (section.fullWidth) {
3585
+ attrs.push('full-width="full-width"');
3586
+ }
3587
+ const sectionPadding = gapAbove ? paddingWithGap(section.padding, gapAbove) : section.padding;
3588
+ if (sectionPadding) {
3589
+ const padding = spacingToString(sectionPadding);
3590
+ if (padding) attrs.push(`padding="${padding}"`);
3591
+ }
3592
+ const columns = section.columns.map((col) => this.columnToMJML(col)).join("\n");
3593
+ const sectionAttrs = finishAttributes(attrs, cssClasses, section.extraAttributes);
3594
+ if (section.noStack && section.columns.length > 1) {
3595
+ return `
3596
+ <mj-section ${sectionAttrs}>
3597
+ <mj-group>
3598
+ ${columns}
3599
+ </mj-group>
3600
+ </mj-section>
3601
+ `.trim();
3602
+ }
3603
+ return `
3604
+ <mj-section ${sectionAttrs}>
3605
+ ${columns}
3606
+ </mj-section>
3607
+ `.trim();
3608
+ }
3609
+ /**
3610
+ * Convert column to MJML
3611
+ */
3612
+ columnToMJML(column) {
3613
+ if (column.hidden) return "";
3614
+ if (column.subColumns && column.subColumns.length > 0) {
3615
+ return this.subColumnsToMJML(column);
3616
+ }
3617
+ const cssClasses = ["el-column", `el-${column.id}`];
3618
+ const attrs = [];
3619
+ if (column.width) {
3620
+ attrs.push(`width="${column.width}%"`);
3621
+ }
3622
+ if (column.backgroundGradient) {
3623
+ const fallbackColor = column.backgroundGradient.stops[0]?.color;
3624
+ if (fallbackColor) attrs.push(`background-color="${fallbackColor}"`);
3625
+ cssClasses.push(`el-grad-${column.id}`);
3626
+ } else {
3627
+ if (column.backgroundColor) {
3628
+ attrs.push(`background-color="${column.backgroundColor}"`);
3629
+ }
3630
+ }
3631
+ if (column.verticalAlign) {
3632
+ attrs.push(`vertical-align="${column.verticalAlign}"`);
3633
+ }
3634
+ if (column.padding) {
3635
+ const padding = spacingToString(column.padding);
3636
+ if (padding) attrs.push(`padding="${padding}"`);
3637
+ }
3638
+ const blocks = column.blocks.map((block) => this.blockToMJML(block)).join("\n");
3639
+ const floor = column.blocks.length === 0 ? this.ee('<mj-spacer height="48px" css-class="ee-ghost ee-empty" />') : "";
3640
+ return `
3641
+ <mj-column ${finishAttributes(attrs, cssClasses, column.extraAttributes)}>
3642
+ ${blocks}${floor}
3643
+ </mj-column>
3644
+ `.trim();
3645
+ }
3646
+ /**
3647
+ * Emit a group column as an mj-column wrapping a hand-built nested
3648
+ * table inside an mj-raw island. The parent mj-section structure
3649
+ * stays standard MJML; only the nested area escapes MJML's parser.
3650
+ *
3651
+ * The responsive media query (table.ee-sub-cols td.ee-sub-col) is
3652
+ * injected once at document head via generateHead when the
3653
+ * needsSubColumnStyles flag is set.
3654
+ */
3655
+ subColumnsToMJML(column) {
3656
+ this.needsSubColumnStyles = true;
3657
+ const subs = column.subColumns ?? [];
3658
+ const sumRaw = subs.reduce((a, s) => a + (s.width || 0), 0) || 100;
3659
+ const widths = subs.map((s) => Math.round((s.width || 0) / sumRaw * 1e4) / 100);
3660
+ const cells = subs.map((sc, i) => {
3661
+ const inner = (sc.blocks ?? []).map((b) => blockToRawHtml(b)).join("\n");
3662
+ const valign = sc.verticalAlign ?? "top";
3663
+ const hasPadding = sc.paddingTop || sc.paddingRight || sc.paddingBottom || sc.paddingLeft;
3664
+ const padding = hasPadding ? `padding:${sc.paddingTop || "0"} ${sc.paddingRight || "0"} ${sc.paddingBottom || "0"} ${sc.paddingLeft || "0"};` : "padding:10px;";
3665
+ const bg = sc.backgroundColor ? `background-color:${sc.backgroundColor};` : "";
3666
+ return `<td class="ee-sub-col el-subcolumn el-${sc.id}" width="${widths[i]}%" valign="${valign}" style="${padding}${bg}">${inner}</td>`;
3667
+ }).join("");
3668
+ const colAttrs = [];
3669
+ if (column.width) colAttrs.push(`width="${column.width}%"`);
3670
+ if (column.backgroundColor) colAttrs.push(`background-color="${column.backgroundColor}"`);
3671
+ if (column.verticalAlign) colAttrs.push(`vertical-align="${column.verticalAlign}"`);
3672
+ if (column.padding) {
3673
+ const padding = spacingToString(column.padding);
3674
+ if (padding) colAttrs.push(`padding="${padding}"`);
3675
+ }
3676
+ return `
3677
+ <mj-column ${finishAttributes(colAttrs, ["el-column", `el-${column.id}`], column.extraAttributes)}>
3678
+ <mj-raw>
3679
+ <table role="presentation" class="ee-sub-cols" ${EDITOR_DATA_ATTRIBUTE.subColumns}="${encodeEditorData(subs)}" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
3680
+ <tr>${cells}</tr>
3681
+ </table>
3682
+ </mj-raw>
3683
+ </mj-column>
3684
+ `.trim();
3685
+ }
3686
+ /**
3687
+ * Convert block to MJML based on type
3688
+ */
3689
+ blockToMJML(block) {
3690
+ if (block.hidden) return this.ee(this.ghostText([`el-${block.type}`, `el-${block.id}`], block.type.charAt(0).toUpperCase() + block.type.slice(1)));
3691
+ switch (block.type) {
3692
+ case "text":
3693
+ return this.textBlockToMJML(block);
3694
+ case "image":
3695
+ return this.imageBlockToMJML(block);
3696
+ case "button":
3697
+ return this.buttonBlockToMJML(block);
3698
+ case "divider":
3699
+ return this.dividerBlockToMJML(block);
3700
+ case "spacer":
3701
+ return this.spacerBlockToMJML(block);
3702
+ case "social":
3703
+ return this.socialBlockToMJML(block);
3704
+ case "hero":
3705
+ return this.heroBlockToMJML(block);
3706
+ case "accordion":
3707
+ return this.accordionBlockToMJML(block);
3708
+ case "raw":
3709
+ return this.rawBlockToMJML(block);
3710
+ case "navbar":
3711
+ return this.navbarBlockToMJML(block);
3712
+ case "carousel":
3713
+ return this.carouselBlockToMJML(block);
3714
+ case "table":
3715
+ return this.tableBlockToMJML(block);
3716
+ case "header":
3717
+ return this.headerBlockToMJML();
3718
+ case "footer":
3719
+ return this.footerBlockToMJML();
3720
+ default:
3721
+ console.warn(`Unknown block type: ${block.type}`);
3722
+ return "";
3723
+ }
3724
+ }
3725
+ /**
3726
+ * Convert text block to MJML
3727
+ *
3728
+ * Note: MJML applies styles to the container <td>, but TipTap content
3729
+ * (wrapped in <p> tags) doesn't inherit these styles. We solve this by
3730
+ * wrapping the content in a <div> with explicit inline styles.
3731
+ */
3732
+ textBlockToMJML(block) {
3733
+ const attrs = [];
3734
+ const inlineStyles = textInlineStyles(block);
3735
+ if (block.align) attrs.push(`align="${block.align}"`);
3736
+ if (block.color) attrs.push(`color="${block.color}"`);
3737
+ if (block.fontSize) attrs.push(`font-size="${block.fontSize}"`);
3738
+ if (block.fontFamily) attrs.push(`font-family="${block.fontFamily}"`);
3739
+ if (block.lineHeight) attrs.push(`line-height="${block.lineHeight}"`);
3740
+ if (block.padding) {
3741
+ const padding = spacingToString(block.padding);
3742
+ if (padding) attrs.push(`padding="${padding}"`);
3743
+ }
3744
+ const styledContent = inlineStyles.length > 0 ? `<div style="${inlineStyles.join(";")}">${block.content}</div>` : block.content;
3745
+ return `<mj-text ${finishAttributes(attrs, ["el-text", `el-${block.id}`], block.extraAttributes)}>${styledContent}</mj-text>`;
3746
+ }
3747
+ /**
3748
+ * Convert image block to MJML
3749
+ */
3750
+ imageBlockToMJML(block) {
3751
+ const attrs = [`src="${block.src}"`];
3752
+ if (block.alt) attrs.push(`alt="${block.alt}"`);
3753
+ if (block.width) attrs.push(`width="${block.width}"`);
3754
+ if (block.height) attrs.push(`height="${block.height}"`);
3755
+ if (block.align) attrs.push(`align="${block.align}"`);
3756
+ if (block.href) attrs.push(`href="${block.href}"`);
3757
+ if (block.borderRadius) attrs.push(`border-radius="${block.borderRadius}"`);
3758
+ if (block.padding) {
3759
+ const padding = spacingToString(block.padding);
3760
+ if (padding) attrs.push(`padding="${padding}"`);
3761
+ }
3762
+ return `<mj-image ${finishAttributes(attrs, ["el-image", `el-${block.id}`], block.extraAttributes)} />`;
3763
+ }
3764
+ /**
3765
+ * Convert button block to MJML
3766
+ */
3767
+ buttonBlockToMJML(block) {
3768
+ const attrs = [`href="${block.href}"`];
3769
+ if (block.align) attrs.push(`align="${block.align}"`);
3770
+ if (block.backgroundColor) attrs.push(`background-color="${block.backgroundColor}"`);
3771
+ if (block.color) attrs.push(`color="${block.color}"`);
3772
+ if (block.borderRadius) attrs.push(`border-radius="${block.borderRadius}"`);
3773
+ if (block.border) attrs.push(`border="${block.border}"`);
3774
+ if (block.innerPadding) attrs.push(`inner-padding="${block.innerPadding}"`);
3775
+ if (block.padding) {
3776
+ const padding = spacingToString(block.padding);
3777
+ if (padding) attrs.push(`padding="${padding}"`);
3778
+ }
3779
+ return `<mj-button ${finishAttributes(attrs, ["el-button", `el-${block.id}`], block.extraAttributes)}>${block.label}</mj-button>`;
3780
+ }
3781
+ /**
3782
+ * Convert divider block to MJML
3783
+ */
3784
+ dividerBlockToMJML(block) {
3785
+ const attrs = [];
3786
+ if (block.borderColor) attrs.push(`border-color="${block.borderColor}"`);
3787
+ if (block.borderWidth) attrs.push(`border-width="${block.borderWidth}"`);
3788
+ if (block.borderStyle) attrs.push(`border-style="${block.borderStyle}"`);
3789
+ if (block.width) attrs.push(`width="${block.width}"`);
3790
+ if (block.padding) {
3791
+ const padding = spacingToString(block.padding);
3792
+ if (padding) attrs.push(`padding="${padding}"`);
3793
+ }
3794
+ return `<mj-divider ${finishAttributes(attrs, ["el-divider", `el-${block.id}`], block.extraAttributes)} />`;
3795
+ }
3796
+ /**
3797
+ * Convert spacer block to MJML
3798
+ */
3799
+ spacerBlockToMJML(block) {
3800
+ return `<mj-spacer ${finishAttributes([`height="${block.height}"`], ["el-spacer", `el-${block.id}`], block.extraAttributes)} />`;
3801
+ }
3802
+ /**
3803
+ * Get white SVG icon as data URI for a platform
3804
+ * Uses clean, minimal SVGs optimized for email
3805
+ */
3806
+ getSocialIconDataUri(platform) {
3807
+ const svgIcons = {
3808
+ facebook: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>',
3809
+ twitter: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/></svg>',
3810
+ instagram: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 0C8.74 0 8.333.015 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.74 0 12s.015 3.667.072 4.947c.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.74 24 12 24s3.667-.015 4.947-.072c4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>',
3811
+ linkedin: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>',
3812
+ youtube: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>',
3813
+ pinterest: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 0C5.373 0 0 5.372 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738a.36.36 0 01.083.345c-.091.378-.293 1.194-.333 1.361-.052.218-.174.265-.402.159-1.495-.696-2.428-2.882-2.428-4.64 0-3.779 2.744-7.253 7.917-7.253 4.158 0 7.389 2.963 7.389 6.923 0 4.13-2.607 7.461-6.229 7.461-1.217 0-2.36-.632-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0z"/></svg>',
3814
+ github: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'
3815
+ };
3816
+ const svg = svgIcons[platform] || svgIcons.facebook;
3817
+ return `data:image/svg+xml;base64,${base64Utf8(svg)}`;
3818
+ }
3819
+ /**
3820
+ * Convert social block to MJML using raw HTML table for reliable horizontal layout
3821
+ * Uses styled anchor tags with background colors for full width control
3822
+ */
3823
+ socialBlockToMJML(block) {
3824
+ const iconSize = block.iconSize || "40px";
3825
+ const iconPadding = block.iconPadding || "8px";
3826
+ const borderRadius = block.borderRadius || "999px";
3827
+ const align = block.align || "center";
3828
+ const isVertical = block.mode === "vertical";
3829
+ const sizeNum = parseInt(iconSize, 10) || 40;
3830
+ const innerIconSize = Math.round(sizeNum * 0.5);
3831
+ const paddingNum = parseInt(iconPadding, 10) || 8;
3832
+ const getLinkColor = (link) => {
3833
+ return link.color || this.SOCIAL_COLORS[link.platform] || "#666666";
3834
+ };
3835
+ if (isVertical) {
3836
+ const verticalButtons = block.links.map((link) => {
3837
+ const bgColor = getLinkColor(link);
3838
+ const iconUrl = this.getSocialIconDataUri(link.platform);
3839
+ return `<mj-button
3840
+ href="${link.url}"
3841
+ background-color="${bgColor}"
3842
+ border-radius="${borderRadius}"
3843
+ width="${iconSize}"
3844
+ height="${iconSize}"
3845
+ padding="${iconPadding} 0"
3846
+ inner-padding="0"
3847
+ align="${align}"
3848
+ css-class="el-social-btn el-${block.id}-${link.platform}"
3849
+ ><img src="${iconUrl}" alt="${link.platform}" width="${innerIconSize}" height="${innerIconSize}" style="display:block;margin:auto;" /></mj-button>`;
3850
+ }).join("\n");
3851
+ return `<!-- Social Icons (vertical) -->
3852
+ ${verticalButtons}`;
3853
+ }
3854
+ const alignMargin = align === "center" ? "0 auto" : align === "right" ? "0 0 0 auto" : "0 auto 0 0";
3855
+ const socialCells = block.links.map((link, index) => {
3856
+ const bgColor = getLinkColor(link);
3857
+ const iconUrl = this.getSocialIconDataUri(link.platform);
3858
+ const isLast = index === block.links.length - 1;
3859
+ const cellPadding = isLast ? "0" : `0 ${paddingNum}px 0 0`;
3860
+ const paddingForCentering = Math.round((sizeNum - innerIconSize) / 2);
3861
+ return `<td style="padding: ${cellPadding};">
3862
+ <a href="${link.url}" target="_blank" style="display: block; width: ${iconSize}; height: ${iconSize}; background-color: ${bgColor}; border-radius: ${borderRadius}; text-decoration: none;">
3863
+ <img src="${iconUrl}" alt="${link.platform}" width="${innerIconSize}" height="${innerIconSize}" style="display: block; margin: ${paddingForCentering}px auto 0 auto;" />
3864
+ </a>
3865
+ </td>`;
3866
+ }).join("\n ");
3867
+ return `<!-- Social Icons (horizontal) -->
3868
+ <mj-raw>
3869
+ <table align="${align}" role="presentation" cellpadding="0" cellspacing="0" style="margin: ${alignMargin};" ${EDITOR_DATA_ATTRIBUTE.social}="${encodeEditorData(block)}">
3870
+ <tr>
3871
+ ${socialCells}
3872
+ </tr>
3873
+ </table>
3874
+ </mj-raw>`;
3875
+ }
3876
+ /**
3877
+ * Convert hero block to MJML
3878
+ */
3879
+ heroBlockToMJML(block) {
3880
+ const attrs = [`background-url="${block.backgroundImage}"`];
3881
+ if (block.backgroundHeight) attrs.push(`background-height="${block.backgroundHeight}"`);
3882
+ if (block.backgroundWidth) attrs.push(`background-width="${block.backgroundWidth}"`);
3883
+ if (block.backgroundColor) attrs.push(`background-color="${block.backgroundColor}"`);
3884
+ if (block.verticalAlign) attrs.push(`vertical-align="${block.verticalAlign}"`);
3885
+ if (block.mode) attrs.push(`mode="${block.mode}"`);
3886
+ return `
3887
+ <mj-hero ${finishAttributes(attrs, ["el-hero", `el-${block.id}`], block.extraAttributes)}>
3888
+ <mj-text align="center" color="#ffffff" font-size="32px" font-weight="bold">
3889
+ Hero Title
3890
+ </mj-text>
3891
+ <mj-text align="center" color="#ffffff" font-size="16px">
3892
+ Add your hero content here
3893
+ </mj-text>
3894
+ <mj-button href="#" background-color="#ffffff" color="#944923">
3895
+ Call to Action
3896
+ </mj-button>
3897
+ </mj-hero>
3898
+ `.trim();
3899
+ }
3900
+ /**
3901
+ * Convert accordion block to MJML
3902
+ */
3903
+ accordionBlockToMJML(block) {
3904
+ const attrs = [];
3905
+ if (block.iconPosition) attrs.push(`icon-position="${block.iconPosition}"`);
3906
+ if (block.borderColor) attrs.push(`border="${block.borderColor}"`);
3907
+ if (block.fontFamily) attrs.push(`font-family="${block.fontFamily}"`);
3908
+ const items = block.items.map(
3909
+ (item) => `
3910
+ <mj-accordion-element>
3911
+ <mj-accordion-title>${item.title}</mj-accordion-title>
3912
+ <mj-accordion-text>${item.content}</mj-accordion-text>
3913
+ </mj-accordion-element>`
3914
+ ).join("\n");
3915
+ return `
3916
+ <mj-accordion ${finishAttributes(attrs, ["el-accordion", `el-${block.id}`], block.extraAttributes)}>
3917
+ ${items}
3918
+ </mj-accordion>
3919
+ `.trim();
3920
+ }
3921
+ /**
3922
+ * Convert raw HTML block to MJML
3923
+ */
3924
+ rawBlockToMJML(block) {
3925
+ return `<mj-raw css-class="el-raw el-${block.id}">${rawMarkers(block.id, block.html, this.editor)}</mj-raw>`;
3926
+ }
3927
+ /**
3928
+ * Convert navbar block to MJML
3929
+ */
3930
+ navbarBlockToMJML(block) {
3931
+ const attrs = [];
3932
+ if (block.hamburger) attrs.push('hamburger="hamburger"');
3933
+ if (block.baseUrl) attrs.push(`base-url="${block.baseUrl}"`);
3934
+ if (block.align) attrs.push(`align="${block.align}"`);
3935
+ if (block.icoColor) attrs.push(`ico-color="${block.icoColor}"`);
3936
+ if (block.padding) {
3937
+ const padding = spacingToString(block.padding);
3938
+ if (padding) attrs.push(`padding="${padding}"`);
3939
+ }
3940
+ const links = block.links.map((link) => {
3941
+ const linkAttrs = [`href="${link.href}"`];
3942
+ if (link.color) linkAttrs.push(`color="${link.color}"`);
3943
+ return ` <mj-navbar-link ${linkAttrs.join(" ")}>${link.label}</mj-navbar-link>`;
3944
+ }).join("\n");
3945
+ return `
3946
+ <mj-navbar ${finishAttributes(attrs, ["el-navbar", `el-${block.id}`], block.extraAttributes)}>
3947
+ ${links}
3948
+ </mj-navbar>
3949
+ `.trim();
3950
+ }
3951
+ /**
3952
+ * Convert carousel block to MJML
3953
+ */
3954
+ carouselBlockToMJML(block) {
3955
+ const attrs = [];
3956
+ if (block.thumbnails) attrs.push(`thumbnails="${block.thumbnails}"`);
3957
+ if (block.borderRadius) attrs.push(`border-radius="${block.borderRadius}"`);
3958
+ if (block.iconWidth) attrs.push(`icon-width="${block.iconWidth}"`);
3959
+ if (block.tbBorderRadius) attrs.push(`tb-border-radius="${block.tbBorderRadius}"`);
3960
+ if (block.padding) {
3961
+ const padding = spacingToString(block.padding);
3962
+ if (padding) attrs.push(`padding="${padding}"`);
3963
+ }
3964
+ const images = block.images.map((img) => {
3965
+ const imgAttrs = [`src="${img.src}"`];
3966
+ if (img.alt) imgAttrs.push(`alt="${img.alt}"`);
3967
+ if (img.href) imgAttrs.push(`href="${img.href}"`);
3968
+ if (img.thumbnailSrc) imgAttrs.push(`thumbnails-src="${img.thumbnailSrc}"`);
3969
+ return ` <mj-carousel-image ${imgAttrs.join(" ")} />`;
3970
+ }).join("\n");
3971
+ return `
3972
+ <mj-carousel ${finishAttributes(attrs, ["el-carousel", `el-${block.id}`], block.extraAttributes)}>
3973
+ ${images}
3974
+ </mj-carousel>
3975
+ `.trim();
3976
+ }
3977
+ /**
3978
+ * Convert table block to MJML
3979
+ */
3980
+ tableBlockToMJML(block) {
3981
+ const attrs = [];
3982
+ if (block.align) attrs.push(`align="${block.align}"`);
3983
+ if (block.color) attrs.push(`color="${block.color}"`);
3984
+ if (block.fontFamily) attrs.push(`font-family="${block.fontFamily}"`);
3985
+ if (block.fontSize) attrs.push(`font-size="${block.fontSize}"`);
3986
+ if (block.cellpadding) attrs.push(`cellpadding="${block.cellpadding}"`);
3987
+ if (block.cellspacing) attrs.push(`cellspacing="${block.cellspacing}"`);
3988
+ if (block.border) attrs.push(`border="${block.border}"`);
3989
+ if (block.padding) {
3990
+ const padding = spacingToString(block.padding);
3991
+ if (padding) attrs.push(`padding="${padding}"`);
3992
+ }
3993
+ const headerCells = block.headers.map((h) => `<th style="padding: 8px; border-bottom: 1px solid #ddd; text-align: left;">${h}</th>`).join("");
3994
+ const headerRow = `<tr style="background-color: #f5f5f5;">${headerCells}</tr>`;
3995
+ const dataRows = block.rows.map((row) => {
3996
+ const cells = row.map((cell) => `<td style="padding: 8px; border-bottom: 1px solid #eee;">${cell}</td>`).join("");
3997
+ return `<tr>${cells}</tr>`;
3998
+ }).join("\n");
3999
+ return `
4000
+ <mj-table ${finishAttributes(attrs, ["el-table", `el-${block.id}`], block.extraAttributes)}>
4001
+ ${headerRow}
4002
+ ${dataRows}
4003
+ </mj-table>
4004
+ `.trim();
4005
+ }
4006
+ /**
4007
+ * Generate the locked header block.
4008
+ *
4009
+ * It used to render one client's company name and a logo from an image host;
4010
+ * both are gone. What is left is a placeholder image slot. Keep this in step
4011
+ * with `headerBlockDefinition` in `packages/blocks/src/branded/index.ts`:
4012
+ * which of the two runs depends on how the registry is configured.
4013
+ *
4014
+ * The placeholder is a `data:` URI rather than an address on an image host,
4015
+ * so a workspace whose `asset_policy` is `service_only` can still send a mail
4016
+ * that contains this block.
4017
+ */
4018
+ headerBlockToMJML() {
4019
+ return `
4020
+ <mj-wrapper background-color="#ffffff" padding="20px" css-class="el-header-wrapper">
4021
+ <mj-section css-class="el-header-section">
4022
+ <mj-column css-class="el-header-column">
4023
+ <!-- Nothing: see headerBlockDefinition in packages/blocks. -->
4024
+ <mj-spacer height="1px" css-class="el-header-spacer" />
4025
+ </mj-column>
4026
+ </mj-section>
4027
+ </mj-wrapper>
4028
+ `.trim();
4029
+ }
4030
+ /**
4031
+ * Generate the locked footer block: the unsubscribe link and nothing else.
4032
+ *
4033
+ * It used to carry a copyright line naming one client's company. A locked
4034
+ * block carries no props, so whoever sends the mail cannot correct a name
4035
+ * that is not theirs; the line is gone rather than replaced with a fake one.
4036
+ * Keep this in step with `footerBlockDefinition` in
4037
+ * `packages/blocks/src/branded/index.ts`.
4038
+ */
4039
+ footerBlockToMJML() {
4040
+ return `
4041
+ <mj-wrapper background-color="#f5f5f5" padding="20px" css-class="el-footer-wrapper">
4042
+ <mj-section css-class="el-footer-section">
4043
+ <mj-column css-class="el-footer-column">
4044
+ <mj-text align="center" font-size="12px" color="#666666" css-class="el-footer-text-2">
4045
+ <a href="{{unsubscribe_url}}" style="color: #374151; text-decoration: underline;">Unsubscribe</a>
4046
+ </mj-text>
4047
+ </mj-column>
4048
+ </mj-section>
4049
+ </mj-wrapper>
4050
+ `.trim();
4051
+ }
4052
+ };
3219
4053
  // Annotate the CommonJS export names for ESM import in node:
3220
4054
  0 && (module.exports = {
3221
4055
  AccordionBlockSchema,
@@ -3233,8 +4067,11 @@ function createEmptyStore() {
3233
4067
  ColumnModel,
3234
4068
  ColumnSchema,
3235
4069
  CustomFontSchema,
4070
+ DEFAULT_MJML_ATTRIBUTES,
3236
4071
  DEFAULT_ON_CHANGE_DEBOUNCE_MS,
3237
4072
  DividerBlockSchema,
4073
+ EDITOR_DATA_ATTRIBUTE,
4074
+ EDITOR_MARKER,
3238
4075
  EditorUIStore,
3239
4076
  EmailTemplateSchema,
3240
4077
  EmailTemplateSchemaV1_0,
@@ -3249,6 +4086,7 @@ function createEmptyStore() {
3249
4086
  ImageBlockSchema,
3250
4087
  LEAF_BLOCK_TYPES,
3251
4088
  MJML_WRAPPER_DEFAULT_PADDING,
4089
+ MjmlBuilder,
3252
4090
  MjmlHeadSchema,
3253
4091
  NavbarBlockSchema,
3254
4092
  NavbarLinkModel,
@@ -3276,7 +4114,9 @@ function createEmptyStore() {
3276
4114
  TopLevelItemSchema,
3277
4115
  WrapperModel,
3278
4116
  WrapperSchema,
4117
+ addPx,
3279
4118
  allSections,
4119
+ base64Utf8,
3280
4120
  buildGradientCSS,
3281
4121
  cloneSectionSnapshot,
3282
4122
  createBlockRegistry,
@@ -3291,12 +4131,17 @@ function createEmptyStore() {
3291
4131
  createTemplate,
3292
4132
  createTemplateWithDefaultSection,
3293
4133
  createWrapper,
4134
+ encodeEditorData,
3294
4135
  isLeafBlockType,
3295
4136
  isTemplateMigrationError,
3296
4137
  isWrapper,
3297
4138
  isWrapperInstance,
3298
4139
  migrateTemplate,
3299
4140
  migrateV1_0ToV1_1,
4141
+ mjmlOptions,
4142
+ paddingWithGap,
4143
+ stripEditorMarkup,
4144
+ textInlineStyles,
3300
4145
  validateTemplate,
3301
4146
  withTemplateId
3302
4147
  });