email-builder-utils 1.1.26 → 1.1.28

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.
@@ -9,7 +9,8 @@ export declare enum BlockType {
9
9
  DIVIDER = "Divider",
10
10
  EMAILLAYOUT = "EmailLayout",
11
11
  VIDEO = "Video",
12
- SHAPE = "Shape"
12
+ SHAPE = "Shape",
13
+ VDivider = "VDivider"
13
14
  }
14
15
  export declare enum visibility {
15
16
  PUBLIC = "PUBLIC",
@@ -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;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"}
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;IACf,QAAQ,aAAa;CACtB;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"}
@@ -14,6 +14,7 @@ var BlockType;
14
14
  BlockType["EMAILLAYOUT"] = "EmailLayout";
15
15
  BlockType["VIDEO"] = "Video";
16
16
  BlockType["SHAPE"] = "Shape";
17
+ BlockType["VDivider"] = "VDivider";
17
18
  })(BlockType || (exports.BlockType = BlockType = {}));
18
19
  var visibility;
19
20
  (function (visibility) {
@@ -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;AAsVD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAoK5E"}
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;AAYD,eAAO,MAAM,gBAAgB,kDAAkD,CAAC;AA2DhF,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AAobD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBA8K5E"}
@@ -6,7 +6,12 @@ exports.convertVideoBlock = convertVideoBlock;
6
6
  const jimp_1 = require("jimp");
7
7
  const types_1 = require("../types");
8
8
  const common_1 = require("./common");
9
- const addPxToAttributes = ["fontSize", "lineHeight", "borderRadius", "borderWidth"];
9
+ const addPxToAttributes = [
10
+ "fontSize",
11
+ "lineHeight",
12
+ "borderRadius",
13
+ "borderWidth",
14
+ ];
10
15
  const addPxOrPerToAttributes = ["width", "height"];
11
16
  const allPxAttributes = [...addPxToAttributes, ...addPxOrPerToAttributes];
12
17
  exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
@@ -37,7 +42,8 @@ function buildStyles(style, { pxChanges, perChanges }) {
37
42
  return;
38
43
  if (value === undefined || value === null || value === "")
39
44
  return;
40
- if ((key === "padding" || key === "buttonPadding") && typeof value === "object") {
45
+ if ((key === "padding" || key === "buttonPadding") &&
46
+ typeof value === "object") {
41
47
  const padding = value;
42
48
  value = `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`;
43
49
  }
@@ -72,6 +78,8 @@ async function convertToHtml(blockData, rootData, cellWidthInPx) {
72
78
  return convertVideoBlock(blockData, cellWidthInPx);
73
79
  case types_1.BlockType.SHAPE:
74
80
  return await convertShapeBlock(blockData);
81
+ case types_1.BlockType.VDivider:
82
+ return convertVerticalDividerBlockToHtml(blockData);
75
83
  default:
76
84
  return "";
77
85
  }
@@ -124,20 +132,36 @@ function convertSpacerBlockToHtml(blockData) {
124
132
  function convertTextBlock(blockData) {
125
133
  const { style, props } = blockData.data;
126
134
  const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, ...rest } = style;
127
- const textBoxStyle = { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth };
135
+ const textBoxStyle = {
136
+ width,
137
+ backgroundColor,
138
+ padding,
139
+ borderRadius,
140
+ borderStyle,
141
+ borderColor,
142
+ borderWidth,
143
+ };
128
144
  const convertedTextStyle = buildStyles(textBoxStyle, {
129
145
  perChanges: [],
130
146
  pxChanges: allPxAttributes,
131
147
  });
132
- const styles = buildStyles({ padding: textContainerPadding, backgroundColor: textContainerBackgroundColor, ...rest }, {
148
+ const styles = buildStyles({
149
+ padding: textContainerPadding,
150
+ backgroundColor: textContainerBackgroundColor,
151
+ ...rest,
152
+ }, {
133
153
  perChanges: [],
134
154
  pxChanges: allPxAttributes,
135
155
  });
136
- const sanitizedText = (props.text ?? "").replaceAll(/<p>/g, "<div>").replaceAll(/<\/p>/g, "</div>");
156
+ const sanitizedText = (props.text ?? "")
157
+ .replaceAll(/<p>/g, "<div>")
158
+ .replaceAll(/<\/p>/g, "</div>");
137
159
  const navigateToUrl = props.navigateToUrl || "";
138
160
  const convertedTextBox = `<div style="display: inline-block; max-width: 100%; box-sizing: border-box; ${convertedTextStyle}">${sanitizedText.replaceAll(/\n/g, "<br>")}</div>`;
139
161
  const textContent = appendOutlookSupport(convertedTextBox, styles);
140
- return navigateToUrl ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit; text-decoration:none; cursor:pointer;">${textContent}</a>` : textContent;
162
+ return navigateToUrl
163
+ ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit; text-decoration:none; cursor:pointer;">${textContent}</a>`
164
+ : textContent;
141
165
  }
142
166
  async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}) {
143
167
  const image = await jimp_1.Jimp.read(imageUrl);
@@ -150,8 +174,12 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
150
174
  const borderColor = style?.borderColor || "transparent";
151
175
  const borderRadius = parseInt(style?.borderRadius) || 0;
152
176
  const useRoundRect = borderRadius > 0;
153
- const arcsize = useRoundRect ? Math.min(borderRadius / scaledHeight, 1).toFixed(2) : "";
154
- const borderAttributes = borderWidth > 0 ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="false"`;
177
+ const arcsize = useRoundRect
178
+ ? Math.min(borderRadius / scaledHeight, 1).toFixed(2)
179
+ : "";
180
+ const borderAttributes = borderWidth > 0
181
+ ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
182
+ : `stroked="false"`;
155
183
  const outlookImage = `<!--[if mso]>
156
184
  <v:${useRoundRect ? "roundrect" : "rect"} xmlns:v="urn:schemas-microsoft-com:vml"
157
185
  style="width:${scaledWidth}px;height:${scaledHeight}px;"
@@ -173,6 +201,9 @@ async function convertImageBlock(blockData, cellWidthInPx) {
173
201
  const { style, props } = blockData.data;
174
202
  const { altText, imageUrl, navigateToUrl } = props;
175
203
  const { width, height, objectFit, borderRadius, borderWidth, borderColor, borderStyle, ...containerStyle } = style;
204
+ const image = await jimp_1.Jimp.read(imageUrl);
205
+ const originalWidth = image.bitmap.width;
206
+ const originalHeight = image.bitmap.height;
176
207
  // Ensure border styles are applied only to the container, not the image
177
208
  const imageStyle = {
178
209
  width,
@@ -181,10 +212,9 @@ async function convertImageBlock(blockData, cellWidthInPx) {
181
212
  borderStyle,
182
213
  borderRadius: borderRadius,
183
214
  borderColor,
215
+ maxWidth: `${originalWidth}px`, // Limit to original size
216
+ maxHeight: `${originalHeight}px`,
184
217
  };
185
- const image = await jimp_1.Jimp.read(imageUrl);
186
- const originalWidth = image.bitmap.width;
187
- const originalHeight = image.bitmap.height;
188
218
  // Add border styles to container for fallback clients
189
219
  const containerStyles = buildStyles({
190
220
  ...containerStyle,
@@ -194,7 +224,11 @@ async function convertImageBlock(blockData, cellWidthInPx) {
194
224
  pxChanges: addPxToAttributes,
195
225
  });
196
226
  const imageElement = `<img src="${imageUrl}" alt="${altText}" style="${imageTagStyles}; max-width: ${originalWidth}px; max-height: ${originalHeight}px;" />`;
197
- const innerContainerWidth = ((typeof width === "string" ? parseInt(width.replace("%", "")) : width) / 100) * (cellWidthInPx - (style?.padding?.left || 0) - (style?.padding?.right || 0));
227
+ const innerContainerWidth = ((typeof width === "string" ? parseInt(width.replace("%", "")) : width) /
228
+ 100) *
229
+ (cellWidthInPx -
230
+ (style?.padding?.left || 0) -
231
+ (style?.padding?.right || 0));
198
232
  const outlookImage = await appendOutlookForImage(imageElement, cellWidthInPx, innerContainerWidth, imageUrl, style);
199
233
  const imageContent = appendOutlookSupport(outlookImage, containerStyles);
200
234
  return navigateToUrl
@@ -203,7 +237,9 @@ async function convertImageBlock(blockData, cellWidthInPx) {
203
237
  }
204
238
  function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
205
239
  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;
206
- const borderAttributes = borderWidth > 0 ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"` : `stroked="false"`;
240
+ const borderAttributes = borderWidth > 0
241
+ ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
242
+ : `stroked="false"`;
207
243
  return `
208
244
  <!--[if mso]>
209
245
  <v:${borderRadius ? "roundrect" : "rect"} xmlns:v="urn:schemas-microsoft-com:vml" href="${navigateToUrl}"
@@ -385,7 +421,9 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
385
421
  <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px; stroked="t"
386
422
  strokeweight="${borderWidth}px"
387
423
  strokecolor="${borderColor}"
388
- ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
424
+ ${borderRadius > 0
425
+ ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"`
426
+ : ""}
389
427
  >
390
428
  <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
391
429
  </v:rect>
@@ -483,92 +521,16 @@ function computeArcSize(borderRadius, widthPx) {
483
521
  const px = parseFloat(s.replace("px", "")) || 0;
484
522
  return Math.min(px / widthPx, 1).toFixed(2);
485
523
  }
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) ----------
524
+ // ---------- Updated convertShapeBlock function ----------
563
525
  async function convertShapeBlock(blockData) {
564
526
  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 || {};
527
+ const { shape, text, imageUrl } = props;
528
+ 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
529
  const borderRadiusMap = {
568
530
  rectangle: "0",
569
531
  rounded: "10px",
570
532
  circle: "50%",
571
- oval: "50%", // Keep this for modern browsers
533
+ oval: "50%",
572
534
  };
573
535
  let resolvedBorderRadius = borderRadius || borderRadiusMap[shape] || "0";
574
536
  let resolvedWidthPx = typeof width === "number"
@@ -577,73 +539,80 @@ async function convertShapeBlock(blockData) {
577
539
  let resolvedHeightPx = typeof height === "number"
578
540
  ? height
579
541
  : parseInt(height.toString().replace("px", ""), 10) || 150;
580
- // Special handling for different shapes
542
+ // --- Shape specific constraints ---
581
543
  if (shape === "circle") {
582
- // Circle: make it a perfect square with 50% border radius
583
544
  const side = Math.min(resolvedWidthPx, resolvedHeightPx);
584
545
  resolvedWidthPx = side;
585
546
  resolvedHeightPx = side;
586
547
  resolvedBorderRadius = "50%";
587
548
  }
588
549
  else if (shape === "oval") {
550
+ resolvedBorderRadius = "50% / 50%";
589
551
  }
590
- const finalWidthPx = resolvedWidthPx;
591
- const finalHeightPx = resolvedHeightPx;
552
+ const finalBackgroundColor = shapeColor || backgroundColor;
592
553
  const alignmentStyles = {
593
554
  left: "margin-right:auto;margin-left:0;",
594
555
  center: "margin-left:auto;margin-right:auto;",
595
556
  right: "margin-left:auto;margin-right:0;",
596
557
  };
597
558
  const alignmentStyle = alignmentStyles[alignment] || "";
598
- const finalBackgroundColor = shapeColor || backgroundColor;
599
- // --- Modern clients content ---
559
+ const verticalAlignStyles = {
560
+ top: "align-items:flex-start;padding-top:8px;",
561
+ center: "align-items:center;",
562
+ bottom: "align-items:flex-end;padding-bottom:8px;",
563
+ };
564
+ const verticalAlignStyle = verticalAlignStyles[verticalAlign] ||
565
+ verticalAlignStyles.center;
566
+ // Text styling (safe across clients)
567
+ const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;text-align:center;color:${color};`;
568
+ // ============================
569
+ // Modern HTML (non-MSO)
570
+ // ============================
600
571
  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
572
  if (imageUrl && text) {
605
573
  nonMsoContent = `
606
- <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
574
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
607
575
  border:${borderWidth}px ${borderStyle} ${borderColor};
608
- border-radius:${modernBorderRadius};
576
+ border-radius:${resolvedBorderRadius};
609
577
  background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
610
578
  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%;">
579
+ <div style="width:100%;height:100%;display:flex;${verticalAlignStyle}justify-content:center;overflow:hidden;">
580
+ <div style="${textSizeStyle}padding:6px;max-width:90%;-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden;">
614
581
  ${text}
615
582
  </div>
616
583
  </div>
617
584
  </div>`;
618
585
  }
619
- // Case 2: Image only → use <img>
620
586
  else if (imageUrl) {
621
587
  nonMsoContent = `
622
- <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
588
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
623
589
  border:${borderWidth}px ${borderStyle} ${borderColor};
624
- border-radius:${modernBorderRadius};
590
+ border-radius:${resolvedBorderRadius};
625
591
  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;" />
592
+ <img src="${imageUrl}" alt="${text || "shape image"}"
593
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
594
+ style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
629
595
  </div>`;
630
596
  }
631
- // Case 3: No image → solid background
632
597
  else {
598
+ const circlePadding = shape === "circle" ? Math.round(resolvedHeightPx * 0.15) : 8;
633
599
  nonMsoContent = `
634
- <div style="display:inline-block;width:${finalWidthPx}px;height:${finalHeightPx}px;
600
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
635
601
  background:${finalBackgroundColor};
636
602
  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 || ""}
603
+ border-radius:${resolvedBorderRadius};
604
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
605
+ <div style="width:100%;height:100%;display:flex;${verticalAlignStyle}justify-content:center;padding:${circlePadding}px;box-sizing:border-box;">
606
+ <div style="${textSizeStyle}max-width:90%;overflow:hidden;">
607
+ ${text || ""}
608
+ </div>
642
609
  </div>
643
610
  </div>`;
644
611
  }
645
- // --- Old Outlook (MSO) VML ---
646
- const outlookContent = await appendOutlookForShape(nonMsoContent, finalWidthPx, finalWidthPx, {
612
+ // ============================
613
+ // Outlook (VML) version
614
+ // ============================
615
+ const outlookContent = await appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
647
616
  shape,
648
617
  imageUrl,
649
618
  backgroundColor,
@@ -651,19 +620,22 @@ async function convertShapeBlock(blockData) {
651
620
  borderWidth,
652
621
  borderColor,
653
622
  borderRadius: resolvedBorderRadius,
654
- heightPx: finalHeightPx,
623
+ heightPx: resolvedHeightPx,
655
624
  text,
656
- textColor,
625
+ textColor: color,
626
+ textSize: fontSize,
627
+ verticalAlign,
657
628
  alignment,
658
629
  padding,
659
- msoBakeImageWithText
630
+ msoBakeImageWithText,
660
631
  });
661
- // Wrap in container table
662
- const containerTable = `
632
+ // ============================
633
+ // Final combined block
634
+ // ============================
635
+ return `
663
636
  <table width="100%" style="border-collapse:collapse;table-layout:fixed;">
664
637
  <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};">
638
+ <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
667
639
  ${outlookContent}
668
640
  <!--[if !mso]><!-->
669
641
  ${nonMsoContent}
@@ -671,8 +643,108 @@ async function convertShapeBlock(blockData) {
671
643
  </td>
672
644
  </tr>
673
645
  </table>`;
674
- return appendOutlookSupport(containerTable, buildStyles(style, {
675
- perChanges: addPxOrPerToAttributes,
646
+ }
647
+ // ---------- Updated VML builder with better text containment ----------
648
+ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor = "#000000", textSize = 14, verticalAlign = "center", msoHasBakedText = false, }) {
649
+ // --- Basic setup ---
650
+ const bw = borderWidth || 0;
651
+ const bc = borderColor || "transparent";
652
+ const borderAttrs = bw > 0 ? `strokeweight="${bw}px" strokecolor="${bc}"` : `stroked="false"`;
653
+ const fillColor = backgroundColor || "#2F80ED";
654
+ const fillMarkup = `<v:fill ${imageUrl ? `src="${imageUrl}" type="frame" aspect="atleast"` : ""} color="${fillColor}" />`;
655
+ // --- Shape tag ---
656
+ let tag = "rect";
657
+ let extraAttr = "";
658
+ if (shape === "circle" || shape === "oval") {
659
+ tag = "oval";
660
+ }
661
+ else if (shape === "rounded") {
662
+ tag = "roundrect";
663
+ extraAttr = `arcsize="${computeArcSize(borderRadius, widthPx)}"`;
664
+ }
665
+ // --- Text alignment ---
666
+ const vAlignMap = { top: "top", center: "middle", bottom: "bottom" };
667
+ const vAlign = vAlignMap[verticalAlign] || "middle";
668
+ const safeFontSize = Math.max(textSize, 10);
669
+ // --- Text inside shape ---
670
+ const textboxMarkup = text && !msoHasBakedText
671
+ ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
672
+ <div style="display:table;width:100%;height:100%;">
673
+ <div style="display:table-cell;vertical-align:${vAlign};text-align:center;">
674
+ <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
675
+ ${text}
676
+ </div>
677
+ </div>
678
+ </div>
679
+ </v:textbox>`
680
+ : `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
681
+ // --- Final shape markup ---
682
+ return `
683
+ <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
684
+ style="width:${widthPx}px;height:${heightPx}px;
685
+ mso-position-horizontal:center;
686
+ mso-position-vertical:center;"
687
+ ${borderAttrs}
688
+ fill="true" fillcolor="${fillColor}"${extraAttr}>
689
+ ${fillMarkup}
690
+ ${textboxMarkup}
691
+ </v:${tag}>`;
692
+ }
693
+ // ---------- Updated appendOutlookForShape ----------
694
+ async function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts) {
695
+ const widthPx = Math.round(Math.min(outerContainerWidth, innerContainerWidth));
696
+ const heightPx = Math.max(1, Math.round(opts.heightPx));
697
+ const vml = buildVMLShape({
698
+ shape: opts.shape,
699
+ widthPx,
700
+ heightPx,
701
+ imageUrl: opts.msoBakeImageWithText || opts.imageUrl,
702
+ backgroundColor: opts.shapeColor || opts.backgroundColor,
703
+ borderWidth: opts.borderWidth,
704
+ borderColor: opts.borderColor,
705
+ borderRadius: opts.borderRadius,
706
+ text: opts.text,
707
+ textColor: opts.textColor,
708
+ textSize: opts.textSize,
709
+ msoHasBakedText: Boolean(opts.msoBakeImageWithText),
710
+ });
711
+ const pad = opts.padding || {};
712
+ const align = opts.alignment || "left";
713
+ const valign = opts.verticalAlign || "middle";
714
+ return `<!--[if mso]>
715
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
716
+ style="width:${widthPx}px;height:${heightPx}px;">
717
+ <tr>
718
+ <td valign="${valign}"
719
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
720
+ ${vml}
721
+ </td>
722
+ </tr>
723
+ </table>
724
+ <![endif]-->`;
725
+ }
726
+ function convertVerticalDividerBlockToHtml(blockData) {
727
+ const { style } = blockData.data;
728
+ const { width, height, dividerColor, ...rest } = style;
729
+ // Convert other styles to inline-safe HTML attributes
730
+ const convertedStyle = buildStyles(rest, {
731
+ perChanges: [],
676
732
  pxChanges: allPxAttributes,
677
- }));
733
+ });
734
+ // Outlook-safe vertical divider
735
+ const dividerContent = `
736
+ <table cellpadding="0" cellspacing="0" border="0" align="center" style="width:auto; ${convertedStyle}">
737
+ <tr>
738
+ <td style="vertical-align: middle; text-align: center;">
739
+ <!--[if mso | IE]>
740
+ <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="${dividerColor}" style="width:${width}px;height:${height}px;" stroke="f"></v:rect>
741
+ <![endif]-->
742
+ <!--[if !mso]><!-- -->
743
+ <div style="display:inline-block;width:${width}px;height:${height}px;background:${dividerColor};line-height:0;font-size:0;">&nbsp;</div>
744
+ <!--<![endif]-->
745
+ </td>
746
+ </tr>
747
+ </table>
748
+ `;
749
+ return appendOutlookSupport(dividerContent, convertedStyle);
678
750
  }
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.28",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [