email-builder-utils 1.1.45 → 1.1.47
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/blocks/button.d.ts +29 -0
- package/dist/utils/blocks/button.d.ts.map +1 -0
- package/dist/utils/blocks/button.js +137 -0
- package/dist/utils/blocks/dividers.d.ts +4 -0
- package/dist/utils/blocks/dividers.d.ts.map +1 -0
- package/dist/utils/blocks/dividers.js +71 -0
- package/dist/utils/blocks/grid.d.ts +6 -0
- package/dist/utils/blocks/grid.d.ts.map +1 -0
- package/dist/utils/blocks/grid.js +248 -0
- package/dist/utils/blocks/image.d.ts +8 -0
- package/dist/utils/blocks/image.d.ts.map +1 -0
- package/dist/utils/blocks/image.js +58 -0
- package/dist/utils/blocks/shape.d.ts +2 -0
- package/dist/utils/blocks/shape.d.ts.map +1 -0
- package/dist/utils/blocks/shape.js +199 -0
- package/dist/utils/blocks/text.d.ts +2 -0
- package/dist/utils/blocks/text.d.ts.map +1 -0
- package/dist/utils/blocks/text.js +106 -0
- package/dist/utils/blocks/video.d.ts +2 -0
- package/dist/utils/blocks/video.d.ts.map +1 -0
- package/dist/utils/blocks/video.js +119 -0
- package/dist/utils/buildStyles.d.ts +10 -0
- package/dist/utils/buildStyles.d.ts.map +1 -0
- package/dist/utils/buildStyles.js +101 -0
- package/dist/utils/gradientUtils.d.ts +8 -0
- package/dist/utils/gradientUtils.d.ts.map +1 -0
- package/dist/utils/gradientUtils.js +68 -0
- package/dist/utils/jsonToHTML.d.ts +2 -29
- package/dist/utils/jsonToHTML.d.ts.map +1 -1
- package/dist/utils/jsonToHTML.js +18 -1490
- package/dist/utils/outlookSupport.d.ts +4 -207
- package/dist/utils/outlookSupport.d.ts.map +1 -1
- package/dist/utils/outlookSupport.js +86 -453
- package/package.json +1 -1
package/dist/utils/jsonToHTML.js
CHANGED
|
@@ -2,1509 +2,37 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.tableCommonStyle = void 0;
|
|
4
4
|
exports.convertToHtml = convertToHtml;
|
|
5
|
-
exports.convertVideoBlock = convertVideoBlock;
|
|
6
5
|
const types_1 = require("../types");
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const allPxAttributes = [...addPxToAttributes, ...addPxOrPerToAttributes];
|
|
17
|
-
exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed";
|
|
18
|
-
function encodeBlockProps(props) {
|
|
19
|
-
return JSON.stringify(props)
|
|
20
|
-
.replace(/&/g, '&')
|
|
21
|
-
.replace(/"/g, '"');
|
|
22
|
-
}
|
|
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
|
-
});
|
|
30
|
-
}
|
|
31
|
-
const GENERIC_FONT_FAMILIES = new Set([
|
|
32
|
-
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
|
33
|
-
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace',
|
|
34
|
-
'ui-rounded', 'emoji', 'math', 'fangsong',
|
|
35
|
-
]);
|
|
36
|
-
/**
|
|
37
|
-
* Normalises a CSS font-family string so every multi-word family name is wrapped
|
|
38
|
-
* in single quotes — safe inside double-quoted HTML style attributes.
|
|
39
|
-
* Handles already-quoted names (single or double), generic keywords, and any
|
|
40
|
-
* number of comma-separated families.
|
|
41
|
-
*/
|
|
42
|
-
function sanitizeFontFamily(fontFamily) {
|
|
43
|
-
if (!fontFamily)
|
|
44
|
-
return fontFamily;
|
|
45
|
-
return fontFamily
|
|
46
|
-
.split(',')
|
|
47
|
-
.map(font => {
|
|
48
|
-
const trimmed = font.trim();
|
|
49
|
-
// Strip any surrounding quotes (single or double) from either end
|
|
50
|
-
const unquoted = trimmed.replace(/^["']|["']$/g, '').trim();
|
|
51
|
-
if (!unquoted)
|
|
52
|
-
return '';
|
|
53
|
-
// Generic families and single-token names need no quotes
|
|
54
|
-
if (GENERIC_FONT_FAMILIES.has(unquoted.toLowerCase()) || !/\s/.test(unquoted)) {
|
|
55
|
-
return unquoted;
|
|
56
|
-
}
|
|
57
|
-
// Multi-word font name: wrap in single quotes, escaping any embedded single quotes
|
|
58
|
-
return `'${unquoted.replace(/'/g, "\\'")}'`;
|
|
59
|
-
})
|
|
60
|
-
.filter(Boolean)
|
|
61
|
-
.join(', ');
|
|
62
|
-
}
|
|
63
|
-
function buildStyles(style, { pxChanges, perChanges }) {
|
|
64
|
-
if (!style)
|
|
65
|
-
style = {};
|
|
66
|
-
const stylesObj = {};
|
|
67
|
-
Object.entries(style).forEach(([key, value]) => {
|
|
68
|
-
if (key === "customCss")
|
|
69
|
-
return;
|
|
70
|
-
const INVALID_KEYS = [
|
|
71
|
-
"columns",
|
|
72
|
-
"cellWidths",
|
|
73
|
-
"cellWidth",
|
|
74
|
-
"childWidth",
|
|
75
|
-
"visibility",
|
|
76
|
-
"hideOnMobile",
|
|
77
|
-
"hideOnDesktop",
|
|
78
|
-
"label",
|
|
79
|
-
"alignment",
|
|
80
|
-
];
|
|
81
|
-
if (INVALID_KEYS.includes(key))
|
|
82
|
-
return;
|
|
83
|
-
// Prevent null/undefined/"" from leaking into CSS
|
|
84
|
-
if (value === undefined || value === null || value === "")
|
|
85
|
-
return;
|
|
86
|
-
// FIX 1 — SANITIZE padding objects
|
|
87
|
-
if ((key === "padding" || key === "buttonPadding") &&
|
|
88
|
-
typeof value === "object") {
|
|
89
|
-
const pad = value;
|
|
90
|
-
const safePad = {
|
|
91
|
-
top: Number.isFinite(pad.top) ? pad.top : 0,
|
|
92
|
-
right: Number.isFinite(pad.right) ? pad.right : 0,
|
|
93
|
-
bottom: Number.isFinite(pad.bottom) ? pad.bottom : 0,
|
|
94
|
-
left: Number.isFinite(pad.left) ? pad.left : 0,
|
|
95
|
-
};
|
|
96
|
-
value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
|
|
97
|
-
}
|
|
98
|
-
if (key === "fontFamily" && typeof value === "string") {
|
|
99
|
-
value = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(value));
|
|
100
|
-
}
|
|
101
|
-
// Wrap backgroundImage values in url() if not already wrapped — skip gradients
|
|
102
|
-
if (key === "backgroundImage" && typeof value === "string"
|
|
103
|
-
&& !String(value).startsWith("url(")
|
|
104
|
-
&& !String(value).toLowerCase().includes("gradient(")) {
|
|
105
|
-
value = `url('${value}')`;
|
|
106
|
-
}
|
|
107
|
-
// lineHeight: values >= 4 are pixel values; smaller values are unitless multipliers (e.g. 1.5)
|
|
108
|
-
if (key === "lineHeight" && typeof value === "number") {
|
|
109
|
-
stylesObj["line-height"] = value >= 4 ? `${value}px` : String(value);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
113
|
-
// FIX 2 — Sanitize invalid px/per values
|
|
114
|
-
if (pxChanges.includes(key)) {
|
|
115
|
-
if (typeof value === "number") {
|
|
116
|
-
const rounded = Math.round(value * 100) / 100;
|
|
117
|
-
stylesObj[cssKey] = `${rounded}px`;
|
|
118
|
-
}
|
|
119
|
-
else if (typeof value === "string" && value.includes("null")) {
|
|
120
|
-
// Skip invalid styles
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
stylesObj[cssKey] = value;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
else if (perChanges.includes(key)) {
|
|
128
|
-
if (typeof value === "number") {
|
|
129
|
-
stylesObj[cssKey] = `${value}%`;
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
stylesObj[cssKey] = value;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
stylesObj[cssKey] = value;
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
const parts = Object.entries(stylesObj)
|
|
140
|
-
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
141
|
-
.map(([k, v]) => `${k}:${v}`);
|
|
142
|
-
if (style.customCss)
|
|
143
|
-
parts.push(style.customCss);
|
|
144
|
-
return parts.join('; ').replace(/;\s*$/, '').trim();
|
|
145
|
-
}
|
|
6
|
+
const text_1 = require("./blocks/text");
|
|
7
|
+
const image_1 = require("./blocks/image");
|
|
8
|
+
const button_1 = require("./blocks/button");
|
|
9
|
+
const grid_1 = require("./blocks/grid");
|
|
10
|
+
const dividers_1 = require("./blocks/dividers");
|
|
11
|
+
const video_1 = require("./blocks/video");
|
|
12
|
+
const shape_1 = require("./blocks/shape");
|
|
13
|
+
var buildStyles_1 = require("./buildStyles");
|
|
14
|
+
Object.defineProperty(exports, "tableCommonStyle", { enumerable: true, get: function () { return buildStyles_1.tableCommonStyle; } });
|
|
146
15
|
async function convertToHtml(blockData, rootData, cellWidthInPx) {
|
|
147
16
|
switch (blockData.type) {
|
|
148
17
|
case types_1.BlockType.TEXT:
|
|
149
|
-
return convertTextBlock(blockData, cellWidthInPx);
|
|
18
|
+
return (0, text_1.convertTextBlock)(blockData, cellWidthInPx);
|
|
150
19
|
case types_1.BlockType.IMAGE:
|
|
151
|
-
return await convertImageBlock(blockData, cellWidthInPx);
|
|
20
|
+
return await (0, image_1.convertImageBlock)(blockData, cellWidthInPx);
|
|
152
21
|
case types_1.BlockType.BUTTON:
|
|
153
|
-
return convertButtonBlock(blockData);
|
|
22
|
+
return (0, button_1.convertButtonBlock)(blockData);
|
|
154
23
|
case types_1.BlockType.GRID:
|
|
155
|
-
return await convertGridBlock(blockData, rootData, cellWidthInPx);
|
|
24
|
+
return await (0, grid_1.convertGridBlock)(blockData, rootData, cellWidthInPx);
|
|
156
25
|
case types_1.BlockType.DIVIDER:
|
|
157
|
-
return convertDividerBlockToHtml(blockData);
|
|
26
|
+
return (0, dividers_1.convertDividerBlockToHtml)(blockData);
|
|
158
27
|
case types_1.BlockType.SPACER:
|
|
159
|
-
return convertSpacerBlockToHtml(blockData);
|
|
28
|
+
return (0, dividers_1.convertSpacerBlockToHtml)(blockData);
|
|
160
29
|
case types_1.BlockType.VIDEO:
|
|
161
|
-
return convertVideoBlock(blockData, cellWidthInPx);
|
|
30
|
+
return (0, video_1.convertVideoBlock)(blockData, cellWidthInPx);
|
|
162
31
|
case types_1.BlockType.SHAPE:
|
|
163
|
-
return await convertShapeBlock(blockData);
|
|
32
|
+
return await (0, shape_1.convertShapeBlock)(blockData);
|
|
164
33
|
case types_1.BlockType.VDivider:
|
|
165
|
-
return convertVerticalDividerBlockToHtml(blockData);
|
|
34
|
+
return (0, dividers_1.convertVerticalDividerBlockToHtml)(blockData);
|
|
166
35
|
default:
|
|
167
36
|
return "";
|
|
168
37
|
}
|
|
169
38
|
}
|
|
170
|
-
function appendOutlookSupport(content, contentStyle, className, msoWidth) {
|
|
171
|
-
const visibilityClass = className || "";
|
|
172
|
-
const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
|
|
173
|
-
if (shouldHideInOutlook) {
|
|
174
|
-
return `
|
|
175
|
-
<!--[if !mso]><!-->
|
|
176
|
-
<table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
|
|
177
|
-
<!--<![endif]-->
|
|
178
|
-
`;
|
|
179
|
-
}
|
|
180
|
-
// When an explicit pixel width is provided (e.g. inside a column cell), use dual MSO/non-MSO
|
|
181
|
-
// tables. Old Outlook (Word engine) ignores max-width and can resolve width="100%" to the
|
|
182
|
-
// full email width (600px) rather than the column width, causing images/buttons to expand.
|
|
183
|
-
if (msoWidth) {
|
|
184
|
-
return `
|
|
185
|
-
<!--[if mso]>
|
|
186
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" style="border-collapse:collapse;width:${msoWidth}px;"><tr><td width="${msoWidth}" style="${contentStyle}">
|
|
187
|
-
<![endif]-->
|
|
188
|
-
<!--[if !mso]><!-->
|
|
189
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">
|
|
190
|
-
<!--<![endif]-->
|
|
191
|
-
${content}
|
|
192
|
-
<!--[if mso]></td></tr></table><![endif]-->
|
|
193
|
-
<!--[if !mso]><!-->
|
|
194
|
-
</td></tr></table>
|
|
195
|
-
<!--<![endif]-->
|
|
196
|
-
`;
|
|
197
|
-
}
|
|
198
|
-
return `
|
|
199
|
-
<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>
|
|
200
|
-
`;
|
|
201
|
-
}
|
|
202
|
-
function convertDividerBlockToHtml(blockData) {
|
|
203
|
-
const { style, props } = blockData.data;
|
|
204
|
-
const { hideOnMobile, hideOnDesktop } = props;
|
|
205
|
-
const { thickness, dividerColor, width, alignment, ...rest } = style;
|
|
206
|
-
const convertedStyle = buildStyles(rest, {
|
|
207
|
-
perChanges: [],
|
|
208
|
-
pxChanges: allPxAttributes,
|
|
209
|
-
});
|
|
210
|
-
const dividerWidth = width || 100;
|
|
211
|
-
const alignAttr = alignment === 'center' ? 'center' : alignment === 'right' ? 'right' : 'left';
|
|
212
|
-
// Append text-align so the import parser can recover alignment via inheritance
|
|
213
|
-
const contentStyle = convertedStyle
|
|
214
|
-
? `${convertedStyle}; text-align:${alignAttr};`
|
|
215
|
-
: `text-align:${alignAttr};`;
|
|
216
|
-
// Build class name based on visibility
|
|
217
|
-
const visibilityClass = [
|
|
218
|
-
hideOnMobile ? "hide-mobile" : "",
|
|
219
|
-
hideOnDesktop ? "hide-desktop" : "",
|
|
220
|
-
]
|
|
221
|
-
.filter(Boolean)
|
|
222
|
-
.join(" ");
|
|
223
|
-
const dividerContent = `
|
|
224
|
-
<table
|
|
225
|
-
align="${alignAttr}"
|
|
226
|
-
width="${dividerWidth}%"
|
|
227
|
-
cellpadding="0"
|
|
228
|
-
cellspacing="0"
|
|
229
|
-
>
|
|
230
|
-
<tr>
|
|
231
|
-
<td
|
|
232
|
-
height="${thickness}"
|
|
233
|
-
style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth}%;"
|
|
234
|
-
>
|
|
235
|
-
|
|
236
|
-
</td>
|
|
237
|
-
</tr>
|
|
238
|
-
</table>
|
|
239
|
-
`;
|
|
240
|
-
return appendOutlookSupport(dividerContent, contentStyle, visibilityClass);
|
|
241
|
-
}
|
|
242
|
-
function convertSpacerBlockToHtml(blockData) {
|
|
243
|
-
const { style, props } = blockData.data;
|
|
244
|
-
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
245
|
-
const styles = buildStyles(style, {
|
|
246
|
-
perChanges: [],
|
|
247
|
-
pxChanges: allPxAttributes,
|
|
248
|
-
});
|
|
249
|
-
return appendOutlookSupport(``, styles, visibilityClass);
|
|
250
|
-
}
|
|
251
|
-
function convertTextBlock(blockData, cellWidthInPx) {
|
|
252
|
-
const { style, props } = blockData.data;
|
|
253
|
-
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
254
|
-
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
|
|
255
|
-
...rest } = style;
|
|
256
|
-
// Text box decoration styles (border, background, padding) — no width
|
|
257
|
-
const textBoxStyle = {
|
|
258
|
-
backgroundColor,
|
|
259
|
-
padding,
|
|
260
|
-
borderRadius,
|
|
261
|
-
borderStyle,
|
|
262
|
-
borderColor,
|
|
263
|
-
borderWidth,
|
|
264
|
-
};
|
|
265
|
-
const convertedTextStyle = buildStyles(textBoxStyle, {
|
|
266
|
-
perChanges: [],
|
|
267
|
-
pxChanges: allPxAttributes,
|
|
268
|
-
});
|
|
269
|
-
// Outer td styles: layout only, no typography, no white-space
|
|
270
|
-
const styles = buildStyles({
|
|
271
|
-
padding: textContainerPadding,
|
|
272
|
-
backgroundColor: textContainerBackgroundColor,
|
|
273
|
-
...rest,
|
|
274
|
-
}, {
|
|
275
|
-
perChanges: [],
|
|
276
|
-
pxChanges: allPxAttributes,
|
|
277
|
-
});
|
|
278
|
-
const sanitizedText = (props.text ?? "")
|
|
279
|
-
.replace(/<p(\s[^>]*)?>/gi, (_, attrs) => `<div${attrs || ""}>`)
|
|
280
|
-
.replace(/<\/p>/gi, "</div>");
|
|
281
|
-
const navigateToUrl = props.navigateToUrl || "";
|
|
282
|
-
const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
|
|
283
|
-
// Email clients apply `a { color: blue }` which overrides inherited color.
|
|
284
|
-
// Inject the block color directly onto <a> tags that don't already have one.
|
|
285
|
-
const blockTextColor = rest.color;
|
|
286
|
-
const processedText = blockTextColor
|
|
287
|
-
? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = '') => {
|
|
288
|
-
if (/style\s*=\s*["'][^"']*\bcolor\s*:/i.test(attrs))
|
|
289
|
-
return match;
|
|
290
|
-
if (/\bstyle\s*=/i.test(attrs)) {
|
|
291
|
-
return `<a${attrs.replace(/(\bstyle\s*=\s*["'])/, `$1color:${blockTextColor};`)}>`;
|
|
292
|
-
}
|
|
293
|
-
return `<a${attrs} style="color:${blockTextColor};">`;
|
|
294
|
-
})
|
|
295
|
-
: sanitizedText;
|
|
296
|
-
const colorStyle = blockTextColor ? `color:${blockTextColor};` : '';
|
|
297
|
-
// Use display:block + width:100% so text fills the column naturally.
|
|
298
|
-
// display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
|
|
299
|
-
const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
|
|
300
|
-
const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
|
|
301
|
-
const textContent = appendOutlookSupport(convertedTextBox, styles, visibilityClass, safeCellWidth);
|
|
302
|
-
const linkColorStyle = blockTextColor ? `color:${blockTextColor};` : 'color:inherit;';
|
|
303
|
-
return navigateToUrl
|
|
304
|
-
? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${textContent}</a>`
|
|
305
|
-
: textContent;
|
|
306
|
-
}
|
|
307
|
-
async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
|
|
308
|
-
// OUTLOOK FIX: Use provided dimensions or calculate from image
|
|
309
|
-
let vmlWidth;
|
|
310
|
-
let vmlHeight;
|
|
311
|
-
if (finalWidth && finalHeight) {
|
|
312
|
-
// Use pre-calculated dimensions (preferred for accuracy)
|
|
313
|
-
vmlWidth = finalWidth;
|
|
314
|
-
vmlHeight = finalHeight;
|
|
315
|
-
}
|
|
316
|
-
else if (imageUrl) {
|
|
317
|
-
try {
|
|
318
|
-
const { width: originalWidth, height: originalHeight } = await loadImageNaturalDimensions(imageUrl);
|
|
319
|
-
const widthScalingFactor = Math.min(outerContainerWidth / originalWidth, innerContainerWidth / originalWidth, 1);
|
|
320
|
-
vmlWidth = Math.round(originalWidth * widthScalingFactor);
|
|
321
|
-
vmlHeight = Math.round(originalHeight * widthScalingFactor);
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
vmlWidth = innerContainerWidth;
|
|
325
|
-
vmlHeight = innerContainerWidth;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
vmlWidth = innerContainerWidth;
|
|
330
|
-
vmlHeight = innerContainerWidth;
|
|
331
|
-
}
|
|
332
|
-
const borderWidth = parseInt(style?.borderWidth) || 0;
|
|
333
|
-
const borderColor = style?.borderColor || "transparent";
|
|
334
|
-
const borderRadius = parseInt(style?.borderRadius) || 0;
|
|
335
|
-
const useRoundRect = borderRadius > 0;
|
|
336
|
-
const arcsize = useRoundRect
|
|
337
|
-
? Math.min(borderRadius / vmlHeight, 1).toFixed(2)
|
|
338
|
-
: "";
|
|
339
|
-
const borderAttributes = borderWidth > 0
|
|
340
|
-
? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
|
|
341
|
-
: `stroked="false"`;
|
|
342
|
-
// OUTLOOK FIX: For Outlook 2019+ (version 2512), VML type="frame" causes stretching
|
|
343
|
-
// Solution: Use simple IMG tag with fixed dimensions for Outlook, only use VML for border radius
|
|
344
|
-
let outlookImage;
|
|
345
|
-
if (useRoundRect && borderRadius > 0) {
|
|
346
|
-
// Use VML for border radius - wrap in table to constrain width for Old Outlook (Word engine)
|
|
347
|
-
// Use aspect="atmost" to prevent image from stretching beyond its bounds
|
|
348
|
-
outlookImage = `<!--[if mso]>
|
|
349
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
350
|
-
<tr>
|
|
351
|
-
<td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
352
|
-
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
353
|
-
style="width:${vmlWidth}px;height:${vmlHeight}px;"
|
|
354
|
-
${borderAttributes}
|
|
355
|
-
arcsize="${arcsize}"
|
|
356
|
-
fill="true" fillcolor="none">
|
|
357
|
-
<v:fill src="${imageUrl}" type="tile" aspect="atmost" />
|
|
358
|
-
<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>
|
|
359
|
-
</v:roundrect>
|
|
360
|
-
</td>
|
|
361
|
-
</tr>
|
|
362
|
-
</table>
|
|
363
|
-
<![endif]-->`;
|
|
364
|
-
}
|
|
365
|
-
else {
|
|
366
|
-
// For images without border radius, wrap in a table with explicit width for Old Outlook (Word engine)
|
|
367
|
-
// This prevents stretching/overflow in Outlook 2007-2019 and Outlook Classic
|
|
368
|
-
const borderStyleAttr = borderWidth > 0
|
|
369
|
-
? `border: ${borderWidth}px solid ${borderColor};`
|
|
370
|
-
: '';
|
|
371
|
-
outlookImage = `<!--[if mso]>
|
|
372
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
373
|
-
<tr>
|
|
374
|
-
<td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
375
|
-
<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}" />
|
|
376
|
-
</td>
|
|
377
|
-
</tr>
|
|
378
|
-
</table>
|
|
379
|
-
<![endif]-->`;
|
|
380
|
-
}
|
|
381
|
-
return `
|
|
382
|
-
${outlookImage}
|
|
383
|
-
<!--[if !mso]><!-->
|
|
384
|
-
${content}
|
|
385
|
-
<!--<![endif]-->
|
|
386
|
-
`;
|
|
387
|
-
}
|
|
388
|
-
async function computeScaledDimensions(imageUrl, maxContainerWidthPx) {
|
|
389
|
-
if (!imageUrl) {
|
|
390
|
-
const w = Math.max(maxContainerWidthPx, 1);
|
|
391
|
-
const h = Math.round(w * (2 / 3));
|
|
392
|
-
return { originalWidth: w, originalHeight: h, scaledWidth: w, scaledHeight: h };
|
|
393
|
-
}
|
|
394
|
-
try {
|
|
395
|
-
const { width: originalWidth, height: originalHeight } = await loadImageNaturalDimensions(imageUrl);
|
|
396
|
-
const widthScalingFactor = Math.min(maxContainerWidthPx / originalWidth, 1);
|
|
397
|
-
const scaledWidth = Math.round(originalWidth * widthScalingFactor);
|
|
398
|
-
const scaledHeight = Math.round(originalHeight * widthScalingFactor);
|
|
399
|
-
return { originalWidth, originalHeight, scaledWidth, scaledHeight };
|
|
400
|
-
}
|
|
401
|
-
catch {
|
|
402
|
-
const w = Math.max(maxContainerWidthPx, 1);
|
|
403
|
-
const h = Math.round(w * (2 / 3));
|
|
404
|
-
return { originalWidth: w, originalHeight: h, scaledWidth: w, scaledHeight: h };
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
async function convertImageBlock(blockData, cellWidthInPx) {
|
|
408
|
-
const { style, props } = blockData.data;
|
|
409
|
-
const { altText, imageUrl, navigateToUrl } = props;
|
|
410
|
-
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
411
|
-
const { width, height, objectFit, borderRadius, borderWidth, borderColor, borderStyle, ...containerStyle } = style;
|
|
412
|
-
// Add border styles to container for fallback clients
|
|
413
|
-
const containerStyles = buildStyles({
|
|
414
|
-
...containerStyle,
|
|
415
|
-
}, { perChanges: [], pxChanges: addPxToAttributes });
|
|
416
|
-
// OUTLOOK FIX: Ensure cellWidthInPx never exceeds 600px
|
|
417
|
-
const safeCellWidth = Math.min(cellWidthInPx, 600);
|
|
418
|
-
// Parse width percentage (default 100%)
|
|
419
|
-
const widthPercent = typeof width === "string" && width.includes("%")
|
|
420
|
-
? parseInt(width.replace("%", ""))
|
|
421
|
-
: typeof width === "number"
|
|
422
|
-
? width
|
|
423
|
-
: 100;
|
|
424
|
-
// OUTLOOK FIX: Calculate inner container width based on safe cell width
|
|
425
|
-
const paddingLeft = style?.padding?.left || 0;
|
|
426
|
-
const paddingRight = style?.padding?.right || 0;
|
|
427
|
-
const availableWidth = safeCellWidth - paddingLeft - paddingRight;
|
|
428
|
-
const innerContainerWidth = Math.round((widthPercent / 100) * availableWidth);
|
|
429
|
-
// Get image dimensions and calculate scaled sizes
|
|
430
|
-
const { originalWidth, originalHeight, scaledWidth, scaledHeight } = await computeScaledDimensions(imageUrl, innerContainerWidth);
|
|
431
|
-
// OUTLOOK FIX: For Outlook, we need exact pixel dimensions
|
|
432
|
-
// Calculate final dimensions that respect both original size and container
|
|
433
|
-
const finalWidth = Math.min(scaledWidth, innerContainerWidth, originalWidth);
|
|
434
|
-
const finalHeight = Math.round((finalWidth / originalWidth) * originalHeight);
|
|
435
|
-
// Build image styles for modern email clients (non-Outlook)
|
|
436
|
-
const imageTagStyles = buildStyles({
|
|
437
|
-
borderStyle,
|
|
438
|
-
borderRadius: borderRadius,
|
|
439
|
-
borderColor,
|
|
440
|
-
borderWidth,
|
|
441
|
-
}, {
|
|
442
|
-
perChanges: [],
|
|
443
|
-
pxChanges: addPxToAttributes,
|
|
444
|
-
});
|
|
445
|
-
// OUTLOOK FIX: Image element with explicit dimensions
|
|
446
|
-
// Outlook will use width/height attributes, modern clients use CSS
|
|
447
|
-
// Use max-width instead of width:100% to prevent stretching
|
|
448
|
-
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;" />`;
|
|
449
|
-
const percentWidth = typeof width === "string" && width.endsWith("%")
|
|
450
|
-
? width
|
|
451
|
-
: typeof width === "number"
|
|
452
|
-
? `${width}%`
|
|
453
|
-
: "100%";
|
|
454
|
-
// Non-MSO wrapper: display:block removes the phantom inline-baseline gap that
|
|
455
|
-
// display:inline-block creates in Gmail / Apple Mail / Yahoo between images.
|
|
456
|
-
// margin handles alignment since text-align won't move block elements.
|
|
457
|
-
const imgTextAlign = containerStyle.textAlign || "left";
|
|
458
|
-
const imgMargin = imgTextAlign === "center" ? "margin:0 auto;" :
|
|
459
|
-
imgTextAlign === "right" ? "margin-left:auto; margin-right:0;" : "";
|
|
460
|
-
// OUTLOOK FIX: Use finalWidth (the actual displayed size) as max-width so the div
|
|
461
|
-
// doesn't claim more space than the image occupies. originalWidth is the natural
|
|
462
|
-
// image size (e.g. 636px for the Beefree logo rendered at 35px) which was
|
|
463
|
-
// misleadingly large and could confuse some rendering engines.
|
|
464
|
-
const nonMsoWrapper = `<div style="display:block; width:${percentWidth}; max-width:${finalWidth}px; line-height:0; font-size:0; ${imgMargin}">${imageElement}</div>`;
|
|
465
|
-
// OUTLOOK FIX: Generate VML with corrected dimensions
|
|
466
|
-
const outlookImage = await appendOutlookForImage(nonMsoWrapper, safeCellWidth, innerContainerWidth, imageUrl, style, finalWidth, finalHeight);
|
|
467
|
-
const imageContent = appendOutlookSupport(outlookImage, containerStyles, visibilityClass, safeCellWidth);
|
|
468
|
-
return navigateToUrl
|
|
469
|
-
? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block;">${imageContent}</a>`
|
|
470
|
-
: imageContent;
|
|
471
|
-
}
|
|
472
|
-
function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
|
|
473
|
-
const pad = buttonStyle.buttonPadding || {};
|
|
474
|
-
const padTop = Number.isFinite(pad.top) ? pad.top : 10;
|
|
475
|
-
const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
|
|
476
|
-
const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
|
|
477
|
-
const padRight = Number.isFinite(pad.right) ? pad.right : 20;
|
|
478
|
-
const fontSize = buttonStyle.fontSize || 16;
|
|
479
|
-
const height = typeof buttonStyle.height === "number" && buttonStyle.height > 0
|
|
480
|
-
? buttonStyle.height
|
|
481
|
-
: null;
|
|
482
|
-
// prevent layout break
|
|
483
|
-
const minHeight = padTop + padBottom + fontSize;
|
|
484
|
-
const finalHeight = height ? Math.max(height, minHeight) : null;
|
|
485
|
-
const borderRadius = buttonStyle.borderRadius || 0;
|
|
486
|
-
const borderColor = buttonStyle.borderColor || "transparent";
|
|
487
|
-
const borderWidth = buttonStyle.borderWidth || 0;
|
|
488
|
-
const borderStyle = buttonStyle.borderStyle || "solid";
|
|
489
|
-
const bgColor = buttonStyle.buttonColor || "transparent";
|
|
490
|
-
const color = buttonStyle.color || "#ffffff";
|
|
491
|
-
const fontFamily = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily));
|
|
492
|
-
const fontWeight = buttonStyle.fontWeight || 400;
|
|
493
|
-
const width = typeof buttonStyle.width === "number"
|
|
494
|
-
? `width="${buttonStyle.width}"`
|
|
495
|
-
: "";
|
|
496
|
-
return `<!--[if mso]>
|
|
497
|
-
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="display:inline-table;">
|
|
498
|
-
<tr>
|
|
499
|
-
<td align="center"
|
|
500
|
-
valign="middle"
|
|
501
|
-
${width}
|
|
502
|
-
${finalHeight ? `height="${finalHeight}"` : ""}
|
|
503
|
-
bgcolor="${bgColor}"
|
|
504
|
-
style="
|
|
505
|
-
${finalHeight ? `height:${finalHeight}px;` : ""}
|
|
506
|
-
background-color:${bgColor};
|
|
507
|
-
border-radius:${borderRadius}px;
|
|
508
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
509
|
-
overflow:hidden;
|
|
510
|
-
mso-line-height-rule:exactly;
|
|
511
|
-
">
|
|
512
|
-
|
|
513
|
-
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
514
|
-
<tr>
|
|
515
|
-
<td align="center" valign="middle"
|
|
516
|
-
style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
|
|
517
|
-
|
|
518
|
-
<a href="${navigateToUrl}"
|
|
519
|
-
style="
|
|
520
|
-
display:inline-block;
|
|
521
|
-
color:${color};
|
|
522
|
-
text-decoration:none;
|
|
523
|
-
font-family:${fontFamily};
|
|
524
|
-
font-size:${fontSize}px;
|
|
525
|
-
font-weight:${fontWeight};
|
|
526
|
-
line-height:normal;
|
|
527
|
-
">
|
|
528
|
-
${text}
|
|
529
|
-
</a>
|
|
530
|
-
|
|
531
|
-
</td>
|
|
532
|
-
</tr>
|
|
533
|
-
</table>
|
|
534
|
-
|
|
535
|
-
</td>
|
|
536
|
-
</tr>
|
|
537
|
-
</table>
|
|
538
|
-
<![endif]-->
|
|
539
|
-
<!--[if !mso]><!-->
|
|
540
|
-
${content}
|
|
541
|
-
<!--<![endif]-->`;
|
|
542
|
-
}
|
|
543
|
-
function convertButtonBlock(blockData) {
|
|
544
|
-
const { style, props } = blockData.data;
|
|
545
|
-
const { text, navigateToUrl } = props;
|
|
546
|
-
const { fontFamily, fontSize, fontWeight, textAlign, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, alignment, padding, backgroundColor: containerBg, margin, } = style;
|
|
547
|
-
const pad = buttonPadding || {};
|
|
548
|
-
const padTop = Number.isFinite(pad.top) ? pad.top : 10;
|
|
549
|
-
const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
|
|
550
|
-
const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
|
|
551
|
-
const padRight = Number.isFinite(pad.right) ? pad.right : 20;
|
|
552
|
-
const fs = fontSize || 16;
|
|
553
|
-
// prevent layout break
|
|
554
|
-
const minHeight = padTop + padBottom + fs;
|
|
555
|
-
const finalHeight = typeof height === "number" && height > 0
|
|
556
|
-
? Math.max(height, minHeight)
|
|
557
|
-
: null;
|
|
558
|
-
const safeFF = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(fontFamily));
|
|
559
|
-
const safeColor = color || "#ffffff";
|
|
560
|
-
const bgColor = buttonColor || "transparent";
|
|
561
|
-
const bdColor = borderColor || "transparent";
|
|
562
|
-
const bdStyle = borderStyle || "solid";
|
|
563
|
-
const bw = borderWidth || 0;
|
|
564
|
-
const br = borderRadius || 0;
|
|
565
|
-
const containerAlign = alignment || textAlign || "left";
|
|
566
|
-
const widthAttr = typeof width === "number"
|
|
567
|
-
? `width="${width}"`
|
|
568
|
-
: "";
|
|
569
|
-
// ✅ FIX: no width=100% anywhere
|
|
570
|
-
const buttonTable = `
|
|
571
|
-
<table role="presentation" cellspacing="0" cellpadding="0" border="0"
|
|
572
|
-
style="display:inline-table; border-collapse:separate;"
|
|
573
|
-
${widthAttr}>
|
|
574
|
-
<tr>
|
|
575
|
-
<td
|
|
576
|
-
align="center"
|
|
577
|
-
valign="middle"
|
|
578
|
-
${finalHeight ? `height="${finalHeight}"` : ""}
|
|
579
|
-
style="
|
|
580
|
-
${finalHeight ? `height:${finalHeight}px;` : ""}
|
|
581
|
-
background-color:${bgColor};
|
|
582
|
-
border-radius:${br}px;
|
|
583
|
-
border:${bw}px ${bdStyle} ${bdColor};
|
|
584
|
-
overflow:hidden;
|
|
585
|
-
mso-line-height-rule:exactly;
|
|
586
|
-
"
|
|
587
|
-
>
|
|
588
|
-
|
|
589
|
-
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
590
|
-
<tr>
|
|
591
|
-
<td align="center" valign="middle"
|
|
592
|
-
style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
|
|
593
|
-
|
|
594
|
-
<a href="${navigateToUrl}"
|
|
595
|
-
style="
|
|
596
|
-
display:inline-block;
|
|
597
|
-
color:${safeColor};
|
|
598
|
-
text-decoration:none;
|
|
599
|
-
font-family:${safeFF};
|
|
600
|
-
font-size:${fs}px;
|
|
601
|
-
font-weight:${fontWeight || 400};
|
|
602
|
-
line-height:normal;
|
|
603
|
-
white-space:nowrap;
|
|
604
|
-
">
|
|
605
|
-
${text}
|
|
606
|
-
</a>
|
|
607
|
-
|
|
608
|
-
</td>
|
|
609
|
-
</tr>
|
|
610
|
-
</table>
|
|
611
|
-
|
|
612
|
-
</td>
|
|
613
|
-
</tr>
|
|
614
|
-
</table>
|
|
615
|
-
`;
|
|
616
|
-
const aligned = containerAlign === "center"
|
|
617
|
-
? `<center>${buttonTable}</center>`
|
|
618
|
-
: `<div style="text-align:${containerAlign};">${buttonTable}</div>`;
|
|
619
|
-
const buttonWithOutlook = appendOutlookForButton(aligned, style, navigateToUrl, text);
|
|
620
|
-
return `
|
|
621
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
|
622
|
-
<tr>
|
|
623
|
-
<td align="${containerAlign}"
|
|
624
|
-
style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;
|
|
625
|
-
background-color:${containerBg || "transparent"};">
|
|
626
|
-
${buttonWithOutlook}
|
|
627
|
-
</td>
|
|
628
|
-
</tr>
|
|
629
|
-
</table>
|
|
630
|
-
`;
|
|
631
|
-
}
|
|
632
|
-
// Words inside a gradient() that are NOT color names
|
|
633
|
-
const GRADIENT_KEYWORDS = new Set([
|
|
634
|
-
'linear', 'radial', 'conic', 'gradient',
|
|
635
|
-
'to', 'at', 'top', 'bottom', 'left', 'right', 'center',
|
|
636
|
-
'closest', 'farthest', 'corner', 'side', 'circle', 'ellipse',
|
|
637
|
-
'deg', 'turn', 'rad', 'grad', 'from', 'in',
|
|
638
|
-
'url', // url() prefix sometimes appears when gradient is wrapped incorrectly
|
|
639
|
-
]);
|
|
640
|
-
/** Extract the first color stop (hex, rgb, or named CSS color) from a gradient string. */
|
|
641
|
-
function firstGradientColor(gradient) {
|
|
642
|
-
const tokenRe = /#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|([a-zA-Z-]+)/g;
|
|
643
|
-
let m;
|
|
644
|
-
while ((m = tokenRe.exec(gradient)) !== null) {
|
|
645
|
-
const namedWord = m[1];
|
|
646
|
-
if (namedWord) {
|
|
647
|
-
if (!GRADIENT_KEYWORDS.has(namedWord.toLowerCase()))
|
|
648
|
-
return namedWord;
|
|
649
|
-
}
|
|
650
|
-
else {
|
|
651
|
-
return m[0]; // hex or rgb()
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
return '';
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* Extract the first solid-color stop from a CSS gradient in customCss.
|
|
658
|
-
* Used as a MSO/Outlook bgcolor fallback. Handles hex, rgb, and named colors.
|
|
659
|
-
*/
|
|
660
|
-
function extractCssFallbackColor(customCss) {
|
|
661
|
-
if (!customCss)
|
|
662
|
-
return '';
|
|
663
|
-
const gradientMatch = customCss.match(/(?:linear|radial|conic)-gradient\(([^)]+(?:\([^)]*\)[^)]*)*)\)/);
|
|
664
|
-
if (!gradientMatch)
|
|
665
|
-
return '';
|
|
666
|
-
return firstGradientColor(gradientMatch[1]);
|
|
667
|
-
}
|
|
668
|
-
function parseGradient(gradient) {
|
|
669
|
-
if (!gradient)
|
|
670
|
-
return null;
|
|
671
|
-
const lower = gradient.toLowerCase();
|
|
672
|
-
// Determine angle from deg value or direction keyword
|
|
673
|
-
const degMatch = gradient.match(/(\d+(?:\.\d+)?)deg/);
|
|
674
|
-
let angle = 180;
|
|
675
|
-
if (degMatch) {
|
|
676
|
-
angle = parseFloat(degMatch[1]);
|
|
677
|
-
}
|
|
678
|
-
else if (lower.includes('to right'))
|
|
679
|
-
angle = 90;
|
|
680
|
-
else if (lower.includes('to left'))
|
|
681
|
-
angle = 270;
|
|
682
|
-
else if (lower.includes('to top'))
|
|
683
|
-
angle = 0;
|
|
684
|
-
// 'to bottom' and bare gradient() both default to 180
|
|
685
|
-
// Extract ALL color tokens: hex, rgb/rgba, and named CSS color words
|
|
686
|
-
const colors = [];
|
|
687
|
-
const tokenRe = /#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|([a-zA-Z-]+)/g;
|
|
688
|
-
let m;
|
|
689
|
-
while ((m = tokenRe.exec(gradient)) !== null) {
|
|
690
|
-
const namedWord = m[1];
|
|
691
|
-
if (namedWord) {
|
|
692
|
-
if (!GRADIENT_KEYWORDS.has(namedWord.toLowerCase()))
|
|
693
|
-
colors.push(namedWord);
|
|
694
|
-
}
|
|
695
|
-
else {
|
|
696
|
-
colors.push(m[0]);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
return {
|
|
700
|
-
angle,
|
|
701
|
-
colors,
|
|
702
|
-
fallback: colors[0] || '#ffffff',
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
function cssAngleToVml(angle) {
|
|
706
|
-
return (angle + 90) % 360;
|
|
707
|
-
}
|
|
708
|
-
async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
709
|
-
const { style = {}, childrenIds = [], props } = blockData.data;
|
|
710
|
-
const { columns = 1, cellWidths = [], responsive = true } = props;
|
|
711
|
-
const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
|
|
712
|
-
const gridVisibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
713
|
-
// Detect gradient — check both backgroundImage prop and customCss (gradient may land in
|
|
714
|
-
// customCss when the block was built via CSS shorthand or custom CSS input).
|
|
715
|
-
const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : '';
|
|
716
|
-
const customCssStr = restStyle.customCss || '';
|
|
717
|
-
// Extract gradient string from customCss if not already in backgroundImage
|
|
718
|
-
const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
|
|
719
|
-
? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
|
|
720
|
-
: '';
|
|
721
|
-
const effectiveGradient = bgImageStr.includes('gradient(')
|
|
722
|
-
? bgImageStr
|
|
723
|
-
: gradientInCustomCss;
|
|
724
|
-
const isGradient = Boolean(effectiveGradient);
|
|
725
|
-
const parsedGradient = isGradient ? parseGradient(effectiveGradient) : null;
|
|
726
|
-
const fallbackBgColor = backgroundColor ||
|
|
727
|
-
parsedGradient?.fallback ||
|
|
728
|
-
extractCssFallbackColor(customCssStr) ||
|
|
729
|
-
"#ffffff";
|
|
730
|
-
const rawBgImageUrl = !isGradient && bgImageStr
|
|
731
|
-
? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
|
|
732
|
-
: null;
|
|
733
|
-
// When gradient came from customCss, strip background-image from customCss so it
|
|
734
|
-
// doesn't duplicate into the inner table style (the outer <td> wrapper carries it).
|
|
735
|
-
const innerCustomCss = gradientInCustomCss
|
|
736
|
-
? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
|
|
737
|
-
: customCssStr;
|
|
738
|
-
// Build inner table styles — when gradient/bg-image is on the outer wrapper, strip
|
|
739
|
-
// background props from the inner table so the outer <td> background shows through.
|
|
740
|
-
const innerRestStyleRaw = (rawBgImageUrl || isGradient)
|
|
741
|
-
? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
|
|
742
|
-
: { ...restStyle, customCss: innerCustomCss };
|
|
743
|
-
// Extract border/radius props — applied via a div wrapper for non-MSO clients so that
|
|
744
|
-
// border-radius is honoured (Gmail/Outlook compose strip border-radius from <table>).
|
|
745
|
-
const { borderRadius, border, borderColor, borderWidth, borderStyle: borderStyleProp, ...innerRestStyle } = innerRestStyleRaw;
|
|
746
|
-
const divBorderParts = [];
|
|
747
|
-
if (borderRadius)
|
|
748
|
-
divBorderParts.push(`border-radius:${typeof borderRadius === 'number' ? borderRadius + 'px' : borderRadius};`, `overflow:hidden;`);
|
|
749
|
-
if (border) {
|
|
750
|
-
divBorderParts.push(`border:${border};`);
|
|
751
|
-
}
|
|
752
|
-
else if (borderWidth || borderColor || borderStyleProp) {
|
|
753
|
-
const bw = borderWidth ? (typeof borderWidth === 'number' ? borderWidth + 'px' : borderWidth) : '1px';
|
|
754
|
-
const bs = borderStyleProp || 'solid';
|
|
755
|
-
const bc = borderColor || '#000000';
|
|
756
|
-
divBorderParts.push(`border:${bw} ${bs} ${bc};`);
|
|
757
|
-
}
|
|
758
|
-
const divBorderStyle = divBorderParts.join(' ');
|
|
759
|
-
const tableBgForNonMso = divBorderStyle
|
|
760
|
-
? 'transparent'
|
|
761
|
-
: ((rawBgImageUrl || isGradient) ? undefined : backgroundColor);
|
|
762
|
-
const tableStyles = buildStyles({ backgroundColor: tableBgForNonMso, ...innerRestStyle }, {
|
|
763
|
-
perChanges: [],
|
|
764
|
-
pxChanges: allPxAttributes,
|
|
765
|
-
});
|
|
766
|
-
const total = childrenIds.length;
|
|
767
|
-
const visualRows = Math.ceil(total / columns);
|
|
768
|
-
// OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
|
|
769
|
-
const msoTableWidth = Math.min(cellWidthInPx, 600);
|
|
770
|
-
// When a background image/gradient is present, the background is applied on an outer
|
|
771
|
-
// wrapper <td> (see bottom of function). The inner grid tables must be clean.
|
|
772
|
-
// When no background, the MSO table gets bgcolor for solid-color sections.
|
|
773
|
-
const msoBgColor = !rawBgImageUrl && !isGradient
|
|
774
|
-
? (backgroundColor || '')
|
|
775
|
-
: '';
|
|
776
|
-
const msoBgAttr = msoBgColor ? ` bgcolor="${msoBgColor}"` : '';
|
|
777
|
-
const msoBgStyle = msoBgColor ? `background-color:${msoBgColor};` : '';
|
|
778
|
-
// Inner tables must be explicitly transparent when outer <td> carries the background.
|
|
779
|
-
const innerBgTransparent = (rawBgImageUrl || isGradient)
|
|
780
|
-
? 'background-color:transparent;'
|
|
781
|
-
: '';
|
|
782
|
-
const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor && !divBorderStyle ? ` bgcolor="${backgroundColor}"` : '';
|
|
783
|
-
// When divBorderStyle is set the non-MSO <table> is transparent, so the Grid's
|
|
784
|
-
// backgroundColor must move onto the div wrapper — otherwise it vanishes in modern clients.
|
|
785
|
-
// Skip this for bg-image/gradient blocks; they apply their background via a separate wrapper.
|
|
786
|
-
const divWrapBg = divBorderStyle && backgroundColor && !rawBgImageUrl && !isGradient
|
|
787
|
-
? ` background-color:${backgroundColor};`
|
|
788
|
-
: '';
|
|
789
|
-
const divWrapOpen = divBorderStyle ? `<div style="${divBorderStyle}${divWrapBg}">` : '';
|
|
790
|
-
const divWrapClose = divBorderStyle ? `</div>` : '';
|
|
791
|
-
let html = `
|
|
792
|
-
<!--[if mso]>
|
|
793
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
|
|
794
|
-
style="border-collapse:collapse;width:${msoTableWidth}px;${msoBgStyle}${innerBgTransparent}"
|
|
795
|
-
class="${gridVisibilityClass}">
|
|
796
|
-
<![endif]-->
|
|
797
|
-
<!--[if !mso]><!-->
|
|
798
|
-
<table border="0" cellpadding="0" cellspacing="0" width="100%"
|
|
799
|
-
role="presentation"${nonMsoBgAttr}
|
|
800
|
-
style="border-collapse:collapse; ${innerBgTransparent}${tableStyles}; max-width:600px;"
|
|
801
|
-
class="${gridVisibilityClass}">
|
|
802
|
-
<!--<![endif]-->
|
|
803
|
-
`;
|
|
804
|
-
for (let r = 0; r < visualRows; r++) {
|
|
805
|
-
html += "<tr>";
|
|
806
|
-
// COUNT visible cells and find last visible column index
|
|
807
|
-
let visibleCells = 0;
|
|
808
|
-
let lastVisibleCol = 0;
|
|
809
|
-
const rowIds = [];
|
|
810
|
-
for (let c = 0; c < columns; c++) {
|
|
811
|
-
const idx = r * columns + c;
|
|
812
|
-
const id = childrenIds[idx] ?? null;
|
|
813
|
-
rowIds.push(id);
|
|
814
|
-
const child = id ? rootData[id] : null;
|
|
815
|
-
const isHidden = child?.data?.props?.hideOnDesktop;
|
|
816
|
-
if (!isHidden) {
|
|
817
|
-
visibleCells++;
|
|
818
|
-
lastVisibleCol = c;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
const safeWidth = visibleCells > 0 ? 100 / visibleCells : 100 / columns;
|
|
822
|
-
// Reserve pixel space for spacer tds between visible cells (N-1 gaps for N visible cells)
|
|
823
|
-
const totalGapPx = columnGap * Math.max(visibleCells - 1, 0);
|
|
824
|
-
const adjustedTableWidth = Math.max(msoTableWidth - totalGapPx, 1);
|
|
825
|
-
let totalWidth = 0;
|
|
826
|
-
const cellWidthPercents = [];
|
|
827
|
-
for (let c = 0; c < columns; c++) {
|
|
828
|
-
const id = rowIds[c];
|
|
829
|
-
let widthPercent = cellWidths[c] ?? safeWidth;
|
|
830
|
-
if (widthPercent <= 0 || widthPercent > 100) {
|
|
831
|
-
widthPercent = safeWidth;
|
|
832
|
-
}
|
|
833
|
-
cellWidthPercents.push(widthPercent);
|
|
834
|
-
if (id) {
|
|
835
|
-
const child = rootData[id];
|
|
836
|
-
const isHidden = child?.data?.props?.hideOnDesktop;
|
|
837
|
-
if (!isHidden) {
|
|
838
|
-
totalWidth += widthPercent;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
const scaleFactor = totalWidth > 0 && totalWidth < 100 ? 100 / totalWidth : 1;
|
|
843
|
-
for (let c = 0; c < columns; c++) {
|
|
844
|
-
const id = rowIds[c];
|
|
845
|
-
let widthPercent = cellWidthPercents[c] * scaleFactor;
|
|
846
|
-
widthPercent = Math.min(widthPercent, 100);
|
|
847
|
-
// Cell pixel width is a share of the gap-adjusted table width
|
|
848
|
-
const cellWidthPx = Math.round((widthPercent / 100) * adjustedTableWidth);
|
|
849
|
-
if (id) {
|
|
850
|
-
const child = rootData[id];
|
|
851
|
-
const { style: cellStyle = {}, props: childProps = {} } = child.data;
|
|
852
|
-
const verticalAlign = cellStyle.verticalAlign || "top";
|
|
853
|
-
const childVisible = !childProps.hideOnDesktop;
|
|
854
|
-
const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
|
|
855
|
-
if (childVisible) {
|
|
856
|
-
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth, Boolean(divBorderStyle));
|
|
857
|
-
// bgcolor on the cell <td> ensures background-color survives Outlook
|
|
858
|
-
// compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
|
|
859
|
-
const cellBgColor = cellStyle.backgroundColor || '';
|
|
860
|
-
const cellBgAttr = cellBgColor ? ` bgcolor="${cellBgColor}"` : '';
|
|
861
|
-
html += `
|
|
862
|
-
<td
|
|
863
|
-
width="${cellWidthPx}"${cellBgAttr}
|
|
864
|
-
class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
|
|
865
|
-
style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
|
|
866
|
-
>
|
|
867
|
-
${childHtml}
|
|
868
|
-
</td>`;
|
|
869
|
-
// Spacer td between columns — fixed pixel width, invisible to screen readers
|
|
870
|
-
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
871
|
-
html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
else {
|
|
876
|
-
html += `
|
|
877
|
-
<td width="${cellWidthPx}"
|
|
878
|
-
${responsive ? 'class="stack-column"' : ""}
|
|
879
|
-
style="width:${cellWidthPx}px;vertical-align:top;">
|
|
880
|
-
</td>`;
|
|
881
|
-
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
882
|
-
html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
html += "</tr>";
|
|
887
|
-
}
|
|
888
|
-
// Close both MSO and non-MSO tables
|
|
889
|
-
html += `
|
|
890
|
-
<!--[if mso]>
|
|
891
|
-
</table>
|
|
892
|
-
<![endif]-->
|
|
893
|
-
<!--[if !mso]><!-->
|
|
894
|
-
</table>
|
|
895
|
-
<!--<![endif]-->
|
|
896
|
-
`;
|
|
897
|
-
// ── Background image: canonical multi-client approach ────────────────────
|
|
898
|
-
//
|
|
899
|
-
// Problem: `background-image` on a <table> element is stripped by:
|
|
900
|
-
// • New Outlook Mac / Windows (Chromium-based app)
|
|
901
|
-
// • Outlook.com
|
|
902
|
-
// • Old Outlook (Word engine) — ignores CSS entirely
|
|
903
|
-
//
|
|
904
|
-
// Solution: wrap the grid in an outer <table><tr><td> where the <td> carries
|
|
905
|
-
// the background. Different clients pick it up via different mechanisms:
|
|
906
|
-
//
|
|
907
|
-
// background="" attribute on <td> → Yahoo Mail, older webmail
|
|
908
|
-
// CSS background-image on <td> → Gmail, Apple Mail, new Outlook Mac ✓
|
|
909
|
-
// VML v:rect inside the <td> → Old Outlook (Word engine) ✓
|
|
910
|
-
//
|
|
911
|
-
// The inner grid tables have NO background so the outer <td> bg shows through.
|
|
912
|
-
if (rawBgImageUrl || isGradient) {
|
|
913
|
-
const vmlFill = isGradient
|
|
914
|
-
? (() => {
|
|
915
|
-
const vmlAngle = cssAngleToVml(parsedGradient?.angle || 180);
|
|
916
|
-
const c1 = parsedGradient?.fallback || '#ffffff';
|
|
917
|
-
const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
|
|
918
|
-
return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
|
|
919
|
-
})()
|
|
920
|
-
: `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
|
|
921
|
-
html = `
|
|
922
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}" role="presentation"
|
|
923
|
-
style="border-collapse:collapse;width:${msoTableWidth}px;">
|
|
924
|
-
<tr>
|
|
925
|
-
<td width="${msoTableWidth}" bgcolor="${fallbackBgColor}" valign="top"
|
|
926
|
-
${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
|
|
927
|
-
style="
|
|
928
|
-
width:${msoTableWidth}px;
|
|
929
|
-
background-color:${fallbackBgColor};
|
|
930
|
-
${isGradient ? `background:${effectiveGradient};` : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`}
|
|
931
|
-
">
|
|
932
|
-
|
|
933
|
-
<!--[if gte mso 9]>
|
|
934
|
-
<v:rect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
935
|
-
fill="true" stroke="false"
|
|
936
|
-
style="width:${msoTableWidth}px;">
|
|
937
|
-
${vmlFill}
|
|
938
|
-
<v:textbox inset="0,0,0,0">
|
|
939
|
-
<![endif]-->
|
|
940
|
-
|
|
941
|
-
${html}
|
|
942
|
-
|
|
943
|
-
<!--[if gte mso 9]>
|
|
944
|
-
</v:textbox>
|
|
945
|
-
</v:rect>
|
|
946
|
-
<![endif]-->
|
|
947
|
-
|
|
948
|
-
</td>
|
|
949
|
-
</tr>
|
|
950
|
-
</table>`;
|
|
951
|
-
}
|
|
952
|
-
// Wrap the entire grid (including any bg-image outer table) in a div when the block
|
|
953
|
-
// has border/radius. An unconditional <div> is used — not gated behind <!--[if !mso]>-->
|
|
954
|
-
// — so Gmail compose paste renders the border-radius reliably. Old Outlook ignores
|
|
955
|
-
// border-radius on <div> but still shows the rectangular border; new Outlook works fully.
|
|
956
|
-
if (divBorderStyle)
|
|
957
|
-
html = `${divWrapOpen}${html}${divWrapClose}`;
|
|
958
|
-
return html;
|
|
959
|
-
}
|
|
960
|
-
async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
|
|
961
|
-
const { style = {}, childrenIds = [], props = {} } = blockData.data;
|
|
962
|
-
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
963
|
-
// Extract border + radius from style so they move to the div wrapper (not the <td>).
|
|
964
|
-
// Gmail strips border-radius from <td> but honours it on <div>. By putting border and
|
|
965
|
-
// radius on the same unconditional <div>, the rounded card border renders in all clients.
|
|
966
|
-
// The <td> keeps bgcolor (via attribute) for Old Outlook background fallback.
|
|
967
|
-
const { borderRadius: cellBorderRadius, borderWidth: cellBorderWidth, borderStyle: cellBorderStyleProp, borderColor: cellBorderColor, border: cellBorderShorthand, ...styleWithoutBorder } = style;
|
|
968
|
-
// backgroundColor must stay on the div wrapper (not the <td>) in two cases:
|
|
969
|
-
// 1. Cell has its own border-radius — the div's overflow:hidden clips the background.
|
|
970
|
-
// 2. Parent grid has a border div (divBorderStyle) — the grid's overflow:hidden clips it.
|
|
971
|
-
// In both cases, the rectangular <td> background bleeds through rounded corners if kept
|
|
972
|
-
// in CSS, creating visible corner squares. The bgcolor attribute stays for Outlook fallback.
|
|
973
|
-
const stripBgFromTd = Boolean(cellBorderRadius) || parentGridHasBorder;
|
|
974
|
-
const styleForTd = stripBgFromTd
|
|
975
|
-
? { ...styleWithoutBorder, backgroundColor: 'transparent' }
|
|
976
|
-
: styleWithoutBorder;
|
|
977
|
-
const styles = buildStyles(styleForTd, {
|
|
978
|
-
perChanges: [],
|
|
979
|
-
pxChanges: allPxAttributes,
|
|
980
|
-
});
|
|
981
|
-
const parts = [];
|
|
982
|
-
// OUTLOOK FIX: Calculate the actual cell width in pixels based on percentage
|
|
983
|
-
// If parent is 600px and cell is 50%, cell width should be 300px, not 600px
|
|
984
|
-
const cellWidthPx = Math.round((cellWidthPercent / 100) * parentCellWidthPx);
|
|
985
|
-
// Subtract the cell's own padding so children receive the actual content-area width.
|
|
986
|
-
// Old Outlook honours explicit img/table width attributes — if a child is sized to the
|
|
987
|
-
// full column width (ignoring padding) it overflows and expands the column.
|
|
988
|
-
const cellPad = styleWithoutBorder?.padding || {};
|
|
989
|
-
const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
|
|
990
|
-
const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
|
|
991
|
-
const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
|
|
992
|
-
// OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
|
|
993
|
-
const safeCellWidthPx = Math.min(contentWidthPx, 600);
|
|
994
|
-
for (const childId of childrenIds) {
|
|
995
|
-
const child = rootData[childId];
|
|
996
|
-
if (child) {
|
|
997
|
-
parts.push(await convertToHtml(child, rootData, safeCellWidthPx));
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
const borderRadius = cellBorderRadius || 0;
|
|
1001
|
-
const bgColor = styleWithoutBorder?.backgroundColor || "transparent";
|
|
1002
|
-
// Build border CSS for the div wrapper.
|
|
1003
|
-
// When the parent grid already has a divBorderStyle wrapper (border + border-radius +
|
|
1004
|
-
// overflow:hidden), the cell must NOT duplicate the same border/radius — that causes
|
|
1005
|
-
// two concentric borders of the same colour (double-border). The grid's wrapper div
|
|
1006
|
-
// already provides the visual container; the cell div only needs background-color.
|
|
1007
|
-
const cellDivBorderParts = [];
|
|
1008
|
-
if (!parentGridHasBorder) {
|
|
1009
|
-
if (borderRadius)
|
|
1010
|
-
cellDivBorderParts.push(`border-radius:${typeof borderRadius === 'number' ? borderRadius + 'px' : borderRadius};`, `overflow:hidden;`);
|
|
1011
|
-
if (cellBorderShorthand) {
|
|
1012
|
-
cellDivBorderParts.push(`border:${cellBorderShorthand};`);
|
|
1013
|
-
}
|
|
1014
|
-
else if (cellBorderWidth || cellBorderColor || cellBorderStyleProp) {
|
|
1015
|
-
const bw = cellBorderWidth ? (typeof cellBorderWidth === 'number' ? cellBorderWidth + 'px' : cellBorderWidth) : '1px';
|
|
1016
|
-
const bs = cellBorderStyleProp || 'solid';
|
|
1017
|
-
const bc = cellBorderColor || '#000000';
|
|
1018
|
-
cellDivBorderParts.push(`border:${bw} ${bs} ${bc};`);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
const cellDivBorderStyle = cellDivBorderParts.join(' ');
|
|
1022
|
-
// Unconditional div — visible to all clients (Gmail, Outlook new/old, Apple Mail).
|
|
1023
|
-
// background-color on the div covers modern clients; bgcolor on <td> covers Old Outlook.
|
|
1024
|
-
const divStyleParts = [`background-color:${bgColor};`];
|
|
1025
|
-
if (cellDivBorderStyle)
|
|
1026
|
-
divStyleParts.push(cellDivBorderStyle);
|
|
1027
|
-
const divStyleStr = divStyleParts.join(' ');
|
|
1028
|
-
const wrapped = `<div style="${divStyleStr}">${parts.join("")}</div>`;
|
|
1029
|
-
return {
|
|
1030
|
-
html: wrapped,
|
|
1031
|
-
styles,
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
// Enhanced Video Block HTML Conversion with centered play button
|
|
1035
|
-
async function convertVideoBlock(blockData, cellWidthInPx) {
|
|
1036
|
-
const { style, props } = blockData.data;
|
|
1037
|
-
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
1038
|
-
const { hideOnDesktop } = props; // Get the hideOnDesktop prop
|
|
1039
|
-
const { videoUrl, youtubeVideoUrl, thumbnailUrl, altText } = props;
|
|
1040
|
-
const videoLink = youtubeVideoUrl || videoUrl || "#";
|
|
1041
|
-
// via.placeholder.com is defunct — use a data-URI grey box as the default thumbnail
|
|
1042
|
-
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`;
|
|
1043
|
-
let resolvedThumbnail = thumbnailUrl || FALLBACK_THUMBNAIL;
|
|
1044
|
-
if (youtubeVideoUrl) {
|
|
1045
|
-
const youtubeId = (0, common_1.extractYouTubeId)(youtubeVideoUrl);
|
|
1046
|
-
const vimeoId = (0, common_1.extractVimeoId)(youtubeVideoUrl);
|
|
1047
|
-
if (youtubeId) {
|
|
1048
|
-
resolvedThumbnail = `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;
|
|
1049
|
-
}
|
|
1050
|
-
else if (vimeoId) {
|
|
1051
|
-
try {
|
|
1052
|
-
const res = await fetch(`https://vimeo.com/api/v2/video/${vimeoId}.json`);
|
|
1053
|
-
if (res.ok) {
|
|
1054
|
-
const data = await res.json();
|
|
1055
|
-
resolvedThumbnail = data?.[0]?.thumbnail_large || resolvedThumbnail;
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
catch (_) { }
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
// Determine width logic
|
|
1062
|
-
let percentWidth;
|
|
1063
|
-
if (typeof style?.width === "string" && style.width.trim().endsWith("%")) {
|
|
1064
|
-
percentWidth = style.width.trim();
|
|
1065
|
-
}
|
|
1066
|
-
else if (typeof style?.width === "number") {
|
|
1067
|
-
percentWidth = `${style.width}%`;
|
|
1068
|
-
}
|
|
1069
|
-
else {
|
|
1070
|
-
percentWidth = "100%";
|
|
1071
|
-
}
|
|
1072
|
-
const innerContainerWidth = (parseFloat(percentWidth) / 100) * (cellWidthInPx - (style?.padding?.left || 0) - (style?.padding?.right || 0));
|
|
1073
|
-
const aspectRatio = 16 / 9;
|
|
1074
|
-
const calculatedHeight = innerContainerWidth / aspectRatio;
|
|
1075
|
-
const outerContainerStyles = buildStyles({
|
|
1076
|
-
...style,
|
|
1077
|
-
width: undefined,
|
|
1078
|
-
borderColor: undefined,
|
|
1079
|
-
borderRadius: undefined,
|
|
1080
|
-
borderWidth: undefined,
|
|
1081
|
-
borderStyle: undefined,
|
|
1082
|
-
}, {
|
|
1083
|
-
perChanges: addPxOrPerToAttributes,
|
|
1084
|
-
pxChanges: addPxToAttributes,
|
|
1085
|
-
});
|
|
1086
|
-
const borderRadius = parseInt(style?.borderRadius) || 0;
|
|
1087
|
-
const borderWidth = parseInt(style?.borderWidth) || 0;
|
|
1088
|
-
const borderColor = style?.borderColor || "transparent";
|
|
1089
|
-
// Play icon size
|
|
1090
|
-
const playIconWidth = 65;
|
|
1091
|
-
const playIconHeight = 46;
|
|
1092
|
-
// VML centering math (for Outlook)
|
|
1093
|
-
const vmlLeft = innerContainerWidth / 2 - playIconWidth / 2;
|
|
1094
|
-
const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
|
|
1095
|
-
const shouldHideInOutlook = hideOnDesktop;
|
|
1096
|
-
const outlookVideoContent = shouldHideInOutlook
|
|
1097
|
-
? `<!--[if !mso]><!-->
|
|
1098
|
-
<v:group xmlns:v="urn:schemas-microsoft-com:vml"
|
|
1099
|
-
coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
1100
|
-
href="${videoLink}"
|
|
1101
|
-
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
1102
|
-
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
|
|
1103
|
-
${borderWidth > 0 ? `stroked="t" strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="f"`}
|
|
1104
|
-
${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
|
|
1105
|
-
>
|
|
1106
|
-
<v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
|
|
1107
|
-
</v:rect>
|
|
1108
|
-
<v:shape type="#_x0000_t75"
|
|
1109
|
-
style="position:absolute;
|
|
1110
|
-
left:${vmlLeft.toFixed(1)}px;
|
|
1111
|
-
top:${vmlTop.toFixed(1)}px;
|
|
1112
|
-
width:${playIconWidth}px;
|
|
1113
|
-
height:${playIconHeight}px;"
|
|
1114
|
-
alt="Play" href="${videoLink}" title="${altText || "Video"}"
|
|
1115
|
-
stroked="f" filled="t">
|
|
1116
|
-
<v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
|
|
1117
|
-
</v:shape>
|
|
1118
|
-
</v:group>
|
|
1119
|
-
<!--<![endif]-->`
|
|
1120
|
-
: `<!--[if mso]>
|
|
1121
|
-
<v:group xmlns:v="urn:schemas-microsoft-com:vml"
|
|
1122
|
-
coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
1123
|
-
href="${videoLink}"
|
|
1124
|
-
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
1125
|
-
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
|
|
1126
|
-
${borderWidth > 0 ? `stroked="t" strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="f"`}
|
|
1127
|
-
${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
|
|
1128
|
-
>
|
|
1129
|
-
<v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
|
|
1130
|
-
</v:rect>
|
|
1131
|
-
<v:shape type="#_x0000_t75"
|
|
1132
|
-
style="position:absolute;
|
|
1133
|
-
left:${vmlLeft.toFixed(1)}px;
|
|
1134
|
-
top:${vmlTop.toFixed(1)}px;
|
|
1135
|
-
width:${playIconWidth}px;
|
|
1136
|
-
height:${playIconHeight}px;"
|
|
1137
|
-
alt="Play" href="${videoLink}" title="${altText || "Video"}"
|
|
1138
|
-
stroked="f" filled="t">
|
|
1139
|
-
<v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
|
|
1140
|
-
</v:shape>
|
|
1141
|
-
</v:group>
|
|
1142
|
-
<![endif]-->`;
|
|
1143
|
-
// Non-Outlook: use a real <img> for the thumbnail so it renders in Gmail / Yahoo / webmail.
|
|
1144
|
-
// background-image on <table> is stripped by virtually every email client.
|
|
1145
|
-
// position:absolute for the play-button overlay is safe here because this block
|
|
1146
|
-
// is already inside <!--[if !mso]> — Outlook is handled separately via VML above.
|
|
1147
|
-
const thumbnailW = Math.round(innerContainerWidth);
|
|
1148
|
-
const thumbnailH = Math.round(calculatedHeight);
|
|
1149
|
-
const playMarginTop = -Math.round(playIconHeight / 2);
|
|
1150
|
-
const playMarginLeft = -Math.round(playIconWidth / 2);
|
|
1151
|
-
const borderAttr = borderWidth > 0 ? `border:${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};` : "border:0;";
|
|
1152
|
-
const radiusAttr = borderRadius > 0 ? `border-radius:${borderRadius}px; overflow:hidden;` : "";
|
|
1153
|
-
const nonOutlookVideoContent = `<!--[if !mso]><!-->
|
|
1154
|
-
<div style="display:block; width:100%; max-width:${thumbnailW}px; position:relative; line-height:0; font-size:0; ${borderAttr}${radiusAttr}">
|
|
1155
|
-
<a href="${videoLink}" target="_blank" style="display:block; text-decoration:none; line-height:0; font-size:0;">
|
|
1156
|
-
<img
|
|
1157
|
-
src="${resolvedThumbnail}"
|
|
1158
|
-
width="${thumbnailW}"
|
|
1159
|
-
height="${thumbnailH}"
|
|
1160
|
-
alt="${altText || "Video"}"
|
|
1161
|
-
style="display:block; width:100%; max-width:${thumbnailW}px; height:auto; border:0;"
|
|
1162
|
-
/>
|
|
1163
|
-
<img
|
|
1164
|
-
src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
|
|
1165
|
-
width="${playIconWidth}"
|
|
1166
|
-
alt="Play"
|
|
1167
|
-
style="display:block; position:absolute; top:50%; left:50%; margin-top:${playMarginTop}px; margin-left:${playMarginLeft}px; border:0; outline:none;"
|
|
1168
|
-
/>
|
|
1169
|
-
</a>
|
|
1170
|
-
</div>
|
|
1171
|
-
<!--<![endif]-->`;
|
|
1172
|
-
const videoContent = `${outlookVideoContent}${nonOutlookVideoContent}`;
|
|
1173
|
-
const wrapperHtml = `
|
|
1174
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
|
|
1175
|
-
<tr>
|
|
1176
|
-
<td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
|
|
1177
|
-
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
1178
|
-
align="${style?.textAlign || "left"}"
|
|
1179
|
-
style="
|
|
1180
|
-
margin:0;
|
|
1181
|
-
max-width:${cellWidthInPx}px;
|
|
1182
|
-
width:${percentWidth};
|
|
1183
|
-
border-collapse:collapse;
|
|
1184
|
-
">
|
|
1185
|
-
<tr>
|
|
1186
|
-
<td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
|
|
1187
|
-
${videoContent}
|
|
1188
|
-
</td>
|
|
1189
|
-
</tr>
|
|
1190
|
-
</table>
|
|
1191
|
-
</td>
|
|
1192
|
-
</tr>
|
|
1193
|
-
</table>
|
|
1194
|
-
`;
|
|
1195
|
-
return wrapperHtml;
|
|
1196
|
-
}
|
|
1197
|
-
// Enhanced Shape Block HTML Conversion using appendOutlookForShape
|
|
1198
|
-
// ---------- helpers ----------
|
|
1199
|
-
function computeArcSize(borderRadius, widthPx) {
|
|
1200
|
-
if (!borderRadius)
|
|
1201
|
-
return "0";
|
|
1202
|
-
if (typeof borderRadius === "number")
|
|
1203
|
-
return Math.min(borderRadius / widthPx, 1).toFixed(2);
|
|
1204
|
-
const s = borderRadius.toString().trim();
|
|
1205
|
-
if (s.endsWith("%")) {
|
|
1206
|
-
const pct = parseFloat(s.replace("%", "")) || 0;
|
|
1207
|
-
return Math.min(pct / 100, 1).toFixed(2);
|
|
1208
|
-
}
|
|
1209
|
-
// assume px or raw number
|
|
1210
|
-
const px = parseFloat(s.replace("px", "")) || 0;
|
|
1211
|
-
return Math.min(px / widthPx, 1).toFixed(2);
|
|
1212
|
-
}
|
|
1213
|
-
async function convertShapeBlock(blockData) {
|
|
1214
|
-
const { style, props } = blockData.data;
|
|
1215
|
-
const { shape, text, imageUrl } = props;
|
|
1216
|
-
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
1217
|
-
const { width = "100", height = "150", padding = {}, backgroundColor = "#2F80ED", borderRadius, borderWidth = 0, borderStyle = "solid", borderColor = "transparent", customCss, shapeColor, alignment = "left", msoBakeImageWithText, color = "#000000", fontSize = 14, textAlign = "center", verticalAlign = "middle", } = style || {};
|
|
1218
|
-
const borderRadiusMap = {
|
|
1219
|
-
rectangle: "0",
|
|
1220
|
-
rounded: "10px",
|
|
1221
|
-
circle: "50%",
|
|
1222
|
-
oval: "50%",
|
|
1223
|
-
};
|
|
1224
|
-
let resolvedBorderRadius = borderRadius || borderRadiusMap[shape] || "0";
|
|
1225
|
-
let resolvedWidthPx = typeof width === "number"
|
|
1226
|
-
? width
|
|
1227
|
-
: parseInt(width.toString().replace("px", ""), 10) || 100;
|
|
1228
|
-
let resolvedHeightPx = typeof height === "number"
|
|
1229
|
-
? height
|
|
1230
|
-
: parseInt(height.toString().replace("px", ""), 10) || 150;
|
|
1231
|
-
// --- Shape-specific constraints ---
|
|
1232
|
-
if (shape === "circle") {
|
|
1233
|
-
const side = Math.min(resolvedWidthPx, resolvedHeightPx);
|
|
1234
|
-
resolvedWidthPx = side;
|
|
1235
|
-
resolvedHeightPx = side;
|
|
1236
|
-
resolvedBorderRadius = "50%";
|
|
1237
|
-
}
|
|
1238
|
-
else if (shape === "oval") {
|
|
1239
|
-
resolvedBorderRadius = "50% / 50%";
|
|
1240
|
-
}
|
|
1241
|
-
const finalBackgroundColor = shapeColor || backgroundColor;
|
|
1242
|
-
// --- Horizontal alignment for outer container ---
|
|
1243
|
-
const alignmentStyles = {
|
|
1244
|
-
left: "margin-right:auto;margin-left:0;",
|
|
1245
|
-
center: "margin-left:auto;margin-right:auto;",
|
|
1246
|
-
right: "margin-left:auto;margin-right:0;",
|
|
1247
|
-
};
|
|
1248
|
-
const alignmentStyle = alignmentStyles[alignment] || "";
|
|
1249
|
-
// --- Text + vertical alignment maps ---
|
|
1250
|
-
const textAlignMap = {
|
|
1251
|
-
left: "left",
|
|
1252
|
-
center: "center",
|
|
1253
|
-
right: "right",
|
|
1254
|
-
justify: "justify",
|
|
1255
|
-
};
|
|
1256
|
-
const textAlignStyle = textAlignMap[textAlign] || "center";
|
|
1257
|
-
// --- Text styling ---
|
|
1258
|
-
const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;color:${color};`;
|
|
1259
|
-
// ============================
|
|
1260
|
-
// Modern HTML (non-MSO)
|
|
1261
|
-
// ============================
|
|
1262
|
-
let nonMsoContent = "";
|
|
1263
|
-
// --- Case 1: Image + Text ---
|
|
1264
|
-
if (imageUrl && text) {
|
|
1265
|
-
nonMsoContent = `
|
|
1266
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1267
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1268
|
-
border-radius:${resolvedBorderRadius};
|
|
1269
|
-
background-color:${finalBackgroundColor};
|
|
1270
|
-
background-image:url('${imageUrl}');
|
|
1271
|
-
background-position:center center;
|
|
1272
|
-
background-size:cover;
|
|
1273
|
-
background-repeat:no-repeat;
|
|
1274
|
-
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1275
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
|
|
1276
|
-
style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
|
|
1277
|
-
<tr>
|
|
1278
|
-
<td align="${textAlignStyle}" valign="${verticalAlign}"
|
|
1279
|
-
width="${resolvedWidthPx}" height="${resolvedHeightPx}"
|
|
1280
|
-
style="padding:6px;vertical-align:${verticalAlign};text-align:${textAlignStyle};overflow:hidden;box-sizing:border-box;">
|
|
1281
|
-
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text}</div>
|
|
1282
|
-
</td>
|
|
1283
|
-
</tr>
|
|
1284
|
-
</table>
|
|
1285
|
-
</div>`;
|
|
1286
|
-
}
|
|
1287
|
-
// --- Case 2: Image only ---
|
|
1288
|
-
else if (imageUrl) {
|
|
1289
|
-
nonMsoContent = `
|
|
1290
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1291
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1292
|
-
border-radius:${resolvedBorderRadius};
|
|
1293
|
-
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1294
|
-
<img src="${imageUrl}" alt="${text || "shape image"}"
|
|
1295
|
-
width="${resolvedWidthPx}" height="${resolvedHeightPx}"
|
|
1296
|
-
style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
|
|
1297
|
-
</div>`;
|
|
1298
|
-
}
|
|
1299
|
-
// --- Case 3: Text only ---
|
|
1300
|
-
else {
|
|
1301
|
-
nonMsoContent = `
|
|
1302
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1303
|
-
background-color:${finalBackgroundColor};
|
|
1304
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1305
|
-
border-radius:${resolvedBorderRadius};
|
|
1306
|
-
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1307
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
|
|
1308
|
-
style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
|
|
1309
|
-
<tr>
|
|
1310
|
-
<td align="${textAlignStyle}" valign="${verticalAlign}"
|
|
1311
|
-
width="${resolvedWidthPx}" height="${resolvedHeightPx}"
|
|
1312
|
-
style="padding:8px;vertical-align:${verticalAlign};text-align:${textAlignStyle};box-sizing:border-box;">
|
|
1313
|
-
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text || ""}</div>
|
|
1314
|
-
</td>
|
|
1315
|
-
</tr>
|
|
1316
|
-
</table>
|
|
1317
|
-
</div>`;
|
|
1318
|
-
}
|
|
1319
|
-
// Outlook (VML) fallback
|
|
1320
|
-
const outlookContent = appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
|
|
1321
|
-
shape,
|
|
1322
|
-
imageUrl,
|
|
1323
|
-
backgroundColor,
|
|
1324
|
-
shapeColor,
|
|
1325
|
-
borderWidth,
|
|
1326
|
-
borderColor,
|
|
1327
|
-
borderRadius: resolvedBorderRadius,
|
|
1328
|
-
heightPx: resolvedHeightPx,
|
|
1329
|
-
text,
|
|
1330
|
-
textColor: color,
|
|
1331
|
-
textSize: fontSize,
|
|
1332
|
-
verticalAlign,
|
|
1333
|
-
textAlign, // ✅ added
|
|
1334
|
-
alignment,
|
|
1335
|
-
padding,
|
|
1336
|
-
msoBakeImageWithText,
|
|
1337
|
-
}, visibilityClass);
|
|
1338
|
-
// Embed block metadata so the HTML importer can reconstruct the Shape block exactly.
|
|
1339
|
-
const shapeProps = encodeBlockProps({
|
|
1340
|
-
shape,
|
|
1341
|
-
width: resolvedWidthPx,
|
|
1342
|
-
height: resolvedHeightPx,
|
|
1343
|
-
shapeColor: String(finalBackgroundColor || '#BEBEBE'),
|
|
1344
|
-
backgroundColor: String(finalBackgroundColor || '#BEBEBE'),
|
|
1345
|
-
borderRadius: borderRadius !== undefined ? borderRadius : 0,
|
|
1346
|
-
borderWidth: borderWidth || 0,
|
|
1347
|
-
borderColor: borderColor || 'transparent',
|
|
1348
|
-
borderStyle: borderStyle || 'solid',
|
|
1349
|
-
imageUrl: imageUrl || '',
|
|
1350
|
-
text: text || '',
|
|
1351
|
-
color: String(color || '#000000'),
|
|
1352
|
-
fontSize: fontSize || 14,
|
|
1353
|
-
textAlign: textAlignStyle,
|
|
1354
|
-
verticalAlign: verticalAlign || 'middle',
|
|
1355
|
-
alignment: alignment || 'left',
|
|
1356
|
-
padding: {
|
|
1357
|
-
top: padding.top || 0,
|
|
1358
|
-
right: padding.right || 0,
|
|
1359
|
-
bottom: padding.bottom || 0,
|
|
1360
|
-
left: padding.left || 0,
|
|
1361
|
-
},
|
|
1362
|
-
hideOnDesktop: Boolean(props.hideOnDesktop),
|
|
1363
|
-
hideOnMobile: Boolean(props.hideOnMobile),
|
|
1364
|
-
customCss: customCss || '',
|
|
1365
|
-
});
|
|
1366
|
-
// Combine into table wrapper
|
|
1367
|
-
return `
|
|
1368
|
-
<table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}" data-block-type="shape" data-block-props="${shapeProps}">
|
|
1369
|
-
<tr>
|
|
1370
|
-
<td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
|
|
1371
|
-
${outlookContent}
|
|
1372
|
-
<!--[if !mso]><!-->
|
|
1373
|
-
${nonMsoContent}
|
|
1374
|
-
<!--<![endif]-->
|
|
1375
|
-
</td>
|
|
1376
|
-
</tr>
|
|
1377
|
-
</table>`;
|
|
1378
|
-
}
|
|
1379
|
-
// ---------- Updated VML builder ----------
|
|
1380
|
-
function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor = "#000000", textSize = 14, verticalAlign = "middle", textAlign = "center", msoHasBakedText = false, }) {
|
|
1381
|
-
const bw = borderWidth || 0;
|
|
1382
|
-
const bc = borderColor || "transparent";
|
|
1383
|
-
const borderAttrs = bw > 0 ? `strokeweight="${bw}px" strokecolor="${bc}"` : `stroked="false"`;
|
|
1384
|
-
const fillColor = backgroundColor || "#2F80ED";
|
|
1385
|
-
// Use frame for img fill so sizing is preserved
|
|
1386
|
-
const fillMarkup = `<v:fill ${imageUrl ? `src="${imageUrl}" type="frame" aspect="atleast"` : ""} color="${fillColor}" />`;
|
|
1387
|
-
let tag = "rect";
|
|
1388
|
-
let extraAttr = "";
|
|
1389
|
-
if (shape === "circle" || shape === "oval") {
|
|
1390
|
-
tag = "oval";
|
|
1391
|
-
}
|
|
1392
|
-
else if (shape === "rounded") {
|
|
1393
|
-
tag = "roundrect";
|
|
1394
|
-
extraAttr = `arcsize="${computeArcSize(borderRadius, widthPx)}"`;
|
|
1395
|
-
}
|
|
1396
|
-
// maps for vml
|
|
1397
|
-
const vAlignMap = { top: "top", middle: "middle", bottom: "bottom" };
|
|
1398
|
-
const hAlignMap = {
|
|
1399
|
-
left: "left",
|
|
1400
|
-
center: "center",
|
|
1401
|
-
right: "right",
|
|
1402
|
-
justify: "left",
|
|
1403
|
-
}; // justify -> left fallback in VML
|
|
1404
|
-
const vAlign = vAlignMap[verticalAlign] || "middle";
|
|
1405
|
-
const hAlign = hAlignMap[textAlign] || "center";
|
|
1406
|
-
const safeFontSize = Math.max(Math.round(textSize), 10);
|
|
1407
|
-
// Build the textbox with table/cell for reliable vertical centering in Outlook
|
|
1408
|
-
const textboxMarkup = text && !msoHasBakedText
|
|
1409
|
-
? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
|
|
1410
|
-
<div style="display:table;width:100%;height:100%;">
|
|
1411
|
-
<div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
|
|
1412
|
-
<div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
|
|
1413
|
-
${text}
|
|
1414
|
-
</div>
|
|
1415
|
-
</div>
|
|
1416
|
-
</div>
|
|
1417
|
-
</v:textbox>`
|
|
1418
|
-
: `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
|
|
1419
|
-
// Return VML shape
|
|
1420
|
-
return `
|
|
1421
|
-
<v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
|
|
1422
|
-
style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
|
|
1423
|
-
${borderAttrs}
|
|
1424
|
-
fill="true" fillcolor="${fillColor}"${extraAttr}>
|
|
1425
|
-
${fillMarkup}
|
|
1426
|
-
${textboxMarkup}
|
|
1427
|
-
</v:${tag}>`;
|
|
1428
|
-
}
|
|
1429
|
-
function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts, visibilityClass) {
|
|
1430
|
-
const widthPx = Math.round(Math.min(outerContainerWidth, innerContainerWidth));
|
|
1431
|
-
const heightPx = Math.max(1, Math.round(opts.heightPx));
|
|
1432
|
-
const vml = buildVMLShape({
|
|
1433
|
-
shape: opts.shape,
|
|
1434
|
-
widthPx,
|
|
1435
|
-
heightPx,
|
|
1436
|
-
imageUrl: opts.msoBakeImageWithText || opts.imageUrl,
|
|
1437
|
-
backgroundColor: opts.shapeColor || opts.backgroundColor,
|
|
1438
|
-
borderWidth: opts.borderWidth,
|
|
1439
|
-
borderColor: opts.borderColor,
|
|
1440
|
-
borderRadius: opts.borderRadius,
|
|
1441
|
-
text: opts.text,
|
|
1442
|
-
textColor: opts.textColor,
|
|
1443
|
-
textSize: opts.textSize,
|
|
1444
|
-
verticalAlign: opts.verticalAlign,
|
|
1445
|
-
textAlign: opts.textAlign,
|
|
1446
|
-
msoHasBakedText: Boolean(opts.msoBakeImageWithText),
|
|
1447
|
-
});
|
|
1448
|
-
const pad = opts.padding || {};
|
|
1449
|
-
const align = opts.alignment || "left";
|
|
1450
|
-
const valign = opts.verticalAlign || "middle";
|
|
1451
|
-
const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
|
|
1452
|
-
// Fix: Properly handle Outlook visibility with conditional comments
|
|
1453
|
-
if (shouldHideInOutlook) {
|
|
1454
|
-
return `<!--[if !mso]><!-->
|
|
1455
|
-
<table align="${align}" border="0" cellpadding="0" cellspacing="0"
|
|
1456
|
-
style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
|
|
1457
|
-
<tr>
|
|
1458
|
-
<td valign="${valign}"
|
|
1459
|
-
style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
|
|
1460
|
-
${vml}
|
|
1461
|
-
</td>
|
|
1462
|
-
</tr>
|
|
1463
|
-
</table>
|
|
1464
|
-
<!--<![endif]-->`;
|
|
1465
|
-
}
|
|
1466
|
-
return `<!--[if mso]>
|
|
1467
|
-
<table align="${align}" border="0" cellpadding="0" cellspacing="0"
|
|
1468
|
-
style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
|
|
1469
|
-
<tr>
|
|
1470
|
-
<td valign="${valign}"
|
|
1471
|
-
style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
|
|
1472
|
-
${vml}
|
|
1473
|
-
</td>
|
|
1474
|
-
</tr>
|
|
1475
|
-
</table>
|
|
1476
|
-
<![endif]-->`;
|
|
1477
|
-
}
|
|
1478
|
-
function convertVerticalDividerBlockToHtml(blockData) {
|
|
1479
|
-
const { style, props } = blockData.data;
|
|
1480
|
-
const { width, height, dividerColor, padding, backgroundColor } = style;
|
|
1481
|
-
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
1482
|
-
const outerStyles = buildStyles({ padding, backgroundColor }, {
|
|
1483
|
-
perChanges: [],
|
|
1484
|
-
pxChanges: allPxAttributes,
|
|
1485
|
-
});
|
|
1486
|
-
const vDividerProps = encodeBlockProps({
|
|
1487
|
-
width: width || 5,
|
|
1488
|
-
height: height || 100,
|
|
1489
|
-
dividerColor: dividerColor || '#808080',
|
|
1490
|
-
backgroundColor: backgroundColor || '',
|
|
1491
|
-
alignment: 'left',
|
|
1492
|
-
padding: padding || { top: 0, right: 0, bottom: 0, left: 0 },
|
|
1493
|
-
hideOnDesktop: Boolean(props.hideOnDesktop),
|
|
1494
|
-
hideOnMobile: Boolean(props.hideOnMobile),
|
|
1495
|
-
});
|
|
1496
|
-
return `
|
|
1497
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
1498
|
-
style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}" data-block-type="vdivider" data-block-props="${vDividerProps}">
|
|
1499
|
-
<tr>
|
|
1500
|
-
<td style="${outerStyles}; text-align:center; vertical-align:middle;">
|
|
1501
|
-
<!--[if mso | IE]>
|
|
1502
|
-
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="${dividerColor}" style="width:${width}px;height:${height}px;" stroke="f"></v:rect>
|
|
1503
|
-
<![endif]-->
|
|
1504
|
-
<!--[if !mso]><!-->
|
|
1505
|
-
<div style="display:inline-block;width:${width}px;height:${height}px;background:${dividerColor};line-height:0;font-size:0;"> </div>
|
|
1506
|
-
<!--<![endif]-->
|
|
1507
|
-
</td>
|
|
1508
|
-
</tr>
|
|
1509
|
-
</table>`;
|
|
1510
|
-
}
|