email-builder-utils 1.1.48 → 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":"AAKA,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;;;GAuD5B"}
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"}
@@ -11,12 +11,17 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
11
11
  const { columns = 1, cellWidths = [], responsive = true } = props;
12
12
  const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
13
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).
14
16
  const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : '';
15
17
  const customCssStr = restStyle.customCss || '';
18
+ // Extract gradient string from customCss if not already in backgroundImage
16
19
  const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
17
20
  ? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
18
21
  : '';
19
- const effectiveGradient = bgImageStr.includes('gradient(') ? bgImageStr : gradientInCustomCss;
22
+ const effectiveGradient = bgImageStr.includes('gradient(')
23
+ ? bgImageStr
24
+ : gradientInCustomCss;
20
25
  const isGradient = Boolean(effectiveGradient);
21
26
  const parsedGradient = isGradient ? (0, gradientUtils_1.parseGradient)(effectiveGradient) : null;
22
27
  const fallbackBgColor = backgroundColor ||
@@ -26,12 +31,18 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
26
31
  const rawBgImageUrl = !isGradient && bgImageStr
27
32
  ? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
28
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).
29
36
  const innerCustomCss = gradientInCustomCss
30
37
  ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
31
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.
32
41
  const innerRestStyleRaw = (rawBgImageUrl || isGradient)
33
42
  ? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
34
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>).
35
46
  const { borderRadius, border, borderColor, borderWidth, borderStyle: borderStyleProp, ...innerRestStyle } = innerRestStyleRaw;
36
47
  const divBorderParts = [];
37
48
  if (borderRadius)
@@ -50,16 +61,31 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
50
61
  ? 'transparent'
51
62
  : ((rawBgImageUrl || isGradient) ? undefined : backgroundColor);
52
63
  const tableStyles = (0, buildStyles_1.buildStyles)({ backgroundColor: tableBgForNonMso, ...innerRestStyle }, {
53
- perChanges: [], pxChanges: buildStyles_1.allPxAttributes,
64
+ perChanges: [],
65
+ pxChanges: buildStyles_1.allPxAttributes,
54
66
  });
55
67
  const total = childrenIds.length;
56
68
  const visualRows = Math.ceil(total / columns);
69
+ // OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
57
70
  const msoTableWidth = Math.min(cellWidthInPx, 600);
58
- const msoBgColor = !rawBgImageUrl && !isGradient ? (backgroundColor || '') : '';
59
- const msoBgAttr = msoBgColor ? ` bgcolor="${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);
60
78
  const msoBgStyle = msoBgColor ? `background-color:${msoBgColor};` : '';
61
- const innerBgTransparent = (rawBgImageUrl || isGradient) ? 'background-color:transparent;' : '';
62
- const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor && !divBorderStyle ? ` bgcolor="${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.
63
89
  const divWrapBg = divBorderStyle && backgroundColor && !rawBgImageUrl && !isGradient
64
90
  ? ` background-color:${backgroundColor};`
65
91
  : '';
@@ -72,14 +98,15 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
72
98
  class="${gridVisibilityClass}">
73
99
  <![endif]-->
74
100
  <!--[if !mso]><!-->
75
- <table border="0" cellpadding="0" cellspacing="0" width="100%"
101
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" align="center"
76
102
  role="presentation"${nonMsoBgAttr}
77
- style="border-collapse:collapse; ${innerBgTransparent}${tableStyles}; max-width:600px;"
103
+ style="border-collapse:collapse; table-layout:fixed; ${innerBgTransparent}${tableStyles}; max-width:600px;"
78
104
  class="${gridVisibilityClass}">
79
105
  <!--<![endif]-->
