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