email-builder-utils 1.1.40 → 1.1.44
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 +91 -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 +797 -432
- package/dist/utils/outlookSupport.d.ts +210 -0
- package/dist/utils/outlookSupport.d.ts.map +1 -0
- package/dist/utils/outlookSupport.js +479 -0
- package/package.json +34 -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,18 @@ 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
|
-
return obj.map(cleanJson);
|
|
23
|
-
return Object.fromEntries(Object.entries(obj)
|
|
24
|
-
.filter(([_, value]) => value !== undefined && value !== null && value !== "")
|
|
25
|
-
.map(([key, value]) => [key, cleanJson(value)]));
|
|
18
|
+
function encodeBlockProps(props) {
|
|
19
|
+
return JSON.stringify(props)
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/"/g, '"');
|
|
26
22
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.join("");
|
|
23
|
+
async function loadImageNaturalDimensions(imageUrl) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const img = new Image();
|
|
26
|
+
img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
27
|
+
img.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`));
|
|
28
|
+
img.src = imageUrl;
|
|
29
|
+
});
|
|
35
30
|
}
|
|
36
31
|
function buildStyles(style, { pxChanges, perChanges }) {
|
|
37
32
|
if (!style)
|
|
@@ -47,7 +42,9 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
47
42
|
"childWidth",
|
|
48
43
|
"visibility",
|
|
49
44
|
"hideOnMobile",
|
|
50
|
-
"hideOnDesktop"
|
|
45
|
+
"hideOnDesktop",
|
|
46
|
+
"label",
|
|
47
|
+
"alignment",
|
|
51
48
|
];
|
|
52
49
|
if (INVALID_KEYS.includes(key))
|
|
53
50
|
return;
|
|
@@ -66,11 +63,26 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
66
63
|
};
|
|
67
64
|
value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
|
|
68
65
|
}
|
|
66
|
+
if (key === "fontFamily" && typeof value === "string") {
|
|
67
|
+
value = (0, fontFallback_1.withFontFallback)(value).replace(/"/g, "'");
|
|
68
|
+
}
|
|
69
|
+
// Wrap backgroundImage values in url() if not already wrapped — skip gradients
|
|
70
|
+
if (key === "backgroundImage" && typeof value === "string"
|
|
71
|
+
&& !String(value).startsWith("url(")
|
|
72
|
+
&& !String(value).toLowerCase().includes("gradient(")) {
|
|
73
|
+
value = `url('${value}')`;
|
|
74
|
+
}
|
|
75
|
+
// lineHeight: values >= 4 are pixel values; smaller values are unitless multipliers (e.g. 1.5)
|
|
76
|
+
if (key === "lineHeight" && typeof value === "number") {
|
|
77
|
+
stylesObj["line-height"] = value >= 4 ? `${value}px` : String(value);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
69
80
|
const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
70
81
|
// FIX 2 — Sanitize invalid px/per values
|
|
71
82
|
if (pxChanges.includes(key)) {
|
|
72
83
|
if (typeof value === "number") {
|
|
73
|
-
|
|
84
|
+
const rounded = Math.round(value * 100) / 100;
|
|
85
|
+
stylesObj[cssKey] = `${rounded}px`;
|
|
74
86
|
}
|
|
75
87
|
else if (typeof value === "string" && value.includes("null")) {
|
|
76
88
|
// Skip invalid styles
|
|
@@ -92,12 +104,17 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
92
104
|
stylesObj[cssKey] = value;
|
|
93
105
|
}
|
|
94
106
|
});
|
|
95
|
-
|
|
107
|
+
const parts = Object.entries(stylesObj)
|
|
108
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
109
|
+
.map(([k, v]) => `${k}:${v}`);
|
|
110
|
+
if (style.customCss)
|
|
111
|
+
parts.push(style.customCss);
|
|
112
|
+
return parts.join('; ').replace(/;\s*$/, '').trim();
|
|
96
113
|
}
|
|
97
114
|
async function convertToHtml(blockData, rootData, cellWidthInPx) {
|
|
98
115
|
switch (blockData.type) {
|
|
99
116
|
case types_1.BlockType.TEXT:
|
|
100
|
-
return convertTextBlock(blockData);
|
|
117
|
+
return convertTextBlock(blockData, cellWidthInPx);
|
|
101
118
|
case types_1.BlockType.IMAGE:
|
|
102
119
|
return await convertImageBlock(blockData, cellWidthInPx);
|
|
103
120
|
case types_1.BlockType.BUTTON:
|
|
@@ -118,26 +135,36 @@ async function convertToHtml(blockData, rootData, cellWidthInPx) {
|
|
|
118
135
|
return "";
|
|
119
136
|
}
|
|
120
137
|
}
|
|
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) {
|
|
138
|
+
function appendOutlookSupport(content, contentStyle, className, msoWidth) {
|
|
127
139
|
const visibilityClass = className || "";
|
|
128
140
|
const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
|
|
129
141
|
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]-->
|
|
142
|
+
return `
|
|
143
|
+
<!--[if !mso]><!-->
|
|
144
|
+
<table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
|
|
145
|
+
<!--<![endif]-->
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
// When an explicit pixel width is provided (e.g. inside a column cell), use dual MSO/non-MSO
|
|
149
|
+
// tables. Old Outlook (Word engine) ignores max-width and can resolve width="100%" to the
|
|
150
|
+
// full email width (600px) rather than the column width, causing images/buttons to expand.
|
|
151
|
+
if (msoWidth) {
|
|
152
|
+
return `
|
|
153
|
+
<!--[if mso]>
|
|
154
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" style="border-collapse:collapse;width:${msoWidth}px;"><tr><td width="${msoWidth}" style="${contentStyle}">
|
|
155
|
+
<![endif]-->
|
|
156
|
+
<!--[if !mso]><!-->
|
|
157
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">
|
|
158
|
+
<!--<![endif]-->
|
|
159
|
+
${content}
|
|
160
|
+
<!--[if mso]></td></tr></table><![endif]-->
|
|
161
|
+
<!--[if !mso]><!-->
|
|
162
|
+
</td></tr></table>
|
|
163
|
+
<!--<![endif]-->
|
|
134
164
|
`;
|
|
135
165
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// width="100%" works for modern clients, Outlook will use the td width from nested tables
|
|
139
|
-
return `
|
|
140
|
-
<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>
|
|
166
|
+
return `
|
|
167
|
+
<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>
|
|
141
168
|
`;
|
|
142
169
|
}
|
|
143
170
|
function convertDividerBlockToHtml(blockData) {
|
|
@@ -156,21 +183,21 @@ function convertDividerBlockToHtml(blockData) {
|
|
|
156
183
|
]
|
|
157
184
|
.filter(Boolean)
|
|
158
185
|
.join(" ");
|
|
159
|
-
const dividerContent = `
|
|
160
|
-
<table
|
|
161
|
-
width="${dividerWidth}%"
|
|
162
|
-
cellpadding="0"
|
|
163
|
-
cellspacing="0"
|
|
164
|
-
>
|
|
165
|
-
<tr>
|
|
166
|
-
<td
|
|
167
|
-
height="${thickness}"
|
|
168
|
-
style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
|
|
169
|
-
>
|
|
170
|
-
|
|
171
|
-
</td>
|
|
172
|
-
</tr>
|
|
173
|
-
</table>
|
|
186
|
+
const dividerContent = `
|
|
187
|
+
<table
|
|
188
|
+
width="${dividerWidth}%"
|
|
189
|
+
cellpadding="0"
|
|
190
|
+
cellspacing="0"
|
|
191
|
+
>
|
|
192
|
+
<tr>
|
|
193
|
+
<td
|
|
194
|
+
height="${thickness}"
|
|
195
|
+
style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
|
|
196
|
+
>
|
|
197
|
+
|
|
198
|
+
</td>
|
|
199
|
+
</tr>
|
|
200
|
+
</table>
|
|
174
201
|
`;
|
|
175
202
|
return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
|
|
176
203
|
}
|
|
@@ -183,28 +210,13 @@ function convertSpacerBlockToHtml(blockData) {
|
|
|
183
210
|
});
|
|
184
211
|
return appendOutlookSupport(``, styles, visibilityClass);
|
|
185
212
|
}
|
|
186
|
-
|
|
187
|
-
// const { style, props } = blockData.data;
|
|
188
|
-
// const styles = buildStyles(style, {
|
|
189
|
-
// perChanges: [],
|
|
190
|
-
// pxChanges: allPxAttributes,
|
|
191
|
-
// });
|
|
192
|
-
// const text = props.text || "";
|
|
193
|
-
// const navigateToUrl = props.navigateToUrl || "";
|
|
194
|
-
// const textContent = appendOutlookSupport(
|
|
195
|
-
// text.replaceAll(/\n/g, "<br>"),
|
|
196
|
-
// styles
|
|
197
|
-
// );
|
|
198
|
-
// return navigateToUrl
|
|
199
|
-
// ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit; text-decoration:none; cursor:pointer;">${textContent}</a>`
|
|
200
|
-
// : textContent;
|
|
201
|
-
// }
|
|
202
|
-
function convertTextBlock(blockData) {
|
|
213
|
+
function convertTextBlock(blockData, cellWidthInPx) {
|
|
203
214
|
const { style, props } = blockData.data;
|
|
204
215
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
205
|
-
const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding,
|
|
216
|
+
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
|
|
217
|
+
...rest } = style;
|
|
218
|
+
// Text box decoration styles (border, background, padding) — no width
|
|
206
219
|
const textBoxStyle = {
|
|
207
|
-
width,
|
|
208
220
|
backgroundColor,
|
|
209
221
|
padding,
|
|
210
222
|
borderRadius,
|
|
@@ -216,6 +228,7 @@ function convertTextBlock(blockData) {
|
|
|
216
228
|
perChanges: [],
|
|
217
229
|
pxChanges: allPxAttributes,
|
|
218
230
|
});
|
|
231
|
+
// Outer td styles: layout only, no typography, no white-space
|
|
219
232
|
const styles = buildStyles({
|
|
220
233
|
padding: textContainerPadding,
|
|
221
234
|
backgroundColor: textContainerBackgroundColor,
|
|
@@ -225,11 +238,15 @@ function convertTextBlock(blockData) {
|
|
|
225
238
|
pxChanges: allPxAttributes,
|
|
226
239
|
});
|
|
227
240
|
const sanitizedText = (props.text ?? "")
|
|
228
|
-
.
|
|
229
|
-
.
|
|
241
|
+
.replace(/<p(\s[^>]*)?>/gi, (_, attrs) => `<div${attrs || ""}>`)
|
|
242
|
+
.replace(/<\/p>/gi, "</div>");
|
|
230
243
|
const navigateToUrl = props.navigateToUrl || "";
|
|
231
|
-
const
|
|
232
|
-
|
|
244
|
+
const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
|
|
245
|
+
// Use display:block + width:100% so text fills the column naturally.
|
|
246
|
+
// display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
|
|
247
|
+
const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${fontSizeStyle}${convertedTextStyle}">${sanitizedText.replaceAll(/\n/g, "<br>")}</div>`;
|
|
248
|
+
const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
|
|
249
|
+
const textContent = appendOutlookSupport(convertedTextBox, styles, visibilityClass, safeCellWidth);
|
|
233
250
|
return navigateToUrl
|
|
234
251
|
? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit;text-decoration:none;cursor:pointer;">${textContent}</a>`
|
|
235
252
|
: textContent;
|
|
@@ -243,15 +260,21 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
243
260
|
vmlWidth = finalWidth;
|
|
244
261
|
vmlHeight = finalHeight;
|
|
245
262
|
}
|
|
263
|
+
else if (imageUrl) {
|
|
264
|
+
try {
|
|
265
|
+
const { width: originalWidth, height: originalHeight } = await loadImageNaturalDimensions(imageUrl);
|
|
266
|
+
const widthScalingFactor = Math.min(outerContainerWidth / originalWidth, innerContainerWidth / originalWidth, 1);
|
|
267
|
+
vmlWidth = Math.round(originalWidth * widthScalingFactor);
|
|
268
|
+
vmlHeight = Math.round(originalHeight * widthScalingFactor);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
vmlWidth = innerContainerWidth;
|
|
272
|
+
vmlHeight = innerContainerWidth;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
246
275
|
else {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const originalWidth = image.bitmap.width;
|
|
250
|
-
const originalHeight = image.bitmap.height;
|
|
251
|
-
const widthScalingFactor = Math.min(outerContainerWidth / originalWidth, innerContainerWidth / originalWidth, 1 // Never scale up
|
|
252
|
-
);
|
|
253
|
-
vmlWidth = Math.round(originalWidth * widthScalingFactor);
|
|
254
|
-
vmlHeight = Math.round(originalHeight * widthScalingFactor);
|
|
276
|
+
vmlWidth = innerContainerWidth;
|
|
277
|
+
vmlHeight = innerContainerWidth;
|
|
255
278
|
}
|
|
256
279
|
const borderWidth = parseInt(style?.borderWidth) || 0;
|
|
257
280
|
const borderColor = style?.borderColor || "transparent";
|
|
@@ -269,21 +292,21 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
269
292
|
if (useRoundRect && borderRadius > 0) {
|
|
270
293
|
// Use VML for border radius - wrap in table to constrain width for Old Outlook (Word engine)
|
|
271
294
|
// Use aspect="atmost" to prevent image from stretching beyond its bounds
|
|
272
|
-
outlookImage = `<!--[if mso]>
|
|
273
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
274
|
-
<tr>
|
|
275
|
-
<td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
276
|
-
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
277
|
-
style="width:${vmlWidth}px;height:${vmlHeight}px;"
|
|
278
|
-
${borderAttributes}
|
|
279
|
-
arcsize="${arcsize}"
|
|
280
|
-
fill="true" fillcolor="none">
|
|
281
|
-
<v:fill src="${imageUrl}" type="tile" aspect="atmost" />
|
|
282
|
-
<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>
|
|
283
|
-
</v:roundrect>
|
|
284
|
-
</td>
|
|
285
|
-
</tr>
|
|
286
|
-
</table>
|
|
295
|
+
outlookImage = `<!--[if mso]>
|
|
296
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
297
|
+
<tr>
|
|
298
|
+
<td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
299
|
+
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
300
|
+
style="width:${vmlWidth}px;height:${vmlHeight}px;"
|
|
301
|
+
${borderAttributes}
|
|
302
|
+
arcsize="${arcsize}"
|
|
303
|
+
fill="true" fillcolor="none">
|
|
304
|
+
<v:fill src="${imageUrl}" type="tile" aspect="atmost" />
|
|
305
|
+
<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>
|
|
306
|
+
</v:roundrect>
|
|
307
|
+
</td>
|
|
308
|
+
</tr>
|
|
309
|
+
</table>
|
|
287
310
|
<![endif]-->`;
|
|
288
311
|
}
|
|
289
312
|
else {
|
|
@@ -292,31 +315,41 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
292
315
|
const borderStyleAttr = borderWidth > 0
|
|
293
316
|
? `border: ${borderWidth}px solid ${borderColor};`
|
|
294
317
|
: '';
|
|
295
|
-
outlookImage = `<!--[if mso]>
|
|
296
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
297
|
-
<tr>
|
|
298
|
-
<td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
299
|
-
<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}" />
|
|
300
|
-
</td>
|
|
301
|
-
</tr>
|
|
302
|
-
</table>
|
|
318
|
+
outlookImage = `<!--[if mso]>
|
|
319
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
320
|
+
<tr>
|
|
321
|
+
<td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
322
|
+
<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}" />
|
|
323
|
+
</td>
|
|
324
|
+
</tr>
|
|
325
|
+
</table>
|
|
303
326
|
<![endif]-->`;
|
|
304
327
|
}
|
|
305
|
-
return `
|
|
306
|
-
${outlookImage}
|
|
307
|
-
<!--[if !mso]><!-->
|
|
308
|
-
${content}
|
|
309
|
-
<!--<![endif]-->
|
|
328
|
+
return `
|
|
329
|
+
${outlookImage}
|
|
330
|
+
<!--[if !mso]><!-->
|
|
331
|
+
${content}
|
|
332
|
+
<!--<![endif]-->
|
|
310
333
|
`;
|
|
311
334
|
}
|
|
312
335
|
async function computeScaledDimensions(imageUrl, maxContainerWidthPx) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
336
|
+
if (!imageUrl) {
|
|
337
|
+
const w = Math.max(maxContainerWidthPx, 1);
|
|
338
|
+
const h = Math.round(w * (2 / 3));
|
|
339
|
+
return { originalWidth: w, originalHeight: h, scaledWidth: w, scaledHeight: h };
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const { width: originalWidth, height: originalHeight } = await loadImageNaturalDimensions(imageUrl);
|
|
343
|
+
const widthScalingFactor = Math.min(maxContainerWidthPx / originalWidth, 1);
|
|
344
|
+
const scaledWidth = Math.round(originalWidth * widthScalingFactor);
|
|
345
|
+
const scaledHeight = Math.round(originalHeight * widthScalingFactor);
|
|
346
|
+
return { originalWidth, originalHeight, scaledWidth, scaledHeight };
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
const w = Math.max(maxContainerWidthPx, 1);
|
|
350
|
+
const h = Math.round(w * (2 / 3));
|
|
351
|
+
return { originalWidth: w, originalHeight: h, scaledWidth: w, scaledHeight: h };
|
|
352
|
+
}
|
|
320
353
|
}
|
|
321
354
|
async function convertImageBlock(blockData, cellWidthInPx) {
|
|
322
355
|
const { style, props } = blockData.data;
|
|
@@ -359,83 +392,302 @@ async function convertImageBlock(blockData, cellWidthInPx) {
|
|
|
359
392
|
// OUTLOOK FIX: Image element with explicit dimensions
|
|
360
393
|
// Outlook will use width/height attributes, modern clients use CSS
|
|
361
394
|
// Use max-width instead of width:100% to prevent stretching
|
|
362
|
-
const imageElement = `<img src="${imageUrl}" alt="${altText || "Image"}" border="0" width="${finalWidth}" height="${finalHeight}" style="${imageTagStyles}; display:block; max-width:100%; height:auto;" />`;
|
|
395
|
+
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;" />`;
|
|
363
396
|
const percentWidth = typeof width === "string" && width.endsWith("%")
|
|
364
397
|
? width
|
|
365
398
|
: typeof width === "number"
|
|
366
399
|
? `${width}%`
|
|
367
400
|
: "100%";
|
|
368
|
-
// Non-MSO wrapper
|
|
369
|
-
|
|
401
|
+
// Non-MSO wrapper: display:block removes the phantom inline-baseline gap that
|
|
402
|
+
// display:inline-block creates in Gmail / Apple Mail / Yahoo between images.
|
|
403
|
+
// margin handles alignment since text-align won't move block elements.
|
|
404
|
+
const imgTextAlign = containerStyle.textAlign || "left";
|
|
405
|
+
const imgMargin = imgTextAlign === "center" ? "margin:0 auto;" :
|
|
406
|
+
imgTextAlign === "right" ? "margin-left:auto; margin-right:0;" : "";
|
|
407
|
+
// OUTLOOK FIX: Use finalWidth (the actual displayed size) as max-width so the div
|
|
408
|
+
// doesn't claim more space than the image occupies. originalWidth is the natural
|
|
409
|
+
// image size (e.g. 636px for the Beefree logo rendered at 35px) which was
|
|
410
|
+
// misleadingly large and could confuse some rendering engines.
|
|
411
|
+
const nonMsoWrapper = `<div style="display:block; width:${percentWidth}; max-width:${finalWidth}px; line-height:0; font-size:0; ${imgMargin}">${imageElement}</div>`;
|
|
370
412
|
// OUTLOOK FIX: Generate VML with corrected dimensions
|
|
371
413
|
const outlookImage = await appendOutlookForImage(nonMsoWrapper, safeCellWidth, innerContainerWidth, imageUrl, style, finalWidth, finalHeight);
|
|
372
|
-
const imageContent = appendOutlookSupport(outlookImage, containerStyles, visibilityClass);
|
|
414
|
+
const imageContent = appendOutlookSupport(outlookImage, containerStyles, visibilityClass, safeCellWidth);
|
|
373
415
|
return navigateToUrl
|
|
374
416
|
? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block;">${imageContent}</a>`
|
|
375
417
|
: imageContent;
|
|
376
418
|
}
|
|
377
419
|
function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
420
|
+
const pad = buttonStyle.buttonPadding || {};
|
|
421
|
+
const padTop = Number.isFinite(pad.top) ? pad.top : 10;
|
|
422
|
+
const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
|
|
423
|
+
const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
|
|
424
|
+
const padRight = Number.isFinite(pad.right) ? pad.right : 20;
|
|
425
|
+
const fontSize = buttonStyle.fontSize || 16;
|
|
426
|
+
const height = typeof buttonStyle.height === "number" && buttonStyle.height > 0
|
|
427
|
+
? buttonStyle.height
|
|
428
|
+
: null;
|
|
429
|
+
// prevent layout break
|
|
430
|
+
const minHeight = padTop + padBottom + fontSize;
|
|
431
|
+
const finalHeight = height ? Math.max(height, minHeight) : null;
|
|
432
|
+
const borderRadius = buttonStyle.borderRadius || 0;
|
|
433
|
+
const borderColor = buttonStyle.borderColor || "transparent";
|
|
434
|
+
const borderWidth = buttonStyle.borderWidth || 0;
|
|
435
|
+
const borderStyle = buttonStyle.borderStyle || "solid";
|
|
436
|
+
const bgColor = buttonStyle.buttonColor || "transparent";
|
|
437
|
+
const color = buttonStyle.color || "#ffffff";
|
|
438
|
+
const fontFamily = (0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily).replace(/"/g, "'");
|
|
439
|
+
const fontWeight = buttonStyle.fontWeight || 400;
|
|
440
|
+
const width = typeof buttonStyle.width === "number"
|
|
441
|
+
? `width="${buttonStyle.width}"`
|
|
442
|
+
: "";
|
|
443
|
+
return `<!--[if mso]>
|
|
444
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="display:inline-table;">
|
|
445
|
+
<tr>
|
|
446
|
+
<td align="center"
|
|
447
|
+
valign="middle"
|
|
448
|
+
${width}
|
|
449
|
+
${finalHeight ? `height="${finalHeight}"` : ""}
|
|
450
|
+
bgcolor="${bgColor}"
|
|
451
|
+
style="
|
|
452
|
+
${finalHeight ? `height:${finalHeight}px;` : ""}
|
|
453
|
+
background-color:${bgColor};
|
|
454
|
+
border-radius:${borderRadius}px;
|
|
455
|
+
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
456
|
+
overflow:hidden;
|
|
457
|
+
mso-line-height-rule:exactly;
|
|
458
|
+
">
|
|
459
|
+
|
|
460
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
461
|
+
<tr>
|
|
462
|
+
<td align="center" valign="middle"
|
|
463
|
+
style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
|
|
464
|
+
|
|
465
|
+
<a href="${navigateToUrl}"
|
|
466
|
+
style="
|
|
467
|
+
display:inline-block;
|
|
468
|
+
color:${color};
|
|
469
|
+
text-decoration:none;
|
|
470
|
+
font-family:${fontFamily};
|
|
471
|
+
font-size:${fontSize}px;
|
|
472
|
+
font-weight:${fontWeight};
|
|
473
|
+
line-height:normal;
|
|
474
|
+
">
|
|
475
|
+
${text}
|
|
476
|
+
</a>
|
|
477
|
+
|
|
478
|
+
</td>
|
|
479
|
+
</tr>
|
|
480
|
+
</table>
|
|
481
|
+
|
|
482
|
+
</td>
|
|
483
|
+
</tr>
|
|
484
|
+
</table>
|
|
485
|
+
<![endif]-->
|
|
486
|
+
<!--[if !mso]><!-->
|
|
487
|
+
${content}
|
|
488
|
+
<!--<![endif]-->`;
|
|
400
489
|
}
|
|
401
490
|
function convertButtonBlock(blockData) {
|
|
402
491
|
const { style, props } = blockData.data;
|
|
403
492
|
const { text, navigateToUrl } = props;
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
493
|
+
const { fontFamily, fontSize, fontWeight, textAlign, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, alignment, padding, backgroundColor: containerBg, margin, } = style;
|
|
494
|
+
const pad = buttonPadding || {};
|
|
495
|
+
const padTop = Number.isFinite(pad.top) ? pad.top : 10;
|
|
496
|
+
const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
|
|
497
|
+
const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
|
|
498
|
+
const padRight = Number.isFinite(pad.right) ? pad.right : 20;
|
|
499
|
+
const fs = fontSize || 16;
|
|
500
|
+
// prevent layout break
|
|
501
|
+
const minHeight = padTop + padBottom + fs;
|
|
502
|
+
const finalHeight = typeof height === "number" && height > 0
|
|
503
|
+
? Math.max(height, minHeight)
|
|
504
|
+
: null;
|
|
505
|
+
const safeFF = (0, fontFallback_1.withFontFallback)(fontFamily).replace(/"/g, "'");
|
|
506
|
+
const safeColor = color || "#ffffff";
|
|
507
|
+
const bgColor = buttonColor || "transparent";
|
|
508
|
+
const bdColor = borderColor || "transparent";
|
|
509
|
+
const bdStyle = borderStyle || "solid";
|
|
510
|
+
const bw = borderWidth || 0;
|
|
511
|
+
const br = borderRadius || 0;
|
|
512
|
+
const containerAlign = alignment || textAlign || "left";
|
|
513
|
+
const widthAttr = typeof width === "number"
|
|
514
|
+
? `width="${width}"`
|
|
515
|
+
: "";
|
|
516
|
+
// ✅ FIX: no width=100% anywhere
|
|
517
|
+
const buttonTable = `
|
|
518
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0"
|
|
519
|
+
style="display:inline-table; border-collapse:separate;"
|
|
520
|
+
${widthAttr}>
|
|
521
|
+
<tr>
|
|
522
|
+
<td
|
|
523
|
+
align="center"
|
|
524
|
+
valign="middle"
|
|
525
|
+
${finalHeight ? `height="${finalHeight}"` : ""}
|
|
526
|
+
style="
|
|
527
|
+
${finalHeight ? `height:${finalHeight}px;` : ""}
|
|
528
|
+
background-color:${bgColor};
|
|
529
|
+
border-radius:${br}px;
|
|
530
|
+
border:${bw}px ${bdStyle} ${bdColor};
|
|
531
|
+
overflow:hidden;
|
|
532
|
+
mso-line-height-rule:exactly;
|
|
533
|
+
"
|
|
534
|
+
>
|
|
535
|
+
|
|
536
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
537
|
+
<tr>
|
|
538
|
+
<td align="center" valign="middle"
|
|
539
|
+
style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
|
|
540
|
+
|
|
541
|
+
<a href="${navigateToUrl}"
|
|
542
|
+
style="
|
|
543
|
+
display:inline-block;
|
|
544
|
+
color:${safeColor};
|
|
545
|
+
text-decoration:none;
|
|
546
|
+
font-family:${safeFF};
|
|
547
|
+
font-size:${fs}px;
|
|
548
|
+
font-weight:${fontWeight || 400};
|
|
549
|
+
line-height:normal;
|
|
550
|
+
white-space:nowrap;
|
|
551
|
+
">
|
|
552
|
+
${text}
|
|
553
|
+
</a>
|
|
554
|
+
|
|
555
|
+
</td>
|
|
556
|
+
</tr>
|
|
557
|
+
</table>
|
|
558
|
+
|
|
559
|
+
</td>
|
|
560
|
+
</tr>
|
|
561
|
+
</table>
|
|
562
|
+
`;
|
|
563
|
+
const aligned = containerAlign === "center"
|
|
564
|
+
? `<center>${buttonTable}</center>`
|
|
565
|
+
: `<div style="text-align:${containerAlign};">${buttonTable}</div>`;
|
|
566
|
+
const buttonWithOutlook = appendOutlookForButton(aligned, style, navigateToUrl, text);
|
|
567
|
+
return `
|
|
568
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
|
569
|
+
<tr>
|
|
570
|
+
<td align="${containerAlign}"
|
|
571
|
+
style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;
|
|
572
|
+
background-color:${containerBg || "transparent"};">
|
|
573
|
+
${buttonWithOutlook}
|
|
574
|
+
</td>
|
|
575
|
+
</tr>
|
|
576
|
+
</table>
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
// Words inside a gradient() that are NOT color names
|
|
580
|
+
const GRADIENT_KEYWORDS = new Set([
|
|
581
|
+
'linear', 'radial', 'conic', 'gradient',
|
|
582
|
+
'to', 'at', 'top', 'bottom', 'left', 'right', 'center',
|
|
583
|
+
'closest', 'farthest', 'corner', 'side', 'circle', 'ellipse',
|
|
584
|
+
'deg', 'turn', 'rad', 'grad', 'from', 'in',
|
|
585
|
+
'url', // url() prefix sometimes appears when gradient is wrapped incorrectly
|
|
586
|
+
]);
|
|
587
|
+
/** Extract the first color stop (hex, rgb, or named CSS color) from a gradient string. */
|
|
588
|
+
function firstGradientColor(gradient) {
|
|
589
|
+
const tokenRe = /#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|([a-zA-Z-]+)/g;
|
|
590
|
+
let m;
|
|
591
|
+
while ((m = tokenRe.exec(gradient)) !== null) {
|
|
592
|
+
const namedWord = m[1];
|
|
593
|
+
if (namedWord) {
|
|
594
|
+
if (!GRADIENT_KEYWORDS.has(namedWord.toLowerCase()))
|
|
595
|
+
return namedWord;
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
return m[0]; // hex or rgb()
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return '';
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Extract the first solid-color stop from a CSS gradient in customCss.
|
|
605
|
+
* Used as a MSO/Outlook bgcolor fallback. Handles hex, rgb, and named colors.
|
|
606
|
+
*/
|
|
607
|
+
function extractCssFallbackColor(customCss) {
|
|
608
|
+
if (!customCss)
|
|
609
|
+
return '';
|
|
610
|
+
const gradientMatch = customCss.match(/(?:linear|radial|conic)-gradient\(([^)]+(?:\([^)]*\)[^)]*)*)\)/);
|
|
611
|
+
if (!gradientMatch)
|
|
612
|
+
return '';
|
|
613
|
+
return firstGradientColor(gradientMatch[1]);
|
|
614
|
+
}
|
|
615
|
+
function parseGradient(gradient) {
|
|
616
|
+
if (!gradient)
|
|
617
|
+
return null;
|
|
618
|
+
const lower = gradient.toLowerCase();
|
|
619
|
+
// Determine angle from deg value or direction keyword
|
|
620
|
+
const degMatch = gradient.match(/(\d+(?:\.\d+)?)deg/);
|
|
621
|
+
let angle = 180;
|
|
622
|
+
if (degMatch) {
|
|
623
|
+
angle = parseFloat(degMatch[1]);
|
|
624
|
+
}
|
|
625
|
+
else if (lower.includes('to right'))
|
|
626
|
+
angle = 90;
|
|
627
|
+
else if (lower.includes('to left'))
|
|
628
|
+
angle = 270;
|
|
629
|
+
else if (lower.includes('to top'))
|
|
630
|
+
angle = 0;
|
|
631
|
+
// 'to bottom' and bare gradient() both default to 180
|
|
632
|
+
// Extract ALL color tokens: hex, rgb/rgba, and named CSS color words
|
|
633
|
+
const colors = [];
|
|
634
|
+
const tokenRe = /#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|([a-zA-Z-]+)/g;
|
|
635
|
+
let m;
|
|
636
|
+
while ((m = tokenRe.exec(gradient)) !== null) {
|
|
637
|
+
const namedWord = m[1];
|
|
638
|
+
if (namedWord) {
|
|
639
|
+
if (!GRADIENT_KEYWORDS.has(namedWord.toLowerCase()))
|
|
640
|
+
colors.push(namedWord);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
colors.push(m[0]);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
angle,
|
|
648
|
+
colors,
|
|
649
|
+
fallback: colors[0] || '#ffffff',
|
|
419
650
|
};
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
});
|
|
424
|
-
const convertedStyles = buildStyles({ maxWidth: "100%", boxSizing: "border-box", ...rest }, {
|
|
425
|
-
perChanges: [],
|
|
426
|
-
pxChanges: allPxAttributes,
|
|
427
|
-
});
|
|
428
|
-
const buttonElement = `<a href="${navigateToUrl}" rel="noreferrer noopener" style="display:inline-block; text-decoration:none; cursor:pointer;"><button style="${convertedButtonStyle}">${text}</button></a>`;
|
|
429
|
-
const buttonContent = appendOutlookSupport(appendOutlookForButton(buttonElement, style, navigateToUrl, text), convertedStyles, visibilityClass);
|
|
430
|
-
return buttonContent;
|
|
651
|
+
}
|
|
652
|
+
function cssAngleToVml(angle) {
|
|
653
|
+
return (angle + 90) % 360;
|
|
431
654
|
}
|
|
432
655
|
async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
433
656
|
const { style = {}, childrenIds = [], props } = blockData.data;
|
|
434
657
|
const { columns = 1, cellWidths = [], responsive = true } = props;
|
|
435
|
-
const { columnGap = 0, ...restStyle } = style;
|
|
658
|
+
const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
|
|
436
659
|
const gridVisibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
437
|
-
//
|
|
438
|
-
|
|
660
|
+
// Detect gradient — check both backgroundImage prop and customCss (gradient may land in
|
|
661
|
+
// customCss when the block was built via CSS shorthand or custom CSS input).
|
|
662
|
+
const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : '';
|
|
663
|
+
const customCssStr = restStyle.customCss || '';
|
|
664
|
+
// Extract gradient string from customCss if not already in backgroundImage
|
|
665
|
+
const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
|
|
666
|
+
? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
|
|
667
|
+
: '';
|
|
668
|
+
const effectiveGradient = bgImageStr.includes('gradient(')
|
|
669
|
+
? bgImageStr
|
|
670
|
+
: gradientInCustomCss;
|
|
671
|
+
const isGradient = Boolean(effectiveGradient);
|
|
672
|
+
const parsedGradient = isGradient ? parseGradient(effectiveGradient) : null;
|
|
673
|
+
const fallbackBgColor = backgroundColor ||
|
|
674
|
+
parsedGradient?.fallback ||
|
|
675
|
+
extractCssFallbackColor(customCssStr) ||
|
|
676
|
+
"#ffffff";
|
|
677
|
+
const rawBgImageUrl = !isGradient && bgImageStr
|
|
678
|
+
? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
|
|
679
|
+
: null;
|
|
680
|
+
// When gradient came from customCss, strip background-image from customCss so it
|
|
681
|
+
// doesn't duplicate into the inner table style (the outer <td> wrapper carries it).
|
|
682
|
+
const innerCustomCss = gradientInCustomCss
|
|
683
|
+
? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
|
|
684
|
+
: customCssStr;
|
|
685
|
+
// Build inner table styles — when gradient/bg-image is on the outer wrapper, strip
|
|
686
|
+
// background props from the inner table so the outer <td> background shows through.
|
|
687
|
+
const innerRestStyle = (rawBgImageUrl || isGradient)
|
|
688
|
+
? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
|
|
689
|
+
: { ...restStyle, customCss: innerCustomCss };
|
|
690
|
+
const tableStyles = buildStyles({ backgroundColor: (rawBgImageUrl || isGradient) ? undefined : backgroundColor, ...innerRestStyle }, {
|
|
439
691
|
perChanges: [],
|
|
440
692
|
pxChanges: allPxAttributes,
|
|
441
693
|
});
|
|
@@ -443,45 +695,58 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
443
695
|
const visualRows = Math.ceil(total / columns);
|
|
444
696
|
// OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
|
|
445
697
|
const msoTableWidth = Math.min(cellWidthInPx, 600);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
698
|
+
// When a background image/gradient is present, the background is applied on an outer
|
|
699
|
+
// wrapper <td> (see bottom of function). The inner grid tables must be clean.
|
|
700
|
+
// When no background, the MSO table gets bgcolor for solid-color sections.
|
|
701
|
+
const msoBgColor = !rawBgImageUrl && !isGradient
|
|
702
|
+
? (backgroundColor || '')
|
|
703
|
+
: '';
|
|
704
|
+
const msoBgAttr = msoBgColor ? ` bgcolor="${msoBgColor}"` : '';
|
|
705
|
+
const msoBgStyle = msoBgColor ? `background-color:${msoBgColor};` : '';
|
|
706
|
+
// Inner tables must be explicitly transparent when outer <td> carries the background.
|
|
707
|
+
const innerBgTransparent = (rawBgImageUrl || isGradient)
|
|
708
|
+
? 'background-color:transparent;'
|
|
709
|
+
: '';
|
|
710
|
+
const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor ? ` bgcolor="${backgroundColor}"` : '';
|
|
711
|
+
let html = `
|
|
712
|
+
<!--[if mso]>
|
|
713
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
|
|
714
|
+
style="border-collapse:collapse;width:${msoTableWidth}px;${msoBgStyle}${innerBgTransparent}"
|
|
715
|
+
class="${gridVisibilityClass}">
|
|
716
|
+
<![endif]-->
|
|
717
|
+
<!--[if !mso]><!-->
|
|
718
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%"
|
|
719
|
+
role="presentation"${nonMsoBgAttr}
|
|
720
|
+
style="border-collapse:collapse; ${innerBgTransparent}${tableStyles}; max-width:600px;"
|
|
721
|
+
class="${gridVisibilityClass}">
|
|
722
|
+
<!--<![endif]-->
|
|
458
723
|
`;
|
|
459
724
|
for (let r = 0; r < visualRows; r++) {
|
|
460
725
|
html += "<tr>";
|
|
461
|
-
// COUNT visible cells
|
|
726
|
+
// COUNT visible cells and find last visible column index
|
|
462
727
|
let visibleCells = 0;
|
|
728
|
+
let lastVisibleCol = 0;
|
|
463
729
|
const rowIds = [];
|
|
464
730
|
for (let c = 0; c < columns; c++) {
|
|
465
731
|
const idx = r * columns + c;
|
|
466
732
|
const id = childrenIds[idx] ?? null;
|
|
467
733
|
rowIds.push(id);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
734
|
+
const child = id ? rootData[id] : null;
|
|
735
|
+
const isHidden = child?.data?.props?.hideOnDesktop;
|
|
736
|
+
if (!isHidden) {
|
|
737
|
+
visibleCells++;
|
|
738
|
+
lastVisibleCol = c;
|
|
473
739
|
}
|
|
474
740
|
}
|
|
475
|
-
// OUTLOOK FIX: Calculate safe width based on visible cells
|
|
476
|
-
// If we have visible cells, distribute 100% width evenly
|
|
477
741
|
const safeWidth = visibleCells > 0 ? 100 / visibleCells : 100 / columns;
|
|
478
|
-
//
|
|
742
|
+
// Reserve pixel space for spacer tds between visible cells (N-1 gaps for N visible cells)
|
|
743
|
+
const totalGapPx = columnGap * Math.max(visibleCells - 1, 0);
|
|
744
|
+
const adjustedTableWidth = Math.max(msoTableWidth - totalGapPx, 1);
|
|
479
745
|
let totalWidth = 0;
|
|
480
746
|
const cellWidthPercents = [];
|
|
481
747
|
for (let c = 0; c < columns; c++) {
|
|
482
748
|
const id = rowIds[c];
|
|
483
749
|
let widthPercent = cellWidths[c] ?? safeWidth;
|
|
484
|
-
// Validate width
|
|
485
750
|
if (widthPercent <= 0 || widthPercent > 100) {
|
|
486
751
|
widthPercent = safeWidth;
|
|
487
752
|
}
|
|
@@ -494,62 +759,115 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
494
759
|
}
|
|
495
760
|
}
|
|
496
761
|
}
|
|
497
|
-
// OUTLOOK FIX: If total width < 100%, scale up to fill container
|
|
498
|
-
// This prevents orphaned space that Outlook handles poorly
|
|
499
762
|
const scaleFactor = totalWidth > 0 && totalWidth < 100 ? 100 / totalWidth : 1;
|
|
500
763
|
for (let c = 0; c < columns; c++) {
|
|
501
|
-
const idx = r * columns + c;
|
|
502
764
|
const id = rowIds[c];
|
|
503
|
-
// OUTLOOK FIX: Scale width to ensure cells fill 100% of container
|
|
504
765
|
let widthPercent = cellWidthPercents[c] * scaleFactor;
|
|
505
|
-
// OUTLOOK FIX: Ensure width doesn't exceed 100% after scaling
|
|
506
766
|
widthPercent = Math.min(widthPercent, 100);
|
|
767
|
+
// Cell pixel width is a share of the gap-adjusted table width
|
|
768
|
+
const cellWidthPx = Math.round((widthPercent / 100) * adjustedTableWidth);
|
|
507
769
|
if (id) {
|
|
508
770
|
const child = rootData[id];
|
|
509
771
|
const { style: cellStyle = {}, props: childProps = {} } = child.data;
|
|
510
772
|
const verticalAlign = cellStyle.verticalAlign || "top";
|
|
511
773
|
const childVisible = !childProps.hideOnDesktop;
|
|
512
774
|
const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
|
|
513
|
-
// Only render if visible
|
|
514
775
|
if (childVisible) {
|
|
515
|
-
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent,
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
${childHtml}
|
|
776
|
+
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth);
|
|
777
|
+
// bgcolor on the cell <td> ensures background-color survives Outlook
|
|
778
|
+
// compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
|
|
779
|
+
const cellBgColor = cellStyle.backgroundColor || '';
|
|
780
|
+
const cellBgAttr = cellBgColor ? ` bgcolor="${cellBgColor}"` : '';
|
|
781
|
+
html += `
|
|
782
|
+
<td
|
|
783
|
+
width="${cellWidthPx}"${cellBgAttr}
|
|
784
|
+
class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
|
|
785
|
+
style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
|
|
786
|
+
>
|
|
787
|
+
${childHtml}
|
|
528
788
|
</td>`;
|
|
789
|
+
// Spacer td between columns — fixed pixel width, invisible to screen readers
|
|
790
|
+
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
791
|
+
html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
792
|
+
}
|
|
529
793
|
}
|
|
530
794
|
}
|
|
531
795
|
else {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
<td width="${Math.round(widthPercent)}%"
|
|
537
|
-
${responsive ? 'class="stack-column"' : ""}
|
|
538
|
-
style="width:${cellWidthPx}px;vertical-align:top;">
|
|
796
|
+
html += `
|
|
797
|
+
<td width="${cellWidthPx}"
|
|
798
|
+
${responsive ? 'class="stack-column"' : ""}
|
|
799
|
+
style="width:${cellWidthPx}px;vertical-align:top;">
|
|
539
800
|
</td>`;
|
|
801
|
+
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
802
|
+
html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
803
|
+
}
|
|
540
804
|
}
|
|
541
805
|
}
|
|
542
806
|
html += "</tr>";
|
|
543
807
|
}
|
|
544
808
|
// Close both MSO and non-MSO tables
|
|
545
|
-
html += `
|
|
546
|
-
<!--[if mso]>
|
|
547
|
-
</table>
|
|
548
|
-
<![endif]-->
|
|
549
|
-
<!--[if !mso]><!-->
|
|
550
|
-
</table>
|
|
551
|
-
<!--<![endif]-->
|
|
809
|
+
html += `
|
|
810
|
+
<!--[if mso]>
|
|
811
|
+
</table>
|
|
812
|
+
<![endif]-->
|
|
813
|
+
<!--[if !mso]><!-->
|
|
814
|
+
</table>
|
|
815
|
+
<!--<![endif]-->
|
|
552
816
|
`;
|
|
817
|
+
// ── Background image: canonical multi-client approach ────────────────────
|
|
818
|
+
//
|
|
819
|
+
// Problem: `background-image` on a <table> element is stripped by:
|
|
820
|
+
// • New Outlook Mac / Windows (Chromium-based app)
|
|
821
|
+
// • Outlook.com
|
|
822
|
+
// • Old Outlook (Word engine) — ignores CSS entirely
|
|
823
|
+
//
|
|
824
|
+
// Solution: wrap the grid in an outer <table><tr><td> where the <td> carries
|
|
825
|
+
// the background. Different clients pick it up via different mechanisms:
|
|
826
|
+
//
|
|
827
|
+
// background="" attribute on <td> → Yahoo Mail, older webmail
|
|
828
|
+
// CSS background-image on <td> → Gmail, Apple Mail, new Outlook Mac ✓
|
|
829
|
+
// VML v:rect inside the <td> → Old Outlook (Word engine) ✓
|
|
830
|
+
//
|
|
831
|
+
// The inner grid tables have NO background so the outer <td> bg shows through.
|
|
832
|
+
if (rawBgImageUrl || isGradient) {
|
|
833
|
+
const vmlFill = isGradient
|
|
834
|
+
? (() => {
|
|
835
|
+
const vmlAngle = cssAngleToVml(parsedGradient?.angle || 180);
|
|
836
|
+
const c1 = parsedGradient?.fallback || '#ffffff';
|
|
837
|
+
const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
|
|
838
|
+
return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
|
|
839
|
+
})()
|
|
840
|
+
: `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
|
|
841
|
+
html = `
|
|
842
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}" role="presentation"
|
|
843
|
+
style="border-collapse:collapse;width:${msoTableWidth}px;">
|
|
844
|
+
<tr>
|
|
845
|
+
<td width="${msoTableWidth}" bgcolor="${fallbackBgColor}" valign="top"
|
|
846
|
+
style="
|
|
847
|
+
width:${msoTableWidth}px;
|
|
848
|
+
background-color:${fallbackBgColor};
|
|
849
|
+
${isGradient ? `background:${effectiveGradient};` : `background:url('${rawBgImageUrl}') center/cover no-repeat;`}
|
|
850
|
+
">
|
|
851
|
+
|
|
852
|
+
<!--[if gte mso 9]>
|
|
853
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
854
|
+
fill="true" stroke="false"
|
|
855
|
+
style="width:${msoTableWidth}px;">
|
|
856
|
+
${vmlFill}
|
|
857
|
+
<v:textbox inset="0,0,0,0">
|
|
858
|
+
<![endif]-->
|
|
859
|
+
|
|
860
|
+
${html}
|
|
861
|
+
|
|
862
|
+
<!--[if gte mso 9]>
|
|
863
|
+
</v:textbox>
|
|
864
|
+
</v:rect>
|
|
865
|
+
<![endif]-->
|
|
866
|
+
|
|
867
|
+
</td>
|
|
868
|
+
</tr>
|
|
869
|
+
</table>`;
|
|
870
|
+
}
|
|
553
871
|
return html;
|
|
554
872
|
}
|
|
555
873
|
async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx) {
|
|
@@ -563,16 +881,39 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
|
|
|
563
881
|
// OUTLOOK FIX: Calculate the actual cell width in pixels based on percentage
|
|
564
882
|
// If parent is 600px and cell is 50%, cell width should be 300px, not 600px
|
|
565
883
|
const cellWidthPx = Math.round((cellWidthPercent / 100) * parentCellWidthPx);
|
|
884
|
+
// Subtract the cell's own padding so children receive the actual content-area width.
|
|
885
|
+
// Old Outlook honours explicit img/table width attributes — if a child is sized to the
|
|
886
|
+
// full column width (ignoring padding) it overflows and expands the column.
|
|
887
|
+
const cellPad = style?.padding || {};
|
|
888
|
+
const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
|
|
889
|
+
const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
|
|
890
|
+
const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
|
|
566
891
|
// OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
|
|
567
|
-
const safeCellWidthPx = Math.min(
|
|
892
|
+
const safeCellWidthPx = Math.min(contentWidthPx, 600);
|
|
568
893
|
for (const childId of childrenIds) {
|
|
569
894
|
const child = rootData[childId];
|
|
570
895
|
if (child) {
|
|
571
896
|
parts.push(await convertToHtml(child, rootData, safeCellWidthPx));
|
|
572
897
|
}
|
|
573
898
|
}
|
|
899
|
+
const borderRadius = style?.borderRadius || 0;
|
|
900
|
+
const bgColor = style?.backgroundColor || "transparent";
|
|
901
|
+
// IMPORTANT: radius only for non-Outlook
|
|
902
|
+
const wrapped = `
|
|
903
|
+
<!--[if !mso]><!-->
|
|
904
|
+
<div style="
|
|
905
|
+
border-radius:${borderRadius}px;
|
|
906
|
+
overflow:hidden;
|
|
907
|
+
background-color:${bgColor};
|
|
908
|
+
">
|
|
909
|
+
<!--<![endif]-->
|
|
910
|
+
${parts.join("")}
|
|
911
|
+
<!--[if !mso]><!-->
|
|
912
|
+
</div>
|
|
913
|
+
<!--<![endif]-->
|
|
914
|
+
`;
|
|
574
915
|
return {
|
|
575
|
-
html:
|
|
916
|
+
html: wrapped,
|
|
576
917
|
styles,
|
|
577
918
|
};
|
|
578
919
|
}
|
|
@@ -583,7 +924,9 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
|
|
|
583
924
|
const { hideOnDesktop } = props; // Get the hideOnDesktop prop
|
|
584
925
|
const { videoUrl, youtubeVideoUrl, thumbnailUrl, altText } = props;
|
|
585
926
|
const videoLink = youtubeVideoUrl || videoUrl || "#";
|
|
586
|
-
|
|
927
|
+
// via.placeholder.com is defunct — use a data-URI grey box as the default thumbnail
|
|
928
|
+
const FALLBACK_THUMBNAIL = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='360'%3E%3Crect width='480' height='360' fill='%23cccccc'/%3E%3C/svg%3E`;
|
|
929
|
+
let resolvedThumbnail = thumbnailUrl || FALLBACK_THUMBNAIL;
|
|
587
930
|
if (youtubeVideoUrl) {
|
|
588
931
|
const youtubeId = (0, common_1.extractYouTubeId)(youtubeVideoUrl);
|
|
589
932
|
const vimeoId = (0, common_1.extractVimeoId)(youtubeVideoUrl);
|
|
@@ -637,116 +980,103 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
|
|
|
637
980
|
const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
|
|
638
981
|
const shouldHideInOutlook = hideOnDesktop;
|
|
639
982
|
const outlookVideoContent = shouldHideInOutlook
|
|
640
|
-
? `<!--[if !mso]><!-->
|
|
641
|
-
<v:group xmlns:v="urn:schemas-microsoft-com:vml"
|
|
642
|
-
coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
643
|
-
href="${videoLink}"
|
|
644
|
-
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
645
|
-
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
|
|
646
|
-
strokeweight="${borderWidth}px"
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
</v:group>
|
|
983
|
+
? `<!--[if !mso]><!-->
|
|
984
|
+
<v:group xmlns:v="urn:schemas-microsoft-com:vml"
|
|
985
|
+
coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
986
|
+
href="${videoLink}"
|
|
987
|
+
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
988
|
+
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
|
|
989
|
+
${borderWidth > 0 ? `stroked="t" strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="f"`}
|
|
990
|
+
${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
|
|
991
|
+
>
|
|
992
|
+
<v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
|
|
993
|
+
</v:rect>
|
|
994
|
+
<v:shape type="#_x0000_t75"
|
|
995
|
+
style="position:absolute;
|
|
996
|
+
left:${vmlLeft.toFixed(1)}px;
|
|
997
|
+
top:${vmlTop.toFixed(1)}px;
|
|
998
|
+
width:${playIconWidth}px;
|
|
999
|
+
height:${playIconHeight}px;"
|
|
1000
|
+
alt="Play" href="${videoLink}" title="${altText || "Video"}"
|
|
1001
|
+
stroked="f" filled="t">
|
|
1002
|
+
<v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
|
|
1003
|
+
</v:shape>
|
|
1004
|
+
</v:group>
|
|
663
1005
|
<!--<![endif]-->`
|
|
664
|
-
: `<!--[if mso]>
|
|
665
|
-
<v:group xmlns:v="urn:schemas-microsoft-com:vml"
|
|
666
|
-
coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
667
|
-
href="${videoLink}"
|
|
668
|
-
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
669
|
-
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;
|
|
670
|
-
strokeweight="${borderWidth}px"
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
</v:group>
|
|
1006
|
+
: `<!--[if mso]>
|
|
1007
|
+
<v:group xmlns:v="urn:schemas-microsoft-com:vml"
|
|
1008
|
+
coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
1009
|
+
href="${videoLink}"
|
|
1010
|
+
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
1011
|
+
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
|
|
1012
|
+
${borderWidth > 0 ? `stroked="t" strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="f"`}
|
|
1013
|
+
${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
|
|
1014
|
+
>
|
|
1015
|
+
<v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
|
|
1016
|
+
</v:rect>
|
|
1017
|
+
<v:shape type="#_x0000_t75"
|
|
1018
|
+
style="position:absolute;
|
|
1019
|
+
left:${vmlLeft.toFixed(1)}px;
|
|
1020
|
+
top:${vmlTop.toFixed(1)}px;
|
|
1021
|
+
width:${playIconWidth}px;
|
|
1022
|
+
height:${playIconHeight}px;"
|
|
1023
|
+
alt="Play" href="${videoLink}" title="${altText || "Video"}"
|
|
1024
|
+
stroked="f" filled="t">
|
|
1025
|
+
<v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
|
|
1026
|
+
</v:shape>
|
|
1027
|
+
</v:group>
|
|
687
1028
|
<![endif]-->`;
|
|
688
|
-
|
|
689
|
-
<table
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
style="display: block;
|
|
718
|
-
border: 0;
|
|
719
|
-
outline: none;
|
|
720
|
-
text-decoration: none;
|
|
721
|
-
height: auto;"
|
|
722
|
-
/>
|
|
723
|
-
</a>
|
|
724
|
-
</td>
|
|
725
|
-
</tr>
|
|
726
|
-
</table>
|
|
727
|
-
<!--<![endif]-->`;
|
|
1029
|
+
// Non-Outlook: use a real <img> for the thumbnail so it renders in Gmail / Yahoo / webmail.
|
|
1030
|
+
// background-image on <table> is stripped by virtually every email client.
|
|
1031
|
+
// position:absolute for the play-button overlay is safe here because this block
|
|
1032
|
+
// is already inside <!--[if !mso]> — Outlook is handled separately via VML above.
|
|
1033
|
+
const thumbnailW = Math.round(innerContainerWidth);
|
|
1034
|
+
const thumbnailH = Math.round(calculatedHeight);
|
|
1035
|
+
const playMarginTop = -Math.round(playIconHeight / 2);
|
|
1036
|
+
const playMarginLeft = -Math.round(playIconWidth / 2);
|
|
1037
|
+
const borderAttr = borderWidth > 0 ? `border:${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};` : "border:0;";
|
|
1038
|
+
const radiusAttr = borderRadius > 0 ? `border-radius:${borderRadius}px; overflow:hidden;` : "";
|
|
1039
|
+
const nonOutlookVideoContent = `<!--[if !mso]><!-->
|
|
1040
|
+
<div style="display:block; width:100%; max-width:${thumbnailW}px; position:relative; line-height:0; font-size:0; ${borderAttr}${radiusAttr}">
|
|
1041
|
+
<a href="${videoLink}" target="_blank" style="display:block; text-decoration:none; line-height:0; font-size:0;">
|
|
1042
|
+
<img
|
|
1043
|
+
src="${resolvedThumbnail}"
|
|
1044
|
+
width="${thumbnailW}"
|
|
1045
|
+
height="${thumbnailH}"
|
|
1046
|
+
alt="${altText || "Video"}"
|
|
1047
|
+
style="display:block; width:100%; max-width:${thumbnailW}px; height:auto; border:0;"
|
|
1048
|
+
/>
|
|
1049
|
+
<img
|
|
1050
|
+
src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
|
|
1051
|
+
width="${playIconWidth}"
|
|
1052
|
+
alt="Play"
|
|
1053
|
+
style="display:block; position:absolute; top:50%; left:50%; margin-top:${playMarginTop}px; margin-left:${playMarginLeft}px; border:0; outline:none;"
|
|
1054
|
+
/>
|
|
1055
|
+
</a>
|
|
1056
|
+
</div>
|
|
1057
|
+
<!--<![endif]-->`;
|
|
728
1058
|
const videoContent = `${outlookVideoContent}${nonOutlookVideoContent}`;
|
|
729
|
-
const wrapperHtml = `
|
|
730
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
|
|
731
|
-
<tr>
|
|
732
|
-
<td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
|
|
733
|
-
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
734
|
-
align="${style?.textAlign || "left"}"
|
|
735
|
-
style="
|
|
736
|
-
margin:0;
|
|
737
|
-
max-width:${cellWidthInPx}px;
|
|
738
|
-
width:${percentWidth};
|
|
739
|
-
border-collapse:collapse;
|
|
740
|
-
">
|
|
741
|
-
<tr>
|
|
742
|
-
<td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
|
|
743
|
-
${videoContent}
|
|
744
|
-
</td>
|
|
745
|
-
</tr>
|
|
746
|
-
</table>
|
|
747
|
-
</td>
|
|
748
|
-
</tr>
|
|
749
|
-
</table>
|
|
1059
|
+
const wrapperHtml = `
|
|
1060
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
|
|
1061
|
+
<tr>
|
|
1062
|
+
<td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
|
|
1063
|
+
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
1064
|
+
align="${style?.textAlign || "left"}"
|
|
1065
|
+
style="
|
|
1066
|
+
margin:0;
|
|
1067
|
+
max-width:${cellWidthInPx}px;
|
|
1068
|
+
width:${percentWidth};
|
|
1069
|
+
border-collapse:collapse;
|
|
1070
|
+
">
|
|
1071
|
+
<tr>
|
|
1072
|
+
<td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
|
|
1073
|
+
${videoContent}
|
|
1074
|
+
</td>
|
|
1075
|
+
</tr>
|
|
1076
|
+
</table>
|
|
1077
|
+
</td>
|
|
1078
|
+
</tr>
|
|
1079
|
+
</table>
|
|
750
1080
|
`;
|
|
751
1081
|
return wrapperHtml;
|
|
752
1082
|
}
|
|
@@ -828,48 +1158,48 @@ async function convertShapeBlock(blockData) {
|
|
|
828
1158
|
let nonMsoContent = "";
|
|
829
1159
|
// --- Case 1: Image + Text ---
|
|
830
1160
|
if (imageUrl && text) {
|
|
831
|
-
nonMsoContent = `
|
|
832
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
833
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
834
|
-
border-radius:${resolvedBorderRadius};
|
|
835
|
-
background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
|
|
836
|
-
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
837
|
-
<div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};overflow:hidden;padding:6px;box-sizing:border-box;">
|
|
838
|
-
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
|
|
839
|
-
${text}
|
|
840
|
-
</div>
|
|
841
|
-
</div>
|
|
1161
|
+
nonMsoContent = `
|
|
1162
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1163
|
+
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1164
|
+
border-radius:${resolvedBorderRadius};
|
|
1165
|
+
background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
|
|
1166
|
+
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1167
|
+
<div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};overflow:hidden;padding:6px;box-sizing:border-box;">
|
|
1168
|
+
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
|
|
1169
|
+
${text}
|
|
1170
|
+
</div>
|
|
1171
|
+
</div>
|
|
842
1172
|
</div>`;
|
|
843
1173
|
}
|
|
844
1174
|
// --- Case 2: Image only ---
|
|
845
1175
|
else if (imageUrl) {
|
|
846
|
-
nonMsoContent = `
|
|
847
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
848
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
849
|
-
border-radius:${resolvedBorderRadius};
|
|
850
|
-
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
851
|
-
<img src="${imageUrl}" alt="${text || "shape image"}"
|
|
852
|
-
width="${resolvedWidthPx}" height="${resolvedHeightPx}"
|
|
853
|
-
style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
|
|
1176
|
+
nonMsoContent = `
|
|
1177
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1178
|
+
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1179
|
+
border-radius:${resolvedBorderRadius};
|
|
1180
|
+
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1181
|
+
<img src="${imageUrl}" alt="${text || "shape image"}"
|
|
1182
|
+
width="${resolvedWidthPx}" height="${resolvedHeightPx}"
|
|
1183
|
+
style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
|
|
854
1184
|
</div>`;
|
|
855
1185
|
}
|
|
856
1186
|
// --- Case 3: Text only ---
|
|
857
1187
|
else {
|
|
858
|
-
nonMsoContent = `
|
|
859
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
860
|
-
background:${finalBackgroundColor};
|
|
861
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
862
|
-
border-radius:${resolvedBorderRadius};
|
|
863
|
-
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
864
|
-
<div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};padding:8px;box-sizing:border-box;">
|
|
865
|
-
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
|
|
866
|
-
${text || ""}
|
|
867
|
-
</div>
|
|
868
|
-
</div>
|
|
1188
|
+
nonMsoContent = `
|
|
1189
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1190
|
+
background:${finalBackgroundColor};
|
|
1191
|
+
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1192
|
+
border-radius:${resolvedBorderRadius};
|
|
1193
|
+
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1194
|
+
<div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};padding:8px;box-sizing:border-box;">
|
|
1195
|
+
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
|
|
1196
|
+
${text || ""}
|
|
1197
|
+
</div>
|
|
1198
|
+
</div>
|
|
869
1199
|
</div>`;
|
|
870
1200
|
}
|
|
871
1201
|
// Outlook (VML) fallback
|
|
872
|
-
const outlookContent =
|
|
1202
|
+
const outlookContent = appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
|
|
873
1203
|
shape,
|
|
874
1204
|
imageUrl,
|
|
875
1205
|
backgroundColor,
|
|
@@ -887,17 +1217,45 @@ async function convertShapeBlock(blockData) {
|
|
|
887
1217
|
padding,
|
|
888
1218
|
msoBakeImageWithText,
|
|
889
1219
|
}, visibilityClass);
|
|
1220
|
+
// Embed block metadata so the HTML importer can reconstruct the Shape block exactly.
|
|
1221
|
+
const shapeProps = encodeBlockProps({
|
|
1222
|
+
shape,
|
|
1223
|
+
width: resolvedWidthPx,
|
|
1224
|
+
height: resolvedHeightPx,
|
|
1225
|
+
shapeColor: String(finalBackgroundColor || '#BEBEBE'),
|
|
1226
|
+
backgroundColor: String(finalBackgroundColor || '#BEBEBE'),
|
|
1227
|
+
borderRadius: borderRadius !== undefined ? borderRadius : 0,
|
|
1228
|
+
borderWidth: borderWidth || 0,
|
|
1229
|
+
borderColor: borderColor || 'transparent',
|
|
1230
|
+
borderStyle: borderStyle || 'solid',
|
|
1231
|
+
imageUrl: imageUrl || '',
|
|
1232
|
+
text: text || '',
|
|
1233
|
+
color: String(color || '#000000'),
|
|
1234
|
+
fontSize: fontSize || 14,
|
|
1235
|
+
textAlign: textAlignStyle,
|
|
1236
|
+
verticalAlign: verticalAlign || 'middle',
|
|
1237
|
+
alignment: alignment || 'left',
|
|
1238
|
+
padding: {
|
|
1239
|
+
top: padding.top || 0,
|
|
1240
|
+
right: padding.right || 0,
|
|
1241
|
+
bottom: padding.bottom || 0,
|
|
1242
|
+
left: padding.left || 0,
|
|
1243
|
+
},
|
|
1244
|
+
hideOnDesktop: Boolean(props.hideOnDesktop),
|
|
1245
|
+
hideOnMobile: Boolean(props.hideOnMobile),
|
|
1246
|
+
customCss: customCss || '',
|
|
1247
|
+
});
|
|
890
1248
|
// Combine into table wrapper
|
|
891
|
-
return `
|
|
892
|
-
<table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}">
|
|
893
|
-
<tr>
|
|
894
|
-
<td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
|
|
895
|
-
${outlookContent}
|
|
896
|
-
<!--[if !mso]><!-->
|
|
897
|
-
${nonMsoContent}
|
|
898
|
-
<!--<![endif]-->
|
|
899
|
-
</td>
|
|
900
|
-
</tr>
|
|
1249
|
+
return `
|
|
1250
|
+
<table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}" data-block-type="shape" data-block-props="${shapeProps}">
|
|
1251
|
+
<tr>
|
|
1252
|
+
<td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
|
|
1253
|
+
${outlookContent}
|
|
1254
|
+
<!--[if !mso]><!-->
|
|
1255
|
+
${nonMsoContent}
|
|
1256
|
+
<!--<![endif]-->
|
|
1257
|
+
</td>
|
|
1258
|
+
</tr>
|
|
901
1259
|
</table>`;
|
|
902
1260
|
}
|
|
903
1261
|
// ---------- Updated VML builder ----------
|
|
@@ -930,24 +1288,24 @@ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, bo
|
|
|
930
1288
|
const safeFontSize = Math.max(Math.round(textSize), 10);
|
|
931
1289
|
// Build the textbox with table/cell for reliable vertical centering in Outlook
|
|
932
1290
|
const textboxMarkup = text && !msoHasBakedText
|
|
933
|
-
? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
|
|
934
|
-
<div style="display:table;width:100%;height:100%;">
|
|
935
|
-
<div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
|
|
936
|
-
<div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
|
|
937
|
-
${text}
|
|
938
|
-
</div>
|
|
939
|
-
</div>
|
|
940
|
-
</div>
|
|
1291
|
+
? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
|
|
1292
|
+
<div style="display:table;width:100%;height:100%;">
|
|
1293
|
+
<div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
|
|
1294
|
+
<div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
|
|
1295
|
+
${text}
|
|
1296
|
+
</div>
|
|
1297
|
+
</div>
|
|
1298
|
+
</div>
|
|
941
1299
|
</v:textbox>`
|
|
942
1300
|
: `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
|
|
943
1301
|
// Return VML shape
|
|
944
|
-
return `
|
|
945
|
-
<v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
|
|
946
|
-
style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
|
|
947
|
-
${borderAttrs}
|
|
948
|
-
fill="true" fillcolor="${fillColor}"${extraAttr}>
|
|
949
|
-
${fillMarkup}
|
|
950
|
-
${textboxMarkup}
|
|
1302
|
+
return `
|
|
1303
|
+
<v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
|
|
1304
|
+
style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
|
|
1305
|
+
${borderAttrs}
|
|
1306
|
+
fill="true" fillcolor="${fillColor}"${extraAttr}>
|
|
1307
|
+
${fillMarkup}
|
|
1308
|
+
${textboxMarkup}
|
|
951
1309
|
</v:${tag}>`;
|
|
952
1310
|
}
|
|
953
1311
|
function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts, visibilityClass) {
|
|
@@ -975,53 +1333,60 @@ function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth
|
|
|
975
1333
|
const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
|
|
976
1334
|
// Fix: Properly handle Outlook visibility with conditional comments
|
|
977
1335
|
if (shouldHideInOutlook) {
|
|
978
|
-
return `<!--[if !mso]><!-->
|
|
979
|
-
<table align="${align}" border="0" cellpadding="0" cellspacing="0"
|
|
980
|
-
style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
|
|
981
|
-
<tr>
|
|
982
|
-
<td valign="${valign}"
|
|
983
|
-
style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
|
|
984
|
-
${vml}
|
|
985
|
-
</td>
|
|
986
|
-
</tr>
|
|
987
|
-
</table>
|
|
1336
|
+
return `<!--[if !mso]><!-->
|
|
1337
|
+
<table align="${align}" border="0" cellpadding="0" cellspacing="0"
|
|
1338
|
+
style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
|
|
1339
|
+
<tr>
|
|
1340
|
+
<td valign="${valign}"
|
|
1341
|
+
style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
|
|
1342
|
+
${vml}
|
|
1343
|
+
</td>
|
|
1344
|
+
</tr>
|
|
1345
|
+
</table>
|
|
988
1346
|
<!--<![endif]-->`;
|
|
989
1347
|
}
|
|
990
|
-
return `<!--[if mso]>
|
|
991
|
-
<table align="${align}" border="0" cellpadding="0" cellspacing="0"
|
|
992
|
-
style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
|
|
993
|
-
<tr>
|
|
994
|
-
<td valign="${valign}"
|
|
995
|
-
style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
|
|
996
|
-
${vml}
|
|
997
|
-
</td>
|
|
998
|
-
</tr>
|
|
999
|
-
</table>
|
|
1348
|
+
return `<!--[if mso]>
|
|
1349
|
+
<table align="${align}" border="0" cellpadding="0" cellspacing="0"
|
|
1350
|
+
style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
|
|
1351
|
+
<tr>
|
|
1352
|
+
<td valign="${valign}"
|
|
1353
|
+
style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
|
|
1354
|
+
${vml}
|
|
1355
|
+
</td>
|
|
1356
|
+
</tr>
|
|
1357
|
+
</table>
|
|
1000
1358
|
<![endif]-->`;
|
|
1001
1359
|
}
|
|
1002
1360
|
function convertVerticalDividerBlockToHtml(blockData) {
|
|
1003
1361
|
const { style, props } = blockData.data;
|
|
1004
|
-
const { width, height, dividerColor,
|
|
1362
|
+
const { width, height, dividerColor, padding, backgroundColor } = style;
|
|
1005
1363
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
1006
|
-
|
|
1007
|
-
const convertedStyle = buildStyles(rest, {
|
|
1364
|
+
const outerStyles = buildStyles({ padding, backgroundColor }, {
|
|
1008
1365
|
perChanges: [],
|
|
1009
1366
|
pxChanges: allPxAttributes,
|
|
1010
1367
|
});
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1368
|
+
const vDividerProps = encodeBlockProps({
|
|
1369
|
+
width: width || 5,
|
|
1370
|
+
height: height || 100,
|
|
1371
|
+
dividerColor: dividerColor || '#808080',
|
|
1372
|
+
backgroundColor: backgroundColor || '',
|
|
1373
|
+
alignment: 'left',
|
|
1374
|
+
padding: padding || { top: 0, right: 0, bottom: 0, left: 0 },
|
|
1375
|
+
hideOnDesktop: Boolean(props.hideOnDesktop),
|
|
1376
|
+
hideOnMobile: Boolean(props.hideOnMobile),
|
|
1377
|
+
});
|
|
1378
|
+
return `
|
|
1379
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
1380
|
+
style="${exports.tableCommonStyle} max-width:600px;" class="${visibilityClass}" data-block-type="vdivider" data-block-props="${vDividerProps}">
|
|
1381
|
+
<tr>
|
|
1382
|
+
<td style="${outerStyles}; text-align:center; vertical-align:middle;">
|
|
1383
|
+
<!--[if mso | IE]>
|
|
1384
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="${dividerColor}" style="width:${width}px;height:${height}px;" stroke="f"></v:rect>
|
|
1385
|
+
<![endif]-->
|
|
1386
|
+
<!--[if !mso]><!-->
|
|
1387
|
+
<div style="display:inline-block;width:${width}px;height:${height}px;background:${dividerColor};line-height:0;font-size:0;"> </div>
|
|
1388
|
+
<!--<![endif]-->
|
|
1389
|
+
</td>
|
|
1390
|
+
</tr>
|
|
1391
|
+
</table>`;
|
|
1027
1392
|
}
|