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