email-builder-utils 1.1.35 → 1.1.42

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.
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.tableCommonStyle = void 0;
4
4
  exports.convertToHtml = convertToHtml;
5
5
  exports.convertVideoBlock = convertVideoBlock;
6
- const jimp_1 = require("jimp");
7
6
  const types_1 = require("../types");
8
7
  const common_1 = require("./common");
8
+ const fontFallback_1 = require("./fontFallback");
9
9
  const addPxToAttributes = [
10
10
  "fontSize",
11
11
  "lineHeight",
@@ -15,23 +15,13 @@ 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 cleanJson(obj) {
19
- if (typeof obj !== "object" || obj === null)
20
- return obj;
21
- if (Array.isArray(obj))
22
- return obj.map(cleanJson);
23
- return Object.fromEntries(Object.entries(obj)
24
- .filter(([_, value]) => value !== undefined && value !== null && value !== "")
25
- .map(([key, value]) => [key, cleanJson(value)]));
26
- }
27
- function jsonToPlainString(obj) {
28
- if (typeof obj !== "object" || obj === null)
29
- return String(obj);
30
- if (Array.isArray(obj))
31
- return obj.map(jsonToPlainString).join(", ");
32
- return Object.entries(obj)
33
- .map(([key, value]) => `${key}:${jsonToPlainString(value)}; `)
34
- .join("");
18
+ async function loadImageNaturalDimensions(imageUrl) {
19
+ return new Promise((resolve, reject) => {
20
+ const img = new Image();
21
+ img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
22
+ img.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`));
23
+ img.src = imageUrl;
24
+ });
35
25
  }
36
26
  function buildStyles(style, { pxChanges, perChanges }) {
37
27
  if (!style)
@@ -47,7 +37,9 @@ function buildStyles(style, { pxChanges, perChanges }) {
47
37
  "childWidth",
48
38
  "visibility",
49
39
  "hideOnMobile",
50
- "hideOnDesktop"
40
+ "hideOnDesktop",
41
+ "label",
42
+ "alignment",
51
43
  ];
52
44
  if (INVALID_KEYS.includes(key))
53
45
  return;
@@ -66,11 +58,26 @@ function buildStyles(style, { pxChanges, perChanges }) {
66
58
  };
67
59
  value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
68
60
  }
61
+ // Sanitize fontFamily: replace double quotes with single quotes to avoid
62
+ // breaking the surrounding style="..." HTML attribute
63
+ if (key === "fontFamily" && typeof value === "string") {
64
+ value = (0, fontFallback_1.withFontFallback)(value).replace(/"/g, "'");
65
+ }
66
+ // Wrap backgroundImage values in url() if not already wrapped
67
+ if (key === "backgroundImage" && typeof value === "string" && !String(value).startsWith("url(")) {
68
+ value = `url('${value}')`;
69
+ }
70
+ // lineHeight: values >= 4 are pixel values; smaller values are unitless multipliers (e.g. 1.5)
71
+ if (key === "lineHeight" && typeof value === "number") {
72
+ stylesObj["line-height"] = value >= 4 ? `${value}px` : String(value);
73
+ return;
74
+ }
69
75
  const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
70
76
  // FIX 2 — Sanitize invalid px/per values
71
77
  if (pxChanges.includes(key)) {
72
78
  if (typeof value === "number") {
73
- stylesObj[cssKey] = `${value}px`;
79
+ const rounded = Math.round(value * 100) / 100;
80
+ stylesObj[cssKey] = `${rounded}px`;
74
81
  }
75
82
  else if (typeof value === "string" && value.includes("null")) {
76
83
  // Skip invalid styles
@@ -92,12 +99,17 @@ function buildStyles(style, { pxChanges, perChanges }) {
92
99
  stylesObj[cssKey] = value;
93
100
  }
94
101
  });
95
- return `${jsonToPlainString(cleanJson(stylesObj))}${style.customCss || ""}`.trim();
102
+ const parts = Object.entries(stylesObj)
103
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
104
+ .map(([k, v]) => `${k}:${v}`);
105
+ if (style.customCss)
106
+ parts.push(style.customCss);
107
+ return parts.join('; ').trim();
96
108
  }
97
109
  async function convertToHtml(blockData, rootData, cellWidthInPx) {
98
110
  switch (blockData.type) {
99
111
  case types_1.BlockType.TEXT:
100
- return convertTextBlock(blockData);
112
+ return convertTextBlock(blockData, cellWidthInPx);
101
113
  case types_1.BlockType.IMAGE:
102
114
  return await convertImageBlock(blockData, cellWidthInPx);
103
115
  case types_1.BlockType.BUTTON:
@@ -118,23 +130,36 @@ async function convertToHtml(blockData, rootData, cellWidthInPx) {
118
130
  return "";
119
131
  }
120
132
  }
121
- // function appendOutlookSupport(content: string, contentStyle: string) {
122
- // return `
123
- // <table width="100%" style="${tableCommonStyle}"><tr><td style="${contentStyle}">${content}</td></tr></table>
124
- // `;
125
- // }
126
- function appendOutlookSupport(content, contentStyle, className) {
133
+ function appendOutlookSupport(content, contentStyle, className, msoWidth) {
127
134
  const visibilityClass = className || "";
128
135
  const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
129
136
  if (shouldHideInOutlook) {
130
- return `
131
- <!--[if !mso]><!-->
132
- <table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
133
- <!--<![endif]-->
137
+ return `
138
+ <!--[if !mso]><!-->
139
+ <table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
140
+ <!--<![endif]-->
141
+ `;
142
+ }
143
+ // When an explicit pixel width is provided (e.g. inside a column cell), use dual MSO/non-MSO
144
+ // tables. Old Outlook (Word engine) ignores max-width and can resolve width="100%" to the
145
+ // full email width (600px) rather than the column width, causing images/buttons to expand.
146
+ 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]-->
134
159
  `;
135
160
  }
136
- return `
137
- <table width="100%" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
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>
138
163
  `;
139
164
  }
140
165
  function convertDividerBlockToHtml(blockData) {
@@ -153,21 +178,21 @@ function convertDividerBlockToHtml(blockData) {
153
178
  ]
154
179
  .filter(Boolean)
155
180
  .join(" ");
156
- const dividerContent = `
157
- <table
158
- width="${dividerWidth}%"
159
- cellpadding="0"
160
- cellspacing="0"
161
- >
162
- <tr>
163
- <td
164
- height="${thickness}"
165
- style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
166
- >
167
- &nbsp;
168
- </td>
169
- </tr>
170
- </table>
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>
171
196
  `;
172
197
  return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
173
198
  }
@@ -180,28 +205,13 @@ function convertSpacerBlockToHtml(blockData) {
180
205
  });
181
206
  return appendOutlookSupport(``, styles, visibilityClass);
182
207
  }
183
- // function convertTextBlock(blockData: IBlockData) {
184
- // const { style, props } = blockData.data;
185
- // const styles = buildStyles(style, {
186
- // perChanges: [],
187
- // pxChanges: allPxAttributes,
188
- // });
189
- // const text = props.text || "";
190
- // const navigateToUrl = props.navigateToUrl || "";
191
- // const textContent = appendOutlookSupport(
192
- // text.replaceAll(/\n/g, "<br>"),
193
- // styles
194
- // );
195
- // return navigateToUrl
196
- // ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit; text-decoration:none; cursor:pointer;">${textContent}</a>`
197
- // : textContent;
198
- // }
199
- function convertTextBlock(blockData) {
208
+ function convertTextBlock(blockData, cellWidthInPx) {
200
209
  const { style, props } = blockData.data;
201
210
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
202
- const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, ...rest } = style;
211
+ const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, whiteSpace: _whiteSpace, // strip from outer td — pre-wrap on a td preserves editor whitespace
212
+ ...rest } = style;
213
+ // Text box decoration styles (border, background, padding) — no width
203
214
  const textBoxStyle = {
204
- width,
205
215
  backgroundColor,
206
216
  padding,
207
217
  borderRadius,
@@ -213,6 +223,7 @@ function convertTextBlock(blockData) {
213
223
  perChanges: [],
214
224
  pxChanges: allPxAttributes,
215
225
  });
226
+ // Outer td styles: layout only, no typography, no white-space
216
227
  const styles = buildStyles({
217
228
  padding: textContainerPadding,
218
229
  backgroundColor: textContainerBackgroundColor,
@@ -222,244 +233,584 @@ function convertTextBlock(blockData) {
222
233
  pxChanges: allPxAttributes,
223
234
  });
224
235
  const sanitizedText = (props.text ?? "")
225
- .replaceAll(/<p>/g, "<div>")
226
- .replaceAll(/<\/p>/g, "</div>");
236
+ .replace(/<p(\s[^>]*)?>/gi, (_, attrs) => `<div${attrs || ""}>`)
237
+ .replace(/<\/p>/gi, "</div>");
227
238
  const navigateToUrl = props.navigateToUrl || "";
228
- const convertedTextBox = `<div style="display: inline-block; max-width: 100%; box-sizing: border-box; ${convertedTextStyle}">${sanitizedText.replaceAll(/\n/g, "<br>")}</div>`;
229
- const textContent = appendOutlookSupport(convertedTextBox, styles, visibilityClass);
239
+ const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
240
+ // Use display:block + width:100% so text fills the column naturally.
241
+ // display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
242
+ const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${fontSizeStyle}${convertedTextStyle}">${sanitizedText.replaceAll(/\n/g, "<br>")}</div>`;
243
+ const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
244
+ const textContent = appendOutlookSupport(convertedTextBox, styles, visibilityClass, safeCellWidth);
230
245
  return navigateToUrl
231
246
  ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit;text-decoration:none;cursor:pointer;">${textContent}</a>`
232
247
  : textContent;
233
248
  }
234
- async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}) {
235
- const image = await jimp_1.Jimp.read(imageUrl);
236
- const originalWidth = image.bitmap.width;
237
- const originalHeight = image.bitmap.height;
238
- const widthScalingFactor = Math.min(outerContainerWidth / originalWidth, innerContainerWidth / originalWidth);
239
- const scaledWidth = Math.round(originalWidth * widthScalingFactor);
240
- const scaledHeight = Math.round(originalHeight * widthScalingFactor);
249
+ async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
250
+ // OUTLOOK FIX: Use provided dimensions or calculate from image
251
+ let vmlWidth;
252
+ let vmlHeight;
253
+ if (finalWidth && finalHeight) {
254
+ // Use pre-calculated dimensions (preferred for accuracy)
255
+ vmlWidth = finalWidth;
256
+ vmlHeight = finalHeight;
257
+ }
258
+ else if (imageUrl) {
259
+ try {
260
+ const { width: originalWidth, height: originalHeight } = await loadImageNaturalDimensions(imageUrl);
261
+ const widthScalingFactor = Math.min(outerContainerWidth / originalWidth, innerContainerWidth / originalWidth, 1);
262
+ vmlWidth = Math.round(originalWidth * widthScalingFactor);
263
+ vmlHeight = Math.round(originalHeight * widthScalingFactor);
264
+ }
265
+ catch {
266
+ vmlWidth = innerContainerWidth;
267
+ vmlHeight = innerContainerWidth;
268
+ }
269
+ }
270
+ else {
271
+ vmlWidth = innerContainerWidth;
272
+ vmlHeight = innerContainerWidth;
273
+ }
241
274
  const borderWidth = parseInt(style?.borderWidth) || 0;
242
275
  const borderColor = style?.borderColor || "transparent";
243
276
  const borderRadius = parseInt(style?.borderRadius) || 0;
244
277
  const useRoundRect = borderRadius > 0;
245
278
  const arcsize = useRoundRect
246
- ? Math.min(borderRadius / scaledHeight, 1).toFixed(2)
279
+ ? Math.min(borderRadius / vmlHeight, 1).toFixed(2)
247
280
  : "";
248
281
  const borderAttributes = borderWidth > 0
249
282
  ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
250
283
  : `stroked="false"`;
251
- const outlookImage = `<!--[if mso]>
252
- <v:${useRoundRect ? "roundrect" : "rect"} xmlns:v="urn:schemas-microsoft-com:vml"
253
- style="width:${scaledWidth}px;height:${scaledHeight}px;"
254
- ${borderAttributes}
255
- ${useRoundRect ? `arcsize="${arcsize}"` : ""}
256
- fill="true" fillcolor="none">
257
- <v:fill src="${imageUrl}" type="frame" />
258
- <v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>
259
- </v:${useRoundRect ? "roundrect" : "rect"}>
284
+ // OUTLOOK FIX: For Outlook 2019+ (version 2512), VML type="frame" causes stretching
285
+ // Solution: Use simple IMG tag with fixed dimensions for Outlook, only use VML for border radius
286
+ let outlookImage;
287
+ if (useRoundRect && borderRadius > 0) {
288
+ // Use VML for border radius - wrap in table to constrain width for Old Outlook (Word engine)
289
+ // 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>
260
305
  <![endif]-->`;
261
- return `
262
- ${outlookImage}
263
- <!--[if !mso]><!-->
264
- ${content}
265
- <!--<![endif]-->
306
+ }
307
+ else {
308
+ // For images without border radius, wrap in a table with explicit width for Old Outlook (Word engine)
309
+ // This prevents stretching/overflow in Outlook 2007-2019 and Outlook Classic
310
+ const borderStyleAttr = borderWidth > 0
311
+ ? `border: ${borderWidth}px solid ${borderColor};`
312
+ : '';
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>
321
+ <![endif]-->`;
322
+ }
323
+ return `
324
+ ${outlookImage}
325
+ <!--[if !mso]><!-->
326
+ ${content}
327
+ <!--<![endif]-->
266
328
  `;
267
329
  }
268
330
  async function computeScaledDimensions(imageUrl, maxContainerWidthPx) {
269
- const image = await jimp_1.Jimp.read(imageUrl);
270
- const originalWidth = image.bitmap.width;
271
- const originalHeight = image.bitmap.height;
272
- const widthScalingFactor = Math.min(maxContainerWidthPx / originalWidth, 1);
273
- const scaledWidth = Math.round(originalWidth * widthScalingFactor);
274
- const scaledHeight = Math.round(originalHeight * widthScalingFactor);
275
- return { originalWidth, originalHeight, scaledWidth, scaledHeight };
331
+ if (!imageUrl) {
332
+ const w = Math.max(maxContainerWidthPx, 1);
333
+ const h = Math.round(w * (2 / 3));
334
+ return { originalWidth: w, originalHeight: h, scaledWidth: w, scaledHeight: h };
335
+ }
336
+ try {
337
+ const { width: originalWidth, height: originalHeight } = await loadImageNaturalDimensions(imageUrl);
338
+ const widthScalingFactor = Math.min(maxContainerWidthPx / originalWidth, 1);
339
+ const scaledWidth = Math.round(originalWidth * widthScalingFactor);
340
+ const scaledHeight = Math.round(originalHeight * widthScalingFactor);
341
+ return { originalWidth, originalHeight, scaledWidth, scaledHeight };
342
+ }
343
+ catch {
344
+ const w = Math.max(maxContainerWidthPx, 1);
345
+ const h = Math.round(w * (2 / 3));
346
+ return { originalWidth: w, originalHeight: h, scaledWidth: w, scaledHeight: h };
347
+ }
276
348
  }
277
349
  async function convertImageBlock(blockData, cellWidthInPx) {
278
350
  const { style, props } = blockData.data;
279
351
  const { altText, imageUrl, navigateToUrl } = props;
280
352
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
281
353
  const { width, height, objectFit, borderRadius, borderWidth, borderColor, borderStyle, ...containerStyle } = style;
282
- // Ensure border styles are applied only to the container, not the image
283
- const imageStyle = {
284
- width,
285
- height,
286
- objectFit,
287
- borderStyle,
288
- borderRadius: borderRadius,
289
- borderColor,
290
- };
291
354
  // Add border styles to container for fallback clients
292
355
  const containerStyles = buildStyles({
293
356
  ...containerStyle,
294
357
  }, { perChanges: [], pxChanges: addPxToAttributes });
295
- const innerContainerWidth = (((typeof width === "string" ? parseInt(width.replace("%", "")) : width) ||
296
- 100) /
297
- 100) *
298
- (cellWidthInPx -
299
- (style?.padding?.left || 0) -
300
- (style?.padding?.right || 0));
358
+ // OUTLOOK FIX: Ensure cellWidthInPx never exceeds 600px
359
+ const safeCellWidth = Math.min(cellWidthInPx, 600);
360
+ // Parse width percentage (default 100%)
361
+ const widthPercent = typeof width === "string" && width.includes("%")
362
+ ? parseInt(width.replace("%", ""))
363
+ : typeof width === "number"
364
+ ? width
365
+ : 100;
366
+ // OUTLOOK FIX: Calculate inner container width based on safe cell width
367
+ const paddingLeft = style?.padding?.left || 0;
368
+ const paddingRight = style?.padding?.right || 0;
369
+ const availableWidth = safeCellWidth - paddingLeft - paddingRight;
370
+ const innerContainerWidth = Math.round((widthPercent / 100) * availableWidth);
371
+ // Get image dimensions and calculate scaled sizes
301
372
  const { originalWidth, originalHeight, scaledWidth, scaledHeight } = await computeScaledDimensions(imageUrl, innerContainerWidth);
373
+ // OUTLOOK FIX: For Outlook, we need exact pixel dimensions
374
+ // Calculate final dimensions that respect both original size and container
375
+ const finalWidth = Math.min(scaledWidth, innerContainerWidth, originalWidth);
376
+ const finalHeight = Math.round((finalWidth / originalWidth) * originalHeight);
377
+ // Build image styles for modern email clients (non-Outlook)
302
378
  const imageTagStyles = buildStyles({
303
- maxWidth: `${originalWidth}px`, // Limit to original size
304
- maxHeight: `${originalHeight}px`,
305
- ...imageStyle,
379
+ borderStyle,
380
+ borderRadius: borderRadius,
381
+ borderColor,
382
+ borderWidth,
306
383
  }, {
307
- perChanges: addPxOrPerToAttributes,
384
+ perChanges: [],
308
385
  pxChanges: addPxToAttributes,
309
386
  });
310
- const imageElement = `<img src="${imageUrl}" alt="${altText}" width="${scaledWidth}" height="${scaledHeight}" style="${imageTagStyles}; width:100%; height:auto; max-width:${originalWidth}px; max-height:${originalHeight}px;" />`;
387
+ // OUTLOOK FIX: Image element with explicit dimensions
388
+ // Outlook will use width/height attributes, modern clients use CSS
389
+ // Use max-width instead of width:100% to prevent stretching
390
+ const imageElement = `<img src="${imageUrl}" alt="${altText || "Image"}" border="0" width="${finalWidth}" height="${finalHeight}" style="${imageTagStyles}; display:block; max-width:100%; height:auto; line-height: 0;" />`;
311
391
  const percentWidth = typeof width === "string" && width.endsWith("%")
312
392
  ? width
313
393
  : typeof width === "number"
314
394
  ? `${width}%`
315
395
  : "100%";
316
- const nonMsoWrapper = `<div style="display:inline-block; width:${percentWidth}; max-width:${originalWidth}px;">${imageElement}</div>`;
317
- const outlookImage = await appendOutlookForImage(nonMsoWrapper, cellWidthInPx, innerContainerWidth, imageUrl, style);
318
- const imageContent = appendOutlookSupport(outlookImage, containerStyles, visibilityClass);
396
+ // Non-MSO wrapper: display:block removes the phantom inline-baseline gap that
397
+ // display:inline-block creates in Gmail / Apple Mail / Yahoo between images.
398
+ // margin handles alignment since text-align won't move block elements.
399
+ const imgTextAlign = containerStyle.textAlign || "left";
400
+ const imgMargin = imgTextAlign === "center" ? "margin:0 auto;" :
401
+ imgTextAlign === "right" ? "margin-left:auto; margin-right:0;" : "";
402
+ // OUTLOOK FIX: Use finalWidth (the actual displayed size) as max-width so the div
403
+ // doesn't claim more space than the image occupies. originalWidth is the natural
404
+ // image size (e.g. 636px for the Beefree logo rendered at 35px) which was
405
+ // misleadingly large and could confuse some rendering engines.
406
+ const nonMsoWrapper = `<div style="display:block; width:${percentWidth}; max-width:${finalWidth}px; line-height:0; font-size:0; ${imgMargin}">${imageElement}</div>`;
407
+ // OUTLOOK FIX: Generate VML with corrected dimensions
408
+ const outlookImage = await appendOutlookForImage(nonMsoWrapper, safeCellWidth, innerContainerWidth, imageUrl, style, finalWidth, finalHeight);
409
+ const imageContent = appendOutlookSupport(outlookImage, containerStyles, visibilityClass, safeCellWidth);
319
410
  return navigateToUrl
320
411
  ? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block;">${imageContent}</a>`
321
412
  : imageContent;
322
413
  }
323
414
  function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
324
- const { width = 200, height = 44, borderRadius = 0, borderColor = "transparent", borderWidth = 0, buttonColor = "none", buttonPadding = { top: 0, bottom: 0, left: 0, right: 0 }, color = "#000000", fontFamily = "Arial, sans-serif", fontSize = 16, fontWeight = 400, } = buttonStyle;
325
- const borderAttributes = borderWidth > 0
326
- ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
327
- : `stroked="false"`;
328
- return `
329
- <!--[if mso]>
330
- <v:${borderRadius ? "roundrect" : "rect"} xmlns:v="urn:schemas-microsoft-com:vml" href="${navigateToUrl}"
331
- style="height:${height}px;v-text-anchor:middle;width:${width}px;"
332
- arcsize="${borderRadius / height}" ${borderAttributes}
333
- fillcolor="${buttonColor}">
334
- <w:anchorlock/>
335
- <v:textbox inset="${buttonPadding.top}px,${buttonPadding.left}px,${buttonPadding.bottom}px,${buttonPadding.right}px">
336
- <center style="font-family:${fontFamily};font-size:${fontSize}px;font-weight:${fontWeight};color:${color};">
337
- ${text}
338
- </center>
339
- </v:textbox>
340
- </v:${borderRadius ? "roundrect" : "rect"}>
341
- <![endif]-->
342
- <!--[if !mso]><!-->
343
- ${content}
344
- <!--<![endif]-->
345
- `;
415
+ const pad = buttonStyle.buttonPadding || {};
416
+ const padTop = Number.isFinite(pad.top) ? pad.top : 10;
417
+ const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
418
+ const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
419
+ const padRight = Number.isFinite(pad.right) ? pad.right : 20;
420
+ const fontSize = buttonStyle.fontSize || 16;
421
+ const height = typeof buttonStyle.height === "number" && buttonStyle.height > 0
422
+ ? buttonStyle.height
423
+ : null;
424
+ // prevent layout break
425
+ const minHeight = padTop + padBottom + fontSize;
426
+ const finalHeight = height ? Math.max(height, minHeight) : null;
427
+ const borderRadius = buttonStyle.borderRadius || 0;
428
+ const borderColor = buttonStyle.borderColor || "transparent";
429
+ const borderWidth = buttonStyle.borderWidth || 0;
430
+ const borderStyle = buttonStyle.borderStyle || "solid";
431
+ const bgColor = buttonStyle.buttonColor || "transparent";
432
+ const color = buttonStyle.color || "#ffffff";
433
+ const fontFamily = (0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily).replace(/"/g, "'");
434
+ const fontWeight = buttonStyle.fontWeight || 400;
435
+ const width = typeof buttonStyle.width === "number"
436
+ ? `width="${buttonStyle.width}"`
437
+ : "";
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}
483
+ <!--<![endif]-->`;
346
484
  }
347
485
  function convertButtonBlock(blockData) {
348
486
  const { style, props } = blockData.data;
349
487
  const { text, navigateToUrl } = props;
350
- const visibilityClass = (0, common_1.getVisibilityClass)(props);
351
- const { fontFamily, fontSize, fontWeight, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, ...rest } = style;
352
- const buttonStyle = {
353
- width,
354
- height,
355
- fontFamily,
356
- fontSize,
357
- fontWeight,
358
- borderColor,
359
- borderRadius,
360
- borderWidth,
361
- borderStyle,
362
- padding: buttonPadding,
363
- color,
364
- backgroundColor: buttonColor,
488
+ const { fontFamily, fontSize, fontWeight, textAlign, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, alignment, padding, backgroundColor: containerBg, margin, } = style;
489
+ const pad = buttonPadding || {};
490
+ const padTop = Number.isFinite(pad.top) ? pad.top : 10;
491
+ const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
492
+ const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
493
+ const padRight = Number.isFinite(pad.right) ? pad.right : 20;
494
+ const fs = fontSize || 16;
495
+ // prevent layout break
496
+ const minHeight = padTop + padBottom + fs;
497
+ const finalHeight = typeof height === "number" && height > 0
498
+ ? Math.max(height, minHeight)
499
+ : null;
500
+ const safeFF = (0, fontFallback_1.withFontFallback)(fontFamily).replace(/"/g, "'");
501
+ const safeColor = color || "#ffffff";
502
+ const bgColor = buttonColor || "transparent";
503
+ const bdColor = borderColor || "transparent";
504
+ const bdStyle = borderStyle || "solid";
505
+ const bw = borderWidth || 0;
506
+ const br = borderRadius || 0;
507
+ const containerAlign = alignment || textAlign || "left";
508
+ const widthAttr = typeof width === "number"
509
+ ? `width="${width}"`
510
+ : "";
511
+ // ✅ 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>
557
+ `;
558
+ const aligned = containerAlign === "center"
559
+ ? `<center>${buttonTable}</center>`
560
+ : `<div style="text-align:${containerAlign};">${buttonTable}</div>`;
561
+ 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>
572
+ `;
573
+ }
574
+ /**
575
+ * 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).
578
+ */
579
+ function extractCssFallbackColor(customCss) {
580
+ if (!customCss)
581
+ return '';
582
+ const m = customCss.match(/(?:linear|radial|conic)-gradient\([^)]*?(#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|rgba\([^)]+\))/);
583
+ return m?.[1] ?? '';
584
+ }
585
+ function parseGradient(gradient) {
586
+ if (!gradient)
587
+ 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) || [];
591
+ return {
592
+ angle,
593
+ colors,
594
+ fallback: colors[0] || "#ffffff",
365
595
  };
366
- const convertedButtonStyle = buildStyles(buttonStyle, {
367
- perChanges: [],
368
- pxChanges: allPxAttributes,
369
- });
370
- const convertedStyles = buildStyles({ maxWidth: "100%", boxSizing: "border-box", ...rest }, {
371
- perChanges: [],
372
- pxChanges: allPxAttributes,
373
- });
374
- const buttonElement = `<a href="${navigateToUrl}" rel="noreferrer noopener" style="display:inline-block; text-decoration:none; cursor:pointer;"><button style="${convertedButtonStyle}">${text}</button></a>`;
375
- const buttonContent = appendOutlookSupport(appendOutlookForButton(buttonElement, style, navigateToUrl, text), convertedStyles, visibilityClass);
376
- return buttonContent;
596
+ }
597
+ function cssAngleToVml(angle) {
598
+ return (angle + 90) % 360;
377
599
  }
378
600
  async function convertGridBlock(blockData, rootData, cellWidthInPx) {
379
601
  const { style = {}, childrenIds = [], props } = blockData.data;
380
602
  const { columns = 1, cellWidths = [], responsive = true } = props;
381
- const { columnGap = 0, ...restStyle } = style;
603
+ const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
382
604
  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;
608
+ const fallbackBgColor = backgroundColor ||
609
+ parsedGradient?.fallback ||
610
+ extractCssFallbackColor(restStyle.customCss) ||
611
+ "#ffffff";
612
+ const rawBgImageUrl = !isGradient && backgroundImage
613
+ ? String(backgroundImage).replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
614
+ : null;
383
615
  // FIX: avoid table-layout:fixed – causes shrink in many clients
384
- const tableStyles = buildStyles(restStyle, {
616
+ const tableStyles = buildStyles({ backgroundColor, ...restStyle }, {
385
617
  perChanges: [],
386
618
  pxChanges: allPxAttributes,
387
619
  });
388
620
  const total = childrenIds.length;
389
621
  const visualRows = Math.ceil(total / columns);
390
- let html = `
391
- <!--[if mso]>
392
- <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%"
393
- style="border-collapse:separate;border-spacing:${columnGap}px;"
394
- class="${gridVisibilityClass}">
395
- <![endif]-->
396
- <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%"
397
- role="presentation"
398
- style="border-collapse:separate;border-spacing:${columnGap}px; ${tableStyles}; max-width:600px;"
399
- class="${gridVisibilityClass}">
622
+ // OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
623
+ 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))
630
+ : '';
631
+ const msoBgAttr = msoBgColor ? ` bgcolor="${msoBgColor}"` : '';
632
+ 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).
648
+ const innerBgTransparent = (rawBgImageUrl || isGradient)
649
+ ? 'background-color:transparent;'
650
+ : '';
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]-->
400
666
  `;
401
667
  for (let r = 0; r < visualRows; r++) {
402
668
  html += "<tr>";
403
- // COUNT visible cells only
669
+ // COUNT visible cells and find last visible column index
404
670
  let visibleCells = 0;
671
+ let lastVisibleCol = 0;
405
672
  const rowIds = [];
406
673
  for (let c = 0; c < columns; c++) {
407
674
  const idx = r * columns + c;
408
675
  const id = childrenIds[idx] ?? null;
409
676
  rowIds.push(id);
410
- if (id) {
411
- const child = rootData[id];
412
- const isHidden = child?.data?.props?.hideOnDesktop;
413
- if (!isHidden)
414
- visibleCells++;
677
+ const child = id ? rootData[id] : null;
678
+ const isHidden = child?.data?.props?.hideOnDesktop;
679
+ if (!isHidden) {
680
+ visibleCells++;
681
+ lastVisibleCol = c;
415
682
  }
416
683
  }
417
- // FIX: fallback safe-width
418
- const safeWidth = visibleCells > 0 ? Math.min(100 / visibleCells, 50) : Math.min(100 / columns, 50);
684
+ const safeWidth = visibleCells > 0 ? 100 / visibleCells : 100 / columns;
685
+ // Reserve pixel space for spacer tds between visible cells (N-1 gaps for N visible cells)
686
+ const totalGapPx = columnGap * Math.max(visibleCells - 1, 0);
687
+ const adjustedTableWidth = Math.max(msoTableWidth - totalGapPx, 1);
688
+ let totalWidth = 0;
689
+ const cellWidthPercents = [];
419
690
  for (let c = 0; c < columns; c++) {
420
- const idx = r * columns + c;
421
691
  const id = rowIds[c];
422
692
  let widthPercent = cellWidths[c] ?? safeWidth;
423
- // FIX: never exceed reasonable width
424
693
  if (widthPercent <= 0 || widthPercent > 100) {
425
694
  widthPercent = safeWidth;
426
695
  }
427
- // FIX: Cap width percent to prevent overflow in Outlook
428
- widthPercent = Math.min(widthPercent, 50);
696
+ cellWidthPercents.push(widthPercent);
697
+ if (id) {
698
+ const child = rootData[id];
699
+ const isHidden = child?.data?.props?.hideOnDesktop;
700
+ if (!isHidden) {
701
+ totalWidth += widthPercent;
702
+ }
703
+ }
704
+ }
705
+ const scaleFactor = totalWidth > 0 && totalWidth < 100 ? 100 / totalWidth : 1;
706
+ for (let c = 0; c < columns; c++) {
707
+ const id = rowIds[c];
708
+ let widthPercent = cellWidthPercents[c] * scaleFactor;
709
+ widthPercent = Math.min(widthPercent, 100);
710
+ // Cell pixel width is a share of the gap-adjusted table width
711
+ const cellWidthPx = Math.round((widthPercent / 100) * adjustedTableWidth);
429
712
  if (id) {
430
713
  const child = rootData[id];
431
714
  const { style: cellStyle = {}, props: childProps = {} } = child.data;
432
715
  const verticalAlign = cellStyle.verticalAlign || "top";
433
716
  const childVisible = !childProps.hideOnDesktop;
434
717
  const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
435
- // Only render if visible
436
718
  if (childVisible) {
437
- const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, cellWidthInPx);
438
- html += `
439
- <td
440
- width="${Math.round(widthPercent)}%"
441
- class="${[
442
- responsive ? "stack-column" : "",
443
- visibilityClass,
444
- ].filter(Boolean).join(" ")}"
445
- style="vertical-align:${verticalAlign};word-break:break-word;${styles}"
446
- >
447
- ${childHtml}
719
+ const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth);
720
+ // bgcolor on the cell <td> ensures background-color survives Outlook
721
+ // compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
722
+ const cellBgColor = cellStyle.backgroundColor || '';
723
+ 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}
448
731
  </td>`;
732
+ // Spacer td between columns — fixed pixel width, invisible to screen readers
733
+ if (columnGap > 0 && c !== lastVisibleCol) {
734
+ html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
735
+ }
449
736
  }
450
737
  }
451
738
  else {
452
- // SAFE empty cell (keeps layout stable)
453
- html += `
454
- <td width="${Math.round(widthPercent)}%"
455
- ${responsive ? 'class="stack-column"' : ""}
456
- style="vertical-align:top;">
739
+ html += `
740
+ <td width="${cellWidthPx}"
741
+ ${responsive ? 'class="stack-column"' : ""}
742
+ style="width:${cellWidthPx}px;vertical-align:top;">
457
743
  </td>`;
