email-builder-utils 1.1.45 → 1.1.47

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