🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@templatical/import-html

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@templatical/import-html - npm Package Compare versions

Comparing version
0.10.1
to
0.10.2
+1
-1
dist/index.js.map

@@ -1,1 +0,1 @@

{"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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n return `<!-- ${escapedMessage} -->\\n${raw}`;\n}\n\n/**\n * Heading element (h1-h6) → Title block.\n */\nfunction convertHeading($el: Cheerio<Element>): Block {\n const tag = tagOf($el[0]);\n const styles = getStyles($el);\n const levelMatch = tag.match(/^h(\\d)$/);\n const rawLevel = levelMatch ? Number(levelMatch[1]) : 2;\n const level: HeadingLevel = (\n rawLevel >= 1 && rawLevel <= 4 ? rawLevel : Math.min(rawLevel, 4)\n ) as HeadingLevel;\n\n const innerHtml = getInnerHtml($el);\n const content = innerHtml.trim() ? `<p>${innerHtml}</p>` : \"<p></p>\";\n\n return createTitleBlock({\n content,\n level,\n color: parseColor(styles.color) || \"#1a1a1a\",\n textAlign: parseAlignment(styles[\"text-align\"]),\n fontFamily: parseFontFamily(styles[\"font-family\"]) || undefined,\n styles: {\n padding: readPaddingFromStyles(styles),\n },\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 `&nbsp;`) AND has an explicit height.\n */\nexport function isSpacerCell($el: Cheerio<Element>): boolean {\n const text = ($el.text() ?? \"\").replace(/\\s| /g, \"\");\n if (text !== \"\") return false;\n if ($el.find(\"img, a, hr\").length > 0) return false;\n\n const styles = getStyles($el);\n const hasHeight =\n parsePxValue($el.attr(\"height\")) > 0 ||\n parsePxValue(styles.height) > 0 ||\n parsePxValue(styles[\"line-height\"]) > 0;\n return hasHeight;\n}\n\n/**\n * Decides whether a `<td>` is a button container — i.e. has exactly one\n * `<a>` inside that itself looks like a button.\n */\nexport function isButtonCell(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n): { match: boolean; anchor?: Cheerio<Element> } {\n const anchors = $el.find(\"a\");\n if (anchors.length !== 1) return { match: false };\n const anchor = $(anchors[0]);\n if (looksLikeButton(getStyles(anchor))) return { match: true, anchor };\n // Cell-level styling (bg, padding) wrapping a plain anchor reads as a\n // button only when the anchor actually has an href. Without one, the\n // anchor is a decorative styled span and should fall through to the\n // text-conversion path; otherwise convertButton defaults href to \"#\"\n // and the import becomes a clickable button to nowhere.\n if (looksLikeButton(getStyles($el))) {\n const href = (anchor.attr(\"href\") ?? \"\").trim();\n if (href !== \"\") {\n return { match: true, anchor };\n }\n }\n return { match: false };\n}\n\n/**\n * Converts a single content-bearing element (heading / paragraph / image /\n * anchor-as-button / divider) to a Templatical block.\n *\n * Returns `null` for elements that do not contain any meaningful content\n * (the caller should skip them).\n */\nexport function convertElement(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n): { block: Block; entry: ImportReportEntry } | null {\n const tag = tagOf($el[0]);\n if (!tag) return null;\n\n if (HEADING_TAGS.has(tag)) {\n return {\n block: convertHeading($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"title\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"img\") {\n return {\n block: convertImage($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"a\") {\n if (looksLikeButton(getStyles($el))) {\n return {\n block: convertButton($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n }\n // Plain anchor — wrap as paragraph.\n return {\n block: convertParagraph($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"paragraph\",\n status: \"approximated\",\n note: \"Inline anchor wrapped in a paragraph block.\",\n },\n };\n }\n\n if (tag === \"hr\") {\n return {\n block: convertDivider($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n }\n\n if (TEXT_TAGS.has(tag)) {\n const text = ($el.text() ?? \"\").trim();\n if (!text && $el.find(\"img, a\").length === 0) return null;\n return {\n block: convertParagraph($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n }\n\n // Unknown element — preserve as HTML.\n return {\n block: convertHtmlFallback(\n $el,\n $,\n `Unsupported element <${tag}>: preserved as raw HTML`,\n ),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `Unknown element \"${tag}\" preserved as HTML block.`,\n },\n };\n}\n\n/**\n * Helpers exported for tests.\n */\nexport const _internal = {\n convertButton,\n convertDivider,\n convertHeading,\n convertImage,\n convertParagraph,\n convertSpacer,\n ensureParagraphWrapped,\n};\n","import type { CheerioAPI, Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n createSectionBlock,\n createButtonBlock,\n createSpacerBlock,\n} from \"@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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"}
{"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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n return `<!-- ${escapedMessage} -->\\n${raw}`;\n}\n\n/**\n * Heading element (h1-h6) → Title block.\n */\nfunction convertHeading($el: Cheerio<Element>): Block {\n const tag = tagOf($el[0]);\n const styles = getStyles($el);\n const levelMatch = tag.match(/^h(\\d)$/);\n const rawLevel = levelMatch ? Number(levelMatch[1]) : 2;\n const level: HeadingLevel = (\n rawLevel >= 1 && rawLevel <= 4 ? rawLevel : Math.min(rawLevel, 4)\n ) as HeadingLevel;\n\n const innerHtml = getInnerHtml($el);\n const content = innerHtml.trim() ? `<p>${innerHtml}</p>` : \"<p></p>\";\n\n return createTitleBlock({\n content,\n level,\n color: parseColor(styles.color) || \"#1a1a1a\",\n textAlign: parseAlignment(styles[\"text-align\"]),\n fontFamily: parseFontFamily(styles[\"font-family\"]) || undefined,\n styles: {\n padding: readPaddingFromStyles(styles),\n },\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 `&nbsp;`) AND has an explicit height.\n */\nexport function isSpacerCell($el: Cheerio<Element>): boolean {\n const text = ($el.text() ?? \"\").replace(/\\s| /g, \"\");\n if (text !== \"\") return false;\n if ($el.find(\"img, a, hr\").length > 0) return false;\n\n const styles = getStyles($el);\n const hasHeight =\n parsePxValue($el.attr(\"height\")) > 0 ||\n parsePxValue(styles.height) > 0 ||\n parsePxValue(styles[\"line-height\"]) > 0;\n return hasHeight;\n}\n\n/**\n * Decides whether a `<td>` is a button container — i.e. has exactly one\n * `<a>` inside that itself looks like a button.\n */\nexport function isButtonCell(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n): { match: boolean; anchor?: Cheerio<Element> } {\n const anchors = $el.find(\"a\");\n if (anchors.length !== 1) return { match: false };\n const anchor = $(anchors[0]);\n if (looksLikeButton(getStyles(anchor))) return { match: true, anchor };\n // Cell-level styling (bg, padding) wrapping a plain anchor reads as a\n // button only when the anchor actually has an href. Without one, the\n // anchor is a decorative styled span and should fall through to the\n // text-conversion path; otherwise convertButton defaults href to \"#\"\n // and the import becomes a clickable button to nowhere.\n if (looksLikeButton(getStyles($el))) {\n const href = (anchor.attr(\"href\") ?? \"\").trim();\n if (href !== \"\") {\n return { match: true, anchor };\n }\n }\n return { match: false };\n}\n\n/**\n * Converts a single content-bearing element (heading / paragraph / image /\n * anchor-as-button / divider) to a Templatical block.\n *\n * Returns `null` for elements that do not contain any meaningful content\n * (the caller should skip them).\n */\nexport function convertElement(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n): { block: Block; entry: ImportReportEntry } | null {\n const tag = tagOf($el[0]);\n if (!tag) return null;\n\n if (HEADING_TAGS.has(tag)) {\n return {\n block: convertHeading($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"title\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"img\") {\n return {\n block: convertImage($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"a\") {\n if (looksLikeButton(getStyles($el))) {\n return {\n block: convertButton($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n }\n // Plain anchor — wrap as paragraph.\n return {\n block: convertParagraph($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"paragraph\",\n status: \"approximated\",\n note: \"Inline anchor wrapped in a paragraph block.\",\n },\n };\n }\n\n if (tag === \"hr\") {\n return {\n block: convertDivider($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n }\n\n if (TEXT_TAGS.has(tag)) {\n const text = ($el.text() ?? \"\").trim();\n if (!text && $el.find(\"img, a\").length === 0) return null;\n return {\n block: convertParagraph($el),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n }\n\n // Unknown element — preserve as HTML.\n return {\n block: convertHtmlFallback(\n $el,\n $,\n `Unsupported element <${tag}>: preserved as raw HTML`,\n ),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `Unknown element \"${tag}\" preserved as HTML block.`,\n },\n };\n}\n\n/**\n * Helpers exported for tests.\n */\nexport const _internal = {\n convertButton,\n convertDivider,\n convertHeading,\n convertImage,\n convertParagraph,\n convertSpacer,\n ensureParagraphWrapped,\n};\n","import type { CheerioAPI, Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n createSectionBlock,\n createButtonBlock,\n createSpacerBlock,\n} from \"@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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"}
{
"name": "@templatical/import-html",
"description": "Convert HTML email templates to Templatical format",
"version": "0.10.1",
"version": "0.10.2",
"bugs": "https://github.com/templatical/sdk/issues",

@@ -9,8 +9,8 @@ "dependencies": {

"domhandler": "^6.0.1",
"@templatical/types": "0.10.1"
"@templatical/types": "0.10.2"
},
"devDependencies": {
"@types/node": "^25.9.1",
"@types/node": "^25.9.2",
"typescript": "^6.0.3",
"vitest": "^4.1.7"
"vitest": "^4.1.8"
},

@@ -17,0 +17,0 @@ "exports": {