80
106
  `;
81
107
  for (let r = 0; r < visualRows; r++) {
82
108
  html += "<tr>";
109
+ // COUNT visible cells and find last visible column index
83
110
  let visibleCells = 0;
84
111
  let lastVisibleCol = 0;
85
112
  const rowIds = [];
@@ -88,12 +115,14 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
88
115
  const id = childrenIds[idx] ?? null;
89
116
  rowIds.push(id);
90
117
  const child = id ? rootData[id] : null;
91
- if (!child?.data?.props?.hideOnDesktop) {
118
+ const isHidden = child?.data?.props?.hideOnDesktop;
119
+ if (!isHidden) {
92
120
  visibleCells++;
93
121
  lastVisibleCol = c;
94
122
  }
95
123
  }
96
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)
97
126
  const totalGapPx = columnGap * Math.max(visibleCells - 1, 0);
98
127
  const adjustedTableWidth = Math.max(msoTableWidth - totalGapPx, 1);
99
128
  let totalWidth = 0;
@@ -101,19 +130,24 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
101
130
  for (let c = 0; c < columns; c++) {
102
131
  const id = rowIds[c];
103
132
  let widthPercent = cellWidths[c] ?? safeWidth;
104
- if (widthPercent <= 0 || widthPercent > 100)
133
+ if (widthPercent <= 0 || widthPercent > 100) {
105
134
  widthPercent = safeWidth;
135
+ }
106
136
  cellWidthPercents.push(widthPercent);
107
137
  if (id) {
108
138
  const child = rootData[id];
109
- if (!child?.data?.props?.hideOnDesktop)
139
+ const isHidden = child?.data?.props?.hideOnDesktop;
140
+ if (!isHidden) {
110
141
  totalWidth += widthPercent;
142
+ }
111
143
  }
112
144
  }
113
145
  const scaleFactor = totalWidth > 0 && totalWidth < 100 ? 100 / totalWidth : 1;
114
146
  for (let c = 0; c < columns; c++) {
115
147
  const id = rowIds[c];
116
- 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
117
151
  const cellWidthPx = Math.round((widthPercent / 100) * adjustedTableWidth);
118
152
  if (id) {
119
153
  const child = rootData[id];
@@ -123,34 +157,40 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
123
157
  const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
124
158
  if (childVisible) {
125
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).
126
162
  const cellBgColor = cellStyle.backgroundColor || '';
127
- const cellBgAttr = cellBgColor ? ` bgcolor="${cellBgColor}"` : '';
163
+ const cellBgAttr = (0, common_1.buildOutlookBgAttr)(cellBgColor);
128
164
  html += `
129
165
  <td
130
- width="${cellWidthPx}"${cellBgAttr}
166
+ width="${Math.max(1, Math.round(widthPercent))}%"${cellBgAttr}
131
167
  class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
132
- style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
168
+ style="width:${widthPercent}%;vertical-align:${verticalAlign};word-break:break-word;${styles}"
133
169
  >
134
170
  ${childHtml}
135
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.
136
175
  if (columnGap > 0 && c !== lastVisibleCol) {
137
- 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>`;
138
177
  }
139
178
  }
140
179
  }
141
180
  else {
142
181
  html += `
143
- <td width="${cellWidthPx}"
182
+ <td width="${Math.max(1, Math.round(widthPercent))}%"
144
183
  ${responsive ? 'class="stack-column"' : ""}
145
- style="width:${cellWidthPx}px;vertical-align:top;">
184
+ style="width:${widthPercent}%;vertical-align:top;">
146
185
  </td>`;
147
186
  if (columnGap > 0 && c !== lastVisibleCol) {
148
- 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>`;
149
188
  }
150
189
  }
151
190
  }
152
191
  html += "</tr>";
153
192
  }
193
+ // Close both MSO and non-MSO tables
154
194
  html += `
155
195
  <!--[if mso]>
156
196
  </table>
@@ -159,6 +199,21 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
159
199
  </table>
160
200
  <!--<![endif]-->
161
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.
162
217
  if (rawBgImageUrl || isGradient) {
163
218
  const vmlFill = isGradient
164
219
  ? (() => {
@@ -170,9 +225,9 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
170
225
  : `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
171
226
  html = `
172
227
  <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
173
- style="border-collapse:collapse;width:100%;max-width:${msoTableWidth}px;">
228
+ style="border-collapse:collapse;table-layout:fixed;width:100%;max-width:${msoTableWidth}px;">
174
229
  <tr>
175
- <td width="100%" bgcolor="${fallbackBgColor}" valign="top"
230
+ <td width="100%"${(0, common_1.buildOutlookBgAttr)(fallbackBgColor)} valign="top"
176
231
  ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
177
232
  style="
178
233
  width:100%;max-width:${msoTableWidth}px;
@@ -199,32 +254,68 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
199
254
  </tr>
200
255
  </table>`;
201
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.
202
261
  if (divBorderStyle)
203
262
  html = `${divWrapOpen}${html}${divWrapClose}`;
204
263
  return html;
205
264
  }
206
265
  async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
207
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.
208
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.
209
278
  const stripBgFromTd = Boolean(cellBorderRadius) || parentGridHasBorder;
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.
210
282
  const styleForTd = stripBgFromTd
211
- ? { ...styleWithoutBorder, backgroundColor: 'transparent' }
283
+ ? { ...styleWithoutBorder, backgroundColor: undefined }
212
284
  : styleWithoutBorder;
213
- 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
+ });
214
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
215
296
  const cellWidthPx = Math.round((cellWidthPercent / 100) * parentCellWidthPx);
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.
216
300
  const cellPad = styleWithoutBorder?.padding || {};
217
301
  const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
218
302
  const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
219
303
  const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
304
+ // OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
220
305
  const safeCellWidthPx = Math.min(contentWidthPx, 600);
221
306
  for (const childId of childrenIds) {
222
307
  const child = rootData[childId];
223
- if (child)
308
+ if (child) {
224
309
  parts.push(await (0, jsonToHTML_1.convertToHtml)(child, rootData, safeCellWidthPx));
310
+ }
225
311
  }
226
312
  const borderRadius = cellBorderRadius || 0;
227
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.
228
319
  const cellDivBorderParts = [];
229
320
  if (!parentGridHasBorder) {
230
321
  if (borderRadius)
@@ -240,9 +331,21 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
240
331
  }
241
332
  }
242
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.
243
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};`);
244
343
  if (cellDivBorderStyle)
245
344
  divStyleParts.push(cellDivBorderStyle);
246
- const wrapped = `<div style="${divStyleParts.join(' ')}">${parts.join("")}</div>`;
247
- 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
+ };
248
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 });
45
- 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;" />`;
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
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,UAsItE"}
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, 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,17 +79,24 @@ 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
  })()
@@ -74,8 +108,8 @@ function convertTextBlock(blockData, cellWidthInPx) {
74
108
  <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
75
109
  style="border-collapse:collapse;width:100%;max-width:${msoWidth}px;" class="${visibilityClass}">
76
110
  <tr>
77
- <td width="100%" bgcolor="${fallbackBgColor}" valign="top"
78
- ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ''}
111
+ <td width="100%"${(0, common_1.buildOutlookBgAttr)(fallbackBgColor)} valign="top"
112
+ ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
79
113
  style="width:100%;max-width:${msoWidth}px;background-color:${fallbackBgColor};${bgCss}">
80
114
 
81
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,oBAqLpD,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,8 +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;
95
+ max-width: 100% !important;
96
+ overflow-x: hidden !important;
91
97
  }
92
98
 
93
99
  .stack-column,
@@ -95,6 +101,29 @@ const convertJsonToHtml = async (jsonData) => {
95
101
  display: block !important;
96
102
  width: 100% !important;
97
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;
98
127
  }
99
128
 
100
129
  .hide-mobile {
@@ -128,6 +157,7 @@ const convertJsonToHtml = async (jsonData) => {
128
157
  <table
129
158
  role="presentation"
130
159
  class="email-container"
160
+ align="center"
131
161
  bgcolor="${canvasColor || '#ffffff'}"
132
162
  cellpadding="0"
133
163
  cellspacing="0"
@@ -138,6 +168,8 @@ const convertJsonToHtml = async (jsonData) => {
138
168
  table-layout: fixed;
139
169
  width: 100%;
140
170
  max-width: 600px;
171
+ margin: 0 auto;
172
+ overflow: hidden;
141
173
  background-color: ${canvasColor || '#ffffff'};
142
174
  ${textColor ? `color: ${textColor};` : ''}
143
175
  ${borderWidth ? `border: ${borderWidth}px ${borderStyle || 'solid'} ${borderColor || 'transparent'};` : ''}
@@ -145,7 +177,7 @@ const convertJsonToHtml = async (jsonData) => {
145
177
  >
146
178
  <tbody>
147
179
  <tr>
148
- <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;">
149
181
  ${blocksHtml.join("")}
150
182
  </td>
151
183
  </tr>
@@ -1,3 +1,2 @@
1
- export { tableCommonStyle } from "./buildStyles";
2
1
  export declare function convertToHtml(blockData: any, rootData: any, cellWidthInPx: number): Promise<string>;
3
2
  //# sourceMappingURL=jsonToHTML.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,wBAAsB,aAAa,CAAC,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAuBvF"}
1
+ {"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AAaA,wBAAsB,aAAa,CAAC,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAuBvF"}
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.tableCommonStyle = void 0;
4
3
  exports.convertToHtml = convertToHtml;
5
4
  const types_1 = require("../types");
6
5
  const text_1 = require("./blocks/text");
@@ -10,8 +9,8 @@ const grid_1 = require("./blocks/grid");
10
9
  const dividers_1 = require("./blocks/dividers");
11
10
  const video_1 = require("./blocks/video");
12
11
  const shape_1 = require("./blocks/shape");
13
- var buildStyles_1 = require("./buildStyles");
14
- Object.defineProperty(exports, "tableCommonStyle", { enumerable: true, get: function () { return buildStyles_1.tableCommonStyle; } });
12
+ // Update: Use Gmail Mobile friendly tableCommonStyle
13
+ // export const tableCommonStyle = "border-collapse:collapse; table-layout:fixed; width:100%; max-width:600px; margin:0 auto;";
15
14
  async function convertToHtml(blockData, rootData, cellWidthInPx) {
16
15
  switch (blockData.type) {
17
16
  case types_1.BlockType.TEXT:
@@ -1,3 +1,4 @@
1
+ export declare function toOutlookBgColor(color: string): string;
1
2
  export declare function appendOutlookSupport(content: string, contentStyle: string, className?: string, msoWidth?: number): string;
2
3
  export declare function appendOutlookForImage(content: string, outerContainerWidth: number, innerContainerWidth: number, imageUrl: string, style?: any, finalWidth?: number, finalHeight?: number): Promise<string>;
3
4
  export declare function loadImageNaturalDimensions(imageUrl: string): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"outlookSupport.d.ts","sourceRoot":"","sources":["../../src/utils/outlookSupport.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,UAgClB;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"}
@@ -1,26 +1,51 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toOutlookBgColor = toOutlookBgColor;
3
4
  exports.appendOutlookSupport = appendOutlookSupport;
4
5
  exports.appendOutlookForImage = appendOutlookForImage;
5
6
  exports.loadImageNaturalDimensions = loadImageNaturalDimensions;
6
7
  const buildStyles_1 = require("./buildStyles");
8
+ const common_1 = require("./common");
9
+ // Converts rgba(r,g,b,a) → #rrggbb for use in HTML bgcolor attributes.
10
+ // Old Outlook's bgcolor attribute only accepts solid hex or named colors — rgba is silently ignored.
11
+ function toOutlookBgColor(color) {
12
+ if (!color || color === 'transparent')
13
+ return '';
14
+ const m = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*[\d.]+)?\s*\)/);
15
+ if (m) {
16
+ return '#' + [m[1], m[2], m[3]].map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
17
+ }
18
+ return color;
19
+ }
20
+ function extractBgColor(styleStr) {
21
+ const m = styleStr.match(/background-color\s*:\s*([^;]+)/i);
22
+ return m ? m[1].trim() : '';
23
+ }
7
24
  function appendOutlookSupport(content, contentStyle, className, msoWidth) {
8
25
  const visibilityClass = className || "";
9
26
  const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
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.
30
+ const rawBg = extractBgColor(contentStyle);
31
+ const bgAttr = (0, common_1.buildOutlookBgAttr)(rawBg);
10
32
  if (shouldHideInOutlook) {
11
33
  return `
12
34
  <!--[if !mso]><!-->
13
- <table data-ebr-role="wrapper" role="presentation" width="100%" style="${buildStyles_1.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
35
+ <table data-ebr-role="wrapper" role="presentation" width="100%" style="${buildStyles_1.tableCommonStyle}" class="${visibilityClass}"><tr><td${bgAttr} style="${contentStyle}">${content}</td></tr></table>
14
36
  <!--<![endif]-->
15
37
  `;
16
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.
17
42
  if (msoWidth) {
18
43
  return `
19
44
  <!--[if mso]>
20
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" style="border-collapse:collapse;width:${msoWidth}px;"><tr><td width="${msoWidth}" style="${contentStyle}">
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}">
21
46
  <![endif]-->
22
47
  <!--[if !mso]><!-->
23
- <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%" 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}">
24
49
  <!--<![endif]-->
25
50
  ${content}
26
51
  <!--[if mso]></td></tr></table><![endif]-->
@@ -30,13 +55,15 @@ ${content}
30
55
  `;
31
56
  }
32
57
  return `
33
- <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%" 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>
34
59
  `;
35
60
  }
36
61
  async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
62
+ // OUTLOOK FIX: Use provided dimensions or calculate from image
37
63
  let vmlWidth;
38
64
  let vmlHeight;
39
65
  if (finalWidth && finalHeight) {
66
+ // Use pre-calculated dimensions (preferred for accuracy)
40
67
  vmlWidth = finalWidth;
41
68
  vmlHeight = finalHeight;
42
69
  }
@@ -60,12 +87,18 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
60
87
  const borderColor = style?.borderColor || "transparent";
61
88
  const borderRadius = parseInt(style?.borderRadius) || 0;
62
89
  const useRoundRect = borderRadius > 0;
63
- const arcsize = useRoundRect ? Math.min(borderRadius / vmlHeight, 1).toFixed(2) : "";
90
+ const arcsize = useRoundRect
91
+ ? Math.min(borderRadius / vmlHeight, 1).toFixed(2)
92
+ : "";
64
93
  const borderAttributes = borderWidth > 0
65
94
  ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
66
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
67
98
  let outlookImage;
68
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
69
102
  outlookImage = `<!--[if mso]>
70
103
  <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
71
104
  <tr>
@@ -84,7 +117,11 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
84
117
  <![endif]-->`;
85
118
  }
86
119
  else {
87
- 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
+ : '';
88
125
  outlookImage = `<!--[if mso]>
89
126
  <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
90
127
  <tr>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-builder-utils",
3
- "version": "1.1.48",
3
+ "version": "1.1.50",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [