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.
package/dist/types/Template.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/types/Template.js
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/utils/jsonToHTML.js
CHANGED
|
@@ -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 = [
|
|
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") &&
|
|
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 = {
|
|
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({
|
|
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 ?? "")
|
|
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
|
|
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
|
|
154
|
-
|
|
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) /
|
|
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
|
|
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
|
|
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
|
-
// ----------
|
|
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,
|
|
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%",
|
|
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
|
-
//
|
|
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
|
|
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
|
|
599
|
-
|
|
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:${
|
|
574
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
607
575
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
608
|
-
border-radius:${
|
|
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
|
|
612
|
-
<div style="
|
|
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:${
|
|
588
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
623
589
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
624
|
-
border-radius:${
|
|
590
|
+
border-radius:${resolvedBorderRadius};
|
|
625
591
|
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
626
|
-
<img src="${imageUrl}" alt="${text || "
|
|
627
|
-
|
|
628
|
-
|
|
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:${
|
|
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:${
|
|
638
|
-
|
|
639
|
-
<div style="width:100%;height:100%;display:flex
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
//
|
|
646
|
-
|
|
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:
|
|
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
|
-
//
|
|
662
|
-
|
|
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
|
-
|
|
675
|
-
|
|
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;"> </div>
|
|
744
|
+
<!--<![endif]-->
|
|
745
|
+
</td>
|
|
746
|
+
</tr>
|
|
747
|
+
</table>
|
|
748
|
+
`;
|
|
749
|
+
return appendOutlookSupport(dividerContent, convertedStyle);
|
|
678
750
|
}
|