email-builder-utils 1.1.40 → 1.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils/convertJsonToHtml.d.ts.map +1 -1
- package/dist/utils/convertJsonToHtml.js +25 -20
- package/dist/utils/fontFallback.d.ts +7 -0
- package/dist/utils/fontFallback.d.ts.map +1 -0
- package/dist/utils/fontFallback.js +65 -0
- package/dist/utils/jsonToHTML.d.ts.map +1 -1
- package/dist/utils/jsonToHTML.js +459 -177
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convertJsonToHtml.d.ts","sourceRoot":"","sources":["../../src/utils/convertJsonToHtml.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"convertJsonToHtml.d.ts","sourceRoot":"","sources":["../../src/utils/convertJsonToHtml.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,GAAU,UAAU,GAAG,oBAiHpD,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.convertJsonToHtml = void 0;
|
|
4
|
+
const fontFallback_1 = require("./fontFallback");
|
|
4
5
|
const jsonToHTML_1 = require("./jsonToHTML");
|
|
5
6
|
const convertJsonToHtml = async (jsonData) => {
|
|
6
7
|
const rootData = jsonData?.root?.data;
|
|
@@ -36,54 +37,58 @@ const convertJsonToHtml = async (jsonData) => {
|
|
|
36
37
|
}
|
|
37
38
|
.hide-mobile {
|
|
38
39
|
display: block !important;
|
|
39
|
-
mso-hide: all !important; /* Hide in Outlook */
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
.hide-desktop {
|
|
43
43
|
display: block !important;
|
|
44
|
-
mso-hide: all !important; /* Hide in Outlook */
|
|
44
|
+
mso-hide: all !important; /* Hide in Outlook (treated as desktop) */
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
@media only screen and (max-width: 600px) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
.hide-mobile {
|
|
49
|
+
display: none !important;
|
|
50
|
+
max-height: 0 !important;
|
|
51
51
|
overflow: hidden !important;
|
|
52
|
-
mso-hide: all !important;
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
mso-hide: all !important;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
@media only screen and (min-width: 601px) {
|
|
57
|
+
.hide-desktop {
|
|
58
|
+
display: none !important;
|
|
59
|
+
max-height: 0 !important;
|
|
60
|
+
overflow: hidden !important;
|
|
61
|
+
mso-hide: all !important;
|
|
62
|
+
}
|
|
62
63
|
}
|
|
63
|
-
}
|
|
64
64
|
|
|
65
65
|
</style>
|
|
66
66
|
</head>
|
|
67
|
-
<body>
|
|
68
|
-
<center>
|
|
67
|
+
<body style="margin:0; padding:0; background-color:${canvasColor};">
|
|
68
|
+
<center style="width:100%; background-color:${canvasColor};">
|
|
69
69
|
<table
|
|
70
70
|
class="responsive-table"
|
|
71
|
+
role="presentation"
|
|
71
72
|
bgcolor="${canvasColor}"
|
|
73
|
+
cellpadding="0"
|
|
74
|
+
cellspacing="0"
|
|
75
|
+
border="0"
|
|
76
|
+
width="600"
|
|
77
|
+
align="center"
|
|
72
78
|
style="
|
|
73
|
-
font-family: ${fontFamily};
|
|
79
|
+
font-family: ${(0, fontFallback_1.withFontFallback)(fontFamily)};
|
|
74
80
|
margin: 0 auto;
|
|
75
81
|
table-layout:fixed;
|
|
76
82
|
width:600px;
|
|
77
83
|
max-width:600px;
|
|
78
84
|
background-color: ${canvasColor};
|
|
79
85
|
color: ${textColor};
|
|
80
|
-
padding: ${top}px ${right}px ${bottom}px ${left}px;
|
|
81
86
|
border: ${borderWidth}px ${borderStyle} ${borderColor};
|
|
82
87
|
border-radius: ${borderRadius}px; "
|
|
83
88
|
>
|
|
84
89
|
<tbody>
|
|
85
90
|
<tr>
|
|
86
|
-
<td style="padding:
|
|
91
|
+
<td style="padding: ${top}px ${right}px ${bottom}px ${left}px;">
|
|
87
92
|
${blocksHtml.join("")}
|
|
88
93
|
</td>
|
|
89
94
|
</tr>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* If the font-family value doesn't already end with a generic/web-safe family,
|
|
3
|
+
* append an appropriate fallback so Outlook desktop (which silently falls back
|
|
4
|
+
* to Times New Roman for any non-web-safe font) picks a reasonable match.
|
|
5
|
+
*/
|
|
6
|
+
export declare function withFontFallback(fontFamily: string | undefined | null): string;
|
|
7
|
+
//# sourceMappingURL=fontFallback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fontFallback.d.ts","sourceRoot":"","sources":["../../src/utils/fontFallback.ts"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAY9E"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withFontFallback = withFontFallback;
|
|
4
|
+
// Web-safe fonts that render natively in Outlook desktop (Word engine).
|
|
5
|
+
// Anything not in this set falls back silently to Times New Roman unless we
|
|
6
|
+
// append a generic stack. Keep lowercase for comparison.
|
|
7
|
+
const WEB_SAFE_FONTS = new Set([
|
|
8
|
+
"arial",
|
|
9
|
+
"arial black",
|
|
10
|
+
"helvetica",
|
|
11
|
+
"helvetica neue",
|
|
12
|
+
"verdana",
|
|
13
|
+
"tahoma",
|
|
14
|
+
"trebuchet ms",
|
|
15
|
+
"times",
|
|
16
|
+
"times new roman",
|
|
17
|
+
"georgia",
|
|
18
|
+
"garamond",
|
|
19
|
+
"courier",
|
|
20
|
+
"courier new",
|
|
21
|
+
"lucida console",
|
|
22
|
+
"lucida sans unicode",
|
|
23
|
+
"palatino",
|
|
24
|
+
"palatino linotype",
|
|
25
|
+
"book antiqua",
|
|
26
|
+
"impact",
|
|
27
|
+
"serif",
|
|
28
|
+
"sans-serif",
|
|
29
|
+
"monospace",
|
|
30
|
+
"cursive",
|
|
31
|
+
"fantasy",
|
|
32
|
+
"system-ui",
|
|
33
|
+
]);
|
|
34
|
+
const DEFAULT_FALLBACK = "Arial, sans-serif";
|
|
35
|
+
const SERIF_FALLBACK = "Georgia, 'Times New Roman', serif";
|
|
36
|
+
const MONO_FALLBACK = "'Courier New', Courier, monospace";
|
|
37
|
+
const SERIF_HINTS = ["serif", "playfair", "merriweather", "lora", "roboto slab", "source serif", "pt serif", "noto serif"];
|
|
38
|
+
const MONO_HINTS = ["mono", "code", "courier", "consolas", "menlo"];
|
|
39
|
+
function pickGenericFallback(name) {
|
|
40
|
+
const lower = name.toLowerCase();
|
|
41
|
+
if (MONO_HINTS.some((h) => lower.includes(h)))
|
|
42
|
+
return MONO_FALLBACK;
|
|
43
|
+
if (SERIF_HINTS.some((h) => lower.includes(h)))
|
|
44
|
+
return SERIF_FALLBACK;
|
|
45
|
+
return DEFAULT_FALLBACK;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* If the font-family value doesn't already end with a generic/web-safe family,
|
|
49
|
+
* append an appropriate fallback so Outlook desktop (which silently falls back
|
|
50
|
+
* to Times New Roman for any non-web-safe font) picks a reasonable match.
|
|
51
|
+
*/
|
|
52
|
+
function withFontFallback(fontFamily) {
|
|
53
|
+
if (!fontFamily)
|
|
54
|
+
return DEFAULT_FALLBACK;
|
|
55
|
+
const trimmed = String(fontFamily).trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
return DEFAULT_FALLBACK;
|
|
58
|
+
const parts = trimmed.split(",").map((p) => p.trim().replace(/^["']|["']$/g, ""));
|
|
59
|
+
const alreadySafe = parts.some((p) => WEB_SAFE_FONTS.has(p.toLowerCase()));
|
|
60
|
+
if (alreadySafe)
|
|
61
|
+
return trimmed;
|
|
62
|
+
const primary = parts[0] || "";
|
|
63
|
+
const fallback = pickGenericFallback(primary);
|
|
64
|
+
return `${trimmed}, ${fallback}`;
|
|
65
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAIrC,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,cAAc,CAAC;QACtB,KAAK,EAAE,GAAG,CAAC;QACX,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KAC7B,CAAC;CACH;AAYD,eAAO,MAAM,gBAAgB,kDAAkD,CAAC;AAqGhF,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AA68BD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBA0L5E"}
|
package/dist/utils/jsonToHTML.js
CHANGED
|
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.tableCommonStyle = void 0;
|
|
4
4
|
exports.convertToHtml = convertToHtml;
|
|
5
5
|
exports.convertVideoBlock = convertVideoBlock;
|
|
6
|
-
const jimp_1 = require("jimp");
|
|
7
6
|
const types_1 = require("../types");
|
|
8
7
|
const common_1 = require("./common");
|
|
8
|
+
const fontFallback_1 = require("./fontFallback");
|
|
9
9
|
const addPxToAttributes = [
|
|
10
10
|
"fontSize",
|
|
11
11
|
"lineHeight",
|
|
@@ -15,23 +15,13 @@ const addPxToAttributes = [
|
|
|
15
15
|
const addPxOrPerToAttributes = ["width", "height"];
|
|
16
16
|
const allPxAttributes = [...addPxToAttributes, ...addPxOrPerToAttributes];
|
|
17
17
|
exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.map(([key, value]) => [key, cleanJson(value)]));
|
|
26
|
-
}
|
|
27
|
-
function jsonToPlainString(obj) {
|
|
28
|
-
if (typeof obj !== "object" || obj === null)
|
|
29
|
-
return String(obj);
|
|
30
|
-
if (Array.isArray(obj))
|
|
31
|
-
return obj.map(jsonToPlainString).join(", ");
|
|
32
|
-
return Object.entries(obj)
|
|
33
|
-
.map(([key, value]) => `${key}:${jsonToPlainString(value)}; `)
|
|
34
|
-
.join("");
|
|
18
|
+
async function loadImageNaturalDimensions(imageUrl) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const img = new Image();
|
|
21
|
+
img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
22
|
+
img.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`));
|
|
23
|
+
img.src = imageUrl;
|
|
24
|
+
});
|
|
35
25
|
}
|
|
36
26
|
function buildStyles(style, { pxChanges, perChanges }) {
|
|
37
27
|
if (!style)
|
|
@@ -47,7 +37,9 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
47
37
|
"childWidth",
|
|
48
38
|
"visibility",
|
|
49
39
|
"hideOnMobile",
|
|
50
|
-
"hideOnDesktop"
|
|
40
|
+
"hideOnDesktop",
|
|
41
|
+
"label",
|
|
42
|
+
"alignment",
|
|
51
43
|
];
|
|
52
44
|
if (INVALID_KEYS.includes(key))
|
|
53
45
|
return;
|
|
@@ -66,11 +58,26 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
66
58
|
};
|
|
67
59
|
value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
|
|
68
60
|
}
|
|
61
|
+
// Sanitize fontFamily: replace double quotes with single quotes to avoid
|
|
62
|
+
// breaking the surrounding style="..." HTML attribute
|
|
63
|
+
if (key === "fontFamily" && typeof value === "string") {
|
|
64
|
+
value = (0, fontFallback_1.withFontFallback)(value).replace(/"/g, "'");
|
|
65
|
+
}
|
|
66
|
+
// Wrap backgroundImage values in url() if not already wrapped
|
|
67
|
+
if (key === "backgroundImage" && typeof value === "string" && !String(value).startsWith("url(")) {
|
|
68
|
+
value = `url('${value}')`;
|
|
69
|
+
}
|
|
70
|
+
// lineHeight: values >= 4 are pixel values; smaller values are unitless multipliers (e.g. 1.5)
|
|
71
|
+
if (key === "lineHeight" && typeof value === "number") {
|
|
72
|
+
stylesObj["line-height"] = value >= 4 ? `${value}px` : String(value);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
69
75
|
const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
70
76
|
// FIX 2 — Sanitize invalid px/per values
|
|
71
77
|
if (pxChanges.includes(key)) {
|
|
72
78
|
if (typeof value === "number") {
|
|
73
|
-
|
|
79
|
+
const rounded = Math.round(value * 100) / 100;
|
|
80
|
+
stylesObj[cssKey] = `${rounded}px`;
|
|
74
81
|
}
|
|
75
82
|
else if (typeof value === "string" && value.includes("null")) {
|
|
76
83
|
// Skip invalid styles
|
|
@@ -92,12 +99,17 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
92
99
|
stylesObj[cssKey] = value;
|
|
93
100
|
}
|
|
94
101
|
});
|
|
95
|
-
|
|
102
|
+
const parts = Object.entries(stylesObj)
|
|
103
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
104
|
+
.map(([k, v]) => `${k}:${v}`);
|
|
105
|
+
if (style.customCss)
|
|
106
|
+
parts.push(style.customCss);
|
|
107
|
+
return parts.join('; ').trim();
|
|
96
108
|
}
|
|
97
109
|
async function convertToHtml(blockData, rootData, cellWidthInPx) {
|
|
98
110
|
switch (blockData.type) {
|
|
99
111
|
case types_1.BlockType.TEXT:
|
|
100
|
-
return convertTextBlock(blockData);
|
|
112
|
+
return convertTextBlock(blockData, cellWidthInPx);
|
|
101
113
|
case types_1.BlockType.IMAGE:
|
|
102
114
|
return await convertImageBlock(blockData, cellWidthInPx);
|
|
103
115
|
case types_1.BlockType.BUTTON:
|
|
@@ -118,12 +130,7 @@ async function convertToHtml(blockData, rootData, cellWidthInPx) {
|
|
|
118
130
|
return "";
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
|
-
|
|
122
|
-
// return `
|
|
123
|
-
// <table width="100%" style="${tableCommonStyle}"><tr><td style="${contentStyle}">${content}</td></tr></table>
|
|
124
|
-
// `;
|
|
125
|
-
// }
|
|
126
|
-
function appendOutlookSupport(content, contentStyle, className) {
|
|
133
|
+
function appendOutlookSupport(content, contentStyle, className, msoWidth) {
|
|
127
134
|
const visibilityClass = className || "";
|
|
128
135
|
const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
|
|
129
136
|
if (shouldHideInOutlook) {
|
|
@@ -133,9 +140,24 @@ function appendOutlookSupport(content, contentStyle, className) {
|
|
|
133
140
|
<!--<![endif]-->
|
|
134
141
|
`;
|
|
135
142
|
}
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
143
|
+
// When an explicit pixel width is provided (e.g. inside a column cell), use dual MSO/non-MSO
|
|
144
|
+
// tables. Old Outlook (Word engine) ignores max-width and can resolve width="100%" to the
|
|
145
|
+
// full email width (600px) rather than the column width, causing images/buttons to expand.
|
|
146
|
+
if (msoWidth) {
|
|
147
|
+
return `
|
|
148
|
+
<!--[if mso]>
|
|
149
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" style="border-collapse:collapse;width:${msoWidth}px;"><tr><td width="${msoWidth}" style="${contentStyle}">
|
|
150
|
+
<![endif]-->
|
|
151
|
+
<!--[if !mso]><!-->
|
|
152
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">
|
|
153
|
+
<!--<![endif]-->
|
|
154
|
+
${content}
|
|
155
|
+
<!--[if mso]></td></tr></table><![endif]-->
|
|
156
|
+
<!--[if !mso]><!-->
|
|
157
|
+
</td></tr></table>
|
|
158
|
+
<!--<![endif]-->
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
139
161
|
return `
|
|
140
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>
|
|
141
163
|
`;
|
|
@@ -183,28 +205,13 @@ function convertSpacerBlockToHtml(blockData) {
|
|
|
183
205
|
});
|
|
184
206
|
return appendOutlookSupport(``, styles, visibilityClass);
|
|
185
207
|
}
|
|
186
|
-
|
|
187
|
-
// const { style, props } = blockData.data;
|
|
188
|
-
// const styles = buildStyles(style, {
|
|
189
|
-
// perChanges: [],
|
|
190
|
-
// pxChanges: allPxAttributes,
|
|
191
|
-
// });
|
|
192
|
-
// const text = props.text || "";
|
|
193
|
-
// const navigateToUrl = props.navigateToUrl || "";
|
|
194
|
-
// const textContent = appendOutlookSupport(
|
|
195
|
-
// text.replaceAll(/\n/g, "<br>"),
|
|
196
|
-
// styles
|
|
197
|
-
// );
|
|
198
|
-
// return navigateToUrl
|
|
199
|
-
// ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit; text-decoration:none; cursor:pointer;">${textContent}</a>`
|
|
200
|
-
// : textContent;
|
|
201
|
-
// }
|
|
202
|
-
function convertTextBlock(blockData) {
|
|
208
|
+
function convertTextBlock(blockData, cellWidthInPx) {
|
|
203
209
|
const { style, props } = blockData.data;
|
|
204
210
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
205
|
-
const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding,
|
|
211
|
+
const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, whiteSpace: _whiteSpace, // strip from outer td — pre-wrap on a td preserves editor whitespace
|
|
212
|
+
...rest } = style;
|
|
213
|
+
// Text box decoration styles (border, background, padding) — no width
|
|
206
214
|
const textBoxStyle = {
|
|
207
|
-
width,
|
|
208
215
|
backgroundColor,
|
|
209
216
|
padding,
|
|
210
217
|
borderRadius,
|
|
@@ -216,6 +223,7 @@ function convertTextBlock(blockData) {
|
|
|
216
223
|
perChanges: [],
|
|
217
224
|
pxChanges: allPxAttributes,
|
|
218
225
|
});
|
|
226
|
+
// Outer td styles: layout only, no typography, no white-space
|
|
219
227
|
const styles = buildStyles({
|
|
220
228
|
padding: textContainerPadding,
|
|
221
229
|
backgroundColor: textContainerBackgroundColor,
|
|
@@ -225,11 +233,15 @@ function convertTextBlock(blockData) {
|
|
|
225
233
|
pxChanges: allPxAttributes,
|
|
226
234
|
});
|
|
227
235
|
const sanitizedText = (props.text ?? "")
|
|
228
|
-
.
|
|
229
|
-
.
|
|
236
|
+
.replace(/<p(\s[^>]*)?>/gi, (_, attrs) => `<div${attrs || ""}>`)
|
|
237
|
+
.replace(/<\/p>/gi, "</div>");
|
|
230
238
|
const navigateToUrl = props.navigateToUrl || "";
|
|
231
|
-
const
|
|
232
|
-
|
|
239
|
+
const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
|
|
240
|
+
// Use display:block + width:100% so text fills the column naturally.
|
|
241
|
+
// display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
|
|
242
|
+
const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${fontSizeStyle}${convertedTextStyle}">${sanitizedText.replaceAll(/\n/g, "<br>")}</div>`;
|
|
243
|
+
const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
|
|
244
|
+
const textContent = appendOutlookSupport(convertedTextBox, styles, visibilityClass, safeCellWidth);
|
|
233
245
|
return navigateToUrl
|
|
234
246
|
? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit;text-decoration:none;cursor:pointer;">${textContent}</a>`
|
|
235
247
|
: textContent;
|
|
@@ -243,15 +255,21 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
243
255
|
vmlWidth = finalWidth;
|
|
244
256
|
vmlHeight = finalHeight;
|
|
245
257
|
}
|
|
258
|
+
else if (imageUrl) {
|
|
259
|
+
try {
|
|
260
|
+
const { width: originalWidth, height: originalHeight } = await loadImageNaturalDimensions(imageUrl);
|
|
261
|
+
const widthScalingFactor = Math.min(outerContainerWidth / originalWidth, innerContainerWidth / originalWidth, 1);
|
|
262
|
+
vmlWidth = Math.round(originalWidth * widthScalingFactor);
|
|
263
|
+
vmlHeight = Math.round(originalHeight * widthScalingFactor);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
vmlWidth = innerContainerWidth;
|
|
267
|
+
vmlHeight = innerContainerWidth;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
246
270
|
else {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const originalWidth = image.bitmap.width;
|
|
250
|
-
const originalHeight = image.bitmap.height;
|
|
251
|
-
const widthScalingFactor = Math.min(outerContainerWidth / originalWidth, innerContainerWidth / originalWidth, 1 // Never scale up
|
|
252
|
-
);
|
|
253
|
-
vmlWidth = Math.round(originalWidth * widthScalingFactor);
|
|
254
|
-
vmlHeight = Math.round(originalHeight * widthScalingFactor);
|
|
271
|
+
vmlWidth = innerContainerWidth;
|
|
272
|
+
vmlHeight = innerContainerWidth;
|
|
255
273
|
}
|
|
256
274
|
const borderWidth = parseInt(style?.borderWidth) || 0;
|
|
257
275
|
const borderColor = style?.borderColor || "transparent";
|
|
@@ -310,13 +328,23 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
310
328
|
`;
|
|
311
329
|
}
|
|
312
330
|
async function computeScaledDimensions(imageUrl, maxContainerWidthPx) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
331
|
+
if (!imageUrl) {
|
|
332
|
+
const w = Math.max(maxContainerWidthPx, 1);
|
|
333
|
+
const h = Math.round(w * (2 / 3));
|
|
334
|
+
return { originalWidth: w, originalHeight: h, scaledWidth: w, scaledHeight: h };
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const { width: originalWidth, height: originalHeight } = await loadImageNaturalDimensions(imageUrl);
|
|
338
|
+
const widthScalingFactor = Math.min(maxContainerWidthPx / originalWidth, 1);
|
|
339
|
+
const scaledWidth = Math.round(originalWidth * widthScalingFactor);
|
|
340
|
+
const scaledHeight = Math.round(originalHeight * widthScalingFactor);
|
|
341
|
+
return { originalWidth, originalHeight, scaledWidth, scaledHeight };
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
const w = Math.max(maxContainerWidthPx, 1);
|
|
345
|
+
const h = Math.round(w * (2 / 3));
|
|
346
|
+
return { originalWidth: w, originalHeight: h, scaledWidth: w, scaledHeight: h };
|
|
347
|
+
}
|
|
320
348
|
}
|
|
321
349
|
async function convertImageBlock(blockData, cellWidthInPx) {
|
|
322
350
|
const { style, props } = blockData.data;
|
|
@@ -359,83 +387,233 @@ async function convertImageBlock(blockData, cellWidthInPx) {
|
|
|
359
387
|
// OUTLOOK FIX: Image element with explicit dimensions
|
|
360
388
|
// Outlook will use width/height attributes, modern clients use CSS
|
|
361
389
|
// Use max-width instead of width:100% to prevent stretching
|
|
362
|
-
const imageElement = `<img src="${imageUrl}" alt="${altText || "Image"}" border="0" width="${finalWidth}" height="${finalHeight}" style="${imageTagStyles}; display:block; max-width:100%; height:auto;" />`;
|
|
390
|
+
const imageElement = `<img src="${imageUrl}" alt="${altText || "Image"}" border="0" width="${finalWidth}" height="${finalHeight}" style="${imageTagStyles}; display:block; max-width:100%; height:auto; line-height: 0;" />`;
|
|
363
391
|
const percentWidth = typeof width === "string" && width.endsWith("%")
|
|
364
392
|
? width
|
|
365
393
|
: typeof width === "number"
|
|
366
394
|
? `${width}%`
|
|
367
395
|
: "100%";
|
|
368
|
-
// Non-MSO wrapper
|
|
369
|
-
|
|
396
|
+
// Non-MSO wrapper: display:block removes the phantom inline-baseline gap that
|
|
397
|
+
// display:inline-block creates in Gmail / Apple Mail / Yahoo between images.
|
|
398
|
+
// margin handles alignment since text-align won't move block elements.
|
|
399
|
+
const imgTextAlign = containerStyle.textAlign || "left";
|
|
400
|
+
const imgMargin = imgTextAlign === "center" ? "margin:0 auto;" :
|
|
401
|
+
imgTextAlign === "right" ? "margin-left:auto; margin-right:0;" : "";
|
|
402
|
+
// OUTLOOK FIX: Use finalWidth (the actual displayed size) as max-width so the div
|
|
403
|
+
// doesn't claim more space than the image occupies. originalWidth is the natural
|
|
404
|
+
// image size (e.g. 636px for the Beefree logo rendered at 35px) which was
|
|
405
|
+
// misleadingly large and could confuse some rendering engines.
|
|
406
|
+
const nonMsoWrapper = `<div style="display:block; width:${percentWidth}; max-width:${finalWidth}px; line-height:0; font-size:0; ${imgMargin}">${imageElement}</div>`;
|
|
370
407
|
// OUTLOOK FIX: Generate VML with corrected dimensions
|
|
371
408
|
const outlookImage = await appendOutlookForImage(nonMsoWrapper, safeCellWidth, innerContainerWidth, imageUrl, style, finalWidth, finalHeight);
|
|
372
|
-
const imageContent = appendOutlookSupport(outlookImage, containerStyles, visibilityClass);
|
|
409
|
+
const imageContent = appendOutlookSupport(outlookImage, containerStyles, visibilityClass, safeCellWidth);
|
|
373
410
|
return navigateToUrl
|
|
374
411
|
? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block;">${imageContent}</a>`
|
|
375
412
|
: imageContent;
|
|
376
413
|
}
|
|
377
414
|
function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
415
|
+
const pad = buttonStyle.buttonPadding || {};
|
|
416
|
+
const padTop = Number.isFinite(pad.top) ? pad.top : 10;
|
|
417
|
+
const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
|
|
418
|
+
const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
|
|
419
|
+
const padRight = Number.isFinite(pad.right) ? pad.right : 20;
|
|
420
|
+
const fontSize = buttonStyle.fontSize || 16;
|
|
421
|
+
const height = typeof buttonStyle.height === "number" && buttonStyle.height > 0
|
|
422
|
+
? buttonStyle.height
|
|
423
|
+
: null;
|
|
424
|
+
// prevent layout break
|
|
425
|
+
const minHeight = padTop + padBottom + fontSize;
|
|
426
|
+
const finalHeight = height ? Math.max(height, minHeight) : null;
|
|
427
|
+
const borderRadius = buttonStyle.borderRadius || 0;
|
|
428
|
+
const borderColor = buttonStyle.borderColor || "transparent";
|
|
429
|
+
const borderWidth = buttonStyle.borderWidth || 0;
|
|
430
|
+
const borderStyle = buttonStyle.borderStyle || "solid";
|
|
431
|
+
const bgColor = buttonStyle.buttonColor || "transparent";
|
|
432
|
+
const color = buttonStyle.color || "#ffffff";
|
|
433
|
+
const fontFamily = (0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily).replace(/"/g, "'");
|
|
434
|
+
const fontWeight = buttonStyle.fontWeight || 400;
|
|
435
|
+
const width = typeof buttonStyle.width === "number"
|
|
436
|
+
? `width="${buttonStyle.width}"`
|
|
437
|
+
: "";
|
|
438
|
+
return `<!--[if mso]>
|
|
439
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="display:inline-table;">
|
|
440
|
+
<tr>
|
|
441
|
+
<td align="center"
|
|
442
|
+
valign="middle"
|
|
443
|
+
${width}
|
|
444
|
+
${finalHeight ? `height="${finalHeight}"` : ""}
|
|
445
|
+
bgcolor="${bgColor}"
|
|
446
|
+
style="
|
|
447
|
+
${finalHeight ? `height:${finalHeight}px;` : ""}
|
|
448
|
+
background-color:${bgColor};
|
|
449
|
+
border-radius:${borderRadius}px;
|
|
450
|
+
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
451
|
+
overflow:hidden;
|
|
452
|
+
mso-line-height-rule:exactly;
|
|
453
|
+
">
|
|
454
|
+
|
|
455
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
456
|
+
<tr>
|
|
457
|
+
<td align="center" valign="middle"
|
|
458
|
+
style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
|
|
459
|
+
|
|
460
|
+
<a href="${navigateToUrl}"
|
|
461
|
+
style="
|
|
462
|
+
display:inline-block;
|
|
463
|
+
color:${color};
|
|
464
|
+
text-decoration:none;
|
|
465
|
+
font-family:${fontFamily};
|
|
466
|
+
font-size:${fontSize}px;
|
|
467
|
+
font-weight:${fontWeight};
|
|
468
|
+
line-height:normal;
|
|
469
|
+
">
|
|
470
|
+
${text}
|
|
471
|
+
</a>
|
|
472
|
+
|
|
473
|
+
</td>
|
|
474
|
+
</tr>
|
|
475
|
+
</table>
|
|
476
|
+
|
|
477
|
+
</td>
|
|
478
|
+
</tr>
|
|
479
|
+
</table>
|
|
395
480
|
<![endif]-->
|
|
396
481
|
<!--[if !mso]><!-->
|
|
397
|
-
|
|
398
|
-
<!--<![endif]
|
|
399
|
-
`;
|
|
482
|
+
${content}
|
|
483
|
+
<!--<![endif]-->`;
|
|
400
484
|
}
|
|
401
485
|
function convertButtonBlock(blockData) {
|
|
402
486
|
const { style, props } = blockData.data;
|
|
403
487
|
const { text, navigateToUrl } = props;
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
488
|
+
const { fontFamily, fontSize, fontWeight, textAlign, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, alignment, padding, backgroundColor: containerBg, margin, } = style;
|
|
489
|
+
const pad = buttonPadding || {};
|
|
490
|
+
const padTop = Number.isFinite(pad.top) ? pad.top : 10;
|
|
491
|
+
const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
|
|
492
|
+
const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
|
|
493
|
+
const padRight = Number.isFinite(pad.right) ? pad.right : 20;
|
|
494
|
+
const fs = fontSize || 16;
|
|
495
|
+
// prevent layout break
|
|
496
|
+
const minHeight = padTop + padBottom + fs;
|
|
497
|
+
const finalHeight = typeof height === "number" && height > 0
|
|
498
|
+
? Math.max(height, minHeight)
|
|
499
|
+
: null;
|
|
500
|
+
const safeFF = (0, fontFallback_1.withFontFallback)(fontFamily).replace(/"/g, "'");
|
|
501
|
+
const safeColor = color || "#ffffff";
|
|
502
|
+
const bgColor = buttonColor || "transparent";
|
|
503
|
+
const bdColor = borderColor || "transparent";
|
|
504
|
+
const bdStyle = borderStyle || "solid";
|
|
505
|
+
const bw = borderWidth || 0;
|
|
506
|
+
const br = borderRadius || 0;
|
|
507
|
+
const containerAlign = alignment || textAlign || "left";
|
|
508
|
+
const widthAttr = typeof width === "number"
|
|
509
|
+
? `width="${width}"`
|
|
510
|
+
: "";
|
|
511
|
+
// ✅ FIX: no width=100% anywhere
|
|
512
|
+
const buttonTable = `
|
|
513
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0"
|
|
514
|
+
style="display:inline-table; border-collapse:separate;"
|
|
515
|
+
${widthAttr}>
|
|
516
|
+
<tr>
|
|
517
|
+
<td
|
|
518
|
+
align="center"
|
|
519
|
+
valign="middle"
|
|
520
|
+
${finalHeight ? `height="${finalHeight}"` : ""}
|
|
521
|
+
style="
|
|
522
|
+
${finalHeight ? `height:${finalHeight}px;` : ""}
|
|
523
|
+
background-color:${bgColor};
|
|
524
|
+
border-radius:${br}px;
|
|
525
|
+
border:${bw}px ${bdStyle} ${bdColor};
|
|
526
|
+
overflow:hidden;
|
|
527
|
+
mso-line-height-rule:exactly;
|
|
528
|
+
"
|
|
529
|
+
>
|
|
530
|
+
|
|
531
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
532
|
+
<tr>
|
|
533
|
+
<td align="center" valign="middle"
|
|
534
|
+
style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
|
|
535
|
+
|
|
536
|
+
<a href="${navigateToUrl}"
|
|
537
|
+
style="
|
|
538
|
+
display:inline-block;
|
|
539
|
+
color:${safeColor};
|
|
540
|
+
text-decoration:none;
|
|
541
|
+
font-family:${safeFF};
|
|
542
|
+
font-size:${fs}px;
|
|
543
|
+
font-weight:${fontWeight || 400};
|
|
544
|
+
line-height:normal;
|
|
545
|
+
white-space:nowrap;
|
|
546
|
+
">
|
|
547
|
+
${text}
|
|
548
|
+
</a>
|
|
549
|
+
|
|
550
|
+
</td>
|
|
551
|
+
</tr>
|
|
552
|
+
</table>
|
|
553
|
+
|
|
554
|
+
</td>
|
|
555
|
+
</tr>
|
|
556
|
+
</table>
|
|
557
|
+
`;
|
|
558
|
+
const aligned = containerAlign === "center"
|
|
559
|
+
? `<center>${buttonTable}</center>`
|
|
560
|
+
: `<div style="text-align:${containerAlign};">${buttonTable}</div>`;
|
|
561
|
+
const buttonWithOutlook = appendOutlookForButton(aligned, style, navigateToUrl, text);
|
|
562
|
+
return `
|
|
563
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
|
564
|
+
<tr>
|
|
565
|
+
<td align="${containerAlign}"
|
|
566
|
+
style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;
|
|
567
|
+
background-color:${containerBg || "transparent"};">
|
|
568
|
+
${buttonWithOutlook}
|
|
569
|
+
</td>
|
|
570
|
+
</tr>
|
|
571
|
+
</table>
|
|
572
|
+
`;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Extract the first solid-color stop from a CSS gradient in customCss.
|
|
576
|
+
* Used as a MSO/Outlook bgcolor fallback when backgroundColor is not set
|
|
577
|
+
* but the block has a gradient in customCss (e.g. linear-gradient imports).
|
|
578
|
+
*/
|
|
579
|
+
function extractCssFallbackColor(customCss) {
|
|
580
|
+
if (!customCss)
|
|
581
|
+
return '';
|
|
582
|
+
const m = customCss.match(/(?:linear|radial|conic)-gradient\([^)]*?(#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|rgba\([^)]+\))/);
|
|
583
|
+
return m?.[1] ?? '';
|
|
584
|
+
}
|
|
585
|
+
function parseGradient(gradient) {
|
|
586
|
+
if (!gradient)
|
|
587
|
+
return null;
|
|
588
|
+
const angleMatch = gradient.match(/(\d+)deg/);
|
|
589
|
+
const angle = angleMatch ? parseInt(angleMatch[1]) : 180;
|
|
590
|
+
const colors = gradient.match(/#([0-9a-fA-F]{3,8})|rgb[a]?\([^)]+\)/g) || [];
|
|
591
|
+
return {
|
|
592
|
+
angle,
|
|
593
|
+
colors,
|
|
594
|
+
fallback: colors[0] || "#ffffff",
|
|
419
595
|
};
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
});
|
|
424
|
-
const convertedStyles = buildStyles({ maxWidth: "100%", boxSizing: "border-box", ...rest }, {
|
|
425
|
-
perChanges: [],
|
|
426
|
-
pxChanges: allPxAttributes,
|
|
427
|
-
});
|
|
428
|
-
const buttonElement = `<a href="${navigateToUrl}" rel="noreferrer noopener" style="display:inline-block; text-decoration:none; cursor:pointer;"><button style="${convertedButtonStyle}">${text}</button></a>`;
|
|
429
|
-
const buttonContent = appendOutlookSupport(appendOutlookForButton(buttonElement, style, navigateToUrl, text), convertedStyles, visibilityClass);
|
|
430
|
-
return buttonContent;
|
|
596
|
+
}
|
|
597
|
+
function cssAngleToVml(angle) {
|
|
598
|
+
return (angle + 90) % 360;
|
|
431
599
|
}
|
|
432
600
|
async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
433
601
|
const { style = {}, childrenIds = [], props } = blockData.data;
|
|
434
602
|
const { columns = 1, cellWidths = [], responsive = true } = props;
|
|
435
|
-
const { columnGap = 0, ...restStyle } = style;
|
|
603
|
+
const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
|
|
436
604
|
const gridVisibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
605
|
+
// Strip url() wrapper if already present, so we have a raw URL for VML
|
|
606
|
+
const isGradient = typeof backgroundImage === "string" && backgroundImage.includes("gradient");
|
|
607
|
+
const parsedGradient = isGradient ? parseGradient(backgroundImage) : null;
|
|
608
|
+
const fallbackBgColor = backgroundColor ||
|
|
609
|
+
parsedGradient?.fallback ||
|
|
610
|
+
extractCssFallbackColor(restStyle.customCss) ||
|
|
611
|
+
"#ffffff";
|
|
612
|
+
const rawBgImageUrl = !isGradient && backgroundImage
|
|
613
|
+
? String(backgroundImage).replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
|
|
614
|
+
: null;
|
|
437
615
|
// FIX: avoid table-layout:fixed – causes shrink in many clients
|
|
438
|
-
const tableStyles = buildStyles(restStyle, {
|
|
616
|
+
const tableStyles = buildStyles({ backgroundColor, ...restStyle }, {
|
|
439
617
|
perChanges: [],
|
|
440
618
|
pxChanges: allPxAttributes,
|
|
441
619
|
});
|
|
@@ -443,45 +621,75 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
443
621
|
const visualRows = Math.ceil(total / columns);
|
|
444
622
|
// OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
|
|
445
623
|
const msoTableWidth = Math.min(cellWidthInPx, 600);
|
|
624
|
+
// When a background image is present, the background is applied on an outer
|
|
625
|
+
// wrapper <td> (see bottom of function). The inner grid tables must be clean
|
|
626
|
+
// (no background) so that outer td background shows through.
|
|
627
|
+
// When no background image, the MSO table gets bgcolor for solid-color sections.
|
|
628
|
+
const msoBgColor = !rawBgImageUrl
|
|
629
|
+
? (backgroundColor || extractCssFallbackColor(restStyle.customCss))
|
|
630
|
+
: '';
|
|
631
|
+
const msoBgAttr = msoBgColor ? ` bgcolor="${msoBgColor}"` : '';
|
|
632
|
+
const msoBgStyle = msoBgColor ? `background-color:${msoBgColor};` : '';
|
|
633
|
+
// Inner non-MSO table: strip ALL background-related props when an outer bg-td
|
|
634
|
+
// wrapper is used. background-position/repeat/size without a background-image
|
|
635
|
+
// are harmless, but keeping them in the inner table style is confusing and
|
|
636
|
+
// could conflict with email client default styles.
|
|
637
|
+
const innerNonMsoStyle = rawBgImageUrl
|
|
638
|
+
? buildStyles({
|
|
639
|
+
...restStyle,
|
|
640
|
+
customCss: '',
|
|
641
|
+
backgroundSize: undefined,
|
|
642
|
+
backgroundPosition: undefined,
|
|
643
|
+
backgroundRepeat: undefined,
|
|
644
|
+
}, { perChanges: [], pxChanges: allPxAttributes })
|
|
645
|
+
: tableStyles;
|
|
646
|
+
// When bg image is present, inner tables must be explicitly transparent so the
|
|
647
|
+
// outer <td> background shows through (email clients may default table bg to white).
|
|
648
|
+
const innerBgTransparent = (rawBgImageUrl || isGradient)
|
|
649
|
+
? 'background-color:transparent;'
|
|
650
|
+
: '';
|
|
651
|
+
// bgcolor attribute on both tables: survives Outlook compose paste (Word + Web
|
|
652
|
+
// both strip background-image CSS but keep the bgcolor HTML attribute).
|
|
653
|
+
const nonMsoBgAttr = !rawBgImageUrl && backgroundColor ? ` bgcolor="${backgroundColor}"` : '';
|
|
446
654
|
let html = `
|
|
447
655
|
<!--[if mso]>
|
|
448
|
-
<table border="0" cellpadding="0" cellspacing="
|
|
449
|
-
style="border-collapse:
|
|
656
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
|
|
657
|
+
style="border-collapse:collapse;width:${msoTableWidth}px;${msoBgStyle}${innerBgTransparent}"
|
|
450
658
|
class="${gridVisibilityClass}">
|
|
451
659
|
<![endif]-->
|
|
452
660
|
<!--[if !mso]><!-->
|
|
453
|
-
<table border="0" cellpadding="0" cellspacing="
|
|
454
|
-
role="presentation"
|
|
455
|
-
style="border-collapse:
|
|
661
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%"
|
|
662
|
+
role="presentation"${nonMsoBgAttr}
|
|
663
|
+
style="border-collapse:collapse; ${innerBgTransparent}${innerNonMsoStyle}; max-width:600px;"
|
|
456
664
|
class="${gridVisibilityClass}">
|
|
457
665
|
<!--<![endif]-->
|
|
458
666
|
`;
|
|
459
667
|
for (let r = 0; r < visualRows; r++) {
|
|
460
668
|
html += "<tr>";
|
|
461
|
-
// COUNT visible cells
|
|
669
|
+
// COUNT visible cells and find last visible column index
|
|
462
670
|
let visibleCells = 0;
|
|
671
|
+
let lastVisibleCol = 0;
|
|
463
672
|
const rowIds = [];
|
|
464
673
|
for (let c = 0; c < columns; c++) {
|
|
465
674
|
const idx = r * columns + c;
|
|
466
675
|
const id = childrenIds[idx] ?? null;
|
|
467
676
|
rowIds.push(id);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
677
|
+
const child = id ? rootData[id] : null;
|
|
678
|
+
const isHidden = child?.data?.props?.hideOnDesktop;
|
|
679
|
+
if (!isHidden) {
|
|
680
|
+
visibleCells++;
|
|
681
|
+
lastVisibleCol = c;
|
|
473
682
|
}
|
|
474
683
|
}
|
|
475
|
-
// OUTLOOK FIX: Calculate safe width based on visible cells
|
|
476
|
-
// If we have visible cells, distribute 100% width evenly
|
|
477
684
|
const safeWidth = visibleCells > 0 ? 100 / visibleCells : 100 / columns;
|
|
478
|
-
//
|
|
685
|
+
// Reserve pixel space for spacer tds between visible cells (N-1 gaps for N visible cells)
|
|
686
|
+
const totalGapPx = columnGap * Math.max(visibleCells - 1, 0);
|
|
687
|
+
const adjustedTableWidth = Math.max(msoTableWidth - totalGapPx, 1);
|
|
479
688
|
let totalWidth = 0;
|
|
480
689
|
const cellWidthPercents = [];
|
|
481
690
|
for (let c = 0; c < columns; c++) {
|
|
482
691
|
const id = rowIds[c];
|
|
483
692
|
let widthPercent = cellWidths[c] ?? safeWidth;
|
|
484
|
-
// Validate width
|
|
485
693
|
if (widthPercent <= 0 || widthPercent > 100) {
|
|
486
694
|
widthPercent = safeWidth;
|
|
487
695
|
}
|
|
@@ -494,49 +702,48 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
494
702
|
}
|
|
495
703
|
}
|
|
496
704
|
}
|
|
497
|
-
// OUTLOOK FIX: If total width < 100%, scale up to fill container
|
|
498
|
-
// This prevents orphaned space that Outlook handles poorly
|
|
499
705
|
const scaleFactor = totalWidth > 0 && totalWidth < 100 ? 100 / totalWidth : 1;
|
|
500
706
|
for (let c = 0; c < columns; c++) {
|
|
501
|
-
const idx = r * columns + c;
|
|
502
707
|
const id = rowIds[c];
|
|
503
|
-
// OUTLOOK FIX: Scale width to ensure cells fill 100% of container
|
|
504
708
|
let widthPercent = cellWidthPercents[c] * scaleFactor;
|
|
505
|
-
// OUTLOOK FIX: Ensure width doesn't exceed 100% after scaling
|
|
506
709
|
widthPercent = Math.min(widthPercent, 100);
|
|
710
|
+
// Cell pixel width is a share of the gap-adjusted table width
|
|
711
|
+
const cellWidthPx = Math.round((widthPercent / 100) * adjustedTableWidth);
|
|
507
712
|
if (id) {
|
|
508
713
|
const child = rootData[id];
|
|
509
714
|
const { style: cellStyle = {}, props: childProps = {} } = child.data;
|
|
510
715
|
const verticalAlign = cellStyle.verticalAlign || "top";
|
|
511
716
|
const childVisible = !childProps.hideOnDesktop;
|
|
512
717
|
const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
|
|
513
|
-
// Only render if visible
|
|
514
718
|
if (childVisible) {
|
|
515
|
-
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent,
|
|
516
|
-
//
|
|
517
|
-
|
|
719
|
+
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth);
|
|
720
|
+
// bgcolor on the cell <td> ensures background-color survives Outlook
|
|
721
|
+
// compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
|
|
722
|
+
const cellBgColor = cellStyle.backgroundColor || '';
|
|
723
|
+
const cellBgAttr = cellBgColor ? ` bgcolor="${cellBgColor}"` : '';
|
|
518
724
|
html += `
|
|
519
725
|
<td
|
|
520
|
-
width="${
|
|
521
|
-
class="${[
|
|
522
|
-
responsive ? "stack-column" : "",
|
|
523
|
-
visibilityClass,
|
|
524
|
-
].filter(Boolean).join(" ")}"
|
|
726
|
+
width="${cellWidthPx}"${cellBgAttr}
|
|
727
|
+
class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
|
|
525
728
|
style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
|
|
526
729
|
>
|
|
527
730
|
${childHtml}
|
|
528
731
|
</td>`;
|
|
732
|
+
// Spacer td between columns — fixed pixel width, invisible to screen readers
|
|
733
|
+
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
734
|
+
html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
735
|
+
}
|
|
529
736
|
}
|
|
530
737
|
}
|
|
531
738
|
else {
|
|
532
|
-
// SAFE empty cell (keeps layout stable)
|
|
533
|
-
// OUTLOOK FIX: Calculate pixel width for Outlook
|
|
534
|
-
const cellWidthPx = Math.round((widthPercent / 100) * cellWidthInPx);
|
|
535
739
|
html += `
|
|
536
|
-
<td width="${
|
|
740
|
+
<td width="${cellWidthPx}"
|
|
537
741
|
${responsive ? 'class="stack-column"' : ""}
|
|
538
742
|
style="width:${cellWidthPx}px;vertical-align:top;">
|
|
539
743
|
</td>`;
|
|
744
|
+
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
745
|
+
html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
746
|
+
}
|
|
540
747
|
}
|
|
541
748
|
}
|
|
542
749
|
html += "</tr>";
|
|
@@ -550,6 +757,60 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
550
757
|
</table>
|
|
551
758
|
<!--<![endif]-->
|
|
552
759
|
`;
|
|
760
|
+
// ── Background image: canonical multi-client approach ────────────────────
|
|
761
|
+
//
|
|
762
|
+
// Problem: `background-image` on a <table> element is stripped by:
|
|
763
|
+
// • New Outlook Mac / Windows (Chromium-based app)
|
|
764
|
+
// • Outlook.com
|
|
765
|
+
// • Old Outlook (Word engine) — ignores CSS entirely
|
|
766
|
+
//
|
|
767
|
+
// Solution: wrap the grid in an outer <table><tr><td> where the <td> carries
|
|
768
|
+
// the background. Different clients pick it up via different mechanisms:
|
|
769
|
+
//
|
|
770
|
+
// background="" attribute on <td> → Yahoo Mail, older webmail
|
|
771
|
+
// CSS background-image on <td> → Gmail, Apple Mail, new Outlook Mac ✓
|
|
772
|
+
// VML v:rect inside the <td> → Old Outlook (Word engine) ✓
|
|
773
|
+
//
|
|
774
|
+
// The inner grid tables have NO background so the outer <td> bg shows through.
|
|
775
|
+
if (rawBgImageUrl || isGradient) {
|
|
776
|
+
const vmlFill = isGradient
|
|
777
|
+
? (() => {
|
|
778
|
+
const vmlAngle = cssAngleToVml(parsedGradient?.angle || 180);
|
|
779
|
+
const c1 = parsedGradient?.fallback || '#ffffff';
|
|
780
|
+
const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
|
|
781
|
+
return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
|
|
782
|
+
})()
|
|
783
|
+
: `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
|
|
784
|
+
html = `
|
|
785
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}" role="presentation"
|
|
786
|
+
style="border-collapse:collapse;width:${msoTableWidth}px;">
|
|
787
|
+
<tr>
|
|
788
|
+
<td width="${msoTableWidth}" bgcolor="${fallbackBgColor}" valign="top"
|
|
789
|
+
style="
|
|
790
|
+
width:${msoTableWidth}px;
|
|
791
|
+
background-color:${fallbackBgColor};
|
|
792
|
+
${isGradient ? `background:${backgroundImage};` : `background:url('${rawBgImageUrl}') center/cover no-repeat;`}
|
|
793
|
+
">
|
|
794
|
+
|
|
795
|
+
<!--[if gte mso 9]>
|
|
796
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
797
|
+
fill="true" stroke="false"
|
|
798
|
+
style="width:${msoTableWidth}px;">
|
|
799
|
+
${vmlFill}
|
|
800
|
+
<v:textbox inset="0,0,0,0">
|
|
801
|
+
<![endif]-->
|
|
802
|
+
|
|
803
|
+
${html}
|
|
804
|
+
|
|
805
|
+
<!--[if gte mso 9]>
|
|
806
|
+
</v:textbox>
|
|
807
|
+
</v:rect>
|
|
808
|
+
<![endif]-->
|
|
809
|
+
|
|
810
|
+
</td>
|
|
811
|
+
</tr>
|
|
812
|
+
</table>`;
|
|
813
|
+
}
|
|
553
814
|
return html;
|
|
554
815
|
}
|
|
555
816
|
async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx) {
|
|
@@ -563,16 +824,39 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
|
|
|
563
824
|
// OUTLOOK FIX: Calculate the actual cell width in pixels based on percentage
|
|
564
825
|
// If parent is 600px and cell is 50%, cell width should be 300px, not 600px
|
|
565
826
|
const cellWidthPx = Math.round((cellWidthPercent / 100) * parentCellWidthPx);
|
|
827
|
+
// Subtract the cell's own padding so children receive the actual content-area width.
|
|
828
|
+
// Old Outlook honours explicit img/table width attributes — if a child is sized to the
|
|
829
|
+
// full column width (ignoring padding) it overflows and expands the column.
|
|
830
|
+
const cellPad = style?.padding || {};
|
|
831
|
+
const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
|
|
832
|
+
const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
|
|
833
|
+
const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
|
|
566
834
|
// OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
|
|
567
|
-
const safeCellWidthPx = Math.min(
|
|
835
|
+
const safeCellWidthPx = Math.min(contentWidthPx, 600);
|
|
568
836
|
for (const childId of childrenIds) {
|
|
569
837
|
const child = rootData[childId];
|
|
570
838
|
if (child) {
|
|
571
839
|
parts.push(await convertToHtml(child, rootData, safeCellWidthPx));
|
|
572
840
|
}
|
|
573
841
|
}
|
|
842
|
+
const borderRadius = style?.borderRadius || 0;
|
|
843
|
+
const bgColor = style?.backgroundColor || "transparent";
|
|
844
|
+
// IMPORTANT: radius only for non-Outlook
|
|
845
|
+
const wrapped = `
|
|
846
|
+
<!--[if !mso]><!-->
|
|
847
|
+
<div style="
|
|
848
|
+
border-radius:${borderRadius}px;
|
|
849
|
+
overflow:hidden;
|
|
850
|
+
background-color:${bgColor};
|
|
851
|
+
">
|
|
852
|
+
<!--<![endif]-->
|
|
853
|
+
${parts.join("")}
|
|
854
|
+
<!--[if !mso]><!-->
|
|
855
|
+
</div>
|
|
856
|
+
<!--<![endif]-->
|
|
857
|
+
`;
|
|
574
858
|
return {
|
|
575
|
-
html:
|
|
859
|
+
html: wrapped,
|
|
576
860
|
styles,
|
|
577
861
|
};
|
|
578
862
|
}
|
|
@@ -666,7 +950,8 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
|
|
|
666
950
|
coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
667
951
|
href="${videoLink}"
|
|
668
952
|
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
669
|
-
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;
|
|
953
|
+
<v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;"
|
|
954
|
+
stroked="t"
|
|
670
955
|
strokeweight="${borderWidth}px"
|
|
671
956
|
strokecolor="${borderColor}"
|
|
672
957
|
${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
|
|
@@ -869,7 +1154,7 @@ async function convertShapeBlock(blockData) {
|
|
|
869
1154
|
</div>`;
|
|
870
1155
|
}
|
|
871
1156
|
// Outlook (VML) fallback
|
|
872
|
-
const outlookContent =
|
|
1157
|
+
const outlookContent = appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
|
|
873
1158
|
shape,
|
|
874
1159
|
imageUrl,
|
|
875
1160
|
backgroundColor,
|
|
@@ -1001,27 +1286,24 @@ function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth
|
|
|
1001
1286
|
}
|
|
1002
1287
|
function convertVerticalDividerBlockToHtml(blockData) {
|
|
1003
1288
|
const { style, props } = blockData.data;
|
|
1004
|
-
const { width, height, dividerColor,
|
|
1289
|
+
const { width, height, dividerColor, padding, backgroundColor } = style;
|
|
1005
1290
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
1006
|
-
|
|
1007
|
-
const convertedStyle = buildStyles(rest, {
|
|
1291
|
+
const outerStyles = buildStyles({ padding, backgroundColor }, {
|
|
1008
1292
|
perChanges: [],
|
|
1009
1293
|
pxChanges: allPxAttributes,
|
|
1010
1294
|
});
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
`;
|
|
1026
|
-
return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
|
|
1295
|
+
return `
|
|
1296
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
1297
|
+
style="${exports.tableCommonStyle} max-width:600px;" class="${visibilityClass}">
|
|
1298
|
+
<tr>
|
|
1299
|
+
<td style="${outerStyles}; text-align:center; vertical-align:middle;">
|
|
1300
|
+
<!--[if mso | IE]>
|
|
1301
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="${dividerColor}" style="width:${width}px;height:${height}px;" stroke="f"></v:rect>
|
|
1302
|
+
<![endif]-->
|
|
1303
|
+
<!--[if !mso]><!-->
|
|
1304
|
+
<div style="display:inline-block;width:${width}px;height:${height}px;background:${dividerColor};line-height:0;font-size:0;"> </div>
|
|
1305
|
+
<!--<![endif]-->
|
|
1306
|
+
</td>
|
|
1307
|
+
</tr>
|
|
1308
|
+
</table>`;
|
|
1027
1309
|
}
|