@templatical/import-html
Advanced tools
+22
-21
@@ -1,3 +0,2 @@ | ||
| import * as _templatical_types from '@templatical/types'; | ||
| //#region src/types.d.ts | ||
| /** | ||
@@ -14,8 +13,8 @@ * Type definitions for the HTML email importer. | ||
| interface ImportReportEntry { | ||
| /** The source HTML element tag name (e.g. "h1", "img", "table"). */ | ||
| sourceTag: string; | ||
| /** The Templatical block type produced, or null if skipped. */ | ||
| templaticalBlockType: string | null; | ||
| status: ConversionStatus; | ||
| note?: string; | ||
| /** The source HTML element tag name (e.g. "h1", "img", "table"). */ | ||
| sourceTag: string; | ||
| /** The Templatical block type produced, or null if skipped. */ | ||
| templaticalBlockType: string | null; | ||
| status: ConversionStatus; | ||
| note?: string; | ||
| } | ||
@@ -26,11 +25,11 @@ /** | ||
| interface ImportReport { | ||
| entries: ImportReportEntry[]; | ||
| warnings: string[]; | ||
| summary: { | ||
| total: number; | ||
| converted: number; | ||
| approximated: number; | ||
| htmlFallback: number; | ||
| skipped: number; | ||
| }; | ||
| entries: ImportReportEntry[]; | ||
| warnings: string[]; | ||
| summary: { | ||
| total: number; | ||
| converted: number; | ||
| approximated: number; | ||
| htmlFallback: number; | ||
| skipped: number; | ||
| }; | ||
| } | ||
@@ -41,6 +40,7 @@ /** | ||
| interface ImportResult { | ||
| content: _templatical_types.TemplateContent; | ||
| report: ImportReport; | ||
| content: import("@templatical/types").TemplateContent; | ||
| report: ImportReport; | ||
| } | ||
| //#endregion | ||
| //#region src/converter.d.ts | ||
| /** | ||
@@ -70,3 +70,4 @@ * Converts an HTML email template to Templatical TemplateContent. | ||
| declare function convertHtmlTemplate(html: string): ImportResult; | ||
| //#endregion | ||
| export { type ConversionStatus, type ImportReport, type ImportReportEntry, type ImportResult, convertHtmlTemplate }; | ||
| //# sourceMappingURL=index.d.ts.map |
+864
-772
@@ -1,868 +0,960 @@ | ||
| // src/converter.ts | ||
| import { load } from "cheerio"; | ||
| import { | ||
| createDefaultTemplateContent, | ||
| createSectionBlock as createSectionBlock2 | ||
| } from "@templatical/types"; | ||
| // src/style-parser.ts | ||
| import { createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createImageBlock, createParagraphBlock, createSectionBlock, createSpacerBlock, createTitleBlock } from "@templatical/types"; | ||
| //#region src/style-parser.ts | ||
| /** | ||
| * Parses a CSS `style="..."` attribute string into a flat key/value record. | ||
| * Keys are lowercased; values are trimmed. Quotes around values are not stripped. | ||
| */ | ||
| function parseStyleAttribute(styleAttr) { | ||
| const result = {}; | ||
| if (!styleAttr) return result; | ||
| for (const decl of styleAttr.split(";")) { | ||
| const idx = decl.indexOf(":"); | ||
| if (idx === -1) continue; | ||
| const key = decl.slice(0, idx).trim().toLowerCase(); | ||
| const value = decl.slice(idx + 1).trim(); | ||
| if (key && value) result[key] = value; | ||
| } | ||
| return result; | ||
| const result = {}; | ||
| if (!styleAttr) return result; | ||
| for (const decl of styleAttr.split(";")) { | ||
| const idx = decl.indexOf(":"); | ||
| if (idx === -1) continue; | ||
| const key = decl.slice(0, idx).trim().toLowerCase(); | ||
| const value = decl.slice(idx + 1).trim(); | ||
| if (key && value) result[key] = value; | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Serializes a flat key/value record back to a `style` attribute string. | ||
| */ | ||
| function serializeStyleAttribute(styles) { | ||
| return Object.entries(styles).map(([k, v]) => `${k}: ${v}`).join("; "); | ||
| return Object.entries(styles).map(([k, v]) => `${k}: ${v}`).join("; "); | ||
| } | ||
| /** | ||
| * Parses a px-like CSS value (`"12px"`, `"12"`, `12`) into a rounded integer. | ||
| * Returns 0 for missing or unparseable input. Ignores em/% units. | ||
| */ | ||
| function parsePxValue(value) { | ||
| if (value === void 0 || value === null || value === "") return 0; | ||
| if (typeof value === "number") return Math.round(value); | ||
| const match = value.match(/^(-?\d+(?:\.\d+)?)\s*(?:px)?\s*$/); | ||
| return match ? Math.round(parseFloat(match[1])) : 0; | ||
| if (value === void 0 || value === null || value === "") return 0; | ||
| if (typeof value === "number") return Math.round(value); | ||
| const match = value.match(/^(-?\d+(?:\.\d+)?)\s*(?:px)?\s*$/); | ||
| return match ? Math.round(parseFloat(match[1])) : 0; | ||
| } | ||
| var NAMED_COLORS = { | ||
| black: "#000000", | ||
| white: "#ffffff", | ||
| red: "#ff0000", | ||
| green: "#008000", | ||
| blue: "#0000ff", | ||
| yellow: "#ffff00", | ||
| cyan: "#00ffff", | ||
| magenta: "#ff00ff", | ||
| gray: "#808080", | ||
| grey: "#808080", | ||
| silver: "#c0c0c0", | ||
| maroon: "#800000", | ||
| olive: "#808000", | ||
| lime: "#00ff00", | ||
| aqua: "#00ffff", | ||
| teal: "#008080", | ||
| navy: "#000080", | ||
| fuchsia: "#ff00ff", | ||
| purple: "#800080", | ||
| orange: "#ffa500", | ||
| pink: "#ffc0cb" | ||
| const NAMED_COLORS = { | ||
| black: "#000000", | ||
| white: "#ffffff", | ||
| red: "#ff0000", | ||
| green: "#008000", | ||
| blue: "#0000ff", | ||
| yellow: "#ffff00", | ||
| cyan: "#00ffff", | ||
| magenta: "#ff00ff", | ||
| gray: "#808080", | ||
| grey: "#808080", | ||
| silver: "#c0c0c0", | ||
| maroon: "#800000", | ||
| olive: "#808000", | ||
| lime: "#00ff00", | ||
| aqua: "#00ffff", | ||
| teal: "#008080", | ||
| navy: "#000080", | ||
| fuchsia: "#ff00ff", | ||
| purple: "#800080", | ||
| orange: "#ffa500", | ||
| pink: "#ffc0cb" | ||
| }; | ||
| function rgbToHex(r, g, b) { | ||
| const clamp = (n) => Math.max(0, Math.min(255, Math.round(n))); | ||
| const hex = (n) => clamp(n).toString(16).padStart(2, "0"); | ||
| return `#${hex(r)}${hex(g)}${hex(b)}`; | ||
| const clamp = (n) => Math.max(0, Math.min(255, Math.round(n))); | ||
| const hex = (n) => clamp(n).toString(16).padStart(2, "0"); | ||
| return `#${hex(r)}${hex(g)}${hex(b)}`; | ||
| } | ||
| /** | ||
| * Normalizes a CSS color value to a 6-digit lowercase hex string. | ||
| * - 3-digit hex expands to 6-digit | ||
| * - rgb()/rgba() converts to hex (alpha is dropped) | ||
| * - Named colors map via lookup | ||
| * - "transparent" / unknown returns "" | ||
| */ | ||
| function parseColor(value) { | ||
| if (!value) return ""; | ||
| const trimmed = value.trim().toLowerCase(); | ||
| if (trimmed === "transparent" || trimmed === "inherit" || trimmed === "none") | ||
| return ""; | ||
| if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed; | ||
| if (/^#[0-9a-f]{3}$/.test(trimmed)) { | ||
| const r = trimmed[1]; | ||
| const g = trimmed[2]; | ||
| const b = trimmed[3]; | ||
| return `#${r}${r}${g}${g}${b}${b}`; | ||
| } | ||
| const rgbMatch = trimmed.match( | ||
| /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+\s*)?\)$/ | ||
| ); | ||
| if (rgbMatch) { | ||
| return rgbToHex( | ||
| parseInt(rgbMatch[1], 10), | ||
| parseInt(rgbMatch[2], 10), | ||
| parseInt(rgbMatch[3], 10) | ||
| ); | ||
| } | ||
| if (NAMED_COLORS[trimmed]) return NAMED_COLORS[trimmed]; | ||
| return ""; | ||
| if (!value) return ""; | ||
| const trimmed = value.trim().toLowerCase(); | ||
| if (trimmed === "transparent" || trimmed === "inherit" || trimmed === "none") return ""; | ||
| if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed; | ||
| if (/^#[0-9a-f]{3}$/.test(trimmed)) { | ||
| const r = trimmed[1]; | ||
| const g = trimmed[2]; | ||
| const b = trimmed[3]; | ||
| return `#${r}${r}${g}${g}${b}${b}`; | ||
| } | ||
| const rgbMatch = trimmed.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+\s*)?\)$/); | ||
| if (rgbMatch) return rgbToHex(parseInt(rgbMatch[1], 10), parseInt(rgbMatch[2], 10), parseInt(rgbMatch[3], 10)); | ||
| if (NAMED_COLORS[trimmed]) return NAMED_COLORS[trimmed]; | ||
| return ""; | ||
| } | ||
| /** | ||
| * Parses a CSS `padding` shorthand (1-4 values) into a SpacingValue. | ||
| */ | ||
| function parsePaddingShorthand(value) { | ||
| if (!value) return { top: 0, right: 0, bottom: 0, left: 0 }; | ||
| const parts = value.trim().split(/\s+/); | ||
| const values = parts.map((p) => parsePxValue(p)); | ||
| switch (values.length) { | ||
| case 1: | ||
| return { | ||
| top: values[0], | ||
| right: values[0], | ||
| bottom: values[0], | ||
| left: values[0] | ||
| }; | ||
| case 2: | ||
| return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[0], | ||
| left: values[1] | ||
| }; | ||
| case 3: | ||
| return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[2], | ||
| left: values[1] | ||
| }; | ||
| default: | ||
| return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[2], | ||
| left: values[3] | ||
| }; | ||
| } | ||
| if (!value) return { | ||
| top: 0, | ||
| right: 0, | ||
| bottom: 0, | ||
| left: 0 | ||
| }; | ||
| const values = value.trim().split(/\s+/).map((p) => parsePxValue(p)); | ||
| switch (values.length) { | ||
| case 1: return { | ||
| top: values[0], | ||
| right: values[0], | ||
| bottom: values[0], | ||
| left: values[0] | ||
| }; | ||
| case 2: return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[0], | ||
| left: values[1] | ||
| }; | ||
| case 3: return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[2], | ||
| left: values[1] | ||
| }; | ||
| default: return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[2], | ||
| left: values[3] | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * Reads CSS padding from a style record, preferring the longhand props | ||
| * (padding-top/right/bottom/left) and falling back to the `padding` shorthand. | ||
| */ | ||
| function readPaddingFromStyles(styles) { | ||
| const shorthand = parsePaddingShorthand(styles.padding); | ||
| return { | ||
| top: parsePxValue(styles["padding-top"]) || shorthand.top, | ||
| right: parsePxValue(styles["padding-right"]) || shorthand.right, | ||
| bottom: parsePxValue(styles["padding-bottom"]) || shorthand.bottom, | ||
| left: parsePxValue(styles["padding-left"]) || shorthand.left | ||
| }; | ||
| const shorthand = parsePaddingShorthand(styles.padding); | ||
| return { | ||
| top: parsePxValue(styles["padding-top"]) || shorthand.top, | ||
| right: parsePxValue(styles["padding-right"]) || shorthand.right, | ||
| bottom: parsePxValue(styles["padding-bottom"]) || shorthand.bottom, | ||
| left: parsePxValue(styles["padding-left"]) || shorthand.left | ||
| }; | ||
| } | ||
| /** | ||
| * Strips quotes and returns the first font in a font-family stack. | ||
| */ | ||
| function parseFontFamily(value) { | ||
| if (!value) return ""; | ||
| return value.split(",")[0].trim().replace(/^['"]|['"]$/g, ""); | ||
| if (!value) return ""; | ||
| return value.split(",")[0].trim().replace(/^['"]|['"]$/g, ""); | ||
| } | ||
| /** | ||
| * Normalizes a font-weight value to a string CSS keyword/number that | ||
| * the editor accepts. Returns "" when the value is the default (normal/400). | ||
| */ | ||
| function parseFontWeight(value) { | ||
| if (!value) return ""; | ||
| const trimmed = value.trim().toLowerCase(); | ||
| if (trimmed === "normal" || trimmed === "400") return ""; | ||
| return trimmed; | ||
| if (!value) return ""; | ||
| const trimmed = value.trim().toLowerCase(); | ||
| if (trimmed === "normal" || trimmed === "400") return ""; | ||
| return trimmed; | ||
| } | ||
| /** | ||
| * Parses CSS text-align to one of the allowed editor alignments. | ||
| */ | ||
| function parseAlignment(value, fallback = "left") { | ||
| const v = (value ?? "").trim().toLowerCase(); | ||
| if (v === "left" || v === "center" || v === "right") return v; | ||
| return fallback; | ||
| const v = (value ?? "").trim().toLowerCase(); | ||
| if (v === "left" || v === "center" || v === "right") return v; | ||
| return fallback; | ||
| } | ||
| /** | ||
| * Parses a CSS `border` shorthand (`"1px solid #ccc"`) into width/style/color. | ||
| * Order-tolerant: each token is classified by content. | ||
| */ | ||
| function parseBorderShorthand(value) { | ||
| const fallback = { width: 0, style: "solid", color: "#000000" }; | ||
| if (!value) return fallback; | ||
| const styleKeywords = /* @__PURE__ */ new Set([ | ||
| "none", | ||
| "hidden", | ||
| "dotted", | ||
| "dashed", | ||
| "solid", | ||
| "double", | ||
| "groove", | ||
| "ridge", | ||
| "inset", | ||
| "outset" | ||
| ]); | ||
| let width = 0; | ||
| let style = "solid"; | ||
| let color = "#000000"; | ||
| for (const token of value.trim().split(/\s+/)) { | ||
| const lower = token.toLowerCase(); | ||
| if (styleKeywords.has(lower)) { | ||
| style = lower; | ||
| } else if (/^-?\d+(?:\.\d+)?(?:px)?$/i.test(lower)) { | ||
| width = parsePxValue(lower); | ||
| } else { | ||
| const c = parseColor(lower); | ||
| if (c) color = c; | ||
| } | ||
| } | ||
| return { width, style, color }; | ||
| const fallback = { | ||
| width: 0, | ||
| style: "solid", | ||
| color: "#000000" | ||
| }; | ||
| if (!value) return fallback; | ||
| const styleKeywords = new Set([ | ||
| "none", | ||
| "hidden", | ||
| "dotted", | ||
| "dashed", | ||
| "solid", | ||
| "double", | ||
| "groove", | ||
| "ridge", | ||
| "inset", | ||
| "outset" | ||
| ]); | ||
| let width = 0; | ||
| let style = "solid"; | ||
| let color = "#000000"; | ||
| for (const token of value.trim().split(/\s+/)) { | ||
| const lower = token.toLowerCase(); | ||
| if (styleKeywords.has(lower)) style = lower; | ||
| else if (/^-?\d+(?:\.\d+)?(?:px)?$/i.test(lower)) width = parsePxValue(lower); | ||
| else { | ||
| const c = parseColor(lower); | ||
| if (c) color = c; | ||
| } | ||
| } | ||
| return { | ||
| width, | ||
| style, | ||
| color | ||
| }; | ||
| } | ||
| // src/css-resolver.ts | ||
| //#endregion | ||
| //#region src/css-resolver.ts | ||
| /** | ||
| * Strips all CSS comments. Handles nested-looking content safely. | ||
| */ | ||
| function stripComments(css) { | ||
| return css.replace(/\/\*[\s\S]*?\*\//g, ""); | ||
| return css.replace(/\/\*[\s\S]*?\*\//g, ""); | ||
| } | ||
| /** | ||
| * Strips at-rule blocks (@media, @font-face, @keyframes, @supports, etc.) | ||
| * and their nested content. Leaves top-level rules in place. | ||
| * | ||
| * Email HTML rarely benefits from @media (we render at one viewport), | ||
| * and resolving it onto elements would not be visually faithful anyway. | ||
| */ | ||
| function stripAtRules(css) { | ||
| let result = ""; | ||
| let i = 0; | ||
| while (i < css.length) { | ||
| if (css[i] === "@") { | ||
| const semiIdx = css.indexOf(";", i); | ||
| const braceIdx = css.indexOf("{", i); | ||
| if (braceIdx === -1 || semiIdx !== -1 && semiIdx < braceIdx) { | ||
| i = semiIdx === -1 ? css.length : semiIdx + 1; | ||
| continue; | ||
| } | ||
| let depth = 0; | ||
| let j = braceIdx; | ||
| for (; j < css.length; j++) { | ||
| if (css[j] === "{") depth++; | ||
| else if (css[j] === "}") { | ||
| depth--; | ||
| if (depth === 0) { | ||
| j++; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| i = j; | ||
| } else { | ||
| result += css[i]; | ||
| i++; | ||
| } | ||
| } | ||
| return result; | ||
| let result = ""; | ||
| let i = 0; | ||
| while (i < css.length) if (css[i] === "@") { | ||
| const semiIdx = css.indexOf(";", i); | ||
| const braceIdx = css.indexOf("{", i); | ||
| if (braceIdx === -1 || semiIdx !== -1 && semiIdx < braceIdx) { | ||
| i = semiIdx === -1 ? css.length : semiIdx + 1; | ||
| continue; | ||
| } | ||
| let depth = 0; | ||
| let j = braceIdx; | ||
| for (; j < css.length; j++) if (css[j] === "{") depth++; | ||
| else if (css[j] === "}") { | ||
| depth--; | ||
| if (depth === 0) { | ||
| j++; | ||
| break; | ||
| } | ||
| } | ||
| i = j; | ||
| } else { | ||
| result += css[i]; | ||
| i++; | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Parses a CSS declarations block (`color: red; font-size: 14px`) into | ||
| * a flat record. `!important` markers are dropped. | ||
| */ | ||
| function parseDeclarations(text) { | ||
| const result = {}; | ||
| for (const decl of text.split(";")) { | ||
| const idx = decl.indexOf(":"); | ||
| if (idx === -1) continue; | ||
| const key = decl.slice(0, idx).trim().toLowerCase(); | ||
| let value = decl.slice(idx + 1).trim(); | ||
| value = value.replace(/!important\s*$/i, "").trim(); | ||
| if (key && value) result[key] = value; | ||
| } | ||
| return result; | ||
| const result = {}; | ||
| for (const decl of text.split(";")) { | ||
| const idx = decl.indexOf(":"); | ||
| if (idx === -1) continue; | ||
| const key = decl.slice(0, idx).trim().toLowerCase(); | ||
| let value = decl.slice(idx + 1).trim(); | ||
| value = value.replace(/!important\s*$/i, "").trim(); | ||
| if (key && value) result[key] = value; | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * A selector is "supported" by cheerio's matcher if it has no pseudo-classes | ||
| * or pseudo-elements. Resolving e.g. `a:hover` onto an inline style would be | ||
| * wrong (it would always apply), so we skip such rules entirely. | ||
| */ | ||
| function isSupportedSelector(selector) { | ||
| if (!selector) return false; | ||
| if (selector.includes(":")) return false; | ||
| if (selector.includes("@")) return false; | ||
| return true; | ||
| if (!selector) return false; | ||
| if (selector.includes(":")) return false; | ||
| if (selector.includes("@")) return false; | ||
| return true; | ||
| } | ||
| /** | ||
| * Parses the full content of one or more `<style>` tags into a list of rules. | ||
| * Skips at-rules and selectors with pseudo-classes. | ||
| */ | ||
| function parseStyleSheet(css) { | ||
| const rules = []; | ||
| const cleaned = stripAtRules(stripComments(css)); | ||
| const blockRe = /([^{}]+)\{([^{}]*)\}/g; | ||
| let match; | ||
| while ((match = blockRe.exec(cleaned)) !== null) { | ||
| const selectorPart = match[1].trim(); | ||
| const declarationPart = match[2]; | ||
| if (!selectorPart) continue; | ||
| const selectors = selectorPart.split(",").map((s) => s.trim()).filter(isSupportedSelector); | ||
| if (selectors.length === 0) continue; | ||
| const declarations = parseDeclarations(declarationPart); | ||
| if (Object.keys(declarations).length === 0) continue; | ||
| rules.push({ selectors, declarations }); | ||
| } | ||
| return rules; | ||
| const rules = []; | ||
| const cleaned = stripAtRules(stripComments(css)); | ||
| const blockRe = /([^{}]+)\{([^{}]*)\}/g; | ||
| let match; | ||
| while ((match = blockRe.exec(cleaned)) !== null) { | ||
| const selectorPart = match[1].trim(); | ||
| const declarationPart = match[2]; | ||
| if (!selectorPart) continue; | ||
| const selectors = selectorPart.split(",").map((s) => s.trim()).filter(isSupportedSelector); | ||
| if (selectors.length === 0) continue; | ||
| const declarations = parseDeclarations(declarationPart); | ||
| if (Object.keys(declarations).length === 0) continue; | ||
| rules.push({ | ||
| selectors, | ||
| declarations | ||
| }); | ||
| } | ||
| return rules; | ||
| } | ||
| /** | ||
| * Reads all `<style>` tags from the document, parses them into rules, | ||
| * applies each rule's declarations to matching elements (merging with | ||
| * existing inline `style=""` attributes — inline always wins), and removes | ||
| * the `<style>` tags from the document. | ||
| * | ||
| * No specificity is computed; rules are applied in source order, with later | ||
| * rules overriding earlier ones. Inline styles always override resolved | ||
| * rules. This is sufficient for typical email HTML, where authors already | ||
| * inline most styles. | ||
| */ | ||
| function resolveCssStyles($) { | ||
| const styleTags = $("style"); | ||
| if (styleTags.length === 0) return; | ||
| const allRules = []; | ||
| styleTags.each((_, el) => { | ||
| const css = $(el).text(); | ||
| if (css) allRules.push(...parseStyleSheet(css)); | ||
| }); | ||
| const inlineByEl = /* @__PURE__ */ new WeakMap(); | ||
| $("[style]").each((_, el) => { | ||
| inlineByEl.set(el, parseStyleAttribute($(el).attr("style"))); | ||
| }); | ||
| const resolvedByEl = /* @__PURE__ */ new WeakMap(); | ||
| for (const rule of allRules) { | ||
| for (const selector of rule.selectors) { | ||
| let matched; | ||
| try { | ||
| matched = $(selector); | ||
| } catch { | ||
| continue; | ||
| } | ||
| matched.each((_, el) => { | ||
| const key = el; | ||
| const current = resolvedByEl.get(key) ?? {}; | ||
| for (const [k, v] of Object.entries(rule.declarations)) { | ||
| current[k] = v; | ||
| } | ||
| resolvedByEl.set(key, current); | ||
| }); | ||
| } | ||
| } | ||
| $("*").each((_, el) => { | ||
| const key = el; | ||
| const resolved = resolvedByEl.get(key); | ||
| if (!resolved) return; | ||
| const inline = inlineByEl.get(key) ?? {}; | ||
| const merged = { ...resolved }; | ||
| for (const [k, v] of Object.entries(inline)) merged[k] = v; | ||
| $(el).attr("style", serializeStyleAttribute(merged)); | ||
| }); | ||
| styleTags.remove(); | ||
| const styleTags = $("style"); | ||
| if (styleTags.length === 0) return; | ||
| const allRules = []; | ||
| styleTags.each((_, el) => { | ||
| const css = $(el).text(); | ||
| if (css) allRules.push(...parseStyleSheet(css)); | ||
| }); | ||
| const inlineByEl = /* @__PURE__ */ new WeakMap(); | ||
| $("[style]").each((_, el) => { | ||
| inlineByEl.set(el, parseStyleAttribute($(el).attr("style"))); | ||
| }); | ||
| const resolvedByEl = /* @__PURE__ */ new WeakMap(); | ||
| for (const rule of allRules) for (const selector of rule.selectors) { | ||
| let matched; | ||
| try { | ||
| matched = $(selector); | ||
| } catch { | ||
| continue; | ||
| } | ||
| matched.each((_, el) => { | ||
| const key = el; | ||
| const current = resolvedByEl.get(key) ?? {}; | ||
| for (const [k, v] of Object.entries(rule.declarations)) current[k] = v; | ||
| resolvedByEl.set(key, current); | ||
| }); | ||
| } | ||
| $("*").each((_, el) => { | ||
| const key = el; | ||
| const resolved = resolvedByEl.get(key); | ||
| if (!resolved) return; | ||
| const inline = inlineByEl.get(key) ?? {}; | ||
| const merged = { ...resolved }; | ||
| for (const [k, v] of Object.entries(inline)) merged[k] = v; | ||
| $(el).attr("style", serializeStyleAttribute(merged)); | ||
| }); | ||
| styleTags.remove(); | ||
| } | ||
| // src/block-mapper.ts | ||
| import { | ||
| createTitleBlock, | ||
| createParagraphBlock, | ||
| createImageBlock, | ||
| createButtonBlock, | ||
| createDividerBlock, | ||
| createSpacerBlock, | ||
| createHtmlBlock | ||
| } from "@templatical/types"; | ||
| var HEADING_TAGS = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]); | ||
| var TEXT_TAGS = /* @__PURE__ */ new Set(["p", "span", "div"]); | ||
| function emptyPadding() { | ||
| return { top: 0, right: 0, bottom: 0, left: 0 }; | ||
| //#endregion | ||
| //#region src/block-mapper.ts | ||
| const HEADING_TAGS = new Set([ | ||
| "h1", | ||
| "h2", | ||
| "h3", | ||
| "h4", | ||
| "h5", | ||
| "h6" | ||
| ]); | ||
| const TEXT_TAGS = new Set([ | ||
| "p", | ||
| "span", | ||
| "div" | ||
| ]); | ||
| function emptyPadding$2() { | ||
| return { | ||
| top: 0, | ||
| right: 0, | ||
| bottom: 0, | ||
| left: 0 | ||
| }; | ||
| } | ||
| function tagOf(el) { | ||
| if ("tagName" in el && typeof el.tagName === "string") | ||
| return el.tagName.toLowerCase(); | ||
| return ""; | ||
| if ("tagName" in el && typeof el.tagName === "string") return el.tagName.toLowerCase(); | ||
| return ""; | ||
| } | ||
| function getStyles($el) { | ||
| return parseStyleAttribute($el.attr("style")); | ||
| function getStyles$1($el) { | ||
| return parseStyleAttribute($el.attr("style")); | ||
| } | ||
| /** | ||
| * Returns the inner HTML of `$el`. | ||
| */ | ||
| function getInnerHtml($el) { | ||
| return $el.html() ?? ""; | ||
| return $el.html() ?? ""; | ||
| } | ||
| function ensureParagraphWrapped(html) { | ||
| if (!html.trim()) return "<p></p>"; | ||
| if (/<(p|h[1-6]|ul|ol|blockquote)[\s>]/i.test(html)) return html; | ||
| return `<p>${html}</p>`; | ||
| if (!html.trim()) return "<p></p>"; | ||
| if (/<(p|h[1-6]|ul|ol|blockquote)[\s>]/i.test(html)) return html; | ||
| return `<p>${html}</p>`; | ||
| } | ||
| function safeHtmlComment(message, raw) { | ||
| const escapedMessage = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); | ||
| return `<!-- ${escapedMessage} --> | ||
| ${raw}`; | ||
| return `<!-- ${message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")} -->\n${raw}`; | ||
| } | ||
| /** | ||
| * Heading element (h1-h6) → Title block. | ||
| */ | ||
| function convertHeading($el) { | ||
| const tag = tagOf($el[0]); | ||
| const styles = getStyles($el); | ||
| const levelMatch = tag.match(/^h(\d)$/); | ||
| const rawLevel = levelMatch ? Number(levelMatch[1]) : 2; | ||
| const level = rawLevel >= 1 && rawLevel <= 4 ? rawLevel : Math.min(rawLevel, 4); | ||
| const innerHtml = getInnerHtml($el); | ||
| const content = innerHtml.trim() ? `<p>${innerHtml}</p>` : "<p></p>"; | ||
| return createTitleBlock({ | ||
| content, | ||
| level, | ||
| color: parseColor(styles.color) || "#1a1a1a", | ||
| textAlign: parseAlignment(styles["text-align"]), | ||
| fontFamily: parseFontFamily(styles["font-family"]) || void 0, | ||
| styles: { | ||
| padding: readPaddingFromStyles(styles) | ||
| } | ||
| }); | ||
| const tag = tagOf($el[0]); | ||
| const styles = getStyles$1($el); | ||
| const levelMatch = tag.match(/^h(\d)$/); | ||
| const rawLevel = levelMatch ? Number(levelMatch[1]) : 2; | ||
| const level = rawLevel >= 1 && rawLevel <= 4 ? rawLevel : Math.min(rawLevel, 4); | ||
| const innerHtml = getInnerHtml($el); | ||
| return createTitleBlock({ | ||
| content: innerHtml.trim() ? `<p>${innerHtml}</p>` : "<p></p>", | ||
| level, | ||
| color: parseColor(styles.color) || "#1a1a1a", | ||
| textAlign: parseAlignment(styles["text-align"]), | ||
| fontFamily: parseFontFamily(styles["font-family"]) || void 0, | ||
| styles: { padding: readPaddingFromStyles(styles) } | ||
| }); | ||
| } | ||
| /** | ||
| * Apply a container-level `text-align` to every `<p>` opening tag in `html`, | ||
| * merging into an existing `style="…"` attribute when present. Tolerant of | ||
| * any other attributes on the `<p>` (class/id/dir/…) — the previous narrow | ||
| * `<p style="…">` + bare-`<p>` matchers silently dropped the alignment when | ||
| * the inner `<p>` carried a non-style attribute. | ||
| */ | ||
| function applyTextAlignToParagraphs(html, textAlign) { | ||
| return html.replace(/<p\b([^>]*)>/gi, (_match, attrs) => { | ||
| const styleMatch = /\sstyle\s*=\s*"([^"]*)"/i.exec(attrs); | ||
| if (styleMatch) { | ||
| const existing = styleMatch[1].trim().replace(/;\s*$/, ""); | ||
| const merged = existing ? `${existing}; text-align: ${textAlign}` : `text-align: ${textAlign}`; | ||
| return `<p${attrs.slice(0, styleMatch.index) + ` style="${merged}"` + attrs.slice(styleMatch.index + styleMatch[0].length)}>`; | ||
| } | ||
| return `<p${attrs} style="text-align: ${textAlign}">`; | ||
| }); | ||
| } | ||
| /** | ||
| * Paragraph or block-level text container → Paragraph block. | ||
| */ | ||
| function convertParagraph($el) { | ||
| const styles = getStyles($el); | ||
| const innerHtml = getInnerHtml($el); | ||
| const wrapped = ensureParagraphWrapped(innerHtml); | ||
| const fontParts = []; | ||
| const fontSize = parsePxValue(styles["font-size"]); | ||
| if (fontSize && fontSize !== 16) fontParts.push(`font-size: ${fontSize}px`); | ||
| const color = parseColor(styles.color); | ||
| if (color && color !== "#1a1a1a") fontParts.push(`color: ${color}`); | ||
| const fontWeight = parseFontWeight(styles["font-weight"]); | ||
| if (fontWeight) fontParts.push(`font-weight: ${fontWeight}`); | ||
| const fontFamily = parseFontFamily(styles["font-family"]); | ||
| if (fontFamily) fontParts.push(`font-family: ${fontFamily}`); | ||
| const textAlign = styles["text-align"]; | ||
| let result = wrapped; | ||
| if (textAlign && textAlign !== "left") { | ||
| result = result.replace( | ||
| /<p style="([^"]*)">/g, | ||
| `<p style="$1; text-align: ${textAlign}">` | ||
| ).replaceAll("<p>", `<p style="text-align: ${textAlign}">`); | ||
| } | ||
| if (fontParts.length > 0) { | ||
| const span = fontParts.join("; "); | ||
| result = result.replace( | ||
| /<p([^>]*)>([\s\S]*?)<\/p>/g, | ||
| `<p$1><span style="${span}">$2</span></p>` | ||
| ); | ||
| } | ||
| return createParagraphBlock({ | ||
| content: result, | ||
| styles: { | ||
| padding: readPaddingFromStyles(styles) | ||
| } | ||
| }); | ||
| const styles = getStyles$1($el); | ||
| const wrapped = ensureParagraphWrapped(getInnerHtml($el)); | ||
| const fontParts = []; | ||
| const fontSize = parsePxValue(styles["font-size"]); | ||
| if (fontSize && fontSize !== 16) fontParts.push(`font-size: ${fontSize}px`); | ||
| const color = parseColor(styles.color); | ||
| if (color && color !== "#1a1a1a") fontParts.push(`color: ${color}`); | ||
| const fontWeight = parseFontWeight(styles["font-weight"]); | ||
| if (fontWeight) fontParts.push(`font-weight: ${fontWeight}`); | ||
| const fontFamily = parseFontFamily(styles["font-family"]); | ||
| if (fontFamily) fontParts.push(`font-family: ${fontFamily}`); | ||
| const textAlign = styles["text-align"]; | ||
| let result = wrapped; | ||
| if (textAlign && textAlign !== "left") result = applyTextAlignToParagraphs(result, textAlign); | ||
| if (fontParts.length > 0) { | ||
| const span = fontParts.join("; "); | ||
| result = result.replace(/<p([^>]*)>([\s\S]*?)<\/p>/g, `<p$1><span style="${span}">$2</span></p>`); | ||
| } | ||
| return createParagraphBlock({ | ||
| content: result, | ||
| styles: { padding: readPaddingFromStyles(styles) } | ||
| }); | ||
| } | ||
| /** | ||
| * <img> → Image block. | ||
| */ | ||
| function convertImage($el) { | ||
| const styles = getStyles($el); | ||
| const src = $el.attr("src") ?? ""; | ||
| const alt = $el.attr("alt") ?? ""; | ||
| const widthAttr = $el.attr("width"); | ||
| const widthStyle = styles.width; | ||
| const width = parsePxValue(widthAttr) || parsePxValue(widthStyle) || 600; | ||
| return createImageBlock({ | ||
| src, | ||
| alt, | ||
| width, | ||
| align: parseAlignment(styles["text-align"], "center"), | ||
| styles: { | ||
| padding: readPaddingFromStyles(styles) | ||
| } | ||
| }); | ||
| const styles = getStyles$1($el); | ||
| const src = $el.attr("src") ?? ""; | ||
| const alt = $el.attr("alt") ?? ""; | ||
| const widthAttr = $el.attr("width"); | ||
| const widthStyle = styles.width; | ||
| return createImageBlock({ | ||
| src, | ||
| alt, | ||
| width: parsePxValue(widthAttr) || parsePxValue(widthStyle) || 600, | ||
| align: parseAlignment(styles["text-align"], "center"), | ||
| styles: { padding: readPaddingFromStyles(styles) } | ||
| }); | ||
| } | ||
| /** | ||
| * <a> styled as a button → Button block. | ||
| * | ||
| * Heuristic: a single `<a>` with a non-transparent background-color OR padding | ||
| * OR border-radius OR display: inline-block / block is treated as a button. | ||
| */ | ||
| function looksLikeButton(styles) { | ||
| if (parseColor(styles["background-color"]) || parseColor(styles.background)) | ||
| return true; | ||
| if (styles.padding || styles["padding-top"] || styles["padding-bottom"] || styles["padding-left"] || styles["padding-right"]) | ||
| return true; | ||
| if (parsePxValue(styles["border-radius"])) return true; | ||
| const display = (styles.display ?? "").toLowerCase(); | ||
| if (display === "inline-block" || display === "block") return true; | ||
| return false; | ||
| if (parseColor(styles["background-color"]) || parseColor(styles.background)) return true; | ||
| if (styles.padding || styles["padding-top"] || styles["padding-bottom"] || styles["padding-left"] || styles["padding-right"]) return true; | ||
| if (parsePxValue(styles["border-radius"])) return true; | ||
| const display = (styles.display ?? "").toLowerCase(); | ||
| if (display === "inline-block" || display === "block") return true; | ||
| return false; | ||
| } | ||
| function convertButton($el) { | ||
| const styles = getStyles($el); | ||
| const text = ($el.text() ?? "Button").trim() || "Button"; | ||
| const url = $el.attr("href") ?? "#"; | ||
| const target = $el.attr("target"); | ||
| return createButtonBlock({ | ||
| text, | ||
| url, | ||
| openInNewTab: target === "_blank" || void 0, | ||
| backgroundColor: parseColor(styles["background-color"]) || parseColor(styles.background) || "#4f46e5", | ||
| textColor: parseColor(styles.color) || "#ffffff", | ||
| borderRadius: parsePxValue(styles["border-radius"]), | ||
| fontSize: parsePxValue(styles["font-size"]) || 16, | ||
| fontFamily: parseFontFamily(styles["font-family"]) || void 0, | ||
| buttonPadding: readPaddingFromStyles(styles), | ||
| styles: { | ||
| padding: emptyPadding() | ||
| } | ||
| }); | ||
| const styles = getStyles$1($el); | ||
| return createButtonBlock({ | ||
| text: ($el.text() ?? "Button").trim() || "Button", | ||
| url: $el.attr("href") ?? "#", | ||
| openInNewTab: $el.attr("target") === "_blank" || void 0, | ||
| backgroundColor: parseColor(styles["background-color"]) || parseColor(styles.background) || "#4f46e5", | ||
| textColor: parseColor(styles.color) || "#ffffff", | ||
| borderRadius: parsePxValue(styles["border-radius"]), | ||
| fontSize: parsePxValue(styles["font-size"]) || 16, | ||
| fontFamily: parseFontFamily(styles["font-family"]) || void 0, | ||
| buttonPadding: readPaddingFromStyles(styles), | ||
| styles: { padding: emptyPadding$2() } | ||
| }); | ||
| } | ||
| /** | ||
| * <hr> → Divider block. | ||
| */ | ||
| function convertDivider($el) { | ||
| const styles = getStyles($el); | ||
| const border = parseBorderShorthand(styles["border-top"] ?? styles.border); | ||
| const lineStyle = border.style === "dashed" || border.style === "dotted" ? border.style : "solid"; | ||
| return createDividerBlock({ | ||
| lineStyle, | ||
| color: border.color || "#e5e7eb", | ||
| thickness: border.width || 1, | ||
| width: 100, | ||
| styles: { | ||
| padding: readPaddingFromStyles(styles) | ||
| } | ||
| }); | ||
| const styles = getStyles$1($el); | ||
| const border = parseBorderShorthand(styles["border-top"] ?? styles.border); | ||
| return createDividerBlock({ | ||
| lineStyle: border.style === "dashed" || border.style === "dotted" ? border.style : "solid", | ||
| color: border.color || "#e5e7eb", | ||
| thickness: border.width || 1, | ||
| width: 100, | ||
| styles: { padding: readPaddingFromStyles(styles) } | ||
| }); | ||
| } | ||
| /** | ||
| * Wraps the element's outerHTML in an HTML block (the lossless fallback). | ||
| */ | ||
| function convertHtmlFallback($el, $, note) { | ||
| const outer = $.html($el) ?? ""; | ||
| const content = note ? safeHtmlComment(note, outer) : outer; | ||
| const styles = getStyles($el); | ||
| return createHtmlBlock({ | ||
| content, | ||
| styles: { | ||
| padding: readPaddingFromStyles(styles) | ||
| } | ||
| }); | ||
| const outer = $.html($el) ?? ""; | ||
| return createHtmlBlock({ | ||
| content: note ? safeHtmlComment(note, outer) : outer, | ||
| styles: { padding: readPaddingFromStyles(getStyles$1($el)) } | ||
| }); | ||
| } | ||
| /** | ||
| * Decides whether a `<td>` looks like a vertical spacer: | ||
| * empty (or only ` `) AND has an explicit height. | ||
| */ | ||
| function isSpacerCell($el) { | ||
| const text = ($el.text() ?? "").replace(/\s| /g, ""); | ||
| if (text !== "") return false; | ||
| if ($el.find("img, a, hr").length > 0) return false; | ||
| const styles = getStyles($el); | ||
| const hasHeight = parsePxValue($el.attr("height")) > 0 || parsePxValue(styles.height) > 0 || parsePxValue(styles["line-height"]) > 0; | ||
| return hasHeight; | ||
| if (($el.text() ?? "").replace(/\s| /g, "") !== "") return false; | ||
| if ($el.find("img, a, hr").length > 0) return false; | ||
| const styles = getStyles$1($el); | ||
| return parsePxValue($el.attr("height")) > 0 || parsePxValue(styles.height) > 0 || parsePxValue(styles["line-height"]) > 0; | ||
| } | ||
| /** | ||
| * Decides whether a `<td>` is a button container — i.e. has exactly one | ||
| * `<a>` inside that itself looks like a button. | ||
| */ | ||
| function isButtonCell($el, $) { | ||
| const anchors = $el.find("a"); | ||
| if (anchors.length !== 1) return { match: false }; | ||
| const anchor = $(anchors[0]); | ||
| if (looksLikeButton(getStyles(anchor))) return { match: true, anchor }; | ||
| if (looksLikeButton(getStyles($el))) { | ||
| const href = (anchor.attr("href") ?? "").trim(); | ||
| if (href !== "") { | ||
| return { match: true, anchor }; | ||
| } | ||
| } | ||
| return { match: false }; | ||
| const anchors = $el.find("a"); | ||
| if (anchors.length !== 1) return { match: false }; | ||
| const anchor = $(anchors[0]); | ||
| if (looksLikeButton(getStyles$1(anchor))) return { | ||
| match: true, | ||
| anchor | ||
| }; | ||
| if (looksLikeButton(getStyles$1($el))) { | ||
| if ((anchor.attr("href") ?? "").trim() !== "") return { | ||
| match: true, | ||
| anchor | ||
| }; | ||
| } | ||
| return { match: false }; | ||
| } | ||
| /** | ||
| * Converts a single content-bearing element (heading / paragraph / image / | ||
| * anchor-as-button / divider) to a Templatical block. | ||
| * | ||
| * Returns `null` for elements that do not contain any meaningful content | ||
| * (the caller should skip them). | ||
| */ | ||
| function convertElement($el, $) { | ||
| const tag = tagOf($el[0]); | ||
| if (!tag) return null; | ||
| if (HEADING_TAGS.has(tag)) { | ||
| return { | ||
| block: convertHeading($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "title", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| } | ||
| if (tag === "img") { | ||
| return { | ||
| block: convertImage($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "image", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| } | ||
| if (tag === "a") { | ||
| if (looksLikeButton(getStyles($el))) { | ||
| return { | ||
| block: convertButton($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "button", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| } | ||
| return { | ||
| block: convertParagraph($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "paragraph", | ||
| status: "approximated", | ||
| note: "Inline anchor wrapped in a paragraph block." | ||
| } | ||
| }; | ||
| } | ||
| if (tag === "hr") { | ||
| return { | ||
| block: convertDivider($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "divider", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| } | ||
| if (TEXT_TAGS.has(tag)) { | ||
| const text = ($el.text() ?? "").trim(); | ||
| if (!text && $el.find("img, a").length === 0) return null; | ||
| return { | ||
| block: convertParagraph($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "paragraph", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| } | ||
| return { | ||
| block: convertHtmlFallback( | ||
| $el, | ||
| $, | ||
| `Unsupported element <${tag}>: preserved as raw HTML` | ||
| ), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "html", | ||
| status: "html-fallback", | ||
| note: `Unknown element "${tag}" preserved as HTML block.` | ||
| } | ||
| }; | ||
| const tag = tagOf($el[0]); | ||
| if (!tag) return null; | ||
| if (HEADING_TAGS.has(tag)) return { | ||
| block: convertHeading($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "title", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| if (tag === "img") return { | ||
| block: convertImage($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "image", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| if (tag === "a") { | ||
| if (looksLikeButton(getStyles$1($el))) return { | ||
| block: convertButton($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "button", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| return { | ||
| block: convertParagraph($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "paragraph", | ||
| status: "approximated", | ||
| note: "Inline anchor wrapped in a paragraph block." | ||
| } | ||
| }; | ||
| } | ||
| if (tag === "hr") return { | ||
| block: convertDivider($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "divider", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| if (TEXT_TAGS.has(tag)) { | ||
| if (!($el.text() ?? "").trim() && $el.find("img, a").length === 0) return null; | ||
| return { | ||
| block: convertParagraph($el), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "paragraph", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| } | ||
| return { | ||
| block: convertHtmlFallback($el, $, `Unsupported element <${tag}>: preserved as raw HTML`), | ||
| entry: { | ||
| sourceTag: tag, | ||
| templaticalBlockType: "html", | ||
| status: "html-fallback", | ||
| note: `Unknown element "${tag}" preserved as HTML block.` | ||
| } | ||
| }; | ||
| } | ||
| // src/section-builder.ts | ||
| import { | ||
| createSectionBlock, | ||
| createButtonBlock as createButtonBlock2, | ||
| createSpacerBlock as createSpacerBlock2 | ||
| } from "@templatical/types"; | ||
| function emptyPadding2() { | ||
| return { top: 0, right: 0, bottom: 0, left: 0 }; | ||
| //#endregion | ||
| //#region src/section-builder.ts | ||
| function emptyPadding$1() { | ||
| return { | ||
| top: 0, | ||
| right: 0, | ||
| bottom: 0, | ||
| left: 0 | ||
| }; | ||
| } | ||
| function getStyles2($el) { | ||
| return parseStyleAttribute($el.attr("style")); | ||
| function getStyles($el) { | ||
| return parseStyleAttribute($el.attr("style")); | ||
| } | ||
| function buildCellButton($cell, $anchor) { | ||
| const cellStyles = getStyles2($cell); | ||
| const aStyles = getStyles2($anchor); | ||
| const merged = { ...cellStyles, ...aStyles }; | ||
| const text = ($anchor.text() ?? "Button").trim() || "Button"; | ||
| const url = $anchor.attr("href") ?? "#"; | ||
| const target = $anchor.attr("target"); | ||
| return createButtonBlock2({ | ||
| text, | ||
| url, | ||
| openInNewTab: target === "_blank" || void 0, | ||
| backgroundColor: parseColor(merged["background-color"]) || parseColor(merged.background) || "#4f46e5", | ||
| textColor: parseColor(merged.color) || "#ffffff", | ||
| borderRadius: parsePxValue(merged["border-radius"]), | ||
| fontSize: parsePxValue(merged["font-size"]) || 16, | ||
| buttonPadding: readPaddingFromStyles(merged), | ||
| styles: { | ||
| padding: emptyPadding2() | ||
| } | ||
| }); | ||
| const cellStyles = getStyles($cell); | ||
| const aStyles = getStyles($anchor); | ||
| const merged = { | ||
| ...cellStyles, | ||
| ...aStyles | ||
| }; | ||
| return createButtonBlock({ | ||
| text: ($anchor.text() ?? "Button").trim() || "Button", | ||
| url: $anchor.attr("href") ?? "#", | ||
| openInNewTab: $anchor.attr("target") === "_blank" || void 0, | ||
| backgroundColor: parseColor(merged["background-color"]) || parseColor(merged.background) || "#4f46e5", | ||
| textColor: parseColor(merged.color) || "#ffffff", | ||
| borderRadius: parsePxValue(merged["border-radius"]), | ||
| fontSize: parsePxValue(merged["font-size"]) || 16, | ||
| buttonPadding: readPaddingFromStyles(merged), | ||
| styles: { padding: emptyPadding$1() } | ||
| }); | ||
| } | ||
| function buildSpacerFromCell($cell) { | ||
| const cellStyles = getStyles2($cell); | ||
| const height = parsePxValue($cell.attr("height")) || parsePxValue(cellStyles.height) || parsePxValue(cellStyles["line-height"]) || 24; | ||
| return createSpacerBlock2({ | ||
| height, | ||
| styles: { | ||
| padding: emptyPadding2() | ||
| } | ||
| }); | ||
| const cellStyles = getStyles($cell); | ||
| return createSpacerBlock({ | ||
| height: parsePxValue($cell.attr("height")) || parsePxValue(cellStyles.height) || parsePxValue(cellStyles["line-height"]) || 24, | ||
| styles: { padding: emptyPadding$1() } | ||
| }); | ||
| } | ||
| /** | ||
| * Returns the direct child `<tr>` rows of a table, including those one level | ||
| * inside `<thead>`, `<tbody>`, or `<tfoot>` (which the HTML parser inserts | ||
| * automatically). | ||
| */ | ||
| function getDirectRows($table, $) { | ||
| const rows = []; | ||
| $table.children("tr").each((_, el) => { | ||
| rows.push($(el)); | ||
| }); | ||
| $table.children("thead, tbody, tfoot").each((_, group) => { | ||
| $(group).children("tr").each((_i, el) => { | ||
| rows.push($(el)); | ||
| }); | ||
| }); | ||
| return rows; | ||
| const rows = []; | ||
| $table.children("tr").each((_, el) => { | ||
| rows.push($(el)); | ||
| }); | ||
| $table.children("thead, tbody, tfoot").each((_, group) => { | ||
| $(group).children("tr").each((_i, el) => { | ||
| rows.push($(el)); | ||
| }); | ||
| }); | ||
| return rows; | ||
| } | ||
| function getDirectCells($row, $) { | ||
| const cells = []; | ||
| $row.children("td, th").each((_, el) => { | ||
| cells.push($(el)); | ||
| }); | ||
| return cells; | ||
| const cells = []; | ||
| $row.children("td, th").each((_, el) => { | ||
| cells.push($(el)); | ||
| }); | ||
| return cells; | ||
| } | ||
| function isLayoutTable($table, $) { | ||
| if ($table.find( | ||
| "img, a, h1, h2, h3, h4, h5, h6, table, hr, p, div, span, ul, ol, li, blockquote, video, iframe" | ||
| ).length > 0) | ||
| return true; | ||
| let hasNonStandardChild = false; | ||
| $table.find("td, th").each((_, td) => { | ||
| if (hasNonStandardChild) return; | ||
| if ($(td).children().length > 0) hasNonStandardChild = true; | ||
| }); | ||
| return hasNonStandardChild; | ||
| if ($table.find("img, a, h1, h2, h3, h4, h5, h6, table, hr, p, div, span, ul, ol, li, blockquote, video, iframe").length > 0) return true; | ||
| let hasNonStandardChild = false; | ||
| $table.find("td, th").each((_, td) => { | ||
| if (hasNonStandardChild) return; | ||
| if ($(td).children().length > 0) hasNonStandardChild = true; | ||
| }); | ||
| return hasNonStandardChild; | ||
| } | ||
| function resolveColumnLayout(cellCount, warnings) { | ||
| if (cellCount <= 1) return "1"; | ||
| if (cellCount === 2) return "2"; | ||
| if (cellCount === 3) return "3"; | ||
| warnings.push( | ||
| `Row with ${cellCount} columns was flattened to a single column. Templatical supports up to 3 columns per section.` | ||
| ); | ||
| return "1"; | ||
| if (cellCount <= 1) return "1"; | ||
| if (cellCount === 2) return "2"; | ||
| if (cellCount === 3) return "3"; | ||
| warnings.push(`Row with ${cellCount} columns was flattened to a single column. Templatical supports up to 3 columns per section.`); | ||
| return "1"; | ||
| } | ||
| function extractCellBlocks($cell, $, entries, warnings) { | ||
| if (isSpacerCell($cell)) { | ||
| entries.push({ | ||
| sourceTag: "td", | ||
| templaticalBlockType: "spacer", | ||
| status: "converted" | ||
| }); | ||
| return [buildSpacerFromCell($cell)]; | ||
| } | ||
| const btn = isButtonCell($cell, $); | ||
| if (btn.match && btn.anchor) { | ||
| entries.push({ | ||
| sourceTag: "td", | ||
| templaticalBlockType: "button", | ||
| status: "converted" | ||
| }); | ||
| return [buildCellButton($cell, btn.anchor)]; | ||
| } | ||
| const blocks = []; | ||
| const childEls = $cell.children().toArray(); | ||
| if (childEls.length === 0) { | ||
| const text = ($cell.text() ?? "").trim(); | ||
| if (!text) return []; | ||
| const r = convertElement($cell, $); | ||
| if (r) { | ||
| entries.push(r.entry); | ||
| blocks.push(r.block); | ||
| } | ||
| return blocks; | ||
| } | ||
| for (const childEl of childEls) { | ||
| const $child = $(childEl); | ||
| const tag = childEl.tagName?.toLowerCase() ?? ""; | ||
| if (tag === "table") { | ||
| const inner = processTable($child, $, entries, warnings, true); | ||
| blocks.push(...inner); | ||
| continue; | ||
| } | ||
| if (tag === "a" && looksLikeButton(getStyles2($child))) { | ||
| const r2 = convertElement($child, $); | ||
| if (r2) { | ||
| entries.push(r2.entry); | ||
| blocks.push(r2.block); | ||
| } | ||
| continue; | ||
| } | ||
| const r = convertElement($child, $); | ||
| if (r) { | ||
| entries.push(r.entry); | ||
| blocks.push(r.block); | ||
| } | ||
| } | ||
| return blocks; | ||
| if (isSpacerCell($cell)) { | ||
| entries.push({ | ||
| sourceTag: "td", | ||
| templaticalBlockType: "spacer", | ||
| status: "converted" | ||
| }); | ||
| return [buildSpacerFromCell($cell)]; | ||
| } | ||
| const btn = isButtonCell($cell, $); | ||
| if (btn.match && btn.anchor) { | ||
| entries.push({ | ||
| sourceTag: "td", | ||
| templaticalBlockType: "button", | ||
| status: "converted" | ||
| }); | ||
| return [buildCellButton($cell, btn.anchor)]; | ||
| } | ||
| const blocks = []; | ||
| const childEls = $cell.children().toArray(); | ||
| if (childEls.length === 0) { | ||
| if (!($cell.text() ?? "").trim()) return []; | ||
| const r = convertElement($cell, $); | ||
| if (r) { | ||
| entries.push(r.entry); | ||
| blocks.push(r.block); | ||
| } | ||
| return blocks; | ||
| } | ||
| for (const childEl of childEls) { | ||
| const $child = $(childEl); | ||
| const tag = childEl.tagName?.toLowerCase() ?? ""; | ||
| if (tag === "table") { | ||
| const inner = processTable($child, $, entries, warnings, true); | ||
| blocks.push(...inner); | ||
| continue; | ||
| } | ||
| if (tag === "a" && looksLikeButton(getStyles($child))) { | ||
| const r = convertElement($child, $); | ||
| if (r) { | ||
| entries.push(r.entry); | ||
| blocks.push(r.block); | ||
| } | ||
| continue; | ||
| } | ||
| const r = convertElement($child, $); | ||
| if (r) { | ||
| entries.push(r.entry); | ||
| blocks.push(r.block); | ||
| } | ||
| } | ||
| return blocks; | ||
| } | ||
| /** | ||
| * Walk a `<table>` and produce Section blocks (one per row). | ||
| * | ||
| * @param flattenInline - When true (used for nested tables), drop the section | ||
| * wrapper and return the flat block list. Templatical sections cannot nest, | ||
| * so nested layout-tables are merged into their parent cell. | ||
| */ | ||
| function processTable($table, $, entries, warnings, flattenInline = false) { | ||
| if (!isLayoutTable($table, $)) { | ||
| entries.push({ | ||
| sourceTag: "table", | ||
| templaticalBlockType: "html", | ||
| status: "html-fallback", | ||
| note: "Data table preserved as HTML block." | ||
| }); | ||
| return [convertHtmlFallback($table, $, "Data table preserved as HTML")]; | ||
| } | ||
| const rows = getDirectRows($table, $); | ||
| if (rows.length === 0) return []; | ||
| const sections = []; | ||
| for (const $row of rows) { | ||
| const cells = getDirectCells($row, $); | ||
| if (cells.length === 0) continue; | ||
| const layout = resolveColumnLayout(cells.length, warnings); | ||
| let columnsBlocks; | ||
| if (layout === "1") { | ||
| const merged = []; | ||
| for (const $cell of cells) { | ||
| merged.push(...extractCellBlocks($cell, $, entries, warnings)); | ||
| } | ||
| columnsBlocks = [merged]; | ||
| } else { | ||
| columnsBlocks = cells.map( | ||
| ($cell) => extractCellBlocks($cell, $, entries, warnings) | ||
| ); | ||
| } | ||
| if (flattenInline) { | ||
| for (const col of columnsBlocks) sections.push(...col); | ||
| continue; | ||
| } | ||
| const rowStyles = getStyles2($row); | ||
| const bgColor = parseColor(rowStyles["background-color"]) || parseColor(rowStyles.background); | ||
| const padding = readPaddingFromStyles(rowStyles); | ||
| sections.push( | ||
| createSectionBlock({ | ||
| columns: layout, | ||
| children: columnsBlocks, | ||
| styles: { | ||
| padding, | ||
| ...bgColor ? { backgroundColor: bgColor } : {} | ||
| } | ||
| }) | ||
| ); | ||
| } | ||
| return sections; | ||
| if (!isLayoutTable($table, $)) { | ||
| entries.push({ | ||
| sourceTag: "table", | ||
| templaticalBlockType: "html", | ||
| status: "html-fallback", | ||
| note: "Data table preserved as HTML block." | ||
| }); | ||
| return [convertHtmlFallback($table, $, "Data table preserved as HTML")]; | ||
| } | ||
| const rows = getDirectRows($table, $); | ||
| if (rows.length === 0) return []; | ||
| const sections = []; | ||
| for (const $row of rows) { | ||
| const cells = getDirectCells($row, $); | ||
| if (cells.length === 0) continue; | ||
| const layout = resolveColumnLayout(cells.length, warnings); | ||
| let columnsBlocks; | ||
| if (layout === "1") { | ||
| const merged = []; | ||
| for (const $cell of cells) merged.push(...extractCellBlocks($cell, $, entries, warnings)); | ||
| columnsBlocks = [merged]; | ||
| } else columnsBlocks = cells.map(($cell) => extractCellBlocks($cell, $, entries, warnings)); | ||
| if (flattenInline) { | ||
| for (const col of columnsBlocks) sections.push(...col); | ||
| continue; | ||
| } | ||
| const rowStyles = getStyles($row); | ||
| const bgColor = parseColor(rowStyles["background-color"]) || parseColor(rowStyles.background); | ||
| const padding = readPaddingFromStyles(rowStyles); | ||
| sections.push(createSectionBlock({ | ||
| columns: layout, | ||
| children: columnsBlocks, | ||
| styles: { | ||
| padding, | ||
| ...bgColor ? { backgroundColor: bgColor } : {} | ||
| } | ||
| })); | ||
| } | ||
| return sections; | ||
| } | ||
| // src/converter.ts | ||
| function emptyPadding3() { | ||
| return { top: 0, right: 0, bottom: 0, left: 0 }; | ||
| //#endregion | ||
| //#region src/converter.ts | ||
| function emptyPadding() { | ||
| return { | ||
| top: 0, | ||
| right: 0, | ||
| bottom: 0, | ||
| left: 0 | ||
| }; | ||
| } | ||
| function readPreheader($) { | ||
| const candidates = $("body").children().slice(0, 5).filter((_, el) => { | ||
| const styles = parseStyleAttribute($(el).attr("style")); | ||
| return (styles.display ?? "").toLowerCase() === "none"; | ||
| }); | ||
| if (candidates.length === 0) return void 0; | ||
| const text = $(candidates[0]).text().trim(); | ||
| return text || void 0; | ||
| const candidates = $("body").children().slice(0, 5).filter((_, el) => { | ||
| return (parseStyleAttribute($(el).attr("style")).display ?? "").toLowerCase() === "none"; | ||
| }); | ||
| if (candidates.length === 0) return void 0; | ||
| return $(candidates[0]).text().trim() || void 0; | ||
| } | ||
| function extractSettings($) { | ||
| const $body = $("body"); | ||
| const bodyStyles = parseStyleAttribute($body.attr("style")); | ||
| const fontFamily = parseFontFamily(bodyStyles["font-family"]) || "Arial"; | ||
| const backgroundColor = parseColor(bodyStyles["background-color"]) || parseColor(bodyStyles.background) || "#ffffff"; | ||
| const $outerTable = $body.find("table").first(); | ||
| const widthAttr = parsePxValue($outerTable.attr("width")); | ||
| const widthStyle = parsePxValue( | ||
| parseStyleAttribute($outerTable.attr("style")).width | ||
| ); | ||
| const width = widthAttr || widthStyle || 600; | ||
| const preheaderText = readPreheader($); | ||
| return { | ||
| width, | ||
| backgroundColor, | ||
| fontFamily, | ||
| locale: "en", | ||
| ...preheaderText ? { preheaderText } : {} | ||
| }; | ||
| const $body = $("body"); | ||
| const bodyStyles = parseStyleAttribute($body.attr("style")); | ||
| const fontFamily = parseFontFamily(bodyStyles["font-family"]) || "Arial"; | ||
| const backgroundColor = parseColor(bodyStyles["background-color"]) || parseColor(bodyStyles.background) || "#ffffff"; | ||
| const $outerTable = $body.find("table").first(); | ||
| const widthAttr = parsePxValue($outerTable.attr("width")); | ||
| const widthStyle = parsePxValue(parseStyleAttribute($outerTable.attr("style")).width); | ||
| const width = widthAttr || widthStyle || 600; | ||
| const preheaderText = readPreheader($); | ||
| return { | ||
| width, | ||
| backgroundColor, | ||
| fontFamily, | ||
| locale: "en", | ||
| ...preheaderText ? { preheaderText } : {} | ||
| }; | ||
| } | ||
| /** | ||
| * Wrap a list of free-floating blocks (those produced by top-level non-table | ||
| * elements) in a single one-column section. | ||
| */ | ||
| function wrapInSection(blocks) { | ||
| return createSectionBlock2({ | ||
| columns: "1", | ||
| children: [blocks], | ||
| styles: { | ||
| padding: emptyPadding3() | ||
| } | ||
| }); | ||
| return createSectionBlock({ | ||
| columns: "1", | ||
| children: [blocks], | ||
| styles: { padding: emptyPadding() } | ||
| }); | ||
| } | ||
| /** | ||
| * Walk top-level body children. Tables become sections; loose content | ||
| * elements are accumulated and wrapped in a single one-column section. | ||
| */ | ||
| function processBody($, entries, warnings) { | ||
| const blocks = []; | ||
| const $body = $("body"); | ||
| const children = $body.children().toArray(); | ||
| let pendingLoose = []; | ||
| const flushLoose = () => { | ||
| if (pendingLoose.length > 0) { | ||
| blocks.push(wrapInSection(pendingLoose)); | ||
| pendingLoose = []; | ||
| } | ||
| }; | ||
| for (const childEl of children) { | ||
| const tag = childEl.tagName?.toLowerCase() ?? ""; | ||
| const $child = $(childEl); | ||
| if (tag === "table") { | ||
| flushLoose(); | ||
| blocks.push(...processTable($child, $, entries, warnings, false)); | ||
| continue; | ||
| } | ||
| const childStyles = parseStyleAttribute($child.attr("style")); | ||
| if ((childStyles.display ?? "").toLowerCase() === "none") continue; | ||
| if ((tag === "div" || tag === "center" || tag === "main") && $child.find("table").length > 0) { | ||
| flushLoose(); | ||
| $child.children().each((_, innerEl) => { | ||
| const innerTag = innerEl.tagName?.toLowerCase() ?? ""; | ||
| const $inner = $(innerEl); | ||
| if (innerTag === "table") { | ||
| blocks.push(...processTable($inner, $, entries, warnings, false)); | ||
| } else { | ||
| const r2 = convertElement($inner, $); | ||
| if (r2) { | ||
| entries.push(r2.entry); | ||
| pendingLoose.push(r2.block); | ||
| } | ||
| } | ||
| }); | ||
| flushLoose(); | ||
| continue; | ||
| } | ||
| const r = convertElement($child, $); | ||
| if (r) { | ||
| entries.push(r.entry); | ||
| pendingLoose.push(r.block); | ||
| } | ||
| } | ||
| flushLoose(); | ||
| return blocks; | ||
| const blocks = []; | ||
| const children = $("body").children().toArray(); | ||
| let pendingLoose = []; | ||
| const flushLoose = () => { | ||
| if (pendingLoose.length > 0) { | ||
| blocks.push(wrapInSection(pendingLoose)); | ||
| pendingLoose = []; | ||
| } | ||
| }; | ||
| for (const childEl of children) { | ||
| const tag = childEl.tagName?.toLowerCase() ?? ""; | ||
| const $child = $(childEl); | ||
| if (tag === "table") { | ||
| flushLoose(); | ||
| blocks.push(...processTable($child, $, entries, warnings, false)); | ||
| continue; | ||
| } | ||
| if ((parseStyleAttribute($child.attr("style")).display ?? "").toLowerCase() === "none") continue; | ||
| if ((tag === "div" || tag === "center" || tag === "main") && $child.find("table").length > 0) { | ||
| flushLoose(); | ||
| $child.children().each((_, innerEl) => { | ||
| const innerTag = innerEl.tagName?.toLowerCase() ?? ""; | ||
| const $inner = $(innerEl); | ||
| if (innerTag === "table") { | ||
| flushLoose(); | ||
| blocks.push(...processTable($inner, $, entries, warnings, false)); | ||
| } else { | ||
| const r = convertElement($inner, $); | ||
| if (r) { | ||
| entries.push(r.entry); | ||
| pendingLoose.push(r.block); | ||
| } | ||
| } | ||
| }); | ||
| flushLoose(); | ||
| continue; | ||
| } | ||
| const r = convertElement($child, $); | ||
| if (r) { | ||
| entries.push(r.entry); | ||
| pendingLoose.push(r.block); | ||
| } | ||
| } | ||
| flushLoose(); | ||
| return blocks; | ||
| } | ||
| /** | ||
| * Converts an HTML email template to Templatical TemplateContent. | ||
| * | ||
| * Designed for table-based marketing email HTML (output of MJML, Mailchimp, | ||
| * SendGrid, Campaign Monitor, hand-coded emails). Modern HTML using flex/grid | ||
| * layouts is preserved via HTML-fallback blocks. | ||
| * | ||
| * @param html - The raw HTML string (full document or body fragment). | ||
| * @returns An ImportResult with the converted content and a detailed report. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { convertHtmlTemplate } from '@templatical/import-html'; | ||
| * | ||
| * const html = await fetch('/email.html').then((r) => r.text()); | ||
| * const { content, report } = convertHtmlTemplate(html); | ||
| * | ||
| * const editor = init({ container: '#editor', content }); | ||
| * | ||
| * console.log(report.summary); | ||
| * console.log(report.warnings); | ||
| * ``` | ||
| */ | ||
| function convertHtmlTemplate(html) { | ||
| if (typeof html !== "string") { | ||
| throw new Error( | ||
| "Invalid HTML template: expected a string. Pass the raw HTML source as a string." | ||
| ); | ||
| } | ||
| if (html.trim().length === 0) { | ||
| throw new Error( | ||
| "Invalid HTML template: input is empty. Pass the raw HTML source of an email." | ||
| ); | ||
| } | ||
| const $ = load(html); | ||
| resolveCssStyles($); | ||
| $("script, noscript, link, meta, title").remove(); | ||
| const entries = []; | ||
| const warnings = []; | ||
| const blocks = processBody($, entries, warnings); | ||
| if (blocks.length === 0) { | ||
| warnings.push( | ||
| "No convertible content was found in the HTML. The email may use a non-table layout \u2014 modern HTML support is limited." | ||
| ); | ||
| } | ||
| const content = { | ||
| ...createDefaultTemplateContent(), | ||
| blocks, | ||
| settings: extractSettings($) | ||
| }; | ||
| const summary = { | ||
| total: entries.length, | ||
| converted: entries.filter((e) => e.status === "converted").length, | ||
| approximated: entries.filter((e) => e.status === "approximated").length, | ||
| htmlFallback: entries.filter((e) => e.status === "html-fallback").length, | ||
| skipped: entries.filter((e) => e.status === "skipped").length | ||
| }; | ||
| const report = { entries, warnings, summary }; | ||
| return { content, report }; | ||
| if (typeof html !== "string") throw new Error("Invalid HTML template: expected a string. Pass the raw HTML source as a string."); | ||
| if (html.trim().length === 0) throw new Error("Invalid HTML template: input is empty. Pass the raw HTML source of an email."); | ||
| const $ = load(html); | ||
| resolveCssStyles($); | ||
| $("script, noscript, link, meta, title").remove(); | ||
| const entries = []; | ||
| const warnings = []; | ||
| const blocks = processBody($, entries, warnings); | ||
| if (blocks.length === 0) warnings.push("No convertible content was found in the HTML. The email may use a non-table layout — modern HTML support is limited."); | ||
| return { | ||
| content: { | ||
| ...createDefaultTemplateContent(), | ||
| blocks, | ||
| settings: extractSettings($) | ||
| }, | ||
| report: { | ||
| entries, | ||
| warnings, | ||
| summary: { | ||
| total: entries.length, | ||
| converted: entries.filter((e) => e.status === "converted").length, | ||
| approximated: entries.filter((e) => e.status === "approximated").length, | ||
| htmlFallback: entries.filter((e) => e.status === "html-fallback").length, | ||
| skipped: entries.filter((e) => e.status === "skipped").length | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| export { | ||
| convertHtmlTemplate | ||
| }; | ||
| //#endregion | ||
| export { convertHtmlTemplate }; | ||
| //# sourceMappingURL=index.js.map |
@@ -1,1 +0,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 \"@templatical/types\";\nimport type { Block, TemplateContent } from \"@templatical/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 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 },\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 '@templatical/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 \"@templatical/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 \"@templatical/types\";\nimport type { Block, HeadingLevel, SpacingValue } from \"@templatical/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 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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\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 },\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 },\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 },\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 },\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 },\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 },\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 },\n });\n}\n\n/**\n * Decides whether a `<td>` looks like a vertical spacer:\n * empty (or only ` `) 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 \"@templatical/types\";\nimport type { Block, ColumnLayout } from \"@templatical/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 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 },\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 },\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 ...(bgColor ? { backgroundColor: bgColor } : {}),\n },\n }),\n );\n }\n\n return sections;\n}\n"],"mappings":"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","names":["createSectionBlock","createButtonBlock","createSpacerBlock","emptyPadding","getStyles","createButtonBlock","createSpacerBlock","r","emptyPadding","createSectionBlock","r"]} | ||
| {"version":3,"file":"index.js","names":["emptyPadding","getStyles","emptyPadding"],"sources":["../src/style-parser.ts","../src/css-resolver.ts","../src/block-mapper.ts","../src/section-builder.ts","../src/converter.ts"],"sourcesContent":["import type { SpacingValue } from \"@templatical/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 \"@templatical/types\";\nimport type { Block, HeadingLevel, SpacingValue } from \"@templatical/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 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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\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 },\n });\n}\n\n/**\n * Apply a container-level `text-align` to every `<p>` opening tag in `html`,\n * merging into an existing `style=\"…\"` attribute when present. Tolerant of\n * any other attributes on the `<p>` (class/id/dir/…) — the previous narrow\n * `<p style=\"…\">` + bare-`<p>` matchers silently dropped the alignment when\n * the inner `<p>` carried a non-style attribute.\n */\nfunction applyTextAlignToParagraphs(html: string, textAlign: string): string {\n return html.replace(/<p\\b([^>]*)>/gi, (_match, attrs: string) => {\n const styleMatch = /\\sstyle\\s*=\\s*\"([^\"]*)\"/i.exec(attrs);\n if (styleMatch) {\n const existing = styleMatch[1].trim().replace(/;\\s*$/, \"\");\n const merged = existing\n ? `${existing}; text-align: ${textAlign}`\n : `text-align: ${textAlign}`;\n const newAttrs =\n attrs.slice(0, styleMatch.index) +\n ` style=\"${merged}\"` +\n attrs.slice(styleMatch.index + styleMatch[0].length);\n return `<p${newAttrs}>`;\n }\n return `<p${attrs} style=\"text-align: ${textAlign}\">`;\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 = applyTextAlignToParagraphs(result, 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 },\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 },\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 },\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 },\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 },\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 },\n });\n}\n\n/**\n * Decides whether a `<td>` looks like a vertical spacer:\n * empty (or only ` `) 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 \"@templatical/types\";\nimport type { Block, ColumnLayout } from \"@templatical/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 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 },\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 },\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 ...(bgColor ? { backgroundColor: bgColor } : {}),\n },\n }),\n );\n }\n\n return sections;\n}\n","import { load } from \"cheerio\";\nimport type { CheerioAPI, Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n createDefaultTemplateContent,\n createSectionBlock,\n} from \"@templatical/types\";\nimport type { Block, TemplateContent } from \"@templatical/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 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 },\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 // Flush loose content accumulated BEFORE this table so it keeps its\n // source position, mirroring the top-level loop. Without this, the\n // table is appended immediately while leading siblings are flushed\n // only after the loop — reordering the document.\n flushLoose();\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 '@templatical/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"],"mappings":"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"} |
+4
-4
| { | ||
| "name": "@templatical/import-html", | ||
| "description": "Convert HTML email templates to Templatical format", | ||
| "version": "0.10.0", | ||
| "version": "0.10.1", | ||
| "bugs": "https://github.com/templatical/sdk/issues", | ||
@@ -9,6 +9,6 @@ "dependencies": { | ||
| "domhandler": "^6.0.1", | ||
| "@templatical/types": "0.10.0" | ||
| "@templatical/types": "0.10.1" | ||
| }, | ||
| "devDependencies": { | ||
| "tsup": "^8.5.1", | ||
| "@types/node": "^25.9.1", | ||
| "typescript": "^6.0.3", | ||
@@ -49,3 +49,3 @@ "vitest": "^4.1.7" | ||
| "scripts": { | ||
| "build": "tsup", | ||
| "build": "tsdown", | ||
| "test": "vitest run --config vitest.config.ts", | ||
@@ -52,0 +52,0 @@ "typecheck": "tsc --noEmit" |
94510
2.63%1026
10.68%+ Added
- Removed
Updated