@templatical/import-unlayer 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +125 -124
- package/dist/index.js +543 -569
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -1,623 +1,597 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
1
|
+
import { createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createImageBlock, createMenuBlock, createParagraphBlock, createSectionBlock, createSocialIconsBlock, createSpacerBlock, createTitleBlock, createVideoBlock, generateId } from "@templatical/types";
|
|
2
|
+
//#region src/style-parser.ts
|
|
3
|
+
/**
|
|
4
|
+
* Parses CSS-like style values from Unlayer content values.
|
|
5
|
+
*/
|
|
23
6
|
function parsePxValue(value) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
7
|
+
if (value === void 0 || value === null) return 0;
|
|
8
|
+
if (typeof value === "number") return Math.round(value);
|
|
9
|
+
const match = value.match(/^(-?\d+(?:\.\d+)?)\s*(?:px\s*)?$/);
|
|
10
|
+
return match ? Math.round(parseFloat(match[1])) : 0;
|
|
28
11
|
}
|
|
29
12
|
function parseColor(value) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
13
|
+
if (!value || value === "transparent") return "";
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed.toLowerCase();
|
|
16
|
+
if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) {
|
|
17
|
+
const r = trimmed[1];
|
|
18
|
+
const g = trimmed[2];
|
|
19
|
+
const b = trimmed[3];
|
|
20
|
+
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
return trimmed;
|
|
40
23
|
}
|
|
41
24
|
function parsePaddingShorthand(value) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
25
|
+
if (!value) return {
|
|
26
|
+
top: 0,
|
|
27
|
+
right: 0,
|
|
28
|
+
bottom: 0,
|
|
29
|
+
left: 0
|
|
30
|
+
};
|
|
31
|
+
const values = value.trim().split(/\s+/).map((p) => parsePxValue(p));
|
|
32
|
+
switch (values.length) {
|
|
33
|
+
case 1: return {
|
|
34
|
+
top: values[0],
|
|
35
|
+
right: values[0],
|
|
36
|
+
bottom: values[0],
|
|
37
|
+
left: values[0]
|
|
38
|
+
};
|
|
39
|
+
case 2: return {
|
|
40
|
+
top: values[0],
|
|
41
|
+
right: values[1],
|
|
42
|
+
bottom: values[0],
|
|
43
|
+
left: values[1]
|
|
44
|
+
};
|
|
45
|
+
case 3: return {
|
|
46
|
+
top: values[0],
|
|
47
|
+
right: values[1],
|
|
48
|
+
bottom: values[2],
|
|
49
|
+
left: values[1]
|
|
50
|
+
};
|
|
51
|
+
default: return {
|
|
52
|
+
top: values[0],
|
|
53
|
+
right: values[1],
|
|
54
|
+
bottom: values[2],
|
|
55
|
+
left: values[3]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
75
58
|
}
|
|
76
59
|
function parseBorderObject(border) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
60
|
+
if (!border) return {
|
|
61
|
+
width: 0,
|
|
62
|
+
style: "solid",
|
|
63
|
+
color: "#000000"
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
width: parsePxValue(border.borderTopWidth),
|
|
67
|
+
style: border.borderTopStyle || "solid",
|
|
68
|
+
color: parseColor(border.borderTopColor) || "#000000"
|
|
69
|
+
};
|
|
83
70
|
}
|
|
84
71
|
function parseWidthPercent(value) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
72
|
+
if (!value) return 100;
|
|
73
|
+
const match = value.match(/^(\d+(?:\.\d+)?)\s*%/);
|
|
74
|
+
if (match) return Math.round(parseFloat(match[1]));
|
|
75
|
+
return 100;
|
|
89
76
|
}
|
|
90
77
|
function parseFontFamily(value) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
dribbble: "dribbble",
|
|
119
|
-
behance: "behance"
|
|
78
|
+
if (!value) return "";
|
|
79
|
+
if (typeof value === "string") return value.split(",")[0].trim().replace(/['"]/g, "");
|
|
80
|
+
if (value.value) return value.value.split(",")[0].trim().replace(/['"]/g, "");
|
|
81
|
+
if (value.label) return value.label;
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/block-mapper.ts
|
|
86
|
+
const SOCIAL_PLATFORM_MAP = {
|
|
87
|
+
facebook: "facebook",
|
|
88
|
+
twitter: "twitter",
|
|
89
|
+
x: "twitter",
|
|
90
|
+
instagram: "instagram",
|
|
91
|
+
linkedin: "linkedin",
|
|
92
|
+
youtube: "youtube",
|
|
93
|
+
tiktok: "tiktok",
|
|
94
|
+
pinterest: "pinterest",
|
|
95
|
+
email: "email",
|
|
96
|
+
mail: "email",
|
|
97
|
+
whatsapp: "whatsapp",
|
|
98
|
+
telegram: "telegram",
|
|
99
|
+
discord: "discord",
|
|
100
|
+
snapchat: "snapchat",
|
|
101
|
+
reddit: "reddit",
|
|
102
|
+
github: "github",
|
|
103
|
+
dribbble: "dribbble",
|
|
104
|
+
behance: "behance"
|
|
120
105
|
};
|
|
121
106
|
function toAlign(value, fallback = "left") {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
107
|
+
if (value === "left" || value === "center" || value === "right") return value;
|
|
108
|
+
return fallback;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Strip every `<…>` from a button label and reject any stray `<` or `>`
|
|
112
|
+
* left behind (e.g. a truncated `<script`). The original
|
|
113
|
+
* `value.replace(/<[^>]*>/g, "")` was both polynomial-ReDoS over
|
|
114
|
+
* `<<<<…` inputs and an incomplete sanitizer — an unterminated `<script`
|
|
115
|
+
* would survive the strip. Downstream HTML-escapes the label at render
|
|
116
|
+
* time, but stripping here keeps the imported JSON clean.
|
|
117
|
+
*/
|
|
125
118
|
function stripTagsPlain(text) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
return out;
|
|
119
|
+
let out = "";
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (i < text.length) {
|
|
122
|
+
if (text[i] === "<") {
|
|
123
|
+
const close = text.indexOf(">", i + 1);
|
|
124
|
+
if (close === -1) break;
|
|
125
|
+
i = close + 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (text[i] === ">") {
|
|
129
|
+
i++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
out += text[i];
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
145
136
|
}
|
|
146
137
|
function toLineStyle(value, fallback = "solid") {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return fallback;
|
|
150
|
-
}
|
|
151
|
-
function defaultMargin() {
|
|
152
|
-
return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
138
|
+
if (value === "solid" || value === "dashed" || value === "dotted") return value;
|
|
139
|
+
return fallback;
|
|
153
140
|
}
|
|
154
141
|
function makeStyles(values) {
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
padding,
|
|
158
|
-
margin: defaultMargin()
|
|
159
|
-
};
|
|
142
|
+
return { padding: parsePaddingShorthand(values.containerPadding) };
|
|
160
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Apply Unlayer text-level styles as TipTap-compatible inline markup.
|
|
146
|
+
* Mirrors the BeeFree importer's helper but reads from Unlayer's flat
|
|
147
|
+
* values shape rather than a CSS style record.
|
|
148
|
+
*/
|
|
161
149
|
function inlineStylesToHtml(html, values) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
150
|
+
const spanParts = [];
|
|
151
|
+
const fontSize = parsePxValue(values.fontSize);
|
|
152
|
+
if (fontSize && fontSize !== 16) spanParts.push(`font-size: ${fontSize}px`);
|
|
153
|
+
const color = parseColor(values.color);
|
|
154
|
+
if (color && color !== "#1a1a1a") spanParts.push(`color: ${color}`);
|
|
155
|
+
const fontWeight = values.fontWeight;
|
|
156
|
+
if (fontWeight !== void 0 && fontWeight !== null && String(fontWeight) !== "normal" && String(fontWeight) !== "400") spanParts.push(`font-weight: ${fontWeight}`);
|
|
157
|
+
const fontFamily = parseFontFamily(values.fontFamily);
|
|
158
|
+
if (fontFamily) spanParts.push(`font-family: ${fontFamily}`);
|
|
159
|
+
const textAlign = values.textAlign;
|
|
160
|
+
const pStyle = textAlign && textAlign !== "left" ? `text-align: ${textAlign}` : "";
|
|
161
|
+
if (!pStyle && spanParts.length === 0) return html;
|
|
162
|
+
const spanStyle = spanParts.join("; ");
|
|
163
|
+
let result = html;
|
|
164
|
+
if (pStyle) result = result.replace(/<p style="([^"]*)">/g, `<p style="$1; ${pStyle}">`).replaceAll("<p>", `<p style="${pStyle}">`);
|
|
165
|
+
if (spanStyle) result = wrapParagraphInner(result, spanStyle);
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Wrap the inner content of every `<p …>…</p>` with `<span style="…">…</span>`.
|
|
170
|
+
*
|
|
171
|
+
* Hand-rolled linear scanner instead of `/<p([^>]*)>([\s\S]*?)<\/p>/g` because
|
|
172
|
+
* the regex is polynomial-ReDoS: the engine retries `[\s\S]*?<\/p>` at every
|
|
173
|
+
* `<p` start, so inputs like `<p>a<p>a<p>a…` (no `</p>` ever) cost O(n²).
|
|
174
|
+
* The scanner advances `i` monotonically via `indexOf`, keeping the work
|
|
175
|
+
* strictly O(n).
|
|
176
|
+
*/
|
|
186
177
|
function wrapParagraphInner(html, spanStyle) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
178
|
+
let out = "";
|
|
179
|
+
let i = 0;
|
|
180
|
+
while (i < html.length) {
|
|
181
|
+
const open = html.indexOf("<p", i);
|
|
182
|
+
if (open === -1) {
|
|
183
|
+
out += html.substring(i);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
const afterTagName = html[open + 2];
|
|
187
|
+
if (afterTagName !== ">" && afterTagName !== " " && afterTagName !== " " && afterTagName !== "\n" && afterTagName !== "\r" && afterTagName !== "/") {
|
|
188
|
+
out += html.substring(i, open + 2);
|
|
189
|
+
i = open + 2;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const openEnd = html.indexOf(">", open + 2);
|
|
193
|
+
if (openEnd === -1) {
|
|
194
|
+
out += html.substring(i);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
const closeStart = html.indexOf("</p>", openEnd + 1);
|
|
198
|
+
if (closeStart === -1) {
|
|
199
|
+
out += html.substring(i);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
const inner = html.substring(openEnd + 1, closeStart);
|
|
203
|
+
out += html.substring(i, openEnd + 1);
|
|
204
|
+
out += `<span style="${spanStyle}">${inner}</span></p>`;
|
|
205
|
+
i = closeStart + 4;
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
217
208
|
}
|
|
218
209
|
function ensureParagraphWrapped(html) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
210
|
+
if (!html) return "<p></p>";
|
|
211
|
+
if (/<p[\s>]/i.test(html)) return html;
|
|
212
|
+
return `<p>${html}</p>`;
|
|
222
213
|
}
|
|
223
214
|
function convertText(values) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
});
|
|
215
|
+
return createParagraphBlock({
|
|
216
|
+
content: inlineStylesToHtml(ensureParagraphWrapped(values.text ?? ""), values),
|
|
217
|
+
styles: makeStyles(values)
|
|
218
|
+
});
|
|
229
219
|
}
|
|
230
220
|
function parseHeadingLevel(tag) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
221
|
+
if (!tag) return 2;
|
|
222
|
+
const match = tag.match(/^h(\d)$/i);
|
|
223
|
+
if (match) {
|
|
224
|
+
const num = Number(match[1]);
|
|
225
|
+
if (num >= 1 && num <= 4) return num;
|
|
226
|
+
}
|
|
227
|
+
return 2;
|
|
238
228
|
}
|
|
239
229
|
function convertHeading(values) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
styles: makeStyles(values)
|
|
250
|
-
});
|
|
230
|
+
const stripped = (values.text ?? "").replace(/^<h\d[^>]*>|<\/h\d>$/gi, "");
|
|
231
|
+
return createTitleBlock({
|
|
232
|
+
content: stripped ? `<p>${stripped}</p>` : "<p></p>",
|
|
233
|
+
level: parseHeadingLevel(values.headingType),
|
|
234
|
+
color: parseColor(values.color) || "#1a1a1a",
|
|
235
|
+
textAlign: toAlign(values.textAlign),
|
|
236
|
+
fontFamily: parseFontFamily(values.fontFamily) || void 0,
|
|
237
|
+
styles: makeStyles(values)
|
|
238
|
+
});
|
|
251
239
|
}
|
|
252
240
|
function convertImage(values) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
241
|
+
const src = values.src;
|
|
242
|
+
const action = values.action?.values;
|
|
243
|
+
return createImageBlock({
|
|
244
|
+
src: src?.url || "",
|
|
245
|
+
alt: values.altText || "",
|
|
246
|
+
width: src?.width ? Math.round(src.width) : 600,
|
|
247
|
+
align: toAlign(values.textAlign, "center"),
|
|
248
|
+
linkUrl: action?.href || void 0,
|
|
249
|
+
linkOpenInNewTab: action?.target === "_blank" || void 0,
|
|
250
|
+
styles: makeStyles(values)
|
|
251
|
+
});
|
|
264
252
|
}
|
|
265
253
|
function convertButton(values) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
254
|
+
const colors = values.buttonColors ?? {};
|
|
255
|
+
const padding = values.padding ? parsePaddingShorthand(values.padding) : {
|
|
256
|
+
top: 12,
|
|
257
|
+
right: 24,
|
|
258
|
+
bottom: 12,
|
|
259
|
+
left: 24
|
|
260
|
+
};
|
|
261
|
+
const label = stripTagsPlain(values.text ?? "Button");
|
|
262
|
+
const linkValues = values.href?.values;
|
|
263
|
+
return createButtonBlock({
|
|
264
|
+
text: label,
|
|
265
|
+
url: linkValues?.href || "#",
|
|
266
|
+
openInNewTab: linkValues?.target === "_blank" || void 0,
|
|
267
|
+
backgroundColor: parseColor(colors.backgroundColor) || "#4f46e5",
|
|
268
|
+
textColor: parseColor(colors.color) || "#ffffff",
|
|
269
|
+
borderRadius: parsePxValue(values.borderRadius),
|
|
270
|
+
fontSize: parsePxValue(values.fontSize) || 16,
|
|
271
|
+
fontFamily: parseFontFamily(values.fontFamily) || void 0,
|
|
272
|
+
buttonPadding: padding,
|
|
273
|
+
styles: makeStyles(values)
|
|
274
|
+
});
|
|
282
275
|
}
|
|
283
276
|
function convertDivider(values) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
277
|
+
const border = parseBorderObject(values.border);
|
|
278
|
+
return createDividerBlock({
|
|
279
|
+
lineStyle: toLineStyle(border.style),
|
|
280
|
+
color: border.color,
|
|
281
|
+
thickness: border.width || 1,
|
|
282
|
+
width: parseWidthPercent(values.width),
|
|
283
|
+
styles: makeStyles(values)
|
|
284
|
+
});
|
|
292
285
|
}
|
|
293
286
|
function convertSpacer(values) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
});
|
|
287
|
+
const padding = parsePaddingShorthand(values.containerPadding);
|
|
288
|
+
return createSpacerBlock({
|
|
289
|
+
height: parsePxValue(values.height) || padding.top + padding.bottom || 24,
|
|
290
|
+
styles: makeStyles(values)
|
|
291
|
+
});
|
|
300
292
|
}
|
|
301
293
|
function convertHtml(values) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
294
|
+
return createHtmlBlock({
|
|
295
|
+
content: values.html ?? "",
|
|
296
|
+
styles: makeStyles(values)
|
|
297
|
+
});
|
|
306
298
|
}
|
|
307
299
|
function convertSocial(values, warnings) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
styles: makeStyles(values)
|
|
329
|
-
});
|
|
300
|
+
const iconList = values.icons?.icons ?? [];
|
|
301
|
+
const icons = [];
|
|
302
|
+
for (const unlayerIcon of iconList) {
|
|
303
|
+
const id = (unlayerIcon.name ?? "").toLowerCase();
|
|
304
|
+
const platform = SOCIAL_PLATFORM_MAP[id];
|
|
305
|
+
if (!platform) {
|
|
306
|
+
warnings.push(`Unrecognized social icon "${unlayerIcon.name || id}" was skipped.`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
icons.push({
|
|
310
|
+
id: generateId(),
|
|
311
|
+
platform,
|
|
312
|
+
url: unlayerIcon.url || "#"
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return createSocialIconsBlock({
|
|
316
|
+
icons,
|
|
317
|
+
align: toAlign(values.textAlign, "center"),
|
|
318
|
+
styles: makeStyles(values)
|
|
319
|
+
});
|
|
330
320
|
}
|
|
331
321
|
function convertVideo(values) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
322
|
+
return createVideoBlock({
|
|
323
|
+
url: values.videoUrl || "",
|
|
324
|
+
thumbnailUrl: values.thumbnailUrl || "",
|
|
325
|
+
alt: values.altText || "",
|
|
326
|
+
width: 600,
|
|
327
|
+
align: toAlign(values.textAlign, "center"),
|
|
328
|
+
styles: makeStyles(values)
|
|
329
|
+
});
|
|
340
330
|
}
|
|
341
331
|
function convertMenu(values) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
styles: makeStyles(values)
|
|
360
|
-
});
|
|
332
|
+
return createMenuBlock({
|
|
333
|
+
items: (values.menu?.items ?? []).map((item) => ({
|
|
334
|
+
id: generateId(),
|
|
335
|
+
text: item.text || "",
|
|
336
|
+
url: item.link?.values?.href || "#",
|
|
337
|
+
openInNewTab: item.link?.values?.target === "_blank",
|
|
338
|
+
bold: false,
|
|
339
|
+
underline: false
|
|
340
|
+
})),
|
|
341
|
+
separator: values.separator || "|",
|
|
342
|
+
separatorColor: "#999999",
|
|
343
|
+
fontSize: parsePxValue(values.fontSize) || 14,
|
|
344
|
+
color: parseColor(values.color) || "#1a1a1a",
|
|
345
|
+
fontFamily: parseFontFamily(values.fontFamily) || void 0,
|
|
346
|
+
textAlign: toAlign(values.textAlign, "center"),
|
|
347
|
+
styles: makeStyles(values)
|
|
348
|
+
});
|
|
361
349
|
}
|
|
362
350
|
function convertHtmlFallback(content, comment) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
351
|
+
return createHtmlBlock({
|
|
352
|
+
content: `<div style="padding:12px;border:1px dashed #d1d5db;border-radius:6px;background:#fafafa;color:#6b7280;font-family:sans-serif;font-size:13px;">${comment.replace(/</g, "<").replace(/>/g, ">")}</div>`,
|
|
353
|
+
styles: makeStyles(content.values)
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Converts a single Unlayer content node to a Templatical block.
|
|
358
|
+
* Returns the block and a report entry.
|
|
359
|
+
*/
|
|
369
360
|
function convertContent(content, warnings) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
templaticalBlockType: null,
|
|
486
|
-
status: "skipped",
|
|
487
|
-
note: "Unlayer forms have no Templatical equivalent and are skipped. Most email clients block form submission anyway."
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
default:
|
|
491
|
-
return {
|
|
492
|
-
block: convertHtmlFallback(
|
|
493
|
-
content,
|
|
494
|
-
`Unsupported Unlayer content type: ${type}`
|
|
495
|
-
),
|
|
496
|
-
entry: {
|
|
497
|
-
unlayerContentType: type,
|
|
498
|
-
templaticalBlockType: "html",
|
|
499
|
-
status: "html-fallback",
|
|
500
|
-
note: `Unknown content type "${type}" converted to HTML block.`
|
|
501
|
-
}
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// src/converter.ts
|
|
361
|
+
const type = content.type;
|
|
362
|
+
const values = content.values ?? {};
|
|
363
|
+
switch (type) {
|
|
364
|
+
case "text": return {
|
|
365
|
+
block: convertText(values),
|
|
366
|
+
entry: {
|
|
367
|
+
unlayerContentType: type,
|
|
368
|
+
templaticalBlockType: "paragraph",
|
|
369
|
+
status: "converted"
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
case "heading": return {
|
|
373
|
+
block: convertHeading(values),
|
|
374
|
+
entry: {
|
|
375
|
+
unlayerContentType: type,
|
|
376
|
+
templaticalBlockType: "title",
|
|
377
|
+
status: "converted"
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
case "image": return {
|
|
381
|
+
block: convertImage(values),
|
|
382
|
+
entry: {
|
|
383
|
+
unlayerContentType: type,
|
|
384
|
+
templaticalBlockType: "image",
|
|
385
|
+
status: "converted"
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
case "button": return {
|
|
389
|
+
block: convertButton(values),
|
|
390
|
+
entry: {
|
|
391
|
+
unlayerContentType: type,
|
|
392
|
+
templaticalBlockType: "button",
|
|
393
|
+
status: "converted"
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
case "divider": return {
|
|
397
|
+
block: convertDivider(values),
|
|
398
|
+
entry: {
|
|
399
|
+
unlayerContentType: type,
|
|
400
|
+
templaticalBlockType: "divider",
|
|
401
|
+
status: "converted"
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
case "spacer": return {
|
|
405
|
+
block: convertSpacer(values),
|
|
406
|
+
entry: {
|
|
407
|
+
unlayerContentType: type,
|
|
408
|
+
templaticalBlockType: "spacer",
|
|
409
|
+
status: "converted"
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
case "html": return {
|
|
413
|
+
block: convertHtml(values),
|
|
414
|
+
entry: {
|
|
415
|
+
unlayerContentType: type,
|
|
416
|
+
templaticalBlockType: "html",
|
|
417
|
+
status: "converted"
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
case "menu": return {
|
|
421
|
+
block: convertMenu(values),
|
|
422
|
+
entry: {
|
|
423
|
+
unlayerContentType: type,
|
|
424
|
+
templaticalBlockType: "menu",
|
|
425
|
+
status: "approximated",
|
|
426
|
+
note: "Menu styles map approximately; review spacing and separator color."
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
case "social": return {
|
|
430
|
+
block: convertSocial(values, warnings),
|
|
431
|
+
entry: {
|
|
432
|
+
unlayerContentType: type,
|
|
433
|
+
templaticalBlockType: "social",
|
|
434
|
+
status: "converted"
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
case "video": return {
|
|
438
|
+
block: convertVideo(values),
|
|
439
|
+
entry: {
|
|
440
|
+
unlayerContentType: type,
|
|
441
|
+
templaticalBlockType: "video",
|
|
442
|
+
status: "converted"
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
case "timer": return {
|
|
446
|
+
block: convertHtmlFallback(content, "Unlayer timer block: rebuild manually in Templatical"),
|
|
447
|
+
entry: {
|
|
448
|
+
unlayerContentType: type,
|
|
449
|
+
templaticalBlockType: "html",
|
|
450
|
+
status: "html-fallback",
|
|
451
|
+
note: "Timer modules have no direct Templatical equivalent; placeholder HTML inserted."
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
case "form": return {
|
|
455
|
+
block: convertHtmlFallback(content, "Unlayer form block: not supported in Templatical (most email clients block form submission)"),
|
|
456
|
+
entry: {
|
|
457
|
+
unlayerContentType: type,
|
|
458
|
+
templaticalBlockType: null,
|
|
459
|
+
status: "skipped",
|
|
460
|
+
note: "Unlayer forms have no Templatical equivalent and are skipped. Most email clients block form submission anyway."
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
default: return {
|
|
464
|
+
block: convertHtmlFallback(content, `Unsupported Unlayer content type: ${type}`),
|
|
465
|
+
entry: {
|
|
466
|
+
unlayerContentType: type,
|
|
467
|
+
templaticalBlockType: "html",
|
|
468
|
+
status: "html-fallback",
|
|
469
|
+
note: `Unknown content type "${type}" converted to HTML block.`
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
//#endregion
|
|
475
|
+
//#region src/converter.ts
|
|
507
476
|
function resolveColumnLayout(cells, warnings) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
477
|
+
if (cells.length <= 1) return "1";
|
|
478
|
+
if (cells.length === 3) return "3";
|
|
479
|
+
if (cells.length === 2) {
|
|
480
|
+
const left = cells[0] ?? 1;
|
|
481
|
+
const ratio = left / (left + (cells[1] ?? 1));
|
|
482
|
+
if (ratio > .58) return "2-1";
|
|
483
|
+
if (ratio < .42) return "1-2";
|
|
484
|
+
return "2";
|
|
485
|
+
}
|
|
486
|
+
warnings.push(`Row with ${cells.length} columns was flattened to a single column. Unlayer supports arbitrary columns, but Templatical supports up to 3.`);
|
|
487
|
+
return "1";
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Converts all contents in a column to Templatical blocks.
|
|
491
|
+
*/
|
|
524
492
|
function convertColumnContents(column, entries, warnings) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
493
|
+
const blocks = [];
|
|
494
|
+
for (const content of column.contents ?? []) {
|
|
495
|
+
const { block, entry } = convertContent(content, warnings);
|
|
496
|
+
blocks.push(block);
|
|
497
|
+
entries.push(entry);
|
|
498
|
+
}
|
|
499
|
+
return blocks;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Processes a single Unlayer row into one or more Templatical blocks.
|
|
503
|
+
*/
|
|
533
504
|
function processRow(row, entries, warnings) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
margin: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
558
|
-
...rowBg ? { backgroundColor: rowBg } : {}
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
return [section];
|
|
562
|
-
}
|
|
505
|
+
const columns = row.columns;
|
|
506
|
+
if (!columns || columns.length === 0) return [];
|
|
507
|
+
const layout = resolveColumnLayout(row.cells ?? columns.map(() => 1), warnings);
|
|
508
|
+
let children;
|
|
509
|
+
if (layout === "1") {
|
|
510
|
+
const merged = [];
|
|
511
|
+
for (const column of columns) merged.push(...convertColumnContents(column, entries, warnings));
|
|
512
|
+
children = [merged];
|
|
513
|
+
} else children = columns.map((col) => convertColumnContents(col, entries, warnings));
|
|
514
|
+
const rowBg = parseColor(row.values?.backgroundColor);
|
|
515
|
+
const padding = parsePaddingShorthand(row.values?.padding);
|
|
516
|
+
return [createSectionBlock({
|
|
517
|
+
columns: layout,
|
|
518
|
+
children,
|
|
519
|
+
styles: {
|
|
520
|
+
padding,
|
|
521
|
+
...rowBg ? { backgroundColor: rowBg } : {}
|
|
522
|
+
}
|
|
523
|
+
})];
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Extracts template-level settings from the Unlayer body values.
|
|
527
|
+
*/
|
|
563
528
|
function extractSettings(template) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
529
|
+
const values = template.body?.values ?? {};
|
|
530
|
+
const width = parsePxValue(values.contentWidth);
|
|
531
|
+
const bgColor = parseColor(values.backgroundColor) || "#ffffff";
|
|
532
|
+
const fontFamily = parseFontFamily(values.fontFamily) || "Arial";
|
|
533
|
+
return {
|
|
534
|
+
width: width || 600,
|
|
535
|
+
backgroundColor: bgColor,
|
|
536
|
+
fontFamily,
|
|
537
|
+
locale: "en"
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Converts an Unlayer design JSON to Templatical TemplateContent.
|
|
542
|
+
*
|
|
543
|
+
* @param template - The parsed Unlayer JSON object (the result of `editor.saveDesign(...)`)
|
|
544
|
+
* @returns An ImportResult with the converted content and a detailed report
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```ts
|
|
548
|
+
* import { convertUnlayerTemplate } from '@templatical/import-unlayer';
|
|
549
|
+
*
|
|
550
|
+
* const unlayerJson = JSON.parse(fileContent);
|
|
551
|
+
* const { content, report } = convertUnlayerTemplate(unlayerJson);
|
|
552
|
+
*
|
|
553
|
+
* const editor = init({ container: '#editor', content });
|
|
554
|
+
*
|
|
555
|
+
* console.log(report.summary);
|
|
556
|
+
* console.log(report.warnings);
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
575
559
|
function convertUnlayerTemplate(template) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
approximated: entries.filter((e) => e.status === "approximated").length,
|
|
614
|
-
htmlFallback: entries.filter((e) => e.status === "html-fallback").length,
|
|
615
|
-
skipped: entries.filter((e) => e.status === "skipped").length
|
|
616
|
-
};
|
|
617
|
-
const report = { entries, warnings, summary };
|
|
618
|
-
return { content, report };
|
|
619
|
-
}
|
|
620
|
-
export {
|
|
621
|
-
convertUnlayerTemplate
|
|
622
|
-
};
|
|
560
|
+
if (!template?.body?.rows) throw new Error("Invalid Unlayer template: missing body.rows. Ensure you are passing a valid Unlayer JSON design (the output of editor.saveDesign).");
|
|
561
|
+
const entries = [];
|
|
562
|
+
const warnings = [];
|
|
563
|
+
const blocks = [];
|
|
564
|
+
const headers = template.body.headers ?? [];
|
|
565
|
+
const footers = template.body.footers ?? [];
|
|
566
|
+
if (headers.length > 0) {
|
|
567
|
+
warnings.push(`${headers.length} Unlayer header row(s) were imported as regular rows at the top of the template.`);
|
|
568
|
+
for (const row of headers) blocks.push(...processRow(row, entries, warnings));
|
|
569
|
+
}
|
|
570
|
+
for (const row of template.body.rows) blocks.push(...processRow(row, entries, warnings));
|
|
571
|
+
if (footers.length > 0) {
|
|
572
|
+
warnings.push(`${footers.length} Unlayer footer row(s) were imported as regular rows at the bottom of the template.`);
|
|
573
|
+
for (const row of footers) blocks.push(...processRow(row, entries, warnings));
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
content: {
|
|
577
|
+
...createDefaultTemplateContent(),
|
|
578
|
+
blocks,
|
|
579
|
+
settings: extractSettings(template)
|
|
580
|
+
},
|
|
581
|
+
report: {
|
|
582
|
+
entries,
|
|
583
|
+
warnings,
|
|
584
|
+
summary: {
|
|
585
|
+
total: entries.length,
|
|
586
|
+
converted: entries.filter((e) => e.status === "converted").length,
|
|
587
|
+
approximated: entries.filter((e) => e.status === "approximated").length,
|
|
588
|
+
htmlFallback: entries.filter((e) => e.status === "html-fallback").length,
|
|
589
|
+
skipped: entries.filter((e) => e.status === "skipped").length
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
export { convertUnlayerTemplate };
|
|
596
|
+
|
|
623
597
|
//# sourceMappingURL=index.js.map
|