@templatical/editor 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/{AiChatSidebar-DWRONNm6.js → AiChatSidebar-H7SIsxeC.js} +49 -59
  2. package/dist/{AiFeatureMenu-U7b4Z8OV.js → AiFeatureMenu-CJGX-fP5.js} +1 -1
  3. package/dist/{CloudEditor-DgiUCNMn.js → CloudEditor-BBs3p-mW.js} +124 -120
  4. package/dist/{CollaboratorBar-BgeaMhrV.js → CollaboratorBar-CiEo25lb.js} +2 -2
  5. package/dist/{CommentsSidebar-BiRtaXYD.js → CommentsSidebar-DikjSLtH.js} +1 -1
  6. package/dist/{CountdownToolbar-CojjlZet.js → CountdownToolbar-Bn4RwJVN.js} +2 -2
  7. package/dist/{DesignReferenceSidebar-C1W02bYt.js → DesignReferenceSidebar-D08CLZPA.js} +1 -1
  8. package/dist/{ModuleBrowserModal-Bjh99_5c.js → ModuleBrowserModal-D6637s8j.js} +4 -4
  9. package/dist/{ModulePreviewCanvas-DDDQmUI_.js → ModulePreviewCanvas-DBFmwq-y.js} +1 -1
  10. package/dist/{NumberWithSuffix-CLTBb2Rj.js → NumberWithSuffix-CyFX9w1e.js} +1 -1
  11. package/dist/{ParagraphEditor-BSaEdasm.js → ParagraphEditor-DC2jOjNh.js} +16 -16
  12. package/dist/{RichTextEditorContent-C3QSg7gd.js → RichTextEditorContent-8NkjTIua.js} +1 -1
  13. package/dist/{SaveModuleDialog-9fX6oSxf.js → SaveModuleDialog-NKT7bWWs.js} +2 -2
  14. package/dist/{SnapshotHistory-gYW-IXkE.js → SnapshotHistory-XNqYxPBj.js} +2 -2
  15. package/dist/{TestEmailModal-B_Rmnngq.js → TestEmailModal-B_-Qo5Us.js} +2 -2
  16. package/dist/{TitleEditor-XbFuw1Vy.js → TitleEditor-DnH3yyTr.js} +7 -7
  17. package/dist/{TplModal-LY9rkDph.js → TplModal-BoEZFe3U.js} +2 -2
  18. package/dist/{blockTypeIcons-CLT2H0yU.js → blockTypeIcons-CUcYuWAO.js} +1 -1
  19. package/dist/bundle-stats.json +7 -7
  20. package/dist/cdn/chunks/{AccessibilityPanel-DaqF4Xzy.js → AccessibilityPanel-CQt87OMk.js} +5 -5
  21. package/dist/cdn/chunks/{AccessibilityPanel-DaqF4Xzy.js.map → AccessibilityPanel-CQt87OMk.js.map} +1 -1
  22. package/dist/cdn/chunks/{AiFeatureMenu-WhZ7G-Wr.js → AiFeatureMenu-SxIqfqA0.js} +8 -8
  23. package/dist/cdn/chunks/{AiFeatureMenu-WhZ7G-Wr.js.map → AiFeatureMenu-SxIqfqA0.js.map} +1 -1
  24. package/dist/cdn/chunks/{BlockA11yBadge-BLKyO0lH.js → BlockA11yBadge-QqW6nomn.js} +3 -3
  25. package/dist/cdn/chunks/{BlockA11yBadge-BLKyO0lH.js.map → BlockA11yBadge-QqW6nomn.js.map} +1 -1
  26. package/dist/cdn/chunks/{CloudEditor-BKEXPt3g.js → CloudEditor-ChWUIMw7.js} +165 -161
  27. package/dist/cdn/chunks/CloudEditor-ChWUIMw7.js.map +1 -0
  28. package/dist/cdn/chunks/{CollaboratorBar-ASDiTitQ.js → CollaboratorBar-73LfbzqF.js} +3 -3
  29. package/dist/cdn/chunks/{CollaboratorBar-ASDiTitQ.js.map → CollaboratorBar-73LfbzqF.js.map} +1 -1
  30. package/dist/cdn/chunks/{CountdownBlock-LAlYJYWL.js → CountdownBlock-WYz9b9Ex.js} +2 -2
  31. package/dist/cdn/chunks/{CountdownBlock-LAlYJYWL.js.map → CountdownBlock-WYz9b9Ex.js.map} +1 -1
  32. package/dist/cdn/chunks/{CountdownToolbar-CbIKGP7u.js → CountdownToolbar-B9fPfFqp.js} +3 -3
  33. package/dist/cdn/chunks/{CountdownToolbar-CbIKGP7u.js.map → CountdownToolbar-B9fPfFqp.js.map} +1 -1
  34. package/dist/cdn/chunks/{ModuleBrowserModal-BjmdleNq.js → ModuleBrowserModal-DfnJQBkM.js} +8 -8
  35. package/dist/cdn/chunks/{ModuleBrowserModal-BjmdleNq.js.map → ModuleBrowserModal-DfnJQBkM.js.map} +1 -1
  36. package/dist/cdn/chunks/{ModulePreviewCanvas-DsmACAdb.js → ModulePreviewCanvas-DE4_oHA_.js} +20 -20
  37. package/dist/cdn/chunks/{ModulePreviewCanvas-DsmACAdb.js.map → ModulePreviewCanvas-DE4_oHA_.js.map} +1 -1
  38. package/dist/cdn/chunks/{NumberWithSuffix-EEDNj1my.js → NumberWithSuffix-BST0yRor.js} +2 -2
  39. package/dist/cdn/chunks/{NumberWithSuffix-EEDNj1my.js.map → NumberWithSuffix-BST0yRor.js.map} +1 -1
  40. package/dist/cdn/chunks/{ParagraphEditor-D6Dw5RtX.js → ParagraphEditor-CQfijF3i.js} +36 -36
  41. package/dist/cdn/chunks/{ParagraphEditor-D6Dw5RtX.js.map → ParagraphEditor-CQfijF3i.js.map} +1 -1
  42. package/dist/cdn/chunks/{RichTextEditorContent-C2yj_yPM.js → RichTextEditorContent-CBds8dnm.js} +5 -5
  43. package/dist/cdn/chunks/{RichTextEditorContent-C2yj_yPM.js.map → RichTextEditorContent-CBds8dnm.js.map} +1 -1
  44. package/dist/cdn/chunks/{SaveModuleDialog-DCcvwYat.js → SaveModuleDialog-DXhuZgZ4.js} +13 -13
  45. package/dist/cdn/chunks/{SaveModuleDialog-DCcvwYat.js.map → SaveModuleDialog-DXhuZgZ4.js.map} +1 -1
  46. package/dist/cdn/chunks/{TitleEditor-isN_t0MJ.js → TitleEditor-BdQvH7eK.js} +7 -7
  47. package/dist/cdn/chunks/{TitleEditor-isN_t0MJ.js.map → TitleEditor-BdQvH7eK.js.map} +1 -1
  48. package/dist/cdn/chunks/{blockTypeIcons-Bgn69Q1-.js → blockTypeIcons-D3CGFW5P.js} +8 -8
  49. package/dist/cdn/chunks/{blockTypeIcons-Bgn69Q1-.js.map → blockTypeIcons-D3CGFW5P.js.map} +1 -1
  50. package/dist/cdn/chunks/{extensions-BPsjY5N8.js → extensions-BE81zpqP.js} +50 -48
  51. package/dist/cdn/chunks/{extensions-BPsjY5N8.js.map → extensions-BE81zpqP.js.map} +1 -1
  52. package/dist/cdn/chunks/{features-BouLRfOO.js → features-CXF_YKlL.js} +1258 -1113
  53. package/dist/cdn/chunks/features-CXF_YKlL.js.map +1 -0
  54. package/dist/cdn/chunks/{icons-BBuZkqC3.js → icons-KbmhF7rg.js} +2 -2
  55. package/dist/cdn/chunks/{icons-BBuZkqC3.js.map → icons-KbmhF7rg.js.map} +1 -1
  56. package/dist/cdn/chunks/{media-library-C-DcHE6w.js → media-library-BERe10wA.js} +103 -103
  57. package/dist/cdn/chunks/{media-library-C-DcHE6w.js.map → media-library-BERe10wA.js.map} +1 -1
  58. package/dist/cdn/chunks/{quality-ConWSEB6.js → quality-CmkY1kMU.js} +309 -309
  59. package/dist/cdn/chunks/{quality-ConWSEB6.js.map → quality-CmkY1kMU.js.map} +1 -1
  60. package/dist/cdn/chunks/{renderer-F8cxEe3t.js → renderer-OttGEYkY.js} +50 -50
  61. package/dist/cdn/chunks/{renderer-F8cxEe3t.js.map → renderer-OttGEYkY.js.map} +1 -1
  62. package/dist/cdn/chunks/{src-C9rMok-R.js → src-C21GI0p6.js} +6 -6
  63. package/dist/cdn/chunks/{src-C9rMok-R.js.map → src-C21GI0p6.js.map} +1 -1
  64. package/dist/cdn/chunks/styles-Cy-My9zK.js +2976 -0
  65. package/dist/cdn/chunks/styles-Cy-My9zK.js.map +1 -0
  66. package/dist/cdn/editor.css +1 -1
  67. package/dist/cdn/editor.js +109 -106
  68. package/dist/cdn/editor.js.map +1 -1
  69. package/dist/{dist-DDJIWTRY.js → dist-B2jcQhv8.js} +1 -1
  70. package/dist/{dist-wzMIGj-D.js → dist-BYt3jdCR.js} +1 -1
  71. package/dist/{dist-aRzjfSRN.js → dist-CISFttkF.js} +1 -1
  72. package/dist/{dist-D6uC2xhi.js → dist-CfQPBf15.js} +1 -1
  73. package/dist/{dist-DjviJBCi.js → dist-DD-9hatO.js} +1 -1
  74. package/dist/{dist-KoBJjK1G.js → dist-DUvhjQ2-.js} +1 -1
  75. package/dist/{dist-BaQIYPsn.js → dist-DVwOAodp.js} +1 -1
  76. package/dist/{dist-Cp0zXPAD.js → dist-DxD1kSdH.js} +1 -1
  77. package/dist/{dist-Pk4MhWMP.js → dist-U5guDm2w.js} +1 -1
  78. package/dist/{dist-D90y8dvT.js → dist-hfu7I2rO.js} +3 -3
  79. package/dist/{dist-us-RpCWN.js → dist-v8h02hhE.js} +1 -1
  80. package/dist/{extensions-D-J02CiP.js → extensions-p6UBqXfg.js} +30 -28
  81. package/dist/index.d.ts +8 -8
  82. package/dist/style.css +1 -1
  83. package/dist/{styles-j_3bu0Bo.js → styles-BP15QmP3.js} +1058 -1052
  84. package/dist/templatical-editor.js +108 -105
  85. package/dist/useAliveFlag-D8GoB4VE.js +10 -0
  86. package/dist/{useEditorCore-B1XrI6fY.js → useEditorCore-_rbu6iMu.js} +921 -778
  87. package/package.json +7 -7
  88. package/dist/cdn/chunks/CloudEditor-BKEXPt3g.js.map +0 -1
  89. package/dist/cdn/chunks/features-BouLRfOO.js.map +0 -1
  90. package/dist/cdn/chunks/styles-BRN0i_zg.js +0 -2971
  91. package/dist/cdn/chunks/styles-BRN0i_zg.js.map +0 -1
  92. /package/dist/{AccessibilityPanel-D-PqmHdH.js → AccessibilityPanel-BeUibrUb.js} +0 -0
  93. /package/dist/{BlockA11yBadge-BFIw0h1m.js → BlockA11yBadge-BhyoMZJ9.js} +0 -0
  94. /package/dist/{CountdownBlock-Cq8A8WrM.js → CountdownBlock-ByJ0_8Pk.js} +0 -0
  95. /package/dist/{TemplateScoringPanel-V79yrEPC.js → TemplateScoringPanel-kYOGtac3.js} +0 -0
  96. /package/dist/{cloud-CTmfCSqf.js → cloud-k0DgeCo2.js} +0 -0
  97. /package/dist/{de-BhIWu_bO.js → de-C9hp3n_a.js} +0 -0
  98. /package/dist/{de-Di4MEjjx.js → de-GOtR9DwW.js} +0 -0
  99. /package/dist/{dist-iLBdeBDR.js → dist-CQQS9SRN.js} +0 -0
  100. /package/dist/{emojiData-DX3E0XT-.js → emojiData-DrBuvEoP.js} +0 -0
  101. /package/dist/{en-D7HRbYah.js → en-Dv776AqL.js} +0 -0
  102. /package/dist/{en-BSuzi-Pd.js → en-dFFQVzNn.js} +0 -0
  103. /package/dist/{formatRelativeTime-BOEf47hq.js → formatRelativeTime-bvx5sFh5.js} +0 -0
  104. /package/dist/{liquid.browser-CdMv1BTn.js → liquid.browser-DX8ZHRdq.js} +0 -0
  105. /package/dist/{pt-BR-K32lt6YP.js → pt-BR-6n58Fl_q.js} +0 -0
  106. /package/dist/{pt-BR-CCVBRais.js → pt-BR-iOr79aDg.js} +0 -0
  107. /package/dist/{readableTextColor-CY3SiRnt.js → readableTextColor-CXzrEnNb.js} +0 -0
  108. /package/dist/{styleConstants-fWzlIIwN.js → styleConstants-DkfOPzGu.js} +0 -0
  109. /package/dist/{usePopoverRoot-BxJrqnMD.js → usePopoverRoot-BwzFSPLy.js} +0 -0
