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