@cymbal/atoms-email-renderer 0.0.1 → 0.1.0

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/index.cjs CHANGED
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
- var components = require('@react-email/components');
3
+ var mjmlReact = require('@faire/mjml-react');
4
+ var renderToMjml = require('@faire/mjml-react/utils/renderToMjml');
5
+ var mjml2html = require('mjml');
4
6
  var React = require('react');
5
7
 
6
8
  const MIN_LAYOUT_BASIS = 1;
7
- const DEFAULT_LAYOUT_BASIS = 100;
8
9
 
9
10
  function resolveLayoutVisualStyleValue({
10
11
  override,
@@ -31,31 +32,10 @@ function mergeResolvedLayoutWidth(overrides, global) {
31
32
  if (widthOverride === void 0 || widthOverride === null) {
32
33
  return { ...global.width };
33
34
  }
34
- if (widthOverride && typeof widthOverride === "object" && "type" in widthOverride && widthOverride.type === "max") {
35
- const legacyMaxWidth = widthOverride;
36
- return {
37
- basis: global.width.basis,
38
- max: legacyMaxWidth.max ?? 200
39
- };
40
- }
41
- if (widthOverride && typeof widthOverride === "object" && "type" in widthOverride && widthOverride.type === "maxWidth") {
42
- const legacyMaxWidth = widthOverride;
43
- return {
44
- basis: global.width.basis,
45
- max: legacyMaxWidth.maxWidth ?? 200
46
- };
47
- }
48
- if (widthOverride && typeof widthOverride === "object" && "type" in widthOverride && widthOverride.type === "basis") {
49
- const legacyBasis = widthOverride;
50
- return {
51
- basis: legacyBasis.basis ?? DEFAULT_LAYOUT_BASIS,
52
- max: "none"
53
- };
54
- }
55
- const normalizedWidth = widthOverride;
56
35
  return {
57
- basis: normalizedWidth.basis ?? global.width.basis,
58
- max: normalizedWidth.max !== void 0 ? normalizedWidth.max : global.width.max
36
+ basis: widthOverride.basis ?? global.width.basis,
37
+ max: widthOverride.max ?? global.width.max,
38
+ type: widthOverride.type ?? global.width.type
59
39
  };
60
40
  }
61
41
  function resolveTextStyles(overrides, global) {
@@ -92,8 +72,9 @@ function resolveColumnsStyles(overrides, global) {
92
72
  }
93
73
  return {
94
74
  gap: overrides.gap ?? global.gap,
95
- verticalAlign: overrides.verticalAlign ?? global.verticalAlign,
96
- horizontalAlign: overrides.horizontalAlign ?? global.horizontalAlign,
75
+ verticalGap: overrides.verticalGap ?? global.verticalGap,
76
+ contentVerticalAlign: overrides.contentVerticalAlign ?? global.contentVerticalAlign,
77
+ contentHorizontalAlign: overrides.contentHorizontalAlign ?? global.contentHorizontalAlign,
97
78
  format: overrides.format ?? global.format
98
79
  };
99
80
  }
@@ -115,7 +96,7 @@ function resolveRowsStyles(overrides, global) {
115
96
  }
116
97
  return {
117
98
  gap: overrides.gap ?? global.gap,
118
- horizontalAlign: overrides.horizontalAlign ?? global.horizontalAlign
99
+ contentHorizontalAlign: overrides.contentHorizontalAlign ?? global.contentHorizontalAlign
119
100
  };
120
101
  }
121
102
  function resolvePadding(overrides, global) {
@@ -237,31 +218,54 @@ function calculateRowChildWidth({
237
218
  globalAtomStyles
238
219
  }) {
239
220
  const resolved = resolveAtomLayoutStyles(child, globalAtomStyles);
240
- const cap = resolved.width.max;
241
- if (cap === "none") {
242
- return contentWidth;
221
+ if (resolved.width.type === "content") {
222
+ return Math.min(estimateContentWidthForAtom({ atom: child, globalAtomStyles }) ?? contentWidth, contentWidth);
223
+ }
224
+ if (resolved.width.type === "max") {
225
+ return Math.min(resolved.width.max, contentWidth);
243
226
  }
244
- return Math.min(cap, contentWidth);
227
+ return contentWidth;
228
+ }
229
+ function estimateContentWidthForAtom({
230
+ atom,
231
+ globalAtomStyles
232
+ }) {
233
+ if (atom.type === "image" && atom.width > 0) {
234
+ return atom.width;
235
+ }
236
+ if (atom.type === "button") {
237
+ const resolvedText = resolveTextStyles(atom.textStyles, globalAtomStyles.buttonStyles.textStyles);
238
+ const resolvedPadding = resolvePadding(atom.innerPadding, globalAtomStyles.buttonStyles.innerPadding);
239
+ const textLength = Math.max(1, atom.text.replace(/<[^>]*>/g, "").length);
240
+ const averageCharacterWidth = resolvedText.fontSizePx * 0.55;
241
+ return Math.ceil(textLength * averageCharacterWidth + resolvedPadding.left + resolvedPadding.right);
242
+ }
243
+ return null;
245
244
  }
246
245
  const COLUMN_WIDTH_SPLIT_EPS = 1e-6;
247
- function calculateColumnChildWidths({
246
+ function calculateColumnChildWidthAllocationFromSpecs({
248
247
  contentWidth,
249
- children,
250
248
  gap,
251
- globalAtomStyles
249
+ widthSpecs
252
250
  }) {
253
- const childCount = children.length;
251
+ const childCount = widthSpecs.length;
254
252
  if (childCount === 0) {
255
- return [];
253
+ return {
254
+ allocatedPercents: [],
255
+ distributableWidthPx: 0,
256
+ isCappedByMax: [],
257
+ minimumFeasiblePercents: [],
258
+ widthsPx: []
259
+ };
256
260
  }
257
261
  const totalGap = gap * (childCount - 1);
258
262
  const distributable = Math.max(0, contentWidth - totalGap);
259
- const resolvedLayouts = children.map((child) => resolveAtomLayoutStyles(child, globalAtomStyles));
260
- const bases = resolvedLayouts.map((layoutStyles) => layoutStyles.width.basis);
261
- const caps = resolvedLayouts.map(
262
- (layoutStyles) => layoutStyles.width.max === "none" ? Number.POSITIVE_INFINITY : layoutStyles.width.max
263
+ const bases = widthSpecs.map((widthSpec) => widthSpec.basis);
264
+ const caps = widthSpecs.map(
265
+ (widthSpec) => widthSpec.type === "max" ? widthSpec.max : Number.POSITIVE_INFINITY
263
266
  );
264
267
  const widths = Array(childCount).fill(0);
268
+ const isCappedByMax = Array(childCount).fill(false);
265
269
  const active = new Set(Array.from({ length: childCount }, (_, index) => index));
266
270
  let remaining = distributable;
267
271
  let guard = 0;
@@ -290,6 +294,7 @@ function calculateColumnChildWidths({
290
294
  for (const index of clampedIndices) {
291
295
  const assigned = caps[index];
292
296
  widths[index] = assigned;
297
+ isCappedByMax[index] = true;
293
298
  remaining -= assigned;
294
299
  active.delete(index);
295
300
  }
@@ -303,507 +308,956 @@ function calculateColumnChildWidths({
303
308
  break;
304
309
  }
305
310
  }
306
- return widths;
311
+ const allocatedPercents = widths.map((width) => {
312
+ if (distributable <= 0) {
313
+ return 0;
314
+ }
315
+ return Math.round(width / distributable * 100);
316
+ });
317
+ const uncappedIndices = widthSpecs.map((widthSpec, index) => widthSpec.type !== "max" ? index : null).filter((index) => index !== null);
318
+ const minimumFeasiblePercents = allocatedPercents.map((allocatedPercent, index) => {
319
+ if (widthSpecs[index]?.type !== "max" && uncappedIndices.length === 1) {
320
+ return allocatedPercent;
321
+ }
322
+ return distributable > 0 ? 1 : 0;
323
+ });
324
+ return {
325
+ allocatedPercents,
326
+ distributableWidthPx: distributable,
327
+ isCappedByMax,
328
+ minimumFeasiblePercents,
329
+ widthsPx: widths
330
+ };
331
+ }
332
+ function calculateColumnChildWidthAllocation({
333
+ contentWidth,
334
+ children,
335
+ gap,
336
+ globalAtomStyles
337
+ }) {
338
+ const widthSpecs = children.map((child) => resolveAtomLayoutStyles(child, globalAtomStyles).width);
339
+ return calculateColumnChildWidthAllocationFromSpecs({ contentWidth, gap, widthSpecs });
307
340
  }
308
341
  function ensureUrlHasProtocolForPreview(url) {
309
342
  const trimmed = url.trim();
310
343
  if (trimmed === "") {
311
344
  return "";
312
345
  }
313
- if (/^https?:\/\//i.test(trimmed)) {
346
+ if (/^\$[a-z-]+:[a-zA-Z0-9-]+\$$/.test(trimmed)) {
347
+ return trimmed;
348
+ }
349
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)) {
314
350
  return trimmed;
315
351
  }
316
- if (trimmed.startsWith("mailto:")) {
352
+ if (/^(?:https?:\/\/|mailto:|tel:)/i.test(trimmed)) {
317
353
  return trimmed;
318
354
  }
319
355
  return `https://${trimmed}`;
320
356
  }
321
357
 
322
- const atomBorderToReactStyle = (border) => {
323
- const side = border.side;
324
- const spec = `${border.width}px ${border.style} ${border.color}`;
325
- const none = "none";
326
- if (side === "all") {
327
- return { border: spec };
328
- }
329
- if (side === "top") {
330
- return { borderTop: spec, borderRight: none, borderBottom: none, borderLeft: none };
331
- }
332
- if (side === "right") {
333
- return { borderTop: none, borderRight: spec, borderBottom: none, borderLeft: none };
358
+ const ZWNJ_PADDING = "&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;";
359
+ const COMMON_CSS = `
360
+ @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&family=Roboto:ital,wght@0,400;0,700;1,400;1,700&display=swap');
361
+
362
+ a.inherit-color {
363
+ color: inherit !important;
364
+ }
365
+
366
+ .display-link-block a {
367
+ display: block !important;
368
+ }
369
+
370
+ .table-min-width-100 table {
371
+ min-width: 100% !important;
372
+ }`;
373
+ function roundWidth(width) {
374
+ return Math.max(0, Math.round(width));
375
+ }
376
+ function boxOuterWidth(availableWidth, layoutStyles) {
377
+ if (layoutStyles.width.type === "max") {
378
+ return roundWidth(Math.min(layoutStyles.width.max, availableWidth));
334
379
  }
335
- if (side === "bottom") {
336
- return { borderTop: none, borderRight: none, borderBottom: spec, borderLeft: none };
380
+ return roundWidth(availableWidth);
381
+ }
382
+ function isTransparentColor(color) {
383
+ if (!color) {
384
+ return true;
337
385
  }
338
- return { borderTop: none, borderRight: none, borderBottom: none, borderLeft: spec };
339
- };
340
-
341
- const SYSTEM_SAFE_FONTS = /* @__PURE__ */ new Set([
342
- "Arial",
343
- "Arial Black",
344
- "Arial Narrow",
345
- "Calibri",
346
- "Consolas",
347
- "Courier",
348
- "Courier New",
349
- "Franklin Gothic",
350
- "Garamond",
351
- "Georgia",
352
- "Helvetica",
353
- "Lucida Console",
354
- "Lucida Grande",
355
- "Tahoma",
356
- "Times New Roman",
357
- "Trebuchet MS",
358
- "Verdana"
359
- ]);
360
- function collectFontFamilies(doc) {
361
- const families = /* @__PURE__ */ new Set();
362
- const rootStyles = doc.globalAtomStyles;
363
- const addFromTextStyles = (ts) => {
364
- if (!ts) {
365
- return;
366
- }
367
- if (ts.fontFamily) {
368
- families.add(ts.fontFamily);
369
- }
370
- };
371
- addFromTextStyles(rootStyles.titleStyles.textStyles);
372
- addFromTextStyles(rootStyles.subtitleStyles.textStyles);
373
- addFromTextStyles(rootStyles.headingStyles.textStyles);
374
- addFromTextStyles(rootStyles.subheadingStyles.textStyles);
375
- addFromTextStyles(rootStyles.sectionStyles.textStyles);
376
- addFromTextStyles(rootStyles.bodyStyles.textStyles);
377
- addFromTextStyles(rootStyles.accentStyles.textStyles);
378
- addFromTextStyles(rootStyles.captionStyles.textStyles);
379
- addFromTextStyles(rootStyles.promoStyles.textStyles);
380
- addFromTextStyles(rootStyles.inputStyles.textStyles);
381
- addFromTextStyles(rootStyles.optionStyles.textStyles);
382
- const walkAtom = (atom) => {
383
- if (atom.type === "text" || atom.type === "button" || atom.type === "input" || atom.type === "contest" || atom.type === "option") {
384
- addFromTextStyles(atom.textStyles);
385
- }
386
- if ("children" in atom && atom.children) {
387
- atom.children.forEach(walkAtom);
388
- }
389
- };
390
- walkAtom(doc.rootRow);
391
- return [...families].filter((f) => !SYSTEM_SAFE_FONTS.has(f));
386
+ const normalized = color.trim().toLowerCase();
387
+ return normalized === "transparent" || normalized === "rgba(0,0,0,0)" || normalized === "#00000000";
392
388
  }
393
389
  function backgroundColorFromAtomBg(bg) {
394
- if (bg === "none") {
390
+ if (bg === "none" || isTransparentColor(bg.color)) {
395
391
  return void 0;
396
392
  }
397
- if (bg.color) {
398
- return bg.color;
399
- }
400
- return void 0;
393
+ return bg.color;
394
+ }
395
+ function isImageBackground(bg) {
396
+ return bg !== "none" && bg.type === "image" && bg.imageUrl.trim() !== "";
401
397
  }
402
398
  function cornerRadiusToCssValue(radius) {
399
+ if (radius.topLeft === 0 && radius.topRight === 0 && radius.bottomRight === 0 && radius.bottomLeft === 0) {
400
+ return void 0;
401
+ }
403
402
  return `${radius.topLeft}px ${radius.topRight}px ${radius.bottomRight}px ${radius.bottomLeft}px`;
404
403
  }
405
- function layoutHorizontalBlockStyle(horizontalAlign) {
404
+ function paddingToCss(padding) {
405
+ return `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`;
406
+ }
407
+ function layoutPaddingToCss(ls) {
408
+ return `${(ls.padding?.top ?? 0) + (ls.margin?.top ?? 0)}px ${ls.padding?.right ?? 0}px ${(ls.padding?.bottom ?? 0) + (ls.margin?.bottom ?? 0)}px ${ls.padding?.left ?? 0}px`;
409
+ }
410
+ function rowChildSectionPadding(ls, index, count, gap) {
411
+ const top = (index === 0 ? (ls.padding?.top ?? 0) + (ls.margin?.top ?? 0) : 0) + (index > 0 ? gap : 0);
412
+ const bottom = index === count - 1 ? (ls.padding?.bottom ?? 0) + (ls.margin?.bottom ?? 0) : 0;
413
+ return `${top}px ${ls.padding?.right ?? 0}px ${bottom}px ${ls.padding?.left ?? 0}px`;
414
+ }
415
+ function atomBorderToCss(border) {
416
+ const spec = `${border.width}px ${border.style} ${border.color}`;
417
+ if (border.side === "all") {
418
+ return { border: spec };
419
+ }
406
420
  return {
407
- display: "flex",
408
- flexDirection: "row",
409
- justifyContent: horizontalAlign === "left" ? "flex-start" : horizontalAlign === "center" ? "center" : "flex-end",
410
- width: "100%"
421
+ borderTop: border.side === "top" ? spec : "none",
422
+ borderRight: border.side === "right" ? spec : "none",
423
+ borderBottom: border.side === "bottom" ? spec : "none",
424
+ borderLeft: border.side === "left" ? spec : "none"
411
425
  };
412
426
  }
413
- function layoutToWrapperStyle(ls, parentRowBlockHorizontalAlign, forceBlockHorizontalAlign) {
414
- const paddingTop = (ls.padding?.top ?? 0) + (ls.margin?.top ?? 0);
415
- const paddingBottom = (ls.padding?.bottom ?? 0) + (ls.margin?.bottom ?? 0);
416
- const paddingLeft = ls.padding?.left ?? 0;
417
- const paddingRight = ls.padding?.right ?? 0;
418
- const hasPadding = paddingTop > 0 || paddingRight > 0 || paddingBottom > 0 || paddingLeft > 0;
419
- const blockAlign = forceBlockHorizontalAlign !== void 0 ? forceBlockHorizontalAlign : effectiveBlockHorizontalAlign({
420
- layoutHorizontalAlign: ls.horizontalAlign,
421
- parentRowBlockHorizontalAlign
427
+ function mjmlLayoutProps(ls) {
428
+ const backgroundColor = backgroundColorFromAtomBg(ls.background);
429
+ const backgroundImageProps = isImageBackground(ls.background) ? {
430
+ backgroundUrl: ls.background.imageUrl,
431
+ backgroundSize: ls.background.imageSize,
432
+ backgroundRepeat: ls.background.imageRepeat ? "repeat" : "no-repeat"
433
+ } : {};
434
+ return compactRecord({
435
+ padding: layoutPaddingToCss(ls),
436
+ backgroundColor,
437
+ borderRadius: cornerRadiusToCssValue(ls.borderRadius),
438
+ ...ls.border !== "none" ? atomBorderToCss(ls.border) : {},
439
+ ...backgroundImageProps
422
440
  });
441
+ }
442
+ function suppressLayoutBackground(layoutStyles) {
423
443
  return {
424
- ...hasPadding ? { padding: `${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px` } : {},
425
- ...backgroundColorFromAtomBg(ls.background) ? { backgroundColor: backgroundColorFromAtomBg(ls.background) } : {},
426
- ...ls.border !== "none" ? atomBorderToReactStyle(ls.border) : {},
427
- ...ls.borderRadius ? { borderRadius: cornerRadiusToCssValue(ls.borderRadius) } : {},
428
- ...layoutHorizontalBlockStyle(blockAlign)
444
+ ...layoutStyles,
445
+ background: "none"
429
446
  };
430
447
  }
448
+ function textStyleToMjmlProps(styles) {
449
+ return compactRecord({
450
+ color: styles.color,
451
+ fontWeight: Number(styles.fontWeight),
452
+ fontStyle: styles.fontStyle === "normal" ? void 0 : styles.fontStyle,
453
+ textTransform: styles.textTransform === "none" ? void 0 : styles.textTransform,
454
+ textDecoration: styles.textDecoration === "none" ? void 0 : styles.textDecoration,
455
+ fontSize: `${styles.fontSizePx}px`,
456
+ fontFamily: styles.fontFamily,
457
+ align: styles.align,
458
+ lineHeight: lineHeightToCss(styles.lineHeight),
459
+ letterSpacing: styles.letterSpacingEm === 0 ? void 0 : `${styles.letterSpacingEm}em`
460
+ });
461
+ }
431
462
  function textStyleToCss(styles) {
432
- return {
463
+ return compactRecord({
433
464
  color: styles.color,
434
465
  fontWeight: Number(styles.fontWeight),
435
- fontStyle: styles.fontStyle,
436
- textTransform: styles.textTransform,
437
- textDecoration: styles.textDecoration,
438
- fontSize: styles.fontSizePx,
466
+ fontStyle: styles.fontStyle === "normal" ? void 0 : styles.fontStyle,
467
+ textTransform: styles.textTransform === "none" ? void 0 : styles.textTransform,
468
+ textDecoration: styles.textDecoration === "none" ? void 0 : styles.textDecoration,
469
+ fontSize: `${styles.fontSizePx}px`,
439
470
  fontFamily: styles.fontFamily,
440
471
  textAlign: styles.align,
441
- lineHeight: styles.lineHeight,
442
- letterSpacing: `${styles.letterSpacingEm}em`,
472
+ lineHeight: lineHeightToCss(styles.lineHeight),
473
+ letterSpacing: styles.letterSpacingEm === 0 ? void 0 : `${styles.letterSpacingEm}em`,
443
474
  margin: 0
444
- };
475
+ });
445
476
  }
446
- function headingLevel(textType) {
447
- switch (textType) {
448
- case "title":
449
- return "h1";
450
- case "subtitle":
451
- return "h2";
452
- case "heading":
453
- return "h3";
454
- default:
455
- return "h1";
477
+ function lineHeightToCss(lineHeight) {
478
+ if (typeof lineHeight === "number") {
479
+ return `${lineHeight * 100}%`;
480
+ }
481
+ const trimmed = lineHeight.trim();
482
+ if (/^\d*\.?\d+$/.test(trimmed)) {
483
+ return `${Number(trimmed) * 100}%`;
484
+ }
485
+ return trimmed;
486
+ }
487
+ function compactRecord(record) {
488
+ return Object.fromEntries(
489
+ Object.entries(record).filter(([, value]) => value !== void 0 && value !== null && value !== "")
490
+ );
491
+ }
492
+ function alignAttr(align) {
493
+ return align;
494
+ }
495
+ function escapeHtml(value) {
496
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
497
+ }
498
+ function cssStyle(style) {
499
+ return Object.entries(style).filter(([, value]) => value !== void 0 && value !== "").map(([key, value]) => `${key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)}:${value}`).join(";");
500
+ }
501
+ function textHtmlWithLineBreaks(html, underlineLinks, linkColor) {
502
+ return paragraphAwareLineBreaks(
503
+ emailTransformHtml(
504
+ markdownLinksToAnchors(html, underlineLinks, linkColor),
505
+ underlineLinks,
506
+ linkColor
507
+ )
508
+ );
509
+ }
510
+ function paragraphAwareLineBreaks(html) {
511
+ const normalizedHtml = html.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
512
+ const paragraphTagRegex = /<p\b([^>]*)>/gi;
513
+ let currentIndex = 0;
514
+ let output = "";
515
+ let match = paragraphTagRegex.exec(normalizedHtml);
516
+ while (match) {
517
+ let plainSegment = normalizedHtml.slice(currentIndex, match.index);
518
+ if (plainSegment.endsWith("\n")) {
519
+ plainSegment = plainSegment.slice(0, -1);
520
+ }
521
+ output += lineBreaksForPlainTextSegment(plainSegment);
522
+ const contentStartIndex = paragraphTagRegex.lastIndex;
523
+ const closingIndex = normalizedHtml.toLowerCase().indexOf("</p>", contentStartIndex);
524
+ if (closingIndex === -1) {
525
+ output += lineBreaksForPlainTextSegment(normalizedHtml.slice(match.index));
526
+ currentIndex = normalizedHtml.length;
527
+ break;
528
+ }
529
+ output += emailParagraphHtml(match[1] ?? "", normalizedHtml.slice(contentStartIndex, closingIndex));
530
+ currentIndex = closingIndex + "</p>".length;
531
+ if (normalizedHtml[currentIndex] === "\n") {
532
+ currentIndex += 1;
533
+ }
534
+ paragraphTagRegex.lastIndex = currentIndex;
535
+ match = paragraphTagRegex.exec(normalizedHtml);
536
+ }
537
+ if (currentIndex < normalizedHtml.length) {
538
+ output += lineBreaksForPlainTextSegment(normalizedHtml.slice(currentIndex));
539
+ }
540
+ return output;
541
+ }
542
+ function lineBreaksForPlainTextSegment(segment) {
543
+ return segment.replace(/\n/g, "<br />");
544
+ }
545
+ function emailParagraphHtml(attrs, content) {
546
+ const textAlign = extractTextAlignFromAttributes(attrs);
547
+ const style = textAlign ? `margin:0;text-align:${escapeHtml(textAlign)};` : "margin:0;";
548
+ return `<p style="${style}">${lineBreaksForPlainTextSegment(content)}</p>`;
549
+ }
550
+ function extractTextAlignFromAttributes(attrs) {
551
+ const styleMatch = attrs.match(/style=(["'])(.*?)\1/i);
552
+ const style = styleMatch?.[2];
553
+ if (!style) {
554
+ return null;
555
+ }
556
+ const textAlignMatch = style.match(/(?:^|;)\s*text-align\s*:\s*(left|right|center|justify)\s*(?:;|$)/i);
557
+ return textAlignMatch?.[1]?.toLowerCase() ?? null;
558
+ }
559
+ function markdownLinksToAnchors(html, underlineLinks, linkColor) {
560
+ return html.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, label, rawHref, offset) => {
561
+ const href = ensureUrlHasProtocolForPreview(rawHref);
562
+ const textDecoration = getActiveSpanTextDecorationAtOffset(html, offset) ?? defaultLinkTextDecoration(underlineLinks);
563
+ const color = getActiveSpanColorAtOffset(html, offset) ?? defaultLinkColor(linkColor);
564
+ return `<a class="inherit-color" style="color: ${escapeHtml(color)} !important; text-decoration: ${escapeHtml(
565
+ textDecoration
566
+ )};" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(label)}</a>`;
567
+ });
568
+ }
569
+ function emailTransformHtml(html, underlineLinks, linkColor) {
570
+ html = html.replace(/style="([^"]*font-size:\s*(\d+)px[^"]*)"/gi, (fullMatch, styleContent, fontSize) => {
571
+ if (/line-height\s*:/i.test(styleContent)) {
572
+ return fullMatch;
573
+ }
574
+ return `style="${styleContent}; line-height: ${textPixelLineHeight(Number(fontSize))};"`;
575
+ });
576
+ html = html.replace(
577
+ /(<span[^>]*>)?<a([^>]*)style="([^"]*?)"([^>]*)>(.*?)<\/a>/gi,
578
+ (_match, span, aAttrsStart, aStyle, aAttrsEnd, aContent) => {
579
+ const nextStyle = appendAnchorEmailStyle(
580
+ aStyle,
581
+ extractTextDecorationFromSpanTag(span) ?? defaultLinkTextDecoration(underlineLinks),
582
+ extractColorFromSpanTag(span) ?? defaultLinkColor(linkColor)
583
+ );
584
+ return `${span ?? ""}<a${ensureClassAttr(`${aAttrsStart}${aAttrsEnd}`, "inherit-color")} style="${nextStyle}">${aContent}</a>`;
585
+ }
586
+ );
587
+ html = html.replace(/(<span[^>]*>)?<a((?:(?!style=)[^>])*)>(.*?)<\/a>/gi, (_match, span, attrs, content) => {
588
+ const textDecoration = extractTextDecorationFromSpanTag(span) ?? defaultLinkTextDecoration(underlineLinks);
589
+ const color = extractColorFromSpanTag(span) ?? defaultLinkColor(linkColor);
590
+ return `${span ?? ""}<a${ensureClassAttr(attrs, "inherit-color")} style="${appendAnchorEmailStyle("", textDecoration, color)}">${content}</a>`;
591
+ });
592
+ return html;
593
+ }
594
+ function defaultLinkColor(linkColor) {
595
+ return linkColor.trim() === "" ? "inherit" : linkColor;
596
+ }
597
+ function defaultLinkTextDecoration(underlineLinks) {
598
+ return underlineLinks ? "underline" : "none";
599
+ }
600
+ function extractTextDecorationFromSpanTag(spanTag) {
601
+ if (!spanTag) {
602
+ return null;
603
+ }
604
+ const match = spanTag.match(/text-decoration\s*:\s*([^;"]+)/i);
605
+ return match?.[1]?.trim() ?? null;
606
+ }
607
+ function extractColorFromSpanTag(spanTag) {
608
+ if (!spanTag) {
609
+ return null;
610
+ }
611
+ const match = spanTag.match(/(?:^|[;"\s])color\s*:\s*([^;"]+)/i);
612
+ const color = match?.[1]?.trim();
613
+ if (!color || color === "undefined") {
614
+ return null;
615
+ }
616
+ return color;
617
+ }
618
+ function getActiveSpanTextDecorationAtOffset(html, offset) {
619
+ const spanStack = [];
620
+ const spanTagRegex = /<span[^>]*>|<\/span>/gi;
621
+ const prefix = html.slice(0, offset);
622
+ let match = spanTagRegex.exec(prefix);
623
+ while (match) {
624
+ if (match[0].startsWith("</")) {
625
+ spanStack.pop();
626
+ } else {
627
+ spanStack.push(extractTextDecorationFromSpanTag(match[0]));
628
+ }
629
+ match = spanTagRegex.exec(prefix);
630
+ }
631
+ for (let i = spanStack.length - 1; i >= 0; i -= 1) {
632
+ if (spanStack[i] !== null) {
633
+ return spanStack[i];
634
+ }
635
+ }
636
+ return null;
637
+ }
638
+ function getActiveSpanColorAtOffset(html, offset) {
639
+ const spanStack = [];
640
+ const spanTagRegex = /<span[^>]*>|<\/span>/gi;
641
+ const prefix = html.slice(0, offset);
642
+ let match = spanTagRegex.exec(prefix);
643
+ while (match) {
644
+ if (match[0].startsWith("</")) {
645
+ spanStack.pop();
646
+ } else {
647
+ spanStack.push(extractColorFromSpanTag(match[0]));
648
+ }
649
+ match = spanTagRegex.exec(prefix);
650
+ }
651
+ for (let i = spanStack.length - 1; i >= 0; i -= 1) {
652
+ if (spanStack[i] !== null) {
653
+ return spanStack[i];
654
+ }
655
+ }
656
+ return null;
657
+ }
658
+ function ensureClassAttr(attrs, className) {
659
+ if (/class="/i.test(attrs)) {
660
+ return attrs.replace(
661
+ /class="([^"]*)"/i,
662
+ (_match, classes) => classes.split(/\s+/).includes(className) ? `class="${classes}"` : `class="${classes} ${className}"`
663
+ );
664
+ }
665
+ return `${attrs} class="${className}"`;
666
+ }
667
+ function appendAnchorEmailStyle(style, textDecoration, color) {
668
+ const withColor = /color\s*:/i.test(style) ? style : `${style} color: ${color} !important;`;
669
+ return /text-decoration\s*:/i.test(withColor) ? withColor.trim() : `${withColor} text-decoration: ${textDecoration};`.trim();
670
+ }
671
+ function textPixelLineHeight(fontSize) {
672
+ if (fontSize < 12) {
673
+ return `${Math.round(1.5 * fontSize)}px`;
674
+ }
675
+ const t = (fontSize - 12) / (40 - 12);
676
+ const factor = 1.5 - 0.5 * t;
677
+ return `${Math.round(fontSize * factor)}px`;
678
+ }
679
+ function rawTableStyle(width, extra) {
680
+ return cssStyle({
681
+ borderCollapse: "collapse",
682
+ borderSpacing: 0,
683
+ margin: 0,
684
+ padding: 0,
685
+ tableLayout: "fixed",
686
+ width: `${width}px`,
687
+ ...extra
688
+ });
689
+ }
690
+ function rawCellStyle(width, extra) {
691
+ return cssStyle({
692
+ margin: 0,
693
+ padding: 0,
694
+ width: `${width}px`,
695
+ ...extra
696
+ });
697
+ }
698
+ function rawLayoutStyle(ls, extra) {
699
+ return cssStyle({
700
+ padding: layoutPaddingToCss(ls),
701
+ backgroundColor: backgroundColorFromAtomBg(ls.background),
702
+ backgroundImage: isImageBackground(ls.background) ? `url("${ls.background.imageUrl}")` : void 0,
703
+ backgroundSize: isImageBackground(ls.background) ? ls.background.imageSize : void 0,
704
+ backgroundRepeat: isImageBackground(ls.background) ? ls.background.imageRepeat ? "repeat" : "no-repeat" : void 0,
705
+ backgroundPosition: isImageBackground(ls.background) ? "center" : void 0,
706
+ ...ls.border !== "none" ? atomBorderToCss(ls.border) : {},
707
+ borderRadius: cornerRadiusToCssValue(ls.borderRadius),
708
+ boxSizing: "border-box",
709
+ ...extra
710
+ });
711
+ }
712
+ function renderRawWrappedBox(html, width, layout) {
713
+ const hasLayout = layoutPaddingToCss(layout) !== "0px 0px 0px 0px" || backgroundColorFromAtomBg(layout.background) || isImageBackground(layout.background) || layout.border !== "none" || cornerRadiusToCssValue(layout.borderRadius);
714
+ if (!hasLayout) {
715
+ return html;
456
716
  }
717
+ return `<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="${width}" style="${rawTableStyle(
718
+ width
719
+ )}"><tr><td width="${width}" style="${rawCellStyle(width, { padding: 0 })};${rawLayoutStyle(layout)}">${html}</td></tr></table>`;
457
720
  }
458
- const EmailTextAtom = ({ atom, ctx }) => {
721
+ const RawHtml = ({ html }) => /* @__PURE__ */ React.createElement(mjmlReact.MjmlRaw, { dangerouslySetInnerHTML: { __html: html } });
722
+ function renderTextAtom(atom, ctx) {
459
723
  const rootTextStyles = getRootTextStyles(atom.textType ?? "body", ctx.globalAtomStyles);
460
724
  const resolvedText = resolveTextStyles(atom.textStyles, rootTextStyles.textStyles);
461
725
  const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
462
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign);
463
- const cssStyles = textStyleToCss(resolvedText);
464
- const inner = /* @__PURE__ */ React.createElement("span", { dangerouslySetInnerHTML: { __html: atom.text } });
465
- if (atom.textType === "title" || atom.textType === "subtitle" || atom.textType === "heading") {
466
- return /* @__PURE__ */ React.createElement(components.Heading, { as: headingLevel(atom.textType), style: { ...wrapperStyle, ...cssStyles, width: "100%" } }, inner);
467
- }
468
- return /* @__PURE__ */ React.createElement(components.Text, { style: { ...wrapperStyle, ...cssStyles, width: "100%" } }, inner);
469
- };
470
- const EmailButtonAtom = ({ atom, ctx }) => {
726
+ return /* @__PURE__ */ React.createElement(
727
+ mjmlReact.MjmlText,
728
+ {
729
+ ...textStyleToMjmlProps(resolvedText),
730
+ ...mjmlLayoutProps(resolvedLayout),
731
+ cssClass: "atoms-text",
732
+ dangerouslySetInnerHTML: { __html: textHtmlWithLineBreaks(atom.text, ctx.underlineLinks, ctx.linkColor) }
733
+ }
734
+ );
735
+ }
736
+ function renderButtonAtom(atom, ctx) {
471
737
  const resolvedText = resolveTextStyles(atom.textStyles, ctx.globalAtomStyles.buttonStyles.textStyles);
472
738
  const resolvedInnerPadding = resolvePadding(atom.innerPadding, ctx.globalAtomStyles.buttonStyles.innerPadding);
473
739
  const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
474
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign);
475
- const cssStyles = textStyleToCss(resolvedText);
476
- const buttonStyle = {
477
- ...cssStyles,
478
- display: "block",
479
- width: "100%",
480
- boxSizing: "border-box",
481
- textDecoration: "none",
482
- padding: `${resolvedInnerPadding.top}px ${resolvedInnerPadding.right}px ${resolvedInnerPadding.bottom}px ${resolvedInnerPadding.left}px`,
483
- maxWidth: "100%"
484
- };
485
- return /* @__PURE__ */ React.createElement("div", { style: wrapperStyle }, /* @__PURE__ */ React.createElement(components.Button, { href: atom.href || "#", style: buttonStyle }, /* @__PURE__ */ React.createElement("span", { dangerouslySetInnerHTML: { __html: atom.text } })));
486
- };
487
- const EmailImageAtom = ({ atom, ctx }) => {
740
+ const href = atom.href ? ensureUrlHasProtocolForPreview(atom.href) : "#";
741
+ const width = roundWidth(ctx.contentWidthPx);
742
+ const widthProp = resolvedLayout.width.type === "content" ? void 0 : `${width}px`;
743
+ return /* @__PURE__ */ React.createElement(
744
+ mjmlReact.MjmlButton,
745
+ {
746
+ ...textStyleToMjmlProps(resolvedText),
747
+ ...mjmlLayoutProps(resolvedLayout),
748
+ cssClass: "display-link-block",
749
+ href,
750
+ width: widthProp,
751
+ padding: layoutPaddingToCss(resolvedLayout),
752
+ innerPadding: paddingToCss(resolvedInnerPadding),
753
+ backgroundColor: backgroundColorFromAtomBg(resolvedLayout.background),
754
+ borderRadius: cornerRadiusToCssValue(resolvedLayout.borderRadius),
755
+ dangerouslySetInnerHTML: { __html: atom.text }
756
+ }
757
+ );
758
+ }
759
+ function renderImageAtom(atom, ctx) {
488
760
  const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
489
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign);
490
- const imgStyle = {
491
- width: "100%",
492
- display: "block",
493
- ...resolvedLayout.borderRadius ? { borderRadius: cornerRadiusToCssValue(resolvedLayout.borderRadius) } : {}
494
- };
495
- const img = /* @__PURE__ */ React.createElement(components.Img, { src: atom.src, alt: "", style: imgStyle });
496
- if (atom.href) {
497
- return /* @__PURE__ */ React.createElement("div", { style: wrapperStyle }, /* @__PURE__ */ React.createElement(components.Link, { href: ensureUrlHasProtocolForPreview(atom.href), style: { display: "inline-block", width: "100%" } }, img));
761
+ const width = roundWidth(ctx.contentWidthPx);
762
+ const height = scaledImageHeight(atom, width);
763
+ return /* @__PURE__ */ React.createElement(
764
+ mjmlReact.MjmlImage,
765
+ {
766
+ ...mjmlLayoutProps(resolvedLayout),
767
+ src: atom.src,
768
+ href: atom.href ? ensureUrlHasProtocolForPreview(atom.href) : void 0,
769
+ alt: "",
770
+ width: `${width}px`,
771
+ height: height === void 0 ? void 0 : `${height}px`,
772
+ padding: layoutPaddingToCss(resolvedLayout),
773
+ borderRadius: cornerRadiusToCssValue(resolvedLayout.borderRadius)
774
+ }
775
+ );
776
+ }
777
+ function scaledImageHeight(atom, width) {
778
+ if (atom.width <= 0 || atom.height <= 0) {
779
+ return void 0;
498
780
  }
499
- return /* @__PURE__ */ React.createElement("div", { style: wrapperStyle }, img);
500
- };
501
- const EmailDividerAtom = ({ atom, ctx }) => {
781
+ return Math.max(1, Math.round(width * atom.height / atom.width));
782
+ }
783
+ function renderDividerAtom(atom, ctx) {
502
784
  const resolvedDivider = resolveDividerStyles(atom.dividerStyles, ctx.globalAtomStyles.dividerStyles.dividerStyles);
503
785
  const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
504
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign);
505
786
  return /* @__PURE__ */ React.createElement(
506
- components.Hr,
787
+ mjmlReact.MjmlDivider,
507
788
  {
508
- style: {
509
- ...wrapperStyle,
510
- ...atomBorderToReactStyle(resolvedDivider)
511
- }
789
+ ...mjmlLayoutProps(resolvedLayout),
790
+ borderColor: resolvedDivider.color,
791
+ borderStyle: resolvedDivider.style,
792
+ borderWidth: `${resolvedDivider.width}px`,
793
+ padding: layoutPaddingToCss(resolvedLayout)
512
794
  }
513
795
  );
514
- };
515
- const fieldBoxStyle = {
516
- border: "1px solid #555",
517
- borderRadius: 8,
518
- boxSizing: "border-box",
519
- color: "#999",
520
- fontSize: 14,
521
- minHeight: 40,
522
- padding: "10px 12px",
523
- width: "100%"
524
- };
525
- const EmailInputAtom = ({ atom, ctx }) => {
526
- const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
527
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign);
528
- const resolvedText = resolveTextStyles(atom.textStyles, ctx.globalAtomStyles.inputStyles.textStyles);
529
- const resolvedInnerPadding = resolvePadding(atom.innerPadding, ctx.globalAtomStyles.inputStyles.innerPadding);
530
- const fieldStyle = {
531
- ...fieldBoxStyle,
532
- ...textStyleToCss(resolvedText),
533
- backgroundColor: backgroundColorFromAtomBg(resolvedLayout.background),
534
- border: resolvedLayout.border !== "none" ? `${resolvedLayout.border.width}px ${resolvedLayout.border.style} ${resolvedLayout.border.color}` : fieldBoxStyle.border,
535
- borderRadius: cornerRadiusToCssValue(resolvedLayout.borderRadius),
536
- padding: `${resolvedInnerPadding.top}px ${resolvedInnerPadding.right}px ${resolvedInnerPadding.bottom}px ${resolvedInnerPadding.left}px`
537
- };
538
- return /* @__PURE__ */ React.createElement("div", { style: wrapperStyle }, /* @__PURE__ */ React.createElement("div", { style: fieldStyle }, atom.placeholder || "Text input"));
539
- };
540
- const EmailQuestionAtom = ({ atom, ctx }) => {
541
- const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
542
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign);
543
- const resolvedInnerPadding = resolvePadding(atom.innerPadding, ctx.globalAtomStyles.questionStyles.innerPadding);
544
- const dropdownStyle = {
545
- ...fieldBoxStyle,
546
- padding: `${resolvedInnerPadding.top}px ${resolvedInnerPadding.right}px ${resolvedInnerPadding.bottom}px ${resolvedInnerPadding.left}px`
547
- };
548
- const optionNodes = atom.children.map((option) => /* @__PURE__ */ React.createElement(
549
- components.Text,
550
- {
551
- key: option.id,
552
- style: {
553
- ...textStyleToCss(resolveTextStyles(option.textStyles, ctx.globalAtomStyles.optionStyles.textStyles)),
554
- margin: "0 0 6px 0"
555
- }
556
- },
557
- "[ ] ",
558
- option.text
559
- ));
560
- return /* @__PURE__ */ React.createElement("div", { style: wrapperStyle }, atom.questionType === "dropdown" ? /* @__PURE__ */ React.createElement("div", { style: dropdownStyle }, "Select an option") : optionNodes);
561
- };
562
- const EmailOptionAtom = ({ atom, ctx }) => {
563
- const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
564
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign);
565
- const resolvedText = resolveTextStyles(atom.textStyles, ctx.globalAtomStyles.optionStyles.textStyles);
566
- return /* @__PURE__ */ React.createElement("div", { style: wrapperStyle }, /* @__PURE__ */ React.createElement(components.Text, { style: { ...textStyleToCss(resolvedText), margin: 0 } }, atom.text));
567
- };
568
- const EmailContestAtom = ({ atom, ctx }) => {
569
- const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
570
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign);
571
- const resolvedText = resolveTextStyles(atom.textStyles, ctx.globalAtomStyles.buttonStyles.textStyles);
572
- const resolvedButtonLayout = resolvedLayout;
573
- const resolvedInnerPadding = resolvePadding(atom.innerPadding, ctx.globalAtomStyles.buttonStyles.innerPadding);
574
- const buttonBackground = resolvedButtonLayout.background !== "none" ? backgroundColorFromAtomBg(resolvedButtonLayout.background) : void 0;
575
- const buttonStyle = {
576
- ...textStyleToCss(resolvedText),
577
- backgroundColor: buttonBackground,
578
- borderRadius: cornerRadiusToCssValue(resolvedButtonLayout.borderRadius),
579
- boxSizing: "border-box",
580
- display: "block",
581
- minHeight: Math.max(42, resolvedText.fontSizePx * 2.5),
582
- padding: `${resolvedInnerPadding.top}px ${resolvedInnerPadding.right}px ${resolvedInnerPadding.bottom}px ${resolvedInnerPadding.left}px`,
583
- textDecoration: "none",
584
- width: "100%"
585
- };
586
- const label = atom.contestType === "facebook" ? "Share link on Facebook" : atom.contestType === "instagram" ? `Follow ${atom.identifier}` : "Share this contest";
587
- return /* @__PURE__ */ React.createElement("div", { style: wrapperStyle }, /* @__PURE__ */ React.createElement(components.Button, { href: "#", style: buttonStyle }, label, " +", atom.entriesPerSubmission));
588
- };
589
- const EmailAutomationUpcomingEventsAtom = ({
590
- atom,
591
- ctx
592
- }) => {
593
- return null;
594
- };
595
- const EmailAutomationTriggeredEventAtom = ({
596
- atom,
597
- ctx
598
- }) => {
599
- return null;
600
- };
601
- const EmailFormAtom = ({ atom, ctx }) => {
602
- const rowsAtom = {
603
- atomKey: atom.atomKey,
604
- type: "rows",
605
- source: null,
606
- children: atom.children,
607
- rowsStyles: atom.rowsStyles,
608
- layoutStyles: atom.layoutStyles
609
- };
610
- const belowSubmitRowsAtom = {
611
- atomKey: atom.atomKey,
612
- type: "rows",
613
- source: null,
614
- children: atom.belowSubmitChildren,
615
- rowsStyles: null,
616
- layoutStyles: null
617
- };
618
- return /* @__PURE__ */ React.createElement(components.Section, null, /* @__PURE__ */ React.createElement(EmailRowsAtom, { atom: rowsAtom, ctx }), atom.belowSubmitChildren.length > 0 && /* @__PURE__ */ React.createElement(EmailRowsAtom, { atom: belowSubmitRowsAtom, ctx }));
619
- };
620
- const EmailAtom = ({ atom, ctx }) => {
796
+ }
797
+ function renderColumnContentAtom(atom, ctx) {
621
798
  switch (atom.type) {
622
799
  case "text":
623
- return /* @__PURE__ */ React.createElement(EmailTextAtom, { atom, ctx });
800
+ return React.cloneElement(renderTextAtom(atom, ctx), { key: ctx.keyPrefix });
624
801
  case "button":
625
- return /* @__PURE__ */ React.createElement(EmailButtonAtom, { atom, ctx });
802
+ return React.cloneElement(renderButtonAtom(atom, ctx), { key: ctx.keyPrefix });
626
803
  case "image":
627
- return /* @__PURE__ */ React.createElement(EmailImageAtom, { atom, ctx });
804
+ return React.cloneElement(renderImageAtom(atom, ctx), { key: ctx.keyPrefix });
628
805
  case "divider":
629
- return /* @__PURE__ */ React.createElement(EmailDividerAtom, { atom, ctx });
630
- case "option":
631
- return /* @__PURE__ */ React.createElement(EmailOptionAtom, { atom, ctx });
806
+ return React.cloneElement(renderDividerAtom(atom, ctx), { key: ctx.keyPrefix });
632
807
  case "rows":
633
- return /* @__PURE__ */ React.createElement(EmailRowsAtom, { atom, ctx });
808
+ return renderRowsAsColumnContent(atom, ctx);
634
809
  case "columns":
635
- return /* @__PURE__ */ React.createElement(EmailColumnsAtom, { atom, ctx });
636
- case "input":
637
- return /* @__PURE__ */ React.createElement(EmailInputAtom, { atom, ctx });
810
+ return /* @__PURE__ */ React.createElement(RawHtml, { key: ctx.keyPrefix, html: renderRawColumnsAtom(atom, ctx) });
811
+ case "automation-upcoming-events":
812
+ case "automation-triggered-event":
813
+ case "form":
638
814
  case "question":
639
- return /* @__PURE__ */ React.createElement(EmailQuestionAtom, { atom, ctx });
815
+ case "input":
816
+ case "option":
640
817
  case "contest":
641
- return /* @__PURE__ */ React.createElement(EmailContestAtom, { atom, ctx });
642
- case "form":
643
- return /* @__PURE__ */ React.createElement(EmailFormAtom, { atom, ctx });
818
+ return null;
819
+ }
820
+ }
821
+ function renderChildrenAsColumnContent({
822
+ children,
823
+ resolvedRows,
824
+ ctx
825
+ }) {
826
+ const gap = resolvedRows.gap;
827
+ const nodes = [];
828
+ children.forEach((child, index) => {
829
+ if (index > 0 && gap > 0) {
830
+ nodes.push(
831
+ /* @__PURE__ */ React.createElement(
832
+ RawHtml,
833
+ {
834
+ key: `${ctx.keyPrefix}-gap-${index}`,
835
+ html: `<div style="height:${gap}px;line-height:${gap}px">&nbsp;</div>`
836
+ }
837
+ )
838
+ );
839
+ }
840
+ const childLayout = resolveAtomLayoutStyles(child, ctx.globalAtomStyles);
841
+ const childOuterWidth = roundWidth(
842
+ calculateRowChildWidth({
843
+ contentWidth: ctx.contentWidthPx,
844
+ child,
845
+ globalAtomStyles: ctx.globalAtomStyles
846
+ })
847
+ );
848
+ const childContentWidth = roundWidth(computeAtomContentWidth(childOuterWidth, childLayout));
849
+ const childCtx = {
850
+ ...ctx,
851
+ contentWidthPx: childContentWidth,
852
+ parentRowBlockHorizontalAlign: resolvedRows.contentHorizontalAlign,
853
+ keyPrefix: `${ctx.keyPrefix}-${child.atomKey}-${index}`
854
+ };
855
+ nodes.push(renderColumnContentAtom(child, childCtx));
856
+ });
857
+ return nodes;
858
+ }
859
+ function renderRowsAsColumnContent(atom, ctx) {
860
+ const resolvedRows = resolveRowsStyles(atom.rowsStyles, ctx.globalAtomStyles.rowsStyles.rowsStyles);
861
+ return renderChildrenAsColumnContent({ children: atom.children, resolvedRows, ctx });
862
+ }
863
+ function renderBodyAtom(atom, ctx) {
864
+ switch (atom.type) {
865
+ case "rows":
866
+ return renderRowsAsBodySections(atom, ctx);
867
+ case "columns":
868
+ return renderColumnsAsBodySection(atom, ctx);
869
+ case "text":
870
+ case "button":
871
+ case "image":
872
+ case "divider":
873
+ return renderLeafAsBodySection(atom, ctx);
644
874
  case "automation-upcoming-events":
645
- return /* @__PURE__ */ React.createElement(EmailAutomationUpcomingEventsAtom, { atom, ctx });
646
875
  case "automation-triggered-event":
647
- return /* @__PURE__ */ React.createElement(EmailAutomationTriggeredEventAtom, { atom, ctx });
876
+ case "form":
877
+ case "question":
878
+ case "input":
879
+ case "option":
880
+ case "contest":
881
+ return null;
648
882
  }
649
- };
650
- const EmailRowsAtom = ({ atom, ctx }) => {
651
- const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
883
+ }
884
+ function renderRowsAsBodySections(atom, ctx) {
885
+ const resolvedLayoutBase = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
886
+ const resolvedLayout = ctx.suppressRowsBackgroundAtomKey === atom.atomKey ? suppressLayoutBackground(resolvedLayoutBase) : resolvedLayoutBase;
652
887
  const resolvedRows = resolveRowsStyles(atom.rowsStyles, ctx.globalAtomStyles.rowsStyles.rowsStyles);
653
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign, "left");
654
- const gap = resolvedRows.gap;
655
- const innerWidth = computeAtomContentWidth(ctx.contentWidthPx, resolvedLayout);
656
- const rowCrossAlign = resolvedRows.horizontalAlign === "left" ? "flex-start" : resolvedRows.horizontalAlign === "center" ? "center" : "flex-end";
657
- const widthStyle = {
658
- width: "100%",
659
- ...resolvedLayout.width.max !== "none" ? { maxWidth: resolvedLayout.width.max } : {}
660
- };
661
- return /* @__PURE__ */ React.createElement(components.Section, { style: { ...wrapperStyle, ...widthStyle } }, /* @__PURE__ */ React.createElement(
662
- "div",
663
- {
664
- style: {
665
- width: "100%",
666
- display: "flex",
667
- flexDirection: "column",
668
- alignItems: rowCrossAlign
669
- }
670
- },
671
- atom.children.map((child, i) => {
672
- const childResolved = resolveAtomLayoutStyles(child, ctx.globalAtomStyles);
673
- const childMaxWidth = childResolved.width.max;
674
- const slotOuterWidth = calculateRowChildWidth({
888
+ const outerWidth = boxOuterWidth(ctx.contentWidthPx, resolvedLayout);
889
+ const innerWidth = roundWidth(computeAtomContentWidth(outerWidth, resolvedLayout));
890
+ if (atom.children.length === 0) {
891
+ return [
892
+ /* @__PURE__ */ React.createElement(mjmlReact.MjmlSection, { key: `${ctx.keyPrefix}-empty`, ...mjmlLayoutProps(resolvedLayout) }, /* @__PURE__ */ React.createElement(mjmlReact.MjmlColumn, { width: `${innerWidth}px`, padding: "0" }, /* @__PURE__ */ React.createElement(mjmlReact.MjmlText, { padding: "0" }, "\xA0")))
893
+ ];
894
+ }
895
+ return atom.children.map((child, index) => {
896
+ const childResolved = resolveAtomLayoutStyles(child, ctx.globalAtomStyles);
897
+ const childOuterWidth = roundWidth(
898
+ calculateRowChildWidth({
675
899
  contentWidth: innerWidth,
676
900
  child,
677
901
  globalAtomStyles: ctx.globalAtomStyles
902
+ })
903
+ );
904
+ const childContentWidth = roundWidth(computeAtomContentWidth(childOuterWidth, childResolved));
905
+ const childHorizontalAlign = effectiveBlockHorizontalAlign({
906
+ layoutHorizontalAlign: childResolved.horizontalAlign,
907
+ parentRowBlockHorizontalAlign: resolvedRows.contentHorizontalAlign
908
+ });
909
+ const childCtx = {
910
+ ...ctx,
911
+ contentWidthPx: childContentWidth,
912
+ parentRowBlockHorizontalAlign: resolvedRows.contentHorizontalAlign,
913
+ keyPrefix: `${ctx.keyPrefix}-${child.atomKey}-${index}`
914
+ };
915
+ if (child.type === "columns") {
916
+ return renderColumnsAsBodySection(child, childCtx, {
917
+ key: `${ctx.keyPrefix}-${child.atomKey}-${index}`,
918
+ paddingTop: index > 0 ? resolvedRows.gap : 0,
919
+ padding: rowChildSectionPadding(resolvedLayout, index, atom.children.length, resolvedRows.gap),
920
+ width: childOuterWidth,
921
+ align: childHorizontalAlign});
922
+ }
923
+ if (child.type === "rows") {
924
+ return renderLeafAsBodySection(child, childCtx, {
925
+ key: `${ctx.keyPrefix}-${child.atomKey}-${index}`,
926
+ paddingTop: index > 0 ? resolvedRows.gap : 0,
927
+ padding: rowChildSectionPadding(resolvedLayout, index, atom.children.length, resolvedRows.gap),
928
+ width: childOuterWidth,
929
+ align: childHorizontalAlign,
930
+ parentLayout: resolvedLayout
678
931
  });
679
- const childContentWidthPx = computeAtomContentWidth(slotOuterWidth, childResolved);
680
- const childWidthStyle = {
681
- width: "100%",
682
- ...childMaxWidth !== "none" ? { maxWidth: childMaxWidth } : {}
683
- };
684
- const gapStyle = i > 0 && gap > 0 ? { paddingTop: gap } : {};
685
- const childCtx = {
686
- ...ctx,
687
- contentWidthPx: childContentWidthPx,
688
- parentRowBlockHorizontalAlign: resolvedRows.horizontalAlign
689
- };
690
- return /* @__PURE__ */ React.createElement("div", { key: child.atomKey, style: { ...gapStyle, ...childWidthStyle } }, /* @__PURE__ */ React.createElement(EmailAtom, { atom: child, ctx: childCtx }));
691
- })
692
- ));
693
- };
694
- const EmailColumnsAtom = ({ atom, ctx }) => {
695
- const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
932
+ }
933
+ return renderLeafAsBodySection(child, childCtx, {
934
+ key: `${ctx.keyPrefix}-${child.atomKey}-${index}`,
935
+ paddingTop: index > 0 ? resolvedRows.gap : 0,
936
+ padding: rowChildSectionPadding(resolvedLayout, index, atom.children.length, resolvedRows.gap),
937
+ width: childOuterWidth,
938
+ align: childHorizontalAlign,
939
+ parentLayout: resolvedLayout
940
+ });
941
+ });
942
+ }
943
+ function renderLeafAsBodySection(atom, ctx, opts) {
944
+ const width = opts?.width ?? ctx.contentWidthPx;
945
+ const sectionPaddingTop = opts?.paddingTop ?? 0;
946
+ const sectionLayout = opts?.parentLayout;
947
+ const sectionProps = compactRecord({
948
+ ...sectionLayout ? mjmlLayoutProps(sectionLayout) : {},
949
+ padding: opts?.padding ?? `${sectionPaddingTop}px 0 0 0`,
950
+ textAlign: opts?.align
951
+ });
952
+ return /* @__PURE__ */ React.createElement(mjmlReact.MjmlSection, { key: opts?.key ?? ctx.keyPrefix, ...sectionProps }, /* @__PURE__ */ React.createElement(mjmlReact.MjmlColumn, { width: `${width}px`, padding: "0" }, renderColumnContentAtom(atom, { ...ctx, contentWidthPx: width })));
953
+ }
954
+ function renderColumnsAsBodySection(atom, ctx, opts) {
696
955
  const resolvedCols = resolveColumnsStyles(atom.columnsStyles, ctx.globalAtomStyles.columnsStyles.columnsStyles);
697
- const wrapperStyle = layoutToWrapperStyle(resolvedLayout, ctx.parentRowBlockHorizontalAlign, "left");
698
- const gap = resolvedCols.gap;
699
- const gapPx = Math.max(0, Math.round(gap));
700
- const innerWidth = computeAtomContentWidth(ctx.contentWidthPx, resolvedLayout);
701
- const innerWidthPx = Math.max(0, Math.round(innerWidth));
702
- const resolvedChildLayouts = atom.children.map(
703
- (child) => resolveAtomLayoutStyles(child, ctx.globalAtomStyles)
704
- );
705
- const verticalAlignMap = {
706
- top: "top",
707
- middle: "middle",
708
- bottom: "bottom"
709
- };
710
- const vAlign = verticalAlignMap[resolvedCols.verticalAlign] ?? "top";
711
- const childCount = atom.children.length;
712
- const totalGapPx = childCount > 1 ? gapPx * (childCount - 1) : 0;
713
- const rawColumnWidths = childCount === 0 ? [] : calculateColumnChildWidths({
714
- contentWidth: innerWidthPx,
956
+ if (resolvedCols.format === "table") {
957
+ return /* @__PURE__ */ React.createElement(mjmlReact.MjmlSection, { key: opts?.key ?? ctx.keyPrefix, padding: opts?.padding ?? `${opts?.paddingTop ?? 0}px 0 0 0` }, /* @__PURE__ */ React.createElement(mjmlReact.MjmlColumn, { padding: "0" }, /* @__PURE__ */ React.createElement(RawHtml, { html: renderRawColumnsAtom(atom, ctx) })));
958
+ }
959
+ const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
960
+ const outerWidth = opts?.width ?? boxOuterWidth(ctx.contentWidthPx, resolvedLayout);
961
+ const innerWidth = roundWidth(computeAtomContentWidth(outerWidth, resolvedLayout));
962
+ const gapPx = Math.max(0, Math.round(resolvedCols.gap));
963
+ const columnWidths = calculateColumnChildWidthAllocation({
964
+ contentWidth: innerWidth,
715
965
  children: atom.children,
716
966
  gap: gapPx,
717
967
  globalAtomStyles: ctx.globalAtomStyles
968
+ }).widthsPx.map(roundWidth);
969
+ const groupWidth = roundWidth(
970
+ columnWidths.reduce((sum, width) => sum + width, 0) + gapPx * Math.max(0, atom.children.length - 1)
971
+ );
972
+ const vAlign = resolvedCols.contentVerticalAlign === "middle" ? "middle" : resolvedCols.contentVerticalAlign;
973
+ const sectionProps = compactRecord({
974
+ ...mjmlLayoutProps(resolvedLayout),
975
+ padding: opts?.padding ?? `${opts?.paddingTop ?? 0}px 0 0 0`,
976
+ textAlign: opts?.align ?? resolvedCols.contentHorizontalAlign
718
977
  });
719
- const columnWidthsPx = rawColumnWidths.map((w) => Math.round(w));
720
- const columnsWidthSum = columnWidthsPx.reduce((a, b) => a + b, 0);
721
- const totalTableWidth = columnsWidthSum + totalGapPx;
722
- const horizontalAlign = resolvedCols.horizontalAlign;
723
- const narrowColumnsMargins = horizontalAlign === "center" ? { marginLeft: "auto", marginRight: "auto" } : horizontalAlign === "right" ? { marginLeft: "auto", marginRight: "0" } : { marginLeft: "0", marginRight: "auto" };
724
- const outerTableStyle = {
725
- ...wrapperStyle,
726
- width: totalTableWidth,
727
- ...narrowColumnsMargins,
728
- ...resolvedLayout.width.max !== "none" ? { maxWidth: resolvedLayout.width.max } : {}
729
- };
730
- const innerRowStyle = {
731
- width: totalTableWidth,
732
- tableLayout: "fixed"
733
- };
734
- const spacerStyle = {
735
- width: gapPx,
736
- padding: 0,
737
- verticalAlign: vAlign,
738
- lineHeight: 0,
739
- fontSize: 0
740
- };
741
- const cells = [];
742
- atom.children.forEach((child, i) => {
743
- if (i > 0 && gapPx > 0) {
744
- cells.push(
745
- /* @__PURE__ */ React.createElement(components.Column, { key: `${atom.atomKey}-gap-${i}`, style: spacerStyle }, "\xA0")
746
- );
747
- }
748
- const wPx = columnWidthsPx[i] ?? 0;
749
- const childResolved = resolvedChildLayouts[i];
750
- const childContentWidthPx = computeAtomContentWidth(wPx, childResolved);
751
- const columnStyle = {
752
- width: wPx,
753
- verticalAlign: vAlign
754
- };
978
+ return /* @__PURE__ */ React.createElement(mjmlReact.MjmlSection, { key: opts?.key ?? ctx.keyPrefix, ...sectionProps }, /* @__PURE__ */ React.createElement(mjmlReact.MjmlGroup, { width: `${groupWidth}px` }, atom.children.map((child, index) => {
979
+ const childWidth = columnWidths[index] ?? 0;
980
+ const gapBefore = index === 0 ? 0 : gapPx;
981
+ const columnWidth = childWidth + gapBefore;
982
+ const childLayout = resolveAtomLayoutStyles(child, ctx.globalAtomStyles);
755
983
  const childCtx = {
756
984
  ...ctx,
757
- contentWidthPx: childContentWidthPx,
758
- parentRowBlockHorizontalAlign: null
985
+ contentWidthPx: roundWidth(computeAtomContentWidth(childWidth, childLayout)),
986
+ parentRowBlockHorizontalAlign: null,
987
+ keyPrefix: `${ctx.keyPrefix}-${child.atomKey}-${index}`
759
988
  };
760
- cells.push(
761
- /* @__PURE__ */ React.createElement(components.Column, { key: child.atomKey, style: columnStyle }, /* @__PURE__ */ React.createElement(EmailAtom, { atom: child, ctx: childCtx }))
989
+ return /* @__PURE__ */ React.createElement(
990
+ mjmlReact.MjmlColumn,
991
+ {
992
+ key: `${child.atomKey}-${index}`,
993
+ width: `${columnWidth}px`,
994
+ verticalAlign: vAlign,
995
+ padding: gapBefore === 0 ? "0" : `0 0 0 ${gapBefore}px`
996
+ },
997
+ renderColumnContentAtom(child, childCtx)
998
+ );
999
+ })));
1000
+ }
1001
+ function renderRawAtom(atom, ctx) {
1002
+ switch (atom.type) {
1003
+ case "text":
1004
+ return renderRawTextAtom(atom, ctx);
1005
+ case "button":
1006
+ return renderRawButtonAtom(atom, ctx);
1007
+ case "image":
1008
+ return renderRawImageAtom(atom, ctx);
1009
+ case "divider":
1010
+ return renderRawDividerAtom(atom, ctx);
1011
+ case "rows":
1012
+ return renderRawRowsAtom(atom, ctx);
1013
+ case "columns":
1014
+ return renderRawColumnsAtom(atom, ctx);
1015
+ case "automation-upcoming-events":
1016
+ case "automation-triggered-event":
1017
+ case "form":
1018
+ case "question":
1019
+ case "input":
1020
+ case "option":
1021
+ case "contest":
1022
+ return "";
1023
+ }
1024
+ }
1025
+ function renderRawTextAtom(atom, ctx) {
1026
+ const rootTextStyles = getRootTextStyles(atom.textType ?? "body", ctx.globalAtomStyles);
1027
+ const resolvedText = resolveTextStyles(atom.textStyles, rootTextStyles.textStyles);
1028
+ const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
1029
+ const width = roundWidth(ctx.contentWidthPx);
1030
+ const html = `<div style="${cssStyle({ ...textStyleToCss(resolvedText), width: `${width}px`, wordBreak: "break-word" })}">${textHtmlWithLineBreaks(
1031
+ atom.text,
1032
+ ctx.underlineLinks,
1033
+ ctx.linkColor
1034
+ )}</div>`;
1035
+ return renderRawWrappedBox(html, width, resolvedLayout);
1036
+ }
1037
+ function renderRawButtonAtom(atom, ctx) {
1038
+ const resolvedText = resolveTextStyles(atom.textStyles, ctx.globalAtomStyles.buttonStyles.textStyles);
1039
+ const resolvedInnerPadding = resolvePadding(atom.innerPadding, ctx.globalAtomStyles.buttonStyles.innerPadding);
1040
+ const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
1041
+ const width = roundWidth(ctx.contentWidthPx);
1042
+ const href = atom.href ? ensureUrlHasProtocolForPreview(atom.href) : "#";
1043
+ const backgroundColor = backgroundColorFromAtomBg(resolvedLayout.background);
1044
+ const borderRadius = cornerRadiusToCssValue(resolvedLayout.borderRadius);
1045
+ const isContentWidth = resolvedLayout.width.type === "content";
1046
+ const linkStyle = cssStyle({
1047
+ ...textStyleToCss(resolvedText),
1048
+ backgroundColor,
1049
+ borderRadius,
1050
+ display: isContentWidth ? "inline-block" : "block",
1051
+ padding: paddingToCss(resolvedInnerPadding),
1052
+ textDecoration: "none",
1053
+ width: isContentWidth ? void 0 : `${width}px`,
1054
+ maxWidth: isContentWidth ? `${width}px` : void 0,
1055
+ boxSizing: "border-box"
1056
+ });
1057
+ const tableWidthAttribute = isContentWidth ? "" : ` width="${width}"`;
1058
+ const tableStyle = isContentWidth ? cssStyle({ borderCollapse: "separate" }) : rawTableStyle(width, { borderCollapse: "separate" });
1059
+ const cellStyle = isContentWidth ? cssStyle({ backgroundColor, borderRadius }) : rawCellStyle(width, { backgroundColor, borderRadius });
1060
+ const html = `<table class="display-link-block" role="presentation" border="0" cellpadding="0" cellspacing="0"${tableWidthAttribute} style="${tableStyle}"><tr><td align="center" bgcolor="${escapeHtml(backgroundColor ?? "")}" style="${cellStyle}"><a href="${escapeHtml(href)}" target="_blank" style="${linkStyle}">${atom.text}</a></td></tr></table>`;
1061
+ return renderRawWrappedBox(html, width, resolvedLayout);
1062
+ }
1063
+ function renderRawImageAtom(atom, ctx) {
1064
+ const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
1065
+ const width = roundWidth(ctx.contentWidthPx);
1066
+ const height = scaledImageHeight(atom, width);
1067
+ const imageStyle = cssStyle({
1068
+ borderRadius: cornerRadiusToCssValue(resolvedLayout.borderRadius),
1069
+ display: "block",
1070
+ width: `${width}px`,
1071
+ height: height === void 0 ? void 0 : `${height}px`
1072
+ });
1073
+ const heightAttribute = height === void 0 ? "" : ` height="${height}"`;
1074
+ const image = `<img src="${escapeHtml(atom.src)}" alt="" width="${width}"${heightAttribute} style="${imageStyle}">`;
1075
+ const html = atom.href ? `<a href="${escapeHtml(ensureUrlHasProtocolForPreview(atom.href))}" target="_blank" style="${cssStyle({
1076
+ display: "inline-block",
1077
+ width: `${width}px`
1078
+ })}">${image}</a>` : image;
1079
+ return renderRawWrappedBox(html, width, resolvedLayout);
1080
+ }
1081
+ function renderRawDividerAtom(atom, ctx) {
1082
+ const resolvedDivider = resolveDividerStyles(atom.dividerStyles, ctx.globalAtomStyles.dividerStyles.dividerStyles);
1083
+ const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
1084
+ const width = roundWidth(ctx.contentWidthPx);
1085
+ const dividerStyle = cssStyle({
1086
+ borderTop: `${resolvedDivider.width}px ${resolvedDivider.style} ${resolvedDivider.color}`,
1087
+ width: `${width}px`,
1088
+ margin: 0
1089
+ });
1090
+ return renderRawWrappedBox(`<div style="${dividerStyle}"></div>`, width, resolvedLayout);
1091
+ }
1092
+ function renderRawChildrenRows({
1093
+ children,
1094
+ resolvedLayout,
1095
+ resolvedRows,
1096
+ ctx
1097
+ }) {
1098
+ const outerWidth = boxOuterWidth(ctx.contentWidthPx, resolvedLayout);
1099
+ const innerWidth = roundWidth(computeAtomContentWidth(outerWidth, resolvedLayout));
1100
+ const rows = children.map((child, index) => {
1101
+ const childLayout = resolveAtomLayoutStyles(child, ctx.globalAtomStyles);
1102
+ const childOuterWidth = roundWidth(
1103
+ calculateRowChildWidth({
1104
+ contentWidth: innerWidth,
1105
+ child,
1106
+ globalAtomStyles: ctx.globalAtomStyles
1107
+ })
762
1108
  );
1109
+ const childContentWidth = roundWidth(computeAtomContentWidth(childOuterWidth, childLayout));
1110
+ const childAlign = effectiveBlockHorizontalAlign({
1111
+ layoutHorizontalAlign: childLayout.horizontalAlign,
1112
+ parentRowBlockHorizontalAlign: resolvedRows.contentHorizontalAlign
1113
+ });
1114
+ const childHtml = renderRawAtom(child, {
1115
+ ...ctx,
1116
+ contentWidthPx: childContentWidth,
1117
+ parentRowBlockHorizontalAlign: resolvedRows.contentHorizontalAlign,
1118
+ keyPrefix: `${ctx.keyPrefix}-${child.atomKey}-${index}`
1119
+ });
1120
+ return `<tr><td width="${innerWidth}" align="${childAlign}" style="${rawCellStyle(innerWidth, {
1121
+ paddingTop: index > 0 && resolvedRows.gap > 0 ? `${resolvedRows.gap}px` : void 0
1122
+ })}">${childHtml}</td></tr>`;
1123
+ }).join("");
1124
+ const table = `<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="${innerWidth}" style="${rawTableStyle(
1125
+ innerWidth
1126
+ )}">${rows}</table>`;
1127
+ return renderRawWrappedBox(table, outerWidth, resolvedLayout);
1128
+ }
1129
+ function renderRawRowsAtom(atom, ctx) {
1130
+ return renderRawChildrenRows({
1131
+ children: atom.children,
1132
+ resolvedLayout: resolveAtomLayoutStyles(atom, ctx.globalAtomStyles),
1133
+ resolvedRows: resolveRowsStyles(atom.rowsStyles, ctx.globalAtomStyles.rowsStyles.rowsStyles),
1134
+ ctx
763
1135
  });
764
- return /* @__PURE__ */ React.createElement(components.Section, { style: outerTableStyle }, /* @__PURE__ */ React.createElement(components.Row, { style: innerRowStyle }, cells));
765
- };
766
- const hiddenPreheaderStyle = {
767
- display: "none",
768
- fontSize: "1px",
769
- lineHeight: "1px",
770
- maxHeight: 0,
771
- maxWidth: 0,
772
- opacity: 0,
773
- overflow: "hidden"
774
- };
775
- const AtomDocumentContentsEmail = ({
776
- document: doc,
777
- options
778
- }) => {
779
- const webFonts = collectFontFamilies(doc);
1136
+ }
1137
+ function renderRawColumnsAtom(atom, ctx) {
1138
+ const resolvedLayout = resolveAtomLayoutStyles(atom, ctx.globalAtomStyles);
1139
+ const resolvedCols = resolveColumnsStyles(atom.columnsStyles, ctx.globalAtomStyles.columnsStyles.columnsStyles);
1140
+ const gapPx = Math.max(0, Math.round(resolvedCols.gap));
1141
+ const verticalGapPx = Math.max(0, Math.round(resolvedCols.verticalGap));
1142
+ const outerWidth = boxOuterWidth(ctx.contentWidthPx, resolvedLayout);
1143
+ const innerWidth = roundWidth(computeAtomContentWidth(outerWidth, resolvedLayout));
1144
+ const columnWidths = calculateColumnChildWidthAllocation({
1145
+ contentWidth: innerWidth,
1146
+ children: atom.children,
1147
+ gap: gapPx,
1148
+ globalAtomStyles: ctx.globalAtomStyles
1149
+ }).widthsPx.map(roundWidth);
1150
+ const maxRows = Math.max(1, ...atom.children.map((child) => child.type === "rows" ? child.children.length : 1));
1151
+ const vAlign = resolvedCols.contentVerticalAlign === "middle" ? "middle" : resolvedCols.contentVerticalAlign;
1152
+ const rows = Array.from({ length: maxRows }).map((_, rowIndex) => {
1153
+ const cells = atom.children.map((child, columnIndex) => {
1154
+ const columnWidth = columnWidths[columnIndex] ?? 0;
1155
+ const rowChild = child.type === "rows" ? child.children[rowIndex] : rowIndex === 0 ? child : null;
1156
+ const rowStyles = child.type === "rows" ? resolveRowsStyles(child.rowsStyles, ctx.globalAtomStyles.rowsStyles.rowsStyles) : ctx.globalAtomStyles.rowsStyles.rowsStyles;
1157
+ const cellHtml = rowChild ? (() => {
1158
+ const rowChildLayout = resolveAtomLayoutStyles(rowChild, ctx.globalAtomStyles);
1159
+ const rowChildOuterWidth = roundWidth(
1160
+ calculateRowChildWidth({
1161
+ contentWidth: columnWidth,
1162
+ child: rowChild,
1163
+ globalAtomStyles: ctx.globalAtomStyles
1164
+ })
1165
+ );
1166
+ const rowChildContentWidth = roundWidth(
1167
+ computeAtomContentWidth(rowChildOuterWidth, rowChildLayout)
1168
+ );
1169
+ return renderRawAtom(rowChild, {
1170
+ ...ctx,
1171
+ contentWidthPx: rowChildContentWidth,
1172
+ parentRowBlockHorizontalAlign: rowStyles.contentHorizontalAlign,
1173
+ keyPrefix: `${ctx.keyPrefix}-${rowChild.atomKey}-${rowIndex}-${columnIndex}`
1174
+ });
1175
+ })() : "&nbsp;";
1176
+ const gapCell = columnIndex > 0 && gapPx > 0 ? `<td width="${gapPx}" valign="${vAlign}" style="${rawCellStyle(gapPx, {
1177
+ fontSize: 0,
1178
+ lineHeight: 0,
1179
+ verticalAlign: vAlign
1180
+ })}">&nbsp;</td>` : "";
1181
+ const cellVerticalAlign = rowChild?.type === "image" ? "middle" : vAlign;
1182
+ return `${gapCell}<td width="${columnWidth}" valign="${cellVerticalAlign}" style="${rawCellStyle(columnWidth, {
1183
+ paddingTop: rowIndex > 0 && verticalGapPx > 0 ? `${verticalGapPx}px` : void 0,
1184
+ verticalAlign: cellVerticalAlign
1185
+ })}">${cellHtml}</td>`;
1186
+ }).join("");
1187
+ return `<tr>${cells}</tr>`;
1188
+ }).join("");
1189
+ const totalWidth = columnWidths.reduce((sum, width) => sum + width, 0) + gapPx * Math.max(0, atom.children.length - 1);
1190
+ const table = `<div class="table-min-width-100"><table role="presentation" border="0" cellpadding="0" cellspacing="0" width="${totalWidth}" align="${alignAttr(
1191
+ resolvedCols.contentHorizontalAlign
1192
+ )}" style="${rawTableStyle(totalWidth)}">${rows}</table></div>`;
1193
+ return renderRawWrappedBox(table, outerWidth, resolvedLayout);
1194
+ }
1195
+ function topUnsubscribeHtml(unsubscribeLink) {
1196
+ return `<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#fff;background-color:#fff;width:100%"><tr><td><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%"><tr><td class="top-unsubscribe" style="font-size:0;padding-top:16px;padding-bottom:16px;text-align:left"><div style="font-family:'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif;font-size:14px;font-weight:500;line-height:1;text-align:left;color:#3771e0"><a style="color:#3771e0;text-decoration:none" href="${escapeHtml(
1197
+ unsubscribeLink
1198
+ )}" target="_blank" rel="noopener noreferrer">Unsubscribe</a></div></td></tr></table></td></tr></table>`;
1199
+ }
1200
+ function preheaderHtml(text) {
1201
+ return `<div class="preview-text" style="display:none!important;overflow:hidden;max-width:0;max-height:0;font-size:1px;line-height:1px;opacity:0">${escapeHtml(
1202
+ text
1203
+ )}${ZWNJ_PADDING.repeat(10)}</div>`;
1204
+ }
1205
+ const AtomDocumentContentsEmail = ({ document: doc, options }) => {
1206
+ const rootLayout = resolveAtomLayoutStyles(doc.rootRow, doc.globalAtomStyles);
1207
+ const canvasBackground = doc.canvasBackground;
1208
+ const bodyBackground = isImageBackground(canvasBackground) || canvasBackground === "none" && isImageBackground(rootLayout.background) ? canvasBackground === "none" ? rootLayout.background : canvasBackground : rootLayout.background;
1209
+ const shouldSuppressRootRowsBackground = canvasBackground === "none" && isImageBackground(rootLayout.background);
1210
+ const bodyBackgroundColor = backgroundColorFromAtomBg(bodyBackground);
1211
+ const bodyBackgroundCss = isImageBackground(bodyBackground) ? `
1212
+ .atoms-body > div {
1213
+ background-image: url("${bodyBackground.imageUrl}");
1214
+ background-size: ${bodyBackground.imageSize};
1215
+ background-repeat: ${bodyBackground.imageRepeat ? "repeat" : "no-repeat"};
1216
+ background-position: center;
1217
+ }
1218
+ ` : "";
780
1219
  const ctx = {
781
1220
  globalAtomStyles: doc.globalAtomStyles,
1221
+ linkColor: doc.linkColor,
1222
+ underlineLinks: doc.underlineLinks,
782
1223
  contentWidthPx: doc.maxWidth,
783
- parentRowBlockHorizontalAlign: null
1224
+ parentRowBlockHorizontalAlign: null,
1225
+ keyPrefix: "root",
1226
+ suppressRowsBackgroundAtomKey: shouldSuppressRootRowsBackground ? doc.rootRow.atomKey : void 0
784
1227
  };
785
- const resolvedRootLayoutStyles = resolveAtomLayoutStyles(doc.rootRow, doc.globalAtomStyles);
786
- const bodyBackgroundColor = resolvedRootLayoutStyles.background === "none" ? void 0 : backgroundColorFromAtomBg(resolvedRootLayoutStyles.background);
787
- return /* @__PURE__ */ React.createElement(components.Html, { lang: "en", dir: "ltr" }, /* @__PURE__ */ React.createElement(components.Head, null, webFonts.map((fontFamily) => /* @__PURE__ */ React.createElement(
788
- "link",
789
- {
790
- key: fontFamily,
791
- rel: "stylesheet",
792
- href: `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily)}:wght@100;200;300;400;500;600;700;800;900&display=swap`
793
- }
794
- ))), /* @__PURE__ */ React.createElement(components.Body, { style: { backgroundColor: bodyBackgroundColor, margin: 0, padding: 0 } }, options?.emailPreviewText ? /* @__PURE__ */ React.createElement("div", { style: hiddenPreheaderStyle }, options.emailPreviewText) : null, options?.automationPreviewHeaderHtml ? /* @__PURE__ */ React.createElement("div", { dangerouslySetInnerHTML: { __html: options.automationPreviewHeaderHtml } }) : null, /* @__PURE__ */ React.createElement(components.Container, { style: { maxWidth: doc.maxWidth, margin: "0 auto" } }, /* @__PURE__ */ React.createElement(EmailRowsAtom, { atom: doc.rootRow, ctx }), options?.cymbalBrandingUrl ? /* @__PURE__ */ React.createElement(components.Section, null, /* @__PURE__ */ React.createElement(components.Text, { style: { color: "#999999", fontSize: 12, textAlign: "center" } }, "Powered by", " ", /* @__PURE__ */ React.createElement(components.Link, { href: options.cymbalBrandingUrl, style: { color: "#999999" } }, "Cymbal"))) : null), options?.trackingPixelUrl ? /* @__PURE__ */ React.createElement(
795
- components.Img,
1228
+ return /* @__PURE__ */ React.createElement(mjmlReact.Mjml, null, /* @__PURE__ */ React.createElement(mjmlReact.MjmlHead, null, /* @__PURE__ */ React.createElement(mjmlReact.MjmlStyle, null, `
1229
+ ${COMMON_CSS}
1230
+ @media screen and (max-width:600px) {
1231
+ .top-unsubscribe {
1232
+ padding-left: 22px;
1233
+ padding-right: 22px;
1234
+ }
1235
+ }
1236
+ .atoms-text div {
1237
+ white-space: normal;
1238
+ word-break: break-word;
1239
+ }
1240
+ ${bodyBackgroundCss}
1241
+ `)), /* @__PURE__ */ React.createElement(mjmlReact.MjmlBody, { width: doc.maxWidth, backgroundColor: bodyBackgroundColor, cssClass: "atoms-body" }, options?.emailPreviewText ? /* @__PURE__ */ React.createElement(RawHtml, { html: preheaderHtml(options.emailPreviewText) }) : null, options?.topUnsubscribeLink ? /* @__PURE__ */ React.createElement(RawHtml, { html: topUnsubscribeHtml(options.topUnsubscribeLink) }) : null, options?.automationPreviewHeaderHtml ? /* @__PURE__ */ React.createElement(RawHtml, { html: options.automationPreviewHeaderHtml }) : null, renderBodyAtom(doc.rootRow, ctx), options?.cymbalBrandingUrl ? /* @__PURE__ */ React.createElement(mjmlReact.MjmlSection, { padding: "0" }, /* @__PURE__ */ React.createElement(mjmlReact.MjmlColumn, null, /* @__PURE__ */ React.createElement(mjmlReact.MjmlText, { color: "#999999", fontSize: "12px", align: "center", padding: "0" }, "Powered by", " ", /* @__PURE__ */ React.createElement("a", { href: options.cymbalBrandingUrl, target: "_blank", style: { color: "#999999" } }, "Cymbal")))) : null, options?.trackingPixelUrl ? /* @__PURE__ */ React.createElement(
1242
+ RawHtml,
796
1243
  {
797
- src: options.trackingPixelUrl,
798
- width: "1",
799
- height: "1",
800
- style: { display: "none", height: 1, width: 1 },
801
- alt: ""
1244
+ html: `<img src="${escapeHtml(
1245
+ options.trackingPixelUrl
1246
+ )}" width="1" height="1" style="display:none;height:1px;width:1px" alt="">`
802
1247
  }
803
1248
  ) : null));
804
1249
  };
805
1250
  async function renderAtomDocumentToHtml(doc, options) {
806
- return components.render(/* @__PURE__ */ React.createElement(AtomDocumentContentsEmail, { document: doc, options }));
1251
+ const mjml = renderToMjml.renderToMjml(/* @__PURE__ */ React.createElement(AtomDocumentContentsEmail, { document: doc, options }));
1252
+ const result = await mjml2html(mjml, { keepComments: false, beautify: false, minify: true });
1253
+ if (typeof result === "string") {
1254
+ return result;
1255
+ }
1256
+ const errors = result.errors ?? [];
1257
+ if (errors.length > 0) {
1258
+ throw new Error(`MJML render failed: ${errors.map((error) => error.formattedMessage).join("; ")}`);
1259
+ }
1260
+ return result.html;
807
1261
  }
808
1262
 
809
1263
  exports.renderAtomDocumentToHtml = renderAtomDocumentToHtml;