email-builder-utils 1.1.44 → 1.1.45

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.
@@ -23,7 +23,7 @@ interface IBlockData {
23
23
  childrenIds?: Array<string>;
24
24
  };
25
25
  }
26
- export declare const tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
26
+ export declare const tableCommonStyle = "border-collapse:collapse; table-layout:fixed";
27
27
  export declare function convertToHtml(blockData: IBlockData, rootData: any, cellWidthInPx: number): Promise<string>;
28
28
  export declare function convertVideoBlock(blockData: any, cellWidthInPx: number): Promise<string>;
29
29
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAIrC,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,cAAc,CAAC;QACtB,KAAK,EAAE,GAAG,CAAC;QACX,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KAC7B,CAAC;CACH;AAYD,eAAO,MAAM,gBAAgB,kDAAkD,CAAC;AA2GhF,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AA2/BD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBA+K5E"}
1
+ {"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAIrC,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,cAAc,CAAC;QACtB,KAAK,EAAE,GAAG,CAAC;QACX,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KAC7B,CAAC;CACH;AAYD,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AA2I/E,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AA0lCD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBA+K5E"}
@@ -14,7 +14,7 @@ const addPxToAttributes = [
14
14
  ];
15
15
  const addPxOrPerToAttributes = ["width", "height"];
16
16
  const allPxAttributes = [...addPxToAttributes, ...addPxOrPerToAttributes];
17
- exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
17
+ exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed";
18
18
  function encodeBlockProps(props) {
19
19
  return JSON.stringify(props)
20
20
  .replace(/&/g, '&amp;')
@@ -28,6 +28,38 @@ async function loadImageNaturalDimensions(imageUrl) {
28
28
  img.src = imageUrl;
29
29
  });
30
30
  }
31
+ const GENERIC_FONT_FAMILIES = new Set([
32
+ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
33
+ 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace',
34
+ 'ui-rounded', 'emoji', 'math', 'fangsong',
35
+ ]);
36
+ /**
37
+ * Normalises a CSS font-family string so every multi-word family name is wrapped
38
+ * in single quotes — safe inside double-quoted HTML style attributes.
39
+ * Handles already-quoted names (single or double), generic keywords, and any
40
+ * number of comma-separated families.
41
+ */
42
+ function sanitizeFontFamily(fontFamily) {
43
+ if (!fontFamily)
44
+ return fontFamily;
45
+ return fontFamily
46
+ .split(',')
47
+ .map(font => {
48
+ const trimmed = font.trim();
49
+ // Strip any surrounding quotes (single or double) from either end
50
+ const unquoted = trimmed.replace(/^["']|["']$/g, '').trim();
51
+ if (!unquoted)
52
+ return '';
53
+ // Generic families and single-token names need no quotes
54
+ if (GENERIC_FONT_FAMILIES.has(unquoted.toLowerCase()) || !/\s/.test(unquoted)) {
55
+ return unquoted;
56
+ }
57
+ // Multi-word font name: wrap in single quotes, escaping any embedded single quotes
58
+ return `'${unquoted.replace(/'/g, "\\'")}'`;
59
+ })
60
+ .filter(Boolean)
61
+ .join(', ');
62
+ }
31
63
  function buildStyles(style, { pxChanges, perChanges }) {
32
64
  if (!style)
33
65
  style = {};
@@ -64,7 +96,7 @@ function buildStyles(style, { pxChanges, perChanges }) {
64
96
  value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
65
97
  }
66
98
  if (key === "fontFamily" && typeof value === "string") {
67
- value = (0, fontFallback_1.withFontFallback)(value).replace(/"/g, "'");
99
+ value = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(value));
68
100
  }
69
101
  // Wrap backgroundImage values in url() if not already wrapped — skip gradients
70
102
  if (key === "backgroundImage" && typeof value === "string"
@@ -170,12 +202,17 @@ ${content}
170
202
  function convertDividerBlockToHtml(blockData) {
171
203
  const { style, props } = blockData.data;
172
204
  const { hideOnMobile, hideOnDesktop } = props;
173
- const { thickness, dividerColor, width, ...rest } = style;
205
+ const { thickness, dividerColor, width, alignment, ...rest } = style;
174
206
  const convertedStyle = buildStyles(rest, {
175
207
  perChanges: [],
176
208
  pxChanges: allPxAttributes,
177
209
  });
178
- const dividerWidth = width || "100%";
210
+ const dividerWidth = width || 100;
211
+ const alignAttr = alignment === 'center' ? 'center' : alignment === 'right' ? 'right' : 'left';
212
+ // Append text-align so the import parser can recover alignment via inheritance
213
+ const contentStyle = convertedStyle
214
+ ? `${convertedStyle}; text-align:${alignAttr};`
215
+ : `text-align:${alignAttr};`;
179
216
  // Build class name based on visibility
180
217
  const visibilityClass = [
181
218
  hideOnMobile ? "hide-mobile" : "",
@@ -185,6 +222,7 @@ function convertDividerBlockToHtml(blockData) {
185
222
  .join(" ");
186
223
  const dividerContent = `
187
224
  <table
225
+ align="${alignAttr}"
188
226
  width="${dividerWidth}%"
189
227
  cellpadding="0"
190
228
  cellspacing="0"
@@ -192,14 +230,14 @@ function convertDividerBlockToHtml(blockData) {
192
230
  <tr>
193
231
  <td
194
232
  height="${thickness}"
195
- style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
233
+ style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth}%;"
196
234
  >
197
235
  &nbsp;
198
236
  </td>
199
237
  </tr>
200
238
  </table>
201
239
  `;
202
- return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
240
+ return appendOutlookSupport(dividerContent, contentStyle, visibilityClass);
203
241
  }
204
242
  function convertSpacerBlockToHtml(blockData) {
205
243
  const { style, props } = blockData.data;
@@ -242,13 +280,28 @@ function convertTextBlock(blockData, cellWidthInPx) {
242
280
  .replace(/<\/p>/gi, "</div>");
243
281
  const navigateToUrl = props.navigateToUrl || "";
244
282
  const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
283
+ // Email clients apply `a { color: blue }` which overrides inherited color.
284
+ // Inject the block color directly onto <a> tags that don't already have one.
285
+ const blockTextColor = rest.color;
286
+ const processedText = blockTextColor
287
+ ? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = '') => {
288
+ if (/style\s*=\s*["'][^"']*\bcolor\s*:/i.test(attrs))
289
+ return match;
290
+ if (/\bstyle\s*=/i.test(attrs)) {
291
+ return `<a${attrs.replace(/(\bstyle\s*=\s*["'])/, `$1color:${blockTextColor};`)}>`;
292
+ }
293
+ return `<a${attrs} style="color:${blockTextColor};">`;
294
+ })
295
+ : sanitizedText;
296
+ const colorStyle = blockTextColor ? `color:${blockTextColor};` : '';
245
297
  // Use display:block + width:100% so text fills the column naturally.
246
298
  // display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
247
- const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${fontSizeStyle}${convertedTextStyle}">${sanitizedText.replaceAll(/\n/g, "<br>")}</div>`;
299
+ const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
248
300
  const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
249
301
  const textContent = appendOutlookSupport(convertedTextBox, styles, visibilityClass, safeCellWidth);
302
+ const linkColorStyle = blockTextColor ? `color:${blockTextColor};` : 'color:inherit;';
250
303
  return navigateToUrl
251
- ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit;text-decoration:none;cursor:pointer;">${textContent}</a>`
304
+ ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${textContent}</a>`
252
305
  : textContent;
253
306
  }
254
307
  async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
@@ -435,7 +488,7 @@ function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
435
488
  const borderStyle = buttonStyle.borderStyle || "solid";
436
489
  const bgColor = buttonStyle.buttonColor || "transparent";
437
490
  const color = buttonStyle.color || "#ffffff";
438
- const fontFamily = (0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily).replace(/"/g, "'");
491
+ const fontFamily = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily));
439
492
  const fontWeight = buttonStyle.fontWeight || 400;
440
493
  const width = typeof buttonStyle.width === "number"
441
494
  ? `width="${buttonStyle.width}"`
@@ -502,7 +555,7 @@ function convertButtonBlock(blockData) {
502
555
  const finalHeight = typeof height === "number" && height > 0
503
556
  ? Math.max(height, minHeight)
504
557
  : null;
505
- const safeFF = (0, fontFallback_1.withFontFallback)(fontFamily).replace(/"/g, "'");
558
+ const safeFF = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(fontFamily));
506
559
  const safeColor = color || "#ffffff";
507
560
  const bgColor = buttonColor || "transparent";
508
561
  const bdColor = borderColor || "transparent";
@@ -684,10 +737,29 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
684
737
  : customCssStr;
685
738
  // Build inner table styles — when gradient/bg-image is on the outer wrapper, strip
686
739
  // background props from the inner table so the outer <td> background shows through.
687
- const innerRestStyle = (rawBgImageUrl || isGradient)
740
+ const innerRestStyleRaw = (rawBgImageUrl || isGradient)
688
741
  ? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
689
742
  : { ...restStyle, customCss: innerCustomCss };
690
- const tableStyles = buildStyles({ backgroundColor: (rawBgImageUrl || isGradient) ? undefined : backgroundColor, ...innerRestStyle }, {
743
+ // Extract border/radius props applied via a div wrapper for non-MSO clients so that
744
+ // border-radius is honoured (Gmail/Outlook compose strip border-radius from <table>).
745
+ const { borderRadius, border, borderColor, borderWidth, borderStyle: borderStyleProp, ...innerRestStyle } = innerRestStyleRaw;
746
+ const divBorderParts = [];
747
+ if (borderRadius)
748
+ divBorderParts.push(`border-radius:${typeof borderRadius === 'number' ? borderRadius + 'px' : borderRadius};`, `overflow:hidden;`);
749
+ if (border) {
750
+ divBorderParts.push(`border:${border};`);
751
+ }
752
+ else if (borderWidth || borderColor || borderStyleProp) {
753
+ const bw = borderWidth ? (typeof borderWidth === 'number' ? borderWidth + 'px' : borderWidth) : '1px';
754
+ const bs = borderStyleProp || 'solid';
755
+ const bc = borderColor || '#000000';
756
+ divBorderParts.push(`border:${bw} ${bs} ${bc};`);
757
+ }
758
+ const divBorderStyle = divBorderParts.join(' ');
759
+ const tableBgForNonMso = divBorderStyle
760
+ ? 'transparent'
761
+ : ((rawBgImageUrl || isGradient) ? undefined : backgroundColor);
762
+ const tableStyles = buildStyles({ backgroundColor: tableBgForNonMso, ...innerRestStyle }, {
691
763
  perChanges: [],
692
764
  pxChanges: allPxAttributes,
693
765
  });
@@ -707,7 +779,15 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
707
779
  const innerBgTransparent = (rawBgImageUrl || isGradient)
708
780
  ? 'background-color:transparent;'
709
781
  : '';
710
- const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor ? ` bgcolor="${backgroundColor}"` : '';
782
+ const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor && !divBorderStyle ? ` bgcolor="${backgroundColor}"` : '';
783
+ // When divBorderStyle is set the non-MSO <table> is transparent, so the Grid's
784
+ // backgroundColor must move onto the div wrapper — otherwise it vanishes in modern clients.
785
+ // Skip this for bg-image/gradient blocks; they apply their background via a separate wrapper.
786
+ const divWrapBg = divBorderStyle && backgroundColor && !rawBgImageUrl && !isGradient
787
+ ? ` background-color:${backgroundColor};`
788
+ : '';
789
+ const divWrapOpen = divBorderStyle ? `<div style="${divBorderStyle}${divWrapBg}">` : '';
790
+ const divWrapClose = divBorderStyle ? `</div>` : '';
711
791
  let html = `
712
792
  <!--[if mso]>
713
793
  <table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
@@ -773,7 +853,7 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
773
853
  const childVisible = !childProps.hideOnDesktop;
774
854
  const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
775
855
  if (childVisible) {
776
- const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth);
856
+ const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth, Boolean(divBorderStyle));
777
857
  // bgcolor on the cell <td> ensures background-color survives Outlook
778
858
  // compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
779
859
  const cellBgColor = cellStyle.backgroundColor || '';
@@ -843,10 +923,11 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
843
923
  style="border-collapse:collapse;width:${msoTableWidth}px;">
844
924
  <tr>
845
925
  <td width="${msoTableWidth}" bgcolor="${fallbackBgColor}" valign="top"
926
+ ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
846
927
  style="
847
928
  width:${msoTableWidth}px;
848
929
  background-color:${fallbackBgColor};
849
- ${isGradient ? `background:${effectiveGradient};` : `background:url('${rawBgImageUrl}') center/cover no-repeat;`}
930
+ ${isGradient ? `background:${effectiveGradient};` : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`}
850
931
  ">
851
932
 
852
933
  <!--[if gte mso 9]>
@@ -868,12 +949,32 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
868
949
  </tr>
869
950
  </table>`;
870
951
  }
952
+ // Wrap the entire grid (including any bg-image outer table) in a div when the block
953
+ // has border/radius. An unconditional <div> is used — not gated behind <!--[if !mso]>-->
954
+ // — so Gmail compose paste renders the border-radius reliably. Old Outlook ignores
955
+ // border-radius on <div> but still shows the rectangular border; new Outlook works fully.
956
+ if (divBorderStyle)
957
+ html = `${divWrapOpen}${html}${divWrapClose}`;
871
958
  return html;
872
959
  }
873
- async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx) {
960
+ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
874
961
  const { style = {}, childrenIds = [], props = {} } = blockData.data;
875
962
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
876
- const styles = buildStyles(style, {
963
+ // Extract border + radius from style so they move to the div wrapper (not the <td>).
964
+ // Gmail strips border-radius from <td> but honours it on <div>. By putting border and
965
+ // radius on the same unconditional <div>, the rounded card border renders in all clients.
966
+ // The <td> keeps bgcolor (via attribute) for Old Outlook background fallback.
967
+ const { borderRadius: cellBorderRadius, borderWidth: cellBorderWidth, borderStyle: cellBorderStyleProp, borderColor: cellBorderColor, border: cellBorderShorthand, ...styleWithoutBorder } = style;
968
+ // backgroundColor must stay on the div wrapper (not the <td>) in two cases:
969
+ // 1. Cell has its own border-radius — the div's overflow:hidden clips the background.
970
+ // 2. Parent grid has a border div (divBorderStyle) — the grid's overflow:hidden clips it.
971
+ // In both cases, the rectangular <td> background bleeds through rounded corners if kept
972
+ // in CSS, creating visible corner squares. The bgcolor attribute stays for Outlook fallback.
973
+ const stripBgFromTd = Boolean(cellBorderRadius) || parentGridHasBorder;
974
+ const styleForTd = stripBgFromTd
975
+ ? { ...styleWithoutBorder, backgroundColor: 'transparent' }
976
+ : styleWithoutBorder;
977
+ const styles = buildStyles(styleForTd, {
877
978
  perChanges: [],
878
979
  pxChanges: allPxAttributes,
879
980
  });
@@ -884,7 +985,7 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
884
985
  // Subtract the cell's own padding so children receive the actual content-area width.
885
986
  // Old Outlook honours explicit img/table width attributes — if a child is sized to the
886
987
  // full column width (ignoring padding) it overflows and expands the column.
887
- const cellPad = style?.padding || {};
988
+ const cellPad = styleWithoutBorder?.padding || {};
888
989
  const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
889
990
  const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
890
991
  const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
@@ -896,22 +997,35 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
896
997
  parts.push(await convertToHtml(child, rootData, safeCellWidthPx));
897
998
  }
898
999
  }
899
- const borderRadius = style?.borderRadius || 0;
900
- const bgColor = style?.backgroundColor || "transparent";
901
- // IMPORTANT: radius only for non-Outlook
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]-->
914
- `;
1000
+ const borderRadius = cellBorderRadius || 0;
1001
+ const bgColor = styleWithoutBorder?.backgroundColor || "transparent";
1002
+ // Build border CSS for the div wrapper.
1003
+ // When the parent grid already has a divBorderStyle wrapper (border + border-radius +
1004
+ // overflow:hidden), the cell must NOT duplicate the same border/radius — that causes
1005
+ // two concentric borders of the same colour (double-border). The grid's wrapper div
1006
+ // already provides the visual container; the cell div only needs background-color.
1007
+ const cellDivBorderParts = [];
1008
+ if (!parentGridHasBorder) {
1009
+ if (borderRadius)
1010
+ cellDivBorderParts.push(`border-radius:${typeof borderRadius === 'number' ? borderRadius + 'px' : borderRadius};`, `overflow:hidden;`);
1011
+ if (cellBorderShorthand) {
1012
+ cellDivBorderParts.push(`border:${cellBorderShorthand};`);
1013
+ }
1014
+ else if (cellBorderWidth || cellBorderColor || cellBorderStyleProp) {
1015
+ const bw = cellBorderWidth ? (typeof cellBorderWidth === 'number' ? cellBorderWidth + 'px' : cellBorderWidth) : '1px';
1016
+ const bs = cellBorderStyleProp || 'solid';
1017
+ const bc = cellBorderColor || '#000000';
1018
+ cellDivBorderParts.push(`border:${bw} ${bs} ${bc};`);
1019
+ }
1020
+ }
1021
+ const cellDivBorderStyle = cellDivBorderParts.join(' ');
1022
+ // Unconditional div — visible to all clients (Gmail, Outlook new/old, Apple Mail).
1023
+ // background-color on the div covers modern clients; bgcolor on <td> covers Old Outlook.
1024
+ const divStyleParts = [`background-color:${bgColor};`];
1025
+ if (cellDivBorderStyle)
1026
+ divStyleParts.push(cellDivBorderStyle);
1027
+ const divStyleStr = divStyleParts.join(' ');
1028
+ const wrapped = `<div style="${divStyleStr}">${parts.join("")}</div>`;
915
1029
  return {
916
1030
  html: wrapped,
917
1031
  styles,
@@ -1140,16 +1254,6 @@ async function convertShapeBlock(blockData) {
1140
1254
  justify: "justify",
1141
1255
  };
1142
1256
  const textAlignStyle = textAlignMap[textAlign] || "center";
1143
- const flexJustify = textAlign === "left"
1144
- ? "flex-start"
1145
- : textAlign === "right"
1146
- ? "flex-end"
1147
- : "center";
1148
- const flexAlign = verticalAlign === "top"
1149
- ? "flex-start"
1150
- : verticalAlign === "bottom"
1151
- ? "flex-end"
1152
- : "center";
1153
1257
  // --- Text styling ---
1154
1258
  const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;color:${color};`;
1155
1259
  // ============================
@@ -1162,13 +1266,22 @@ async function convertShapeBlock(blockData) {
1162
1266
  <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1163
1267
  border:${borderWidth}px ${borderStyle} ${borderColor};
1164
1268
  border-radius:${resolvedBorderRadius};
1165
- background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
1269
+ background-color:${finalBackgroundColor};
1270
+ background-image:url('${imageUrl}');
1271
+ background-position:center center;
1272
+ background-size:cover;
1273
+ background-repeat:no-repeat;
1166
1274
  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>
1275
+ <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
1276
+ style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
1277
+ <tr>
1278
+ <td align="${textAlignStyle}" valign="${verticalAlign}"
1279
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
1280
+ style="padding:6px;vertical-align:${verticalAlign};text-align:${textAlignStyle};overflow:hidden;box-sizing:border-box;">
1281
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text}</div>
1282
+ </td>
1283
+ </tr>
1284
+ </table>
1172
1285
  </div>`;
1173
1286
  }
1174
1287
  // --- Case 2: Image only ---
@@ -1187,15 +1300,20 @@ async function convertShapeBlock(blockData) {
1187
1300
  else {
1188
1301
  nonMsoContent = `
1189
1302
  <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1190
- background:${finalBackgroundColor};
1303
+ background-color:${finalBackgroundColor};
1191
1304
  border:${borderWidth}px ${borderStyle} ${borderColor};
1192
1305
  border-radius:${resolvedBorderRadius};
1193
1306
  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>
1307
+ <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
1308
+ style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
1309
+ <tr>
1310
+ <td align="${textAlignStyle}" valign="${verticalAlign}"
1311
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
1312
+ style="padding:8px;vertical-align:${verticalAlign};text-align:${textAlignStyle};box-sizing:border-box;">
1313
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text || ""}</div>
1314
+ </td>
1315
+ </tr>
1316
+ </table>
1199
1317
  </div>`;
1200
1318
  }
1201
1319
  // Outlook (VML) fallback
@@ -1377,7 +1495,7 @@ function convertVerticalDividerBlockToHtml(blockData) {
1377
1495
  });
1378
1496
  return `
1379
1497
  <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}">
1498
+ style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}" data-block-type="vdivider" data-block-props="${vDividerProps}">
1381
1499
  <tr>
1382
1500
  <td style="${outerStyles}; text-align:center; vertical-align:middle;">
1383
1501
  <!--[if mso | IE]>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-builder-utils",
3
- "version": "1.1.44",
3
+ "version": "1.1.45",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [