email-builder-utils 1.1.42 → 1.1.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils/convertJsonToHtml.d.ts.map +1 -1
- package/dist/utils/convertJsonToHtml.js +90 -83
- package/dist/utils/jsonToHTML.d.ts +1 -1
- package/dist/utils/jsonToHTML.d.ts.map +1 -1
- package/dist/utils/jsonToHTML.js +701 -500
- package/dist/utils/outlookSupport.d.ts +210 -0
- package/dist/utils/outlookSupport.d.ts.map +1 -0
- package/dist/utils/outlookSupport.js +479 -0
- package/package.json +34 -33
package/dist/utils/jsonToHTML.js
CHANGED
|
@@ -14,7 +14,12 @@ const addPxToAttributes = [
|
|
|
14
14
|
];
|
|
15
15
|
const addPxOrPerToAttributes = ["width", "height"];
|
|
16
16
|
const allPxAttributes = [...addPxToAttributes, ...addPxOrPerToAttributes];
|
|
17
|
-
exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed
|
|
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
|
+
}
|
|
18
23
|
async function loadImageNaturalDimensions(imageUrl) {
|
|
19
24
|
return new Promise((resolve, reject) => {
|
|
20
25
|
const img = new Image();
|
|
@@ -23,6 +28,38 @@ async function loadImageNaturalDimensions(imageUrl) {
|
|
|
23
28
|
img.src = imageUrl;
|
|
24
29
|
});
|
|
25
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
|
+
}
|
|
26
63
|
function buildStyles(style, { pxChanges, perChanges }) {
|
|
27
64
|
if (!style)
|
|
28
65
|
style = {};
|
|
@@ -58,13 +95,13 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
58
95
|
};
|
|
59
96
|
value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
|
|
60
97
|
}
|
|
61
|
-
// Sanitize fontFamily: replace double quotes with single quotes to avoid
|
|
62
|
-
// breaking the surrounding style="..." HTML attribute
|
|
63
98
|
if (key === "fontFamily" && typeof value === "string") {
|
|
64
|
-
value = (0, fontFallback_1.withFontFallback)(value)
|
|
99
|
+
value = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(value));
|
|
65
100
|
}
|
|
66
|
-
// Wrap backgroundImage values in url() if not already wrapped
|
|
67
|
-
if (key === "backgroundImage" && typeof value === "string"
|
|
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(")) {
|
|
68
105
|
value = `url('${value}')`;
|
|
69
106
|
}
|
|
70
107
|
// lineHeight: values >= 4 are pixel values; smaller values are unitless multipliers (e.g. 1.5)
|
|
@@ -104,7 +141,7 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
104
141
|
.map(([k, v]) => `${k}:${v}`);
|
|
105
142
|
if (style.customCss)
|
|
106
143
|
parts.push(style.customCss);
|
|
107
|
-
return parts.join('; ').trim();
|
|
144
|
+
return parts.join('; ').replace(/;\s*$/, '').trim();
|
|
108
145
|
}
|
|
109
146
|
async function convertToHtml(blockData, rootData, cellWidthInPx) {
|
|
110
147
|
switch (blockData.type) {
|
|
@@ -134,43 +171,48 @@ function appendOutlookSupport(content, contentStyle, className, msoWidth) {
|
|
|
134
171
|
const visibilityClass = className || "";
|
|
135
172
|
const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
|
|
136
173
|
if (shouldHideInOutlook) {
|
|
137
|
-
return `
|
|
138
|
-
<!--[if !mso]><!-->
|
|
139
|
-
<table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
|
|
140
|
-
<!--<![endif]-->
|
|
174
|
+
return `
|
|
175
|
+
<!--[if !mso]><!-->
|
|
176
|
+
<table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
|
|
177
|
+
<!--<![endif]-->
|
|
141
178
|
`;
|
|
142
179
|
}
|
|
143
180
|
// When an explicit pixel width is provided (e.g. inside a column cell), use dual MSO/non-MSO
|
|
144
181
|
// tables. Old Outlook (Word engine) ignores max-width and can resolve width="100%" to the
|
|
145
182
|
// full email width (600px) rather than the column width, causing images/buttons to expand.
|
|
146
183
|
if (msoWidth) {
|
|
147
|
-
return `
|
|
148
|
-
<!--[if mso]>
|
|
149
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" style="border-collapse:collapse;width:${msoWidth}px;"><tr><td width="${msoWidth}" style="${contentStyle}">
|
|
150
|
-
<![endif]-->
|
|
151
|
-
<!--[if !mso]><!-->
|
|
152
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">
|
|
153
|
-
<!--<![endif]-->
|
|
154
|
-
${content}
|
|
155
|
-
<!--[if mso]></td></tr></table><![endif]-->
|
|
156
|
-
<!--[if !mso]><!-->
|
|
157
|
-
</td></tr></table>
|
|
158
|
-
<!--<![endif]-->
|
|
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]-->
|
|
159
196
|
`;
|
|
160
197
|
}
|
|
161
|
-
return `
|
|
162
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">${content}</td></tr></table>
|
|
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>
|
|
163
200
|
`;
|
|
164
201
|
}
|
|
165
202
|
function convertDividerBlockToHtml(blockData) {
|
|
166
203
|
const { style, props } = blockData.data;
|
|
167
204
|
const { hideOnMobile, hideOnDesktop } = props;
|
|
168
|
-
const { thickness, dividerColor, width, ...rest } = style;
|
|
205
|
+
const { thickness, dividerColor, width, alignment, ...rest } = style;
|
|
169
206
|
const convertedStyle = buildStyles(rest, {
|
|
170
207
|
perChanges: [],
|
|
171
208
|
pxChanges: allPxAttributes,
|
|
172
209
|
});
|
|
173
|
-
const dividerWidth = width ||
|
|
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};`;
|
|
174
216
|
// Build class name based on visibility
|
|
175
217
|
const visibilityClass = [
|
|
176
218
|
hideOnMobile ? "hide-mobile" : "",
|
|
@@ -178,23 +220,24 @@ function convertDividerBlockToHtml(blockData) {
|
|
|
178
220
|
]
|
|
179
221
|
.filter(Boolean)
|
|
180
222
|
.join(" ");
|
|
181
|
-
const dividerContent = `
|
|
182
|
-
<table
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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>
|
|
196
239
|
`;
|
|
197
|
-
return appendOutlookSupport(dividerContent,
|
|
240
|
+
return appendOutlookSupport(dividerContent, contentStyle, visibilityClass);
|
|
198
241
|
}
|
|
199
242
|
function convertSpacerBlockToHtml(blockData) {
|
|
200
243
|
const { style, props } = blockData.data;
|
|
@@ -237,13 +280,28 @@ function convertTextBlock(blockData, cellWidthInPx) {
|
|
|
237
280
|
.replace(/<\/p>/gi, "</div>");
|
|
238
281
|
const navigateToUrl = props.navigateToUrl || "";
|
|
239
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};` : '';
|
|
240
297
|
// Use display:block + width:100% so text fills the column naturally.
|
|
241
298
|
// display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
|
|
242
|
-
const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${fontSizeStyle}${convertedTextStyle}">${
|
|
299
|
+
const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
|
|
243
300
|
const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
|
|
244
301
|
const textContent = appendOutlookSupport(convertedTextBox, styles, visibilityClass, safeCellWidth);
|
|
302
|
+
const linkColorStyle = blockTextColor ? `color:${blockTextColor};` : 'color:inherit;';
|
|
245
303
|
return navigateToUrl
|
|
246
|
-
? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="
|
|
304
|
+
? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${textContent}</a>`
|
|
247
305
|
: textContent;
|
|
248
306
|
}
|
|
249
307
|
async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
|
|
@@ -287,21 +345,21 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
287
345
|
if (useRoundRect && borderRadius > 0) {
|
|
288
346
|
// Use VML for border radius - wrap in table to constrain width for Old Outlook (Word engine)
|
|
289
347
|
// Use aspect="atmost" to prevent image from stretching beyond its bounds
|
|
290
|
-
outlookImage = `<!--[if mso]>
|
|
291
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
292
|
-
<tr>
|
|
293
|
-
<td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
294
|
-
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
295
|
-
style="width:${vmlWidth}px;height:${vmlHeight}px;"
|
|
296
|
-
${borderAttributes}
|
|
297
|
-
arcsize="${arcsize}"
|
|
298
|
-
fill="true" fillcolor="none">
|
|
299
|
-
<v:fill src="${imageUrl}" type="tile" aspect="atmost" />
|
|
300
|
-
<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>
|
|
301
|
-
</v:roundrect>
|
|
302
|
-
</td>
|
|
303
|
-
</tr>
|
|
304
|
-
</table>
|
|
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>
|
|
305
363
|
<![endif]-->`;
|
|
306
364
|
}
|
|
307
365
|
else {
|
|
@@ -310,21 +368,21 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
310
368
|
const borderStyleAttr = borderWidth > 0
|
|
311
369
|
? `border: ${borderWidth}px solid ${borderColor};`
|
|
312
370
|
: '';
|
|
313
|
-
outlookImage = `<!--[if mso]>
|
|
314
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
315
|
-
<tr>
|
|
316
|
-
<td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
317
|
-
<img src="${imageUrl}" alt="Image" border="0" width="${vmlWidth}" height="${vmlHeight}" style="display:block; width:${vmlWidth}px; height:${vmlHeight}px; max-width:${vmlWidth}px; ${borderStyleAttr}" />
|
|
318
|
-
</td>
|
|
319
|
-
</tr>
|
|
320
|
-
</table>
|
|
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>
|
|
321
379
|
<![endif]-->`;
|
|
322
380
|
}
|
|
323
|
-
return `
|
|
324
|
-
${outlookImage}
|
|
325
|
-
<!--[if !mso]><!-->
|
|
326
|
-
${content}
|
|
327
|
-
<!--<![endif]-->
|
|
381
|
+
return `
|
|
382
|
+
${outlookImage}
|
|
383
|
+
<!--[if !mso]><!-->
|
|
384
|
+
${content}
|
|
385
|
+
<!--<![endif]-->
|
|
328
386
|
`;
|
|
329
387
|
}
|
|
330
388
|
async function computeScaledDimensions(imageUrl, maxContainerWidthPx) {
|
|
@@ -430,56 +488,56 @@ function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
|
|
|
430
488
|
const borderStyle = buttonStyle.borderStyle || "solid";
|
|
431
489
|
const bgColor = buttonStyle.buttonColor || "transparent";
|
|
432
490
|
const color = buttonStyle.color || "#ffffff";
|
|
433
|
-
const fontFamily = (0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily)
|
|
491
|
+
const fontFamily = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily));
|
|
434
492
|
const fontWeight = buttonStyle.fontWeight || 400;
|
|
435
493
|
const width = typeof buttonStyle.width === "number"
|
|
436
494
|
? `width="${buttonStyle.width}"`
|
|
437
495
|
: "";
|
|
438
|
-
return `<!--[if mso]>
|
|
439
|
-
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="display:inline-table;">
|
|
440
|
-
<tr>
|
|
441
|
-
<td align="center"
|
|
442
|
-
valign="middle"
|
|
443
|
-
${width}
|
|
444
|
-
${finalHeight ? `height="${finalHeight}"` : ""}
|
|
445
|
-
bgcolor="${bgColor}"
|
|
446
|
-
style="
|
|
447
|
-
${finalHeight ? `height:${finalHeight}px;` : ""}
|
|
448
|
-
background-color:${bgColor};
|
|
449
|
-
border-radius:${borderRadius}px;
|
|
450
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
451
|
-
overflow:hidden;
|
|
452
|
-
mso-line-height-rule:exactly;
|
|
453
|
-
">
|
|
454
|
-
|
|
455
|
-
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
456
|
-
<tr>
|
|
457
|
-
<td align="center" valign="middle"
|
|
458
|
-
style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
|
|
459
|
-
|
|
460
|
-
<a href="${navigateToUrl}"
|
|
461
|
-
style="
|
|
462
|
-
display:inline-block;
|
|
463
|
-
color:${color};
|
|
464
|
-
text-decoration:none;
|
|
465
|
-
font-family:${fontFamily};
|
|
466
|
-
font-size:${fontSize}px;
|
|
467
|
-
font-weight:${fontWeight};
|
|
468
|
-
line-height:normal;
|
|
469
|
-
">
|
|
470
|
-
${text}
|
|
471
|
-
</a>
|
|
472
|
-
|
|
473
|
-
</td>
|
|
474
|
-
</tr>
|
|
475
|
-
</table>
|
|
476
|
-
|
|
477
|
-
</td>
|
|
478
|
-
</tr>
|
|
479
|
-
</table>
|
|
480
|
-
<![endif]-->
|
|
481
|
-
<!--[if !mso]><!-->
|
|
482
|
-
${content}
|
|
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}
|
|
483
541
|
<!--<![endif]-->`;
|
|
484
542
|
}
|
|
485
543
|
function convertButtonBlock(blockData) {
|
|
@@ -497,7 +555,7 @@ function convertButtonBlock(blockData) {
|
|
|
497
555
|
const finalHeight = typeof height === "number" && height > 0
|
|
498
556
|
? Math.max(height, minHeight)
|
|
499
557
|
: null;
|
|
500
|
-
const safeFF = (0, fontFallback_1.withFontFallback)(fontFamily)
|
|
558
|
+
const safeFF = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(fontFamily));
|
|
501
559
|
const safeColor = color || "#ffffff";
|
|
502
560
|
const bgColor = buttonColor || "transparent";
|
|
503
561
|
const bdColor = borderColor || "transparent";
|
|
@@ -509,89 +567,139 @@ function convertButtonBlock(blockData) {
|
|
|
509
567
|
? `width="${width}"`
|
|
510
568
|
: "";
|
|
511
569
|
// ✅ FIX: no width=100% anywhere
|
|
512
|
-
const buttonTable = `
|
|
513
|
-
<table role="presentation" cellspacing="0" cellpadding="0" border="0"
|
|
514
|
-
style="display:inline-table; border-collapse:separate;"
|
|
515
|
-
${widthAttr}>
|
|
516
|
-
<tr>
|
|
517
|
-
<td
|
|
518
|
-
align="center"
|
|
519
|
-
valign="middle"
|
|
520
|
-
${finalHeight ? `height="${finalHeight}"` : ""}
|
|
521
|
-
style="
|
|
522
|
-
${finalHeight ? `height:${finalHeight}px;` : ""}
|
|
523
|
-
background-color:${bgColor};
|
|
524
|
-
border-radius:${br}px;
|
|
525
|
-
border:${bw}px ${bdStyle} ${bdColor};
|
|
526
|
-
overflow:hidden;
|
|
527
|
-
mso-line-height-rule:exactly;
|
|
528
|
-
"
|
|
529
|
-
>
|
|
530
|
-
|
|
531
|
-
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
532
|
-
<tr>
|
|
533
|
-
<td align="center" valign="middle"
|
|
534
|
-
style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
|
|
535
|
-
|
|
536
|
-
<a href="${navigateToUrl}"
|
|
537
|
-
style="
|
|
538
|
-
display:inline-block;
|
|
539
|
-
color:${safeColor};
|
|
540
|
-
text-decoration:none;
|
|
541
|
-
font-family:${safeFF};
|
|
542
|
-
font-size:${fs}px;
|
|
543
|
-
font-weight:${fontWeight || 400};
|
|
544
|
-
line-height:normal;
|
|
545
|
-
white-space:nowrap;
|
|
546
|
-
">
|
|
547
|
-
${text}
|
|
548
|
-
</a>
|
|
549
|
-
|
|
550
|
-
</td>
|
|
551
|
-
</tr>
|
|
552
|
-
</table>
|
|
553
|
-
|
|
554
|
-
</td>
|
|
555
|
-
</tr>
|
|
556
|
-
</table>
|
|
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>
|
|
557
615
|
`;
|
|
558
616
|
const aligned = containerAlign === "center"
|
|
559
617
|
? `<center>${buttonTable}</center>`
|
|
560
618
|
: `<div style="text-align:${containerAlign};">${buttonTable}</div>`;
|
|
561
619
|
const buttonWithOutlook = appendOutlookForButton(aligned, style, navigateToUrl, text);
|
|
562
|
-
return `
|
|
563
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
|
564
|
-
<tr>
|
|
565
|
-
<td align="${containerAlign}"
|
|
566
|
-
style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;
|
|
567
|
-
background-color:${containerBg || "transparent"};">
|
|
568
|
-
${buttonWithOutlook}
|
|
569
|
-
</td>
|
|
570
|
-
</tr>
|
|
571
|
-
</table>
|
|
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>
|
|
572
630
|
`;
|
|
573
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
|
+
}
|
|
574
656
|
/**
|
|
575
657
|
* Extract the first solid-color stop from a CSS gradient in customCss.
|
|
576
|
-
* Used as a MSO/Outlook bgcolor fallback
|
|
577
|
-
* but the block has a gradient in customCss (e.g. linear-gradient imports).
|
|
658
|
+
* Used as a MSO/Outlook bgcolor fallback. Handles hex, rgb, and named colors.
|
|
578
659
|
*/
|
|
579
660
|
function extractCssFallbackColor(customCss) {
|
|
580
661
|
if (!customCss)
|
|
581
662
|
return '';
|
|
582
|
-
const
|
|
583
|
-
|
|
663
|
+
const gradientMatch = customCss.match(/(?:linear|radial|conic)-gradient\(([^)]+(?:\([^)]*\)[^)]*)*)\)/);
|
|
664
|
+
if (!gradientMatch)
|
|
665
|
+
return '';
|
|
666
|
+
return firstGradientColor(gradientMatch[1]);
|
|
584
667
|
}
|
|
585
668
|
function parseGradient(gradient) {
|
|
586
669
|
if (!gradient)
|
|
587
670
|
return null;
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
const
|
|
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
|
+
}
|
|
591
699
|
return {
|
|
592
700
|
angle,
|
|
593
701
|
colors,
|
|
594
|
-
fallback: colors[0] ||
|
|
702
|
+
fallback: colors[0] || '#ffffff',
|
|
595
703
|
};
|
|
596
704
|
}
|
|
597
705
|
function cssAngleToVml(angle) {
|
|
@@ -602,18 +710,56 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
602
710
|
const { columns = 1, cellWidths = [], responsive = true } = props;
|
|
603
711
|
const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
|
|
604
712
|
const gridVisibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
const
|
|
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;
|
|
608
726
|
const fallbackBgColor = backgroundColor ||
|
|
609
727
|
parsedGradient?.fallback ||
|
|
610
|
-
extractCssFallbackColor(
|
|
728
|
+
extractCssFallbackColor(customCssStr) ||
|
|
611
729
|
"#ffffff";
|
|
612
|
-
const rawBgImageUrl = !isGradient &&
|
|
613
|
-
?
|
|
730
|
+
const rawBgImageUrl = !isGradient && bgImageStr
|
|
731
|
+
? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
|
|
614
732
|
: null;
|
|
615
|
-
//
|
|
616
|
-
|
|
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 }, {
|
|
617
763
|
perChanges: [],
|
|
618
764
|
pxChanges: allPxAttributes,
|
|
619
765
|
});
|
|
@@ -621,48 +767,39 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
621
767
|
const visualRows = Math.ceil(total / columns);
|
|
622
768
|
// OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
|
|
623
769
|
const msoTableWidth = Math.min(cellWidthInPx, 600);
|
|
624
|
-
// When a background image is present, the background is applied on an outer
|
|
625
|
-
// wrapper <td> (see bottom of function). The inner grid tables must be clean
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
? (backgroundColor || extractCssFallbackColor(restStyle.customCss))
|
|
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 || '')
|
|
630
775
|
: '';
|
|
631
776
|
const msoBgAttr = msoBgColor ? ` bgcolor="${msoBgColor}"` : '';
|
|
632
777
|
const msoBgStyle = msoBgColor ? `background-color:${msoBgColor};` : '';
|
|
633
|
-
// Inner
|
|
634
|
-
// wrapper is used. background-position/repeat/size without a background-image
|
|
635
|
-
// are harmless, but keeping them in the inner table style is confusing and
|
|
636
|
-
// could conflict with email client default styles.
|
|
637
|
-
const innerNonMsoStyle = rawBgImageUrl
|
|
638
|
-
? buildStyles({
|
|
639
|
-
...restStyle,
|
|
640
|
-
customCss: '',
|
|
641
|
-
backgroundSize: undefined,
|
|
642
|
-
backgroundPosition: undefined,
|
|
643
|
-
backgroundRepeat: undefined,
|
|
644
|
-
}, { perChanges: [], pxChanges: allPxAttributes })
|
|
645
|
-
: tableStyles;
|
|
646
|
-
// When bg image is present, inner tables must be explicitly transparent so the
|
|
647
|
-
// outer <td> background shows through (email clients may default table bg to white).
|
|
778
|
+
// Inner tables must be explicitly transparent when outer <td> carries the background.
|
|
648
779
|
const innerBgTransparent = (rawBgImageUrl || isGradient)
|
|
649
780
|
? 'background-color:transparent;'
|
|
650
781
|
: '';
|
|
651
|
-
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
style="border-collapse:collapse;
|
|
664
|
-
class="${gridVisibilityClass}">
|
|
665
|
-
|
|
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]-->
|
|
666
803
|
`;
|
|
667
804
|
for (let r = 0; r < visualRows; r++) {
|
|
668
805
|
html += "<tr>";
|
|
@@ -716,18 +853,18 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
716
853
|
const childVisible = !childProps.hideOnDesktop;
|
|
717
854
|
const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
|
|
718
855
|
if (childVisible) {
|
|
719
|
-
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth);
|
|
856
|
+
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth, Boolean(divBorderStyle));
|
|
720
857
|
// bgcolor on the cell <td> ensures background-color survives Outlook
|
|
721
858
|
// compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
|
|
722
859
|
const cellBgColor = cellStyle.backgroundColor || '';
|
|
723
860
|
const cellBgAttr = cellBgColor ? ` bgcolor="${cellBgColor}"` : '';
|
|
724
|
-
html += `
|
|
725
|
-
<td
|
|
726
|
-
width="${cellWidthPx}"${cellBgAttr}
|
|
727
|
-
class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
|
|
728
|
-
style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
|
|
729
|
-
>
|
|
730
|
-
${childHtml}
|
|
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}
|
|
731
868
|
</td>`;
|
|
732
869
|
// Spacer td between columns — fixed pixel width, invisible to screen readers
|
|
733
870
|
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
@@ -736,10 +873,10 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
736
873
|
}
|
|
737
874
|
}
|
|
738
875
|
else {
|
|
739
|
-
html += `
|
|
740
|
-
<td width="${cellWidthPx}"
|
|
741
|
-
${responsive ? 'class="stack-column"' : ""}
|
|
742
|
-
style="width:${cellWidthPx}px;vertical-align:top;">
|
|
876
|
+
html += `
|
|
877
|
+
<td width="${cellWidthPx}"
|
|
878
|
+
${responsive ? 'class="stack-column"' : ""}
|
|
879
|
+
style="width:${cellWidthPx}px;vertical-align:top;">
|
|
743
880
|
</td>`;
|
|
744
881
|
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
745
882
|
html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
@@ -749,13 +886,13 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
749
886
|
html += "</tr>";
|
|
750
887
|
}
|
|
751
888
|
// Close both MSO and non-MSO tables
|
|
752
|
-
html += `
|
|
753
|
-
<!--[if mso]>
|
|
754
|
-
</table>
|
|
755
|
-
<![endif]-->
|
|
756
|
-
<!--[if !mso]><!-->
|
|
757
|
-
</table>
|
|
758
|
-
<!--<![endif]-->
|
|
889
|
+
html += `
|
|
890
|
+
<!--[if mso]>
|
|
891
|
+
</table>
|
|
892
|
+
<![endif]-->
|
|
893
|
+
<!--[if !mso]><!-->
|
|
894
|
+
</table>
|
|
895
|
+
<!--<![endif]-->
|
|
759
896
|
`;
|
|
760
897
|
// ── Background image: canonical multi-client approach ────────────────────
|
|
761
898
|
//
|
|
@@ -781,42 +918,63 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
781
918
|
return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
|
|
782
919
|
})()
|
|
783
920
|
: `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
|
|
784
|
-
html = `
|
|
785
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}" role="presentation"
|
|
786
|
-
style="border-collapse:collapse;width:${msoTableWidth}px;">
|
|
787
|
-
<tr>
|
|
788
|
-
<td width="${msoTableWidth}" bgcolor="${fallbackBgColor}" valign="top"
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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>
|
|
812
950
|
</table>`;
|
|
813
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}`;
|
|
814
958
|
return html;
|
|
815
959
|
}
|
|
816
|
-
async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx) {
|
|
960
|
+
async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
|
|
817
961
|
const { style = {}, childrenIds = [], props = {} } = blockData.data;
|
|
818
962
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
819
|
-
|
|
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, {
|
|
820
978
|
perChanges: [],
|
|
821
979
|
pxChanges: allPxAttributes,
|
|
822
980
|
});
|
|
@@ -827,7 +985,7 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
|
|
|
827
985
|
// Subtract the cell's own padding so children receive the actual content-area width.
|
|
828
986
|
// Old Outlook honours explicit img/table width attributes — if a child is sized to the
|
|
829
987
|
// full column width (ignoring padding) it overflows and expands the column.
|
|
830
|
-
const cellPad =
|
|
988
|
+
const cellPad = styleWithoutBorder?.padding || {};
|
|
831
989
|
const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
|
|
832
990
|
const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
|
|
833
991
|
const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
|
|
@@ -839,22 +997,35 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
|
|
|
839
997
|
parts.push(await convertToHtml(child, rootData, safeCellWidthPx));
|
|
840
998
|
}
|
|
841
999
|
}
|
|
842
|
-
const borderRadius =
|
|
843
|
-
const bgColor =
|
|
844
|
-
//
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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>`;
|
|
858
1029
|
return {
|
|
859
1030
|
html: wrapped,
|
|
860
1031
|
styles,
|
|
@@ -867,7 +1038,9 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
|
|
|
867
1038
|
const { hideOnDesktop } = props; // Get the hideOnDesktop prop
|
|
868
1039
|
const { videoUrl, youtubeVideoUrl, thumbnailUrl, altText } = props;
|
|
869
1040
|
const videoLink = youtubeVideoUrl || videoUrl || "#";
|
|
870
|
-
|
|
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;
|
|
871
1044
|
if (youtubeVideoUrl) {
|
|
872
1045
|
const youtubeId = (0, common_1.extractYouTubeId)(youtubeVideoUrl);
|
|
873
1046
|
const vimeoId = (0, common_1.extractVimeoId)(youtubeVideoUrl);
|
|
@@ -921,117 +1094,103 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
|
|
|
921
1094
|
const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
|
|
922
1095
|
const shouldHideInOutlook = hideOnDesktop;
|
|
923
1096
|
const outlookVideoContent = shouldHideInOutlook
|
|
924
|
-
? `<!--[if !mso]><!-->
|
|
925
|
-
<v:group xmlns:v="urn:schemas-microsoft-com:vml"
|
|
926
|
-
coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
927
|
-
href="${videoLink}"
|
|
928
|
-
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
929
|
-
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
|
|
930
|
-
strokeweight="${borderWidth}px"
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
</v:group>
|
|
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>
|
|
947
1119
|
<!--<![endif]-->`
|
|
948
|
-
: `<!--[if mso]>
|
|
949
|
-
<v:group xmlns:v="urn:schemas-microsoft-com:vml"
|
|
950
|
-
coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
951
|
-
href="${videoLink}"
|
|
952
|
-
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
953
|
-
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
|
|
954
|
-
stroked="t"
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
${
|
|
958
|
-
>
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
</v:shape>
|
|
971
|
-
</v:group>
|
|
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>
|
|
972
1142
|
<![endif]-->`;
|
|
973
|
-
|
|
974
|
-
<table
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
style="display: block;
|
|
1003
|
-
border: 0;
|
|
1004
|
-
outline: none;
|
|
1005
|
-
text-decoration: none;
|
|
1006
|
-
height: auto;"
|
|
1007
|
-
/>
|
|
1008
|
-
</a>
|
|
1009
|
-
</td>
|
|
1010
|
-
</tr>
|
|
1011
|
-
</table>
|
|
1012
|
-
<!--<![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]-->`;
|
|
1013
1172
|
const videoContent = `${outlookVideoContent}${nonOutlookVideoContent}`;
|
|
1014
|
-
const wrapperHtml = `
|
|
1015
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
|
|
1016
|
-
<tr>
|
|
1017
|
-
<td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
|
|
1018
|
-
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
1019
|
-
align="${style?.textAlign || "left"}"
|
|
1020
|
-
style="
|
|
1021
|
-
margin:0;
|
|
1022
|
-
max-width:${cellWidthInPx}px;
|
|
1023
|
-
width:${percentWidth};
|
|
1024
|
-
border-collapse:collapse;
|
|
1025
|
-
">
|
|
1026
|
-
<tr>
|
|
1027
|
-
<td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
|
|
1028
|
-
${videoContent}
|
|
1029
|
-
</td>
|
|
1030
|
-
</tr>
|
|
1031
|
-
</table>
|
|
1032
|
-
</td>
|
|
1033
|
-
</tr>
|
|
1034
|
-
</table>
|
|
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>
|
|
1035
1194
|
`;
|
|
1036
1195
|
return wrapperHtml;
|
|
1037
1196
|
}
|
|
@@ -1095,16 +1254,6 @@ async function convertShapeBlock(blockData) {
|
|
|
1095
1254
|
justify: "justify",
|
|
1096
1255
|
};
|
|
1097
1256
|
const textAlignStyle = textAlignMap[textAlign] || "center";
|
|
1098
|
-
const flexJustify = textAlign === "left"
|
|
1099
|
-
? "flex-start"
|
|
1100
|
-
: textAlign === "right"
|
|
1101
|
-
? "flex-end"
|
|
1102
|
-
: "center";
|
|
1103
|
-
const flexAlign = verticalAlign === "top"
|
|
1104
|
-
? "flex-start"
|
|
1105
|
-
: verticalAlign === "bottom"
|
|
1106
|
-
? "flex-end"
|
|
1107
|
-
: "center";
|
|
1108
1257
|
// --- Text styling ---
|
|
1109
1258
|
const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;color:${color};`;
|
|
1110
1259
|
// ============================
|
|
@@ -1113,44 +1262,58 @@ async function convertShapeBlock(blockData) {
|
|
|
1113
1262
|
let nonMsoContent = "";
|
|
1114
1263
|
// --- Case 1: Image + Text ---
|
|
1115
1264
|
if (imageUrl && text) {
|
|
1116
|
-
nonMsoContent = `
|
|
1117
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1118
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1119
|
-
border-radius:${resolvedBorderRadius};
|
|
1120
|
-
background:${finalBackgroundColor}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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>
|
|
1127
1285
|
</div>`;
|
|
1128
1286
|
}
|
|
1129
1287
|
// --- Case 2: Image only ---
|
|
1130
1288
|
else if (imageUrl) {
|
|
1131
|
-
nonMsoContent = `
|
|
1132
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1133
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1134
|
-
border-radius:${resolvedBorderRadius};
|
|
1135
|
-
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1136
|
-
<img src="${imageUrl}" alt="${text || "shape image"}"
|
|
1137
|
-
width="${resolvedWidthPx}" height="${resolvedHeightPx}"
|
|
1138
|
-
style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
|
|
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;" />
|
|
1139
1297
|
</div>`;
|
|
1140
1298
|
}
|
|
1141
1299
|
// --- Case 3: Text only ---
|
|
1142
1300
|
else {
|
|
1143
|
-
nonMsoContent = `
|
|
1144
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1145
|
-
background:${finalBackgroundColor};
|
|
1146
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1147
|
-
border-radius:${resolvedBorderRadius};
|
|
1148
|
-
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1149
|
-
<
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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>
|
|
1154
1317
|
</div>`;
|
|
1155
1318
|
}
|
|
1156
1319
|
// Outlook (VML) fallback
|
|
@@ -1172,17 +1335,45 @@ async function convertShapeBlock(blockData) {
|
|
|
1172
1335
|
padding,
|
|
1173
1336
|
msoBakeImageWithText,
|
|
1174
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
|
+
});
|
|
1175
1366
|
// Combine into table wrapper
|
|
1176
|
-
return `
|
|
1177
|
-
<table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}">
|
|
1178
|
-
<tr>
|
|
1179
|
-
<td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
|
|
1180
|
-
${outlookContent}
|
|
1181
|
-
<!--[if !mso]><!-->
|
|
1182
|
-
${nonMsoContent}
|
|
1183
|
-
<!--<![endif]-->
|
|
1184
|
-
</td>
|
|
1185
|
-
</tr>
|
|
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>
|
|
1186
1377
|
</table>`;
|
|
1187
1378
|
}
|
|
1188
1379
|
// ---------- Updated VML builder ----------
|
|
@@ -1215,24 +1406,24 @@ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, bo
|
|
|
1215
1406
|
const safeFontSize = Math.max(Math.round(textSize), 10);
|
|
1216
1407
|
// Build the textbox with table/cell for reliable vertical centering in Outlook
|
|
1217
1408
|
const textboxMarkup = text && !msoHasBakedText
|
|
1218
|
-
? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
|
|
1219
|
-
<div style="display:table;width:100%;height:100%;">
|
|
1220
|
-
<div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
|
|
1221
|
-
<div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
|
|
1222
|
-
${text}
|
|
1223
|
-
</div>
|
|
1224
|
-
</div>
|
|
1225
|
-
</div>
|
|
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>
|
|
1226
1417
|
</v:textbox>`
|
|
1227
1418
|
: `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
|
|
1228
1419
|
// Return VML shape
|
|
1229
|
-
return `
|
|
1230
|
-
<v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
|
|
1231
|
-
style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
|
|
1232
|
-
${borderAttrs}
|
|
1233
|
-
fill="true" fillcolor="${fillColor}"${extraAttr}>
|
|
1234
|
-
${fillMarkup}
|
|
1235
|
-
${textboxMarkup}
|
|
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}
|
|
1236
1427
|
</v:${tag}>`;
|
|
1237
1428
|
}
|
|
1238
1429
|
function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts, visibilityClass) {
|
|
@@ -1260,28 +1451,28 @@ function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth
|
|
|
1260
1451
|
const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
|
|
1261
1452
|
// Fix: Properly handle Outlook visibility with conditional comments
|
|
1262
1453
|
if (shouldHideInOutlook) {
|
|
1263
|
-
return `<!--[if !mso]><!-->
|
|
1264
|
-
<table align="${align}" border="0" cellpadding="0" cellspacing="0"
|
|
1265
|
-
style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
|
|
1266
|
-
<tr>
|
|
1267
|
-
<td valign="${valign}"
|
|
1268
|
-
style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
|
|
1269
|
-
${vml}
|
|
1270
|
-
</td>
|
|
1271
|
-
</tr>
|
|
1272
|
-
</table>
|
|
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>
|
|
1273
1464
|
<!--<![endif]-->`;
|
|
1274
1465
|
}
|
|
1275
|
-
return `<!--[if mso]>
|
|
1276
|
-
<table align="${align}" border="0" cellpadding="0" cellspacing="0"
|
|
1277
|
-
style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
|
|
1278
|
-
<tr>
|
|
1279
|
-
<td valign="${valign}"
|
|
1280
|
-
style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
|
|
1281
|
-
${vml}
|
|
1282
|
-
</td>
|
|
1283
|
-
</tr>
|
|
1284
|
-
</table>
|
|
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>
|
|
1285
1476
|
<![endif]-->`;
|
|
1286
1477
|
}
|
|
1287
1478
|
function convertVerticalDividerBlockToHtml(blockData) {
|
|
@@ -1292,18 +1483,28 @@ function convertVerticalDividerBlockToHtml(blockData) {
|
|
|
1292
1483
|
perChanges: [],
|
|
1293
1484
|
pxChanges: allPxAttributes,
|
|
1294
1485
|
});
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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>
|
|
1308
1509
|
</table>`;
|
|
1309
1510
|
}
|