@@ -1,11 +1,11 @@
1
1
  import { n as e } from "./rolldown-runtime-C266TIVP.js";
2
- import { An as t, Cn as n, Dn as r, En as i, Fn as a, Mn as o, Nn as s, On as c, Pn as l, Sn as u, Tn as d, jn as f, kn as p, wn as m } from "./features-BouLRfOO.js";
2
+ import { An as t, Cn as n, Dn as r, En as i, Fn as a, In as o, Mn as s, Nn as c, On as l, Pn as u, Tn as d, jn as f, kn as p, wn as m } from "./features-CXF_YKlL.js";
3
3
  //#endregion
4
4
  //#region ../renderer/src/render-context.ts
5
5
  var h = `https://unpkg.com/@templatical/renderer@${{
6
6
  name: "@templatical/renderer",
7
7
  description: "Render Templatical email templates to MJML",
8
- version: "0.7.2",
8
+ version: "0.7.3",
9
9
  bugs: "https://github.com/templatical/sdk/issues",
10
10
  dependencies: { "@templatical/types": "workspace:*" },
11
11
  devDependencies: {
@@ -116,27 +116,27 @@ function O(e) {
116
116
  }
117
117
  //#endregion
118
118
  //#region ../renderer/src/renderers/title.ts
119
- function k(e, t) {
119
+ function ee(e, t) {
120
120
  if (E(e)) return "";
121
- let n = w(e.styles.padding), r = T(e.styles.backgroundColor, "container"), i = A(C(e.content)), o = a[e.level] ?? a[2], s = x(e.color), c = e.textAlign, l = ee(e.fontFamily, t), u = D(e), d = `h${a[e.level] ? e.level : 2}`;
121
+ let n = w(e.styles.padding), r = T(e.styles.backgroundColor, "container"), i = k(C(e.content)), a = o[e.level] ?? o[2], s = x(e.color), c = e.textAlign, l = te(e.fontFamily, t), u = D(e), d = `h${o[e.level] ? e.level : 2}`;
122
122
  return `<mj-text
123
- font-size="${o}px"
123
+ font-size="${a}px"
124
124
  color="${s}"
125
125
  align="${c}"
126
126
  line-height="1.3"
127
127
  padding="${n}"${r}${l}${u}
128
128
  ><${d} style="margin:0;font-size:inherit;color:inherit;line-height:inherit">${i}</${d}></mj-text>`;
129
129
  }
130
- function A(e) {
130
+ function k(e) {
131
131
  let t = e.match(/^\s*<p\b[^>]*>([\s\S]*)<\/p>\s*$/);
132
132
  return !t || /<\/p>\s*<p\b/i.test(t[1]) ? e : t[1];
133
133
  }
134
- function ee(e, t) {
134
+ function te(e, t) {
135
135
  return e ? ` font-family="${t.resolveFontFamily(e)}"` : "";
136
136
  }
137
137
  //#endregion
138
138
  //#region ../renderer/src/renderers/paragraph.ts
139
- function te(e, t) {
139
+ function ne(e, t) {
140
140
  if (E(e) || e.content.replace(/<\/?p[^>]*>/gi, "").trim() === "") return "";
141
141
  let n = w(e.styles.padding), r = T(e.styles.backgroundColor, "container"), i = C(e.content);
142
142
  return `<mj-text
@@ -146,7 +146,7 @@ function te(e, t) {
146
146
  }
147
147
  //#endregion
148
148
  //#region ../renderer/src/renderers/image.ts
