email-builder-utils 1.1.24 → 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.
@@ -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;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,mBAgJ5E"}
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;AAsVD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAoK5E"}
@@ -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,
@@ -265,9 +267,9 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
265
267
  const visualRows = Math.ceil(total / columns);
266
268
  let html = `
267
269
  <!--[if mso]>
268
- <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%" style="${exports.tableCommonStyle}">
270
+ <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%" style="${exports.tableCommonStyle}border-collapse: separate;border-spacing:${columnGap}px;">
269
271
  <![endif]-->
270
- <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%" role="presentation" style="${exports.tableCommonStyle} ${tableStyles}">
272
+ <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%" role="presentation" style="${exports.tableCommonStyle} ${tableStyles}border-collapse: separate;border-spacing:${columnGap}px;">
271
273
  `;
272
274
  for (let r = 0; r < visualRows; r++) {
273
275
  html += "<tr>";
@@ -284,13 +286,13 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
284
286
  <td
285
287
  width="${widthPercent}%"
286
288
  ${responsive ? 'class="stack-column"' : ""}
287
- style="vertical-align:${verticalAlign}; padding:0; word-break:break-word; ${styles}"
289
+ style="vertical-align:${verticalAlign}; word-break:break-word; ${styles} "
288
290
  >
289
291
  ${childHtml}
290
292
  </td>`;
291
293
  }
292
294
  else {
293
- html += `<td width="${widthPercent}%" ${responsive ? 'class="stack-column"' : ""} style="padding:0;"></td>`;
295
+ html += `<td width="${widthPercent}%" ${responsive ? 'class="stack-column"' : ""} style=""></td>`;
294
296
  }
295
297
  }
296
298
  html += "</tr>";
@@ -359,6 +361,10 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
359
361
  const outerContainerStyles = buildStyles({
360
362
  ...style,
361
363
  width: undefined,
364
+ borderColor: undefined,
365
+ borderRadius: undefined,
366
+ borderWidth: undefined,
367
+ borderStyle: undefined,
362
368
  }, {
363
369
  perChanges: addPxOrPerToAttributes,
364
370
  pxChanges: addPxToAttributes,
@@ -373,78 +379,94 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
373
379
  const vmlLeft = innerContainerWidth / 2 - playIconWidth / 2;
374
380
  const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
375
381
  const videoContent = `
376
- <!--[if mso]>
377
- <v:group xmlns:v="urn:schemas-microsoft-com:vml" coordsize="${innerContainerWidth},${calculatedHeight}"
378
- coordorigin="0,0"
379
- href="${videoLink}"
380
- style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
381
- <v:rect fill="t" stroked="f" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;">
382
- <v:fill src="${resolvedThumbnail}" type="frame"/>
383
- </v:rect>
384
- <v:shape type="#_x0000_t75"
385
- style="position:absolute;
386
- left:${vmlLeft.toFixed(1)}px;
387
- top:${vmlTop.toFixed(1)}px;
388
- width:${playIconWidth}px;
389
- height:${playIconHeight}px;"
390
- alt="Play" href="${videoLink}" title="${altText || "Video"}"
391
- stroked="f" filled="t">
392
- <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
393
- </v:shape>
394
- </v:group>
395
- <![endif]-->
396
-
397
- <!--[if !mso]><!-->
398
- <table
399
- width="${innerContainerWidth}"
400
- cellpadding="0"
401
- cellspacing="0"
402
- border="0"
403
- role="presentation"
404
- style="
405
- background-image: url('${resolvedThumbnail}');
406
- background-size: cover;
407
- background-position: center;
408
-
409
- max-width: ${innerContainerWidth}px;
410
- height: ${calculatedHeight}px;
411
- box-sizing: border-box;
412
- "
413
- align="center"
382
+ <!--[if mso]>
383
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml"
384
+ coordsize="${innerContainerWidth},${calculatedHeight}"
385
+ href="${videoLink}"
386
+ style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
387
+ <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px; stroked="t"
388
+ strokeweight="${borderWidth}px"
389
+ strokecolor="${borderColor}"
390
+ ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
414
391
  >
415
- <tr>
416
- <td style="height: ${calculatedHeight}px; padding: 0; text-align: center; vertical-align: middle; border-radius: ${borderRadius}px;
417
- border: ${borderWidth}px solid ${borderColor};" align="center" valign="middle">
418
- <a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
419
- <img
420
- src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
421
- width="${playIconWidth}"
422
- alt="Play"
423
- style="display: block; border: 0; outline: none; text-decoration: none; height: auto;"
424
- />
425
- </a>
426
- </td>
427
- </tr>
428
- </table>
429
- <!--<![endif]-->
430
- `;
392
+ <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
393
+ </v:rect>
394
+ <v:shape type="#_x0000_t75"
395
+ style="position:absolute;
396
+ left:${vmlLeft.toFixed(1)}px;
397
+ top:${vmlTop.toFixed(1)}px;
398
+ width:${playIconWidth}px;
399
+ height:${playIconHeight}px;"
400
+ alt="Play" href="${videoLink}" title="${altText || "Video"}"
401
+ stroked="f" filled="t">
402
+ <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
403
+ </v:shape>
404
+ </v:group>
405
+ <![endif]-->
406
+
407
+ <!--[if !mso]><!-->
408
+ <table
409
+ width="${innerContainerWidth}"
410
+ cellpadding="0"
411
+ cellspacing="0"
412
+ border="0"
413
+ role="presentation"
414
+ align="${style?.textAlign || "left"}"
415
+ style="
416
+ max-width: ${innerContainerWidth}px;
417
+ width: 100%;
418
+ height: ${calculatedHeight}px;
419
+ background-color: ${style?.backgroundColor || "#FFFFFF"};
420
+ background-image: url('${resolvedThumbnail}');
421
+ background-size: cover;
422
+ background-position: center;
423
+ background-repeat: no-repeat;
424
+ box-sizing: border-box;
425
+ border: ${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};
426
+ border-radius: ${borderRadius}px;
427
+ "
428
+ >
429
+ <tr>
430
+ <td style="padding: 0; height: ${calculatedHeight}px; text-align: center; vertical-align: middle;" valign="middle">
431
+ <a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
432
+ <img
433
+ src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
434
+ width="${playIconWidth}"
435
+ alt="Play"
436
+ style="display: block;
437
+ border: 0;
438
+ outline: none;
439
+ text-decoration: none;
440
+ height: auto;"
441
+ />
442
+ </a>
443
+ </td>
444
+ </tr>
445
+ </table>
446
+ <!--<![endif]-->
447
+ `;
431
448
  const wrapperHtml = `
432
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse;">
433
- <tr>
434
- <td align="center" style="padding:0; ${outerContainerStyles}">
435
- <table border="0" cellpadding="0" cellspacing="0" role="presentation"
436
- align="center"
437
- style="margin:0 auto; max-width:${cellWidthInPx}px; width:${percentWidth}; border-collapse:collapse;">
438
- <tr>
439
- <td align="center" style="text-align:center; padding:0;">
440
- ${videoContent}
441
- </td>
442
- </tr>
443
- </table>
444
- </td>
445
- </tr>
446
- </table>
447
- `;
449
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse;">
450
+ <tr>
451
+ <td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
452
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation"
453
+ align="${style?.textAlign || "left"}"
454
+ style="
455
+ margin:0;
456
+ max-width:${cellWidthInPx}px;
457
+ width:${percentWidth};
458
+ border-collapse:collapse;
459
+ ">
460
+ <tr>
461
+ <td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
462
+ ${videoContent}
463
+ </td>
464
+ </tr>
465
+ </table>
466
+ </td>
467
+ </tr>
468
+ </table>
469
+ `;
448
470
  return wrapperHtml;
449
471
  }
450
472
  // Enhanced Shape Block HTML Conversion using appendOutlookForShape
@@ -463,92 +485,16 @@ function computeArcSize(borderRadius, widthPx) {
463
485
  const px = parseFloat(s.replace("px", "")) || 0;
464
486
  return Math.min(px / widthPx, 1).toFixed(2);
465
487
  }
466
- // ---------- Outlook (MSO) wrapper ----------
467
- async function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts) {
468
- // Use the inner container width for VML sizing (exact user dims)
469
- const widthPx = Math.round(Math.min(outerContainerWidth, innerContainerWidth));
470
- const heightPx = Math.max(1, Math.round(opts.heightPx));
471
- const vml = buildVMLShape({
472
- shape: opts.shape,
473
- widthPx,
474
- heightPx,
475
- imageUrl: opts.msoBakeImageWithText || opts.imageUrl,
476
- backgroundColor: opts.shapeColor || opts.backgroundColor,
477
- borderWidth: opts.borderWidth,
478
- borderColor: opts.borderColor,
479
- borderRadius: opts.borderRadius,
480
- text: opts.text,
481
- textColor: opts.textColor,
482
- // pass raw flag so buildVMLShape knows if image already has text baked-in
483
- msoHasBakedText: Boolean(opts.msoBakeImageWithText),
484
- });
485
- const outlookAlignment = opts.alignment === "center" ? "center" : opts.alignment === "right" ? "right" : "left";
486
- // Wrap the VML inside a table so Outlook aligns it correctly
487
- return `<!--[if mso]>
488
- <table align="${outlookAlignment}" border="0" cellpadding="0" cellspacing="0" style="display:inline-block;">
489
- <tr>
490
- <td style="padding:${opts.padding?.top || 0}px ${opts.padding?.right || 0}px ${opts.padding?.bottom || 0}px ${opts.padding?.left || 0}px;">
491
- ${vml}
492
- </td>
493
- </tr>
494
- </table>
495
- <![endif]-->`;
496
- }
497
- // ---------- VML builder (produces shape + text inside it for MSO) ----------
498
- function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor, msoHasBakedText = false, }) {
499
- const bw = borderWidth || 0;
500
- const bc = borderColor || "transparent";
501
- const hasBorder = bw > 0;
502
- const borderAttributes = hasBorder ? `strokeweight="${bw}px" strokecolor="${bc}"` : `stroked="false"`;
503
- const fillColor = backgroundColor || "#2F80ED";
504
- // choose tag and extra attributes
505
- let tag = "rect";
506
- let extraAttr = "";
507
- if (shape === "circle" || shape === "oval")
508
- tag = "oval";
509
- if (shape === "rounded" || (borderRadius && borderRadius !== "0")) {
510
- tag = "roundrect";
511
- extraAttr = ` arcsize="${computeArcSize(borderRadius, widthPx)}"`;
512
- }
513
- // image fill (if provided)
514
- const fillMarkup = imageUrl ? `<v:fill src="${imageUrl}" type="frame" aspect="atleast" />` : "";
515
- // If MSO is given a baked image with text, don't produce a v:textbox overlay text (image already contains text)
516
- const includeTextbox = !!text && !msoHasBakedText;
517
- // v:textbox: use a table + cell to center the text; avoids many Word quirks
518
- const textboxInner = includeTextbox
519
- ? `<v:textbox inset="0,0,0,0">
520
- <center style="width:${widthPx}px;height:${heightPx}px;display:block;">
521
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="${widthPx}" height="${heightPx}" style="border-collapse:collapse;">
522
- <tr>
523
- <td align="center" valign="middle" style="font-family:Arial, sans-serif;font-size:14px;line-height:1;color:${textColor || "#000"};padding:6px;">
524
- ${text}
525
- </td>
526
- </tr>
527
- </table>
528
- </center>
529
- </v:textbox>`
530
- : // keep an empty textbox so shape sizing behaves consistently when no text
531
- `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
532
- // If there is no imageUrl and no textbox content, use fillcolor for background
533
- const fillAttr = imageUrl ? 'fill="true"' : `fill="true" fillcolor="${fillColor}"`;
534
- return `
535
- <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
536
- style="width:${widthPx}px;height:${heightPx}px;v-text-anchor:middle;"
537
- ${borderAttributes} ${fillAttr}${extraAttr}>
538
- ${fillMarkup}
539
- ${textboxInner}
540
- </v:${tag}>`;
541
- }
542
- // ---------- convertShapeBlock (updated, keeps your structure) ----------
488
+ // ---------- Updated convertShapeBlock function ----------
543
489
  async function convertShapeBlock(blockData) {
544
490
  const { style, props } = blockData.data;
545
- const { shape, text, textColor = "#000000", imageUrl } = props;
546
- 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 || {};
547
493
  const borderRadiusMap = {
548
494
  rectangle: "0",
549
495
  rounded: "10px",
550
496
  circle: "50%",
551
- oval: "50%", // Keep this for modern browsers
497
+ oval: "50%",
552
498
  };
553
499
  let resolvedBorderRadius = borderRadius || borderRadiusMap[shape] || "0";
554
500
  let resolvedWidthPx = typeof width === "number"
@@ -557,73 +503,80 @@ async function convertShapeBlock(blockData) {
557
503
  let resolvedHeightPx = typeof height === "number"
558
504
  ? height
559
505
  : parseInt(height.toString().replace("px", ""), 10) || 150;
560
- // Special handling for different shapes
506
+ // --- Shape specific constraints ---
561
507
  if (shape === "circle") {
562
- // Circle: make it a perfect square with 50% border radius
563
508
  const side = Math.min(resolvedWidthPx, resolvedHeightPx);
564
509
  resolvedWidthPx = side;
565
510
  resolvedHeightPx = side;
566
511
  resolvedBorderRadius = "50%";
567
512
  }
568
513
  else if (shape === "oval") {
514
+ resolvedBorderRadius = "50% / 50%";
569
515
  }
570
- const finalWidthPx = resolvedWidthPx;
571
- const finalHeightPx = resolvedHeightPx;
516
+ const finalBackgroundColor = shapeColor || backgroundColor;
572
517
  const alignmentStyles = {
573
518
  left: "margin-right:auto;margin-left:0;",
574
519
  center: "margin-left:auto;margin-right:auto;",
575
520
  right: "margin-left:auto;margin-right:0;",
576
521
  };
577
522
  const alignmentStyle = alignmentStyles[alignment] || "";
578
- const finalBackgroundColor = shapeColor || backgroundColor;
579
- // --- 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
+ // ============================
580
535
  let nonMsoContent = "";
581
- // For modern browsers, use CSS border-radius
582
- const modernBorderRadius = shape === "oval" ? "50%" : resolvedBorderRadius;
583
- // Case 1: Image + Text → use background-image
584
536
  if (imageUrl && text) {
585
537
  nonMsoContent = `
586
- <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
538
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
587
539
  border:${borderWidth}px ${borderStyle} ${borderColor};
588
- border-radius:${modernBorderRadius};
540
+ border-radius:${resolvedBorderRadius};
589
541
  background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
590
542
  overflow:hidden;${alignmentStyle}${customCss || ""}">
591
- <div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;">
592
- <div style="color:${textColor};text-align:center;padding:8px;box-sizing:border-box;word-break:break-word;
593
- 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;">
594
545
  ${text}
595
546
  </div>
596
547
  </div>
597
548
  </div>`;
598
549
  }
599
- // Case 2: Image only → use <img>
600
550
  else if (imageUrl) {
601
551
  nonMsoContent = `
602
- <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
552
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
603
553
  border:${borderWidth}px ${borderStyle} ${borderColor};
604
- border-radius:${modernBorderRadius};
554
+ border-radius:${resolvedBorderRadius};
605
555
  overflow:hidden;${alignmentStyle}${customCss || ""}">
606
- <img src="${imageUrl}" alt="${text || "Shape image"}"
607
- width="${finalWidthPx}" height="${finalHeightPx}"
608
- 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;" />
609
559
  </div>`;
610
560
  }
611
- // Case 3: No image → solid background
612
561
  else {
562
+ const circlePadding = shape === "circle" ? Math.round(resolvedHeightPx * 0.15) : 8;
613
563
  nonMsoContent = `
614
- <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
564
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
615
565
  background:${finalBackgroundColor};
616
566
  border:${borderWidth}px ${borderStyle} ${borderColor};
617
- border-radius:${modernBorderRadius};
618
- ${alignmentStyle}${customCss || ""}">
619
- <div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;
620
- color:${textColor};text-align:center;padding:8px;box-sizing:border-box;word-break:break-word;">
621
- ${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>
622
573
  </div>
623
574
  </div>`;
624
575
  }
625
- // --- Old Outlook (MSO) VML ---
626
- const outlookContent = await appendOutlookForShape(nonMsoContent, finalWidthPx, finalWidthPx, {
576
+ // ============================
577
+ // Outlook (VML) version
578
+ // ============================
579
+ const outlookContent = await appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
627
580
  shape,
628
581
  imageUrl,
629
582
  backgroundColor,
@@ -631,19 +584,22 @@ async function convertShapeBlock(blockData) {
631
584
  borderWidth,
632
585
  borderColor,
633
586
  borderRadius: resolvedBorderRadius,
634
- heightPx: finalHeightPx,
587
+ heightPx: resolvedHeightPx,
635
588
  text,
636
- textColor,
589
+ textColor: color,
590
+ textSize: fontSize,
591
+ verticalAlign,
637
592
  alignment,
638
593
  padding,
639
- msoBakeImageWithText
594
+ msoBakeImageWithText,
640
595
  });
641
- // Wrap in container table
642
- const containerTable = `
596
+ // ============================
597
+ // Final combined block
598
+ // ============================
599
+ return `
643
600
  <table width="100%" style="border-collapse:collapse;table-layout:fixed;">
644
601
  <tr>
645
- <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;
646
- 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};">
647
603
  ${outlookContent}
648
604
  <!--[if !mso]><!-->
649
605
  ${nonMsoContent}
@@ -651,8 +607,85 @@ async function convertShapeBlock(blockData) {
651
607
  </td>
652
608
  </tr>
653
609
  </table>`;
654
- return appendOutlookSupport(containerTable, buildStyles(style, {
655
- perChanges: addPxOrPerToAttributes,
656
- pxChanges: allPxAttributes,
657
- }));
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]-->`;
658
691
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-builder-utils",
3
- "version": "1.1.24",
3
+ "version": "1.1.27",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [