@marlinjai/email-editor-core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/TemplateModel-Dq4GoWpl.d.mts +14743 -0
- package/dist/TemplateModel-Dq4GoWpl.d.ts +14743 -0
- package/dist/TemplateModel-v__r5Nvi.d.mts +8786 -0
- package/dist/TemplateModel-v__r5Nvi.d.ts +8786 -0
- package/dist/chunk-2ZL7A2I6.mjs +1029 -0
- package/dist/chunk-MBTNU2JZ.mjs +395 -0
- package/dist/index.d.mts +35664 -0
- package/dist/index.d.ts +35664 -0
- package/dist/index.js +3299 -0
- package/dist/index.mjs +2287 -0
- package/dist/server.d.mts +341 -0
- package/dist/server.d.ts +341 -0
- package/dist/server.js +2492 -0
- package/dist/server.mjs +1966 -0
- package/package.json +73 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,2492 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/server.ts
|
|
31
|
+
var server_exports = {};
|
|
32
|
+
__export(server_exports, {
|
|
33
|
+
DEFAULT_MJML_ATTRIBUTES: () => DEFAULT_MJML_ATTRIBUTES,
|
|
34
|
+
EDITOR_DATA_ATTRIBUTE: () => EDITOR_DATA_ATTRIBUTE,
|
|
35
|
+
MAX_MJML_BYTES: () => MAX_MJML_BYTES,
|
|
36
|
+
MAX_MJML_DEPTH: () => MAX_MJML_DEPTH,
|
|
37
|
+
MAX_MJML_ELEMENTS: () => MAX_MJML_ELEMENTS,
|
|
38
|
+
MJMLCompiler: () => MJMLCompiler,
|
|
39
|
+
MJMLExporter: () => MJMLExporter,
|
|
40
|
+
MjmlImportError: () => MjmlImportError,
|
|
41
|
+
createMJMLCompiler: () => createMJMLCompiler,
|
|
42
|
+
createMJMLExporter: () => createMJMLExporter,
|
|
43
|
+
encodeEditorData: () => encodeEditorData,
|
|
44
|
+
exportTemplate: () => exportTemplate,
|
|
45
|
+
importMjml: () => importMjml,
|
|
46
|
+
isMjmlImportError: () => isMjmlImportError
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(server_exports);
|
|
49
|
+
|
|
50
|
+
// src/compiler/MJMLCompiler.ts
|
|
51
|
+
var import_mjml = __toESM(require("mjml"));
|
|
52
|
+
|
|
53
|
+
// src/registry/blockCategories.ts
|
|
54
|
+
var LEAF_BLOCK_TYPES = [
|
|
55
|
+
"text" /* TEXT */,
|
|
56
|
+
"image" /* IMAGE */,
|
|
57
|
+
"button" /* BUTTON */,
|
|
58
|
+
"divider" /* DIVIDER */,
|
|
59
|
+
"spacer" /* SPACER */,
|
|
60
|
+
"social" /* SOCIAL */
|
|
61
|
+
];
|
|
62
|
+
var CONTAINER_BLOCK_TYPES = [
|
|
63
|
+
"hero" /* HERO */,
|
|
64
|
+
"accordion" /* ACCORDION */,
|
|
65
|
+
"raw" /* RAW */,
|
|
66
|
+
"navbar" /* NAVBAR */,
|
|
67
|
+
"carousel" /* CAROUSEL */,
|
|
68
|
+
"table" /* TABLE */,
|
|
69
|
+
"header" /* HEADER */,
|
|
70
|
+
"footer" /* FOOTER */
|
|
71
|
+
];
|
|
72
|
+
function isLeafBlockType(t) {
|
|
73
|
+
return LEAF_BLOCK_TYPES.includes(t);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/compiler/blockToRawHtml.ts
|
|
77
|
+
function escapeAttr(s) {
|
|
78
|
+
return s.replace(/"/g, """).replace(/</g, "<");
|
|
79
|
+
}
|
|
80
|
+
function spacingToCss(p) {
|
|
81
|
+
if (!p) return "";
|
|
82
|
+
const t = p.top ?? "0", r = p.right ?? "0", b = p.bottom ?? "0", l = p.left ?? "0";
|
|
83
|
+
return `padding:${t} ${r} ${b} ${l};`;
|
|
84
|
+
}
|
|
85
|
+
function textToHtml(b) {
|
|
86
|
+
const styles = [];
|
|
87
|
+
if (b.color) styles.push(`color:${b.color}`);
|
|
88
|
+
if (b.fontSize) styles.push(`font-size:${b.fontSize}`);
|
|
89
|
+
if (b.fontFamily) styles.push(`font-family:${b.fontFamily}`);
|
|
90
|
+
if (b.lineHeight) styles.push(`line-height:${b.lineHeight}`);
|
|
91
|
+
if (b.align) styles.push(`text-align:${b.align}`);
|
|
92
|
+
styles.push("margin:0");
|
|
93
|
+
const padding = spacingToCss(b.padding);
|
|
94
|
+
return `<div style="${styles.join(";")};${padding}">${b.content}</div>`;
|
|
95
|
+
}
|
|
96
|
+
function imageToHtml(b) {
|
|
97
|
+
const align = b.align ?? "center";
|
|
98
|
+
const wrap = `style="text-align:${align};${spacingToCss(b.padding)}"`;
|
|
99
|
+
const widthAttr = b.width ? ` width="${escapeAttr(b.width)}"` : "";
|
|
100
|
+
const heightAttr = b.height ? ` height="${escapeAttr(b.height)}"` : "";
|
|
101
|
+
const radius = b.borderRadius ? `border-radius:${b.borderRadius};` : "";
|
|
102
|
+
const img = `<img src="${escapeAttr(b.src)}" alt="${escapeAttr(b.alt ?? "")}"${widthAttr}${heightAttr} style="display:block;max-width:100%;${radius}border:0;outline:none;text-decoration:none;" />`;
|
|
103
|
+
const inner = b.href ? `<a href="${escapeAttr(b.href)}" target="_blank" rel="noopener" style="text-decoration:none;">${img}</a>` : img;
|
|
104
|
+
return `<div ${wrap}>${inner}</div>`;
|
|
105
|
+
}
|
|
106
|
+
function buttonToHtml(b) {
|
|
107
|
+
const align = b.align ?? "center";
|
|
108
|
+
const bg = b.backgroundColor ?? "#000000";
|
|
109
|
+
const fg = b.color ?? "#ffffff";
|
|
110
|
+
const radius = b.borderRadius ?? "3px";
|
|
111
|
+
const innerPadding = b.innerPadding ?? "10px 25px";
|
|
112
|
+
const border = b.border ? `border:${b.border};` : "";
|
|
113
|
+
return `<div style="text-align:${align};${spacingToCss(b.padding)}">
|
|
114
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="border-collapse:separate;line-height:100%;display:inline-block;">
|
|
115
|
+
<tr>
|
|
116
|
+
<td align="center" valign="middle" role="presentation" style="background-color:${bg};border-radius:${radius};${border}padding:${innerPadding};">
|
|
117
|
+
<a href="${escapeAttr(b.href)}" target="_blank" rel="noopener" style="display:inline-block;color:${fg};font-family:inherit;font-size:14px;font-weight:600;line-height:120%;text-decoration:none;text-transform:none;">${escapeAttr(b.label)}</a>
|
|
118
|
+
</td>
|
|
119
|
+
</tr>
|
|
120
|
+
</table>
|
|
121
|
+
</div>`;
|
|
122
|
+
}
|
|
123
|
+
function dividerToHtml(b) {
|
|
124
|
+
const color = b.borderColor ?? "#cccccc";
|
|
125
|
+
const w = b.borderWidth ?? "1px";
|
|
126
|
+
const style = b.borderStyle ?? "solid";
|
|
127
|
+
const width = b.width ?? "100%";
|
|
128
|
+
return `<div style="${spacingToCss(b.padding)}">
|
|
129
|
+
<div style="border-top:${w} ${style} ${color};width:${width};font-size:1px;line-height:1px;"> </div>
|
|
130
|
+
</div>`;
|
|
131
|
+
}
|
|
132
|
+
function spacerToHtml(b) {
|
|
133
|
+
return `<div style="height:${b.height};line-height:${b.height};font-size:1px;"> </div>`;
|
|
134
|
+
}
|
|
135
|
+
function socialToHtml(b) {
|
|
136
|
+
const items = (b.links ?? []).map((link) => {
|
|
137
|
+
const icon = link.icon ?? "";
|
|
138
|
+
return `<a href="${escapeAttr(link.url)}" target="_blank" rel="noopener" style="display:inline-block;margin:0 4px;">
|
|
139
|
+
<img src="${escapeAttr(icon)}" alt="${escapeAttr(link.platform)}" width="24" height="24" style="display:inline-block;border:0;" />
|
|
140
|
+
</a>`;
|
|
141
|
+
}).join("");
|
|
142
|
+
return `<div style="text-align:center;">${items}</div>`;
|
|
143
|
+
}
|
|
144
|
+
function blockToRawHtml(block) {
|
|
145
|
+
if (!isLeafBlockType(block.type)) {
|
|
146
|
+
throw new Error(`blockToRawHtml only handles leaf blocks; got "${block.type}"`);
|
|
147
|
+
}
|
|
148
|
+
switch (block.type) {
|
|
149
|
+
case "text" /* TEXT */:
|
|
150
|
+
return textToHtml(block);
|
|
151
|
+
case "image" /* IMAGE */:
|
|
152
|
+
return imageToHtml(block);
|
|
153
|
+
case "button" /* BUTTON */:
|
|
154
|
+
return buttonToHtml(block);
|
|
155
|
+
case "divider" /* DIVIDER */:
|
|
156
|
+
return dividerToHtml(block);
|
|
157
|
+
case "spacer" /* SPACER */:
|
|
158
|
+
return spacerToHtml(block);
|
|
159
|
+
case "social" /* SOCIAL */:
|
|
160
|
+
return socialToHtml(block);
|
|
161
|
+
default:
|
|
162
|
+
throw new Error(`Unhandled leaf block type: ${block.type}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/schema/types.ts
|
|
167
|
+
function isWrapper(item) {
|
|
168
|
+
return item.type === "wrapper";
|
|
169
|
+
}
|
|
170
|
+
function allSections(items) {
|
|
171
|
+
return items.flatMap((item) => isWrapper(item) ? item.sections : [item]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/schema/gradient.ts
|
|
175
|
+
function buildGradientCSS(gradient) {
|
|
176
|
+
if (gradient.stops.length === 0) return void 0;
|
|
177
|
+
const stopList = gradient.stops.map((s) => `${s.color} ${s.position}%`).join(", ");
|
|
178
|
+
if (gradient.type === "radial") {
|
|
179
|
+
return `radial-gradient(circle, ${stopList})`;
|
|
180
|
+
}
|
|
181
|
+
return `linear-gradient(${gradient.angle}deg, ${stopList})`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/compiler/MJMLCompiler.ts
|
|
185
|
+
var DEFAULT_MJML_ATTRIBUTES = `
|
|
186
|
+
<mj-all font-family="Georgia, serif" />
|
|
187
|
+
<mj-text font-size="14px" line-height="1.6" />
|
|
188
|
+
`;
|
|
189
|
+
var EDITOR_DATA_ATTRIBUTE = {
|
|
190
|
+
social: "data-ee-social",
|
|
191
|
+
subColumns: "data-ee-subcols"
|
|
192
|
+
};
|
|
193
|
+
function encodeEditorData(value) {
|
|
194
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
195
|
+
}
|
|
196
|
+
function finishAttributes(attrs, classes, extra) {
|
|
197
|
+
const out = [...attrs];
|
|
198
|
+
const taken = new Set(attrs.map((a) => a.slice(0, a.indexOf("="))));
|
|
199
|
+
const allClasses = [...classes];
|
|
200
|
+
if (extra) {
|
|
201
|
+
for (const [name, value] of Object.entries(extra)) {
|
|
202
|
+
if (name === "css-class") {
|
|
203
|
+
for (const c of value.replace(/"/g, """).split(/\s+/)) if (c && !allClasses.includes(c)) allClasses.push(c);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (taken.has(name)) continue;
|
|
207
|
+
out.push(`${name}="${value.replace(/"/g, """)}"`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (allClasses.length > 0) out.push(`css-class="${allClasses.join(" ")}"`);
|
|
211
|
+
return out.join(" ");
|
|
212
|
+
}
|
|
213
|
+
function quoted(value) {
|
|
214
|
+
return value.replace(/"/g, """);
|
|
215
|
+
}
|
|
216
|
+
function spacingToString(spacing) {
|
|
217
|
+
if (!spacing) return "";
|
|
218
|
+
const { top, right, bottom, left } = spacing;
|
|
219
|
+
if (!top && !right && !bottom && !left) return "";
|
|
220
|
+
return [top || "0", right || "0", bottom || "0", left || "0"].join(" ");
|
|
221
|
+
}
|
|
222
|
+
var MJMLCompiler = class {
|
|
223
|
+
constructor() {
|
|
224
|
+
/**
|
|
225
|
+
* Set true while emitting a column with sub-columns. Causes generateHead
|
|
226
|
+
* to inject the responsive media query exactly once per template.
|
|
227
|
+
*/
|
|
228
|
+
this.needsSubColumnStyles = false;
|
|
229
|
+
/**
|
|
230
|
+
* Platform brand colors for social icons
|
|
231
|
+
*/
|
|
232
|
+
this.SOCIAL_COLORS = {
|
|
233
|
+
facebook: "#1877F2",
|
|
234
|
+
twitter: "#000000",
|
|
235
|
+
instagram: "#E4405F",
|
|
236
|
+
linkedin: "#0A66C2",
|
|
237
|
+
youtube: "#FF0000",
|
|
238
|
+
pinterest: "#BD081C",
|
|
239
|
+
github: "#181717"
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Compile email template to MJML and HTML
|
|
244
|
+
*/
|
|
245
|
+
compile(template, options = {}) {
|
|
246
|
+
try {
|
|
247
|
+
this.needsSubColumnStyles = false;
|
|
248
|
+
const mjml = this.templateToMJML(template);
|
|
249
|
+
const result = (0, import_mjml.default)(mjml, {
|
|
250
|
+
// Never resolve <mj-include>: its path is read from the server's disk,
|
|
251
|
+
// and a stored document (a Raw block's html) must not reach local files.
|
|
252
|
+
ignoreIncludes: true,
|
|
253
|
+
validationLevel: "soft",
|
|
254
|
+
minify: false,
|
|
255
|
+
...options.webFonts === false ? { fonts: {} } : {}
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
mjml,
|
|
259
|
+
html: result.html,
|
|
260
|
+
errors: result.errors.length > 0 ? result.errors.map((e) => e.formattedMessage) : void 0
|
|
261
|
+
};
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return {
|
|
264
|
+
mjml: "",
|
|
265
|
+
html: "",
|
|
266
|
+
errors: [error instanceof Error ? error.message : "Unknown compilation error"]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The MJML markup for a document, without compiling it to HTML.
|
|
272
|
+
*/
|
|
273
|
+
toMJML(template) {
|
|
274
|
+
this.needsSubColumnStyles = false;
|
|
275
|
+
return this.templateToMJML(template);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Convert template to MJML markup
|
|
279
|
+
*/
|
|
280
|
+
templateToMJML(template) {
|
|
281
|
+
const { metadata, sections } = template;
|
|
282
|
+
const body = sections.map((item) => isWrapper(item) ? this.wrapperToMJML(item) : this.sectionToMJML(item)).join("\n");
|
|
283
|
+
const gradientCSS = this.collectGradientStyles(sections);
|
|
284
|
+
const head = this.generateHead(metadata, gradientCSS);
|
|
285
|
+
const bodyAttributes = finishAttributes([], [], metadata.mjmlHead?.bodyAttributes);
|
|
286
|
+
return `
|
|
287
|
+
<mjml>
|
|
288
|
+
${head}
|
|
289
|
+
<mj-body${bodyAttributes ? ` ${bodyAttributes}` : ""}>
|
|
290
|
+
${body}
|
|
291
|
+
</mj-body>
|
|
292
|
+
</mjml>
|
|
293
|
+
`.trim();
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Generate MJML head section with all head components
|
|
297
|
+
* Supports: mj-title, mj-preview, mj-font, mj-breakpoint, mj-style
|
|
298
|
+
*/
|
|
299
|
+
generateHead(metadata, gradientCSS) {
|
|
300
|
+
const parts = ["<mj-head>"];
|
|
301
|
+
if (metadata.title) {
|
|
302
|
+
parts.push(`<mj-title>${metadata.title}</mj-title>`);
|
|
303
|
+
}
|
|
304
|
+
if (metadata.previewText || metadata.subject) {
|
|
305
|
+
parts.push(`<mj-preview>${metadata.previewText || metadata.subject || ""}</mj-preview>`);
|
|
306
|
+
}
|
|
307
|
+
if (metadata.fonts && metadata.fonts.length > 0) {
|
|
308
|
+
for (const font of metadata.fonts) {
|
|
309
|
+
parts.push(`<mj-font name="${font.name}" href="${font.href}" />`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (metadata.breakpoint) {
|
|
313
|
+
parts.push(`<mj-breakpoint width="${metadata.breakpoint}" />`);
|
|
314
|
+
}
|
|
315
|
+
const mjmlHead = metadata.mjmlHead;
|
|
316
|
+
const attributes = mjmlHead?.attributes ?? DEFAULT_MJML_ATTRIBUTES;
|
|
317
|
+
if (attributes.trim()) parts.push(`<mj-attributes>${attributes}</mj-attributes>`);
|
|
318
|
+
if (mjmlHead?.headRaw) parts.push(mjmlHead.headRaw);
|
|
319
|
+
if (metadata.customCSS) {
|
|
320
|
+
parts.push(`<mj-style>${metadata.customCSS}</mj-style>`);
|
|
321
|
+
}
|
|
322
|
+
if (metadata.inlineCSS) {
|
|
323
|
+
parts.push(`<mj-style inline="inline">${metadata.inlineCSS}</mj-style>`);
|
|
324
|
+
}
|
|
325
|
+
if (gradientCSS) {
|
|
326
|
+
parts.push(`<mj-style>${gradientCSS}</mj-style>`);
|
|
327
|
+
}
|
|
328
|
+
if (this.needsSubColumnStyles) {
|
|
329
|
+
parts.push(`<mj-style>
|
|
330
|
+
@media only screen and (max-width:480px) {
|
|
331
|
+
table.ee-sub-cols td.ee-sub-col {
|
|
332
|
+
display: block !important;
|
|
333
|
+
width: 100% !important;
|
|
334
|
+
padding-bottom: 12px !important;
|
|
335
|
+
}
|
|
336
|
+
table.ee-sub-cols td.ee-sub-col:last-child {
|
|
337
|
+
padding-bottom: 0 !important;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
</mj-style>`);
|
|
341
|
+
}
|
|
342
|
+
parts.push("</mj-head>");
|
|
343
|
+
return parts.join("\n");
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Collect background-image CSS rules for all wrappers, sections and columns that have gradients.
|
|
347
|
+
* Returns a string of CSS rules to inject as a single mj-style block.
|
|
348
|
+
*/
|
|
349
|
+
collectGradientStyles(items) {
|
|
350
|
+
const rules = [];
|
|
351
|
+
for (const item of items) {
|
|
352
|
+
if (isWrapper(item) && item.backgroundGradient) {
|
|
353
|
+
const css = buildGradientCSS(item.backgroundGradient);
|
|
354
|
+
if (css) rules.push(`.el-grad-${item.id} { background-image: ${css}; }`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const section of allSections(items)) {
|
|
358
|
+
if (section.backgroundGradient) {
|
|
359
|
+
const css = buildGradientCSS(section.backgroundGradient);
|
|
360
|
+
if (css) {
|
|
361
|
+
rules.push(`.el-grad-${section.id} { background-image: ${css}; }`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (const column of section.columns) {
|
|
365
|
+
if (column.backgroundGradient) {
|
|
366
|
+
const css = buildGradientCSS(column.backgroundGradient);
|
|
367
|
+
if (css) {
|
|
368
|
+
rules.push(`.el-grad-${column.id} { background-image: ${css}; }`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return rules.join("\n");
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Convert a wrapper to an `<mj-wrapper>` around its sections. The wrapper's
|
|
377
|
+
* attributes are MJML's own for `mj-wrapper`; the sections inside follow
|
|
378
|
+
* MJML's rules there (a full-width section inside a full-width wrapper
|
|
379
|
+
* renders at standard width, which the inspector explains).
|
|
380
|
+
*/
|
|
381
|
+
wrapperToMJML(wrapper) {
|
|
382
|
+
if (wrapper.hidden) return "";
|
|
383
|
+
const attrs = [];
|
|
384
|
+
const cssClasses = ["el-wrapper", `el-${wrapper.id}`];
|
|
385
|
+
this.pushBackground(attrs, cssClasses, wrapper);
|
|
386
|
+
if (wrapper.border) attrs.push(`border="${quoted(wrapper.border)}"`);
|
|
387
|
+
if (wrapper.borderTop) attrs.push(`border-top="${quoted(wrapper.borderTop)}"`);
|
|
388
|
+
if (wrapper.borderRight) attrs.push(`border-right="${quoted(wrapper.borderRight)}"`);
|
|
389
|
+
if (wrapper.borderBottom) attrs.push(`border-bottom="${quoted(wrapper.borderBottom)}"`);
|
|
390
|
+
if (wrapper.borderLeft) attrs.push(`border-left="${quoted(wrapper.borderLeft)}"`);
|
|
391
|
+
if (wrapper.borderRadius) attrs.push(`border-radius="${quoted(wrapper.borderRadius)}"`);
|
|
392
|
+
if (wrapper.fullWidth) attrs.push('full-width="full-width"');
|
|
393
|
+
const padding = spacingToString(wrapper.padding);
|
|
394
|
+
if (padding) attrs.push(`padding="${quoted(padding)}"`);
|
|
395
|
+
if (wrapper.gap) attrs.push(`gap="${quoted(wrapper.gap)}"`);
|
|
396
|
+
if (wrapper.textAlign) attrs.push(`text-align="${quoted(wrapper.textAlign)}"`);
|
|
397
|
+
for (const c of (wrapper.cssClass ?? "").split(/\s+/)) if (c && !cssClasses.includes(quoted(c))) cssClasses.push(quoted(c));
|
|
398
|
+
const sections = wrapper.sections.map((section) => this.sectionToMJML(section)).filter(Boolean).join("\n");
|
|
399
|
+
return `
|
|
400
|
+
<mj-wrapper ${finishAttributes(attrs, cssClasses, wrapper.extraAttributes)}>
|
|
401
|
+
${sections}
|
|
402
|
+
</mj-wrapper>
|
|
403
|
+
`.trim();
|
|
404
|
+
}
|
|
405
|
+
/** Background attributes shared by sections and wrappers: a gradient (with its Outlook fallback colour), or colour and image. */
|
|
406
|
+
pushBackground(attrs, cssClasses, node) {
|
|
407
|
+
if (node.backgroundGradient) {
|
|
408
|
+
const fallbackColor = node.backgroundGradient.stops[0]?.color;
|
|
409
|
+
if (fallbackColor) attrs.push(`background-color="${quoted(fallbackColor)}"`);
|
|
410
|
+
cssClasses.push(`el-grad-${node.id}`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (node.backgroundColor) attrs.push(`background-color="${quoted(node.backgroundColor)}"`);
|
|
414
|
+
if (node.backgroundImage) attrs.push(`background-url="${quoted(node.backgroundImage)}"`);
|
|
415
|
+
if (node.backgroundPosition) attrs.push(`background-position="${quoted(node.backgroundPosition)}"`);
|
|
416
|
+
if (node.backgroundRepeat) attrs.push(`background-repeat="${quoted(node.backgroundRepeat)}"`);
|
|
417
|
+
if (node.backgroundSize) attrs.push(`background-size="${quoted(node.backgroundSize)}"`);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Convert section to MJML
|
|
421
|
+
* Supports background images, full-width, and mj-group for non-stacking columns
|
|
422
|
+
*/
|
|
423
|
+
sectionToMJML(section) {
|
|
424
|
+
if (section.hidden) return "";
|
|
425
|
+
if (section.bodyRaw) {
|
|
426
|
+
const blocks = section.columns.flatMap((c) => c.blocks).filter((b) => !b.hidden);
|
|
427
|
+
if (blocks.every((b) => b.type === "raw") && section.columns.every((c) => !c.subColumns?.length)) {
|
|
428
|
+
return blocks.map((b) => `<mj-raw>${b.html}</mj-raw>`).join("\n");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const attrs = [];
|
|
432
|
+
const cssClasses = ["el-section", `el-${section.id}`];
|
|
433
|
+
this.pushBackground(attrs, cssClasses, section);
|
|
434
|
+
if (section.fullWidth) {
|
|
435
|
+
attrs.push('full-width="full-width"');
|
|
436
|
+
}
|
|
437
|
+
if (section.padding) {
|
|
438
|
+
const padding = spacingToString(section.padding);
|
|
439
|
+
if (padding) attrs.push(`padding="${padding}"`);
|
|
440
|
+
}
|
|
441
|
+
const columns = section.columns.map((col) => this.columnToMJML(col)).join("\n");
|
|
442
|
+
const sectionAttrs = finishAttributes(attrs, cssClasses, section.extraAttributes);
|
|
443
|
+
if (section.noStack && section.columns.length > 1) {
|
|
444
|
+
return `
|
|
445
|
+
<mj-section ${sectionAttrs}>
|
|
446
|
+
<mj-group>
|
|
447
|
+
${columns}
|
|
448
|
+
</mj-group>
|
|
449
|
+
</mj-section>
|
|
450
|
+
`.trim();
|
|
451
|
+
}
|
|
452
|
+
return `
|
|
453
|
+
<mj-section ${sectionAttrs}>
|
|
454
|
+
${columns}
|
|
455
|
+
</mj-section>
|
|
456
|
+
`.trim();
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Convert column to MJML
|
|
460
|
+
*/
|
|
461
|
+
columnToMJML(column) {
|
|
462
|
+
if (column.hidden) return "";
|
|
463
|
+
if (column.subColumns && column.subColumns.length > 0) {
|
|
464
|
+
return this.subColumnsToMJML(column);
|
|
465
|
+
}
|
|
466
|
+
const cssClasses = ["el-column", `el-${column.id}`];
|
|
467
|
+
const attrs = [];
|
|
468
|
+
if (column.width) {
|
|
469
|
+
attrs.push(`width="${column.width}%"`);
|
|
470
|
+
}
|
|
471
|
+
if (column.backgroundGradient) {
|
|
472
|
+
const fallbackColor = column.backgroundGradient.stops[0]?.color;
|
|
473
|
+
if (fallbackColor) attrs.push(`background-color="${fallbackColor}"`);
|
|
474
|
+
cssClasses.push(`el-grad-${column.id}`);
|
|
475
|
+
} else {
|
|
476
|
+
if (column.backgroundColor) {
|
|
477
|
+
attrs.push(`background-color="${column.backgroundColor}"`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (column.verticalAlign) {
|
|
481
|
+
attrs.push(`vertical-align="${column.verticalAlign}"`);
|
|
482
|
+
}
|
|
483
|
+
if (column.padding) {
|
|
484
|
+
const padding = spacingToString(column.padding);
|
|
485
|
+
if (padding) attrs.push(`padding="${padding}"`);
|
|
486
|
+
}
|
|
487
|
+
const blocks = column.blocks.map((block) => this.blockToMJML(block)).join("\n");
|
|
488
|
+
return `
|
|
489
|
+
<mj-column ${finishAttributes(attrs, cssClasses, column.extraAttributes)}>
|
|
490
|
+
${blocks}
|
|
491
|
+
</mj-column>
|
|
492
|
+
`.trim();
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Emit a group column as an mj-column wrapping a hand-built nested
|
|
496
|
+
* table inside an mj-raw island. The parent mj-section structure
|
|
497
|
+
* stays standard MJML; only the nested area escapes MJML's parser.
|
|
498
|
+
*
|
|
499
|
+
* The responsive media query (table.ee-sub-cols td.ee-sub-col) is
|
|
500
|
+
* injected once at document head via generateHead when the
|
|
501
|
+
* needsSubColumnStyles flag is set.
|
|
502
|
+
*/
|
|
503
|
+
subColumnsToMJML(column) {
|
|
504
|
+
this.needsSubColumnStyles = true;
|
|
505
|
+
const subs = column.subColumns ?? [];
|
|
506
|
+
const sumRaw = subs.reduce((a, s) => a + (s.width || 0), 0) || 100;
|
|
507
|
+
const widths = subs.map((s) => Math.round((s.width || 0) / sumRaw * 1e4) / 100);
|
|
508
|
+
const cells = subs.map((sc, i) => {
|
|
509
|
+
const inner = (sc.blocks ?? []).map((b) => blockToRawHtml(b)).join("\n");
|
|
510
|
+
const valign = sc.verticalAlign ?? "top";
|
|
511
|
+
const hasPadding = sc.paddingTop || sc.paddingRight || sc.paddingBottom || sc.paddingLeft;
|
|
512
|
+
const padding = hasPadding ? `padding:${sc.paddingTop || "0"} ${sc.paddingRight || "0"} ${sc.paddingBottom || "0"} ${sc.paddingLeft || "0"};` : "padding:10px;";
|
|
513
|
+
const bg = sc.backgroundColor ? `background-color:${sc.backgroundColor};` : "";
|
|
514
|
+
return `<td class="ee-sub-col" width="${widths[i]}%" valign="${valign}" style="${padding}${bg}">${inner}</td>`;
|
|
515
|
+
}).join("");
|
|
516
|
+
const colAttrs = [];
|
|
517
|
+
if (column.width) colAttrs.push(`width="${column.width}%"`);
|
|
518
|
+
if (column.backgroundColor) colAttrs.push(`background-color="${column.backgroundColor}"`);
|
|
519
|
+
if (column.verticalAlign) colAttrs.push(`vertical-align="${column.verticalAlign}"`);
|
|
520
|
+
if (column.padding) {
|
|
521
|
+
const padding = spacingToString(column.padding);
|
|
522
|
+
if (padding) colAttrs.push(`padding="${padding}"`);
|
|
523
|
+
}
|
|
524
|
+
return `
|
|
525
|
+
<mj-column ${finishAttributes(colAttrs, ["el-column", `el-${column.id}`], column.extraAttributes)}>
|
|
526
|
+
<mj-raw>
|
|
527
|
+
<table role="presentation" class="ee-sub-cols" ${EDITOR_DATA_ATTRIBUTE.subColumns}="${encodeEditorData(subs)}" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
|
528
|
+
<tr>${cells}</tr>
|
|
529
|
+
</table>
|
|
530
|
+
</mj-raw>
|
|
531
|
+
</mj-column>
|
|
532
|
+
`.trim();
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Convert block to MJML based on type
|
|
536
|
+
*/
|
|
537
|
+
blockToMJML(block) {
|
|
538
|
+
if (block.hidden) return "";
|
|
539
|
+
switch (block.type) {
|
|
540
|
+
case "text":
|
|
541
|
+
return this.textBlockToMJML(block);
|
|
542
|
+
case "image":
|
|
543
|
+
return this.imageBlockToMJML(block);
|
|
544
|
+
case "button":
|
|
545
|
+
return this.buttonBlockToMJML(block);
|
|
546
|
+
case "divider":
|
|
547
|
+
return this.dividerBlockToMJML(block);
|
|
548
|
+
case "spacer":
|
|
549
|
+
return this.spacerBlockToMJML(block);
|
|
550
|
+
case "social":
|
|
551
|
+
return this.socialBlockToMJML(block);
|
|
552
|
+
case "hero":
|
|
553
|
+
return this.heroBlockToMJML(block);
|
|
554
|
+
case "accordion":
|
|
555
|
+
return this.accordionBlockToMJML(block);
|
|
556
|
+
case "raw":
|
|
557
|
+
return this.rawBlockToMJML(block);
|
|
558
|
+
case "navbar":
|
|
559
|
+
return this.navbarBlockToMJML(block);
|
|
560
|
+
case "carousel":
|
|
561
|
+
return this.carouselBlockToMJML(block);
|
|
562
|
+
case "table":
|
|
563
|
+
return this.tableBlockToMJML(block);
|
|
564
|
+
case "header":
|
|
565
|
+
return this.headerBlockToMJML();
|
|
566
|
+
case "footer":
|
|
567
|
+
return this.footerBlockToMJML();
|
|
568
|
+
default:
|
|
569
|
+
console.warn(`Unknown block type: ${block.type}`);
|
|
570
|
+
return "";
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Convert text block to MJML
|
|
575
|
+
*
|
|
576
|
+
* Note: MJML applies styles to the container <td>, but TipTap content
|
|
577
|
+
* (wrapped in <p> tags) doesn't inherit these styles. We solve this by
|
|
578
|
+
* wrapping the content in a <div> with explicit inline styles.
|
|
579
|
+
*/
|
|
580
|
+
textBlockToMJML(block) {
|
|
581
|
+
const attrs = [];
|
|
582
|
+
const inlineStyles = [];
|
|
583
|
+
if (block.align) {
|
|
584
|
+
attrs.push(`align="${block.align}"`);
|
|
585
|
+
inlineStyles.push(`text-align:${block.align}`);
|
|
586
|
+
}
|
|
587
|
+
if (block.color) {
|
|
588
|
+
attrs.push(`color="${block.color}"`);
|
|
589
|
+
inlineStyles.push(`color:${block.color}`);
|
|
590
|
+
}
|
|
591
|
+
if (block.fontSize) {
|
|
592
|
+
attrs.push(`font-size="${block.fontSize}"`);
|
|
593
|
+
inlineStyles.push(`font-size:${block.fontSize}`);
|
|
594
|
+
}
|
|
595
|
+
if (block.fontFamily) {
|
|
596
|
+
attrs.push(`font-family="${block.fontFamily}"`);
|
|
597
|
+
inlineStyles.push(`font-family:${block.fontFamily}`);
|
|
598
|
+
}
|
|
599
|
+
if (block.lineHeight) {
|
|
600
|
+
attrs.push(`line-height="${block.lineHeight}"`);
|
|
601
|
+
inlineStyles.push(`line-height:${block.lineHeight}`);
|
|
602
|
+
}
|
|
603
|
+
if (block.padding) {
|
|
604
|
+
const padding = spacingToString(block.padding);
|
|
605
|
+
if (padding) attrs.push(`padding="${padding}"`);
|
|
606
|
+
}
|
|
607
|
+
const styledContent = inlineStyles.length > 0 ? `<div style="${inlineStyles.join(";")}">${block.content}</div>` : block.content;
|
|
608
|
+
return `<mj-text ${finishAttributes(attrs, ["el-text", `el-${block.id}`], block.extraAttributes)}>${styledContent}</mj-text>`;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Convert image block to MJML
|
|
612
|
+
*/
|
|
613
|
+
imageBlockToMJML(block) {
|
|
614
|
+
const attrs = [`src="${block.src}"`];
|
|
615
|
+
if (block.alt) attrs.push(`alt="${block.alt}"`);
|
|
616
|
+
if (block.width) attrs.push(`width="${block.width}"`);
|
|
617
|
+
if (block.height) attrs.push(`height="${block.height}"`);
|
|
618
|
+
if (block.align) attrs.push(`align="${block.align}"`);
|
|
619
|
+
if (block.href) attrs.push(`href="${block.href}"`);
|
|
620
|
+
if (block.borderRadius) attrs.push(`border-radius="${block.borderRadius}"`);
|
|
621
|
+
if (block.padding) {
|
|
622
|
+
const padding = spacingToString(block.padding);
|
|
623
|
+
if (padding) attrs.push(`padding="${padding}"`);
|
|
624
|
+
}
|
|
625
|
+
return `<mj-image ${finishAttributes(attrs, ["el-image", `el-${block.id}`], block.extraAttributes)} />`;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Convert button block to MJML
|
|
629
|
+
*/
|
|
630
|
+
buttonBlockToMJML(block) {
|
|
631
|
+
const attrs = [`href="${block.href}"`];
|
|
632
|
+
if (block.align) attrs.push(`align="${block.align}"`);
|
|
633
|
+
if (block.backgroundColor) attrs.push(`background-color="${block.backgroundColor}"`);
|
|
634
|
+
if (block.color) attrs.push(`color="${block.color}"`);
|
|
635
|
+
if (block.borderRadius) attrs.push(`border-radius="${block.borderRadius}"`);
|
|
636
|
+
if (block.border) attrs.push(`border="${block.border}"`);
|
|
637
|
+
if (block.innerPadding) attrs.push(`inner-padding="${block.innerPadding}"`);
|
|
638
|
+
if (block.padding) {
|
|
639
|
+
const padding = spacingToString(block.padding);
|
|
640
|
+
if (padding) attrs.push(`padding="${padding}"`);
|
|
641
|
+
}
|
|
642
|
+
return `<mj-button ${finishAttributes(attrs, ["el-button", `el-${block.id}`], block.extraAttributes)}>${block.label}</mj-button>`;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Convert divider block to MJML
|
|
646
|
+
*/
|
|
647
|
+
dividerBlockToMJML(block) {
|
|
648
|
+
const attrs = [];
|
|
649
|
+
if (block.borderColor) attrs.push(`border-color="${block.borderColor}"`);
|
|
650
|
+
if (block.borderWidth) attrs.push(`border-width="${block.borderWidth}"`);
|
|
651
|
+
if (block.borderStyle) attrs.push(`border-style="${block.borderStyle}"`);
|
|
652
|
+
if (block.width) attrs.push(`width="${block.width}"`);
|
|
653
|
+
if (block.padding) {
|
|
654
|
+
const padding = spacingToString(block.padding);
|
|
655
|
+
if (padding) attrs.push(`padding="${padding}"`);
|
|
656
|
+
}
|
|
657
|
+
return `<mj-divider ${finishAttributes(attrs, ["el-divider", `el-${block.id}`], block.extraAttributes)} />`;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Convert spacer block to MJML
|
|
661
|
+
*/
|
|
662
|
+
spacerBlockToMJML(block) {
|
|
663
|
+
return `<mj-spacer ${finishAttributes([`height="${block.height}"`], ["el-spacer", `el-${block.id}`], block.extraAttributes)} />`;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get white SVG icon as data URI for a platform
|
|
667
|
+
* Uses clean, minimal SVGs optimized for email
|
|
668
|
+
*/
|
|
669
|
+
getSocialIconDataUri(platform) {
|
|
670
|
+
const svgIcons = {
|
|
671
|
+
facebook: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>',
|
|
672
|
+
twitter: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/></svg>',
|
|
673
|
+
instagram: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 0C8.74 0 8.333.015 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.74 0 12s.015 3.667.072 4.947c.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.74 24 12 24s3.667-.015 4.947-.072c4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>',
|
|
674
|
+
linkedin: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>',
|
|
675
|
+
youtube: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>',
|
|
676
|
+
pinterest: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 0C5.373 0 0 5.372 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738a.36.36 0 01.083.345c-.091.378-.293 1.194-.333 1.361-.052.218-.174.265-.402.159-1.495-.696-2.428-2.882-2.428-4.64 0-3.779 2.744-7.253 7.917-7.253 4.158 0 7.389 2.963 7.389 6.923 0 4.13-2.607 7.461-6.229 7.461-1.217 0-2.36-.632-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0z"/></svg>',
|
|
677
|
+
github: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'
|
|
678
|
+
};
|
|
679
|
+
const svg = svgIcons[platform] || svgIcons.facebook;
|
|
680
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Convert social block to MJML using raw HTML table for reliable horizontal layout
|
|
684
|
+
* Uses styled anchor tags with background colors for full width control
|
|
685
|
+
*/
|
|
686
|
+
socialBlockToMJML(block) {
|
|
687
|
+
const iconSize = block.iconSize || "40px";
|
|
688
|
+
const iconPadding = block.iconPadding || "8px";
|
|
689
|
+
const borderRadius = block.borderRadius || "999px";
|
|
690
|
+
const align = block.align || "center";
|
|
691
|
+
const isVertical = block.mode === "vertical";
|
|
692
|
+
const sizeNum = parseInt(iconSize, 10) || 40;
|
|
693
|
+
const innerIconSize = Math.round(sizeNum * 0.5);
|
|
694
|
+
const paddingNum = parseInt(iconPadding, 10) || 8;
|
|
695
|
+
const getLinkColor = (link) => {
|
|
696
|
+
return link.color || this.SOCIAL_COLORS[link.platform] || "#666666";
|
|
697
|
+
};
|
|
698
|
+
if (isVertical) {
|
|
699
|
+
const verticalButtons = block.links.map((link) => {
|
|
700
|
+
const bgColor = getLinkColor(link);
|
|
701
|
+
const iconUrl = this.getSocialIconDataUri(link.platform);
|
|
702
|
+
return `<mj-button
|
|
703
|
+
href="${link.url}"
|
|
704
|
+
background-color="${bgColor}"
|
|
705
|
+
border-radius="${borderRadius}"
|
|
706
|
+
width="${iconSize}"
|
|
707
|
+
height="${iconSize}"
|
|
708
|
+
padding="${iconPadding} 0"
|
|
709
|
+
inner-padding="0"
|
|
710
|
+
align="${align}"
|
|
711
|
+
css-class="el-social-btn el-${block.id}-${link.platform}"
|
|
712
|
+
><img src="${iconUrl}" alt="${link.platform}" width="${innerIconSize}" height="${innerIconSize}" style="display:block;margin:auto;" /></mj-button>`;
|
|
713
|
+
}).join("\n");
|
|
714
|
+
return `<!-- Social Icons (vertical) -->
|
|
715
|
+
${verticalButtons}`;
|
|
716
|
+
}
|
|
717
|
+
const alignMargin = align === "center" ? "0 auto" : align === "right" ? "0 0 0 auto" : "0 auto 0 0";
|
|
718
|
+
const socialCells = block.links.map((link, index) => {
|
|
719
|
+
const bgColor = getLinkColor(link);
|
|
720
|
+
const iconUrl = this.getSocialIconDataUri(link.platform);
|
|
721
|
+
const isLast = index === block.links.length - 1;
|
|
722
|
+
const cellPadding = isLast ? "0" : `0 ${paddingNum}px 0 0`;
|
|
723
|
+
const paddingForCentering = Math.round((sizeNum - innerIconSize) / 2);
|
|
724
|
+
return `<td style="padding: ${cellPadding};">
|
|
725
|
+
<a href="${link.url}" target="_blank" style="display: block; width: ${iconSize}; height: ${iconSize}; background-color: ${bgColor}; border-radius: ${borderRadius}; text-decoration: none;">
|
|
726
|
+
<img src="${iconUrl}" alt="${link.platform}" width="${innerIconSize}" height="${innerIconSize}" style="display: block; margin: ${paddingForCentering}px auto 0 auto;" />
|
|
727
|
+
</a>
|
|
728
|
+
</td>`;
|
|
729
|
+
}).join("\n ");
|
|
730
|
+
return `<!-- Social Icons (horizontal) -->
|
|
731
|
+
<mj-raw>
|
|
732
|
+
<table align="${align}" role="presentation" cellpadding="0" cellspacing="0" style="margin: ${alignMargin};" ${EDITOR_DATA_ATTRIBUTE.social}="${encodeEditorData(block)}">
|
|
733
|
+
<tr>
|
|
734
|
+
${socialCells}
|
|
735
|
+
</tr>
|
|
736
|
+
</table>
|
|
737
|
+
</mj-raw>`;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Convert hero block to MJML
|
|
741
|
+
*/
|
|
742
|
+
heroBlockToMJML(block) {
|
|
743
|
+
const attrs = [`background-url="${block.backgroundImage}"`];
|
|
744
|
+
if (block.backgroundHeight) attrs.push(`background-height="${block.backgroundHeight}"`);
|
|
745
|
+
if (block.backgroundWidth) attrs.push(`background-width="${block.backgroundWidth}"`);
|
|
746
|
+
if (block.backgroundColor) attrs.push(`background-color="${block.backgroundColor}"`);
|
|
747
|
+
if (block.verticalAlign) attrs.push(`vertical-align="${block.verticalAlign}"`);
|
|
748
|
+
if (block.mode) attrs.push(`mode="${block.mode}"`);
|
|
749
|
+
return `
|
|
750
|
+
<mj-hero ${finishAttributes(attrs, ["el-hero", `el-${block.id}`], block.extraAttributes)}>
|
|
751
|
+
<mj-text align="center" color="#ffffff" font-size="32px" font-weight="bold">
|
|
752
|
+
Hero Title
|
|
753
|
+
</mj-text>
|
|
754
|
+
<mj-text align="center" color="#ffffff" font-size="16px">
|
|
755
|
+
Add your hero content here
|
|
756
|
+
</mj-text>
|
|
757
|
+
<mj-button href="#" background-color="#ffffff" color="#944923">
|
|
758
|
+
Call to Action
|
|
759
|
+
</mj-button>
|
|
760
|
+
</mj-hero>
|
|
761
|
+
`.trim();
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Convert accordion block to MJML
|
|
765
|
+
*/
|
|
766
|
+
accordionBlockToMJML(block) {
|
|
767
|
+
const attrs = [];
|
|
768
|
+
if (block.iconPosition) attrs.push(`icon-position="${block.iconPosition}"`);
|
|
769
|
+
if (block.borderColor) attrs.push(`border="${block.borderColor}"`);
|
|
770
|
+
if (block.fontFamily) attrs.push(`font-family="${block.fontFamily}"`);
|
|
771
|
+
const items = block.items.map(
|
|
772
|
+
(item) => `
|
|
773
|
+
<mj-accordion-element>
|
|
774
|
+
<mj-accordion-title>${item.title}</mj-accordion-title>
|
|
775
|
+
<mj-accordion-text>${item.content}</mj-accordion-text>
|
|
776
|
+
</mj-accordion-element>`
|
|
777
|
+
).join("\n");
|
|
778
|
+
return `
|
|
779
|
+
<mj-accordion ${finishAttributes(attrs, ["el-accordion", `el-${block.id}`], block.extraAttributes)}>
|
|
780
|
+
${items}
|
|
781
|
+
</mj-accordion>
|
|
782
|
+
`.trim();
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Convert raw HTML block to MJML
|
|
786
|
+
*/
|
|
787
|
+
rawBlockToMJML(block) {
|
|
788
|
+
return `<mj-raw css-class="el-raw el-${block.id}">${block.html}</mj-raw>`;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Convert navbar block to MJML
|
|
792
|
+
*/
|
|
793
|
+
navbarBlockToMJML(block) {
|
|
794
|
+
const attrs = [];
|
|
795
|
+
if (block.hamburger) attrs.push('hamburger="hamburger"');
|
|
796
|
+
if (block.baseUrl) attrs.push(`base-url="${block.baseUrl}"`);
|
|
797
|
+
if (block.align) attrs.push(`align="${block.align}"`);
|
|
798
|
+
if (block.icoColor) attrs.push(`ico-color="${block.icoColor}"`);
|
|
799
|
+
if (block.padding) {
|
|
800
|
+
const padding = spacingToString(block.padding);
|
|
801
|
+
if (padding) attrs.push(`padding="${padding}"`);
|
|
802
|
+
}
|
|
803
|
+
const links = block.links.map((link) => {
|
|
804
|
+
const linkAttrs = [`href="${link.href}"`];
|
|
805
|
+
if (link.color) linkAttrs.push(`color="${link.color}"`);
|
|
806
|
+
return ` <mj-navbar-link ${linkAttrs.join(" ")}>${link.label}</mj-navbar-link>`;
|
|
807
|
+
}).join("\n");
|
|
808
|
+
return `
|
|
809
|
+
<mj-navbar ${finishAttributes(attrs, ["el-navbar", `el-${block.id}`], block.extraAttributes)}>
|
|
810
|
+
${links}
|
|
811
|
+
</mj-navbar>
|
|
812
|
+
`.trim();
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Convert carousel block to MJML
|
|
816
|
+
*/
|
|
817
|
+
carouselBlockToMJML(block) {
|
|
818
|
+
const attrs = [];
|
|
819
|
+
if (block.thumbnails) attrs.push(`thumbnails="${block.thumbnails}"`);
|
|
820
|
+
if (block.borderRadius) attrs.push(`border-radius="${block.borderRadius}"`);
|
|
821
|
+
if (block.iconWidth) attrs.push(`icon-width="${block.iconWidth}"`);
|
|
822
|
+
if (block.tbBorderRadius) attrs.push(`tb-border-radius="${block.tbBorderRadius}"`);
|
|
823
|
+
if (block.padding) {
|
|
824
|
+
const padding = spacingToString(block.padding);
|
|
825
|
+
if (padding) attrs.push(`padding="${padding}"`);
|
|
826
|
+
}
|
|
827
|
+
const images = block.images.map((img) => {
|
|
828
|
+
const imgAttrs = [`src="${img.src}"`];
|
|
829
|
+
if (img.alt) imgAttrs.push(`alt="${img.alt}"`);
|
|
830
|
+
if (img.href) imgAttrs.push(`href="${img.href}"`);
|
|
831
|
+
if (img.thumbnailSrc) imgAttrs.push(`thumbnails-src="${img.thumbnailSrc}"`);
|
|
832
|
+
return ` <mj-carousel-image ${imgAttrs.join(" ")} />`;
|
|
833
|
+
}).join("\n");
|
|
834
|
+
return `
|
|
835
|
+
<mj-carousel ${finishAttributes(attrs, ["el-carousel", `el-${block.id}`], block.extraAttributes)}>
|
|
836
|
+
${images}
|
|
837
|
+
</mj-carousel>
|
|
838
|
+
`.trim();
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Convert table block to MJML
|
|
842
|
+
*/
|
|
843
|
+
tableBlockToMJML(block) {
|
|
844
|
+
const attrs = [];
|
|
845
|
+
if (block.align) attrs.push(`align="${block.align}"`);
|
|
846
|
+
if (block.color) attrs.push(`color="${block.color}"`);
|
|
847
|
+
if (block.fontFamily) attrs.push(`font-family="${block.fontFamily}"`);
|
|
848
|
+
if (block.fontSize) attrs.push(`font-size="${block.fontSize}"`);
|
|
849
|
+
if (block.cellpadding) attrs.push(`cellpadding="${block.cellpadding}"`);
|
|
850
|
+
if (block.cellspacing) attrs.push(`cellspacing="${block.cellspacing}"`);
|
|
851
|
+
if (block.border) attrs.push(`border="${block.border}"`);
|
|
852
|
+
if (block.padding) {
|
|
853
|
+
const padding = spacingToString(block.padding);
|
|
854
|
+
if (padding) attrs.push(`padding="${padding}"`);
|
|
855
|
+
}
|
|
856
|
+
const headerCells = block.headers.map((h) => `<th style="padding: 8px; border-bottom: 1px solid #ddd; text-align: left;">${h}</th>`).join("");
|
|
857
|
+
const headerRow = `<tr style="background-color: #f5f5f5;">${headerCells}</tr>`;
|
|
858
|
+
const dataRows = block.rows.map((row) => {
|
|
859
|
+
const cells = row.map((cell) => `<td style="padding: 8px; border-bottom: 1px solid #eee;">${cell}</td>`).join("");
|
|
860
|
+
return `<tr>${cells}</tr>`;
|
|
861
|
+
}).join("\n");
|
|
862
|
+
return `
|
|
863
|
+
<mj-table ${finishAttributes(attrs, ["el-table", `el-${block.id}`], block.extraAttributes)}>
|
|
864
|
+
${headerRow}
|
|
865
|
+
${dataRows}
|
|
866
|
+
</mj-table>
|
|
867
|
+
`.trim();
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Generate the locked header block.
|
|
871
|
+
*
|
|
872
|
+
* It used to render one client's company name and a logo from an image host;
|
|
873
|
+
* both are gone. What is left is a placeholder image slot. Keep this in step
|
|
874
|
+
* with `headerBlockDefinition` in `packages/blocks/src/branded/index.ts`:
|
|
875
|
+
* which of the two runs depends on how the registry is configured.
|
|
876
|
+
*
|
|
877
|
+
* The placeholder is a `data:` URI rather than an address on an image host,
|
|
878
|
+
* so a workspace whose `asset_policy` is `service_only` can still send a mail
|
|
879
|
+
* that contains this block.
|
|
880
|
+
*/
|
|
881
|
+
headerBlockToMJML() {
|
|
882
|
+
return `
|
|
883
|
+
<mj-wrapper background-color="#ffffff" padding="20px" css-class="el-header-wrapper">
|
|
884
|
+
<mj-section css-class="el-header-section">
|
|
885
|
+
<mj-column css-class="el-header-column">
|
|
886
|
+
<!-- Nothing: see headerBlockDefinition in packages/blocks. -->
|
|
887
|
+
<mj-spacer height="1px" css-class="el-header-spacer" />
|
|
888
|
+
</mj-column>
|
|
889
|
+
</mj-section>
|
|
890
|
+
</mj-wrapper>
|
|
891
|
+
`.trim();
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Generate the locked footer block: the unsubscribe link and nothing else.
|
|
895
|
+
*
|
|
896
|
+
* It used to carry a copyright line naming one client's company. A locked
|
|
897
|
+
* block carries no props, so whoever sends the mail cannot correct a name
|
|
898
|
+
* that is not theirs; the line is gone rather than replaced with a fake one.
|
|
899
|
+
* Keep this in step with `footerBlockDefinition` in
|
|
900
|
+
* `packages/blocks/src/branded/index.ts`.
|
|
901
|
+
*/
|
|
902
|
+
footerBlockToMJML() {
|
|
903
|
+
return `
|
|
904
|
+
<mj-wrapper background-color="#f5f5f5" padding="20px" css-class="el-footer-wrapper">
|
|
905
|
+
<mj-section css-class="el-footer-section">
|
|
906
|
+
<mj-column css-class="el-footer-column">
|
|
907
|
+
<mj-text align="center" font-size="12px" color="#666666" css-class="el-footer-text-2">
|
|
908
|
+
<a href="{{unsubscribe_url}}" style="color: #374151; text-decoration: underline;">Unsubscribe</a>
|
|
909
|
+
</mj-text>
|
|
910
|
+
</mj-column>
|
|
911
|
+
</mj-section>
|
|
912
|
+
</mj-wrapper>
|
|
913
|
+
`.trim();
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
function createMJMLCompiler() {
|
|
917
|
+
return new MJMLCompiler();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/store/mst/MJMLExporter.ts
|
|
921
|
+
var import_mjml2 = __toESM(require("mjml"));
|
|
922
|
+
var import_mobx_state_tree = require("mobx-state-tree");
|
|
923
|
+
var MJMLExporter = class {
|
|
924
|
+
constructor(options = {}) {
|
|
925
|
+
this.compiler = new MJMLCompiler();
|
|
926
|
+
this.options = {
|
|
927
|
+
validationLevel: options.validationLevel || "soft",
|
|
928
|
+
minify: options.minify ?? false,
|
|
929
|
+
beautify: options.beautify ?? false
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Export template to MJML + HTML
|
|
934
|
+
*/
|
|
935
|
+
export(template) {
|
|
936
|
+
const mjml = this.exportMJML(template);
|
|
937
|
+
try {
|
|
938
|
+
const result = (0, import_mjml2.default)(mjml, {
|
|
939
|
+
// Never resolve <mj-include>: see MJMLCompiler.compile.
|
|
940
|
+
ignoreIncludes: true,
|
|
941
|
+
validationLevel: this.options.validationLevel,
|
|
942
|
+
minify: this.options.minify,
|
|
943
|
+
beautify: this.options.beautify
|
|
944
|
+
});
|
|
945
|
+
return {
|
|
946
|
+
mjml,
|
|
947
|
+
html: result.html,
|
|
948
|
+
errors: result.errors.length > 0 ? result.errors.map((e) => e.formattedMessage) : void 0
|
|
949
|
+
};
|
|
950
|
+
} catch (error) {
|
|
951
|
+
return { mjml, html: "", errors: [error instanceof Error ? error.message : "Unknown compilation error"] };
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Export only MJML (without HTML compilation)
|
|
956
|
+
*/
|
|
957
|
+
exportMJML(template) {
|
|
958
|
+
return this.compiler.toMJML(JSON.parse(JSON.stringify((0, import_mobx_state_tree.getSnapshot)(template))));
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
function createMJMLExporter(options) {
|
|
962
|
+
return new MJMLExporter(options);
|
|
963
|
+
}
|
|
964
|
+
function exportTemplate(template, options) {
|
|
965
|
+
const exporter = new MJMLExporter(options);
|
|
966
|
+
return exporter.export(template);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/importer/importMjml.ts
|
|
970
|
+
var import_mjml3 = __toESM(require("mjml"));
|
|
971
|
+
var import_nanoid2 = require("nanoid");
|
|
972
|
+
|
|
973
|
+
// src/schema/migrate.ts
|
|
974
|
+
var import_nanoid = require("nanoid");
|
|
975
|
+
|
|
976
|
+
// src/schema/validation.ts
|
|
977
|
+
var import_zod = require("zod");
|
|
978
|
+
var SpacingSchema = import_zod.z.object({
|
|
979
|
+
top: import_zod.z.string().optional(),
|
|
980
|
+
right: import_zod.z.string().optional(),
|
|
981
|
+
bottom: import_zod.z.string().optional(),
|
|
982
|
+
left: import_zod.z.string().optional()
|
|
983
|
+
});
|
|
984
|
+
var GradientStopSchema = import_zod.z.object({
|
|
985
|
+
color: import_zod.z.string().min(1),
|
|
986
|
+
position: import_zod.z.number().min(0).max(100)
|
|
987
|
+
});
|
|
988
|
+
var BackgroundGradientSchema = import_zod.z.object({
|
|
989
|
+
type: import_zod.z.enum(["linear", "radial"]),
|
|
990
|
+
angle: import_zod.z.number().min(0).max(360),
|
|
991
|
+
stops: import_zod.z.array(GradientStopSchema).min(1)
|
|
992
|
+
});
|
|
993
|
+
var ExtraAttributesSchema = import_zod.z.record(import_zod.z.string().regex(/^[a-z][a-z0-9-]*$/), import_zod.z.string());
|
|
994
|
+
var MjmlHeadSchema = import_zod.z.object({
|
|
995
|
+
attributes: import_zod.z.string().optional(),
|
|
996
|
+
bodyAttributes: ExtraAttributesSchema.optional(),
|
|
997
|
+
headRaw: import_zod.z.string().optional()
|
|
998
|
+
});
|
|
999
|
+
var CustomFontSchema = import_zod.z.object({
|
|
1000
|
+
name: import_zod.z.string(),
|
|
1001
|
+
href: import_zod.z.string()
|
|
1002
|
+
});
|
|
1003
|
+
var TemplateMetadataSchema = import_zod.z.object({
|
|
1004
|
+
name: import_zod.z.string().optional(),
|
|
1005
|
+
subject: import_zod.z.string().optional(),
|
|
1006
|
+
previewText: import_zod.z.string().optional(),
|
|
1007
|
+
title: import_zod.z.string().optional(),
|
|
1008
|
+
// Epoch milliseconds (what the editor's store emits) or an ISO 8601 string.
|
|
1009
|
+
createdAt: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).optional(),
|
|
1010
|
+
updatedAt: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).optional(),
|
|
1011
|
+
// Head component settings
|
|
1012
|
+
fonts: import_zod.z.array(CustomFontSchema).optional(),
|
|
1013
|
+
breakpoint: import_zod.z.string().optional(),
|
|
1014
|
+
customCSS: import_zod.z.string().optional(),
|
|
1015
|
+
inlineCSS: import_zod.z.string().optional(),
|
|
1016
|
+
mjmlHead: MjmlHeadSchema.optional()
|
|
1017
|
+
});
|
|
1018
|
+
var TextBlockSchema = import_zod.z.object({
|
|
1019
|
+
id: import_zod.z.string(),
|
|
1020
|
+
type: import_zod.z.literal("text"),
|
|
1021
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1022
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1023
|
+
content: import_zod.z.string(),
|
|
1024
|
+
align: import_zod.z.enum(["left", "center", "right", "justify"]).optional(),
|
|
1025
|
+
color: import_zod.z.string().optional(),
|
|
1026
|
+
fontSize: import_zod.z.string().optional(),
|
|
1027
|
+
fontFamily: import_zod.z.string().optional(),
|
|
1028
|
+
padding: SpacingSchema.optional(),
|
|
1029
|
+
lineHeight: import_zod.z.string().optional()
|
|
1030
|
+
});
|
|
1031
|
+
var ImageBlockSchema = import_zod.z.object({
|
|
1032
|
+
id: import_zod.z.string(),
|
|
1033
|
+
type: import_zod.z.literal("image"),
|
|
1034
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1035
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1036
|
+
src: import_zod.z.string().min(1),
|
|
1037
|
+
// Allow any non-empty string, not just URLs
|
|
1038
|
+
alt: import_zod.z.string().optional(),
|
|
1039
|
+
width: import_zod.z.string().optional(),
|
|
1040
|
+
height: import_zod.z.string().optional(),
|
|
1041
|
+
align: import_zod.z.enum(["left", "center", "right"]).optional(),
|
|
1042
|
+
href: import_zod.z.string().optional(),
|
|
1043
|
+
// Allow any string for link
|
|
1044
|
+
padding: SpacingSchema.optional(),
|
|
1045
|
+
borderRadius: import_zod.z.string().optional()
|
|
1046
|
+
// Rounded corners
|
|
1047
|
+
});
|
|
1048
|
+
var ButtonBlockSchema = import_zod.z.object({
|
|
1049
|
+
id: import_zod.z.string(),
|
|
1050
|
+
type: import_zod.z.literal("button"),
|
|
1051
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1052
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1053
|
+
label: import_zod.z.string().min(1),
|
|
1054
|
+
href: import_zod.z.string().min(1),
|
|
1055
|
+
// Allow any non-empty string
|
|
1056
|
+
align: import_zod.z.enum(["left", "center", "right"]).optional(),
|
|
1057
|
+
backgroundColor: import_zod.z.string().optional(),
|
|
1058
|
+
color: import_zod.z.string().optional(),
|
|
1059
|
+
borderRadius: import_zod.z.string().optional(),
|
|
1060
|
+
border: import_zod.z.string().optional(),
|
|
1061
|
+
// CSS border shorthand
|
|
1062
|
+
padding: SpacingSchema.optional(),
|
|
1063
|
+
innerPadding: import_zod.z.string().optional()
|
|
1064
|
+
});
|
|
1065
|
+
var DividerBlockSchema = import_zod.z.object({
|
|
1066
|
+
id: import_zod.z.string(),
|
|
1067
|
+
type: import_zod.z.literal("divider"),
|
|
1068
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1069
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1070
|
+
borderColor: import_zod.z.string().optional(),
|
|
1071
|
+
borderWidth: import_zod.z.string().optional(),
|
|
1072
|
+
borderStyle: import_zod.z.enum(["solid", "dashed", "dotted"]).optional(),
|
|
1073
|
+
width: import_zod.z.string().optional(),
|
|
1074
|
+
// Width of the divider line
|
|
1075
|
+
padding: SpacingSchema.optional()
|
|
1076
|
+
});
|
|
1077
|
+
var SpacerBlockSchema = import_zod.z.object({
|
|
1078
|
+
id: import_zod.z.string(),
|
|
1079
|
+
type: import_zod.z.literal("spacer"),
|
|
1080
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1081
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1082
|
+
height: import_zod.z.string()
|
|
1083
|
+
});
|
|
1084
|
+
var HeaderBlockSchema = import_zod.z.object({
|
|
1085
|
+
id: import_zod.z.string(),
|
|
1086
|
+
type: import_zod.z.literal("header"),
|
|
1087
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1088
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1089
|
+
locked: import_zod.z.literal(true)
|
|
1090
|
+
});
|
|
1091
|
+
var FooterBlockSchema = import_zod.z.object({
|
|
1092
|
+
id: import_zod.z.string(),
|
|
1093
|
+
type: import_zod.z.literal("footer"),
|
|
1094
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1095
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1096
|
+
locked: import_zod.z.literal(true)
|
|
1097
|
+
});
|
|
1098
|
+
var SocialBlockSchema = import_zod.z.object({
|
|
1099
|
+
id: import_zod.z.string(),
|
|
1100
|
+
type: import_zod.z.literal("social"),
|
|
1101
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1102
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1103
|
+
mode: import_zod.z.enum(["horizontal", "vertical"]).optional(),
|
|
1104
|
+
align: import_zod.z.enum(["left", "center", "right"]).optional(),
|
|
1105
|
+
iconSize: import_zod.z.string().optional(),
|
|
1106
|
+
iconPadding: import_zod.z.string().optional(),
|
|
1107
|
+
borderRadius: import_zod.z.string().optional(),
|
|
1108
|
+
// For round icons
|
|
1109
|
+
links: import_zod.z.array(
|
|
1110
|
+
import_zod.z.object({
|
|
1111
|
+
platform: import_zod.z.enum(["facebook", "twitter", "instagram", "linkedin", "youtube", "pinterest", "github"]),
|
|
1112
|
+
url: import_zod.z.string(),
|
|
1113
|
+
color: import_zod.z.string().optional()
|
|
1114
|
+
// Per-icon color override (uses platform default if not set)
|
|
1115
|
+
})
|
|
1116
|
+
)
|
|
1117
|
+
});
|
|
1118
|
+
var HeroBlockSchema = import_zod.z.object({
|
|
1119
|
+
id: import_zod.z.string(),
|
|
1120
|
+
type: import_zod.z.literal("hero"),
|
|
1121
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1122
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1123
|
+
backgroundImage: import_zod.z.string().min(1),
|
|
1124
|
+
backgroundHeight: import_zod.z.string().optional(),
|
|
1125
|
+
backgroundWidth: import_zod.z.string().optional(),
|
|
1126
|
+
backgroundColor: import_zod.z.string().optional(),
|
|
1127
|
+
verticalAlign: import_zod.z.enum(["top", "middle", "bottom"]).optional(),
|
|
1128
|
+
mode: import_zod.z.enum(["fluid-height", "fixed-height"]).optional()
|
|
1129
|
+
});
|
|
1130
|
+
var AccordionBlockSchema = import_zod.z.object({
|
|
1131
|
+
id: import_zod.z.string(),
|
|
1132
|
+
type: import_zod.z.literal("accordion"),
|
|
1133
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1134
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1135
|
+
items: import_zod.z.array(
|
|
1136
|
+
import_zod.z.object({
|
|
1137
|
+
title: import_zod.z.string(),
|
|
1138
|
+
content: import_zod.z.string()
|
|
1139
|
+
})
|
|
1140
|
+
),
|
|
1141
|
+
iconPosition: import_zod.z.enum(["left", "right"]).optional(),
|
|
1142
|
+
borderColor: import_zod.z.string().optional(),
|
|
1143
|
+
fontFamily: import_zod.z.string().optional()
|
|
1144
|
+
});
|
|
1145
|
+
var RawBlockSchema = import_zod.z.object({
|
|
1146
|
+
id: import_zod.z.string(),
|
|
1147
|
+
type: import_zod.z.literal("raw"),
|
|
1148
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1149
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1150
|
+
html: import_zod.z.string()
|
|
1151
|
+
});
|
|
1152
|
+
var NavbarBlockSchema = import_zod.z.object({
|
|
1153
|
+
id: import_zod.z.string(),
|
|
1154
|
+
type: import_zod.z.literal("navbar"),
|
|
1155
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1156
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1157
|
+
links: import_zod.z.array(
|
|
1158
|
+
import_zod.z.object({
|
|
1159
|
+
label: import_zod.z.string(),
|
|
1160
|
+
href: import_zod.z.string(),
|
|
1161
|
+
color: import_zod.z.string().optional()
|
|
1162
|
+
})
|
|
1163
|
+
),
|
|
1164
|
+
hamburger: import_zod.z.boolean().optional(),
|
|
1165
|
+
baseUrl: import_zod.z.string().optional(),
|
|
1166
|
+
align: import_zod.z.enum(["left", "center", "right"]).optional(),
|
|
1167
|
+
icoColor: import_zod.z.string().optional(),
|
|
1168
|
+
padding: SpacingSchema.optional()
|
|
1169
|
+
});
|
|
1170
|
+
var CarouselBlockSchema = import_zod.z.object({
|
|
1171
|
+
id: import_zod.z.string(),
|
|
1172
|
+
type: import_zod.z.literal("carousel"),
|
|
1173
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1174
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1175
|
+
images: import_zod.z.array(
|
|
1176
|
+
import_zod.z.object({
|
|
1177
|
+
src: import_zod.z.string(),
|
|
1178
|
+
alt: import_zod.z.string().optional(),
|
|
1179
|
+
href: import_zod.z.string().optional(),
|
|
1180
|
+
thumbnailSrc: import_zod.z.string().optional()
|
|
1181
|
+
})
|
|
1182
|
+
),
|
|
1183
|
+
thumbnails: import_zod.z.enum(["visible", "hidden"]).optional(),
|
|
1184
|
+
borderRadius: import_zod.z.string().optional(),
|
|
1185
|
+
iconWidth: import_zod.z.string().optional(),
|
|
1186
|
+
tbBorderRadius: import_zod.z.string().optional(),
|
|
1187
|
+
padding: SpacingSchema.optional()
|
|
1188
|
+
});
|
|
1189
|
+
var TableBlockSchema = import_zod.z.object({
|
|
1190
|
+
id: import_zod.z.string(),
|
|
1191
|
+
type: import_zod.z.literal("table"),
|
|
1192
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1193
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1194
|
+
headers: import_zod.z.array(import_zod.z.string()),
|
|
1195
|
+
rows: import_zod.z.array(import_zod.z.array(import_zod.z.string())),
|
|
1196
|
+
align: import_zod.z.enum(["left", "center", "right"]).optional(),
|
|
1197
|
+
color: import_zod.z.string().optional(),
|
|
1198
|
+
fontFamily: import_zod.z.string().optional(),
|
|
1199
|
+
fontSize: import_zod.z.string().optional(),
|
|
1200
|
+
cellpadding: import_zod.z.string().optional(),
|
|
1201
|
+
cellspacing: import_zod.z.string().optional(),
|
|
1202
|
+
border: import_zod.z.string().optional(),
|
|
1203
|
+
padding: SpacingSchema.optional()
|
|
1204
|
+
});
|
|
1205
|
+
var BlockSchema = import_zod.z.discriminatedUnion("type", [
|
|
1206
|
+
TextBlockSchema,
|
|
1207
|
+
ImageBlockSchema,
|
|
1208
|
+
ButtonBlockSchema,
|
|
1209
|
+
DividerBlockSchema,
|
|
1210
|
+
SpacerBlockSchema,
|
|
1211
|
+
HeaderBlockSchema,
|
|
1212
|
+
FooterBlockSchema,
|
|
1213
|
+
SocialBlockSchema,
|
|
1214
|
+
HeroBlockSchema,
|
|
1215
|
+
AccordionBlockSchema,
|
|
1216
|
+
RawBlockSchema,
|
|
1217
|
+
NavbarBlockSchema,
|
|
1218
|
+
CarouselBlockSchema,
|
|
1219
|
+
TableBlockSchema
|
|
1220
|
+
]);
|
|
1221
|
+
var ColumnSchema = import_zod.z.object({
|
|
1222
|
+
id: import_zod.z.string(),
|
|
1223
|
+
width: import_zod.z.number().min(0).max(100).optional(),
|
|
1224
|
+
backgroundColor: import_zod.z.string().optional(),
|
|
1225
|
+
backgroundGradient: BackgroundGradientSchema.optional(),
|
|
1226
|
+
padding: SpacingSchema.optional(),
|
|
1227
|
+
verticalAlign: import_zod.z.enum(["top", "middle", "bottom"]).optional(),
|
|
1228
|
+
// Vertical content alignment
|
|
1229
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1230
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1231
|
+
blocks: import_zod.z.array(BlockSchema)
|
|
1232
|
+
});
|
|
1233
|
+
var sectionFields = {
|
|
1234
|
+
id: import_zod.z.string(),
|
|
1235
|
+
type: import_zod.z.literal("section"),
|
|
1236
|
+
backgroundColor: import_zod.z.string().optional(),
|
|
1237
|
+
backgroundImage: import_zod.z.string().optional(),
|
|
1238
|
+
backgroundPosition: import_zod.z.string().optional(),
|
|
1239
|
+
backgroundRepeat: import_zod.z.enum(["repeat", "no-repeat"]).optional(),
|
|
1240
|
+
backgroundSize: import_zod.z.string().optional(),
|
|
1241
|
+
backgroundGradient: BackgroundGradientSchema.optional(),
|
|
1242
|
+
padding: SpacingSchema.optional(),
|
|
1243
|
+
noStack: import_zod.z.boolean().optional(),
|
|
1244
|
+
fullWidth: import_zod.z.boolean().optional(),
|
|
1245
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1246
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1247
|
+
bodyRaw: import_zod.z.boolean().optional(),
|
|
1248
|
+
columns: import_zod.z.array(ColumnSchema).min(1)
|
|
1249
|
+
};
|
|
1250
|
+
var SectionSchema = import_zod.z.object({
|
|
1251
|
+
...sectionFields,
|
|
1252
|
+
isWrapper: import_zod.z.undefined({ invalid_type_error: "isWrapper is a schema 1.0 field; a 1.1 document holds a wrapper instead" }).optional()
|
|
1253
|
+
});
|
|
1254
|
+
var SectionSchemaV1_0 = import_zod.z.object({
|
|
1255
|
+
...sectionFields,
|
|
1256
|
+
isWrapper: import_zod.z.boolean().optional()
|
|
1257
|
+
});
|
|
1258
|
+
var WrapperSchema = import_zod.z.object({
|
|
1259
|
+
id: import_zod.z.string(),
|
|
1260
|
+
type: import_zod.z.literal("wrapper"),
|
|
1261
|
+
hidden: import_zod.z.boolean().optional(),
|
|
1262
|
+
backgroundColor: import_zod.z.string().optional(),
|
|
1263
|
+
backgroundImage: import_zod.z.string().optional(),
|
|
1264
|
+
backgroundGradient: BackgroundGradientSchema.optional(),
|
|
1265
|
+
backgroundPosition: import_zod.z.string().optional(),
|
|
1266
|
+
backgroundRepeat: import_zod.z.enum(["repeat", "no-repeat"]).optional(),
|
|
1267
|
+
backgroundSize: import_zod.z.string().optional(),
|
|
1268
|
+
border: import_zod.z.string().optional(),
|
|
1269
|
+
borderTop: import_zod.z.string().optional(),
|
|
1270
|
+
borderRight: import_zod.z.string().optional(),
|
|
1271
|
+
borderBottom: import_zod.z.string().optional(),
|
|
1272
|
+
borderLeft: import_zod.z.string().optional(),
|
|
1273
|
+
borderRadius: import_zod.z.string().optional(),
|
|
1274
|
+
padding: SpacingSchema.optional(),
|
|
1275
|
+
fullWidth: import_zod.z.boolean().optional(),
|
|
1276
|
+
cssClass: import_zod.z.string().optional(),
|
|
1277
|
+
gap: import_zod.z.string().regex(/^[0-9]+(\.[0-9]+)?px$/, "gap is a length in px, e.g. 16px").optional(),
|
|
1278
|
+
textAlign: import_zod.z.enum(["left", "center", "right"]).optional(),
|
|
1279
|
+
extraAttributes: ExtraAttributesSchema.optional(),
|
|
1280
|
+
sections: import_zod.z.array(SectionSchema)
|
|
1281
|
+
});
|
|
1282
|
+
var TopLevelItemSchema = import_zod.z.discriminatedUnion("type", [SectionSchema, WrapperSchema]);
|
|
1283
|
+
var EmailTemplateSchemaV1_0 = import_zod.z.object({
|
|
1284
|
+
id: import_zod.z.string().optional(),
|
|
1285
|
+
version: import_zod.z.literal("1.0"),
|
|
1286
|
+
metadata: TemplateMetadataSchema,
|
|
1287
|
+
sections: import_zod.z.array(SectionSchemaV1_0)
|
|
1288
|
+
});
|
|
1289
|
+
var EmailTemplateSchemaV1_1 = import_zod.z.object({
|
|
1290
|
+
id: import_zod.z.string().optional(),
|
|
1291
|
+
version: import_zod.z.literal("1.1"),
|
|
1292
|
+
metadata: TemplateMetadataSchema,
|
|
1293
|
+
sections: import_zod.z.array(TopLevelItemSchema)
|
|
1294
|
+
});
|
|
1295
|
+
var EmailTemplateSchema = import_zod.z.discriminatedUnion("version", [EmailTemplateSchemaV1_0, EmailTemplateSchemaV1_1]);
|
|
1296
|
+
|
|
1297
|
+
// src/schema/migrate.ts
|
|
1298
|
+
var CURRENT_TEMPLATE_VERSION = "1.1";
|
|
1299
|
+
var SUPPORTED_TEMPLATE_VERSIONS = ["1.0", CURRENT_TEMPLATE_VERSION];
|
|
1300
|
+
var TemplateMigrationError = class extends Error {
|
|
1301
|
+
constructor(code, message, options = {}) {
|
|
1302
|
+
super(message);
|
|
1303
|
+
this.name = "TemplateMigrationError";
|
|
1304
|
+
this.code = code;
|
|
1305
|
+
this.version = options.version;
|
|
1306
|
+
this.issues = options.issues ?? [];
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
function isTemplateMigrationError(error) {
|
|
1310
|
+
return error instanceof TemplateMigrationError;
|
|
1311
|
+
}
|
|
1312
|
+
function parseVersion(version) {
|
|
1313
|
+
const match = /^(\d+)\.(\d+)$/.exec(version);
|
|
1314
|
+
if (!match) return null;
|
|
1315
|
+
return [Number(match[1]), Number(match[2])];
|
|
1316
|
+
}
|
|
1317
|
+
function compareVersions(a, b) {
|
|
1318
|
+
return a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1];
|
|
1319
|
+
}
|
|
1320
|
+
function migrateTemplate(doc) {
|
|
1321
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
1322
|
+
throw new TemplateMigrationError(
|
|
1323
|
+
"INVALID_INPUT",
|
|
1324
|
+
`Expected a template document object, received ${doc === null ? "null" : Array.isArray(doc) ? "an array" : typeof doc}.`
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
const version = doc.version;
|
|
1328
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
1329
|
+
throw new TemplateMigrationError(
|
|
1330
|
+
"MISSING_VERSION",
|
|
1331
|
+
'The template document has no "version" field, so its schema cannot be determined.'
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
const parsed = parseVersion(version);
|
|
1335
|
+
if (!parsed) {
|
|
1336
|
+
throw new TemplateMigrationError(
|
|
1337
|
+
"UNSUPPORTED_VERSION",
|
|
1338
|
+
`Template schema version "${version}" is not of the form "major.minor".`,
|
|
1339
|
+
{ version }
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
const current = parseVersion(CURRENT_TEMPLATE_VERSION);
|
|
1343
|
+
if (compareVersions(parsed, current) > 0) {
|
|
1344
|
+
throw new TemplateMigrationError(
|
|
1345
|
+
"NEWER_VERSION",
|
|
1346
|
+
`Template schema version "${version}" is newer than this editor supports (${CURRENT_TEMPLATE_VERSION}). Upgrade the @marlinjai/email-editor packages to open it.`,
|
|
1347
|
+
{ version }
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
if (!SUPPORTED_TEMPLATE_VERSIONS.includes(version)) {
|
|
1351
|
+
throw new TemplateMigrationError(
|
|
1352
|
+
"UNSUPPORTED_VERSION",
|
|
1353
|
+
`Template schema version "${version}" is not supported and has no migration to ${CURRENT_TEMPLATE_VERSION}.`,
|
|
1354
|
+
{ version }
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
let next = doc;
|
|
1358
|
+
if (version === "1.0") {
|
|
1359
|
+
assertValid(EmailTemplateSchemaV1_0, next, version);
|
|
1360
|
+
next = migrateV1_0ToV1_1(next);
|
|
1361
|
+
}
|
|
1362
|
+
assertValid(EmailTemplateSchemaV1_1, next, version);
|
|
1363
|
+
return next;
|
|
1364
|
+
}
|
|
1365
|
+
function assertValid(schema, doc, version) {
|
|
1366
|
+
const result = schema.safeParse(doc);
|
|
1367
|
+
if (result.success) return;
|
|
1368
|
+
const issues = result.error.issues.map((issue) => ({ path: issue.path, message: issue.message }));
|
|
1369
|
+
const first = issues[0];
|
|
1370
|
+
throw new TemplateMigrationError(
|
|
1371
|
+
"INVALID_DOCUMENT",
|
|
1372
|
+
`Template document does not match schema version ${version}: ${first ? `${first.path.join(".") || "(root)"}: ${first.message}` : "unknown validation error"}${issues.length > 1 ? ` (and ${issues.length - 1} more)` : ""}.`,
|
|
1373
|
+
{ version, issues }
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
function migrateV1_0ToV1_1(doc) {
|
|
1377
|
+
if (!doc.sections.some((s) => s.isWrapper === true && !s.bodyRaw)) {
|
|
1378
|
+
return { ...doc, version: "1.1", sections: doc.sections.map(stripIsWrapper) };
|
|
1379
|
+
}
|
|
1380
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1381
|
+
for (const s of doc.sections) {
|
|
1382
|
+
ids.add(s.id);
|
|
1383
|
+
for (const c of s.columns ?? []) {
|
|
1384
|
+
ids.add(c.id);
|
|
1385
|
+
for (const b of c.blocks ?? []) ids.add(b.id);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const freshId = (base) => {
|
|
1389
|
+
let candidate = `${base}-inner`;
|
|
1390
|
+
for (let n = 2; ids.has(candidate); n++) candidate = `${base}-inner-${n}`;
|
|
1391
|
+
ids.add(candidate);
|
|
1392
|
+
return candidate;
|
|
1393
|
+
};
|
|
1394
|
+
const sections = doc.sections.map((s) => {
|
|
1395
|
+
if (s.isWrapper !== true || s.bodyRaw) return stripIsWrapper(s);
|
|
1396
|
+
const { isWrapper: _flag, noStack: _noStack, ...rest } = s;
|
|
1397
|
+
const wrapper = { id: s.id, type: "wrapper", sections: [{ id: freshId(s.id), type: "section", columns: s.columns }] };
|
|
1398
|
+
if (rest.hidden !== void 0) wrapper.hidden = rest.hidden;
|
|
1399
|
+
if (rest.backgroundColor !== void 0) wrapper.backgroundColor = rest.backgroundColor;
|
|
1400
|
+
if (rest.backgroundImage !== void 0) wrapper.backgroundImage = rest.backgroundImage;
|
|
1401
|
+
if (rest.backgroundGradient !== void 0) wrapper.backgroundGradient = rest.backgroundGradient;
|
|
1402
|
+
if (rest.backgroundPosition !== void 0) wrapper.backgroundPosition = rest.backgroundPosition;
|
|
1403
|
+
if (rest.backgroundRepeat !== void 0) wrapper.backgroundRepeat = rest.backgroundRepeat;
|
|
1404
|
+
if (rest.backgroundSize !== void 0) wrapper.backgroundSize = rest.backgroundSize;
|
|
1405
|
+
if (rest.fullWidth !== void 0) wrapper.fullWidth = rest.fullWidth;
|
|
1406
|
+
if (rest.padding !== void 0) wrapper.padding = rest.padding;
|
|
1407
|
+
if (rest.extraAttributes !== void 0) wrapper.extraAttributes = rest.extraAttributes;
|
|
1408
|
+
return wrapper;
|
|
1409
|
+
});
|
|
1410
|
+
return { ...doc, version: "1.1", sections };
|
|
1411
|
+
}
|
|
1412
|
+
function stripIsWrapper(s) {
|
|
1413
|
+
if (!("isWrapper" in s)) return s;
|
|
1414
|
+
const { isWrapper: _flag, ...rest } = s;
|
|
1415
|
+
return rest;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/importer/types.ts
|
|
1419
|
+
var MAX_MJML_BYTES = 512 * 1024;
|
|
1420
|
+
var MAX_MJML_DEPTH = 32;
|
|
1421
|
+
var MAX_MJML_ELEMENTS = 5e3;
|
|
1422
|
+
var MjmlImportError = class extends Error {
|
|
1423
|
+
constructor(code, message, position = {}) {
|
|
1424
|
+
super(position.line !== void 0 ? `Line ${position.line}${position.column !== void 0 ? `, column ${position.column}` : ""}: ${message}` : message);
|
|
1425
|
+
this.name = "MjmlImportError";
|
|
1426
|
+
this.code = code;
|
|
1427
|
+
this.line = position.line;
|
|
1428
|
+
this.column = position.column;
|
|
1429
|
+
}
|
|
1430
|
+
/** A plain object that survives a worker thread boundary. */
|
|
1431
|
+
toJSON() {
|
|
1432
|
+
return { code: this.code, message: this.message, line: this.line, column: this.column };
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
function isMjmlImportError(error) {
|
|
1436
|
+
return error instanceof MjmlImportError;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/importer/scan.ts
|
|
1440
|
+
function locator(source) {
|
|
1441
|
+
const starts = [0];
|
|
1442
|
+
for (let i = 0; i < source.length; i++) if (source.charCodeAt(i) === 10) starts.push(i + 1);
|
|
1443
|
+
return (index) => {
|
|
1444
|
+
let lo = 0;
|
|
1445
|
+
let hi = starts.length - 1;
|
|
1446
|
+
while (lo < hi) {
|
|
1447
|
+
const mid = lo + hi + 1 >> 1;
|
|
1448
|
+
if (starts[mid] <= index) lo = mid;
|
|
1449
|
+
else hi = mid - 1;
|
|
1450
|
+
}
|
|
1451
|
+
return { line: lo + 1, column: index - starts[lo] + 1 };
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
var NAME = /[A-Za-z_][A-Za-z0-9_.:-]*/y;
|
|
1455
|
+
var ATTRIBUTE = /\s+([^\s=/>"'<]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/y;
|
|
1456
|
+
var TAG_END = /\s*(\/?)>/y;
|
|
1457
|
+
function scanMjml(source, endingTags) {
|
|
1458
|
+
if (Buffer.byteLength(source, "utf8") > MAX_MJML_BYTES) {
|
|
1459
|
+
throw new MjmlImportError("too_large", `The MJML is larger than ${MAX_MJML_BYTES} bytes.`);
|
|
1460
|
+
}
|
|
1461
|
+
const at = locator(source);
|
|
1462
|
+
const include = /<\s*mj-include\b/i.exec(source);
|
|
1463
|
+
if (include) {
|
|
1464
|
+
throw new MjmlImportError(
|
|
1465
|
+
"include_not_supported",
|
|
1466
|
+
"mj-include is not supported: an import has no files to include. Paste the included MJML in its place.",
|
|
1467
|
+
at(include.index)
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
const fail = (code, message, index) => {
|
|
1471
|
+
throw new MjmlImportError(code, message, at(index));
|
|
1472
|
+
};
|
|
1473
|
+
const stack = [];
|
|
1474
|
+
let elements = 0;
|
|
1475
|
+
let rootClosed = false;
|
|
1476
|
+
let rootSeen = false;
|
|
1477
|
+
let i = 0;
|
|
1478
|
+
const skipPast = (terminator, from, what) => {
|
|
1479
|
+
const end = source.indexOf(terminator, from);
|
|
1480
|
+
if (end < 0) fail("invalid_xml", `${what} is never closed.`, from);
|
|
1481
|
+
return end + terminator.length;
|
|
1482
|
+
};
|
|
1483
|
+
while (i < source.length) {
|
|
1484
|
+
const lt = source.indexOf("<", i);
|
|
1485
|
+
const textEnd = lt < 0 ? source.length : lt;
|
|
1486
|
+
if (source.slice(i, textEnd).trim() !== "" && (stack.length === 0 || rootClosed)) {
|
|
1487
|
+
fail(rootSeen ? "invalid_xml" : "not_mjml", rootSeen ? "Text after the closing </mjml>." : "The document must start with <mjml>.", i + source.slice(i, textEnd).search(/\S/));
|
|
1488
|
+
}
|
|
1489
|
+
if (lt < 0) break;
|
|
1490
|
+
i = lt;
|
|
1491
|
+
if (source.startsWith("<!--", i)) {
|
|
1492
|
+
i = skipPast("-->", i + 4, "A comment");
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
if (source.startsWith("<![CDATA[", i)) {
|
|
1496
|
+
i = skipPast("]]>", i + 9, "A CDATA section");
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (source.startsWith("<?", i)) {
|
|
1500
|
+
if (rootSeen) fail("invalid_xml", "A processing instruction inside the document.", i);
|
|
1501
|
+
i = skipPast("?>", i + 2, "A processing instruction");
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
if (source.startsWith("<!", i)) {
|
|
1505
|
+
if (rootSeen) fail("invalid_xml", "A declaration inside the document.", i);
|
|
1506
|
+
i = skipPast(">", i + 2, "A declaration");
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
const closing = source[i + 1] === "/";
|
|
1510
|
+
NAME.lastIndex = i + (closing ? 2 : 1);
|
|
1511
|
+
const nameMatch = NAME.exec(source);
|
|
1512
|
+
if (!nameMatch) fail("invalid_xml", 'A "<" that does not start a tag. Write it as < outside content.', i);
|
|
1513
|
+
const name = nameMatch[0];
|
|
1514
|
+
let cursor = NAME.lastIndex;
|
|
1515
|
+
if (closing) {
|
|
1516
|
+
TAG_END.lastIndex = cursor;
|
|
1517
|
+
const end2 = TAG_END.exec(source);
|
|
1518
|
+
if (!end2 || end2[1] === "/") fail("invalid_xml", `The closing tag </${name}> is malformed.`, i);
|
|
1519
|
+
const open2 = stack.pop();
|
|
1520
|
+
if (!open2) fail("invalid_xml", `</${name}> closes nothing.`, i);
|
|
1521
|
+
if (open2.name !== name) {
|
|
1522
|
+
const where = at(open2.index);
|
|
1523
|
+
fail("invalid_xml", `</${name}> does not match <${open2.name}> opened on line ${where.line}, column ${where.column}.`, i);
|
|
1524
|
+
}
|
|
1525
|
+
if (stack.length === 0) rootClosed = true;
|
|
1526
|
+
i = TAG_END.lastIndex;
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
if (rootClosed) fail("invalid_xml", `<${name}> after the closing </mjml>.`, i);
|
|
1530
|
+
if (!rootSeen) {
|
|
1531
|
+
if (name !== "mjml") fail("not_mjml", `The document must start with <mjml>, not <${name}>.`, i);
|
|
1532
|
+
rootSeen = true;
|
|
1533
|
+
}
|
|
1534
|
+
if (name === "mj-include") {
|
|
1535
|
+
fail("include_not_supported", "mj-include is not supported: an import has no files to include. Paste the included MJML in its place.", i);
|
|
1536
|
+
}
|
|
1537
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1538
|
+
for (; ; ) {
|
|
1539
|
+
ATTRIBUTE.lastIndex = cursor;
|
|
1540
|
+
const attr = ATTRIBUTE.exec(source);
|
|
1541
|
+
if (!attr) break;
|
|
1542
|
+
if (seen.has(attr[1])) fail("invalid_xml", `The attribute "${attr[1]}" appears twice on <${name}>.`, cursor);
|
|
1543
|
+
seen.add(attr[1]);
|
|
1544
|
+
cursor = ATTRIBUTE.lastIndex;
|
|
1545
|
+
}
|
|
1546
|
+
TAG_END.lastIndex = cursor;
|
|
1547
|
+
const end = TAG_END.exec(source);
|
|
1548
|
+
if (!end) fail("invalid_xml", `The tag <${name}> is malformed (an unquoted or unclosed attribute value?).`, cursor);
|
|
1549
|
+
i = TAG_END.lastIndex;
|
|
1550
|
+
elements += 1;
|
|
1551
|
+
if (elements > MAX_MJML_ELEMENTS) fail("too_many_elements", `More than ${MAX_MJML_ELEMENTS} MJML elements.`, i);
|
|
1552
|
+
if (end[1] === "/") {
|
|
1553
|
+
if (stack.length === 0) rootClosed = true;
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
if (stack.length + 1 > MAX_MJML_DEPTH) fail("too_deep", `MJML elements are nested more than ${MAX_MJML_DEPTH} deep.`, i);
|
|
1557
|
+
if (endingTags.has(name)) {
|
|
1558
|
+
let depth = 1;
|
|
1559
|
+
const token = new RegExp(`<!--|<(/?)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=[\\s/>])`, "g");
|
|
1560
|
+
token.lastIndex = i;
|
|
1561
|
+
let m;
|
|
1562
|
+
while (m = token.exec(source)) {
|
|
1563
|
+
if (m[0] === "<!--") {
|
|
1564
|
+
const close = source.indexOf("-->", m.index + 4);
|
|
1565
|
+
if (close < 0) fail("invalid_xml", "A comment is never closed.", m.index);
|
|
1566
|
+
token.lastIndex = close + 3;
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
const gt = source.indexOf(">", m.index);
|
|
1570
|
+
if (gt < 0) fail("invalid_xml", `The tag at this position is never closed.`, m.index);
|
|
1571
|
+
if (m[1] === "/") depth -= 1;
|
|
1572
|
+
else if (source[gt - 1] !== "/") depth += 1;
|
|
1573
|
+
token.lastIndex = gt + 1;
|
|
1574
|
+
if (depth === 0) break;
|
|
1575
|
+
}
|
|
1576
|
+
if (depth !== 0) fail("invalid_xml", `<${name}> is never closed.`, i);
|
|
1577
|
+
i = token.lastIndex;
|
|
1578
|
+
if (stack.length === 0) rootClosed = true;
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
stack.push({ name, index: lt });
|
|
1582
|
+
}
|
|
1583
|
+
if (!rootSeen) fail("not_mjml", "The document is empty: it must start with <mjml>.", 0);
|
|
1584
|
+
const open = stack.pop();
|
|
1585
|
+
if (open) fail("invalid_xml", `<${open.name}> is never closed.`, open.index);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/importer/tree.ts
|
|
1589
|
+
var import_mjml_parser_xml = __toESM(require("mjml-parser-xml"));
|
|
1590
|
+
var import_mjml_preset_core = __toESM(require("mjml-preset-core"));
|
|
1591
|
+
function unwrap(mod) {
|
|
1592
|
+
const m = mod;
|
|
1593
|
+
return m && typeof m === "object" && "default" in m && m.default ? m.default : mod;
|
|
1594
|
+
}
|
|
1595
|
+
var parseXml = unwrap(import_mjml_parser_xml.default);
|
|
1596
|
+
var preset = unwrap(import_mjml_preset_core.default);
|
|
1597
|
+
var componentName = (c) => c.getTagName ? c.getTagName() : c.componentName ?? "";
|
|
1598
|
+
var COMPONENTS = Object.fromEntries(preset.components.map((c) => [componentName(c), c]));
|
|
1599
|
+
var ENDING_TAGS = new Set(
|
|
1600
|
+
preset.components.filter((c) => c.endingTag).map((c) => componentName(c))
|
|
1601
|
+
);
|
|
1602
|
+
var KNOWN_TAGS = /* @__PURE__ */ new Set(["mjml", ...Object.keys(COMPONENTS), "mj-all", "mj-class", "mj-selector", "mj-html-attribute"]);
|
|
1603
|
+
function normalize(node) {
|
|
1604
|
+
const attributes = {};
|
|
1605
|
+
for (const [k, v] of Object.entries(node.attributes ?? {})) attributes[k] = String(v).replace(/"/g, """);
|
|
1606
|
+
return {
|
|
1607
|
+
tagName: node.tagName,
|
|
1608
|
+
attributes,
|
|
1609
|
+
content: node.content,
|
|
1610
|
+
children: (node.children ?? []).map(normalize),
|
|
1611
|
+
line: node.line
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
function parseMjml(source) {
|
|
1615
|
+
const root = parseXml(source, {
|
|
1616
|
+
components: COMPONENTS,
|
|
1617
|
+
keepComments: true,
|
|
1618
|
+
ignoreIncludes: true,
|
|
1619
|
+
convertBooleans: false,
|
|
1620
|
+
addEmptyAttributes: true
|
|
1621
|
+
});
|
|
1622
|
+
return normalize(root);
|
|
1623
|
+
}
|
|
1624
|
+
function serialize(node) {
|
|
1625
|
+
const attrs = Object.entries(node.attributes).map(([k, v]) => ` ${k}="${v}"`).join("");
|
|
1626
|
+
const inner = node.content !== void 0 ? node.content : node.children.map(serialize).join("");
|
|
1627
|
+
if (inner === "" && node.children.length === 0 && node.content === void 0) return `<${node.tagName}${attrs} />`;
|
|
1628
|
+
return `<${node.tagName}${attrs}>${inner}</${node.tagName}>`;
|
|
1629
|
+
}
|
|
1630
|
+
function isComment(node) {
|
|
1631
|
+
return node.tagName === "mj-raw" && Object.keys(node.attributes).length === 0 && node.children.length === 0 && /^<!--[\s\S]*-->$/.test(node.content ?? "");
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/importer/importMjml.ts
|
|
1635
|
+
var ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
1636
|
+
var ATTRIBUTE_NAME = /^[a-z][a-z0-9-]*$/;
|
|
1637
|
+
var FRAGMENT_LIMIT = 2e3;
|
|
1638
|
+
var EDITOR_SUB_COLUMN_STYLE = `@media only screen and (max-width:480px) {
|
|
1639
|
+
table.ee-sub-cols td.ee-sub-col {
|
|
1640
|
+
display: block !important;
|
|
1641
|
+
width: 100% !important;
|
|
1642
|
+
padding-bottom: 12px !important;
|
|
1643
|
+
}
|
|
1644
|
+
table.ee-sub-cols td.ee-sub-col:last-child {
|
|
1645
|
+
padding-bottom: 0 !important;
|
|
1646
|
+
}
|
|
1647
|
+
}`;
|
|
1648
|
+
var EDITOR_COMMENTS = /* @__PURE__ */ new Set(["<!-- Social Icons (horizontal) -->", "<!-- Social Icons (vertical) -->"]);
|
|
1649
|
+
var SOCIAL_PLATFORMS = /* @__PURE__ */ new Set(["facebook", "twitter", "instagram", "linkedin", "youtube", "pinterest", "github"]);
|
|
1650
|
+
var SOCIAL_COLORS = {
|
|
1651
|
+
facebook: "#1877F2",
|
|
1652
|
+
twitter: "#000000",
|
|
1653
|
+
instagram: "#E4405F",
|
|
1654
|
+
linkedin: "#0A66C2",
|
|
1655
|
+
youtube: "#FF0000",
|
|
1656
|
+
pinterest: "#BD081C",
|
|
1657
|
+
github: "#181717"
|
|
1658
|
+
};
|
|
1659
|
+
var squash = (s) => s.replace(/\s+/g, " ").trim();
|
|
1660
|
+
var shorten = (s) => s.length > FRAGMENT_LIMIT ? `${s.slice(0, FRAGMENT_LIMIT)}...` : s;
|
|
1661
|
+
function decodeEditorData(value) {
|
|
1662
|
+
if (!value || !/^[A-Za-z0-9+/=]+$/.test(value)) return null;
|
|
1663
|
+
try {
|
|
1664
|
+
return JSON.parse(Buffer.from(value, "base64").toString("utf8"));
|
|
1665
|
+
} catch {
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function parsePadding(value) {
|
|
1670
|
+
const parts = value.trim().split(/\s+/);
|
|
1671
|
+
if (parts.length < 1 || parts.length > 4 || parts.some((p) => !/^-?[0-9.]+(px|%|em|rem)?$/.test(p))) return null;
|
|
1672
|
+
const [top, right = top, bottom = top, left = right] = parts;
|
|
1673
|
+
const all = { top, right, bottom, left };
|
|
1674
|
+
const out = {};
|
|
1675
|
+
for (const side of ["top", "right", "bottom", "left"]) if (all[side] !== "0") out[side] = all[side];
|
|
1676
|
+
if (Object.keys(out).length === 0) return { top: "0", right: "0", bottom: "0", left: "0" };
|
|
1677
|
+
return out;
|
|
1678
|
+
}
|
|
1679
|
+
function parseGradientRules(css) {
|
|
1680
|
+
const rules = css.trim().split("\n").filter((l) => l.trim() !== "");
|
|
1681
|
+
const out = /* @__PURE__ */ new Map();
|
|
1682
|
+
for (const rule of rules) {
|
|
1683
|
+
const m = /^\.el-grad-([A-Za-z0-9_-]+) \{ background-image: (linear|radial)-gradient\((?:(-?[0-9.]+)deg|circle), (.*)\); \}$/.exec(rule.trim());
|
|
1684
|
+
if (!m) return null;
|
|
1685
|
+
const stops = m[4].split(/,\s(?![^()]*\))/).map((s) => {
|
|
1686
|
+
const sm = /^(.*) (-?[0-9.]+)%$/.exec(s.trim());
|
|
1687
|
+
return sm ? { color: sm[1], position: Number(sm[2]) } : null;
|
|
1688
|
+
});
|
|
1689
|
+
if (stops.some((s) => s === null)) return null;
|
|
1690
|
+
out.set(m[1], { type: m[2], angle: m[3] ? Number(m[3]) : 0, stops });
|
|
1691
|
+
}
|
|
1692
|
+
return out;
|
|
1693
|
+
}
|
|
1694
|
+
var Importer = class {
|
|
1695
|
+
constructor(root) {
|
|
1696
|
+
this.root = root;
|
|
1697
|
+
this.warnings = [];
|
|
1698
|
+
this.usedIds = /* @__PURE__ */ new Set();
|
|
1699
|
+
this.fallbacks = [];
|
|
1700
|
+
this.gradients = /* @__PURE__ */ new Map();
|
|
1701
|
+
this.kept = /* @__PURE__ */ new Map();
|
|
1702
|
+
this.usesMjClass = false;
|
|
1703
|
+
this.hasOwnDefaults = false;
|
|
1704
|
+
}
|
|
1705
|
+
warn(code, severity, path, node, message, withFragment = false) {
|
|
1706
|
+
this.warnings.push({
|
|
1707
|
+
severity,
|
|
1708
|
+
code,
|
|
1709
|
+
path,
|
|
1710
|
+
line: node?.line,
|
|
1711
|
+
message,
|
|
1712
|
+
...withFragment && node ? { fragment: shorten(serialize(node)) } : {}
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
id(candidate) {
|
|
1716
|
+
if (candidate && ID.test(candidate) && !this.usedIds.has(candidate)) {
|
|
1717
|
+
this.usedIds.add(candidate);
|
|
1718
|
+
return candidate;
|
|
1719
|
+
}
|
|
1720
|
+
let fresh = (0, import_nanoid2.nanoid)();
|
|
1721
|
+
while (this.usedIds.has(fresh)) fresh = (0, import_nanoid2.nanoid)();
|
|
1722
|
+
this.usedIds.add(fresh);
|
|
1723
|
+
return fresh;
|
|
1724
|
+
}
|
|
1725
|
+
/** `css-class="el-<type> el-<id> ..."`: the editor's id, and the author's own classes. */
|
|
1726
|
+
identity(attrs, marker) {
|
|
1727
|
+
const classes = (attrs["css-class"] ?? "").split(/\s+/).filter(Boolean);
|
|
1728
|
+
delete attrs["css-class"];
|
|
1729
|
+
if (classes[0] === marker && classes[1]?.startsWith("el-")) {
|
|
1730
|
+
const id = classes[1].slice(3);
|
|
1731
|
+
return { id, classes: classes.slice(2).filter((c) => c !== `el-grad-${id}`) };
|
|
1732
|
+
}
|
|
1733
|
+
return { classes };
|
|
1734
|
+
}
|
|
1735
|
+
/** Whatever is left of `attrs` (plus the author's classes), as kept attributes. */
|
|
1736
|
+
extras(attrs, classes, path, node) {
|
|
1737
|
+
const out = {};
|
|
1738
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
1739
|
+
if (!ATTRIBUTE_NAME.test(name)) {
|
|
1740
|
+
this.warn("attribute_dropped", "info", path, node, `The attribute "${name}" is not an MJML attribute name; MJML ignored it and so does the editor.`);
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
if (name === "mj-class") this.usesMjClass = true;
|
|
1744
|
+
out[name] = value;
|
|
1745
|
+
this.kept.set(name, (this.kept.get(name) ?? 0) + 1);
|
|
1746
|
+
}
|
|
1747
|
+
if (classes.length > 0) {
|
|
1748
|
+
out["css-class"] = classes.join(" ");
|
|
1749
|
+
this.kept.set("css-class", (this.kept.get("css-class") ?? 0) + 1);
|
|
1750
|
+
}
|
|
1751
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1752
|
+
}
|
|
1753
|
+
/** Marks `node` (a child of `parent`) to be kept as compiled HTML. */
|
|
1754
|
+
fallback(node, parent, path, reason, unknown = false) {
|
|
1755
|
+
const block = { id: this.id(), type: "raw", html: "" };
|
|
1756
|
+
this.fallbacks.push({ node, parent, block, unknown });
|
|
1757
|
+
if (unknown) {
|
|
1758
|
+
this.warn("unknown_component", "warning", path, node, `<${node.tagName}> is not an MJML component, so MJML renders nothing for it. Its source is kept, as a comment, in a Raw block.`, true);
|
|
1759
|
+
} else {
|
|
1760
|
+
this.warn("kept_as_html", "warning", path, node, `${reason} It is kept as compiled HTML in a Raw block: the mail looks the same, but it is not editable as a block. Rebuild it in the editor to edit it.`, true);
|
|
1761
|
+
}
|
|
1762
|
+
return block;
|
|
1763
|
+
}
|
|
1764
|
+
// Head
|
|
1765
|
+
head(head, body) {
|
|
1766
|
+
const metadata = {};
|
|
1767
|
+
const mjmlHead = {};
|
|
1768
|
+
const fonts = [];
|
|
1769
|
+
const css = [];
|
|
1770
|
+
const inline = [];
|
|
1771
|
+
const raw = [];
|
|
1772
|
+
let attributes;
|
|
1773
|
+
for (const [index, node] of (head?.children ?? []).entries()) {
|
|
1774
|
+
const path = `mj-head > ${node.tagName}[${index + 1}]`;
|
|
1775
|
+
const a = node.attributes;
|
|
1776
|
+
switch (node.tagName) {
|
|
1777
|
+
case "mj-title":
|
|
1778
|
+
metadata.title = node.content ?? "";
|
|
1779
|
+
break;
|
|
1780
|
+
case "mj-preview":
|
|
1781
|
+
metadata.previewText = node.content ?? "";
|
|
1782
|
+
break;
|
|
1783
|
+
case "mj-font":
|
|
1784
|
+
if (a.name && a.href && Object.keys(a).length === 2) fonts.push({ name: a.name, href: a.href });
|
|
1785
|
+
else raw.push(serialize(node));
|
|
1786
|
+
break;
|
|
1787
|
+
case "mj-breakpoint":
|
|
1788
|
+
if (a.width && Object.keys(a).length === 1) metadata.breakpoint = a.width;
|
|
1789
|
+
else raw.push(serialize(node));
|
|
1790
|
+
break;
|
|
1791
|
+
case "mj-style": {
|
|
1792
|
+
const content = node.content ?? "";
|
|
1793
|
+
const keys = Object.keys(a);
|
|
1794
|
+
if (keys.length === 1 && a.inline === "inline") inline.push(content);
|
|
1795
|
+
else if (keys.length > 0) {
|
|
1796
|
+
raw.push(serialize(node));
|
|
1797
|
+
this.warn("head_element_kept", "info", path, node, "An mj-style with attributes the editor has no field for is kept as it is.");
|
|
1798
|
+
} else if (squash(content) === squash(EDITOR_SUB_COLUMN_STYLE)) {
|
|
1799
|
+
} else {
|
|
1800
|
+
const gradients = parseGradientRules(content);
|
|
1801
|
+
if (gradients && gradients.size > 0) for (const [id, g] of gradients) this.gradients.set(id, g);
|
|
1802
|
+
else css.push(content);
|
|
1803
|
+
}
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1806
|
+
case "mj-attributes": {
|
|
1807
|
+
const markup = node.children.map(serialize).join("");
|
|
1808
|
+
attributes = (attributes ?? "") + markup;
|
|
1809
|
+
break;
|
|
1810
|
+
}
|
|
1811
|
+
default:
|
|
1812
|
+
raw.push(serialize(node));
|
|
1813
|
+
if (isComment(node)) break;
|
|
1814
|
+
if (!KNOWN_TAGS.has(node.tagName)) {
|
|
1815
|
+
this.warn("unknown_component", "warning", path, node, `<${node.tagName}> is not an MJML head element; it is kept as it is, and MJML ignores it.`, true);
|
|
1816
|
+
} else {
|
|
1817
|
+
this.warn("head_element_kept", "info", path, node, `<${node.tagName}> has no field in the editor; it is kept as it is and applies to the mail.`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (fonts.length > 0) metadata.fonts = fonts;
|
|
1822
|
+
if (css.length > 0) metadata.customCSS = css.join("\n");
|
|
1823
|
+
if (inline.length > 0) metadata.inlineCSS = inline.join("\n");
|
|
1824
|
+
const isEditorDefault = attributes !== void 0 && squash(attributes) === squash(DEFAULT_MJML_ATTRIBUTES.replace(/\s*\n\s*/g, ""));
|
|
1825
|
+
if (!isEditorDefault) {
|
|
1826
|
+
mjmlHead.attributes = attributes ?? "";
|
|
1827
|
+
if ((attributes ?? "").trim() !== "") this.hasOwnDefaults = true;
|
|
1828
|
+
}
|
|
1829
|
+
if (raw.length > 0) mjmlHead.headRaw = raw.join("\n");
|
|
1830
|
+
const bodyAttrs = { ...body.attributes };
|
|
1831
|
+
const bodyExtra = this.extras(bodyAttrs, [], "mj-body", body);
|
|
1832
|
+
if (bodyExtra) mjmlHead.bodyAttributes = bodyExtra;
|
|
1833
|
+
if (Object.keys(mjmlHead).length > 0) metadata.mjmlHead = mjmlHead;
|
|
1834
|
+
return metadata;
|
|
1835
|
+
}
|
|
1836
|
+
// Body
|
|
1837
|
+
body(body) {
|
|
1838
|
+
const items = [];
|
|
1839
|
+
const raw = this.rawRuns((section) => items.push(section));
|
|
1840
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1841
|
+
for (const node of body.children) {
|
|
1842
|
+
const n = (counts.get(node.tagName) ?? 0) + 1;
|
|
1843
|
+
counts.set(node.tagName, n);
|
|
1844
|
+
const path = `mj-body > ${node.tagName}[${n}]`;
|
|
1845
|
+
if (node.tagName === "mj-raw") {
|
|
1846
|
+
if (isComment(node) && EDITOR_COMMENTS.has(node.content.trim())) continue;
|
|
1847
|
+
raw.push(this.raw(node, body, path));
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
if (node.tagName === "mj-wrapper") {
|
|
1851
|
+
raw.end();
|
|
1852
|
+
items.push(this.wrapper(node, path));
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
if (node.tagName === "mj-section") {
|
|
1856
|
+
const section = this.section(node, path);
|
|
1857
|
+
if (section) {
|
|
1858
|
+
raw.end();
|
|
1859
|
+
items.push(section);
|
|
1860
|
+
} else {
|
|
1861
|
+
raw.push(this.fallback(node, body, path, "The editor cannot hold this section as it is (see the other warnings for it)."));
|
|
1862
|
+
}
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
if (node.tagName === "mj-hero") {
|
|
1866
|
+
raw.push(this.fallback(node, body, path, "The editor's Hero block has no content of its own, so an mj-hero with text and buttons cannot become one."));
|
|
1867
|
+
continue;
|
|
1868
|
+
}
|
|
1869
|
+
if (!KNOWN_TAGS.has(node.tagName)) {
|
|
1870
|
+
raw.push(this.fallback(node, body, path, "", true));
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
raw.push(this.fallback(node, body, path, `<${node.tagName}> does not belong directly in mj-body; MJML renders it as it can.`));
|
|
1874
|
+
}
|
|
1875
|
+
if (typeof body.content === "string" && body.content.trim()) {
|
|
1876
|
+
this.warn("stray_text", "info", "mj-body", body, "Text directly in mj-body is ignored by MJML, and by the import.");
|
|
1877
|
+
}
|
|
1878
|
+
return items;
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Consecutive raw markup (in mj-body or in an mj-wrapper) collects in one
|
|
1882
|
+
* raw-only section (`bodyRaw`), which the compiler emits back in place as
|
|
1883
|
+
* `mj-raw`; any other child ends the run.
|
|
1884
|
+
*/
|
|
1885
|
+
rawRuns(add) {
|
|
1886
|
+
let run = null;
|
|
1887
|
+
return {
|
|
1888
|
+
push: (block) => {
|
|
1889
|
+
if (!run) {
|
|
1890
|
+
run = [];
|
|
1891
|
+
add({ id: this.id(), type: "section", bodyRaw: true, columns: [{ id: this.id(), width: 100, blocks: run }] });
|
|
1892
|
+
}
|
|
1893
|
+
run.push(block);
|
|
1894
|
+
},
|
|
1895
|
+
end: () => {
|
|
1896
|
+
run = null;
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
/** An `mj-raw` child of mj-body or mj-wrapper: its markup, or its compiled output when it has attributes the editor cannot keep. */
|
|
1901
|
+
raw(node, parent, path) {
|
|
1902
|
+
const attrs = { ...node.attributes };
|
|
1903
|
+
const { id, classes } = this.identity(attrs, "el-raw");
|
|
1904
|
+
if (Object.keys(attrs).length > 0 || classes.length > 0) {
|
|
1905
|
+
return this.fallback(node, parent, path, "This mj-raw has attributes (such as position) the editor cannot keep.");
|
|
1906
|
+
}
|
|
1907
|
+
return { id: this.id(id), type: "raw", html: node.content ?? "" };
|
|
1908
|
+
}
|
|
1909
|
+
wrapper(node, path) {
|
|
1910
|
+
const attrs = { ...node.attributes };
|
|
1911
|
+
const { id, classes } = this.identity(attrs, "el-wrapper");
|
|
1912
|
+
const wrapper = { id: this.id(id), type: "wrapper", sections: [] };
|
|
1913
|
+
const gradient = id ? this.gradients.get(id) : void 0;
|
|
1914
|
+
if (gradient) {
|
|
1915
|
+
wrapper.backgroundGradient = gradient;
|
|
1916
|
+
delete attrs["background-color"];
|
|
1917
|
+
}
|
|
1918
|
+
this.mapCommon(attrs, wrapper, {
|
|
1919
|
+
"background-color": "backgroundColor",
|
|
1920
|
+
"background-url": "backgroundImage",
|
|
1921
|
+
"background-position": "backgroundPosition",
|
|
1922
|
+
"background-size": "backgroundSize",
|
|
1923
|
+
border: "border",
|
|
1924
|
+
"border-top": "borderTop",
|
|
1925
|
+
"border-right": "borderRight",
|
|
1926
|
+
"border-bottom": "borderBottom",
|
|
1927
|
+
"border-left": "borderLeft",
|
|
1928
|
+
"border-radius": "borderRadius"
|
|
1929
|
+
});
|
|
1930
|
+
this.mapEnum(attrs, wrapper, "background-repeat", "backgroundRepeat", ["repeat", "no-repeat"]);
|
|
1931
|
+
this.mapEnum(attrs, wrapper, "text-align", "textAlign", ["left", "center", "right"]);
|
|
1932
|
+
if (attrs["full-width"] === "full-width") {
|
|
1933
|
+
wrapper.fullWidth = true;
|
|
1934
|
+
delete attrs["full-width"];
|
|
1935
|
+
}
|
|
1936
|
+
if (attrs.gap !== void 0 && /^[0-9]+(\.[0-9]+)?px$/.test(attrs.gap)) {
|
|
1937
|
+
wrapper.gap = attrs.gap;
|
|
1938
|
+
delete attrs.gap;
|
|
1939
|
+
}
|
|
1940
|
+
this.mapPadding(attrs, wrapper);
|
|
1941
|
+
if (classes.length > 0) wrapper.cssClass = classes.join(" ");
|
|
1942
|
+
const extra = this.extras(attrs, [], path, node);
|
|
1943
|
+
if (extra) wrapper.extraAttributes = extra;
|
|
1944
|
+
const raw = this.rawRuns((section) => wrapper.sections.push(section));
|
|
1945
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1946
|
+
for (const child of node.children) {
|
|
1947
|
+
const n = (counts.get(child.tagName) ?? 0) + 1;
|
|
1948
|
+
counts.set(child.tagName, n);
|
|
1949
|
+
const childPath = `${path} > ${child.tagName}[${n}]`;
|
|
1950
|
+
if (child.tagName === "mj-raw") {
|
|
1951
|
+
if (isComment(child) && EDITOR_COMMENTS.has(child.content.trim())) continue;
|
|
1952
|
+
raw.push(this.raw(child, node, childPath));
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
if (child.tagName === "mj-section") {
|
|
1956
|
+
const section = this.section(child, childPath);
|
|
1957
|
+
if (section) {
|
|
1958
|
+
raw.end();
|
|
1959
|
+
wrapper.sections.push(section);
|
|
1960
|
+
} else {
|
|
1961
|
+
raw.push(this.fallback(child, node, childPath, "The editor cannot hold this section as it is (see the other warnings for it); it stays in its wrapper."));
|
|
1962
|
+
}
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
if (!KNOWN_TAGS.has(child.tagName)) {
|
|
1966
|
+
raw.push(this.fallback(child, node, childPath, "", true));
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
const why = child.tagName === "mj-hero" ? "The editor's Hero block has no content of its own, so an mj-hero with text and buttons cannot become one; it stays in its wrapper." : child.tagName === "mj-wrapper" ? "A wrapper cannot hold another wrapper; MJML renders this one as it can, and it stays in its outer wrapper." : `<${child.tagName}> does not belong directly in mj-wrapper; MJML renders it as it can, and it stays in its wrapper.`;
|
|
1970
|
+
raw.push(this.fallback(child, node, childPath, why));
|
|
1971
|
+
}
|
|
1972
|
+
if (typeof node.content === "string" && node.content.trim()) {
|
|
1973
|
+
this.warn("stray_text", "info", path, node, "Text directly in mj-wrapper is ignored by MJML, and by the import.");
|
|
1974
|
+
}
|
|
1975
|
+
return wrapper;
|
|
1976
|
+
}
|
|
1977
|
+
section(node, path) {
|
|
1978
|
+
const attrs = { ...node.attributes };
|
|
1979
|
+
const { id, classes } = this.identity(attrs, "el-section");
|
|
1980
|
+
const sectionId = this.id(id);
|
|
1981
|
+
const section = { id: sectionId, type: "section", columns: [] };
|
|
1982
|
+
const gradient = id ? this.gradients.get(id) : void 0;
|
|
1983
|
+
if (gradient) {
|
|
1984
|
+
section.backgroundGradient = gradient;
|
|
1985
|
+
delete attrs["background-color"];
|
|
1986
|
+
}
|
|
1987
|
+
this.mapCommon(attrs, section, {
|
|
1988
|
+
"background-color": "backgroundColor",
|
|
1989
|
+
"background-url": "backgroundImage",
|
|
1990
|
+
"background-position": "backgroundPosition",
|
|
1991
|
+
"background-size": "backgroundSize"
|
|
1992
|
+
});
|
|
1993
|
+
if (attrs["background-repeat"] === "repeat" || attrs["background-repeat"] === "no-repeat") {
|
|
1994
|
+
section.backgroundRepeat = attrs["background-repeat"];
|
|
1995
|
+
delete attrs["background-repeat"];
|
|
1996
|
+
}
|
|
1997
|
+
if (attrs["full-width"] === "full-width") {
|
|
1998
|
+
section.fullWidth = true;
|
|
1999
|
+
delete attrs["full-width"];
|
|
2000
|
+
}
|
|
2001
|
+
this.mapPadding(attrs, section);
|
|
2002
|
+
const children = [];
|
|
2003
|
+
for (const child of node.children) {
|
|
2004
|
+
if (isComment(child)) {
|
|
2005
|
+
if (/^<!--\[if/.test(child.content ?? "")) return null;
|
|
2006
|
+
this.warn("comment_dropped", "info", `${path} > comment`, child, "A comment between columns is left out: it renders nothing.");
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
children.push(child);
|
|
2010
|
+
}
|
|
2011
|
+
let columnNodes = children;
|
|
2012
|
+
let parent = node;
|
|
2013
|
+
if (children.length === 1 && children[0].tagName === "mj-group") {
|
|
2014
|
+
const group = children[0];
|
|
2015
|
+
if (Object.keys(group.attributes).length > 0) return null;
|
|
2016
|
+
columnNodes = group.children.filter((c) => {
|
|
2017
|
+
if (!isComment(c)) return true;
|
|
2018
|
+
if (/^<!--\[if/.test(c.content ?? "")) columnNodes = [];
|
|
2019
|
+
return false;
|
|
2020
|
+
});
|
|
2021
|
+
parent = group;
|
|
2022
|
+
if (columnNodes.length > 1) section.noStack = true;
|
|
2023
|
+
}
|
|
2024
|
+
if (columnNodes.length === 0 || columnNodes.some((c) => c.tagName !== "mj-column")) return null;
|
|
2025
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2026
|
+
for (const [index, col] of columnNodes.entries()) {
|
|
2027
|
+
const n = (counts.get(col.tagName) ?? 0) + 1;
|
|
2028
|
+
counts.set(col.tagName, n);
|
|
2029
|
+
section.columns.push(this.column(col, columnNodes.length, `${path}${parent !== node ? " > mj-group[1]" : ""} > mj-column[${index + 1}]`));
|
|
2030
|
+
}
|
|
2031
|
+
const extra = this.extras(attrs, classes, path, node);
|
|
2032
|
+
if (extra) section.extraAttributes = extra;
|
|
2033
|
+
return section;
|
|
2034
|
+
}
|
|
2035
|
+
column(node, siblings, path) {
|
|
2036
|
+
const attrs = { ...node.attributes };
|
|
2037
|
+
const { id, classes } = this.identity(attrs, "el-column");
|
|
2038
|
+
const column = { id: this.id(id), blocks: [] };
|
|
2039
|
+
const gradient = id ? this.gradients.get(id) : void 0;
|
|
2040
|
+
if (gradient) {
|
|
2041
|
+
column.backgroundGradient = gradient;
|
|
2042
|
+
delete attrs["background-color"];
|
|
2043
|
+
}
|
|
2044
|
+
const width = attrs.width;
|
|
2045
|
+
if (width === void 0) {
|
|
2046
|
+
if (!id) column.width = 100 / siblings;
|
|
2047
|
+
} else if (/^[0-9]+(\.[0-9]+)?%$/.test(width) && Number.parseFloat(width) <= 100) {
|
|
2048
|
+
column.width = Number.parseFloat(width);
|
|
2049
|
+
delete attrs.width;
|
|
2050
|
+
} else {
|
|
2051
|
+
this.warn("column_width_px", "info", path, node, `The column width "${width}" is kept as it is; the editor's width control works in percent.`);
|
|
2052
|
+
}
|
|
2053
|
+
this.mapCommon(attrs, column, { "background-color": "backgroundColor" });
|
|
2054
|
+
if (attrs["vertical-align"] === "top" || attrs["vertical-align"] === "middle" || attrs["vertical-align"] === "bottom") {
|
|
2055
|
+
column.verticalAlign = attrs["vertical-align"];
|
|
2056
|
+
delete attrs["vertical-align"];
|
|
2057
|
+
}
|
|
2058
|
+
this.mapPadding(attrs, column);
|
|
2059
|
+
const only = node.children.length === 1 ? node.children[0] : void 0;
|
|
2060
|
+
if (only?.tagName === "mj-raw") {
|
|
2061
|
+
const m = /^\s*<table role="presentation" class="ee-sub-cols" data-ee-subcols="([A-Za-z0-9+/=]+)"/.exec(only.content ?? "");
|
|
2062
|
+
const subs = decodeEditorData(m?.[1]);
|
|
2063
|
+
if (subs && Array.isArray(subs) && subs.length > 0) {
|
|
2064
|
+
column.subColumns = subs;
|
|
2065
|
+
for (const s of subs) this.id(s.id);
|
|
2066
|
+
const extra2 = this.extras(attrs, classes, path, node);
|
|
2067
|
+
if (extra2) column.extraAttributes = extra2;
|
|
2068
|
+
return column;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2072
|
+
const children = node.children;
|
|
2073
|
+
for (let i = 0; i < children.length; i++) {
|
|
2074
|
+
const child = children[i];
|
|
2075
|
+
const n = (counts.get(child.tagName) ?? 0) + 1;
|
|
2076
|
+
counts.set(child.tagName, n);
|
|
2077
|
+
const childPath = `${path} > ${child.tagName}[${n}]`;
|
|
2078
|
+
if (isComment(child) && EDITOR_COMMENTS.has(child.content.trim())) continue;
|
|
2079
|
+
const social = this.socialButtons(children, i);
|
|
2080
|
+
if (social) {
|
|
2081
|
+
column.blocks.push(social.block);
|
|
2082
|
+
i = social.end - 1;
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
const block = this.block(child, childPath);
|
|
2086
|
+
column.blocks.push(block ?? this.fallback(child, node, childPath, this.fallbackReason(child), !KNOWN_TAGS.has(child.tagName)));
|
|
2087
|
+
}
|
|
2088
|
+
const extra = this.extras(attrs, classes, path, node);
|
|
2089
|
+
if (extra) column.extraAttributes = extra;
|
|
2090
|
+
return column;
|
|
2091
|
+
}
|
|
2092
|
+
fallbackReason(node) {
|
|
2093
|
+
switch (node.tagName) {
|
|
2094
|
+
case "mj-social":
|
|
2095
|
+
return "The editor's Social block draws its own icons, so an mj-social would look different as one.";
|
|
2096
|
+
case "mj-table":
|
|
2097
|
+
return "The editor's Table block styles every cell itself, so this table would look different as one.";
|
|
2098
|
+
case "mj-hero":
|
|
2099
|
+
return "The editor's Hero block has no content of its own.";
|
|
2100
|
+
case "mj-button":
|
|
2101
|
+
return "A button without a link or a label cannot be an editor Button block.";
|
|
2102
|
+
case "mj-image":
|
|
2103
|
+
return "An image without a src cannot be an editor Image block.";
|
|
2104
|
+
default:
|
|
2105
|
+
return `The editor has no block for this <${node.tagName}> as it is written.`;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
socialButtons(children, start) {
|
|
2109
|
+
const read = (node) => {
|
|
2110
|
+
if (!node || node.tagName !== "mj-button") return null;
|
|
2111
|
+
const classes = (node.attributes["css-class"] ?? "").split(/\s+/);
|
|
2112
|
+
if (classes[0] !== "el-social-btn" || !classes[1]?.startsWith("el-")) return null;
|
|
2113
|
+
const m = /^el-(.+)-([a-z]+)$/.exec(classes[1]);
|
|
2114
|
+
if (!m || !SOCIAL_PLATFORMS.has(m[2])) return null;
|
|
2115
|
+
return { id: m[1], platform: m[2], a: node.attributes };
|
|
2116
|
+
};
|
|
2117
|
+
const first = read(children[start]);
|
|
2118
|
+
if (!first) return null;
|
|
2119
|
+
const links = [];
|
|
2120
|
+
let end = start;
|
|
2121
|
+
let cur = first;
|
|
2122
|
+
while (cur && cur.id === first.id) {
|
|
2123
|
+
const color = cur.a["background-color"];
|
|
2124
|
+
links.push({ platform: cur.platform, url: cur.a.href ?? "", ...color && color !== SOCIAL_COLORS[cur.platform] ? { color } : {} });
|
|
2125
|
+
end += 1;
|
|
2126
|
+
cur = read(children[end]);
|
|
2127
|
+
}
|
|
2128
|
+
const a = first.a;
|
|
2129
|
+
const block = { id: this.id(first.id), type: "social", mode: "vertical", links };
|
|
2130
|
+
if (a.width) block.iconSize = a.width;
|
|
2131
|
+
if (a.padding) block.iconPadding = a.padding.split(/\s+/)[0];
|
|
2132
|
+
if (a["border-radius"]) block.borderRadius = a["border-radius"];
|
|
2133
|
+
if (a.align === "left" || a.align === "center" || a.align === "right") block.align = a.align;
|
|
2134
|
+
return { block, end };
|
|
2135
|
+
}
|
|
2136
|
+
// Blocks
|
|
2137
|
+
block(node, path) {
|
|
2138
|
+
const attrs = { ...node.attributes };
|
|
2139
|
+
switch (node.tagName) {
|
|
2140
|
+
case "mj-text":
|
|
2141
|
+
return this.text(node, attrs, path);
|
|
2142
|
+
case "mj-image": {
|
|
2143
|
+
if (!attrs.src) return null;
|
|
2144
|
+
const { id, classes } = this.identity(attrs, "el-image");
|
|
2145
|
+
const b = { id: this.id(id), type: "image", src: attrs.src };
|
|
2146
|
+
delete attrs.src;
|
|
2147
|
+
this.mapCommon(attrs, b, { alt: "alt", width: "width", height: "height", href: "href", "border-radius": "borderRadius" });
|
|
2148
|
+
this.mapEnum(attrs, b, "align", "align", ["left", "center", "right"]);
|
|
2149
|
+
this.mapPadding(attrs, b);
|
|
2150
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2151
|
+
}
|
|
2152
|
+
case "mj-button": {
|
|
2153
|
+
const label = node.content ?? "";
|
|
2154
|
+
if (!attrs.href || label.trim() === "") return null;
|
|
2155
|
+
const { id, classes } = this.identity(attrs, "el-button");
|
|
2156
|
+
const b = { id: this.id(id), type: "button", label, href: attrs.href };
|
|
2157
|
+
delete attrs.href;
|
|
2158
|
+
this.mapCommon(attrs, b, {
|
|
2159
|
+
"background-color": "backgroundColor",
|
|
2160
|
+
color: "color",
|
|
2161
|
+
"border-radius": "borderRadius",
|
|
2162
|
+
border: "border",
|
|
2163
|
+
"inner-padding": "innerPadding"
|
|
2164
|
+
});
|
|
2165
|
+
this.mapEnum(attrs, b, "align", "align", ["left", "center", "right"]);
|
|
2166
|
+
this.mapPadding(attrs, b);
|
|
2167
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2168
|
+
}
|
|
2169
|
+
case "mj-divider": {
|
|
2170
|
+
const { id, classes } = this.identity(attrs, "el-divider");
|
|
2171
|
+
const b = { id: this.id(id), type: "divider" };
|
|
2172
|
+
this.mapCommon(attrs, b, { "border-color": "borderColor", "border-width": "borderWidth", width: "width" });
|
|
2173
|
+
this.mapEnum(attrs, b, "border-style", "borderStyle", ["solid", "dashed", "dotted"]);
|
|
2174
|
+
this.mapPadding(attrs, b);
|
|
2175
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2176
|
+
}
|
|
2177
|
+
case "mj-spacer": {
|
|
2178
|
+
if (!attrs.height) return null;
|
|
2179
|
+
const { id, classes } = this.identity(attrs, "el-spacer");
|
|
2180
|
+
const b = { id: this.id(id), type: "spacer", height: attrs.height };
|
|
2181
|
+
delete attrs.height;
|
|
2182
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2183
|
+
}
|
|
2184
|
+
case "mj-raw": {
|
|
2185
|
+
if (isComment(node)) return { id: this.id(), type: "raw", html: node.content ?? "" };
|
|
2186
|
+
const social = /^\s*<table [^>]*data-ee-social="([A-Za-z0-9+/=]+)"/.exec(node.content ?? "");
|
|
2187
|
+
const decoded = decodeEditorData(social?.[1]);
|
|
2188
|
+
if (decoded && decoded.type === "social" && Array.isArray(decoded.links)) {
|
|
2189
|
+
return { ...decoded, id: this.id(decoded.id) };
|
|
2190
|
+
}
|
|
2191
|
+
const { id, classes } = this.identity(attrs, "el-raw");
|
|
2192
|
+
if (Object.keys(attrs).length > 0 || classes.length > 0) return null;
|
|
2193
|
+
return { id: this.id(id), type: "raw", html: node.content ?? "" };
|
|
2194
|
+
}
|
|
2195
|
+
case "mj-hero": {
|
|
2196
|
+
const { id, classes } = this.identity(attrs, "el-hero");
|
|
2197
|
+
if (!id || !attrs["background-url"]) return null;
|
|
2198
|
+
const b = { id: this.id(id), type: "hero", backgroundImage: attrs["background-url"] };
|
|
2199
|
+
delete attrs["background-url"];
|
|
2200
|
+
this.mapCommon(attrs, b, {
|
|
2201
|
+
"background-height": "backgroundHeight",
|
|
2202
|
+
"background-width": "backgroundWidth",
|
|
2203
|
+
"background-color": "backgroundColor"
|
|
2204
|
+
});
|
|
2205
|
+
this.mapEnum(attrs, b, "vertical-align", "verticalAlign", ["top", "middle", "bottom"]);
|
|
2206
|
+
this.mapEnum(attrs, b, "mode", "mode", ["fixed-height", "fluid-height"]);
|
|
2207
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2208
|
+
}
|
|
2209
|
+
case "mj-accordion":
|
|
2210
|
+
return this.accordion(node, attrs, path);
|
|
2211
|
+
case "mj-navbar":
|
|
2212
|
+
return this.navbar(node, attrs, path);
|
|
2213
|
+
case "mj-carousel":
|
|
2214
|
+
return this.carousel(node, attrs, path);
|
|
2215
|
+
case "mj-table":
|
|
2216
|
+
return this.table(node, attrs, path);
|
|
2217
|
+
case "mj-wrapper": {
|
|
2218
|
+
const cls = (attrs["css-class"] ?? "").trim();
|
|
2219
|
+
if (cls === "el-header-wrapper") return { id: this.id(), type: "header", locked: true };
|
|
2220
|
+
if (cls === "el-footer-wrapper") return { id: this.id(), type: "footer", locked: true };
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2223
|
+
default:
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
text(node, attrs, path) {
|
|
2228
|
+
const { id, classes } = this.identity(attrs, "el-text");
|
|
2229
|
+
const b = { id: this.id(id), type: "text", content: node.content ?? "" };
|
|
2230
|
+
this.mapEnum(attrs, b, "align", "align", ["left", "center", "right", "justify"]);
|
|
2231
|
+
this.mapCommon(attrs, b, { color: "color", "font-size": "fontSize", "font-family": "fontFamily", "line-height": "lineHeight" });
|
|
2232
|
+
this.mapPadding(attrs, b);
|
|
2233
|
+
const styles = [];
|
|
2234
|
+
if (b.align) styles.push(`text-align:${b.align}`);
|
|
2235
|
+
if (b.color) styles.push(`color:${b.color}`);
|
|
2236
|
+
if (b.fontSize) styles.push(`font-size:${b.fontSize}`);
|
|
2237
|
+
if (b.fontFamily) styles.push(`font-family:${b.fontFamily}`);
|
|
2238
|
+
if (b.lineHeight) styles.push(`line-height:${b.lineHeight}`);
|
|
2239
|
+
if (styles.length > 0) {
|
|
2240
|
+
const open = `<div style="${styles.join(";")}">`;
|
|
2241
|
+
if (b.content.startsWith(open) && b.content.endsWith("</div>")) {
|
|
2242
|
+
const inner = b.content.slice(open.length, -"</div>".length);
|
|
2243
|
+
if (!/<\/div>/i.test(inner) || balancedDivs(inner)) b.content = inner;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2247
|
+
}
|
|
2248
|
+
accordion(node, attrs, path) {
|
|
2249
|
+
const items = [];
|
|
2250
|
+
for (const el of node.children) {
|
|
2251
|
+
if (isComment(el)) return null;
|
|
2252
|
+
if (el.tagName !== "mj-accordion-element" || Object.keys(el.attributes).length > 0) return null;
|
|
2253
|
+
const parts = el.children.filter((c) => !isComment(c));
|
|
2254
|
+
const title = parts.find((c) => c.tagName === "mj-accordion-title");
|
|
2255
|
+
const text = parts.find((c) => c.tagName === "mj-accordion-text");
|
|
2256
|
+
if (parts.length !== 2 || !title || !text || parts[0] !== title) return null;
|
|
2257
|
+
if (Object.keys(title.attributes).length > 0 || Object.keys(text.attributes).length > 0) return null;
|
|
2258
|
+
items.push({ title: title.content ?? "", content: text.content ?? "" });
|
|
2259
|
+
}
|
|
2260
|
+
const { id, classes } = this.identity(attrs, "el-accordion");
|
|
2261
|
+
const b = { id: this.id(id), type: "accordion", items };
|
|
2262
|
+
this.mapEnum(attrs, b, "icon-position", "iconPosition", ["left", "right"]);
|
|
2263
|
+
this.mapCommon(attrs, b, { border: "borderColor", "font-family": "fontFamily" });
|
|
2264
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2265
|
+
}
|
|
2266
|
+
navbar(node, attrs, path) {
|
|
2267
|
+
const links = [];
|
|
2268
|
+
for (const el of node.children) {
|
|
2269
|
+
if (el.tagName !== "mj-navbar-link") return null;
|
|
2270
|
+
const { href, color, ...rest } = el.attributes;
|
|
2271
|
+
if (!href || Object.keys(rest).length > 0) return null;
|
|
2272
|
+
links.push({ href, label: el.content ?? "", ...color ? { color } : {} });
|
|
2273
|
+
}
|
|
2274
|
+
const { id, classes } = this.identity(attrs, "el-navbar");
|
|
2275
|
+
const b = { id: this.id(id), type: "navbar", links };
|
|
2276
|
+
if (attrs.hamburger === "hamburger") {
|
|
2277
|
+
b.hamburger = true;
|
|
2278
|
+
delete attrs.hamburger;
|
|
2279
|
+
}
|
|
2280
|
+
this.mapCommon(attrs, b, { "base-url": "baseUrl", "ico-color": "icoColor" });
|
|
2281
|
+
this.mapEnum(attrs, b, "align", "align", ["left", "center", "right"]);
|
|
2282
|
+
this.mapPadding(attrs, b);
|
|
2283
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2284
|
+
}
|
|
2285
|
+
carousel(node, attrs, path) {
|
|
2286
|
+
const images = [];
|
|
2287
|
+
for (const el of node.children) {
|
|
2288
|
+
if (el.tagName !== "mj-carousel-image") return null;
|
|
2289
|
+
const { src, alt, href, "thumbnails-src": thumbnailSrc, ...rest } = el.attributes;
|
|
2290
|
+
if (!src || Object.keys(rest).length > 0) return null;
|
|
2291
|
+
images.push({ src, ...alt ? { alt } : {}, ...href ? { href } : {}, ...thumbnailSrc ? { thumbnailSrc } : {} });
|
|
2292
|
+
}
|
|
2293
|
+
const { id, classes } = this.identity(attrs, "el-carousel");
|
|
2294
|
+
const b = { id: this.id(id), type: "carousel", images };
|
|
2295
|
+
this.mapEnum(attrs, b, "thumbnails", "thumbnails", ["visible", "hidden"]);
|
|
2296
|
+
this.mapCommon(attrs, b, { "border-radius": "borderRadius", "icon-width": "iconWidth", "tb-border-radius": "tbBorderRadius" });
|
|
2297
|
+
this.mapPadding(attrs, b);
|
|
2298
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2299
|
+
}
|
|
2300
|
+
/** Only the editor's own table (`el-table`), whose markup the compiler writes in one exact shape. */
|
|
2301
|
+
table(node, attrs, path) {
|
|
2302
|
+
const { id, classes } = this.identity(attrs, "el-table");
|
|
2303
|
+
if (!id) return null;
|
|
2304
|
+
const lines = (node.content ?? "").split("\n");
|
|
2305
|
+
const head = /^<tr style="background-color: #f5f5f5;">(.*)<\/tr>$/.exec(lines[0] ?? "");
|
|
2306
|
+
if (!head) return null;
|
|
2307
|
+
const cells = (row, tag, style) => {
|
|
2308
|
+
const out = [];
|
|
2309
|
+
const open = `<${tag} style="${style}">`;
|
|
2310
|
+
let rest = row;
|
|
2311
|
+
while (rest.length > 0) {
|
|
2312
|
+
if (!rest.startsWith(open)) return null;
|
|
2313
|
+
const close = rest.indexOf(`</${tag}>`);
|
|
2314
|
+
if (close < 0) return null;
|
|
2315
|
+
out.push(rest.slice(open.length, close));
|
|
2316
|
+
rest = rest.slice(close + tag.length + 3);
|
|
2317
|
+
}
|
|
2318
|
+
return out;
|
|
2319
|
+
};
|
|
2320
|
+
const headers = cells(head[1], "th", "padding: 8px; border-bottom: 1px solid #ddd; text-align: left;");
|
|
2321
|
+
if (!headers) return null;
|
|
2322
|
+
const rows = [];
|
|
2323
|
+
for (const line of lines.slice(1)) {
|
|
2324
|
+
const m = /^<tr>(.*)<\/tr>$/.exec(line);
|
|
2325
|
+
const row = m ? cells(m[1], "td", "padding: 8px; border-bottom: 1px solid #eee;") : null;
|
|
2326
|
+
if (!row) return null;
|
|
2327
|
+
rows.push(row);
|
|
2328
|
+
}
|
|
2329
|
+
const b = { id: this.id(id), type: "table", headers, rows };
|
|
2330
|
+
this.mapEnum(attrs, b, "align", "align", ["left", "center", "right"]);
|
|
2331
|
+
this.mapCommon(attrs, b, {
|
|
2332
|
+
color: "color",
|
|
2333
|
+
"font-family": "fontFamily",
|
|
2334
|
+
"font-size": "fontSize",
|
|
2335
|
+
cellpadding: "cellpadding",
|
|
2336
|
+
cellspacing: "cellspacing",
|
|
2337
|
+
border: "border"
|
|
2338
|
+
});
|
|
2339
|
+
this.mapPadding(attrs, b);
|
|
2340
|
+
return this.finish(b, attrs, classes, path, node);
|
|
2341
|
+
}
|
|
2342
|
+
// Attribute helpers
|
|
2343
|
+
mapCommon(attrs, target, map) {
|
|
2344
|
+
for (const [attr, field] of Object.entries(map)) {
|
|
2345
|
+
if (attrs[attr] === void 0) continue;
|
|
2346
|
+
target[field] = attrs[attr];
|
|
2347
|
+
delete attrs[attr];
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
mapEnum(attrs, target, attr, field, allowed) {
|
|
2351
|
+
const v = attrs[attr];
|
|
2352
|
+
if (v !== void 0 && allowed.includes(v)) {
|
|
2353
|
+
target[field] = v;
|
|
2354
|
+
delete attrs[attr];
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
mapPadding(attrs, target) {
|
|
2358
|
+
if (attrs.padding === void 0) return;
|
|
2359
|
+
const spacing = parsePadding(attrs.padding);
|
|
2360
|
+
if (spacing) {
|
|
2361
|
+
target.padding = spacing;
|
|
2362
|
+
delete attrs.padding;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
finish(block, attrs, classes, path, node) {
|
|
2366
|
+
const extra = this.extras(attrs, classes, path, node);
|
|
2367
|
+
if (extra) block.extraAttributes = extra;
|
|
2368
|
+
return block;
|
|
2369
|
+
}
|
|
2370
|
+
// Fallbacks: compile the whole source once, with each fallback marked, and cut them out.
|
|
2371
|
+
resolveFallbacks(metadata) {
|
|
2372
|
+
if (this.fallbacks.length === 0) return;
|
|
2373
|
+
const marker = (i, edge) => ({
|
|
2374
|
+
tagName: "mj-raw",
|
|
2375
|
+
attributes: {},
|
|
2376
|
+
content: `<!--ee-import-${i}-${edge}-->`,
|
|
2377
|
+
children: []
|
|
2378
|
+
});
|
|
2379
|
+
for (const [i, f] of this.fallbacks.entries()) {
|
|
2380
|
+
const at = f.parent.children.indexOf(f.node);
|
|
2381
|
+
f.parent.children.splice(at, 1, marker(i, "start"), f.node, marker(i, "end"));
|
|
2382
|
+
}
|
|
2383
|
+
let html = "";
|
|
2384
|
+
try {
|
|
2385
|
+
html = (0, import_mjml3.default)(serialize(this.root), { ignoreIncludes: true, validationLevel: "skip", minify: false, keepComments: true }).html;
|
|
2386
|
+
} catch {
|
|
2387
|
+
html = "";
|
|
2388
|
+
}
|
|
2389
|
+
const needed = [];
|
|
2390
|
+
for (const [i, f] of this.fallbacks.entries()) {
|
|
2391
|
+
const start = html.indexOf(`<!--ee-import-${i}-start-->`);
|
|
2392
|
+
const end = html.indexOf(`<!--ee-import-${i}-end-->`);
|
|
2393
|
+
let fragment = start >= 0 && end > start ? html.slice(start + `<!--ee-import-${i}-start-->`.length, end).trim() : "";
|
|
2394
|
+
if (f.unknown || fragment === "") {
|
|
2395
|
+
const source = serialize(f.node).replace(/--/g, "- -");
|
|
2396
|
+
fragment = `<!-- Imported MJML that renders nothing: ${source} -->`;
|
|
2397
|
+
}
|
|
2398
|
+
f.block.html = fragment;
|
|
2399
|
+
needed.push(fragment);
|
|
2400
|
+
}
|
|
2401
|
+
const css = supportingCss(html, needed.join("\n"), metadata.breakpoint);
|
|
2402
|
+
if (css) metadata.customCSS = metadata.customCSS ? `${metadata.customCSS}
|
|
2403
|
+
${css}` : css;
|
|
2404
|
+
}
|
|
2405
|
+
summarize() {
|
|
2406
|
+
if (this.kept.size > 0) {
|
|
2407
|
+
const list = [...this.kept.entries()].sort((a, b) => b[1] - a[1]).map(([name, n]) => `${name} (${n})`).join(", ");
|
|
2408
|
+
this.warnings.push({
|
|
2409
|
+
severity: "info",
|
|
2410
|
+
code: "attribute_kept",
|
|
2411
|
+
path: "mj-body",
|
|
2412
|
+
message: `Attributes the editor has no control for are kept and apply to the mail as before: ${list}. The editor canvas may not show their effect.`
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
if (this.hasOwnDefaults || this.usesMjClass) {
|
|
2416
|
+
this.warnings.push({
|
|
2417
|
+
severity: "info",
|
|
2418
|
+
code: "document_defaults",
|
|
2419
|
+
path: "mj-head > mj-attributes",
|
|
2420
|
+
message: "The document-wide defaults (mj-attributes, mj-class) are kept and apply to the mail. The editor canvas shows blocks without them, so the preview is the reference for how the mail looks."
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
function balancedDivs(html) {
|
|
2426
|
+
let depth = 0;
|
|
2427
|
+
for (const m of html.matchAll(/<(\/?)div\b[^>]*>/gi)) {
|
|
2428
|
+
depth += m[1] ? -1 : 1;
|
|
2429
|
+
if (depth < 0) return false;
|
|
2430
|
+
}
|
|
2431
|
+
return depth === 0;
|
|
2432
|
+
}
|
|
2433
|
+
function supportingCss(fullHtml, fragments, breakpoint = "480px") {
|
|
2434
|
+
const out = [];
|
|
2435
|
+
const classes = new Set([...fragments.matchAll(/mj-column-(?:per|px)-[0-9-]+/g)].map((m) => m[0]));
|
|
2436
|
+
const rules = [];
|
|
2437
|
+
for (const cls of classes) {
|
|
2438
|
+
const m = new RegExp(`\\.${cls}\\s*\\{[^}]*\\}`).exec(fullHtml);
|
|
2439
|
+
if (m) rules.push(m[0]);
|
|
2440
|
+
}
|
|
2441
|
+
if (rules.length > 0) out.push(`@media only screen and (min-width:${breakpoint}) { ${rules.join(" ")} }`);
|
|
2442
|
+
const styles = [...fullHtml.matchAll(/<style type="text\/css">([\s\S]*?)<\/style>/g)].map((m) => m[1]);
|
|
2443
|
+
for (const token of ["mj-accordion", "mj-carousel", "mj-menu"]) {
|
|
2444
|
+
if (!fragments.includes(token)) continue;
|
|
2445
|
+
for (const s of styles) if (s.includes(token) && !out.includes(s.trim())) out.push(s.trim());
|
|
2446
|
+
}
|
|
2447
|
+
return out.join("\n");
|
|
2448
|
+
}
|
|
2449
|
+
function importMjml(source) {
|
|
2450
|
+
if (typeof source !== "string") throw new MjmlImportError("not_mjml", "The MJML must be a string.");
|
|
2451
|
+
scanMjml(source, ENDING_TAGS);
|
|
2452
|
+
const root = parseMjml(source);
|
|
2453
|
+
if (root.tagName !== "mjml") throw new MjmlImportError("not_mjml", "The document must start with <mjml>.");
|
|
2454
|
+
const head = root.children.find((c) => c.tagName === "mj-head");
|
|
2455
|
+
const body = root.children.find((c) => c.tagName === "mj-body");
|
|
2456
|
+
if (!body) throw new MjmlImportError("not_mjml", "The document has no <mj-body>.");
|
|
2457
|
+
const importer = new Importer(root);
|
|
2458
|
+
for (const [i, c] of root.children.entries()) {
|
|
2459
|
+
if (c !== head && c !== body && !isComment(c)) {
|
|
2460
|
+
importer.warn("stray_text", "info", `mjml > ${c.tagName}[${i + 1}]`, c, `<${c.tagName}> outside mj-head and mj-body is ignored by MJML, and by the import.`, true);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
const metadata = importer.head(head, body);
|
|
2464
|
+
const sections = importer.body(body);
|
|
2465
|
+
importer.resolveFallbacks(metadata);
|
|
2466
|
+
importer.summarize();
|
|
2467
|
+
const document = { version: CURRENT_TEMPLATE_VERSION, metadata, sections };
|
|
2468
|
+
try {
|
|
2469
|
+
migrateTemplate(document);
|
|
2470
|
+
} catch (err) {
|
|
2471
|
+
const detail = isTemplateMigrationError(err) ? err.message : String(err);
|
|
2472
|
+
throw new MjmlImportError("invalid_document", `The imported document does not pass the editor's schema: ${detail}`);
|
|
2473
|
+
}
|
|
2474
|
+
return { document, warnings: importer.warnings };
|
|
2475
|
+
}
|
|
2476
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2477
|
+
0 && (module.exports = {
|
|
2478
|
+
DEFAULT_MJML_ATTRIBUTES,
|
|
2479
|
+
EDITOR_DATA_ATTRIBUTE,
|
|
2480
|
+
MAX_MJML_BYTES,
|
|
2481
|
+
MAX_MJML_DEPTH,
|
|
2482
|
+
MAX_MJML_ELEMENTS,
|
|
2483
|
+
MJMLCompiler,
|
|
2484
|
+
MJMLExporter,
|
|
2485
|
+
MjmlImportError,
|
|
2486
|
+
createMJMLCompiler,
|
|
2487
|
+
createMJMLExporter,
|
|
2488
|
+
encodeEditorData,
|
|
2489
|
+
exportTemplate,
|
|
2490
|
+
importMjml,
|
|
2491
|
+
isMjmlImportError
|
|
2492
|
+
});
|