@aswin.dev/import-html 0.7.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
+ (@aswin.dev/core, @aswin.dev/editor, and @aswin.dev/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 @aswin.dev/types, @aswin.dev/renderer, and
55
+ @aswin.dev/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,29 @@
1
+ # @aswin.dev/import-html
2
+
3
+ Convert HTML email templates to Templatical format.
4
+
5
+ Designed for table-based marketing email HTML — output of MJML, Mailchimp/SendGrid/Campaign Monitor exports, hand-coded HTML emails. Modern (flex/grid) HTML is preserved via HTML-fallback blocks.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @aswin.dev/import-html
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { convertHtmlTemplate } from '@aswin.dev/import-html';
17
+
18
+ const html = await fetch('/path/to/email.html').then((r) => r.text());
19
+ const { content, report } = convertHtmlTemplate(html);
20
+
21
+ console.log(report.summary);
22
+ console.log(report.warnings);
23
+ ```
24
+
25
+ See [Migration from HTML](https://docs.templatical.com/guide/migration-from-html) for the full element-mapping table.
26
+
27
+ ## License
28
+
29
+ MIT
@@ -0,0 +1,68 @@
1
+ import * as _aswin_dev_types from '@aswin.dev/types';
2
+
3
+ /**
4
+ * Type definitions for the HTML email importer.
5
+ */
6
+ /**
7
+ * Conversion status for each element processed in the import report.
8
+ */
9
+ type ConversionStatus = "converted" | "approximated" | "html-fallback" | "skipped";
10
+ /**
11
+ * A single entry in the import report.
12
+ */
13
+ interface ImportReportEntry {
14
+ /** The source HTML element tag name (e.g. "h1", "img", "table"). */
15
+ sourceTag: string;
16
+ /** The Templatical block type produced, or null if skipped. */
17
+ templaticalBlockType: string | null;
18
+ status: ConversionStatus;
19
+ note?: string;
20
+ }
21
+ /**
22
+ * The full import report returned alongside the converted template.
23
+ */
24
+ interface ImportReport {
25
+ entries: ImportReportEntry[];
26
+ warnings: string[];
27
+ summary: {
28
+ total: number;
29
+ converted: number;
30
+ approximated: number;
31
+ htmlFallback: number;
32
+ skipped: number;
33
+ };
34
+ }
35
+ /**
36
+ * The result of an HTML import operation.
37
+ */
38
+ interface ImportResult {
39
+ content: _aswin_dev_types.TemplateContent;
40
+ report: ImportReport;
41
+ }
42
+
43
+ /**
44
+ * Converts an HTML email template to Templatical TemplateContent.
45
+ *
46
+ * Designed for table-based marketing email HTML (output of MJML, Mailchimp,
47
+ * SendGrid, Campaign Monitor, hand-coded emails). Modern HTML using flex/grid
48
+ * layouts is preserved via HTML-fallback blocks.
49
+ *
50
+ * @param html - The raw HTML string (full document or body fragment).
51
+ * @returns An ImportResult with the converted content and a detailed report.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { convertHtmlTemplate } from '@aswin.dev/import-html';
56
+ *
57
+ * const html = await fetch('/email.html').then((r) => r.text());
58
+ * const { content, report } = convertHtmlTemplate(html);
59
+ *
60
+ * const editor = init({ container: '#editor', content });
61
+ *
62
+ * console.log(report.summary);
63
+ * console.log(report.warnings);
64
+ * ```
65
+ */
66
+ declare function convertHtmlTemplate(html: string): ImportResult;
67
+
68
+ export { type ConversionStatus, type ImportReport, type ImportReportEntry, type ImportResult, convertHtmlTemplate };
package/dist/index.js ADDED
@@ -0,0 +1,887 @@
1
+ // src/converter.ts
2
+ import { load } from "cheerio";
3
+ import {
4
+ createDefaultTemplateContent,
5
+ createSectionBlock as createSectionBlock2
6
+ } from "@aswin.dev/types";
7
+
8
+ // src/style-parser.ts
9
+ function parseStyleAttribute(styleAttr) {
10
+ const result = {};
11
+ if (!styleAttr) return result;
12
+ for (const decl of styleAttr.split(";")) {
13
+ const idx = decl.indexOf(":");
14
+ if (idx === -1) continue;
15
+ const key = decl.slice(0, idx).trim().toLowerCase();
16
+ const value = decl.slice(idx + 1).trim();
17
+ if (key && value) result[key] = value;
18
+ }
19
+ return result;
20
+ }
21
+ function serializeStyleAttribute(styles) {
22
+ return Object.entries(styles).map(([k, v]) => `${k}: ${v}`).join("; ");
23
+ }
24
+ function parsePxValue(value) {
25
+ if (value === void 0 || value === null || value === "") return 0;
26
+ if (typeof value === "number") return Math.round(value);
27
+ const match = value.match(/^(-?\d+(?:\.\d+)?)\s*(?:px)?\s*$/);
28
+ return match ? Math.round(parseFloat(match[1])) : 0;
29
+ }
30
+ var NAMED_COLORS = {
31
+ black: "#000000",
32
+ white: "#ffffff",
33
+ red: "#ff0000",
34
+ green: "#008000",
35
+ blue: "#0000ff",
36
+ yellow: "#ffff00",
37
+ cyan: "#00ffff",
38
+ magenta: "#ff00ff",
39
+ gray: "#808080",
40
+ grey: "#808080",
41
+ silver: "#c0c0c0",
42
+ maroon: "#800000",
43
+ olive: "#808000",
44
+ lime: "#00ff00",
45
+ aqua: "#00ffff",
46
+ teal: "#008080",
47
+ navy: "#000080",
48
+ fuchsia: "#ff00ff",
49
+ purple: "#800080",
50
+ orange: "#ffa500",
51
+ pink: "#ffc0cb"
52
+ };
53
+ function rgbToHex(r, g, b) {
54
+ const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
55
+ const hex = (n) => clamp(n).toString(16).padStart(2, "0");
56
+ return `#${hex(r)}${hex(g)}${hex(b)}`;
57
+ }
58
+ function parseColor(value) {
59
+ if (!value) return "";
60
+ const trimmed = value.trim().toLowerCase();
61
+ if (trimmed === "transparent" || trimmed === "inherit" || trimmed === "none")
62
+ return "";
63
+ if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed;
64
+ if (/^#[0-9a-f]{3}$/.test(trimmed)) {
65
+ const r = trimmed[1];
66
+ const g = trimmed[2];
67
+ const b = trimmed[3];
68
+ return `#${r}${r}${g}${g}${b}${b}`;
69
+ }
70
+ const rgbMatch = trimmed.match(
71
+ /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+\s*)?\)$/
72
+ );
73
+ if (rgbMatch) {
74
+ return rgbToHex(
75
+ parseInt(rgbMatch[1], 10),
76
+ parseInt(rgbMatch[2], 10),
77
+ parseInt(rgbMatch[3], 10)
78
+ );
79
+ }
80
+ if (NAMED_COLORS[trimmed]) return NAMED_COLORS[trimmed];
81
+ return "";
82
+ }
83
+ function parsePaddingShorthand(value) {
84
+ if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };
85
+ const parts = value.trim().split(/\s+/);
86
+ const values = parts.map((p) => parsePxValue(p));
87
+ switch (values.length) {
88
+ case 1:
89
+ return {
90
+ top: values[0],
91
+ right: values[0],
92
+ bottom: values[0],
93
+ left: values[0]
94
+ };
95
+ case 2:
96
+ return {
97
+ top: values[0],
98
+ right: values[1],
99
+ bottom: values[0],
100
+ left: values[1]
101
+ };
102
+ case 3:
103
+ return {
104
+ top: values[0],
105
+ right: values[1],
106
+ bottom: values[2],
107
+ left: values[1]
108
+ };
109
+ default:
110
+ return {
111
+ top: values[0],
112
+ right: values[1],
113
+ bottom: values[2],
114
+ left: values[3]
115
+ };
116
+ }
117
+ }
118
+ function readPaddingFromStyles(styles) {
119
+ const shorthand = parsePaddingShorthand(styles.padding);
120
+ return {
121
+ top: parsePxValue(styles["padding-top"]) || shorthand.top,
122
+ right: parsePxValue(styles["padding-right"]) || shorthand.right,
123
+ bottom: parsePxValue(styles["padding-bottom"]) || shorthand.bottom,
124
+ left: parsePxValue(styles["padding-left"]) || shorthand.left
125
+ };
126
+ }
127
+ function parseFontFamily(value) {
128
+ if (!value) return "";
129
+ return value.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
130
+ }
131
+ function parseFontWeight(value) {
132
+ if (!value) return "";
133
+ const trimmed = value.trim().toLowerCase();
134
+ if (trimmed === "normal" || trimmed === "400") return "";
135
+ return trimmed;
136
+ }
137
+ function parseAlignment(value, fallback = "left") {
138
+ const v = (value ?? "").trim().toLowerCase();
139
+ if (v === "left" || v === "center" || v === "right") return v;
140
+ return fallback;
141
+ }
142
+ function parseBorderShorthand(value) {
143
+ const fallback = { width: 0, style: "solid", color: "#000000" };
144
+ if (!value) return fallback;
145
+ const styleKeywords = /* @__PURE__ */ new Set([
146
+ "none",
147
+ "hidden",
148
+ "dotted",
149
+ "dashed",
150
+ "solid",
151
+ "double",
152
+ "groove",
153
+ "ridge",
154
+ "inset",
155
+ "outset"
156
+ ]);
157
+ let width = 0;
158
+ let style = "solid";
159
+ let color = "#000000";
160
+ for (const token of value.trim().split(/\s+/)) {
161
+ const lower = token.toLowerCase();
162
+ if (styleKeywords.has(lower)) {
163
+ style = lower;
164
+ } else if (/^-?\d+(?:\.\d+)?(?:px)?$/i.test(lower)) {
165
+ width = parsePxValue(lower);
166
+ } else {
167
+ const c = parseColor(lower);
168
+ if (c) color = c;
169
+ }
170
+ }
171
+ return { width, style, color };
172
+ }
173
+
174
+ // src/css-resolver.ts
175
+ function stripComments(css) {
176
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
177
+ }
178
+ function stripAtRules(css) {
179
+ let result = "";
180
+ let i = 0;
181
+ while (i < css.length) {
182
+ if (css[i] === "@") {
183
+ const semiIdx = css.indexOf(";", i);
184
+ const braceIdx = css.indexOf("{", i);
185
+ if (braceIdx === -1 || semiIdx !== -1 && semiIdx < braceIdx) {
186
+ i = semiIdx === -1 ? css.length : semiIdx + 1;
187
+ continue;
188
+ }
189
+ let depth = 0;
190
+ let j = braceIdx;
191
+ for (; j < css.length; j++) {
192
+ if (css[j] === "{") depth++;
193
+ else if (css[j] === "}") {
194
+ depth--;
195
+ if (depth === 0) {
196
+ j++;
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ i = j;
202
+ } else {
203
+ result += css[i];
204
+ i++;
205
+ }
206
+ }
207
+ return result;
208
+ }
209
+ function parseDeclarations(text) {
210
+ const result = {};
211
+ for (const decl of text.split(";")) {
212
+ const idx = decl.indexOf(":");
213
+ if (idx === -1) continue;
214
+ const key = decl.slice(0, idx).trim().toLowerCase();
215
+ let value = decl.slice(idx + 1).trim();
216
+ value = value.replace(/!important\s*$/i, "").trim();
217
+ if (key && value) result[key] = value;
218
+ }
219
+ return result;
220
+ }
221
+ function isSupportedSelector(selector) {
222
+ if (!selector) return false;
223
+ if (selector.includes(":")) return false;
224
+ if (selector.includes("@")) return false;
225
+ return true;
226
+ }
227
+ function parseStyleSheet(css) {
228
+ const rules = [];
229
+ const cleaned = stripAtRules(stripComments(css));
230
+ const blockRe = /([^{}]+)\{([^{}]*)\}/g;
231
+ let match;
232
+ while ((match = blockRe.exec(cleaned)) !== null) {
233
+ const selectorPart = match[1].trim();
234
+ const declarationPart = match[2];
235
+ if (!selectorPart) continue;
236
+ const selectors = selectorPart.split(",").map((s) => s.trim()).filter(isSupportedSelector);
237
+ if (selectors.length === 0) continue;
238
+ const declarations = parseDeclarations(declarationPart);
239
+ if (Object.keys(declarations).length === 0) continue;
240
+ rules.push({ selectors, declarations });
241
+ }
242
+ return rules;
243
+ }
244
+ function resolveCssStyles($) {
245
+ const styleTags = $("style");
246
+ if (styleTags.length === 0) return;
247
+ const allRules = [];
248
+ styleTags.each((_, el) => {
249
+ const css = $(el).text();
250
+ if (css) allRules.push(...parseStyleSheet(css));
251
+ });
252
+ const inlineByEl = /* @__PURE__ */ new WeakMap();
253
+ $("[style]").each((_, el) => {
254
+ inlineByEl.set(el, parseStyleAttribute($(el).attr("style")));
255
+ });
256
+ const resolvedByEl = /* @__PURE__ */ new WeakMap();
257
+ for (const rule of allRules) {
258
+ for (const selector of rule.selectors) {
259
+ let matched;
260
+ try {
261
+ matched = $(selector);
262
+ } catch {
263
+ continue;
264
+ }
265
+ matched.each((_, el) => {
266
+ const key = el;
267
+ const current = resolvedByEl.get(key) ?? {};
268
+ for (const [k, v] of Object.entries(rule.declarations)) {
269
+ current[k] = v;
270
+ }
271
+ resolvedByEl.set(key, current);
272
+ });
273
+ }
274
+ }
275
+ $("*").each((_, el) => {
276
+ const key = el;
277
+ const resolved = resolvedByEl.get(key);
278
+ if (!resolved) return;
279
+ const inline = inlineByEl.get(key) ?? {};
280
+ const merged = { ...resolved };
281
+ for (const [k, v] of Object.entries(inline)) merged[k] = v;
282
+ $(el).attr("style", serializeStyleAttribute(merged));
283
+ });
284
+ styleTags.remove();
285
+ }
286
+
287
+ // src/block-mapper.ts
288
+ import {
289
+ createTitleBlock,
290
+ createParagraphBlock,
291
+ createImageBlock,
292
+ createButtonBlock,
293
+ createDividerBlock,
294
+ createSpacerBlock,
295
+ createHtmlBlock
296
+ } from "@aswin.dev/types";
297
+ var HEADING_TAGS = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
298
+ var TEXT_TAGS = /* @__PURE__ */ new Set(["p", "span", "div"]);
299
+ function emptyMargin() {
300
+ return { top: 0, right: 0, bottom: 0, left: 0 };
301
+ }
302
+ function emptyPadding() {
303
+ return { top: 0, right: 0, bottom: 0, left: 0 };
304
+ }
305
+ function tagOf(el) {
306
+ if ("tagName" in el && typeof el.tagName === "string")
307
+ return el.tagName.toLowerCase();
308
+ return "";
309
+ }
310
+ function getStyles($el) {
311
+ return parseStyleAttribute($el.attr("style"));
312
+ }
313
+ function getInnerHtml($el) {
314
+ return $el.html() ?? "";
315
+ }
316
+ function ensureParagraphWrapped(html) {
317
+ if (!html.trim()) return "<p></p>";
318
+ if (/<(p|h[1-6]|ul|ol|blockquote)[\s>]/i.test(html)) return html;
319
+ return `<p>${html}</p>`;
320
+ }
321
+ function safeHtmlComment(message, raw) {
322
+ const escapedMessage = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
323
+ return `<!-- ${escapedMessage} -->
324
+ ${raw}`;
325
+ }
326
+ function convertHeading($el) {
327
+ const tag = tagOf($el[0]);
328
+ const styles = getStyles($el);
329
+ const levelMatch = tag.match(/^h(\d)$/);
330
+ const rawLevel = levelMatch ? Number(levelMatch[1]) : 2;
331
+ const level = rawLevel >= 1 && rawLevel <= 4 ? rawLevel : Math.min(rawLevel, 4);
332
+ const innerHtml = getInnerHtml($el);
333
+ const content = innerHtml.trim() ? `<p>${innerHtml}</p>` : "<p></p>";
334
+ return createTitleBlock({
335
+ content,
336
+ level,
337
+ color: parseColor(styles.color) || "#1a1a1a",
338
+ textAlign: parseAlignment(styles["text-align"]),
339
+ fontFamily: parseFontFamily(styles["font-family"]) || void 0,
340
+ styles: {
341
+ padding: readPaddingFromStyles(styles),
342
+ margin: emptyMargin()
343
+ }
344
+ });
345
+ }
346
+ function convertParagraph($el) {
347
+ const styles = getStyles($el);
348
+ const innerHtml = getInnerHtml($el);
349
+ const wrapped = ensureParagraphWrapped(innerHtml);
350
+ const fontParts = [];
351
+ const fontSize = parsePxValue(styles["font-size"]);
352
+ if (fontSize && fontSize !== 16) fontParts.push(`font-size: ${fontSize}px`);
353
+ const color = parseColor(styles.color);
354
+ if (color && color !== "#1a1a1a") fontParts.push(`color: ${color}`);
355
+ const fontWeight = parseFontWeight(styles["font-weight"]);
356
+ if (fontWeight) fontParts.push(`font-weight: ${fontWeight}`);
357
+ const fontFamily = parseFontFamily(styles["font-family"]);
358
+ if (fontFamily) fontParts.push(`font-family: ${fontFamily}`);
359
+ const textAlign = styles["text-align"];
360
+ let result = wrapped;
361
+ if (textAlign && textAlign !== "left") {
362
+ result = result.replace(
363
+ /<p style="([^"]*)">/g,
364
+ `<p style="$1; text-align: ${textAlign}">`
365
+ ).replaceAll("<p>", `<p style="text-align: ${textAlign}">`);
366
+ }
367
+ if (fontParts.length > 0) {
368
+ const span = fontParts.join("; ");
369
+ result = result.replace(
370
+ /<p([^>]*)>([\s\S]*?)<\/p>/g,
371
+ `<p$1><span style="${span}">$2</span></p>`
372
+ );
373
+ }
374
+ return createParagraphBlock({
375
+ content: result,
376
+ styles: {
377
+ padding: readPaddingFromStyles(styles),
378
+ margin: emptyMargin()
379
+ }
380
+ });
381
+ }
382
+ function convertImage($el) {
383
+ const styles = getStyles($el);
384
+ const src = $el.attr("src") ?? "";
385
+ const alt = $el.attr("alt") ?? "";
386
+ const widthAttr = $el.attr("width");
387
+ const widthStyle = styles.width;
388
+ const width = parsePxValue(widthAttr) || parsePxValue(widthStyle) || 600;
389
+ return createImageBlock({
390
+ src,
391
+ alt,
392
+ width,
393
+ align: parseAlignment(styles["text-align"], "center"),
394
+ styles: {
395
+ padding: readPaddingFromStyles(styles),
396
+ margin: emptyMargin()
397
+ }
398
+ });
399
+ }
400
+ function looksLikeButton(styles) {
401
+ if (parseColor(styles["background-color"]) || parseColor(styles.background))
402
+ return true;
403
+ if (styles.padding || styles["padding-top"] || styles["padding-bottom"] || styles["padding-left"] || styles["padding-right"])
404
+ return true;
405
+ if (parsePxValue(styles["border-radius"])) return true;
406
+ const display = (styles.display ?? "").toLowerCase();
407
+ if (display === "inline-block" || display === "block") return true;
408
+ return false;
409
+ }
410
+ function convertButton($el) {
411
+ const styles = getStyles($el);
412
+ const text = ($el.text() ?? "Button").trim() || "Button";
413
+ const url = $el.attr("href") ?? "#";
414
+ const target = $el.attr("target");
415
+ return createButtonBlock({
416
+ text,
417
+ url,
418
+ openInNewTab: target === "_blank" || void 0,
419
+ backgroundColor: parseColor(styles["background-color"]) || parseColor(styles.background) || "#4f46e5",
420
+ textColor: parseColor(styles.color) || "#ffffff",
421
+ borderRadius: parsePxValue(styles["border-radius"]),
422
+ fontSize: parsePxValue(styles["font-size"]) || 16,
423
+ fontFamily: parseFontFamily(styles["font-family"]) || void 0,
424
+ buttonPadding: readPaddingFromStyles(styles),
425
+ styles: {
426
+ padding: emptyPadding(),
427
+ margin: emptyMargin()
428
+ }
429
+ });
430
+ }
431
+ function convertDivider($el) {
432
+ const styles = getStyles($el);
433
+ const border = parseBorderShorthand(styles["border-top"] ?? styles.border);
434
+ const lineStyle = border.style === "dashed" || border.style === "dotted" ? border.style : "solid";
435
+ return createDividerBlock({
436
+ lineStyle,
437
+ color: border.color || "#e5e7eb",
438
+ thickness: border.width || 1,
439
+ width: 100,
440
+ styles: {
441
+ padding: readPaddingFromStyles(styles),
442
+ margin: emptyMargin()
443
+ }
444
+ });
445
+ }
446
+ function convertHtmlFallback($el, $, note) {
447
+ const outer = $.html($el) ?? "";
448
+ const content = note ? safeHtmlComment(note, outer) : outer;
449
+ const styles = getStyles($el);
450
+ return createHtmlBlock({
451
+ content,
452
+ styles: {
453
+ padding: readPaddingFromStyles(styles),
454
+ margin: emptyMargin()
455
+ }
456
+ });
457
+ }
458
+ function isSpacerCell($el) {
459
+ const text = ($el.text() ?? "").replace(/\s| /g, "");
460
+ if (text !== "") return false;
461
+ if ($el.find("img, a, hr").length > 0) return false;
462
+ const styles = getStyles($el);
463
+ const hasHeight = parsePxValue($el.attr("height")) > 0 || parsePxValue(styles.height) > 0 || parsePxValue(styles["line-height"]) > 0;
464
+ return hasHeight;
465
+ }
466
+ function isButtonCell($el, $) {
467
+ const anchors = $el.find("a");
468
+ if (anchors.length !== 1) return { match: false };
469
+ const anchor = $(anchors[0]);
470
+ if (looksLikeButton(getStyles(anchor))) return { match: true, anchor };
471
+ if (looksLikeButton(getStyles($el))) {
472
+ const href = (anchor.attr("href") ?? "").trim();
473
+ if (href !== "") {
474
+ return { match: true, anchor };
475
+ }
476
+ }
477
+ return { match: false };
478
+ }
479
+ function convertElement($el, $) {
480
+ const tag = tagOf($el[0]);
481
+ if (!tag) return null;
482
+ if (HEADING_TAGS.has(tag)) {
483
+ return {
484
+ block: convertHeading($el),
485
+ entry: {
486
+ sourceTag: tag,
487
+ templaticalBlockType: "title",
488
+ status: "converted"
489
+ }
490
+ };
491
+ }
492
+ if (tag === "img") {
493
+ return {
494
+ block: convertImage($el),
495
+ entry: {
496
+ sourceTag: tag,
497
+ templaticalBlockType: "image",
498
+ status: "converted"
499
+ }
500
+ };
501
+ }
502
+ if (tag === "a") {
503
+ if (looksLikeButton(getStyles($el))) {
504
+ return {
505
+ block: convertButton($el),
506
+ entry: {
507
+ sourceTag: tag,
508
+ templaticalBlockType: "button",
509
+ status: "converted"
510
+ }
511
+ };
512
+ }
513
+ return {
514
+ block: convertParagraph($el),
515
+ entry: {
516
+ sourceTag: tag,
517
+ templaticalBlockType: "paragraph",
518
+ status: "approximated",
519
+ note: "Inline anchor wrapped in a paragraph block."
520
+ }
521
+ };
522
+ }
523
+ if (tag === "hr") {
524
+ return {
525
+ block: convertDivider($el),
526
+ entry: {
527
+ sourceTag: tag,
528
+ templaticalBlockType: "divider",
529
+ status: "converted"
530
+ }
531
+ };
532
+ }
533
+ if (TEXT_TAGS.has(tag)) {
534
+ const text = ($el.text() ?? "").trim();
535
+ if (!text && $el.find("img, a").length === 0) return null;
536
+ return {
537
+ block: convertParagraph($el),
538
+ entry: {
539
+ sourceTag: tag,
540
+ templaticalBlockType: "paragraph",
541
+ status: "converted"
542
+ }
543
+ };
544
+ }
545
+ return {
546
+ block: convertHtmlFallback(
547
+ $el,
548
+ $,
549
+ `Unsupported element <${tag}>: preserved as raw HTML`
550
+ ),
551
+ entry: {
552
+ sourceTag: tag,
553
+ templaticalBlockType: "html",
554
+ status: "html-fallback",
555
+ note: `Unknown element "${tag}" preserved as HTML block.`
556
+ }
557
+ };
558
+ }
559
+
560
+ // src/section-builder.ts
561
+ import {
562
+ createSectionBlock,
563
+ createButtonBlock as createButtonBlock2,
564
+ createSpacerBlock as createSpacerBlock2
565
+ } from "@aswin.dev/types";
566
+ function emptyMargin2() {
567
+ return { top: 0, right: 0, bottom: 0, left: 0 };
568
+ }
569
+ function emptyPadding2() {
570
+ return { top: 0, right: 0, bottom: 0, left: 0 };
571
+ }
572
+ function getStyles2($el) {
573
+ return parseStyleAttribute($el.attr("style"));
574
+ }
575
+ function buildCellButton($cell, $anchor) {
576
+ const cellStyles = getStyles2($cell);
577
+ const aStyles = getStyles2($anchor);
578
+ const merged = { ...cellStyles, ...aStyles };
579
+ const text = ($anchor.text() ?? "Button").trim() || "Button";
580
+ const url = $anchor.attr("href") ?? "#";
581
+ const target = $anchor.attr("target");
582
+ return createButtonBlock2({
583
+ text,
584
+ url,
585
+ openInNewTab: target === "_blank" || void 0,
586
+ backgroundColor: parseColor(merged["background-color"]) || parseColor(merged.background) || "#4f46e5",
587
+ textColor: parseColor(merged.color) || "#ffffff",
588
+ borderRadius: parsePxValue(merged["border-radius"]),
589
+ fontSize: parsePxValue(merged["font-size"]) || 16,
590
+ buttonPadding: readPaddingFromStyles(merged),
591
+ styles: {
592
+ padding: emptyPadding2(),
593
+ margin: emptyMargin2()
594
+ }
595
+ });
596
+ }
597
+ function buildSpacerFromCell($cell) {
598
+ const cellStyles = getStyles2($cell);
599
+ const height = parsePxValue($cell.attr("height")) || parsePxValue(cellStyles.height) || parsePxValue(cellStyles["line-height"]) || 24;
600
+ return createSpacerBlock2({
601
+ height,
602
+ styles: {
603
+ padding: emptyPadding2(),
604
+ margin: emptyMargin2()
605
+ }
606
+ });
607
+ }
608
+ function getDirectRows($table, $) {
609
+ const rows = [];
610
+ $table.children("tr").each((_, el) => {
611
+ rows.push($(el));
612
+ });
613
+ $table.children("thead, tbody, tfoot").each((_, group) => {
614
+ $(group).children("tr").each((_i, el) => {
615
+ rows.push($(el));
616
+ });
617
+ });
618
+ return rows;
619
+ }
620
+ function getDirectCells($row, $) {
621
+ const cells = [];
622
+ $row.children("td, th").each((_, el) => {
623
+ cells.push($(el));
624
+ });
625
+ return cells;
626
+ }
627
+ function isLayoutTable($table, $) {
628
+ if ($table.find(
629
+ "img, a, h1, h2, h3, h4, h5, h6, table, hr, p, div, span, ul, ol, li, blockquote, video, iframe"
630
+ ).length > 0)
631
+ return true;
632
+ let hasNonStandardChild = false;
633
+ $table.find("td, th").each((_, td) => {
634
+ if (hasNonStandardChild) return;
635
+ if ($(td).children().length > 0) hasNonStandardChild = true;
636
+ });
637
+ return hasNonStandardChild;
638
+ }
639
+ function resolveColumnLayout(cellCount, warnings) {
640
+ if (cellCount <= 1) return "1";
641
+ if (cellCount === 2) return "2";
642
+ if (cellCount === 3) return "3";
643
+ warnings.push(
644
+ `Row with ${cellCount} columns was flattened to a single column. Templatical supports up to 3 columns per section.`
645
+ );
646
+ return "1";
647
+ }
648
+ function extractCellBlocks($cell, $, entries, warnings) {
649
+ if (isSpacerCell($cell)) {
650
+ entries.push({
651
+ sourceTag: "td",
652
+ templaticalBlockType: "spacer",
653
+ status: "converted"
654
+ });
655
+ return [buildSpacerFromCell($cell)];
656
+ }
657
+ const btn = isButtonCell($cell, $);
658
+ if (btn.match && btn.anchor) {
659
+ entries.push({
660
+ sourceTag: "td",
661
+ templaticalBlockType: "button",
662
+ status: "converted"
663
+ });
664
+ return [buildCellButton($cell, btn.anchor)];
665
+ }
666
+ const blocks = [];
667
+ const childEls = $cell.children().toArray();
668
+ if (childEls.length === 0) {
669
+ const text = ($cell.text() ?? "").trim();
670
+ if (!text) return [];
671
+ const r = convertElement($cell, $);
672
+ if (r) {
673
+ entries.push(r.entry);
674
+ blocks.push(r.block);
675
+ }
676
+ return blocks;
677
+ }
678
+ for (const childEl of childEls) {
679
+ const $child = $(childEl);
680
+ const tag = childEl.tagName?.toLowerCase() ?? "";
681
+ if (tag === "table") {
682
+ const inner = processTable($child, $, entries, warnings, true);
683
+ blocks.push(...inner);
684
+ continue;
685
+ }
686
+ if (tag === "a" && looksLikeButton(getStyles2($child))) {
687
+ const r2 = convertElement($child, $);
688
+ if (r2) {
689
+ entries.push(r2.entry);
690
+ blocks.push(r2.block);
691
+ }
692
+ continue;
693
+ }
694
+ const r = convertElement($child, $);
695
+ if (r) {
696
+ entries.push(r.entry);
697
+ blocks.push(r.block);
698
+ }
699
+ }
700
+ return blocks;
701
+ }
702
+ function processTable($table, $, entries, warnings, flattenInline = false) {
703
+ if (!isLayoutTable($table, $)) {
704
+ entries.push({
705
+ sourceTag: "table",
706
+ templaticalBlockType: "html",
707
+ status: "html-fallback",
708
+ note: "Data table preserved as HTML block."
709
+ });
710
+ return [convertHtmlFallback($table, $, "Data table preserved as HTML")];
711
+ }
712
+ const rows = getDirectRows($table, $);
713
+ if (rows.length === 0) return [];
714
+ const sections = [];
715
+ for (const $row of rows) {
716
+ const cells = getDirectCells($row, $);
717
+ if (cells.length === 0) continue;
718
+ const layout = resolveColumnLayout(cells.length, warnings);
719
+ let columnsBlocks;
720
+ if (layout === "1") {
721
+ const merged = [];
722
+ for (const $cell of cells) {
723
+ merged.push(...extractCellBlocks($cell, $, entries, warnings));
724
+ }
725
+ columnsBlocks = [merged];
726
+ } else {
727
+ columnsBlocks = cells.map(
728
+ ($cell) => extractCellBlocks($cell, $, entries, warnings)
729
+ );
730
+ }
731
+ if (flattenInline) {
732
+ for (const col of columnsBlocks) sections.push(...col);
733
+ continue;
734
+ }
735
+ const rowStyles = getStyles2($row);
736
+ const bgColor = parseColor(rowStyles["background-color"]) || parseColor(rowStyles.background);
737
+ const padding = readPaddingFromStyles(rowStyles);
738
+ sections.push(
739
+ createSectionBlock({
740
+ columns: layout,
741
+ children: columnsBlocks,
742
+ styles: {
743
+ padding,
744
+ margin: emptyMargin2(),
745
+ ...bgColor ? { backgroundColor: bgColor } : {}
746
+ }
747
+ })
748
+ );
749
+ }
750
+ return sections;
751
+ }
752
+
753
+ // src/converter.ts
754
+ function emptyMargin3() {
755
+ return { top: 0, right: 0, bottom: 0, left: 0 };
756
+ }
757
+ function emptyPadding3() {
758
+ return { top: 0, right: 0, bottom: 0, left: 0 };
759
+ }
760
+ function readPreheader($) {
761
+ const candidates = $("body").children().slice(0, 5).filter((_, el) => {
762
+ const styles = parseStyleAttribute($(el).attr("style"));
763
+ return (styles.display ?? "").toLowerCase() === "none";
764
+ });
765
+ if (candidates.length === 0) return void 0;
766
+ const text = $(candidates[0]).text().trim();
767
+ return text || void 0;
768
+ }
769
+ function extractSettings($) {
770
+ const $body = $("body");
771
+ const bodyStyles = parseStyleAttribute($body.attr("style"));
772
+ const fontFamily = parseFontFamily(bodyStyles["font-family"]) || "Arial";
773
+ const backgroundColor = parseColor(bodyStyles["background-color"]) || parseColor(bodyStyles.background) || "#ffffff";
774
+ const $outerTable = $body.find("table").first();
775
+ const widthAttr = parsePxValue($outerTable.attr("width"));
776
+ const widthStyle = parsePxValue(
777
+ parseStyleAttribute($outerTable.attr("style")).width
778
+ );
779
+ const width = widthAttr || widthStyle || 600;
780
+ const preheaderText = readPreheader($);
781
+ return {
782
+ width,
783
+ backgroundColor,
784
+ fontFamily,
785
+ locale: "en",
786
+ ...preheaderText ? { preheaderText } : {}
787
+ };
788
+ }
789
+ function wrapInSection(blocks) {
790
+ return createSectionBlock2({
791
+ columns: "1",
792
+ children: [blocks],
793
+ styles: {
794
+ padding: emptyPadding3(),
795
+ margin: emptyMargin3()
796
+ }
797
+ });
798
+ }
799
+ function processBody($, entries, warnings) {
800
+ const blocks = [];
801
+ const $body = $("body");
802
+ const children = $body.children().toArray();
803
+ let pendingLoose = [];
804
+ const flushLoose = () => {
805
+ if (pendingLoose.length > 0) {
806
+ blocks.push(wrapInSection(pendingLoose));
807
+ pendingLoose = [];
808
+ }
809
+ };
810
+ for (const childEl of children) {
811
+ const tag = childEl.tagName?.toLowerCase() ?? "";
812
+ const $child = $(childEl);
813
+ if (tag === "table") {
814
+ flushLoose();
815
+ blocks.push(...processTable($child, $, entries, warnings, false));
816
+ continue;
817
+ }
818
+ const childStyles = parseStyleAttribute($child.attr("style"));
819
+ if ((childStyles.display ?? "").toLowerCase() === "none") continue;
820
+ if ((tag === "div" || tag === "center" || tag === "main") && $child.find("table").length > 0) {
821
+ flushLoose();
822
+ $child.children().each((_, innerEl) => {
823
+ const innerTag = innerEl.tagName?.toLowerCase() ?? "";
824
+ const $inner = $(innerEl);
825
+ if (innerTag === "table") {
826
+ blocks.push(...processTable($inner, $, entries, warnings, false));
827
+ } else {
828
+ const r2 = convertElement($inner, $);
829
+ if (r2) {
830
+ entries.push(r2.entry);
831
+ pendingLoose.push(r2.block);
832
+ }
833
+ }
834
+ });
835
+ flushLoose();
836
+ continue;
837
+ }
838
+ const r = convertElement($child, $);
839
+ if (r) {
840
+ entries.push(r.entry);
841
+ pendingLoose.push(r.block);
842
+ }
843
+ }
844
+ flushLoose();
845
+ return blocks;
846
+ }
847
+ function convertHtmlTemplate(html) {
848
+ if (typeof html !== "string") {
849
+ throw new Error(
850
+ "Invalid HTML template: expected a string. Pass the raw HTML source as a string."
851
+ );
852
+ }
853
+ if (html.trim().length === 0) {
854
+ throw new Error(
855
+ "Invalid HTML template: input is empty. Pass the raw HTML source of an email."
856
+ );
857
+ }
858
+ const $ = load(html);
859
+ resolveCssStyles($);
860
+ $("script, noscript, link, meta, title").remove();
861
+ const entries = [];
862
+ const warnings = [];
863
+ const blocks = processBody($, entries, warnings);
864
+ if (blocks.length === 0) {
865
+ warnings.push(
866
+ "No convertible content was found in the HTML. The email may use a non-table layout \u2014 modern HTML support is limited."
867
+ );
868
+ }
869
+ const content = {
870
+ ...createDefaultTemplateContent(),
871
+ blocks,
872
+ settings: extractSettings($)
873
+ };
874
+ const summary = {
875
+ total: entries.length,
876
+ converted: entries.filter((e) => e.status === "converted").length,
877
+ approximated: entries.filter((e) => e.status === "approximated").length,
878
+ htmlFallback: entries.filter((e) => e.status === "html-fallback").length,
879
+ skipped: entries.filter((e) => e.status === "skipped").length
880
+ };
881
+ const report = { entries, warnings, summary };
882
+ return { content, report };
883
+ }
884
+ export {
885
+ convertHtmlTemplate
886
+ };
887
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/converter.ts","../src/style-parser.ts","../src/css-resolver.ts","../src/block-mapper.ts","../src/section-builder.ts"],"sourcesContent":["import { load } from \"cheerio\";\nimport type { CheerioAPI, Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n createDefaultTemplateContent,\n createSectionBlock,\n} from \"@aswin.dev/types\";\nimport type { Block, TemplateContent } from \"@aswin.dev/types\";\nimport { resolveCssStyles } from \"./css-resolver\";\nimport { convertElement } from \"./block-mapper\";\nimport { processTable } from \"./section-builder\";\nimport {\n parseColor,\n parseFontFamily,\n parsePxValue,\n parseStyleAttribute,\n} from \"./style-parser\";\nimport type { ImportReport, ImportReportEntry, ImportResult } from \"./types\";\n\nfunction emptyMargin() {\n return { top: 0, right: 0, bottom: 0, left: 0 };\n}\n\nfunction emptyPadding() {\n return { top: 0, right: 0, bottom: 0, left: 0 };\n}\n\nfunction readPreheader($: CheerioAPI): string | undefined {\n // Convention: a hidden <div> at the very top with `display:none` containing\n // the preheader text. Match if it appears in the first ~3 children of body.\n const candidates = $(\"body\")\n .children()\n .slice(0, 5)\n .filter((_, el) => {\n const styles = parseStyleAttribute($(el).attr(\"style\"));\n return (styles.display ?? \"\").toLowerCase() === \"none\";\n });\n if (candidates.length === 0) return undefined;\n const text = $(candidates[0]).text().trim();\n return text || undefined;\n}\n\nfunction extractSettings($: CheerioAPI): TemplateContent[\"settings\"] {\n const $body = $(\"body\");\n const bodyStyles = parseStyleAttribute($body.attr(\"style\"));\n const fontFamily = parseFontFamily(bodyStyles[\"font-family\"]) || \"Arial\";\n const backgroundColor =\n parseColor(bodyStyles[\"background-color\"]) ||\n parseColor(bodyStyles.background) ||\n \"#ffffff\";\n\n // Width heuristic: outermost table width attr, or \".container\" width style.\n const $outerTable = $body.find(\"table\").first();\n const widthAttr = parsePxValue($outerTable.attr(\"width\"));\n const widthStyle = parsePxValue(\n parseStyleAttribute($outerTable.attr(\"style\")).width,\n );\n const width = widthAttr || widthStyle || 600;\n\n const preheaderText = readPreheader($);\n\n return {\n width,\n backgroundColor,\n fontFamily,\n locale: \"en\",\n ...(preheaderText ? { preheaderText } : {}),\n };\n}\n\n/**\n * Wrap a list of free-floating blocks (those produced by top-level non-table\n * elements) in a single one-column section.\n */\nfunction wrapInSection(blocks: Block[]): Block {\n return createSectionBlock({\n columns: \"1\",\n children: [blocks],\n styles: {\n padding: emptyPadding(),\n margin: emptyMargin(),\n },\n });\n}\n\n/**\n * Walk top-level body children. Tables become sections; loose content\n * elements are accumulated and wrapped in a single one-column section.\n */\nfunction processBody(\n $: CheerioAPI,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n const blocks: Block[] = [];\n const $body = $(\"body\");\n const children = $body.children().toArray();\n\n let pendingLoose: Block[] = [];\n\n const flushLoose = () => {\n if (pendingLoose.length > 0) {\n blocks.push(wrapInSection(pendingLoose));\n pendingLoose = [];\n }\n };\n\n for (const childEl of children) {\n const tag = childEl.tagName?.toLowerCase() ?? \"\";\n const $child = $(childEl) as unknown as Cheerio<Element>;\n\n if (tag === \"table\") {\n flushLoose();\n blocks.push(...processTable($child, $, entries, warnings, false));\n continue;\n }\n\n // Skip hidden preheader divs — already captured in settings.\n const childStyles = parseStyleAttribute($child.attr(\"style\"));\n if ((childStyles.display ?? \"\").toLowerCase() === \"none\") continue;\n\n // Containers like a wrapping <div> with table children: recurse.\n if (\n (tag === \"div\" || tag === \"center\" || tag === \"main\") &&\n $child.find(\"table\").length > 0\n ) {\n flushLoose();\n $child.children().each((_, innerEl) => {\n const innerTag = innerEl.tagName?.toLowerCase() ?? \"\";\n const $inner = $(innerEl) as unknown as Cheerio<Element>;\n if (innerTag === \"table\") {\n blocks.push(...processTable($inner, $, entries, warnings, false));\n } else {\n const r = convertElement($inner, $);\n if (r) {\n entries.push(r.entry);\n pendingLoose.push(r.block);\n }\n }\n });\n flushLoose();\n continue;\n }\n\n const r = convertElement($child, $);\n if (r) {\n entries.push(r.entry);\n pendingLoose.push(r.block);\n }\n }\n\n flushLoose();\n return blocks;\n}\n\n/**\n * Converts an HTML email template to Templatical TemplateContent.\n *\n * Designed for table-based marketing email HTML (output of MJML, Mailchimp,\n * SendGrid, Campaign Monitor, hand-coded emails). Modern HTML using flex/grid\n * layouts is preserved via HTML-fallback blocks.\n *\n * @param html - The raw HTML string (full document or body fragment).\n * @returns An ImportResult with the converted content and a detailed report.\n *\n * @example\n * ```ts\n * import { convertHtmlTemplate } from '@aswin.dev/import-html';\n *\n * const html = await fetch('/email.html').then((r) => r.text());\n * const { content, report } = convertHtmlTemplate(html);\n *\n * const editor = init({ container: '#editor', content });\n *\n * console.log(report.summary);\n * console.log(report.warnings);\n * ```\n */\nexport function convertHtmlTemplate(html: string): ImportResult {\n if (typeof html !== \"string\") {\n throw new Error(\n \"Invalid HTML template: expected a string. Pass the raw HTML source as a string.\",\n );\n }\n if (html.trim().length === 0) {\n throw new Error(\n \"Invalid HTML template: input is empty. Pass the raw HTML source of an email.\",\n );\n }\n\n const $ = load(html);\n resolveCssStyles($);\n\n // Drop tags that are never useful in the editor canvas.\n $(\"script, noscript, link, meta, title\").remove();\n\n const entries: ImportReportEntry[] = [];\n const warnings: string[] = [];\n\n const blocks = processBody($, entries, warnings);\n\n if (blocks.length === 0) {\n warnings.push(\n \"No convertible content was found in the HTML. The email may use a non-table layout — modern HTML support is limited.\",\n );\n }\n\n const content: TemplateContent = {\n ...createDefaultTemplateContent(),\n blocks,\n settings: extractSettings($),\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 type { SpacingValue } from \"@aswin.dev/types\";\n\n/**\n * Parses a CSS `style=\"...\"` attribute string into a flat key/value record.\n * Keys are lowercased; values are trimmed. Quotes around values are not stripped.\n */\nexport function parseStyleAttribute(\n styleAttr: string | undefined,\n): Record<string, string> {\n const result: Record<string, string> = {};\n if (!styleAttr) return result;\n\n for (const decl of styleAttr.split(\";\")) {\n const idx = decl.indexOf(\":\");\n if (idx === -1) continue;\n const key = decl.slice(0, idx).trim().toLowerCase();\n const value = decl.slice(idx + 1).trim();\n if (key && value) result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Serializes a flat key/value record back to a `style` attribute string.\n */\nexport function serializeStyleAttribute(\n styles: Record<string, string>,\n): string {\n return Object.entries(styles)\n .map(([k, v]) => `${k}: ${v}`)\n .join(\"; \");\n}\n\n/**\n * Parses a px-like CSS value (`\"12px\"`, `\"12\"`, `12`) into a rounded integer.\n * Returns 0 for missing or unparseable input. Ignores em/% units.\n */\nexport function parsePxValue(value: string | number | undefined): number {\n if (value === undefined || value === null || value === \"\") 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\n/**\n * Parses a width value that may be a percentage. Returns the numeric percent\n * (0-100). For non-percent values, returns 100.\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\nconst NAMED_COLORS: Record<string, string> = {\n black: \"#000000\",\n white: \"#ffffff\",\n red: \"#ff0000\",\n green: \"#008000\",\n blue: \"#0000ff\",\n yellow: \"#ffff00\",\n cyan: \"#00ffff\",\n magenta: \"#ff00ff\",\n gray: \"#808080\",\n grey: \"#808080\",\n silver: \"#c0c0c0\",\n maroon: \"#800000\",\n olive: \"#808000\",\n lime: \"#00ff00\",\n aqua: \"#00ffff\",\n teal: \"#008080\",\n navy: \"#000080\",\n fuchsia: \"#ff00ff\",\n purple: \"#800080\",\n orange: \"#ffa500\",\n pink: \"#ffc0cb\",\n};\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n const clamp = (n: number) => Math.max(0, Math.min(255, Math.round(n)));\n const hex = (n: number) => clamp(n).toString(16).padStart(2, \"0\");\n return `#${hex(r)}${hex(g)}${hex(b)}`;\n}\n\n/**\n * Normalizes a CSS color value to a 6-digit lowercase hex string.\n * - 3-digit hex expands to 6-digit\n * - rgb()/rgba() converts to hex (alpha is dropped)\n * - Named colors map via lookup\n * - \"transparent\" / unknown returns \"\"\n */\nexport function parseColor(value: string | undefined): string {\n if (!value) return \"\";\n const trimmed = value.trim().toLowerCase();\n if (trimmed === \"transparent\" || trimmed === \"inherit\" || trimmed === \"none\")\n return \"\";\n\n if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed;\n\n if (/^#[0-9a-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}`;\n }\n\n const rgbMatch = trimmed.match(\n /^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*[\\d.]+\\s*)?\\)$/,\n );\n if (rgbMatch) {\n return rgbToHex(\n parseInt(rgbMatch[1], 10),\n parseInt(rgbMatch[2], 10),\n parseInt(rgbMatch[3], 10),\n );\n }\n\n if (NAMED_COLORS[trimmed]) return NAMED_COLORS[trimmed];\n\n return \"\";\n}\n\n/**\n * Parses a CSS `padding` shorthand (1-4 values) into a SpacingValue.\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\n/**\n * Reads CSS padding from a style record, preferring the longhand props\n * (padding-top/right/bottom/left) and falling back to the `padding` shorthand.\n */\nexport function readPaddingFromStyles(\n styles: Record<string, string>,\n): SpacingValue {\n const shorthand = parsePaddingShorthand(styles.padding);\n return {\n top: parsePxValue(styles[\"padding-top\"]) || shorthand.top,\n right: parsePxValue(styles[\"padding-right\"]) || shorthand.right,\n bottom: parsePxValue(styles[\"padding-bottom\"]) || shorthand.bottom,\n left: parsePxValue(styles[\"padding-left\"]) || shorthand.left,\n };\n}\n\n/**\n * Strips quotes and returns the first font in a font-family stack.\n */\nexport function parseFontFamily(value: string | undefined): string {\n if (!value) return \"\";\n return value\n .split(\",\")[0]\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\");\n}\n\n/**\n * Normalizes a font-weight value to a string CSS keyword/number that\n * the editor accepts. Returns \"\" when the value is the default (normal/400).\n */\nexport function parseFontWeight(value: string | undefined): string {\n if (!value) return \"\";\n const trimmed = value.trim().toLowerCase();\n if (trimmed === \"normal\" || trimmed === \"400\") return \"\";\n return trimmed;\n}\n\n/**\n * Parses CSS text-align to one of the allowed editor alignments.\n */\nexport function parseAlignment(\n value: string | undefined,\n fallback: \"left\" | \"center\" | \"right\" = \"left\",\n): \"left\" | \"center\" | \"right\" {\n const v = (value ?? \"\").trim().toLowerCase();\n if (v === \"left\" || v === \"center\" || v === \"right\") return v;\n return fallback;\n}\n\n/**\n * Parses a CSS `border` shorthand (`\"1px solid #ccc\"`) into width/style/color.\n * Order-tolerant: each token is classified by content.\n */\nexport function parseBorderShorthand(value: string | undefined): {\n width: number;\n style: string;\n color: string;\n} {\n const fallback = { width: 0, style: \"solid\", color: \"#000000\" };\n if (!value) return fallback;\n\n const styleKeywords = new Set([\n \"none\",\n \"hidden\",\n \"dotted\",\n \"dashed\",\n \"solid\",\n \"double\",\n \"groove\",\n \"ridge\",\n \"inset\",\n \"outset\",\n ]);\n\n let width = 0;\n let style = \"solid\";\n let color = \"#000000\";\n\n for (const token of value.trim().split(/\\s+/)) {\n const lower = token.toLowerCase();\n if (styleKeywords.has(lower)) {\n style = lower;\n } else if (/^-?\\d+(?:\\.\\d+)?(?:px)?$/i.test(lower)) {\n width = parsePxValue(lower);\n } else {\n const c = parseColor(lower);\n if (c) color = c;\n }\n }\n\n return { width, style, color };\n}\n","import type { CheerioAPI } from \"cheerio\";\nimport { parseStyleAttribute, serializeStyleAttribute } from \"./style-parser\";\n\ninterface CssRule {\n selectors: string[];\n declarations: Record<string, string>;\n}\n\n/**\n * Strips all CSS comments. Handles nested-looking content safely.\n */\nfunction stripComments(css: string): string {\n return css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n}\n\n/**\n * Strips at-rule blocks (@media, @font-face, @keyframes, @supports, etc.)\n * and their nested content. Leaves top-level rules in place.\n *\n * Email HTML rarely benefits from @media (we render at one viewport),\n * and resolving it onto elements would not be visually faithful anyway.\n */\nfunction stripAtRules(css: string): string {\n let result = \"\";\n let i = 0;\n while (i < css.length) {\n if (css[i] === \"@\") {\n // skip until matching `{...}` block or terminating `;`\n const semiIdx = css.indexOf(\";\", i);\n const braceIdx = css.indexOf(\"{\", i);\n\n if (braceIdx === -1 || (semiIdx !== -1 && semiIdx < braceIdx)) {\n i = semiIdx === -1 ? css.length : semiIdx + 1;\n continue;\n }\n\n // skip the entire `{...}` block, accounting for nesting\n let depth = 0;\n let j = braceIdx;\n for (; j < css.length; j++) {\n if (css[j] === \"{\") depth++;\n else if (css[j] === \"}\") {\n depth--;\n if (depth === 0) {\n j++;\n break;\n }\n }\n }\n i = j;\n } else {\n result += css[i];\n i++;\n }\n }\n return result;\n}\n\n/**\n * Parses a CSS declarations block (`color: red; font-size: 14px`) into\n * a flat record. `!important` markers are dropped.\n */\nfunction parseDeclarations(text: string): Record<string, string> {\n const result: Record<string, string> = {};\n for (const decl of text.split(\";\")) {\n const idx = decl.indexOf(\":\");\n if (idx === -1) continue;\n const key = decl.slice(0, idx).trim().toLowerCase();\n let value = decl.slice(idx + 1).trim();\n value = value.replace(/!important\\s*$/i, \"\").trim();\n if (key && value) result[key] = value;\n }\n return result;\n}\n\n/**\n * A selector is \"supported\" by cheerio's matcher if it has no pseudo-classes\n * or pseudo-elements. Resolving e.g. `a:hover` onto an inline style would be\n * wrong (it would always apply), so we skip such rules entirely.\n */\nfunction isSupportedSelector(selector: string): boolean {\n if (!selector) return false;\n if (selector.includes(\":\")) return false;\n if (selector.includes(\"@\")) return false;\n return true;\n}\n\n/**\n * Parses the full content of one or more `<style>` tags into a list of rules.\n * Skips at-rules and selectors with pseudo-classes.\n */\nexport function parseStyleSheet(css: string): CssRule[] {\n const rules: CssRule[] = [];\n const cleaned = stripAtRules(stripComments(css));\n\n // Greedily walk top-level `selectors { decls }` blocks.\n const blockRe = /([^{}]+)\\{([^{}]*)\\}/g;\n let match: RegExpExecArray | null;\n while ((match = blockRe.exec(cleaned)) !== null) {\n const selectorPart = match[1].trim();\n const declarationPart = match[2];\n if (!selectorPart) continue;\n\n const selectors = selectorPart\n .split(\",\")\n .map((s) => s.trim())\n .filter(isSupportedSelector);\n if (selectors.length === 0) continue;\n\n const declarations = parseDeclarations(declarationPart);\n if (Object.keys(declarations).length === 0) continue;\n\n rules.push({ selectors, declarations });\n }\n\n return rules;\n}\n\n/**\n * Reads all `<style>` tags from the document, parses them into rules,\n * applies each rule's declarations to matching elements (merging with\n * existing inline `style=\"\"` attributes — inline always wins), and removes\n * the `<style>` tags from the document.\n *\n * No specificity is computed; rules are applied in source order, with later\n * rules overriding earlier ones. Inline styles always override resolved\n * rules. This is sufficient for typical email HTML, where authors already\n * inline most styles.\n */\nexport function resolveCssStyles($: CheerioAPI): void {\n const styleTags = $(\"style\");\n if (styleTags.length === 0) return;\n\n const allRules: CssRule[] = [];\n styleTags.each((_, el) => {\n const css = $(el).text();\n if (css) allRules.push(...parseStyleSheet(css));\n });\n\n // First pass: capture each element's original inline styles, so we can\n // distinguish \"author wrote this inline\" from \"we just resolved a rule into it\".\n const inlineByEl = new WeakMap<object, Record<string, string>>();\n $(\"[style]\").each((_, el) => {\n inlineByEl.set(el as object, parseStyleAttribute($(el).attr(\"style\")));\n });\n\n // Second pass: apply rules in source order. Later rules override earlier\n // resolved ones; original inline always wins at the end.\n const resolvedByEl = new WeakMap<object, Record<string, string>>();\n\n for (const rule of allRules) {\n for (const selector of rule.selectors) {\n let matched: ReturnType<CheerioAPI>;\n try {\n matched = $(selector);\n } catch {\n // cheerio threw on an exotic selector — skip the rule.\n continue;\n }\n matched.each((_, el) => {\n const key = el as object;\n const current = resolvedByEl.get(key) ?? {};\n for (const [k, v] of Object.entries(rule.declarations)) {\n current[k] = v;\n }\n resolvedByEl.set(key, current);\n });\n }\n }\n\n // Third pass: merge resolved + original inline (inline wins) and write back.\n $(\"*\").each((_, el) => {\n const key = el as object;\n const resolved = resolvedByEl.get(key);\n if (!resolved) return;\n const inline = inlineByEl.get(key) ?? {};\n const merged: Record<string, string> = { ...resolved };\n for (const [k, v] of Object.entries(inline)) merged[k] = v;\n $(el).attr(\"style\", serializeStyleAttribute(merged));\n });\n\n styleTags.remove();\n}\n","import type { CheerioAPI, Cheerio } from \"cheerio\";\nimport type { Element, AnyNode } from \"domhandler\";\nimport {\n createTitleBlock,\n createParagraphBlock,\n createImageBlock,\n createButtonBlock,\n createDividerBlock,\n createSpacerBlock,\n createHtmlBlock,\n} from \"@aswin.dev/types\";\nimport type { Block, HeadingLevel, SpacingValue } from \"@aswin.dev/types\";\nimport type { ImportReportEntry } from \"./types\";\nimport {\n parseAlignment,\n parseBorderShorthand,\n parseColor,\n parseFontFamily,\n parseFontWeight,\n parsePxValue,\n parseStyleAttribute,\n readPaddingFromStyles,\n} from \"./style-parser\";\n\nconst HEADING_TAGS = new Set([\"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"]);\nconst TEXT_TAGS = new Set([\"p\", \"span\", \"div\"]);\n\nfunction emptyMargin(): SpacingValue {\n return { top: 0, right: 0, bottom: 0, left: 0 };\n}\n\nfunction emptyPadding(): SpacingValue {\n return { top: 0, right: 0, bottom: 0, left: 0 };\n}\n\nfunction tagOf(el: Element | AnyNode): string {\n if (\"tagName\" in el && typeof el.tagName === \"string\")\n return el.tagName.toLowerCase();\n return \"\";\n}\n\nfunction getStyles($el: Cheerio<Element>): Record<string, string> {\n return parseStyleAttribute($el.attr(\"style\"));\n}\n\n/**\n * Returns the inner HTML of `$el`.\n */\nexport function getInnerHtml($el: Cheerio<Element>): string {\n return $el.html() ?? \"\";\n}\n\nfunction ensureParagraphWrapped(html: string): string {\n if (!html.trim()) return \"<p></p>\";\n if (/<(p|h[1-6]|ul|ol|blockquote)[\\s>]/i.test(html)) return html;\n return `<p>${html}</p>`;\n}\n\nfunction safeHtmlComment(message: string, raw: string): string {\n const escapedMessage = message\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n return `<!-- ${escapedMessage} -->\\n${raw}`;\n}\n\n/**\n * Heading element (h1-h6) → Title block.\n */\nfunction convertHeading($el: Cheerio<Element>): Block {\n const tag = tagOf($el[0]);\n const styles = getStyles($el);\n const levelMatch = tag.match(/^h(\\d)$/);\n const rawLevel = levelMatch ? Number(levelMatch[1]) : 2;\n const level: HeadingLevel = (\n rawLevel >= 1 && rawLevel <= 4 ? rawLevel : Math.min(rawLevel, 4)\n ) as HeadingLevel;\n\n const innerHtml = getInnerHtml($el);\n const content = innerHtml.trim() ? `<p>${innerHtml}</p>` : \"<p></p>\";\n\n return createTitleBlock({\n content,\n level,\n color: parseColor(styles.color) || \"#1a1a1a\",\n textAlign: parseAlignment(styles[\"text-align\"]),\n fontFamily: parseFontFamily(styles[\"font-family\"]) || undefined,\n styles: {\n padding: readPaddingFromStyles(styles),\n margin: emptyMargin(),\n },\n });\n}\n\n/**\n * Paragraph or block-level text container → Paragraph block.\n */\nfunction convertParagraph($el: Cheerio<Element>): Block {\n const styles = getStyles($el);\n const innerHtml = getInnerHtml($el);\n const wrapped = ensureParagraphWrapped(innerHtml);\n\n // Apply container-level styles to the wrapping <p>.\n const fontParts: string[] = [];\n const fontSize = parsePxValue(styles[\"font-size\"]);\n if (fontSize && fontSize !== 16) fontParts.push(`font-size: ${fontSize}px`);\n const color = parseColor(styles.color);\n if (color && color !== \"#1a1a1a\") fontParts.push(`color: ${color}`);\n const fontWeight = parseFontWeight(styles[\"font-weight\"]);\n if (fontWeight) fontParts.push(`font-weight: ${fontWeight}`);\n const fontFamily = parseFontFamily(styles[\"font-family\"]);\n if (fontFamily) fontParts.push(`font-family: ${fontFamily}`);\n const textAlign = styles[\"text-align\"];\n\n let result = wrapped;\n if (textAlign && textAlign !== \"left\") {\n result = result\n .replace(\n /<p style=\"([^\"]*)\">/g,\n `<p style=\"$1; text-align: ${textAlign}\">`,\n )\n .replaceAll(\"<p>\", `<p style=\"text-align: ${textAlign}\">`);\n }\n if (fontParts.length > 0) {\n const span = fontParts.join(\"; \");\n result = result.replace(\n /<p([^>]*)>([\\s\\S]*?)<\\/p>/g,\n `<p$1><span style=\"${span}\">$2</span></p>`,\n );\n }\n\n return createParagraphBlock({\n content: result,\n styles: {\n padding: readPaddingFromStyles(styles),\n margin: emptyMargin(),\n },\n });\n}\n\n/**\n * <img> → Image block.\n */\nfunction convertImage($el: Cheerio<Element>): Block {\n const styles = getStyles($el);\n const src = $el.attr(\"src\") ?? \"\";\n const alt = $el.attr(\"alt\") ?? \"\";\n const widthAttr = $el.attr(\"width\");\n const widthStyle = styles.width;\n const width = parsePxValue(widthAttr) || parsePxValue(widthStyle) || 600;\n\n return createImageBlock({\n src,\n alt,\n width,\n align: parseAlignment(styles[\"text-align\"], \"center\"),\n styles: {\n padding: readPaddingFromStyles(styles),\n margin: emptyMargin(),\n },\n });\n}\n\n/**\n * <a> styled as a button → Button block.\n *\n * Heuristic: a single `<a>` with a non-transparent background-color OR padding\n * OR border-radius OR display: inline-block / block is treated as a button.\n */\nexport function looksLikeButton(styles: Record<string, string>): boolean {\n if (parseColor(styles[\"background-color\"]) || parseColor(styles.background))\n return true;\n if (\n styles.padding ||\n styles[\"padding-top\"] ||\n styles[\"padding-bottom\"] ||\n styles[\"padding-left\"] ||\n styles[\"padding-right\"]\n )\n return true;\n if (parsePxValue(styles[\"border-radius\"])) return true;\n const display = (styles.display ?? \"\").toLowerCase();\n if (display === \"inline-block\" || display === \"block\") return true;\n return false;\n}\n\nfunction convertButton($el: Cheerio<Element>): Block {\n const styles = getStyles($el);\n const text = ($el.text() ?? \"Button\").trim() || \"Button\";\n const url = $el.attr(\"href\") ?? \"#\";\n const target = $el.attr(\"target\");\n\n return createButtonBlock({\n text,\n url,\n openInNewTab: target === \"_blank\" || undefined,\n backgroundColor:\n parseColor(styles[\"background-color\"]) ||\n parseColor(styles.background) ||\n \"#4f46e5\",\n textColor: parseColor(styles.color) || \"#ffffff\",\n borderRadius: parsePxValue(styles[\"border-radius\"]),\n fontSize: parsePxValue(styles[\"font-size\"]) || 16,\n fontFamily: parseFontFamily(styles[\"font-family\"]) || undefined,\n buttonPadding: readPaddingFromStyles(styles),\n styles: {\n padding: emptyPadding(),\n margin: emptyMargin(),\n },\n });\n}\n\n/**\n * <hr> → Divider block.\n */\nfunction convertDivider($el: Cheerio<Element>): Block {\n const styles = getStyles($el);\n const border = parseBorderShorthand(styles[\"border-top\"] ?? styles.border);\n const lineStyle =\n border.style === \"dashed\" || border.style === \"dotted\"\n ? border.style\n : \"solid\";\n\n return createDividerBlock({\n lineStyle: lineStyle as \"solid\" | \"dashed\" | \"dotted\",\n color: border.color || \"#e5e7eb\",\n thickness: border.width || 1,\n width: 100,\n styles: {\n padding: readPaddingFromStyles(styles),\n margin: emptyMargin(),\n },\n });\n}\n\n/**\n * Empty `<td>` with explicit height → Spacer block.\n */\nfunction convertSpacer($el: Cheerio<Element>): Block {\n const styles = getStyles($el);\n const heightAttr = $el.attr(\"height\");\n const height =\n parsePxValue(heightAttr) ||\n parsePxValue(styles.height) ||\n parsePxValue(styles[\"line-height\"]) ||\n 24;\n\n return createSpacerBlock({\n height,\n styles: {\n padding: emptyPadding(),\n margin: emptyMargin(),\n },\n });\n}\n\n/**\n * Wraps the element's outerHTML in an HTML block (the lossless fallback).\n */\nexport function convertHtmlFallback(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n note?: string,\n): Block {\n const outer = $.html($el) ?? \"\";\n const content = note ? safeHtmlComment(note, outer) : outer;\n const styles = getStyles($el);\n\n return createHtmlBlock({\n content,\n styles: {\n padding: readPaddingFromStyles(styles),\n margin: emptyMargin(),\n },\n });\n}\n\n/**\n * Decides whether a `<td>` looks like a vertical spacer:\n * empty (or only `&nbsp;`) AND has an explicit height.\n */\nexport function isSpacerCell($el: Cheerio<Element>): boolean {\n const text = ($el.text() ?? \"\").replace(/\\s| /g, \"\");\n if (text !== \"\") return false;\n if ($el.find(\"img, a, hr\").length > 0) return false;\n\n const styles = getStyles($el);\n const hasHeight =\n parsePxValue($el.attr(\"height\")) > 0 ||\n parsePxValue(styles.height) > 0 ||\n parsePxValue(styles[\"line-height\"]) > 0;\n return hasHeight;\n}\n\n/**\n * Decides whether a `<td>` is a button container — i.e. has exactly one\n * `<a>` inside that itself looks like a button.\n */\nexport function isButtonCell(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n): { match: boolean; anchor?: Cheerio<Element> } {\n const anchors = $el.find(\"a\");\n if (anchors.length !== 1) return { match: false };\n const anchor = $(anchors[0]);\n if (looksLikeButton(getStyles(anchor))) return { match: true, anchor };\n // Cell-level styling (bg, padding) wrapping a plain anchor reads as a\n // button only when the anchor actually has an href. Without one, the\n // anchor is a decorative styled span and should fall through to the\n // text-conversion path; otherwise convertButton defaults href to \"#\"\n // and the import becomes a clickable button to nowhere.\n if (looksLikeButton(getStyles($el))) {\n const href = (anchor.attr(\"href\") ?? \"\").trim();\n if (href !== \"\") {\n return { match: true, anchor };\n }\n }\n return { match: false };\n}\n\n/**\n * Converts a single content-bearing element (heading / paragraph / image /\n * anchor-as-button / divider) to a Templatical block.\n *\n * Returns `null` for elements that do not contain any meaningful content\n * (the caller should skip them).\n */\nexport function convertElement(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n): { block: Block; entry: ImportReportEntry } | null {\n const tag = tagOf($el[0]);\n if (!tag) return null;\n\n if (HEADING_TAGS.has(tag)) {\n return {\n block: convertHeading($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"title\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"img\") {\n return {\n block: convertImage($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"a\") {\n if (looksLikeButton(getStyles($el))) {\n return {\n block: convertButton($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n }\n // Plain anchor — wrap as paragraph.\n return {\n block: convertParagraph($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"paragraph\",\n status: \"approximated\",\n note: \"Inline anchor wrapped in a paragraph block.\",\n },\n };\n }\n\n if (tag === \"hr\") {\n return {\n block: convertDivider($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n }\n\n if (TEXT_TAGS.has(tag)) {\n const text = ($el.text() ?? \"\").trim();\n if (!text && $el.find(\"img, a\").length === 0) return null;\n return {\n block: convertParagraph($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n }\n\n // Unknown element — preserve as HTML.\n return {\n block: convertHtmlFallback(\n $el,\n $,\n `Unsupported element <${tag}>: preserved as raw HTML`,\n ),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `Unknown element \"${tag}\" preserved as HTML block.`,\n },\n };\n}\n\n/**\n * Helpers exported for tests.\n */\nexport const _internal = {\n convertButton,\n convertDivider,\n convertHeading,\n convertImage,\n convertParagraph,\n convertSpacer,\n ensureParagraphWrapped,\n};\n","import type { CheerioAPI, Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n createSectionBlock,\n createButtonBlock,\n createSpacerBlock,\n} from \"@aswin.dev/types\";\nimport type { Block, ColumnLayout } from \"@aswin.dev/types\";\nimport {\n convertElement,\n convertHtmlFallback,\n isButtonCell,\n isSpacerCell,\n looksLikeButton,\n} from \"./block-mapper\";\nimport {\n parseColor,\n parsePxValue,\n parseStyleAttribute,\n readPaddingFromStyles,\n} from \"./style-parser\";\nimport type { ImportReportEntry } from \"./types\";\n\nfunction emptyMargin() {\n return { top: 0, right: 0, bottom: 0, left: 0 };\n}\n\nfunction emptyPadding() {\n return { top: 0, right: 0, bottom: 0, left: 0 };\n}\n\nfunction getStyles($el: Cheerio<Element>): Record<string, string> {\n return parseStyleAttribute($el.attr(\"style\"));\n}\n\nfunction buildCellButton(\n $cell: Cheerio<Element>,\n $anchor: Cheerio<Element>,\n): Block {\n const cellStyles = getStyles($cell);\n const aStyles = getStyles($anchor);\n // Anchor styles win when they overlap (typical: anchor sets text color, cell sets bg).\n const merged = { ...cellStyles, ...aStyles };\n const text = ($anchor.text() ?? \"Button\").trim() || \"Button\";\n const url = $anchor.attr(\"href\") ?? \"#\";\n const target = $anchor.attr(\"target\");\n\n return createButtonBlock({\n text,\n url,\n openInNewTab: target === \"_blank\" || undefined,\n backgroundColor:\n parseColor(merged[\"background-color\"]) ||\n parseColor(merged.background) ||\n \"#4f46e5\",\n textColor: parseColor(merged.color) || \"#ffffff\",\n borderRadius: parsePxValue(merged[\"border-radius\"]),\n fontSize: parsePxValue(merged[\"font-size\"]) || 16,\n buttonPadding: readPaddingFromStyles(merged),\n styles: {\n padding: emptyPadding(),\n margin: emptyMargin(),\n },\n });\n}\n\nfunction buildSpacerFromCell($cell: Cheerio<Element>): Block {\n const cellStyles = getStyles($cell);\n const height =\n parsePxValue($cell.attr(\"height\")) ||\n parsePxValue(cellStyles.height) ||\n parsePxValue(cellStyles[\"line-height\"]) ||\n 24;\n return createSpacerBlock({\n height,\n styles: {\n padding: emptyPadding(),\n margin: emptyMargin(),\n },\n });\n}\n\n/**\n * Returns the direct child `<tr>` rows of a table, including those one level\n * inside `<thead>`, `<tbody>`, or `<tfoot>` (which the HTML parser inserts\n * automatically).\n */\nfunction getDirectRows(\n $table: Cheerio<Element>,\n $: CheerioAPI,\n): Cheerio<Element>[] {\n const rows: Cheerio<Element>[] = [];\n $table.children(\"tr\").each((_, el) => {\n rows.push($(el) as unknown as Cheerio<Element>);\n });\n $table.children(\"thead, tbody, tfoot\").each((_, group) => {\n $(group)\n .children(\"tr\")\n .each((_i, el) => {\n rows.push($(el) as unknown as Cheerio<Element>);\n });\n });\n return rows;\n}\n\nfunction getDirectCells(\n $row: Cheerio<Element>,\n $: CheerioAPI,\n): Cheerio<Element>[] {\n const cells: Cheerio<Element>[] = [];\n $row.children(\"td, th\").each((_, el) => {\n cells.push($(el) as unknown as Cheerio<Element>);\n });\n return cells;\n}\n\nfunction isLayoutTable($table: Cheerio<Element>, $: CheerioAPI): boolean {\n // A table is \"layout\" if any descendant carries content email blocks rely on,\n // OR if any cell contains a non-text element (custom tags, semantic blocks,\n // etc. — those should be preserved as html-fallback at the element level\n // rather than collapsing the entire table into one html block).\n // A bare data table (cells contain only text) is preserved as HTML.\n if (\n $table.find(\n \"img, a, h1, h2, h3, h4, h5, h6, table, hr, p, div, span, ul, ol, li, blockquote, video, iframe\",\n ).length > 0\n )\n return true;\n\n let hasNonStandardChild = false;\n $table.find(\"td, th\").each((_, td) => {\n if (hasNonStandardChild) return;\n if ($(td).children().length > 0) hasNonStandardChild = true;\n });\n return hasNonStandardChild;\n}\n\nfunction resolveColumnLayout(\n cellCount: number,\n warnings: string[],\n): ColumnLayout {\n if (cellCount <= 1) return \"1\";\n if (cellCount === 2) return \"2\";\n if (cellCount === 3) return \"3\";\n warnings.push(\n `Row with ${cellCount} columns was flattened to a single column. Templatical supports up to 3 columns per section.`,\n );\n return \"1\";\n}\n\nfunction extractCellBlocks(\n $cell: Cheerio<Element>,\n $: CheerioAPI,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n if (isSpacerCell($cell)) {\n entries.push({\n sourceTag: \"td\",\n templaticalBlockType: \"spacer\",\n status: \"converted\",\n });\n return [buildSpacerFromCell($cell)];\n }\n\n const btn = isButtonCell($cell, $);\n if (btn.match && btn.anchor) {\n entries.push({\n sourceTag: \"td\",\n templaticalBlockType: \"button\",\n status: \"converted\",\n });\n return [buildCellButton($cell, btn.anchor)];\n }\n\n const blocks: Block[] = [];\n const childEls = $cell.children().toArray();\n\n if (childEls.length === 0) {\n const text = ($cell.text() ?? \"\").trim();\n if (!text) return [];\n const r = convertElement($cell, $);\n if (r) {\n entries.push(r.entry);\n blocks.push(r.block);\n }\n return blocks;\n }\n\n for (const childEl of childEls) {\n const $child = $(childEl) as unknown as Cheerio<Element>;\n const tag = childEl.tagName?.toLowerCase() ?? \"\";\n\n if (tag === \"table\") {\n const inner = processTable($child, $, entries, warnings, true);\n blocks.push(...inner);\n continue;\n }\n\n if (tag === \"a\" && looksLikeButton(getStyles($child))) {\n const r = convertElement($child, $);\n if (r) {\n entries.push(r.entry);\n blocks.push(r.block);\n }\n continue;\n }\n\n const r = convertElement($child, $);\n if (r) {\n entries.push(r.entry);\n blocks.push(r.block);\n }\n }\n\n return blocks;\n}\n\n/**\n * Walk a `<table>` and produce Section blocks (one per row).\n *\n * @param flattenInline - When true (used for nested tables), drop the section\n * wrapper and return the flat block list. Templatical sections cannot nest,\n * so nested layout-tables are merged into their parent cell.\n */\nexport function processTable(\n $table: Cheerio<Element>,\n $: CheerioAPI,\n entries: ImportReportEntry[],\n warnings: string[],\n flattenInline = false,\n): Block[] {\n if (!isLayoutTable($table, $)) {\n entries.push({\n sourceTag: \"table\",\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: \"Data table preserved as HTML block.\",\n });\n return [convertHtmlFallback($table, $, \"Data table preserved as HTML\")];\n }\n\n const rows = getDirectRows($table, $);\n if (rows.length === 0) return [];\n\n const sections: Block[] = [];\n\n for (const $row of rows) {\n const cells = getDirectCells($row, $);\n if (cells.length === 0) continue;\n\n const layout = resolveColumnLayout(cells.length, warnings);\n\n let columnsBlocks: Block[][];\n if (layout === \"1\") {\n const merged: Block[] = [];\n for (const $cell of cells) {\n merged.push(...extractCellBlocks($cell, $, entries, warnings));\n }\n columnsBlocks = [merged];\n } else {\n columnsBlocks = cells.map(($cell) =>\n extractCellBlocks($cell, $, entries, warnings),\n );\n }\n\n if (flattenInline) {\n for (const col of columnsBlocks) sections.push(...col);\n continue;\n }\n\n const rowStyles = getStyles($row);\n const bgColor =\n parseColor(rowStyles[\"background-color\"]) ||\n parseColor(rowStyles.background);\n const padding = readPaddingFromStyles(rowStyles);\n\n sections.push(\n createSectionBlock({\n columns: layout,\n children: columnsBlocks,\n styles: {\n padding,\n margin: emptyMargin(),\n ...(bgColor ? { backgroundColor: bgColor } : {}),\n },\n }),\n );\n }\n\n return sections;\n}\n"],"mappings":"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","names":["createSectionBlock","createButtonBlock","createSpacerBlock","emptyMargin","emptyPadding","getStyles","createButtonBlock","createSpacerBlock","r","emptyMargin","emptyPadding","createSectionBlock","r"]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@aswin.dev/import-html",
3
+ "description": "Convert HTML email templates to Templatical format",
4
+ "version": "0.7.0",
5
+ "bugs": "https://github.com/templatical/sdk/issues",
6
+ "dependencies": {
7
+ "cheerio": "^1.0.0",
8
+ "domhandler": "^5.0.3",
9
+ "@aswin.dev/types": "0.7.0"
10
+ },
11
+ "devDependencies": {
12
+ "tsup": "^8.5.1",
13
+ "typescript": "^6.0.3",
14
+ "vitest": "^4.1.5"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "homepage": "https://templatical.com",
26
+ "keywords": [
27
+ "email",
28
+ "email-template",
29
+ "html",
30
+ "html-import",
31
+ "importer",
32
+ "migration",
33
+ "templatical"
34
+ ],
35
+ "license": "MIT",
36
+ "module": "./dist/index.js",
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/templatical/sdk.git",
43
+ "directory": "packages/import-html"
44
+ },
45
+ "type": "module",
46
+ "types": "./dist/index.d.ts",
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "test": "vitest run --config vitest.config.ts",
50
+ "typecheck": "tsc --noEmit"
51
+ }
52
+ }