email-builder-utils 1.1.46 → 1.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) 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 +130 -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 +72 -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 +256 -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 +151 -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/common.d.ts +1 -0
  26. package/dist/utils/common.d.ts.map +1 -1
  27. package/dist/utils/common.js +10 -0
  28. package/dist/utils/convertJsonToHtml.d.ts.map +1 -1
  29. package/dist/utils/convertJsonToHtml.js +135 -74
  30. package/dist/utils/gradientUtils.d.ts +8 -0
  31. package/dist/utils/gradientUtils.d.ts.map +1 -0
  32. package/dist/utils/gradientUtils.js +68 -0
  33. package/dist/utils/jsonToHTML.d.ts +2 -29
  34. package/dist/utils/jsonToHTML.d.ts.map +1 -1
  35. package/dist/utils/jsonToHTML.js +18 -1560
  36. package/dist/utils/outlookSupport.d.ts +4 -207
  37. package/dist/utils/outlookSupport.d.ts.map +1 -1
  38. package/dist/utils/outlookSupport.js +86 -453
  39. package/package.json +1 -1
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertShapeBlock = convertShapeBlock;
4
+ const common_1 = require("../common");
5
+ function computeArcSize(borderRadius, widthPx) {
6
+ if (!borderRadius)
7
+ return "0";
8
+ if (typeof borderRadius === "number")
9
+ return Math.min(borderRadius / widthPx, 1).toFixed(2);
10
+ const s = borderRadius.toString().trim();
11
+ if (s.endsWith("%"))
12
+ return Math.min((parseFloat(s.replace("%", "")) || 0) / 100, 1).toFixed(2);
13
+ return Math.min((parseFloat(s.replace("px", "")) || 0) / widthPx, 1).toFixed(2);
14
+ }
15
+ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor = "#000000", textSize = 14, verticalAlign = "middle", textAlign = "center", msoHasBakedText = false, }) {
16
+ const bw = borderWidth || 0;
17
+ const bc = borderColor || "transparent";
18
+ const borderAttrs = bw > 0 ? `strokeweight="${bw}px" strokecolor="${bc}"` : `stroked="false"`;
19
+ const fillColor = backgroundColor || "#2F80ED";
20
+ const fillMarkup = `<v:fill ${imageUrl ? `src="${imageUrl}" type="frame" aspect="atleast"` : ""} color="${fillColor}" />`;
21
+ let tag = "rect";
22
+ let extraAttr = "";
23
+ if (shape === "circle" || shape === "oval") {
24
+ tag = "oval";
25
+ }
26
+ else if (shape === "rounded") {
27
+ tag = "roundrect";
28
+ extraAttr = `arcsize="${computeArcSize(borderRadius, widthPx)}"`;
29
+ }
30
+ const vAlignMap = { top: "top", middle: "middle", bottom: "bottom" };
31
+ const hAlignMap = { left: "left", center: "center", right: "right", justify: "left" };
32
+ const vAlign = vAlignMap[verticalAlign] || "middle";
33
+ const hAlign = hAlignMap[textAlign] || "center";
34
+ const safeFontSize = Math.max(Math.round(textSize), 10);
35
+ const textboxMarkup = text && !msoHasBakedText
36
+ ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
37
+ <div style="display:table;width:100%;height:100%;">
38
+ <div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
39
+ <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
40
+ ${text}
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </v:textbox>`
45
+ : `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
46
+ return `
47
+ <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
48
+ style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
49
+ ${borderAttrs}
50
+ fill="true" fillcolor="${fillColor}"${extraAttr}>
51
+ ${fillMarkup}
52
+ ${textboxMarkup}
53
+ </v:${tag}>`;
54
+ }
55
+ function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts, visibilityClass) {
56
+ const widthPx = Math.round(Math.min(outerContainerWidth, innerContainerWidth));
57
+ const heightPx = Math.max(1, Math.round(opts.heightPx));
58
+ const vml = buildVMLShape({
59
+ shape: opts.shape, widthPx, heightPx,
60
+ imageUrl: opts.msoBakeImageWithText || opts.imageUrl,
61
+ backgroundColor: opts.shapeColor || opts.backgroundColor,
62
+ borderWidth: opts.borderWidth, borderColor: opts.borderColor,
63
+ borderRadius: opts.borderRadius, text: opts.text,
64
+ textColor: opts.textColor, textSize: opts.textSize,
65
+ verticalAlign: opts.verticalAlign, textAlign: opts.textAlign,
66
+ msoHasBakedText: Boolean(opts.msoBakeImageWithText),
67
+ });
68
+ const pad = opts.padding || {};
69
+ const align = opts.alignment || "left";
70
+ const valign = opts.verticalAlign || "middle";
71
+ const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
72
+ if (shouldHideInOutlook) {
73
+ return `<!--[if !mso]><!-->
74
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
75
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
76
+ <tr>
77
+ <td valign="${valign}"
78
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
79
+ ${vml}
80
+ </td>
81
+ </tr>
82
+ </table>
83
+ <!--<![endif]-->`;
84
+ }
85
+ return `<!--[if mso]>
86
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
87
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
88
+ <tr>
89
+ <td valign="${valign}"
90
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
91
+ ${vml}
92
+ </td>
93
+ </tr>
94
+ </table>
95
+ <![endif]-->`;
96
+ }
97
+ async function convertShapeBlock(blockData) {
98
+ const { style, props } = blockData.data;
99
+ const { shape, text, imageUrl } = props;
100
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
101
+ 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 || {};
102
+ const borderRadiusMap = {
103
+ rectangle: "0",
104
+ rounded: "10px",
105
+ circle: "50%",
106
+ oval: "50%",
107
+ };
108
+ let resolvedBorderRadius = borderRadius || borderRadiusMap[shape] || "0";
109
+ let resolvedWidthPx = typeof width === "number"
110
+ ? width
111
+ : parseInt(width.toString().replace("px", ""), 10) || 100;
112
+ let resolvedHeightPx = typeof height === "number"
113
+ ? height
114
+ : parseInt(height.toString().replace("px", ""), 10) || 150;
115
+ // --- Shape-specific constraints ---
116
+ if (shape === "circle") {
117
+ const side = Math.min(resolvedWidthPx, resolvedHeightPx);
118
+ resolvedWidthPx = side;
119
+ resolvedHeightPx = side;
120
+ resolvedBorderRadius = "50%";
121
+ }
122
+ else if (shape === "oval") {
123
+ resolvedBorderRadius = "50% / 50%";
124
+ }
125
+ const finalBackgroundColor = shapeColor || backgroundColor;
126
+ // --- Horizontal alignment for outer container ---
127
+ const alignmentStyles = {
128
+ left: "margin-right:auto;margin-left:0;",
129
+ center: "margin-left:auto;margin-right:auto;",
130
+ right: "margin-left:auto;margin-right:0;",
131
+ };
132
+ const alignmentStyle = alignmentStyles[alignment] || "";
133
+ // --- Text + vertical alignment maps ---
134
+ const textAlignMap = {
135
+ left: "left",
136
+ center: "center",
137
+ right: "right",
138
+ justify: "justify",
139
+ };
140
+ const textAlignStyle = textAlignMap[textAlign] || "center";
141
+ // --- Text styling ---
142
+ const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;color:${color};`;
143
+ // ============================
144
+ // Modern HTML (non-MSO)
145
+ // ============================
146
+ let nonMsoContent = "";
147
+ // --- Case 1: Image + Text ---
148
+ if (imageUrl && text) {
149
+ nonMsoContent = `
150
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
151
+ border:${borderWidth}px ${borderStyle} ${borderColor};
152
+ border-radius:${resolvedBorderRadius};
153
+ background-color:${finalBackgroundColor};
154
+ background-image:url('${imageUrl}');
155
+ background-position:center center;
156
+ background-size:cover;
157
+ background-repeat:no-repeat;
158
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
159
+ <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
160
+ style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
161
+ <tr>
162
+ <td align="${textAlignStyle}" valign="${verticalAlign}"
163
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
164
+ style="padding:6px;vertical-align:${verticalAlign};text-align:${textAlignStyle};overflow:hidden;box-sizing:border-box;">
165
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text}</div>
166
+ </td>
167
+ </tr>
168
+ </table>
169
+ </div>`;
170
+ }
171
+ // --- Case 2: Image only ---
172
+ else if (imageUrl) {
173
+ nonMsoContent = `
174
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
175
+ border:${borderWidth}px ${borderStyle} ${borderColor};
176
+ border-radius:${resolvedBorderRadius};
177
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
178
+ <img src="${imageUrl}" alt="${text || "shape image"}"
179
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
180
+ style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
181
+ </div>`;
182
+ }
183
+ // --- Case 3: Text only ---
184
+ else {
185
+ nonMsoContent = `
186
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
187
+ background-color:${finalBackgroundColor};
188
+ border:${borderWidth}px ${borderStyle} ${borderColor};
189
+ border-radius:${resolvedBorderRadius};
190
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
191
+ <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
192
+ style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
193
+ <tr>
194
+ <td align="${textAlignStyle}" valign="${verticalAlign}"
195
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
196
+ style="padding:8px;vertical-align:${verticalAlign};text-align:${textAlignStyle};box-sizing:border-box;">
197
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text || ""}</div>
198
+ </td>
199
+ </tr>
200
+ </table>
201
+ </div>`;
202
+ }
203
+ // Outlook (VML) fallback
204
+ const outlookContent = appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
205
+ shape,
206
+ imageUrl,
207
+ backgroundColor,
208
+ shapeColor,
209
+ borderWidth,
210
+ borderColor,
211
+ borderRadius: resolvedBorderRadius,
212
+ heightPx: resolvedHeightPx,
213
+ text,
214
+ textColor: color,
215
+ textSize: fontSize,
216
+ verticalAlign,
217
+ textAlign, // ✅ added
218
+ alignment,
219
+ padding,
220
+ msoBakeImageWithText,
221
+ }, visibilityClass);
222
+ const shapeBlockProps = (0, common_1.encodeBlockPropsAttr)({
223
+ shape: shape || 'rectangle',
224
+ width: resolvedWidthPx,
225
+ height: resolvedHeightPx,
226
+ shapeColor: shapeColor || '',
227
+ backgroundColor: backgroundColor || '',
228
+ borderRadius: resolvedBorderRadius,
229
+ borderWidth,
230
+ borderColor,
231
+ borderStyle,
232
+ imageUrl: imageUrl || '',
233
+ text: text || '',
234
+ color,
235
+ fontSize,
236
+ textAlign,
237
+ verticalAlign,
238
+ alignment,
239
+ padding,
240
+ customCss: customCss || '',
241
+ hideOnDesktop: Boolean(props.hideOnDesktop),
242
+ hideOnMobile: Boolean(props.hideOnMobile),
243
+ });
244
+ // Combine into table wrapper
245
+ return `
246
+ <table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}" data-block-type="shape" data-block-props="${shapeBlockProps}">
247
+ <tr>
248
+ <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
249
+ ${outlookContent}
250
+ <!--[if !mso]><!-->
251
+ ${nonMsoContent}
252
+ <!--<![endif]-->
253
+ </td>
254
+ </tr>
255
+ </table>`;
256
+ }
@@ -0,0 +1,2 @@
1
+ export declare function convertTextBlock(blockData: any, cellWidthInPx?: number): string;
2
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/text.ts"],"names":[],"mappings":"AAKA,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,MAAM,UAsItE"}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertTextBlock = convertTextBlock;
4
+ const buildStyles_1 = require("../buildStyles");
5
+ const gradientUtils_1 = require("../gradientUtils");
6
+ const outlookSupport_1 = require("../outlookSupport");
7
+ const common_1 = require("../common");
8
+ function convertTextBlock(blockData, cellWidthInPx) {
9
+ const { style, props } = blockData.data;
10
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
11
+ const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, backgroundImage, whiteSpace: _whiteSpace, ...rest } = style;
12
+ const bgImageStr = typeof backgroundImage === 'string' ? backgroundImage : '';
13
+ const customCssStr = rest.customCss || '';
14
+ const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
15
+ ? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
16
+ : '';
17
+ const effectiveGradient = bgImageStr.includes('gradient(') ? bgImageStr : gradientInCustomCss;
18
+ const isGradient = Boolean(effectiveGradient);
19
+ const parsedGradient = isGradient ? (0, gradientUtils_1.parseGradient)(effectiveGradient) : null;
20
+ const rawBgImageUrl = !isGradient && bgImageStr
21
+ ? bgImageStr.replace(/^url\(['"]?/, '').replace(/['"]?\)$/, '')
22
+ : null;
23
+ const hasBgImage = Boolean(rawBgImageUrl || isGradient);
24
+ const fallbackBgColor = textContainerBackgroundColor ||
25
+ parsedGradient?.fallback ||
26
+ (0, gradientUtils_1.extractCssFallbackColor)(customCssStr) ||
27
+ '#ffffff';
28
+ const textBoxStyle = { backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth };
29
+ const convertedTextStyle = (0, buildStyles_1.buildStyles)(textBoxStyle, { perChanges: [], pxChanges: buildStyles_1.allPxAttributes });
30
+ const innerCustomCss = gradientInCustomCss
31
+ ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
32
+ : customCssStr;
33
+ const restForStyles = gradientInCustomCss ? { ...rest, customCss: innerCustomCss } : rest;
34
+ const styles = (0, buildStyles_1.buildStyles)({
35
+ padding: textContainerPadding,
36
+ backgroundColor: hasBgImage ? undefined : textContainerBackgroundColor,
37
+ ...restForStyles,
38
+ }, { perChanges: [], pxChanges: buildStyles_1.allPxAttributes });
39
+ const sanitizedText = (props.text ?? "")
40
+ .replace(/<p(\s[^>]*)?>/gi, (_, attrs) => `<div${attrs || ""}>`)
41
+ .replace(/<\/p>/gi, "</div>");
42
+ const navigateToUrl = props.navigateToUrl || "";
43
+ const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
44
+ const blockTextColor = rest.color;
45
+ const processedText = blockTextColor
46
+ ? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = '') => {
47
+ if (/style\s*=\s*["'][^"']*\bcolor\s*:/i.test(attrs))
48
+ return match;
49
+ if (/\bstyle\s*=/i.test(attrs)) {
50
+ return `<a${attrs.replace(/(\bstyle\s*=\s*["'])/, `$1color:${blockTextColor};`)}>`;
51
+ }
52
+ return `<a${attrs} style="color:${blockTextColor};">`;
53
+ })
54
+ : sanitizedText;
55
+ const colorStyle = blockTextColor ? `color:${blockTextColor};` : '';
56
+ const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
57
+ const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
58
+ const textContent = (0, outlookSupport_1.appendOutlookSupport)(convertedTextBox, styles, hasBgImage ? '' : visibilityClass, safeCellWidth);
59
+ const linkColorStyle = blockTextColor ? `color:${blockTextColor};` : 'color:inherit;';
60
+ if (hasBgImage) {
61
+ const msoWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : 600;
62
+ const vmlFill = isGradient
63
+ ? (() => {
64
+ const vmlAngle = (0, gradientUtils_1.cssAngleToVml)(parsedGradient?.angle || 180);
65
+ const c1 = parsedGradient?.fallback || '#ffffff';
66
+ const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
67
+ return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
68
+ })()
69
+ : `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
70
+ const bgCss = isGradient
71
+ ? `background:${effectiveGradient};`
72
+ : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`;
73
+ const wrappedContent = `
74
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
75
+ style="border-collapse:collapse;width:100%;max-width:${msoWidth}px;" class="${visibilityClass}">
76
+ <tr>
77
+ <td width="100%" bgcolor="${fallbackBgColor}" valign="top"
78
+ ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ''}
79
+ style="width:100%;max-width:${msoWidth}px;background-color:${fallbackBgColor};${bgCss}">
80
+
81
+ <!--[if gte mso 9]>
82
+ <v:rect xmlns:v="urn:schemas-microsoft-com:vml"
83
+ fill="true" stroke="false"
84
+ style="width:${msoWidth}px;">
85
+ ${vmlFill}
86
+ <v:textbox inset="0,0,0,0">
87
+ <![endif]-->
88
+
89
+ ${textContent}
90
+
91
+ <!--[if gte mso 9]>
92
+ </v:textbox>
93
+ </v:rect>
94
+ <![endif]-->
95
+
96
+ </td>
97
+ </tr>
98
+ </table>`;
99
+ return navigateToUrl
100
+ ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${wrappedContent}</a>`
101
+ : wrappedContent;
102
+ }
103
+ return navigateToUrl
104
+ ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${textContent}</a>`
105
+ : textContent;
106
+ }
@@ -0,0 +1,2 @@
1
+ export declare function convertVideoBlock(blockData: any, cellWidthInPx: number): Promise<string>;
2
+ //# sourceMappingURL=video.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/video.ts"],"names":[],"mappings":"AAUA,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,GAAG,EACd,aAAa,EAAE,MAAM,mBAiKtB"}
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertVideoBlock = convertVideoBlock;
4
+ const buildStyles_1 = require("../buildStyles");
5
+ const common_1 = require("../common");
6
+ 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`;
7
+ const PLAY_ICON_URL = 'https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png';
8
+ const SPACER_GIF_URL = 'https://app-rsrc.getbee.io/public/resources/multiparser/video_block/video_ratio_16-9.gif';
9
+ async function convertVideoBlock(blockData, cellWidthInPx) {
10
+ const { style = {}, props = {} } = blockData.data;
11
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
12
+ const { videoUrl, youtubeVideoUrl, thumbnailUrl, altText } = props;
13
+ const hideOnDesktop = Boolean(props.hideOnDesktop);
14
+ const videoLink = youtubeVideoUrl || videoUrl || '#';
15
+ let resolvedThumbnail = thumbnailUrl || FALLBACK_THUMBNAIL;
16
+ if (youtubeVideoUrl) {
17
+ const youtubeId = (0, common_1.extractYouTubeId)(youtubeVideoUrl);
18
+ const vimeoId = (0, common_1.extractVimeoId)(youtubeVideoUrl);
19
+ if (youtubeId) {
20
+ resolvedThumbnail = `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;
21
+ }
22
+ else if (vimeoId) {
23
+ try {
24
+ const res = await fetch(`https://vimeo.com/api/v2/video/${vimeoId}.json`);
25
+ if (res.ok) {
26
+ const data = await res.json();
27
+ resolvedThumbnail = data?.[0]?.thumbnail_large || resolvedThumbnail;
28
+ }
29
+ }
30
+ catch (_) { }
31
+ }
32
+ }
33
+ // Width / dimensions
34
+ let percentWidth = '100%';
35
+ if (typeof style?.width === 'string' && style.width.trim().endsWith('%')) {
36
+ percentWidth = style.width.trim();
37
+ }
38
+ else if (typeof style?.width === 'number') {
39
+ percentWidth = `${style.width}%`;
40
+ }
41
+ const paddingLeft = style?.padding?.left || 0;
42
+ const paddingRight = style?.padding?.right || 0;
43
+ const innerContainerWidth = (parseFloat(percentWidth) / 100) * (cellWidthInPx - paddingLeft - paddingRight);
44
+ const thumbnailW = Math.round(innerContainerWidth);
45
+ const thumbnailH = Math.round(thumbnailW / (16 / 9));
46
+ // Border
47
+ const borderRadius = parseInt(style?.borderRadius || 0);
48
+ const borderWidth = parseInt(style?.borderWidth || 0);
49
+ const borderColor = style?.borderColor || 'transparent';
50
+ const borderStyleProp = style?.borderStyle || 'solid';
51
+ const outerContainerStyles = (0, buildStyles_1.buildStyles)({
52
+ ...style,
53
+ width: undefined,
54
+ borderColor: undefined,
55
+ borderRadius: undefined,
56
+ borderWidth: undefined,
57
+ borderStyle: undefined,
58
+ }, { perChanges: buildStyles_1.addPxOrPerToAttributes, pxChanges: buildStyles_1.addPxToAttributes });
59
+ const align = style?.textAlign || 'left';
60
+ const playIconW = 65;
61
+ const playIconH = 46;
62
+ // ── Industry-standard VML + background-image table approach ──────────────────
63
+ // Mirrors the pattern used by professional email builders (e.g. Beefree):
64
+ //
65
+ // VML (<!--[if vml]>): all Outlook / Word rendering engine versions.
66
+ // v:group wraps thumbnail (v:rect frame fill) + vector play button
67
+ // (v:oval white circle + v:shape black triangle). No external image needed.
68
+ //
69
+ // Non-VML (<!--[if !vml]>): Gmail, Apple Mail, Yahoo, Outlook.com, EML viewers.
70
+ // thumbnail as `background` attribute + inline background-image on <table>.
71
+ // 3-column layout: [25% spacer GIF] [50% play icon, centred] [25% spacer].
72
+ // The spacer GIF has a 4:9 aspect ratio — when it fills the 25% column its
73
+ // rendered height equals thumbnailWidth × 9/16, keeping the table at 16:9
74
+ // without any fixed `height` CSS (which is stripped by many clients).
75
+ // VML coordinate positions for the play button (in the v:group coord space).
76
+ const ovalSize = 65;
77
+ const ovalLeft = Math.round(thumbnailW / 2 - ovalSize / 2);
78
+ const ovalTop = Math.round(thumbnailH / 2 - ovalSize / 2);
79
+ const triW = 23;
80
+ const triH = 33;
81
+ // Triangle centre is slightly right of geometric centre for visual balance.
82
+ const triLeft = Math.round(thumbnailW / 2 - triW / 2 + 3);
83
+ const triTop = Math.round(thumbnailH / 2 - triH / 2);
84
+ const borderCss = borderWidth > 0
85
+ ? `border:${borderWidth}px ${borderStyleProp} ${borderColor};`
86
+ : '';
87
+ const radiusCss = borderRadius > 0 ? `border-radius:${borderRadius}px; overflow:hidden;` : '';
88
+ // min-height fallback for clients that ignore the spacer GIF (e.g. some webmail).
89
+ const minHeight = Math.round(thumbnailW * 0.3);
90
+ // VML block — shown only in Outlook (Word engine supports VML, not standard HTML).
91
+ const outlookContent = hideOnDesktop ? '' : `<!--[if vml]>
92
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
93
+ coordsize="${thumbnailW},${thumbnailH}" coordorigin="0,0"
94
+ href="${videoLink}"
95
+ style="width:${thumbnailW}px;height:${thumbnailH}px;">
96
+ <v:rect fill="t" stroked="f" style="position:absolute;width:${thumbnailW};height:${thumbnailH};">
97
+ <v:fill src="${resolvedThumbnail}" type="frame"/>
98
+ </v:rect>
99
+ <v:oval fill="t" strokecolor="#ffffff" strokeweight="3px"
100
+ style="position:absolute;left:${ovalLeft};top:${ovalTop};width:${ovalSize};height:${ovalSize}">
101
+ <v:fill color="#ffffff" opacity="100%" />
102
+ </v:oval>
103
+ <v:shape coordsize="24,32" path="m,l,32,24,16,xe" fillcolor="#000000" stroked="f"
104
+ style="position:absolute;left:${triLeft};top:${triTop};width:${triW};height:${triH};" />
105
+ </v:group>
106
+ <![endif]-->`;
107
+ // Non-VML block — shown in all clients except Outlook.
108
+ const nonMsoContent = `<!--[if !vml]><!-->
109
+ <a href="${videoLink}" target="_blank"
110
+ style="display:block; text-decoration:none; background-image:url('${resolvedThumbnail}'); background-size:cover; background-position:center; ${borderCss}${radiusCss}">
111
+ <table cellpadding="0" cellspacing="0" border="0" width="100%"
112
+ background="${resolvedThumbnail}"
113
+ role="presentation"
114
+ style="background-image:url('${resolvedThumbnail}'); background-size:cover; background-position:center center; background-repeat:no-repeat; min-height:${minHeight}px;">
115
+ <tr>
116
+ <td width="25%" style="line-height:0; font-size:0; padding:0;">
117
+ <img src="${SPACER_GIF_URL}" width="100%" border="0" alt=""
118
+ style="display:block; height:auto; opacity:0; visibility:hidden;" />
119
+ </td>
120
+ <td width="50%" align="center" valign="middle"
121
+ style="text-align:center; vertical-align:middle; padding:0;">
122
+ <img src="${PLAY_ICON_URL}" width="${playIconW}" height="${playIconH}" alt="Play"
123
+ style="display:block; width:${playIconW}px; height:${playIconH}px; border:0; margin:0 auto;" />
124
+ </td>
125
+ <td width="25%" style="padding:0;">&nbsp;</td>
126
+ </tr>
127
+ </table>
128
+ </a>
129
+ <!--<![endif]-->`;
130
+ return `
131
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
132
+ style="border-collapse:collapse; max-width:600px; margin:0; padding:0;"
133
+ class="${visibilityClass}" data-block-type="video"
134
+ data-video-url="${videoUrl || ''}" data-youtube-url="${youtubeVideoUrl || ''}"
135
+ data-thumbnail-url="${thumbnailUrl || ''}" data-alt-text="${altText || ''}"
136
+ data-width="${typeof style?.width === 'number' ? style.width : 100}">
137
+ <tr>
138
+ <td align="${align}" style="${outerContainerStyles}">
139
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation"
140
+ align="${align}"
141
+ style="border-collapse:collapse; max-width:${thumbnailW}px; width:${percentWidth};">
142
+ <tr>
143
+ <td align="${align}" style="padding:0;">
144
+ ${outlookContent}${nonMsoContent}
145
+ </td>
146
+ </tr>
147
+ </table>
148
+ </td>
149
+ </tr>
150
+ </table>`;
151
+ }
@@ -0,0 +1,10 @@
1
+ export declare const addPxToAttributes: string[];
2
+ export declare const addPxOrPerToAttributes: string[];
3
+ export declare const allPxAttributes: string[];
4
+ export declare const tableCommonStyle = "border-collapse:collapse; table-layout:fixed";
5
+ export declare function sanitizeFontFamily(fontFamily: string): string;
6
+ export declare function buildStyles(style: any, { pxChanges, perChanges }: {
7
+ pxChanges: string[];
8
+ perChanges: string[];
9
+ }): string;
10
+ //# sourceMappingURL=buildStyles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildStyles.d.ts","sourceRoot":"","sources":["../../src/utils/buildStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,UAA8C,CAAC;AAC7E,eAAO,MAAM,sBAAsB,UAAsB,CAAC;AAC1D,eAAO,MAAM,eAAe,UAAoD,CAAC;AAEjF,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAQ/E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAe7D;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,GAAG,EACV,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,UAmEzE"}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tableCommonStyle = exports.allPxAttributes = exports.addPxOrPerToAttributes = exports.addPxToAttributes = void 0;
4
+ exports.sanitizeFontFamily = sanitizeFontFamily;
5
+ exports.buildStyles = buildStyles;
6
+ const fontFallback_1 = require("./fontFallback");
7
+ exports.addPxToAttributes = ["fontSize", "borderRadius", "borderWidth"];
8
+ exports.addPxOrPerToAttributes = ["width", "height"];
9
+ exports.allPxAttributes = [...exports.addPxToAttributes, ...exports.addPxOrPerToAttributes];
10
+ exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed";
11
+ const GENERIC_FONT_FAMILIES = new Set([
12
+ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
13
+ 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace',
14
+ 'ui-rounded', 'emoji', 'math', 'fangsong',
15
+ ]);
16
+ function sanitizeFontFamily(fontFamily) {
17
+ if (!fontFamily)
18
+ return fontFamily;
19
+ return fontFamily
20
+ .split(',')
21
+ .map(font => {
22
+ const trimmed = font.trim();
23
+ const unquoted = trimmed.replace(/^["']|["']$/g, '').trim();
24
+ if (!unquoted)
25
+ return '';
26
+ if (GENERIC_FONT_FAMILIES.has(unquoted.toLowerCase()) || !/\s/.test(unquoted)) {
27
+ return unquoted;
28
+ }
29
+ return `'${unquoted.replace(/'/g, "\\'")}'`;
30
+ })
31
+ .filter(Boolean)
32
+ .join(', ');
33
+ }
34
+ function buildStyles(style, { pxChanges, perChanges }) {
35
+ if (!style)
36
+ style = {};
37
+ const stylesObj = {};
38
+ const INVALID_KEYS = [
39
+ "columns", "cellWidths", "cellWidth", "childWidth",
40
+ "visibility", "hideOnMobile", "hideOnDesktop", "label", "alignment",
41
+ ];
42
+ Object.entries(style).forEach(([key, value]) => {
43
+ if (key === "customCss")
44
+ return;
45
+ if (INVALID_KEYS.includes(key))
46
+ return;
47
+ if (value === undefined || value === null || value === "")
48
+ return;
49
+ if ((key === "padding" || key === "buttonPadding") && typeof value === "object") {
50
+ const pad = value;
51
+ const safePad = {
52
+ top: Number.isFinite(pad.top) ? pad.top : 0,
53
+ right: Number.isFinite(pad.right) ? pad.right : 0,
54
+ bottom: Number.isFinite(pad.bottom) ? pad.bottom : 0,
55
+ left: Number.isFinite(pad.left) ? pad.left : 0,
56
+ };
57
+ value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
58
+ }
59
+ if (key === "fontFamily" && typeof value === "string") {
60
+ value = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(value));
61
+ }
62
+ if (key === "backgroundImage" && typeof value === "string"
63
+ && !String(value).startsWith("url(")
64
+ && !String(value).toLowerCase().includes("gradient(")) {
65
+ value = `url('${value}')`;
66
+ }
67
+ if (key === "lineHeight" && typeof value === "number") {
68
+ stylesObj["line-height"] = value >= 4 ? `${value}px` : String(value);
69
+ return;
70
+ }
71
+ const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
72
+ if (pxChanges.includes(key)) {
73
+ if (typeof value === "number") {
74
+ stylesObj[cssKey] = `${Math.round(value * 100) / 100}px`;
75
+ }
76
+ else if (typeof value === "string" && value.includes("null")) {
77
+ return;
78
+ }
79
+ else {
80
+ stylesObj[cssKey] = value;
81
+ }
82
+ }
83
+ else if (perChanges.includes(key)) {
84
+ if (typeof value === "number") {
85
+ stylesObj[cssKey] = `${value}%`;
86
+ }
87
+ else {
88
+ stylesObj[cssKey] = value;
89
+ }
90
+ }
91
+ else {
92
+ stylesObj[cssKey] = value;
93
+ }
94
+ });
95
+ const parts = Object.entries(stylesObj)
96
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
97
+ .map(([k, v]) => `${k}:${v}`);
98
+ if (style.customCss)
99
+ parts.push(style.customCss);
100
+ return parts.join('; ').replace(/;\s*$/, '').trim();
101
+ }
@@ -4,4 +4,5 @@ export declare function getVisibilityClass(props?: {
4
4
  hideOnDesktop?: boolean;
5
5
  hideOnMobile?: boolean;
6
6
  }): string;
7
+ export declare function encodeBlockPropsAttr(props: Record<string, any>): string;
7
8
  //# sourceMappingURL=common.d.ts.map
@@ -1 +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;AAGF,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,UASA"}
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;AAGF,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,UASA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAQvE"}