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.
- package/dist/utils/jsonToHTML.js +136 -123
- package/package.json +1 -1
package/dist/utils/jsonToHTML.js
CHANGED
|
@@ -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
|
-
// ----------
|
|
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,
|
|
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%",
|
|
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
|
-
//
|
|
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
|
|
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
|
|
599
|
-
|
|
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:${
|
|
538
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
607
539
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
608
|
-
border-radius:${
|
|
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
|
|
612
|
-
<div style="
|
|
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:${
|
|
552
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
623
553
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
624
|
-
border-radius:${
|
|
554
|
+
border-radius:${resolvedBorderRadius};
|
|
625
555
|
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
626
|
-
<img src="${imageUrl}" alt="${text || "
|
|
627
|
-
|
|
628
|
-
|
|
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:${
|
|
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:${
|
|
638
|
-
|
|
639
|
-
<div style="width:100%;height:100%;display:flex
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
//
|
|
646
|
-
|
|
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:
|
|
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
|
-
//
|
|
662
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
}
|