@aswin.dev/renderer 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1221 @@
1
+ // src/index.ts
2
+ import { isSection as isSection3, isCustomBlock as isCustomBlock2 } from "@templatical/types";
3
+
4
+ // src/render-context.ts
5
+ var BUILT_IN_FONT_FALLBACKS = {
6
+ arial: "Arial, sans-serif",
7
+ helvetica: "Helvetica, sans-serif",
8
+ georgia: "Georgia, serif",
9
+ "times new roman": "'Times New Roman', serif",
10
+ verdana: "Verdana, sans-serif",
11
+ "trebuchet ms": "'Trebuchet MS', sans-serif",
12
+ "courier new": "'Courier New', monospace",
13
+ tahoma: "Tahoma, sans-serif"
14
+ };
15
+ var RenderContext = class _RenderContext {
16
+ constructor(containerWidth, customFonts, defaultFallbackFont, allowHtmlBlocks, customBlockHtml = /* @__PURE__ */ new Map()) {
17
+ this.containerWidth = containerWidth;
18
+ this.customFonts = customFonts;
19
+ this.defaultFallbackFont = defaultFallbackFont;
20
+ this.allowHtmlBlocks = allowHtmlBlocks;
21
+ this.customBlockHtml = customBlockHtml;
22
+ }
23
+ containerWidth;
24
+ customFonts;
25
+ defaultFallbackFont;
26
+ allowHtmlBlocks;
27
+ customBlockHtml;
28
+ /**
29
+ * Create a new context with a different container width.
30
+ * Used when rendering columns with narrower widths.
31
+ */
32
+ withContainerWidth(width) {
33
+ return new _RenderContext(
34
+ width,
35
+ this.customFonts,
36
+ this.defaultFallbackFont,
37
+ this.allowHtmlBlocks,
38
+ this.customBlockHtml
39
+ );
40
+ }
41
+ /**
42
+ * Resolve a font family name to include custom font fallbacks.
43
+ * If the font matches a custom font, returns `'FontName', fallback`.
44
+ * Otherwise returns the original font family string.
45
+ */
46
+ resolveFontFamily(fontFamily) {
47
+ for (const customFont of this.customFonts) {
48
+ if (customFont.name.toLowerCase() === fontFamily.toLowerCase()) {
49
+ const fallback = customFont.fallback ?? this.defaultFallbackFont;
50
+ return `'${customFont.name}', ${fallback}`;
51
+ }
52
+ }
53
+ const builtIn = BUILT_IN_FONT_FALLBACKS[fontFamily.toLowerCase()];
54
+ if (builtIn) {
55
+ return builtIn;
56
+ }
57
+ return fontFamily;
58
+ }
59
+ };
60
+
61
+ // src/renderers/index.ts
62
+ import {
63
+ isSection as isSection2,
64
+ isTitle,
65
+ isParagraph,
66
+ isImage,
67
+ isInput,
68
+ isButton,
69
+ isDivider,
70
+ isSpacer,
71
+ isHtml,
72
+ isSocialIcons,
73
+ isMenu,
74
+ isTable,
75
+ isVideo,
76
+ isCustomBlock
77
+ } from "@templatical/types";
78
+
79
+ // src/renderers/title.ts
80
+ import { HEADING_LEVEL_FONT_SIZE } from "@templatical/types";
81
+
82
+ // src/escape.ts
83
+ var HTML_ENTITIES = {
84
+ "&": "&",
85
+ "<": "&lt;",
86
+ ">": "&gt;",
87
+ '"': "&quot;",
88
+ "'": "&#039;"
89
+ };
90
+ var HTML_ENTITY_REGEX = /[&<>"']/g;
91
+ function escapeHtml(text) {
92
+ if (text === "") {
93
+ return "";
94
+ }
95
+ return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);
96
+ }
97
+ function escapeAttr(text) {
98
+ if (text === "") {
99
+ return "";
100
+ }
101
+ return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);
102
+ }
103
+ function escapeCssValue(text) {
104
+ if (text === "") {
105
+ return "";
106
+ }
107
+ return escapeAttr(text).replace(/[;{}\r\n]/g, "");
108
+ }
109
+ function convertMergeTagsToValues(html) {
110
+ if (html === "") {
111
+ return "";
112
+ }
113
+ let result = html.replace(
114
+ /<span[^>]*\bdata-merge-tag="([^"]*)"[^>]*>.*?<\/span>/gs,
115
+ "$1"
116
+ );
117
+ result = result.replace(
118
+ /<span[^>]*\bdata-logic-merge-tag="([^"]*)"[^>]*>.*?<\/span>/gs,
119
+ "$1"
120
+ );
121
+ return result;
122
+ }
123
+
124
+ // src/padding.ts
125
+ function toPaddingString(padding) {
126
+ return `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`;
127
+ }
128
+ function toMarginString(margin) {
129
+ return toPaddingString(margin);
130
+ }
131
+
132
+ // src/utils.ts
133
+ function bgAttr(backgroundColor, placement) {
134
+ if (!backgroundColor) {
135
+ return "";
136
+ }
137
+ const name = placement === "native" ? "background-color" : "container-background-color";
138
+ return ` ${name}="${backgroundColor}"`;
139
+ }
140
+
141
+ // src/visibility.ts
142
+ function isHiddenOnAll(block) {
143
+ const visibility = block.visibility;
144
+ if (!visibility) {
145
+ return false;
146
+ }
147
+ return !visibility.desktop && !visibility.tablet && !visibility.mobile;
148
+ }
149
+ function getCssClassAttr(block) {
150
+ const classes = getCssClasses(block);
151
+ if (classes === "") {
152
+ return "";
153
+ }
154
+ return ` css-class="${classes}"`;
155
+ }
156
+ function getCssClasses(block) {
157
+ const visibility = block.visibility;
158
+ if (!visibility) {
159
+ return "";
160
+ }
161
+ const classes = [];
162
+ if (!visibility.desktop) {
163
+ classes.push("tpl-hide-desktop");
164
+ }
165
+ if (!visibility.tablet) {
166
+ classes.push("tpl-hide-tablet");
167
+ }
168
+ if (!visibility.mobile) {
169
+ classes.push("tpl-hide-mobile");
170
+ }
171
+ return classes.join(" ");
172
+ }
173
+
174
+ // src/renderers/title.ts
175
+ function renderTitle(block, context) {
176
+ if (isHiddenOnAll(block)) {
177
+ return "";
178
+ }
179
+ const padding = toPaddingString(block.styles.padding);
180
+ const bgColor = bgAttr(block.styles.backgroundColor, "container");
181
+ const content = unwrapParagraph(convertMergeTagsToValues(block.content));
182
+ const fontSize = HEADING_LEVEL_FONT_SIZE[block.level] ?? HEADING_LEVEL_FONT_SIZE[2];
183
+ const color = escapeAttr(block.color);
184
+ const align = block.textAlign;
185
+ const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);
186
+ const visibilityAttr = getCssClassAttr(block);
187
+ const safeLevel = HEADING_LEVEL_FONT_SIZE[block.level] ? block.level : 2;
188
+ const tag = `h${safeLevel}`;
189
+ return `<mj-text
190
+ font-size="${fontSize}px"
191
+ color="${color}"
192
+ align="${align}"
193
+ line-height="1.3"
194
+ padding="${padding}"${bgColor}${fontFamilyAttr}${visibilityAttr}
195
+ ><${tag} style="margin:0;font-size:inherit;color:inherit;line-height:inherit">${content}</${tag}></mj-text>`;
196
+ }
197
+ function unwrapParagraph(html) {
198
+ const match = html.match(/^\s*<p\b[^>]*>([\s\S]*)<\/p>\s*$/);
199
+ if (!match) return html;
200
+ if (/<\/p>\s*<p\b/i.test(match[1])) return html;
201
+ return match[1];
202
+ }
203
+ function renderFontFamilyAttr(fontFamily, context) {
204
+ if (!fontFamily) {
205
+ return "";
206
+ }
207
+ const resolved = context.resolveFontFamily(fontFamily);
208
+ return ` font-family="${resolved}"`;
209
+ }
210
+
211
+ // src/renderers/paragraph.ts
212
+ function renderParagraph(block, _context) {
213
+ if (isHiddenOnAll(block)) {
214
+ return "";
215
+ }
216
+ const stripped = block.content.replace(/<\/?p[^>]*>/gi, "").trim();
217
+ if (stripped === "") {
218
+ return "";
219
+ }
220
+ const padding = toPaddingString(block.styles.padding);
221
+ const bgColor = bgAttr(block.styles.backgroundColor, "container");
222
+ const content = convertMergeTagsToValues(block.content);
223
+ const visibilityAttr = getCssClassAttr(block);
224
+ return `<mj-text
225
+ line-height="1.5"
226
+ padding="${padding}"${bgColor}${visibilityAttr}
227
+ >${content}</mj-text>`;
228
+ }
229
+
230
+ // src/renderers/image.ts
231
+ function renderImage(block, context) {
232
+ if (isHiddenOnAll(block)) {
233
+ return "";
234
+ }
235
+ if (block.src === "") {
236
+ return "";
237
+ }
238
+ const padding = toPaddingString(block.styles.padding);
239
+ const bgColor = bgAttr(block.styles.backgroundColor, "container");
240
+ const width = block.width === "full" ? context.containerWidth + "px" : block.width + "px";
241
+ const visibilityAttr = getCssClassAttr(block);
242
+ let linkAttr = "";
243
+ if (block.linkUrl) {
244
+ linkAttr = ` href="${escapeAttr(block.linkUrl)}"`;
245
+ if (block.linkOpenInNewTab) {
246
+ linkAttr += ' target="_blank" rel="noopener"';
247
+ }
248
+ }
249
+ const src = escapeAttr(block.src);
250
+ const decorative = block.decorative === true;
251
+ const alt = decorative ? "" : escapeAttr(block.alt);
252
+ const align = block.align;
253
+ return `<mj-image
254
+ src="${src}"
255
+ alt="${alt}"
256
+ width="${width}"
257
+ align="${align}"
258
+ padding="${padding}"${bgColor}${linkAttr}${visibilityAttr}
259
+ />`;
260
+ }
261
+
262
+ // src/renderers/input.ts
263
+ function fontFamilyDeclaration(fontFamily, context) {
264
+ if (!fontFamily) {
265
+ return "";
266
+ }
267
+ const resolved = context.resolveFontFamily(fontFamily);
268
+ return `font-family:${escapeCssValue(resolved)};`;
269
+ }
270
+ function inputBorderCss(widthPx, color, sides) {
271
+ if (widthPx <= 0) {
272
+ return "border:none;";
273
+ }
274
+ const w = `${widthPx}px`;
275
+ const c = escapeCssValue(color);
276
+ if (sides === "all") {
277
+ return `border:${w} solid ${c};`;
278
+ }
279
+ const z = "0";
280
+ const tw = sides === "top" ? w : z;
281
+ const rw = sides === "right" ? w : z;
282
+ const bw = sides === "bottom" ? w : z;
283
+ const lw = sides === "left" ? w : z;
284
+ return `border-style:solid;border-color:${c};border-top-width:${tw};border-right-width:${rw};border-bottom-width:${bw};border-left-width:${lw};`;
285
+ }
286
+ function mergePlain(text) {
287
+ return escapeHtml(convertMergeTagsToValues(text));
288
+ }
289
+ function renderInput(block, context) {
290
+ if (isHiddenOnAll(block)) {
291
+ return "";
292
+ }
293
+ const padding = toPaddingString(block.styles.padding);
294
+ const bgColor = bgAttr(block.styles.backgroundColor, "container");
295
+ const visibilityAttr = getCssClassAttr(block);
296
+ const htmlInputId = `tpl-in-${block.id}`;
297
+ const htmlInputIdAttr = escapeAttr(htmlInputId);
298
+ const dataIdAttr = escapeAttr(block.id);
299
+ const labelText = mergePlain(block.label);
300
+ const placeholderPlain = convertMergeTagsToValues(block.placeholder);
301
+ const valuePlain = convertMergeTagsToValues(block.defaultValue ?? "");
302
+ const labelMargin = toMarginString(block.labelMargin);
303
+ const labelPadding = toPaddingString(block.labelPadding);
304
+ const labelStyle = [
305
+ "display:block;",
306
+ `margin:${labelMargin};`,
307
+ `padding:${labelPadding};`,
308
+ `font-size:${block.labelFontSize}px;`,
309
+ `font-weight:${block.labelFontWeight};`,
310
+ `color:${escapeCssValue(block.labelColor)};`,
311
+ `text-align:${block.labelTextAlign};`,
312
+ fontFamilyDeclaration(block.labelFontFamily, context)
313
+ ].filter(Boolean).join("");
314
+ const inputMargin = toMarginString(block.inputMargin);
315
+ const inputPad = toPaddingString(block.inputPadding);
316
+ const widthCss = block.inputWidth === "full" ? "100%" : `${block.inputWidth}px`;
317
+ const bw = block.inputBorderWidth ?? 0;
318
+ const borderCss = inputBorderCss(
319
+ bw,
320
+ block.inputBorderColor ?? "#d1d5db",
321
+ block.inputBorderSides ?? "all"
322
+ );
323
+ const inputStyle = [
324
+ "box-sizing:border-box;",
325
+ `width:${widthCss};`,
326
+ `margin:${inputMargin};`,
327
+ `padding:${inputPad};`,
328
+ `background-color:${escapeCssValue(block.inputBackgroundColor)};`,
329
+ `color:${escapeCssValue(block.inputTextColor)};`,
330
+ `font-size:${block.inputFontSize}px;`,
331
+ `border-radius:${block.inputBorderRadius}px;`,
332
+ borderCss,
333
+ fontFamilyDeclaration(block.inputFontFamily, context),
334
+ "outline:none;"
335
+ ].join("");
336
+ const typeAttr = escapeAttr(block.inputType);
337
+ const requiredAttr = block.required ? " required" : "";
338
+ const nameAttr = block.name.trim() === "" ? "" : ` name="${escapeAttr(block.name.trim())}"`;
339
+ const placeholderStyle = `<style type="text/css">[data-tpl-input="${dataIdAttr}"]::placeholder{color:${escapeCssValue(block.inputPlaceholderColor)}!important;opacity:1;}</style>`;
340
+ const html = `${placeholderStyle}<label for="${htmlInputIdAttr}" style="${labelStyle}">${labelText}</label><input id="${htmlInputIdAttr}"${nameAttr} type="${typeAttr}" data-tpl-input="${dataIdAttr}" placeholder="${escapeAttr(placeholderPlain)}" value="${escapeAttr(valuePlain)}" style="${inputStyle}"${requiredAttr} />`;
341
+ return `<mj-text
342
+ line-height="1.5"
343
+ padding="${padding}"${bgColor}${visibilityAttr}
344
+ >${html}</mj-text>`;
345
+ }
346
+
347
+ // src/renderers/button.ts
348
+ function renderButton(block, context) {
349
+ if (isHiddenOnAll(block)) {
350
+ return "";
351
+ }
352
+ const padding = toPaddingString(block.styles.padding);
353
+ const bgColor = bgAttr(block.styles.backgroundColor, "container");
354
+ const buttonPadding = toPaddingString(block.buttonPadding);
355
+ const href = resolveButtonHref(block);
356
+ const hrefAttr = href === "" ? "" : ` href="${href}"`;
357
+ const backgroundColor = escapeAttr(block.backgroundColor);
358
+ const textColor = escapeAttr(block.textColor);
359
+ const fontSize = block.fontSize;
360
+ const borderRadius = block.borderRadius;
361
+ const text = escapeHtml(block.text);
362
+ const action = block.clickAction ?? "none";
363
+ const targetAttr = block.openInNewTab && action !== "next_step" && action !== "previous_step" ? ' target="_blank" rel="noopener"' : "";
364
+ const fontFamilyAttr = renderFontFamilyAttr2(block.fontFamily, context);
365
+ const btnCssClassAttr = renderMjButtonCssClassAttr(block);
366
+ const borderAttrs = renderMjButtonBorderAttrs(block);
367
+ return `<mj-button${hrefAttr}${targetAttr}
368
+ background-color="${backgroundColor}"
369
+ color="${textColor}"
370
+ font-size="${fontSize}px"
371
+ font-weight="bold"
372
+ border-radius="${borderRadius}px"
373
+ inner-padding="${buttonPadding}"
374
+ padding="${padding}"${bgColor}${fontFamilyAttr}${btnCssClassAttr}${borderAttrs}
375
+ >${text}</mj-button>`;
376
+ }
377
+ function renderMjButtonCssClassAttr(block) {
378
+ const parts = [];
379
+ const vis = getCssClasses(block);
380
+ if (vis) {
381
+ parts.push(...vis.split(/\s+/).filter(Boolean));
382
+ }
383
+ const action = block.clickAction ?? "none";
384
+ if (action === "next_step") {
385
+ parts.push("tpl-btn-next-step");
386
+ } else if (action === "previous_step") {
387
+ parts.push("tpl-btn-prev-step");
388
+ }
389
+ if (parts.length === 0) {
390
+ return "";
391
+ }
392
+ return ` css-class="${escapeAttr(parts.join(" "))}"`;
393
+ }
394
+ function renderMjButtonBorderAttrs(block) {
395
+ const w = block.borderWidth ?? 0;
396
+ if (w <= 0) {
397
+ return "";
398
+ }
399
+ const color = block.borderColor ?? "#000000";
400
+ const segment = `${w}px solid ${color}`;
401
+ const escSegment = escapeAttr(segment);
402
+ const escNone = escapeAttr("none");
403
+ const sides = block.borderSides ?? "all";
404
+ if (sides === "all") {
405
+ return ` border="${escSegment}"`;
406
+ }
407
+ const top = sides === "top" ? escSegment : escNone;
408
+ const right = sides === "right" ? escSegment : escNone;
409
+ const bottom = sides === "bottom" ? escSegment : escNone;
410
+ const left = sides === "left" ? escSegment : escNone;
411
+ return ` border-top="${top}" border-right="${right}" border-bottom="${bottom}" border-left="${left}"`;
412
+ }
413
+ function resolveButtonHref(block) {
414
+ const action = block.clickAction ?? "none";
415
+ if (action === "scroll_anchor") {
416
+ const raw = block.clickActionAnchorId?.trim().replace(/^#/, "") ?? "";
417
+ if (raw !== "") {
418
+ return escapeAttr(`#${raw}`);
419
+ }
420
+ return block.url === "" ? "" : escapeAttr(block.url);
421
+ }
422
+ if (action === "none" || action === "load_page") {
423
+ return block.url === "" ? "" : escapeAttr(block.url);
424
+ }
425
+ if (action === "next_step" || action === "previous_step" || action === "close_popup") {
426
+ return escapeAttr(block.url === "" ? "#" : block.url);
427
+ }
428
+ return "";
429
+ }
430
+ function renderFontFamilyAttr2(fontFamily, context) {
431
+ if (!fontFamily) {
432
+ return "";
433
+ }
434
+ const resolved = context.resolveFontFamily(fontFamily);
435
+ return ` font-family="${resolved}"`;
436
+ }
437
+
438
+ // src/renderers/divider.ts
439
+ function renderDivider(block, _context) {
440
+ if (isHiddenOnAll(block)) {
441
+ return "";
442
+ }
443
+ const padding = toPaddingString(block.styles.padding);
444
+ const bgColor = block.styles.backgroundColor ? ` container-background-color="${escapeAttr(block.styles.backgroundColor)}"` : "";
445
+ const width = block.width === "full" ? "100%" : block.width + "px";
446
+ const thickness = block.thickness;
447
+ const lineStyle = block.lineStyle;
448
+ const color = escapeAttr(block.color);
449
+ const visibilityAttr = getCssClassAttr(block);
450
+ return `<mj-divider
451
+ border-width="${thickness}px"
452
+ border-style="${lineStyle}"
453
+ border-color="${color}"
454
+ width="${width}"
455
+ padding="${padding}"${bgColor}${visibilityAttr}
456
+ />`;
457
+ }
458
+
459
+ // src/renderers/spacer.ts
460
+ function renderSpacer(block, _context) {
461
+ if (isHiddenOnAll(block)) {
462
+ return "";
463
+ }
464
+ const height = block.height;
465
+ const bgColor = block.styles.backgroundColor ? ` container-background-color="${escapeAttr(block.styles.backgroundColor)}"` : "";
466
+ const visibilityAttr = getCssClassAttr(block);
467
+ return `<mj-spacer height="${height}px" padding="0"${bgColor}${visibilityAttr} />`;
468
+ }
469
+
470
+ // src/renderers/html.ts
471
+ function renderHtml(block, context) {
472
+ if (isHiddenOnAll(block)) {
473
+ return "";
474
+ }
475
+ if (!context.allowHtmlBlocks) {
476
+ return "";
477
+ }
478
+ const content = block.content;
479
+ if (content === "") {
480
+ return "";
481
+ }
482
+ const visibilityAttr = getCssClassAttr(block);
483
+ return `<mj-text${visibilityAttr}>
484
+ ${content}
485
+ </mj-text>`;
486
+ }
487
+
488
+ // src/social-icons.ts
489
+ var SOCIAL_ICONS = {
490
+ facebook: {
491
+ path: "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z",
492
+ color: "#1877F2"
493
+ },
494
+ twitter: {
495
+ path: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z",
496
+ color: "#000000"
497
+ },
498
+ instagram: {
499
+ path: "M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z",
500
+ color: "#E4405F"
501
+ },
502
+ linkedin: {
503
+ path: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z",
504
+ color: "#0A66C2"
505
+ },
506
+ youtube: {
507
+ path: "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z",
508
+ color: "#FF0000"
509
+ },
510
+ tiktok: {
511
+ path: "M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z",
512
+ color: "#000000"
513
+ },
514
+ pinterest: {
515
+ path: "M12 0C5.373 0 0 5.372 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738.098.119.112.224.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.631-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12 0-6.628-5.373-12-12-12z",
516
+ color: "#BD081C"
517
+ },
518
+ email: {
519
+ path: "M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z",
520
+ color: "#6B7280"
521
+ },
522
+ whatsapp: {
523
+ path: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z",
524
+ color: "#25D366"
525
+ },
526
+ telegram: {
527
+ path: "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z",
528
+ color: "#26A5E4"
529
+ },
530
+ discord: {
531
+ path: "M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z",
532
+ color: "#5865F2"
533
+ },
534
+ snapchat: {
535
+ path: "M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738a.36.36 0 01.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.631-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146 1.124.347 2.317.535 3.554.535 6.627 0 12.017-5.373 12.017-12.001C24.034 5.367 18.644 0 12.017 0z",
536
+ color: "#FFFC00"
537
+ },
538
+ reddit: {
539
+ path: "M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z",
540
+ color: "#FF4500"
541
+ },
542
+ github: {
543
+ path: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12",
544
+ color: "#181717"
545
+ },
546
+ dribbble: {
547
+ path: "M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12-5.385 12-12 12zm10.12-10.358c-.35-.11-3.17-.953-6.384-.438 1.34 3.684 1.887 6.684 1.992 7.308 2.3-1.555 3.936-4.02 4.392-6.87zm-6.115 7.808c-.153-.9-.75-4.032-2.19-7.77l-.066.02c-5.79 2.015-7.86 6.025-8.04 6.4 1.73 1.358 3.92 2.166 6.29 2.166 1.42 0 2.77-.29 4-.814zm-11.62-2.58c.232-.4 3.045-5.055 8.332-6.765.135-.045.27-.084.405-.12-.26-.585-.54-1.167-.832-1.74C7.17 11.775 2.206 11.71 1.756 11.7l-.004.312c0 2.633.998 5.037 2.634 6.855zm-2.42-8.955c.46.008 4.683.026 9.477-1.248-1.698-3.018-3.53-5.558-3.8-5.928-2.868 1.35-5.01 3.99-5.676 7.17zM9.6 2.052c.282.38 2.145 2.914 3.822 6 3.645-1.365 5.19-3.44 5.373-3.702-1.81-1.61-4.19-2.586-6.795-2.586-.825 0-1.63.1-2.4.285zm10.335 3.483c-.218.29-1.935 2.493-5.724 4.04.24.49.47.985.68 1.486.08.18.15.36.22.53 3.41-.43 6.8.26 7.14.33-.02-2.42-.88-4.64-2.31-6.38z",
548
+ color: "#EA4C89"
549
+ },
550
+ behance: {
551
+ path: "M22 7h-7V5h7v2zm1.726 10c-.442 1.297-2.029 3-5.101 3-3.074 0-5.564-1.729-5.564-5.675 0-3.91 2.325-5.92 5.466-5.92 3.082 0 4.964 1.782 5.375 4.426.078.506.109 1.188.095 2.14H15.97c.13 3.211 3.483 3.312 4.588 2.029h3.168zm-7.686-4h4.965c-.105-1.547-1.136-2.219-2.477-2.219-1.466 0-2.277.768-2.488 2.219zm-9.574 6.988H0V5.021h6.953c5.476.081 5.58 5.444 2.72 6.906 3.461 1.26 3.577 8.061-3.207 8.061zM3 11h3.584c2.508 0 2.906-3-.312-3H3v3zm3.391 3H3v3.016h3.341c3.055 0 2.868-3.016.05-3.016z",
552
+ color: "#1769FF"
553
+ }
554
+ };
555
+ function generateSocialIconDataUri(platform, style, size) {
556
+ const iconData = SOCIAL_ICONS[platform];
557
+ const path = iconData?.path ?? "";
558
+ const brandColor = iconData?.color ?? "#6B7280";
559
+ if (path === "") {
560
+ return "";
561
+ }
562
+ const isOutlined = style === "outlined";
563
+ const iconColor = isOutlined ? brandColor : "#ffffff";
564
+ let bgShape;
565
+ switch (style) {
566
+ case "circle":
567
+ bgShape = `<circle cx="12" cy="12" r="12" fill="${brandColor}"/>`;
568
+ break;
569
+ case "rounded":
570
+ bgShape = `<rect width="24" height="24" rx="6" fill="${brandColor}"/>`;
571
+ break;
572
+ case "square":
573
+ bgShape = `<rect width="24" height="24" rx="0" fill="${brandColor}"/>`;
574
+ break;
575
+ case "outlined":
576
+ bgShape = `<rect width="22" height="22" x="1" y="1" rx="3" fill="transparent" stroke="${brandColor}" stroke-width="2"/>`;
577
+ break;
578
+ default:
579
+ bgShape = `<rect width="24" height="24" rx="3" fill="${brandColor}"/>`;
580
+ break;
581
+ }
582
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="${size}" height="${size}">` + bgShape + `<g transform="translate(4.8, 4.8) scale(0.6)"><path d="${path}" fill="${iconColor}"/></g></svg>`;
583
+ return "data:image/svg+xml;base64," + btoa(svg);
584
+ }
585
+
586
+ // src/renderers/social.ts
587
+ function renderSocialIcons(block, _context) {
588
+ if (isHiddenOnAll(block)) {
589
+ return "";
590
+ }
591
+ const icons = block.icons;
592
+ if (icons.length === 0) {
593
+ return "";
594
+ }
595
+ const padding = toPaddingString(block.styles.padding);
596
+ const bgColor = block.styles.backgroundColor ? ` container-background-color="${escapeAttr(block.styles.backgroundColor)}"` : "";
597
+ const visibilityAttr = getCssClassAttr(block);
598
+ const align = block.align;
599
+ const iconSize = block.iconSize;
600
+ const iconStyle = block.iconStyle;
601
+ const spacing = block.spacing;
602
+ let iconSizePx;
603
+ switch (iconSize) {
604
+ case "small":
605
+ iconSizePx = 24;
606
+ break;
607
+ case "large":
608
+ iconSizePx = 48;
609
+ break;
610
+ default:
611
+ iconSizePx = 32;
612
+ break;
613
+ }
614
+ let borderRadius;
615
+ switch (iconStyle) {
616
+ case "circle":
617
+ borderRadius = "50%";
618
+ break;
619
+ case "rounded":
620
+ borderRadius = "8px";
621
+ break;
622
+ case "square":
623
+ borderRadius = "0";
624
+ break;
625
+ default:
626
+ borderRadius = "4px";
627
+ break;
628
+ }
629
+ const iconCount = icons.length;
630
+ const socialElements = icons.map((icon, index) => {
631
+ const platform = icon.platform;
632
+ const url = escapeAttr(icon.url);
633
+ const iconSrc = generateSocialIconDataUri(platform, iconStyle, iconSizePx);
634
+ const rightPad = index === iconCount - 1 ? 0 : spacing;
635
+ 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>`;
636
+ });
637
+ const socialContent = socialElements.join("\n");
638
+ return `<mj-social
639
+ mode="horizontal"
640
+ align="${align}"
641
+ icon-padding="0"
642
+ padding="${padding}"${bgColor}${visibilityAttr}
643
+ >
644
+ ${socialContent}
645
+ </mj-social>`;
646
+ }
647
+
648
+ // src/renderers/menu.ts
649
+ function renderMenu(block, context) {
650
+ if (isHiddenOnAll(block)) {
651
+ return "";
652
+ }
653
+ if (block.items.length === 0) {
654
+ return "";
655
+ }
656
+ const padding = toPaddingString(block.styles.padding);
657
+ const bgColor = bgAttr(block.styles.backgroundColor, "container");
658
+ const visibilityAttr = getCssClassAttr(block);
659
+ const fontFamilyAttr = renderFontFamilyAttr3(block.fontFamily, context);
660
+ const align = block.textAlign;
661
+ const fontSize = block.fontSize;
662
+ const color = escapeAttr(block.color);
663
+ const content = renderMenuItems(block);
664
+ return `<mj-text
665
+ font-size="${fontSize}px"
666
+ color="${color}"
667
+ align="${align}"
668
+ line-height="1.5"
669
+ padding="${padding}"${bgColor}${fontFamilyAttr}${visibilityAttr}
670
+ >${content}</mj-text>`;
671
+ }
672
+ function renderMenuItems(block) {
673
+ const items = block.items;
674
+ const separator = escapeHtml(block.separator);
675
+ const separatorColor = escapeCssValue(block.separatorColor);
676
+ const spacing = block.spacing;
677
+ const linkColor = block.linkColor ?? block.color;
678
+ const parts = [];
679
+ const itemCount = items.length;
680
+ for (let index = 0; index < itemCount; index++) {
681
+ parts.push(renderMenuItem(items[index], linkColor));
682
+ if (index < itemCount - 1) {
683
+ parts.push(
684
+ `<span style="color: ${separatorColor}; padding: 0 ${spacing}px;">${separator}</span>`
685
+ );
686
+ }
687
+ }
688
+ return parts.join("");
689
+ }
690
+ function renderMenuItem(item, linkColor) {
691
+ const text = escapeHtml(item.text);
692
+ const url = escapeAttr(item.url);
693
+ const color = escapeCssValue(item.color ?? linkColor);
694
+ const target = item.openInNewTab ? ' target="_blank" rel="noopener"' : "";
695
+ const styles = [`color: ${color}`, "text-decoration: none"];
696
+ if (item.bold) {
697
+ styles.push("font-weight: bold");
698
+ }
699
+ if (item.underline) {
700
+ styles.push("text-decoration: underline");
701
+ }
702
+ const styleAttr = styles.join("; ");
703
+ return `<a href="${url}" style="${styleAttr}"${target}>${text}</a>`;
704
+ }
705
+ function renderFontFamilyAttr3(fontFamily, context) {
706
+ if (!fontFamily) {
707
+ return "";
708
+ }
709
+ const resolved = context.resolveFontFamily(fontFamily);
710
+ return ` font-family="${resolved}"`;
711
+ }
712
+
713
+ // src/renderers/table.ts
714
+ function renderTable(block, context) {
715
+ if (isHiddenOnAll(block)) {
716
+ return "";
717
+ }
718
+ if (block.rows.length === 0) {
719
+ return "";
720
+ }
721
+ const padding = toPaddingString(block.styles.padding);
722
+ const bgColor = bgAttr(block.styles.backgroundColor, "container");
723
+ const visibilityAttr = getCssClassAttr(block);
724
+ const fontFamilyAttr = renderFontFamilyAttr4(block.fontFamily, context);
725
+ const fontSize = block.fontSize;
726
+ const color = escapeAttr(block.color);
727
+ const align = block.textAlign;
728
+ const tableHtml = renderTableElement(block);
729
+ return `<mj-text
730
+ font-size="${fontSize}px"
731
+ color="${color}"
732
+ align="${align}"
733
+ line-height="1.5"
734
+ padding="${padding}"${bgColor}${fontFamilyAttr}${visibilityAttr}
735
+ >${tableHtml}</mj-text>`;
736
+ }
737
+ function renderTableElement(block) {
738
+ const borderColor = escapeCssValue(block.borderColor);
739
+ const borderWidth = block.borderWidth;
740
+ const tableStyle = "width: 100%; border-collapse: collapse;";
741
+ let rowsHtml = "";
742
+ for (let index = 0; index < block.rows.length; index++) {
743
+ const row = block.rows[index];
744
+ const isHeader = block.hasHeaderRow && index === 0;
745
+ rowsHtml += renderRow(row, block, isHeader, borderColor, borderWidth);
746
+ }
747
+ return `<table style="${tableStyle}">${rowsHtml}</table>`;
748
+ }
749
+ function renderRow(row, block, isHeader, borderColor, borderWidth) {
750
+ let cellsHtml = "";
751
+ for (const cell of row.cells) {
752
+ cellsHtml += renderCell(cell, block, isHeader, borderColor, borderWidth);
753
+ }
754
+ return `<tr>${cellsHtml}</tr>`;
755
+ }
756
+ function renderCell(cell, block, isHeader, borderColor, borderWidth) {
757
+ const cellPadding = block.cellPadding;
758
+ const styles = [
759
+ `border: ${borderWidth}px solid ${borderColor}`,
760
+ `padding: ${cellPadding}px`
761
+ ];
762
+ if (isHeader) {
763
+ styles.push("font-weight: bold");
764
+ if (block.headerBackgroundColor) {
765
+ styles.push(
766
+ `background-color: ${escapeCssValue(block.headerBackgroundColor)}`
767
+ );
768
+ }
769
+ }
770
+ const styleAttr = styles.join("; ");
771
+ const content = convertMergeTagsToValues(cell.content);
772
+ const tag = isHeader ? "th" : "td";
773
+ return `<${tag} style="${styleAttr}">${content}</${tag}>`;
774
+ }
775
+ function renderFontFamilyAttr4(fontFamily, context) {
776
+ if (!fontFamily) {
777
+ return "";
778
+ }
779
+ const resolved = context.resolveFontFamily(fontFamily);
780
+ return ` font-family="${resolved}"`;
781
+ }
782
+
783
+ // src/renderers/custom.ts
784
+ function renderCustom(block, context) {
785
+ if (isHiddenOnAll(block)) {
786
+ return "";
787
+ }
788
+ const fromContext = context.customBlockHtml.get(block.id);
789
+ const content = fromContext ?? block.renderedHtml;
790
+ if (!content || content === "") {
791
+ return "";
792
+ }
793
+ const visibilityAttr = getCssClassAttr(block);
794
+ const bgColor = bgAttr(block.styles?.backgroundColor, "container");
795
+ return `<mj-text${bgColor}${visibilityAttr}>
796
+ ${content}
797
+ </mj-text>`;
798
+ }
799
+
800
+ // src/renderers/section.ts
801
+ import { isSection } from "@templatical/types";
802
+
803
+ // src/columns.ts
804
+ function getWidthPercentages(layout) {
805
+ switch (layout) {
806
+ case "2":
807
+ return ["50%", "50%"];
808
+ case "3":
809
+ return ["33.33%", "33.33%", "33.34%"];
810
+ case "1-2":
811
+ return ["33.33%", "66.67%"];
812
+ case "2-1":
813
+ return ["66.67%", "33.33%"];
814
+ default:
815
+ return ["100%"];
816
+ }
817
+ }
818
+ function getWidthPixels(layout, containerWidth) {
819
+ switch (layout) {
820
+ case "2":
821
+ return [containerWidth * 0.5, containerWidth * 0.5];
822
+ case "3":
823
+ return [containerWidth / 3, containerWidth / 3, containerWidth / 3];
824
+ case "1-2":
825
+ return [containerWidth / 3, containerWidth * 2 / 3];
826
+ case "2-1":
827
+ return [containerWidth * 2 / 3, containerWidth / 3];
828
+ default:
829
+ return [containerWidth];
830
+ }
831
+ }
832
+
833
+ // src/renderers/section.ts
834
+ function renderSection(block, context, renderBlock2) {
835
+ if (isHiddenOnAll(block)) {
836
+ return "";
837
+ }
838
+ const columnsLayout = block.columns;
839
+ const columnWidths = getWidthPercentages(columnsLayout);
840
+ const columnWidthsPx = getWidthPixels(columnsLayout, context.containerWidth);
841
+ const padding = toPaddingString(block.styles.padding);
842
+ const bgColor = bgAttr(block.styles.backgroundColor, "native");
843
+ const visibilityAttr = getCssClassAttr(block);
844
+ const children = block.children;
845
+ const columnsContent = [];
846
+ for (let index = 0; index < children.length; index++) {
847
+ const column = children[index];
848
+ const width = columnWidths[index] ?? "100%";
849
+ const columnWidth = Math.floor(
850
+ columnWidthsPx[index] ?? context.containerWidth
851
+ );
852
+ const filteredColumn = filterHtmlBlocks(
853
+ column,
854
+ context.allowHtmlBlocks
855
+ ).filter((child) => !isSection(child));
856
+ const columnContext = context.withContainerWidth(columnWidth);
857
+ const columnBlocks = filteredColumn.map((child) => renderBlock2(child, columnContext)).filter((value) => value !== "").join("\n");
858
+ const content = columnBlocks === "" ? "<mj-text>&nbsp;</mj-text>" : columnBlocks;
859
+ columnsContent.push(`<mj-column width="${width}">
860
+ ${content}
861
+ </mj-column>`);
862
+ }
863
+ const columns = columnsContent.join("\n");
864
+ return `<mj-section${bgColor} padding="${padding}"${visibilityAttr}>
865
+ ${columns}
866
+ </mj-section>`;
867
+ }
868
+ function filterHtmlBlocks(blocks, allowHtmlBlocks) {
869
+ if (allowHtmlBlocks) {
870
+ return blocks;
871
+ }
872
+ return blocks.filter((block) => block.type !== "html");
873
+ }
874
+
875
+ // src/renderers/video.ts
876
+ function getVideoThumbnail(url, customThumbnail) {
877
+ if (customThumbnail) {
878
+ return customThumbnail;
879
+ }
880
+ if (!url) {
881
+ return null;
882
+ }
883
+ const youtubePatterns = [
884
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
885
+ /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/
886
+ ];
887
+ for (const pattern of youtubePatterns) {
888
+ const match = url.match(pattern);
889
+ if (match) {
890
+ return `https://img.youtube.com/vi/${match[1]}/maxresdefault.jpg`;
891
+ }
892
+ }
893
+ const vimeoMatch = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
894
+ if (vimeoMatch) {
895
+ return `https://vumbnail.com/${vimeoMatch[1]}.jpg`;
896
+ }
897
+ return null;
898
+ }
899
+ function renderVideo(block, context) {
900
+ if (isHiddenOnAll(block)) {
901
+ return "";
902
+ }
903
+ const thumbnailUrl = getVideoThumbnail(block.url, block.thumbnailUrl);
904
+ if (!thumbnailUrl) {
905
+ return "";
906
+ }
907
+ const padding = toPaddingString(block.styles.padding);
908
+ const bgColor = bgAttr(block.styles.backgroundColor, "container");
909
+ const width = block.width === "full" ? context.containerWidth + "px" : block.width + "px";
910
+ const visibilityAttr = getCssClassAttr(block);
911
+ const src = escapeAttr(thumbnailUrl);
912
+ const alt = escapeAttr(block.alt);
913
+ const align = block.align;
914
+ const href = escapeAttr(block.url);
915
+ return `<mj-image
916
+ src="${src}"
917
+ alt="${alt}"
918
+ width="${width}"
919
+ align="${align}"
920
+ padding="${padding}"
921
+ href="${href}"
922
+ target="_blank"
923
+ rel="noopener"${bgColor}${visibilityAttr}
924
+ />`;
925
+ }
926
+
927
+ // src/renderers/index.ts
928
+ function renderBlock(block, context) {
929
+ if (isSection2(block)) {
930
+ return renderSection(block, context, renderBlock);
931
+ }
932
+ if (isTitle(block)) {
933
+ return renderTitle(block, context);
934
+ }
935
+ if (isParagraph(block)) {
936
+ return renderParagraph(block, context);
937
+ }
938
+ if (isImage(block)) {
939
+ return renderImage(block, context);
940
+ }
941
+ if (isInput(block)) {
942
+ return renderInput(block, context);
943
+ }
944
+ if (isButton(block)) {
945
+ return renderButton(block, context);
946
+ }
947
+ if (isDivider(block)) {
948
+ return renderDivider(block, context);
949
+ }
950
+ if (isSpacer(block)) {
951
+ return renderSpacer(block, context);
952
+ }
953
+ if (isHtml(block)) {
954
+ return renderHtml(block, context);
955
+ }
956
+ if (isSocialIcons(block)) {
957
+ return renderSocialIcons(block, context);
958
+ }
959
+ if (isMenu(block)) {
960
+ return renderMenu(block, context);
961
+ }
962
+ if (isTable(block)) {
963
+ return renderTable(block, context);
964
+ }
965
+ if (isVideo(block)) {
966
+ return renderVideo(block, context);
967
+ }
968
+ if (isCustomBlock(block)) {
969
+ return renderCustom(block, context);
970
+ }
971
+ return "";
972
+ }
973
+
974
+ // src/index.ts
975
+ async function renderToMjml(content, options) {
976
+ const customFonts = options?.customFonts ?? [];
977
+ const defaultFallbackFont = options?.defaultFallbackFont ?? "Arial, sans-serif";
978
+ const allowHtmlBlocks = options?.allowHtmlBlocks ?? true;
979
+ const customBlockHtml = await resolveCustomBlocks(
980
+ content,
981
+ options?.renderCustomBlock
982
+ );
983
+ const renderContext = new RenderContext(
984
+ content.settings.width,
985
+ customFonts,
986
+ defaultFallbackFont,
987
+ allowHtmlBlocks,
988
+ customBlockHtml
989
+ );
990
+ const fontFamily = renderContext.resolveFontFamily(
991
+ content.settings.fontFamily
992
+ );
993
+ const backgroundColor = content.settings.backgroundColor;
994
+ const multiCanvas = (content.canvasPages?.length ?? 0) > 0;
995
+ const bodyContent = buildMjBodyContent(
996
+ content,
997
+ renderContext,
998
+ allowHtmlBlocks
999
+ );
1000
+ const fontDeclarations = generateFontDeclarations(customFonts);
1001
+ const previewTag = generatePreviewTag(content.settings.preheaderText);
1002
+ const lang = escapeAttr(content.settings.locale);
1003
+ const mjBodyAttrs = `width="${renderContext.containerWidth}px" background-color="${backgroundColor}"` + (multiCanvas ? ` css-class="tpl-canvas-multi"` : "");
1004
+ return `<mjml lang="${lang}">
1005
+ <mj-head>${previewTag}
1006
+ <mj-attributes>
1007
+ <mj-all font-family="${fontFamily}" />
1008
+ <mj-text font-size="14px" />
1009
+ <mj-section padding="0" />
1010
+ <mj-column padding="0" />
1011
+ <mj-image fluid-on-mobile="true" />
1012
+ </mj-attributes>${fontDeclarations}
1013
+ <mj-style>
1014
+ a { color: inherit; text-decoration: none; }
1015
+ @media only screen and (max-width: 480px) {
1016
+ .tpl-hide-mobile { display: none !important; mso-hide: all !important; }
1017
+ }
1018
+ @media only screen and (min-width: 481px) and (max-width: 768px) {
1019
+ .tpl-hide-tablet { display: none !important; mso-hide: all !important; }
1020
+ }
1021
+ @media only screen and (min-width: 769px) {
1022
+ .tpl-hide-desktop { display: none !important; mso-hide: all !important; }
1023
+ }${multiCanvas ? multiCanvasStepCss() : ""}
1024
+ </mj-style>
1025
+ </mj-head>
1026
+ <mj-body ${mjBodyAttrs}>
1027
+ ${bodyContent}${multiCanvas ? `
1028
+ ${multiCanvasStepScriptMjRaw()}
1029
+ ` : ""}
1030
+ </mj-body>
1031
+ </mjml>`;
1032
+ }
1033
+ function buildMjBodyContent(content, context, allowHtmlBlocks) {
1034
+ const pages = content.canvasPages;
1035
+ if (pages?.length) {
1036
+ const chunks = [];
1037
+ for (let i = 0; i < pages.length; i++) {
1038
+ const pageBlocks = pages[i].blocks ?? [];
1039
+ const inner = renderBlocksTopLevel(pageBlocks, context, allowHtmlBlocks);
1040
+ if (inner.trim() === "") {
1041
+ continue;
1042
+ }
1043
+ const rawClasses = i === 0 ? `tpl-canvas-page tpl-canvas-page-idx-${i} tpl-canvas-page--current` : `tpl-canvas-page tpl-canvas-page-idx-${i}`;
1044
+ chunks.push(
1045
+ `<mj-wrapper css-class="${escapeAttr(rawClasses)}" padding="0">
1046
+ ${inner}
1047
+ </mj-wrapper>`
1048
+ );
1049
+ }
1050
+ return chunks.join("\n");
1051
+ }
1052
+ return renderBlocksTopLevel(content.blocks, context, allowHtmlBlocks);
1053
+ }
1054
+ function renderBlocksTopLevel(blocks, context, allowHtmlBlocks) {
1055
+ return filterHtmlBlocks2(blocks, allowHtmlBlocks).map((block) => renderTopLevelBlock(block, context)).filter((value) => value !== "").join("\n");
1056
+ }
1057
+ function renderTopLevelBlock(block, context) {
1058
+ if (isSection3(block)) {
1059
+ const rendered = renderBlock(block, context);
1060
+ return wrapWithDisplayCondition(block, rendered);
1061
+ }
1062
+ const content = renderBlock(block, context);
1063
+ const wrapped = wrapInSection(content);
1064
+ return wrapWithDisplayCondition(block, wrapped);
1065
+ }
1066
+ function wrapWithDisplayCondition(block, rendered) {
1067
+ if (rendered === "") {
1068
+ return "";
1069
+ }
1070
+ const displayCondition = block.displayCondition;
1071
+ if (!displayCondition) {
1072
+ return rendered;
1073
+ }
1074
+ return `<mj-raw>${displayCondition.before}</mj-raw>
1075
+ ` + rendered + `
1076
+ <mj-raw>${displayCondition.after}</mj-raw>`;
1077
+ }
1078
+ function wrapInSection(content) {
1079
+ if (content === "") {
1080
+ return "";
1081
+ }
1082
+ return `<mj-section>
1083
+ <mj-column>
1084
+ ${content}
1085
+ </mj-column>
1086
+ </mj-section>`;
1087
+ }
1088
+ function generatePreviewTag(preheaderText) {
1089
+ if (!preheaderText) {
1090
+ return "";
1091
+ }
1092
+ const trimmed = preheaderText.trim();
1093
+ if (trimmed === "") {
1094
+ return "";
1095
+ }
1096
+ const escaped = escapeHtml(trimmed);
1097
+ return `
1098
+ <mj-preview>${escaped}</mj-preview>`;
1099
+ }
1100
+ function generateFontDeclarations(customFonts) {
1101
+ if (customFonts.length === 0) {
1102
+ return "";
1103
+ }
1104
+ return customFonts.map(
1105
+ (font) => `
1106
+ <mj-font name="${escapeAttr(font.name)}" href="${escapeAttr(font.url)}" />`
1107
+ ).join("");
1108
+ }
1109
+ function filterHtmlBlocks2(blocks, allowHtmlBlocks) {
1110
+ if (allowHtmlBlocks) {
1111
+ return blocks;
1112
+ }
1113
+ return blocks.filter((block) => block.type !== "html");
1114
+ }
1115
+ async function resolveCustomBlocks(content, renderCustomBlock) {
1116
+ const result = /* @__PURE__ */ new Map();
1117
+ if (!renderCustomBlock) {
1118
+ return result;
1119
+ }
1120
+ const customBlocks = [];
1121
+ collectCustomBlocksFromContent(content, customBlocks);
1122
+ if (customBlocks.length === 0) {
1123
+ return result;
1124
+ }
1125
+ const rendered = await Promise.all(
1126
+ customBlocks.map((block) => renderCustomBlock(block))
1127
+ );
1128
+ for (let index = 0; index < customBlocks.length; index++) {
1129
+ result.set(customBlocks[index].id, rendered[index]);
1130
+ }
1131
+ return result;
1132
+ }
1133
+ function collectCustomBlocks(blocks, out) {
1134
+ for (const block of blocks) {
1135
+ if (isCustomBlock2(block)) {
1136
+ out.push(block);
1137
+ continue;
1138
+ }
1139
+ if (isSection3(block)) {
1140
+ for (const column of block.children) {
1141
+ collectCustomBlocks(column, out);
1142
+ }
1143
+ }
1144
+ }
1145
+ }
1146
+ function collectCustomBlocksFromContent(content, out) {
1147
+ const pages = content.canvasPages;
1148
+ if (pages?.length) {
1149
+ for (const page of pages) {
1150
+ collectCustomBlocks(page.blocks ?? [], out);
1151
+ }
1152
+ return;
1153
+ }
1154
+ collectCustomBlocks(content.blocks, out);
1155
+ }
1156
+ function multiCanvasStepCss() {
1157
+ return `
1158
+ .tpl-canvas-multi .tpl-canvas-page { display: none !important; }
1159
+ .tpl-canvas-multi .tpl-canvas-page.tpl-canvas-page--current { display: block !important; }`;
1160
+ }
1161
+ function multiCanvasStepScriptMjRaw() {
1162
+ return `<mj-raw>
1163
+ <script>
1164
+ (function () {
1165
+ var root = document.body;
1166
+ if (!root || !root.classList.contains("tpl-canvas-multi")) return;
1167
+ root.addEventListener(
1168
+ "click",
1169
+ function (e) {
1170
+ var link = e.target.closest("a");
1171
+ if (!link || !root.contains(link)) return;
1172
+ var isNext =
1173
+ link.classList.contains("tpl-btn-next-step") ||
1174
+ !!link.closest("td.tpl-btn-next-step");
1175
+ var isPrev =
1176
+ link.classList.contains("tpl-btn-prev-step") ||
1177
+ !!link.closest("td.tpl-btn-prev-step");
1178
+ if (!isNext && !isPrev) return;
1179
+ e.preventDefault();
1180
+ e.stopPropagation();
1181
+ var pages = Array.prototype.slice.call(
1182
+ root.querySelectorAll(".tpl-canvas-page"),
1183
+ );
1184
+ var cur = root.querySelector(
1185
+ ".tpl-canvas-page.tpl-canvas-page--current",
1186
+ );
1187
+ var i = pages.indexOf(cur);
1188
+ if (i < 0) return;
1189
+ if (isNext) {
1190
+ if (i >= pages.length - 1) return;
1191
+ pages[i].classList.remove("tpl-canvas-page--current");
1192
+ pages[i + 1].classList.add("tpl-canvas-page--current");
1193
+ } else {
1194
+ if (i <= 0) return;
1195
+ pages[i].classList.remove("tpl-canvas-page--current");
1196
+ pages[i - 1].classList.add("tpl-canvas-page--current");
1197
+ }
1198
+ },
1199
+ true,
1200
+ );
1201
+ })();
1202
+ </script>
1203
+ </mj-raw>`;
1204
+ }
1205
+ export {
1206
+ RenderContext,
1207
+ SOCIAL_ICONS,
1208
+ convertMergeTagsToValues,
1209
+ escapeAttr,
1210
+ escapeHtml,
1211
+ generateSocialIconDataUri,
1212
+ getCssClassAttr,
1213
+ getCssClasses,
1214
+ getWidthPercentages,
1215
+ getWidthPixels,
1216
+ isHiddenOnAll,
1217
+ renderBlock,
1218
+ renderToMjml,
1219
+ toPaddingString
1220
+ };
1221
+ //# sourceMappingURL=index.js.map