@templatical/renderer 0.1.1 → 0.2.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/README.md +23 -5
- package/dist/index.d.ts +36 -4
- package/dist/index.js +64 -15
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -24,26 +24,28 @@ import { renderToMjml } from '@templatical/renderer';
|
|
|
24
24
|
import type { TemplateContent } from '@templatical/types';
|
|
25
25
|
|
|
26
26
|
const content: TemplateContent = JSON.parse(stored);
|
|
27
|
-
const mjml = renderToMjml(content);
|
|
27
|
+
const mjml = await renderToMjml(content);
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
`renderToMjml` is async — it returns `Promise<string>`. Custom blocks may need async work to resolve, so the call always returns a promise even when content has none.
|
|
31
|
+
|
|
30
32
|
### Node — MJML → HTML for sending
|
|
31
33
|
|
|
32
34
|
```ts
|
|
33
35
|
import { renderToMjml } from '@templatical/renderer';
|
|
34
36
|
import mjml2html from 'mjml';
|
|
35
37
|
|
|
36
|
-
const mjml = renderToMjml(content);
|
|
38
|
+
const mjml = await renderToMjml(content);
|
|
37
39
|
const { html } = mjml2html(mjml);
|
|
38
40
|
|
|
39
41
|
// Send via Postmark, Resend, SES, Mailgun, anything
|
|
40
42
|
await mailer.send({ to, subject, html });
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
### With custom fonts
|
|
45
|
+
### With custom fonts
|
|
44
46
|
|
|
45
47
|
```ts
|
|
46
|
-
const mjml = renderToMjml(content, {
|
|
48
|
+
const mjml = await renderToMjml(content, {
|
|
47
49
|
customFonts: [
|
|
48
50
|
{ name: 'Geist', url: 'https://fonts.googleapis.com/...' },
|
|
49
51
|
],
|
|
@@ -52,14 +54,30 @@ const mjml = renderToMjml(content, {
|
|
|
52
54
|
});
|
|
53
55
|
```
|
|
54
56
|
|
|
57
|
+
### With custom blocks
|
|
58
|
+
|
|
59
|
+
The renderer doesn't know how to render custom blocks on its own — you supply a callback. Editor consumers wire this automatically through `editor.toMjml()`. Headless callers provide their own resolver:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const mjml = await renderToMjml(content, {
|
|
63
|
+
async renderCustomBlock(block) {
|
|
64
|
+
// e.g. run the same liquid template the editor uses, against block.fieldValues
|
|
65
|
+
return await myLiquidEngine.render(block.customType, block.fieldValues);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If you don't pass `renderCustomBlock`, the renderer falls back to the block's `renderedHtml` field (if any) and otherwise omits the block from output.
|
|
71
|
+
|
|
55
72
|
## API
|
|
56
73
|
|
|
57
|
-
- `renderToMjml(content, options?)
|
|
74
|
+
- `renderToMjml(content, options?): Promise<string>` — render `TemplateContent` to an MJML string. Pair with the [`mjml`](https://www.npmjs.com/package/mjml) package to compile to final HTML.
|
|
58
75
|
|
|
59
76
|
Options:
|
|
60
77
|
- `customFonts` — `CustomFont[]` declarations rendered into `<mj-attributes>`
|
|
61
78
|
- `defaultFallbackFont` — fallback when a block doesn't specify a font
|
|
62
79
|
- `allowHtmlBlocks` — pass `false` to strip HTML blocks before render (e.g. for untrusted content)
|
|
80
|
+
- `renderCustomBlock` — `(block: CustomBlock) => Promise<string>` — resolves custom blocks to HTML
|
|
63
81
|
|
|
64
82
|
## Documentation
|
|
65
83
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CustomFont, Block, ColumnLayout, SpacingValue, TemplateContent } from '@templatical/types';
|
|
1
|
+
import { CustomFont, Block, ColumnLayout, SpacingValue, CustomBlock, TemplateContent } from '@templatical/types';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Immutable context passed through the block rendering chain.
|
|
@@ -8,7 +8,19 @@ declare class RenderContext {
|
|
|
8
8
|
readonly customFonts: CustomFont[];
|
|
9
9
|
readonly defaultFallbackFont: string;
|
|
10
10
|
readonly allowHtmlBlocks: boolean;
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Map of custom block id → pre-rendered HTML, populated by `renderToMjml`
|
|
13
|
+
* before the synchronous render pass. Set when the consumer provides a
|
|
14
|
+
* `renderCustomBlock` option. Empty by default.
|
|
15
|
+
*/
|
|
16
|
+
readonly customBlockHtml: ReadonlyMap<string, string>;
|
|
17
|
+
constructor(containerWidth: number, customFonts: CustomFont[], defaultFallbackFont: string, allowHtmlBlocks: boolean,
|
|
18
|
+
/**
|
|
19
|
+
* Map of custom block id → pre-rendered HTML, populated by `renderToMjml`
|
|
20
|
+
* before the synchronous render pass. Set when the consumer provides a
|
|
21
|
+
* `renderCustomBlock` option. Empty by default.
|
|
22
|
+
*/
|
|
23
|
+
customBlockHtml?: ReadonlyMap<string, string>);
|
|
12
24
|
/**
|
|
13
25
|
* Create a new context with a different container width.
|
|
14
26
|
* Used when rendering columns with narrower widths.
|
|
@@ -95,11 +107,31 @@ interface RenderOptions {
|
|
|
95
107
|
customFonts?: CustomFont[];
|
|
96
108
|
defaultFallbackFont?: string;
|
|
97
109
|
allowHtmlBlocks?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Resolves custom blocks to their HTML representation. Called once per
|
|
112
|
+
* custom block in the content tree before MJML rendering. The renderer
|
|
113
|
+
* has no built-in knowledge of how to render custom blocks; consumers
|
|
114
|
+
* provide this function.
|
|
115
|
+
*
|
|
116
|
+
* Editor consumers: pass `editor.renderCustomBlock`.
|
|
117
|
+
*
|
|
118
|
+
* Headless consumers (Node.js, server, CLI): provide your own resolver,
|
|
119
|
+
* typically using the same liquid template + field values pipeline as
|
|
120
|
+
* the editor uses. If omitted, custom blocks fall back to
|
|
121
|
+
* `block.renderedHtml` (if present) and otherwise are omitted from the
|
|
122
|
+
* output.
|
|
123
|
+
*/
|
|
124
|
+
renderCustomBlock?: (block: CustomBlock) => Promise<string>;
|
|
98
125
|
}
|
|
99
126
|
/**
|
|
100
127
|
* Render template content to an MJML string.
|
|
101
|
-
*
|
|
128
|
+
*
|
|
129
|
+
* The function is async because resolving custom blocks may require
|
|
130
|
+
* asynchronous work (e.g., the editor's liquid renderer dynamically imports
|
|
131
|
+
* `liquidjs`). When the content has no custom blocks or `renderCustomBlock`
|
|
132
|
+
* is omitted, no async work is performed but the function still resolves
|
|
133
|
+
* synchronously — i.e., it always returns a Promise.
|
|
102
134
|
*/
|
|
103
|
-
declare function renderToMjml(content: TemplateContent, options?: RenderOptions): string
|
|
135
|
+
declare function renderToMjml(content: TemplateContent, options?: RenderOptions): Promise<string>;
|
|
104
136
|
|
|
105
137
|
export { type BlockRenderer, RenderContext, type RenderOptions, SOCIAL_ICONS, convertMergeTagsToValues, escapeAttr, escapeHtml, generateSocialIconDataUri, getCssClassAttr, getCssClasses, getWidthPercentages, getWidthPixels, isHiddenOnAll, renderBlock, renderToMjml, toPaddingString };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { isSection as isSection2 } from "@templatical/types";
|
|
2
|
+
import { isSection as isSection2, isCustomBlock as isCustomBlock2 } from "@templatical/types";
|
|
3
3
|
|
|
4
4
|
// src/render-context.ts
|
|
5
5
|
var BUILT_IN_FONT_FALLBACKS = {
|
|
@@ -13,16 +13,18 @@ var BUILT_IN_FONT_FALLBACKS = {
|
|
|
13
13
|
tahoma: "Tahoma, sans-serif"
|
|
14
14
|
};
|
|
15
15
|
var RenderContext = class _RenderContext {
|
|
16
|
-
constructor(containerWidth, customFonts, defaultFallbackFont, allowHtmlBlocks) {
|
|
16
|
+
constructor(containerWidth, customFonts, defaultFallbackFont, allowHtmlBlocks, customBlockHtml = /* @__PURE__ */ new Map()) {
|
|
17
17
|
this.containerWidth = containerWidth;
|
|
18
18
|
this.customFonts = customFonts;
|
|
19
19
|
this.defaultFallbackFont = defaultFallbackFont;
|
|
20
20
|
this.allowHtmlBlocks = allowHtmlBlocks;
|
|
21
|
+
this.customBlockHtml = customBlockHtml;
|
|
21
22
|
}
|
|
22
23
|
containerWidth;
|
|
23
24
|
customFonts;
|
|
24
25
|
defaultFallbackFont;
|
|
25
26
|
allowHtmlBlocks;
|
|
27
|
+
customBlockHtml;
|
|
26
28
|
/**
|
|
27
29
|
* Create a new context with a different container width.
|
|
28
30
|
* Used when rendering columns with narrower widths.
|
|
@@ -32,7 +34,8 @@ var RenderContext = class _RenderContext {
|
|
|
32
34
|
width,
|
|
33
35
|
this.customFonts,
|
|
34
36
|
this.defaultFallbackFont,
|
|
35
|
-
this.allowHtmlBlocks
|
|
37
|
+
this.allowHtmlBlocks,
|
|
38
|
+
this.customBlockHtml
|
|
36
39
|
);
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
@@ -116,6 +119,15 @@ function toPaddingString(padding) {
|
|
|
116
119
|
return `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`;
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
// src/utils.ts
|
|
123
|
+
function bgAttr(backgroundColor, placement) {
|
|
124
|
+
if (!backgroundColor) {
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
const name = placement === "native" ? "background-color" : "container-background-color";
|
|
128
|
+
return ` ${name}="${backgroundColor}"`;
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
// src/visibility.ts
|
|
120
132
|
function isHiddenOnAll(block) {
|
|
121
133
|
const visibility = block.visibility;
|
|
@@ -155,7 +167,7 @@ function renderTitle(block, context) {
|
|
|
155
167
|
return "";
|
|
156
168
|
}
|
|
157
169
|
const padding = toPaddingString(block.styles.padding);
|
|
158
|
-
const bgColor = block.styles.backgroundColor
|
|
170
|
+
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
159
171
|
const content = convertMergeTagsToValues(block.content);
|
|
160
172
|
const fontSize = HEADING_LEVEL_FONT_SIZE[block.level];
|
|
161
173
|
const color = block.color;
|
|
@@ -185,7 +197,7 @@ function renderParagraph(block, _context) {
|
|
|
185
197
|
return "";
|
|
186
198
|
}
|
|
187
199
|
const padding = toPaddingString(block.styles.padding);
|
|
188
|
-
const bgColor = block.styles.backgroundColor
|
|
200
|
+
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
189
201
|
const content = convertMergeTagsToValues(block.content);
|
|
190
202
|
const visibilityAttr = getCssClassAttr(block);
|
|
191
203
|
return `<mj-text
|
|
@@ -200,7 +212,7 @@ function renderImage(block, context) {
|
|
|
200
212
|
return "";
|
|
201
213
|
}
|
|
202
214
|
const padding = toPaddingString(block.styles.padding);
|
|
203
|
-
const bgColor = block.styles.backgroundColor
|
|
215
|
+
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
204
216
|
const width = block.width === "full" ? context.containerWidth + "px" : block.width + "px";
|
|
205
217
|
const visibilityAttr = getCssClassAttr(block);
|
|
206
218
|
let linkAttr = "";
|
|
@@ -228,7 +240,7 @@ function renderButton(block, context) {
|
|
|
228
240
|
return "";
|
|
229
241
|
}
|
|
230
242
|
const padding = toPaddingString(block.styles.padding);
|
|
231
|
-
const bgColor = block.styles.backgroundColor
|
|
243
|
+
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
232
244
|
const buttonPadding = toPaddingString(block.buttonPadding);
|
|
233
245
|
const href = escapeAttr(block.url);
|
|
234
246
|
const backgroundColor = block.backgroundColor;
|
|
@@ -478,7 +490,7 @@ function renderMenu(block, context) {
|
|
|
478
490
|
return "";
|
|
479
491
|
}
|
|
480
492
|
const padding = toPaddingString(block.styles.padding);
|
|
481
|
-
const bgColor = block.styles.backgroundColor
|
|
493
|
+
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
482
494
|
const visibilityAttr = getCssClassAttr(block);
|
|
483
495
|
const fontFamilyAttr = renderFontFamilyAttr3(block.fontFamily, context);
|
|
484
496
|
const align = block.textAlign;
|
|
@@ -543,7 +555,7 @@ function renderTable(block, context) {
|
|
|
543
555
|
return "";
|
|
544
556
|
}
|
|
545
557
|
const padding = toPaddingString(block.styles.padding);
|
|
546
|
-
const bgColor = block.styles.backgroundColor
|
|
558
|
+
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
547
559
|
const visibilityAttr = getCssClassAttr(block);
|
|
548
560
|
const fontFamilyAttr = renderFontFamilyAttr4(block.fontFamily, context);
|
|
549
561
|
const fontSize = block.fontSize;
|
|
@@ -605,11 +617,12 @@ function renderFontFamilyAttr4(fontFamily, context) {
|
|
|
605
617
|
}
|
|
606
618
|
|
|
607
619
|
// src/renderers/custom.ts
|
|
608
|
-
function renderCustom(block,
|
|
620
|
+
function renderCustom(block, context) {
|
|
609
621
|
if (isHiddenOnAll(block)) {
|
|
610
622
|
return "";
|
|
611
623
|
}
|
|
612
|
-
const
|
|
624
|
+
const fromContext = context.customBlockHtml.get(block.id);
|
|
625
|
+
const content = fromContext ?? block.renderedHtml;
|
|
613
626
|
if (!content || content === "") {
|
|
614
627
|
return "";
|
|
615
628
|
}
|
|
@@ -658,7 +671,7 @@ function renderSection(block, context, renderBlock2) {
|
|
|
658
671
|
const columnWidths = getWidthPercentages(columnsLayout);
|
|
659
672
|
const columnWidthsPx = getWidthPixels(columnsLayout, context.containerWidth);
|
|
660
673
|
const padding = toPaddingString(block.styles.padding);
|
|
661
|
-
const bgColor = block.styles.backgroundColor
|
|
674
|
+
const bgColor = bgAttr(block.styles.backgroundColor, "native");
|
|
662
675
|
const visibilityAttr = getCssClassAttr(block);
|
|
663
676
|
const children = block.children;
|
|
664
677
|
const columnsContent = [];
|
|
@@ -721,7 +734,7 @@ function renderVideo(block, context) {
|
|
|
721
734
|
return "";
|
|
722
735
|
}
|
|
723
736
|
const padding = toPaddingString(block.styles.padding);
|
|
724
|
-
const bgColor = block.styles.backgroundColor
|
|
737
|
+
const bgColor = bgAttr(block.styles.backgroundColor, "container");
|
|
725
738
|
const width = block.width === "full" ? context.containerWidth + "px" : block.width + "px";
|
|
726
739
|
const visibilityAttr = getCssClassAttr(block);
|
|
727
740
|
const src = escapeAttr(thumbnailUrl);
|
|
@@ -784,15 +797,20 @@ function renderBlock(block, context) {
|
|
|
784
797
|
}
|
|
785
798
|
|
|
786
799
|
// src/index.ts
|
|
787
|
-
function renderToMjml(content, options) {
|
|
800
|
+
async function renderToMjml(content, options) {
|
|
788
801
|
const customFonts = options?.customFonts ?? [];
|
|
789
802
|
const defaultFallbackFont = options?.defaultFallbackFont ?? "Arial, sans-serif";
|
|
790
803
|
const allowHtmlBlocks = options?.allowHtmlBlocks ?? true;
|
|
804
|
+
const customBlockHtml = await resolveCustomBlocks(
|
|
805
|
+
content,
|
|
806
|
+
options?.renderCustomBlock
|
|
807
|
+
);
|
|
791
808
|
const renderContext = new RenderContext(
|
|
792
809
|
content.settings.width,
|
|
793
810
|
customFonts,
|
|
794
811
|
defaultFallbackFont,
|
|
795
|
-
allowHtmlBlocks
|
|
812
|
+
allowHtmlBlocks,
|
|
813
|
+
customBlockHtml
|
|
796
814
|
);
|
|
797
815
|
const blocks = filterHtmlBlocks2(content.blocks, allowHtmlBlocks);
|
|
798
816
|
const fontFamily = renderContext.resolveFontFamily(
|
|
@@ -886,6 +904,37 @@ function filterHtmlBlocks2(blocks, allowHtmlBlocks) {
|
|
|
886
904
|
}
|
|
887
905
|
return blocks.filter((block) => block.type !== "html");
|
|
888
906
|
}
|
|
907
|
+
async function resolveCustomBlocks(content, renderCustomBlock) {
|
|
908
|
+
const result = /* @__PURE__ */ new Map();
|
|
909
|
+
if (!renderCustomBlock) {
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
const customBlocks = [];
|
|
913
|
+
collectCustomBlocks(content.blocks, customBlocks);
|
|
914
|
+
if (customBlocks.length === 0) {
|
|
915
|
+
return result;
|
|
916
|
+
}
|
|
917
|
+
const rendered = await Promise.all(
|
|
918
|
+
customBlocks.map((block) => renderCustomBlock(block))
|
|
919
|
+
);
|
|
920
|
+
for (let index = 0; index < customBlocks.length; index++) {
|
|
921
|
+
result.set(customBlocks[index].id, rendered[index]);
|
|
922
|
+
}
|
|
923
|
+
return result;
|
|
924
|
+
}
|
|
925
|
+
function collectCustomBlocks(blocks, out) {
|
|
926
|
+
for (const block of blocks) {
|
|
927
|
+
if (isCustomBlock2(block)) {
|
|
928
|
+
out.push(block);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (isSection2(block)) {
|
|
932
|
+
for (const column of block.children) {
|
|
933
|
+
collectCustomBlocks(column, out);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
889
938
|
export {
|
|
890
939
|
RenderContext,
|
|
891
940
|
SOCIAL_ICONS,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/render-context.ts","../src/renderers/index.ts","../src/renderers/title.ts","../src/escape.ts","../src/padding.ts","../src/visibility.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/social-icons.ts","../src/renderers/social.ts","../src/renderers/menu.ts","../src/renderers/table.ts","../src/renderers/custom.ts","../src/columns.ts","../src/renderers/section.ts","../src/renderers/video.ts"],"sourcesContent":["import type { Block, TemplateContent, CustomFont } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport { RenderContext } from \"./render-context\";\nimport { renderBlock } from \"./renderers\";\nimport { escapeHtml, escapeAttr } from \"./escape\";\n\nexport interface RenderOptions {\n customFonts?: CustomFont[];\n defaultFallbackFont?: string;\n allowHtmlBlocks?: boolean;\n}\n\n/**\n * Render template content to an MJML string.\n * This is the main entry point that matches the PHP MjmlRenderingService.export() output.\n */\nexport function renderToMjml(\n content: TemplateContent,\n options?: RenderOptions,\n): string {\n const customFonts = options?.customFonts ?? [];\n const defaultFallbackFont =\n options?.defaultFallbackFont ?? \"Arial, sans-serif\";\n const allowHtmlBlocks = options?.allowHtmlBlocks ?? true;\n\n const renderContext = new RenderContext(\n content.settings.width,\n customFonts,\n defaultFallbackFont,\n allowHtmlBlocks,\n );\n\n const blocks = filterHtmlBlocks(content.blocks, allowHtmlBlocks);\n const fontFamily = renderContext.resolveFontFamily(\n content.settings.fontFamily,\n );\n const backgroundColor = content.settings.backgroundColor;\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(content.settings.preheaderText);\n\n return `<mjml>\n <mj-head>${previewTag}\n <mj-attributes>\n <mj-all font-family=\"${fontFamily}\" />\n <mj-section padding=\"0\" />\n <mj-column padding=\"0\" />\n <mj-image fluid-on-mobile=\"true\" />\n </mj-attributes>${fontDeclarations}\n <mj-style>\n a { color: inherit; text-decoration: none; }\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) and (max-width: 768px) {\n .tpl-hide-tablet { display: none !important; mso-hide: all !important; }\n }\n @media only screen and (min-width: 769px) {\n .tpl-hide-desktop { display: none !important; mso-hide: all !important; }\n }\n </mj-style>\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. Sections are rendered directly,\n * non-section blocks are wrapped in a default section/column.\n */\nfunction renderTopLevelBlock(block: Block, context: RenderContext): string {\n if (isSection(block)) {\n const rendered = renderBlock(block, context);\n return wrapWithDisplayCondition(block, rendered);\n }\n\n const content = renderBlock(block, context);\n const wrapped = wrapInSection(content);\n return wrapWithDisplayCondition(block, wrapped);\n}\n\n/**\n * Wrap rendered block content with display condition tags if present.\n */\nfunction wrapWithDisplayCondition(block: Block, rendered: string): 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\n/**\n * Wrap block content in a default mj-section/mj-column for non-section blocks.\n */\nfunction wrapInSection(content: string): string {\n if (content === \"\") {\n return \"\";\n }\n\n return `<mj-section>\n <mj-column>\n${content}\n </mj-column>\n</mj-section>`;\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// Re-export utilities for consumers\nexport { RenderContext } from \"./render-context\";\nexport { escapeHtml, escapeAttr, convertMergeTagsToValues } from \"./escape\";\nexport { isHiddenOnAll, getCssClassAttr, getCssClasses } from \"./visibility\";\nexport { getWidthPercentages, getWidthPixels } from \"./columns\";\nexport { toPaddingString } from \"./padding\";\nexport { SOCIAL_ICONS, generateSocialIconDataUri } from \"./social-icons\";\nexport { renderBlock } from \"./renderers\";\nexport type { BlockRenderer } from \"./renderers/section\";\n","import type { CustomFont } from \"@templatical/types\";\n\nconst BUILT_IN_FONT_FALLBACKS: Record<string, string> = {\n arial: \"Arial, sans-serif\",\n helvetica: \"Helvetica, sans-serif\",\n georgia: \"Georgia, serif\",\n \"times new roman\": \"'Times New Roman', serif\",\n verdana: \"Verdana, sans-serif\",\n \"trebuchet ms\": \"'Trebuchet MS', sans-serif\",\n \"courier new\": \"'Courier New', monospace\",\n tahoma: \"Tahoma, sans-serif\",\n};\n\n/**\n * Immutable context passed through the block rendering chain.\n */\nexport class RenderContext {\n constructor(\n public readonly containerWidth: number,\n public readonly customFonts: CustomFont[],\n public readonly defaultFallbackFont: string,\n public readonly allowHtmlBlocks: boolean,\n ) {}\n\n /**\n * Create a new context with a different container width.\n * Used when rendering columns with narrower widths.\n */\n withContainerWidth(width: number): RenderContext {\n return new RenderContext(\n width,\n this.customFonts,\n this.defaultFallbackFont,\n this.allowHtmlBlocks,\n );\n }\n\n /**\n * Resolve a font family name to include custom font fallbacks.\n * If the font matches a custom font, returns `'FontName', fallback`.\n * Otherwise returns the original font family string.\n */\n resolveFontFamily(fontFamily: string): string {\n // Check custom fonts first\n for (const customFont of this.customFonts) {\n if (customFont.name.toLowerCase() === fontFamily.toLowerCase()) {\n const fallback = customFont.fallback ?? this.defaultFallbackFont;\n\n return `'${customFont.name}', ${fallback}`;\n }\n }\n\n // Resolve built-in fonts to include fallback stacks\n const builtIn = BUILT_IN_FONT_FALLBACKS[fontFamily.toLowerCase()];\n if (builtIn) {\n return builtIn;\n }\n\n return fontFamily;\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 { 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 */\nexport function renderBlock(block: Block, context: RenderContext): string {\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 // Countdown blocks are rendered by the Templatical Cloud backend.\n // In OSS mode they return empty — use initCloud() for full countdown support.\n return \"\";\n}\n","import type { TitleBlock } from \"@templatical/types\";\nimport { HEADING_LEVEL_FONT_SIZE } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { convertMergeTagsToValues } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\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 = block.styles.backgroundColor\n ? ` background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const content = convertMergeTagsToValues(block.content);\n const fontSize = HEADING_LEVEL_FONT_SIZE[block.level];\n const color = block.color;\n const align = block.textAlign;\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const visibilityAttr = getCssClassAttr(block);\n const tag = `h${block.level}`;\n\n return `<mj-text\n font-size=\"${fontSize}px\"\n color=\"${color}\"\n align=\"${align}\"\n line-height=\"1.3\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n><${tag} style=\"margin:0;font-size:inherit;color:inherit;line-height:inherit\">${content}</${tag}></mj-text>`;\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=\"${resolved}\"`;\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 * Replace merge tag span elements with their data attribute values.\n * Converts `<span data-merge-tag=\"{{name}}\">Label</span>` to `{{name}}`.\n * Also handles `data-logic-merge-tag` attributes.\n */\nexport function convertMergeTagsToValues(html: string): string {\n if (html === \"\") {\n return \"\";\n }\n\n // Replace <span data-merge-tag=\"...\">...</span> with the merge tag value\n let result = html.replace(\n /<span[^>]*\\bdata-merge-tag=\"([^\"]*)\"[^>]*>.*?<\\/span>/gs,\n \"$1\",\n );\n\n // Replace <span data-logic-merge-tag=\"...\">...</span> with the merge tag value\n result = result.replace(\n /<span[^>]*\\bdata-logic-merge-tag=\"([^\"]*)\"[^>]*>.*?<\\/span>/gs,\n \"$1\",\n );\n\n return result;\n}\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","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.tablet && !visibility.mobile;\n}\n\n/**\n * Get the MJML css-class attribute string for visibility hiding.\n * Returns a string like ` css-class=\"tpl-hide-desktop tpl-hide-tablet\"` or empty string.\n */\nexport function getCssClassAttr(block: Block): string {\n const classes = getCssClasses(block);\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.tablet) {\n classes.push(\"tpl-hide-tablet\");\n }\n\n if (!visibility.mobile) {\n classes.push(\"tpl-hide-mobile\");\n }\n\n return classes.join(\" \");\n}\n","import type { ParagraphBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { convertMergeTagsToValues } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\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 const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const content = convertMergeTagsToValues(block.content);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-text\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\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 { 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 const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const width =\n block.width === \"full\" ? context.containerWidth + \"px\" : block.width + \"px\";\n\n const visibilityAttr = getCssClassAttr(block);\n\n let linkAttr = \"\";\n if (block.linkUrl) {\n linkAttr = ` href=\"${escapeAttr(block.linkUrl)}\"`;\n if (block.linkOpenInNewTab) {\n linkAttr += ' target=\"_blank\"';\n }\n }\n\n const src = escapeAttr(block.src);\n const alt = escapeAttr(block.alt);\n const align = block.align;\n\n return `<mj-image\n src=\"${src}\"\n alt=\"${alt}\"\n width=\"${width}\"\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${linkAttr}${visibilityAttr}\n/>`;\n}\n","import type { ButtonBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr, escapeHtml } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\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 = block.styles.backgroundColor\n ? ` container-background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const buttonPadding = toPaddingString(block.buttonPadding);\n\n const href = escapeAttr(block.url);\n const backgroundColor = block.backgroundColor;\n const textColor = block.textColor;\n const fontSize = block.fontSize;\n const borderRadius = block.borderRadius;\n const text = escapeHtml(block.text);\n const targetAttr = block.openInNewTab ? ' target=\"_blank\"' : \"\";\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-button\n href=\"${href}\"${targetAttr}\n background-color=\"${backgroundColor}\"\n color=\"${textColor}\"\n font-size=\"${fontSize}px\"\n font-weight=\"bold\"\n border-radius=\"${borderRadius}px\"\n inner-padding=\"${buttonPadding}\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${text}</mj-button>`;\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=\"${resolved}\"`;\n}\n","import type { DividerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\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=\"${block.styles.backgroundColor}\"`\n : \"\";\n const width = block.width === \"full\" ? \"100%\" : block.width + \"px\";\n const thickness = block.thickness;\n const lineStyle = block.lineStyle;\n const color = 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 { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a spacer block to MJML markup.\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 padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-spacer height=\"${height}px\" padding=\"${padding}\"${bgColor}${visibilityAttr} />`;\n}\n","import type { HtmlBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\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\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${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","/**\n * Social icon SVG path data and brand colors for all supported platforms.\n */\nexport const SOCIAL_ICONS: Record<string, { path: string; color: string }> = {\n facebook: {\n 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\",\n color: \"#1877F2\",\n },\n twitter: {\n 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\",\n color: \"#000000\",\n },\n instagram: {\n 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\",\n color: \"#E4405F\",\n },\n linkedin: {\n 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\",\n color: \"#0A66C2\",\n },\n youtube: {\n 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\",\n color: \"#FF0000\",\n },\n tiktok: {\n 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\",\n color: \"#000000\",\n },\n pinterest: {\n 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\",\n color: \"#BD081C\",\n },\n email: {\n 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\",\n color: \"#6B7280\",\n },\n whatsapp: {\n 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\",\n color: \"#25D366\",\n },\n telegram: {\n 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\",\n color: \"#26A5E4\",\n },\n discord: {\n 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\",\n color: \"#5865F2\",\n },\n snapchat: {\n 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\",\n color: \"#FFFC00\",\n },\n reddit: {\n 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\",\n color: \"#FF4500\",\n },\n github: {\n 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\",\n color: \"#181717\",\n },\n dribbble: {\n 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\",\n color: \"#EA4C89\",\n },\n behance: {\n 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\",\n color: \"#1769FF\",\n },\n};\n\n/**\n * Generate a base64-encoded SVG data URI for a social icon.\n * Ported from SocialIconSvgGenerator.php.\n */\nexport function generateSocialIconDataUri(\n platform: string,\n style: string,\n size: number,\n): string {\n const iconData = SOCIAL_ICONS[platform];\n const path = iconData?.path ?? \"\";\n const brandColor = iconData?.color ?? \"#6B7280\";\n\n if (path === \"\") {\n return \"\";\n }\n\n // Only outlined style has transparent bg with colored icon\n // All other styles (solid, rounded, square, circle) have colored bg with white icon\n const isOutlined = style === \"outlined\";\n const iconColor = isOutlined ? brandColor : \"#ffffff\";\n\n // Border radius values proportional to editor (based on 24x24 viewBox)\n let bgShape: string;\n switch (style) {\n case \"circle\":\n bgShape = `<circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"${brandColor}\"/>`;\n break;\n case \"rounded\":\n bgShape = `<rect width=\"24\" height=\"24\" rx=\"6\" fill=\"${brandColor}\"/>`;\n break;\n case \"square\":\n bgShape = `<rect width=\"24\" height=\"24\" rx=\"0\" fill=\"${brandColor}\"/>`;\n break;\n case \"outlined\":\n bgShape = `<rect width=\"22\" height=\"22\" x=\"1\" y=\"1\" rx=\"3\" fill=\"transparent\" stroke=\"${brandColor}\" stroke-width=\"2\"/>`;\n break;\n default:\n bgShape = `<rect width=\"24\" height=\"24\" rx=\"3\" fill=\"${brandColor}\"/>`;\n break;\n }\n\n // Icon size = 60% of container (matching editor's Math.floor(size * 0.6))\n // In 24x24 viewBox: 60% = 14.4, so translate by (24-14.4)/2 = 4.8 and scale by 0.6\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width=\"${size}\" height=\"${size}\">` +\n bgShape +\n `<g transform=\"translate(4.8, 4.8) scale(0.6)\">` +\n `<path d=\"${path}\" fill=\"${iconColor}\"/>` +\n `</g></svg>`;\n\n return \"data:image/svg+xml;base64,\" + btoa(svg);\n}\n","import type { SocialIconsBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\nimport { generateSocialIconDataUri } from \"../social-icons\";\n\n/**\n * Render a social icons block to MJML markup.\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=\"${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 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\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\n // Generate custom SVG icon as data URI to match editor appearance\n const iconSrc = generateSocialIconDataUri(platform, iconStyle, iconSizePx);\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 { escapeHtml, escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\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 = block.styles.backgroundColor\n ? ` background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const align = block.textAlign;\n const fontSize = block.fontSize;\n const color = block.color;\n\n const content = renderMenuItems(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"\n color=\"${color}\"\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 = escapeAttr(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(item: MenuItemData, linkColor: string): string {\n const text = escapeHtml(item.text);\n const url = escapeAttr(item.url);\n const color = item.color ?? linkColor;\n const target = item.openInNewTab ? ' target=\"_blank\"' : \"\";\n\n const styles: string[] = [`color: ${color}`, \"text-decoration: none\"];\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=\"${resolved}\"`;\n}\n","import type {\n TableBlock,\n TableRowData,\n TableCellData,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr, convertMergeTagsToValues } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\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 = block.styles.backgroundColor\n ? ` background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const fontSize = block.fontSize;\n const color = block.color;\n const align = block.textAlign;\n\n const tableHtml = renderTableElement(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"\n color=\"${color}\"\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 = escapeAttr(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: ${escapeAttr(block.headerBackgroundColor)}`,\n );\n }\n }\n\n const styleAttr = styles.join(\"; \");\n const content = 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=\"${resolved}\"`;\n}\n","import type { CustomBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a custom block to MJML markup using pre-rendered HTML from the SDK.\n */\nexport function renderCustom(\n block: CustomBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const content = block.renderedHtml;\n\n if (!content || content === \"\") {\n return \"\";\n }\n\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-text${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 return [\"33.33%\", \"33.33%\", \"33.33%\"];\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 { Block, SectionBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { getWidthPercentages, getWidthPixels } from \"../columns\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\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 * 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 columnsLayout = block.columns;\n const columnWidths = getWidthPercentages(columnsLayout);\n const columnWidthsPx = getWidthPixels(columnsLayout, context.containerWidth);\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\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 const filteredColumn = filterHtmlBlocks(column, context.allowHtmlBlocks);\n const columnContext = context.withContainerWidth(columnWidth);\n\n const columnBlocks = filteredColumn\n .map((child) => renderBlock(child, columnContext))\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}\">\n${content}\n</mj-column>`);\n }\n\n const columns = columnsContent.join(\"\\n\");\n\n return `<mj-section${bgColor} padding=\"${padding}\"${visibilityAttr}>\n${columns}\n</mj-section>`;\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 { 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 = block.styles.backgroundColor\n ? ` background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const width =\n block.width === \"full\" ? context.containerWidth + \"px\" : block.width + \"px\";\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}\"\n align=\"${align}\"\n padding=\"${padding}\"\n href=\"${href}\"\n target=\"_blank\"${bgColor}${visibilityAttr}\n/>`;\n}\n"],"mappings":"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","names":["isSection","renderFontFamilyAttr","renderFontFamilyAttr","renderFontFamilyAttr","renderBlock","filterHtmlBlocks","isSection"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/render-context.ts","../src/renderers/index.ts","../src/renderers/title.ts","../src/escape.ts","../src/padding.ts","../src/utils.ts","../src/visibility.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/social-icons.ts","../src/renderers/social.ts","../src/renderers/menu.ts","../src/renderers/table.ts","../src/renderers/custom.ts","../src/columns.ts","../src/renderers/section.ts","../src/renderers/video.ts"],"sourcesContent":["import type {\n Block,\n CustomBlock,\n TemplateContent,\n CustomFont,\n} from \"@templatical/types\";\nimport { isSection, isCustomBlock } from \"@templatical/types\";\nimport { RenderContext } from \"./render-context\";\nimport { renderBlock } from \"./renderers\";\nimport { escapeHtml, escapeAttr } from \"./escape\";\n\nexport interface RenderOptions {\n customFonts?: CustomFont[];\n defaultFallbackFont?: string;\n allowHtmlBlocks?: boolean;\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 * has no built-in knowledge of how to render custom blocks; consumers\n * provide this function.\n *\n * Editor consumers: pass `editor.renderCustomBlock`.\n *\n * Headless consumers (Node.js, server, CLI): provide your own resolver,\n * typically using the same liquid template + field values pipeline as\n * the editor uses. If omitted, custom blocks fall back to\n * `block.renderedHtml` (if present) and otherwise are omitted from the\n * output.\n */\n renderCustomBlock?: (block: CustomBlock) => Promise<string>;\n}\n\n/**\n * Render template content to an MJML string.\n *\n * The function is async because resolving custom blocks may require\n * asynchronous work (e.g., the editor's liquid renderer dynamically imports\n * `liquidjs`). When the content has no custom blocks or `renderCustomBlock`\n * is omitted, no async work is performed but the function still resolves\n * synchronously — i.e., it always returns a Promise.\n */\nexport async function renderToMjml(\n content: TemplateContent,\n options?: RenderOptions,\n): Promise<string> {\n const customFonts = options?.customFonts ?? [];\n const defaultFallbackFont =\n options?.defaultFallbackFont ?? \"Arial, sans-serif\";\n const allowHtmlBlocks = options?.allowHtmlBlocks ?? true;\n\n const customBlockHtml = await resolveCustomBlocks(\n content,\n options?.renderCustomBlock,\n );\n\n const renderContext = new RenderContext(\n content.settings.width,\n customFonts,\n defaultFallbackFont,\n allowHtmlBlocks,\n customBlockHtml,\n );\n\n const blocks = filterHtmlBlocks(content.blocks, allowHtmlBlocks);\n const fontFamily = renderContext.resolveFontFamily(\n content.settings.fontFamily,\n );\n const backgroundColor = content.settings.backgroundColor;\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(content.settings.preheaderText);\n\n return `<mjml>\n <mj-head>${previewTag}\n <mj-attributes>\n <mj-all font-family=\"${fontFamily}\" />\n <mj-section padding=\"0\" />\n <mj-column padding=\"0\" />\n <mj-image fluid-on-mobile=\"true\" />\n </mj-attributes>${fontDeclarations}\n <mj-style>\n a { color: inherit; text-decoration: none; }\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) and (max-width: 768px) {\n .tpl-hide-tablet { display: none !important; mso-hide: all !important; }\n }\n @media only screen and (min-width: 769px) {\n .tpl-hide-desktop { display: none !important; mso-hide: all !important; }\n }\n </mj-style>\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. Sections are rendered directly,\n * non-section blocks are wrapped in a default section/column.\n */\nfunction renderTopLevelBlock(block: Block, context: RenderContext): string {\n if (isSection(block)) {\n const rendered = renderBlock(block, context);\n return wrapWithDisplayCondition(block, rendered);\n }\n\n const content = renderBlock(block, context);\n const wrapped = wrapInSection(content);\n return wrapWithDisplayCondition(block, wrapped);\n}\n\n/**\n * Wrap rendered block content with display condition tags if present.\n */\nfunction wrapWithDisplayCondition(block: Block, rendered: string): 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\n/**\n * Wrap block content in a default mj-section/mj-column for non-section blocks.\n */\nfunction wrapInSection(content: string): string {\n if (content === \"\") {\n return \"\";\n }\n\n return `<mj-section>\n <mj-column>\n${content}\n </mj-column>\n</mj-section>`;\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\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 }\n }\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\";\nexport { getWidthPercentages, getWidthPixels } from \"./columns\";\nexport { toPaddingString } from \"./padding\";\nexport { SOCIAL_ICONS, generateSocialIconDataUri } from \"./social-icons\";\nexport { renderBlock } from \"./renderers\";\nexport type { BlockRenderer } from \"./renderers/section\";\n","import type { CustomFont } from \"@templatical/types\";\n\nconst BUILT_IN_FONT_FALLBACKS: Record<string, string> = {\n arial: \"Arial, sans-serif\",\n helvetica: \"Helvetica, sans-serif\",\n georgia: \"Georgia, serif\",\n \"times new roman\": \"'Times New Roman', serif\",\n verdana: \"Verdana, sans-serif\",\n \"trebuchet ms\": \"'Trebuchet MS', sans-serif\",\n \"courier new\": \"'Courier New', monospace\",\n tahoma: \"Tahoma, sans-serif\",\n};\n\n/**\n * Immutable context passed through the block rendering chain.\n */\nexport class RenderContext {\n constructor(\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\n /**\n * Create a new context with a different container width.\n * Used when rendering columns with narrower widths.\n */\n withContainerWidth(width: number): RenderContext {\n return new RenderContext(\n width,\n this.customFonts,\n this.defaultFallbackFont,\n this.allowHtmlBlocks,\n this.customBlockHtml,\n );\n }\n\n /**\n * Resolve a font family name to include custom font fallbacks.\n * If the font matches a custom font, returns `'FontName', fallback`.\n * Otherwise returns the original font family string.\n */\n resolveFontFamily(fontFamily: string): string {\n // Check custom fonts first\n for (const customFont of this.customFonts) {\n if (customFont.name.toLowerCase() === fontFamily.toLowerCase()) {\n const fallback = customFont.fallback ?? this.defaultFallbackFont;\n\n return `'${customFont.name}', ${fallback}`;\n }\n }\n\n // Resolve built-in fonts to include fallback stacks\n const builtIn = BUILT_IN_FONT_FALLBACKS[fontFamily.toLowerCase()];\n if (builtIn) {\n return builtIn;\n }\n\n return fontFamily;\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 { 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 */\nexport function renderBlock(block: Block, context: RenderContext): string {\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 // Countdown blocks are rendered by the Templatical Cloud backend.\n // In OSS mode they return empty — use initCloud() for full countdown support.\n return \"\";\n}\n","import type { TitleBlock } from \"@templatical/types\";\nimport { HEADING_LEVEL_FONT_SIZE } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { convertMergeTagsToValues } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\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 const content = convertMergeTagsToValues(block.content);\n const fontSize = HEADING_LEVEL_FONT_SIZE[block.level];\n const color = block.color;\n const align = block.textAlign;\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const visibilityAttr = getCssClassAttr(block);\n const tag = `h${block.level}`;\n\n return `<mj-text\n font-size=\"${fontSize}px\"\n color=\"${color}\"\n align=\"${align}\"\n line-height=\"1.3\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n><${tag} style=\"margin:0;font-size:inherit;color:inherit;line-height:inherit\">${content}</${tag}></mj-text>`;\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=\"${resolved}\"`;\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 * Replace merge tag span elements with their data attribute values.\n * Converts `<span data-merge-tag=\"{{name}}\">Label</span>` to `{{name}}`.\n * Also handles `data-logic-merge-tag` attributes.\n */\nexport function convertMergeTagsToValues(html: string): string {\n if (html === \"\") {\n return \"\";\n }\n\n // Replace <span data-merge-tag=\"...\">...</span> with the merge tag value\n let result = html.replace(\n /<span[^>]*\\bdata-merge-tag=\"([^\"]*)\"[^>]*>.*?<\\/span>/gs,\n \"$1\",\n );\n\n // Replace <span data-logic-merge-tag=\"...\">...</span> with the merge tag value\n result = result.replace(\n /<span[^>]*\\bdata-logic-merge-tag=\"([^\"]*)\"[^>]*>.*?<\\/span>/gs,\n \"$1\",\n );\n\n return result;\n}\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\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","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.tablet && !visibility.mobile;\n}\n\n/**\n * Get the MJML css-class attribute string for visibility hiding.\n * Returns a string like ` css-class=\"tpl-hide-desktop tpl-hide-tablet\"` or empty string.\n */\nexport function getCssClassAttr(block: Block): string {\n const classes = getCssClasses(block);\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.tablet) {\n classes.push(\"tpl-hide-tablet\");\n }\n\n if (!visibility.mobile) {\n classes.push(\"tpl-hide-mobile\");\n }\n\n return classes.join(\" \");\n}\n","import type { ParagraphBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { convertMergeTagsToValues } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\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 const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const content = convertMergeTagsToValues(block.content);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-text\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\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 { bgAttr } 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 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\n const visibilityAttr = getCssClassAttr(block);\n\n let linkAttr = \"\";\n if (block.linkUrl) {\n linkAttr = ` href=\"${escapeAttr(block.linkUrl)}\"`;\n if (block.linkOpenInNewTab) {\n linkAttr += ' target=\"_blank\"';\n }\n }\n\n const src = escapeAttr(block.src);\n const alt = escapeAttr(block.alt);\n const align = block.align;\n\n return `<mj-image\n src=\"${src}\"\n alt=\"${alt}\"\n width=\"${width}\"\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${linkAttr}${visibilityAttr}\n/>`;\n}\n","import type { ButtonBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr, escapeHtml } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\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 const href = escapeAttr(block.url);\n const backgroundColor = block.backgroundColor;\n const textColor = block.textColor;\n const fontSize = block.fontSize;\n const borderRadius = block.borderRadius;\n const text = escapeHtml(block.text);\n const targetAttr = block.openInNewTab ? ' target=\"_blank\"' : \"\";\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-button\n href=\"${href}\"${targetAttr}\n background-color=\"${backgroundColor}\"\n color=\"${textColor}\"\n font-size=\"${fontSize}px\"\n font-weight=\"bold\"\n border-radius=\"${borderRadius}px\"\n inner-padding=\"${buttonPadding}\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${text}</mj-button>`;\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=\"${resolved}\"`;\n}\n","import type { DividerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\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=\"${block.styles.backgroundColor}\"`\n : \"\";\n const width = block.width === \"full\" ? \"100%\" : block.width + \"px\";\n const thickness = block.thickness;\n const lineStyle = block.lineStyle;\n const color = 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 { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a spacer block to MJML markup.\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 padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${block.styles.backgroundColor}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-spacer height=\"${height}px\" padding=\"${padding}\"${bgColor}${visibilityAttr} />`;\n}\n","import type { HtmlBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\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\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${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","/**\n * Social icon SVG path data and brand colors for all supported platforms.\n */\nexport const SOCIAL_ICONS: Record<string, { path: string; color: string }> = {\n facebook: {\n 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\",\n color: \"#1877F2\",\n },\n twitter: {\n 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\",\n color: \"#000000\",\n },\n instagram: {\n 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\",\n color: \"#E4405F\",\n },\n linkedin: {\n 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\",\n color: \"#0A66C2\",\n },\n youtube: {\n 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\",\n color: \"#FF0000\",\n },\n tiktok: {\n 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\",\n color: \"#000000\",\n },\n pinterest: {\n 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\",\n color: \"#BD081C\",\n },\n email: {\n 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\",\n color: \"#6B7280\",\n },\n whatsapp: {\n 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\",\n color: \"#25D366\",\n },\n telegram: {\n 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\",\n color: \"#26A5E4\",\n },\n discord: {\n 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\",\n color: \"#5865F2\",\n },\n snapchat: {\n 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\",\n color: \"#FFFC00\",\n },\n reddit: {\n 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\",\n color: \"#FF4500\",\n },\n github: {\n 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\",\n color: \"#181717\",\n },\n dribbble: {\n 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\",\n color: \"#EA4C89\",\n },\n behance: {\n 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\",\n color: \"#1769FF\",\n },\n};\n\n/**\n * Generate a base64-encoded SVG data URI for a social icon.\n * Ported from SocialIconSvgGenerator.php.\n */\nexport function generateSocialIconDataUri(\n platform: string,\n style: string,\n size: number,\n): string {\n const iconData = SOCIAL_ICONS[platform];\n const path = iconData?.path ?? \"\";\n const brandColor = iconData?.color ?? \"#6B7280\";\n\n if (path === \"\") {\n return \"\";\n }\n\n // Only outlined style has transparent bg with colored icon\n // All other styles (solid, rounded, square, circle) have colored bg with white icon\n const isOutlined = style === \"outlined\";\n const iconColor = isOutlined ? brandColor : \"#ffffff\";\n\n // Border radius values proportional to editor (based on 24x24 viewBox)\n let bgShape: string;\n switch (style) {\n case \"circle\":\n bgShape = `<circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"${brandColor}\"/>`;\n break;\n case \"rounded\":\n bgShape = `<rect width=\"24\" height=\"24\" rx=\"6\" fill=\"${brandColor}\"/>`;\n break;\n case \"square\":\n bgShape = `<rect width=\"24\" height=\"24\" rx=\"0\" fill=\"${brandColor}\"/>`;\n break;\n case \"outlined\":\n bgShape = `<rect width=\"22\" height=\"22\" x=\"1\" y=\"1\" rx=\"3\" fill=\"transparent\" stroke=\"${brandColor}\" stroke-width=\"2\"/>`;\n break;\n default:\n bgShape = `<rect width=\"24\" height=\"24\" rx=\"3\" fill=\"${brandColor}\"/>`;\n break;\n }\n\n // Icon size = 60% of container (matching editor's Math.floor(size * 0.6))\n // In 24x24 viewBox: 60% = 14.4, so translate by (24-14.4)/2 = 4.8 and scale by 0.6\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width=\"${size}\" height=\"${size}\">` +\n bgShape +\n `<g transform=\"translate(4.8, 4.8) scale(0.6)\">` +\n `<path d=\"${path}\" fill=\"${iconColor}\"/>` +\n `</g></svg>`;\n\n return \"data:image/svg+xml;base64,\" + btoa(svg);\n}\n","import type { SocialIconsBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\nimport { generateSocialIconDataUri } from \"../social-icons\";\n\n/**\n * Render a social icons block to MJML markup.\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=\"${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 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\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\n // Generate custom SVG icon as data URI to match editor appearance\n const iconSrc = generateSocialIconDataUri(platform, iconStyle, iconSizePx);\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 { escapeHtml, escapeAttr } 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 const color = block.color;\n\n const content = renderMenuItems(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"\n color=\"${color}\"\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 = escapeAttr(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(item: MenuItemData, linkColor: string): string {\n const text = escapeHtml(item.text);\n const url = escapeAttr(item.url);\n const color = item.color ?? linkColor;\n const target = item.openInNewTab ? ' target=\"_blank\"' : \"\";\n\n const styles: string[] = [`color: ${color}`, \"text-decoration: none\"];\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=\"${resolved}\"`;\n}\n","import type {\n TableBlock,\n TableRowData,\n TableCellData,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr, convertMergeTagsToValues } 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 const color = block.color;\n const align = block.textAlign;\n\n const tableHtml = renderTableElement(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"\n color=\"${color}\"\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 = escapeAttr(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: ${escapeAttr(block.headerBackgroundColor)}`,\n );\n }\n }\n\n const styleAttr = styles.join(\"; \");\n const content = 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=\"${resolved}\"`;\n}\n","import type { CustomBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\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 a `renderCustomBlock` option (typical for editor\n * consumers and headless callers wiring their own resolver).\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\n return `<mj-text${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 return [\"33.33%\", \"33.33%\", \"33.33%\"];\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 { Block, SectionBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { getWidthPercentages, getWidthPixels } from \"../columns\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\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 * 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 columnsLayout = block.columns;\n const columnWidths = getWidthPercentages(columnsLayout);\n const columnWidthsPx = getWidthPixels(columnsLayout, context.containerWidth);\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"native\");\n const visibilityAttr = getCssClassAttr(block);\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 const filteredColumn = filterHtmlBlocks(column, context.allowHtmlBlocks);\n const columnContext = context.withContainerWidth(columnWidth);\n\n const columnBlocks = filteredColumn\n .map((child) => renderBlock(child, columnContext))\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}\">\n${content}\n</mj-column>`);\n }\n\n const columns = columnsContent.join(\"\\n\");\n\n return `<mj-section${bgColor} padding=\"${padding}\"${visibilityAttr}>\n${columns}\n</mj-section>`;\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 } 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 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}\"\n align=\"${align}\"\n padding=\"${padding}\"\n href=\"${href}\"\n target=\"_blank\"${bgColor}${visibilityAttr}\n/>`;\n}\n"],"mappings":"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","names":["isSection","isCustomBlock","renderFontFamilyAttr","renderFontFamilyAttr","renderFontFamilyAttr","renderBlock","filterHtmlBlocks","isSection","isCustomBlock"]}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@templatical/renderer",
|
|
3
3
|
"description": "Render Templatical email templates to MJML",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"bugs": "https://github.com/templatical/sdk/issues",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@templatical/types": "0.
|
|
7
|
+
"@templatical/types": "0.2.0"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
|
+
"mjml": "^5.1.0",
|
|
10
11
|
"tsup": "^8.5.1",
|
|
11
12
|
"typescript": "^6.0.3",
|
|
12
13
|
"vitest": "^4.1.5"
|