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.
- package/dist/utils/jsonToHTML.d.ts.map +1 -1
- package/dist/utils/jsonToHTML.js +230 -197
- package/package.json +1 -1
|
@@ -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;
|
|
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"}
|
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,
|
|
@@ -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};
|
|
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="
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
<
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
// ----------
|
|
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,
|
|
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%",
|
|
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
|
-
//
|
|
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
|
|
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
|
|
579
|
-
|
|
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:${
|
|
538
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
587
539
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
588
|
-
border-radius:${
|
|
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
|
|
592
|
-
<div style="
|
|
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:${
|
|
552
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
603
553
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
604
|
-
border-radius:${
|
|
554
|
+
border-radius:${resolvedBorderRadius};
|
|
605
555
|
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
606
|
-
<img src="${imageUrl}" alt="${text || "
|
|
607
|
-
|
|
608
|
-
|
|
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:${
|
|
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:${
|
|
618
|
-
|
|
619
|
-
<div style="width:100%;height:100%;display:flex
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
//
|
|
626
|
-
|
|
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:
|
|
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
|
-
//
|
|
642
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
}
|