email-builder-utils 1.1.49 → 1.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,2 @@
1
- export declare function appendOutlookForButton(buttonData: {
2
- style: any;
3
- text: string;
4
- navigateToUrl: string;
5
- }): {
6
- innerContent: string;
7
- computed: {
8
- fs: any;
9
- fontWeight: any;
10
- containerAlign: any;
11
- padTop: any;
12
- padRight: any;
13
- padBottom: any;
14
- padLeft: any;
15
- explicitWidth: any;
16
- vmlWidth: number;
17
- safeColor: any;
18
- bgColor: any;
19
- safeFF: string;
20
- finalHeight: any;
21
- explicitHeight: any;
22
- bw: any;
23
- br: any;
24
- bdColor: any;
25
- bdStyle: any;
26
- };
27
- };
28
1
  export declare function convertButtonBlock(blockData: any): string;
29
2
  //# sourceMappingURL=button.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/button.ts"],"names":[],"mappings":"AAIA,wBAAgB,sBAAsB,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;EA6ErG;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,GAAG,UAgEhD"}
1
+ {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/button.ts"],"names":[],"mappings":"AA6IA,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,GAAG,UAuFhD"}
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appendOutlookForButton = appendOutlookForButton;
4
3
  exports.convertButtonBlock = convertButtonBlock;
5
4
  const buildStyles_1 = require("../buildStyles");
6
5
  const fontFallback_1 = require("../fontFallback");
@@ -26,27 +25,55 @@ function appendOutlookForButton(buttonData) {
26
25
  const fontWeight = style.fontWeight || 400;
27
26
  const containerAlign = style.alignment || style.textAlign || "left";
28
27
  const explicitWidth = typeof style.width === "number" && style.width > 0 ? style.width : 0;
28
+ // VML requires explicit pixel width — estimate from text + padding when not set.
29
+ // Slightly conservative multiplier to reduce clipping risk in Outlook.
29
30
  const estimatedTextWidth = Math.ceil((text || "").length * fs * 0.62);
30
31
  const vmlWidthBase = explicitWidth || Math.max(120, estimatedTextWidth + padLeft + padRight + bw * 2);
32
+ // For pill buttons (large radius), ensure enough width so the curved ends don't encroach.
31
33
  const minPillWidth = br > 0 ? Math.ceil(finalHeight * 2) : 0;
32
34
  const vmlWidth = Math.max(vmlWidthBase, minPillWidth);
33
35
  const borderCss = bw > 0 ? `border:${bw}px ${bdStyle} ${bdColor};` : "";
34
- const widthCss = explicitWidth ? `width:${explicitWidth}px;` : "";
35
- const nonMsoVerticalSizing = explicitHeight > 0
36
- ? `height:${finalHeight}px;line-height:${finalHeight}px;padding:0 ${padRight}px 0 ${padLeft}px;`
37
- : `padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;line-height:${fs}px;`;
36
+ // Empty href triggers New Outlook's link sanitizer to strip surrounding styles.
37
+ const safeHref = navigateToUrl && navigateToUrl.trim() ? navigateToUrl : "#";
38
+ // ── Non-MSO fallback ──
39
+ // All visual box styling (bg, border, radius, width, height) lives on the <td>.
40
+ // New Outlook (and many web mail clients) strip these from <a> inline styles,
41
+ // treating anchors as inline text. The <td> is a block container and is preserved.
42
+ // The <a> fills the cell so the entire button is the click target — typography only.
43
+ // Down-level-hidden conditional comments hide this branch from classic Outlook
44
+ // (which renders the VML below), so both engines see exactly one button.
45
+ const tdWidthCss = explicitWidth ? `width:${explicitWidth}px;` : "";
46
+ const tdHeightCss = explicitHeight > 0 ? `height:${finalHeight}px;` : "";
47
+ const tdWidthAttr = explicitWidth ? ` width="${explicitWidth}"` : "";
48
+ const tdHeightAttr = explicitHeight > 0 ? ` height="${finalHeight}"` : "";
49
+ // <a> fills the cell. When height is explicit, line-height = inner height for vertical
50
+ // centering. When not, padding provides the vertical sizing (matches canvas behavior).
51
+ const anchorBoxStyles = explicitHeight > 0
52
+ ? `display:block;line-height:${finalHeight - 2 * bw}px;padding:0 ${padRight}px;`
53
+ : `display:block;padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;line-height:${fs}px;`;
54
+ const tableAlignAttr = ` align="${containerAlign}"`;
55
+ const tableMargin = containerAlign === 'center' ? 'margin:0 auto;'
56
+ : containerAlign === 'right' ? 'margin-left:auto;margin-right:0;'
57
+ : '';
38
58
  const nonMsoAnchor = `<!--[if !mso]><!-->
39
- <a href="${navigateToUrl}"
40
- target="_blank" rel="noreferrer noopener"
41
- style="display:inline-block;background-color:${bgColor};border-radius:${br}px;${borderCss}color:${safeColor};font-family:${safeFF};font-size:${fs}px;font-weight:${fontWeight};text-decoration:none;${nonMsoVerticalSizing}text-align:center;white-space:nowrap;-webkit-text-size-adjust:none;box-sizing:border-box;${widthCss}mso-hide:all;">${text}</a>
59
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation"${tableAlignAttr} style="border-collapse:separate;${tableMargin}">
60
+ <tr>
61
+ <td bgcolor="${bgColor}" align="center" valign="middle"${tdWidthAttr}${tdHeightAttr} style="background-color:${bgColor};border-radius:${br}px;${borderCss}${tdWidthCss}${tdHeightCss}box-sizing:border-box;mso-padding-alt:0;text-align:center;">
62
+ <a href="${safeHref}" target="_blank" rel="noreferrer noopener"
63
+ style="${anchorBoxStyles}color:${safeColor};font-family:${safeFF};font-size:${fs}px;font-weight:${fontWeight};text-decoration:none;text-align:center;white-space:nowrap;-webkit-text-size-adjust:none;box-sizing:border-box;">${text}</a>
64
+ </td>
65
+ </tr>
66
+ </table>
42
67
  <!--<![endif]-->`;
68
+ // ── MSO: VML bulletproof button (classic Outlook / Word engine) ──
69
+ // VML arcsize is a percentage of half the shorter side. Clamp to 50% (pill).
43
70
  const arcSizePct = br > 0 ? Math.min(Math.round((br / (finalHeight / 2)) * 100), 50) : 0;
44
71
  const strokeAttrs = bw > 0
45
72
  ? `stroke="true" strokecolor="${bdColor}" strokeweight="${bw}px"`
46
73
  : `stroke="false"`;
47
74
  const msoButton = `<!--[if mso]>
48
75
  <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
49
- href="${navigateToUrl}"
76
+ href="${safeHref}"
50
77
  style="height:${finalHeight}px;v-text-anchor:middle;width:${vmlWidth}px;"
51
78
  arcsize="${arcSizePct}%"
52
79
  ${strokeAttrs}
@@ -63,12 +90,24 @@ function appendOutlookForButton(buttonData) {
63
90
  return {
64
91
  innerContent,
65
92
  computed: {
66
- fs, fontWeight, containerAlign,
67
- padTop, padRight, padBottom, padLeft,
68
- explicitWidth, vmlWidth,
69
- safeColor, bgColor, safeFF,
70
- finalHeight, explicitHeight,
71
- bw, br, bdColor, bdStyle,
93
+ fs,
94
+ fontWeight,
95
+ containerAlign,
96
+ padTop,
97
+ padRight,
98
+ padBottom,
99
+ padLeft,
100
+ explicitWidth,
101
+ vmlWidth,
102
+ safeColor,
103
+ bgColor,
104
+ safeFF,
105
+ finalHeight,
106
+ explicitHeight,
107
+ bw,
108
+ br,
109
+ bdColor,
110
+ bdStyle,
72
111
  },
73
112
  };
74
113
  }
@@ -79,9 +118,20 @@ function convertButtonBlock(blockData) {
79
118
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
80
119
  const { innerContent, computed } = appendOutlookForButton({
81
120
  style: {
82
- fontFamily, fontSize, fontWeight, textAlign,
83
- borderColor, borderRadius, borderWidth, borderStyle,
84
- buttonPadding, color, buttonColor, width, height, alignment,
121
+ fontFamily,
122
+ fontSize,
123
+ fontWeight,
124
+ textAlign,
125
+ borderColor,
126
+ borderRadius,
127
+ borderWidth,
128
+ borderStyle,
129
+ buttonPadding,
130
+ color,
131
+ buttonColor,
132
+ width,
133
+ height,
134
+ alignment,
85
135
  },
86
136
  text: text || "",
87
137
  navigateToUrl: navigateToUrl || "",
@@ -118,9 +168,9 @@ function convertButtonBlock(blockData) {
118
168
  hideOnMobile: Boolean(props.hideOnMobile),
119
169
  });
120
170
  return `
121
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" class="${visibilityClass}" data-block-type="button" data-block-props="${buttonBlockProps}">
171
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" class="${visibilityClass}" data-block-type="button" data-block-props="${buttonBlockProps}" style="border-collapse:collapse;table-layout:fixed;">
122
172
  <tr>
123
- <td align="${computed.containerAlign}"
173
+ <td align="${computed.containerAlign}"${(0, common_1.buildOutlookBgAttr)(containerBg)}
124
174
  style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;background-color:${containerBg || 'transparent'};">
125
175
  ${innerContent}
126
176
  </td>
@@ -1 +1 @@
1
- {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/grid.ts"],"names":[],"mappings":"AAMA,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAiO1F;AAED,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,GAAG,EACd,QAAQ,EAAE,GAAG,EACb,gBAAgB,EAAE,MAAM,EACxB,iBAAiB,EAAE,MAAM,EACzB,mBAAmB,UAAQ;;;GAoE5B"}
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/grid.ts"],"names":[],"mappings":"AAMA,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,GAAG,EACd,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAgTtB;AAED,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,GAAG,EACd,QAAQ,EAAE,GAAG,EACb,gBAAgB,EAAE,MAAM,EACxB,iBAAiB,EAAE,MAAM,EACzB,mBAAmB,UAAQ;;;GAwG5B"}
@@ -6,18 +6,22 @@ const buildStyles_1 = require("../buildStyles");
6
6
  const gradientUtils_1 = require("../gradientUtils");
7
7
  const common_1 = require("../common");
8
8
  const jsonToHTML_1 = require("../jsonToHTML");
9
- const outlookSupport_1 = require("../outlookSupport");
10
9
  async function convertGridBlock(blockData, rootData, cellWidthInPx) {
11
10
  const { style = {}, childrenIds = [], props } = blockData.data;
12
11
  const { columns = 1, cellWidths = [], responsive = true } = props;
13
12
  const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
14
13
  const gridVisibilityClass = (0, common_1.getVisibilityClass)(props);
14
+ // Detect gradient — check both backgroundImage prop and customCss (gradient may land in
15
+ // customCss when the block was built via CSS shorthand or custom CSS input).
15
16
  const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : '';
16
17
  const customCssStr = restStyle.customCss || '';
18
+ // Extract gradient string from customCss if not already in backgroundImage
17
19
  const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
18
20
  ? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
19
21
  : '';
20
- const effectiveGradient = bgImageStr.includes('gradient(') ? bgImageStr : gradientInCustomCss;
22
+ const effectiveGradient = bgImageStr.includes('gradient(')
23
+ ? bgImageStr
24
+ : gradientInCustomCss;
21
25
  const isGradient = Boolean(effectiveGradient);
22
26
  const parsedGradient = isGradient ? (0, gradientUtils_1.parseGradient)(effectiveGradient) : null;
23
27
  const fallbackBgColor = backgroundColor ||
@@ -27,12 +31,18 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
27
31
  const rawBgImageUrl = !isGradient && bgImageStr
28
32
  ? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
29
33
  : null;
34
+ // When gradient came from customCss, strip background-image from customCss so it
35
+ // doesn't duplicate into the inner table style (the outer <td> wrapper carries it).
30
36
  const innerCustomCss = gradientInCustomCss
31
37
  ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
32
38
  : customCssStr;
39
+ // Build inner table styles — when gradient/bg-image is on the outer wrapper, strip
40
+ // background props from the inner table so the outer <td> background shows through.
33
41
  const innerRestStyleRaw = (rawBgImageUrl || isGradient)
34
42
  ? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
35
43
  : { ...restStyle, customCss: innerCustomCss };
44
+ // Extract border/radius props — applied via a div wrapper for non-MSO clients so that
45
+ // border-radius is honoured (Gmail/Outlook compose strip border-radius from <table>).
36
46
  const { borderRadius, border, borderColor, borderWidth, borderStyle: borderStyleProp, ...innerRestStyle } = innerRestStyleRaw;
37
47
  const divBorderParts = [];
38
48
  if (borderRadius)
@@ -51,16 +61,31 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
51
61
  ? 'transparent'
52
62
  : ((rawBgImageUrl || isGradient) ? undefined : backgroundColor);
53
63
  const tableStyles = (0, buildStyles_1.buildStyles)({ backgroundColor: tableBgForNonMso, ...innerRestStyle }, {
54
- perChanges: [], pxChanges: buildStyles_1.allPxAttributes,
64
+ perChanges: [],
65
+ pxChanges: buildStyles_1.allPxAttributes,
55
66
  });
56
67
  const total = childrenIds.length;
57
68
  const visualRows = Math.ceil(total / columns);
69
+ // OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
58
70
  const msoTableWidth = Math.min(cellWidthInPx, 600);
59
- const msoBgColor = !rawBgImageUrl && !isGradient ? (backgroundColor || '') : '';
60
- const msoBgAttr = msoBgColor ? ` bgcolor="${(0, outlookSupport_1.toOutlookBgColor)(msoBgColor)}"` : '';
71
+ // When a background image/gradient is present, the background is applied on an outer
72
+ // wrapper <td> (see bottom of function). The inner grid tables must be clean.
73
+ // When no background, the MSO table gets bgcolor for solid-color sections.
74
+ const msoBgColor = !rawBgImageUrl && !isGradient
75
+ ? (backgroundColor || '')
76
+ : '';
77
+ const msoBgAttr = (0, common_1.buildOutlookBgAttr)(msoBgColor);
61
78
  const msoBgStyle = msoBgColor ? `background-color:${msoBgColor};` : '';
62
- const innerBgTransparent = (rawBgImageUrl || isGradient) ? 'background-color:transparent;' : '';
63
- const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor && !divBorderStyle ? ` bgcolor="${(0, outlookSupport_1.toOutlookBgColor)(backgroundColor)}"` : '';
79
+ // Inner tables must be explicitly transparent when outer <td> carries the background.
80
+ const innerBgTransparent = (rawBgImageUrl || isGradient)
81
+ ? 'background-color:transparent;'
82
+ : '';
83
+ const nonMsoBgAttr = !rawBgImageUrl && !isGradient && !divBorderStyle
84
+ ? (0, common_1.buildOutlookBgAttr)(backgroundColor || '')
85
+ : '';
86
+ // When divBorderStyle is set the non-MSO <table> is transparent, so the Grid's
87
+ // backgroundColor must move onto the div wrapper — otherwise it vanishes in modern clients.
88
+ // Skip this for bg-image/gradient blocks; they apply their background via a separate wrapper.
64
89
  const divWrapBg = divBorderStyle && backgroundColor && !rawBgImageUrl && !isGradient
65
90
  ? ` background-color:${backgroundColor};`
66
91
  : '';
@@ -73,14 +98,15 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
73
98
  class="${gridVisibilityClass}">
74
99
  <![endif]-->
75
100
  <!--[if !mso]><!-->
76
- <table border="0" cellpadding="0" cellspacing="0" width="100%"
101
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" align="center"
77
102
  role="presentation"${nonMsoBgAttr}
78
- style="border-collapse:collapse; ${innerBgTransparent}${tableStyles}; max-width:600px;"
103
+ style="border-collapse:collapse; table-layout:fixed; ${innerBgTransparent}${tableStyles}; max-width:600px;"
79
104
  class="${gridVisibilityClass}">
80
105
  <!--<![endif]-->
81
106
  `;
82
107
  for (let r = 0; r < visualRows; r++) {
83
108
  html += "<tr>";
109
+ // COUNT visible cells and find last visible column index
84
110
  let visibleCells = 0;
85
111
  let lastVisibleCol = 0;
86
112
  const rowIds = [];
@@ -89,12 +115,14 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
89
115
  const id = childrenIds[idx] ?? null;
90
116
  rowIds.push(id);
91
117
  const child = id ? rootData[id] : null;
92
- if (!child?.data?.props?.hideOnDesktop) {
118
+ const isHidden = child?.data?.props?.hideOnDesktop;
119
+ if (!isHidden) {
93
120
  visibleCells++;
94
121
  lastVisibleCol = c;
95
122
  }
96
123
  }
97
124
  const safeWidth = visibleCells > 0 ? 100 / visibleCells : 100 / columns;
125
+ // Reserve pixel space for spacer tds between visible cells (N-1 gaps for N visible cells)
98
126
  const totalGapPx = columnGap * Math.max(visibleCells - 1, 0);
99
127
  const adjustedTableWidth = Math.max(msoTableWidth - totalGapPx, 1);
100
128
  let totalWidth = 0;
@@ -102,19 +130,24 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
102
130
  for (let c = 0; c < columns; c++) {
103
131
  const id = rowIds[c];
104
132
  let widthPercent = cellWidths[c] ?? safeWidth;
105
- if (widthPercent <= 0 || widthPercent > 100)
133
+ if (widthPercent <= 0 || widthPercent > 100) {
106
134
  widthPercent = safeWidth;
135
+ }
107
136
  cellWidthPercents.push(widthPercent);
108
137
  if (id) {
109
138
  const child = rootData[id];
110
- if (!child?.data?.props?.hideOnDesktop)
139
+ const isHidden = child?.data?.props?.hideOnDesktop;
140
+ if (!isHidden) {
111
141
  totalWidth += widthPercent;
142
+ }
112
143
  }
113
144
  }
114
145
  const scaleFactor = totalWidth > 0 && totalWidth < 100 ? 100 / totalWidth : 1;
115
146
  for (let c = 0; c < columns; c++) {
116
147
  const id = rowIds[c];
117
- let widthPercent = Math.min(cellWidthPercents[c] * scaleFactor, 100);
148
+ let widthPercent = cellWidthPercents[c] * scaleFactor;
149
+ widthPercent = Math.min(widthPercent, 100);
150
+ // Cell pixel width is a share of the gap-adjusted table width
118
151
  const cellWidthPx = Math.round((widthPercent / 100) * adjustedTableWidth);
119
152
  if (id) {
120
153
  const child = rootData[id];
@@ -124,34 +157,40 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
124
157
  const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
125
158
  if (childVisible) {
126
159
  const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth, Boolean(divBorderStyle));
160
+ // bgcolor on the cell <td> ensures background-color survives Outlook
161
+ // compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
127
162
  const cellBgColor = cellStyle.backgroundColor || '';
128
- const cellBgAttr = cellBgColor ? ` bgcolor="${(0, outlookSupport_1.toOutlookBgColor)(cellBgColor)}"` : '';
163
+ const cellBgAttr = (0, common_1.buildOutlookBgAttr)(cellBgColor);
129
164
  html += `
130
165
  <td
131
- width="${cellWidthPx}"${cellBgAttr}
166
+ width="${Math.max(1, Math.round(widthPercent))}%"${cellBgAttr}
132
167
  class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
133
- style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
168
+ style="width:${widthPercent}%;vertical-align:${verticalAlign};word-break:break-word;${styles}"
134
169
  >
135
170
  ${childHtml}
136
171
  </td>`;
172
+ // Spacer td between columns — uses width attribute only (no inline style width) so
173
+ // Outlook mobile (which strips <style>) treats it proportionally, not as a fixed blocker.
174
+ // col-gap-spacer class hides it when columns stack via CSS media query.
137
175
  if (columnGap > 0 && c !== lastVisibleCol) {
138
- html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
176
+ html += `<td width="${columnGap}" class="col-gap-spacer" style="font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
139
177
  }
140
178
  }
141
179
  }
142
180
  else {
143
181
  html += `
144
- <td width="${cellWidthPx}"
182
+ <td width="${Math.max(1, Math.round(widthPercent))}%"
145
183
  ${responsive ? 'class="stack-column"' : ""}
146
- style="width:${cellWidthPx}px;vertical-align:top;">
184
+ style="width:${widthPercent}%;vertical-align:top;">
147
185
  </td>`;
148
186
  if (columnGap > 0 && c !== lastVisibleCol) {
149
- html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
187
+ html += `<td width="${columnGap}" class="col-gap-spacer" style="font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
150
188
  }
151
189
  }
152
190
  }
153
191
  html += "</tr>";
154
192
  }
193
+ // Close both MSO and non-MSO tables
155
194
  html += `
156
195
  <!--[if mso]>
157
196
  </table>
@@ -160,6 +199,21 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
160
199
  </table>
161
200
  <!--<![endif]-->
162
201
  `;
202
+ // ── Background image: canonical multi-client approach ────────────────────
203
+ //
204
+ // Problem: `background-image` on a <table> element is stripped by:
205
+ // • New Outlook Mac / Windows (Chromium-based app)
206
+ // • Outlook.com
207
+ // • Old Outlook (Word engine) — ignores CSS entirely
208
+ //
209
+ // Solution: wrap the grid in an outer <table><tr><td> where the <td> carries
210
+ // the background. Different clients pick it up via different mechanisms:
211
+ //
212
+ // background="" attribute on <td> → Yahoo Mail, older webmail
213
+ // CSS background-image on <td> → Gmail, Apple Mail, new Outlook Mac ✓
214
+ // VML v:rect inside the <td> → Old Outlook (Word engine) ✓
215
+ //
216
+ // The inner grid tables have NO background so the outer <td> bg shows through.
163
217
  if (rawBgImageUrl || isGradient) {
164
218
  const vmlFill = isGradient
165
219
  ? (() => {
@@ -171,9 +225,9 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
171
225
  : `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
172
226
  html = `
173
227
  <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
174
- style="border-collapse:collapse;width:100%;max-width:${msoTableWidth}px;">
228
+ style="border-collapse:collapse;table-layout:fixed;width:100%;max-width:${msoTableWidth}px;">
175
229
  <tr>
176
- <td width="100%" bgcolor="${fallbackBgColor}" valign="top"
230
+ <td width="100%"${(0, common_1.buildOutlookBgAttr)(fallbackBgColor)} valign="top"
177
231
  ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
178
232
  style="
179
233
  width:100%;max-width:${msoTableWidth}px;
@@ -200,58 +254,68 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
200
254
  </tr>
201
255
  </table>`;
202
256
  }
257
+ // Wrap the entire grid (including any bg-image outer table) in a div when the block
258
+ // has border/radius. An unconditional <div> is used — not gated behind <!--[if !mso]>-->
259
+ // — so Gmail compose paste renders the border-radius reliably. Old Outlook ignores
260
+ // border-radius on <div> but still shows the rectangular border; new Outlook works fully.
203
261
  if (divBorderStyle)
204
262
  html = `${divWrapOpen}${html}${divWrapClose}`;
205
263
  return html;
206
264
  }
207
265
  async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
208
266
  const { style = {}, childrenIds = [], props = {} } = blockData.data;
267
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
268
+ // Extract border + radius from style so they move to the div wrapper (not the <td>).
269
+ // Gmail strips border-radius from <td> but honours it on <div>. By putting border and
270
+ // radius on the same unconditional <div>, the rounded card border renders in all clients.
271
+ // The <td> keeps bgcolor (via attribute) for Old Outlook background fallback.
209
272
  const { borderRadius: cellBorderRadius, borderWidth: cellBorderWidth, borderStyle: cellBorderStyleProp, borderColor: cellBorderColor, border: cellBorderShorthand, ...styleWithoutBorder } = style;
273
+ // backgroundColor must stay on the div wrapper (not the <td>) in two cases:
274
+ // 1. Cell has its own border-radius — the div's overflow:hidden clips the background.
275
+ // 2. Parent grid has a border div (divBorderStyle) — the grid's overflow:hidden clips it.
276
+ // In both cases, the rectangular <td> background bleeds through rounded corners if kept
277
+ // in CSS, creating visible corner squares. The bgcolor attribute stays for Outlook fallback.
210
278
  const stripBgFromTd = Boolean(cellBorderRadius) || parentGridHasBorder;
211
- // Omit backgroundColor entirely rather than setting 'transparent' — an explicit
212
- // background-color:transparent can defeat the bgcolor HTML attribute in Old Outlook
213
- // (Word engine), leaving the cell with no background at all.
279
+ // When stripping bg from <td>, omit backgroundColor entirely rather than setting 'transparent'.
280
+ // An explicit background-color:transparent in CSS can defeat the bgcolor HTML attribute in
281
+ // Old Outlook (Word engine), leaving the cell with no background at all.
214
282
  const styleForTd = stripBgFromTd
215
283
  ? { ...styleWithoutBorder, backgroundColor: undefined }
216
284
  : styleWithoutBorder;
217
- const styles = (0, buildStyles_1.buildStyles)(styleForTd, { perChanges: [], pxChanges: buildStyles_1.allPxAttributes });
285
+ // Outlook treats <td width="..."> as content width and then adds horizontal padding,
286
+ // which can push the total row width beyond the parent table and shift columns.
287
+ // Keep sizing styles on <td>, but move padding into the inner div wrapper.
288
+ const styleForTdWithoutPadding = { ...styleForTd, padding: undefined };
289
+ const styles = (0, buildStyles_1.buildStyles)(styleForTdWithoutPadding, {
290
+ perChanges: [],
291
+ pxChanges: buildStyles_1.allPxAttributes,
292
+ });
218
293
  const parts = [];
294
+ // OUTLOOK FIX: Calculate the actual cell width in pixels based on percentage
295
+ // If parent is 600px and cell is 50%, cell width should be 300px, not 600px
219
296
  const cellWidthPx = Math.round((cellWidthPercent / 100) * parentCellWidthPx);
220
- const rawPad = styleWithoutBorder?.padding;
221
- let cellPadLeft = 0;
222
- let cellPadRight = 0;
223
- if (rawPad && typeof rawPad === 'object') {
224
- cellPadLeft = Number.isFinite(rawPad.left) ? rawPad.left : 0;
225
- cellPadRight = Number.isFinite(rawPad.right) ? rawPad.right : 0;
226
- }
227
- else if (typeof rawPad === 'string') {
228
- const parts = rawPad.trim().split(/\s+/).map(v => parseFloat(v) || 0);
229
- if (parts.length >= 4) {
230
- cellPadRight = parts[1];
231
- cellPadLeft = parts[3];
232
- }
233
- else if (parts.length === 3) {
234
- cellPadRight = parts[1];
235
- cellPadLeft = parts[1];
236
- }
237
- else if (parts.length === 2) {
238
- cellPadRight = parts[1];
239
- cellPadLeft = parts[1];
240
- }
241
- else if (parts.length === 1) {
242
- cellPadRight = parts[0];
243
- cellPadLeft = parts[0];
244
- }
245
- }
297
+ // Subtract the cell's own padding so children receive the actual content-area width.
298
+ // Old Outlook honours explicit img/table width attributes — if a child is sized to the
299
+ // full column width (ignoring padding) it overflows and expands the column.
300
+ const cellPad = styleWithoutBorder?.padding || {};
301
+ const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
302
+ const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
246
303
  const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
304
+ // OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
247
305
  const safeCellWidthPx = Math.min(contentWidthPx, 600);
248
306
  for (const childId of childrenIds) {
249
307
  const child = rootData[childId];
250
- if (child)
308
+ if (child) {
251
309
  parts.push(await (0, jsonToHTML_1.convertToHtml)(child, rootData, safeCellWidthPx));
310
+ }
252
311
  }
253
312
  const borderRadius = cellBorderRadius || 0;
254
313
  const bgColor = styleWithoutBorder?.backgroundColor || "transparent";
314
+ // Build border CSS for the div wrapper.
315
+ // When the parent grid already has a divBorderStyle wrapper (border + border-radius +
316
+ // overflow:hidden), the cell must NOT duplicate the same border/radius — that causes
317
+ // two concentric borders of the same colour (double-border). The grid's wrapper div
318
+ // already provides the visual container; the cell div only needs background-color.
255
319
  const cellDivBorderParts = [];
256
320
  if (!parentGridHasBorder) {
257
321
  if (borderRadius)
@@ -267,9 +331,21 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
267
331
  }
268
332
  }
269
333
  const cellDivBorderStyle = cellDivBorderParts.join(' ');
334
+ // Unconditional div — visible to all clients (Gmail, Outlook new/old, Apple Mail).
335
+ // background-color on the div covers modern clients; bgcolor on <td> covers Old Outlook.
270
336
  const divStyleParts = [`background-color:${bgColor};`];
337
+ const formatPad = (value) => (typeof value === 'number' ? `${value}px` : (value || '0'));
338
+ const cellPadTop = formatPad(cellPad.top);
339
+ const cellPadBottom = formatPad(cellPad.bottom);
340
+ const cellPadLeftCss = formatPad(cellPad.left);
341
+ const cellPadRightCss = formatPad(cellPad.right);
342
+ divStyleParts.push(`padding:${cellPadTop} ${cellPadRightCss} ${cellPadBottom} ${cellPadLeftCss};`);
271
343
  if (cellDivBorderStyle)
272
344
  divStyleParts.push(cellDivBorderStyle);
273
- const wrapped = `<div style="${divStyleParts.join(' ')}">${parts.join("")}</div>`;
274
- return { html: wrapped, styles };
345
+ const divStyleStr = divStyleParts.join(' ');
346
+ const wrapped = `<div style="${divStyleStr}">${parts.join("")}</div>`;
347
+ return {
348
+ html: wrapped,
349
+ styles,
350
+ };
275
351
  }
@@ -1 +1 @@
1
- {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/image.ts"],"names":[],"mappings":"AAIA,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM;;;;;GAiB1F;AAED,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBA2D5E"}
1
+ {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/image.ts"],"names":[],"mappings":"AAIA,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM;;;;;GAoB5B;AAED,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAiH5E"}
@@ -29,27 +29,65 @@ async function convertImageBlock(blockData, cellWidthInPx) {
29
29
  const { altText, imageUrl, navigateToUrl } = props;
30
30
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
31
31
  const { width, height, objectFit, borderRadius, borderWidth, borderColor, borderStyle, ...containerStyle } = style;
32
- const containerStyles = (0, buildStyles_1.buildStyles)({ ...containerStyle }, { perChanges: [], pxChanges: buildStyles_1.addPxToAttributes });
32
+ // Add border styles to container for fallback clients
33
+ const containerStyles = (0, buildStyles_1.buildStyles)({
34
+ ...containerStyle,
35
+ }, { perChanges: [], pxChanges: buildStyles_1.addPxToAttributes });
36
+ // OUTLOOK FIX: Ensure cellWidthInPx never exceeds 600px
33
37
  const safeCellWidth = Math.min(cellWidthInPx, 600);
38
+ // Parse width percentage (default 100%)
34
39
  const widthPercent = typeof width === "string" && width.includes("%")
35
40
  ? parseInt(width.replace("%", ""))
36
- : typeof width === "number" ? width : 100;
41
+ : typeof width === "number"
42
+ ? width
43
+ : 100;
44
+ // OUTLOOK FIX: Calculate inner container width based on safe cell width
37
45
  const paddingLeft = style?.padding?.left || 0;
38
46
  const paddingRight = style?.padding?.right || 0;
39
47
  const availableWidth = safeCellWidth - paddingLeft - paddingRight;
40
48
  const innerContainerWidth = Math.round((widthPercent / 100) * availableWidth);
49
+ // Get image dimensions and calculate scaled sizes
41
50
  const { originalWidth, originalHeight, scaledWidth, scaledHeight } = await computeScaledDimensions(imageUrl, innerContainerWidth);
51
+ // OUTLOOK FIX: For Outlook, we need exact pixel dimensions
52
+ // Calculate final dimensions that respect both original size and container
42
53
  const finalWidth = Math.min(scaledWidth, innerContainerWidth, originalWidth);
43
54
  const finalHeight = Math.round((finalWidth / originalWidth) * originalHeight);
44
- const imageTagStyles = (0, buildStyles_1.buildStyles)({ borderStyle, borderRadius, borderColor, borderWidth }, { perChanges: [], pxChanges: buildStyles_1.addPxToAttributes });
55
+ // Build image styles for modern email clients (non-Outlook)
56
+ const imageTagStyles = (0, buildStyles_1.buildStyles)({
57
+ borderStyle,
58
+ borderRadius: borderRadius,
59
+ borderColor,
60
+ borderWidth,
61
+ }, {
62
+ perChanges: [],
63
+ pxChanges: buildStyles_1.addPxToAttributes,
64
+ });
65
+ // OUTLOOK RENDERING CONTRACT
66
+ // width/height HTML attributes → Old Outlook Word engine (CSS is lower priority in Word)
67
+ // width/height inline CSS → New Outlook, OWA, and all CSS-capable clients
68
+ // max-width:100% → prevents overflow in any client that ignores pixel constraints
69
+ // height:auto !important → @media query (convertJsonToHtml.ts) overrides this on mobile
70
+ // so proportional scaling is preserved on small screens
71
+ // -ms-interpolation-mode → inline fallback; the <style> block is stripped by
72
+ // Outlook during forward/reply MIME rewriting
45
73
  const imageElement = `<img src="${imageUrl}" alt="${altText || "Image"}" border="0" width="${finalWidth}" height="${finalHeight}" style="${imageTagStyles}; display:block; width:${finalWidth}px; height:${finalHeight}px; max-width:100%; line-height:0; -ms-interpolation-mode:bicubic;" />`;
46
74
  const percentWidth = typeof width === "string" && width.endsWith("%")
47
75
  ? width
48
- : typeof width === "number" ? `${width}%` : "100%";
76
+ : typeof width === "number"
77
+ ? `${width}%`
78
+ : "100%";
79
+ // Non-MSO wrapper: display:block removes the phantom inline-baseline gap that
80
+ // display:inline-block creates in Gmail / Apple Mail / Yahoo between images.
81
+ // margin handles alignment since text-align won't move block elements.
49
82
  const imgTextAlign = containerStyle.textAlign || "left";
50
83
  const imgMargin = imgTextAlign === "center" ? "margin:0 auto;" :
51
84
  imgTextAlign === "right" ? "margin-left:auto; margin-right:0;" : "";
85
+ // OUTLOOK FIX: Use finalWidth (the actual displayed size) as max-width so the div
86
+ // doesn't claim more space than the image occupies. originalWidth is the natural
87
+ // image size (e.g. 636px for the Beefree logo rendered at 35px) which was
88
+ // misleadingly large and could confuse some rendering engines.
52
89
  const nonMsoWrapper = `<div style="display:block; width:${percentWidth}; max-width:${finalWidth}px; line-height:0; font-size:0; ${imgMargin}">${imageElement}</div>`;
90
+ // OUTLOOK FIX: Generate VML with corrected dimensions
53
91
  const outlookImage = await (0, outlookSupport_1.appendOutlookForImage)(nonMsoWrapper, safeCellWidth, innerContainerWidth, imageUrl, style, finalWidth, finalHeight);
54
92
  const imageContent = (0, outlookSupport_1.appendOutlookSupport)(outlookImage, containerStyles, visibilityClass, safeCellWidth);
55
93
  return navigateToUrl
@@ -147,7 +147,7 @@ async function convertShapeBlock(blockData) {
147
147
  // --- Case 1: Image + Text ---
148
148
  if (imageUrl && text) {
149
149
  nonMsoContent = `
150
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
150
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;max-width:100%;box-sizing:border-box;
151
151
  border:${borderWidth}px ${borderStyle} ${borderColor};
152
152
  border-radius:${resolvedBorderRadius};
153
153
  background-color:${finalBackgroundColor};
@@ -156,11 +156,11 @@ async function convertShapeBlock(blockData) {
156
156
  background-size:cover;
157
157
  background-repeat:no-repeat;
158
158
  overflow:hidden;${alignmentStyle}${customCss || ""}">
159
- <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
160
- style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
159
+ <table border="0" cellpadding="0" cellspacing="0" width="100%"
160
+ style="width:100%;height:${resolvedHeightPx}px;border-collapse:collapse;">
161
161
  <tr>
162
162
  <td align="${textAlignStyle}" valign="${verticalAlign}"
163
- width="${resolvedWidthPx}" height="${resolvedHeightPx}"
163
+ height="${resolvedHeightPx}"
164
164
  style="padding:6px;vertical-align:${verticalAlign};text-align:${textAlignStyle};overflow:hidden;box-sizing:border-box;">
165
165
  <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text}</div>
166
166
  </td>
@@ -171,7 +171,7 @@ async function convertShapeBlock(blockData) {
171
171
  // --- Case 2: Image only ---
172
172
  else if (imageUrl) {
173
173
  nonMsoContent = `
174
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
174
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;max-width:100%;box-sizing:border-box;
175
175
  border:${borderWidth}px ${borderStyle} ${borderColor};
176
176
  border-radius:${resolvedBorderRadius};
177
177
  overflow:hidden;${alignmentStyle}${customCss || ""}">
@@ -183,16 +183,16 @@ async function convertShapeBlock(blockData) {
183
183
  // --- Case 3: Text only ---
184
184
  else {
185
185
  nonMsoContent = `
186
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
186
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;max-width:100%;box-sizing:border-box;
187
187
  background-color:${finalBackgroundColor};
188
188
  border:${borderWidth}px ${borderStyle} ${borderColor};
189
189
  border-radius:${resolvedBorderRadius};
190
190
  overflow:hidden;${alignmentStyle}${customCss || ""}">
191
- <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
192
- style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
191
+ <table border="0" cellpadding="0" cellspacing="0" width="100%"
192
+ style="width:100%;height:${resolvedHeightPx}px;border-collapse:collapse;">
193
193
  <tr>
194
194
  <td align="${textAlignStyle}" valign="${verticalAlign}"
195
- width="${resolvedWidthPx}" height="${resolvedHeightPx}"
195
+ height="${resolvedHeightPx}"
196
196
  style="padding:8px;vertical-align:${verticalAlign};text-align:${textAlignStyle};box-sizing:border-box;">
197
197
  <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text || ""}</div>
198
198
  </td>
@@ -1 +1 @@
1
- {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/text.ts"],"names":[],"mappings":"AAKA,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,MAAM,UA4ItE"}
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/text.ts"],"names":[],"mappings":"AASA,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,MAAM,UAkLtE"}
@@ -8,42 +8,69 @@ const common_1 = require("../common");
8
8
  function convertTextBlock(blockData, cellWidthInPx) {
9
9
  const { style, props } = blockData.data;
10
10
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
11
- const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, backgroundImage, backgroundPosition, backgroundRepeat, backgroundSize, whiteSpace: _whiteSpace, ...rest } = style;
12
- const bgImageStr = typeof backgroundImage === 'string' ? backgroundImage : '';
13
- const customCssStr = rest.customCss || '';
14
- const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
15
- ? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
16
- : '';
17
- const effectiveGradient = bgImageStr.includes('gradient(') ? bgImageStr : gradientInCustomCss;
11
+ const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, backgroundImage, whiteSpace: _whiteSpace, // strip from outer td — pre-wrap on a td preserves editor whitespace
12
+ ...rest } = style;
13
+ // Detect background image or gradient (may live in backgroundImage or customCss).
14
+ // Same multi-client approach as Grid block: outer wrapper <td> carries the background
15
+ // via CSS (Gmail/New Outlook), background attribute (Yahoo), and VML (Old Outlook).
16
+ const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : "";
17
+ const customCssStr = rest.customCss || "";
18
+ const gradientInCustomCss = !bgImageStr.includes("gradient(") && customCssStr.includes("gradient(")
19
+ ? customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || ""
20
+ : "";
21
+ const effectiveGradient = bgImageStr.includes("gradient(")
22
+ ? bgImageStr
23
+ : gradientInCustomCss;
18
24
  const isGradient = Boolean(effectiveGradient);
19
25
  const parsedGradient = isGradient ? (0, gradientUtils_1.parseGradient)(effectiveGradient) : null;
20
26
  const rawBgImageUrl = !isGradient && bgImageStr
21
- ? bgImageStr.replace(/^url\(['"]?/, '').replace(/['"]?\)$/, '')
27
+ ? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
22
28
  : null;
23
29
  const hasBgImage = Boolean(rawBgImageUrl || isGradient);
24
30
  const fallbackBgColor = textContainerBackgroundColor ||
25
31
  parsedGradient?.fallback ||
26
32
  (0, gradientUtils_1.extractCssFallbackColor)(customCssStr) ||
27
- '#ffffff';
28
- const textBoxStyle = { backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth };
29
- const convertedTextStyle = (0, buildStyles_1.buildStyles)(textBoxStyle, { perChanges: [], pxChanges: buildStyles_1.allPxAttributes });
33
+ "#ffffff";
34
+ // Text box decoration styles (border, background, padding) no width
35
+ const textBoxStyle = {
36
+ backgroundColor,
37
+ padding,
38
+ borderRadius,
39
+ borderStyle,
40
+ borderColor,
41
+ borderWidth,
42
+ };
43
+ const convertedTextStyle = (0, buildStyles_1.buildStyles)(textBoxStyle, {
44
+ perChanges: [],
45
+ pxChanges: buildStyles_1.allPxAttributes,
46
+ });
47
+ // Strip gradient from customCss when it is hoisted to the outer bg wrapper.
30
48
  const innerCustomCss = gradientInCustomCss
31
- ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
49
+ ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, "").trim()
32
50
  : customCssStr;
33
- const restForStyles = gradientInCustomCss ? { ...rest, customCss: innerCustomCss } : rest;
51
+ const restForStyles = gradientInCustomCss
52
+ ? { ...rest, customCss: innerCustomCss }
53
+ : rest;
54
+ // Outer td styles: strip container background when a bg-image wrapper is present
55
+ // so the outer wrapper's background is not double-applied.
34
56
  const styles = (0, buildStyles_1.buildStyles)({
35
57
  padding: textContainerPadding,
36
58
  backgroundColor: hasBgImage ? undefined : textContainerBackgroundColor,
37
59
  ...restForStyles,
38
- }, { perChanges: [], pxChanges: buildStyles_1.allPxAttributes });
60
+ }, {
61
+ perChanges: [],
62
+ pxChanges: buildStyles_1.allPxAttributes,
63
+ });
39
64
  const sanitizedText = (props.text ?? "")
40
65
  .replace(/<p(\s[^>]*)?>/gi, (_, attrs) => `<div${attrs || ""}>`)
41
66
  .replace(/<\/p>/gi, "</div>");
42
67
  const navigateToUrl = props.navigateToUrl || "";
43
68
  const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
69
+ // Email clients apply `a { color: blue }` which overrides inherited color.
70
+ // Inject the block color directly onto <a> tags that don't already have one.
44
71
  const blockTextColor = rest.color;
45
72
  const processedText = blockTextColor
46
- ? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = '') => {
73
+ ? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = "") => {
47
74
  if (/style\s*=\s*["'][^"']*\bcolor\s*:/i.test(attrs))
48
75
  return match;
49
76
  if (/\bstyle\s*=/i.test(attrs)) {
@@ -52,33 +79,37 @@ function convertTextBlock(blockData, cellWidthInPx) {
52
79
  return `<a${attrs} style="color:${blockTextColor};">`;
53
80
  })
54
81
  : sanitizedText;
55
- const colorStyle = blockTextColor ? `color:${blockTextColor};` : '';
82
+ const colorStyle = blockTextColor ? `color:${blockTextColor};` : "";
83
+ // Use display:block + width:100% so text fills the column naturally.
84
+ // display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
56
85
  const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
57
- const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
58
- const textContent = (0, outlookSupport_1.appendOutlookSupport)(convertedTextBox, styles, hasBgImage ? '' : visibilityClass, safeCellWidth);
59
- const linkColorStyle = blockTextColor ? `color:${blockTextColor};` : 'color:inherit;';
86
+ const safeCellWidth = cellWidthInPx
87
+ ? Math.min(cellWidthInPx, 600)
88
+ : undefined;
89
+ // When a bg-image wrapper is present, visibilityClass moves to the outer table.
90
+ const textContent = (0, outlookSupport_1.appendOutlookSupport)(convertedTextBox, styles, hasBgImage ? "" : visibilityClass, safeCellWidth);
91
+ const linkColorStyle = blockTextColor
92
+ ? `color:${blockTextColor};`
93
+ : "color:inherit;";
60
94
  if (hasBgImage) {
61
95
  const msoWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : 600;
62
96
  const vmlFill = isGradient
63
97
  ? (() => {
64
98
  const vmlAngle = (0, gradientUtils_1.cssAngleToVml)(parsedGradient?.angle || 180);
65
- const c1 = parsedGradient?.fallback || '#ffffff';
99
+ const c1 = parsedGradient?.fallback || "#ffffff";
66
100
  const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
67
101
  return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
68
102
  })()
69
103
  : `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
70
- const bgPosition = backgroundPosition ?? 'center center';
71
- const bgSize = backgroundSize ?? 'cover';
72
- const bgRepeat = backgroundRepeat ?? 'no-repeat';
73
104
  const bgCss = isGradient
74
105
  ? `background:${effectiveGradient};`
75
- : `background-image:url('${rawBgImageUrl}'); background-position:${bgPosition}; background-size:${bgSize}; background-repeat:${bgRepeat};`;
106
+ : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`;
76
107
  const wrappedContent = `
77
108
  <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
78
109
  style="border-collapse:collapse;width:100%;max-width:${msoWidth}px;" class="${visibilityClass}">
79
110
  <tr>
80
- <td width="100%" bgcolor="${fallbackBgColor}" valign="top"
81
- ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ''}
111
+ <td width="100%"${(0, common_1.buildOutlookBgAttr)(fallbackBgColor)} valign="top"
112
+ ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
82
113
  style="width:100%;max-width:${msoWidth}px;background-color:${fallbackBgColor};${bgCss}">
83
114
 
84
115
  <!--[if gte mso 9]>
@@ -1 +1 @@
1
- {"version":3,"file":"buildStyles.d.ts","sourceRoot":"","sources":["../../src/utils/buildStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,UAA8C,CAAC;AAC7E,eAAO,MAAM,sBAAsB,UAAsB,CAAC;AAC1D,eAAO,MAAM,eAAe,UAAoD,CAAC;AAEjF,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAQ/E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAe7D;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,GAAG,EACV,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,UAmEzE"}
1
+ {"version":3,"file":"buildStyles.d.ts","sourceRoot":"","sources":["../../src/utils/buildStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,UAA8C,CAAC;AAC7E,eAAO,MAAM,sBAAsB,UAAsB,CAAC;AAC1D,eAAO,MAAM,eAAe,UAAoD,CAAC;AAEjF,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAQ/E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAkB7D;AACD,wBAAgB,WAAW,CACzB,KAAK,EAAE,GAAG,EACV,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,UAqFzE"}
@@ -20,12 +20,15 @@ function sanitizeFontFamily(fontFamily) {
20
20
  .split(',')
21
21
  .map(font => {
22
22
  const trimmed = font.trim();
23
+ // Strip any surrounding quotes (single or double) from either end
23
24
  const unquoted = trimmed.replace(/^["']|["']$/g, '').trim();
24
25
  if (!unquoted)
25
26
  return '';
27
+ // Generic families and single-token names need no quotes
26
28
  if (GENERIC_FONT_FAMILIES.has(unquoted.toLowerCase()) || !/\s/.test(unquoted)) {
27
29
  return unquoted;
28
30
  }
31
+ // Multi-word font name: wrap in single quotes, escaping any embedded single quotes
29
32
  return `'${unquoted.replace(/'/g, "\\'")}'`;
30
33
  })
31
34
  .filter(Boolean)
@@ -35,18 +38,28 @@ function buildStyles(style, { pxChanges, perChanges }) {
35
38
  if (!style)
36
39
  style = {};
37
40
  const stylesObj = {};
38
- const INVALID_KEYS = [
39
- "columns", "cellWidths", "cellWidth", "childWidth",
40
- "visibility", "hideOnMobile", "hideOnDesktop", "label", "alignment",
41
- ];
42
41
  Object.entries(style).forEach(([key, value]) => {
43
42
  if (key === "customCss")
44
43
  return;
44
+ const INVALID_KEYS = [
45
+ "columns",
46
+ "cellWidths",
47
+ "cellWidth",
48
+ "childWidth",
49
+ "visibility",
50
+ "hideOnMobile",
51
+ "hideOnDesktop",
52
+ "label",
53
+ "alignment",
54
+ ];
45
55
  if (INVALID_KEYS.includes(key))
46
56
  return;
57
+ // Prevent null/undefined/"" from leaking into CSS
47
58
  if (value === undefined || value === null || value === "")
48
59
  return;
49
- if ((key === "padding" || key === "buttonPadding") && typeof value === "object") {
60
+ // FIX 1 SANITIZE padding objects
61
+ if ((key === "padding" || key === "buttonPadding") &&
62
+ typeof value === "object") {
50
63
  const pad = value;
51
64
  const safePad = {
52
65
  top: Number.isFinite(pad.top) ? pad.top : 0,
@@ -59,21 +72,26 @@ function buildStyles(style, { pxChanges, perChanges }) {
59
72
  if (key === "fontFamily" && typeof value === "string") {
60
73
  value = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(value));
61
74
  }
75
+ // Wrap backgroundImage values in url() if not already wrapped — skip gradients
62
76
  if (key === "backgroundImage" && typeof value === "string"
63
77
  && !String(value).startsWith("url(")
64
78
  && !String(value).toLowerCase().includes("gradient(")) {
65
79
  value = `url('${value}')`;
66
80
  }
81
+ // lineHeight: values >= 4 are pixel values; smaller values are unitless multipliers (e.g. 1.5)
67
82
  if (key === "lineHeight" && typeof value === "number") {
68
83
  stylesObj["line-height"] = value >= 4 ? `${value}px` : String(value);
69
84
  return;
70
85
  }
71
86
  const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
87
+ // FIX 2 — Sanitize invalid px/per values
72
88
  if (pxChanges.includes(key)) {
73
89
  if (typeof value === "number") {
74
- stylesObj[cssKey] = `${Math.round(value * 100) / 100}px`;
90
+ const rounded = Math.round(value * 100) / 100;
91
+ stylesObj[cssKey] = `${rounded}px`;
75
92
  }
76
93
  else if (typeof value === "string" && value.includes("null")) {
94
+ // Skip invalid styles
77
95
  return;
78
96
  }
79
97
  else {
@@ -5,4 +5,5 @@ export declare function getVisibilityClass(props?: {
5
5
  hideOnMobile?: boolean;
6
6
  }): string;
7
7
  export declare function encodeBlockPropsAttr(props: Record<string, any>): string;
8
+ export declare function buildOutlookBgAttr(color: string): string;
8
9
  //# sourceMappingURL=common.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAqBvD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAIrD,CAAC;AAGF,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,UASA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAQvE"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAqBvD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAIrD,CAAC;AAGF,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,UASA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAQvE;AAYD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGxD"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractVimeoId = exports.extractYouTubeId = void 0;
4
4
  exports.getVisibilityClass = getVisibilityClass;
5
5
  exports.encodeBlockPropsAttr = encodeBlockPropsAttr;
6
+ exports.buildOutlookBgAttr = buildOutlookBgAttr;
6
7
  const extractYouTubeId = (url) => {
7
8
  try {
8
9
  const u = new URL(url);
@@ -53,3 +54,16 @@ function encodeBlockPropsAttr(props) {
53
54
  .replace(/</g, "&lt;")
54
55
  .replace(/>/g, "&gt;");
55
56
  }
57
+ function toOutlookBgColor(color) {
58
+ if (!color || color === 'transparent')
59
+ return '';
60
+ const m = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*[\d.]+)?\s*\)/);
61
+ if (m) {
62
+ return '#' + [m[1], m[2], m[3]].map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
63
+ }
64
+ return color;
65
+ }
66
+ function buildOutlookBgAttr(color) {
67
+ const normalized = toOutlookBgColor(color);
68
+ return normalized ? ` bgcolor="${normalized}"` : '';
69
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"convertJsonToHtml.d.ts","sourceRoot":"","sources":["../../src/utils/convertJsonToHtml.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,GAAU,UAAU,GAAG,oBAuLpD,CAAC"}
1
+ {"version":3,"file":"convertJsonToHtml.d.ts","sourceRoot":"","sources":["../../src/utils/convertJsonToHtml.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,GAAU,UAAU,GAAG,oBAqNpD,CAAC"}
@@ -86,9 +86,14 @@ const convertJsonToHtml = async (jsonData) => {
86
86
  }
87
87
 
88
88
  @media only screen and (max-width: 600px) {
89
+ body {
90
+ overflow-x: hidden !important;
91
+ }
92
+
89
93
  .email-container {
90
94
  width: 100% !important;
91
95
  max-width: 100% !important;
96
+ overflow-x: hidden !important;
92
97
  }
93
98
 
94
99
  .stack-column,
@@ -96,6 +101,29 @@ const convertJsonToHtml = async (jsonData) => {
96
101
  display: block !important;
97
102
  width: 100% !important;
98
103
  max-width: 100% !important;
104
+ /* Prevents horizontal padding from adding to 100% width once display:block is applied */
105
+ box-sizing: border-box !important;
106
+ }
107
+
108
+ /* Images: drop the inline pixel height so aspect ratio is preserved as width shrinks,
109
+ and cap width to the container. Matches the canvas ImageBlock (height:auto, max-width:100%). */
110
+ img {
111
+ height: auto !important;
112
+ max-width: 100% !important;
113
+ }
114
+
115
+ /* Safety net for clients that keep inline max-width:600px on nested wrappers */
116
+ table[style*="max-width:600px"] {
117
+ width: 100% !important;
118
+ max-width: 100% !important;
119
+ }
120
+
121
+ /* Column-gap spacer tds have no stacking behaviour — collapse them when columns stack */
122
+ .col-gap-spacer {
123
+ display: none !important;
124
+ width: 0 !important;
125
+ max-height: 0 !important;
126
+ overflow: hidden !important;
99
127
  }
100
128
 
101
129
  .hide-mobile {
@@ -129,6 +157,7 @@ const convertJsonToHtml = async (jsonData) => {
129
157
  <table
130
158
  role="presentation"
131
159
  class="email-container"
160
+ align="center"
132
161
  bgcolor="${canvasColor || '#ffffff'}"
133
162
  cellpadding="0"
134
163
  cellspacing="0"
@@ -140,6 +169,7 @@ const convertJsonToHtml = async (jsonData) => {
140
169
  width: 100%;
141
170
  max-width: 600px;
142
171
  margin: 0 auto;
172
+ overflow: hidden;
143
173
  background-color: ${canvasColor || '#ffffff'};
144
174
  ${textColor ? `color: ${textColor};` : ''}
145
175
  ${borderWidth ? `border: ${borderWidth}px ${borderStyle || 'solid'} ${borderColor || 'transparent'};` : ''}
@@ -147,7 +177,7 @@ const convertJsonToHtml = async (jsonData) => {
147
177
  >
148
178
  <tbody>
149
179
  <tr>
150
- <td style="padding: ${top}px ${right}px ${bottom}px ${left}px;">
180
+ <td style="padding: ${top}px ${right}px ${bottom}px ${left}px; max-width: 600px; overflow: hidden; box-sizing: border-box; word-break: break-word; overflow-wrap: anywhere;">
151
181
  ${blocksHtml.join("")}
152
182
  </td>
153
183
  </tr>
@@ -1 +1 @@
1
- {"version":3,"file":"outlookSupport.d.ts","sourceRoot":"","sources":["../../src/utils/outlookSupport.ts"],"names":[],"mappings":"AAIA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOtD;AAOD,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,UAqClB;AAED,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,GAAQ,EACf,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,mBA2ErB;AAED,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAO7G"}
1
+ {"version":3,"file":"outlookSupport.d.ts","sourceRoot":"","sources":["../../src/utils/outlookSupport.ts"],"names":[],"mappings":"AAKA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOtD;AAOD,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,UAyClB;AAED,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,GAAQ,EACf,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,mBA2FrB;AAED,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAO7G"}
@@ -5,6 +5,7 @@ exports.appendOutlookSupport = appendOutlookSupport;
5
5
  exports.appendOutlookForImage = appendOutlookForImage;
6
6
  exports.loadImageNaturalDimensions = loadImageNaturalDimensions;
7
7
  const buildStyles_1 = require("./buildStyles");
8
+ const common_1 = require("./common");
8
9
  // Converts rgba(r,g,b,a) → #rrggbb for use in HTML bgcolor attributes.
9
10
  // Old Outlook's bgcolor attribute only accepts solid hex or named colors — rgba is silently ignored.
10
11
  function toOutlookBgColor(color) {
@@ -23,10 +24,11 @@ function extractBgColor(styleStr) {
23
24
  function appendOutlookSupport(content, contentStyle, className, msoWidth) {
24
25
  const visibilityClass = className || "";
25
26
  const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
26
- // bgcolor attribute is the reliable fallback for solid colors in Outlook and webmail
27
- // forwarding chains where CSS background-color gets stripped.
27
+ // Old Outlook (Word engine) often ignores CSS background-color on deeply-nested <td>
28
+ // elements. The bgcolor HTML attribute is the reliable fallback for solid colors.
29
+ // rgba() is stripped from CSS by the Word engine — convert to hex first.
28
30
  const rawBg = extractBgColor(contentStyle);
29
- const bgAttr = rawBg ? ` bgcolor="${toOutlookBgColor(rawBg)}"` : '';
31
+ const bgAttr = (0, common_1.buildOutlookBgAttr)(rawBg);
30
32
  if (shouldHideInOutlook) {
31
33
  return `
32
34
  <!--[if !mso]><!-->
@@ -34,13 +36,16 @@ function appendOutlookSupport(content, contentStyle, className, msoWidth) {
34
36
  <!--<![endif]-->
35
37
  `;
36
38
  }
39
+ // When an explicit pixel width is provided (e.g. inside a column cell), use dual MSO/non-MSO
40
+ // tables. Old Outlook (Word engine) ignores max-width and can resolve width="100%" to the
41
+ // full email width (600px) rather than the column width, causing images/buttons to expand.
37
42
  if (msoWidth) {
38
43
  return `
39
44
  <!--[if mso]>
40
45
  <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" style="border-collapse:collapse;width:${msoWidth}px;"><tr><td width="${msoWidth}"${bgAttr} style="${contentStyle}">
41
46
  <![endif]-->
42
47
  <!--[if !mso]><!-->
43
- <table data-ebr-role="wrapper" role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" style="${buildStyles_1.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%"${bgAttr} style="${contentStyle}">
48
+ <table data-ebr-role="wrapper" role="presentation" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" style="${buildStyles_1.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%"${bgAttr} style="${contentStyle}">
44
49
  <!--<![endif]-->
45
50
  ${content}
46
51
  <!--[if mso]></td></tr></table><![endif]-->
@@ -50,13 +55,15 @@ ${content}
50
55
  `;
51
56
  }
52
57
  return `
53
- <table data-ebr-role="wrapper" role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" style="${buildStyles_1.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%"${bgAttr} style="${contentStyle}">${content}</td></tr></table>
58
+ <table data-ebr-role="wrapper" role="presentation" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" style="${buildStyles_1.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%"${bgAttr} style="${contentStyle}">${content}</td></tr></table>
54
59
  `;
55
60
  }
56
61
  async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
62
+ // OUTLOOK FIX: Use provided dimensions or calculate from image
57
63
  let vmlWidth;
58
64
  let vmlHeight;
59
65
  if (finalWidth && finalHeight) {
66
+ // Use pre-calculated dimensions (preferred for accuracy)
60
67
  vmlWidth = finalWidth;
61
68
  vmlHeight = finalHeight;
62
69
  }
@@ -80,12 +87,18 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
80
87
  const borderColor = style?.borderColor || "transparent";
81
88
  const borderRadius = parseInt(style?.borderRadius) || 0;
82
89
  const useRoundRect = borderRadius > 0;
83
- const arcsize = useRoundRect ? Math.min(borderRadius / vmlHeight, 1).toFixed(2) : "";
90
+ const arcsize = useRoundRect
91
+ ? Math.min(borderRadius / vmlHeight, 1).toFixed(2)
92
+ : "";
84
93
  const borderAttributes = borderWidth > 0
85
94
  ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
86
95
  : `stroked="false"`;
96
+ // OUTLOOK FIX: For Outlook 2019+ (version 2512), VML type="frame" causes stretching
97
+ // Solution: Use simple IMG tag with fixed dimensions for Outlook, only use VML for border radius
87
98
  let outlookImage;
88
99
  if (useRoundRect && borderRadius > 0) {
100
+ // Use VML for border radius - wrap in table to constrain width for Old Outlook (Word engine)
101
+ // Use aspect="atmost" to prevent image from stretching beyond its bounds
89
102
  outlookImage = `<!--[if mso]>
90
103
  <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
91
104
  <tr>
@@ -104,7 +117,11 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
104
117
  <![endif]-->`;
105
118
  }
106
119
  else {
107
- const borderStyleAttr = borderWidth > 0 ? `border: ${borderWidth}px solid ${borderColor};` : '';
120
+ // For images without border radius, wrap in a table with explicit width for Old Outlook (Word engine)
121
+ // This prevents stretching/overflow in Outlook 2007-2019 and Outlook Classic
122
+ const borderStyleAttr = borderWidth > 0
123
+ ? `border: ${borderWidth}px solid ${borderColor};`
124
+ : '';
108
125
  outlookImage = `<!--[if mso]>
109
126
  <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
110
127
  <tr>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-builder-utils",
3
- "version": "1.1.49",
3
+ "version": "1.1.50",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [