email-builder-utils 1.1.21 → 1.1.23
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/types/Template.d.ts +8 -1
- package/dist/types/Template.d.ts.map +1 -1
- package/dist/types/Template.js +2 -0
- package/dist/utils/common.d.ts +3 -0
- package/dist/utils/common.d.ts.map +1 -0
- package/dist/utils/common.js +33 -0
- package/dist/utils/jsonToHTML.d.ts +6 -0
- package/dist/utils/jsonToHTML.d.ts.map +1 -1
- package/dist/utils/jsonToHTML.js +336 -34
- package/package.json +1 -1
package/dist/types/Template.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export declare enum BlockType {
|
|
|
7
7
|
GRIDCELL = "Column",
|
|
8
8
|
SPACER = "Spacer",
|
|
9
9
|
DIVIDER = "Divider",
|
|
10
|
-
EMAILLAYOUT = "EmailLayout"
|
|
10
|
+
EMAILLAYOUT = "EmailLayout",
|
|
11
|
+
VIDEO = "Video",
|
|
12
|
+
SHAPE = "Shape"
|
|
11
13
|
}
|
|
12
14
|
export declare enum visibility {
|
|
13
15
|
PUBLIC = "PUBLIC",
|
|
@@ -23,6 +25,11 @@ interface IProps {
|
|
|
23
25
|
navigateToUrl: string;
|
|
24
26
|
altText: string;
|
|
25
27
|
cellWidths: number[];
|
|
28
|
+
responsive?: boolean;
|
|
29
|
+
videoUrl?: string;
|
|
30
|
+
youtubeVideoUrl?: string;
|
|
31
|
+
thumbnailUrl?: string;
|
|
32
|
+
shape?: string;
|
|
26
33
|
}
|
|
27
34
|
interface IStyle {
|
|
28
35
|
[key: string]: any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Template.d.ts","sourceRoot":"","sources":["../../src/types/Template.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,IAAI,SAAS;IACb,KAAK,UAAU;IACf,MAAM,WAAW;IACjB,IAAI,YAAY;IAChB,KAAK,UAAU;IACf,QAAQ,WAAW;IACnB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,WAAW,gBAAgB;
|
|
1
|
+
{"version":3,"file":"Template.d.ts","sourceRoot":"","sources":["../../src/types/Template.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,IAAI,SAAS;IACb,KAAK,UAAU;IACf,MAAM,WAAW;IACjB,IAAI,YAAY;IAChB,KAAK,UAAU;IACf,QAAQ,WAAW;IACnB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,WAAW,gBAAgB;IAC3B,KAAK,UAAU;IACf,KAAK,UAAU;CAChB;AAED,oBAAY,UAAU;IACpB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,WAAW,gBAAgB;CAC5B;AAED,UAAU,MAAM;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,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;CAChB;AAED,UAAU,MAAM;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;CACH"}
|
package/dist/types/Template.js
CHANGED
|
@@ -12,6 +12,8 @@ var BlockType;
|
|
|
12
12
|
BlockType["SPACER"] = "Spacer";
|
|
13
13
|
BlockType["DIVIDER"] = "Divider";
|
|
14
14
|
BlockType["EMAILLAYOUT"] = "EmailLayout";
|
|
15
|
+
BlockType["VIDEO"] = "Video";
|
|
16
|
+
BlockType["SHAPE"] = "Shape";
|
|
15
17
|
})(BlockType || (exports.BlockType = BlockType = {}));
|
|
16
18
|
var visibility;
|
|
17
19
|
(function (visibility) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAqBvD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAIrD,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractVimeoId = exports.extractYouTubeId = void 0;
|
|
4
|
+
const extractYouTubeId = (url) => {
|
|
5
|
+
try {
|
|
6
|
+
const u = new URL(url);
|
|
7
|
+
// Accept youtube.com, music.youtube.com, m.youtube.com, etc.
|
|
8
|
+
if (u.hostname === "youtu.be" || u.hostname.endsWith("youtube.com")) {
|
|
9
|
+
// Case: normal watch links (including music.youtube.com/watch?v=xxx)
|
|
10
|
+
const v = u.searchParams.get("v");
|
|
11
|
+
if (v && v.length === 11)
|
|
12
|
+
return v;
|
|
13
|
+
// Case: short links like https://youtu.be/xxxxxxx
|
|
14
|
+
if (u.hostname === "youtu.be") {
|
|
15
|
+
const id = u.pathname.replace("/", "");
|
|
16
|
+
if (id && id.length === 11)
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
// Invalid URL
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
exports.extractYouTubeId = extractYouTubeId;
|
|
28
|
+
const extractVimeoId = (url) => {
|
|
29
|
+
const vimeoRegex = /(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/;
|
|
30
|
+
const match = url.match(vimeoRegex);
|
|
31
|
+
return match ? match[1] : null;
|
|
32
|
+
};
|
|
33
|
+
exports.extractVimeoId = extractVimeoId;
|
|
@@ -8,6 +8,10 @@ interface BlockJsonProps {
|
|
|
8
8
|
altText: string;
|
|
9
9
|
imageUrl: string;
|
|
10
10
|
responsive?: boolean;
|
|
11
|
+
videoUrl?: string;
|
|
12
|
+
youtubeVideoUrl?: string;
|
|
13
|
+
thumbnailUrl?: string;
|
|
14
|
+
shape?: string;
|
|
11
15
|
}
|
|
12
16
|
interface IBlockData {
|
|
13
17
|
type: BlockType;
|
|
@@ -19,5 +23,7 @@ interface IBlockData {
|
|
|
19
23
|
}
|
|
20
24
|
export declare const tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
|
|
21
25
|
export declare function convertToHtml(blockData: IBlockData, rootData: any, cellWidthInPx: number): Promise<string>;
|
|
26
|
+
export declare function convertVideoBlock(blockData: any, cellWidthInPx: number): Promise<string>;
|
|
27
|
+
export declare function convertShapeBlock(blockData: IBlockData): Promise<string>;
|
|
22
28
|
export {};
|
|
23
29
|
//# sourceMappingURL=jsonToHTML.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAUrC,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;CAChB;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;AAOD,eAAO,MAAM,gBAAgB,kDAAkD,CAAC;AAiDhF,wBAAsB,aAAa,CAAC,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAqB9F;AA2UD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAmI5E;AAqJD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,UAAU,mBAuI5D"}
|
package/dist/utils/jsonToHTML.js
CHANGED
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.tableCommonStyle = void 0;
|
|
4
4
|
exports.convertToHtml = convertToHtml;
|
|
5
|
+
exports.convertVideoBlock = convertVideoBlock;
|
|
6
|
+
exports.convertShapeBlock = convertShapeBlock;
|
|
5
7
|
const jimp_1 = require("jimp");
|
|
6
8
|
const types_1 = require("../types");
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
"lineHeight",
|
|
10
|
-
"borderRadius",
|
|
11
|
-
"borderWidth",
|
|
12
|
-
];
|
|
9
|
+
const common_1 = require("./common");
|
|
10
|
+
const addPxToAttributes = ["fontSize", "lineHeight", "borderRadius", "borderWidth"];
|
|
13
11
|
const addPxOrPerToAttributes = ["width", "height"];
|
|
14
12
|
const allPxAttributes = [...addPxToAttributes, ...addPxOrPerToAttributes];
|
|
15
13
|
exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
|
|
@@ -40,8 +38,7 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
40
38
|
return;
|
|
41
39
|
if (value === undefined || value === null || value === "")
|
|
42
40
|
return;
|
|
43
|
-
if ((key === "padding" || key === "buttonPadding") &&
|
|
44
|
-
typeof value === "object") {
|
|
41
|
+
if ((key === "padding" || key === "buttonPadding") && typeof value === "object") {
|
|
45
42
|
const padding = value;
|
|
46
43
|
value = `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`;
|
|
47
44
|
}
|
|
@@ -72,6 +69,10 @@ async function convertToHtml(blockData, rootData, cellWidthInPx) {
|
|
|
72
69
|
return convertDividerBlockToHtml(blockData);
|
|
73
70
|
case types_1.BlockType.SPACER:
|
|
74
71
|
return convertSpacerBlockToHtml(blockData);
|
|
72
|
+
case types_1.BlockType.VIDEO:
|
|
73
|
+
return convertVideoBlock(blockData, cellWidthInPx);
|
|
74
|
+
case types_1.BlockType.SHAPE:
|
|
75
|
+
return await convertShapeBlock(blockData);
|
|
75
76
|
default:
|
|
76
77
|
return "";
|
|
77
78
|
}
|
|
@@ -81,12 +82,6 @@ function appendOutlookSupport(content, contentStyle) {
|
|
|
81
82
|
<table width="100%" style="${exports.tableCommonStyle}"><tr><td style="${contentStyle}">${content}</td></tr></table>
|
|
82
83
|
`;
|
|
83
84
|
}
|
|
84
|
-
// function convertDividerBlockToHtml(blockData: IBlockData) {
|
|
85
|
-
// const { style } = blockData.data;
|
|
86
|
-
// const { thickness, dividerColor, ...rest } = style;
|
|
87
|
-
// const convertedStyle = buildStyles(rest, {perChanges: [], pxChanges: allPxAttributes});
|
|
88
|
-
// return appendOutlookSupport(`<hr style="height:${thickness}px; background-color: ${dividerColor};" />`, convertedStyle);
|
|
89
|
-
// }
|
|
90
85
|
function convertDividerBlockToHtml(blockData) {
|
|
91
86
|
const { style } = blockData.data;
|
|
92
87
|
const { thickness, dividerColor, ...rest } = style;
|
|
@@ -139,15 +134,11 @@ function convertTextBlock(blockData) {
|
|
|
139
134
|
perChanges: [],
|
|
140
135
|
pxChanges: allPxAttributes,
|
|
141
136
|
});
|
|
142
|
-
const sanitizedText = (props.text ?? "")
|
|
143
|
-
.replaceAll(/<p>/g, "<div>")
|
|
144
|
-
.replaceAll(/<\/p>/g, "</div>");
|
|
137
|
+
const sanitizedText = (props.text ?? "").replaceAll(/<p>/g, "<div>").replaceAll(/<\/p>/g, "</div>");
|
|
145
138
|
const navigateToUrl = props.navigateToUrl || "";
|
|
146
139
|
const convertedTextBox = `<div style="display: inline-block; max-width: 100%; box-sizing: border-box; ${convertedTextStyle}">${sanitizedText.replaceAll(/\n/g, "<br>")}</div>`;
|
|
147
140
|
const textContent = appendOutlookSupport(convertedTextBox, styles);
|
|
148
|
-
return navigateToUrl
|
|
149
|
-
? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit; text-decoration:none; cursor:pointer;">${textContent}</a>`
|
|
150
|
-
: textContent;
|
|
141
|
+
return navigateToUrl ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit; text-decoration:none; cursor:pointer;">${textContent}</a>` : textContent;
|
|
151
142
|
}
|
|
152
143
|
async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}) {
|
|
153
144
|
const image = await jimp_1.Jimp.read(imageUrl);
|
|
@@ -160,12 +151,8 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
160
151
|
const borderColor = style?.borderColor || "transparent";
|
|
161
152
|
const borderRadius = parseInt(style?.borderRadius) || 0;
|
|
162
153
|
const useRoundRect = borderRadius > 0;
|
|
163
|
-
const arcsize = useRoundRect
|
|
164
|
-
|
|
165
|
-
: "";
|
|
166
|
-
const borderAttributes = borderWidth > 0
|
|
167
|
-
? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
|
|
168
|
-
: `stroked="false"`;
|
|
154
|
+
const arcsize = useRoundRect ? Math.min(borderRadius / scaledHeight, 1).toFixed(2) : "";
|
|
155
|
+
const borderAttributes = borderWidth > 0 ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="false"`;
|
|
169
156
|
const outlookImage = `<!--[if mso]>
|
|
170
157
|
<v:${useRoundRect ? "roundrect" : "rect"} xmlns:v="urn:schemas-microsoft-com:vml"
|
|
171
158
|
style="width:${scaledWidth}px;height:${scaledHeight}px;"
|
|
@@ -208,11 +195,7 @@ async function convertImageBlock(blockData, cellWidthInPx) {
|
|
|
208
195
|
pxChanges: addPxToAttributes,
|
|
209
196
|
});
|
|
210
197
|
const imageElement = `<img src="${imageUrl}" alt="${altText}" style="${imageTagStyles}; max-width: ${originalWidth}px; max-height: ${originalHeight}px;" />`;
|
|
211
|
-
const innerContainerWidth = ((typeof width === "string" ? parseInt(width.replace("%", "")) : width) /
|
|
212
|
-
100) *
|
|
213
|
-
(cellWidthInPx -
|
|
214
|
-
(style?.padding?.left || 0) -
|
|
215
|
-
(style?.padding?.right || 0));
|
|
198
|
+
const innerContainerWidth = ((typeof width === "string" ? parseInt(width.replace("%", "")) : width) / 100) * (cellWidthInPx - (style?.padding?.left || 0) - (style?.padding?.right || 0));
|
|
216
199
|
const outlookImage = await appendOutlookForImage(imageElement, cellWidthInPx, innerContainerWidth, imageUrl, style);
|
|
217
200
|
const imageContent = appendOutlookSupport(outlookImage, containerStyles);
|
|
218
201
|
return navigateToUrl
|
|
@@ -221,9 +204,7 @@ async function convertImageBlock(blockData, cellWidthInPx) {
|
|
|
221
204
|
}
|
|
222
205
|
function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
|
|
223
206
|
const { width = 200, height = 44, borderRadius = 0, borderColor = "transparent", borderWidth = 0, buttonColor = "none", buttonPadding = { top: 0, bottom: 0, left: 0, right: 0 }, color = "#000000", fontFamily = "Arial, sans-serif", fontSize = 16, fontWeight = 400, } = buttonStyle;
|
|
224
|
-
const borderAttributes = borderWidth > 0
|
|
225
|
-
? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
|
|
226
|
-
: `stroked="false"`;
|
|
207
|
+
const borderAttributes = borderWidth > 0 ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="false"`;
|
|
227
208
|
return `
|
|
228
209
|
<!--[if mso]>
|
|
229
210
|
<v:${borderRadius ? "roundrect" : "rect"} xmlns:v="urn:schemas-microsoft-com:vml" href="${navigateToUrl}"
|
|
@@ -337,3 +318,324 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
|
|
|
337
318
|
styles,
|
|
338
319
|
};
|
|
339
320
|
}
|
|
321
|
+
async function convertVideoBlock(blockData, cellWidthInPx) {
|
|
322
|
+
const { style, props } = blockData.data;
|
|
323
|
+
const { videoUrl, youtubeVideoUrl, thumbnailUrl, altText } = props || {};
|
|
324
|
+
const videoLink = youtubeVideoUrl || videoUrl || "#";
|
|
325
|
+
let resolvedThumbnail = thumbnailUrl || "https://via.placeholder.com/480x360?text=No+Thumbnail";
|
|
326
|
+
if (youtubeVideoUrl) {
|
|
327
|
+
const youtubeId = (0, common_1.extractYouTubeId)(youtubeVideoUrl);
|
|
328
|
+
const vimeoId = (0, common_1.extractVimeoId)(youtubeVideoUrl);
|
|
329
|
+
if (youtubeId) {
|
|
330
|
+
resolvedThumbnail = `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;
|
|
331
|
+
}
|
|
332
|
+
else if (vimeoId) {
|
|
333
|
+
try {
|
|
334
|
+
const res = await fetch(`https://vimeo.com/api/v2/video/${vimeoId}.json`);
|
|
335
|
+
if (res.ok) {
|
|
336
|
+
const data = await res.json();
|
|
337
|
+
resolvedThumbnail = data?.[0]?.thumbnail_large || resolvedThumbnail;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch (_) { }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Determine width logic
|
|
344
|
+
let percentWidth;
|
|
345
|
+
if (typeof style?.width === "string" && style.width.trim().endsWith("%")) {
|
|
346
|
+
percentWidth = style.width.trim();
|
|
347
|
+
}
|
|
348
|
+
else if (typeof style?.width === "number") {
|
|
349
|
+
percentWidth = `${style.width}%`;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
percentWidth = "100%";
|
|
353
|
+
}
|
|
354
|
+
const innerContainerWidth = (parseFloat(percentWidth) / 100) * (cellWidthInPx - (style?.padding?.left || 0) - (style?.padding?.right || 0));
|
|
355
|
+
const aspectRatio = 16 / 9;
|
|
356
|
+
const calculatedHeight = innerContainerWidth / aspectRatio;
|
|
357
|
+
const outerContainerStyles = buildStyles({
|
|
358
|
+
...style,
|
|
359
|
+
width: undefined,
|
|
360
|
+
}, {
|
|
361
|
+
perChanges: addPxOrPerToAttributes,
|
|
362
|
+
pxChanges: addPxToAttributes,
|
|
363
|
+
});
|
|
364
|
+
const borderRadius = parseInt(style?.borderRadius) || 0;
|
|
365
|
+
const borderWidth = parseInt(style?.borderWidth) || 0;
|
|
366
|
+
const borderColor = style?.borderColor || "transparent";
|
|
367
|
+
// Play icon size
|
|
368
|
+
const playIconWidth = 65;
|
|
369
|
+
const playIconHeight = 46;
|
|
370
|
+
// VML centering math (for Outlook)
|
|
371
|
+
const vmlLeft = innerContainerWidth / 2 - playIconWidth / 2;
|
|
372
|
+
const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
|
|
373
|
+
const videoContent = `
|
|
374
|
+
<!--[if mso]>
|
|
375
|
+
<v:group xmlns:v="urn:schemas-microsoft-com:vml" coordsize="${innerContainerWidth},${calculatedHeight}"
|
|
376
|
+
coordorigin="0,0"
|
|
377
|
+
href="${videoLink}"
|
|
378
|
+
style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
379
|
+
<v:rect fill="t" stroked="f" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;">
|
|
380
|
+
<v:fill src="${resolvedThumbnail}" type="frame"/>
|
|
381
|
+
</v:rect>
|
|
382
|
+
<v:shape type="#_x0000_t75"
|
|
383
|
+
style="position:absolute;
|
|
384
|
+
left:${vmlLeft.toFixed(1)}px;
|
|
385
|
+
top:${vmlTop.toFixed(1)}px;
|
|
386
|
+
width:${playIconWidth}px;
|
|
387
|
+
height:${playIconHeight}px;"
|
|
388
|
+
alt="Play" href="${videoLink}" title="${altText || "Video"}"
|
|
389
|
+
stroked="f" filled="t">
|
|
390
|
+
<v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
|
|
391
|
+
</v:shape>
|
|
392
|
+
</v:group>
|
|
393
|
+
<![endif]-->
|
|
394
|
+
|
|
395
|
+
<!--[if !mso]><!-->
|
|
396
|
+
<table
|
|
397
|
+
width="${innerContainerWidth}"
|
|
398
|
+
cellpadding="0"
|
|
399
|
+
cellspacing="0"
|
|
400
|
+
border="0"
|
|
401
|
+
role="presentation"
|
|
402
|
+
style="
|
|
403
|
+
background-image: url('${resolvedThumbnail}');
|
|
404
|
+
background-size: cover;
|
|
405
|
+
background-position: center;
|
|
406
|
+
|
|
407
|
+
max-width: ${innerContainerWidth}px;
|
|
408
|
+
height: ${calculatedHeight}px;
|
|
409
|
+
box-sizing: border-box;
|
|
410
|
+
"
|
|
411
|
+
align="center"
|
|
412
|
+
>
|
|
413
|
+
<tr>
|
|
414
|
+
<td style="height: ${calculatedHeight}px; padding: 0; text-align: center; vertical-align: middle; border-radius: ${borderRadius}px;
|
|
415
|
+
border: ${borderWidth}px solid ${borderColor};" align="center" valign="middle">
|
|
416
|
+
<a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
|
|
417
|
+
<img
|
|
418
|
+
src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
|
|
419
|
+
width="${playIconWidth}"
|
|
420
|
+
alt="Play"
|
|
421
|
+
style="display: block; border: 0; outline: none; text-decoration: none; height: auto;"
|
|
422
|
+
/>
|
|
423
|
+
</a>
|
|
424
|
+
</td>
|
|
425
|
+
</tr>
|
|
426
|
+
</table>
|
|
427
|
+
<!--<![endif]-->
|
|
428
|
+
`;
|
|
429
|
+
const wrapperHtml = `
|
|
430
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse;">
|
|
431
|
+
<tr>
|
|
432
|
+
<td align="center" style="padding:0; ${outerContainerStyles}">
|
|
433
|
+
<div style="display: inline-block; width: ${percentWidth}; max-width: ${cellWidthInPx}px; box-sizing: border-box;">
|
|
434
|
+
${videoContent}
|
|
435
|
+
</div>
|
|
436
|
+
</td>
|
|
437
|
+
</tr>
|
|
438
|
+
</table>
|
|
439
|
+
`;
|
|
440
|
+
return wrapperHtml;
|
|
441
|
+
}
|
|
442
|
+
// Enhanced Shape Block HTML Conversion using appendOutlookForShape
|
|
443
|
+
// ---------- helpers ----------
|
|
444
|
+
function computeArcSize(borderRadius, widthPx) {
|
|
445
|
+
if (!borderRadius)
|
|
446
|
+
return "0";
|
|
447
|
+
if (typeof borderRadius === "number")
|
|
448
|
+
return Math.min(borderRadius / widthPx, 1).toFixed(2);
|
|
449
|
+
const s = borderRadius.toString().trim();
|
|
450
|
+
if (s.endsWith("%")) {
|
|
451
|
+
const pct = parseFloat(s.replace("%", "")) || 0;
|
|
452
|
+
return Math.min(pct / 100, 1).toFixed(2);
|
|
453
|
+
}
|
|
454
|
+
// assume px or raw number
|
|
455
|
+
const px = parseFloat(s.replace("px", "")) || 0;
|
|
456
|
+
return Math.min(px / widthPx, 1).toFixed(2);
|
|
457
|
+
}
|
|
458
|
+
// ---------- Outlook (MSO) wrapper ----------
|
|
459
|
+
async function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts) {
|
|
460
|
+
// Use the inner container width for VML sizing (exact user dims)
|
|
461
|
+
const widthPx = Math.round(Math.min(outerContainerWidth, innerContainerWidth));
|
|
462
|
+
const heightPx = Math.max(1, Math.round(opts.heightPx));
|
|
463
|
+
const vml = buildVMLShape({
|
|
464
|
+
shape: opts.shape,
|
|
465
|
+
widthPx,
|
|
466
|
+
heightPx,
|
|
467
|
+
imageUrl: opts.msoBakeImageWithText || opts.imageUrl,
|
|
468
|
+
backgroundColor: opts.shapeColor || opts.backgroundColor,
|
|
469
|
+
borderWidth: opts.borderWidth,
|
|
470
|
+
borderColor: opts.borderColor,
|
|
471
|
+
borderRadius: opts.borderRadius,
|
|
472
|
+
text: opts.text,
|
|
473
|
+
textColor: opts.textColor,
|
|
474
|
+
// pass raw flag so buildVMLShape knows if image already has text baked-in
|
|
475
|
+
msoHasBakedText: Boolean(opts.msoBakeImageWithText),
|
|
476
|
+
});
|
|
477
|
+
const outlookAlignment = opts.alignment === "center" ? "center" : opts.alignment === "right" ? "right" : "left";
|
|
478
|
+
// Wrap the VML inside a table so Outlook aligns it correctly
|
|
479
|
+
return `<!--[if mso]>
|
|
480
|
+
<table align="${outlookAlignment}" border="0" cellpadding="0" cellspacing="0" style="display:inline-block;">
|
|
481
|
+
<tr>
|
|
482
|
+
<td style="padding:${opts.padding?.top || 0}px ${opts.padding?.right || 0}px ${opts.padding?.bottom || 0}px ${opts.padding?.left || 0}px;">
|
|
483
|
+
${vml}
|
|
484
|
+
</td>
|
|
485
|
+
</tr>
|
|
486
|
+
</table>
|
|
487
|
+
<![endif]-->`;
|
|
488
|
+
}
|
|
489
|
+
// ---------- VML builder (produces shape + text inside it for MSO) ----------
|
|
490
|
+
function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor, msoHasBakedText = false, }) {
|
|
491
|
+
const bw = borderWidth || 0;
|
|
492
|
+
const bc = borderColor || "transparent";
|
|
493
|
+
const hasBorder = bw > 0;
|
|
494
|
+
const borderAttributes = hasBorder ? `strokeweight="${bw}px" strokecolor="${bc}"` : `stroked="false"`;
|
|
495
|
+
const fillColor = backgroundColor || "#2F80ED";
|
|
496
|
+
// choose tag and extra attributes
|
|
497
|
+
let tag = "rect";
|
|
498
|
+
let extraAttr = "";
|
|
499
|
+
if (shape === "circle" || shape === "oval")
|
|
500
|
+
tag = "oval";
|
|
501
|
+
if (shape === "rounded" || (borderRadius && borderRadius !== "0")) {
|
|
502
|
+
tag = "roundrect";
|
|
503
|
+
extraAttr = ` arcsize="${computeArcSize(borderRadius, widthPx)}"`;
|
|
504
|
+
}
|
|
505
|
+
// image fill (if provided)
|
|
506
|
+
const fillMarkup = imageUrl ? `<v:fill src="${imageUrl}" type="frame" aspect="atleast" />` : "";
|
|
507
|
+
// If MSO is given a baked image with text, don't produce a v:textbox overlay text (image already contains text)
|
|
508
|
+
const includeTextbox = !!text && !msoHasBakedText;
|
|
509
|
+
// v:textbox: use a table + cell to center the text; avoids many Word quirks
|
|
510
|
+
const textboxInner = includeTextbox
|
|
511
|
+
? `<v:textbox inset="0,0,0,0">
|
|
512
|
+
<center style="width:${widthPx}px;height:${heightPx}px;display:block;">
|
|
513
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="${widthPx}" height="${heightPx}" style="border-collapse:collapse;">
|
|
514
|
+
<tr>
|
|
515
|
+
<td align="center" valign="middle" style="font-family:Arial, sans-serif;font-size:14px;line-height:1;color:${textColor || "#000"};padding:6px;">
|
|
516
|
+
${text}
|
|
517
|
+
</td>
|
|
518
|
+
</tr>
|
|
519
|
+
</table>
|
|
520
|
+
</center>
|
|
521
|
+
</v:textbox>`
|
|
522
|
+
: // keep an empty textbox so shape sizing behaves consistently when no text
|
|
523
|
+
`<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
|
|
524
|
+
// If there is no imageUrl and no textbox content, use fillcolor for background
|
|
525
|
+
const fillAttr = imageUrl ? 'fill="true"' : `fill="true" fillcolor="${fillColor}"`;
|
|
526
|
+
return `
|
|
527
|
+
<v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
|
|
528
|
+
style="width:${widthPx}px;height:${heightPx}px;v-text-anchor:middle;"
|
|
529
|
+
${borderAttributes} ${fillAttr}${extraAttr}>
|
|
530
|
+
${fillMarkup}
|
|
531
|
+
${textboxInner}
|
|
532
|
+
</v:${tag}>`;
|
|
533
|
+
}
|
|
534
|
+
// ---------- convertShapeBlock (updated, keeps your structure) ----------
|
|
535
|
+
async function convertShapeBlock(blockData) {
|
|
536
|
+
const { style, props } = blockData.data;
|
|
537
|
+
const { shape, text, textColor = "#000000", imageUrl } = props || {};
|
|
538
|
+
const { width = "100", height = "150", padding = {}, backgroundColor = "#2F80ED", borderRadius, borderWidth = 0, borderStyle = "solid", borderColor = "transparent", customCss, shapeColor, alignment = "left", msoBakeImageWithText, } = style || {};
|
|
539
|
+
const borderRadiusMap = {
|
|
540
|
+
rectangle: "0",
|
|
541
|
+
rounded: "10px",
|
|
542
|
+
circle: "50%",
|
|
543
|
+
oval: "50%",
|
|
544
|
+
};
|
|
545
|
+
let resolvedBorderRadius = borderRadius || borderRadiusMap[shape] || "0";
|
|
546
|
+
let resolvedWidthPx = typeof width === "number" ? width : parseInt(width.toString().replace("px", ""), 10) || 100;
|
|
547
|
+
let resolvedHeightPx = typeof height === "number" ? height : parseInt(height.toString().replace("px", ""), 10) || 150;
|
|
548
|
+
// Force circle → square
|
|
549
|
+
if (shape === "circle") {
|
|
550
|
+
const side = Math.min(resolvedWidthPx, resolvedHeightPx);
|
|
551
|
+
resolvedWidthPx = side;
|
|
552
|
+
resolvedHeightPx = side;
|
|
553
|
+
resolvedBorderRadius = "50%";
|
|
554
|
+
}
|
|
555
|
+
const finalWidthPx = resolvedWidthPx;
|
|
556
|
+
const finalHeightPx = resolvedHeightPx;
|
|
557
|
+
const alignmentStyles = {
|
|
558
|
+
left: "margin-right:auto;margin-left:0;",
|
|
559
|
+
center: "margin-left:auto;margin-right:auto;",
|
|
560
|
+
right: "margin-left:auto;margin-right:0;",
|
|
561
|
+
};
|
|
562
|
+
const alignmentStyle = alignmentStyles[alignment] || "";
|
|
563
|
+
const finalBackgroundColor = shapeColor || backgroundColor;
|
|
564
|
+
// --- Modern clients content ---
|
|
565
|
+
let nonMsoContent = "";
|
|
566
|
+
// Case 1: Image + Text → use background-image
|
|
567
|
+
if (imageUrl && text) {
|
|
568
|
+
nonMsoContent = `
|
|
569
|
+
<div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
|
|
570
|
+
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
571
|
+
border-radius:${resolvedBorderRadius};
|
|
572
|
+
background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
|
|
573
|
+
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
574
|
+
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;">
|
|
575
|
+
<div style="color:${textColor};text-align:center;padding:8px;box-sizing:border-box;word-break:break-word;
|
|
576
|
+
border-radius:4px;max-width:90%;">
|
|
577
|
+
${text}
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
</div>`;
|
|
581
|
+
}
|
|
582
|
+
// Case 2: Image only → use <img>
|
|
583
|
+
else if (imageUrl) {
|
|
584
|
+
nonMsoContent = `
|
|
585
|
+
<div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
|
|
586
|
+
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
587
|
+
border-radius:${resolvedBorderRadius};
|
|
588
|
+
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
589
|
+
<img src="${imageUrl}" alt="${text || "Shape image"}"
|
|
590
|
+
width="${finalWidthPx}" height="${finalHeightPx}"
|
|
591
|
+
style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
|
|
592
|
+
</div>`;
|
|
593
|
+
}
|
|
594
|
+
// Case 3: No image → solid background
|
|
595
|
+
else {
|
|
596
|
+
nonMsoContent = `
|
|
597
|
+
<div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
|
|
598
|
+
background:${finalBackgroundColor};
|
|
599
|
+
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
600
|
+
border-radius:${resolvedBorderRadius};
|
|
601
|
+
${alignmentStyle}${customCss || ""}">
|
|
602
|
+
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;
|
|
603
|
+
color:${textColor};text-align:center;padding:8px;box-sizing:border-box;word-break:break-word;">
|
|
604
|
+
${text || ""}
|
|
605
|
+
</div>
|
|
606
|
+
</div>`;
|
|
607
|
+
}
|
|
608
|
+
// --- Old Outlook (MSO) VML ---
|
|
609
|
+
const outlookContent = await appendOutlookForShape(nonMsoContent, finalWidthPx, finalWidthPx, {
|
|
610
|
+
shape,
|
|
611
|
+
imageUrl,
|
|
612
|
+
backgroundColor,
|
|
613
|
+
shapeColor,
|
|
614
|
+
borderWidth,
|
|
615
|
+
borderColor,
|
|
616
|
+
borderRadius: resolvedBorderRadius,
|
|
617
|
+
heightPx: finalHeightPx,
|
|
618
|
+
text,
|
|
619
|
+
textColor,
|
|
620
|
+
alignment,
|
|
621
|
+
padding,
|
|
622
|
+
msoBakeImageWithText,
|
|
623
|
+
});
|
|
624
|
+
// Wrap in container table
|
|
625
|
+
const containerTable = `
|
|
626
|
+
<table width="100%" style="border-collapse:collapse;table-layout:fixed;">
|
|
627
|
+
<tr>
|
|
628
|
+
<td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;
|
|
629
|
+
background-color:transparent;text-align:${alignment};">
|
|
630
|
+
${outlookContent}
|
|
631
|
+
<!--[if !mso]><!-->
|
|
632
|
+
${nonMsoContent}
|
|
633
|
+
<!--<![endif]-->
|
|
634
|
+
</td>
|
|
635
|
+
</tr>
|
|
636
|
+
</table>`;
|
|
637
|
+
return appendOutlookSupport(containerTable, buildStyles(style, {
|
|
638
|
+
perChanges: addPxOrPerToAttributes,
|
|
639
|
+
pxChanges: allPxAttributes,
|
|
640
|
+
}));
|
|
641
|
+
}
|