email-builder-utils 1.1.21 → 1.1.22

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,7 @@ interface IProps {
23
25
  navigateToUrl: string;
24
26
  altText: string;
25
27
  cellWidths: number[];
28
+ responsive?: boolean;
26
29
  }
27
30
  interface IStyle {
28
31
  [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;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"}
@@ -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;
@@ -19,5 +19,7 @@ interface IBlockData {
19
19
  }
20
20
  export declare const tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
21
21
  export declare function convertToHtml(blockData: IBlockData, rootData: any, cellWidthInPx: number): Promise<string>;
22
+ export declare function convertVideoBlock(blockData: any, cellWidthInPx: number): Promise<string>;
23
+ export declare function convertShapeBlock(blockData: IBlockData): Promise<string>;
22
24
  export {};
23
25
  //# 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;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,mBAsBtB;AA2ZD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBA0I5E;AA0JD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,UAAU,mBAgJ5D"}
@@ -2,8 +2,11 @@
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");
9
+ const common_1 = require("./common");
7
10
  const addPxToAttributes = [
8
11
  "fontSize",
9
12
  "lineHeight",
@@ -72,6 +75,10 @@ async function convertToHtml(blockData, rootData, cellWidthInPx) {
72
75
  return convertDividerBlockToHtml(blockData);
73
76
  case types_1.BlockType.SPACER:
74
77
  return convertSpacerBlockToHtml(blockData);
78
+ case types_1.BlockType.VIDEO:
79
+ return convertVideoBlock(blockData, cellWidthInPx);
80
+ case types_1.BlockType.SHAPE:
81
+ return await convertShapeBlock(blockData);
75
82
  default:
76
83
  return "";
77
84
  }
@@ -81,12 +88,6 @@ function appendOutlookSupport(content, contentStyle) {
81
88
  <table width="100%" style="${exports.tableCommonStyle}"><tr><td style="${contentStyle}">${content}</td></tr></table>
82
89
  `;
83
90
  }
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
91
  function convertDividerBlockToHtml(blockData) {
91
92
  const { style } = blockData.data;
92
93
  const { thickness, dividerColor, ...rest } = style;
@@ -337,3 +338,334 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
337
338
  styles,
338
339
  };
339
340
  }
341
+ async function convertVideoBlock(blockData, cellWidthInPx) {
342
+ const { style, props } = blockData.data;
343
+ const { videoUrl, youtubeVideoUrl, thumbnailUrl, altText } = props || {};
344
+ const videoLink = youtubeVideoUrl || videoUrl || "#";
345
+ let resolvedThumbnail = thumbnailUrl || "https://via.placeholder.com/480x360?text=No+Thumbnail";
346
+ if (youtubeVideoUrl) {
347
+ const youtubeId = (0, common_1.extractYouTubeId)(youtubeVideoUrl);
348
+ const vimeoId = (0, common_1.extractVimeoId)(youtubeVideoUrl);
349
+ if (youtubeId) {
350
+ resolvedThumbnail = `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;
351
+ }
352
+ else if (vimeoId) {
353
+ try {
354
+ const res = await fetch(`https://vimeo.com/api/v2/video/${vimeoId}.json`);
355
+ if (res.ok) {
356
+ const data = await res.json();
357
+ resolvedThumbnail = data?.[0]?.thumbnail_large || resolvedThumbnail;
358
+ }
359
+ }
360
+ catch (_) { }
361
+ }
362
+ }
363
+ // Determine width logic
364
+ let percentWidth;
365
+ if (typeof style?.width === "string" && style.width.trim().endsWith("%")) {
366
+ percentWidth = style.width.trim();
367
+ }
368
+ else if (typeof style?.width === "number") {
369
+ percentWidth = `${style.width}%`;
370
+ }
371
+ else {
372
+ percentWidth = "100%";
373
+ }
374
+ const innerContainerWidth = (parseFloat(percentWidth) / 100) *
375
+ (cellWidthInPx -
376
+ (style?.padding?.left || 0) -
377
+ (style?.padding?.right || 0));
378
+ const aspectRatio = 16 / 9;
379
+ const calculatedHeight = innerContainerWidth / aspectRatio;
380
+ const outerContainerStyles = buildStyles({
381
+ ...style,
382
+ width: undefined,
383
+ }, {
384
+ perChanges: addPxOrPerToAttributes,
385
+ pxChanges: addPxToAttributes,
386
+ });
387
+ const borderRadius = parseInt(style?.borderRadius) || 0;
388
+ const borderWidth = parseInt(style?.borderWidth) || 0;
389
+ const borderColor = style?.borderColor || "transparent";
390
+ // Play icon size
391
+ const playIconWidth = 65;
392
+ const playIconHeight = 46;
393
+ // VML centering math (for Outlook)
394
+ const vmlLeft = innerContainerWidth / 2 - playIconWidth / 2;
395
+ const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
396
+ const videoContent = `
397
+ <!--[if mso]>
398
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml" coordsize="${innerContainerWidth},${calculatedHeight}"
399
+ coordorigin="0,0"
400
+ href="${videoLink}"
401
+ style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
402
+ <v:rect fill="t" stroked="f" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;">
403
+ <v:fill src="${resolvedThumbnail}" type="frame"/>
404
+ </v:rect>
405
+ <v:shape type="#_x0000_t75"
406
+ style="position:absolute;
407
+ left:${vmlLeft.toFixed(1)}px;
408
+ top:${vmlTop.toFixed(1)}px;
409
+ width:${playIconWidth}px;
410
+ height:${playIconHeight}px;"
411
+ alt="Play" href="${videoLink}" title="${altText || "Video"}"
412
+ stroked="f" filled="t">
413
+ <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
414
+ </v:shape>
415
+ </v:group>
416
+ <![endif]-->
417
+
418
+ <!--[if !mso]><!-->
419
+ <table
420
+ width="${innerContainerWidth}"
421
+ cellpadding="0"
422
+ cellspacing="0"
423
+ border="0"
424
+ role="presentation"
425
+ style="
426
+ background-image: url('${resolvedThumbnail}');
427
+ background-size: cover;
428
+ background-position: center;
429
+
430
+ max-width: ${innerContainerWidth}px;
431
+ height: ${calculatedHeight}px;
432
+ box-sizing: border-box;
433
+ "
434
+ align="center"
435
+ >
436
+ <tr>
437
+ <td style="height: ${calculatedHeight}px; padding: 0; text-align: center; vertical-align: middle; border-radius: ${borderRadius}px;
438
+ border: ${borderWidth}px solid ${borderColor};" align="center" valign="middle">
439
+ <a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
440
+ <img
441
+ src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
442
+ width="${playIconWidth}"
443
+ alt="Play"
444
+ style="display: block; border: 0; outline: none; text-decoration: none; height: auto;"
445
+ />
446
+ </a>
447
+ </td>
448
+ </tr>
449
+ </table>
450
+ <!--<![endif]-->
451
+ `;
452
+ const wrapperHtml = `
453
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse;">
454
+ <tr>
455
+ <td align="center" style="padding:0; ${outerContainerStyles}">
456
+ <div style="display: inline-block; width: ${percentWidth}; max-width: ${cellWidthInPx}px; box-sizing: border-box;">
457
+ ${videoContent}
458
+ </div>
459
+ </td>
460
+ </tr>
461
+ </table>
462
+ `;
463
+ return wrapperHtml;
464
+ }
465
+ // Enhanced Shape Block HTML Conversion using appendOutlookForShape
466
+ // ---------- helpers ----------
467
+ function computeArcSize(borderRadius, widthPx) {
468
+ if (!borderRadius)
469
+ return "0";
470
+ if (typeof borderRadius === "number")
471
+ return Math.min(borderRadius / widthPx, 1).toFixed(2);
472
+ const s = borderRadius.toString().trim();
473
+ if (s.endsWith("%")) {
474
+ const pct = parseFloat(s.replace("%", "")) || 0;
475
+ return Math.min(pct / 100, 1).toFixed(2);
476
+ }
477
+ // assume px or raw number
478
+ const px = parseFloat(s.replace("px", "")) || 0;
479
+ return Math.min(px / widthPx, 1).toFixed(2);
480
+ }
481
+ // ---------- Outlook (MSO) wrapper ----------
482
+ async function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts) {
483
+ // Use the inner container width for VML sizing (exact user dims)
484
+ const widthPx = Math.round(Math.min(outerContainerWidth, innerContainerWidth));
485
+ const heightPx = Math.max(1, Math.round(opts.heightPx));
486
+ const vml = buildVMLShape({
487
+ shape: opts.shape,
488
+ widthPx,
489
+ heightPx,
490
+ imageUrl: opts.msoBakeImageWithText || opts.imageUrl,
491
+ backgroundColor: opts.shapeColor || opts.backgroundColor,
492
+ borderWidth: opts.borderWidth,
493
+ borderColor: opts.borderColor,
494
+ borderRadius: opts.borderRadius,
495
+ text: opts.text,
496
+ textColor: opts.textColor,
497
+ // pass raw flag so buildVMLShape knows if image already has text baked-in
498
+ msoHasBakedText: Boolean(opts.msoBakeImageWithText),
499
+ });
500
+ const outlookAlignment = opts.alignment === "center" ? "center" :
501
+ opts.alignment === "right" ? "right" : "left";
502
+ // Wrap the VML inside a table so Outlook aligns it correctly
503
+ return `<!--[if mso]>
504
+ <table align="${outlookAlignment}" border="0" cellpadding="0" cellspacing="0" style="display:inline-block;">
505
+ <tr>
506
+ <td style="padding:${opts.padding?.top || 0}px ${opts.padding?.right || 0}px ${opts.padding?.bottom || 0}px ${opts.padding?.left || 0}px;">
507
+ ${vml}
508
+ </td>
509
+ </tr>
510
+ </table>
511
+ <![endif]-->`;
512
+ }
513
+ // ---------- VML builder (produces shape + text inside it for MSO) ----------
514
+ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor, msoHasBakedText = false, }) {
515
+ const bw = borderWidth || 0;
516
+ const bc = borderColor || "transparent";
517
+ const hasBorder = bw > 0;
518
+ const borderAttributes = hasBorder ? `strokeweight="${bw}px" strokecolor="${bc}"` : `stroked="false"`;
519
+ const fillColor = backgroundColor || "#2F80ED";
520
+ // choose tag and extra attributes
521
+ let tag = "rect";
522
+ let extraAttr = "";
523
+ if (shape === "circle" || shape === "oval")
524
+ tag = "oval";
525
+ if (shape === "rounded" || (borderRadius && borderRadius !== "0")) {
526
+ tag = "roundrect";
527
+ extraAttr = ` arcsize="${computeArcSize(borderRadius, widthPx)}"`;
528
+ }
529
+ // image fill (if provided)
530
+ const fillMarkup = imageUrl
531
+ ? `<v:fill src="${imageUrl}" type="frame" aspect="atleast" />`
532
+ : "";
533
+ // If MSO is given a baked image with text, don't produce a v:textbox overlay text (image already contains text)
534
+ const includeTextbox = !!text && !msoHasBakedText;
535
+ // v:textbox: use a table + cell to center the text; avoids many Word quirks
536
+ const textboxInner = includeTextbox
537
+ ? `<v:textbox inset="0,0,0,0">
538
+ <center style="width:${widthPx}px;height:${heightPx}px;display:block;">
539
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="${widthPx}" height="${heightPx}" style="border-collapse:collapse;">
540
+ <tr>
541
+ <td align="center" valign="middle" style="font-family:Arial, sans-serif;font-size:14px;line-height:1;color:${textColor || "#000"};padding:6px;">
542
+ ${text}
543
+ </td>
544
+ </tr>
545
+ </table>
546
+ </center>
547
+ </v:textbox>`
548
+ : // keep an empty textbox so shape sizing behaves consistently when no text
549
+ `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
550
+ // If there is no imageUrl and no textbox content, use fillcolor for background
551
+ const fillAttr = imageUrl ? 'fill="true"' : `fill="true" fillcolor="${fillColor}"`;
552
+ return `
553
+ <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
554
+ style="width:${widthPx}px;height:${heightPx}px;v-text-anchor:middle;"
555
+ ${borderAttributes} ${fillAttr}${extraAttr}>
556
+ ${fillMarkup}
557
+ ${textboxInner}
558
+ </v:${tag}>`;
559
+ }
560
+ // ---------- convertShapeBlock (updated, keeps your structure) ----------
561
+ async function convertShapeBlock(blockData) {
562
+ const { style, props } = blockData.data;
563
+ const { shape, text, textColor = "#000000", imageUrl } = props || {};
564
+ const { width = "100", height = "150", padding = {}, backgroundColor = "#2F80ED", borderRadius, borderWidth = 0, borderStyle = "solid", borderColor = "transparent", customCss, shapeColor, alignment = "left", msoBakeImageWithText } = style || {};
565
+ const borderRadiusMap = {
566
+ rectangle: "0",
567
+ rounded: "10px",
568
+ circle: "50%",
569
+ oval: "50%",
570
+ };
571
+ let resolvedBorderRadius = borderRadius || borderRadiusMap[shape] || "0";
572
+ let resolvedWidthPx = typeof width === "number"
573
+ ? width
574
+ : parseInt(width.toString().replace("px", ""), 10) || 100;
575
+ let resolvedHeightPx = typeof height === "number"
576
+ ? height
577
+ : parseInt(height.toString().replace("px", ""), 10) || 150;
578
+ // Force circle → square
579
+ if (shape === "circle") {
580
+ const side = Math.min(resolvedWidthPx, resolvedHeightPx);
581
+ resolvedWidthPx = side;
582
+ resolvedHeightPx = side;
583
+ resolvedBorderRadius = "50%";
584
+ }
585
+ const finalWidthPx = resolvedWidthPx;
586
+ const finalHeightPx = resolvedHeightPx;
587
+ const alignmentStyles = {
588
+ left: "margin-right:auto;margin-left:0;",
589
+ center: "margin-left:auto;margin-right:auto;",
590
+ right: "margin-left:auto;margin-right:0;",
591
+ };
592
+ const alignmentStyle = alignmentStyles[alignment] || "";
593
+ const finalBackgroundColor = shapeColor || backgroundColor;
594
+ // --- Modern clients content ---
595
+ let nonMsoContent = "";
596
+ // Case 1: Image + Text → use background-image
597
+ if (imageUrl && text) {
598
+ nonMsoContent = `
599
+ <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
600
+ border:${borderWidth}px ${borderStyle} ${borderColor};
601
+ border-radius:${resolvedBorderRadius};
602
+ background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
603
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
604
+ <div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;">
605
+ <div style="color:${textColor};text-align:center;padding:8px;box-sizing:border-box;word-break:break-word;
606
+ border-radius:4px;max-width:90%;">
607
+ ${text}
608
+ </div>
609
+ </div>
610
+ </div>`;
611
+ }
612
+ // Case 2: Image only → use <img>
613
+ else if (imageUrl) {
614
+ nonMsoContent = `
615
+ <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
616
+ border:${borderWidth}px ${borderStyle} ${borderColor};
617
+ border-radius:${resolvedBorderRadius};
618
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
619
+ <img src="${imageUrl}" alt="${text || "Shape image"}"
620
+ width="${finalWidthPx}" height="${finalHeightPx}"
621
+ style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
622
+ </div>`;
623
+ }
624
+ // Case 3: No image → solid background
625
+ else {
626
+ nonMsoContent = `
627
+ <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
628
+ background:${finalBackgroundColor};
629
+ border:${borderWidth}px ${borderStyle} ${borderColor};
630
+ border-radius:${resolvedBorderRadius};
631
+ ${alignmentStyle}${customCss || ""}">
632
+ <div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;
633
+ color:${textColor};text-align:center;padding:8px;box-sizing:border-box;word-break:break-word;">
634
+ ${text || ""}
635
+ </div>
636
+ </div>`;
637
+ }
638
+ // --- Old Outlook (MSO) VML ---
639
+ const outlookContent = await appendOutlookForShape(nonMsoContent, finalWidthPx, finalWidthPx, {
640
+ shape,
641
+ imageUrl,
642
+ backgroundColor,
643
+ shapeColor,
644
+ borderWidth,
645
+ borderColor,
646
+ borderRadius: resolvedBorderRadius,
647
+ heightPx: finalHeightPx,
648
+ text,
649
+ textColor,
650
+ alignment,
651
+ padding,
652
+ msoBakeImageWithText
653
+ });
654
+ // Wrap in container table
655
+ const containerTable = `
656
+ <table width="100%" style="border-collapse:collapse;table-layout:fixed;">
657
+ <tr>
658
+ <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;
659
+ background-color:transparent;text-align:${alignment};">
660
+ ${outlookContent}
661
+ <!--[if !mso]><!-->
662
+ ${nonMsoContent}
663
+ <!--<![endif]-->
664
+ </td>
665
+ </tr>
666
+ </table>`;
667
+ return appendOutlookSupport(containerTable, buildStyles(style, {
668
+ perChanges: addPxOrPerToAttributes,
669
+ pxChanges: allPxAttributes,
670
+ }));
671
+ }
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.22",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [