email-builder-utils 1.1.26 → 1.1.27

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.
@@ -173,6 +173,9 @@ async function convertImageBlock(blockData, cellWidthInPx) {
173
173
  const { style, props } = blockData.data;
174
174
  const { altText, imageUrl, navigateToUrl } = props;
175
175
  const { width, height, objectFit, borderRadius, borderWidth, borderColor, borderStyle, ...containerStyle } = style;
176
+ const image = await jimp_1.Jimp.read(imageUrl);
177
+ const originalWidth = image.bitmap.width;
178
+ const originalHeight = image.bitmap.height;
176
179
  // Ensure border styles are applied only to the container, not the image
177
180
  const imageStyle = {
178
181
  width,
@@ -181,10 +184,9 @@ async function convertImageBlock(blockData, cellWidthInPx) {
181
184
  borderStyle,
182
185
  borderRadius: borderRadius,
183
186
  borderColor,
187
+ maxWidth: `${originalWidth}px`, // Limit to original size
188
+ maxHeight: `${originalHeight}px`,
184
189
  };
185
- const image = await jimp_1.Jimp.read(imageUrl);
186
- const originalWidth = image.bitmap.width;
187
- const originalHeight = image.bitmap.height;
188
190
  // Add border styles to container for fallback clients
189
191
  const containerStyles = buildStyles({
190
192
  ...containerStyle,
@@ -483,92 +485,16 @@ function computeArcSize(borderRadius, widthPx) {
483
485
  const px = parseFloat(s.replace("px", "")) || 0;
484
486
  return Math.min(px / widthPx, 1).toFixed(2);
485
487
  }
486
- // ---------- Outlook (MSO) wrapper ----------
487
- async function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts) {
488
- // Use the inner container width for VML sizing (exact user dims)
489
- const widthPx = Math.round(Math.min(outerContainerWidth, innerContainerWidth));
490
- const heightPx = Math.max(1, Math.round(opts.heightPx));
491
- const vml = buildVMLShape({
492
- shape: opts.shape,
493
- widthPx,
494
- heightPx,
495
- imageUrl: opts.msoBakeImageWithText || opts.imageUrl,
496
- backgroundColor: opts.shapeColor || opts.backgroundColor,
497
- borderWidth: opts.borderWidth,
498
- borderColor: opts.borderColor,
499
- borderRadius: opts.borderRadius,
500
- text: opts.text,
501
- textColor: opts.textColor,
502
- // pass raw flag so buildVMLShape knows if image already has text baked-in
503
- msoHasBakedText: Boolean(opts.msoBakeImageWithText),
504
- });
505
- const outlookAlignment = opts.alignment === "center" ? "center" : opts.alignment === "right" ? "right" : "left";
506
- // Wrap the VML inside a table so Outlook aligns it correctly
507
- return `<!--[if mso]>
508
- <table align="${outlookAlignment}" border="0" cellpadding="0" cellspacing="0" style="display:inline-block;">
509
- <tr>
510
- <td style="padding:${opts.padding?.top || 0}px ${opts.padding?.right || 0}px ${opts.padding?.bottom || 0}px ${opts.padding?.left || 0}px;">
511
- ${vml}
512
- </td>
513
- </tr>
514
- </table>
515
- <![endif]-->`;
516
- }
517
- // ---------- VML builder (produces shape + text inside it for MSO) ----------
518
- function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor, msoHasBakedText = false, }) {
519
- const bw = borderWidth || 0;
520
- const bc = borderColor || "transparent";
521
- const hasBorder = bw > 0;
522
- const borderAttributes = hasBorder ? `strokeweight="${bw}px" strokecolor="${bc}"` : `stroked="false"`;
523
- const fillColor = backgroundColor || "#2F80ED";
524
- // choose tag and extra attributes
525
- let tag = "rect";
526
- let extraAttr = "";
527
- if (shape === "circle" || shape === "oval")
528
- tag = "oval";
529
- if (shape === "rounded" || (borderRadius && borderRadius !== "0")) {
530
- tag = "roundrect";
531
- extraAttr = ` arcsize="${computeArcSize(borderRadius, widthPx)}"`;
532
- }
533
- // image fill (if provided)
534
- const fillMarkup = imageUrl ? `<v:fill src="${imageUrl}" type="frame" aspect="atleast" />` : "";
535
- // If MSO is given a baked image with text, don't produce a v:textbox overlay text (image already contains text)
536
- const includeTextbox = !!text && !msoHasBakedText;
537
- // v:textbox: use a table + cell to center the text; avoids many Word quirks
538
- const textboxInner = includeTextbox
539
- ? `<v:textbox inset="0,0,0,0">
540
- <center style="width:${widthPx}px;height:${heightPx}px;display:block;">
541
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="${widthPx}" height="${heightPx}" style="border-collapse:collapse;">
542
- <tr>
543
- <td align="center" valign="middle" style="font-family:Arial, sans-serif;font-size:14px;line-height:1;color:${textColor || "#000"};padding:6px;">
544
- ${text}
545
- </td>
546
- </tr>
547
- </table>
548
- </center>
549
- </v:textbox>`
550
- : // keep an empty textbox so shape sizing behaves consistently when no text
551
- `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
552
- // If there is no imageUrl and no textbox content, use fillcolor for background
553
- const fillAttr = imageUrl ? 'fill="true"' : `fill="true" fillcolor="${fillColor}"`;
554
- return `
555
- <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
556
- style="width:${widthPx}px;height:${heightPx}px;v-text-anchor:middle;"
557
- ${borderAttributes} ${fillAttr}${extraAttr}>
558
- ${fillMarkup}
559
- ${textboxInner}
560
- </v:${tag}>`;
561
- }
562
- // ---------- convertShapeBlock (updated, keeps your structure) ----------
488
+ // ---------- Updated convertShapeBlock function ----------
563
489
  async function convertShapeBlock(blockData) {
564
490
  const { style, props } = blockData.data;
565
- const { shape, text, textColor = "#000000", imageUrl } = props;
566
- const { width = "100", height = "150", padding = {}, backgroundColor = "#2F80ED", borderRadius, borderWidth = 0, borderStyle = "solid", borderColor = "transparent", customCss, shapeColor, alignment = "left", msoBakeImageWithText } = style || {};
491
+ const { shape, text, imageUrl } = props;
492
+ const { width = "100", height = "150", padding = {}, backgroundColor = "#2F80ED", borderRadius, borderWidth = 0, borderStyle = "solid", borderColor = "transparent", customCss, shapeColor, alignment = "left", msoBakeImageWithText, color = "#000000", fontSize = 14, verticalAlign = "center", } = style || {};
567
493
  const borderRadiusMap = {
568
494
  rectangle: "0",
569
495
  rounded: "10px",
570
496
  circle: "50%",
571
- oval: "50%", // Keep this for modern browsers
497
+ oval: "50%",
572
498
  };
573
499
  let resolvedBorderRadius = borderRadius || borderRadiusMap[shape] || "0";
574
500
  let resolvedWidthPx = typeof width === "number"
@@ -577,73 +503,80 @@ async function convertShapeBlock(blockData) {
577
503
  let resolvedHeightPx = typeof height === "number"
578
504
  ? height
579
505
  : parseInt(height.toString().replace("px", ""), 10) || 150;
580
- // Special handling for different shapes
506
+ // --- Shape specific constraints ---
581
507
  if (shape === "circle") {
582
- // Circle: make it a perfect square with 50% border radius
583
508
  const side = Math.min(resolvedWidthPx, resolvedHeightPx);
584
509
  resolvedWidthPx = side;
585
510
  resolvedHeightPx = side;
586
511
  resolvedBorderRadius = "50%";
587
512
  }
588
513
  else if (shape === "oval") {
514
+ resolvedBorderRadius = "50% / 50%";
589
515
  }
590
- const finalWidthPx = resolvedWidthPx;
591
- const finalHeightPx = resolvedHeightPx;
516
+ const finalBackgroundColor = shapeColor || backgroundColor;
592
517
  const alignmentStyles = {
593
518
  left: "margin-right:auto;margin-left:0;",
594
519
  center: "margin-left:auto;margin-right:auto;",
595
520
  right: "margin-left:auto;margin-right:0;",
596
521
  };
597
522
  const alignmentStyle = alignmentStyles[alignment] || "";
598
- const finalBackgroundColor = shapeColor || backgroundColor;
599
- // --- Modern clients content ---
523
+ const verticalAlignStyles = {
524
+ top: "align-items:flex-start;padding-top:8px;",
525
+ center: "align-items:center;",
526
+ bottom: "align-items:flex-end;padding-bottom:8px;",
527
+ };
528
+ const verticalAlignStyle = verticalAlignStyles[verticalAlign] ||
529
+ verticalAlignStyles.center;
530
+ // Text styling (safe across clients)
531
+ const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;text-align:center;color:${color};`;
532
+ // ============================
533
+ // Modern HTML (non-MSO)
534
+ // ============================
600
535
  let nonMsoContent = "";
601
- // For modern browsers, use CSS border-radius
602
- const modernBorderRadius = shape === "oval" ? "50%" : resolvedBorderRadius;
603
- // Case 1: Image + Text → use background-image
604
536
  if (imageUrl && text) {
605
537
  nonMsoContent = `
606
- <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
538
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
607
539
  border:${borderWidth}px ${borderStyle} ${borderColor};
608
- border-radius:${modernBorderRadius};
540
+ border-radius:${resolvedBorderRadius};
609
541
  background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
610
542
  overflow:hidden;${alignmentStyle}${customCss || ""}">
611
- <div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;">
612
- <div style="color:${textColor};text-align:center;padding:8px;box-sizing:border-box;word-break:break-word;
613
- border-radius:4px;max-width:90%;">
543
+ <div style="width:100%;height:100%;display:flex;${verticalAlignStyle}justify-content:center;overflow:hidden;">
544
+ <div style="${textSizeStyle}padding:6px;max-width:90%;-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden;">
614
545
  ${text}
615
546
  </div>
616
547
  </div>
617
548
  </div>`;
618
549
  }
619
- // Case 2: Image only → use <img>
620
550
  else if (imageUrl) {
621
551
  nonMsoContent = `
622
- <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
552
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
623
553
  border:${borderWidth}px ${borderStyle} ${borderColor};
624
- border-radius:${modernBorderRadius};
554
+ border-radius:${resolvedBorderRadius};
625
555
  overflow:hidden;${alignmentStyle}${customCss || ""}">
626
- <img src="${imageUrl}" alt="${text || "Shape image"}"
627
- width="${finalWidthPx}" height="${finalHeightPx}"
628
- style="width:100%;height:100%;object-fit:cover;border-radius:${modernBorderRadius};display:block;" />
556
+ <img src="${imageUrl}" alt="${text || "shape image"}"
557
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
558
+ style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
629
559
  </div>`;
630
560
  }
631
- // Case 3: No image → solid background
632
561
  else {
562
+ const circlePadding = shape === "circle" ? Math.round(resolvedHeightPx * 0.15) : 8;
633
563
  nonMsoContent = `
634
- <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
564
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
635
565
  background:${finalBackgroundColor};
636
566
  border:${borderWidth}px ${borderStyle} ${borderColor};
637
- border-radius:${modernBorderRadius};
638
- ${alignmentStyle}${customCss || ""}">
639
- <div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;
640
- color:${textColor};text-align:center;padding:8px;box-sizing:border-box;word-break:break-word;">
641
- ${text || ""}
567
+ border-radius:${resolvedBorderRadius};
568
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
569
+ <div style="width:100%;height:100%;display:flex;${verticalAlignStyle}justify-content:center;padding:${circlePadding}px;box-sizing:border-box;">
570
+ <div style="${textSizeStyle}max-width:90%;overflow:hidden;">
571
+ ${text || ""}
572
+ </div>
642
573
  </div>
643
574
  </div>`;
644
575
  }
645
- // --- Old Outlook (MSO) VML ---
646
- const outlookContent = await appendOutlookForShape(nonMsoContent, finalWidthPx, finalWidthPx, {
576
+ // ============================
577
+ // Outlook (VML) version
578
+ // ============================
579
+ const outlookContent = await appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
647
580
  shape,
648
581
  imageUrl,
649
582
  backgroundColor,
@@ -651,19 +584,22 @@ async function convertShapeBlock(blockData) {
651
584
  borderWidth,
652
585
  borderColor,
653
586
  borderRadius: resolvedBorderRadius,
654
- heightPx: finalHeightPx,
587
+ heightPx: resolvedHeightPx,
655
588
  text,
656
- textColor,
589
+ textColor: color,
590
+ textSize: fontSize,
591
+ verticalAlign,
657
592
  alignment,
658
593
  padding,
659
- msoBakeImageWithText
594
+ msoBakeImageWithText,
660
595
  });
661
- // Wrap in container table
662
- const containerTable = `
596
+ // ============================
597
+ // Final combined block
598
+ // ============================
599
+ return `
663
600
  <table width="100%" style="border-collapse:collapse;table-layout:fixed;">
664
601
  <tr>
665
- <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;
666
- background-color:transparent;text-align:${alignment};">
602
+ <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
667
603
  ${outlookContent}
668
604
  <!--[if !mso]><!-->
669
605
  ${nonMsoContent}
@@ -671,8 +607,85 @@ async function convertShapeBlock(blockData) {
671
607
  </td>
672
608
  </tr>
673
609
  </table>`;
674
- return appendOutlookSupport(containerTable, buildStyles(style, {
675
- perChanges: addPxOrPerToAttributes,
676
- pxChanges: allPxAttributes,
677
- }));
610
+ }
611
+ // ---------- Updated VML builder with better text containment ----------
612
+ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor = "#000000", textSize = 14, verticalAlign = "center", msoHasBakedText = false, }) {
613
+ // --- Basic setup ---
614
+ const bw = borderWidth || 0;
615
+ const bc = borderColor || "transparent";
616
+ const borderAttrs = bw > 0
617
+ ? `strokeweight="${bw}px" strokecolor="${bc}"`
618
+ : `stroked="false"`;
619
+ const fillColor = backgroundColor || "#2F80ED";
620
+ const fillMarkup = `<v:fill ${imageUrl ? `src="${imageUrl}" type="frame" aspect="atleast"` : ""} color="${fillColor}" />`;
621
+ // --- Shape tag ---
622
+ let tag = "rect";
623
+ let extraAttr = "";
624
+ if (shape === "circle" || shape === "oval") {
625
+ tag = "oval";
626
+ }
627
+ else if (shape === "rounded") {
628
+ tag = "roundrect";
629
+ extraAttr = `arcsize="${computeArcSize(borderRadius, widthPx)}"`;
630
+ }
631
+ // --- Text alignment ---
632
+ const vAlignMap = { top: "top", center: "middle", bottom: "bottom" };
633
+ const vAlign = vAlignMap[verticalAlign] || "middle";
634
+ const safeFontSize = Math.max(textSize, 10);
635
+ // --- Text inside shape ---
636
+ const textboxMarkup = text && !msoHasBakedText
637
+ ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
638
+ <div style="display:table;width:100%;height:100%;">
639
+ <div style="display:table-cell;vertical-align:${vAlign};text-align:center;">
640
+ <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
641
+ ${text}
642
+ </div>
643
+ </div>
644
+ </div>
645
+ </v:textbox>`
646
+ : `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
647
+ // --- Final shape markup ---
648
+ return `
649
+ <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
650
+ style="width:${widthPx}px;height:${heightPx}px;
651
+ mso-position-horizontal:center;
652
+ mso-position-vertical:center;"
653
+ ${borderAttrs}
654
+ fill="true" fillcolor="${fillColor}"${extraAttr}>
655
+ ${fillMarkup}
656
+ ${textboxMarkup}
657
+ </v:${tag}>`;
658
+ }
659
+ // ---------- Updated appendOutlookForShape ----------
660
+ async function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts) {
661
+ const widthPx = Math.round(Math.min(outerContainerWidth, innerContainerWidth));
662
+ const heightPx = Math.max(1, Math.round(opts.heightPx));
663
+ const vml = buildVMLShape({
664
+ shape: opts.shape,
665
+ widthPx,
666
+ heightPx,
667
+ imageUrl: opts.msoBakeImageWithText || opts.imageUrl,
668
+ backgroundColor: opts.shapeColor || opts.backgroundColor,
669
+ borderWidth: opts.borderWidth,
670
+ borderColor: opts.borderColor,
671
+ borderRadius: opts.borderRadius,
672
+ text: opts.text,
673
+ textColor: opts.textColor,
674
+ textSize: opts.textSize,
675
+ msoHasBakedText: Boolean(opts.msoBakeImageWithText),
676
+ });
677
+ const pad = opts.padding || {};
678
+ const align = opts.alignment || "left";
679
+ const valign = opts.verticalAlign || "middle";
680
+ return `<!--[if mso]>
681
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
682
+ style="width:${widthPx}px;height:${heightPx}px;">
683
+ <tr>
684
+ <td valign="${valign}"
685
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
686
+ ${vml}
687
+ </td>
688
+ </tr>
689
+ </table>
690
+ <![endif]-->`;
678
691
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-builder-utils",
3
- "version": "1.1.26",
3
+ "version": "1.1.27",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [