email-builder-utils 1.1.42 → 1.1.44

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.
@@ -15,6 +15,11 @@ const addPxToAttributes = [
15
15
  const addPxOrPerToAttributes = ["width", "height"];
16
16
  const allPxAttributes = [...addPxToAttributes, ...addPxOrPerToAttributes];
17
17
  exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
18
+ function encodeBlockProps(props) {
19
+ return JSON.stringify(props)
20
+ .replace(/&/g, '&')
21
+ .replace(/"/g, '"');
22
+ }
18
23
  async function loadImageNaturalDimensions(imageUrl) {
19
24
  return new Promise((resolve, reject) => {
20
25
  const img = new Image();
@@ -58,13 +63,13 @@ function buildStyles(style, { pxChanges, perChanges }) {
58
63
  };
59
64
  value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
60
65
  }
61
- // Sanitize fontFamily: replace double quotes with single quotes to avoid
62
- // breaking the surrounding style="..." HTML attribute
63
66
  if (key === "fontFamily" && typeof value === "string") {
64
67
  value = (0, fontFallback_1.withFontFallback)(value).replace(/"/g, "'");
65
68
  }
66
- // Wrap backgroundImage values in url() if not already wrapped
67
- if (key === "backgroundImage" && typeof value === "string" && !String(value).startsWith("url(")) {
69
+ // Wrap backgroundImage values in url() if not already wrapped — skip gradients
70
+ if (key === "backgroundImage" && typeof value === "string"
71
+ && !String(value).startsWith("url(")
72
+ && !String(value).toLowerCase().includes("gradient(")) {
68
73
  value = `url('${value}')`;
69
74
  }
70
75
  // lineHeight: values >= 4 are pixel values; smaller values are unitless multipliers (e.g. 1.5)
@@ -104,7 +109,7 @@ function buildStyles(style, { pxChanges, perChanges }) {
104
109
  .map(([k, v]) => `${k}:${v}`);
105
110
  if (style.customCss)
106
111
  parts.push(style.customCss);
107
- return parts.join('; ').trim();
112
+ return parts.join('; ').replace(/;\s*$/, '').trim();
108
113
  }
109
114
  async function convertToHtml(blockData, rootData, cellWidthInPx) {
110
115
  switch (blockData.type) {
@@ -134,32 +139,32 @@ function appendOutlookSupport(content, contentStyle, className, msoWidth) {
134
139
  const visibilityClass = className || "";
135
140
  const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
136
141
  if (shouldHideInOutlook) {
137
- return `
138
- <!--[if !mso]><!-->
139
- <table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
140
- <!--<![endif]-->
142
+ return `
143
+ <!--[if !mso]><!-->
144
+ <table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
145
+ <!--<![endif]-->
141
146
  `;
142
147
  }
143
148
  // When an explicit pixel width is provided (e.g. inside a column cell), use dual MSO/non-MSO
144
149
  // tables. Old Outlook (Word engine) ignores max-width and can resolve width="100%" to the
145
150
  // full email width (600px) rather than the column width, causing images/buttons to expand.
146
151
  if (msoWidth) {
147
- return `
148
- <!--[if mso]>
149
- <table border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" style="border-collapse:collapse;width:${msoWidth}px;"><tr><td width="${msoWidth}" style="${contentStyle}">
150
- <![endif]-->
151
- <!--[if !mso]><!-->
152
- <table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">
153
- <!--<![endif]-->
154
- ${content}
155
- <!--[if mso]></td></tr></table><![endif]-->
156
- <!--[if !mso]><!-->
157
- </td></tr></table>
158
- <!--<![endif]-->
152
+ return `
153
+ <!--[if mso]>
154
+ <table border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" style="border-collapse:collapse;width:${msoWidth}px;"><tr><td width="${msoWidth}" style="${contentStyle}">
155
+ <![endif]-->
156
+ <!--[if !mso]><!-->
157
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">
158
+ <!--<![endif]-->
159
+ ${content}
160
+ <!--[if mso]></td></tr></table><![endif]-->
161
+ <!--[if !mso]><!-->
162
+ </td></tr></table>
163
+ <!--<![endif]-->
159
164
  `;
160
165
  }
161
- return `
162
- <table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">${content}</td></tr></table>
166
+ return `
167
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">${content}</td></tr></table>
163
168
  `;
164
169
  }
165
170
  function convertDividerBlockToHtml(blockData) {
@@ -178,21 +183,21 @@ function convertDividerBlockToHtml(blockData) {
178
183
  ]
179
184
  .filter(Boolean)
180
185
  .join(" ");
181
- const dividerContent = `
182
- <table
183
- width="${dividerWidth}%"
184
- cellpadding="0"
185
- cellspacing="0"
186
- >
187
- <tr>
188
- <td
189
- height="${thickness}"
190
- style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
191
- >
192
- &nbsp;
193
- </td>
194
- </tr>
195
- </table>
186
+ const dividerContent = `
187
+ <table
188
+ width="${dividerWidth}%"
189
+ cellpadding="0"
190
+ cellspacing="0"
191
+ >
192
+ <tr>
193
+ <td
194
+ height="${thickness}"
195
+ style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
196
+ >
197
+ &nbsp;
198
+ </td>
199
+ </tr>
200
+ </table>
196
201
  `;
197
202
  return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
198
203
  }
@@ -287,21 +292,21 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
287
292
  if (useRoundRect && borderRadius > 0) {
288
293
  // Use VML for border radius - wrap in table to constrain width for Old Outlook (Word engine)
289
294
  // Use aspect="atmost" to prevent image from stretching beyond its bounds
290
- outlookImage = `<!--[if mso]>
291
- <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
292
- <tr>
293
- <td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
294
- <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
295
- style="width:${vmlWidth}px;height:${vmlHeight}px;"
296
- ${borderAttributes}
297
- arcsize="${arcsize}"
298
- fill="true" fillcolor="none">
299
- <v:fill src="${imageUrl}" type="tile" aspect="atmost" />
300
- <v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>
301
- </v:roundrect>
302
- </td>
303
- </tr>
304
- </table>
295
+ outlookImage = `<!--[if mso]>
296
+ <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
297
+ <tr>
298
+ <td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
299
+ <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
300
+ style="width:${vmlWidth}px;height:${vmlHeight}px;"
301
+ ${borderAttributes}
302
+ arcsize="${arcsize}"
303
+ fill="true" fillcolor="none">
304
+ <v:fill src="${imageUrl}" type="tile" aspect="atmost" />
305
+ <v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>
306
+ </v:roundrect>
307
+ </td>
308
+ </tr>
309
+ </table>
305
310
  <![endif]-->`;
306
311
  }
307
312
  else {
@@ -310,21 +315,21 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
310
315
  const borderStyleAttr = borderWidth > 0
311
316
  ? `border: ${borderWidth}px solid ${borderColor};`
312
317
  : '';
313
- outlookImage = `<!--[if mso]>
314
- <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
315
- <tr>
316
- <td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
317
- <img src="${imageUrl}" alt="Image" border="0" width="${vmlWidth}" height="${vmlHeight}" style="display:block; width:${vmlWidth}px; height:${vmlHeight}px; max-width:${vmlWidth}px; ${borderStyleAttr}" />
318
- </td>
319
- </tr>
320
- </table>
318
+ outlookImage = `<!--[if mso]>
319
+ <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
320
+ <tr>
321
+ <td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
322
+ <img src="${imageUrl}" alt="Image" border="0" width="${vmlWidth}" height="${vmlHeight}" style="display:block; width:${vmlWidth}px; height:${vmlHeight}px; max-width:${vmlWidth}px; ${borderStyleAttr}" />
323
+ </td>
324
+ </tr>
325
+ </table>
321
326
  <![endif]-->`;
322
327
  }
323
- return `
324
- ${outlookImage}
325
- <!--[if !mso]><!-->
326
- ${content}
327
- <!--<![endif]-->
328
+ return `
329
+ ${outlookImage}
330
+ <!--[if !mso]><!-->
331
+ ${content}
332
+ <!--<![endif]-->
328
333
  `;
329
334
  }
330
335
  async function computeScaledDimensions(imageUrl, maxContainerWidthPx) {
@@ -435,51 +440,51 @@ function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
435
440
  const width = typeof buttonStyle.width === "number"
436
441
  ? `width="${buttonStyle.width}"`
437
442
  : "";
438
- return `<!--[if mso]>
439
- <table role="presentation" cellspacing="0" cellpadding="0" border="0" style="display:inline-table;">
440
- <tr>
441
- <td align="center"
442
- valign="middle"
443
- ${width}
444
- ${finalHeight ? `height="${finalHeight}"` : ""}
445
- bgcolor="${bgColor}"
446
- style="
447
- ${finalHeight ? `height:${finalHeight}px;` : ""}
448
- background-color:${bgColor};
449
- border-radius:${borderRadius}px;
450
- border:${borderWidth}px ${borderStyle} ${borderColor};
451
- overflow:hidden;
452
- mso-line-height-rule:exactly;
453
- ">
454
-
455
- <table role="presentation" cellspacing="0" cellpadding="0" border="0">
456
- <tr>
457
- <td align="center" valign="middle"
458
- style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
459
-
460
- <a href="${navigateToUrl}"
461
- style="
462
- display:inline-block;
463
- color:${color};
464
- text-decoration:none;
465
- font-family:${fontFamily};
466
- font-size:${fontSize}px;
467
- font-weight:${fontWeight};
468
- line-height:normal;
469
- ">
470
- ${text}
471
- </a>
472
-
473
- </td>
474
- </tr>
475
- </table>
476
-
477
- </td>
478
- </tr>
479
- </table>
480
- <![endif]-->
481
- <!--[if !mso]><!-->
482
- ${content}
443
+ return `<!--[if mso]>
444
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" style="display:inline-table;">
445
+ <tr>
446
+ <td align="center"
447
+ valign="middle"
448
+ ${width}
449
+ ${finalHeight ? `height="${finalHeight}"` : ""}
450
+ bgcolor="${bgColor}"
451
+ style="
452
+ ${finalHeight ? `height:${finalHeight}px;` : ""}
453
+ background-color:${bgColor};
454
+ border-radius:${borderRadius}px;
455
+ border:${borderWidth}px ${borderStyle} ${borderColor};
456
+ overflow:hidden;
457
+ mso-line-height-rule:exactly;
458
+ ">
459
+
460
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0">
461
+ <tr>
462
+ <td align="center" valign="middle"
463
+ style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
464
+
465
+ <a href="${navigateToUrl}"
466
+ style="
467
+ display:inline-block;
468
+ color:${color};
469
+ text-decoration:none;
470
+ font-family:${fontFamily};
471
+ font-size:${fontSize}px;
472
+ font-weight:${fontWeight};
473
+ line-height:normal;
474
+ ">
475
+ ${text}
476
+ </a>
477
+
478
+ </td>
479
+ </tr>
480
+ </table>
481
+
482
+ </td>
483
+ </tr>
484
+ </table>
485
+ <![endif]-->
486
+ <!--[if !mso]><!-->
487
+ ${content}
483
488
  <!--<![endif]-->`;
484
489
  }
485
490
  function convertButtonBlock(blockData) {
@@ -509,89 +514,139 @@ function convertButtonBlock(blockData) {
509
514
  ? `width="${width}"`
510
515
  : "";
511
516
  // ✅ FIX: no width=100% anywhere
512
- const buttonTable = `
513
- <table role="presentation" cellspacing="0" cellpadding="0" border="0"
514
- style="display:inline-table; border-collapse:separate;"
515
- ${widthAttr}>
516
- <tr>
517
- <td
518
- align="center"
519
- valign="middle"
520
- ${finalHeight ? `height="${finalHeight}"` : ""}
521
- style="
522
- ${finalHeight ? `height:${finalHeight}px;` : ""}
523
- background-color:${bgColor};
524
- border-radius:${br}px;
525
- border:${bw}px ${bdStyle} ${bdColor};
526
- overflow:hidden;
527
- mso-line-height-rule:exactly;
528
- "
529
- >
530
-
531
- <table role="presentation" cellspacing="0" cellpadding="0" border="0">
532
- <tr>
533
- <td align="center" valign="middle"
534
- style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
535
-
536
- <a href="${navigateToUrl}"
537
- style="
538
- display:inline-block;
539
- color:${safeColor};
540
- text-decoration:none;
541
- font-family:${safeFF};
542
- font-size:${fs}px;
543
- font-weight:${fontWeight || 400};
544
- line-height:normal;
545
- white-space:nowrap;
546
- ">
547
- ${text}
548
- </a>
549
-
550
- </td>
551
- </tr>
552
- </table>
553
-
554
- </td>
555
- </tr>
556
- </table>
517
+ const buttonTable = `
518
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0"
519
+ style="display:inline-table; border-collapse:separate;"
520
+ ${widthAttr}>
521
+ <tr>
522
+ <td
523
+ align="center"
524
+ valign="middle"
525
+ ${finalHeight ? `height="${finalHeight}"` : ""}
526
+ style="
527
+ ${finalHeight ? `height:${finalHeight}px;` : ""}
528
+ background-color:${bgColor};
529
+ border-radius:${br}px;
530
+ border:${bw}px ${bdStyle} ${bdColor};
531
+ overflow:hidden;
532
+ mso-line-height-rule:exactly;
533
+ "
534
+ >
535
+
536
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0">
537
+ <tr>
538
+ <td align="center" valign="middle"
539
+ style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
540
+
541
+ <a href="${navigateToUrl}"
542
+ style="
543
+ display:inline-block;
544
+ color:${safeColor};
545
+ text-decoration:none;
546
+ font-family:${safeFF};
547
+ font-size:${fs}px;
548
+ font-weight:${fontWeight || 400};
549
+ line-height:normal;
550
+ white-space:nowrap;
551
+ ">
552
+ ${text}
553
+ </a>
554
+
555
+ </td>
556
+ </tr>
557
+ </table>
558
+
559
+ </td>
560
+ </tr>
561
+ </table>
557
562
  `;
558
563
  const aligned = containerAlign === "center"
559
564
  ? `<center>${buttonTable}</center>`
560
565
  : `<div style="text-align:${containerAlign};">${buttonTable}</div>`;
561
566
  const buttonWithOutlook = appendOutlookForButton(aligned, style, navigateToUrl, text);
562
- return `
563
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
564
- <tr>
565
- <td align="${containerAlign}"
566
- style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;
567
- background-color:${containerBg || "transparent"};">
568
- ${buttonWithOutlook}
569
- </td>
570
- </tr>
571
- </table>
567
+ return `
568
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
569
+ <tr>
570
+ <td align="${containerAlign}"
571
+ style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;
572
+ background-color:${containerBg || "transparent"};">
573
+ ${buttonWithOutlook}
574
+ </td>
575
+ </tr>
576
+ </table>
572
577
  `;
573
578
  }
579
+ // Words inside a gradient() that are NOT color names
580
+ const GRADIENT_KEYWORDS = new Set([
581
+ 'linear', 'radial', 'conic', 'gradient',
582
+ 'to', 'at', 'top', 'bottom', 'left', 'right', 'center',
583
+ 'closest', 'farthest', 'corner', 'side', 'circle', 'ellipse',
584
+ 'deg', 'turn', 'rad', 'grad', 'from', 'in',
585
+ 'url', // url() prefix sometimes appears when gradient is wrapped incorrectly
586
+ ]);
587
+ /** Extract the first color stop (hex, rgb, or named CSS color) from a gradient string. */
588
+ function firstGradientColor(gradient) {
589
+ const tokenRe = /#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|([a-zA-Z-]+)/g;
590
+ let m;
591
+ while ((m = tokenRe.exec(gradient)) !== null) {
592
+ const namedWord = m[1];
593
+ if (namedWord) {
594
+ if (!GRADIENT_KEYWORDS.has(namedWord.toLowerCase()))
595
+ return namedWord;
596
+ }
597
+ else {
598
+ return m[0]; // hex or rgb()
599
+ }
600
+ }
601
+ return '';
602
+ }
574
603
  /**
575
604
  * Extract the first solid-color stop from a CSS gradient in customCss.
576
- * Used as a MSO/Outlook bgcolor fallback when backgroundColor is not set
577
- * but the block has a gradient in customCss (e.g. linear-gradient imports).
605
+ * Used as a MSO/Outlook bgcolor fallback. Handles hex, rgb, and named colors.
578
606
  */
579
607
  function extractCssFallbackColor(customCss) {
580
608
  if (!customCss)
581
609
  return '';
582
- const m = customCss.match(/(?:linear|radial|conic)-gradient\([^)]*?(#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|rgba\([^)]+\))/);
583
- return m?.[1] ?? '';
610
+ const gradientMatch = customCss.match(/(?:linear|radial|conic)-gradient\(([^)]+(?:\([^)]*\)[^)]*)*)\)/);
611
+ if (!gradientMatch)
612
+ return '';
613
+ return firstGradientColor(gradientMatch[1]);
584
614
  }
585
615
  function parseGradient(gradient) {
586
616
  if (!gradient)
587
617
  return null;
588
- const angleMatch = gradient.match(/(\d+)deg/);
589
- const angle = angleMatch ? parseInt(angleMatch[1]) : 180;
590
- const colors = gradient.match(/#([0-9a-fA-F]{3,8})|rgb[a]?\([^)]+\)/g) || [];
618
+ const lower = gradient.toLowerCase();
619
+ // Determine angle from deg value or direction keyword
620
+ const degMatch = gradient.match(/(\d+(?:\.\d+)?)deg/);
621
+ let angle = 180;
622
+ if (degMatch) {
623
+ angle = parseFloat(degMatch[1]);
624
+ }
625
+ else if (lower.includes('to right'))
626
+ angle = 90;
627
+ else if (lower.includes('to left'))
628
+ angle = 270;
629
+ else if (lower.includes('to top'))
630
+ angle = 0;
631
+ // 'to bottom' and bare gradient() both default to 180
632
+ // Extract ALL color tokens: hex, rgb/rgba, and named CSS color words
633
+ const colors = [];
634
+ const tokenRe = /#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|([a-zA-Z-]+)/g;
635
+ let m;
636
+ while ((m = tokenRe.exec(gradient)) !== null) {
637
+ const namedWord = m[1];
638
+ if (namedWord) {
639
+ if (!GRADIENT_KEYWORDS.has(namedWord.toLowerCase()))
640
+ colors.push(namedWord);
641
+ }
642
+ else {
643
+ colors.push(m[0]);
644
+ }
645
+ }
591
646
  return {
592
647
  angle,
593
648
  colors,
594
- fallback: colors[0] || "#ffffff",
649
+ fallback: colors[0] || '#ffffff',
595
650
  };
596
651
  }
597
652
  function cssAngleToVml(angle) {
@@ -602,18 +657,37 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
602
657
  const { columns = 1, cellWidths = [], responsive = true } = props;
603
658
  const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
604
659
  const gridVisibilityClass = (0, common_1.getVisibilityClass)(props);
605
- // Strip url() wrapper if already present, so we have a raw URL for VML
606
- const isGradient = typeof backgroundImage === "string" && backgroundImage.includes("gradient");
607
- const parsedGradient = isGradient ? parseGradient(backgroundImage) : null;
660
+ // Detect gradient check both backgroundImage prop and customCss (gradient may land in
661
+ // customCss when the block was built via CSS shorthand or custom CSS input).
662
+ const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : '';
663
+ const customCssStr = restStyle.customCss || '';
664
+ // Extract gradient string from customCss if not already in backgroundImage
665
+ const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
666
+ ? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
667
+ : '';
668
+ const effectiveGradient = bgImageStr.includes('gradient(')
669
+ ? bgImageStr
670
+ : gradientInCustomCss;
671
+ const isGradient = Boolean(effectiveGradient);
672
+ const parsedGradient = isGradient ? parseGradient(effectiveGradient) : null;
608
673
  const fallbackBgColor = backgroundColor ||
609
674
  parsedGradient?.fallback ||
610
- extractCssFallbackColor(restStyle.customCss) ||
675
+ extractCssFallbackColor(customCssStr) ||
611
676
  "#ffffff";
612
- const rawBgImageUrl = !isGradient && backgroundImage
613
- ? String(backgroundImage).replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
677
+ const rawBgImageUrl = !isGradient && bgImageStr
678
+ ? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
614
679
  : null;
615
- // FIX: avoid table-layout:fixed causes shrink in many clients
616
- const tableStyles = buildStyles({ backgroundColor, ...restStyle }, {
680
+ // When gradient came from customCss, strip background-image from customCss so it
681
+ // doesn't duplicate into the inner table style (the outer <td> wrapper carries it).
682
+ const innerCustomCss = gradientInCustomCss
683
+ ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
684
+ : customCssStr;
685
+ // Build inner table styles — when gradient/bg-image is on the outer wrapper, strip
686
+ // background props from the inner table so the outer <td> background shows through.
687
+ const innerRestStyle = (rawBgImageUrl || isGradient)
688
+ ? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
689
+ : { ...restStyle, customCss: innerCustomCss };
690
+ const tableStyles = buildStyles({ backgroundColor: (rawBgImageUrl || isGradient) ? undefined : backgroundColor, ...innerRestStyle }, {
617
691
  perChanges: [],
618
692
  pxChanges: allPxAttributes,
619
693
  });
@@ -621,48 +695,31 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
621
695
  const visualRows = Math.ceil(total / columns);
622
696
  // OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
623
697
  const msoTableWidth = Math.min(cellWidthInPx, 600);
624
- // When a background image is present, the background is applied on an outer
625
- // wrapper <td> (see bottom of function). The inner grid tables must be clean
626
- // (no background) so that outer td background shows through.
627
- // When no background image, the MSO table gets bgcolor for solid-color sections.
628
- const msoBgColor = !rawBgImageUrl
629
- ? (backgroundColor || extractCssFallbackColor(restStyle.customCss))
698
+ // When a background image/gradient is present, the background is applied on an outer
699
+ // wrapper <td> (see bottom of function). The inner grid tables must be clean.
700
+ // When no background, the MSO table gets bgcolor for solid-color sections.
701
+ const msoBgColor = !rawBgImageUrl && !isGradient
702
+ ? (backgroundColor || '')
630
703
  : '';
631
704
  const msoBgAttr = msoBgColor ? ` bgcolor="${msoBgColor}"` : '';
632
705
  const msoBgStyle = msoBgColor ? `background-color:${msoBgColor};` : '';
633
- // Inner non-MSO table: strip ALL background-related props when an outer bg-td
634
- // wrapper is used. background-position/repeat/size without a background-image
635
- // are harmless, but keeping them in the inner table style is confusing and
636
- // could conflict with email client default styles.
637
- const innerNonMsoStyle = rawBgImageUrl
638
- ? buildStyles({
639
- ...restStyle,
640
- customCss: '',
641
- backgroundSize: undefined,
642
- backgroundPosition: undefined,
643
- backgroundRepeat: undefined,
644
- }, { perChanges: [], pxChanges: allPxAttributes })
645
- : tableStyles;
646
- // When bg image is present, inner tables must be explicitly transparent so the
647
- // outer <td> background shows through (email clients may default table bg to white).
706
+ // Inner tables must be explicitly transparent when outer <td> carries the background.
648
707
  const innerBgTransparent = (rawBgImageUrl || isGradient)
649
708
  ? 'background-color:transparent;'
650
709
  : '';
651
- // bgcolor attribute on both tables: survives Outlook compose paste (Word + Web
652
- // both strip background-image CSS but keep the bgcolor HTML attribute).
653
- const nonMsoBgAttr = !rawBgImageUrl && backgroundColor ? ` bgcolor="${backgroundColor}"` : '';
654
- let html = `
655
- <!--[if mso]>
656
- <table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
657
- style="border-collapse:collapse;width:${msoTableWidth}px;${msoBgStyle}${innerBgTransparent}"
658
- class="${gridVisibilityClass}">
659
- <![endif]-->
660
- <!--[if !mso]><!-->
661
- <table border="0" cellpadding="0" cellspacing="0" width="100%"
662
- role="presentation"${nonMsoBgAttr}
663
- style="border-collapse:collapse; ${innerBgTransparent}${innerNonMsoStyle}; max-width:600px;"
664
- class="${gridVisibilityClass}">
665
- <!--<![endif]-->
710
+ const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor ? ` bgcolor="${backgroundColor}"` : '';
711
+ let html = `
712
+ <!--[if mso]>
713
+ <table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
714
+ style="border-collapse:collapse;width:${msoTableWidth}px;${msoBgStyle}${innerBgTransparent}"
715
+ class="${gridVisibilityClass}">
716
+ <![endif]-->
717
+ <!--[if !mso]><!-->
718
+ <table border="0" cellpadding="0" cellspacing="0" width="100%"
719
+ role="presentation"${nonMsoBgAttr}
720
+ style="border-collapse:collapse; ${innerBgTransparent}${tableStyles}; max-width:600px;"
721
+ class="${gridVisibilityClass}">
722
+ <!--<![endif]-->
666
723
  `;
667
724
  for (let r = 0; r < visualRows; r++) {
668
725
  html += "<tr>";
@@ -721,13 +778,13 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
721
778
  // compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
722
779
  const cellBgColor = cellStyle.backgroundColor || '';
723
780
  const cellBgAttr = cellBgColor ? ` bgcolor="${cellBgColor}"` : '';
724
- html += `
725
- <td
726
- width="${cellWidthPx}"${cellBgAttr}
727
- class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
728
- style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
729
- >
730
- ${childHtml}
781
+ html += `
782
+ <td
783
+ width="${cellWidthPx}"${cellBgAttr}
784
+ class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
785
+ style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
786
+ >
787
+ ${childHtml}
731
788
  </td>`;
732
789
  // Spacer td between columns — fixed pixel width, invisible to screen readers
733
790
  if (columnGap > 0 && c !== lastVisibleCol) {
@@ -736,10 +793,10 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
736
793
  }
737
794
  }
738
795
  else {
739
- html += `
740
- <td width="${cellWidthPx}"
741
- ${responsive ? 'class="stack-column"' : ""}
742
- style="width:${cellWidthPx}px;vertical-align:top;">
796
+ html += `
797
+ <td width="${cellWidthPx}"
798
+ ${responsive ? 'class="stack-column"' : ""}
799
+ style="width:${cellWidthPx}px;vertical-align:top;">
743
800
  </td>`;
744
801
  if (columnGap > 0 && c !== lastVisibleCol) {
745
802
  html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
@@ -749,13 +806,13 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
749
806
  html += "</tr>";
750
807
  }
751
808
  // Close both MSO and non-MSO tables
752
- html += `
753
- <!--[if mso]>
754
- </table>
755
- <![endif]-->
756
- <!--[if !mso]><!-->
757
- </table>
758
- <!--<![endif]-->
809
+ html += `
810
+ <!--[if mso]>
811
+ </table>
812
+ <![endif]-->
813
+ <!--[if !mso]><!-->
814
+ </table>
815
+ <!--<![endif]-->
759
816
  `;
760
817
  // ── Background image: canonical multi-client approach ────────────────────
761
818
  //
@@ -781,34 +838,34 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
781
838
  return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
782
839
  })()
783
840
  : `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
784
- html = `
785
- <table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}" role="presentation"
786
- style="border-collapse:collapse;width:${msoTableWidth}px;">
787
- <tr>
788
- <td width="${msoTableWidth}" bgcolor="${fallbackBgColor}" valign="top"
789
- style="
790
- width:${msoTableWidth}px;
791
- background-color:${fallbackBgColor};
792
- ${isGradient ? `background:${backgroundImage};` : `background:url('${rawBgImageUrl}') center/cover no-repeat;`}
793
- ">
794
-
795
- <!--[if gte mso 9]>
796
- <v:rect xmlns:v="urn:schemas-microsoft-com:vml"
797
- fill="true" stroke="false"
798
- style="width:${msoTableWidth}px;">
799
- ${vmlFill}
800
- <v:textbox inset="0,0,0,0">
801
- <![endif]-->
802
-
803
- ${html}
804
-
805
- <!--[if gte mso 9]>
806
- </v:textbox>
807
- </v:rect>
808
- <![endif]-->
809
-
810
- </td>
811
- </tr>
841
+ html = `
842
+ <table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}" role="presentation"
843
+ style="border-collapse:collapse;width:${msoTableWidth}px;">
844
+ <tr>
845
+ <td width="${msoTableWidth}" bgcolor="${fallbackBgColor}" valign="top"
846
+ style="
847
+ width:${msoTableWidth}px;
848
+ background-color:${fallbackBgColor};
849
+ ${isGradient ? `background:${effectiveGradient};` : `background:url('${rawBgImageUrl}') center/cover no-repeat;`}
850
+ ">
851
+
852
+ <!--[if gte mso 9]>
853
+ <v:rect xmlns:v="urn:schemas-microsoft-com:vml"
854
+ fill="true" stroke="false"
855
+ style="width:${msoTableWidth}px;">
856
+ ${vmlFill}
857
+ <v:textbox inset="0,0,0,0">
858
+ <![endif]-->
859
+
860
+ ${html}
861
+
862
+ <!--[if gte mso 9]>
863
+ </v:textbox>
864
+ </v:rect>
865
+ <![endif]-->
866
+
867
+ </td>
868
+ </tr>
812
869
  </table>`;
813
870
  }
814
871
  return html;
@@ -842,18 +899,18 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
842
899
  const borderRadius = style?.borderRadius || 0;
843
900
  const bgColor = style?.backgroundColor || "transparent";
844
901
  // IMPORTANT: radius only for non-Outlook
845
- const wrapped = `
846
- <!--[if !mso]><!-->
847
- <div style="
848
- border-radius:${borderRadius}px;
849
- overflow:hidden;
850
- background-color:${bgColor};
851
- ">
852
- <!--<![endif]-->
853
- ${parts.join("")}
854
- <!--[if !mso]><!-->
855
- </div>
856
- <!--<![endif]-->
902
+ const wrapped = `
903
+ <!--[if !mso]><!-->
904
+ <div style="
905
+ border-radius:${borderRadius}px;
906
+ overflow:hidden;
907
+ background-color:${bgColor};
908
+ ">
909
+ <!--<![endif]-->
910
+ ${parts.join("")}
911
+ <!--[if !mso]><!-->
912
+ </div>
913
+ <!--<![endif]-->
857
914
  `;
858
915
  return {
859
916
  html: wrapped,
@@ -867,7 +924,9 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
867
924
  const { hideOnDesktop } = props; // Get the hideOnDesktop prop
868
925
  const { videoUrl, youtubeVideoUrl, thumbnailUrl, altText } = props;
869
926
  const videoLink = youtubeVideoUrl || videoUrl || "#";
870
- let resolvedThumbnail = thumbnailUrl || "https://via.placeholder.com/480x360?text=No+Thumbnail";
927
+ // via.placeholder.com is defunct — use a data-URI grey box as the default thumbnail
928
+ const FALLBACK_THUMBNAIL = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='360'%3E%3Crect width='480' height='360' fill='%23cccccc'/%3E%3C/svg%3E`;
929
+ let resolvedThumbnail = thumbnailUrl || FALLBACK_THUMBNAIL;
871
930
  if (youtubeVideoUrl) {
872
931
  const youtubeId = (0, common_1.extractYouTubeId)(youtubeVideoUrl);
873
932
  const vimeoId = (0, common_1.extractVimeoId)(youtubeVideoUrl);
@@ -921,117 +980,103 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
921
980
  const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
922
981
  const shouldHideInOutlook = hideOnDesktop;
923
982
  const outlookVideoContent = shouldHideInOutlook
924
- ? `<!--[if !mso]><!-->
925
- <v:group xmlns:v="urn:schemas-microsoft-com:vml"
926
- coordsize="${innerContainerWidth},${calculatedHeight}"
927
- href="${videoLink}"
928
- style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
929
- <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;" stroked="t"
930
- strokeweight="${borderWidth}px"
931
- strokecolor="${borderColor}"
932
- ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
933
- >
934
- <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
935
- </v:rect>
936
- <v:shape type="#_x0000_t75"
937
- style="position:absolute;
938
- left:${vmlLeft.toFixed(1)}px;
939
- top:${vmlTop.toFixed(1)}px;
940
- width:${playIconWidth}px;
941
- height:${playIconHeight}px;"
942
- alt="Play" href="${videoLink}" title="${altText || "Video"}"
943
- stroked="f" filled="t">
944
- <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
945
- </v:shape>
946
- </v:group>
983
+ ? `<!--[if !mso]><!-->
984
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml"
985
+ coordsize="${innerContainerWidth},${calculatedHeight}"
986
+ href="${videoLink}"
987
+ style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
988
+ <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
989
+ ${borderWidth > 0 ? `stroked="t" strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="f"`}
990
+ ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
991
+ >
992
+ <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
993
+ </v:rect>
994
+ <v:shape type="#_x0000_t75"
995
+ style="position:absolute;
996
+ left:${vmlLeft.toFixed(1)}px;
997
+ top:${vmlTop.toFixed(1)}px;
998
+ width:${playIconWidth}px;
999
+ height:${playIconHeight}px;"
1000
+ alt="Play" href="${videoLink}" title="${altText || "Video"}"
1001
+ stroked="f" filled="t">
1002
+ <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
1003
+ </v:shape>
1004
+ </v:group>
947
1005
  <!--<![endif]-->`
948
- : `<!--[if mso]>
949
- <v:group xmlns:v="urn:schemas-microsoft-com:vml"
950
- coordsize="${innerContainerWidth},${calculatedHeight}"
951
- href="${videoLink}"
952
- style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
953
- <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
954
- stroked="t"
955
- strokeweight="${borderWidth}px"
956
- strokecolor="${borderColor}"
957
- ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
958
- >
959
- <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
960
- </v:rect>
961
- <v:shape type="#_x0000_t75"
962
- style="position:absolute;
963
- left:${vmlLeft.toFixed(1)}px;
964
- top:${vmlTop.toFixed(1)}px;
965
- width:${playIconWidth}px;
966
- height:${playIconHeight}px;"
967
- alt="Play" href="${videoLink}" title="${altText || "Video"}"
968
- stroked="f" filled="t">
969
- <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
970
- </v:shape>
971
- </v:group>
1006
+ : `<!--[if mso]>
1007
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml"
1008
+ coordsize="${innerContainerWidth},${calculatedHeight}"
1009
+ href="${videoLink}"
1010
+ style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
1011
+ <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
1012
+ ${borderWidth > 0 ? `stroked="t" strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="f"`}
1013
+ ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
1014
+ >
1015
+ <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
1016
+ </v:rect>
1017
+ <v:shape type="#_x0000_t75"
1018
+ style="position:absolute;
1019
+ left:${vmlLeft.toFixed(1)}px;
1020
+ top:${vmlTop.toFixed(1)}px;
1021
+ width:${playIconWidth}px;
1022
+ height:${playIconHeight}px;"
1023
+ alt="Play" href="${videoLink}" title="${altText || "Video"}"
1024
+ stroked="f" filled="t">
1025
+ <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
1026
+ </v:shape>
1027
+ </v:group>
972
1028
  <![endif]-->`;
973
- const nonOutlookVideoContent = `<!--[if !mso]><!-->
974
- <table
975
- width="${innerContainerWidth}"
976
- cellpadding="0"
977
- cellspacing="0"
978
- border="0"
979
- role="presentation"
980
- align="${style?.textAlign || "left"}"
981
- style="
982
- max-width: ${innerContainerWidth}px;
983
- width: 100%;
984
- height: ${calculatedHeight}px;
985
- background-color: ${style?.backgroundColor || "#FFFFFF"};
986
- background-image: url('${resolvedThumbnail}');
987
- background-size: contain;
988
- background-position: center;
989
- background-repeat: no-repeat;
990
- box-sizing: border-box;
991
- border: ${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};
992
- border-radius: ${borderRadius}px;
993
- "
994
- >
995
- <tr>
996
- <td style="padding: 0; height: ${calculatedHeight}px; text-align: center; vertical-align: middle;" valign="middle">
997
- <a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
998
- <img
999
- src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
1000
- width="${playIconWidth}"
1001
- alt="Play"
1002
- style="display: block;
1003
- border: 0;
1004
- outline: none;
1005
- text-decoration: none;
1006
- height: auto;"
1007
- />
1008
- </a>
1009
- </td>
1010
- </tr>
1011
- </table>
1012
- <!--<![endif]-->`;
1029
+ // Non-Outlook: use a real <img> for the thumbnail so it renders in Gmail / Yahoo / webmail.
1030
+ // background-image on <table> is stripped by virtually every email client.
1031
+ // position:absolute for the play-button overlay is safe here because this block
1032
+ // is already inside <!--[if !mso]> — Outlook is handled separately via VML above.
1033
+ const thumbnailW = Math.round(innerContainerWidth);
1034
+ const thumbnailH = Math.round(calculatedHeight);
1035
+ const playMarginTop = -Math.round(playIconHeight / 2);
1036
+ const playMarginLeft = -Math.round(playIconWidth / 2);
1037
+ const borderAttr = borderWidth > 0 ? `border:${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};` : "border:0;";
1038
+ const radiusAttr = borderRadius > 0 ? `border-radius:${borderRadius}px; overflow:hidden;` : "";
1039
+ const nonOutlookVideoContent = `<!--[if !mso]><!-->
1040
+ <div style="display:block; width:100%; max-width:${thumbnailW}px; position:relative; line-height:0; font-size:0; ${borderAttr}${radiusAttr}">
1041
+ <a href="${videoLink}" target="_blank" style="display:block; text-decoration:none; line-height:0; font-size:0;">
1042
+ <img
1043
+ src="${resolvedThumbnail}"
1044
+ width="${thumbnailW}"
1045
+ height="${thumbnailH}"
1046
+ alt="${altText || "Video"}"
1047
+ style="display:block; width:100%; max-width:${thumbnailW}px; height:auto; border:0;"
1048
+ />
1049
+ <img
1050
+ src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
1051
+ width="${playIconWidth}"
1052
+ alt="Play"
1053
+ style="display:block; position:absolute; top:50%; left:50%; margin-top:${playMarginTop}px; margin-left:${playMarginLeft}px; border:0; outline:none;"
1054
+ />
1055
+ </a>
1056
+ </div>
1057
+ <!--<![endif]-->`;
1013
1058
  const videoContent = `${outlookVideoContent}${nonOutlookVideoContent}`;
1014
- const wrapperHtml = `
1015
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
1016
- <tr>
1017
- <td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
1018
- <table border="0" cellpadding="0" cellspacing="0" role="presentation"
1019
- align="${style?.textAlign || "left"}"
1020
- style="
1021
- margin:0;
1022
- max-width:${cellWidthInPx}px;
1023
- width:${percentWidth};
1024
- border-collapse:collapse;
1025
- ">
1026
- <tr>
1027
- <td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
1028
- ${videoContent}
1029
- </td>
1030
- </tr>
1031
- </table>
1032
- </td>
1033
- </tr>
1034
- </table>
1059
+ const wrapperHtml = `
1060
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
1061
+ <tr>
1062
+ <td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
1063
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation"
1064
+ align="${style?.textAlign || "left"}"
1065
+ style="
1066
+ margin:0;
1067
+ max-width:${cellWidthInPx}px;
1068
+ width:${percentWidth};
1069
+ border-collapse:collapse;
1070
+ ">
1071
+ <tr>
1072
+ <td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
1073
+ ${videoContent}
1074
+ </td>
1075
+ </tr>
1076
+ </table>
1077
+ </td>
1078
+ </tr>
1079
+ </table>
1035
1080
  `;
1036
1081
  return wrapperHtml;
1037
1082
  }
@@ -1113,44 +1158,44 @@ async function convertShapeBlock(blockData) {
1113
1158
  let nonMsoContent = "";
1114
1159
  // --- Case 1: Image + Text ---
1115
1160
  if (imageUrl && text) {
1116
- nonMsoContent = `
1117
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1118
- border:${borderWidth}px ${borderStyle} ${borderColor};
1119
- border-radius:${resolvedBorderRadius};
1120
- background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
1121
- overflow:hidden;${alignmentStyle}${customCss || ""}">
1122
- <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};overflow:hidden;padding:6px;box-sizing:border-box;">
1123
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
1124
- ${text}
1125
- </div>
1126
- </div>
1161
+ nonMsoContent = `
1162
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1163
+ border:${borderWidth}px ${borderStyle} ${borderColor};
1164
+ border-radius:${resolvedBorderRadius};
1165
+ background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
1166
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
1167
+ <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};overflow:hidden;padding:6px;box-sizing:border-box;">
1168
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
1169
+ ${text}
1170
+ </div>
1171
+ </div>
1127
1172
  </div>`;
1128
1173
  }
1129
1174
  // --- Case 2: Image only ---
1130
1175
  else if (imageUrl) {
1131
- nonMsoContent = `
1132
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1133
- border:${borderWidth}px ${borderStyle} ${borderColor};
1134
- border-radius:${resolvedBorderRadius};
1135
- overflow:hidden;${alignmentStyle}${customCss || ""}">
1136
- <img src="${imageUrl}" alt="${text || "shape image"}"
1137
- width="${resolvedWidthPx}" height="${resolvedHeightPx}"
1138
- style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
1176
+ nonMsoContent = `
1177
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1178
+ border:${borderWidth}px ${borderStyle} ${borderColor};
1179
+ border-radius:${resolvedBorderRadius};
1180
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
1181
+ <img src="${imageUrl}" alt="${text || "shape image"}"
1182
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
1183
+ style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
1139
1184
  </div>`;
1140
1185
  }
1141
1186
  // --- Case 3: Text only ---
1142
1187
  else {
1143
- nonMsoContent = `
1144
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1145
- background:${finalBackgroundColor};
1146
- border:${borderWidth}px ${borderStyle} ${borderColor};
1147
- border-radius:${resolvedBorderRadius};
1148
- overflow:hidden;${alignmentStyle}${customCss || ""}">
1149
- <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};padding:8px;box-sizing:border-box;">
1150
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
1151
- ${text || ""}
1152
- </div>
1153
- </div>
1188
+ nonMsoContent = `
1189
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1190
+ background:${finalBackgroundColor};
1191
+ border:${borderWidth}px ${borderStyle} ${borderColor};
1192
+ border-radius:${resolvedBorderRadius};
1193
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
1194
+ <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};padding:8px;box-sizing:border-box;">
1195
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
1196
+ ${text || ""}
1197
+ </div>
1198
+ </div>
1154
1199
  </div>`;
1155
1200
  }
1156
1201
  // Outlook (VML) fallback
@@ -1172,17 +1217,45 @@ async function convertShapeBlock(blockData) {
1172
1217
  padding,
1173
1218
  msoBakeImageWithText,
1174
1219
  }, visibilityClass);
1220
+ // Embed block metadata so the HTML importer can reconstruct the Shape block exactly.
1221
+ const shapeProps = encodeBlockProps({
1222
+ shape,
1223
+ width: resolvedWidthPx,
1224
+ height: resolvedHeightPx,
1225
+ shapeColor: String(finalBackgroundColor || '#BEBEBE'),
1226
+ backgroundColor: String(finalBackgroundColor || '#BEBEBE'),
1227
+ borderRadius: borderRadius !== undefined ? borderRadius : 0,
1228
+ borderWidth: borderWidth || 0,
1229
+ borderColor: borderColor || 'transparent',
1230
+ borderStyle: borderStyle || 'solid',
1231
+ imageUrl: imageUrl || '',
1232
+ text: text || '',
1233
+ color: String(color || '#000000'),
1234
+ fontSize: fontSize || 14,
1235
+ textAlign: textAlignStyle,
1236
+ verticalAlign: verticalAlign || 'middle',
1237
+ alignment: alignment || 'left',
1238
+ padding: {
1239
+ top: padding.top || 0,
1240
+ right: padding.right || 0,
1241
+ bottom: padding.bottom || 0,
1242
+ left: padding.left || 0,
1243
+ },
1244
+ hideOnDesktop: Boolean(props.hideOnDesktop),
1245
+ hideOnMobile: Boolean(props.hideOnMobile),
1246
+ customCss: customCss || '',
1247
+ });
1175
1248
  // Combine into table wrapper
1176
- return `
1177
- <table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}">
1178
- <tr>
1179
- <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
1180
- ${outlookContent}
1181
- <!--[if !mso]><!-->
1182
- ${nonMsoContent}
1183
- <!--<![endif]-->
1184
- </td>
1185
- </tr>
1249
+ return `
1250
+ <table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}" data-block-type="shape" data-block-props="${shapeProps}">
1251
+ <tr>
1252
+ <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
1253
+ ${outlookContent}
1254
+ <!--[if !mso]><!-->
1255
+ ${nonMsoContent}
1256
+ <!--<![endif]-->
1257
+ </td>
1258
+ </tr>
1186
1259
  </table>`;
1187
1260
  }
1188
1261
  // ---------- Updated VML builder ----------
@@ -1215,24 +1288,24 @@ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, bo
1215
1288
  const safeFontSize = Math.max(Math.round(textSize), 10);
1216
1289
  // Build the textbox with table/cell for reliable vertical centering in Outlook
1217
1290
  const textboxMarkup = text && !msoHasBakedText
1218
- ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
1219
- <div style="display:table;width:100%;height:100%;">
1220
- <div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
1221
- <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
1222
- ${text}
1223
- </div>
1224
- </div>
1225
- </div>
1291
+ ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
1292
+ <div style="display:table;width:100%;height:100%;">
1293
+ <div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
1294
+ <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
1295
+ ${text}
1296
+ </div>
1297
+ </div>
1298
+ </div>
1226
1299
  </v:textbox>`
1227
1300
  : `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
1228
1301
  // Return VML shape
1229
- return `
1230
- <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
1231
- style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
1232
- ${borderAttrs}
1233
- fill="true" fillcolor="${fillColor}"${extraAttr}>
1234
- ${fillMarkup}
1235
- ${textboxMarkup}
1302
+ return `
1303
+ <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
1304
+ style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
1305
+ ${borderAttrs}
1306
+ fill="true" fillcolor="${fillColor}"${extraAttr}>
1307
+ ${fillMarkup}
1308
+ ${textboxMarkup}
1236
1309
  </v:${tag}>`;
1237
1310
  }
1238
1311
  function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts, visibilityClass) {
@@ -1260,28 +1333,28 @@ function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth
1260
1333
  const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
1261
1334
  // Fix: Properly handle Outlook visibility with conditional comments
1262
1335
  if (shouldHideInOutlook) {
1263
- return `<!--[if !mso]><!-->
1264
- <table align="${align}" border="0" cellpadding="0" cellspacing="0"
1265
- style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
1266
- <tr>
1267
- <td valign="${valign}"
1268
- style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
1269
- ${vml}
1270
- </td>
1271
- </tr>
1272
- </table>
1336
+ return `<!--[if !mso]><!-->
1337
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
1338
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
1339
+ <tr>
1340
+ <td valign="${valign}"
1341
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
1342
+ ${vml}
1343
+ </td>
1344
+ </tr>
1345
+ </table>
1273
1346
  <!--<![endif]-->`;
1274
1347
  }
1275
- return `<!--[if mso]>
1276
- <table align="${align}" border="0" cellpadding="0" cellspacing="0"
1277
- style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
1278
- <tr>
1279
- <td valign="${valign}"
1280
- style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
1281
- ${vml}
1282
- </td>
1283
- </tr>
1284
- </table>
1348
+ return `<!--[if mso]>
1349
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
1350
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
1351
+ <tr>
1352
+ <td valign="${valign}"
1353
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
1354
+ ${vml}
1355
+ </td>
1356
+ </tr>
1357
+ </table>
1285
1358
  <![endif]-->`;
1286
1359
  }
1287
1360
  function convertVerticalDividerBlockToHtml(blockData) {
@@ -1292,18 +1365,28 @@ function convertVerticalDividerBlockToHtml(blockData) {
1292
1365
  perChanges: [],
1293
1366
  pxChanges: allPxAttributes,
1294
1367
  });
1295
- return `
1296
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
1297
- style="${exports.tableCommonStyle} max-width:600px;" class="${visibilityClass}">
1298
- <tr>
1299
- <td style="${outerStyles}; text-align:center; vertical-align:middle;">
1300
- <!--[if mso | IE]>
1301
- <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="${dividerColor}" style="width:${width}px;height:${height}px;" stroke="f"></v:rect>
1302
- <![endif]-->
1303
- <!--[if !mso]><!-->
1304
- <div style="display:inline-block;width:${width}px;height:${height}px;background:${dividerColor};line-height:0;font-size:0;">&nbsp;</div>
1305
- <!--<![endif]-->
1306
- </td>
1307
- </tr>
1368
+ const vDividerProps = encodeBlockProps({
1369
+ width: width || 5,
1370
+ height: height || 100,
1371
+ dividerColor: dividerColor || '#808080',
1372
+ backgroundColor: backgroundColor || '',
1373
+ alignment: 'left',
1374
+ padding: padding || { top: 0, right: 0, bottom: 0, left: 0 },
1375
+ hideOnDesktop: Boolean(props.hideOnDesktop),
1376
+ hideOnMobile: Boolean(props.hideOnMobile),
1377
+ });
1378
+ return `
1379
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
1380
+ style="${exports.tableCommonStyle} max-width:600px;" class="${visibilityClass}" data-block-type="vdivider" data-block-props="${vDividerProps}">
1381
+ <tr>
1382
+ <td style="${outerStyles}; text-align:center; vertical-align:middle;">
1383
+ <!--[if mso | IE]>
1384
+ <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="${dividerColor}" style="width:${width}px;height:${height}px;" stroke="f"></v:rect>
1385
+ <![endif]-->
1386
+ <!--[if !mso]><!-->
1387
+ <div style="display:inline-block;width:${width}px;height:${height}px;background:${dividerColor};line-height:0;font-size:0;">&nbsp;</div>
1388
+ <!--<![endif]-->
1389
+ </td>
1390
+ </tr>
1308
1391
  </table>`;
1309
1392
  }