744
+ if (columnGap > 0 && c !== lastVisibleCol) {
745
+ html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
746
+ }
458
747
  }
459
748
  }
460
749
  html += "</tr>";
461
750
  }
462
- html += "</table><!--[if mso]></table><![endif]-->";
751
+ // Close both MSO and non-MSO tables
752
+ html += `
753
+ <!--[if mso]>
754
+ </table>
755
+ <![endif]-->
756
+ <!--[if !mso]><!-->
757
+ </table>
758
+ <!--<![endif]-->
759
+ `;
760
+ // ── Background image: canonical multi-client approach ────────────────────
761
+ //
762
+ // Problem: `background-image` on a <table> element is stripped by:
763
+ // • New Outlook Mac / Windows (Chromium-based app)
764
+ // • Outlook.com
765
+ // • Old Outlook (Word engine) — ignores CSS entirely
766
+ //
767
+ // Solution: wrap the grid in an outer <table><tr><td> where the <td> carries
768
+ // the background. Different clients pick it up via different mechanisms:
769
+ //
770
+ // background="" attribute on <td> → Yahoo Mail, older webmail
771
+ // CSS background-image on <td> → Gmail, Apple Mail, new Outlook Mac ✓
772
+ // VML v:rect inside the <td> → Old Outlook (Word engine) ✓
773
+ //
774
+ // The inner grid tables have NO background so the outer <td> bg shows through.
775
+ if (rawBgImageUrl || isGradient) {
776
+ const vmlFill = isGradient
777
+ ? (() => {
778
+ const vmlAngle = cssAngleToVml(parsedGradient?.angle || 180);
779
+ const c1 = parsedGradient?.fallback || '#ffffff';
780
+ const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
781
+ return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
782
+ })()
783
+ : `<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>
812
+ </table>`;
813
+ }
463
814
  return html;