149
- function ne(e, t) {
149
+ function re(e, t) {
150
150
  if (E(e) || e.src === "") return "";
151
151
  let n = w(e.styles.padding), r = T(e.styles.backgroundColor, "container"), i = e.width === "full" ? t.containerWidth + "px" : e.width + "px", a = D(e), o = "";
152
152
  e.linkUrl && (o = ` href="${x(e.linkUrl)}"`, e.linkOpenInNewTab && (o += " target=\"_blank\" rel=\"noopener\""));
@@ -161,7 +161,7 @@ function ne(e, t) {
161
161
  }
162
162
  //#endregion
163
163
  //#region ../renderer/src/renderers/button.ts
164
- function j(e, t) {
164
+ function A(e, t) {
165
165
  if (E(e)) return "";
166
166
  let n = w(e.styles.padding), r = T(e.styles.backgroundColor, "container"), i = w(e.buttonPadding), a = e.url === "" ? "" : x(e.url), o = a === "" ? "" : ` href="${a}"`, s = x(e.backgroundColor), c = x(e.textColor), l = e.fontSize, u = e.borderRadius, d = b(e.text);
167
167
  return `<mj-button${o}${e.openInNewTab ? " target=\"_blank\" rel=\"noopener\"" : ""}
@@ -171,15 +171,15 @@ function j(e, t) {
171
171
  font-weight="bold"
172
172
  border-radius="${u}px"
173
173
  inner-padding="${i}"
174
- padding="${n}"${r}${M(e.fontFamily, t)}${D(e)}
174
+ padding="${n}"${r}${j(e.fontFamily, t)}${D(e)}
175
175
  >${d}</mj-button>`;
176
176
  }
177
- function M(e, t) {
177
+ function j(e, t) {
178
178
  return e ? ` font-family="${t.resolveFontFamily(e)}"` : "";
179
179
  }
180
180
  //#endregion
181
181
  //#region ../renderer/src/renderers/divider.ts
182
- function N(e, t) {
182
+ function M(e, t) {
183
183
  if (E(e)) return "";
184
184
  let n = w(e.styles.padding), r = e.styles.backgroundColor ? ` container-background-color="${x(e.styles.backgroundColor)}"` : "", i = e.width === "full" ? "100%" : e.width + "px";
185
185
  return `<mj-divider
@@ -192,12 +192,12 @@ function N(e, t) {
192
192
  }
193
193
  //#endregion
194
194
  //#region ../renderer/src/renderers/spacer.ts
195
- function P(e, t) {
195
+ function N(e, t) {
196
196
  return E(e) ? "" : `<mj-spacer height="${e.height}px" padding="0"${e.styles.backgroundColor ? ` container-background-color="${x(e.styles.backgroundColor)}"` : ""}${D(e)} />`;
197
197
  }
198
198
  //#endregion
199
199
  //#region ../renderer/src/renderers/html.ts
200
- function F(e, t) {
200
+ function P(e, t) {
201
201
  if (E(e) || !t.allowHtmlBlocks) return "";
202
202
  let n = e.content;
203
203
  return n === "" ? "" : `<mj-text${D(e)}>
@@ -206,7 +206,7 @@ ${n}
206
206
  }
207
207
  //#endregion
208
208
  //#region ../renderer/src/renderers/social.ts
209
- function I(e, t) {
209
+ function F(e, t) {
210
210
  if (E(e)) return "";
211
211
  let n = e.icons;
212
212
  if (n.length === 0) return "";
@@ -252,67 +252,67 @@ ${n.map((e, n) => {
252
252
  }
253
253
  //#endregion
254
254
  //#region ../renderer/src/renderers/menu.ts
255
- function L(e, t) {
255
+ function I(e, t) {
256
256
  if (E(e) || e.items.length === 0) return "";
257
- let n = w(e.styles.padding), r = T(e.styles.backgroundColor, "container"), i = D(e), a = B(e.fontFamily, t), o = e.textAlign;
257
+ let n = w(e.styles.padding), r = T(e.styles.backgroundColor, "container"), i = D(e), a = z(e.fontFamily, t), o = e.textAlign;
258
258
  return `<mj-text
259
259
  font-size="${e.fontSize}px"
260
260
  color="${x(e.color)}"
261
261
  align="${o}"
262
262
  line-height="1.5"
263
263
  padding="${n}"${r}${a}${i}
264
- >${R(e)}</mj-text>`;
264
+ >${L(e)}</mj-text>`;
265
265
  }
266
- function R(e) {
266
+ function L(e) {
267
267
  let t = e.items, n = b(e.separator), r = S(e.separatorColor), i = e.spacing, a = e.linkColor ?? e.color, o = [], s = t.length;
268
- for (let e = 0; e < s; e++) o.push(z(t[e], a)), e < s - 1 && o.push(`<span style="color: ${r}; padding: 0 ${i}px;">${n}</span>`);
268
+ for (let e = 0; e < s; e++) o.push(R(t[e], a)), e < s - 1 && o.push(`<span style="color: ${r}; padding: 0 ${i}px;">${n}</span>`);
269
269
  return o.join("");
270
270
  }
271
- function z(e, t) {
271
+ function R(e, t) {
272
272
  let n = b(e.text), r = x(e.url), i = S(e.color ?? t), a = e.openInNewTab ? " target=\"_blank\" rel=\"noopener\"" : "", o = [`color: ${i}`, "text-decoration: none"];
273
273
  return e.bold && o.push("font-weight: bold"), e.underline && o.push("text-decoration: underline"), `<a href="${r}" style="${o.join("; ")}"${a}>${n}</a>`;
274
274
  }
275
- function B(e, t) {
275
+ function z(e, t) {
276
276
  return e ? ` font-family="${t.resolveFontFamily(e)}"` : "";
277
277
  }
278
278
  //#endregion
279
279
  //#region ../renderer/src/renderers/table.ts
280
- function V(e, t) {
280
+ function B(e, t) {
281
281
  if (E(e) || e.rows.length === 0) return "";
282
- let n = w(e.styles.padding), r = T(e.styles.backgroundColor, "container"), i = D(e), a = G(e.fontFamily, t);
282
+ let n = w(e.styles.padding), r = T(e.styles.backgroundColor, "container"), i = D(e), a = W(e.fontFamily, t);
283
283
  return `<mj-text
284
284
  font-size="${e.fontSize}px"
285
285
  color="${x(e.color)}"
286
286
  align="${e.textAlign}"
287
287
  line-height="1.5"
288
288
  padding="${n}"${r}${a}${i}
289
- >${H(e)}</mj-text>`;
289
+ >${V(e)}</mj-text>`;
290
290
  }
291
- function H(e) {
291
+ function V(e) {
292
292
  let t = S(e.borderColor), n = e.borderWidth, r = "";
293
293
  for (let i = 0; i < e.rows.length; i++) {
294
294
  let a = e.rows[i];
295
- r += U(a, e, e.hasHeaderRow && i === 0, t, n);
295
+ r += H(a, e, e.hasHeaderRow && i === 0, t, n);
296
296
  }
297
297
  return `<table style="width: 100%; border-collapse: collapse;">${r}</table>`;
298
298
  }
299
- function U(e, t, n, r, i) {
299
+ function H(e, t, n, r, i) {
300
300
  let a = "";
301
- for (let o of e.cells) a += W(o, t, n, r, i);
301
+ for (let o of e.cells) a += U(o, t, n, r, i);
302
302
  return `<tr>${a}</tr>`;
303
303
  }
304
- function W(e, t, n, r, i) {
304
+ function U(e, t, n, r, i) {
305
305
  let a = t.cellPadding, o = [`border: ${i}px solid ${r}`, `padding: ${a}px`];
306
306
  n && (o.push("font-weight: bold"), t.headerBackgroundColor && o.push(`background-color: ${S(t.headerBackgroundColor)}`));
307
307
  let s = o.join("; "), c = C(e.content), l = n ? "th" : "td";
308
308
  return `<${l} style="${s}">${c}</${l}>`;
309
309
  }
310
- function G(e, t) {
310
+ function W(e, t) {
311
311
  return e ? ` font-family="${t.resolveFontFamily(e)}"` : "";
312
312
  }
313
313
  //#endregion
314
314
  //#region ../renderer/src/renderers/custom.ts
315
- function re(e, t) {
315
+ function G(e, t) {
316
316
  if (E(e)) return "";
317
317
  let n = t.customBlockHtml.get(e.id) ?? e.renderedHtml;
318
318
  if (!n || n === "") return "";
@@ -351,17 +351,17 @@ function q(e, t) {
351
351
  }
352
352
  //#endregion
353
353
  //#region ../renderer/src/renderers/section.ts
354
- function J(e, t, n) {
354
+ function J(e, n, r) {
355
355
  if (E(e)) return "";
356
- let r = e.columns, i = K(r), a = q(r, t.containerWidth), o = w(e.styles.padding), s = T(e.styles.backgroundColor, "native"), c = D(e), l = e.children, u = [];
357
- for (let e = 0; e < l.length; e++) {
358
- let r = l[e], o = i[e] ?? "100%", s = Math.floor(a[e] ?? t.containerWidth), c = Y(r, t.allowHtmlBlocks).filter((e) => !p(e)), d = t.withContainerWidth(s), f = c.map((e) => n(e, d)).filter((e) => e !== "").join("\n");
359
- u.push(`<mj-column width="${o}">
360
- ${f === "" ? "<mj-text>&nbsp;</mj-text>" : f}
356
+ let i = e.columns, a = K(i), o = q(i, n.containerWidth), s = w(e.styles.padding), c = T(e.styles.backgroundColor, "native"), l = D(e), u = e.children, d = [];
357
+ for (let e = 0; e < u.length; e++) {
358
+ let i = u[e], s = a[e] ?? "100%", c = Math.floor(o[e] ?? n.containerWidth), l = Y(i, n.allowHtmlBlocks).filter((e) => !t(e)), f = n.withContainerWidth(c), p = l.map((e) => r(e, f)).filter((e) => e !== "").join("\n");
359
+ d.push(`<mj-column width="${s}">
360
+ ${p === "" ? "<mj-text>&nbsp;</mj-text>" : p}
361
361
  </mj-column>`);
362
362
  }
363
- return `<mj-section${s} padding="${o}"${c}>
364
- ${u.join("\n")}
363
+ return `<mj-section${c} padding="${s}"${l}>
364
+ ${d.join("\n")}
365
365
  </mj-section>`;
366
366
  }
367
367
  function Y(e, t) {
@@ -397,8 +397,8 @@ function ie(e, t) {
397
397
  }
398
398
  //#endregion
399
399
  //#region ../renderer/src/renderers/index.ts
400
- function Z(e, a) {
401
- return p(e) ? J(e, a, Z) : s(e) ? k(e, a) : c(e) ? te(e, a) : i(e) ? ne(e, a) : u(e) ? j(e, a) : m(e) ? N(e, a) : f(e) ? P(e, a) : d(e) ? F(e, a) : t(e) ? I(e, a) : r(e) ? L(e, a) : o(e) ? V(e, a) : l(e) ? ie(e, a) : n(e) ? re(e, a) : "";
400
+ function Z(e, o) {
401
+ return t(e) ? J(e, o, Z) : u(e) ? ee(e, o) : p(e) ? ne(e, o) : r(e) ? re(e, o) : n(e) ? A(e, o) : d(e) ? M(e, o) : s(e) ? N(e, o) : i(e) ? P(e, o) : f(e) ? F(e, o) : l(e) ? I(e, o) : c(e) ? B(e, o) : a(e) ? ie(e, o) : m(e) ? G(e, o) : "";
402
402
  }
403
403
  //#endregion
404
404
  //#region ../renderer/src/index.ts
@@ -446,8 +446,8 @@ ${d}
446
446
  </mj-body>
447
447
  </mjml>`;
448
448
  }
449
- function se(e, t) {
450
- return p(e) ? Q(e, Z(e, t)) : Q(e, ce(Z(e, t)));
449
+ function se(e, n) {
450
+ return t(e) ? Q(e, Z(e, n)) : Q(e, ce(Z(e, n)));
451
451
  }
452
452
  function Q(e, t) {
453
453
  if (t === "") return "";
@@ -486,16 +486,16 @@ async function pe(e, t) {
486
486
  for (let e = 0; e < r.length; e++) n.set(r[e].id, i[e]);
487
487
  return n;
488
488
  }
489
- function $(e, t) {
489
+ function $(e, n) {
490
490
  for (let r of e) {
491
- if (n(r)) {
492
- t.push(r);
491
+ if (m(r)) {
492
+ n.push(r);
493
493
  continue;
494
494
  }
495
- if (p(r)) for (let e of r.children) $(e, t);
495
+ if (t(r)) for (let e of r.children) $(e, n);
496
496
  }
497
497
  }
498
498
  //#endregion
499
499
  export { ae as t };
500
500
 
501
- //# sourceMappingURL=renderer-F8cxEe3t.js.map
501
+ //# sourceMappingURL=renderer-OttGEYkY.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderer-F8cxEe3t.js","names":[],"sources":["../../../../renderer/package.json","../../../../renderer/src/render-context.ts","../../../../renderer/src/escape.ts","../../../../renderer/src/padding.ts","../../../../renderer/src/utils.ts","../../../../renderer/src/visibility.ts","../../../../renderer/src/renderers/title.ts","../../../../renderer/src/renderers/paragraph.ts","../../../../renderer/src/renderers/image.ts","../../../../renderer/src/renderers/button.ts","../../../../renderer/src/renderers/divider.ts","../../../../renderer/src/renderers/spacer.ts","../../../../renderer/src/renderers/html.ts","../../../../renderer/src/renderers/social.ts","../../../../renderer/src/renderers/menu.ts","../../../../renderer/src/renderers/table.ts","../../../../renderer/src/renderers/custom.ts","../../../../renderer/src/columns.ts","../../../../renderer/src/renderers/section.ts","../../../../renderer/src/renderers/video.ts","../../../../renderer/src/renderers/index.ts","../../../../renderer/src/index.ts"],"sourcesContent":["{\n \"name\": \"@templatical/renderer\",\n \"description\": \"Render Templatical email templates to MJML\",\n \"version\": \"0.7.2\",\n \"bugs\": \"https://github.com/templatical/sdk/issues\",\n \"dependencies\": {\n \"@templatical/types\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"@resvg/resvg-js\": \"^2.6.2\",\n \"mjml\": \"^5.1.0\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^6.0.3\",\n \"vitest\": \"^4.1.5\"\n },\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"assets\"\n ],\n \"homepage\": \"https://templatical.com\",\n \"keywords\": [\n \"email\",\n \"email-template\",\n \"html-email\",\n \"mjml\",\n \"renderer\",\n \"templatical\"\n ],\n \"license\": \"MIT\",\n \"module\": \"./dist/index.js\",\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/templatical/sdk.git\",\n \"directory\": \"packages/renderer\"\n },\n \"scripts\": {\n \"build\": \"tsup && node scripts/rasterize-social.mjs\",\n \"test\": \"vitest run --config vitest.config.ts\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"type\": \"module\",\n \"types\": \"./dist/index.d.ts\"\n}\n","import type { CustomFont } from \"@templatical/types\";\nimport pkg from \"../package.json\" with { type: \"json\" };\n\nexport const DEFAULT_SOCIAL_ICONS_BASE_URL = `https://unpkg.com/@templatical/renderer@${pkg.version}/assets/social`;\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 * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png`. Outlook desktop has no SVG support\n * and rejects base64 data URIs in `<img src>`, so PNGs must be served over\n * HTTP. Default points at the version-pinned unpkg mirror of this\n * package; consumers can override to self-host.\n */\n public readonly socialIconsBaseUrl: string = DEFAULT_SOCIAL_ICONS_BASE_URL,\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 this.socialIconsBaseUrl,\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","const HTML_ENTITIES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#039;\",\n};\n\nconst HTML_ENTITY_REGEX = /[&<>\"']/g;\n\n/**\n * Escape HTML special characters (& < > \" ').\n * Equivalent to PHP htmlspecialchars with ENT_QUOTES | ENT_HTML5.\n */\nexport function escapeHtml(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use in an HTML attribute value.\n * Same implementation as escapeHtml for consistency with PHP.\n */\nexport function escapeAttr(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use as a CSS property value inside an inline\n * `style=\"prop: ${value}\"` attribute. Beyond HTML entity escaping (so the\n * value survives the attribute boundary), this strips characters that\n * could break out of the property value into a sibling property:\n *\n * `;` — separates CSS declarations\n * `{`/`}` — opens/closes a CSS rule (rejected by attribute parsers but\n * still safer to remove)\n * `\\n`/`\\r` — would smuggle past line-based CSS sanitizers\n *\n * Without this, an attacker-controlled color like\n * `\"red; background: url('//attacker/log')\"` lands as a real CSS rule.\n */\nexport function escapeCssValue(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n return escapeAttr(text).replace(/[;{}\\r\\n]/g, \"\");\n}\n\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 { TitleBlock } from \"@templatical/types\";\nimport { HEADING_LEVEL_FONT_SIZE } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { convertMergeTagsToValues, escapeAttr } 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 = unwrapParagraph(convertMergeTagsToValues(block.content));\n // Clamp out-of-range levels to a defined entry so we never interpolate\n // `undefined` into `font-size=\"${...}px\"` (mjml@5 rejects \"undefinedpx\").\n const fontSize =\n HEADING_LEVEL_FONT_SIZE[block.level] ?? HEADING_LEVEL_FONT_SIZE[2];\n const color = escapeAttr(block.color);\n const align = block.textAlign;\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const visibilityAttr = getCssClassAttr(block);\n // Same clamp logic as fontSize so that an out-of-range level still\n // produces a valid heading element.\n const safeLevel = HEADING_LEVEL_FONT_SIZE[block.level] ? block.level : 2;\n const tag = `h${safeLevel}`;\n\n return `<mj-text\n font-size=\"${fontSize}px\"\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\n/**\n * The editor stores title content as a TipTap paragraph (`<p>...</p>`),\n * but the renderer wraps it in `<h${level}>`. `<p>` is invalid inside a\n * heading, so strip a single outer `<p>` wrapper if present.\n */\nfunction unwrapParagraph(html: string): string {\n const match = html.match(/^\\s*<p\\b[^>]*>([\\s\\S]*)<\\/p>\\s*$/);\n if (!match) return html;\n // Only unwrap when there is exactly one top-level <p> — otherwise the\n // content has multiple paragraphs and we must leave it alone.\n if (/<\\/p>\\s*<p\\b/i.test(match[1])) return html;\n return match[1];\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${resolved}\"`;\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 // Skip when the user has cleared the paragraph — otherwise we emit a\n // styled `<td>` cell that adds visible whitespace to the email even\n // though the canvas shows nothing for an empty paragraph.\n const stripped = block.content.replace(/<\\/?p[^>]*>/gi, \"\").trim();\n if (stripped === \"\") {\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 if (block.src === \"\") {\n // Skip rather than emit `<mj-image src=\"\">` which compiles to a\n // broken-image icon. Mirrors video.ts behavior when no source is set.\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const width =\n block.width === \"full\" ? context.containerWidth + \"px\" : block.width + \"px\";\n\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\" rel=\"noopener\"';\n }\n }\n\n const src = escapeAttr(block.src);\n const decorative = block.decorative === true;\n const alt = decorative ? \"\" : escapeAttr(block.alt);\n const align = block.align;\n const roleAttr = decorative ? ' role=\"presentation\"' : \"\";\n\n return `<mj-image\n src=\"${src}\"\n alt=\"${alt}\"\n width=\"${width}\"\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${linkAttr}${visibilityAttr}${roleAttr}\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 // Omit href entirely when no URL is set so we don't emit a clickable\n // `<a href=\"\">` that navigates to whatever URL the email is opened from.\n const href = block.url === \"\" ? \"\" : escapeAttr(block.url);\n const hrefAttr = href === \"\" ? \"\" : ` href=\"${href}\"`;\n const backgroundColor = escapeAttr(block.backgroundColor);\n const textColor = escapeAttr(block.textColor);\n const fontSize = block.fontSize;\n const borderRadius = block.borderRadius;\n const text = escapeHtml(block.text);\n const targetAttr = block.openInNewTab\n ? ' target=\"_blank\" rel=\"noopener\"'\n : \"\";\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-button${hrefAttr}${targetAttr}\n background-color=\"${backgroundColor}\"\n color=\"${textColor}\"\n font-size=\"${fontSize}px\"\n font-weight=\"bold\"\n border-radius=\"${borderRadius}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 { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a divider block to MJML markup.\n */\nexport function renderDivider(\n block: DividerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const width = block.width === \"full\" ? \"100%\" : block.width + \"px\";\n const thickness = block.thickness;\n const lineStyle = block.lineStyle;\n const color = escapeAttr(block.color);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-divider\n border-width=\"${thickness}px\"\n border-style=\"${lineStyle}\"\n border-color=\"${color}\"\n width=\"${width}\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\n/>`;\n}\n","import type { SpacerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a spacer block to MJML markup.\n *\n * The canvas renders a spacer at exactly `block.height` pixels and ignores\n * `block.styles.padding`. Match that here: emit `padding=\"0\"` so the\n * exported email's spacer occupies the same vertical space the user saw\n * in the editor preview. Any non-zero `block.styles.padding` on a spacer\n * is meaningless and silently dropped from the export.\n */\nexport function renderSpacer(\n block: SpacerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const height = block.height;\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-spacer height=\"${height}px\" padding=\"0\"${bgColor}${visibilityAttr} />`;\n}\n","import type { HtmlBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { 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","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\";\n\n/**\n * Render a social icons block to MJML markup.\n *\n * Icons are emitted as `<img src=\"…/{style}/{platform}.png\">` rather than\n * inline SVG or base64 data URIs. Outlook desktop (Word rendering engine)\n * does not support SVG and rejects base64 in `<img src>`, so hosted PNGs are\n * the only format that renders across every mainstream client. The base URL\n * is read from `context.socialIconsBaseUrl` (configurable via\n * `RenderOptions.socialIconsBaseUrl`; default is the version-pinned unpkg\n * mirror of this package).\n */\nexport function renderSocialIcons(\n block: SocialIconsBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const icons = block.icons;\n\n if (icons.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n const align = block.align;\n const iconSize = block.iconSize;\n const iconStyle = block.iconStyle;\n const 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 const iconSrc = `${context.socialIconsBaseUrl}/${iconStyle}/${platform}.png`;\n\n // Apply spacing as right padding only (except last icon) to match CSS gap behavior\n const rightPad = index === iconCount - 1 ? 0 : spacing;\n\n // Empty content hides the platform name text, matching the editor display\n return `<mj-social-element src=\"${iconSrc}\" href=\"${url}\" icon-size=\"${iconSizePx}px\" padding=\"0 ${rightPad}px 0 0\" border-radius=\"${borderRadius}\" background-color=\"transparent\"></mj-social-element>`;\n });\n\n const socialContent = socialElements.join(\"\\n\");\n\n return `<mj-social\n mode=\"horizontal\"\n align=\"${align}\"\n icon-padding=\"0\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\n>\n${socialContent}\n</mj-social>`;\n}\n","import type { MenuBlock, MenuItemData } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeHtml, escapeAttr, escapeCssValue } 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 = escapeAttr(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 = escapeCssValue(block.separatorColor);\n const spacing = block.spacing;\n const linkColor = block.linkColor ?? block.color;\n\n const parts: string[] = [];\n const itemCount = items.length;\n\n for (let index = 0; index < itemCount; index++) {\n parts.push(renderMenuItem(items[index], linkColor));\n\n if (index < itemCount - 1) {\n parts.push(\n `<span style=\"color: ${separatorColor}; padding: 0 ${spacing}px;\">${separator}</span>`,\n );\n }\n }\n\n return parts.join(\"\");\n}\n\nfunction renderMenuItem(item: MenuItemData, linkColor: string): string {\n const text = escapeHtml(item.text);\n const url = escapeAttr(item.url);\n const color = escapeCssValue(item.color ?? linkColor);\n const target = item.openInNewTab ? ' target=\"_blank\" rel=\"noopener\"' : \"\";\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 {\n escapeAttr,\n escapeCssValue,\n convertMergeTagsToValues,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a table block to MJML markup.\n * Uses mj-text wrapping an HTML <table> with styled <tr>/<td> elements.\n */\nexport function renderTable(block: TableBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.rows.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const fontSize = block.fontSize;\n const color = escapeAttr(block.color);\n const align = block.textAlign;\n\n const tableHtml = renderTableElement(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"\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 = escapeCssValue(block.borderColor);\n const borderWidth = block.borderWidth;\n\n const tableStyle = \"width: 100%; border-collapse: collapse;\";\n\n let rowsHtml = \"\";\n\n for (let index = 0; index < block.rows.length; index++) {\n const row = block.rows[index];\n const isHeader = block.hasHeaderRow && index === 0;\n rowsHtml += renderRow(row, block, isHeader, borderColor, borderWidth);\n }\n\n return `<table style=\"${tableStyle}\">${rowsHtml}</table>`;\n}\n\nfunction renderRow(\n row: TableRowData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n let cellsHtml = \"\";\n\n for (const cell of row.cells) {\n cellsHtml += renderCell(cell, block, isHeader, borderColor, borderWidth);\n }\n\n return `<tr>${cellsHtml}</tr>`;\n}\n\nfunction renderCell(\n cell: TableCellData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n const cellPadding = block.cellPadding;\n\n const styles: string[] = [\n `border: ${borderWidth}px solid ${borderColor}`,\n `padding: ${cellPadding}px`,\n ];\n\n if (isHeader) {\n styles.push(\"font-weight: bold\");\n\n if (block.headerBackgroundColor) {\n styles.push(\n `background-color: ${escapeCssValue(block.headerBackgroundColor)}`,\n );\n }\n }\n\n const styleAttr = styles.join(\"; \");\n 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 { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a custom block to MJML markup.\n *\n * Custom block HTML resolution order:\n * 1. `context.customBlockHtml` map — populated by `renderToMjml` when the\n * caller passes 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 const bgColor = bgAttr(block.styles?.backgroundColor, \"container\");\n\n return `<mj-text${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { ColumnLayout } from \"@templatical/types\";\n\n/**\n * Get width percentages for each column in a layout.\n */\nexport function getWidthPercentages(layout: ColumnLayout): string[] {\n switch (layout) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n // 33.33 × 3 = 99.99% — let the last column absorb the rounding gap\n // so widths sum to exactly 100% in the compiled HTML.\n return [\"33.33%\", \"33.33%\", \"33.34%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n}\n\n/**\n * Get width in pixels for each column in a layout.\n */\nexport function getWidthPixels(\n layout: ColumnLayout,\n containerWidth: number,\n): number[] {\n switch (layout) {\n case \"2\":\n return [containerWidth * 0.5, containerWidth * 0.5];\n case \"3\":\n return [containerWidth / 3, containerWidth / 3, containerWidth / 3];\n case \"1-2\":\n return [containerWidth / 3, (containerWidth * 2) / 3];\n case \"2-1\":\n return [(containerWidth * 2) / 3, containerWidth / 3];\n default:\n return [containerWidth];\n }\n}\n","import type { Block, SectionBlock } from \"@templatical/types\";\nimport { isSection } 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 // Sections cannot be nested inside columns per MJML spec — drop any\n // SectionBlocks that landed inside a column (via tampered JSON or a\n // future API). Keeping them would emit `<mj-section>` inside\n // `<mj-column>`, which mjml@5 rejects with a hard error.\n const filteredColumn = filterHtmlBlocks(\n column,\n context.allowHtmlBlocks,\n ).filter((child) => !isSection(child));\n const columnContext = context.withContainerWidth(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>&nbsp;</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\"\n rel=\"noopener\"${bgColor}${visibilityAttr}\n/>`;\n}\n","import type { Block } from \"@templatical/types\";\nimport {\n isSection,\n isTitle,\n isParagraph,\n isImage,\n isButton,\n isDivider,\n isSpacer,\n isHtml,\n isSocialIcons,\n isMenu,\n isTable,\n isVideo,\n isCustomBlock,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { 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 {\n Block,\n CustomBlock,\n TemplateContent,\n CustomFont,\n} from \"@templatical/types\";\nimport { isSection, isCustomBlock } from \"@templatical/types\";\nimport { RenderContext, DEFAULT_SOCIAL_ICONS_BASE_URL } 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 * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png` per icon. Defaults to the\n * version-pinned unpkg mirror of this package. Override to self-host\n * (e.g., behind your own CDN or for air-gapped environments).\n *\n * Why PNGs: Outlook desktop (Word rendering engine) does not support SVG\n * and rejects base64 data URIs in `<img src>`, so social icons must be\n * served as raster images over HTTP for cross-client compatibility.\n */\n socialIconsBaseUrl?: string;\n}\n\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 const socialIconsBaseUrl = stripTrailingSlash(\n options?.socialIconsBaseUrl ?? DEFAULT_SOCIAL_ICONS_BASE_URL,\n );\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 socialIconsBaseUrl,\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 const lang = escapeAttr(content.settings.locale);\n\n return `<mjml lang=\"${lang}\">\n <mj-head>${previewTag}\n <mj-attributes>\n <mj-all font-family=\"${fontFamily}\" />\n <mj-text font-size=\"14px\" />\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 stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\nfunction generatePreviewTag(preheaderText?: string): string {\n if (!preheaderText) {\n return \"\";\n }\n\n const trimmed = preheaderText.trim();\n\n if (trimmed === \"\") {\n return \"\";\n }\n\n const escaped = escapeHtml(trimmed);\n\n return `\\n <mj-preview>${escaped}</mj-preview>`;\n}\n\nfunction generateFontDeclarations(customFonts: CustomFont[]): string {\n if (customFonts.length === 0) {\n return \"\";\n }\n\n return customFonts\n .map(\n (font) =>\n `\\n <mj-font name=\"${escapeAttr(font.name)}\" href=\"${escapeAttr(font.url)}\" />`,\n )\n .join(\"\");\n}\n\n/**\n * Filter out HTML blocks if they are not allowed.\n */\nfunction filterHtmlBlocks(blocks: Block[], allowHtmlBlocks: boolean): Block[] {\n if (allowHtmlBlocks) {\n return blocks;\n }\n\n return blocks.filter((block) => block.type !== \"html\");\n}\n\n/**\n * Walk the content tree, collect every custom block, then resolve each in\n * parallel via the supplied callback. Returns a map keyed by block id that\n * the synchronous render pass reads from. If no callback is provided, returns\n * an empty map and the sync pass falls back to `block.renderedHtml`.\n *\n * Per-block failures bubble up — the caller decides whether to swallow or\n * rethrow. We don't replace failures with placeholders here because that's\n * a policy decision (the editor swallows; a strict CLI may want to fail).\n */\nasync function resolveCustomBlocks(\n content: TemplateContent,\n renderCustomBlock: RenderOptions[\"renderCustomBlock\"],\n): Promise<Map<string, string>> {\n const result = new Map<string, string>();\n\n if (!renderCustomBlock) {\n return result;\n }\n\n const customBlocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, customBlocks);\n\n if (customBlocks.length === 0) {\n return result;\n }\n\n const rendered = await Promise.all(\n customBlocks.map((block) => renderCustomBlock(block)),\n );\n\n for (let index = 0; index < customBlocks.length; index++) {\n result.set(customBlocks[index].id, rendered[index]);\n }\n\n return result;\n}\n\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 { DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nexport { renderBlock } from \"./renderers\";\nexport type { BlockRenderer } from \"./renderers/section\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderer-OttGEYkY.js","names":[],"sources":["../../../../renderer/package.json","../../../../renderer/src/render-context.ts","../../../../renderer/src/escape.ts","../../../../renderer/src/padding.ts","../../../../renderer/src/utils.ts","../../../../renderer/src/visibility.ts","../../../../renderer/src/renderers/title.ts","../../../../renderer/src/renderers/paragraph.ts","../../../../renderer/src/renderers/image.ts","../../../../renderer/src/renderers/button.ts","../../../../renderer/src/renderers/divider.ts","../../../../renderer/src/renderers/spacer.ts","../../../../renderer/src/renderers/html.ts","../../../../renderer/src/renderers/social.ts","../../../../renderer/src/renderers/menu.ts","../../../../renderer/src/renderers/table.ts","../../../../renderer/src/renderers/custom.ts","../../../../renderer/src/columns.ts","../../../../renderer/src/renderers/section.ts","../../../../renderer/src/renderers/video.ts","../../../../renderer/src/renderers/index.ts","../../../../renderer/src/index.ts"],"sourcesContent":["{\n \"name\": \"@templatical/renderer\",\n \"description\": \"Render Templatical email templates to MJML\",\n \"version\": \"0.7.3\",\n \"bugs\": \"https://github.com/templatical/sdk/issues\",\n \"dependencies\": {\n \"@templatical/types\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"@resvg/resvg-js\": \"^2.6.2\",\n \"mjml\": \"^5.1.0\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^6.0.3\",\n \"vitest\": \"^4.1.5\"\n },\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"assets\"\n ],\n \"homepage\": \"https://templatical.com\",\n \"keywords\": [\n \"email\",\n \"email-template\",\n \"html-email\",\n \"mjml\",\n \"renderer\",\n \"templatical\"\n ],\n \"license\": \"MIT\",\n \"module\": \"./dist/index.js\",\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/templatical/sdk.git\",\n \"directory\": \"packages/renderer\"\n },\n \"scripts\": {\n \"build\": \"tsup && node scripts/rasterize-social.mjs\",\n \"test\": \"vitest run --config vitest.config.ts\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"type\": \"module\",\n \"types\": \"./dist/index.d.ts\"\n}\n","import type { CustomFont } from \"@templatical/types\";\nimport pkg from \"../package.json\" with { type: \"json\" };\n\nexport const DEFAULT_SOCIAL_ICONS_BASE_URL = `https://unpkg.com/@templatical/renderer@${pkg.version}/assets/social`;\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 * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png`. Outlook desktop has no SVG support\n * and rejects base64 data URIs in `<img src>`, so PNGs must be served over\n * HTTP. Default points at the version-pinned unpkg mirror of this\n * package; consumers can override to self-host.\n */\n public readonly socialIconsBaseUrl: string = DEFAULT_SOCIAL_ICONS_BASE_URL,\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 this.socialIconsBaseUrl,\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","const HTML_ENTITIES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#039;\",\n};\n\nconst HTML_ENTITY_REGEX = /[&<>\"']/g;\n\n/**\n * Escape HTML special characters (& < > \" ').\n * Equivalent to PHP htmlspecialchars with ENT_QUOTES | ENT_HTML5.\n */\nexport function escapeHtml(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use in an HTML attribute value.\n * Same implementation as escapeHtml for consistency with PHP.\n */\nexport function escapeAttr(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use as a CSS property value inside an inline\n * `style=\"prop: ${value}\"` attribute. Beyond HTML entity escaping (so the\n * value survives the attribute boundary), this strips characters that\n * could break out of the property value into a sibling property:\n *\n * `;` — separates CSS declarations\n * `{`/`}` — opens/closes a CSS rule (rejected by attribute parsers but\n * still safer to remove)\n * `\\n`/`\\r` — would smuggle past line-based CSS sanitizers\n *\n * Without this, an attacker-controlled color like\n * `\"red; background: url('//attacker/log')\"` lands as a real CSS rule.\n */\nexport function escapeCssValue(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n return escapeAttr(text).replace(/[;{}\\r\\n]/g, \"\");\n}\n\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 { TitleBlock } from \"@templatical/types\";\nimport { HEADING_LEVEL_FONT_SIZE } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { convertMergeTagsToValues, escapeAttr } 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 = unwrapParagraph(convertMergeTagsToValues(block.content));\n // Clamp out-of-range levels to a defined entry so we never interpolate\n // `undefined` into `font-size=\"${...}px\"` (mjml@5 rejects \"undefinedpx\").\n const fontSize =\n HEADING_LEVEL_FONT_SIZE[block.level] ?? HEADING_LEVEL_FONT_SIZE[2];\n const color = escapeAttr(block.color);\n const align = block.textAlign;\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const visibilityAttr = getCssClassAttr(block);\n // Same clamp logic as fontSize so that an out-of-range level still\n // produces a valid heading element.\n const safeLevel = HEADING_LEVEL_FONT_SIZE[block.level] ? block.level : 2;\n const tag = `h${safeLevel}`;\n\n return `<mj-text\n font-size=\"${fontSize}px\"\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\n/**\n * The editor stores title content as a TipTap paragraph (`<p>...</p>`),\n * but the renderer wraps it in `<h${level}>`. `<p>` is invalid inside a\n * heading, so strip a single outer `<p>` wrapper if present.\n */\nfunction unwrapParagraph(html: string): string {\n const match = html.match(/^\\s*<p\\b[^>]*>([\\s\\S]*)<\\/p>\\s*$/);\n if (!match) return html;\n // Only unwrap when there is exactly one top-level <p> — otherwise the\n // content has multiple paragraphs and we must leave it alone.\n if (/<\\/p>\\s*<p\\b/i.test(match[1])) return html;\n return match[1];\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${resolved}\"`;\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 // Skip when the user has cleared the paragraph — otherwise we emit a\n // styled `<td>` cell that adds visible whitespace to the email even\n // though the canvas shows nothing for an empty paragraph.\n const stripped = block.content.replace(/<\\/?p[^>]*>/gi, \"\").trim();\n if (stripped === \"\") {\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 if (block.src === \"\") {\n // Skip rather than emit `<mj-image src=\"\">` which compiles to a\n // broken-image icon. Mirrors video.ts behavior when no source is set.\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const width =\n block.width === \"full\" ? context.containerWidth + \"px\" : block.width + \"px\";\n\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\" rel=\"noopener\"';\n }\n }\n\n const src = escapeAttr(block.src);\n const decorative = block.decorative === true;\n const alt = decorative ? \"\" : escapeAttr(block.alt);\n const align = block.align;\n const roleAttr = decorative ? ' role=\"presentation\"' : \"\";\n\n return `<mj-image\n src=\"${src}\"\n alt=\"${alt}\"\n width=\"${width}\"\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${linkAttr}${visibilityAttr}${roleAttr}\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 // Omit href entirely when no URL is set so we don't emit a clickable\n // `<a href=\"\">` that navigates to whatever URL the email is opened from.\n const href = block.url === \"\" ? \"\" : escapeAttr(block.url);\n const hrefAttr = href === \"\" ? \"\" : ` href=\"${href}\"`;\n const backgroundColor = escapeAttr(block.backgroundColor);\n const textColor = escapeAttr(block.textColor);\n const fontSize = block.fontSize;\n const borderRadius = block.borderRadius;\n const text = escapeHtml(block.text);\n const targetAttr = block.openInNewTab\n ? ' target=\"_blank\" rel=\"noopener\"'\n : \"\";\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-button${hrefAttr}${targetAttr}\n background-color=\"${backgroundColor}\"\n color=\"${textColor}\"\n font-size=\"${fontSize}px\"\n font-weight=\"bold\"\n border-radius=\"${borderRadius}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 { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a divider block to MJML markup.\n */\nexport function renderDivider(\n block: DividerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const width = block.width === \"full\" ? \"100%\" : block.width + \"px\";\n const thickness = block.thickness;\n const lineStyle = block.lineStyle;\n const color = escapeAttr(block.color);\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-divider\n border-width=\"${thickness}px\"\n border-style=\"${lineStyle}\"\n border-color=\"${color}\"\n width=\"${width}\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\n/>`;\n}\n","import type { SpacerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a spacer block to MJML markup.\n *\n * The canvas renders a spacer at exactly `block.height` pixels and ignores\n * `block.styles.padding`. Match that here: emit `padding=\"0\"` so the\n * exported email's spacer occupies the same vertical space the user saw\n * in the editor preview. Any non-zero `block.styles.padding` on a spacer\n * is meaningless and silently dropped from the export.\n */\nexport function renderSpacer(\n block: SpacerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const height = block.height;\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n\n return `<mj-spacer height=\"${height}px\" padding=\"0\"${bgColor}${visibilityAttr} />`;\n}\n","import type { HtmlBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { 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","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\";\n\n/**\n * Render a social icons block to MJML markup.\n *\n * Icons are emitted as `<img src=\"…/{style}/{platform}.png\">` rather than\n * inline SVG or base64 data URIs. Outlook desktop (Word rendering engine)\n * does not support SVG and rejects base64 in `<img src>`, so hosted PNGs are\n * the only format that renders across every mainstream client. The base URL\n * is read from `context.socialIconsBaseUrl` (configurable via\n * `RenderOptions.socialIconsBaseUrl`; default is the version-pinned unpkg\n * mirror of this package).\n */\nexport function renderSocialIcons(\n block: SocialIconsBlock,\n context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const icons = block.icons;\n\n if (icons.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n const visibilityAttr = getCssClassAttr(block);\n const align = block.align;\n const iconSize = block.iconSize;\n const iconStyle = block.iconStyle;\n const 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 const iconSrc = `${context.socialIconsBaseUrl}/${iconStyle}/${platform}.png`;\n\n // Apply spacing as right padding only (except last icon) to match CSS gap behavior\n const rightPad = index === iconCount - 1 ? 0 : spacing;\n\n // Empty content hides the platform name text, matching the editor display\n return `<mj-social-element src=\"${iconSrc}\" href=\"${url}\" icon-size=\"${iconSizePx}px\" padding=\"0 ${rightPad}px 0 0\" border-radius=\"${borderRadius}\" background-color=\"transparent\"></mj-social-element>`;\n });\n\n const socialContent = socialElements.join(\"\\n\");\n\n return `<mj-social\n mode=\"horizontal\"\n align=\"${align}\"\n icon-padding=\"0\"\n padding=\"${padding}\"${bgColor}${visibilityAttr}\n>\n${socialContent}\n</mj-social>`;\n}\n","import type { MenuBlock, MenuItemData } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeHtml, escapeAttr, escapeCssValue } 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 = escapeAttr(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 = escapeCssValue(block.separatorColor);\n const spacing = block.spacing;\n const linkColor = block.linkColor ?? block.color;\n\n const parts: string[] = [];\n const itemCount = items.length;\n\n for (let index = 0; index < itemCount; index++) {\n parts.push(renderMenuItem(items[index], linkColor));\n\n if (index < itemCount - 1) {\n parts.push(\n `<span style=\"color: ${separatorColor}; padding: 0 ${spacing}px;\">${separator}</span>`,\n );\n }\n }\n\n return parts.join(\"\");\n}\n\nfunction renderMenuItem(item: MenuItemData, linkColor: string): string {\n const text = escapeHtml(item.text);\n const url = escapeAttr(item.url);\n const color = escapeCssValue(item.color ?? linkColor);\n const target = item.openInNewTab ? ' target=\"_blank\" rel=\"noopener\"' : \"\";\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 {\n escapeAttr,\n escapeCssValue,\n convertMergeTagsToValues,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a table block to MJML markup.\n * Uses mj-text wrapping an HTML <table> with styled <tr>/<td> elements.\n */\nexport function renderTable(block: TableBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.rows.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const fontSize = block.fontSize;\n const color = escapeAttr(block.color);\n const align = block.textAlign;\n\n const tableHtml = renderTableElement(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"\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 = escapeCssValue(block.borderColor);\n const borderWidth = block.borderWidth;\n\n const tableStyle = \"width: 100%; border-collapse: collapse;\";\n\n let rowsHtml = \"\";\n\n for (let index = 0; index < block.rows.length; index++) {\n const row = block.rows[index];\n const isHeader = block.hasHeaderRow && index === 0;\n rowsHtml += renderRow(row, block, isHeader, borderColor, borderWidth);\n }\n\n return `<table style=\"${tableStyle}\">${rowsHtml}</table>`;\n}\n\nfunction renderRow(\n row: TableRowData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n let cellsHtml = \"\";\n\n for (const cell of row.cells) {\n cellsHtml += renderCell(cell, block, isHeader, borderColor, borderWidth);\n }\n\n return `<tr>${cellsHtml}</tr>`;\n}\n\nfunction renderCell(\n cell: TableCellData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n const cellPadding = block.cellPadding;\n\n const styles: string[] = [\n `border: ${borderWidth}px solid ${borderColor}`,\n `padding: ${cellPadding}px`,\n ];\n\n if (isHeader) {\n styles.push(\"font-weight: bold\");\n\n if (block.headerBackgroundColor) {\n styles.push(\n `background-color: ${escapeCssValue(block.headerBackgroundColor)}`,\n );\n }\n }\n\n const styleAttr = styles.join(\"; \");\n 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 { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a custom block to MJML markup.\n *\n * Custom block HTML resolution order:\n * 1. `context.customBlockHtml` map — populated by `renderToMjml` when the\n * caller passes 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 const bgColor = bgAttr(block.styles?.backgroundColor, \"container\");\n\n return `<mj-text${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { ColumnLayout } from \"@templatical/types\";\n\n/**\n * Get width percentages for each column in a layout.\n */\nexport function getWidthPercentages(layout: ColumnLayout): string[] {\n switch (layout) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n // 33.33 × 3 = 99.99% — let the last column absorb the rounding gap\n // so widths sum to exactly 100% in the compiled HTML.\n return [\"33.33%\", \"33.33%\", \"33.34%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n}\n\n/**\n * Get width in pixels for each column in a layout.\n */\nexport function getWidthPixels(\n layout: ColumnLayout,\n containerWidth: number,\n): number[] {\n switch (layout) {\n case \"2\":\n return [containerWidth * 0.5, containerWidth * 0.5];\n case \"3\":\n return [containerWidth / 3, containerWidth / 3, containerWidth / 3];\n case \"1-2\":\n return [containerWidth / 3, (containerWidth * 2) / 3];\n case \"2-1\":\n return [(containerWidth * 2) / 3, containerWidth / 3];\n default:\n return [containerWidth];\n }\n}\n","import type { Block, SectionBlock } from \"@templatical/types\";\nimport { isSection } 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 // Sections cannot be nested inside columns per MJML spec — drop any\n // SectionBlocks that landed inside a column (via tampered JSON or a\n // future API). Keeping them would emit `<mj-section>` inside\n // `<mj-column>`, which mjml@5 rejects with a hard error.\n const filteredColumn = filterHtmlBlocks(\n column,\n context.allowHtmlBlocks,\n ).filter((child) => !isSection(child));\n const columnContext = context.withContainerWidth(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>&nbsp;</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\"\n rel=\"noopener\"${bgColor}${visibilityAttr}\n/>`;\n}\n","import type { Block } from \"@templatical/types\";\nimport {\n isSection,\n isTitle,\n isParagraph,\n isImage,\n isButton,\n isDivider,\n isSpacer,\n isHtml,\n isSocialIcons,\n isMenu,\n isTable,\n isVideo,\n isCustomBlock,\n} from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { 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 {\n Block,\n CustomBlock,\n TemplateContent,\n CustomFont,\n} from \"@templatical/types\";\nimport { isSection, isCustomBlock } from \"@templatical/types\";\nimport { RenderContext, DEFAULT_SOCIAL_ICONS_BASE_URL } 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 * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png` per icon. Defaults to the\n * version-pinned unpkg mirror of this package. Override to self-host\n * (e.g., behind your own CDN or for air-gapped environments).\n *\n * Why PNGs: Outlook desktop (Word rendering engine) does not support SVG\n * and rejects base64 data URIs in `<img src>`, so social icons must be\n * served as raster images over HTTP for cross-client compatibility.\n */\n socialIconsBaseUrl?: string;\n}\n\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 const socialIconsBaseUrl = stripTrailingSlash(\n options?.socialIconsBaseUrl ?? DEFAULT_SOCIAL_ICONS_BASE_URL,\n );\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 socialIconsBaseUrl,\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 const lang = escapeAttr(content.settings.locale);\n\n return `<mjml lang=\"${lang}\">\n <mj-head>${previewTag}\n <mj-attributes>\n <mj-all font-family=\"${fontFamily}\" />\n <mj-text font-size=\"14px\" />\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 stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\nfunction generatePreviewTag(preheaderText?: string): string {\n if (!preheaderText) {\n return \"\";\n }\n\n const trimmed = preheaderText.trim();\n\n if (trimmed === \"\") {\n return \"\";\n }\n\n const escaped = escapeHtml(trimmed);\n\n return `\\n <mj-preview>${escaped}</mj-preview>`;\n}\n\nfunction generateFontDeclarations(customFonts: CustomFont[]): string {\n if (customFonts.length === 0) {\n return \"\";\n }\n\n return customFonts\n .map(\n (font) =>\n `\\n <mj-font name=\"${escapeAttr(font.name)}\" href=\"${escapeAttr(font.url)}\" />`,\n )\n .join(\"\");\n}\n\n/**\n * Filter out HTML blocks if they are not allowed.\n */\nfunction filterHtmlBlocks(blocks: Block[], allowHtmlBlocks: boolean): Block[] {\n if (allowHtmlBlocks) {\n return blocks;\n }\n\n return blocks.filter((block) => block.type !== \"html\");\n}\n\n/**\n * Walk the content tree, collect every custom block, then resolve each in\n * parallel via the supplied callback. Returns a map keyed by block id that\n * the synchronous render pass reads from. If no callback is provided, returns\n * an empty map and the sync pass falls back to `block.renderedHtml`.\n *\n * Per-block failures bubble up — the caller decides whether to swallow or\n * rethrow. We don't replace failures with placeholders here because that's\n * a policy decision (the editor swallows; a strict CLI may want to fail).\n */\nasync function resolveCustomBlocks(\n content: TemplateContent,\n renderCustomBlock: RenderOptions[\"renderCustomBlock\"],\n): Promise<Map<string, string>> {\n const result = new Map<string, string>();\n\n if (!renderCustomBlock) {\n return result;\n }\n\n const customBlocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, customBlocks);\n\n if (customBlocks.length === 0) {\n return result;\n }\n\n const rendered = await Promise.all(\n customBlocks.map((block) => renderCustomBlock(block)),\n );\n\n for (let index = 0; index < customBlocks.length; index++) {\n result.set(customBlocks[index].id, rendered[index]);\n }\n\n return result;\n}\n\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 { DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nexport { renderBlock } from \"./renderers\";\nexport type { BlockRenderer } from \"./renderers/section\";\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { A as e, C as t, M as n, N as r, P as i, V as a, Z as o, ct as s, f as c, g as l, h as u, it as d, l as ee, m as f, n as p, ot as te, p as m, r as h, st as g, u as _, v, x as y, y as b } from "./draggable-P6QWzy4g.js";
2
- import { Ct as x, T as S, k as C, kt as ne } from "./features-BouLRfOO.js";
3
- import { A as re, B as ie, R as ae, W as oe, b as se } from "./icons-BBuZkqC3.js";
4
- import { a as ce, c as le, d as ue, f as de, h as fe, i as w, l as T, n as E, o as D, p as O, r as k, s as A, t as j, u as M } from "./media-library-C-DcHE6w.js";
2
+ import { At as x, T as S, k as C, wt as ne } from "./features-CXF_YKlL.js";
3
+ import { A as re, B as ie, R as ae, W as oe, b as se } from "./icons-KbmhF7rg.js";
4
+ import { a as ce, c as le, d as ue, f as de, h as fe, i as w, l as T, n as E, o as D, p as O, r as k, s as A, t as j, u as M } from "./media-library-BERe10wA.js";
5
5
  //#region ../media-library/src/standalone/MediaLibrary.vue?vue&type=script&setup=true&lang.ts
6
6
  var N = {
7
7
  class: "tpl tpl:flex tpl:flex-col tpl:overflow-hidden tpl:rounded-[var(--tpl-radius-lg)]",
@@ -384,11 +384,11 @@ var N = {
384
384
  backgroundColor: "var(--tpl-bg)"
385
385
  }),
386
386
  onClick: r[13] ||= (e) => d(J).copy(d(J).selectedUrl.value)
387
- }, [d(J).copied.value ? (n(), f(d(ne), {
387
+ }, [d(J).copied.value ? (n(), f(d(x), {
388
388
  key: 1,
389
389
  size: 12,
390
390
  "stroke-width": 2
391
- })) : (n(), f(d(x), {
391
+ })) : (n(), f(d(ne), {
392
392
  key: 0,
393
393
  size: 12,
394
394
  "stroke-width": 2
@@ -509,4 +509,4 @@ typeof window < "u" && (window.TemplaticalMedia = {
509
509
  //#endregion
510
510
  export { j as MediaLibraryModal };
511
511
 
512
- //# sourceMappingURL=src-C9rMok-R.js.map
512
+ //# sourceMappingURL=src-C21GI0p6.js.map