@templatical/renderer 0.54.1 → 0.55.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/assets/social/circle/xing.png +0 -0
- package/assets/social/circle-dark/xing.png +0 -0
- package/assets/social/circle-light/xing.png +0 -0
- package/assets/social/outlined/xing.png +0 -0
- package/assets/social/outlined-dark/xing.png +0 -0
- package/assets/social/outlined-light/xing.png +0 -0
- package/assets/social/plain/xing.png +0 -0
- package/assets/social/plain-dark/xing.png +0 -0
- package/assets/social/plain-light/xing.png +0 -0
- package/assets/social/rounded/xing.png +0 -0
- package/assets/social/rounded-dark/xing.png +0 -0
- package/assets/social/rounded-light/xing.png +0 -0
- package/assets/social/solid/xing.png +0 -0
- package/assets/social/solid-dark/xing.png +0 -0
- package/assets/social/solid-light/xing.png +0 -0
- package/assets/social/square/xing.png +0 -0
- package/assets/social/square-dark/xing.png +0 -0
- package/assets/social/square-light/xing.png +0 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +137 -99
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.d.ts
CHANGED
|
@@ -186,19 +186,25 @@ export declare function resolveParagraphGap(spacing: number | undefined): number
|
|
|
186
186
|
*/
|
|
187
187
|
export declare const BUTTON_FULL_WIDTH_CSS_CLASS = "tpl-button-full";
|
|
188
188
|
/**
|
|
189
|
-
* `fluid-on-mobile` for an `mj-image
|
|
190
|
-
*
|
|
189
|
+
* `fluid-on-mobile` for an `mj-image`. An image fills its column on desktop
|
|
190
|
+
* when its width is `"full"`, or a pixel width MJML caps at the room it gives
|
|
191
191
|
* the image (the column less the image's own side padding and border). On a
|
|
192
192
|
* phone the attribute widens the image to the stacked column, so an image set
|
|
193
193
|
* narrower must not carry it: a 120px logo would grow to the whole column, and
|
|
194
194
|
* one with a set height would stretch out of shape. An image wider than the
|
|
195
195
|
* phone shrinks to fit without it.
|
|
196
196
|
*
|
|
197
|
+
* `fluidOnMobile: true` always emits the attribute and `false` never does.
|
|
198
|
+
* Omitted keeps the rule above. Video thumbnails have no such field and stay
|
|
199
|
+
* on the rule.
|
|
200
|
+
*
|
|
197
201
|
* Exported for `blockRenderers` entries that emit their own `mj-image`. Pass
|
|
198
202
|
* the context the renderer received, whose `contentWidth` already excludes the
|
|
199
203
|
* wrappers and sections around the column.
|
|
200
204
|
*/
|
|
201
|
-
export declare function fluidOnMobileAttr(block: Pick<ImageBlock, "width" | "styles" | "border"
|
|
205
|
+
export declare function fluidOnMobileAttr(block: Pick<ImageBlock, "width" | "styles" | "border"> & {
|
|
206
|
+
fluidOnMobile?: boolean;
|
|
207
|
+
}, context: RenderContext): string;
|
|
202
208
|
/**
|
|
203
209
|
* Get width percentages for each column in a layout.
|
|
204
210
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HEADING_LEVEL_FONT_SIZE, RICH_TEXT_SPACING, TITLE_FONT_WEIGHT, TITLE_LINE_HEIGHT, applyLayout, assertNoSlotInContent, assertNoWrapperInContent, convertMergeTagsToValues, createCustomBlockLiquid, defaultLocaleOf, fillEmptyParagraphs, findOpenTagEnd, isBlankRichText, isButton, isCustomBlock, isDivider, isHtml, isImage, isMenu, isParagraph, isSection, isSocialIcons, isSpacer, isTable, isTitle, isVideo, isWrapper, resolveBackgroundImage, resolveContentDirection, resolveFontStack, resolveHero, resolveLocale, socialIconAssetDir, toBorderCss, toBorderDeclarations, toBorderRadiusCss } from "@templatical/types";
|
|
2
2
|
// src/render-context.ts
|
|
3
|
-
const DEFAULT_SOCIAL_ICONS_BASE_URL = `https://cdn.jsdelivr.net/npm/@templatical/renderer@0.
|
|
3
|
+
const DEFAULT_SOCIAL_ICONS_BASE_URL = `https://cdn.jsdelivr.net/npm/@templatical/renderer@0.55.0/assets/social`;
|
|
4
4
|
/**
|
|
5
5
|
* Immutable context passed through the block rendering chain.
|
|
6
6
|
*/
|
|
@@ -59,6 +59,61 @@ var RenderContext = class RenderContext {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
|
+
// src/escape.ts
|
|
63
|
+
const HTML_ENTITIES = {
|
|
64
|
+
"&": "&",
|
|
65
|
+
"<": "<",
|
|
66
|
+
">": ">",
|
|
67
|
+
"\"": """,
|
|
68
|
+
"'": "'"
|
|
69
|
+
};
|
|
70
|
+
const HTML_ENTITY_REGEX = /[&<>"']/g;
|
|
71
|
+
/**
|
|
72
|
+
* Escape HTML special characters (& < > " ').
|
|
73
|
+
* Equivalent to PHP htmlspecialchars with ENT_QUOTES | ENT_HTML5.
|
|
74
|
+
*/
|
|
75
|
+
function escapeHtml(text) {
|
|
76
|
+
if (text === "") return "";
|
|
77
|
+
return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Escape a string for use in an HTML attribute value.
|
|
81
|
+
* Same implementation as escapeHtml for consistency with PHP.
|
|
82
|
+
*/
|
|
83
|
+
function escapeAttr(text) {
|
|
84
|
+
if (text === "") return "";
|
|
85
|
+
return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Escape a string for use as a CSS property value inside an inline
|
|
89
|
+
* `style="prop: ${value}"` attribute. Beyond HTML entity escaping (so the
|
|
90
|
+
* value survives the attribute boundary), this strips characters that
|
|
91
|
+
* could break out of the property value into a sibling property:
|
|
92
|
+
*
|
|
93
|
+
* `;` — separates CSS declarations
|
|
94
|
+
* `{`/`}` — opens/closes a CSS rule (rejected by attribute parsers but
|
|
95
|
+
* still safer to remove)
|
|
96
|
+
* `\n`/`\r` — would smuggle past line-based CSS sanitizers
|
|
97
|
+
*
|
|
98
|
+
* Without this, an attacker-controlled color like
|
|
99
|
+
* `"red; background: url('//attacker/log')"` lands as a real CSS rule.
|
|
100
|
+
*/
|
|
101
|
+
function escapeCssValue(text) {
|
|
102
|
+
if (text === "") return "";
|
|
103
|
+
return escapeAttr(text.replace(/[;{}\r\n]/g, ""));
|
|
104
|
+
}
|
|
105
|
+
const FONT_FAMILY_ENTITY_REGEX = /[&<>"]/g;
|
|
106
|
+
/**
|
|
107
|
+
* Escape a font stack for a double-quoted `font-family="…"` MJML attribute.
|
|
108
|
+
* A `"`-quoted family (`"Helvetica Neue", Arial`) must not end the attribute,
|
|
109
|
+
* or the compiler reads it as empty. Unlike `escapeAttr`, single quotes are
|
|
110
|
+
* left alone: they can't end a double-quoted attribute, and they're how the
|
|
111
|
+
* renderer quotes families itself (`'Times New Roman', serif`), so every
|
|
112
|
+
* stack without `&<>"` is emitted byte-for-byte.
|
|
113
|
+
*/
|
|
114
|
+
function escapeFontFamily(text) {
|
|
115
|
+
return text.replace(FONT_FAMILY_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);
|
|
116
|
+
}
|
|
62
117
|
// src/visibility.ts
|
|
63
118
|
/**
|
|
64
119
|
* Check if a block is hidden on all viewports.
|
|
@@ -79,7 +134,7 @@ function isHiddenOnAll(block) {
|
|
|
79
134
|
function getCssClassAttr(block, extraClasses = []) {
|
|
80
135
|
const classes = [getCssClasses(block), ...extraClasses].filter((entry) => entry !== "").join(" ");
|
|
81
136
|
if (classes === "") return "";
|
|
82
|
-
return ` css-class="${classes}"`;
|
|
137
|
+
return ` css-class="${escapeAttr(classes)}"`;
|
|
83
138
|
}
|
|
84
139
|
/**
|
|
85
140
|
* Get the CSS classes for visibility hiding.
|
|
@@ -134,67 +189,12 @@ function renderUnrenderableBlock(block) {
|
|
|
134
189
|
const id = commentSafe(block.id);
|
|
135
190
|
return `<mj-raw><!-- ${UNRENDERABLE_MARKER_PREFIX} type="${type}" id="${id}" --></mj-raw>`;
|
|
136
191
|
}
|
|
137
|
-
// src/escape.ts
|
|
138
|
-
const HTML_ENTITIES = {
|
|
139
|
-
"&": "&",
|
|
140
|
-
"<": "<",
|
|
141
|
-
">": ">",
|
|
142
|
-
"\"": """,
|
|
143
|
-
"'": "'"
|
|
144
|
-
};
|
|
145
|
-
const HTML_ENTITY_REGEX = /[&<>"']/g;
|
|
146
|
-
/**
|
|
147
|
-
* Escape HTML special characters (& < > " ').
|
|
148
|
-
* Equivalent to PHP htmlspecialchars with ENT_QUOTES | ENT_HTML5.
|
|
149
|
-
*/
|
|
150
|
-
function escapeHtml(text) {
|
|
151
|
-
if (text === "") return "";
|
|
152
|
-
return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Escape a string for use in an HTML attribute value.
|
|
156
|
-
* Same implementation as escapeHtml for consistency with PHP.
|
|
157
|
-
*/
|
|
158
|
-
function escapeAttr(text) {
|
|
159
|
-
if (text === "") return "";
|
|
160
|
-
return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Escape a string for use as a CSS property value inside an inline
|
|
164
|
-
* `style="prop: ${value}"` attribute. Beyond HTML entity escaping (so the
|
|
165
|
-
* value survives the attribute boundary), this strips characters that
|
|
166
|
-
* could break out of the property value into a sibling property:
|
|
167
|
-
*
|
|
168
|
-
* `;` — separates CSS declarations
|
|
169
|
-
* `{`/`}` — opens/closes a CSS rule (rejected by attribute parsers but
|
|
170
|
-
* still safer to remove)
|
|
171
|
-
* `\n`/`\r` — would smuggle past line-based CSS sanitizers
|
|
172
|
-
*
|
|
173
|
-
* Without this, an attacker-controlled color like
|
|
174
|
-
* `"red; background: url('//attacker/log')"` lands as a real CSS rule.
|
|
175
|
-
*/
|
|
176
|
-
function escapeCssValue(text) {
|
|
177
|
-
if (text === "") return "";
|
|
178
|
-
return escapeAttr(text).replace(/[;{}\r\n]/g, "");
|
|
179
|
-
}
|
|
180
|
-
const FONT_FAMILY_ENTITY_REGEX = /[&<>"]/g;
|
|
181
|
-
/**
|
|
182
|
-
* Escape a font stack for a double-quoted `font-family="…"` MJML attribute.
|
|
183
|
-
* A `"`-quoted family (`"Helvetica Neue", Arial`) must not end the attribute,
|
|
184
|
-
* or the compiler reads it as empty. Unlike `escapeAttr`, single quotes are
|
|
185
|
-
* left alone: they can't end a double-quoted attribute, and they're how the
|
|
186
|
-
* renderer quotes families itself (`'Times New Roman', serif`), so every
|
|
187
|
-
* stack without `&<>"` is emitted byte-for-byte.
|
|
188
|
-
*/
|
|
189
|
-
function escapeFontFamily(text) {
|
|
190
|
-
return text.replace(FONT_FAMILY_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);
|
|
191
|
-
}
|
|
192
192
|
// src/padding.ts
|
|
193
193
|
/**
|
|
194
194
|
* Convert a SpacingValue to a CSS padding string like "10px 10px 10px 10px".
|
|
195
195
|
*/
|
|
196
196
|
function toPaddingString(padding) {
|
|
197
|
-
return `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px
|
|
197
|
+
return escapeAttr(`${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`);
|
|
198
198
|
}
|
|
199
199
|
// src/utils.ts
|
|
200
200
|
/**
|
|
@@ -204,7 +204,7 @@ function toPaddingString(padding) {
|
|
|
204
204
|
*/
|
|
205
205
|
function bgAttr(backgroundColor, placement) {
|
|
206
206
|
if (!backgroundColor) return "";
|
|
207
|
-
return ` ${placement === "native" ? "background-color" : "container-background-color"}="${backgroundColor}"`;
|
|
207
|
+
return ` ${placement === "native" ? "background-color" : "container-background-color"}="${escapeCssValue(backgroundColor)}"`;
|
|
208
208
|
}
|
|
209
209
|
/**
|
|
210
210
|
* Render `mj-image`'s height attribute. Returns an empty string when no height
|
|
@@ -232,19 +232,25 @@ function horizontalInset(padding, border) {
|
|
|
232
232
|
return whole(padding?.left) + whole(padding?.right) + drawn(border?.left) + drawn(border?.right);
|
|
233
233
|
}
|
|
234
234
|
/**
|
|
235
|
-
* `fluid-on-mobile` for an `mj-image
|
|
236
|
-
*
|
|
235
|
+
* `fluid-on-mobile` for an `mj-image`. An image fills its column on desktop
|
|
236
|
+
* when its width is `"full"`, or a pixel width MJML caps at the room it gives
|
|
237
237
|
* the image (the column less the image's own side padding and border). On a
|
|
238
238
|
* phone the attribute widens the image to the stacked column, so an image set
|
|
239
239
|
* narrower must not carry it: a 120px logo would grow to the whole column, and
|
|
240
240
|
* one with a set height would stretch out of shape. An image wider than the
|
|
241
241
|
* phone shrinks to fit without it.
|
|
242
242
|
*
|
|
243
|
+
* `fluidOnMobile: true` always emits the attribute and `false` never does.
|
|
244
|
+
* Omitted keeps the rule above. Video thumbnails have no such field and stay
|
|
245
|
+
* on the rule.
|
|
246
|
+
*
|
|
243
247
|
* Exported for `blockRenderers` entries that emit their own `mj-image`. Pass
|
|
244
248
|
* the context the renderer received, whose `contentWidth` already excludes the
|
|
245
249
|
* wrappers and sections around the column.
|
|
246
250
|
*/
|
|
247
251
|
function fluidOnMobileAttr(block, context) {
|
|
252
|
+
if (block.fluidOnMobile === true) return " fluid-on-mobile=\"true\"";
|
|
253
|
+
if (block.fluidOnMobile === false) return "";
|
|
248
254
|
const room = Math.floor(context.contentWidth) - horizontalInset(block.styles.padding, block.border);
|
|
249
255
|
return block.width === "full" || block.width >= room ? " fluid-on-mobile=\"true\"" : "";
|
|
250
256
|
}
|
|
@@ -272,7 +278,7 @@ function borderAttr(border) {
|
|
|
272
278
|
function borderRadiusAttr(radius) {
|
|
273
279
|
const css = toBorderRadiusCss(radius);
|
|
274
280
|
if (css === null) return "";
|
|
275
|
-
return ` border-radius="${css}"`;
|
|
281
|
+
return ` border-radius="${escapeCssValue(css)}"`;
|
|
276
282
|
}
|
|
277
283
|
/**
|
|
278
284
|
* Render the background-image attributes `mj-section` and `mj-wrapper` accept.
|
|
@@ -286,7 +292,7 @@ function borderRadiusAttr(radius) {
|
|
|
286
292
|
function backgroundImageAttrs(bg) {
|
|
287
293
|
const resolved = resolveBackgroundImage(bg);
|
|
288
294
|
if (resolved === null) return "";
|
|
289
|
-
return ` background-url="${escapeAttr(resolved.url)}" background-size="${resolved.size}" background-position="${resolved.position}" background-repeat="${resolved.repeat}"`;
|
|
295
|
+
return ` background-url="${escapeAttr(resolved.url)}" background-size="${escapeCssValue(resolved.size)}" background-position="${escapeCssValue(resolved.position)}" background-repeat="${escapeCssValue(resolved.repeat)}"`;
|
|
290
296
|
}
|
|
291
297
|
// src/rich-text.ts
|
|
292
298
|
/**
|
|
@@ -498,7 +504,7 @@ function renderTitle(block, context) {
|
|
|
498
504
|
const content = unwrapParagraph(decodeStyleReferences(fillEmptyParagraphs(convertMergeTagsToValues(block.content))));
|
|
499
505
|
const fontSize = HEADING_LEVEL_FONT_SIZE[block.level] ?? HEADING_LEVEL_FONT_SIZE[2];
|
|
500
506
|
const colorAttr = block.color ? `\n color="${escapeAttr(block.color)}"` : "";
|
|
501
|
-
const align = block.textAlign;
|
|
507
|
+
const align = escapeAttr(block.textAlign);
|
|
502
508
|
const fontFamilyAttr = renderFontFamilyAttr$3(block.fontFamily, context);
|
|
503
509
|
const cssClassAttr = getCssClassAttr(block, [RICH_TEXT_CSS_CLASS, richTextGapClass(resolveParagraphGap(void 0))]);
|
|
504
510
|
const tag = `h${HEADING_LEVEL_FONT_SIZE[block.level] ? block.level : 2}`;
|
|
@@ -515,10 +521,15 @@ function renderTitle(block, context) {
|
|
|
515
521
|
* heading, so strip a single outer `<p>` wrapper if present.
|
|
516
522
|
*/
|
|
517
523
|
function unwrapParagraph(html) {
|
|
518
|
-
const
|
|
519
|
-
if (!
|
|
520
|
-
|
|
521
|
-
return
|
|
524
|
+
const openingTag = /^\s*<p\b/.exec(html);
|
|
525
|
+
if (!openingTag) return html;
|
|
526
|
+
const openingTagEnd = findOpenTagEnd(html, openingTag[0].length);
|
|
527
|
+
if (openingTagEnd === -1) return html;
|
|
528
|
+
const closingTag = /<\/p>\s*$/.exec(html);
|
|
529
|
+
if (!closingTag || closingTag.index <= openingTagEnd) return html;
|
|
530
|
+
const content = html.slice(openingTagEnd + 1, closingTag.index);
|
|
531
|
+
if (/<\/p>\s*<p\b/i.test(content)) return html;
|
|
532
|
+
return content;
|
|
522
533
|
}
|
|
523
534
|
function renderFontFamilyAttr$3(fontFamily, context) {
|
|
524
535
|
if (!fontFamily) return "";
|
|
@@ -550,7 +561,7 @@ function renderImage(block, context) {
|
|
|
550
561
|
if (block.src === "") return "";
|
|
551
562
|
const padding = toPaddingString(block.styles.padding);
|
|
552
563
|
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
553
|
-
const width = block.width === "full" ? context.containerWidth + "px" : block.width + "px";
|
|
564
|
+
const width = escapeAttr(block.width === "full" ? context.containerWidth + "px" : block.width + "px");
|
|
554
565
|
const height = heightAttr(block.height);
|
|
555
566
|
const fluid = fluidOnMobileAttr(block, context);
|
|
556
567
|
const visibilityAttr = getCssClassAttr(block);
|
|
@@ -567,7 +578,7 @@ function renderImage(block, context) {
|
|
|
567
578
|
src="${src}"
|
|
568
579
|
alt="${decorative ? "" : escapeAttr(block.alt)}"
|
|
569
580
|
width="${width}"${height}${fluid}
|
|
570
|
-
align="${block.align}"
|
|
581
|
+
align="${escapeAttr(block.align)}"
|
|
571
582
|
padding="${padding}"${bgColor}${borderAttrStr}${borderRadiusAttrStr}${linkAttr}${visibilityAttr}${decorative ? " role=\"presentation\"" : ""}
|
|
572
583
|
/>`;
|
|
573
584
|
}
|
|
@@ -593,20 +604,23 @@ function renderButton(block, context) {
|
|
|
593
604
|
const backgroundColor = buttonBackgroundAttr(block.backgroundColor);
|
|
594
605
|
const textColor = escapeAttr(block.textColor);
|
|
595
606
|
const fontSize = block.fontSize;
|
|
596
|
-
const
|
|
607
|
+
const fontWeight = renderFontWeight(block.fontWeight);
|
|
608
|
+
const borderRadius = escapeAttr(renderButtonRadius(block.borderRadius));
|
|
597
609
|
const text = escapeHtml(block.text);
|
|
598
610
|
const targetAttr = block.openInNewTab ? " target=\"_blank\" rel=\"noopener\"" : "";
|
|
599
611
|
const fontFamilyAttr = renderFontFamilyAttr$2(block.fontFamily, context);
|
|
600
612
|
const widthAttr = renderWidthAttr(block.width);
|
|
601
613
|
const cssClassAttr = getCssClassAttr(block, block.width === "full" ? [BUTTON_FULL_WIDTH_CSS_CLASS] : []);
|
|
614
|
+
const borderAttrStr = borderAttr(block.border);
|
|
615
|
+
const align = escapeAttr(block.align ?? "center");
|
|
602
616
|
return `<mj-button${hrefAttr}${targetAttr}
|
|
603
617
|
background-color="${backgroundColor}"
|
|
604
618
|
color="${textColor}"
|
|
605
|
-
font-size="${fontSize}px"
|
|
606
|
-
font-weight="
|
|
607
|
-
border-radius="${borderRadius}"${
|
|
619
|
+
font-size="${escapeAttr(String(fontSize))}px"
|
|
620
|
+
font-weight="${fontWeight}"
|
|
621
|
+
border-radius="${borderRadius}"${borderAttrStr}
|
|
608
622
|
inner-padding="${buttonPadding}"
|
|
609
|
-
align="${
|
|
623
|
+
align="${align}"
|
|
610
624
|
padding="${padding}"${bgColor}${fontFamilyAttr}${widthAttr}${cssClassAttr}
|
|
611
625
|
>${text}</mj-button>`;
|
|
612
626
|
}
|
|
@@ -618,6 +632,16 @@ function buttonBackgroundAttr(color) {
|
|
|
618
632
|
if (color.trim() === "") return "transparent";
|
|
619
633
|
return escapeAttr(color);
|
|
620
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Omitted weight stays `bold`, the attribute a button has always rendered
|
|
637
|
+
* with. A stored weight is that step's CSS number.
|
|
638
|
+
*/
|
|
639
|
+
function renderFontWeight(weight) {
|
|
640
|
+
if (weight === "regular") return "400";
|
|
641
|
+
if (weight === "medium") return "500";
|
|
642
|
+
if (weight === "bold") return "700";
|
|
643
|
+
return "bold";
|
|
644
|
+
}
|
|
621
645
|
function renderFontFamilyAttr$2(fontFamily, context) {
|
|
622
646
|
if (!fontFamily) return "";
|
|
623
647
|
return ` font-family="${escapeFontFamily(context.resolveFontFamily(fontFamily))}"`;
|
|
@@ -633,7 +657,7 @@ function renderButtonRadius(radius) {
|
|
|
633
657
|
}
|
|
634
658
|
function renderWidthAttr(width) {
|
|
635
659
|
if (width === void 0) return "";
|
|
636
|
-
return ` width="${width === "full" ? "100%" : `${width}px`}"`;
|
|
660
|
+
return ` width="${escapeAttr(width === "full" ? "100%" : `${width}px`)}"`;
|
|
637
661
|
}
|
|
638
662
|
// src/renderers/divider.ts
|
|
639
663
|
/**
|
|
@@ -644,12 +668,16 @@ function renderDivider(block, _context) {
|
|
|
644
668
|
const padding = toPaddingString(block.styles.padding);
|
|
645
669
|
const bgColor = block.styles.backgroundColor ? ` container-background-color="${escapeAttr(block.styles.backgroundColor)}"` : "";
|
|
646
670
|
const width = block.width === "full" ? "100%" : typeof block.width === "number" ? `${block.width}px` : block.width;
|
|
671
|
+
const thickness = block.thickness;
|
|
672
|
+
const lineStyle = escapeAttr(block.lineStyle);
|
|
673
|
+
const color = escapeAttr(block.color);
|
|
674
|
+
const visibilityAttr = getCssClassAttr(block);
|
|
647
675
|
return `<mj-divider
|
|
648
|
-
border-width="${
|
|
649
|
-
border-style="${
|
|
650
|
-
border-color="${
|
|
651
|
-
width="${width}"
|
|
652
|
-
padding="${padding}"${bgColor}${
|
|
676
|
+
border-width="${escapeAttr(String(thickness))}px"
|
|
677
|
+
border-style="${lineStyle}"
|
|
678
|
+
border-color="${color}"
|
|
679
|
+
width="${escapeAttr(width)}"
|
|
680
|
+
padding="${padding}"${bgColor}${visibilityAttr}
|
|
653
681
|
/>`;
|
|
654
682
|
}
|
|
655
683
|
// src/renderers/spacer.ts
|
|
@@ -664,7 +692,10 @@ function renderDivider(block, _context) {
|
|
|
664
692
|
*/
|
|
665
693
|
function renderSpacer(block, _context) {
|
|
666
694
|
if (isHiddenOnAll(block)) return "";
|
|
667
|
-
|
|
695
|
+
const height = block.height;
|
|
696
|
+
const bgColor = block.styles.backgroundColor ? ` container-background-color="${escapeAttr(block.styles.backgroundColor)}"` : "";
|
|
697
|
+
const visibilityAttr = getCssClassAttr(block);
|
|
698
|
+
return `<mj-spacer height="${escapeAttr(String(height))}px" padding="0"${bgColor}${visibilityAttr} />`;
|
|
668
699
|
}
|
|
669
700
|
// src/renderers/html.ts
|
|
670
701
|
/**
|
|
@@ -701,10 +732,10 @@ function renderSocialIcons(block, context) {
|
|
|
701
732
|
const padding = toPaddingString(block.styles.padding);
|
|
702
733
|
const bgColor = block.styles.backgroundColor ? ` container-background-color="${escapeAttr(block.styles.backgroundColor)}"` : "";
|
|
703
734
|
const visibilityAttr = getCssClassAttr(block);
|
|
704
|
-
const align = block.align;
|
|
735
|
+
const align = escapeAttr(block.align);
|
|
705
736
|
const iconSize = block.iconSize;
|
|
706
737
|
const iconStyle = block.iconStyle;
|
|
707
|
-
const assetDir = socialIconAssetDir(iconStyle, block.iconTone);
|
|
738
|
+
const assetDir = escapeAttr(socialIconAssetDir(iconStyle, block.iconTone));
|
|
708
739
|
const spacing = block.spacing;
|
|
709
740
|
let iconSizePx;
|
|
710
741
|
switch (iconSize) {
|
|
@@ -737,11 +768,11 @@ function renderSocialIcons(block, context) {
|
|
|
737
768
|
padding="${padding}"${bgColor}${visibilityAttr}
|
|
738
769
|
>
|
|
739
770
|
${icons.map((icon, index) => {
|
|
740
|
-
const platform = icon.platform;
|
|
771
|
+
const platform = escapeAttr(icon.platform);
|
|
741
772
|
const url = escapeAttr(icon.url);
|
|
742
773
|
const iconSrc = `${context.socialIconsBaseUrl}/${assetDir}/${platform}.png`;
|
|
743
774
|
const rightPad = index === iconCount - 1 ? 0 : spacing;
|
|
744
|
-
return `<mj-social-element src="${iconSrc}" href="${url}" icon-size="${iconSizePx}px" padding="0 ${rightPad}px 0 0" border-radius="${borderRadius}" background-color="transparent"></mj-social-element>`;
|
|
775
|
+
return `<mj-social-element src="${iconSrc}" href="${url}" icon-size="${iconSizePx}px" padding="0 ${escapeAttr(String(rightPad))}px 0 0" border-radius="${borderRadius}" background-color="transparent"></mj-social-element>`;
|
|
745
776
|
}).join("\n")}
|
|
746
777
|
</mj-social>`;
|
|
747
778
|
}
|
|
@@ -757,13 +788,16 @@ function renderMenu(block, context) {
|
|
|
757
788
|
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
758
789
|
const visibilityAttr = getCssClassAttr(block);
|
|
759
790
|
const fontFamilyAttr = renderFontFamilyAttr$1(block.fontFamily, context);
|
|
760
|
-
const align = block.textAlign;
|
|
791
|
+
const align = escapeAttr(block.textAlign);
|
|
792
|
+
const fontSize = block.fontSize;
|
|
793
|
+
const colorAttr = block.color ? `\n color="${escapeAttr(block.color)}"` : "";
|
|
794
|
+
const content = renderMenuItems(block);
|
|
761
795
|
return `<mj-text
|
|
762
|
-
font-size="${
|
|
796
|
+
font-size="${escapeAttr(String(fontSize))}px"${colorAttr}
|
|
763
797
|
align="${align}"
|
|
764
798
|
line-height="1.5"
|
|
765
799
|
padding="${padding}"${bgColor}${fontFamilyAttr}${visibilityAttr}
|
|
766
|
-
>${
|
|
800
|
+
>${content}</mj-text>`;
|
|
767
801
|
}
|
|
768
802
|
function renderMenuItems(block) {
|
|
769
803
|
const items = block.items;
|
|
@@ -775,7 +809,7 @@ function renderMenuItems(block) {
|
|
|
775
809
|
const itemCount = items.length;
|
|
776
810
|
for (let index = 0; index < itemCount; index++) {
|
|
777
811
|
parts.push(renderMenuItem(items[index], linkColor));
|
|
778
|
-
if (index < itemCount - 1) parts.push(`<span style="color: ${separatorColor}; padding: 0 ${spacing}px;">${separator}</span>`);
|
|
812
|
+
if (index < itemCount - 1) parts.push(`<span style="color: ${separatorColor}; padding: 0 ${escapeCssValue(String(spacing))}px;">${separator}</span>`);
|
|
779
813
|
}
|
|
780
814
|
return parts.join("");
|
|
781
815
|
}
|
|
@@ -806,12 +840,16 @@ function renderTable(block, context) {
|
|
|
806
840
|
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
807
841
|
const visibilityAttr = getCssClassAttr(block);
|
|
808
842
|
const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);
|
|
843
|
+
const fontSize = block.fontSize;
|
|
844
|
+
const colorAttr = block.color ? `\n color="${escapeAttr(block.color)}"` : "";
|
|
845
|
+
const align = escapeAttr(block.textAlign);
|
|
846
|
+
const tableHtml = renderTableElement(block);
|
|
809
847
|
return `<mj-text
|
|
810
|
-
font-size="${
|
|
811
|
-
align="${
|
|
848
|
+
font-size="${escapeAttr(String(fontSize))}px"${colorAttr}
|
|
849
|
+
align="${align}"
|
|
812
850
|
line-height="1.5"
|
|
813
851
|
padding="${padding}"${bgColor}${fontFamilyAttr}${visibilityAttr}
|
|
814
|
-
>${
|
|
852
|
+
>${tableHtml}</mj-text>`;
|
|
815
853
|
}
|
|
816
854
|
function renderTableElement(block) {
|
|
817
855
|
const borderColor = escapeCssValue(block.borderColor);
|
|
@@ -832,7 +870,7 @@ function renderRow(row, block, isHeader, borderColor, borderWidth) {
|
|
|
832
870
|
}
|
|
833
871
|
function renderCell(cell, block, isHeader, borderColor, borderWidth) {
|
|
834
872
|
const cellPadding = block.cellPadding;
|
|
835
|
-
const styles = [`border: ${borderWidth}px solid ${borderColor}`, `padding: ${cellPadding}px`];
|
|
873
|
+
const styles = [`border: ${escapeCssValue(String(borderWidth))}px solid ${borderColor}`, `padding: ${escapeCssValue(String(cellPadding))}px`];
|
|
836
874
|
if (isHeader) {
|
|
837
875
|
styles.push("font-weight: bold");
|
|
838
876
|
if (block.headerBackgroundColor) styles.push(`background-color: ${escapeCssValue(block.headerBackgroundColor)}`);
|
|
@@ -1025,7 +1063,7 @@ function renderHero(block, hero, context, renderBlock) {
|
|
|
1025
1063
|
const visibilityAttr = getCssClassAttr(block);
|
|
1026
1064
|
const contentContext = context.withContainerWidth(boxWidth, boxWidth - horizontalInset(block.styles.padding));
|
|
1027
1065
|
const content = filterHtmlBlocks$1(block.children[0] ?? [], context.allowHtmlBlocks).filter((child) => !isSection(child)).map((child) => wrapWithDisplayCondition(child, renderBlock(child, contentContext))).filter((value) => value !== "").join("\n");
|
|
1028
|
-
return `<mj-hero mode="${geometry.mode}" height="${geometry.height}px" background-url="${escapeAttr(hero.image.url)}" background-width="${geometry.backgroundWidth}px" background-height="${geometry.backgroundHeight}px" background-position="${hero.image.position}"${bgColor} padding="${padding}" vertical-align="${hero.verticalAlign}"${borderRadiusAttrStr}${visibilityAttr}>
|
|
1066
|
+
return `<mj-hero mode="${geometry.mode}" height="${geometry.height}px" background-url="${escapeAttr(hero.image.url)}" background-width="${geometry.backgroundWidth}px" background-height="${geometry.backgroundHeight}px" background-position="${escapeAttr(hero.image.position)}"${bgColor} padding="${padding}" vertical-align="${escapeAttr(hero.verticalAlign)}"${borderRadiusAttrStr}${visibilityAttr}>
|
|
1029
1067
|
${content}
|
|
1030
1068
|
</mj-hero>`;
|
|
1031
1069
|
}
|
|
@@ -1062,7 +1100,7 @@ function renderVideo(block, context) {
|
|
|
1062
1100
|
if (!thumbnailUrl) return "";
|
|
1063
1101
|
const padding = toPaddingString(block.styles.padding);
|
|
1064
1102
|
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
1065
|
-
const width = block.width === "full" ? context.containerWidth + "px" : block.width + "px";
|
|
1103
|
+
const width = escapeAttr(block.width === "full" ? context.containerWidth + "px" : block.width + "px");
|
|
1066
1104
|
const height = heightAttr(block.height);
|
|
1067
1105
|
const fluid = fluidOnMobileAttr(block, context);
|
|
1068
1106
|
const visibilityAttr = getCssClassAttr(block);
|
|
@@ -1070,7 +1108,7 @@ function renderVideo(block, context) {
|
|
|
1070
1108
|
src="${escapeAttr(thumbnailUrl)}"
|
|
1071
1109
|
alt="${escapeAttr(block.alt)}"
|
|
1072
1110
|
width="${width}"${height}${fluid}
|
|
1073
|
-
align="${block.align}"
|
|
1111
|
+
align="${escapeAttr(block.align)}"
|
|
1074
1112
|
padding="${padding}"
|
|
1075
1113
|
href="${escapeAttr(block.url)}"
|
|
1076
1114
|
target="_blank"
|
|
@@ -1143,7 +1181,7 @@ async function renderToMjml(content, options) {
|
|
|
1143
1181
|
const renderContext = new RenderContext(toRender.settings.width, customFonts, defaultFallbackFont, allowHtmlBlocks, customBlockHtml, socialIconsBaseUrl, options?.blockRenderers ?? {}, contentDirection);
|
|
1144
1182
|
const blocks = filterHtmlBlocks(toRender.blocks, allowHtmlBlocks);
|
|
1145
1183
|
const fontFamily = renderContext.resolveFontFamily(toRender.settings.fontFamily);
|
|
1146
|
-
const backgroundColor = toRender.settings.backgroundColor;
|
|
1184
|
+
const backgroundColor = /[<>]/.test(toRender.settings.backgroundColor) ? "transparent" : toRender.settings.backgroundColor;
|
|
1147
1185
|
const textColorAttr = toRender.settings.textColor ? ` color="${escapeAttr(toRender.settings.textColor)}"` : "";
|
|
1148
1186
|
const linkColor = toRender.settings.linkColor ? escapeCssValue(toRender.settings.linkColor) : "inherit";
|
|
1149
1187
|
const linkDecoration = toRender.settings.linkUnderline ? "underline" : "none";
|
|
@@ -1172,7 +1210,7 @@ async function renderToMjml(content, options) {
|
|
|
1172
1210
|
}
|
|
1173
1211
|
</mj-style>${renderCustomBlockStylesheets(customBlockStylesheets)}
|
|
1174
1212
|
</mj-head>
|
|
1175
|
-
<mj-body width="${renderContext.containerWidth}px" background-color="${backgroundColor}">
|
|
1213
|
+
<mj-body width="${escapeAttr(String(renderContext.containerWidth))}px" background-color="${escapeCssValue(backgroundColor)}">
|
|
1176
1214
|
${bodyContent}
|
|
1177
1215
|
</mj-body>
|
|
1178
1216
|
</mjml>`;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["pkg.version","renderFontFamilyAttr","renderFontFamilyAttr","renderFontFamilyAttr","filterHtmlBlocks"],"sources":["../package.json","../src/render-context.ts","../src/visibility.ts","../src/unrenderable.ts","../src/escape.ts","../src/padding.ts","../src/utils.ts","../src/rich-text.ts","../src/renderers/title.ts","../src/renderers/paragraph.ts","../src/renderers/image.ts","../src/renderers/button.ts","../src/renderers/divider.ts","../src/renderers/spacer.ts","../src/renderers/html.ts","../src/renderers/social.ts","../src/renderers/menu.ts","../src/renderers/table.ts","../src/renderers/custom.ts","../src/columns.ts","../src/hero-geometry.ts","../src/display-condition.ts","../src/renderers/section.ts","../src/renderers/video.ts","../src/renderers/index.ts","../src/index.ts"],"sourcesContent":["","import type {\n Block,\n BlockType,\n ContentDirection,\n CustomFont,\n} from \"@templatical/types\";\nimport { resolveFontStack } from \"@templatical/types\";\nimport pkg from \"../package.json\" with { type: \"json\" };\n\n/**\n * A function type that renders a single block to MJML markup.\n */\nexport type BlockRenderer = (block: Block, context: RenderContext) => string;\n\n/**\n * Per-block-type renderer overrides, keyed by `block.type`. An entry replaces the\n * built-in renderer for that type wholesale — including its hidden-on-all-\n * viewports check, which becomes the override's responsibility.\n *\n * Generalises `renderCustomBlock`, which is the same idea for one block type. Its\n * reason to exist is that Templatical Cloud's render output is a deliberate\n * *superset*: countdown blocks resolve to a server-generated animated GIF and\n * video thumbnails get a composited play button, neither of which a browser can\n * do at render time. With this hook Cloud's renderer is the published one plus\n * two injected functions, so parity for every other block type holds by\n * construction instead of by review.\n */\nexport type BlockRendererMap = Partial<Record<BlockType, BlockRenderer>>;\n\nexport const DEFAULT_SOCIAL_ICONS_BASE_URL = `https://cdn.jsdelivr.net/npm/@templatical/renderer@${pkg.version}/assets/social`;\n\n/**\n * Immutable context passed through the block rendering chain.\n */\nexport class RenderContext {\n /**\n * The whole pixels MJML gives this container's content: the body width less\n * the side padding and border of every wrapper and section around it, then\n * the column's own percentage of what is left, floored as MJML floors it.\n * Width-aware decisions read this one, since it is the box MJML caps a block\n * at. Defaults to `containerWidth`.\n */\n public readonly contentWidth: number;\n\n constructor(\n /**\n * The column's share of the body width, ignoring any padding or border\n * around it. Emitted as the pixel width of a `\"full\"` image or video, which\n * MJML then caps at the real box ({@link contentWidth}).\n */\n public readonly containerWidth: number,\n public readonly customFonts: CustomFont[],\n public readonly defaultFallbackFont: string,\n public readonly allowHtmlBlocks: boolean,\n /**\n * Map of custom block id → pre-rendered HTML, populated by `renderToMjml`\n * before the synchronous render pass. Set when the consumer provides a\n * `renderCustomBlock` option. Empty by default.\n */\n public readonly customBlockHtml: ReadonlyMap<string, string> = new Map(),\n /**\n * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png`. Outlook desktop has no SVG support\n * and rejects base64 data URIs in `<img src>`, so PNGs must be served over\n * HTTP. Default points at the version-pinned jsDelivr mirror of this\n * package; consumers can override to self-host.\n */\n public readonly socialIconsBaseUrl: string = DEFAULT_SOCIAL_ICONS_BASE_URL,\n /**\n * Per-block-type renderer overrides from `RenderOptions.blockRenderers`.\n * Consulted before every built-in renderer — see {@link BlockRendererMap}.\n */\n public readonly blockRenderers: BlockRendererMap = {},\n /**\n * Writing direction of the email, resolved once in `renderToMjml`.\n * Nested columns copy it via `withContainerWidth` — dropping it there\n * would leave a two-column RTL section rendering LTR inside the columns.\n */\n public readonly contentDirection: ContentDirection = \"ltr\",\n contentWidth?: number,\n ) {\n this.contentWidth = contentWidth ?? containerWidth;\n }\n\n /**\n * Create a new context with a different container width. Used when rendering\n * columns, and wrappers, whose content is narrower. `contentWidth` defaults\n * to `width`, a container with nothing around it.\n */\n withContainerWidth(\n width: number,\n contentWidth: number = width,\n ): RenderContext {\n return new RenderContext(\n width,\n this.customFonts,\n this.defaultFallbackFont,\n this.allowHtmlBlocks,\n this.customBlockHtml,\n this.socialIconsBaseUrl,\n this.blockRenderers,\n this.contentDirection,\n contentWidth,\n );\n }\n\n /**\n * The stack to emit for a block's or the template's `fontFamily`. A stack is\n * the author's own fallback chain and is emitted as written, as is anything\n * that doesn't parse as a font. A lone family gets a fallback: a custom\n * font's own `fallback` (or `defaultFallbackFont`), a built-in's fixed stack,\n * or, for any other family, `defaultFallbackFont`, so a client without the\n * face falls back to it rather than to its own default (Times New Roman in\n * Outlook on Windows). A generic family needs nothing appended.\n *\n * The rule is {@link resolveFontStack}, shared with the editor canvas so a\n * font previews with the fallbacks the recipient gets.\n */\n resolveFontFamily(fontFamily: string): string {\n return resolveFontStack(fontFamily, {\n customFonts: this.customFonts,\n defaultFallback: this.defaultFallbackFont,\n });\n }\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Check if a block is hidden on all viewports.\n */\nexport function isHiddenOnAll(block: Block): boolean {\n const visibility = block.visibility;\n\n if (!visibility) {\n return false;\n }\n\n return !visibility.desktop && !visibility.mobile;\n}\n\n/**\n * Get the MJML css-class attribute string for a block.\n * Returns a string like ` css-class=\"tpl-hide-desktop\"` or empty string.\n *\n * `extraClasses` are appended to the visibility classes on the *same*\n * attribute. MJML keeps only one `css-class` per element, so a caller needing\n * both must merge here rather than emitting a second attribute.\n */\nexport function getCssClassAttr(\n block: Block,\n extraClasses: string[] = [],\n): string {\n const classes = [getCssClasses(block), ...extraClasses]\n .filter((entry) => entry !== \"\")\n .join(\" \");\n\n if (classes === \"\") {\n return \"\";\n }\n\n return ` css-class=\"${classes}\"`;\n}\n\n/**\n * Get the CSS classes for visibility hiding.\n */\nexport function getCssClasses(block: Block): string {\n const visibility = block.visibility;\n\n if (!visibility) {\n return \"\";\n }\n\n const classes: string[] = [];\n\n if (!visibility.desktop) {\n classes.push(\"tpl-hide-desktop\");\n }\n\n if (!visibility.mobile) {\n classes.push(\"tpl-hide-mobile\");\n }\n\n return classes.join(\" \");\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Prefix every unrenderable-block marker carries. Stable and greppable on\n * purpose: it is what a consumer searches for after noticing a gap in a rendered\n * email, and what a send pipeline can scan for before shipping.\n */\nexport const UNRENDERABLE_MARKER_PREFIX = \"templatical:unrenderable-block\";\n\n/**\n * Strip anything that would terminate or nest an HTML comment. `-->` and `--!>`\n * inside the marker would close it early and leak the rest as body text; a lone\n * `--` is enough to make the comment non-conforming, so both collapse.\n *\n * Brackets go **before** hyphens collapse, and the order is load-bearing.\n * Removing `<` from `-<-` brings two hyphens together that a collapse pass has\n * already walked past, so collapsing first leaves a `--` behind — the sanitizer\n * reintroducing the sequence it exists to remove.\n */\nfunction commentSafe(value: string): string {\n return value.replace(/[<>]/g, \"\").replace(/-{2,}/g, \"-\");\n}\n\n/**\n * Emit the placeholder for a block no renderer can handle — no built-in renderer\n * and no `blockRenderers` override for its type.\n *\n * **A marker, not a throw and not silence.** The renderer runs inside send\n * pipelines, so killing an entire render over one block is worse than shipping a\n * marked gap. Silence is worse still: a block that simply vanishes reaches\n * recipients as a missing section with nothing anywhere explaining why. The rule\n * is general rather than countdown-specific, so\n * a block type added to `@templatical/types` before its renderer lands degrades\n * the same way.\n *\n * `mj-raw` is used because it is the one element that passes its content through\n * verbatim and is valid everywhere a block renders — inside `mj-column`, which is\n * where every non-section block ends up.\n */\nexport function renderUnrenderableBlock(block: Block): string {\n console.warn(\n `[Templatical] No renderer for block type \"${block.type}\" (id: ${block.id}). ` +\n \"A placeholder comment was emitted in its place. Pass a `blockRenderers` \" +\n \"entry for this type to render it.\",\n );\n\n const type = commentSafe(block.type);\n const id = commentSafe(block.id);\n\n return `<mj-raw><!-- ${UNRENDERABLE_MARKER_PREFIX} type=\"${type}\" id=\"${id}\" --></mj-raw>`;\n}\n","const HTML_ENTITIES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\nconst HTML_ENTITY_REGEX = /[&<>\"']/g;\n\n/**\n * Escape HTML special characters (& < > \" ').\n * Equivalent to PHP htmlspecialchars with ENT_QUOTES | ENT_HTML5.\n */\nexport function escapeHtml(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use in an HTML attribute value.\n * Same implementation as escapeHtml for consistency with PHP.\n */\nexport function escapeAttr(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use as a CSS property value inside an inline\n * `style=\"prop: ${value}\"` attribute. Beyond HTML entity escaping (so the\n * value survives the attribute boundary), this strips characters that\n * could break out of the property value into a sibling property:\n *\n * `;` — separates CSS declarations\n * `{`/`}` — opens/closes a CSS rule (rejected by attribute parsers but\n * still safer to remove)\n * `\\n`/`\\r` — would smuggle past line-based CSS sanitizers\n *\n * Without this, an attacker-controlled color like\n * `\"red; background: url('//attacker/log')\"` lands as a real CSS rule.\n */\nexport function escapeCssValue(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n return escapeAttr(text).replace(/[;{}\\r\\n]/g, \"\");\n}\n\nconst FONT_FAMILY_ENTITY_REGEX = /[&<>\"]/g;\n\n/**\n * Escape a font stack for a double-quoted `font-family=\"…\"` MJML attribute.\n * A `\"`-quoted family (`\"Helvetica Neue\", Arial`) must not end the attribute,\n * or the compiler reads it as empty. Unlike `escapeAttr`, single quotes are\n * left alone: they can't end a double-quoted attribute, and they're how the\n * renderer quotes families itself (`'Times New Roman', serif`), so every\n * stack without `&<>\"` is emitted byte-for-byte.\n */\nexport function escapeFontFamily(text: string): string {\n return text.replace(\n FONT_FAMILY_ENTITY_REGEX,\n (char) => HTML_ENTITIES[char] ?? char,\n );\n}\n\nexport { convertMergeTagsToValues } from \"@templatical/types\";\n","import type { SpacingValue } from \"@templatical/types\";\n\n/**\n * Convert a SpacingValue to a CSS padding string like \"10px 10px 10px 10px\".\n */\nexport function toPaddingString(padding: SpacingValue): string {\n return `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`;\n}\n","/**\n * MJML attribute helpers — single source of truth for placement rules\n * that the MJML spec enforces but accepts silently when violated.\n *\n * MJML drops unrecognized attributes without error. The most common trap is\n * background placement: only `mj-section`, `mj-button`, `mj-wrapper`, `mj-hero`\n * accept the native `background-color` attribute. Inner content elements\n * (`mj-text`, `mj-image`, `mj-table`, `mj-navbar`, `mj-video`) require\n * `container-background-color`, which paints the enclosing `<td>`. Passing\n * `background-color` to an inner element results in the attribute being\n * silently dropped — the email ships without the bg.\n *\n * https://documentation.mjml.io/\n */\n\nimport type {\n BackgroundImage,\n BorderRadiusValue,\n BorderValue,\n ImageBlock,\n SpacingValue,\n} from \"@templatical/types\";\nimport {\n resolveBackgroundImage,\n toBorderCss,\n toBorderDeclarations,\n toBorderRadiusCss,\n} from \"@templatical/types\";\nimport { escapeAttr, escapeCssValue } from \"./escape\";\nimport type { RenderContext } from \"./render-context\";\n\n/**\n * Where the MJML element accepts a background-color attribute.\n * - `native`: the element has its own `background-color` (mj-section, mj-button).\n * - `container`: the element only accepts `container-background-color`,\n * which colors the wrapping `<td>` (mj-text, mj-image, mj-table, mj-navbar, mj-video).\n */\nexport type BgPlacement = \"native\" | \"container\";\n\n/**\n * Render the appropriate background-color attribute for an MJML element.\n * Returns an empty string when no color is set, or a leading-space attribute\n * fragment ready to interpolate into a tag's attribute list.\n */\nexport function bgAttr(\n backgroundColor: string | undefined,\n placement: BgPlacement,\n): string {\n if (!backgroundColor) {\n return \"\";\n }\n\n const name =\n placement === \"native\" ? \"background-color\" : \"container-background-color\";\n\n return ` ${name}=\"${backgroundColor}\"`;\n}\n\n/**\n * Render `mj-image`'s height attribute. Returns an empty string when no height\n * is set, so MJML applies its own `auto` and the image keeps its aspect ratio.\n *\n * The px suffix is not cosmetic: `height` is a Unit attribute accepting only\n * `px` or `auto`, so a bare number is a validation error and MJML drops it.\n * Non-finite or non-positive values are treated as unset — a `0` here would\n * collapse the image rather than express \"no opinion\".\n */\nexport function heightAttr(height: number | undefined): string {\n if (typeof height !== \"number\" || !Number.isFinite(height) || height <= 0) {\n return \"\";\n }\n\n return ` height=\"${height}px\"`;\n}\n\n/**\n * The width an element's side padding and drawn side borders take from its\n * box. MJML subtracts exactly these when it sizes a wrapper's, a section's or\n * an image's content, so a side `toBorderCss` would not draw counts for 0.\n * MJML reads each side as a whole pixel (`parseInt` in its `shorthandParser`\n * and `borderParser`), so 20.5px of padding takes 20px and a 2.5px border 2px.\n */\nexport function horizontalInset(\n padding: SpacingValue | undefined,\n border?: BorderValue,\n): number {\n const whole = (value: number | undefined) => Math.trunc(value ?? 0);\n const drawn = (side: BorderValue[\"left\"] | undefined) =>\n toBorderCss(side) === null ? 0 : Math.trunc(side!.width);\n return (\n whole(padding?.left) +\n whole(padding?.right) +\n drawn(border?.left) +\n drawn(border?.right)\n );\n}\n\n/**\n * `fluid-on-mobile` for an `mj-image`, set only on an image that fills its\n * column on desktop: `\"full\"`, or a pixel width MJML caps at the room it gives\n * the image (the column less the image's own side padding and border). On a\n * phone the attribute widens the image to the stacked column, so an image set\n * narrower must not carry it: a 120px logo would grow to the whole column, and\n * one with a set height would stretch out of shape. An image wider than the\n * phone shrinks to fit without it.\n *\n * Exported for `blockRenderers` entries that emit their own `mj-image`. Pass\n * the context the renderer received, whose `contentWidth` already excludes the\n * wrappers and sections around the column.\n */\nexport function fluidOnMobileAttr(\n block: Pick<ImageBlock, \"width\" | \"styles\" | \"border\">,\n context: RenderContext,\n): string {\n const room =\n Math.floor(context.contentWidth) -\n horizontalInset(block.styles.padding, block.border);\n return block.width === \"full\" || block.width >= room\n ? ' fluid-on-mobile=\"true\"'\n : \"\";\n}\n\n/**\n * Render the border attributes for the MJML elements that accept them natively\n * (`mj-section`, `mj-image`, `mj-button`): a single `border` when it covers all\n * four sides, otherwise one `border-<side>` per chosen side. Returns an empty\n * string when there is nothing to draw, so templates without a border render\n * exactly as before.\n *\n * MJML copies the values into an inline `style`, so they go through\n * `escapeCssValue` — a tampered color must not smuggle in a sibling\n * declaration.\n */\nexport function borderAttr(border: BorderValue | undefined): string {\n return Object.entries(toBorderDeclarations(border))\n .map(([name, value]) => ` ${name}=\"${escapeCssValue(value)}\"`)\n .join(\"\");\n}\n\n/**\n * Render the `border-radius` attribute: `\"8px\"` for matching corners, or the\n * four-value shorthand (top-left, top-right, bottom-right, bottom-left) that\n * `mj-section`, `mj-wrapper`, `mj-image` and `mj-button` all accept. Returns an\n * empty string when every corner is square, rather than a `border-radius=\"0px\"`\n * every existing template would suddenly grow.\n */\nexport function borderRadiusAttr(\n radius: BorderRadiusValue | undefined,\n): string {\n const css = toBorderRadiusCss(radius);\n\n if (css === null) {\n return \"\";\n }\n\n return ` border-radius=\"${css}\"`;\n}\n\n/**\n * Render the background-image attributes `mj-section` and `mj-wrapper` accept.\n * All four are emitted together, so MJML's own defaults (`auto`, `top center`,\n * `repeat`) never apply. Returns an empty string when there is no image.\n *\n * MJML pastes `background-url` verbatim into `url('…')` and the Outlook VML\n * `src`; `resolveBackgroundImage` has already percent-encoded the characters\n * that could end the CSS string, and `escapeAttr` keeps the attribute closed.\n */\nexport function backgroundImageAttrs(bg: BackgroundImage | undefined): string {\n const resolved = resolveBackgroundImage(bg);\n\n if (resolved === null) {\n return \"\";\n }\n\n return ` background-url=\"${escapeAttr(resolved.url)}\" background-size=\"${resolved.size}\" background-position=\"${resolved.position}\" background-repeat=\"${resolved.repeat}\"`;\n}\n","import {\n RICH_TEXT_SPACING,\n findOpenTagEnd,\n isSection,\n isWrapper,\n} from \"@templatical/types\";\nimport type { Block, ContentDirection } from \"@templatical/types\";\n\n/**\n * Marks the `mj-text` of a block whose content is editor-authored rich text\n * (paragraph and title). MJML puts a `css-class` on the wrapping `<td>`, which\n * is what {@link richTextStylesheet} scopes its selectors to.\n *\n * The scope is the point. A bare `p { … }` rule would also reach an `html`\n * block — the renderer puts consumer markup in `mj-text` too — and the editor\n * previews those in a sandboxed `srcdoc` iframe on UA defaults. Styling them\n * would trade this mismatch for another one, and an inlined declaration would\n * beat the consumer's own stylesheet.\n */\nexport const RICH_TEXT_CSS_CLASS = \"tpl-rich-text\";\n\n/**\n * The class carrying one specific paragraph gap, e.g. `tpl-rich-text-8`.\n *\n * Every rich-text block gets one of these *instead of* a shared paragraph rule.\n * That is a hard constraint, not a preference: `.tpl-rich-text p` and\n * `.tpl-rich-text-4 p` have identical specificity (one class, one type), so a\n * surviving base rule would win on source order and flatten every per-block gap\n * back to the default.\n */\nexport function richTextGapClass(gap: number): string {\n return `${RICH_TEXT_CSS_CLASS}-${gap}`;\n}\n\n/**\n * The global rules that make exported rich text match the editor canvas.\n *\n * Two gaps they close, neither visible until you measure the delivered email:\n *\n * - MJML's core skeleton injects `p { display:block;margin:13px 0; }` into\n * every document. Against the canvas's paragraph gap that made a\n * three-paragraph block 99px in the editor and 135px in the inbox — 13px\n * above the first paragraph, 13px below the last, and 13px rather than 8px\n * between each (issue #616).\n * - The skeleton resets nothing for `ul` / `ol` / `li`, so those fall through\n * to each client's UA stylesheet — around 14px margin and a 40px bullet\n * indent in Chromium, against the canvas's 8px and 24px. Unlike the\n * paragraph case the value is not even a fixed constant, so matching MJML in\n * the editor could not have fixed it.\n *\n * Emitted inside `<mj-style inline=\"inline\">`, so MJML runs it through juice\n * and the declarations land as real inline styles. That matters twice: they\n * survive a client that strips `<style>` from the head, and juice resolves\n * `:last-child` structurally at build time instead of dropping it.\n *\n * One paragraph rule per *distinct* gap in the document, so a template using\n * two gaps costs two rules rather than one per block. The list values stay on\n * the unversioned class: MJML resets nothing for `ul`/`ol`/`li`, so they exist\n * to pin down client defaults rather than to be designed with.\n */\nexport function richTextStylesheet(\n gaps: number[],\n direction: ContentDirection = \"ltr\",\n): string {\n const s = RICH_TEXT_SPACING;\n const scope = `.${RICH_TEXT_CSS_CLASS}`;\n const distinct = [...new Set(gaps)].sort((a, b) => a - b);\n\n const paragraphRules = distinct.flatMap((gap) => {\n const gapScope = `.${richTextGapClass(gap)}`;\n return [\n `${gapScope} p { margin: 0 0 ${gap}px; }`,\n `${gapScope} p:last-child { margin-bottom: 0; }`,\n ];\n });\n\n // Physical sides: Outlook does not honour `padding-inline-start`.\n const listPaddingSide =\n direction === \"rtl\" ? \"padding-right\" : \"padding-left\";\n\n return [\n ...paragraphRules,\n `${scope} ul, ${scope} ol { margin: ${s.listMarginY}px 0; ${listPaddingSide}: ${s.listPaddingLeft}px; }`,\n `${scope} li { margin: ${s.listItemMarginY}px 0; }`,\n ].join(\"\\n \");\n}\n\n/**\n * The paragraph gap a block asks for, or the built-in default.\n *\n * The fallback must produce a *rule* rather than nothing: silence hands the\n * paragraph back to MJML's `p { margin: 13px 0 }` skeleton default and reopens\n * the canvas/export mismatch. `0` is a legitimate gap, so this tests for\n * absence rather than falsiness.\n *\n * Mirrored on the canvas side by `getBlockWrapperStyle` in\n * `@templatical/editor`.\n */\nexport function resolveParagraphGap(spacing: number | undefined): number {\n return spacing ?? RICH_TEXT_SPACING.paragraphGap;\n}\n\n/**\n * Every paragraph gap the document uses, so the head can carry a rule for each.\n *\n * Walks section columns and wrapper children too — a paragraph inside a column\n * or a layout card needs its rule as much as a top-level one. Titles always\n * use the default gap: their content is normally a single paragraph the\n * renderer unwraps, and the multi-paragraph case is an edge worth spacing\n * consistently rather than configuring.\n */\nexport function collectParagraphGaps(blocks: Block[]): number[] {\n const gaps: number[] = [RICH_TEXT_SPACING.paragraphGap];\n\n const visit = (block: Block): void => {\n if (block.type === \"paragraph\") {\n gaps.push(resolveParagraphGap(block.paragraphSpacing));\n }\n if (isSection(block)) {\n for (const column of block.children) {\n for (const child of column) {\n visit(child);\n }\n }\n } else if (isWrapper(block)) {\n for (const child of block.children) {\n visit(child);\n }\n }\n };\n\n for (const block of blocks) {\n visit(block);\n }\n\n return gaps;\n}\n\n/**\n * The named references {@link decodeStyleReferences} writes out. `<` and `>`\n * stay encoded, named or numeric: MJML parses its source as XML, which forbids\n * a raw `<` in an attribute, and no CSS value needs one.\n */\nconst NAMED_STYLE_REFERENCES: Record<string, string> = {\n apos: \"'\",\n quot: '\"',\n nbsp: String.fromCharCode(0xa0),\n};\n\nconst STYLE_REFERENCE = /&(?:#(\\d+)|#x([0-9a-f]+)|(apos|quot|nbsp));/gi;\nconst TAG_NAME_START = /[a-z]/i;\nconst HTML_SPACE = /[ \\t\\n\\f\\r]/;\n\nfunction decodeStyleReference(\n reference: string,\n quote: string,\n decimal: string | undefined,\n hex: string | undefined,\n name: string | undefined,\n): string {\n let char: string;\n if (name !== undefined) {\n char = NAMED_STYLE_REFERENCES[name.toLowerCase()];\n } else {\n const codePoint = Number(decimal ?? `0x${hex}`);\n const valid =\n codePoint > 0 &&\n codePoint <= 0x10ffff &&\n !(codePoint >= 0xd800 && codePoint <= 0xdfff);\n if (!valid) return reference;\n char = String.fromCodePoint(codePoint);\n }\n // A raw `&` could start a reference, and the delimiter would end the\n // attribute; CSS reads either quote the same.\n if (char === \"&\") return \"&\";\n if (char === \"<\" || char === \">\") return reference;\n if (char === quote) return quote === '\"' ? \"'\" : '\"';\n return char;\n}\n\n/**\n * The open tag `tag` (from `<` up to, not including, its `>`) with the\n * references in its first quoted `style` attribute decoded. Attributes are\n * walked in order and quoted values skipped whole, so `style=\"…\"` text inside\n * another attribute's value is never taken for the attribute.\n */\nfunction decodeTagStyle(tag: string): string {\n let i = 1;\n while (i < tag.length && !HTML_SPACE.test(tag[i]) && tag[i] !== \"/\") i++;\n while (i < tag.length) {\n while (i < tag.length && (HTML_SPACE.test(tag[i]) || tag[i] === \"/\")) i++;\n const nameStart = i;\n while (i < tag.length && !/[\\s=/]/.test(tag[i])) i++;\n const name = tag.slice(nameStart, i).toLowerCase();\n if (name === \"\") {\n i++;\n continue;\n }\n while (i < tag.length && HTML_SPACE.test(tag[i])) i++;\n if (tag[i] !== \"=\") continue;\n i++;\n while (i < tag.length && HTML_SPACE.test(tag[i])) i++;\n const quote = tag[i];\n if (quote === '\"' || quote === \"'\") {\n const valueEnd = tag.indexOf(quote, i + 1);\n if (valueEnd === -1) return tag;\n if (name === \"style\") {\n const value = tag\n .slice(i + 1, valueEnd)\n .replace(STYLE_REFERENCE, (reference, decimal, hex, named) =>\n decodeStyleReference(reference, quote, decimal, hex, named),\n );\n return `${tag.slice(0, i + 1)}${value}${tag.slice(valueEnd)}`;\n }\n i = valueEnd + 1;\n continue;\n }\n while (i < tag.length && !HTML_SPACE.test(tag[i])) i++;\n }\n return tag;\n}\n\n/**\n * Rich-text HTML with the character references in its `style` attributes\n * written as the characters they stand for: `'`, `"`, ` ` and\n * numeric references (`'`, `'`). Other named references stay as\n * written, and so do `&`, `<` and `>` in any form; a reference to `&`\n * becomes `&`. Tags are found with the quote-aware `findOpenTagEnd`, so a\n * `>` inside an attribute value doesn't end one, and a tag whose quote never\n * closes is left alone.\n *\n * MJML inlines {@link richTextStylesheet} with juice, which re-parses the\n * style attribute of every element a rule matches and splits it on `;`\n * without decoding references first. The `;` that ends `'`, `"` or\n * ` ` outside a `url()` therefore cuts a declaration in two, and the\n * compile throws. Imported content carries such references: BeeFree's TinyMCE\n * output and import-mjml's serializer write them.\n */\nexport function decodeStyleReferences(html: string): string {\n if (!html.includes(\"&\")) return html;\n let out = \"\";\n let copied = 0;\n let open = html.indexOf(\"<\");\n while (open !== -1) {\n if (!TAG_NAME_START.test(html.charAt(open + 1))) {\n open = html.indexOf(\"<\", open + 1);\n continue;\n }\n const end = findOpenTagEnd(html, open + 1);\n if (end === -1) break;\n const tag = html.slice(open, end);\n const decoded = decodeTagStyle(tag);\n if (decoded !== tag) {\n out += html.slice(copied, open) + decoded;\n copied = end;\n }\n open = html.indexOf(\"<\", end);\n }\n return out + html.slice(copied);\n}\n","import type { TitleBlock } from \"@templatical/types\";\nimport {\n HEADING_LEVEL_FONT_SIZE,\n TITLE_FONT_WEIGHT,\n TITLE_LINE_HEIGHT,\n fillEmptyParagraphs,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport {\n convertMergeTagsToValues,\n escapeAttr,\n escapeFontFamily,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport {\n RICH_TEXT_CSS_CLASS,\n decodeStyleReferences,\n richTextGapClass,\n resolveParagraphGap,\n} from \"../rich-text\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a title block to MJML markup.\n */\nexport function renderTitle(block: TitleBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n // Blank lines between a title's paragraphs are filled as the canvas fills\n // them; a single paragraph is unwrapped into the heading.\n const content = unwrapParagraph(\n decodeStyleReferences(\n fillEmptyParagraphs(convertMergeTagsToValues(block.content)),\n ),\n );\n // Clamp out-of-range levels to a defined entry so we never interpolate\n // `undefined` into `font-size=\"${...}px\"` (mjml@5 rejects \"undefinedpx\").\n const fontSize =\n HEADING_LEVEL_FONT_SIZE[block.level] ?? HEADING_LEVEL_FONT_SIZE[2];\n // Emit the color only when the block sets one; otherwise the `mj-text`\n // inherits the document `textColor` default from `<mj-attributes>`.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n const align = block.textAlign;\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n // `unwrapParagraph` strips only a *single* outer `<p>`, so a title the user\n // pressed Enter in keeps its paragraphs and needs a spacing rule. Titles\n // carry no gap of their own, so they always take the default.\n const cssClassAttr = getCssClassAttr(block, [\n RICH_TEXT_CSS_CLASS,\n richTextGapClass(resolveParagraphGap(undefined)),\n ]);\n // Same clamp logic as fontSize so that an out-of-range level still\n // produces a valid heading element.\n const safeLevel = HEADING_LEVEL_FONT_SIZE[block.level] ? block.level : 2;\n const tag = `h${safeLevel}`;\n\n return `<mj-text\n font-size=\"${fontSize}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"${TITLE_LINE_HEIGHT}\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${cssClassAttr}\n><${tag} style=\"margin:0;font-size:inherit;color:inherit;line-height:inherit;font-weight:${TITLE_FONT_WEIGHT}\">${content}</${tag}></mj-text>`;\n}\n\n/**\n * The editor stores title content as a TipTap paragraph (`<p>...</p>`),\n * but the renderer wraps it in `<h${level}>`. `<p>` is invalid inside a\n * heading, so strip a single outer `<p>` wrapper if present.\n */\nfunction unwrapParagraph(html: string): string {\n const match = html.match(/^\\s*<p\\b[^>]*>([\\s\\S]*)<\\/p>\\s*$/);\n if (!match) return html;\n // Only unwrap when there is exactly one top-level <p> — otherwise the\n // content has multiple paragraphs and we must leave it alone.\n if (/<\\/p>\\s*<p\\b/i.test(match[1])) return html;\n return match[1];\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${escapeFontFamily(resolved)}\"`;\n}\n","import type { ParagraphBlock } from \"@templatical/types\";\nimport { fillEmptyParagraphs, isBlankRichText } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { convertMergeTagsToValues } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport {\n RICH_TEXT_CSS_CLASS,\n decodeStyleReferences,\n richTextGapClass,\n resolveParagraphGap,\n} from \"../rich-text\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a paragraph block to MJML markup.\n * All text formatting is inline in the HTML content (managed by TipTap).\n */\nexport function renderParagraph(\n block: ParagraphBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n // Skip when the user has cleared the paragraph — otherwise we emit a\n // styled `<td>` cell that adds visible whitespace to the email even\n // though the canvas shows nothing for an empty paragraph.\n if (isBlankRichText(block.content)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n // The canvas fills blank lines with the same helper, so the two agree.\n const content = decodeStyleReferences(\n fillEmptyParagraphs(convertMergeTagsToValues(block.content)),\n );\n const cssClassAttr = getCssClassAttr(block, [\n RICH_TEXT_CSS_CLASS,\n richTextGapClass(resolveParagraphGap(block.paragraphSpacing)),\n ]);\n\n // RTL templates need a start-edge default on the wrapper. Inner\n // `<p style=\"text-align:…\">` from TipTap still wins per paragraph.\n const alignAttr =\n context.contentDirection === \"rtl\" ? '\\n align=\"right\"' : \"\";\n\n return `<mj-text\n line-height=\"1.5\"${alignAttr}\n padding=\"${padding}\"${bgColor}${cssClassAttr}\n>${content}</mj-text>`;\n}\n","import type { ImageBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport {\n bgAttr,\n borderAttr,\n borderRadiusAttr,\n fluidOnMobileAttr,\n heightAttr,\n} from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render an image block to MJML markup.\n */\nexport function renderImage(block: ImageBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.src === \"\") {\n // Skip rather than emit `<mj-image src=\"\">` which compiles to a\n // broken-image icon. Mirrors video.ts behavior when no source is set.\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const width =\n block.width === \"full\" ? context.containerWidth + \"px\" : block.width + \"px\";\n const height = heightAttr(block.height);\n const fluid = fluidOnMobileAttr(block, context);\n\n const visibilityAttr = getCssClassAttr(block);\n\n const borderRadiusAttrStr = borderRadiusAttr(block.borderRadius);\n const borderAttrStr = borderAttr(block.border);\n\n let linkAttr = \"\";\n if (block.linkUrl) {\n linkAttr = ` href=\"${escapeAttr(block.linkUrl)}\"`;\n if (block.linkOpenInNewTab) {\n linkAttr += ' target=\"_blank\" rel=\"noopener\"';\n }\n }\n\n const src = escapeAttr(block.src);\n const decorative = block.decorative === true;\n const alt = decorative ? \"\" : escapeAttr(block.alt);\n const align = block.align;\n const roleAttr = decorative ? ' role=\"presentation\"' : \"\";\n\n return `<mj-image\n src=\"${src}\"\n alt=\"${alt}\"\n width=\"${width}\"${height}${fluid}\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${borderAttrStr}${borderRadiusAttrStr}${linkAttr}${visibilityAttr}${roleAttr}\n/>`;\n}\n","import type { ButtonBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr, escapeFontFamily, escapeHtml } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport type { BorderRadiusValue } from \"@templatical/types\";\nimport { toBorderRadiusCss } from \"@templatical/types\";\nimport { bgAttr, borderAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Marks the `mj-button` of a full-width button. MJML gives the button's `<a>`\n * a width only for a px `width`; for `100%` it widens the `<table>` and leaves\n * the `<a>` at `display:inline-block`, so only the label is clickable.\n * `renderToMjml` scopes the head rule that makes the `<a>` fill the button to\n * this class.\n */\nexport const BUTTON_FULL_WIDTH_CSS_CLASS = \"tpl-button-full\";\n\n/**\n * Render a button block to MJML markup.\n */\nexport function renderButton(\n block: ButtonBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const buttonPadding = toPaddingString(block.buttonPadding);\n\n // Omit href entirely when no URL is set so we don't emit a clickable\n // `<a href=\"\">` that navigates to whatever URL the email is opened from.\n const href = block.url === \"\" ? \"\" : escapeAttr(block.url);\n const hrefAttr = href === \"\" ? \"\" : ` href=\"${href}\"`;\n const backgroundColor = buttonBackgroundAttr(block.backgroundColor);\n const textColor = escapeAttr(block.textColor);\n const fontSize = block.fontSize;\n const borderRadius = renderButtonRadius(block.borderRadius);\n const text = escapeHtml(block.text);\n const targetAttr = block.openInNewTab\n ? ' target=\"_blank\" rel=\"noopener\"'\n : \"\";\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const widthAttr = renderWidthAttr(block.width);\n const cssClassAttr = getCssClassAttr(\n block,\n block.width === \"full\" ? [BUTTON_FULL_WIDTH_CSS_CLASS] : [],\n );\n const borderAttrStr = borderAttr(block.border);\n // Templates stored before `align` existed have no value for it. Fall back to\n // MJML's own default so they keep rendering exactly as they did.\n const align = block.align ?? \"center\";\n\n return `<mj-button${hrefAttr}${targetAttr}\n background-color=\"${backgroundColor}\"\n color=\"${textColor}\"\n font-size=\"${fontSize}px\"\n font-weight=\"bold\"\n border-radius=\"${borderRadius}\"${borderAttrStr}\n inner-padding=\"${buttonPadding}\"\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${widthAttr}${cssClassAttr}\n>${text}</mj-button>`;\n}\n\n/**\n * MJML paints `#414141` when `background-color` is omitted, and `\"\"` is not a\n * color it accepts. An unset button fill is the keyword `transparent`.\n */\nfunction buttonBackgroundAttr(color: string): string {\n if (color.trim() === \"\") return \"transparent\";\n return escapeAttr(color);\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${escapeFontFamily(resolved)}\"`;\n}\n\n/**\n * The button has always emitted its radius, `0px` included, so a plain number\n * keeps rendering exactly as it did. Per-corner radii go through the shared\n * formatter, with all-square corners as `0px`.\n */\nfunction renderButtonRadius(radius: BorderRadiusValue): string {\n if (typeof radius === \"number\") {\n return `${radius}px`;\n }\n\n return toBorderRadiusCss(radius) ?? \"0px\";\n}\n\nfunction renderWidthAttr(width: number | \"full\" | undefined): string {\n if (width === undefined) {\n return \"\";\n }\n\n const value = width === \"full\" ? \"100%\" : `${width}px`;\n\n return ` width=\"${value}\"`;\n}\n","import type { DividerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a divider block to MJML markup.\n */\nexport function renderDivider(\n block: DividerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n // A percentage passes through as is: MJML takes it natively.\n const width =\n block.width === \"full\"\n ? \"100%\"\n : typeof block.width === \"number\"\n ? `${block.width}px`\n : block.width;\n const thickness = block.thickness;\n const lineStyle = block.lineStyle;\n const color = escapeAttr(block.color);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-divider\n border-width=\"${thickness}px\"\n border-style=\"${lineStyle}\"\n border-color=\"${color}\"\n width=\"${width}\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\n/>`;\n}\n","import type { SpacerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a spacer block to MJML markup.\n *\n * The canvas renders a spacer at exactly `block.height` pixels and ignores\n * `block.styles.padding`. Match that here: emit `padding=\"0\"` so the\n * exported email's spacer occupies the same vertical space the user saw\n * in the editor preview. Any non-zero `block.styles.padding` on a spacer\n * is meaningless and silently dropped from the export.\n */\nexport function renderSpacer(\n block: SpacerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const height = block.height;\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-spacer height=\"${height}px\" padding=\"0\"${bgColor}${visibilityAttr} />`;\n}\n","import type { HtmlBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render an HTML block to MJML markup.\n * No sanitization in the OSS version -- consumers are responsible for content safety.\n */\nexport function renderHtml(block: HtmlBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (!context.allowHtmlBlocks) {\n return \"\";\n }\n\n const content = block.content;\n\n if (content === \"\") {\n return \"\";\n }\n\n const visibilityAttr = getCssClassAttr(block);\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n\n // Use mj-text to render HTML content inside proper table cell structure.\n // mj-raw bypasses MJML's table layout and content won't be visible.\n return `<mj-text padding=\"${padding}\"${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { SocialIconsBlock } from \"@templatical/types\";\nimport { socialIconAssetDir } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a social icons block to MJML markup.\n *\n * Icons are emitted as `<img src=\"…/{dir}/{platform}.png\">` rather than\n * inline SVG or base64 data URIs. Outlook desktop (Word rendering engine)\n * does not support SVG and rejects base64 in `<img src>`, so hosted PNGs are\n * the only format that renders across every mainstream client. The base URL\n * is read from `context.socialIconsBaseUrl` (configurable via\n * `RenderOptions.socialIconsBaseUrl`; default is the version-pinned jsDelivr\n * mirror of this package). `{dir}` is the style, plus `-{tone}` for a\n * one-color tone (`socialIconAssetDir`).\n */\nexport function renderSocialIcons(\n block: SocialIconsBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const icons = block.icons;\n\n if (icons.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n const align = block.align;\n const iconSize = block.iconSize;\n const iconStyle = block.iconStyle;\n const assetDir = socialIconAssetDir(iconStyle, block.iconTone);\n const spacing = block.spacing;\n\n let iconSizePx: number;\n switch (iconSize) {\n case \"small\":\n iconSizePx = 24;\n break;\n case \"large\":\n iconSizePx = 48;\n break;\n default:\n iconSizePx = 32;\n break;\n }\n\n // MJML mj-social-element has default border-radius of 3px, override per style\n let borderRadius: string;\n switch (iconStyle) {\n case \"circle\":\n borderRadius = \"50%\";\n break;\n case \"rounded\":\n borderRadius = \"8px\";\n break;\n case \"square\":\n borderRadius = \"0\";\n break;\n default:\n borderRadius = \"4px\"; // solid, outlined, plain\n break;\n }\n\n const iconCount = icons.length;\n const socialElements = icons.map((icon, index) => {\n const platform = icon.platform;\n const url = escapeAttr(icon.url);\n const iconSrc = `${context.socialIconsBaseUrl}/${assetDir}/${platform}.png`;\n\n // Apply spacing as right padding only (except last icon) to match CSS gap behavior\n const rightPad = index === iconCount - 1 ? 0 : spacing;\n\n // Empty content hides the platform name text, matching the editor display\n return `<mj-social-element src=\"${iconSrc}\" href=\"${url}\" icon-size=\"${iconSizePx}px\" padding=\"0 ${rightPad}px 0 0\" border-radius=\"${borderRadius}\" background-color=\"transparent\"></mj-social-element>`;\n });\n\n const socialContent = socialElements.join(\"\\n\");\n\n return `<mj-social\n mode=\"horizontal\"\n align=\"${align}\"\n icon-padding=\"0\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\n>\n${socialContent}\n</mj-social>`;\n}\n","import type { MenuBlock, MenuItemData } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport {\n escapeHtml,\n escapeAttr,\n escapeCssValue,\n escapeFontFamily,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a menu block to MJML markup.\n * Uses mj-text with inline <a> tags separated by styled <span> separators.\n */\nexport function renderMenu(block: MenuBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.items.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const align = block.textAlign;\n const fontSize = block.fontSize;\n // Emit the color only when set; otherwise the `mj-text` inherits the\n // document `textColor` and the item anchors below inherit it in turn.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n\n const content = renderMenuItems(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${content}</mj-text>`;\n}\n\nfunction renderMenuItems(block: MenuBlock): string {\n const items = block.items;\n const separator = escapeHtml(block.separator);\n const separatorColor = escapeCssValue(block.separatorColor);\n const spacing = block.spacing;\n const linkColor = block.linkColor ?? block.color;\n\n const parts: string[] = [];\n const itemCount = items.length;\n\n for (let index = 0; index < itemCount; index++) {\n parts.push(renderMenuItem(items[index], linkColor));\n\n if (index < itemCount - 1) {\n parts.push(\n `<span style=\"color: ${separatorColor}; padding: 0 ${spacing}px;\">${separator}</span>`,\n );\n }\n }\n\n return parts.join(\"\");\n}\n\nfunction renderMenuItem(\n item: MenuItemData,\n linkColor: string | undefined,\n): string {\n const text = escapeHtml(item.text);\n const url = escapeAttr(item.url);\n // Item color, else the menu link color; when neither is set the anchor omits\n // an explicit color and inherits the mj-text color (the document `textColor`).\n const resolvedColor = item.color ?? linkColor;\n const target = item.openInNewTab ? ' target=\"_blank\" rel=\"noopener\"' : \"\";\n\n const styles: string[] = [\"text-decoration: none\"];\n\n if (resolvedColor) {\n styles.unshift(`color: ${escapeCssValue(resolvedColor)}`);\n }\n\n if (item.bold) {\n styles.push(\"font-weight: bold\");\n }\n\n if (item.underline) {\n styles.push(\"text-decoration: underline\");\n }\n\n const styleAttr = styles.join(\"; \");\n\n return `<a href=\"${url}\" style=\"${styleAttr}\"${target}>${text}</a>`;\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${escapeFontFamily(resolved)}\"`;\n}\n","import type {\n TableBlock,\n TableRowData,\n TableCellData,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport {\n escapeAttr,\n escapeCssValue,\n escapeFontFamily,\n escapeHtml,\n convertMergeTagsToValues,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a table block to MJML markup.\n * Uses mj-text wrapping an HTML <table> with styled <tr>/<td> elements.\n */\nexport function renderTable(block: TableBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.rows.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const fontSize = block.fontSize;\n // Emit the color only when set; otherwise the `mj-text` (and its cells,\n // which carry no color of their own) inherit the document `textColor`.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n const align = block.textAlign;\n\n const tableHtml = renderTableElement(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${tableHtml}</mj-text>`;\n}\n\nfunction renderTableElement(block: TableBlock): string {\n const borderColor = escapeCssValue(block.borderColor);\n const borderWidth = block.borderWidth;\n\n const tableStyle = \"width: 100%; border-collapse: collapse;\";\n\n let rowsHtml = \"\";\n\n for (let index = 0; index < block.rows.length; index++) {\n const row = block.rows[index];\n const isHeader = block.hasHeaderRow && index === 0;\n rowsHtml += renderRow(row, block, isHeader, borderColor, borderWidth);\n }\n\n return `<table style=\"${tableStyle}\">${rowsHtml}</table>`;\n}\n\nfunction renderRow(\n row: TableRowData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n let cellsHtml = \"\";\n\n for (const cell of row.cells) {\n cellsHtml += renderCell(cell, block, isHeader, borderColor, borderWidth);\n }\n\n return `<tr>${cellsHtml}</tr>`;\n}\n\nfunction renderCell(\n cell: TableCellData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n const cellPadding = block.cellPadding;\n\n const styles: string[] = [\n `border: ${borderWidth}px solid ${borderColor}`,\n `padding: ${cellPadding}px`,\n ];\n\n if (isHeader) {\n styles.push(\"font-weight: bold\");\n\n if (block.headerBackgroundColor) {\n styles.push(\n `background-color: ${escapeCssValue(block.headerBackgroundColor)}`,\n );\n }\n }\n\n const styleAttr = styles.join(\"; \");\n // Cells are a plain-text field (the editor stores innerText; TableCellData\n // carries no rich-text hint), so escape the content like menu/button. Merge\n // tags are resolved first: convertMergeTagsToValues matches a literal\n // `<span`, so escaping has to run after it — escaping first would turn the\n // span into `<span` and the `{{value}}` placeholder would never resolve.\n const content = escapeHtml(convertMergeTagsToValues(cell.content));\n\n const tag = isHeader ? \"th\" : \"td\";\n\n return `<${tag} style=\"${styleAttr}\">${content}</${tag}>`;\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${escapeFontFamily(resolved)}\"`;\n}\n","import type { CustomBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a custom block to MJML markup.\n *\n * Custom block HTML resolution order:\n * 1. `context.customBlockHtml` map — populated by `renderToMjml` when the\n * caller passes `customBlocks` definitions or a `renderCustomBlock`\n * callback (the editor's path).\n * 2. `block.renderedHtml` — populated by an external pre-render step\n * (e.g., a previous render pass that mutated the block).\n * 3. Empty — block omitted from output.\n */\nexport function renderCustom(\n block: CustomBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const fromContext = context.customBlockHtml.get(block.id);\n const content = fromContext ?? block.renderedHtml;\n\n if (!content || content === \"\") {\n return \"\";\n }\n\n const visibilityAttr = getCssClassAttr(block);\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n\n return `<mj-text padding=\"${padding}\"${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { ColumnLayout } from \"@templatical/types\";\n\n/**\n * Get width percentages for each column in a layout.\n */\nexport function getWidthPercentages(layout: ColumnLayout): string[] {\n switch (layout) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n // 33.33 × 3 = 99.99% — let the last column absorb the rounding gap\n // so widths sum to exactly 100% in the compiled HTML.\n return [\"33.33%\", \"33.33%\", \"33.34%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n}\n\n/**\n * Get width in pixels for each column in a layout.\n */\nexport function getWidthPixels(\n layout: ColumnLayout,\n containerWidth: number,\n): number[] {\n switch (layout) {\n case \"2\":\n return [containerWidth * 0.5, containerWidth * 0.5];\n case \"3\":\n return [containerWidth / 3, containerWidth / 3, containerWidth / 3];\n case \"1-2\":\n return [containerWidth / 3, (containerWidth * 2) / 3];\n case \"2-1\":\n return [(containerWidth * 2) / 3, containerWidth / 3];\n default:\n return [containerWidth];\n }\n}\n","import type { AppliedHero, SpacingValue } from \"@templatical/types\";\n\n/** The sizing attributes of an `mj-hero`. */\nexport interface HeroGeometry {\n mode: \"fixed-height\";\n /** Total height in px, padding included. */\n height: number;\n backgroundWidth: number;\n backgroundHeight: number;\n}\n\n/**\n * The `mj-hero` sizing for a hero in a box `boxWidth` px wide. Every hero's\n * mode, height and image size come from here, so an Outlook rendering\n * finding changes this function and nothing else.\n *\n * Every hero is `fixed-height`: classic Outlook on Windows ignores\n * `fluid-height` (its spacer cells carry `mso-padding-bottom-alt:0`) and\n * shrinks the box to its content, showing only the top of the image.\n *\n * - `height: \"image\"` → the image's own height at the box width, so the box\n * matches the image on desktop. A phone keeps that height and crops the\n * image's sides to cover the narrower box.\n * - a number → that height.\n *\n * Either is at least the top and bottom `padding`, each a whole pixel as MJML\n * reads it: MJML sizes the cell as the height less that padding and would\n * write a negative height for a shorter box.\n *\n * Outlook draws the image as a `v:image` of exactly `backgroundWidth ×\n * backgroundHeight` px from the box's top, clipped to the box, so the width is\n * always the box's and the height is the image's own at that width. An image\n * with no known size takes the fixed box's height, which fills the box and\n * stretches the image to it.\n */\nexport function heroGeometry(\n hero: AppliedHero,\n boxWidth: number,\n padding: SpacingValue,\n): HeroGeometry {\n const backgroundWidth = Math.max(1, Math.round(boxWidth));\n const imageHeight =\n hero.aspectRatio === null\n ? null\n : Math.max(1, Math.round(backgroundWidth * hero.aspectRatio));\n\n // resolveHero applies \"image\" only with a known ratio.\n const wanted =\n hero.height === \"image\" ? (imageHeight ?? backgroundWidth) : hero.height;\n const height = Math.max(\n wanted,\n Math.trunc(padding.top) + Math.trunc(padding.bottom),\n );\n\n return {\n mode: \"fixed-height\",\n height,\n backgroundWidth,\n backgroundHeight: imageHeight ?? height,\n };\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Wrap rendered block markup in the block's liquid display-condition guards\n * (`<mj-raw>{% if %}</mj-raw>` … `<mj-raw>{% endif %}</mj-raw>`), if present.\n *\n * Returns the input unchanged when the block has no display condition, and an\n * empty string when the rendered markup is empty (a hidden block) so callers\n * can keep using an `=== \"\"` filter to drop it.\n *\n * Used for BOTH top-level blocks (`index.ts`) and blocks nested inside section\n * columns (`renderers/section.ts`). A condition on a nested block must emit the\n * same guards as a top-level one — otherwise conditional content placed inside\n * a multi-column section renders unconditionally for every recipient.\n */\nexport function wrapWithDisplayCondition(\n block: Block,\n rendered: string,\n): string {\n if (rendered === \"\") {\n return \"\";\n }\n\n const displayCondition = block.displayCondition;\n\n if (!displayCondition) {\n return rendered;\n }\n\n return (\n `<mj-raw>${displayCondition.before}</mj-raw>` +\n \"\\n\" +\n rendered +\n \"\\n\" +\n `<mj-raw>${displayCondition.after}</mj-raw>`\n );\n}\n","import type { AppliedHero, Block, SectionBlock } from \"@templatical/types\";\nimport { isSection, resolveHero } from \"@templatical/types\";\nimport type { BlockRenderer, RenderContext } from \"../render-context\";\nimport { getWidthPercentages, getWidthPixels } from \"../columns\";\nimport { escapeAttr } from \"../escape\";\nimport { heroGeometry } from \"../hero-geometry\";\nimport { toPaddingString } from \"../padding\";\nimport {\n backgroundImageAttrs,\n bgAttr,\n borderAttr,\n borderRadiusAttr,\n horizontalInset,\n} from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\nimport { wrapWithDisplayCondition } from \"../display-condition\";\n\n// `BlockRenderer` is defined in `../render-context`, next to the `blockRenderers`\n// override map it types. Re-exported here so the package's\n// `export type { BlockRenderer } from \"./renderers/section\"` entry point resolves\n// for consumers.\nexport type { BlockRenderer };\n\n/**\n * Render a section block with columns to MJML markup.\n */\nexport function renderSection(\n block: SectionBlock,\n context: RenderContext,\n renderBlock: BlockRenderer,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const hero = resolveHero(block);\n if (hero?.applied) {\n return renderHero(block, hero, context, renderBlock);\n }\n\n const columnsLayout = block.columns;\n const columnWidths = getWidthPercentages(columnsLayout);\n const columnWidthsPx = getWidthPixels(columnsLayout, context.containerWidth);\n // What MJML gives each column's content: the emitted percentage of the room\n // inside the section's padding and border, floored as MJML floors it. From\n // the percentage strings, not `columnWidthsPx`: 33.33% of 600 is 199, a\n // third is 200.\n const sectionBox =\n Math.floor(context.contentWidth) -\n horizontalInset(block.styles.padding, block.border);\n const columnContentWidths = columnWidths.map((width) =>\n Math.floor((sectionBox * parseFloat(width)) / 100),\n );\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"native\");\n const bgImage = backgroundImageAttrs(block.backgroundImage);\n const visibilityAttr = getCssClassAttr(block);\n const borderRadiusAttrStr = borderRadiusAttr(block.borderRadius);\n const borderAttrStr = borderAttr(block.border);\n // MJML's `mj-section`, `mj-group` and `mj-column` each default to\n // `direction=\"ltr\"` and inline it on their own element, so an RTL email\n // needs it on all three — the column's inline style otherwise resets the\n // text inside to LTR under a section that reverses the column order.\n const dirAttr = context.contentDirection === \"rtl\" ? ' direction=\"rtl\"' : \"\";\n\n const children = block.children;\n const columnsContent: string[] = [];\n\n for (let index = 0; index < children.length; index++) {\n const column = children[index];\n const width = columnWidths[index] ?? \"100%\";\n const columnWidth = Math.floor(\n columnWidthsPx[index] ?? context.containerWidth,\n );\n\n // Sections cannot be nested inside columns per MJML spec — drop any\n // SectionBlocks that landed inside a column (via tampered JSON or a\n // future API). Keeping them would emit `<mj-section>` inside\n // `<mj-column>`, which mjml@5 rejects with a hard error.\n const filteredColumn = filterHtmlBlocks(\n column,\n context.allowHtmlBlocks,\n ).filter((child) => !isSection(child));\n const columnContext = context.withContainerWidth(\n columnWidth,\n columnContentWidths[index] ?? sectionBox,\n );\n\n const columnBlocks = filteredColumn\n .map((child) =>\n wrapWithDisplayCondition(child, renderBlock(child, columnContext)),\n )\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n const content =\n columnBlocks === \"\" ? \"<mj-text> </mj-text>\" : columnBlocks;\n\n columnsContent.push(`<mj-column width=\"${width}\"${dirAttr}>\n${content}\n</mj-column>`);\n }\n\n const columns = columnsContent.join(\"\\n\");\n\n // Opting out of mobile stacking wraps the columns in an `<mj-group>`, which\n // keeps them side-by-side below 480px instead of stacking (MJML's default).\n // `mj-group` requires percentage column widths — `getWidthPercentages`\n // already guarantees that. Only meaningful with 2+ columns.\n const body =\n block.stackOnMobile === false && columnsContent.length > 1\n ? `<mj-group${dirAttr}>\n${columns}\n</mj-group>`\n : columns;\n\n return `<mj-section${bgColor}${bgImage} padding=\"${padding}\"${borderAttrStr}${borderRadiusAttrStr}${visibilityAttr}${dirAttr}>\n${body}\n</mj-section>`;\n}\n\n/**\n * Render a section `resolveHero` applies as an `<mj-hero>`. The content is\n * column 0's blocks, filtered as a column's are; mj-hero takes content\n * elements directly, with no `mj-column`.\n *\n * The hero's box is the room its bands leave it, `context.contentWidth`\n * floored as MJML floors every band; Outlook draws the image at exactly that\n * width. Its content renders `\"full\"` at the box and gets the box less the\n * hero's own side padding as its room, as mj-hero gives it: no border comes\n * off, since a hero draws none.\n *\n * Left out on purpose: `border` (mj-hero has none), `background-size` and\n * `background-repeat` (it always paints `cover`, unrepeated) and `direction`\n * (it inlines none, so its text keeps the body's `dir`). An unset fill is\n * `transparent`, since mj-hero's own default is white.\n */\nfunction renderHero(\n block: SectionBlock,\n hero: AppliedHero,\n context: RenderContext,\n renderBlock: BlockRenderer,\n): string {\n const boxWidth = Math.floor(context.contentWidth);\n const geometry = heroGeometry(hero, boxWidth, block.styles.padding);\n const bgColor =\n bgAttr(block.styles.backgroundColor, \"native\") ||\n ' background-color=\"transparent\"';\n const padding = toPaddingString(block.styles.padding);\n const borderRadiusAttrStr = borderRadiusAttr(block.borderRadius);\n const visibilityAttr = getCssClassAttr(block);\n const contentContext = context.withContainerWidth(\n boxWidth,\n boxWidth - horizontalInset(block.styles.padding),\n );\n\n const content = filterHtmlBlocks(\n block.children[0] ?? [],\n context.allowHtmlBlocks,\n )\n .filter((child) => !isSection(child))\n .map((child) =>\n wrapWithDisplayCondition(child, renderBlock(child, contentContext)),\n )\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n return `<mj-hero mode=\"${geometry.mode}\" height=\"${geometry.height}px\" background-url=\"${escapeAttr(hero.image.url)}\" background-width=\"${geometry.backgroundWidth}px\" background-height=\"${geometry.backgroundHeight}px\" background-position=\"${hero.image.position}\"${bgColor} padding=\"${padding}\" vertical-align=\"${hero.verticalAlign}\"${borderRadiusAttrStr}${visibilityAttr}>\n${content}\n</mj-hero>`;\n}\n\n/**\n * Filter out HTML blocks if they are not allowed.\n */\nfunction filterHtmlBlocks(blocks: Block[], allowHtmlBlocks: boolean): Block[] {\n if (allowHtmlBlocks) {\n return blocks;\n }\n\n return blocks.filter((block) => block.type !== \"html\");\n}\n","import type { VideoBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr, fluidOnMobileAttr, heightAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Extract video thumbnail URL from common platforms.\n * Works without server-side processing — YouTube and Vimeo thumbnails are publicly accessible.\n */\nfunction getVideoThumbnail(\n url: string,\n customThumbnail?: string,\n): string | null {\n if (customThumbnail) {\n return customThumbnail;\n }\n\n if (!url) {\n return null;\n }\n\n // YouTube\n const youtubePatterns = [\n /(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/|youtube\\.com\\/embed\\/)([a-zA-Z0-9_-]{11})/,\n /youtube\\.com\\/shorts\\/([a-zA-Z0-9_-]{11})/,\n ];\n\n for (const pattern of youtubePatterns) {\n const match = url.match(pattern);\n if (match) {\n return `https://img.youtube.com/vi/${match[1]}/maxresdefault.jpg`;\n }\n }\n\n // Vimeo\n const vimeoMatch = url.match(/vimeo\\.com\\/(?:video\\/)?(\\d+)/);\n if (vimeoMatch) {\n return `https://vumbnail.com/${vimeoMatch[1]}.jpg`;\n }\n\n return null;\n}\n\n/**\n * Render a video block to MJML markup.\n * Videos in email are rendered as a linked thumbnail image pointing to the video URL.\n */\nexport function renderVideo(block: VideoBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const thumbnailUrl = getVideoThumbnail(block.url, block.thumbnailUrl);\n\n if (!thumbnailUrl) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const width =\n block.width === \"full\" ? context.containerWidth + \"px\" : block.width + \"px\";\n const height = heightAttr(block.height);\n const fluid = fluidOnMobileAttr(block, context);\n const visibilityAttr = getCssClassAttr(block);\n\n const src = escapeAttr(thumbnailUrl);\n const alt = escapeAttr(block.alt);\n const align = block.align;\n const href = escapeAttr(block.url);\n\n return `<mj-image\n src=\"${src}\"\n alt=\"${alt}\"\n width=\"${width}\"${height}${fluid}\n align=\"${align}\"\n padding=\"${padding}\"\n href=\"${href}\"\n target=\"_blank\"\n rel=\"noopener\"${bgColor}${visibilityAttr}\n/>`;\n}\n","import type { Block } from \"@templatical/types\";\nimport {\n isSection,\n isTitle,\n isParagraph,\n isImage,\n isButton,\n isDivider,\n isSpacer,\n isHtml,\n isSocialIcons,\n isMenu,\n isTable,\n isVideo,\n isCustomBlock,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { isHiddenOnAll } from \"../visibility\";\nimport { renderUnrenderableBlock } from \"../unrenderable\";\nimport { renderTitle } from \"./title\";\nimport { renderParagraph } from \"./paragraph\";\nimport { renderImage } from \"./image\";\nimport { renderButton } from \"./button\";\nimport { renderDivider } from \"./divider\";\nimport { renderSpacer } from \"./spacer\";\nimport { renderHtml } from \"./html\";\nimport { renderSocialIcons } from \"./social\";\nimport { renderMenu } from \"./menu\";\nimport { renderTable } from \"./table\";\nimport { renderCustom } from \"./custom\";\nimport { renderSection } from \"./section\";\nimport { renderVideo } from \"./video\";\n\n/**\n * Render a single block to MJML markup.\n * Dispatches to the appropriate block-type renderer.\n *\n * A `blockRenderers` override for the block's type wins over the built-in\n * renderer — see {@link RenderContext.blockRenderers}. A type with neither gets a\n * placeholder marker plus a warning rather than disappearing.\n */\nexport function renderBlock(block: Block, context: RenderContext): string {\n // Checked first so an override replaces the built-in wholesale rather than\n // running after it. The override then owns everything the built-in did,\n // including the hidden-on-all-viewports early return.\n const override = context.blockRenderers[block.type];\n if (override) {\n return override(block, context);\n }\n\n if (isSection(block)) {\n return renderSection(block, context, renderBlock);\n }\n\n if (isTitle(block)) {\n return renderTitle(block, context);\n }\n\n if (isParagraph(block)) {\n return renderParagraph(block, context);\n }\n\n if (isImage(block)) {\n return renderImage(block, context);\n }\n\n if (isButton(block)) {\n return renderButton(block, context);\n }\n\n if (isDivider(block)) {\n return renderDivider(block, context);\n }\n\n if (isSpacer(block)) {\n return renderSpacer(block, context);\n }\n\n if (isHtml(block)) {\n return renderHtml(block, context);\n }\n\n if (isSocialIcons(block)) {\n return renderSocialIcons(block, context);\n }\n\n if (isMenu(block)) {\n return renderMenu(block, context);\n }\n\n if (isTable(block)) {\n return renderTable(block, context);\n }\n\n if (isVideo(block)) {\n return renderVideo(block, context);\n }\n\n if (isCustomBlock(block)) {\n return renderCustom(block, context);\n }\n\n // Nothing here knows how to render this type. A block the author hid on every\n // viewport is meant to produce nothing, so it still does — every built-in\n // renderer above makes the same call, and warning about a deliberate hide would\n // be noise. Everything else gets a marker: `countdown` today (Cloud renders it\n // server-side as an animated GIF, which no browser can do), and any future type\n // whose renderer hasn't landed yet.\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n return renderUnrenderableBlock(block);\n}\n","import type {\n Block,\n ContentDirection,\n CustomBlock,\n CustomBlockDefinition,\n TemplateContent,\n CustomFont,\n SectionWrapper,\n SpacingValue,\n} from \"@templatical/types\";\nimport {\n applyLayout,\n defaultLocaleOf,\n assertNoSlotInContent,\n assertNoWrapperInContent,\n createCustomBlockLiquid,\n isCustomBlock,\n isSection,\n isWrapper,\n resolveContentDirection,\n resolveLocale,\n} from \"@templatical/types\";\nimport { RenderContext, DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nimport type { BlockRendererMap } from \"./render-context\";\nimport { renderBlock } from \"./renderers\";\nimport {\n escapeHtml,\n escapeAttr,\n escapeCssValue,\n escapeFontFamily,\n} from \"./escape\";\nimport { wrapWithDisplayCondition } from \"./display-condition\";\nimport {\n backgroundImageAttrs,\n bgAttr,\n borderRadiusAttr,\n horizontalInset,\n} from \"./utils\";\nimport { richTextStylesheet, collectParagraphGaps } from \"./rich-text\";\nimport { BUTTON_FULL_WIDTH_CSS_CLASS } from \"./renderers/button\";\nimport { toPaddingString } from \"./padding\";\n\nexport interface RenderOptions {\n /** Template language to render. Defaults to `content.settings.locale`. */\n locale?: string;\n customFonts?: CustomFont[];\n defaultFallbackFont?: string;\n allowHtmlBlocks?: boolean;\n /** Headless custom block definitions. The last definition per type wins. */\n customBlocks?: CustomBlockDefinition[];\n /**\n * Resolves custom blocks to their HTML representation. Called once per\n * custom block in the content tree before MJML rendering. The renderer\n * accepts `customBlocks` for headless rendering; this callback is the\n * editor's integration path.\n *\n * Editor consumers: pass `editor.renderCustomBlock`.\n *\n * With neither this callback nor `customBlocks`, each custom block uses\n * `block.renderedHtml` (if present) and is otherwise omitted.\n */\n renderCustomBlock?: (block: CustomBlock) => Promise<string>;\n /**\n * Per-block-type renderer overrides, keyed by `block.type`. An entry replaces\n * the built-in renderer for that type wholesale — including its\n * hidden-on-all-viewports check, which becomes the override's responsibility.\n *\n * ```ts\n * renderToMjml(content, {\n * blockRenderers: {\n * countdown: (block) => `<mj-image src=\"${countdownGifUrl(block)}\" />`,\n * video: (block, ctx) => renderVideoWithPlayButton(block, ctx),\n * },\n * });\n * ```\n *\n * This generalises {@link renderCustomBlock}, which is the same idea for a\n * single block type. It exists so a backend whose render output is a *superset*\n * of the browser's — a server-generated countdown GIF, a composited video\n * thumbnail — can inject exactly that delta instead of forking the renderer:\n * parity for every other block type then holds by construction.\n *\n * A block type with neither a built-in renderer nor an override emits an\n * `mj-raw` placeholder comment and warns, rather than vanishing from the\n * output.\n */\n blockRenderers?: BlockRendererMap;\n /**\n * Resolves the definition-level CSS for a custom block type. Called once\n * per unique `customType` present in the content tree (not per instance).\n * The non-empty results are deduped by content and emitted as additional\n * `<mj-style>` blocks inside `<mj-head>` alongside the built-in visibility\n * media queries.\n *\n * Editor consumers: pass a function that reads\n * `blockRegistry.getDefinition(customType)?.stylesheet`.\n *\n * Headless consumers can pass `customBlocks` instead. A listed definition\n * decides its type's CSS; no stylesheet means none is emitted.\n *\n * Without a resolver or matching definition, each type's saved\n * `block.renderedStylesheet` is emitted. A resolver decides every type and\n * ignores saved copies, so a stale one never overrides a live definition.\n */\n getCustomBlockStylesheet?: (customType: string) => string | undefined | null;\n /**\n * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png` per icon. Defaults to the\n * version-pinned jsDelivr mirror of this package. Override to self-host\n * (e.g., behind your own CDN or for air-gapped environments).\n *\n * Why PNGs: Outlook desktop (Word rendering engine) does not support SVG\n * and rejects base64 data URIs in `<img src>`, so social icons must be\n * served as raster images over HTTP for cross-client compatibility.\n */\n socialIconsBaseUrl?: string;\n /**\n * Embedder shell applied at render. Cloned and spliced around `content`\n * via `applyLayout`; `content` is not mutated. Omitted, the document is\n * rendered as authored and must not contain `slot` or `wrapper`.\n */\n layout?: TemplateContent;\n}\n\n/**\n * Makes a full-width button's whole fill clickable. MJML sizes a button's `<a>`\n * only for a px width, so without this rule the link of a `100%` button stays\n * the size of its label.\n *\n * It sits in the inline `<mj-style>` so `mjml`'s juice pass writes it onto the\n * `<a>`; MRML's default build emits that block as a plain `<style>` instead.\n * `!important` lets it beat MJML's inline `display:inline-block` either way.\n * Every template carries it, so nothing has to walk the tree for full-width\n * buttons: juice drops a rule that matches nothing, so with `mjml` it costs\n * nothing in the compiled HTML.\n */\nconst BUTTON_FULL_WIDTH_RULE = `.${BUTTON_FULL_WIDTH_CSS_CLASS} a { display: block !important; }`;\n\n/**\n * Render template content to an MJML string.\n *\n * With `customBlocks`, this imports the optional `liquidjs` peer only when\n * the content holds a custom block. The editor uses `renderCustomBlock` via\n * `toMjml()`; headless callers can pass definitions directly. The function\n * always returns a Promise.\n */\nexport async function renderToMjml(\n content: TemplateContent,\n options?: RenderOptions,\n): Promise<string> {\n if (\n options?.customBlocks !== undefined &&\n options.renderCustomBlock !== undefined\n ) {\n throw new TypeError(\n \"[Templatical] customBlocks cannot be combined with renderCustomBlock\",\n );\n }\n if (\n options?.customBlocks !== undefined &&\n options.getCustomBlockStylesheet !== undefined\n ) {\n throw new TypeError(\n \"[Templatical] customBlocks cannot be combined with getCustomBlockStylesheet\",\n );\n }\n\n const locale = options?.locale ?? defaultLocaleOf(content);\n const localized = resolveLocale(content, locale);\n const toRender = options?.layout\n ? applyLayout(\n resolveLocale(options.layout, locale, { missing: \"default\" }),\n localized,\n )\n : (assertNoSlotInContent(localized),\n assertNoWrapperInContent(localized),\n localized);\n\n const customFonts = options?.customFonts ?? [];\n const defaultFallbackFont =\n options?.defaultFallbackFont ?? \"Arial, sans-serif\";\n const allowHtmlBlocks = options?.allowHtmlBlocks ?? true;\n const socialIconsBaseUrl = stripTrailingSlash(\n options?.socialIconsBaseUrl ?? DEFAULT_SOCIAL_ICONS_BASE_URL,\n );\n\n const customBlockHtml =\n options?.customBlocks !== undefined\n ? await resolveCustomBlockDefinitions(toRender, options.customBlocks)\n : await resolveCustomBlocks(toRender, options?.renderCustomBlock);\n\n const customBlockStylesheets = collectCustomBlockStylesheets(\n toRender,\n options?.getCustomBlockStylesheet,\n options?.customBlocks === undefined\n ? undefined\n : new Map(\n options.customBlocks.map((definition) => [\n definition.type,\n definition,\n ]),\n ),\n );\n\n const contentDirection = resolveContentDirection(toRender.settings);\n\n const renderContext = new RenderContext(\n toRender.settings.width,\n customFonts,\n defaultFallbackFont,\n allowHtmlBlocks,\n customBlockHtml,\n socialIconsBaseUrl,\n options?.blockRenderers ?? {},\n contentDirection,\n );\n\n const blocks = filterHtmlBlocks(toRender.blocks, allowHtmlBlocks);\n const fontFamily = renderContext.resolveFontFamily(\n toRender.settings.fontFamily,\n );\n const backgroundColor = toRender.settings.backgroundColor;\n // Document-level default text color → the `<mj-text>` default below. Blocks\n // that set their own color (e.g. Title) or carry inline text-color marks\n // override it. Omitted when unset so existing templates render identically.\n const textColorAttr = toRender.settings.textColor\n ? ` color=\"${escapeAttr(toRender.settings.textColor)}\"`\n : \"\";\n // Document-level link styling → the global `a { … }` rule below, cascading to\n // every link (rich-text and menu). An unset `linkColor` keeps `color: inherit`\n // so links follow the text color and legacy content renders identically; a\n // per-link/per-item color still overrides (inline styles beat this rule).\n const linkColor = toRender.settings.linkColor\n ? escapeCssValue(toRender.settings.linkColor)\n : \"inherit\";\n const linkDecoration = toRender.settings.linkUnderline ? \"underline\" : \"none\";\n\n const bodyContent = blocks\n .map((block) => renderTopLevelBlock(block, renderContext))\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n const fontDeclarations = generateFontDeclarations(customFonts);\n const previewTag = generatePreviewTag(toRender.settings.preheaderText);\n\n const lang = escapeAttr(toRender.settings.locale);\n const dir = escapeAttr(contentDirection);\n\n return `<mjml lang=\"${lang}\" dir=\"${dir}\">\n <mj-head>${previewTag}\n <mj-attributes>\n <mj-all font-family=\"${escapeFontFamily(fontFamily)}\" />\n <mj-text font-size=\"14px\"${textColorAttr} />\n <mj-section padding=\"0\" />\n <mj-column padding=\"0\" />\n </mj-attributes>${fontDeclarations}\n <mj-style inline=\"inline\">\n ${richTextStylesheet(collectParagraphGaps(blocks), contentDirection)}\n ${BUTTON_FULL_WIDTH_RULE}\n </mj-style>\n <mj-style>\n a { color: ${linkColor}; text-decoration: ${linkDecoration}; }\n @media only screen and (max-width: 480px) {\n .tpl-hide-mobile { display: none !important; mso-hide: all !important; }\n }\n @media only screen and (min-width: 481px) {\n .tpl-hide-desktop { display: none !important; mso-hide: all !important; }\n }\n </mj-style>${renderCustomBlockStylesheets(customBlockStylesheets)}\n </mj-head>\n <mj-body width=\"${renderContext.containerWidth}px\" background-color=\"${backgroundColor}\">\n${bodyContent}\n </mj-body>\n</mjml>`;\n}\n\n/**\n * Render a top-level block. Wrappers emit `mj-wrapper` around their children.\n * Sections are rendered directly; other blocks are wrapped in a default\n * section/column.\n */\nfunction renderTopLevelBlock(block: Block, context: RenderContext): string {\n if (isWrapper(block)) {\n const innerContext = insideWrapper(context, block.styles.padding);\n const inner = filterHtmlBlocks(block.children, context.allowHtmlBlocks)\n .map((child) => renderTopLevelBlock(child, innerContext))\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n const framed = renderMjWrapper(\n inner,\n {\n backgroundColor: block.styles.backgroundColor,\n padding: block.styles.padding,\n borderRadius: block.borderRadius,\n backgroundImage: block.backgroundImage,\n },\n context.contentDirection,\n );\n return wrapWithDisplayCondition(block, framed);\n }\n\n if (isSection(block)) {\n const rendered = renderBlock(\n block,\n block.wrapper ? insideWrapper(context, block.wrapper.padding) : context,\n );\n // An empty render (hidden section) stays empty — never emit a bare wrapper.\n const framed =\n block.wrapper && rendered !== \"\"\n ? renderMjWrapper(rendered, block.wrapper, context.contentDirection)\n : rendered;\n return wrapWithDisplayCondition(block, framed);\n }\n\n const content = renderBlock(block, context);\n const wrapped = wrapInSection(content, context.contentDirection);\n return wrapWithDisplayCondition(block, wrapped);\n}\n\n/**\n * The context for what an `mj-wrapper` holds: MJML narrows its children by the\n * wrapper's side padding. `containerWidth` stays the nominal share of the body.\n */\nfunction insideWrapper(\n context: RenderContext,\n padding: SpacingValue | undefined,\n): RenderContext {\n return context.withContainerWidth(\n context.containerWidth,\n Math.floor(context.contentWidth) - horizontalInset(padding),\n );\n}\n\n/**\n * `mj-wrapper` attributes shared by a layout `wrapper` block and\n * `section.wrapper`. Padding is always explicit: MJML's wrapper default is\n * `20px 0`, ours is 0. Direction is explicit for the same reason: MJML's\n * default `ltr` would otherwise be inlined on the band of an RTL email.\n */\nfunction renderMjWrapper(\n inner: string,\n wrapper: SectionWrapper,\n direction: ContentDirection,\n): string {\n const bg = bgAttr(wrapper.backgroundColor, \"native\");\n const bgImage = backgroundImageAttrs(wrapper.backgroundImage);\n const padding = ` padding=\"${\n wrapper.padding ? toPaddingString(wrapper.padding) : \"0\"\n }\"`;\n const radius = borderRadiusAttr(wrapper.borderRadius);\n const dirAttr = direction === \"rtl\" ? ' direction=\"rtl\"' : \"\";\n return `<mj-wrapper${bg}${bgImage}${padding}${radius}${dirAttr}>\n${inner}\n</mj-wrapper>`;\n}\n\n/**\n * Wrap block content in a default mj-section/mj-column for non-section blocks.\n */\nfunction wrapInSection(content: string, direction: ContentDirection): string {\n if (content === \"\") {\n return \"\";\n }\n\n const dirAttr = direction === \"rtl\" ? ' direction=\"rtl\"' : \"\";\n return `<mj-section${dirAttr}>\n <mj-column${dirAttr}>\n${content}\n </mj-column>\n</mj-section>`;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\nfunction generatePreviewTag(preheaderText?: string): string {\n if (!preheaderText) {\n return \"\";\n }\n\n const trimmed = preheaderText.trim();\n\n if (trimmed === \"\") {\n return \"\";\n }\n\n const escaped = escapeHtml(trimmed);\n\n return `\\n <mj-preview>${escaped}</mj-preview>`;\n}\n\nfunction generateFontDeclarations(customFonts: CustomFont[]): string {\n if (customFonts.length === 0) {\n return \"\";\n }\n\n return customFonts\n .map(\n (font) =>\n `\\n <mj-font name=\"${escapeAttr(font.name)}\" href=\"${escapeAttr(font.url)}\" />`,\n )\n .join(\"\");\n}\n\n/**\n * Filter out HTML blocks if they are not allowed.\n */\nfunction filterHtmlBlocks(blocks: Block[], allowHtmlBlocks: boolean): Block[] {\n if (allowHtmlBlocks) {\n return blocks;\n }\n\n return blocks.filter((block) => block.type !== \"html\");\n}\n\n/**\n * Walk the content tree, collect every custom block, then resolve each in\n * parallel via the supplied callback. Returns a map keyed by block id that\n * the synchronous render pass reads from. If no callback is provided, returns\n * an empty map and the sync pass falls back to `block.renderedHtml`.\n *\n * Per-block failures bubble up — the caller decides whether to swallow or\n * rethrow. We don't replace failures with placeholders here because that's\n * a policy decision (the editor swallows; a strict CLI may want to fail).\n */\nasync function resolveCustomBlocks(\n content: TemplateContent,\n renderCustomBlock: RenderOptions[\"renderCustomBlock\"],\n): Promise<Map<string, string>> {\n const result = new Map<string, string>();\n\n if (!renderCustomBlock) {\n return result;\n }\n\n const customBlocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, customBlocks);\n\n if (customBlocks.length === 0) {\n return result;\n }\n\n const rendered = await Promise.all(\n customBlocks.map((block) => renderCustomBlock(block)),\n );\n\n for (let index = 0; index < customBlocks.length; index++) {\n result.set(customBlocks[index].id, rendered[index]);\n }\n\n return result;\n}\n\n/** One Liquid engine serves every definition in this render call. Template\n * errors propagate as callback rejections do. */\nasync function resolveCustomBlockDefinitions(\n content: TemplateContent,\n definitions: CustomBlockDefinition[],\n): Promise<Map<string, string>> {\n const result = new Map<string, string>();\n const blocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, blocks);\n if (blocks.length === 0) return result;\n\n let liquidjs: typeof import(\"liquidjs\");\n try {\n liquidjs = await import(\"liquidjs\");\n } catch {\n throw new Error(\n \"[Templatical] customBlocks requires the optional liquidjs package.\",\n );\n }\n const liquid = createCustomBlockLiquid(liquidjs);\n const byType = new Map(\n definitions.map((definition) => [definition.type, definition]),\n );\n const rendered = await Promise.all(\n blocks.map(async (block) => {\n const definition = byType.get(block.customType);\n return definition\n ? String(\n await liquid.parseAndRender(definition.template, block.fieldValues),\n )\n : undefined;\n }),\n );\n for (let index = 0; index < blocks.length; index++) {\n const html = rendered[index];\n if (html !== undefined) result.set(blocks[index].id, html);\n }\n return result;\n}\n\nfunction collectCustomBlocks(blocks: Block[], out: CustomBlock[]): void {\n for (const block of blocks) {\n if (isCustomBlock(block)) {\n out.push(block);\n continue;\n }\n\n if (isSection(block)) {\n for (const column of block.children) {\n collectCustomBlocks(column, out);\n }\n } else if (isWrapper(block)) {\n collectCustomBlocks(block.children, out);\n }\n }\n}\n\n/**\n * Walk the content tree, find every unique `customType`, ask the consumer's\n * resolver for that definition's stylesheet (or, with no resolver, take the\n * first `renderedStylesheet` a block of that type carries), and return the\n * non-empty, content-deduped set in insertion order.\n *\n * Content-level dedupe (not just by customType) means two definitions that\n * happen to ship the same stylesheet string emit it only once — cheap and\n * matches the \"one rule, emitted once\" mental model. Whitespace-only and\n * empty stylesheets are skipped.\n */\nfunction collectCustomBlockStylesheets(\n content: TemplateContent,\n resolver: RenderOptions[\"getCustomBlockStylesheet\"],\n definitions?: Map<string, CustomBlockDefinition>,\n): string[] {\n const customBlocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, customBlocks);\n\n if (customBlocks.length === 0) {\n return [];\n }\n\n // Without a resolver, the first copy each type carries stands in for it.\n const copies = new Map<string, string>();\n if (!resolver) {\n for (const block of customBlocks) {\n if (block.renderedStylesheet && !copies.has(block.customType)) {\n copies.set(block.customType, block.renderedStylesheet);\n }\n }\n }\n\n const seenTypes = new Set<string>();\n const seenContent = new Set<string>();\n const stylesheets: string[] = [];\n\n for (const block of customBlocks) {\n if (seenTypes.has(block.customType)) {\n continue;\n }\n seenTypes.add(block.customType);\n\n const definition = definitions?.get(block.customType);\n const css = resolver\n ? resolver(block.customType)\n : definition\n ? definition.stylesheet\n : copies.get(block.customType);\n if (!css) {\n continue;\n }\n\n const trimmed = css.trim();\n if (trimmed === \"\" || seenContent.has(trimmed)) {\n continue;\n }\n seenContent.add(trimmed);\n stylesheets.push(trimmed);\n }\n\n return stylesheets;\n}\n\nfunction renderCustomBlockStylesheets(stylesheets: string[]): string {\n if (stylesheets.length === 0) {\n return \"\";\n }\n\n // One `<mj-style>` per unique stylesheet so per-definition CSS can be\n // independently inspected in the rendered MJML, and authors can grep their\n // contribution without disentangling a merged blob.\n return stylesheets\n .map((css) => `\\n <mj-style>\\n${css}\\n </mj-style>`)\n .join(\"\");\n}\n\n// Re-export utilities for consumers\nexport { RenderContext } from \"./render-context\";\nexport { escapeHtml, escapeAttr, convertMergeTagsToValues } from \"./escape\";\nexport { isHiddenOnAll, getCssClassAttr, getCssClasses } from \"./visibility\";\n// Exported for `blockRenderers` consumers replacing the paragraph or title\n// renderer: without both classes on their `mj-text`, their block silently\n// loses the spacing rules and drifts back to MJML's 13px.\nexport {\n RICH_TEXT_CSS_CLASS,\n richTextGapClass,\n resolveParagraphGap,\n} from \"./rich-text\";\n// Exported for `blockRenderers` consumers replacing the button renderer: a\n// full-width button whose `mj-button` lacks this class is clickable only on\n// its label.\nexport { BUTTON_FULL_WIDTH_CSS_CLASS } from \"./renderers/button\";\n// Exported for `blockRenderers` consumers that emit their own `mj-image` (a\n// video thumbnail, a countdown GIF): `fluid-on-mobile` is set per image, so an\n// `mj-image` without it keeps its desktop width on a phone.\nexport { fluidOnMobileAttr } from \"./utils\";\nexport { getWidthPercentages, getWidthPixels } from \"./columns\";\nexport { toPaddingString } from \"./padding\";\nexport { DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nexport { renderBlock } from \"./renderers\";\nexport {\n renderUnrenderableBlock,\n UNRENDERABLE_MARKER_PREFIX,\n} from \"./unrenderable\";\nexport type { BlockRenderer, BlockRendererMap } from \"./render-context\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["pkg.version","renderFontFamilyAttr","renderFontFamilyAttr","renderFontFamilyAttr","filterHtmlBlocks"],"sources":["../package.json","../src/render-context.ts","../src/escape.ts","../src/visibility.ts","../src/unrenderable.ts","../src/padding.ts","../src/utils.ts","../src/rich-text.ts","../src/renderers/title.ts","../src/renderers/paragraph.ts","../src/renderers/image.ts","../src/renderers/button.ts","../src/renderers/divider.ts","../src/renderers/spacer.ts","../src/renderers/html.ts","../src/renderers/social.ts","../src/renderers/menu.ts","../src/renderers/table.ts","../src/renderers/custom.ts","../src/columns.ts","../src/hero-geometry.ts","../src/display-condition.ts","../src/renderers/section.ts","../src/renderers/video.ts","../src/renderers/index.ts","../src/index.ts"],"sourcesContent":["","import type {\n Block,\n BlockType,\n ContentDirection,\n CustomFont,\n} from \"@templatical/types\";\nimport { resolveFontStack } from \"@templatical/types\";\nimport pkg from \"../package.json\" with { type: \"json\" };\n\n/**\n * A function type that renders a single block to MJML markup.\n */\nexport type BlockRenderer = (block: Block, context: RenderContext) => string;\n\n/**\n * Per-block-type renderer overrides, keyed by `block.type`. An entry replaces the\n * built-in renderer for that type wholesale — including its hidden-on-all-\n * viewports check, which becomes the override's responsibility.\n *\n * Generalises `renderCustomBlock`, which is the same idea for one block type. Its\n * reason to exist is that Templatical Cloud's render output is a deliberate\n * *superset*: countdown blocks resolve to a server-generated animated GIF and\n * video thumbnails get a composited play button, neither of which a browser can\n * do at render time. With this hook Cloud's renderer is the published one plus\n * two injected functions, so parity for every other block type holds by\n * construction instead of by review.\n */\nexport type BlockRendererMap = Partial<Record<BlockType, BlockRenderer>>;\n\nexport const DEFAULT_SOCIAL_ICONS_BASE_URL = `https://cdn.jsdelivr.net/npm/@templatical/renderer@${pkg.version}/assets/social`;\n\n/**\n * Immutable context passed through the block rendering chain.\n */\nexport class RenderContext {\n /**\n * The whole pixels MJML gives this container's content: the body width less\n * the side padding and border of every wrapper and section around it, then\n * the column's own percentage of what is left, floored as MJML floors it.\n * Width-aware decisions read this one, since it is the box MJML caps a block\n * at. Defaults to `containerWidth`.\n */\n public readonly contentWidth: number;\n\n constructor(\n /**\n * The column's share of the body width, ignoring any padding or border\n * around it. Emitted as the pixel width of a `\"full\"` image or video, which\n * MJML then caps at the real box ({@link contentWidth}).\n */\n public readonly containerWidth: number,\n public readonly customFonts: CustomFont[],\n public readonly defaultFallbackFont: string,\n public readonly allowHtmlBlocks: boolean,\n /**\n * Map of custom block id → pre-rendered HTML, populated by `renderToMjml`\n * before the synchronous render pass. Set when the consumer provides a\n * `renderCustomBlock` option. Empty by default.\n */\n public readonly customBlockHtml: ReadonlyMap<string, string> = new Map(),\n /**\n * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png`. Outlook desktop has no SVG support\n * and rejects base64 data URIs in `<img src>`, so PNGs must be served over\n * HTTP. Default points at the version-pinned jsDelivr mirror of this\n * package; consumers can override to self-host.\n */\n public readonly socialIconsBaseUrl: string = DEFAULT_SOCIAL_ICONS_BASE_URL,\n /**\n * Per-block-type renderer overrides from `RenderOptions.blockRenderers`.\n * Consulted before every built-in renderer — see {@link BlockRendererMap}.\n */\n public readonly blockRenderers: BlockRendererMap = {},\n /**\n * Writing direction of the email, resolved once in `renderToMjml`.\n * Nested columns copy it via `withContainerWidth` — dropping it there\n * would leave a two-column RTL section rendering LTR inside the columns.\n */\n public readonly contentDirection: ContentDirection = \"ltr\",\n contentWidth?: number,\n ) {\n this.contentWidth = contentWidth ?? containerWidth;\n }\n\n /**\n * Create a new context with a different container width. Used when rendering\n * columns, and wrappers, whose content is narrower. `contentWidth` defaults\n * to `width`, a container with nothing around it.\n */\n withContainerWidth(\n width: number,\n contentWidth: number = width,\n ): RenderContext {\n return new RenderContext(\n width,\n this.customFonts,\n this.defaultFallbackFont,\n this.allowHtmlBlocks,\n this.customBlockHtml,\n this.socialIconsBaseUrl,\n this.blockRenderers,\n this.contentDirection,\n contentWidth,\n );\n }\n\n /**\n * The stack to emit for a block's or the template's `fontFamily`. A stack is\n * the author's own fallback chain and is emitted as written, as is anything\n * that doesn't parse as a font. A lone family gets a fallback: a custom\n * font's own `fallback` (or `defaultFallbackFont`), a built-in's fixed stack,\n * or, for any other family, `defaultFallbackFont`, so a client without the\n * face falls back to it rather than to its own default (Times New Roman in\n * Outlook on Windows). A generic family needs nothing appended.\n *\n * The rule is {@link resolveFontStack}, shared with the editor canvas so a\n * font previews with the fallbacks the recipient gets.\n */\n resolveFontFamily(fontFamily: string): string {\n return resolveFontStack(fontFamily, {\n customFonts: this.customFonts,\n defaultFallback: this.defaultFallbackFont,\n });\n }\n}\n","const HTML_ENTITIES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\nconst HTML_ENTITY_REGEX = /[&<>\"']/g;\n\n/**\n * Escape HTML special characters (& < > \" ').\n * Equivalent to PHP htmlspecialchars with ENT_QUOTES | ENT_HTML5.\n */\nexport function escapeHtml(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use in an HTML attribute value.\n * Same implementation as escapeHtml for consistency with PHP.\n */\nexport function escapeAttr(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use as a CSS property value inside an inline\n * `style=\"prop: ${value}\"` attribute. Beyond HTML entity escaping (so the\n * value survives the attribute boundary), this strips characters that\n * could break out of the property value into a sibling property:\n *\n * `;` — separates CSS declarations\n * `{`/`}` — opens/closes a CSS rule (rejected by attribute parsers but\n * still safer to remove)\n * `\\n`/`\\r` — would smuggle past line-based CSS sanitizers\n *\n * Without this, an attacker-controlled color like\n * `\"red; background: url('//attacker/log')\"` lands as a real CSS rule.\n */\nexport function escapeCssValue(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n // Strip before escaping, so the `;` that ends each `"` survives.\n return escapeAttr(text.replace(/[;{}\\r\\n]/g, \"\"));\n}\n\nconst FONT_FAMILY_ENTITY_REGEX = /[&<>\"]/g;\n\n/**\n * Escape a font stack for a double-quoted `font-family=\"…\"` MJML attribute.\n * A `\"`-quoted family (`\"Helvetica Neue\", Arial`) must not end the attribute,\n * or the compiler reads it as empty. Unlike `escapeAttr`, single quotes are\n * left alone: they can't end a double-quoted attribute, and they're how the\n * renderer quotes families itself (`'Times New Roman', serif`), so every\n * stack without `&<>\"` is emitted byte-for-byte.\n */\nexport function escapeFontFamily(text: string): string {\n return text.replace(\n FONT_FAMILY_ENTITY_REGEX,\n (char) => HTML_ENTITIES[char] ?? char,\n );\n}\n\nexport { convertMergeTagsToValues } from \"@templatical/types\";\n","import type { Block } from \"@templatical/types\";\nimport { escapeAttr } from \"./escape\";\n\n/**\n * Check if a block is hidden on all viewports.\n */\nexport function isHiddenOnAll(block: Block): boolean {\n const visibility = block.visibility;\n\n if (!visibility) {\n return false;\n }\n\n return !visibility.desktop && !visibility.mobile;\n}\n\n/**\n * Get the MJML css-class attribute string for a block.\n * Returns a string like ` css-class=\"tpl-hide-desktop\"` or empty string.\n *\n * `extraClasses` are appended to the visibility classes on the *same*\n * attribute. MJML keeps only one `css-class` per element, so a caller needing\n * both must merge here rather than emitting a second attribute.\n */\nexport function getCssClassAttr(\n block: Block,\n extraClasses: string[] = [],\n): string {\n const classes = [getCssClasses(block), ...extraClasses]\n .filter((entry) => entry !== \"\")\n .join(\" \");\n\n if (classes === \"\") {\n return \"\";\n }\n\n return ` css-class=\"${escapeAttr(classes)}\"`;\n}\n\n/**\n * Get the CSS classes for visibility hiding.\n */\nexport function getCssClasses(block: Block): string {\n const visibility = block.visibility;\n\n if (!visibility) {\n return \"\";\n }\n\n const classes: string[] = [];\n\n if (!visibility.desktop) {\n classes.push(\"tpl-hide-desktop\");\n }\n\n if (!visibility.mobile) {\n classes.push(\"tpl-hide-mobile\");\n }\n\n return classes.join(\" \");\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Prefix every unrenderable-block marker carries. Stable and greppable on\n * purpose: it is what a consumer searches for after noticing a gap in a rendered\n * email, and what a send pipeline can scan for before shipping.\n */\nexport const UNRENDERABLE_MARKER_PREFIX = \"templatical:unrenderable-block\";\n\n/**\n * Strip anything that would terminate or nest an HTML comment. `-->` and `--!>`\n * inside the marker would close it early and leak the rest as body text; a lone\n * `--` is enough to make the comment non-conforming, so both collapse.\n *\n * Brackets go **before** hyphens collapse, and the order is load-bearing.\n * Removing `<` from `-<-` brings two hyphens together that a collapse pass has\n * already walked past, so collapsing first leaves a `--` behind — the sanitizer\n * reintroducing the sequence it exists to remove.\n */\nfunction commentSafe(value: string): string {\n return value.replace(/[<>]/g, \"\").replace(/-{2,}/g, \"-\");\n}\n\n/**\n * Emit the placeholder for a block no renderer can handle — no built-in renderer\n * and no `blockRenderers` override for its type.\n *\n * **A marker, not a throw and not silence.** The renderer runs inside send\n * pipelines, so killing an entire render over one block is worse than shipping a\n * marked gap. Silence is worse still: a block that simply vanishes reaches\n * recipients as a missing section with nothing anywhere explaining why. The rule\n * is general rather than countdown-specific, so\n * a block type added to `@templatical/types` before its renderer lands degrades\n * the same way.\n *\n * `mj-raw` is used because it is the one element that passes its content through\n * verbatim and is valid everywhere a block renders — inside `mj-column`, which is\n * where every non-section block ends up.\n */\nexport function renderUnrenderableBlock(block: Block): string {\n console.warn(\n `[Templatical] No renderer for block type \"${block.type}\" (id: ${block.id}). ` +\n \"A placeholder comment was emitted in its place. Pass a `blockRenderers` \" +\n \"entry for this type to render it.\",\n );\n\n const type = commentSafe(block.type);\n const id = commentSafe(block.id);\n\n return `<mj-raw><!-- ${UNRENDERABLE_MARKER_PREFIX} type=\"${type}\" id=\"${id}\" --></mj-raw>`;\n}\n","import type { SpacingValue } from \"@templatical/types\";\nimport { escapeAttr } from \"./escape\";\n\n/**\n * Convert a SpacingValue to a CSS padding string like \"10px 10px 10px 10px\".\n */\nexport function toPaddingString(padding: SpacingValue): string {\n return escapeAttr(\n `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`,\n );\n}\n","/**\n * MJML attribute helpers — single source of truth for placement rules\n * that the MJML spec enforces but accepts silently when violated.\n *\n * MJML drops unrecognized attributes without error. The most common trap is\n * background placement: only `mj-section`, `mj-button`, `mj-wrapper`, `mj-hero`\n * accept the native `background-color` attribute. Inner content elements\n * (`mj-text`, `mj-image`, `mj-table`, `mj-navbar`, `mj-video`) require\n * `container-background-color`, which paints the enclosing `<td>`. Passing\n * `background-color` to an inner element results in the attribute being\n * silently dropped — the email ships without the bg.\n *\n * https://documentation.mjml.io/\n */\n\nimport type {\n BackgroundImage,\n BorderRadiusValue,\n BorderValue,\n ImageBlock,\n SpacingValue,\n} from \"@templatical/types\";\nimport {\n resolveBackgroundImage,\n toBorderCss,\n toBorderDeclarations,\n toBorderRadiusCss,\n} from \"@templatical/types\";\nimport { escapeAttr, escapeCssValue } from \"./escape\";\nimport type { RenderContext } from \"./render-context\";\n\n/**\n * Where the MJML element accepts a background-color attribute.\n * - `native`: the element has its own `background-color` (mj-section, mj-button).\n * - `container`: the element only accepts `container-background-color`,\n * which colors the wrapping `<td>` (mj-text, mj-image, mj-table, mj-navbar, mj-video).\n */\nexport type BgPlacement = \"native\" | \"container\";\n\n/**\n * Render the appropriate background-color attribute for an MJML element.\n * Returns an empty string when no color is set, or a leading-space attribute\n * fragment ready to interpolate into a tag's attribute list.\n */\nexport function bgAttr(\n backgroundColor: string | undefined,\n placement: BgPlacement,\n): string {\n if (!backgroundColor) {\n return \"\";\n }\n\n const name =\n placement === \"native\" ? \"background-color\" : \"container-background-color\";\n\n return ` ${name}=\"${escapeCssValue(backgroundColor)}\"`;\n}\n\n/**\n * Render `mj-image`'s height attribute. Returns an empty string when no height\n * is set, so MJML applies its own `auto` and the image keeps its aspect ratio.\n *\n * The px suffix is not cosmetic: `height` is a Unit attribute accepting only\n * `px` or `auto`, so a bare number is a validation error and MJML drops it.\n * Non-finite or non-positive values are treated as unset — a `0` here would\n * collapse the image rather than express \"no opinion\".\n */\nexport function heightAttr(height: number | undefined): string {\n if (typeof height !== \"number\" || !Number.isFinite(height) || height <= 0) {\n return \"\";\n }\n\n return ` height=\"${height}px\"`;\n}\n\n/**\n * The width an element's side padding and drawn side borders take from its\n * box. MJML subtracts exactly these when it sizes a wrapper's, a section's or\n * an image's content, so a side `toBorderCss` would not draw counts for 0.\n * MJML reads each side as a whole pixel (`parseInt` in its `shorthandParser`\n * and `borderParser`), so 20.5px of padding takes 20px and a 2.5px border 2px.\n */\nexport function horizontalInset(\n padding: SpacingValue | undefined,\n border?: BorderValue,\n): number {\n const whole = (value: number | undefined) => Math.trunc(value ?? 0);\n const drawn = (side: BorderValue[\"left\"] | undefined) =>\n toBorderCss(side) === null ? 0 : Math.trunc(side!.width);\n return (\n whole(padding?.left) +\n whole(padding?.right) +\n drawn(border?.left) +\n drawn(border?.right)\n );\n}\n\n/**\n * `fluid-on-mobile` for an `mj-image`. An image fills its column on desktop\n * when its width is `\"full\"`, or a pixel width MJML caps at the room it gives\n * the image (the column less the image's own side padding and border). On a\n * phone the attribute widens the image to the stacked column, so an image set\n * narrower must not carry it: a 120px logo would grow to the whole column, and\n * one with a set height would stretch out of shape. An image wider than the\n * phone shrinks to fit without it.\n *\n * `fluidOnMobile: true` always emits the attribute and `false` never does.\n * Omitted keeps the rule above. Video thumbnails have no such field and stay\n * on the rule.\n *\n * Exported for `blockRenderers` entries that emit their own `mj-image`. Pass\n * the context the renderer received, whose `contentWidth` already excludes the\n * wrappers and sections around the column.\n */\nexport function fluidOnMobileAttr(\n block: Pick<ImageBlock, \"width\" | \"styles\" | \"border\"> & {\n fluidOnMobile?: boolean;\n },\n context: RenderContext,\n): string {\n if (block.fluidOnMobile === true) return ' fluid-on-mobile=\"true\"';\n if (block.fluidOnMobile === false) return \"\";\n const room =\n Math.floor(context.contentWidth) -\n horizontalInset(block.styles.padding, block.border);\n return block.width === \"full\" || block.width >= room\n ? ' fluid-on-mobile=\"true\"'\n : \"\";\n}\n\n/**\n * Render the border attributes for the MJML elements that accept them natively\n * (`mj-section`, `mj-image`, `mj-button`): a single `border` when it covers all\n * four sides, otherwise one `border-<side>` per chosen side. Returns an empty\n * string when there is nothing to draw, so templates without a border render\n * exactly as before.\n *\n * MJML copies the values into an inline `style`, so they go through\n * `escapeCssValue` — a tampered color must not smuggle in a sibling\n * declaration.\n */\nexport function borderAttr(border: BorderValue | undefined): string {\n return Object.entries(toBorderDeclarations(border))\n .map(([name, value]) => ` ${name}=\"${escapeCssValue(value)}\"`)\n .join(\"\");\n}\n\n/**\n * Render the `border-radius` attribute: `\"8px\"` for matching corners, or the\n * four-value shorthand (top-left, top-right, bottom-right, bottom-left) that\n * `mj-section`, `mj-wrapper`, `mj-image` and `mj-button` all accept. Returns an\n * empty string when every corner is square, rather than a `border-radius=\"0px\"`\n * every existing template would suddenly grow.\n */\nexport function borderRadiusAttr(\n radius: BorderRadiusValue | undefined,\n): string {\n const css = toBorderRadiusCss(radius);\n\n if (css === null) {\n return \"\";\n }\n\n return ` border-radius=\"${escapeCssValue(css)}\"`;\n}\n\n/**\n * Render the background-image attributes `mj-section` and `mj-wrapper` accept.\n * All four are emitted together, so MJML's own defaults (`auto`, `top center`,\n * `repeat`) never apply. Returns an empty string when there is no image.\n *\n * MJML pastes `background-url` verbatim into `url('…')` and the Outlook VML\n * `src`; `resolveBackgroundImage` has already percent-encoded the characters\n * that could end the CSS string, and `escapeAttr` keeps the attribute closed.\n */\nexport function backgroundImageAttrs(bg: BackgroundImage | undefined): string {\n const resolved = resolveBackgroundImage(bg);\n\n if (resolved === null) {\n return \"\";\n }\n\n return ` background-url=\"${escapeAttr(resolved.url)}\" background-size=\"${escapeCssValue(resolved.size)}\" background-position=\"${escapeCssValue(resolved.position)}\" background-repeat=\"${escapeCssValue(resolved.repeat)}\"`;\n}\n","import {\n RICH_TEXT_SPACING,\n findOpenTagEnd,\n isSection,\n isWrapper,\n} from \"@templatical/types\";\nimport type { Block, ContentDirection } from \"@templatical/types\";\n\n/**\n * Marks the `mj-text` of a block whose content is editor-authored rich text\n * (paragraph and title). MJML puts a `css-class` on the wrapping `<td>`, which\n * is what {@link richTextStylesheet} scopes its selectors to.\n *\n * The scope is the point. A bare `p { … }` rule would also reach an `html`\n * block — the renderer puts consumer markup in `mj-text` too — and the editor\n * previews those in a sandboxed `srcdoc` iframe on UA defaults. Styling them\n * would trade this mismatch for another one, and an inlined declaration would\n * beat the consumer's own stylesheet.\n */\nexport const RICH_TEXT_CSS_CLASS = \"tpl-rich-text\";\n\n/**\n * The class carrying one specific paragraph gap, e.g. `tpl-rich-text-8`.\n *\n * Every rich-text block gets one of these *instead of* a shared paragraph rule.\n * That is a hard constraint, not a preference: `.tpl-rich-text p` and\n * `.tpl-rich-text-4 p` have identical specificity (one class, one type), so a\n * surviving base rule would win on source order and flatten every per-block gap\n * back to the default.\n */\nexport function richTextGapClass(gap: number): string {\n return `${RICH_TEXT_CSS_CLASS}-${gap}`;\n}\n\n/**\n * The global rules that make exported rich text match the editor canvas.\n *\n * Two gaps they close, neither visible until you measure the delivered email:\n *\n * - MJML's core skeleton injects `p { display:block;margin:13px 0; }` into\n * every document. Against the canvas's paragraph gap that made a\n * three-paragraph block 99px in the editor and 135px in the inbox — 13px\n * above the first paragraph, 13px below the last, and 13px rather than 8px\n * between each (issue #616).\n * - The skeleton resets nothing for `ul` / `ol` / `li`, so those fall through\n * to each client's UA stylesheet — around 14px margin and a 40px bullet\n * indent in Chromium, against the canvas's 8px and 24px. Unlike the\n * paragraph case the value is not even a fixed constant, so matching MJML in\n * the editor could not have fixed it.\n *\n * Emitted inside `<mj-style inline=\"inline\">`, so MJML runs it through juice\n * and the declarations land as real inline styles. That matters twice: they\n * survive a client that strips `<style>` from the head, and juice resolves\n * `:last-child` structurally at build time instead of dropping it.\n *\n * One paragraph rule per *distinct* gap in the document, so a template using\n * two gaps costs two rules rather than one per block. The list values stay on\n * the unversioned class: MJML resets nothing for `ul`/`ol`/`li`, so they exist\n * to pin down client defaults rather than to be designed with.\n */\nexport function richTextStylesheet(\n gaps: number[],\n direction: ContentDirection = \"ltr\",\n): string {\n const s = RICH_TEXT_SPACING;\n const scope = `.${RICH_TEXT_CSS_CLASS}`;\n const distinct = [...new Set(gaps)].sort((a, b) => a - b);\n\n const paragraphRules = distinct.flatMap((gap) => {\n const gapScope = `.${richTextGapClass(gap)}`;\n return [\n `${gapScope} p { margin: 0 0 ${gap}px; }`,\n `${gapScope} p:last-child { margin-bottom: 0; }`,\n ];\n });\n\n // Physical sides: Outlook does not honour `padding-inline-start`.\n const listPaddingSide =\n direction === \"rtl\" ? \"padding-right\" : \"padding-left\";\n\n return [\n ...paragraphRules,\n `${scope} ul, ${scope} ol { margin: ${s.listMarginY}px 0; ${listPaddingSide}: ${s.listPaddingLeft}px; }`,\n `${scope} li { margin: ${s.listItemMarginY}px 0; }`,\n ].join(\"\\n \");\n}\n\n/**\n * The paragraph gap a block asks for, or the built-in default.\n *\n * The fallback must produce a *rule* rather than nothing: silence hands the\n * paragraph back to MJML's `p { margin: 13px 0 }` skeleton default and reopens\n * the canvas/export mismatch. `0` is a legitimate gap, so this tests for\n * absence rather than falsiness.\n *\n * Mirrored on the canvas side by `getBlockWrapperStyle` in\n * `@templatical/editor`.\n */\nexport function resolveParagraphGap(spacing: number | undefined): number {\n return spacing ?? RICH_TEXT_SPACING.paragraphGap;\n}\n\n/**\n * Every paragraph gap the document uses, so the head can carry a rule for each.\n *\n * Walks section columns and wrapper children too — a paragraph inside a column\n * or a layout card needs its rule as much as a top-level one. Titles always\n * use the default gap: their content is normally a single paragraph the\n * renderer unwraps, and the multi-paragraph case is an edge worth spacing\n * consistently rather than configuring.\n */\nexport function collectParagraphGaps(blocks: Block[]): number[] {\n const gaps: number[] = [RICH_TEXT_SPACING.paragraphGap];\n\n const visit = (block: Block): void => {\n if (block.type === \"paragraph\") {\n gaps.push(resolveParagraphGap(block.paragraphSpacing));\n }\n if (isSection(block)) {\n for (const column of block.children) {\n for (const child of column) {\n visit(child);\n }\n }\n } else if (isWrapper(block)) {\n for (const child of block.children) {\n visit(child);\n }\n }\n };\n\n for (const block of blocks) {\n visit(block);\n }\n\n return gaps;\n}\n\n/**\n * The named references {@link decodeStyleReferences} writes out. `<` and `>`\n * stay encoded, named or numeric: MJML parses its source as XML, which forbids\n * a raw `<` in an attribute, and no CSS value needs one.\n */\nconst NAMED_STYLE_REFERENCES: Record<string, string> = {\n apos: \"'\",\n quot: '\"',\n nbsp: String.fromCharCode(0xa0),\n};\n\nconst STYLE_REFERENCE = /&(?:#(\\d+)|#x([0-9a-f]+)|(apos|quot|nbsp));/gi;\nconst TAG_NAME_START = /[a-z]/i;\nconst HTML_SPACE = /[ \\t\\n\\f\\r]/;\n\nfunction decodeStyleReference(\n reference: string,\n quote: string,\n decimal: string | undefined,\n hex: string | undefined,\n name: string | undefined,\n): string {\n let char: string;\n if (name !== undefined) {\n char = NAMED_STYLE_REFERENCES[name.toLowerCase()];\n } else {\n const codePoint = Number(decimal ?? `0x${hex}`);\n const valid =\n codePoint > 0 &&\n codePoint <= 0x10ffff &&\n !(codePoint >= 0xd800 && codePoint <= 0xdfff);\n if (!valid) return reference;\n char = String.fromCodePoint(codePoint);\n }\n // A raw `&` could start a reference, and the delimiter would end the\n // attribute; CSS reads either quote the same.\n if (char === \"&\") return \"&\";\n if (char === \"<\" || char === \">\") return reference;\n if (char === quote) return quote === '\"' ? \"'\" : '\"';\n return char;\n}\n\n/**\n * The open tag `tag` (from `<` up to, not including, its `>`) with the\n * references in its first quoted `style` attribute decoded. Attributes are\n * walked in order and quoted values skipped whole, so `style=\"…\"` text inside\n * another attribute's value is never taken for the attribute.\n */\nfunction decodeTagStyle(tag: string): string {\n let i = 1;\n while (i < tag.length && !HTML_SPACE.test(tag[i]) && tag[i] !== \"/\") i++;\n while (i < tag.length) {\n while (i < tag.length && (HTML_SPACE.test(tag[i]) || tag[i] === \"/\")) i++;\n const nameStart = i;\n while (i < tag.length && !/[\\s=/]/.test(tag[i])) i++;\n const name = tag.slice(nameStart, i).toLowerCase();\n if (name === \"\") {\n i++;\n continue;\n }\n while (i < tag.length && HTML_SPACE.test(tag[i])) i++;\n if (tag[i] !== \"=\") continue;\n i++;\n while (i < tag.length && HTML_SPACE.test(tag[i])) i++;\n const quote = tag[i];\n if (quote === '\"' || quote === \"'\") {\n const valueEnd = tag.indexOf(quote, i + 1);\n if (valueEnd === -1) return tag;\n if (name === \"style\") {\n const value = tag\n .slice(i + 1, valueEnd)\n .replace(STYLE_REFERENCE, (reference, decimal, hex, named) =>\n decodeStyleReference(reference, quote, decimal, hex, named),\n );\n return `${tag.slice(0, i + 1)}${value}${tag.slice(valueEnd)}`;\n }\n i = valueEnd + 1;\n continue;\n }\n while (i < tag.length && !HTML_SPACE.test(tag[i])) i++;\n }\n return tag;\n}\n\n/**\n * Rich-text HTML with the character references in its `style` attributes\n * written as the characters they stand for: `'`, `"`, ` ` and\n * numeric references (`'`, `'`). Other named references stay as\n * written, and so do `&`, `<` and `>` in any form; a reference to `&`\n * becomes `&`. Tags are found with the quote-aware `findOpenTagEnd`, so a\n * `>` inside an attribute value doesn't end one, and a tag whose quote never\n * closes is left alone.\n *\n * MJML inlines {@link richTextStylesheet} with juice, which re-parses the\n * style attribute of every element a rule matches and splits it on `;`\n * without decoding references first. The `;` that ends `'`, `"` or\n * ` ` outside a `url()` therefore cuts a declaration in two, and the\n * compile throws. Imported content carries such references: BeeFree's TinyMCE\n * output and import-mjml's serializer write them.\n */\nexport function decodeStyleReferences(html: string): string {\n if (!html.includes(\"&\")) return html;\n let out = \"\";\n let copied = 0;\n let open = html.indexOf(\"<\");\n while (open !== -1) {\n if (!TAG_NAME_START.test(html.charAt(open + 1))) {\n open = html.indexOf(\"<\", open + 1);\n continue;\n }\n const end = findOpenTagEnd(html, open + 1);\n if (end === -1) break;\n const tag = html.slice(open, end);\n const decoded = decodeTagStyle(tag);\n if (decoded !== tag) {\n out += html.slice(copied, open) + decoded;\n copied = end;\n }\n open = html.indexOf(\"<\", end);\n }\n return out + html.slice(copied);\n}\n","import type { TitleBlock } from \"@templatical/types\";\nimport {\n HEADING_LEVEL_FONT_SIZE,\n TITLE_FONT_WEIGHT,\n TITLE_LINE_HEIGHT,\n fillEmptyParagraphs,\n findOpenTagEnd,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport {\n convertMergeTagsToValues,\n escapeAttr,\n escapeFontFamily,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport {\n RICH_TEXT_CSS_CLASS,\n decodeStyleReferences,\n richTextGapClass,\n resolveParagraphGap,\n} from \"../rich-text\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a title block to MJML markup.\n */\nexport function renderTitle(block: TitleBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n // Blank lines between a title's paragraphs are filled as the canvas fills\n // them; a single paragraph is unwrapped into the heading.\n const content = unwrapParagraph(\n decodeStyleReferences(\n fillEmptyParagraphs(convertMergeTagsToValues(block.content)),\n ),\n );\n // Clamp out-of-range levels to a defined entry so we never interpolate\n // `undefined` into `font-size=\"${...}px\"` (mjml@5 rejects \"undefinedpx\").\n const fontSize =\n HEADING_LEVEL_FONT_SIZE[block.level] ?? HEADING_LEVEL_FONT_SIZE[2];\n // Emit the color only when the block sets one; otherwise the `mj-text`\n // inherits the document `textColor` default from `<mj-attributes>`.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n const align = escapeAttr(block.textAlign);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n // `unwrapParagraph` strips only a *single* outer `<p>`, so a title the user\n // pressed Enter in keeps its paragraphs and needs a spacing rule. Titles\n // carry no gap of their own, so they always take the default.\n const cssClassAttr = getCssClassAttr(block, [\n RICH_TEXT_CSS_CLASS,\n richTextGapClass(resolveParagraphGap(undefined)),\n ]);\n // Same clamp logic as fontSize so that an out-of-range level still\n // produces a valid heading element.\n const safeLevel = HEADING_LEVEL_FONT_SIZE[block.level] ? block.level : 2;\n const tag = `h${safeLevel}`;\n\n return `<mj-text\n font-size=\"${fontSize}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"${TITLE_LINE_HEIGHT}\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${cssClassAttr}\n><${tag} style=\"margin:0;font-size:inherit;color:inherit;line-height:inherit;font-weight:${TITLE_FONT_WEIGHT}\">${content}</${tag}></mj-text>`;\n}\n\n/**\n * The editor stores title content as a TipTap paragraph (`<p>...</p>`),\n * but the renderer wraps it in `<h${level}>`. `<p>` is invalid inside a\n * heading, so strip a single outer `<p>` wrapper if present.\n */\nfunction unwrapParagraph(html: string): string {\n const openingTag = /^\\s*<p\\b/.exec(html);\n if (!openingTag) return html;\n const openingTagEnd = findOpenTagEnd(html, openingTag[0].length);\n if (openingTagEnd === -1) return html;\n const closingTag = /<\\/p>\\s*$/.exec(html);\n if (!closingTag || closingTag.index <= openingTagEnd) return html;\n const content = html.slice(openingTagEnd + 1, closingTag.index);\n // Only unwrap when there is exactly one top-level <p> — otherwise the\n // content has multiple paragraphs and we must leave it alone.\n if (/<\\/p>\\s*<p\\b/i.test(content)) return html;\n return content;\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${escapeFontFamily(resolved)}\"`;\n}\n","import type { ParagraphBlock } from \"@templatical/types\";\nimport { fillEmptyParagraphs, isBlankRichText } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { convertMergeTagsToValues } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport {\n RICH_TEXT_CSS_CLASS,\n decodeStyleReferences,\n richTextGapClass,\n resolveParagraphGap,\n} from \"../rich-text\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a paragraph block to MJML markup.\n * All text formatting is inline in the HTML content (managed by TipTap).\n */\nexport function renderParagraph(\n block: ParagraphBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n // Skip when the user has cleared the paragraph — otherwise we emit a\n // styled `<td>` cell that adds visible whitespace to the email even\n // though the canvas shows nothing for an empty paragraph.\n if (isBlankRichText(block.content)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n // The canvas fills blank lines with the same helper, so the two agree.\n const content = decodeStyleReferences(\n fillEmptyParagraphs(convertMergeTagsToValues(block.content)),\n );\n const cssClassAttr = getCssClassAttr(block, [\n RICH_TEXT_CSS_CLASS,\n richTextGapClass(resolveParagraphGap(block.paragraphSpacing)),\n ]);\n\n // RTL templates need a start-edge default on the wrapper. Inner\n // `<p style=\"text-align:…\">` from TipTap still wins per paragraph.\n const alignAttr =\n context.contentDirection === \"rtl\" ? '\\n align=\"right\"' : \"\";\n\n return `<mj-text\n line-height=\"1.5\"${alignAttr}\n padding=\"${padding}\"${bgColor}${cssClassAttr}\n>${content}</mj-text>`;\n}\n","import type { ImageBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport {\n bgAttr,\n borderAttr,\n borderRadiusAttr,\n fluidOnMobileAttr,\n heightAttr,\n} from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render an image block to MJML markup.\n */\nexport function renderImage(block: ImageBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.src === \"\") {\n // Skip rather than emit `<mj-image src=\"\">` which compiles to a\n // broken-image icon. Mirrors video.ts behavior when no source is set.\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const width = escapeAttr(\n block.width === \"full\" ? context.containerWidth + \"px\" : block.width + \"px\",\n );\n const height = heightAttr(block.height);\n const fluid = fluidOnMobileAttr(block, context);\n\n const visibilityAttr = getCssClassAttr(block);\n\n const borderRadiusAttrStr = borderRadiusAttr(block.borderRadius);\n const borderAttrStr = borderAttr(block.border);\n\n let linkAttr = \"\";\n if (block.linkUrl) {\n linkAttr = ` href=\"${escapeAttr(block.linkUrl)}\"`;\n if (block.linkOpenInNewTab) {\n linkAttr += ' target=\"_blank\" rel=\"noopener\"';\n }\n }\n\n const src = escapeAttr(block.src);\n const decorative = block.decorative === true;\n const alt = decorative ? \"\" : escapeAttr(block.alt);\n const align = escapeAttr(block.align);\n const roleAttr = decorative ? ' role=\"presentation\"' : \"\";\n\n return `<mj-image\n src=\"${src}\"\n alt=\"${alt}\"\n width=\"${width}\"${height}${fluid}\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${borderAttrStr}${borderRadiusAttrStr}${linkAttr}${visibilityAttr}${roleAttr}\n/>`;\n}\n","import type { ButtonBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr, escapeFontFamily, escapeHtml } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport type { BorderRadiusValue } from \"@templatical/types\";\nimport { toBorderRadiusCss } from \"@templatical/types\";\nimport { bgAttr, borderAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Marks the `mj-button` of a full-width button. MJML gives the button's `<a>`\n * a width only for a px `width`; for `100%` it widens the `<table>` and leaves\n * the `<a>` at `display:inline-block`, so only the label is clickable.\n * `renderToMjml` scopes the head rule that makes the `<a>` fill the button to\n * this class.\n */\nexport const BUTTON_FULL_WIDTH_CSS_CLASS = \"tpl-button-full\";\n\n/**\n * Render a button block to MJML markup.\n */\nexport function renderButton(\n block: ButtonBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const buttonPadding = toPaddingString(block.buttonPadding);\n\n // Omit href entirely when no URL is set so we don't emit a clickable\n // `<a href=\"\">` that navigates to whatever URL the email is opened from.\n const href = block.url === \"\" ? \"\" : escapeAttr(block.url);\n const hrefAttr = href === \"\" ? \"\" : ` href=\"${href}\"`;\n const backgroundColor = buttonBackgroundAttr(block.backgroundColor);\n const textColor = escapeAttr(block.textColor);\n const fontSize = block.fontSize;\n const fontWeight = renderFontWeight(block.fontWeight);\n const borderRadius = escapeAttr(renderButtonRadius(block.borderRadius));\n const text = escapeHtml(block.text);\n const targetAttr = block.openInNewTab\n ? ' target=\"_blank\" rel=\"noopener\"'\n : \"\";\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const widthAttr = renderWidthAttr(block.width);\n const cssClassAttr = getCssClassAttr(\n block,\n block.width === \"full\" ? [BUTTON_FULL_WIDTH_CSS_CLASS] : [],\n );\n const borderAttrStr = borderAttr(block.border);\n // Templates stored before `align` existed have no value for it. Fall back to\n // MJML's own default so they keep rendering exactly as they did.\n const align = escapeAttr(block.align ?? \"center\");\n\n return `<mj-button${hrefAttr}${targetAttr}\n background-color=\"${backgroundColor}\"\n color=\"${textColor}\"\n font-size=\"${escapeAttr(String(fontSize))}px\"\n font-weight=\"${fontWeight}\"\n border-radius=\"${borderRadius}\"${borderAttrStr}\n inner-padding=\"${buttonPadding}\"\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${widthAttr}${cssClassAttr}\n>${text}</mj-button>`;\n}\n\n/**\n * MJML paints `#414141` when `background-color` is omitted, and `\"\"` is not a\n * color it accepts. An unset button fill is the keyword `transparent`.\n */\nfunction buttonBackgroundAttr(color: string): string {\n if (color.trim() === \"\") return \"transparent\";\n return escapeAttr(color);\n}\n\n/**\n * Omitted weight stays `bold`, the attribute a button has always rendered\n * with. A stored weight is that step's CSS number.\n */\nfunction renderFontWeight(weight: ButtonBlock[\"fontWeight\"]): string {\n if (weight === \"regular\") return \"400\";\n if (weight === \"medium\") return \"500\";\n if (weight === \"bold\") return \"700\";\n return \"bold\";\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${escapeFontFamily(resolved)}\"`;\n}\n\n/**\n * The button has always emitted its radius, `0px` included, so a plain number\n * keeps rendering exactly as it did. Per-corner radii go through the shared\n * formatter, with all-square corners as `0px`.\n */\nfunction renderButtonRadius(radius: BorderRadiusValue): string {\n if (typeof radius === \"number\") {\n return `${radius}px`;\n }\n\n return toBorderRadiusCss(radius) ?? \"0px\";\n}\n\nfunction renderWidthAttr(width: number | \"full\" | undefined): string {\n if (width === undefined) {\n return \"\";\n }\n\n const value = width === \"full\" ? \"100%\" : `${width}px`;\n\n return ` width=\"${escapeAttr(value)}\"`;\n}\n","import type { DividerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a divider block to MJML markup.\n */\nexport function renderDivider(\n block: DividerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n // A percentage passes through as is: MJML takes it natively.\n const width =\n block.width === \"full\"\n ? \"100%\"\n : typeof block.width === \"number\"\n ? `${block.width}px`\n : block.width;\n const thickness = block.thickness;\n const lineStyle = escapeAttr(block.lineStyle);\n const color = escapeAttr(block.color);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-divider\n border-width=\"${escapeAttr(String(thickness))}px\"\n border-style=\"${lineStyle}\"\n border-color=\"${color}\"\n width=\"${escapeAttr(width)}\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\n/>`;\n}\n","import type { SpacerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a spacer block to MJML markup.\n *\n * The canvas renders a spacer at exactly `block.height` pixels and ignores\n * `block.styles.padding`. Match that here: emit `padding=\"0\"` so the\n * exported email's spacer occupies the same vertical space the user saw\n * in the editor preview. Any non-zero `block.styles.padding` on a spacer\n * is meaningless and silently dropped from the export.\n */\nexport function renderSpacer(\n block: SpacerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const height = block.height;\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-spacer height=\"${escapeAttr(String(height))}px\" padding=\"0\"${bgColor}${visibilityAttr} />`;\n}\n","import type { HtmlBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render an HTML block to MJML markup.\n * No sanitization in the OSS version -- consumers are responsible for content safety.\n */\nexport function renderHtml(block: HtmlBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (!context.allowHtmlBlocks) {\n return \"\";\n }\n\n const content = block.content;\n\n if (content === \"\") {\n return \"\";\n }\n\n const visibilityAttr = getCssClassAttr(block);\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n\n // Use mj-text to render HTML content inside proper table cell structure.\n // mj-raw bypasses MJML's table layout and content won't be visible.\n return `<mj-text padding=\"${padding}\"${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { SocialIconsBlock } from \"@templatical/types\";\nimport { socialIconAssetDir } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a social icons block to MJML markup.\n *\n * Icons are emitted as `<img src=\"…/{dir}/{platform}.png\">` rather than\n * inline SVG or base64 data URIs. Outlook desktop (Word rendering engine)\n * does not support SVG and rejects base64 in `<img src>`, so hosted PNGs are\n * the only format that renders across every mainstream client. The base URL\n * is read from `context.socialIconsBaseUrl` (configurable via\n * `RenderOptions.socialIconsBaseUrl`; default is the version-pinned jsDelivr\n * mirror of this package). `{dir}` is the style, plus `-{tone}` for a\n * one-color tone (`socialIconAssetDir`).\n */\nexport function renderSocialIcons(\n block: SocialIconsBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const icons = block.icons;\n\n if (icons.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n const align = escapeAttr(block.align);\n const iconSize = block.iconSize;\n const iconStyle = block.iconStyle;\n const assetDir = escapeAttr(socialIconAssetDir(iconStyle, block.iconTone));\n const spacing = block.spacing;\n\n let iconSizePx: number;\n switch (iconSize) {\n case \"small\":\n iconSizePx = 24;\n break;\n case \"large\":\n iconSizePx = 48;\n break;\n default:\n iconSizePx = 32;\n break;\n }\n\n // MJML mj-social-element has default border-radius of 3px, override per style\n let borderRadius: string;\n switch (iconStyle) {\n case \"circle\":\n borderRadius = \"50%\";\n break;\n case \"rounded\":\n borderRadius = \"8px\";\n break;\n case \"square\":\n borderRadius = \"0\";\n break;\n default:\n borderRadius = \"4px\"; // solid, outlined, plain\n break;\n }\n\n const iconCount = icons.length;\n const socialElements = icons.map((icon, index) => {\n const platform = escapeAttr(icon.platform);\n const url = escapeAttr(icon.url);\n const iconSrc = `${context.socialIconsBaseUrl}/${assetDir}/${platform}.png`;\n\n // Apply spacing as right padding only (except last icon) to match CSS gap behavior\n const rightPad = index === iconCount - 1 ? 0 : spacing;\n\n // Empty content hides the platform name text, matching the editor display\n return `<mj-social-element src=\"${iconSrc}\" href=\"${url}\" icon-size=\"${iconSizePx}px\" padding=\"0 ${escapeAttr(String(rightPad))}px 0 0\" border-radius=\"${borderRadius}\" background-color=\"transparent\"></mj-social-element>`;\n });\n\n const socialContent = socialElements.join(\"\\n\");\n\n return `<mj-social\n mode=\"horizontal\"\n align=\"${align}\"\n icon-padding=\"0\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\n>\n${socialContent}\n</mj-social>`;\n}\n","import type { MenuBlock, MenuItemData } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport {\n escapeHtml,\n escapeAttr,\n escapeCssValue,\n escapeFontFamily,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a menu block to MJML markup.\n * Uses mj-text with inline <a> tags separated by styled <span> separators.\n */\nexport function renderMenu(block: MenuBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.items.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const align = escapeAttr(block.textAlign);\n const fontSize = block.fontSize;\n // Emit the color only when set; otherwise the `mj-text` inherits the\n // document `textColor` and the item anchors below inherit it in turn.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n\n const content = renderMenuItems(block);\n\n return `<mj-text\n font-size=\"${escapeAttr(String(fontSize))}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${content}</mj-text>`;\n}\n\nfunction renderMenuItems(block: MenuBlock): string {\n const items = block.items;\n const separator = escapeHtml(block.separator);\n const separatorColor = escapeCssValue(block.separatorColor);\n const spacing = block.spacing;\n const linkColor = block.linkColor ?? block.color;\n\n const parts: string[] = [];\n const itemCount = items.length;\n\n for (let index = 0; index < itemCount; index++) {\n parts.push(renderMenuItem(items[index], linkColor));\n\n if (index < itemCount - 1) {\n parts.push(\n `<span style=\"color: ${separatorColor}; padding: 0 ${escapeCssValue(String(spacing))}px;\">${separator}</span>`,\n );\n }\n }\n\n return parts.join(\"\");\n}\n\nfunction renderMenuItem(\n item: MenuItemData,\n linkColor: string | undefined,\n): string {\n const text = escapeHtml(item.text);\n const url = escapeAttr(item.url);\n // Item color, else the menu link color; when neither is set the anchor omits\n // an explicit color and inherits the mj-text color (the document `textColor`).\n const resolvedColor = item.color ?? linkColor;\n const target = item.openInNewTab ? ' target=\"_blank\" rel=\"noopener\"' : \"\";\n\n const styles: string[] = [\"text-decoration: none\"];\n\n if (resolvedColor) {\n styles.unshift(`color: ${escapeCssValue(resolvedColor)}`);\n }\n\n if (item.bold) {\n styles.push(\"font-weight: bold\");\n }\n\n if (item.underline) {\n styles.push(\"text-decoration: underline\");\n }\n\n const styleAttr = styles.join(\"; \");\n\n return `<a href=\"${url}\" style=\"${styleAttr}\"${target}>${text}</a>`;\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${escapeFontFamily(resolved)}\"`;\n}\n","import type {\n TableBlock,\n TableRowData,\n TableCellData,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport {\n escapeAttr,\n escapeCssValue,\n escapeFontFamily,\n escapeHtml,\n convertMergeTagsToValues,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a table block to MJML markup.\n * Uses mj-text wrapping an HTML <table> with styled <tr>/<td> elements.\n */\nexport function renderTable(block: TableBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.rows.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const fontSize = block.fontSize;\n // Emit the color only when set; otherwise the `mj-text` (and its cells,\n // which carry no color of their own) inherit the document `textColor`.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n const align = escapeAttr(block.textAlign);\n\n const tableHtml = renderTableElement(block);\n\n return `<mj-text\n font-size=\"${escapeAttr(String(fontSize))}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${tableHtml}</mj-text>`;\n}\n\nfunction renderTableElement(block: TableBlock): string {\n const borderColor = escapeCssValue(block.borderColor);\n const borderWidth = block.borderWidth;\n\n const tableStyle = \"width: 100%; border-collapse: collapse;\";\n\n let rowsHtml = \"\";\n\n for (let index = 0; index < block.rows.length; index++) {\n const row = block.rows[index];\n const isHeader = block.hasHeaderRow && index === 0;\n rowsHtml += renderRow(row, block, isHeader, borderColor, borderWidth);\n }\n\n return `<table style=\"${tableStyle}\">${rowsHtml}</table>`;\n}\n\nfunction renderRow(\n row: TableRowData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n let cellsHtml = \"\";\n\n for (const cell of row.cells) {\n cellsHtml += renderCell(cell, block, isHeader, borderColor, borderWidth);\n }\n\n return `<tr>${cellsHtml}</tr>`;\n}\n\nfunction renderCell(\n cell: TableCellData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n const cellPadding = block.cellPadding;\n\n const styles: string[] = [\n `border: ${escapeCssValue(String(borderWidth))}px solid ${borderColor}`,\n `padding: ${escapeCssValue(String(cellPadding))}px`,\n ];\n\n if (isHeader) {\n styles.push(\"font-weight: bold\");\n\n if (block.headerBackgroundColor) {\n styles.push(\n `background-color: ${escapeCssValue(block.headerBackgroundColor)}`,\n );\n }\n }\n\n const styleAttr = styles.join(\"; \");\n // Cells are a plain-text field (the editor stores innerText; TableCellData\n // carries no rich-text hint), so escape the content like menu/button. Merge\n // tags are resolved first: convertMergeTagsToValues matches a literal\n // `<span`, so escaping has to run after it — escaping first would turn the\n // span into `<span` and the `{{value}}` placeholder would never resolve.\n const content = escapeHtml(convertMergeTagsToValues(cell.content));\n\n const tag = isHeader ? \"th\" : \"td\";\n\n return `<${tag} style=\"${styleAttr}\">${content}</${tag}>`;\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${escapeFontFamily(resolved)}\"`;\n}\n","import type { CustomBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a custom block to MJML markup.\n *\n * Custom block HTML resolution order:\n * 1. `context.customBlockHtml` map — populated by `renderToMjml` when the\n * caller passes `customBlocks` definitions or a `renderCustomBlock`\n * callback (the editor's path).\n * 2. `block.renderedHtml` — populated by an external pre-render step\n * (e.g., a previous render pass that mutated the block).\n * 3. Empty — block omitted from output.\n */\nexport function renderCustom(\n block: CustomBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const fromContext = context.customBlockHtml.get(block.id);\n const content = fromContext ?? block.renderedHtml;\n\n if (!content || content === \"\") {\n return \"\";\n }\n\n const visibilityAttr = getCssClassAttr(block);\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n\n return `<mj-text padding=\"${padding}\"${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { ColumnLayout } from \"@templatical/types\";\n\n/**\n * Get width percentages for each column in a layout.\n */\nexport function getWidthPercentages(layout: ColumnLayout): string[] {\n switch (layout) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n // 33.33 × 3 = 99.99% — let the last column absorb the rounding gap\n // so widths sum to exactly 100% in the compiled HTML.\n return [\"33.33%\", \"33.33%\", \"33.34%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n}\n\n/**\n * Get width in pixels for each column in a layout.\n */\nexport function getWidthPixels(\n layout: ColumnLayout,\n containerWidth: number,\n): number[] {\n switch (layout) {\n case \"2\":\n return [containerWidth * 0.5, containerWidth * 0.5];\n case \"3\":\n return [containerWidth / 3, containerWidth / 3, containerWidth / 3];\n case \"1-2\":\n return [containerWidth / 3, (containerWidth * 2) / 3];\n case \"2-1\":\n return [(containerWidth * 2) / 3, containerWidth / 3];\n default:\n return [containerWidth];\n }\n}\n","import type { AppliedHero, SpacingValue } from \"@templatical/types\";\n\n/** The sizing attributes of an `mj-hero`. */\nexport interface HeroGeometry {\n mode: \"fixed-height\";\n /** Total height in px, padding included. */\n height: number;\n backgroundWidth: number;\n backgroundHeight: number;\n}\n\n/**\n * The `mj-hero` sizing for a hero in a box `boxWidth` px wide. Every hero's\n * mode, height and image size come from here, so an Outlook rendering\n * finding changes this function and nothing else.\n *\n * Every hero is `fixed-height`: classic Outlook on Windows ignores\n * `fluid-height` (its spacer cells carry `mso-padding-bottom-alt:0`) and\n * shrinks the box to its content, showing only the top of the image.\n *\n * - `height: \"image\"` → the image's own height at the box width, so the box\n * matches the image on desktop. A phone keeps that height and crops the\n * image's sides to cover the narrower box.\n * - a number → that height.\n *\n * Either is at least the top and bottom `padding`, each a whole pixel as MJML\n * reads it: MJML sizes the cell as the height less that padding and would\n * write a negative height for a shorter box.\n *\n * Outlook draws the image as a `v:image` of exactly `backgroundWidth ×\n * backgroundHeight` px from the box's top, clipped to the box, so the width is\n * always the box's and the height is the image's own at that width. An image\n * with no known size takes the fixed box's height, which fills the box and\n * stretches the image to it.\n */\nexport function heroGeometry(\n hero: AppliedHero,\n boxWidth: number,\n padding: SpacingValue,\n): HeroGeometry {\n const backgroundWidth = Math.max(1, Math.round(boxWidth));\n const imageHeight =\n hero.aspectRatio === null\n ? null\n : Math.max(1, Math.round(backgroundWidth * hero.aspectRatio));\n\n // resolveHero applies \"image\" only with a known ratio.\n const wanted =\n hero.height === \"image\" ? (imageHeight ?? backgroundWidth) : hero.height;\n const height = Math.max(\n wanted,\n Math.trunc(padding.top) + Math.trunc(padding.bottom),\n );\n\n return {\n mode: \"fixed-height\",\n height,\n backgroundWidth,\n backgroundHeight: imageHeight ?? height,\n };\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Wrap rendered block markup in the block's liquid display-condition guards\n * (`<mj-raw>{% if %}</mj-raw>` … `<mj-raw>{% endif %}</mj-raw>`), if present.\n *\n * Returns the input unchanged when the block has no display condition, and an\n * empty string when the rendered markup is empty (a hidden block) so callers\n * can keep using an `=== \"\"` filter to drop it.\n *\n * Used for BOTH top-level blocks (`index.ts`) and blocks nested inside section\n * columns (`renderers/section.ts`). A condition on a nested block must emit the\n * same guards as a top-level one — otherwise conditional content placed inside\n * a multi-column section renders unconditionally for every recipient.\n */\nexport function wrapWithDisplayCondition(\n block: Block,\n rendered: string,\n): string {\n if (rendered === \"\") {\n return \"\";\n }\n\n const displayCondition = block.displayCondition;\n\n if (!displayCondition) {\n return rendered;\n }\n\n return (\n `<mj-raw>${displayCondition.before}</mj-raw>` +\n \"\\n\" +\n rendered +\n \"\\n\" +\n `<mj-raw>${displayCondition.after}</mj-raw>`\n );\n}\n","import type { AppliedHero, Block, SectionBlock } from \"@templatical/types\";\nimport { isSection, resolveHero } from \"@templatical/types\";\nimport type { BlockRenderer, RenderContext } from \"../render-context\";\nimport { getWidthPercentages, getWidthPixels } from \"../columns\";\nimport { escapeAttr } from \"../escape\";\nimport { heroGeometry } from \"../hero-geometry\";\nimport { toPaddingString } from \"../padding\";\nimport {\n backgroundImageAttrs,\n bgAttr,\n borderAttr,\n borderRadiusAttr,\n horizontalInset,\n} from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\nimport { wrapWithDisplayCondition } from \"../display-condition\";\n\n// `BlockRenderer` is defined in `../render-context`, next to the `blockRenderers`\n// override map it types. Re-exported here so the package's\n// `export type { BlockRenderer } from \"./renderers/section\"` entry point resolves\n// for consumers.\nexport type { BlockRenderer };\n\n/**\n * Render a section block with columns to MJML markup.\n */\nexport function renderSection(\n block: SectionBlock,\n context: RenderContext,\n renderBlock: BlockRenderer,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const hero = resolveHero(block);\n if (hero?.applied) {\n return renderHero(block, hero, context, renderBlock);\n }\n\n const columnsLayout = block.columns;\n const columnWidths = getWidthPercentages(columnsLayout);\n const columnWidthsPx = getWidthPixels(columnsLayout, context.containerWidth);\n // What MJML gives each column's content: the emitted percentage of the room\n // inside the section's padding and border, floored as MJML floors it. From\n // the percentage strings, not `columnWidthsPx`: 33.33% of 600 is 199, a\n // third is 200.\n const sectionBox =\n Math.floor(context.contentWidth) -\n horizontalInset(block.styles.padding, block.border);\n const columnContentWidths = columnWidths.map((width) =>\n Math.floor((sectionBox * parseFloat(width)) / 100),\n );\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"native\");\n const bgImage = backgroundImageAttrs(block.backgroundImage);\n const visibilityAttr = getCssClassAttr(block);\n const borderRadiusAttrStr = borderRadiusAttr(block.borderRadius);\n const borderAttrStr = borderAttr(block.border);\n // MJML's `mj-section`, `mj-group` and `mj-column` each default to\n // `direction=\"ltr\"` and inline it on their own element, so an RTL email\n // needs it on all three — the column's inline style otherwise resets the\n // text inside to LTR under a section that reverses the column order.\n const dirAttr = context.contentDirection === \"rtl\" ? ' direction=\"rtl\"' : \"\";\n\n const children = block.children;\n const columnsContent: string[] = [];\n\n for (let index = 0; index < children.length; index++) {\n const column = children[index];\n const width = columnWidths[index] ?? \"100%\";\n const columnWidth = Math.floor(\n columnWidthsPx[index] ?? context.containerWidth,\n );\n\n // Sections cannot be nested inside columns per MJML spec — drop any\n // SectionBlocks that landed inside a column (via tampered JSON or a\n // future API). Keeping them would emit `<mj-section>` inside\n // `<mj-column>`, which mjml@5 rejects with a hard error.\n const filteredColumn = filterHtmlBlocks(\n column,\n context.allowHtmlBlocks,\n ).filter((child) => !isSection(child));\n const columnContext = context.withContainerWidth(\n columnWidth,\n columnContentWidths[index] ?? sectionBox,\n );\n\n const columnBlocks = filteredColumn\n .map((child) =>\n wrapWithDisplayCondition(child, renderBlock(child, columnContext)),\n )\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n const content =\n columnBlocks === \"\" ? \"<mj-text> </mj-text>\" : columnBlocks;\n\n columnsContent.push(`<mj-column width=\"${width}\"${dirAttr}>\n${content}\n</mj-column>`);\n }\n\n const columns = columnsContent.join(\"\\n\");\n\n // Opting out of mobile stacking wraps the columns in an `<mj-group>`, which\n // keeps them side-by-side below 480px instead of stacking (MJML's default).\n // `mj-group` requires percentage column widths — `getWidthPercentages`\n // already guarantees that. Only meaningful with 2+ columns.\n const body =\n block.stackOnMobile === false && columnsContent.length > 1\n ? `<mj-group${dirAttr}>\n${columns}\n</mj-group>`\n : columns;\n\n return `<mj-section${bgColor}${bgImage} padding=\"${padding}\"${borderAttrStr}${borderRadiusAttrStr}${visibilityAttr}${dirAttr}>\n${body}\n</mj-section>`;\n}\n\n/**\n * Render a section `resolveHero` applies as an `<mj-hero>`. The content is\n * column 0's blocks, filtered as a column's are; mj-hero takes content\n * elements directly, with no `mj-column`.\n *\n * The hero's box is the room its bands leave it, `context.contentWidth`\n * floored as MJML floors every band; Outlook draws the image at exactly that\n * width. Its content renders `\"full\"` at the box and gets the box less the\n * hero's own side padding as its room, as mj-hero gives it: no border comes\n * off, since a hero draws none.\n *\n * Left out on purpose: `border` (mj-hero has none), `background-size` and\n * `background-repeat` (it always paints `cover`, unrepeated) and `direction`\n * (it inlines none, so its text keeps the body's `dir`). An unset fill is\n * `transparent`, since mj-hero's own default is white.\n */\nfunction renderHero(\n block: SectionBlock,\n hero: AppliedHero,\n context: RenderContext,\n renderBlock: BlockRenderer,\n): string {\n const boxWidth = Math.floor(context.contentWidth);\n const geometry = heroGeometry(hero, boxWidth, block.styles.padding);\n const bgColor =\n bgAttr(block.styles.backgroundColor, \"native\") ||\n ' background-color=\"transparent\"';\n const padding = toPaddingString(block.styles.padding);\n const borderRadiusAttrStr = borderRadiusAttr(block.borderRadius);\n const visibilityAttr = getCssClassAttr(block);\n const contentContext = context.withContainerWidth(\n boxWidth,\n boxWidth - horizontalInset(block.styles.padding),\n );\n\n const content = filterHtmlBlocks(\n block.children[0] ?? [],\n context.allowHtmlBlocks,\n )\n .filter((child) => !isSection(child))\n .map((child) =>\n wrapWithDisplayCondition(child, renderBlock(child, contentContext)),\n )\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n return `<mj-hero mode=\"${geometry.mode}\" height=\"${geometry.height}px\" background-url=\"${escapeAttr(hero.image.url)}\" background-width=\"${geometry.backgroundWidth}px\" background-height=\"${geometry.backgroundHeight}px\" background-position=\"${escapeAttr(hero.image.position)}\"${bgColor} padding=\"${padding}\" vertical-align=\"${escapeAttr(hero.verticalAlign)}\"${borderRadiusAttrStr}${visibilityAttr}>\n${content}\n</mj-hero>`;\n}\n\n/**\n * Filter out HTML blocks if they are not allowed.\n */\nfunction filterHtmlBlocks(blocks: Block[], allowHtmlBlocks: boolean): Block[] {\n if (allowHtmlBlocks) {\n return blocks;\n }\n\n return blocks.filter((block) => block.type !== \"html\");\n}\n","import type { VideoBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr, fluidOnMobileAttr, heightAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Extract video thumbnail URL from common platforms.\n * Works without server-side processing — YouTube and Vimeo thumbnails are publicly accessible.\n */\nfunction getVideoThumbnail(\n url: string,\n customThumbnail?: string,\n): string | null {\n if (customThumbnail) {\n return customThumbnail;\n }\n\n if (!url) {\n return null;\n }\n\n // YouTube\n const youtubePatterns = [\n /(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/|youtube\\.com\\/embed\\/)([a-zA-Z0-9_-]{11})/,\n /youtube\\.com\\/shorts\\/([a-zA-Z0-9_-]{11})/,\n ];\n\n for (const pattern of youtubePatterns) {\n const match = url.match(pattern);\n if (match) {\n return `https://img.youtube.com/vi/${match[1]}/maxresdefault.jpg`;\n }\n }\n\n // Vimeo\n const vimeoMatch = url.match(/vimeo\\.com\\/(?:video\\/)?(\\d+)/);\n if (vimeoMatch) {\n return `https://vumbnail.com/${vimeoMatch[1]}.jpg`;\n }\n\n return null;\n}\n\n/**\n * Render a video block to MJML markup.\n * Videos in email are rendered as a linked thumbnail image pointing to the video URL.\n */\nexport function renderVideo(block: VideoBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const thumbnailUrl = getVideoThumbnail(block.url, block.thumbnailUrl);\n\n if (!thumbnailUrl) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const width = escapeAttr(\n block.width === \"full\" ? context.containerWidth + \"px\" : block.width + \"px\",\n );\n const height = heightAttr(block.height);\n const fluid = fluidOnMobileAttr(block, context);\n const visibilityAttr = getCssClassAttr(block);\n\n const src = escapeAttr(thumbnailUrl);\n const alt = escapeAttr(block.alt);\n const align = escapeAttr(block.align);\n const href = escapeAttr(block.url);\n\n return `<mj-image\n src=\"${src}\"\n alt=\"${alt}\"\n width=\"${width}\"${height}${fluid}\n align=\"${align}\"\n padding=\"${padding}\"\n href=\"${href}\"\n target=\"_blank\"\n rel=\"noopener\"${bgColor}${visibilityAttr}\n/>`;\n}\n","import type { Block } from \"@templatical/types\";\nimport {\n isSection,\n isTitle,\n isParagraph,\n isImage,\n isButton,\n isDivider,\n isSpacer,\n isHtml,\n isSocialIcons,\n isMenu,\n isTable,\n isVideo,\n isCustomBlock,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { isHiddenOnAll } from \"../visibility\";\nimport { renderUnrenderableBlock } from \"../unrenderable\";\nimport { renderTitle } from \"./title\";\nimport { renderParagraph } from \"./paragraph\";\nimport { renderImage } from \"./image\";\nimport { renderButton } from \"./button\";\nimport { renderDivider } from \"./divider\";\nimport { renderSpacer } from \"./spacer\";\nimport { renderHtml } from \"./html\";\nimport { renderSocialIcons } from \"./social\";\nimport { renderMenu } from \"./menu\";\nimport { renderTable } from \"./table\";\nimport { renderCustom } from \"./custom\";\nimport { renderSection } from \"./section\";\nimport { renderVideo } from \"./video\";\n\n/**\n * Render a single block to MJML markup.\n * Dispatches to the appropriate block-type renderer.\n *\n * A `blockRenderers` override for the block's type wins over the built-in\n * renderer — see {@link RenderContext.blockRenderers}. A type with neither gets a\n * placeholder marker plus a warning rather than disappearing.\n */\nexport function renderBlock(block: Block, context: RenderContext): string {\n // Checked first so an override replaces the built-in wholesale rather than\n // running after it. The override then owns everything the built-in did,\n // including the hidden-on-all-viewports early return.\n const override = context.blockRenderers[block.type];\n if (override) {\n return override(block, context);\n }\n\n if (isSection(block)) {\n return renderSection(block, context, renderBlock);\n }\n\n if (isTitle(block)) {\n return renderTitle(block, context);\n }\n\n if (isParagraph(block)) {\n return renderParagraph(block, context);\n }\n\n if (isImage(block)) {\n return renderImage(block, context);\n }\n\n if (isButton(block)) {\n return renderButton(block, context);\n }\n\n if (isDivider(block)) {\n return renderDivider(block, context);\n }\n\n if (isSpacer(block)) {\n return renderSpacer(block, context);\n }\n\n if (isHtml(block)) {\n return renderHtml(block, context);\n }\n\n if (isSocialIcons(block)) {\n return renderSocialIcons(block, context);\n }\n\n if (isMenu(block)) {\n return renderMenu(block, context);\n }\n\n if (isTable(block)) {\n return renderTable(block, context);\n }\n\n if (isVideo(block)) {\n return renderVideo(block, context);\n }\n\n if (isCustomBlock(block)) {\n return renderCustom(block, context);\n }\n\n // Nothing here knows how to render this type. A block the author hid on every\n // viewport is meant to produce nothing, so it still does — every built-in\n // renderer above makes the same call, and warning about a deliberate hide would\n // be noise. Everything else gets a marker: `countdown` today (Cloud renders it\n // server-side as an animated GIF, which no browser can do), and any future type\n // whose renderer hasn't landed yet.\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n return renderUnrenderableBlock(block);\n}\n","import type {\n Block,\n ContentDirection,\n CustomBlock,\n CustomBlockDefinition,\n TemplateContent,\n CustomFont,\n SectionWrapper,\n SpacingValue,\n} from \"@templatical/types\";\nimport {\n applyLayout,\n defaultLocaleOf,\n assertNoSlotInContent,\n assertNoWrapperInContent,\n createCustomBlockLiquid,\n isCustomBlock,\n isSection,\n isWrapper,\n resolveContentDirection,\n resolveLocale,\n} from \"@templatical/types\";\nimport { RenderContext, DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nimport type { BlockRendererMap } from \"./render-context\";\nimport { renderBlock } from \"./renderers\";\nimport {\n escapeHtml,\n escapeAttr,\n escapeCssValue,\n escapeFontFamily,\n} from \"./escape\";\nimport { wrapWithDisplayCondition } from \"./display-condition\";\nimport {\n backgroundImageAttrs,\n bgAttr,\n borderRadiusAttr,\n horizontalInset,\n} from \"./utils\";\nimport { richTextStylesheet, collectParagraphGaps } from \"./rich-text\";\nimport { BUTTON_FULL_WIDTH_CSS_CLASS } from \"./renderers/button\";\nimport { toPaddingString } from \"./padding\";\n\nexport interface RenderOptions {\n /** Template language to render. Defaults to `content.settings.locale`. */\n locale?: string;\n customFonts?: CustomFont[];\n defaultFallbackFont?: string;\n allowHtmlBlocks?: boolean;\n /** Headless custom block definitions. The last definition per type wins. */\n customBlocks?: CustomBlockDefinition[];\n /**\n * Resolves custom blocks to their HTML representation. Called once per\n * custom block in the content tree before MJML rendering. The renderer\n * accepts `customBlocks` for headless rendering; this callback is the\n * editor's integration path.\n *\n * Editor consumers: pass `editor.renderCustomBlock`.\n *\n * With neither this callback nor `customBlocks`, each custom block uses\n * `block.renderedHtml` (if present) and is otherwise omitted.\n */\n renderCustomBlock?: (block: CustomBlock) => Promise<string>;\n /**\n * Per-block-type renderer overrides, keyed by `block.type`. An entry replaces\n * the built-in renderer for that type wholesale — including its\n * hidden-on-all-viewports check, which becomes the override's responsibility.\n *\n * ```ts\n * renderToMjml(content, {\n * blockRenderers: {\n * countdown: (block) => `<mj-image src=\"${countdownGifUrl(block)}\" />`,\n * video: (block, ctx) => renderVideoWithPlayButton(block, ctx),\n * },\n * });\n * ```\n *\n * This generalises {@link renderCustomBlock}, which is the same idea for a\n * single block type. It exists so a backend whose render output is a *superset*\n * of the browser's — a server-generated countdown GIF, a composited video\n * thumbnail — can inject exactly that delta instead of forking the renderer:\n * parity for every other block type then holds by construction.\n *\n * A block type with neither a built-in renderer nor an override emits an\n * `mj-raw` placeholder comment and warns, rather than vanishing from the\n * output.\n */\n blockRenderers?: BlockRendererMap;\n /**\n * Resolves the definition-level CSS for a custom block type. Called once\n * per unique `customType` present in the content tree (not per instance).\n * The non-empty results are deduped by content and emitted as additional\n * `<mj-style>` blocks inside `<mj-head>` alongside the built-in visibility\n * media queries.\n *\n * Editor consumers: pass a function that reads\n * `blockRegistry.getDefinition(customType)?.stylesheet`.\n *\n * Headless consumers can pass `customBlocks` instead. A listed definition\n * decides its type's CSS; no stylesheet means none is emitted.\n *\n * Without a resolver or matching definition, each type's saved\n * `block.renderedStylesheet` is emitted. A resolver decides every type and\n * ignores saved copies, so a stale one never overrides a live definition.\n */\n getCustomBlockStylesheet?: (customType: string) => string | undefined | null;\n /**\n * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png` per icon. Defaults to the\n * version-pinned jsDelivr mirror of this package. Override to self-host\n * (e.g., behind your own CDN or for air-gapped environments).\n *\n * Why PNGs: Outlook desktop (Word rendering engine) does not support SVG\n * and rejects base64 data URIs in `<img src>`, so social icons must be\n * served as raster images over HTTP for cross-client compatibility.\n */\n socialIconsBaseUrl?: string;\n /**\n * Embedder shell applied at render. Cloned and spliced around `content`\n * via `applyLayout`; `content` is not mutated. Omitted, the document is\n * rendered as authored and must not contain `slot` or `wrapper`.\n */\n layout?: TemplateContent;\n}\n\n/**\n * Makes a full-width button's whole fill clickable. MJML sizes a button's `<a>`\n * only for a px width, so without this rule the link of a `100%` button stays\n * the size of its label.\n *\n * It sits in the inline `<mj-style>` so `mjml`'s juice pass writes it onto the\n * `<a>`; MRML's default build emits that block as a plain `<style>` instead.\n * `!important` lets it beat MJML's inline `display:inline-block` either way.\n * Every template carries it, so nothing has to walk the tree for full-width\n * buttons: juice drops a rule that matches nothing, so with `mjml` it costs\n * nothing in the compiled HTML.\n */\nconst BUTTON_FULL_WIDTH_RULE = `.${BUTTON_FULL_WIDTH_CSS_CLASS} a { display: block !important; }`;\n\n/**\n * Render template content to an MJML string.\n *\n * With `customBlocks`, this imports the optional `liquidjs` peer only when\n * the content holds a custom block. The editor uses `renderCustomBlock` via\n * `toMjml()`; headless callers can pass definitions directly. The function\n * always returns a Promise.\n */\nexport async function renderToMjml(\n content: TemplateContent,\n options?: RenderOptions,\n): Promise<string> {\n if (\n options?.customBlocks !== undefined &&\n options.renderCustomBlock !== undefined\n ) {\n throw new TypeError(\n \"[Templatical] customBlocks cannot be combined with renderCustomBlock\",\n );\n }\n if (\n options?.customBlocks !== undefined &&\n options.getCustomBlockStylesheet !== undefined\n ) {\n throw new TypeError(\n \"[Templatical] customBlocks cannot be combined with getCustomBlockStylesheet\",\n );\n }\n\n const locale = options?.locale ?? defaultLocaleOf(content);\n const localized = resolveLocale(content, locale);\n const toRender = options?.layout\n ? applyLayout(\n resolveLocale(options.layout, locale, { missing: \"default\" }),\n localized,\n )\n : (assertNoSlotInContent(localized),\n assertNoWrapperInContent(localized),\n localized);\n\n const customFonts = options?.customFonts ?? [];\n const defaultFallbackFont =\n options?.defaultFallbackFont ?? \"Arial, sans-serif\";\n const allowHtmlBlocks = options?.allowHtmlBlocks ?? true;\n const socialIconsBaseUrl = stripTrailingSlash(\n options?.socialIconsBaseUrl ?? DEFAULT_SOCIAL_ICONS_BASE_URL,\n );\n\n const customBlockHtml =\n options?.customBlocks !== undefined\n ? await resolveCustomBlockDefinitions(toRender, options.customBlocks)\n : await resolveCustomBlocks(toRender, options?.renderCustomBlock);\n\n const customBlockStylesheets = collectCustomBlockStylesheets(\n toRender,\n options?.getCustomBlockStylesheet,\n options?.customBlocks === undefined\n ? undefined\n : new Map(\n options.customBlocks.map((definition) => [\n definition.type,\n definition,\n ]),\n ),\n );\n\n const contentDirection = resolveContentDirection(toRender.settings);\n\n const renderContext = new RenderContext(\n toRender.settings.width,\n customFonts,\n defaultFallbackFont,\n allowHtmlBlocks,\n customBlockHtml,\n socialIconsBaseUrl,\n options?.blockRenderers ?? {},\n contentDirection,\n );\n\n const blocks = filterHtmlBlocks(toRender.blocks, allowHtmlBlocks);\n const fontFamily = renderContext.resolveFontFamily(\n toRender.settings.fontFamily,\n );\n // MJML copies the body colour into compiled HTML styles. Markup in a colour\n // has no valid CSS meaning and must not survive into that output.\n const backgroundColor = /[<>]/.test(toRender.settings.backgroundColor)\n ? \"transparent\"\n : toRender.settings.backgroundColor;\n // Document-level default text color → the `<mj-text>` default below. Blocks\n // that set their own color (e.g. Title) or carry inline text-color marks\n // override it. Omitted when unset so existing templates render identically.\n const textColorAttr = toRender.settings.textColor\n ? ` color=\"${escapeAttr(toRender.settings.textColor)}\"`\n : \"\";\n // Document-level link styling → the global `a { … }` rule below, cascading to\n // every link (rich-text and menu). An unset `linkColor` keeps `color: inherit`\n // so links follow the text color and legacy content renders identically; a\n // per-link/per-item color still overrides (inline styles beat this rule).\n const linkColor = toRender.settings.linkColor\n ? escapeCssValue(toRender.settings.linkColor)\n : \"inherit\";\n const linkDecoration = toRender.settings.linkUnderline ? \"underline\" : \"none\";\n\n const bodyContent = blocks\n .map((block) => renderTopLevelBlock(block, renderContext))\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n const fontDeclarations = generateFontDeclarations(customFonts);\n const previewTag = generatePreviewTag(toRender.settings.preheaderText);\n\n const lang = escapeAttr(toRender.settings.locale);\n const dir = escapeAttr(contentDirection);\n\n return `<mjml lang=\"${lang}\" dir=\"${dir}\">\n <mj-head>${previewTag}\n <mj-attributes>\n <mj-all font-family=\"${escapeFontFamily(fontFamily)}\" />\n <mj-text font-size=\"14px\"${textColorAttr} />\n <mj-section padding=\"0\" />\n <mj-column padding=\"0\" />\n </mj-attributes>${fontDeclarations}\n <mj-style inline=\"inline\">\n ${richTextStylesheet(collectParagraphGaps(blocks), contentDirection)}\n ${BUTTON_FULL_WIDTH_RULE}\n </mj-style>\n <mj-style>\n a { color: ${linkColor}; text-decoration: ${linkDecoration}; }\n @media only screen and (max-width: 480px) {\n .tpl-hide-mobile { display: none !important; mso-hide: all !important; }\n }\n @media only screen and (min-width: 481px) {\n .tpl-hide-desktop { display: none !important; mso-hide: all !important; }\n }\n </mj-style>${renderCustomBlockStylesheets(customBlockStylesheets)}\n </mj-head>\n <mj-body width=\"${escapeAttr(String(renderContext.containerWidth))}px\" background-color=\"${escapeCssValue(backgroundColor)}\">\n${bodyContent}\n </mj-body>\n</mjml>`;\n}\n\n/**\n * Render a top-level block. Wrappers emit `mj-wrapper` around their children.\n * Sections are rendered directly; other blocks are wrapped in a default\n * section/column.\n */\nfunction renderTopLevelBlock(block: Block, context: RenderContext): string {\n if (isWrapper(block)) {\n const innerContext = insideWrapper(context, block.styles.padding);\n const inner = filterHtmlBlocks(block.children, context.allowHtmlBlocks)\n .map((child) => renderTopLevelBlock(child, innerContext))\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n const framed = renderMjWrapper(\n inner,\n {\n backgroundColor: block.styles.backgroundColor,\n padding: block.styles.padding,\n borderRadius: block.borderRadius,\n backgroundImage: block.backgroundImage,\n },\n context.contentDirection,\n );\n return wrapWithDisplayCondition(block, framed);\n }\n\n if (isSection(block)) {\n const rendered = renderBlock(\n block,\n block.wrapper ? insideWrapper(context, block.wrapper.padding) : context,\n );\n // An empty render (hidden section) stays empty — never emit a bare wrapper.\n const framed =\n block.wrapper && rendered !== \"\"\n ? renderMjWrapper(rendered, block.wrapper, context.contentDirection)\n : rendered;\n return wrapWithDisplayCondition(block, framed);\n }\n\n const content = renderBlock(block, context);\n const wrapped = wrapInSection(content, context.contentDirection);\n return wrapWithDisplayCondition(block, wrapped);\n}\n\n/**\n * The context for what an `mj-wrapper` holds: MJML narrows its children by the\n * wrapper's side padding. `containerWidth` stays the nominal share of the body.\n */\nfunction insideWrapper(\n context: RenderContext,\n padding: SpacingValue | undefined,\n): RenderContext {\n return context.withContainerWidth(\n context.containerWidth,\n Math.floor(context.contentWidth) - horizontalInset(padding),\n );\n}\n\n/**\n * `mj-wrapper` attributes shared by a layout `wrapper` block and\n * `section.wrapper`. Padding is always explicit: MJML's wrapper default is\n * `20px 0`, ours is 0. Direction is explicit for the same reason: MJML's\n * default `ltr` would otherwise be inlined on the band of an RTL email.\n */\nfunction renderMjWrapper(\n inner: string,\n wrapper: SectionWrapper,\n direction: ContentDirection,\n): string {\n const bg = bgAttr(wrapper.backgroundColor, \"native\");\n const bgImage = backgroundImageAttrs(wrapper.backgroundImage);\n const padding = ` padding=\"${\n wrapper.padding ? toPaddingString(wrapper.padding) : \"0\"\n }\"`;\n const radius = borderRadiusAttr(wrapper.borderRadius);\n const dirAttr = direction === \"rtl\" ? ' direction=\"rtl\"' : \"\";\n return `<mj-wrapper${bg}${bgImage}${padding}${radius}${dirAttr}>\n${inner}\n</mj-wrapper>`;\n}\n\n/**\n * Wrap block content in a default mj-section/mj-column for non-section blocks.\n */\nfunction wrapInSection(content: string, direction: ContentDirection): string {\n if (content === \"\") {\n return \"\";\n }\n\n const dirAttr = direction === \"rtl\" ? ' direction=\"rtl\"' : \"\";\n return `<mj-section${dirAttr}>\n <mj-column${dirAttr}>\n${content}\n </mj-column>\n</mj-section>`;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\nfunction generatePreviewTag(preheaderText?: string): string {\n if (!preheaderText) {\n return \"\";\n }\n\n const trimmed = preheaderText.trim();\n\n if (trimmed === \"\") {\n return \"\";\n }\n\n const escaped = escapeHtml(trimmed);\n\n return `\\n <mj-preview>${escaped}</mj-preview>`;\n}\n\nfunction generateFontDeclarations(customFonts: CustomFont[]): string {\n if (customFonts.length === 0) {\n return \"\";\n }\n\n return customFonts\n .map(\n (font) =>\n `\\n <mj-font name=\"${escapeAttr(font.name)}\" href=\"${escapeAttr(font.url)}\" />`,\n )\n .join(\"\");\n}\n\n/**\n * Filter out HTML blocks if they are not allowed.\n */\nfunction filterHtmlBlocks(blocks: Block[], allowHtmlBlocks: boolean): Block[] {\n if (allowHtmlBlocks) {\n return blocks;\n }\n\n return blocks.filter((block) => block.type !== \"html\");\n}\n\n/**\n * Walk the content tree, collect every custom block, then resolve each in\n * parallel via the supplied callback. Returns a map keyed by block id that\n * the synchronous render pass reads from. If no callback is provided, returns\n * an empty map and the sync pass falls back to `block.renderedHtml`.\n *\n * Per-block failures bubble up — the caller decides whether to swallow or\n * rethrow. We don't replace failures with placeholders here because that's\n * a policy decision (the editor swallows; a strict CLI may want to fail).\n */\nasync function resolveCustomBlocks(\n content: TemplateContent,\n renderCustomBlock: RenderOptions[\"renderCustomBlock\"],\n): Promise<Map<string, string>> {\n const result = new Map<string, string>();\n\n if (!renderCustomBlock) {\n return result;\n }\n\n const customBlocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, customBlocks);\n\n if (customBlocks.length === 0) {\n return result;\n }\n\n const rendered = await Promise.all(\n customBlocks.map((block) => renderCustomBlock(block)),\n );\n\n for (let index = 0; index < customBlocks.length; index++) {\n result.set(customBlocks[index].id, rendered[index]);\n }\n\n return result;\n}\n\n/** One Liquid engine serves every definition in this render call. Template\n * errors propagate as callback rejections do. */\nasync function resolveCustomBlockDefinitions(\n content: TemplateContent,\n definitions: CustomBlockDefinition[],\n): Promise<Map<string, string>> {\n const result = new Map<string, string>();\n const blocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, blocks);\n if (blocks.length === 0) return result;\n\n let liquidjs: typeof import(\"liquidjs\");\n try {\n liquidjs = await import(\"liquidjs\");\n } catch {\n throw new Error(\n \"[Templatical] customBlocks requires the optional liquidjs package.\",\n );\n }\n const liquid = createCustomBlockLiquid(liquidjs);\n const byType = new Map(\n definitions.map((definition) => [definition.type, definition]),\n );\n const rendered = await Promise.all(\n blocks.map(async (block) => {\n const definition = byType.get(block.customType);\n return definition\n ? String(\n await liquid.parseAndRender(definition.template, block.fieldValues),\n )\n : undefined;\n }),\n );\n for (let index = 0; index < blocks.length; index++) {\n const html = rendered[index];\n if (html !== undefined) result.set(blocks[index].id, html);\n }\n return result;\n}\n\nfunction collectCustomBlocks(blocks: Block[], out: CustomBlock[]): void {\n for (const block of blocks) {\n if (isCustomBlock(block)) {\n out.push(block);\n continue;\n }\n\n if (isSection(block)) {\n for (const column of block.children) {\n collectCustomBlocks(column, out);\n }\n } else if (isWrapper(block)) {\n collectCustomBlocks(block.children, out);\n }\n }\n}\n\n/**\n * Walk the content tree, find every unique `customType`, ask the consumer's\n * resolver for that definition's stylesheet (or, with no resolver, take the\n * first `renderedStylesheet` a block of that type carries), and return the\n * non-empty, content-deduped set in insertion order.\n *\n * Content-level dedupe (not just by customType) means two definitions that\n * happen to ship the same stylesheet string emit it only once — cheap and\n * matches the \"one rule, emitted once\" mental model. Whitespace-only and\n * empty stylesheets are skipped.\n */\nfunction collectCustomBlockStylesheets(\n content: TemplateContent,\n resolver: RenderOptions[\"getCustomBlockStylesheet\"],\n definitions?: Map<string, CustomBlockDefinition>,\n): string[] {\n const customBlocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, customBlocks);\n\n if (customBlocks.length === 0) {\n return [];\n }\n\n // Without a resolver, the first copy each type carries stands in for it.\n const copies = new Map<string, string>();\n if (!resolver) {\n for (const block of customBlocks) {\n if (block.renderedStylesheet && !copies.has(block.customType)) {\n copies.set(block.customType, block.renderedStylesheet);\n }\n }\n }\n\n const seenTypes = new Set<string>();\n const seenContent = new Set<string>();\n const stylesheets: string[] = [];\n\n for (const block of customBlocks) {\n if (seenTypes.has(block.customType)) {\n continue;\n }\n seenTypes.add(block.customType);\n\n const definition = definitions?.get(block.customType);\n const css = resolver\n ? resolver(block.customType)\n : definition\n ? definition.stylesheet\n : copies.get(block.customType);\n if (!css) {\n continue;\n }\n\n const trimmed = css.trim();\n if (trimmed === \"\" || seenContent.has(trimmed)) {\n continue;\n }\n seenContent.add(trimmed);\n stylesheets.push(trimmed);\n }\n\n return stylesheets;\n}\n\nfunction renderCustomBlockStylesheets(stylesheets: string[]): string {\n if (stylesheets.length === 0) {\n return \"\";\n }\n\n // One `<mj-style>` per unique stylesheet so per-definition CSS can be\n // independently inspected in the rendered MJML, and authors can grep their\n // contribution without disentangling a merged blob.\n return stylesheets\n .map((css) => `\\n <mj-style>\\n${css}\\n </mj-style>`)\n .join(\"\");\n}\n\n// Re-export utilities for consumers\nexport { RenderContext } from \"./render-context\";\nexport { escapeHtml, escapeAttr, convertMergeTagsToValues } from \"./escape\";\nexport { isHiddenOnAll, getCssClassAttr, getCssClasses } from \"./visibility\";\n// Exported for `blockRenderers` consumers replacing the paragraph or title\n// renderer: without both classes on their `mj-text`, their block silently\n// loses the spacing rules and drifts back to MJML's 13px.\nexport {\n RICH_TEXT_CSS_CLASS,\n richTextGapClass,\n resolveParagraphGap,\n} from \"./rich-text\";\n// Exported for `blockRenderers` consumers replacing the button renderer: a\n// full-width button whose `mj-button` lacks this class is clickable only on\n// its label.\nexport { BUTTON_FULL_WIDTH_CSS_CLASS } from \"./renderers/button\";\n// Exported for `blockRenderers` consumers that emit their own `mj-image` (a\n// video thumbnail, a countdown GIF): `fluid-on-mobile` is set per image, so an\n// `mj-image` without it keeps its desktop width on a phone.\nexport { fluidOnMobileAttr } from \"./utils\";\nexport { getWidthPercentages, getWidthPixels } from \"./columns\";\nexport { toPaddingString } from \"./padding\";\nexport { DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nexport { renderBlock } from \"./renderers\";\nexport {\n renderUnrenderableBlock,\n UNRENDERABLE_MARKER_PREFIX,\n} from \"./unrenderable\";\nexport type { BlockRenderer, BlockRendererMap } from \"./render-context\";\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@templatical/renderer",
|
|
3
3
|
"description": "Render Templatical email templates to MJML",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.55.0",
|
|
5
5
|
"bugs": "https://github.com/templatical/sdk/issues",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@templatical/types": "0.
|
|
7
|
+
"@templatical/types": "0.55.0"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@jsquash/oxipng": "^2.3.0",
|
|
11
11
|
"@resvg/resvg-js": "^2.6.2",
|
|
12
12
|
"liquidjs": "^10.30.1",
|
|
13
13
|
"mjml": "^5.4.1",
|
|
14
|
+
"mjml-browser": "^5.4.1",
|
|
14
15
|
"typescript": "^6.0.3",
|
|
15
16
|
"vitest": "^5.0.5"
|
|
16
17
|
},
|