464
815
  }
465
816
  async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx) {
@@ -470,16 +821,42 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
470
821
  pxChanges: allPxAttributes,
471
822
  });
472
823
  const parts = [];
473
- // FIX: do NOT re-calc px based on parent causes shrinking
474
- const safeCellWidthPx = Math.max(parentCellWidthPx, 20);
824
+ // OUTLOOK FIX: Calculate the actual cell width in pixels based on percentage
825
+ // If parent is 600px and cell is 50%, cell width should be 300px, not 600px
826
+ const cellWidthPx = Math.round((cellWidthPercent / 100) * parentCellWidthPx);
827
+ // Subtract the cell's own padding so children receive the actual content-area width.
828
+ // Old Outlook honours explicit img/table width attributes — if a child is sized to the
829
+ // full column width (ignoring padding) it overflows and expands the column.
830
+ const cellPad = style?.padding || {};
831
+ const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
832
+ const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
833
+ const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
834
+ // OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
835
+ const safeCellWidthPx = Math.min(contentWidthPx, 600);
475
836
  for (const childId of childrenIds) {
476
837
  const child = rootData[childId];
477
838
  if (child) {
478
839
  parts.push(await convertToHtml(child, rootData, safeCellWidthPx));
479
840
  }
480
841
  }
842
+ const borderRadius = style?.borderRadius || 0;
843
+ const bgColor = style?.backgroundColor || "transparent";
844
+ // 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]-->
857
+ `;
481
858
  return {
482
- html: parts.join(""),
859
+ html: wrapped,
483
860
  styles,
484
861
  };
485
862
  }
@@ -544,116 +921,117 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
544
921
  const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
545
922
  const shouldHideInOutlook = hideOnDesktop;
546
923
  const outlookVideoContent = shouldHideInOutlook
547
- ? `<!--[if !mso]><!-->
548
- <v:group xmlns:v="urn:schemas-microsoft-com:vml"
549
- coordsize="${innerContainerWidth},${calculatedHeight}"
550
- href="${videoLink}"
551
- style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
552
- <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;" stroked="t"
553
- strokeweight="${borderWidth}px"
554
- strokecolor="${borderColor}"
555
- ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
556
- >
557
- <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
558
- </v:rect>
559
- <v:shape type="#_x0000_t75"
560
- style="position:absolute;
561
- left:${vmlLeft.toFixed(1)}px;
562
- top:${vmlTop.toFixed(1)}px;
563
- width:${playIconWidth}px;
564
- height:${playIconHeight}px;"
565
- alt="Play" href="${videoLink}" title="${altText || "Video"}"
566
- stroked="f" filled="t">
567
- <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
568
- </v:shape>
569
- </v:group>
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>
570
947
  <!--<![endif]-->`
571
- : `<!--[if mso]>
572
- <v:group xmlns:v="urn:schemas-microsoft-com:vml"
573
- coordsize="${innerContainerWidth},${calculatedHeight}"
574
- href="${videoLink}"
575
- style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
576
- <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px; stroked="t"
577
- strokeweight="${borderWidth}px"
578
- strokecolor="${borderColor}"
579
- ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
580
- >
581
- <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
582
- </v:rect>
583
- <v:shape type="#_x0000_t75"
584
- style="position:absolute;
585
- left:${vmlLeft.toFixed(1)}px;
586
- top:${vmlTop.toFixed(1)}px;
587
- width:${playIconWidth}px;
588
- height:${playIconHeight}px;"
589
- alt="Play" href="${videoLink}" title="${altText || "Video"}"
590
- stroked="f" filled="t">
591
- <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
592
- </v:shape>
593
- </v:group>
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>
594
972
  <![endif]-->`;
595
- const nonOutlookVideoContent = `<!--[if !mso]><!-->
596
- <table
597
- width="${innerContainerWidth}"
598
- cellpadding="0"
599
- cellspacing="0"
600
- border="0"
601
- role="presentation"
602
- align="${style?.textAlign || "left"}"
603
- style="
604
- max-width: ${innerContainerWidth}px;
605
- width: 100%;
606
- height: ${calculatedHeight}px;
607
- background-color: ${style?.backgroundColor || "#FFFFFF"};
608
- background-image: url('${resolvedThumbnail}');
609
- background-size: contain;
610
- background-position: center;
611
- background-repeat: no-repeat;
612
- box-sizing: border-box;
613
- border: ${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};
614
- border-radius: ${borderRadius}px;
615
- "
616
- >
617
- <tr>
618
- <td style="padding: 0; height: ${calculatedHeight}px; text-align: center; vertical-align: middle;" valign="middle">
619
- <a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
620
- <img
621
- src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
622
- width="${playIconWidth}"
623
- alt="Play"
624
- style="display: block;
625
- border: 0;
626
- outline: none;
627
- text-decoration: none;
628
- height: auto;"
629
- />
630
- </a>
631
- </td>
632
- </tr>
633
- </table>
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>
634
1012
  <!--<![endif]-->`;
635
1013
  const videoContent = `${outlookVideoContent}${nonOutlookVideoContent}`;
636
- const wrapperHtml = `
637
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
638
- <tr>
639
- <td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
640
- <table border="0" cellpadding="0" cellspacing="0" role="presentation"
641
- align="${style?.textAlign || "left"}"
642
- style="
643
- margin:0;
644
- max-width:${cellWidthInPx}px;
645
- width:${percentWidth};
646
- border-collapse:collapse;
647
- ">
648
- <tr>
649
- <td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
650
- ${videoContent}
651
- </td>
652
- </tr>
653
- </table>
654
- </td>
655
- </tr>
656
- </table>
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>
657
1035
  `;
658
1036
  return wrapperHtml;
659
1037
  }
@@ -735,48 +1113,48 @@ async function convertShapeBlock(blockData) {
735
1113
  let nonMsoContent = "";
736
1114
  // --- Case 1: Image + Text ---
737
1115
  if (imageUrl && text) {
738
- nonMsoContent = `
739
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
740
- border:${borderWidth}px ${borderStyle} ${borderColor};
741
- border-radius:${resolvedBorderRadius};
742
- background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
743
- overflow:hidden;${alignmentStyle}${customCss || ""}">
744
- <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};overflow:hidden;padding:6px;box-sizing:border-box;">
745
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
746
- ${text}
747
- </div>
748
- </div>
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>
749
1127
  </div>`;
750
1128
  }
751
1129
  // --- Case 2: Image only ---
752
1130
  else if (imageUrl) {
753
- nonMsoContent = `
754
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
755
- border:${borderWidth}px ${borderStyle} ${borderColor};
756
- border-radius:${resolvedBorderRadius};
757
- overflow:hidden;${alignmentStyle}${customCss || ""}">
758
- <img src="${imageUrl}" alt="${text || "shape image"}"
759
- width="${resolvedWidthPx}" height="${resolvedHeightPx}"
760
- style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
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;" />
761
1139
  </div>`;
762
1140
  }
763
1141
  // --- Case 3: Text only ---
764
1142
  else {
765
- nonMsoContent = `
766
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
767
- background:${finalBackgroundColor};
768
- border:${borderWidth}px ${borderStyle} ${borderColor};
769
- border-radius:${resolvedBorderRadius};
770
- overflow:hidden;${alignmentStyle}${customCss || ""}">
771
- <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};padding:8px;box-sizing:border-box;">
772
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
773
- ${text || ""}
774
- </div>
775
- </div>
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>
776
1154
  </div>`;
777
1155
  }
778
1156
  // Outlook (VML) fallback
779
- const outlookContent = await appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
1157
+ const outlookContent = appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
780
1158
  shape,
781
1159
  imageUrl,
782
1160
  backgroundColor,
@@ -795,16 +1173,16 @@ async function convertShapeBlock(blockData) {
795
1173
  msoBakeImageWithText,
796
1174
  }, visibilityClass);
797
1175
  // Combine into table wrapper
798
- return `
799
- <table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}">
800
- <tr>
801
- <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
802
- ${outlookContent}
803
- <!--[if !mso]><!-->
804
- ${nonMsoContent}
805
- <!--<![endif]-->
806
- </td>
807
- </tr>
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>
808
1186
  </table>`;
809
1187
  }
810
1188
  // ---------- Updated VML builder ----------
@@ -837,24 +1215,24 @@ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, bo
837
1215
  const safeFontSize = Math.max(Math.round(textSize), 10);
838
1216
  // Build the textbox with table/cell for reliable vertical centering in Outlook
839
1217
  const textboxMarkup = text && !msoHasBakedText
840
- ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
841
- <div style="display:table;width:100%;height:100%;">
842
- <div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
843
- <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
844
- ${text}
845
- </div>
846
- </div>
847
- </div>
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>
848
1226
  </v:textbox>`
