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.
@@ -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;CAC5B;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;CACtB;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"}
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"}
@@ -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,3 @@
1
+ export declare const extractYouTubeId: (url: string) => string | null;
2
+ export declare const extractVimeoId: (url: string) => string | null;
3
+ //# sourceMappingURL=common.d.ts.map
@@ -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;AASrC,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;CACtB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,cAAc,CAAC;QACtB,KAAK,EAAE,GAAG,CAAC;QACX,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KAC7B,CAAC;CACH;AAYD,eAAO,MAAM,gBAAgB,kDAAkD,CAAC;AA2DhF,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAkBtB"}
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"}
@@ -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 addPxToAttributes = [
8
- "fontSize",
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
- ? Math.min(borderRadius / scaledHeight, 1).toFixed(2)
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-builder-utils",
3
- "version": "1.1.21",
3
+ "version": "1.1.23",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [