@marlinjai/email-editor-core 0.3.0 → 0.4.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.
@@ -0,0 +1,30 @@
1
+ import {
2
+ EDITOR_MARKER,
3
+ MjmlBuilder,
4
+ mjmlOptions,
5
+ stripEditorMarkup
6
+ } from "./chunk-OOCYGZMB.mjs";
7
+
8
+ // src/browser.ts
9
+ import mjml2html from "mjml-browser";
10
+ var builder = new MjmlBuilder();
11
+ async function compileInBrowser(template, options = {}) {
12
+ try {
13
+ const mjml = builder.toMJML(template, { editor: options.editor });
14
+ const result = await mjml2html(mjml, mjmlOptions(options));
15
+ return {
16
+ mjml,
17
+ html: result.html,
18
+ errors: result.errors.length > 0 ? result.errors.map((e) => e.formattedMessage) : void 0
19
+ };
20
+ } catch (error) {
21
+ return { mjml: "", html: "", errors: [error instanceof Error ? error.message : "Unknown compilation error"] };
22
+ }
23
+ }
24
+ export {
25
+ EDITOR_MARKER,
26
+ MjmlBuilder,
27
+ compileInBrowser,
28
+ mjmlOptions,
29
+ stripEditorMarkup
30
+ };
@@ -0,0 +1,490 @@
1
+ // src/schema/validation.ts
2
+ import { z } from "zod";
3
+ var SpacingSchema = z.object({
4
+ top: z.string().optional(),
5
+ right: z.string().optional(),
6
+ bottom: z.string().optional(),
7
+ left: z.string().optional()
8
+ });
9
+ var GradientStopSchema = z.object({
10
+ color: z.string().min(1),
11
+ position: z.number().min(0).max(100)
12
+ });
13
+ var BackgroundGradientSchema = z.object({
14
+ type: z.enum(["linear", "radial"]),
15
+ angle: z.number().min(0).max(360),
16
+ stops: z.array(GradientStopSchema).min(1)
17
+ });
18
+ var ExtraAttributesSchema = z.record(z.string().regex(/^[a-z][a-z0-9-]*$/), z.string());
19
+ var MjmlHeadSchema = z.object({
20
+ attributes: z.string().optional(),
21
+ bodyAttributes: ExtraAttributesSchema.optional(),
22
+ headRaw: z.string().optional()
23
+ });
24
+ var CustomFontSchema = z.object({
25
+ name: z.string(),
26
+ href: z.string()
27
+ });
28
+ var TemplateMetadataSchema = z.object({
29
+ name: z.string().optional(),
30
+ subject: z.string().optional(),
31
+ previewText: z.string().optional(),
32
+ title: z.string().optional(),
33
+ // Epoch milliseconds (what the editor's store emits) or an ISO 8601 string.
34
+ createdAt: z.union([z.string(), z.number()]).optional(),
35
+ updatedAt: z.union([z.string(), z.number()]).optional(),
36
+ // Head component settings
37
+ fonts: z.array(CustomFontSchema).optional(),
38
+ breakpoint: z.string().optional(),
39
+ customCSS: z.string().optional(),
40
+ inlineCSS: z.string().optional(),
41
+ mjmlHead: MjmlHeadSchema.optional()
42
+ });
43
+ var TextBlockSchema = z.object({
44
+ id: z.string(),
45
+ type: z.literal("text"),
46
+ hidden: z.boolean().optional(),
47
+ extraAttributes: ExtraAttributesSchema.optional(),
48
+ content: z.string(),
49
+ align: z.enum(["left", "center", "right", "justify"]).optional(),
50
+ color: z.string().optional(),
51
+ fontSize: z.string().optional(),
52
+ fontFamily: z.string().optional(),
53
+ padding: SpacingSchema.optional(),
54
+ lineHeight: z.string().optional()
55
+ });
56
+ var ImageBlockSchema = z.object({
57
+ id: z.string(),
58
+ type: z.literal("image"),
59
+ hidden: z.boolean().optional(),
60
+ extraAttributes: ExtraAttributesSchema.optional(),
61
+ src: z.string().min(1),
62
+ // Allow any non-empty string, not just URLs
63
+ alt: z.string().optional(),
64
+ width: z.string().optional(),
65
+ height: z.string().optional(),
66
+ align: z.enum(["left", "center", "right"]).optional(),
67
+ href: z.string().optional(),
68
+ // Allow any string for link
69
+ padding: SpacingSchema.optional(),
70
+ borderRadius: z.string().optional()
71
+ // Rounded corners
72
+ });
73
+ var ButtonBlockSchema = z.object({
74
+ id: z.string(),
75
+ type: z.literal("button"),
76
+ hidden: z.boolean().optional(),
77
+ extraAttributes: ExtraAttributesSchema.optional(),
78
+ label: z.string().min(1),
79
+ href: z.string().min(1),
80
+ // Allow any non-empty string
81
+ align: z.enum(["left", "center", "right"]).optional(),
82
+ backgroundColor: z.string().optional(),
83
+ color: z.string().optional(),
84
+ borderRadius: z.string().optional(),
85
+ border: z.string().optional(),
86
+ // CSS border shorthand
87
+ padding: SpacingSchema.optional(),
88
+ innerPadding: z.string().optional()
89
+ });
90
+ var DividerBlockSchema = z.object({
91
+ id: z.string(),
92
+ type: z.literal("divider"),
93
+ hidden: z.boolean().optional(),
94
+ extraAttributes: ExtraAttributesSchema.optional(),
95
+ borderColor: z.string().optional(),
96
+ borderWidth: z.string().optional(),
97
+ borderStyle: z.enum(["solid", "dashed", "dotted"]).optional(),
98
+ width: z.string().optional(),
99
+ // Width of the divider line
100
+ padding: SpacingSchema.optional()
101
+ });
102
+ var SpacerBlockSchema = z.object({
103
+ id: z.string(),
104
+ type: z.literal("spacer"),
105
+ hidden: z.boolean().optional(),
106
+ extraAttributes: ExtraAttributesSchema.optional(),
107
+ height: z.string()
108
+ });
109
+ var HeaderBlockSchema = z.object({
110
+ id: z.string(),
111
+ type: z.literal("header"),
112
+ hidden: z.boolean().optional(),
113
+ extraAttributes: ExtraAttributesSchema.optional(),
114
+ locked: z.literal(true)
115
+ });
116
+ var FooterBlockSchema = z.object({
117
+ id: z.string(),
118
+ type: z.literal("footer"),
119
+ hidden: z.boolean().optional(),
120
+ extraAttributes: ExtraAttributesSchema.optional(),
121
+ locked: z.literal(true)
122
+ });
123
+ var SocialBlockSchema = z.object({
124
+ id: z.string(),
125
+ type: z.literal("social"),
126
+ hidden: z.boolean().optional(),
127
+ extraAttributes: ExtraAttributesSchema.optional(),
128
+ mode: z.enum(["horizontal", "vertical"]).optional(),
129
+ align: z.enum(["left", "center", "right"]).optional(),
130
+ iconSize: z.string().optional(),
131
+ iconPadding: z.string().optional(),
132
+ borderRadius: z.string().optional(),
133
+ // For round icons
134
+ links: z.array(
135
+ z.object({
136
+ platform: z.enum(["facebook", "twitter", "instagram", "linkedin", "youtube", "pinterest", "github"]),
137
+ url: z.string(),
138
+ color: z.string().optional()
139
+ // Per-icon color override (uses platform default if not set)
140
+ })
141
+ )
142
+ });
143
+ var HeroBlockSchema = z.object({
144
+ id: z.string(),
145
+ type: z.literal("hero"),
146
+ hidden: z.boolean().optional(),
147
+ extraAttributes: ExtraAttributesSchema.optional(),
148
+ backgroundImage: z.string().min(1),
149
+ backgroundHeight: z.string().optional(),
150
+ backgroundWidth: z.string().optional(),
151
+ backgroundColor: z.string().optional(),
152
+ verticalAlign: z.enum(["top", "middle", "bottom"]).optional(),
153
+ mode: z.enum(["fluid-height", "fixed-height"]).optional()
154
+ });
155
+ var AccordionBlockSchema = z.object({
156
+ id: z.string(),
157
+ type: z.literal("accordion"),
158
+ hidden: z.boolean().optional(),
159
+ extraAttributes: ExtraAttributesSchema.optional(),
160
+ items: z.array(
161
+ z.object({
162
+ title: z.string(),
163
+ content: z.string()
164
+ })
165
+ ),
166
+ iconPosition: z.enum(["left", "right"]).optional(),
167
+ borderColor: z.string().optional(),
168
+ fontFamily: z.string().optional()
169
+ });
170
+ var RawBlockSchema = z.object({
171
+ id: z.string(),
172
+ type: z.literal("raw"),
173
+ hidden: z.boolean().optional(),
174
+ extraAttributes: ExtraAttributesSchema.optional(),
175
+ html: z.string()
176
+ });
177
+ var NavbarBlockSchema = z.object({
178
+ id: z.string(),
179
+ type: z.literal("navbar"),
180
+ hidden: z.boolean().optional(),
181
+ extraAttributes: ExtraAttributesSchema.optional(),
182
+ links: z.array(
183
+ z.object({
184
+ label: z.string(),
185
+ href: z.string(),
186
+ color: z.string().optional()
187
+ })
188
+ ),
189
+ hamburger: z.boolean().optional(),
190
+ baseUrl: z.string().optional(),
191
+ align: z.enum(["left", "center", "right"]).optional(),
192
+ icoColor: z.string().optional(),
193
+ padding: SpacingSchema.optional()
194
+ });
195
+ var CarouselBlockSchema = z.object({
196
+ id: z.string(),
197
+ type: z.literal("carousel"),
198
+ hidden: z.boolean().optional(),
199
+ extraAttributes: ExtraAttributesSchema.optional(),
200
+ images: z.array(
201
+ z.object({
202
+ src: z.string(),
203
+ alt: z.string().optional(),
204
+ href: z.string().optional(),
205
+ thumbnailSrc: z.string().optional()
206
+ })
207
+ ),
208
+ thumbnails: z.enum(["visible", "hidden"]).optional(),
209
+ borderRadius: z.string().optional(),
210
+ iconWidth: z.string().optional(),
211
+ tbBorderRadius: z.string().optional(),
212
+ padding: SpacingSchema.optional()
213
+ });
214
+ var TableBlockSchema = z.object({
215
+ id: z.string(),
216
+ type: z.literal("table"),
217
+ hidden: z.boolean().optional(),
218
+ extraAttributes: ExtraAttributesSchema.optional(),
219
+ headers: z.array(z.string()),
220
+ rows: z.array(z.array(z.string())),
221
+ align: z.enum(["left", "center", "right"]).optional(),
222
+ color: z.string().optional(),
223
+ fontFamily: z.string().optional(),
224
+ fontSize: z.string().optional(),
225
+ cellpadding: z.string().optional(),
226
+ cellspacing: z.string().optional(),
227
+ border: z.string().optional(),
228
+ padding: SpacingSchema.optional()
229
+ });
230
+ var BlockSchema = z.discriminatedUnion("type", [
231
+ TextBlockSchema,
232
+ ImageBlockSchema,
233
+ ButtonBlockSchema,
234
+ DividerBlockSchema,
235
+ SpacerBlockSchema,
236
+ HeaderBlockSchema,
237
+ FooterBlockSchema,
238
+ SocialBlockSchema,
239
+ HeroBlockSchema,
240
+ AccordionBlockSchema,
241
+ RawBlockSchema,
242
+ NavbarBlockSchema,
243
+ CarouselBlockSchema,
244
+ TableBlockSchema
245
+ ]);
246
+ var ColumnSchema = z.object({
247
+ id: z.string(),
248
+ width: z.number().min(0).max(100).optional(),
249
+ backgroundColor: z.string().optional(),
250
+ backgroundGradient: BackgroundGradientSchema.optional(),
251
+ padding: SpacingSchema.optional(),
252
+ verticalAlign: z.enum(["top", "middle", "bottom"]).optional(),
253
+ // Vertical content alignment
254
+ hidden: z.boolean().optional(),
255
+ extraAttributes: ExtraAttributesSchema.optional(),
256
+ blocks: z.array(BlockSchema)
257
+ });
258
+ var sectionFields = {
259
+ id: z.string(),
260
+ type: z.literal("section"),
261
+ backgroundColor: z.string().optional(),
262
+ backgroundImage: z.string().optional(),
263
+ backgroundPosition: z.string().optional(),
264
+ backgroundRepeat: z.enum(["repeat", "no-repeat"]).optional(),
265
+ backgroundSize: z.string().optional(),
266
+ backgroundGradient: BackgroundGradientSchema.optional(),
267
+ padding: SpacingSchema.optional(),
268
+ noStack: z.boolean().optional(),
269
+ fullWidth: z.boolean().optional(),
270
+ hidden: z.boolean().optional(),
271
+ extraAttributes: ExtraAttributesSchema.optional(),
272
+ bodyRaw: z.boolean().optional(),
273
+ columns: z.array(ColumnSchema).min(1)
274
+ };
275
+ var SectionSchema = z.object({
276
+ ...sectionFields,
277
+ isWrapper: z.undefined({ invalid_type_error: "isWrapper is a schema 1.0 field; a 1.1 document holds a wrapper instead" }).optional()
278
+ });
279
+ var SectionSchemaV1_0 = z.object({
280
+ ...sectionFields,
281
+ isWrapper: z.boolean().optional()
282
+ });
283
+ var WrapperSchema = z.object({
284
+ id: z.string(),
285
+ type: z.literal("wrapper"),
286
+ hidden: z.boolean().optional(),
287
+ backgroundColor: z.string().optional(),
288
+ backgroundImage: z.string().optional(),
289
+ backgroundGradient: BackgroundGradientSchema.optional(),
290
+ backgroundPosition: z.string().optional(),
291
+ backgroundRepeat: z.enum(["repeat", "no-repeat"]).optional(),
292
+ backgroundSize: z.string().optional(),
293
+ border: z.string().optional(),
294
+ borderTop: z.string().optional(),
295
+ borderRight: z.string().optional(),
296
+ borderBottom: z.string().optional(),
297
+ borderLeft: z.string().optional(),
298
+ borderRadius: z.string().optional(),
299
+ padding: SpacingSchema.optional(),
300
+ fullWidth: z.boolean().optional(),
301
+ cssClass: z.string().optional(),
302
+ gap: z.string().regex(/^[0-9]+(\.[0-9]+)?px$/, "gap is a length in px, e.g. 16px").optional(),
303
+ textAlign: z.enum(["left", "center", "right"]).optional(),
304
+ extraAttributes: ExtraAttributesSchema.optional(),
305
+ sections: z.array(SectionSchema)
306
+ });
307
+ var TopLevelItemSchema = z.discriminatedUnion("type", [SectionSchema, WrapperSchema]);
308
+ var EmailTemplateSchemaV1_0 = z.object({
309
+ id: z.string().optional(),
310
+ version: z.literal("1.0"),
311
+ metadata: TemplateMetadataSchema,
312
+ sections: z.array(SectionSchemaV1_0)
313
+ });
314
+ var EmailTemplateSchemaV1_1 = z.object({
315
+ id: z.string().optional(),
316
+ version: z.literal("1.1"),
317
+ metadata: TemplateMetadataSchema,
318
+ sections: z.array(TopLevelItemSchema)
319
+ });
320
+ var EmailTemplateSchema = z.discriminatedUnion("version", [EmailTemplateSchemaV1_0, EmailTemplateSchemaV1_1]);
321
+ function validateTemplate(template) {
322
+ return EmailTemplateSchema.safeParse(template);
323
+ }
324
+
325
+ // src/schema/migrate.ts
326
+ import { nanoid } from "nanoid";
327
+ var CURRENT_TEMPLATE_VERSION = "1.1";
328
+ var SUPPORTED_TEMPLATE_VERSIONS = ["1.0", CURRENT_TEMPLATE_VERSION];
329
+ var TemplateMigrationError = class extends Error {
330
+ constructor(code, message, options = {}) {
331
+ super(message);
332
+ this.name = "TemplateMigrationError";
333
+ this.code = code;
334
+ this.version = options.version;
335
+ this.issues = options.issues ?? [];
336
+ }
337
+ };
338
+ function isTemplateMigrationError(error) {
339
+ return error instanceof TemplateMigrationError;
340
+ }
341
+ function parseVersion(version) {
342
+ const match = /^(\d+)\.(\d+)$/.exec(version);
343
+ if (!match) return null;
344
+ return [Number(match[1]), Number(match[2])];
345
+ }
346
+ function compareVersions(a, b) {
347
+ return a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1];
348
+ }
349
+ function migrateTemplate(doc) {
350
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
351
+ throw new TemplateMigrationError(
352
+ "INVALID_INPUT",
353
+ `Expected a template document object, received ${doc === null ? "null" : Array.isArray(doc) ? "an array" : typeof doc}.`
354
+ );
355
+ }
356
+ const version = doc.version;
357
+ if (typeof version !== "string" || version.length === 0) {
358
+ throw new TemplateMigrationError(
359
+ "MISSING_VERSION",
360
+ 'The template document has no "version" field, so its schema cannot be determined.'
361
+ );
362
+ }
363
+ const parsed = parseVersion(version);
364
+ if (!parsed) {
365
+ throw new TemplateMigrationError(
366
+ "UNSUPPORTED_VERSION",
367
+ `Template schema version "${version}" is not of the form "major.minor".`,
368
+ { version }
369
+ );
370
+ }
371
+ const current = parseVersion(CURRENT_TEMPLATE_VERSION);
372
+ if (compareVersions(parsed, current) > 0) {
373
+ throw new TemplateMigrationError(
374
+ "NEWER_VERSION",
375
+ `Template schema version "${version}" is newer than this editor supports (${CURRENT_TEMPLATE_VERSION}). Upgrade the @marlinjai/email-editor packages to open it.`,
376
+ { version }
377
+ );
378
+ }
379
+ if (!SUPPORTED_TEMPLATE_VERSIONS.includes(version)) {
380
+ throw new TemplateMigrationError(
381
+ "UNSUPPORTED_VERSION",
382
+ `Template schema version "${version}" is not supported and has no migration to ${CURRENT_TEMPLATE_VERSION}.`,
383
+ { version }
384
+ );
385
+ }
386
+ let next = doc;
387
+ if (version === "1.0") {
388
+ assertValid(EmailTemplateSchemaV1_0, next, version);
389
+ next = migrateV1_0ToV1_1(next);
390
+ }
391
+ assertValid(EmailTemplateSchemaV1_1, next, version);
392
+ return next;
393
+ }
394
+ function assertValid(schema, doc, version) {
395
+ const result = schema.safeParse(doc);
396
+ if (result.success) return;
397
+ const issues = result.error.issues.map((issue) => ({ path: issue.path, message: issue.message }));
398
+ const first = issues[0];
399
+ throw new TemplateMigrationError(
400
+ "INVALID_DOCUMENT",
401
+ `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)` : ""}.`,
402
+ { version, issues }
403
+ );
404
+ }
405
+ function migrateV1_0ToV1_1(doc) {
406
+ if (!doc.sections.some((s) => s.isWrapper === true && !s.bodyRaw)) {
407
+ return { ...doc, version: "1.1", sections: doc.sections.map(stripIsWrapper) };
408
+ }
409
+ const ids = /* @__PURE__ */ new Set();
410
+ for (const s of doc.sections) {
411
+ ids.add(s.id);
412
+ for (const c of s.columns ?? []) {
413
+ ids.add(c.id);
414
+ for (const b of c.blocks ?? []) ids.add(b.id);
415
+ }
416
+ }
417
+ const freshId = (base) => {
418
+ let candidate = `${base}-inner`;
419
+ for (let n = 2; ids.has(candidate); n++) candidate = `${base}-inner-${n}`;
420
+ ids.add(candidate);
421
+ return candidate;
422
+ };
423
+ const sections = doc.sections.map((s) => {
424
+ if (s.isWrapper !== true || s.bodyRaw) return stripIsWrapper(s);
425
+ const { isWrapper: _flag, noStack: _noStack, ...rest } = s;
426
+ const wrapper = { id: s.id, type: "wrapper", sections: [{ id: freshId(s.id), type: "section", columns: s.columns }] };
427
+ if (rest.hidden !== void 0) wrapper.hidden = rest.hidden;
428
+ if (rest.backgroundColor !== void 0) wrapper.backgroundColor = rest.backgroundColor;
429
+ if (rest.backgroundImage !== void 0) wrapper.backgroundImage = rest.backgroundImage;
430
+ if (rest.backgroundGradient !== void 0) wrapper.backgroundGradient = rest.backgroundGradient;
431
+ if (rest.backgroundPosition !== void 0) wrapper.backgroundPosition = rest.backgroundPosition;
432
+ if (rest.backgroundRepeat !== void 0) wrapper.backgroundRepeat = rest.backgroundRepeat;
433
+ if (rest.backgroundSize !== void 0) wrapper.backgroundSize = rest.backgroundSize;
434
+ if (rest.fullWidth !== void 0) wrapper.fullWidth = rest.fullWidth;
435
+ if (rest.padding !== void 0) wrapper.padding = rest.padding;
436
+ if (rest.extraAttributes !== void 0) wrapper.extraAttributes = rest.extraAttributes;
437
+ return wrapper;
438
+ });
439
+ return { ...doc, version: "1.1", sections };
440
+ }
441
+ function stripIsWrapper(s) {
442
+ if (!("isWrapper" in s)) return s;
443
+ const { isWrapper: _flag, ...rest } = s;
444
+ return rest;
445
+ }
446
+ function withTemplateId(doc) {
447
+ if (typeof doc.id === "string" && doc.id.length > 0) return doc;
448
+ return { ...doc, id: nanoid() };
449
+ }
450
+
451
+ export {
452
+ SpacingSchema,
453
+ GradientStopSchema,
454
+ BackgroundGradientSchema,
455
+ ExtraAttributesSchema,
456
+ MjmlHeadSchema,
457
+ CustomFontSchema,
458
+ TemplateMetadataSchema,
459
+ TextBlockSchema,
460
+ ImageBlockSchema,
461
+ ButtonBlockSchema,
462
+ DividerBlockSchema,
463
+ SpacerBlockSchema,
464
+ HeaderBlockSchema,
465
+ FooterBlockSchema,
466
+ SocialBlockSchema,
467
+ HeroBlockSchema,
468
+ AccordionBlockSchema,
469
+ RawBlockSchema,
470
+ NavbarBlockSchema,
471
+ CarouselBlockSchema,
472
+ TableBlockSchema,
473
+ BlockSchema,
474
+ ColumnSchema,
475
+ SectionSchema,
476
+ SectionSchemaV1_0,
477
+ WrapperSchema,
478
+ TopLevelItemSchema,
479
+ EmailTemplateSchemaV1_0,
480
+ EmailTemplateSchemaV1_1,
481
+ EmailTemplateSchema,
482
+ validateTemplate,
483
+ CURRENT_TEMPLATE_VERSION,
484
+ SUPPORTED_TEMPLATE_VERSIONS,
485
+ TemplateMigrationError,
486
+ isTemplateMigrationError,
487
+ migrateTemplate,
488
+ migrateV1_0ToV1_1,
489
+ withTemplateId
490
+ };