849
1227
  : `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
850
1228
  // Return VML shape
851
- return `
852
- <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
853
- style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
854
- ${borderAttrs}
855
- fill="true" fillcolor="${fillColor}"${extraAttr}>
856
- ${fillMarkup}
857
- ${textboxMarkup}
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}
858
1236
  </v:${tag}>`;
859
1237
  }
860
1238
  function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts, visibilityClass) {
@@ -882,53 +1260,50 @@ function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth
882
1260
  const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
883
1261
  // Fix: Properly handle Outlook visibility with conditional comments
884
1262
  if (shouldHideInOutlook) {
885
- return `<!--[if !mso]><!-->
886
- <table align="${align}" border="0" cellpadding="0" cellspacing="0"
887
- style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
888
- <tr>
889
- <td valign="${valign}"
890
- style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
891
- ${vml}
892
- </td>
893
- </tr>
894
- </table>
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>
895
1273
  <!--<![endif]-->`;
896
1274
  }
897
- return `<!--[if mso]>
898
- <table align="${align}" border="0" cellpadding="0" cellspacing="0"
899
- style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
900
- <tr>
901
- <td valign="${valign}"
902
- style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
903
- ${vml}
904
- </td>
905
- </tr>
906
- </table>
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>
907
1285
  <![endif]-->`;
908
1286
  }
909
1287
  function convertVerticalDividerBlockToHtml(blockData) {
910
1288
  const { style, props } = blockData.data;
911
- const { width, height, dividerColor, ...rest } = style;
1289
+ const { width, height, dividerColor, padding, backgroundColor } = style;
912
1290
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
913
- // Convert other styles to inline-safe HTML attributes
914
- const convertedStyle = buildStyles(rest, {
1291
+ const outerStyles = buildStyles({ padding, backgroundColor }, {
915
1292
  perChanges: [],
916
1293
  pxChanges: allPxAttributes,
917
1294
  });
918
- // Outlook-safe vertical divider
919
- const dividerContent = `
920
- <table cellpadding="0" cellspacing="0" border="0" align="center" style="width:auto; ${convertedStyle}">
921
- <tr>
922
- <td style="vertical-align: middle; text-align: center;">
923
- <!--[if mso | IE]>
924
- <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="${dividerColor}" style="width:${width}px;height:${height}px;" stroke="f"></v:rect>
925
- <![endif]-->
926
- <!--[if !mso]><!-- -->
927
- <div style="display:inline-block;width:${width}px;height:${height}px;background:${dividerColor};line-height:0;font-size:0;">&nbsp;</div>
928
- <!--<![endif]-->
929
- </td>
930
- </tr>
931
- </table>
932
- `;
933
- return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
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>
1308
+ </table>`;
934
1309
  }