@templatical/import-unlayer 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.
package/LICENSE ADDED
@@ -0,0 +1,56 @@
1
+ Functional Source License, Version 1.1, MIT Future License
2
+
3
+ Copyright (c) 2026-present Templatical
4
+
5
+ ## Terms and Conditions
6
+
7
+ ### Licensor ("We")
8
+
9
+ Templatical
10
+
11
+ ### The Software
12
+
13
+ Templatical Email Editor — the visual drag-and-drop email template editor
14
+ (@templatical/core, @templatical/editor, and @templatical/media-library
15
+ packages).
16
+
17
+ ### Grant of Rights
18
+
19
+ Subject to the terms and conditions of this License, We hereby grant You a
20
+ non-exclusive, worldwide, non-transferable license to use, copy, modify,
21
+ create derivative works, and redistribute the Software, subject to the
22
+ following conditions:
23
+
24
+ ### Permitted Uses
25
+
26
+ You may use the Software for any purpose, including commercial purposes,
27
+ **provided that** you do not offer the Software, or a substantially similar
28
+ product built using the Software, as a hosted or managed service that
29
+ competes with Templatical's commercial offerings.
30
+
31
+ ### Change Date
32
+
33
+ Two (2) years from the date of each release of the Software.
34
+
35
+ ### Change License
36
+
37
+ MIT License
38
+
39
+ On the Change Date, the above copyright notice and this permission notice
40
+ shall be replaced with the MIT License, and the Software will be available
41
+ under the MIT License for all purposes without restriction.
42
+
43
+ ### Notices
44
+
45
+ You must retain this license notice in all copies or substantial portions
46
+ of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
49
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
50
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
51
+
52
+ ---
53
+
54
+ Note: The @templatical/types, @templatical/renderer, and
55
+ @templatical/import-beefree packages are licensed separately under the MIT
56
+ License. See LICENSE-MIT for those packages' terms.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # @templatical/import-unlayer
2
+
3
+ > Convert Unlayer email templates to Templatical's JSON format.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@templatical/import-unlayer?label=npm&color=cb3837)](https://www.npmjs.com/package/@templatical/import-unlayer)
6
+ [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/templatical/sdk/blob/main/LICENSE-MIT)
7
+
8
+ Migrate from [Unlayer](https://unlayer.com) (the `react-email-editor` design JSON) to [Templatical](https://github.com/templatical/sdk) without rebuilding your templates by hand. Maps Unlayer content types to Templatical block types and reports anything that needs manual review.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @templatical/import-unlayer
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { convertUnlayerTemplate } from '@templatical/import-unlayer';
20
+
21
+ // Whatever your Unlayer editor returned from `editor.saveDesign(...)`.
22
+ const unlayerJson = await fetch('/path/to/unlayer-design.json').then(r => r.json());
23
+
24
+ const result = convertUnlayerTemplate(unlayerJson);
25
+
26
+ console.log(result.content); // → Templatical TemplateContent
27
+ console.log(result.report); // → conversion report (per-content status, warnings, summary)
28
+
29
+ // Inspect content that didn't convert cleanly
30
+ for (const entry of result.report.entries) {
31
+ if (entry.status !== 'converted') {
32
+ console.warn(
33
+ `${entry.unlayerContentType} → ${entry.templaticalBlockType ?? 'n/a'} ` +
34
+ `(${entry.status})${entry.note ? `: ${entry.note}` : ''}`,
35
+ );
36
+ }
37
+ }
38
+
39
+ console.log(result.report.summary);
40
+ // { total, converted, approximated, htmlFallback, skipped }
41
+ ```
42
+
43
+ ## What's converted
44
+
45
+ | Unlayer content | Templatical block |
46
+ |---|---|
47
+ | text | `ParagraphBlock` |
48
+ | heading | `TitleBlock` |
49
+ | image | `ImageBlock` |
50
+ | button | `ButtonBlock` |
51
+ | divider | `DividerBlock` |
52
+ | html | `HtmlBlock` |
53
+ | menu | `MenuBlock` |
54
+ | social | `SocialIconsBlock` |
55
+ | video | `VideoBlock` |
56
+ | timer | `HtmlBlock` (fallback) |
57
+ | form | skipped |
58
+ | Unknown content | Fallback to `HtmlBlock` |
59
+
60
+ Unknown or partially-supported content types are flagged in `result.report` so you can review them.
61
+
62
+ ## API
63
+
64
+ - `convertUnlayerTemplate(template)` — converts an Unlayer design JSON, returns `{ content, report }`
65
+
66
+ Types:
67
+ - `UnlayerTemplate` — input shape (see source for full structure)
68
+ - `ImportResult` — `{ content: TemplateContent, report: ImportReport }`
69
+ - `ImportReport` — `{ entries: ImportReportEntry[], warnings: string[], summary: { total, converted, approximated, htmlFallback, skipped } }`
70
+ - `ImportReportEntry` — `{ unlayerContentType, templaticalBlockType, status, note? }`
71
+ - `ConversionStatus` — `'converted' | 'approximated' | 'html-fallback' | 'skipped'`
72
+
73
+ ## Documentation
74
+
75
+ - [Migrating from Unlayer](https://docs.templatical.com/guide/migration-from-unlayer)
76
+
77
+ Full reference at **[docs.templatical.com](https://docs.templatical.com)**.
78
+
79
+ ## License
80
+
81
+ MIT
@@ -0,0 +1,209 @@
1
+ import * as _templatical_types from '@templatical/types';
2
+
3
+ /**
4
+ * Unlayer JSON type definitions.
5
+ * Based on the design JSON shape produced by `editor.saveDesign()` in
6
+ * react-email-editor / Unlayer's hosted editor.
7
+ */
8
+ interface UnlayerTemplate {
9
+ counters?: Record<string, number>;
10
+ body: UnlayerBody;
11
+ schemaVersion?: number;
12
+ }
13
+ interface UnlayerBody {
14
+ id?: string;
15
+ rows: UnlayerRow[];
16
+ headers?: UnlayerRow[];
17
+ footers?: UnlayerRow[];
18
+ values: UnlayerBodyValues;
19
+ }
20
+ interface UnlayerBackgroundImage {
21
+ url?: string;
22
+ fullWidth?: boolean;
23
+ repeat?: string;
24
+ size?: string;
25
+ position?: string;
26
+ }
27
+ interface UnlayerFontFamily {
28
+ label?: string;
29
+ value?: string;
30
+ }
31
+ interface UnlayerBodyValues {
32
+ backgroundColor?: string;
33
+ backgroundImage?: UnlayerBackgroundImage;
34
+ contentWidth?: string;
35
+ contentAlignment?: string;
36
+ fontFamily?: UnlayerFontFamily;
37
+ textColor?: string;
38
+ linkStyle?: Record<string, string>;
39
+ preheaderText?: string;
40
+ [key: string]: unknown;
41
+ }
42
+ interface UnlayerRow {
43
+ id?: string;
44
+ cells: number[];
45
+ columns: UnlayerColumn[];
46
+ values: UnlayerRowValues;
47
+ }
48
+ interface UnlayerRowValues {
49
+ backgroundColor?: string;
50
+ backgroundImage?: UnlayerBackgroundImage;
51
+ padding?: string;
52
+ columnsBackgroundColor?: string;
53
+ hideMobile?: boolean;
54
+ hideDesktop?: boolean;
55
+ noStackMobile?: boolean;
56
+ _meta?: Record<string, unknown>;
57
+ [key: string]: unknown;
58
+ }
59
+ interface UnlayerColumn {
60
+ id?: string;
61
+ contents: UnlayerContent[];
62
+ values: UnlayerColumnValues;
63
+ }
64
+ interface UnlayerColumnValues {
65
+ backgroundColor?: string;
66
+ padding?: string;
67
+ border?: Record<string, string>;
68
+ _meta?: Record<string, unknown>;
69
+ [key: string]: unknown;
70
+ }
71
+ interface UnlayerContent {
72
+ type: string;
73
+ values: UnlayerContentValues;
74
+ }
75
+ interface UnlayerLinkAction {
76
+ name?: string;
77
+ values?: {
78
+ href?: string;
79
+ target?: string;
80
+ };
81
+ }
82
+ interface UnlayerImageSrc {
83
+ url?: string;
84
+ width?: number;
85
+ height?: number;
86
+ }
87
+ interface UnlayerButtonColors {
88
+ color?: string;
89
+ backgroundColor?: string;
90
+ hoverColor?: string;
91
+ hoverBackgroundColor?: string;
92
+ }
93
+ interface UnlayerBorder {
94
+ borderTopWidth?: string;
95
+ borderTopStyle?: string;
96
+ borderTopColor?: string;
97
+ }
98
+ interface UnlayerMenuItem {
99
+ key?: string;
100
+ text: string;
101
+ link?: UnlayerLinkAction;
102
+ }
103
+ interface UnlayerMenu {
104
+ items: UnlayerMenuItem[];
105
+ }
106
+ interface UnlayerSocialIcon {
107
+ name?: string;
108
+ url?: string;
109
+ type?: string;
110
+ }
111
+ interface UnlayerSocialIcons {
112
+ iconType?: string;
113
+ editor?: {
114
+ data?: {
115
+ showTitle?: boolean;
116
+ };
117
+ };
118
+ icons?: UnlayerSocialIcon[];
119
+ }
120
+ interface UnlayerContentValues {
121
+ containerPadding?: string;
122
+ textAlign?: string;
123
+ fontFamily?: UnlayerFontFamily;
124
+ fontSize?: string;
125
+ fontWeight?: string | number;
126
+ color?: string;
127
+ hideMobile?: boolean;
128
+ hideDesktop?: boolean;
129
+ text?: string;
130
+ headingType?: string;
131
+ src?: UnlayerImageSrc;
132
+ altText?: string;
133
+ action?: UnlayerLinkAction;
134
+ buttonColors?: UnlayerButtonColors;
135
+ borderRadius?: string;
136
+ padding?: string;
137
+ size?: {
138
+ autoWidth?: boolean;
139
+ width?: string;
140
+ };
141
+ href?: UnlayerLinkAction;
142
+ border?: UnlayerBorder;
143
+ width?: string;
144
+ html?: string;
145
+ menu?: UnlayerMenu;
146
+ layout?: "horizontal" | "vertical";
147
+ separator?: string;
148
+ icons?: UnlayerSocialIcons;
149
+ videoUrl?: string;
150
+ thumbnailUrl?: string;
151
+ [key: string]: unknown;
152
+ }
153
+ /**
154
+ * Conversion status for each content node in the import report.
155
+ */
156
+ type ConversionStatus = "converted" | "approximated" | "html-fallback" | "skipped";
157
+ /**
158
+ * A single entry in the import report.
159
+ */
160
+ interface ImportReportEntry {
161
+ unlayerContentType: string;
162
+ templaticalBlockType: string | null;
163
+ status: ConversionStatus;
164
+ note?: string;
165
+ }
166
+ /**
167
+ * The full import report returned alongside the converted template.
168
+ */
169
+ interface ImportReport {
170
+ entries: ImportReportEntry[];
171
+ warnings: string[];
172
+ summary: {
173
+ total: number;
174
+ converted: number;
175
+ approximated: number;
176
+ htmlFallback: number;
177
+ skipped: number;
178
+ };
179
+ }
180
+ /**
181
+ * The result of an Unlayer import operation.
182
+ */
183
+ interface ImportResult {
184
+ content: _templatical_types.TemplateContent;
185
+ report: ImportReport;
186
+ }
187
+
188
+ /**
189
+ * Converts an Unlayer design JSON to Templatical TemplateContent.
190
+ *
191
+ * @param template - The parsed Unlayer JSON object (the result of `editor.saveDesign(...)`)
192
+ * @returns An ImportResult with the converted content and a detailed report
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * import { convertUnlayerTemplate } from '@templatical/import-unlayer';
197
+ *
198
+ * const unlayerJson = JSON.parse(fileContent);
199
+ * const { content, report } = convertUnlayerTemplate(unlayerJson);
200
+ *
201
+ * const editor = init({ container: '#editor', content });
202
+ *
203
+ * console.log(report.summary);
204
+ * console.log(report.warnings);
205
+ * ```
206
+ */
207
+ declare function convertUnlayerTemplate(template: UnlayerTemplate): ImportResult;
208
+
209
+ export { type ConversionStatus, type ImportReport, type ImportReportEntry, type ImportResult, type UnlayerBody, type UnlayerColumn, type UnlayerContent, type UnlayerContentValues, type UnlayerRow, type UnlayerTemplate, convertUnlayerTemplate };
package/dist/index.js ADDED
@@ -0,0 +1,572 @@
1
+ // src/converter.ts
2
+ import {
3
+ createSectionBlock,
4
+ createDefaultTemplateContent
5
+ } from "@templatical/types";
6
+
7
+ // src/block-mapper.ts
8
+ import {
9
+ createTitleBlock,
10
+ createParagraphBlock,
11
+ createImageBlock,
12
+ createButtonBlock,
13
+ createDividerBlock,
14
+ createSpacerBlock,
15
+ createHtmlBlock,
16
+ createSocialIconsBlock,
17
+ createMenuBlock,
18
+ createVideoBlock,
19
+ generateId
20
+ } from "@templatical/types";
21
+
22
+ // src/style-parser.ts
23
+ function parsePxValue(value) {
24
+ if (value === void 0 || value === null) return 0;
25
+ if (typeof value === "number") return Math.round(value);
26
+ const match = value.match(/^(-?\d+(?:\.\d+)?)\s*(?:px)?\s*$/);
27
+ return match ? Math.round(parseFloat(match[1])) : 0;
28
+ }
29
+ function parseColor(value) {
30
+ if (!value || value === "transparent") return "";
31
+ const trimmed = value.trim();
32
+ if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed.toLowerCase();
33
+ if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) {
34
+ const r = trimmed[1];
35
+ const g = trimmed[2];
36
+ const b = trimmed[3];
37
+ return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
38
+ }
39
+ return trimmed;
40
+ }
41
+ function parsePaddingShorthand(value) {
42
+ if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };
43
+ const parts = value.trim().split(/\s+/);
44
+ const values = parts.map((p) => parsePxValue(p));
45
+ switch (values.length) {
46
+ case 1:
47
+ return {
48
+ top: values[0],
49
+ right: values[0],
50
+ bottom: values[0],
51
+ left: values[0]
52
+ };
53
+ case 2:
54
+ return {
55
+ top: values[0],
56
+ right: values[1],
57
+ bottom: values[0],
58
+ left: values[1]
59
+ };
60
+ case 3:
61
+ return {
62
+ top: values[0],
63
+ right: values[1],
64
+ bottom: values[2],
65
+ left: values[1]
66
+ };
67
+ default:
68
+ return {
69
+ top: values[0],
70
+ right: values[1],
71
+ bottom: values[2],
72
+ left: values[3]
73
+ };
74
+ }
75
+ }
76
+ function parseBorderObject(border) {
77
+ if (!border) return { width: 0, style: "solid", color: "#000000" };
78
+ return {
79
+ width: parsePxValue(border.borderTopWidth),
80
+ style: border.borderTopStyle || "solid",
81
+ color: parseColor(border.borderTopColor) || "#000000"
82
+ };
83
+ }
84
+ function parseWidthPercent(value) {
85
+ if (!value) return 100;
86
+ const match = value.match(/^(\d+(?:\.\d+)?)\s*%/);
87
+ if (match) return Math.round(parseFloat(match[1]));
88
+ return 100;
89
+ }
90
+ function parseFontFamily(value) {
91
+ if (!value) return "";
92
+ if (typeof value === "string") {
93
+ return value.split(",")[0].trim().replace(/['"]/g, "");
94
+ }
95
+ if (value.value) return value.value.split(",")[0].trim().replace(/['"]/g, "");
96
+ if (value.label) return value.label;
97
+ return "";
98
+ }
99
+
100
+ // src/block-mapper.ts
101
+ var SOCIAL_PLATFORM_MAP = {
102
+ facebook: "facebook",
103
+ twitter: "twitter",
104
+ x: "twitter",
105
+ instagram: "instagram",
106
+ linkedin: "linkedin",
107
+ youtube: "youtube",
108
+ tiktok: "tiktok",
109
+ pinterest: "pinterest",
110
+ email: "email",
111
+ mail: "email",
112
+ whatsapp: "whatsapp",
113
+ telegram: "telegram",
114
+ discord: "discord",
115
+ snapchat: "snapchat",
116
+ reddit: "reddit",
117
+ github: "github",
118
+ dribbble: "dribbble",
119
+ behance: "behance"
120
+ };
121
+ function toAlign(value, fallback = "left") {
122
+ if (value === "left" || value === "center" || value === "right") return value;
123
+ return fallback;
124
+ }
125
+ function toLineStyle(value, fallback = "solid") {
126
+ if (value === "solid" || value === "dashed" || value === "dotted")
127
+ return value;
128
+ return fallback;
129
+ }
130
+ function defaultMargin() {
131
+ return { top: 0, right: 0, bottom: 0, left: 0 };
132
+ }
133
+ function makeStyles(values) {
134
+ const padding = parsePaddingShorthand(values.containerPadding);
135
+ return {
136
+ padding,
137
+ margin: defaultMargin()
138
+ };
139
+ }
140
+ function inlineStylesToHtml(html, values) {
141
+ const spanParts = [];
142
+ const fontSize = parsePxValue(values.fontSize);
143
+ if (fontSize && fontSize !== 16) spanParts.push(`font-size: ${fontSize}px`);
144
+ const color = parseColor(values.color);
145
+ if (color && color !== "#1a1a1a") spanParts.push(`color: ${color}`);
146
+ const fontWeight = values.fontWeight;
147
+ if (fontWeight !== void 0 && fontWeight !== null && String(fontWeight) !== "normal" && String(fontWeight) !== "400") {
148
+ spanParts.push(`font-weight: ${fontWeight}`);
149
+ }
150
+ const fontFamily = parseFontFamily(values.fontFamily);
151
+ if (fontFamily) spanParts.push(`font-family: ${fontFamily}`);
152
+ const textAlign = values.textAlign;
153
+ const pStyle = textAlign && textAlign !== "left" ? `text-align: ${textAlign}` : "";
154
+ if (!pStyle && spanParts.length === 0) return html;
155
+ const spanStyle = spanParts.join("; ");
156
+ let result = html;
157
+ if (pStyle) {
158
+ result = result.replace(/<p style="([^"]*)">/g, `<p style="$1; ${pStyle}">`).replaceAll("<p>", `<p style="${pStyle}">`);
159
+ }
160
+ if (spanStyle) {
161
+ result = result.replace(
162
+ /<p([^>]*)>([\s\S]*?)<\/p>/g,
163
+ `<p$1><span style="${spanStyle}">$2</span></p>`
164
+ );
165
+ }
166
+ return result;
167
+ }
168
+ function ensureParagraphWrapped(html) {
169
+ if (!html) return "<p></p>";
170
+ if (/<p[\s>]/i.test(html)) return html;
171
+ return `<p>${html}</p>`;
172
+ }
173
+ function convertText(values) {
174
+ const html = ensureParagraphWrapped(values.text ?? "");
175
+ return createParagraphBlock({
176
+ content: inlineStylesToHtml(html, values),
177
+ styles: makeStyles(values)
178
+ });
179
+ }
180
+ function parseHeadingLevel(tag) {
181
+ if (!tag) return 2;
182
+ const match = tag.match(/^h(\d)$/i);
183
+ if (match) {
184
+ const num = Number(match[1]);
185
+ if (num >= 1 && num <= 4) return num;
186
+ }
187
+ return 2;
188
+ }
189
+ function convertHeading(values) {
190
+ const text = values.text ?? "";
191
+ const stripped = text.replace(/^<h\d[^>]*>|<\/h\d>$/gi, "");
192
+ const content = stripped ? `<p>${stripped}</p>` : "<p></p>";
193
+ return createTitleBlock({
194
+ content,
195
+ level: parseHeadingLevel(values.headingType),
196
+ color: parseColor(values.color) || "#1a1a1a",
197
+ textAlign: toAlign(values.textAlign),
198
+ fontFamily: parseFontFamily(values.fontFamily) || void 0,
199
+ styles: makeStyles(values)
200
+ });
201
+ }
202
+ function convertImage(values) {
203
+ const src = values.src;
204
+ const action = values.action?.values;
205
+ return createImageBlock({
206
+ src: src?.url || "",
207
+ alt: values.altText || "",
208
+ width: src?.width ? Math.round(src.width) : 600,
209
+ align: toAlign(values.textAlign, "center"),
210
+ linkUrl: action?.href || void 0,
211
+ linkOpenInNewTab: action?.target === "_blank" || void 0,
212
+ styles: makeStyles(values)
213
+ });
214
+ }
215
+ function convertButton(values) {
216
+ const colors = values.buttonColors ?? {};
217
+ const padding = values.padding ? parsePaddingShorthand(values.padding) : { top: 12, right: 24, bottom: 12, left: 24 };
218
+ const label = (values.text ?? "Button").replace(/<[^>]*>/g, "");
219
+ const linkValues = values.href?.values;
220
+ return createButtonBlock({
221
+ text: label,
222
+ url: linkValues?.href || "#",
223
+ openInNewTab: linkValues?.target === "_blank" || void 0,
224
+ backgroundColor: parseColor(colors.backgroundColor) || "#4f46e5",
225
+ textColor: parseColor(colors.color) || "#ffffff",
226
+ borderRadius: parsePxValue(values.borderRadius),
227
+ fontSize: parsePxValue(values.fontSize) || 16,
228
+ fontFamily: parseFontFamily(values.fontFamily) || void 0,
229
+ buttonPadding: padding,
230
+ styles: makeStyles(values)
231
+ });
232
+ }
233
+ function convertDivider(values) {
234
+ const border = parseBorderObject(values.border);
235
+ return createDividerBlock({
236
+ lineStyle: toLineStyle(border.style),
237
+ color: border.color,
238
+ thickness: border.width || 1,
239
+ width: parseWidthPercent(values.width),
240
+ styles: makeStyles(values)
241
+ });
242
+ }
243
+ function convertSpacer(values) {
244
+ const padding = parsePaddingShorthand(values.containerPadding);
245
+ const height = parsePxValue(values.height) || padding.top + padding.bottom || 24;
246
+ return createSpacerBlock({
247
+ height,
248
+ styles: makeStyles(values)
249
+ });
250
+ }
251
+ function convertHtml(values) {
252
+ return createHtmlBlock({
253
+ content: values.html ?? "",
254
+ styles: makeStyles(values)
255
+ });
256
+ }
257
+ function convertSocial(values, warnings) {
258
+ const iconList = values.icons?.icons ?? [];
259
+ const icons = [];
260
+ for (const unlayerIcon of iconList) {
261
+ const id = (unlayerIcon.name ?? "").toLowerCase();
262
+ const platform = SOCIAL_PLATFORM_MAP[id];
263
+ if (!platform) {
264
+ warnings.push(
265
+ `Unrecognized social icon "${unlayerIcon.name || id}" was skipped.`
266
+ );
267
+ continue;
268
+ }
269
+ icons.push({
270
+ id: generateId(),
271
+ platform,
272
+ url: unlayerIcon.url || "#"
273
+ });
274
+ }
275
+ return createSocialIconsBlock({
276
+ icons,
277
+ align: toAlign(values.textAlign, "center"),
278
+ styles: makeStyles(values)
279
+ });
280
+ }
281
+ function convertVideo(values) {
282
+ return createVideoBlock({
283
+ url: values.videoUrl || "",
284
+ thumbnailUrl: values.thumbnailUrl || "",
285
+ alt: values.altText || "",
286
+ width: 600,
287
+ align: toAlign(values.textAlign, "center"),
288
+ styles: makeStyles(values)
289
+ });
290
+ }
291
+ function convertMenu(values) {
292
+ const menu = values.menu;
293
+ const items = (menu?.items ?? []).map((item) => ({
294
+ id: generateId(),
295
+ text: item.text || "",
296
+ url: item.link?.values?.href || "#",
297
+ openInNewTab: item.link?.values?.target === "_blank",
298
+ bold: false,
299
+ underline: false
300
+ }));
301
+ return createMenuBlock({
302
+ items,
303
+ separator: values.separator || "|",
304
+ separatorColor: "#999999",
305
+ fontSize: parsePxValue(values.fontSize) || 14,
306
+ color: parseColor(values.color) || "#1a1a1a",
307
+ fontFamily: parseFontFamily(values.fontFamily) || void 0,
308
+ textAlign: toAlign(values.textAlign, "center"),
309
+ styles: makeStyles(values)
310
+ });
311
+ }
312
+ function convertHtmlFallback(content, comment) {
313
+ const safe = comment.replace(/</g, "&lt;").replace(/>/g, "&gt;");
314
+ return createHtmlBlock({
315
+ content: `<div style="padding:12px;border:1px dashed #d1d5db;border-radius:6px;background:#fafafa;color:#6b7280;font-family:sans-serif;font-size:13px;">${safe}</div>`,
316
+ styles: makeStyles(content.values)
317
+ });
318
+ }
319
+ function convertContent(content, warnings) {
320
+ const type = content.type;
321
+ const values = content.values ?? {};
322
+ switch (type) {
323
+ case "text":
324
+ return {
325
+ block: convertText(values),
326
+ entry: {
327
+ unlayerContentType: type,
328
+ templaticalBlockType: "paragraph",
329
+ status: "converted"
330
+ }
331
+ };
332
+ case "heading":
333
+ return {
334
+ block: convertHeading(values),
335
+ entry: {
336
+ unlayerContentType: type,
337
+ templaticalBlockType: "title",
338
+ status: "converted"
339
+ }
340
+ };
341
+ case "image":
342
+ return {
343
+ block: convertImage(values),
344
+ entry: {
345
+ unlayerContentType: type,
346
+ templaticalBlockType: "image",
347
+ status: "converted"
348
+ }
349
+ };
350
+ case "button":
351
+ return {
352
+ block: convertButton(values),
353
+ entry: {
354
+ unlayerContentType: type,
355
+ templaticalBlockType: "button",
356
+ status: "converted"
357
+ }
358
+ };
359
+ case "divider":
360
+ return {
361
+ block: convertDivider(values),
362
+ entry: {
363
+ unlayerContentType: type,
364
+ templaticalBlockType: "divider",
365
+ status: "converted"
366
+ }
367
+ };
368
+ case "spacer":
369
+ return {
370
+ block: convertSpacer(values),
371
+ entry: {
372
+ unlayerContentType: type,
373
+ templaticalBlockType: "spacer",
374
+ status: "converted"
375
+ }
376
+ };
377
+ case "html":
378
+ return {
379
+ block: convertHtml(values),
380
+ entry: {
381
+ unlayerContentType: type,
382
+ templaticalBlockType: "html",
383
+ status: "converted"
384
+ }
385
+ };
386
+ case "menu":
387
+ return {
388
+ block: convertMenu(values),
389
+ entry: {
390
+ unlayerContentType: type,
391
+ templaticalBlockType: "menu",
392
+ status: "approximated",
393
+ note: "Menu styles map approximately; review spacing and separator color."
394
+ }
395
+ };
396
+ case "social":
397
+ return {
398
+ block: convertSocial(values, warnings),
399
+ entry: {
400
+ unlayerContentType: type,
401
+ templaticalBlockType: "social",
402
+ status: "converted"
403
+ }
404
+ };
405
+ case "video":
406
+ return {
407
+ block: convertVideo(values),
408
+ entry: {
409
+ unlayerContentType: type,
410
+ templaticalBlockType: "video",
411
+ status: "converted"
412
+ }
413
+ };
414
+ case "timer":
415
+ return {
416
+ block: convertHtmlFallback(
417
+ content,
418
+ "Unlayer timer block: rebuild manually in Templatical"
419
+ ),
420
+ entry: {
421
+ unlayerContentType: type,
422
+ templaticalBlockType: "html",
423
+ status: "html-fallback",
424
+ note: "Timer modules have no direct Templatical equivalent; placeholder HTML inserted."
425
+ }
426
+ };
427
+ case "form":
428
+ return {
429
+ block: convertHtmlFallback(
430
+ content,
431
+ "Unlayer form block: not supported in Templatical (most email clients block form submission)"
432
+ ),
433
+ entry: {
434
+ unlayerContentType: type,
435
+ templaticalBlockType: null,
436
+ status: "skipped",
437
+ note: "Unlayer forms have no Templatical equivalent and are skipped. Most email clients block form submission anyway."
438
+ }
439
+ };
440
+ default:
441
+ return {
442
+ block: convertHtmlFallback(
443
+ content,
444
+ `Unsupported Unlayer content type: ${type}`
445
+ ),
446
+ entry: {
447
+ unlayerContentType: type,
448
+ templaticalBlockType: "html",
449
+ status: "html-fallback",
450
+ note: `Unknown content type "${type}" converted to HTML block.`
451
+ }
452
+ };
453
+ }
454
+ }
455
+
456
+ // src/converter.ts
457
+ function resolveColumnLayout(cells, warnings) {
458
+ if (cells.length <= 1) return "1";
459
+ if (cells.length === 3) return "3";
460
+ if (cells.length === 2) {
461
+ const left = cells[0] ?? 1;
462
+ const right = cells[1] ?? 1;
463
+ const total = left + right;
464
+ const ratio = left / total;
465
+ if (ratio > 0.58) return "2-1";
466
+ if (ratio < 0.42) return "1-2";
467
+ return "2";
468
+ }
469
+ warnings.push(
470
+ `Row with ${cells.length} columns was flattened to a single column. Unlayer supports arbitrary columns, but Templatical supports up to 3.`
471
+ );
472
+ return "1";
473
+ }
474
+ function convertColumnContents(column, entries, warnings) {
475
+ const blocks = [];
476
+ for (const content of column.contents ?? []) {
477
+ const { block, entry } = convertContent(content, warnings);
478
+ blocks.push(block);
479
+ entries.push(entry);
480
+ }
481
+ return blocks;
482
+ }
483
+ function processRow(row, entries, warnings) {
484
+ const columns = row.columns;
485
+ if (!columns || columns.length === 0) return [];
486
+ const cells = row.cells ?? columns.map(() => 1);
487
+ const layout = resolveColumnLayout(cells, warnings);
488
+ let children;
489
+ if (layout === "1") {
490
+ const merged = [];
491
+ for (const column of columns) {
492
+ merged.push(...convertColumnContents(column, entries, warnings));
493
+ }
494
+ children = [merged];
495
+ } else {
496
+ children = columns.map(
497
+ (col) => convertColumnContents(col, entries, warnings)
498
+ );
499
+ }
500
+ const rowBg = parseColor(row.values?.backgroundColor);
501
+ const padding = parsePaddingShorthand(row.values?.padding);
502
+ const section = createSectionBlock({
503
+ columns: layout,
504
+ children,
505
+ styles: {
506
+ padding,
507
+ margin: { top: 0, right: 0, bottom: 0, left: 0 },
508
+ ...rowBg ? { backgroundColor: rowBg } : {}
509
+ }
510
+ });
511
+ return [section];
512
+ }
513
+ function extractSettings(template) {
514
+ const values = template.body?.values ?? {};
515
+ const width = parsePxValue(values.contentWidth);
516
+ const bgColor = parseColor(values.backgroundColor) || "#ffffff";
517
+ const fontFamily = parseFontFamily(values.fontFamily) || "Arial";
518
+ return {
519
+ width: width || 600,
520
+ backgroundColor: bgColor,
521
+ fontFamily
522
+ };
523
+ }
524
+ function convertUnlayerTemplate(template) {
525
+ if (!template?.body?.rows) {
526
+ throw new Error(
527
+ "Invalid Unlayer template: missing body.rows. Ensure you are passing a valid Unlayer JSON design (the output of editor.saveDesign)."
528
+ );
529
+ }
530
+ const entries = [];
531
+ const warnings = [];
532
+ const blocks = [];
533
+ const headers = template.body.headers ?? [];
534
+ const footers = template.body.footers ?? [];
535
+ if (headers.length > 0) {
536
+ warnings.push(
537
+ `${headers.length} Unlayer header row(s) were imported as regular rows at the top of the template.`
538
+ );
539
+ for (const row of headers) {
540
+ blocks.push(...processRow(row, entries, warnings));
541
+ }
542
+ }
543
+ for (const row of template.body.rows) {
544
+ blocks.push(...processRow(row, entries, warnings));
545
+ }
546
+ if (footers.length > 0) {
547
+ warnings.push(
548
+ `${footers.length} Unlayer footer row(s) were imported as regular rows at the bottom of the template.`
549
+ );
550
+ for (const row of footers) {
551
+ blocks.push(...processRow(row, entries, warnings));
552
+ }
553
+ }
554
+ const content = {
555
+ ...createDefaultTemplateContent(),
556
+ blocks,
557
+ settings: extractSettings(template)
558
+ };
559
+ const summary = {
560
+ total: entries.length,
561
+ converted: entries.filter((e) => e.status === "converted").length,
562
+ approximated: entries.filter((e) => e.status === "approximated").length,
563
+ htmlFallback: entries.filter((e) => e.status === "html-fallback").length,
564
+ skipped: entries.filter((e) => e.status === "skipped").length
565
+ };
566
+ const report = { entries, warnings, summary };
567
+ return { content, report };
568
+ }
569
+ export {
570
+ convertUnlayerTemplate
571
+ };
572
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/converter.ts","../src/block-mapper.ts","../src/style-parser.ts"],"sourcesContent":["import {\n createSectionBlock,\n createDefaultTemplateContent,\n} from \"@templatical/types\";\nimport type { Block, ColumnLayout, TemplateContent } from \"@templatical/types\";\nimport type {\n UnlayerTemplate,\n UnlayerRow,\n UnlayerColumn,\n ImportResult,\n ImportReport,\n ImportReportEntry,\n} from \"./types\";\nimport { convertContent } from \"./block-mapper\";\nimport {\n parsePxValue,\n parseColor,\n parseFontFamily,\n parsePaddingShorthand,\n} from \"./style-parser\";\n\nfunction resolveColumnLayout(\n cells: number[],\n warnings: string[],\n): ColumnLayout {\n if (cells.length <= 1) return \"1\";\n if (cells.length === 3) return \"3\";\n\n if (cells.length === 2) {\n const left = cells[0] ?? 1;\n const right = cells[1] ?? 1;\n const total = left + right;\n const ratio = left / total;\n\n if (ratio > 0.58) return \"2-1\";\n if (ratio < 0.42) return \"1-2\";\n return \"2\";\n }\n\n warnings.push(\n `Row with ${cells.length} columns was flattened to a single column. Unlayer supports arbitrary columns, but Templatical supports up to 3.`,\n );\n return \"1\";\n}\n\n/**\n * Converts all contents in a column to Templatical blocks.\n */\nfunction convertColumnContents(\n column: UnlayerColumn,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n const blocks: Block[] = [];\n\n for (const content of column.contents ?? []) {\n const { block, entry } = convertContent(content, warnings);\n blocks.push(block);\n entries.push(entry);\n }\n\n return blocks;\n}\n\n/**\n * Processes a single Unlayer row into one or more Templatical blocks.\n */\nfunction processRow(\n row: UnlayerRow,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n const columns = row.columns;\n if (!columns || columns.length === 0) return [];\n\n const cells = row.cells ?? columns.map(() => 1);\n const layout = resolveColumnLayout(cells, warnings);\n\n let children: Block[][];\n if (layout === \"1\") {\n const merged: Block[] = [];\n for (const column of columns) {\n merged.push(...convertColumnContents(column, entries, warnings));\n }\n children = [merged];\n } else {\n children = columns.map((col) =>\n convertColumnContents(col, entries, warnings),\n );\n }\n\n const rowBg = parseColor(row.values?.backgroundColor);\n const padding = parsePaddingShorthand(row.values?.padding);\n\n const section = createSectionBlock({\n columns: layout,\n children,\n styles: {\n padding,\n margin: { top: 0, right: 0, bottom: 0, left: 0 },\n ...(rowBg ? { backgroundColor: rowBg } : {}),\n },\n });\n\n return [section];\n}\n\n/**\n * Extracts template-level settings from the Unlayer body values.\n */\nfunction extractSettings(\n template: UnlayerTemplate,\n): TemplateContent[\"settings\"] {\n const values = template.body?.values ?? {};\n\n const width = parsePxValue(values.contentWidth);\n const bgColor = parseColor(values.backgroundColor) || \"#ffffff\";\n const fontFamily = parseFontFamily(values.fontFamily) || \"Arial\";\n\n return {\n width: width || 600,\n backgroundColor: bgColor,\n fontFamily,\n };\n}\n\n/**\n * Converts an Unlayer design JSON to Templatical TemplateContent.\n *\n * @param template - The parsed Unlayer JSON object (the result of `editor.saveDesign(...)`)\n * @returns An ImportResult with the converted content and a detailed report\n *\n * @example\n * ```ts\n * import { convertUnlayerTemplate } from '@templatical/import-unlayer';\n *\n * const unlayerJson = JSON.parse(fileContent);\n * const { content, report } = convertUnlayerTemplate(unlayerJson);\n *\n * const editor = init({ container: '#editor', content });\n *\n * console.log(report.summary);\n * console.log(report.warnings);\n * ```\n */\nexport function convertUnlayerTemplate(\n template: UnlayerTemplate,\n): ImportResult {\n if (!template?.body?.rows) {\n throw new Error(\n \"Invalid Unlayer template: missing body.rows. Ensure you are passing a valid Unlayer JSON design (the output of editor.saveDesign).\",\n );\n }\n\n const entries: ImportReportEntry[] = [];\n const warnings: string[] = [];\n const blocks: Block[] = [];\n\n const headers = template.body.headers ?? [];\n const footers = template.body.footers ?? [];\n\n if (headers.length > 0) {\n warnings.push(\n `${headers.length} Unlayer header row(s) were imported as regular rows at the top of the template.`,\n );\n for (const row of headers) {\n blocks.push(...processRow(row, entries, warnings));\n }\n }\n\n for (const row of template.body.rows) {\n blocks.push(...processRow(row, entries, warnings));\n }\n\n if (footers.length > 0) {\n warnings.push(\n `${footers.length} Unlayer footer row(s) were imported as regular rows at the bottom of the template.`,\n );\n for (const row of footers) {\n blocks.push(...processRow(row, entries, warnings));\n }\n }\n\n const content: TemplateContent = {\n ...createDefaultTemplateContent(),\n blocks,\n settings: extractSettings(template),\n };\n\n const summary = {\n total: entries.length,\n converted: entries.filter((e) => e.status === \"converted\").length,\n approximated: entries.filter((e) => e.status === \"approximated\").length,\n htmlFallback: entries.filter((e) => e.status === \"html-fallback\").length,\n skipped: entries.filter((e) => e.status === \"skipped\").length,\n };\n\n const report: ImportReport = { entries, warnings, summary };\n\n return { content, report };\n}\n","import {\n createTitleBlock,\n createParagraphBlock,\n createImageBlock,\n createButtonBlock,\n createDividerBlock,\n createSpacerBlock,\n createHtmlBlock,\n createSocialIconsBlock,\n createMenuBlock,\n createVideoBlock,\n generateId,\n} from \"@templatical/types\";\nimport type {\n Block,\n HeadingLevel,\n SocialPlatform,\n SocialIcon,\n MenuItemData,\n SpacingValue,\n} from \"@templatical/types\";\nimport type {\n UnlayerContent,\n UnlayerContentValues,\n ImportReportEntry,\n} from \"./types\";\nimport {\n parsePxValue,\n parseColor,\n parseFontFamily,\n parsePaddingShorthand,\n parseBorderObject,\n parseWidthPercent,\n} from \"./style-parser\";\n\nconst SOCIAL_PLATFORM_MAP: Record<string, SocialPlatform> = {\n facebook: \"facebook\",\n twitter: \"twitter\",\n x: \"twitter\",\n instagram: \"instagram\",\n linkedin: \"linkedin\",\n youtube: \"youtube\",\n tiktok: \"tiktok\",\n pinterest: \"pinterest\",\n email: \"email\",\n mail: \"email\",\n whatsapp: \"whatsapp\",\n telegram: \"telegram\",\n discord: \"discord\",\n snapchat: \"snapchat\",\n reddit: \"reddit\",\n github: \"github\",\n dribbble: \"dribbble\",\n behance: \"behance\",\n};\n\ntype Align = \"left\" | \"center\" | \"right\";\ntype LineStyle = \"solid\" | \"dashed\" | \"dotted\";\n\nfunction toAlign(value: string | undefined, fallback: Align = \"left\"): Align {\n if (value === \"left\" || value === \"center\" || value === \"right\") return value;\n return fallback;\n}\n\nfunction toLineStyle(\n value: string | undefined,\n fallback: LineStyle = \"solid\",\n): LineStyle {\n if (value === \"solid\" || value === \"dashed\" || value === \"dotted\")\n return value;\n return fallback;\n}\n\nfunction defaultMargin(): SpacingValue {\n return { top: 0, right: 0, bottom: 0, left: 0 };\n}\n\nfunction makeStyles(values: UnlayerContentValues): Block[\"styles\"] {\n const padding = parsePaddingShorthand(values.containerPadding);\n return {\n padding,\n margin: defaultMargin(),\n };\n}\n\n/**\n * Apply Unlayer text-level styles as TipTap-compatible inline markup.\n * Mirrors the BeeFree importer's helper but reads from Unlayer's flat\n * values shape rather than a CSS style record.\n */\nfunction inlineStylesToHtml(\n html: string,\n values: UnlayerContentValues,\n): string {\n const spanParts: string[] = [];\n const fontSize = parsePxValue(values.fontSize);\n if (fontSize && fontSize !== 16) spanParts.push(`font-size: ${fontSize}px`);\n const color = parseColor(values.color);\n if (color && color !== \"#1a1a1a\") spanParts.push(`color: ${color}`);\n const fontWeight = values.fontWeight;\n if (\n fontWeight !== undefined &&\n fontWeight !== null &&\n String(fontWeight) !== \"normal\" &&\n String(fontWeight) !== \"400\"\n ) {\n spanParts.push(`font-weight: ${fontWeight}`);\n }\n const fontFamily = parseFontFamily(values.fontFamily);\n if (fontFamily) spanParts.push(`font-family: ${fontFamily}`);\n\n const textAlign = values.textAlign;\n const pStyle =\n textAlign && textAlign !== \"left\" ? `text-align: ${textAlign}` : \"\";\n\n if (!pStyle && spanParts.length === 0) return html;\n\n const spanStyle = spanParts.join(\"; \");\n let result = html;\n\n if (pStyle) {\n result = result\n .replace(/<p style=\"([^\"]*)\">/g, `<p style=\"$1; ${pStyle}\">`)\n .replaceAll(\"<p>\", `<p style=\"${pStyle}\">`);\n }\n\n if (spanStyle) {\n result = result.replace(\n /<p([^>]*)>([\\s\\S]*?)<\\/p>/g,\n `<p$1><span style=\"${spanStyle}\">$2</span></p>`,\n );\n }\n\n return result;\n}\n\nfunction ensureParagraphWrapped(html: string): string {\n if (!html) return \"<p></p>\";\n if (/<p[\\s>]/i.test(html)) return html;\n return `<p>${html}</p>`;\n}\n\nfunction convertText(values: UnlayerContentValues): Block {\n const html = ensureParagraphWrapped(values.text ?? \"\");\n\n return createParagraphBlock({\n content: inlineStylesToHtml(html, values),\n styles: makeStyles(values),\n });\n}\n\nfunction parseHeadingLevel(tag: string | undefined): HeadingLevel {\n if (!tag) return 2;\n const match = tag.match(/^h(\\d)$/i);\n if (match) {\n const num = Number(match[1]);\n if (num >= 1 && num <= 4) return num as HeadingLevel;\n }\n return 2;\n}\n\nfunction convertHeading(values: UnlayerContentValues): Block {\n const text = values.text ?? \"\";\n const stripped = text.replace(/^<h\\d[^>]*>|<\\/h\\d>$/gi, \"\");\n const content = stripped ? `<p>${stripped}</p>` : \"<p></p>\";\n\n return createTitleBlock({\n content,\n level: parseHeadingLevel(values.headingType),\n color: parseColor(values.color) || \"#1a1a1a\",\n textAlign: toAlign(values.textAlign),\n fontFamily: parseFontFamily(values.fontFamily) || undefined,\n styles: makeStyles(values),\n });\n}\n\nfunction convertImage(values: UnlayerContentValues): Block {\n const src = values.src;\n const action = values.action?.values;\n\n return createImageBlock({\n src: src?.url || \"\",\n alt: values.altText || \"\",\n width: src?.width ? Math.round(src.width) : 600,\n align: toAlign(values.textAlign, \"center\"),\n linkUrl: action?.href || undefined,\n linkOpenInNewTab: action?.target === \"_blank\" || undefined,\n styles: makeStyles(values),\n });\n}\n\nfunction convertButton(values: UnlayerContentValues): Block {\n const colors = values.buttonColors ?? {};\n const padding = values.padding\n ? parsePaddingShorthand(values.padding)\n : { top: 12, right: 24, bottom: 12, left: 24 };\n const label = (values.text ?? \"Button\").replace(/<[^>]*>/g, \"\");\n const linkValues = values.href?.values;\n\n return createButtonBlock({\n text: label,\n url: linkValues?.href || \"#\",\n openInNewTab: linkValues?.target === \"_blank\" || undefined,\n backgroundColor: parseColor(colors.backgroundColor) || \"#4f46e5\",\n textColor: parseColor(colors.color) || \"#ffffff\",\n borderRadius: parsePxValue(values.borderRadius),\n fontSize: parsePxValue(values.fontSize) || 16,\n fontFamily: parseFontFamily(values.fontFamily) || undefined,\n buttonPadding: padding,\n styles: makeStyles(values),\n });\n}\n\nfunction convertDivider(values: UnlayerContentValues): Block {\n const border = parseBorderObject(values.border);\n\n return createDividerBlock({\n lineStyle: toLineStyle(border.style),\n color: border.color,\n thickness: border.width || 1,\n width: parseWidthPercent(values.width),\n styles: makeStyles(values),\n });\n}\n\nfunction convertSpacer(values: UnlayerContentValues): Block {\n const padding = parsePaddingShorthand(values.containerPadding);\n const height =\n parsePxValue((values as { height?: string | number }).height) ||\n padding.top + padding.bottom ||\n 24;\n\n return createSpacerBlock({\n height,\n styles: makeStyles(values),\n });\n}\n\nfunction convertHtml(values: UnlayerContentValues): Block {\n return createHtmlBlock({\n content: values.html ?? \"\",\n styles: makeStyles(values),\n });\n}\n\nfunction convertSocial(\n values: UnlayerContentValues,\n warnings: string[],\n): Block {\n const iconList = values.icons?.icons ?? [];\n const icons: SocialIcon[] = [];\n\n for (const unlayerIcon of iconList) {\n const id = (unlayerIcon.name ?? \"\").toLowerCase();\n const platform = SOCIAL_PLATFORM_MAP[id];\n\n if (!platform) {\n warnings.push(\n `Unrecognized social icon \"${unlayerIcon.name || id}\" was skipped.`,\n );\n continue;\n }\n\n icons.push({\n id: generateId(),\n platform,\n url: unlayerIcon.url || \"#\",\n });\n }\n\n return createSocialIconsBlock({\n icons,\n align: toAlign(values.textAlign, \"center\"),\n styles: makeStyles(values),\n });\n}\n\nfunction convertVideo(values: UnlayerContentValues): Block {\n return createVideoBlock({\n url: values.videoUrl || \"\",\n thumbnailUrl: values.thumbnailUrl || \"\",\n alt: values.altText || \"\",\n width: 600,\n align: toAlign(values.textAlign, \"center\"),\n styles: makeStyles(values),\n });\n}\n\nfunction convertMenu(values: UnlayerContentValues): Block {\n const menu = values.menu;\n const items: MenuItemData[] = (menu?.items ?? []).map((item) => ({\n id: generateId(),\n text: item.text || \"\",\n url: item.link?.values?.href || \"#\",\n openInNewTab: item.link?.values?.target === \"_blank\",\n bold: false,\n underline: false,\n }));\n\n return createMenuBlock({\n items,\n separator: values.separator || \"|\",\n separatorColor: \"#999999\",\n fontSize: parsePxValue(values.fontSize) || 14,\n color: parseColor(values.color) || \"#1a1a1a\",\n fontFamily: parseFontFamily(values.fontFamily) || undefined,\n textAlign: toAlign(values.textAlign, \"center\"),\n styles: makeStyles(values),\n });\n}\n\nfunction convertHtmlFallback(content: UnlayerContent, comment: string): Block {\n const safe = comment.replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n return createHtmlBlock({\n content: `<div style=\"padding:12px;border:1px dashed #d1d5db;border-radius:6px;background:#fafafa;color:#6b7280;font-family:sans-serif;font-size:13px;\">${safe}</div>`,\n styles: makeStyles(content.values),\n });\n}\n\n/**\n * Converts a single Unlayer content node to a Templatical block.\n * Returns the block and a report entry.\n */\nexport function convertContent(\n content: UnlayerContent,\n warnings: string[],\n): { block: Block; entry: ImportReportEntry } {\n const type = content.type;\n const values = content.values ?? ({} as UnlayerContentValues);\n\n switch (type) {\n case \"text\":\n return {\n block: convertText(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n case \"heading\":\n return {\n block: convertHeading(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"title\",\n status: \"converted\",\n },\n };\n case \"image\":\n return {\n block: convertImage(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n case \"button\":\n return {\n block: convertButton(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n case \"divider\":\n return {\n block: convertDivider(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n case \"spacer\":\n return {\n block: convertSpacer(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"spacer\",\n status: \"converted\",\n },\n };\n case \"html\":\n return {\n block: convertHtml(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"html\",\n status: \"converted\",\n },\n };\n case \"menu\":\n return {\n block: convertMenu(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"menu\",\n status: \"approximated\",\n note: \"Menu styles map approximately; review spacing and separator color.\",\n },\n };\n case \"social\":\n return {\n block: convertSocial(values, warnings),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"social\",\n status: \"converted\",\n },\n };\n case \"video\":\n return {\n block: convertVideo(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"video\",\n status: \"converted\",\n },\n };\n case \"timer\":\n return {\n block: convertHtmlFallback(\n content,\n \"Unlayer timer block: rebuild manually in Templatical\",\n ),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: \"Timer modules have no direct Templatical equivalent; placeholder HTML inserted.\",\n },\n };\n case \"form\":\n return {\n block: convertHtmlFallback(\n content,\n \"Unlayer form block: not supported in Templatical (most email clients block form submission)\",\n ),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: null,\n status: \"skipped\",\n note: \"Unlayer forms have no Templatical equivalent and are skipped. Most email clients block form submission anyway.\",\n },\n };\n default:\n return {\n block: convertHtmlFallback(\n content,\n `Unsupported Unlayer content type: ${type}`,\n ),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `Unknown content type \"${type}\" converted to HTML block.`,\n },\n };\n }\n}\n","import type { SpacingValue } from \"@templatical/types\";\nimport type { UnlayerBorder, UnlayerFontFamily } from \"./types\";\n\n/**\n * Parses CSS-like style values from Unlayer content values.\n */\n\nexport function parsePxValue(value: string | number | undefined): number {\n if (value === undefined || value === null) return 0;\n if (typeof value === \"number\") return Math.round(value);\n const match = value.match(/^(-?\\d+(?:\\.\\d+)?)\\s*(?:px)?\\s*$/);\n return match ? Math.round(parseFloat(match[1])) : 0;\n}\n\nexport function parseColor(value: string | undefined): string {\n if (!value || value === \"transparent\") return \"\";\n\n const trimmed = value.trim();\n\n if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed.toLowerCase();\n\n if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) {\n const r = trimmed[1];\n const g = trimmed[2];\n const b = trimmed[3];\n return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();\n }\n\n return trimmed;\n}\n\nexport function parsePaddingShorthand(value: string | undefined): SpacingValue {\n if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };\n\n const parts = value.trim().split(/\\s+/);\n const values = parts.map((p) => parsePxValue(p));\n\n switch (values.length) {\n case 1:\n return {\n top: values[0],\n right: values[0],\n bottom: values[0],\n left: values[0],\n };\n case 2:\n return {\n top: values[0],\n right: values[1],\n bottom: values[0],\n left: values[1],\n };\n case 3:\n return {\n top: values[0],\n right: values[1],\n bottom: values[2],\n left: values[1],\n };\n default:\n return {\n top: values[0],\n right: values[1],\n bottom: values[2],\n left: values[3],\n };\n }\n}\n\nexport function parseBorderObject(border: UnlayerBorder | undefined): {\n width: number;\n style: string;\n color: string;\n} {\n if (!border) return { width: 0, style: \"solid\", color: \"#000000\" };\n return {\n width: parsePxValue(border.borderTopWidth),\n style: border.borderTopStyle || \"solid\",\n color: parseColor(border.borderTopColor) || \"#000000\",\n };\n}\n\nexport function parseWidthPercent(value: string | undefined): number {\n if (!value) return 100;\n const match = value.match(/^(\\d+(?:\\.\\d+)?)\\s*%/);\n if (match) return Math.round(parseFloat(match[1]));\n return 100;\n}\n\nexport function parseFontFamily(\n value: UnlayerFontFamily | string | undefined,\n): string {\n if (!value) return \"\";\n if (typeof value === \"string\") {\n return value.split(\",\")[0].trim().replace(/['\"]/g, \"\");\n }\n if (value.value) return value.value.split(\",\")[0].trim().replace(/['\"]/g, \"\");\n if (value.label) return value.label;\n return \"\";\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@templatical/import-unlayer",
3
+ "description": "Convert Unlayer email templates to Templatical format",
4
+ "version": "0.4.0",
5
+ "bugs": "https://github.com/templatical/sdk/issues",
6
+ "dependencies": {
7
+ "@templatical/types": "0.4.0"
8
+ },
9
+ "devDependencies": {
10
+ "tsup": "^8.5.1",
11
+ "typescript": "^6.0.3",
12
+ "vitest": "^4.1.5"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "homepage": "https://templatical.com",
24
+ "keywords": [
25
+ "email",
26
+ "email-template",
27
+ "importer",
28
+ "migration",
29
+ "templatical",
30
+ "unlayer"
31
+ ],
32
+ "license": "MIT",
33
+ "module": "./dist/index.js",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/templatical/sdk.git",
40
+ "directory": "packages/import-unlayer"
41
+ },
42
+ "type": "module",
43
+ "types": "./dist/index.d.ts",
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "test": "vitest run --config vitest.config.ts",
47
+ "typecheck": "tsc --noEmit"
48
+ }
49
+ }