@templatical/editor 0.27.2 → 0.27.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AiChatSidebar-iVJDzqjr.js → AiChatSidebar-LstHjwBU.js} +8 -8
- package/dist/{AiFeatureMenu-DrLGc2H2.js → AiFeatureMenu-DVQrznsN.js} +6 -6
- package/dist/{BlockIssueBadge-BHysq7XQ.js → BlockIssueBadge-DKaFQNxW.js} +4 -4
- package/dist/{BlockPreviewCanvas-2WASf5A4.js → BlockPreviewCanvas-D9eC04OA.js} +2 -2
- package/dist/{CloudHeaderExtras-DWApyc4P.js → CloudHeaderExtras-DX0j3CJ3.js} +3 -3
- package/dist/{CloudPanels-DvOhGdRj.js → CloudPanels-DCeNSwXy.js} +5 -5
- package/dist/{CollaboratorBar-C6MC0UCk.js → CollaboratorBar-BZ-JCjdM.js} +3 -3
- package/dist/{ColorPicker-Di8L5TLQ.js → ColorPicker-j0TQu5lE.js} +4 -4
- package/dist/{CommentsPanel-D5wQB1Kf.js → CommentsPanel-c5LmzlNN.js} +1 -1
- package/dist/{CommentsSidebar-BoTIxpLE.js → CommentsSidebar-Bm1DxFb5.js} +12 -12
- package/dist/{CountdownBlock-BFX8iU90.js → CountdownBlock-qKtJVRIV.js} +1 -1
- package/dist/{CountdownToolbar-d9QM4m5x.js → CountdownToolbar-UPdwNvVI.js} +2 -2
- package/dist/{DesignReferenceSidebar-DKsx2TiL.js → DesignReferenceSidebar-C8WTApc5.js} +7 -7
- package/dist/{IssuesPanel-flKKwKpM.js → IssuesPanel-D49aJjLS.js} +41 -40
- package/dist/{MergeTagModeToggle-PKCoKAoF.js → MergeTagModeToggle-CVzJY9b2.js} +4 -4
- package/dist/{ParagraphEditor-iwTjkjtx.js → ParagraphEditor-C9-jQ5zP.js} +11 -11
- package/dist/{RestoreVersionDialog-DyMPGfsr.js → RestoreVersionDialog-Ds6gEF5u.js} +4 -4
- package/dist/{RichTextEditorContent-BVXG9oF4.js → RichTextEditorContent-CO1se6ln.js} +6 -6
- package/dist/{SaveBlockDialog-BMCAIfK9.js → SaveBlockDialog-DJIcncsU.js} +8 -8
- package/dist/{SavedBlocksBrowserModal-p5ma1qay.js → SavedBlocksBrowserModal-BUhhkGEb.js} +9 -9
- package/dist/{SavedBlocksPanels-BsiSstsZ.js → SavedBlocksPanels-DBfPz4iB.js} +1 -1
- package/dist/{SavedBlocksPickBar-fU4Mjm-8.js → SavedBlocksPickBar-D9NsfzCB.js} +3 -3
- package/dist/{TemplateScoringPanel-Bv_bBsJi.js → TemplateScoringPanel-BFsUqnXR.js} +12 -12
- package/dist/{TestEmailModal-EFYcrmTk.js → TestEmailModal-DARwBqpV.js} +8 -8
- package/dist/{TestEmailPanel-ljriUqs1.js → TestEmailPanel-BkgAew1S.js} +1 -1
- package/dist/{TitleEditor-BqRW7HTz.js → TitleEditor-BWLwZh84.js} +8 -8
- package/dist/{TplModal-CTC9-w6B.js → TplModal-CJLs8as_.js} +3 -3
- package/dist/{VersionHistoryMenu-Cq4rH56t.js → VersionHistoryMenu-CrIVG_SN.js} +11 -11
- package/dist/{VersionHistoryPanels-GkxkBiSi.js → VersionHistoryPanels-s45U7XY-.js} +1 -1
- package/dist/{VersionPreviewBanner-BdtTu9mI.js → VersionPreviewBanner-BIy95B34.js} +2 -2
- package/dist/{blockTypeIcons-CIrMr1SJ.js → blockTypeIcons-BjUkQsAe.js} +2 -2
- package/dist/bundle-stats.json +7 -7
- package/dist/cdn/chunks/{AiChatSidebar-DTUoBH5a.js → AiChatSidebar-tZqbhmuJ.js} +3 -3
- package/dist/cdn/chunks/{AiChatSidebar-DTUoBH5a.js.map → AiChatSidebar-tZqbhmuJ.js.map} +1 -1
- package/dist/cdn/chunks/{AiFeatureMenu-BQz9Kczt.js → AiFeatureMenu-BhtNkpIq.js} +5 -5
- package/dist/cdn/chunks/{AiFeatureMenu-BQz9Kczt.js.map → AiFeatureMenu-BhtNkpIq.js.map} +1 -1
- package/dist/cdn/chunks/{BlockIssueBadge-UzSSOM_2.js → BlockIssueBadge-D9L8j7Db.js} +4 -4
- package/dist/cdn/chunks/{BlockIssueBadge-UzSSOM_2.js.map → BlockIssueBadge-D9L8j7Db.js.map} +1 -1
- package/dist/cdn/chunks/{BlockPreviewCanvas-40ocpXDD.js → BlockPreviewCanvas-Bt7fFzIQ.js} +3 -3
- package/dist/cdn/chunks/{BlockPreviewCanvas-40ocpXDD.js.map → BlockPreviewCanvas-Bt7fFzIQ.js.map} +1 -1
- package/dist/cdn/chunks/{CloudHeaderExtras-F_ySlW1S.js → CloudHeaderExtras-CScGpeLl.js} +4 -4
- package/dist/cdn/chunks/{CloudHeaderExtras-F_ySlW1S.js.map → CloudHeaderExtras-CScGpeLl.js.map} +1 -1
- package/dist/cdn/chunks/{CloudPanels-C34bcHpc.js → CloudPanels-DVtJkWsj.js} +6 -6
- package/dist/cdn/chunks/{CloudPanels-C34bcHpc.js.map → CloudPanels-DVtJkWsj.js.map} +1 -1
- package/dist/cdn/chunks/{CollaboratorBar-WBtrPPnM.js → CollaboratorBar-BH1lM_Ta.js} +4 -4
- package/dist/cdn/chunks/{CollaboratorBar-WBtrPPnM.js.map → CollaboratorBar-BH1lM_Ta.js.map} +1 -1
- package/dist/cdn/chunks/{ColorPicker-CxKYd_8N.js → ColorPicker-d4kY7WWd.js} +5 -5
- package/dist/cdn/chunks/{ColorPicker-CxKYd_8N.js.map → ColorPicker-d4kY7WWd.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsPanel-jzZ2X0P4.js → CommentsPanel-COhQMagD.js} +2 -2
- package/dist/cdn/chunks/{CommentsPanel-jzZ2X0P4.js.map → CommentsPanel-COhQMagD.js.map} +1 -1
- package/dist/cdn/chunks/{CommentsSidebar-Dxke-vUP.js → CommentsSidebar-vzLX-_h3.js} +4 -4
- package/dist/cdn/chunks/{CommentsSidebar-Dxke-vUP.js.map → CommentsSidebar-vzLX-_h3.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownBlock-BYSrJWxg.js → CountdownBlock-BKrYGRiT.js} +2 -2
- package/dist/cdn/chunks/{CountdownBlock-BYSrJWxg.js.map → CountdownBlock-BKrYGRiT.js.map} +1 -1
- package/dist/cdn/chunks/{CountdownToolbar-pVbkPNVo.js → CountdownToolbar-BRDbI1mM.js} +3 -3
- package/dist/cdn/chunks/{CountdownToolbar-pVbkPNVo.js.map → CountdownToolbar-BRDbI1mM.js.map} +1 -1
- package/dist/cdn/chunks/{DesignReferenceSidebar-sZWciECR.js → DesignReferenceSidebar-npLxDRJI.js} +4 -4
- package/dist/cdn/chunks/{DesignReferenceSidebar-sZWciECR.js.map → DesignReferenceSidebar-npLxDRJI.js.map} +1 -1
- package/dist/cdn/chunks/{IssuesPanel-CHtetM9i.js → IssuesPanel-CTGlv7HF.js} +37 -35
- package/dist/cdn/chunks/IssuesPanel-CTGlv7HF.js.map +1 -0
- package/dist/cdn/chunks/{MergeTagModeToggle-ZwW-5EeN.js → MergeTagModeToggle-CkpKFx9N.js} +4 -4
- package/dist/cdn/chunks/{MergeTagModeToggle-ZwW-5EeN.js.map → MergeTagModeToggle-CkpKFx9N.js.map} +1 -1
- package/dist/cdn/chunks/{ParagraphEditor-BBcIQQK2.js → ParagraphEditor-BgeeE0Gj.js} +9 -9
- package/dist/cdn/chunks/{ParagraphEditor-BBcIQQK2.js.map → ParagraphEditor-BgeeE0Gj.js.map} +1 -1
- package/dist/cdn/chunks/{RestoreVersionDialog-DNUZHFa_.js → RestoreVersionDialog-tfoAQAeP.js} +4 -4
- package/dist/cdn/chunks/{RestoreVersionDialog-DNUZHFa_.js.map → RestoreVersionDialog-tfoAQAeP.js.map} +1 -1
- package/dist/cdn/chunks/{RichTextEditorContent-Cl-acKAD.js → RichTextEditorContent-DvLBtUcW.js} +6 -6
- package/dist/cdn/chunks/{RichTextEditorContent-Cl-acKAD.js.map → RichTextEditorContent-DvLBtUcW.js.map} +1 -1
- package/dist/cdn/chunks/{SaveBlockDialog-BXTZ729e.js → SaveBlockDialog-D67K2lHn.js} +7 -7
- package/dist/cdn/chunks/{SaveBlockDialog-BXTZ729e.js.map → SaveBlockDialog-D67K2lHn.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-BqGNBpRR.js → SavedBlocksBrowserModal-BxVR-yDt.js} +8 -8
- package/dist/cdn/chunks/{SavedBlocksBrowserModal-BqGNBpRR.js.map → SavedBlocksBrowserModal-BxVR-yDt.js.map} +1 -1
- package/dist/cdn/chunks/{SavedBlocksPanels-nbK0UQTp.js → SavedBlocksPanels-VkKCO0uY.js} +2 -2
- package/dist/cdn/chunks/SavedBlocksPanels-VkKCO0uY.js.map +1 -0
- package/dist/cdn/chunks/{SavedBlocksPickBar-B9mbw3Gk.js → SavedBlocksPickBar-BJHQDgUT.js} +4 -4
- package/dist/cdn/chunks/{SavedBlocksPickBar-B9mbw3Gk.js.map → SavedBlocksPickBar-BJHQDgUT.js.map} +1 -1
- package/dist/cdn/chunks/{TemplateScoringPanel-Xdkp2EMY.js → TemplateScoringPanel-MvdrkgqL.js} +4 -4
- package/dist/cdn/chunks/{TemplateScoringPanel-Xdkp2EMY.js.map → TemplateScoringPanel-MvdrkgqL.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailModal-DE6fi3he.js → TestEmailModal-CEBFCevy.js} +8 -8
- package/dist/cdn/chunks/{TestEmailModal-DE6fi3he.js.map → TestEmailModal-CEBFCevy.js.map} +1 -1
- package/dist/cdn/chunks/{TestEmailPanel-CAjQDXle.js → TestEmailPanel-C35nqIIl.js} +2 -2
- package/dist/cdn/chunks/TestEmailPanel-C35nqIIl.js.map +1 -0
- package/dist/cdn/chunks/{TitleEditor-BP0ewaWF.js → TitleEditor-c2aQCyds.js} +8 -8
- package/dist/cdn/chunks/{TitleEditor-BP0ewaWF.js.map → TitleEditor-c2aQCyds.js.map} +1 -1
- package/dist/cdn/chunks/{TplModal-C4kJduTr.js → TplModal-DSTVZKuI.js} +4 -4
- package/dist/cdn/chunks/{TplModal-C4kJduTr.js.map → TplModal-DSTVZKuI.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryMenu-_MAj3qZj.js → VersionHistoryMenu-BNYSIjVj.js} +5 -5
- package/dist/cdn/chunks/{VersionHistoryMenu-_MAj3qZj.js.map → VersionHistoryMenu-BNYSIjVj.js.map} +1 -1
- package/dist/cdn/chunks/{VersionHistoryPanels-DMq2PRee.js → VersionHistoryPanels-CvWySHUE.js} +2 -2
- package/dist/cdn/chunks/{VersionHistoryPanels-DMq2PRee.js.map → VersionHistoryPanels-CvWySHUE.js.map} +1 -1
- package/dist/cdn/chunks/{VersionPreviewBanner-04CL1SGY.js → VersionPreviewBanner-CBVDWnhq.js} +3 -3
- package/dist/cdn/chunks/VersionPreviewBanner-CBVDWnhq.js.map +1 -0
- package/dist/cdn/chunks/{blockTypeIcons-hX_W4dqo.js → blockTypeIcons-A9qFb1cR.js} +2 -2
- package/dist/cdn/chunks/{blockTypeIcons-hX_W4dqo.js.map → blockTypeIcons-A9qFb1cR.js.map} +1 -1
- package/dist/cdn/chunks/{createCloudRuntime-gXA8_QRU.js → createCloudRuntime-DYDf_-rN.js} +3 -3
- package/dist/cdn/chunks/{createCloudRuntime-gXA8_QRU.js.map → createCloudRuntime-DYDf_-rN.js.map} +1 -1
- package/dist/cdn/chunks/{extensions-CMcVw2uu.js → extensions-Byo2sA1r.js} +3 -3
- package/dist/cdn/chunks/{extensions-CMcVw2uu.js.map → extensions-Byo2sA1r.js.map} +1 -1
- package/dist/cdn/chunks/{icons-C36Ha5VU.js → icons-Dl5bcXmT.js} +7 -7
- package/dist/cdn/chunks/{icons-C36Ha5VU.js.map → icons-Dl5bcXmT.js.map} +1 -1
- package/dist/cdn/chunks/{keys-CbQuRX85.js → keys-oJdkdtPn.js} +2 -2
- package/dist/cdn/chunks/{keys-CbQuRX85.js.map → keys-oJdkdtPn.js.map} +1 -1
- package/dist/cdn/chunks/{src-D5lajkK5.js → src-BAWlZu4B.js} +3 -3
- package/dist/cdn/chunks/{src-D5lajkK5.js.map → src-BAWlZu4B.js.map} +1 -1
- package/dist/cdn/chunks/{src-CoHDh8Ua.js → src-BDUi-9js.js} +15 -15
- package/dist/cdn/chunks/{src-CoHDh8Ua.js.map → src-BDUi-9js.js.map} +1 -1
- package/dist/cdn/chunks/{useCloudI18n-NVa1aeY8.js → useCloudI18n-Ch-SiRjU.js} +3 -3
- package/dist/cdn/chunks/{useCloudI18n-NVa1aeY8.js.map → useCloudI18n-Ch-SiRjU.js.map} +1 -1
- package/dist/cdn/chunks/{useEditorCore-DC8gmfs6.js → useEditorCore-D84laTMj.js} +128 -135
- package/dist/cdn/chunks/useEditorCore-D84laTMj.js.map +1 -0
- package/dist/cdn/chunks/useEditorRoot-Cxnmf5hM.js +10 -0
- package/dist/cdn/chunks/useEditorRoot-Cxnmf5hM.js.map +1 -0
- package/dist/cdn/chunks/{useI18n-DVWCJJFH.js → useI18n-4sUbTM3Y.js} +3 -3
- package/dist/cdn/chunks/useI18n-4sUbTM3Y.js.map +1 -0
- package/dist/cdn/chunks/{useMergeTag-BfZBRPQp.js → useMergeTag-DxjTEyin.js} +2 -2
- package/dist/cdn/chunks/{useMergeTag-BfZBRPQp.js.map → useMergeTag-DxjTEyin.js.map} +1 -1
- package/dist/cdn/chunks/{usePopoverPosition-BC-HA7TK.js → usePopoverPosition-C1N2kaiZ.js} +2 -2
- package/dist/cdn/chunks/{usePopoverPosition-BC-HA7TK.js.map → usePopoverPosition-C1N2kaiZ.js.map} +1 -1
- package/dist/cdn/chunks/useScrollToBlock-BvWAfWd8.js +22 -0
- package/dist/cdn/chunks/useScrollToBlock-BvWAfWd8.js.map +1 -0
- package/dist/cdn/editor.css +1 -1
- package/dist/cdn/editor.js +974 -937
- package/dist/cdn/editor.js.map +1 -1
- package/dist/{check-DZbmkMQe.js → check-GRcTaHlZ.js} +1 -1
- package/dist/{chevron-down-nYScOLeZ.js → chevron-down-NUIAa2Qf.js} +1 -1
- package/dist/{chevron-up-CpYpDLnc.js → chevron-up-Bjf2BQtL.js} +1 -1
- package/dist/{circle-alert-CuuK6thq.js → circle-alert-FpQ8ykQL.js} +1 -1
- package/dist/{clock-DxkrtqOr.js → clock-CaOM1DC-.js} +1 -1
- package/dist/{createCloudRuntime-Bt8W-ajI.js → createCloudRuntime-FEZG-Gve.js} +1 -1
- package/dist/{createLucideIcon-L-wQXW_R.js → createLucideIcon-CAmDKr5Z.js} +2 -2
- package/dist/{extensions-CX87G4aL.js → extensions-BV9qfsYr.js} +2 -2
- package/dist/{image-up-DouZRGVt.js → image-up-CIGT4cSb.js} +1 -1
- package/dist/{info-puX5V6Iq.js → info-9YwKinXj.js} +1 -1
- package/dist/{keys-BxixS0TN.js → keys-CVpi8xCT.js} +1 -1
- package/dist/{loader-circle-DqTxB7Kr.js → loader-circle-QyURl47g.js} +1 -1
- package/dist/{message-circle-BZ7YpB22.js → message-circle-C9pycEzV.js} +1 -1
- package/dist/{pencil-DmbRhTJu.js → pencil-BvW86jVn.js} +1 -1
- package/dist/{refresh-cw-OK_O-cL0.js → refresh-cw-BDAUomac.js} +1 -1
- package/dist/{scan-line-DyaSTsJ5.js → scan-line-B5EvAPSG.js} +1 -1
- package/dist/{send-B-wasJJC.js → send-BAUn7DLp.js} +1 -1
- package/dist/{shield-check-DUIyRrTo.js → shield-check-CLGywlg0.js} +1 -1
- package/dist/{sparkles-DYEMATWC.js → sparkles-X6C0spo6.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/templatical-editor.js +931 -895
- package/dist/{text-align-start-CzjLeBLe.js → text-align-start-C6FXOoEj.js} +1 -1
- package/dist/{trash-2-C0shEPry.js → trash-2-balHkQP4.js} +1 -1
- package/dist/{triangle-alert-Fw0ce8oQ.js → triangle-alert-D2JBEr6k.js} +1 -1
- package/dist/{upload-D69C2CBx.js → upload-D70OYprI.js} +1 -1
- package/dist/{useCloudI18n-CAZTZ9_a.js → useCloudI18n-DmWo4nzs.js} +2 -2
- package/dist/{useEditorCore-CFLzobo_.js → useEditorCore-D7Gz7_CW.js} +474 -481
- package/dist/useEditorRoot-DKyG95pb.js +8 -0
- package/dist/{useI18n-BfXff_Sy.js → useI18n-CvYvat9_.js} +2 -2
- package/dist/{useMergeTag-Dd4I7kge.js → useMergeTag-D6Z6cyDa.js} +1 -1
- package/dist/{usePopoverPosition-CxhCX3SQ.js → usePopoverPosition-WslxM_PU.js} +1 -1
- package/dist/useScrollToBlock-DcImB73T.js +44 -0
- package/dist/{x-BjTlFqRv.js → x-DTUPbuwx.js} +1 -1
- package/package.json +9 -9
- package/dist/cdn/chunks/IssuesPanel-CHtetM9i.js.map +0 -1
- package/dist/cdn/chunks/SavedBlocksPanels-nbK0UQTp.js.map +0 -1
- package/dist/cdn/chunks/TestEmailPanel-CAjQDXle.js.map +0 -1
- package/dist/cdn/chunks/VersionPreviewBanner-04CL1SGY.js.map +0 -1
- package/dist/cdn/chunks/useEditorCore-DC8gmfs6.js.map +0 -1
- package/dist/cdn/chunks/useI18n-DVWCJJFH.js.map +0 -1
- package/dist/list-checks-jw-ZlvWY.js +0 -25
|
@@ -3,10 +3,10 @@ import { E as e } from "./draggable-BfpsXFg7.js";
|
|
|
3
3
|
var t = Symbol("translations"), n = Symbol("editor"), r = Symbol("history"), i = Symbol("blockActions"), a = Symbol("conditionPreview"), o = Symbol("appliesConditionFilter"), s = Symbol("fontsManager"), c = Symbol("themeStyles"), l = Symbol("tplUiTheme"), u = Symbol("blockDefaults"), d = Symbol("blockRegistry"), f = Symbol("customBlockDefinitions"), p = Symbol("paletteBlocks"), m = Symbol("htmlBlockPreview"), h = Symbol("colors"), g = Symbol("customBlockStylesheets"), _ = Symbol("mergeTags"), v = Symbol("mergeTagSyntax"), y = Symbol("onRequestMergeTag"), b = Symbol("mergeTagAutocomplete"), x = Symbol("mergeTagSampleMode"), S = Symbol("useMergeTagSamples"), C = Symbol("resolvePreview"), w = Symbol("previewResolution"), T = Symbol("mergeTagPicker"), E = Symbol("logicTags"), D = Symbol("logicPairs"), O = Symbol("logicPicker"), k = Symbol("onRequestLogic"), A = Symbol("onRequestMedia"), j = Symbol("imageUrlResolver"), M = Symbol("displayConditions"), N = Symbol("allowCustomConditions"), P = Symbol("capabilities"), F = Symbol("keyboardReorder"), I = Symbol("templateLint"), L = Symbol("editorRoot"), R = Symbol("popoverRoot"), z = Symbol("savedBlocks"), B = Symbol("authManager"), V = Symbol("aiConfig"), H = Symbol("scoring"), U = Symbol("cloudTranslations");
|
|
4
4
|
function W(t, n) {
|
|
5
5
|
let r = e(t, null);
|
|
6
|
-
if (r == null) throw Error(`${n} requires a provider for ${t.description ?? "unknown key"}. Ensure it
|
|
6
|
+
if (r == null) throw Error(`${n} requires a provider for ${t.description ?? "unknown key"}. Ensure it renders inside the editor, below Editor.vue.`);
|
|
7
7
|
return r;
|
|
8
8
|
}
|
|
9
9
|
//#endregion
|
|
10
10
|
export { k as A, c as B, E as C, T as D, b as E, w as F, l as H, C as I, z as L, y as M, p as N, x as O, R as P, H as R, D as S, _ as T, S as U, t as V, W, s as _, i as a, j as b, P as c, a as d, f, L as g, n as h, B as i, A as j, v as k, U as l, M as m, N as n, u as o, g as p, o as r, d as s, V as t, h as u, r as v, O as w, F as x, m as y, I as z };
|
|
11
11
|
|
|
12
|
-
//# sourceMappingURL=keys-
|
|
12
|
+
//# sourceMappingURL=keys-oJdkdtPn.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys-CbQuRX85.js","names":[],"sources":["../../../src/keys.ts"],"sourcesContent":["import { inject, type InjectionKey, type ComputedRef, type Ref } from \"vue\";\nimport type {\n UseHistoryReturn,\n UseBlockActionsReturn,\n UseConditionPreviewReturn,\n} from \"@templatical/core\";\nimport type {\n BlockDefaults,\n CustomBlockDefinition,\n DisplayCondition,\n LogicPair,\n LogicTag,\n MergeTag,\n ResolvePreview,\n SyntaxPreset,\n} from \"@templatical/types\";\nimport type { Translations, CloudTranslations } from \"./i18n\";\nimport type {\n AuthManager,\n UseAiConfigReturn,\n UseTemplateScoringReturn,\n} from \"@templatical/core/cloud\";\nimport type { UseSavedBlocksReturn } from \"@templatical/core\";\nimport type { BaseEditorReturn } from \"./composables/useEditorCore\";\nimport type { ImageUrlResolver } from \"./composables/useImageUrlResolver\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\nimport type { UseBlockRegistryReturn } from \"./composables/useBlockRegistry\";\nimport type { UseKeyboardReorderReturn } from \"./composables/useKeyboardReorder\";\nimport type { EditorCapabilities } from \"./types/editor-capabilities\";\nimport type { OnRequestMedia } from \"./index\";\n\n// ---------------------------------------------------------------------------\n// Shared keys (provided by useEditorCore, consumed by all components)\n// ---------------------------------------------------------------------------\n\nexport const TRANSLATIONS_KEY: InjectionKey<Translations> =\n Symbol(\"translations\");\n\nexport const EDITOR_KEY: InjectionKey<BaseEditorReturn> = Symbol(\"editor\");\n\nexport const HISTORY_KEY: InjectionKey<UseHistoryReturn> = Symbol(\"history\");\n\nexport const BLOCK_ACTIONS_KEY: InjectionKey<UseBlockActionsReturn> =\n Symbol(\"blockActions\");\n\nexport const CONDITION_PREVIEW_KEY: InjectionKey<UseConditionPreviewReturn> =\n Symbol(\"conditionPreview\");\n\n/**\n * Whether the hand-toggled display-condition filter applies on this surface —\n * provided per-surface, exactly like `USE_MERGE_TAG_SAMPLES_KEY`, and for the\n * same reason: the rule folds in both `previewMode` and whether a resolver owns\n * the preview, so a component that re-derived it from `CONDITION_PREVIEW_KEY`\n * plus the editor's `previewMode` could get one half wrong on its own.\n *\n * Absent means \"applies\" — a surface with no provider filters as it always has.\n */\nexport const APPLIES_CONDITION_FILTER_KEY: InjectionKey<ComputedRef<boolean>> =\n Symbol(\"appliesConditionFilter\");\n\nexport const FONTS_MANAGER_KEY: InjectionKey<UseFontsReturn> =\n Symbol(\"fontsManager\");\n\nexport const THEME_STYLES_KEY: InjectionKey<\n ComputedRef<Record<string, string>>\n> = Symbol(\"themeStyles\");\n\nexport const UI_THEME_KEY: InjectionKey<ComputedRef<string>> =\n Symbol(\"tplUiTheme\");\n\nexport const BLOCK_DEFAULTS_KEY: InjectionKey<BlockDefaults | undefined> =\n Symbol(\"blockDefaults\");\n\nexport const BLOCK_REGISTRY_KEY: InjectionKey<UseBlockRegistryReturn> =\n Symbol(\"blockRegistry\");\n\nexport const CUSTOM_BLOCK_DEFINITIONS_KEY: InjectionKey<\n CustomBlockDefinition[]\n> = Symbol(\"customBlockDefinitions\");\n\n/**\n * Consumer-supplied block-palette allowlist + order (`config.paletteBlocks`).\n * `undefined` means the default full palette. Consumed by `Sidebar.vue`.\n */\nexport const PALETTE_BLOCKS_KEY: InjectionKey<string[] | undefined> =\n Symbol(\"paletteBlocks\");\n\n/**\n * Whether HTML blocks render a live sandboxed-iframe preview in the canvas\n * (`config.htmlBlockPreview`), normalized to a boolean. `false` (the default)\n * keeps the static placeholder card. Consumed by `HtmlBlock.vue`.\n */\nexport const HTML_BLOCK_PREVIEW_KEY: InjectionKey<boolean> =\n Symbol(\"htmlBlockPreview\");\n\n/**\n * Consumer-supplied color-picker palette (`config.colors`), normalized to\n * `{ presets, allowCustom, allowCustomIgnored }` by `resolveColorsConfig`.\n * Provided by `useEditorCore`, consumed by every `ColorPicker`. The inject\n * default (`DEFAULT_RESOLVED_COLORS` = `{ presets: [], allowCustom: true,\n * allowCustomIgnored: false }`) makes an unconfigured picker render exactly\n * as before.\n */\nexport const COLORS_KEY: InjectionKey<\n import(\"./utils/resolveColorsConfig\").ResolvedColors\n> = Symbol(\"colors\");\n\nexport const CUSTOM_BLOCK_STYLESHEETS_KEY: InjectionKey<ComputedRef<string[]>> =\n Symbol(\"customBlockStylesheets\");\n\nexport const MERGE_TAGS_KEY: InjectionKey<MergeTag[]> = Symbol(\"mergeTags\");\n\nexport const MERGE_TAG_SYNTAX_KEY: InjectionKey<SyntaxPreset> =\n Symbol(\"mergeTagSyntax\");\n\nexport const ON_REQUEST_MERGE_TAG_KEY: InjectionKey<\n (() => Promise<MergeTag | null>) | null\n> = Symbol(\"onRequestMergeTag\");\n\nexport const MERGE_TAG_AUTOCOMPLETE_KEY: InjectionKey<boolean> = Symbol(\n \"mergeTagAutocomplete\",\n);\n\n/**\n * The user's choice between the previews' two merge-tag views: `true` for\n * Sample (tags replaced by their `MergeTag.sample`, rendered as ordinary text)\n * and `false` for Label (tags shown as their label with the usual cue).\n *\n * Session state owned by `useEditorCore` and driven by a toggle that exists\n * only on preview surfaces. Defaults to Sample.\n */\nexport const MERGE_TAG_SAMPLE_MODE_KEY: InjectionKey<Ref<boolean>> =\n Symbol(\"mergeTagSampleMode\");\n\n/**\n * Whether **the current render surface** substitutes samples.\n *\n * Provided per surface rather than globally, and that is the whole point:\n * substitution must never happen while editing, so `Canvas.vue` folds in its\n * `previewMode` flag and provides `false` whenever the canvas is editable, no\n * matter what the mode ref says. Preview surfaces provide the mode directly.\n *\n * Block components inject **this** key. They must never read\n * `MERGE_TAG_SAMPLE_MODE_KEY`, which would bypass the editing guard.\n */\nexport const USE_MERGE_TAG_SAMPLES_KEY: InjectionKey<ComputedRef<boolean>> =\n Symbol(\"useMergeTagSamples\");\n\n/**\n * The `resolvePreview` seam — resolved content plus its loading and failure\n * state. Provided by `useEditorCore` so every preview surface shares one\n * lifecycle; `null` when the consumer configured no hook.\n *\n * Surfaces read `content` from here instead of `editor.content`, and gate the\n * Sample/Label toggle on `supersedesSamples`.\n */\n/**\n * The raw `config.resolvePreview` hook.\n *\n * Provided alongside PREVIEW_RESOLUTION_KEY because the test-email dialog needs\n * its **own** resolution instance — it resolves for the selected recipient,\n * which the editor's canvas has no notion of — and so cannot reuse the shared\n * one. `undefined` when the consumer configured no hook.\n */\nexport const RESOLVE_PREVIEW_KEY: InjectionKey<ResolvePreview | undefined> =\n Symbol(\"resolvePreview\");\n\nexport const PREVIEW_RESOLUTION_KEY: InjectionKey<\n import(\"./composables/usePreviewResolution\").UsePreviewResolutionReturn | null\n> = Symbol(\"previewResolution\");\n\n/**\n * Singleton state for the built-in merge tag picker modal. Provided by\n * `useEditorCore`, consumed by `useMergeTag.requestMergeTag()` (to open\n * the modal as a fall-through when only `mergeTags.tags` is configured)\n * and by `MergeTagPickerModal.vue` (to render and resolve).\n */\nexport const MERGE_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useMergeTagPicker\").UseMergeTagPickerReturn\n> = Symbol(\"mergeTagPicker\");\n\n// ---------------------------------------------------------------------------\n// Logic tags — a standalone feature, separate from merge tags. Native\n// highlighting (LogicMergeTagNode) is always on; these drive the dedicated\n// logic picker + insertion.\n// ---------------------------------------------------------------------------\n\nexport const LOGIC_TAGS_KEY: InjectionKey<LogicTag[]> = Symbol(\"logicTags\");\n\nexport const LOGIC_PAIRS_KEY: InjectionKey<LogicPair[]> = Symbol(\"logicPairs\");\n\n/**\n * Singleton state for the built-in logic picker modal. Provided by\n * `useEditorCore`, consumed by `useLogicTag.requestLogicTag()` (to open the modal)\n * and by `LogicTagPickerModal.vue` (to render and resolve).\n */\nexport const LOGIC_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useLogicTagPicker\").UseLogicTagPickerReturn\n> = Symbol(\"logicPicker\");\n\nexport const ON_REQUEST_LOGIC_TAG_KEY: InjectionKey<\n (() => Promise<LogicTag | LogicPair | null>) | null\n> = Symbol(\"onRequestLogic\");\n\nexport const ON_REQUEST_MEDIA_KEY: InjectionKey<OnRequestMedia | null> =\n Symbol(\"onRequestMedia\");\n\n/**\n * Per-editor display-only image URL resolver (`config.resolveImageUrl`),\n * wrapped with a per-src cache by `createImageUrlResolver`. `null` when the\n * host doesn't resolve — the canvas then displays canonical src values\n * verbatim. Consumed via `useResolvedImageSrc` in image-displaying blocks.\n */\nexport const IMAGE_URL_RESOLVER_KEY: InjectionKey<ImageUrlResolver | null> =\n Symbol(\"imageUrlResolver\");\n\nexport const DISPLAY_CONDITIONS_KEY: InjectionKey<DisplayCondition[]> =\n Symbol(\"displayConditions\");\n\nexport const ALLOW_CUSTOM_CONDITIONS_KEY: InjectionKey<boolean> = Symbol(\n \"allowCustomConditions\",\n);\n\nexport const CAPABILITIES_KEY: InjectionKey<EditorCapabilities> =\n Symbol(\"capabilities\");\n\nexport const KEYBOARD_REORDER_KEY: InjectionKey<UseKeyboardReorderReturn> =\n Symbol(\"keyboardReorder\");\n\nexport const TEMPLATE_LINT_KEY: InjectionKey<\n import(\"./composables/useTemplateLint\").UseTemplateLintReturn | null\n> = Symbol(\"templateLint\");\n\n/**\n * The editor's effective DOM root — `Document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true`. Composables that read `document.activeElement`,\n * popup mount targets, etc. should inject this and use the root-aware API\n * (both `Document` and `ShadowRoot` expose `activeElement` etc.).\n */\nexport const EDITOR_ROOT_KEY: InjectionKey<Document | ShadowRoot> =\n Symbol(\"editorRoot\");\n\n/**\n * Mount target for popovers, toolbars, and modal dialogs — never\n * `document.body`. Provided by `useEditorCore` as a ref bound\n * to a `<div class=\"tpl-popover-root\" />` rendered at the top level of the\n * editor template. Teleports use `:to=\"popoverRoot\"` so popups land inside\n * the editor's effective root — `document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true` — instead of escaping the shadow boundary.\n *\n * Null until the editor's template mounts; consumers must guard with\n * `v-if=\"popoverRoot\"` before passing to `<Teleport>`.\n */\nexport const POPOVER_ROOT_KEY: InjectionKey<Ref<HTMLElement | null>> =\n Symbol(\"popoverRoot\");\n\n/**\n * Reactive saved-blocks state, provided by `useSavedBlocksFeature` when a\n * `SavedBlocksProvider` is configured. Shared by OSS and Cloud — the storage\n * transport lives behind the provider, so this key is not cloud-specific.\n */\nexport const SAVED_BLOCKS_KEY: InjectionKey<UseSavedBlocksReturn> =\n Symbol(\"savedBlocks\");\n\n// ---------------------------------------------------------------------------\n// Cloud-only keys (provided by CloudEditor, consumed by cloud components)\n// ---------------------------------------------------------------------------\n\nexport const AUTH_MANAGER_KEY: InjectionKey<AuthManager> =\n Symbol(\"authManager\");\n\nexport const AI_CONFIG_KEY: InjectionKey<UseAiConfigReturn> =\n Symbol(\"aiConfig\");\n\n// Comments are deliberately not injected: the panel takes `useCommentsFeature`'s\n// return as a **prop** the way `TestEmailPanel` and `SavedBlocksPanels` do, and the\n// per-block indicators read the comments entry on `CAPABILITIES_KEY`. Nothing\n// injects the composable itself.\n\nexport const SCORING_KEY: InjectionKey<UseTemplateScoringReturn> =\n Symbol(\"scoring\");\n\nexport const CLOUD_TRANSLATIONS_KEY: InjectionKey<CloudTranslations> =\n Symbol(\"cloudTranslations\");\n\n// ---------------------------------------------------------------------------\n// Helper for required injections with explicit null default + throw\n// ---------------------------------------------------------------------------\n\nexport function requireInject<T>(\n key: InjectionKey<T>,\n componentName: string,\n): T {\n const value = inject(key, null) as T | null;\n if (value === null || value === undefined) {\n throw new Error(\n `${componentName} requires a provider for ${key.description ?? \"unknown key\"}. Ensure it is a descendant of Editor or CloudEditor.`,\n );\n }\n return value;\n}\n"],"mappings":";;AAmCA,IAAa,IACX,OAAO,cAAc,GAEV,IAA6C,OAAO,QAAQ,GAE5D,IAA8C,OAAO,SAAS,GAE9D,IACX,OAAO,cAAc,GAEV,IACX,OAAO,kBAAkB,GAWd,IACX,OAAO,wBAAwB,GAEpB,IACX,OAAO,cAAc,GAEV,IAET,OAAO,aAAa,GAEX,IACX,OAAO,YAAY,GAER,IACX,OAAO,eAAe,GAEX,IACX,OAAO,eAAe,GAEX,IAET,OAAO,wBAAwB,GAMtB,IACX,OAAO,eAAe,GAOX,IACX,OAAO,kBAAkB,GAUd,IAET,OAAO,QAAQ,GAEN,IACX,OAAO,wBAAwB,GAEpB,IAA2C,OAAO,WAAW,GAE7D,IACX,OAAO,gBAAgB,GAEZ,IAET,OAAO,mBAAmB,GAEjB,IAAoD,OAC/D,sBACF,GAUa,IACX,OAAO,oBAAoB,GAahB,IACX,OAAO,oBAAoB,GAkBhB,IACX,OAAO,gBAAgB,GAEZ,IAET,OAAO,mBAAmB,GAQjB,IAET,OAAO,gBAAgB,GAQd,IAA2C,OAAO,WAAW,GAE7D,IAA6C,OAAO,YAAY,GAOhE,IAET,OAAO,aAAa,GAEX,IAET,OAAO,gBAAgB,GAEd,IACX,OAAO,gBAAgB,GAQZ,IACX,OAAO,kBAAkB,GAEd,IACX,OAAO,mBAAmB,GAEf,IAAqD,OAChE,uBACF,GAEa,IACX,OAAO,cAAc,GAEV,IACX,OAAO,iBAAiB,GAEb,IAET,OAAO,cAAc,GAQZ,IACX,OAAO,YAAY,GAaR,IACX,OAAO,aAAa,GAOT,IACX,OAAO,aAAa,GAMT,IACX,OAAO,aAAa,GAET,IACX,OAAO,UAAU,GAON,IACX,OAAO,SAAS,GAEL,IACX,OAAO,mBAAmB;AAM5B,SAAgB,EACd,GACA,GACG;CACH,IAAM,IAAQ,EAAO,GAAK,IAAI;CAC9B,IAAI,KAAU,MACZ,MAAU,MACR,GAAG,EAAc,2BAA2B,EAAI,eAAe,cAAc,sDAC/E;CAEF,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"keys-oJdkdtPn.js","names":[],"sources":["../../../src/keys.ts"],"sourcesContent":["import { inject, type InjectionKey, type ComputedRef, type Ref } from \"vue\";\nimport type {\n UseHistoryReturn,\n UseBlockActionsReturn,\n UseConditionPreviewReturn,\n} from \"@templatical/core\";\nimport type {\n BlockDefaults,\n CustomBlockDefinition,\n DisplayCondition,\n LogicPair,\n LogicTag,\n MergeTag,\n ResolvePreview,\n SyntaxPreset,\n} from \"@templatical/types\";\nimport type { Translations, CloudTranslations } from \"./i18n\";\nimport type {\n AuthManager,\n UseAiConfigReturn,\n UseTemplateScoringReturn,\n} from \"@templatical/core/cloud\";\nimport type { UseSavedBlocksReturn } from \"@templatical/core\";\nimport type { BaseEditorReturn } from \"./composables/useEditorCore\";\nimport type { ImageUrlResolver } from \"./composables/useImageUrlResolver\";\nimport type { UseFontsReturn } from \"./composables/useFonts\";\nimport type { UseBlockRegistryReturn } from \"./composables/useBlockRegistry\";\nimport type { UseKeyboardReorderReturn } from \"./composables/useKeyboardReorder\";\nimport type { EditorCapabilities } from \"./types/editor-capabilities\";\nimport type { OnRequestMedia } from \"./index\";\n\n// ---------------------------------------------------------------------------\n// Shared keys (provided by useEditorCore, consumed by all components)\n// ---------------------------------------------------------------------------\n\nexport const TRANSLATIONS_KEY: InjectionKey<Translations> =\n Symbol(\"translations\");\n\nexport const EDITOR_KEY: InjectionKey<BaseEditorReturn> = Symbol(\"editor\");\n\nexport const HISTORY_KEY: InjectionKey<UseHistoryReturn> = Symbol(\"history\");\n\nexport const BLOCK_ACTIONS_KEY: InjectionKey<UseBlockActionsReturn> =\n Symbol(\"blockActions\");\n\nexport const CONDITION_PREVIEW_KEY: InjectionKey<UseConditionPreviewReturn> =\n Symbol(\"conditionPreview\");\n\n/**\n * Whether the hand-toggled display-condition filter applies on this surface —\n * provided per-surface, exactly like `USE_MERGE_TAG_SAMPLES_KEY`, and for the\n * same reason: the rule folds in both `previewMode` and whether a resolver owns\n * the preview, so a component that re-derived it from `CONDITION_PREVIEW_KEY`\n * plus the editor's `previewMode` could get one half wrong on its own.\n *\n * Absent means \"applies\" — a surface with no provider filters as it always has.\n */\nexport const APPLIES_CONDITION_FILTER_KEY: InjectionKey<ComputedRef<boolean>> =\n Symbol(\"appliesConditionFilter\");\n\nexport const FONTS_MANAGER_KEY: InjectionKey<UseFontsReturn> =\n Symbol(\"fontsManager\");\n\nexport const THEME_STYLES_KEY: InjectionKey<\n ComputedRef<Record<string, string>>\n> = Symbol(\"themeStyles\");\n\nexport const UI_THEME_KEY: InjectionKey<ComputedRef<string>> =\n Symbol(\"tplUiTheme\");\n\nexport const BLOCK_DEFAULTS_KEY: InjectionKey<BlockDefaults | undefined> =\n Symbol(\"blockDefaults\");\n\nexport const BLOCK_REGISTRY_KEY: InjectionKey<UseBlockRegistryReturn> =\n Symbol(\"blockRegistry\");\n\nexport const CUSTOM_BLOCK_DEFINITIONS_KEY: InjectionKey<\n CustomBlockDefinition[]\n> = Symbol(\"customBlockDefinitions\");\n\n/**\n * Consumer-supplied block-palette allowlist + order (`config.paletteBlocks`).\n * `undefined` means the default full palette. Consumed by `Sidebar.vue`.\n */\nexport const PALETTE_BLOCKS_KEY: InjectionKey<string[] | undefined> =\n Symbol(\"paletteBlocks\");\n\n/**\n * Whether HTML blocks render a live sandboxed-iframe preview in the canvas\n * (`config.htmlBlockPreview`), normalized to a boolean. `false` (the default)\n * keeps the static placeholder card. Consumed by `HtmlBlock.vue`.\n */\nexport const HTML_BLOCK_PREVIEW_KEY: InjectionKey<boolean> =\n Symbol(\"htmlBlockPreview\");\n\n/**\n * Consumer-supplied color-picker palette (`config.colors`), normalized to\n * `{ presets, allowCustom, allowCustomIgnored }` by `resolveColorsConfig`.\n * Provided by `useEditorCore`, consumed by every `ColorPicker`. The inject\n * default (`DEFAULT_RESOLVED_COLORS` = `{ presets: [], allowCustom: true,\n * allowCustomIgnored: false }`) makes an unconfigured picker render exactly\n * as before.\n */\nexport const COLORS_KEY: InjectionKey<\n import(\"./utils/resolveColorsConfig\").ResolvedColors\n> = Symbol(\"colors\");\n\nexport const CUSTOM_BLOCK_STYLESHEETS_KEY: InjectionKey<ComputedRef<string[]>> =\n Symbol(\"customBlockStylesheets\");\n\nexport const MERGE_TAGS_KEY: InjectionKey<MergeTag[]> = Symbol(\"mergeTags\");\n\nexport const MERGE_TAG_SYNTAX_KEY: InjectionKey<SyntaxPreset> =\n Symbol(\"mergeTagSyntax\");\n\nexport const ON_REQUEST_MERGE_TAG_KEY: InjectionKey<\n (() => Promise<MergeTag | null>) | null\n> = Symbol(\"onRequestMergeTag\");\n\nexport const MERGE_TAG_AUTOCOMPLETE_KEY: InjectionKey<boolean> = Symbol(\n \"mergeTagAutocomplete\",\n);\n\n/**\n * The user's choice between the previews' two merge-tag views: `true` for\n * Sample (tags replaced by their `MergeTag.sample`, rendered as ordinary text)\n * and `false` for Label (tags shown as their label with the usual cue).\n *\n * Session state owned by `useEditorCore` and driven by a toggle that exists\n * only on preview surfaces. Defaults to Sample.\n */\nexport const MERGE_TAG_SAMPLE_MODE_KEY: InjectionKey<Ref<boolean>> =\n Symbol(\"mergeTagSampleMode\");\n\n/**\n * Whether **the current render surface** substitutes samples.\n *\n * Provided per surface rather than globally, and that is the whole point:\n * substitution must never happen while editing, so `Canvas.vue` folds in its\n * `previewMode` flag and provides `false` whenever the canvas is editable, no\n * matter what the mode ref says. Preview surfaces provide the mode directly.\n *\n * Block components inject **this** key. They must never read\n * `MERGE_TAG_SAMPLE_MODE_KEY`, which would bypass the editing guard.\n */\nexport const USE_MERGE_TAG_SAMPLES_KEY: InjectionKey<ComputedRef<boolean>> =\n Symbol(\"useMergeTagSamples\");\n\n/**\n * The `resolvePreview` seam — resolved content plus its loading and failure\n * state. Provided by `useEditorCore` so every preview surface shares one\n * lifecycle; `null` when the consumer configured no hook.\n *\n * Surfaces read `content` from here instead of `editor.content`, and gate the\n * Sample/Label toggle on `supersedesSamples`.\n */\n/**\n * The raw `config.resolvePreview` hook.\n *\n * Provided alongside PREVIEW_RESOLUTION_KEY because the test-email dialog needs\n * its **own** resolution instance — it resolves for the selected recipient,\n * which the editor's canvas has no notion of — and so cannot reuse the shared\n * one. `undefined` when the consumer configured no hook.\n */\nexport const RESOLVE_PREVIEW_KEY: InjectionKey<ResolvePreview | undefined> =\n Symbol(\"resolvePreview\");\n\nexport const PREVIEW_RESOLUTION_KEY: InjectionKey<\n import(\"./composables/usePreviewResolution\").UsePreviewResolutionReturn | null\n> = Symbol(\"previewResolution\");\n\n/**\n * Singleton state for the built-in merge tag picker modal. Provided by\n * `useEditorCore`, consumed by `useMergeTag.requestMergeTag()` (to open\n * the modal as a fall-through when only `mergeTags.tags` is configured)\n * and by `MergeTagPickerModal.vue` (to render and resolve).\n */\nexport const MERGE_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useMergeTagPicker\").UseMergeTagPickerReturn\n> = Symbol(\"mergeTagPicker\");\n\n// ---------------------------------------------------------------------------\n// Logic tags — a standalone feature, separate from merge tags. Native\n// highlighting (LogicMergeTagNode) is always on; these drive the dedicated\n// logic picker + insertion.\n// ---------------------------------------------------------------------------\n\nexport const LOGIC_TAGS_KEY: InjectionKey<LogicTag[]> = Symbol(\"logicTags\");\n\nexport const LOGIC_PAIRS_KEY: InjectionKey<LogicPair[]> = Symbol(\"logicPairs\");\n\n/**\n * Singleton state for the built-in logic picker modal. Provided by\n * `useEditorCore`, consumed by `useLogicTag.requestLogicTag()` (to open the modal)\n * and by `LogicTagPickerModal.vue` (to render and resolve).\n */\nexport const LOGIC_TAG_PICKER_KEY: InjectionKey<\n import(\"./composables/useLogicTagPicker\").UseLogicTagPickerReturn\n> = Symbol(\"logicPicker\");\n\nexport const ON_REQUEST_LOGIC_TAG_KEY: InjectionKey<\n (() => Promise<LogicTag | LogicPair | null>) | null\n> = Symbol(\"onRequestLogic\");\n\nexport const ON_REQUEST_MEDIA_KEY: InjectionKey<OnRequestMedia | null> =\n Symbol(\"onRequestMedia\");\n\n/**\n * Per-editor display-only image URL resolver (`config.resolveImageUrl`),\n * wrapped with a per-src cache by `createImageUrlResolver`. `null` when the\n * host doesn't resolve — the canvas then displays canonical src values\n * verbatim. Consumed via `useResolvedImageSrc` in image-displaying blocks.\n */\nexport const IMAGE_URL_RESOLVER_KEY: InjectionKey<ImageUrlResolver | null> =\n Symbol(\"imageUrlResolver\");\n\nexport const DISPLAY_CONDITIONS_KEY: InjectionKey<DisplayCondition[]> =\n Symbol(\"displayConditions\");\n\nexport const ALLOW_CUSTOM_CONDITIONS_KEY: InjectionKey<boolean> = Symbol(\n \"allowCustomConditions\",\n);\n\nexport const CAPABILITIES_KEY: InjectionKey<EditorCapabilities> =\n Symbol(\"capabilities\");\n\nexport const KEYBOARD_REORDER_KEY: InjectionKey<UseKeyboardReorderReturn> =\n Symbol(\"keyboardReorder\");\n\nexport const TEMPLATE_LINT_KEY: InjectionKey<\n import(\"./composables/useTemplateLint\").UseTemplateLintReturn | null\n> = Symbol(\"templateLint\");\n\n/**\n * The editor's effective DOM root — `Document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true`. Composables that read `document.activeElement`,\n * popup mount targets, etc. should inject this and use the root-aware API\n * (both `Document` and `ShadowRoot` expose `activeElement` etc.).\n */\nexport const EDITOR_ROOT_KEY: InjectionKey<Document | ShadowRoot> =\n Symbol(\"editorRoot\");\n\n/**\n * Mount target for popovers, toolbars, and modal dialogs — never\n * `document.body`. Provided by `useEditorCore` as a ref bound\n * to a `<div class=\"tpl-popover-root\" />` rendered at the top level of the\n * editor template. Teleports use `:to=\"popoverRoot\"` so popups land inside\n * the editor's effective root — `document` in light-DOM mode, `ShadowRoot`\n * when `shadowDom: true` — instead of escaping the shadow boundary.\n *\n * Null until the editor's template mounts; consumers must guard with\n * `v-if=\"popoverRoot\"` before passing to `<Teleport>`.\n */\nexport const POPOVER_ROOT_KEY: InjectionKey<Ref<HTMLElement | null>> =\n Symbol(\"popoverRoot\");\n\n/**\n * Reactive saved-blocks state, provided by `useSavedBlocksFeature` when a\n * `SavedBlocksProvider` is configured. Shared by OSS and Cloud — the storage\n * transport lives behind the provider, so this key is not cloud-specific.\n */\nexport const SAVED_BLOCKS_KEY: InjectionKey<UseSavedBlocksReturn> =\n Symbol(\"savedBlocks\");\n\n// ---------------------------------------------------------------------------\n// Cloud-only keys (provided by createCloudRuntime, consumed by cloud components)\n// ---------------------------------------------------------------------------\n\nexport const AUTH_MANAGER_KEY: InjectionKey<AuthManager> =\n Symbol(\"authManager\");\n\nexport const AI_CONFIG_KEY: InjectionKey<UseAiConfigReturn> =\n Symbol(\"aiConfig\");\n\n// Comments are deliberately not injected: the panel takes `useCommentsFeature`'s\n// return as a **prop** the way `TestEmailPanel` and `SavedBlocksPanels` do, and the\n// per-block indicators read the comments entry on `CAPABILITIES_KEY`. Nothing\n// injects the composable itself.\n\nexport const SCORING_KEY: InjectionKey<UseTemplateScoringReturn> =\n Symbol(\"scoring\");\n\nexport const CLOUD_TRANSLATIONS_KEY: InjectionKey<CloudTranslations> =\n Symbol(\"cloudTranslations\");\n\n// ---------------------------------------------------------------------------\n// Helper for required injections with explicit null default + throw\n// ---------------------------------------------------------------------------\n\nexport function requireInject<T>(\n key: InjectionKey<T>,\n componentName: string,\n): T {\n const value = inject(key, null) as T | null;\n if (value === null || value === undefined) {\n throw new Error(\n `${componentName} requires a provider for ${key.description ?? \"unknown key\"}. Ensure it renders inside the editor, below Editor.vue.`,\n );\n }\n return value;\n}\n"],"mappings":";;AAmCA,IAAa,IACX,OAAO,cAAc,GAEV,IAA6C,OAAO,QAAQ,GAE5D,IAA8C,OAAO,SAAS,GAE9D,IACX,OAAO,cAAc,GAEV,IACX,OAAO,kBAAkB,GAWd,IACX,OAAO,wBAAwB,GAEpB,IACX,OAAO,cAAc,GAEV,IAET,OAAO,aAAa,GAEX,IACX,OAAO,YAAY,GAER,IACX,OAAO,eAAe,GAEX,IACX,OAAO,eAAe,GAEX,IAET,OAAO,wBAAwB,GAMtB,IACX,OAAO,eAAe,GAOX,IACX,OAAO,kBAAkB,GAUd,IAET,OAAO,QAAQ,GAEN,IACX,OAAO,wBAAwB,GAEpB,IAA2C,OAAO,WAAW,GAE7D,IACX,OAAO,gBAAgB,GAEZ,IAET,OAAO,mBAAmB,GAEjB,IAAoD,OAC/D,sBACF,GAUa,IACX,OAAO,oBAAoB,GAahB,IACX,OAAO,oBAAoB,GAkBhB,IACX,OAAO,gBAAgB,GAEZ,IAET,OAAO,mBAAmB,GAQjB,IAET,OAAO,gBAAgB,GAQd,IAA2C,OAAO,WAAW,GAE7D,IAA6C,OAAO,YAAY,GAOhE,IAET,OAAO,aAAa,GAEX,IAET,OAAO,gBAAgB,GAEd,IACX,OAAO,gBAAgB,GAQZ,IACX,OAAO,kBAAkB,GAEd,IACX,OAAO,mBAAmB,GAEf,IAAqD,OAChE,uBACF,GAEa,IACX,OAAO,cAAc,GAEV,IACX,OAAO,iBAAiB,GAEb,IAET,OAAO,cAAc,GAQZ,IACX,OAAO,YAAY,GAaR,IACX,OAAO,aAAa,GAOT,IACX,OAAO,aAAa,GAMT,IACX,OAAO,aAAa,GAET,IACX,OAAO,UAAU,GAON,IACX,OAAO,SAAS,GAEL,IACX,OAAO,mBAAmB;AAM5B,SAAgB,EACd,GACA,GACG;CACH,IAAM,IAAQ,EAAO,GAAK,IAAI;CAC9B,IAAI,KAAU,MACZ,MAAU,MACR,GAAG,EAAc,2BAA2B,EAAI,eAAe,cAAc,yDAC/E;CAEF,OAAO;AACT"}
|
|
@@ -4,14 +4,14 @@ import { $ as e, G as t, H as n, J as r, K as i, Q as a, U as o, V as s, W as c,
|
|
|
4
4
|
var g = `https://cdn.jsdelivr.net/npm/@templatical/renderer@${{
|
|
5
5
|
name: "@templatical/renderer",
|
|
6
6
|
description: "Render Templatical email templates to MJML",
|
|
7
|
-
version: "0.27.
|
|
7
|
+
version: "0.27.4",
|
|
8
8
|
bugs: "https://github.com/templatical/sdk/issues",
|
|
9
9
|
dependencies: { "@templatical/types": "workspace:*" },
|
|
10
10
|
devDependencies: {
|
|
11
11
|
"@resvg/resvg-js": "^2.6.2",
|
|
12
12
|
mjml: "^5.4.0",
|
|
13
13
|
typescript: "^6.0.3",
|
|
14
|
-
vitest: "^4.1.
|
|
14
|
+
vitest: "^4.1.11"
|
|
15
15
|
},
|
|
16
16
|
exports: { ".": {
|
|
17
17
|
types: "./dist/index.d.ts",
|
|
@@ -562,4 +562,4 @@ function Se(e) {
|
|
|
562
562
|
//#endregion
|
|
563
563
|
export { g as DEFAULT_SOCIAL_ICONS_BASE_URL, v as RenderContext, S as UNRENDERABLE_MARKER_PREFIX, A as convertMergeTagsToValues, O as escapeAttr, D as escapeHtml, b as getCssClassAttr, x as getCssClasses, J as getWidthPercentages, Y as getWidthPixels, y as isHiddenOnAll, Z as renderBlock, pe as renderToMjml, w as renderUnrenderableBlock, M as toPaddingString };
|
|
564
564
|
|
|
565
|
-
//# sourceMappingURL=src-
|
|
565
|
+
//# sourceMappingURL=src-BAWlZu4B.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src-D5lajkK5.js","names":[],"sources":["../../../../renderer/package.json","../../../../renderer/src/render-context.ts","../../../../renderer/src/visibility.ts","../../../../renderer/src/unrenderable.ts","../../../../renderer/src/escape.ts","../../../../renderer/src/padding.ts","../../../../renderer/src/utils.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/display-condition.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.27.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.4.0\",\n \"typescript\": \"^6.0.3\",\n \"vitest\": \"^4.1.10\"\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\": \"tsdown && 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 { Block, BlockType, CustomFont } from \"@templatical/types\";\nimport pkg from \"../package.json\" with { type: \"json\" };\n\n/**\n * A function type that renders a single block to MJML markup.\n */\nexport type BlockRenderer = (block: Block, context: RenderContext) => string;\n\n/**\n * Per-block-type renderer overrides, keyed by `block.type`. An entry replaces the\n * built-in renderer for that type wholesale — including its hidden-on-all-\n * viewports check, which becomes the override's responsibility.\n *\n * Generalises `renderCustomBlock`, which is the same idea for one block type. Its\n * reason to exist is that Templatical Cloud's render output is a deliberate\n * *superset*: countdown blocks resolve to a server-generated animated GIF and\n * video thumbnails get a composited play button, neither of which a browser can\n * do at render time. With this hook Cloud's renderer is the published one plus\n * two injected functions, so parity for every other block type holds by\n * construction instead of by review.\n */\nexport type BlockRendererMap = Partial<Record<BlockType, BlockRenderer>>;\n\nexport const DEFAULT_SOCIAL_ICONS_BASE_URL = `https://cdn.jsdelivr.net/npm/@templatical/renderer@${pkg.version}/assets/social`;\n\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 jsDelivr mirror of this\n * package; consumers can override to self-host.\n */\n public readonly socialIconsBaseUrl: string = DEFAULT_SOCIAL_ICONS_BASE_URL,\n /**\n * Per-block-type renderer overrides from `RenderOptions.blockRenderers`.\n * Consulted before every built-in renderer — see {@link BlockRendererMap}.\n */\n public readonly blockRenderers: BlockRendererMap = {},\n ) {}\n\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 this.blockRenderers,\n );\n }\n\n /**\n * Resolve a font family name to include custom font fallbacks.\n * If the font matches a custom font, returns `'FontName', fallback`.\n * Otherwise returns the original font family string.\n */\n resolveFontFamily(fontFamily: string): string {\n // Check custom fonts first\n for (const customFont of this.customFonts) {\n if (customFont.name.toLowerCase() === fontFamily.toLowerCase()) {\n const fallback = customFont.fallback ?? this.defaultFallbackFont;\n\n return `'${customFont.name}', ${fallback}`;\n }\n }\n\n // Resolve built-in fonts to include fallback stacks\n const builtIn = BUILT_IN_FONT_FALLBACKS[fontFamily.toLowerCase()];\n if (builtIn) {\n return builtIn;\n }\n\n return fontFamily;\n }\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Check if a block is hidden on all viewports.\n */\nexport function isHiddenOnAll(block: Block): boolean {\n const visibility = block.visibility;\n\n if (!visibility) {\n return false;\n }\n\n return !visibility.desktop && !visibility.mobile;\n}\n\n/**\n * Get the MJML css-class attribute string for visibility hiding.\n * Returns a string like ` css-class=\"tpl-hide-desktop\"` 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.mobile) {\n classes.push(\"tpl-hide-mobile\");\n }\n\n return classes.join(\" \");\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Prefix every unrenderable-block marker carries. Stable and greppable on\n * purpose: it is what a consumer searches for after noticing a gap in a rendered\n * email, and what a send pipeline can scan for before shipping.\n */\nexport const UNRENDERABLE_MARKER_PREFIX = \"templatical:unrenderable-block\";\n\n/**\n * Strip anything that would terminate or nest an HTML comment. `-->` and `--!>`\n * inside the marker would close it early and leak the rest as body text; a lone\n * `--` is enough to make the comment non-conforming, so both collapse.\n *\n * Brackets go **before** hyphens collapse, and the order is load-bearing.\n * Removing `<` from `-<-` brings two hyphens together that a collapse pass has\n * already walked past, so collapsing first leaves a `--` behind — the sanitizer\n * reintroducing the sequence it exists to remove.\n */\nfunction commentSafe(value: string): string {\n return value.replace(/[<>]/g, \"\").replace(/-{2,}/g, \"-\");\n}\n\n/**\n * Emit the placeholder for a block no renderer can handle — no built-in renderer\n * and no `blockRenderers` override for its type.\n *\n * **A marker, not a throw and not silence.** The renderer runs inside send\n * pipelines, so killing an entire render over one block is worse than shipping a\n * marked gap. Silence is worse still: a block that simply vanishes reaches\n * recipients as a missing section with nothing anywhere explaining why. The rule\n * is general rather than countdown-specific, so\n * a block type added to `@templatical/types` before its renderer lands degrades\n * the same way.\n *\n * `mj-raw` is used because it is the one element that passes its content through\n * verbatim and is valid everywhere a block renders — inside `mj-column`, which is\n * where every non-section block ends up.\n */\nexport function renderUnrenderableBlock(block: Block): string {\n console.warn(\n `[Templatical] No renderer for block type \"${block.type}\" (id: ${block.id}). ` +\n \"A placeholder comment was emitted in its place. Pass a `blockRenderers` \" +\n \"entry for this type to render it.\",\n );\n\n const type = commentSafe(block.type);\n const id = commentSafe(block.id);\n\n return `<mj-raw><!-- ${UNRENDERABLE_MARKER_PREFIX} type=\"${type}\" id=\"${id}\" --></mj-raw>`;\n}\n","import { findOpenTagEnd, getTagAttrValue } from \"@templatical/types\";\n\nconst HTML_ENTITIES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\nconst HTML_ENTITY_REGEX = /[&<>\"']/g;\n\n/**\n * Escape HTML special characters (& < > \" ').\n * Equivalent to PHP htmlspecialchars with ENT_QUOTES | ENT_HTML5.\n */\nexport function escapeHtml(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use in an HTML attribute value.\n * Same implementation as escapeHtml for consistency with PHP.\n */\nexport function escapeAttr(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use as a CSS property value inside an inline\n * `style=\"prop: ${value}\"` attribute. Beyond HTML entity escaping (so the\n * value survives the attribute boundary), this strips characters that\n * could break out of the property value into a sibling property:\n *\n * `;` — separates CSS declarations\n * `{`/`}` — opens/closes a CSS rule (rejected by attribute parsers but\n * still safer to remove)\n * `\\n`/`\\r` — would smuggle past line-based CSS sanitizers\n *\n * Without this, an attacker-controlled color like\n * `\"red; background: url('//attacker/log')\"` lands as a real CSS rule.\n */\nexport function escapeCssValue(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n return escapeAttr(text).replace(/[;{}\\r\\n]/g, \"\");\n}\n\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 *\n * Uses a single-pass linear scan instead of an `[^>]*…[^>]*` regex because\n * the latter is polynomial-ReDoS over inputs that contain many `<span`\n * starts but no closing `>` — the engine retries `[^>]*` at every span\n * position. The scan below resolves each `<span>` open tag with a bounded,\n * quote-aware `findOpenTagEnd`, keeping the work strictly O(n).\n */\nexport function convertMergeTagsToValues(html: string): string {\n if (html === \"\") {\n return \"\";\n }\n\n return rewriteMergeTagSpans(\n html,\n (attrs) =>\n findAttr(attrs, \"data-merge-tag\") ??\n findAttr(attrs, \"data-logic-merge-tag\"),\n );\n}\n\n/**\n * Walk `html`, find every `<span …>…</span>`, and replace the entire span\n * with whatever `extract` returns for its attribute string (or leave it\n * alone if `extract` returns `null`). Linear in the length of `html`:\n * every `indexOf` advances the cursor monotonically.\n */\nfunction rewriteMergeTagSpans(\n html: string,\n extract: (attrs: string) => string | null,\n): string {\n let out = \"\";\n let i = 0;\n while (i < html.length) {\n const open = html.indexOf(\"<span\", i);\n if (open === -1) {\n out += html.substring(i);\n break;\n }\n // `<span` must be followed by `>` or whitespace to be a real opening tag.\n const afterTagName = html[open + 5];\n if (\n afterTagName !== \">\" &&\n afterTagName !== \" \" &&\n afterTagName !== \"\\t\" &&\n afterTagName !== \"\\n\" &&\n afterTagName !== \"\\r\" &&\n afterTagName !== \"/\"\n ) {\n out += html.substring(i, open + 5);\n i = open + 5;\n continue;\n }\n const openEnd = findOpenTagEnd(html, open + 5);\n if (openEnd === -1) {\n out += html.substring(i);\n break;\n }\n const closeStart = html.indexOf(\"</span>\", openEnd + 1);\n if (closeStart === -1) {\n out += html.substring(i);\n break;\n }\n const attrs = html.substring(open + 5, openEnd);\n const replacement = extract(attrs);\n if (replacement === null) {\n // This `<span>` isn't a merge-tag — emit up to and including the\n // `<span` literal and let the next iteration scan inward, so any\n // nested merge-tag span still gets a chance to match.\n out += html.substring(i, open + 5);\n i = open + 5;\n continue;\n }\n out += html.substring(i, open);\n out += replacement;\n i = closeStart + 7;\n }\n return out;\n}\n\n/**\n * Extract the value of `name=\"…\"` from an HTML attribute string, or `null`\n * if absent.\n *\n * Delegates to `@templatical/types` so the editor's preview rewriter and this\n * export rewriter read markup identically. They were separate regexes once, and\n * both excluded `<` / `>` from the value — which silently left the whole span\n * in the rendered MJML whenever a consumer configured a merge-tag syntax whose\n * values contain those characters (`<% $email %>`), so the ESP never received\n * the token.\n */\nfunction findAttr(attrs: string, name: string): string | null {\n return getTagAttrValue(attrs, name);\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 { 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 // Emit the color only when the block sets one; otherwise the `mj-text`\n // inherits the document `textColor` default from `<mj-attributes>`.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n const align = block.textAlign;\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n 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\"${colorAttr}\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 //\n // Use `[^<>]*` (not `[^>]*`) inside the tag to keep the regex linear:\n // `[^>]*` lets the engine consume across nested `<` chars and then\n // backtrack one char at a time when no closing `>` exists, which is\n // O(n²) over inputs like `<p<p<p<p…`. `[^<>]*` fails fast at the\n // next `<` and keeps each match attempt O(1). Real HTML attributes\n // never contain a raw `<`, so the restriction is semantics-preserving.\n const stripped = block.content.replace(/<\\/?p\\b[^<>]*>/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 widthAttr = renderWidthAttr(block.width);\n const visibilityAttr = getCssClassAttr(block);\n // Templates stored before `align` existed have no value for it. Fall back to\n // MJML's own default so they keep rendering exactly as they did.\n const align = block.align ?? \"center\";\n\n return `<mj-button${hrefAttr}${targetAttr}\n background-color=\"${backgroundColor}\"\n color=\"${textColor}\"\n font-size=\"${fontSize}px\"\n font-weight=\"bold\"\n border-radius=\"${borderRadius}px\"\n inner-padding=\"${buttonPadding}\"\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${widthAttr}${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\nfunction renderWidthAttr(width: number | \"full\" | undefined): string {\n if (width === undefined) {\n return \"\";\n }\n\n const value = width === \"full\" ? \"100%\" : `${width}px`;\n\n return ` width=\"${value}\"`;\n}\n","import type { DividerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a divider block to MJML markup.\n */\nexport function renderDivider(\n block: DividerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n 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 { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render an HTML block to MJML markup.\n * No sanitization in the OSS version -- consumers are responsible for content safety.\n */\nexport function renderHtml(block: HtmlBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (!context.allowHtmlBlocks) {\n return \"\";\n }\n\n const content = block.content;\n\n if (content === \"\") {\n return \"\";\n }\n\n const visibilityAttr = getCssClassAttr(block);\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n\n // Use mj-text to render HTML content inside proper table cell structure.\n // mj-raw bypasses MJML's table layout and content won't be visible.\n return `<mj-text padding=\"${padding}\"${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { SocialIconsBlock } from \"@templatical/types\";\nimport 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 jsDelivr\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 // Emit the color only when set; otherwise the `mj-text` inherits the\n // document `textColor` and the item anchors below inherit it in turn.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n\n const content = renderMenuItems(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${content}</mj-text>`;\n}\n\nfunction renderMenuItems(block: MenuBlock): string {\n const items = block.items;\n const separator = escapeHtml(block.separator);\n const separatorColor = escapeCssValue(block.separatorColor);\n const spacing = block.spacing;\n const linkColor = block.linkColor ?? block.color;\n\n const parts: string[] = [];\n const itemCount = items.length;\n\n for (let index = 0; index < itemCount; index++) {\n parts.push(renderMenuItem(items[index], linkColor));\n\n if (index < itemCount - 1) {\n parts.push(\n `<span style=\"color: ${separatorColor}; padding: 0 ${spacing}px;\">${separator}</span>`,\n );\n }\n }\n\n return parts.join(\"\");\n}\n\nfunction renderMenuItem(\n item: MenuItemData,\n linkColor: string | undefined,\n): string {\n const text = escapeHtml(item.text);\n const url = escapeAttr(item.url);\n // Item color, else the menu link color; when neither is set the anchor omits\n // an explicit color and inherits the mj-text color (the document `textColor`).\n const resolvedColor = item.color ?? linkColor;\n const target = item.openInNewTab ? ' target=\"_blank\" rel=\"noopener\"' : \"\";\n\n const styles: string[] = [\"text-decoration: none\"];\n\n if (resolvedColor) {\n styles.unshift(`color: ${escapeCssValue(resolvedColor)}`);\n }\n\n if (item.bold) {\n styles.push(\"font-weight: bold\");\n }\n\n if (item.underline) {\n styles.push(\"text-decoration: underline\");\n }\n\n const styleAttr = styles.join(\"; \");\n\n return `<a href=\"${url}\" style=\"${styleAttr}\"${target}>${text}</a>`;\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${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 escapeHtml,\n convertMergeTagsToValues,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a table block to MJML markup.\n * Uses mj-text wrapping an HTML <table> with styled <tr>/<td> elements.\n */\nexport function renderTable(block: TableBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.rows.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const fontSize = block.fontSize;\n // Emit the color only when set; otherwise the `mj-text` (and its cells,\n // which carry no color of their own) inherit the document `textColor`.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n const align = block.textAlign;\n\n const tableHtml = renderTableElement(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${tableHtml}</mj-text>`;\n}\n\nfunction renderTableElement(block: TableBlock): string {\n const borderColor = escapeCssValue(block.borderColor);\n const borderWidth = block.borderWidth;\n\n const tableStyle = \"width: 100%; border-collapse: collapse;\";\n\n let rowsHtml = \"\";\n\n for (let index = 0; index < block.rows.length; index++) {\n const row = block.rows[index];\n const isHeader = block.hasHeaderRow && index === 0;\n rowsHtml += renderRow(row, block, isHeader, borderColor, borderWidth);\n }\n\n return `<table style=\"${tableStyle}\">${rowsHtml}</table>`;\n}\n\nfunction renderRow(\n row: TableRowData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n let cellsHtml = \"\";\n\n for (const cell of row.cells) {\n cellsHtml += renderCell(cell, block, isHeader, borderColor, borderWidth);\n }\n\n return `<tr>${cellsHtml}</tr>`;\n}\n\nfunction renderCell(\n cell: TableCellData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n const cellPadding = block.cellPadding;\n\n const styles: string[] = [\n `border: ${borderWidth}px solid ${borderColor}`,\n `padding: ${cellPadding}px`,\n ];\n\n if (isHeader) {\n styles.push(\"font-weight: bold\");\n\n if (block.headerBackgroundColor) {\n styles.push(\n `background-color: ${escapeCssValue(block.headerBackgroundColor)}`,\n );\n }\n }\n\n const styleAttr = styles.join(\"; \");\n // Cells are a plain-text field (the editor stores innerText; TableCellData\n // carries no rich-text hint), so escape the content like menu/button. Merge\n // tags are resolved first: convertMergeTagsToValues matches a literal\n // `<span`, so escaping has to run after it — escaping first would turn the\n // span into `<span` and the `{{value}}` placeholder would never resolve.\n const content = escapeHtml(convertMergeTagsToValues(cell.content));\n\n const tag = isHeader ? \"th\" : \"td\";\n\n return `<${tag} style=\"${styleAttr}\">${content}</${tag}>`;\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${resolved}\"`;\n}\n","import type { CustomBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a custom block to MJML markup.\n *\n * Custom block HTML resolution order:\n * 1. `context.customBlockHtml` map — populated by `renderToMjml` when the\n * caller passes 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 padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n\n return `<mj-text padding=\"${padding}\"${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { ColumnLayout } from \"@templatical/types\";\n\n/**\n * Get width percentages for each column in a layout.\n */\nexport function getWidthPercentages(layout: ColumnLayout): string[] {\n switch (layout) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n // 33.33 × 3 = 99.99% — let the last column absorb the rounding gap\n // so widths sum to exactly 100% in the compiled HTML.\n return [\"33.33%\", \"33.33%\", \"33.34%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n}\n\n/**\n * Get width in pixels for each column in a layout.\n */\nexport function getWidthPixels(\n layout: ColumnLayout,\n containerWidth: number,\n): number[] {\n switch (layout) {\n case \"2\":\n return [containerWidth * 0.5, containerWidth * 0.5];\n case \"3\":\n return [containerWidth / 3, containerWidth / 3, containerWidth / 3];\n case \"1-2\":\n return [containerWidth / 3, (containerWidth * 2) / 3];\n case \"2-1\":\n return [(containerWidth * 2) / 3, containerWidth / 3];\n default:\n return [containerWidth];\n }\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Wrap rendered block markup in the block's liquid display-condition guards\n * (`<mj-raw>{% if %}</mj-raw>` … `<mj-raw>{% endif %}</mj-raw>`), if present.\n *\n * Returns the input unchanged when the block has no display condition, and an\n * empty string when the rendered markup is empty (a hidden block) so callers\n * can keep using an `=== \"\"` filter to drop it.\n *\n * Used for BOTH top-level blocks (`index.ts`) and blocks nested inside section\n * columns (`renderers/section.ts`). A condition on a nested block must emit the\n * same guards as a top-level one — otherwise conditional content placed inside\n * a multi-column section renders unconditionally for every recipient.\n */\nexport function wrapWithDisplayCondition(\n block: Block,\n rendered: string,\n): string {\n if (rendered === \"\") {\n return \"\";\n }\n\n const displayCondition = block.displayCondition;\n\n if (!displayCondition) {\n return rendered;\n }\n\n return (\n `<mj-raw>${displayCondition.before}</mj-raw>` +\n \"\\n\" +\n rendered +\n \"\\n\" +\n `<mj-raw>${displayCondition.after}</mj-raw>`\n );\n}\n","import type { Block, SectionBlock } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { BlockRenderer, RenderContext } from \"../render-context\";\nimport { getWidthPercentages, getWidthPixels } from \"../columns\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\nimport { wrapWithDisplayCondition } from \"../display-condition\";\n\n// `BlockRenderer` is defined in `../render-context`, next to the `blockRenderers`\n// override map it types. Re-exported here so the package's\n// `export type { BlockRenderer } from \"./renderers/section\"` entry point resolves\n// for consumers.\nexport type { BlockRenderer };\n\n/**\n * Render a section block with columns to MJML markup.\n */\nexport function renderSection(\n block: SectionBlock,\n context: RenderContext,\n renderBlock: BlockRenderer,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const 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 const borderRadiusAttr =\n block.borderRadius && block.borderRadius > 0\n ? ` border-radius=\"${block.borderRadius}px\"`\n : \"\";\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) =>\n wrapWithDisplayCondition(child, renderBlock(child, columnContext)),\n )\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n const content =\n columnBlocks === \"\" ? \"<mj-text> </mj-text>\" : columnBlocks;\n\n columnsContent.push(`<mj-column width=\"${width}\">\n${content}\n</mj-column>`);\n }\n\n const columns = columnsContent.join(\"\\n\");\n\n // Opting out of mobile stacking wraps the columns in an `<mj-group>`, which\n // keeps them side-by-side below 480px instead of stacking (MJML's default).\n // `mj-group` requires percentage column widths — `getWidthPercentages`\n // already guarantees that. Only meaningful with 2+ columns.\n const body =\n block.stackOnMobile === false && columnsContent.length > 1\n ? `<mj-group>\n${columns}\n</mj-group>`\n : columns;\n\n return `<mj-section${bgColor} padding=\"${padding}\"${borderRadiusAttr}${visibilityAttr}>\n${body}\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 { isHiddenOnAll } from \"../visibility\";\nimport { renderUnrenderableBlock } from \"../unrenderable\";\nimport { renderTitle } from \"./title\";\nimport { renderParagraph } from \"./paragraph\";\nimport { renderImage } from \"./image\";\nimport { renderButton } from \"./button\";\nimport { renderDivider } from \"./divider\";\nimport { renderSpacer } from \"./spacer\";\nimport { renderHtml } from \"./html\";\nimport { renderSocialIcons } from \"./social\";\nimport { renderMenu } from \"./menu\";\nimport { renderTable } from \"./table\";\nimport { renderCustom } from \"./custom\";\nimport { renderSection } from \"./section\";\nimport { renderVideo } from \"./video\";\n\n/**\n * Render a single block to MJML markup.\n * Dispatches to the appropriate block-type renderer.\n *\n * A `blockRenderers` override for the block's type wins over the built-in\n * renderer — see {@link RenderContext.blockRenderers}. A type with neither gets a\n * placeholder marker plus a warning rather than disappearing.\n */\nexport function renderBlock(block: Block, context: RenderContext): string {\n // Checked first so an override replaces the built-in wholesale rather than\n // running after it. The override then owns everything the built-in did,\n // including the hidden-on-all-viewports early return.\n const override = context.blockRenderers[block.type];\n if (override) {\n return override(block, context);\n }\n\n if (isSection(block)) {\n return renderSection(block, context, renderBlock);\n }\n\n if (isTitle(block)) {\n return renderTitle(block, context);\n }\n\n if (isParagraph(block)) {\n return renderParagraph(block, context);\n }\n\n if (isImage(block)) {\n return renderImage(block, context);\n }\n\n if (isButton(block)) {\n return renderButton(block, context);\n }\n\n if (isDivider(block)) {\n return renderDivider(block, context);\n }\n\n if (isSpacer(block)) {\n return renderSpacer(block, context);\n }\n\n if (isHtml(block)) {\n return renderHtml(block, context);\n }\n\n if (isSocialIcons(block)) {\n return renderSocialIcons(block, context);\n }\n\n if (isMenu(block)) {\n return renderMenu(block, context);\n }\n\n if (isTable(block)) {\n return renderTable(block, context);\n }\n\n if (isVideo(block)) {\n return renderVideo(block, context);\n }\n\n if (isCustomBlock(block)) {\n return renderCustom(block, context);\n }\n\n // Nothing here knows how to render this type. A block the author hid on every\n // viewport is meant to produce nothing, so it still does — every built-in\n // renderer above makes the same call, and warning about a deliberate hide would\n // be noise. Everything else gets a marker: `countdown` today (Cloud renders it\n // server-side as an animated GIF, which no browser can do), and any future type\n // whose renderer hasn't landed yet.\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n return renderUnrenderableBlock(block);\n}\n","import type {\n Block,\n CustomBlock,\n TemplateContent,\n CustomFont,\n SectionWrapper,\n} from \"@templatical/types\";\nimport { isSection, isCustomBlock } from \"@templatical/types\";\nimport { RenderContext, DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nimport type { BlockRendererMap } from \"./render-context\";\nimport { renderBlock } from \"./renderers\";\nimport { escapeHtml, escapeAttr, escapeCssValue } from \"./escape\";\nimport { wrapWithDisplayCondition } from \"./display-condition\";\nimport { bgAttr } from \"./utils\";\nimport { toPaddingString } from \"./padding\";\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 * Per-block-type renderer overrides, keyed by `block.type`. An entry replaces\n * the built-in renderer for that type wholesale — including its\n * hidden-on-all-viewports check, which becomes the override's responsibility.\n *\n * ```ts\n * renderToMjml(content, {\n * blockRenderers: {\n * countdown: (block) => `<mj-image src=\"${countdownGifUrl(block)}\" />`,\n * video: (block, ctx) => renderVideoWithPlayButton(block, ctx),\n * },\n * });\n * ```\n *\n * This generalises {@link renderCustomBlock}, which is the same idea for a\n * single block type. It exists so a backend whose render output is a *superset*\n * of the browser's — a server-generated countdown GIF, a composited video\n * thumbnail — can inject exactly that delta instead of forking the renderer:\n * parity for every other block type then holds by construction.\n *\n * A block type with neither a built-in renderer nor an override emits an\n * `mj-raw` placeholder comment and warns, rather than vanishing from the\n * output.\n */\n blockRenderers?: BlockRendererMap;\n /**\n * Resolves the definition-level CSS for a custom block type. Called once\n * per unique `customType` present in the content tree (not per instance).\n * The non-empty results are deduped by content and emitted as additional\n * `<mj-style>` blocks inside `<mj-head>` alongside the built-in visibility\n * media queries.\n *\n * Editor consumers: pass a function that reads\n * `blockRegistry.getDefinition(customType)?.stylesheet`.\n *\n * Headless consumers: provide your own resolver, typically from the same\n * definitions map used by `renderCustomBlock`. Return `undefined` or `null`\n * for definitions without a stylesheet — those are skipped.\n */\n getCustomBlockStylesheet?: (customType: string) => string | undefined | null;\n /**\n * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png` per icon. Defaults to the\n * version-pinned jsDelivr mirror of this package. Override to self-host\n * (e.g., behind your own CDN or for air-gapped environments).\n *\n * Why PNGs: Outlook desktop (Word rendering engine) does not support SVG\n * and rejects base64 data URIs in `<img src>`, so social icons must be\n * served as raster images over HTTP for cross-client compatibility.\n */\n socialIconsBaseUrl?: string;\n}\n\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 customBlockStylesheets = collectCustomBlockStylesheets(\n content,\n options?.getCustomBlockStylesheet,\n );\n\n const renderContext = new RenderContext(\n content.settings.width,\n customFonts,\n defaultFallbackFont,\n allowHtmlBlocks,\n customBlockHtml,\n socialIconsBaseUrl,\n options?.blockRenderers ?? {},\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 // Document-level default text color → the `<mj-text>` default below. Blocks\n // that set their own color (e.g. Title) or carry inline text-color marks\n // override it. Omitted when unset so existing templates render identically.\n const textColorAttr = content.settings.textColor\n ? ` color=\"${escapeAttr(content.settings.textColor)}\"`\n : \"\";\n // Document-level link styling → the global `a { … }` rule below, cascading to\n // every link (rich-text and menu). An unset `linkColor` keeps `color: inherit`\n // so links follow the text color and legacy content renders identically; a\n // per-link/per-item color still overrides (inline styles beat this rule).\n const linkColor = content.settings.linkColor\n ? escapeCssValue(content.settings.linkColor)\n : \"inherit\";\n const linkDecoration = content.settings.linkUnderline ? \"underline\" : \"none\";\n\n const bodyContent = blocks\n .map((block) => renderTopLevelBlock(block, renderContext))\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n const fontDeclarations = generateFontDeclarations(customFonts);\n const previewTag = generatePreviewTag(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\"${textColorAttr} />\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: ${linkColor}; text-decoration: ${linkDecoration}; }\n @media only screen and (max-width: 480px) {\n .tpl-hide-mobile { display: none !important; mso-hide: all !important; }\n }\n @media only screen and (min-width: 481px) {\n .tpl-hide-desktop { display: none !important; mso-hide: all !important; }\n }\n </mj-style>${renderCustomBlockStylesheets(customBlockStylesheets)}\n </mj-head>\n <mj-body width=\"${renderContext.containerWidth}px\" background-color=\"${backgroundColor}\">\n${bodyContent}\n </mj-body>\n</mjml>`;\n}\n\n/**\n * Render a top-level block. 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 // An empty render (hidden section) stays empty — never emit a bare wrapper.\n const framed =\n block.wrapper && rendered !== \"\"\n ? renderSectionWrapper(rendered, block.wrapper)\n : rendered;\n return wrapWithDisplayCondition(block, framed);\n }\n\n const content = renderBlock(block, context);\n const wrapped = wrapInSection(content);\n return wrapWithDisplayCondition(block, wrapped);\n}\n\n/**\n * Wrap a rendered section's `mj-section` in an `mj-wrapper` — a full-width band\n * (its own background + padding + optional radius) that frames the section.\n * `mj-wrapper` is the only MJML element that may contain `mj-section`.\n */\nfunction renderSectionWrapper(inner: string, wrapper: SectionWrapper): string {\n const bg = bgAttr(wrapper.backgroundColor, \"native\");\n const padding = ` padding=\"${\n wrapper.padding ? toPaddingString(wrapper.padding) : \"0\"\n }\"`;\n const radius =\n wrapper.borderRadius && wrapper.borderRadius > 0\n ? ` border-radius=\"${wrapper.borderRadius}px\"`\n : \"\";\n return `<mj-wrapper${bg}${padding}${radius}>\n${inner}\n</mj-wrapper>`;\n}\n\n/**\n * Wrap block content in a default mj-section/mj-column for non-section blocks.\n */\nfunction wrapInSection(content: string): 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/**\n * Walk the content tree, find every unique `customType`, ask the consumer's\n * resolver for that definition's stylesheet, and return the non-empty,\n * content-deduped set in insertion order.\n *\n * Content-level dedupe (not just by customType) means two definitions that\n * happen to ship the same stylesheet string emit it only once — cheap and\n * matches the \"one rule, emitted once\" mental model. Whitespace-only and\n * empty stylesheets are skipped.\n */\nfunction collectCustomBlockStylesheets(\n content: TemplateContent,\n resolver: RenderOptions[\"getCustomBlockStylesheet\"],\n): string[] {\n if (!resolver) {\n return [];\n }\n\n const customBlocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, customBlocks);\n\n if (customBlocks.length === 0) {\n return [];\n }\n\n const seenTypes = new Set<string>();\n const seenContent = new Set<string>();\n const stylesheets: string[] = [];\n\n for (const block of customBlocks) {\n if (seenTypes.has(block.customType)) {\n continue;\n }\n seenTypes.add(block.customType);\n\n const css = resolver(block.customType);\n if (!css) {\n continue;\n }\n\n const trimmed = css.trim();\n if (trimmed === \"\" || seenContent.has(trimmed)) {\n continue;\n }\n seenContent.add(trimmed);\n stylesheets.push(trimmed);\n }\n\n return stylesheets;\n}\n\nfunction renderCustomBlockStylesheets(stylesheets: string[]): string {\n if (stylesheets.length === 0) {\n return \"\";\n }\n\n // One `<mj-style>` per unique stylesheet so per-definition CSS can be\n // independently inspected in the rendered MJML, and authors can grep their\n // contribution without disentangling a merged blob.\n return stylesheets\n .map((css) => `\\n <mj-style>\\n${css}\\n </mj-style>`)\n .join(\"\");\n}\n\n// Re-export utilities for consumers\nexport { RenderContext } from \"./render-context\";\nexport { escapeHtml, escapeAttr, convertMergeTagsToValues } from \"./escape\";\nexport { isHiddenOnAll, getCssClassAttr, getCssClasses } from \"./visibility\";\nexport { getWidthPercentages, getWidthPixels } from \"./columns\";\nexport { toPaddingString } from \"./padding\";\nexport { DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nexport { renderBlock } from \"./renderers\";\nexport {\n renderUnrenderableBlock,\n UNRENDERABLE_MARKER_PREFIX,\n} from \"./unrenderable\";\nexport type { BlockRenderer, BlockRendererMap } from \"./render-context\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"src-BAWlZu4B.js","names":[],"sources":["../../../../renderer/package.json","../../../../renderer/src/render-context.ts","../../../../renderer/src/visibility.ts","../../../../renderer/src/unrenderable.ts","../../../../renderer/src/escape.ts","../../../../renderer/src/padding.ts","../../../../renderer/src/utils.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/display-condition.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.27.4\",\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.4.0\",\n \"typescript\": \"^6.0.3\",\n \"vitest\": \"^4.1.11\"\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\": \"tsdown && 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 { Block, BlockType, CustomFont } from \"@templatical/types\";\nimport pkg from \"../package.json\" with { type: \"json\" };\n\n/**\n * A function type that renders a single block to MJML markup.\n */\nexport type BlockRenderer = (block: Block, context: RenderContext) => string;\n\n/**\n * Per-block-type renderer overrides, keyed by `block.type`. An entry replaces the\n * built-in renderer for that type wholesale — including its hidden-on-all-\n * viewports check, which becomes the override's responsibility.\n *\n * Generalises `renderCustomBlock`, which is the same idea for one block type. Its\n * reason to exist is that Templatical Cloud's render output is a deliberate\n * *superset*: countdown blocks resolve to a server-generated animated GIF and\n * video thumbnails get a composited play button, neither of which a browser can\n * do at render time. With this hook Cloud's renderer is the published one plus\n * two injected functions, so parity for every other block type holds by\n * construction instead of by review.\n */\nexport type BlockRendererMap = Partial<Record<BlockType, BlockRenderer>>;\n\nexport const DEFAULT_SOCIAL_ICONS_BASE_URL = `https://cdn.jsdelivr.net/npm/@templatical/renderer@${pkg.version}/assets/social`;\n\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 jsDelivr mirror of this\n * package; consumers can override to self-host.\n */\n public readonly socialIconsBaseUrl: string = DEFAULT_SOCIAL_ICONS_BASE_URL,\n /**\n * Per-block-type renderer overrides from `RenderOptions.blockRenderers`.\n * Consulted before every built-in renderer — see {@link BlockRendererMap}.\n */\n public readonly blockRenderers: BlockRendererMap = {},\n ) {}\n\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 this.blockRenderers,\n );\n }\n\n /**\n * Resolve a font family name to include custom font fallbacks.\n * If the font matches a custom font, returns `'FontName', fallback`.\n * Otherwise returns the original font family string.\n */\n resolveFontFamily(fontFamily: string): string {\n // Check custom fonts first\n for (const customFont of this.customFonts) {\n if (customFont.name.toLowerCase() === fontFamily.toLowerCase()) {\n const fallback = customFont.fallback ?? this.defaultFallbackFont;\n\n return `'${customFont.name}', ${fallback}`;\n }\n }\n\n // Resolve built-in fonts to include fallback stacks\n const builtIn = BUILT_IN_FONT_FALLBACKS[fontFamily.toLowerCase()];\n if (builtIn) {\n return builtIn;\n }\n\n return fontFamily;\n }\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Check if a block is hidden on all viewports.\n */\nexport function isHiddenOnAll(block: Block): boolean {\n const visibility = block.visibility;\n\n if (!visibility) {\n return false;\n }\n\n return !visibility.desktop && !visibility.mobile;\n}\n\n/**\n * Get the MJML css-class attribute string for visibility hiding.\n * Returns a string like ` css-class=\"tpl-hide-desktop\"` 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.mobile) {\n classes.push(\"tpl-hide-mobile\");\n }\n\n return classes.join(\" \");\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Prefix every unrenderable-block marker carries. Stable and greppable on\n * purpose: it is what a consumer searches for after noticing a gap in a rendered\n * email, and what a send pipeline can scan for before shipping.\n */\nexport const UNRENDERABLE_MARKER_PREFIX = \"templatical:unrenderable-block\";\n\n/**\n * Strip anything that would terminate or nest an HTML comment. `-->` and `--!>`\n * inside the marker would close it early and leak the rest as body text; a lone\n * `--` is enough to make the comment non-conforming, so both collapse.\n *\n * Brackets go **before** hyphens collapse, and the order is load-bearing.\n * Removing `<` from `-<-` brings two hyphens together that a collapse pass has\n * already walked past, so collapsing first leaves a `--` behind — the sanitizer\n * reintroducing the sequence it exists to remove.\n */\nfunction commentSafe(value: string): string {\n return value.replace(/[<>]/g, \"\").replace(/-{2,}/g, \"-\");\n}\n\n/**\n * Emit the placeholder for a block no renderer can handle — no built-in renderer\n * and no `blockRenderers` override for its type.\n *\n * **A marker, not a throw and not silence.** The renderer runs inside send\n * pipelines, so killing an entire render over one block is worse than shipping a\n * marked gap. Silence is worse still: a block that simply vanishes reaches\n * recipients as a missing section with nothing anywhere explaining why. The rule\n * is general rather than countdown-specific, so\n * a block type added to `@templatical/types` before its renderer lands degrades\n * the same way.\n *\n * `mj-raw` is used because it is the one element that passes its content through\n * verbatim and is valid everywhere a block renders — inside `mj-column`, which is\n * where every non-section block ends up.\n */\nexport function renderUnrenderableBlock(block: Block): string {\n console.warn(\n `[Templatical] No renderer for block type \"${block.type}\" (id: ${block.id}). ` +\n \"A placeholder comment was emitted in its place. Pass a `blockRenderers` \" +\n \"entry for this type to render it.\",\n );\n\n const type = commentSafe(block.type);\n const id = commentSafe(block.id);\n\n return `<mj-raw><!-- ${UNRENDERABLE_MARKER_PREFIX} type=\"${type}\" id=\"${id}\" --></mj-raw>`;\n}\n","import { findOpenTagEnd, getTagAttrValue } from \"@templatical/types\";\n\nconst HTML_ENTITIES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\nconst HTML_ENTITY_REGEX = /[&<>\"']/g;\n\n/**\n * Escape HTML special characters (& < > \" ').\n * Equivalent to PHP htmlspecialchars with ENT_QUOTES | ENT_HTML5.\n */\nexport function escapeHtml(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use in an HTML attribute value.\n * Same implementation as escapeHtml for consistency with PHP.\n */\nexport function escapeAttr(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n\n return text.replace(HTML_ENTITY_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Escape a string for use as a CSS property value inside an inline\n * `style=\"prop: ${value}\"` attribute. Beyond HTML entity escaping (so the\n * value survives the attribute boundary), this strips characters that\n * could break out of the property value into a sibling property:\n *\n * `;` — separates CSS declarations\n * `{`/`}` — opens/closes a CSS rule (rejected by attribute parsers but\n * still safer to remove)\n * `\\n`/`\\r` — would smuggle past line-based CSS sanitizers\n *\n * Without this, an attacker-controlled color like\n * `\"red; background: url('//attacker/log')\"` lands as a real CSS rule.\n */\nexport function escapeCssValue(text: string): string {\n if (text === \"\") {\n return \"\";\n }\n return escapeAttr(text).replace(/[;{}\\r\\n]/g, \"\");\n}\n\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 *\n * Uses a single-pass linear scan instead of an `[^>]*…[^>]*` regex because\n * the latter is polynomial-ReDoS over inputs that contain many `<span`\n * starts but no closing `>` — the engine retries `[^>]*` at every span\n * position. The scan below resolves each `<span>` open tag with a bounded,\n * quote-aware `findOpenTagEnd`, keeping the work strictly O(n).\n */\nexport function convertMergeTagsToValues(html: string): string {\n if (html === \"\") {\n return \"\";\n }\n\n return rewriteMergeTagSpans(\n html,\n (attrs) =>\n findAttr(attrs, \"data-merge-tag\") ??\n findAttr(attrs, \"data-logic-merge-tag\"),\n );\n}\n\n/**\n * Walk `html`, find every `<span …>…</span>`, and replace the entire span\n * with whatever `extract` returns for its attribute string (or leave it\n * alone if `extract` returns `null`). Linear in the length of `html`:\n * every `indexOf` advances the cursor monotonically.\n */\nfunction rewriteMergeTagSpans(\n html: string,\n extract: (attrs: string) => string | null,\n): string {\n let out = \"\";\n let i = 0;\n while (i < html.length) {\n const open = html.indexOf(\"<span\", i);\n if (open === -1) {\n out += html.substring(i);\n break;\n }\n // `<span` must be followed by `>` or whitespace to be a real opening tag.\n const afterTagName = html[open + 5];\n if (\n afterTagName !== \">\" &&\n afterTagName !== \" \" &&\n afterTagName !== \"\\t\" &&\n afterTagName !== \"\\n\" &&\n afterTagName !== \"\\r\" &&\n afterTagName !== \"/\"\n ) {\n out += html.substring(i, open + 5);\n i = open + 5;\n continue;\n }\n const openEnd = findOpenTagEnd(html, open + 5);\n if (openEnd === -1) {\n out += html.substring(i);\n break;\n }\n const closeStart = html.indexOf(\"</span>\", openEnd + 1);\n if (closeStart === -1) {\n out += html.substring(i);\n break;\n }\n const attrs = html.substring(open + 5, openEnd);\n const replacement = extract(attrs);\n if (replacement === null) {\n // This `<span>` isn't a merge-tag — emit up to and including the\n // `<span` literal and let the next iteration scan inward, so any\n // nested merge-tag span still gets a chance to match.\n out += html.substring(i, open + 5);\n i = open + 5;\n continue;\n }\n out += html.substring(i, open);\n out += replacement;\n i = closeStart + 7;\n }\n return out;\n}\n\n/**\n * Extract the value of `name=\"…\"` from an HTML attribute string, or `null`\n * if absent.\n *\n * Delegates to `@templatical/types` so the editor's preview rewriter and this\n * export rewriter read markup identically. They were separate regexes once, and\n * both excluded `<` / `>` from the value — which silently left the whole span\n * in the rendered MJML whenever a consumer configured a merge-tag syntax whose\n * values contain those characters (`<% $email %>`), so the ESP never received\n * the token.\n */\nfunction findAttr(attrs: string, name: string): string | null {\n return getTagAttrValue(attrs, name);\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 { 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 // Emit the color only when the block sets one; otherwise the `mj-text`\n // inherits the document `textColor` default from `<mj-attributes>`.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n const align = block.textAlign;\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n 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\"${colorAttr}\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 //\n // Use `[^<>]*` (not `[^>]*`) inside the tag to keep the regex linear:\n // `[^>]*` lets the engine consume across nested `<` chars and then\n // backtrack one char at a time when no closing `>` exists, which is\n // O(n²) over inputs like `<p<p<p<p…`. `[^<>]*` fails fast at the\n // next `<` and keeps each match attempt O(1). Real HTML attributes\n // never contain a raw `<`, so the restriction is semantics-preserving.\n const stripped = block.content.replace(/<\\/?p\\b[^<>]*>/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 widthAttr = renderWidthAttr(block.width);\n const visibilityAttr = getCssClassAttr(block);\n // Templates stored before `align` existed have no value for it. Fall back to\n // MJML's own default so they keep rendering exactly as they did.\n const align = block.align ?? \"center\";\n\n return `<mj-button${hrefAttr}${targetAttr}\n background-color=\"${backgroundColor}\"\n color=\"${textColor}\"\n font-size=\"${fontSize}px\"\n font-weight=\"bold\"\n border-radius=\"${borderRadius}px\"\n inner-padding=\"${buttonPadding}\"\n align=\"${align}\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${widthAttr}${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\nfunction renderWidthAttr(width: number | \"full\" | undefined): string {\n if (width === undefined) {\n return \"\";\n }\n\n const value = width === \"full\" ? \"100%\" : `${width}px`;\n\n return ` width=\"${value}\"`;\n}\n","import type { DividerBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { escapeAttr } from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a divider block to MJML markup.\n */\nexport function renderDivider(\n block: DividerBlock,\n _context: RenderContext,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = block.styles.backgroundColor\n ? ` container-background-color=\"${escapeAttr(block.styles.backgroundColor)}\"`\n : \"\";\n 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 { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render an HTML block to MJML markup.\n * No sanitization in the OSS version -- consumers are responsible for content safety.\n */\nexport function renderHtml(block: HtmlBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (!context.allowHtmlBlocks) {\n return \"\";\n }\n\n const content = block.content;\n\n if (content === \"\") {\n return \"\";\n }\n\n const visibilityAttr = getCssClassAttr(block);\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n\n // Use mj-text to render HTML content inside proper table cell structure.\n // mj-raw bypasses MJML's table layout and content won't be visible.\n return `<mj-text padding=\"${padding}\"${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { SocialIconsBlock } from \"@templatical/types\";\nimport 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 jsDelivr\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 // Emit the color only when set; otherwise the `mj-text` inherits the\n // document `textColor` and the item anchors below inherit it in turn.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n\n const content = renderMenuItems(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${content}</mj-text>`;\n}\n\nfunction renderMenuItems(block: MenuBlock): string {\n const items = block.items;\n const separator = escapeHtml(block.separator);\n const separatorColor = escapeCssValue(block.separatorColor);\n const spacing = block.spacing;\n const linkColor = block.linkColor ?? block.color;\n\n const parts: string[] = [];\n const itemCount = items.length;\n\n for (let index = 0; index < itemCount; index++) {\n parts.push(renderMenuItem(items[index], linkColor));\n\n if (index < itemCount - 1) {\n parts.push(\n `<span style=\"color: ${separatorColor}; padding: 0 ${spacing}px;\">${separator}</span>`,\n );\n }\n }\n\n return parts.join(\"\");\n}\n\nfunction renderMenuItem(\n item: MenuItemData,\n linkColor: string | undefined,\n): string {\n const text = escapeHtml(item.text);\n const url = escapeAttr(item.url);\n // Item color, else the menu link color; when neither is set the anchor omits\n // an explicit color and inherits the mj-text color (the document `textColor`).\n const resolvedColor = item.color ?? linkColor;\n const target = item.openInNewTab ? ' target=\"_blank\" rel=\"noopener\"' : \"\";\n\n const styles: string[] = [\"text-decoration: none\"];\n\n if (resolvedColor) {\n styles.unshift(`color: ${escapeCssValue(resolvedColor)}`);\n }\n\n if (item.bold) {\n styles.push(\"font-weight: bold\");\n }\n\n if (item.underline) {\n styles.push(\"text-decoration: underline\");\n }\n\n const styleAttr = styles.join(\"; \");\n\n return `<a href=\"${url}\" style=\"${styleAttr}\"${target}>${text}</a>`;\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${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 escapeHtml,\n convertMergeTagsToValues,\n} from \"../escape\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a table block to MJML markup.\n * Uses mj-text wrapping an HTML <table> with styled <tr>/<td> elements.\n */\nexport function renderTable(block: TableBlock, context: RenderContext): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n if (block.rows.length === 0) {\n return \"\";\n }\n\n const padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n const visibilityAttr = getCssClassAttr(block);\n const fontFamilyAttr = renderFontFamilyAttr(block.fontFamily, context);\n const fontSize = block.fontSize;\n // Emit the color only when set; otherwise the `mj-text` (and its cells,\n // which carry no color of their own) inherit the document `textColor`.\n const colorAttr = block.color ? `\\n color=\"${escapeAttr(block.color)}\"` : \"\";\n const align = block.textAlign;\n\n const tableHtml = renderTableElement(block);\n\n return `<mj-text\n font-size=\"${fontSize}px\"${colorAttr}\n align=\"${align}\"\n line-height=\"1.5\"\n padding=\"${padding}\"${bgColor}${fontFamilyAttr}${visibilityAttr}\n>${tableHtml}</mj-text>`;\n}\n\nfunction renderTableElement(block: TableBlock): string {\n const borderColor = escapeCssValue(block.borderColor);\n const borderWidth = block.borderWidth;\n\n const tableStyle = \"width: 100%; border-collapse: collapse;\";\n\n let rowsHtml = \"\";\n\n for (let index = 0; index < block.rows.length; index++) {\n const row = block.rows[index];\n const isHeader = block.hasHeaderRow && index === 0;\n rowsHtml += renderRow(row, block, isHeader, borderColor, borderWidth);\n }\n\n return `<table style=\"${tableStyle}\">${rowsHtml}</table>`;\n}\n\nfunction renderRow(\n row: TableRowData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n let cellsHtml = \"\";\n\n for (const cell of row.cells) {\n cellsHtml += renderCell(cell, block, isHeader, borderColor, borderWidth);\n }\n\n return `<tr>${cellsHtml}</tr>`;\n}\n\nfunction renderCell(\n cell: TableCellData,\n block: TableBlock,\n isHeader: boolean,\n borderColor: string,\n borderWidth: number,\n): string {\n const cellPadding = block.cellPadding;\n\n const styles: string[] = [\n `border: ${borderWidth}px solid ${borderColor}`,\n `padding: ${cellPadding}px`,\n ];\n\n if (isHeader) {\n styles.push(\"font-weight: bold\");\n\n if (block.headerBackgroundColor) {\n styles.push(\n `background-color: ${escapeCssValue(block.headerBackgroundColor)}`,\n );\n }\n }\n\n const styleAttr = styles.join(\"; \");\n // Cells are a plain-text field (the editor stores innerText; TableCellData\n // carries no rich-text hint), so escape the content like menu/button. Merge\n // tags are resolved first: convertMergeTagsToValues matches a literal\n // `<span`, so escaping has to run after it — escaping first would turn the\n // span into `<span` and the `{{value}}` placeholder would never resolve.\n const content = escapeHtml(convertMergeTagsToValues(cell.content));\n\n const tag = isHeader ? \"th\" : \"td\";\n\n return `<${tag} style=\"${styleAttr}\">${content}</${tag}>`;\n}\n\nfunction renderFontFamilyAttr(\n fontFamily: string | undefined,\n context: RenderContext,\n): string {\n if (!fontFamily) {\n return \"\";\n }\n\n const resolved = context.resolveFontFamily(fontFamily);\n\n return ` font-family=\"${resolved}\"`;\n}\n","import type { CustomBlock } from \"@templatical/types\";\nimport type { RenderContext } from \"../render-context\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\n\n/**\n * Render a custom block to MJML markup.\n *\n * Custom block HTML resolution order:\n * 1. `context.customBlockHtml` map — populated by `renderToMjml` when the\n * caller passes 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 padding = toPaddingString(block.styles.padding);\n const bgColor = bgAttr(block.styles.backgroundColor, \"container\");\n\n return `<mj-text padding=\"${padding}\"${bgColor}${visibilityAttr}>\n${content}\n</mj-text>`;\n}\n","import type { ColumnLayout } from \"@templatical/types\";\n\n/**\n * Get width percentages for each column in a layout.\n */\nexport function getWidthPercentages(layout: ColumnLayout): string[] {\n switch (layout) {\n case \"2\":\n return [\"50%\", \"50%\"];\n case \"3\":\n // 33.33 × 3 = 99.99% — let the last column absorb the rounding gap\n // so widths sum to exactly 100% in the compiled HTML.\n return [\"33.33%\", \"33.33%\", \"33.34%\"];\n case \"1-2\":\n return [\"33.33%\", \"66.67%\"];\n case \"2-1\":\n return [\"66.67%\", \"33.33%\"];\n default:\n return [\"100%\"];\n }\n}\n\n/**\n * Get width in pixels for each column in a layout.\n */\nexport function getWidthPixels(\n layout: ColumnLayout,\n containerWidth: number,\n): number[] {\n switch (layout) {\n case \"2\":\n return [containerWidth * 0.5, containerWidth * 0.5];\n case \"3\":\n return [containerWidth / 3, containerWidth / 3, containerWidth / 3];\n case \"1-2\":\n return [containerWidth / 3, (containerWidth * 2) / 3];\n case \"2-1\":\n return [(containerWidth * 2) / 3, containerWidth / 3];\n default:\n return [containerWidth];\n }\n}\n","import type { Block } from \"@templatical/types\";\n\n/**\n * Wrap rendered block markup in the block's liquid display-condition guards\n * (`<mj-raw>{% if %}</mj-raw>` … `<mj-raw>{% endif %}</mj-raw>`), if present.\n *\n * Returns the input unchanged when the block has no display condition, and an\n * empty string when the rendered markup is empty (a hidden block) so callers\n * can keep using an `=== \"\"` filter to drop it.\n *\n * Used for BOTH top-level blocks (`index.ts`) and blocks nested inside section\n * columns (`renderers/section.ts`). A condition on a nested block must emit the\n * same guards as a top-level one — otherwise conditional content placed inside\n * a multi-column section renders unconditionally for every recipient.\n */\nexport function wrapWithDisplayCondition(\n block: Block,\n rendered: string,\n): string {\n if (rendered === \"\") {\n return \"\";\n }\n\n const displayCondition = block.displayCondition;\n\n if (!displayCondition) {\n return rendered;\n }\n\n return (\n `<mj-raw>${displayCondition.before}</mj-raw>` +\n \"\\n\" +\n rendered +\n \"\\n\" +\n `<mj-raw>${displayCondition.after}</mj-raw>`\n );\n}\n","import type { Block, SectionBlock } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { BlockRenderer, RenderContext } from \"../render-context\";\nimport { getWidthPercentages, getWidthPixels } from \"../columns\";\nimport { toPaddingString } from \"../padding\";\nimport { bgAttr } from \"../utils\";\nimport { isHiddenOnAll, getCssClassAttr } from \"../visibility\";\nimport { wrapWithDisplayCondition } from \"../display-condition\";\n\n// `BlockRenderer` is defined in `../render-context`, next to the `blockRenderers`\n// override map it types. Re-exported here so the package's\n// `export type { BlockRenderer } from \"./renderers/section\"` entry point resolves\n// for consumers.\nexport type { BlockRenderer };\n\n/**\n * Render a section block with columns to MJML markup.\n */\nexport function renderSection(\n block: SectionBlock,\n context: RenderContext,\n renderBlock: BlockRenderer,\n): string {\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n const 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 const borderRadiusAttr =\n block.borderRadius && block.borderRadius > 0\n ? ` border-radius=\"${block.borderRadius}px\"`\n : \"\";\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) =>\n wrapWithDisplayCondition(child, renderBlock(child, columnContext)),\n )\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n const content =\n columnBlocks === \"\" ? \"<mj-text> </mj-text>\" : columnBlocks;\n\n columnsContent.push(`<mj-column width=\"${width}\">\n${content}\n</mj-column>`);\n }\n\n const columns = columnsContent.join(\"\\n\");\n\n // Opting out of mobile stacking wraps the columns in an `<mj-group>`, which\n // keeps them side-by-side below 480px instead of stacking (MJML's default).\n // `mj-group` requires percentage column widths — `getWidthPercentages`\n // already guarantees that. Only meaningful with 2+ columns.\n const body =\n block.stackOnMobile === false && columnsContent.length > 1\n ? `<mj-group>\n${columns}\n</mj-group>`\n : columns;\n\n return `<mj-section${bgColor} padding=\"${padding}\"${borderRadiusAttr}${visibilityAttr}>\n${body}\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 { isHiddenOnAll } from \"../visibility\";\nimport { renderUnrenderableBlock } from \"../unrenderable\";\nimport { renderTitle } from \"./title\";\nimport { renderParagraph } from \"./paragraph\";\nimport { renderImage } from \"./image\";\nimport { renderButton } from \"./button\";\nimport { renderDivider } from \"./divider\";\nimport { renderSpacer } from \"./spacer\";\nimport { renderHtml } from \"./html\";\nimport { renderSocialIcons } from \"./social\";\nimport { renderMenu } from \"./menu\";\nimport { renderTable } from \"./table\";\nimport { renderCustom } from \"./custom\";\nimport { renderSection } from \"./section\";\nimport { renderVideo } from \"./video\";\n\n/**\n * Render a single block to MJML markup.\n * Dispatches to the appropriate block-type renderer.\n *\n * A `blockRenderers` override for the block's type wins over the built-in\n * renderer — see {@link RenderContext.blockRenderers}. A type with neither gets a\n * placeholder marker plus a warning rather than disappearing.\n */\nexport function renderBlock(block: Block, context: RenderContext): string {\n // Checked first so an override replaces the built-in wholesale rather than\n // running after it. The override then owns everything the built-in did,\n // including the hidden-on-all-viewports early return.\n const override = context.blockRenderers[block.type];\n if (override) {\n return override(block, context);\n }\n\n if (isSection(block)) {\n return renderSection(block, context, renderBlock);\n }\n\n if (isTitle(block)) {\n return renderTitle(block, context);\n }\n\n if (isParagraph(block)) {\n return renderParagraph(block, context);\n }\n\n if (isImage(block)) {\n return renderImage(block, context);\n }\n\n if (isButton(block)) {\n return renderButton(block, context);\n }\n\n if (isDivider(block)) {\n return renderDivider(block, context);\n }\n\n if (isSpacer(block)) {\n return renderSpacer(block, context);\n }\n\n if (isHtml(block)) {\n return renderHtml(block, context);\n }\n\n if (isSocialIcons(block)) {\n return renderSocialIcons(block, context);\n }\n\n if (isMenu(block)) {\n return renderMenu(block, context);\n }\n\n if (isTable(block)) {\n return renderTable(block, context);\n }\n\n if (isVideo(block)) {\n return renderVideo(block, context);\n }\n\n if (isCustomBlock(block)) {\n return renderCustom(block, context);\n }\n\n // Nothing here knows how to render this type. A block the author hid on every\n // viewport is meant to produce nothing, so it still does — every built-in\n // renderer above makes the same call, and warning about a deliberate hide would\n // be noise. Everything else gets a marker: `countdown` today (Cloud renders it\n // server-side as an animated GIF, which no browser can do), and any future type\n // whose renderer hasn't landed yet.\n if (isHiddenOnAll(block)) {\n return \"\";\n }\n\n return renderUnrenderableBlock(block);\n}\n","import type {\n Block,\n CustomBlock,\n TemplateContent,\n CustomFont,\n SectionWrapper,\n} from \"@templatical/types\";\nimport { isSection, isCustomBlock } from \"@templatical/types\";\nimport { RenderContext, DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nimport type { BlockRendererMap } from \"./render-context\";\nimport { renderBlock } from \"./renderers\";\nimport { escapeHtml, escapeAttr, escapeCssValue } from \"./escape\";\nimport { wrapWithDisplayCondition } from \"./display-condition\";\nimport { bgAttr } from \"./utils\";\nimport { toPaddingString } from \"./padding\";\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 * Per-block-type renderer overrides, keyed by `block.type`. An entry replaces\n * the built-in renderer for that type wholesale — including its\n * hidden-on-all-viewports check, which becomes the override's responsibility.\n *\n * ```ts\n * renderToMjml(content, {\n * blockRenderers: {\n * countdown: (block) => `<mj-image src=\"${countdownGifUrl(block)}\" />`,\n * video: (block, ctx) => renderVideoWithPlayButton(block, ctx),\n * },\n * });\n * ```\n *\n * This generalises {@link renderCustomBlock}, which is the same idea for a\n * single block type. It exists so a backend whose render output is a *superset*\n * of the browser's — a server-generated countdown GIF, a composited video\n * thumbnail — can inject exactly that delta instead of forking the renderer:\n * parity for every other block type then holds by construction.\n *\n * A block type with neither a built-in renderer nor an override emits an\n * `mj-raw` placeholder comment and warns, rather than vanishing from the\n * output.\n */\n blockRenderers?: BlockRendererMap;\n /**\n * Resolves the definition-level CSS for a custom block type. Called once\n * per unique `customType` present in the content tree (not per instance).\n * The non-empty results are deduped by content and emitted as additional\n * `<mj-style>` blocks inside `<mj-head>` alongside the built-in visibility\n * media queries.\n *\n * Editor consumers: pass a function that reads\n * `blockRegistry.getDefinition(customType)?.stylesheet`.\n *\n * Headless consumers: provide your own resolver, typically from the same\n * definitions map used by `renderCustomBlock`. Return `undefined` or `null`\n * for definitions without a stylesheet — those are skipped.\n */\n getCustomBlockStylesheet?: (customType: string) => string | undefined | null;\n /**\n * Base URL (no trailing slash) for the social icon PNG assets. Resolved to\n * `${baseUrl}/${style}/${platform}.png` per icon. Defaults to the\n * version-pinned jsDelivr mirror of this package. Override to self-host\n * (e.g., behind your own CDN or for air-gapped environments).\n *\n * Why PNGs: Outlook desktop (Word rendering engine) does not support SVG\n * and rejects base64 data URIs in `<img src>`, so social icons must be\n * served as raster images over HTTP for cross-client compatibility.\n */\n socialIconsBaseUrl?: string;\n}\n\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 customBlockStylesheets = collectCustomBlockStylesheets(\n content,\n options?.getCustomBlockStylesheet,\n );\n\n const renderContext = new RenderContext(\n content.settings.width,\n customFonts,\n defaultFallbackFont,\n allowHtmlBlocks,\n customBlockHtml,\n socialIconsBaseUrl,\n options?.blockRenderers ?? {},\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 // Document-level default text color → the `<mj-text>` default below. Blocks\n // that set their own color (e.g. Title) or carry inline text-color marks\n // override it. Omitted when unset so existing templates render identically.\n const textColorAttr = content.settings.textColor\n ? ` color=\"${escapeAttr(content.settings.textColor)}\"`\n : \"\";\n // Document-level link styling → the global `a { … }` rule below, cascading to\n // every link (rich-text and menu). An unset `linkColor` keeps `color: inherit`\n // so links follow the text color and legacy content renders identically; a\n // per-link/per-item color still overrides (inline styles beat this rule).\n const linkColor = content.settings.linkColor\n ? escapeCssValue(content.settings.linkColor)\n : \"inherit\";\n const linkDecoration = content.settings.linkUnderline ? \"underline\" : \"none\";\n\n const bodyContent = blocks\n .map((block) => renderTopLevelBlock(block, renderContext))\n .filter((value) => value !== \"\")\n .join(\"\\n\");\n\n const fontDeclarations = generateFontDeclarations(customFonts);\n const previewTag = generatePreviewTag(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\"${textColorAttr} />\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: ${linkColor}; text-decoration: ${linkDecoration}; }\n @media only screen and (max-width: 480px) {\n .tpl-hide-mobile { display: none !important; mso-hide: all !important; }\n }\n @media only screen and (min-width: 481px) {\n .tpl-hide-desktop { display: none !important; mso-hide: all !important; }\n }\n </mj-style>${renderCustomBlockStylesheets(customBlockStylesheets)}\n </mj-head>\n <mj-body width=\"${renderContext.containerWidth}px\" background-color=\"${backgroundColor}\">\n${bodyContent}\n </mj-body>\n</mjml>`;\n}\n\n/**\n * Render a top-level block. 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 // An empty render (hidden section) stays empty — never emit a bare wrapper.\n const framed =\n block.wrapper && rendered !== \"\"\n ? renderSectionWrapper(rendered, block.wrapper)\n : rendered;\n return wrapWithDisplayCondition(block, framed);\n }\n\n const content = renderBlock(block, context);\n const wrapped = wrapInSection(content);\n return wrapWithDisplayCondition(block, wrapped);\n}\n\n/**\n * Wrap a rendered section's `mj-section` in an `mj-wrapper` — a full-width band\n * (its own background + padding + optional radius) that frames the section.\n * `mj-wrapper` is the only MJML element that may contain `mj-section`.\n */\nfunction renderSectionWrapper(inner: string, wrapper: SectionWrapper): string {\n const bg = bgAttr(wrapper.backgroundColor, \"native\");\n const padding = ` padding=\"${\n wrapper.padding ? toPaddingString(wrapper.padding) : \"0\"\n }\"`;\n const radius =\n wrapper.borderRadius && wrapper.borderRadius > 0\n ? ` border-radius=\"${wrapper.borderRadius}px\"`\n : \"\";\n return `<mj-wrapper${bg}${padding}${radius}>\n${inner}\n</mj-wrapper>`;\n}\n\n/**\n * Wrap block content in a default mj-section/mj-column for non-section blocks.\n */\nfunction wrapInSection(content: string): 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/**\n * Walk the content tree, find every unique `customType`, ask the consumer's\n * resolver for that definition's stylesheet, and return the non-empty,\n * content-deduped set in insertion order.\n *\n * Content-level dedupe (not just by customType) means two definitions that\n * happen to ship the same stylesheet string emit it only once — cheap and\n * matches the \"one rule, emitted once\" mental model. Whitespace-only and\n * empty stylesheets are skipped.\n */\nfunction collectCustomBlockStylesheets(\n content: TemplateContent,\n resolver: RenderOptions[\"getCustomBlockStylesheet\"],\n): string[] {\n if (!resolver) {\n return [];\n }\n\n const customBlocks: CustomBlock[] = [];\n collectCustomBlocks(content.blocks, customBlocks);\n\n if (customBlocks.length === 0) {\n return [];\n }\n\n const seenTypes = new Set<string>();\n const seenContent = new Set<string>();\n const stylesheets: string[] = [];\n\n for (const block of customBlocks) {\n if (seenTypes.has(block.customType)) {\n continue;\n }\n seenTypes.add(block.customType);\n\n const css = resolver(block.customType);\n if (!css) {\n continue;\n }\n\n const trimmed = css.trim();\n if (trimmed === \"\" || seenContent.has(trimmed)) {\n continue;\n }\n seenContent.add(trimmed);\n stylesheets.push(trimmed);\n }\n\n return stylesheets;\n}\n\nfunction renderCustomBlockStylesheets(stylesheets: string[]): string {\n if (stylesheets.length === 0) {\n return \"\";\n }\n\n // One `<mj-style>` per unique stylesheet so per-definition CSS can be\n // independently inspected in the rendered MJML, and authors can grep their\n // contribution without disentangling a merged blob.\n return stylesheets\n .map((css) => `\\n <mj-style>\\n${css}\\n </mj-style>`)\n .join(\"\");\n}\n\n// Re-export utilities for consumers\nexport { RenderContext } from \"./render-context\";\nexport { escapeHtml, escapeAttr, convertMergeTagsToValues } from \"./escape\";\nexport { isHiddenOnAll, getCssClassAttr, getCssClasses } from \"./visibility\";\nexport { getWidthPercentages, getWidthPixels } from \"./columns\";\nexport { toPaddingString } from \"./padding\";\nexport { DEFAULT_SOCIAL_ICONS_BASE_URL } from \"./render-context\";\nexport { renderBlock } from \"./renderers\";\nexport {\n renderUnrenderableBlock,\n UNRENDERABLE_MARKER_PREFIX,\n} from \"./unrenderable\";\nexport type { BlockRenderer, BlockRendererMap } from \"./render-context\";\n"],"mappings":"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"}
|