@templatical/import-html 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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 * An image's corner radius in px, or `undefined` for square corners.\n *\n * Accepts a percentage as well as a px length, because `border-radius: 50%` is\n * the idiomatic way to write a circular avatar and the block stores px only. A\n * percentage resolves against the shorter rendered side, which is what turns a\n * square image into a circle and a wide one into a pill.\n *\n * `width` and `height` must be the dimensions actually read from the source. A\n * percentage with neither known is dropped rather than resolved against the\n * caller's fallback width, which would invent a radius the template never\n * expressed. A non-positive result is dropped for the same reason a px `0` is:\n * both mean square, and only absence keeps it out of the exported JSON.\n */\nexport function parseImageBorderRadius(\n value: string | number | undefined,\n width: number | undefined,\n height: number | undefined,\n): number | undefined {\n if (value === undefined || value === null || value === \"\") return undefined;\n if (typeof value === \"number\") {\n return value > 0 ? Math.round(value) : undefined;\n }\n\n // Deliberately no `-?`: a negative percentage falls through to the px parse,\n // which rejects it too.\n const percent = value.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*%$/);\n if (percent) {\n const side = Math.min(width ?? Infinity, height ?? Infinity);\n if (!Number.isFinite(side)) return undefined;\n const radius = Math.round((parseFloat(percent[1]) / 100) * side);\n return radius > 0 ? radius : undefined;\n }\n\n const px = parsePxValue(value);\n return px > 0 ? px : undefined;\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 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 parseImageBorderRadius,\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 // Kept separate from the `?? 600` fallback: a percentage radius has to\n // resolve against a width the template actually stated.\n const readWidth =\n parsePxValue(widthAttr) || parsePxValue(widthStyle) || undefined;\n const width = readWidth ?? 600;\n // No default, unlike width: an absent height is what keeps the aspect ratio,\n // and `height=\"auto\"` / `height: auto` parse to 0, which reads the same way.\n const height =\n parsePxValue($el.attr(\"height\")) ||\n parsePxValue(styles.height) ||\n undefined;\n\n return createImageBlock({\n src,\n alt,\n width,\n height,\n borderRadius: parseImageBorderRadius(\n styles[\"border-radius\"],\n readWidth,\n height,\n ),\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\n/**\n * Reads a button's placement from the cell that wraps it. An anchor styled as\n * a button is sized to its own content, so its `text-align` says nothing about\n * where it sits — table-based email puts that on the containing `<td>`, as\n * either a `text-align` style or the legacy `align` attribute. Only the\n * immediate parent is consulted; walking further up would start reporting the\n * alignment of the surrounding layout rather than of the button.\n */\nfunction readButtonAlign($el: Cheerio<Element>): \"left\" | \"center\" | \"right\" {\n const parent = $el.parent();\n if (parent.length === 0) return \"center\";\n\n const fromStyle = parseStyleAttribute(parent.attr(\"style\"))[\"text-align\"];\n if (fromStyle) return parseAlignment(fromStyle, \"center\");\n\n return parseAlignment(parent.attr(\"align\"), \"center\");\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 align: readButtonAlign($el),\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 * 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","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 textColor: \"#1a1a1a\",\n linkUnderline: false,\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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"}
1
+ {"version":3,"file":"index.js","names":["getStyles","emptyPadding","emptyPadding"],"sources":["../src/style-parser.ts","../src/css-resolver.ts","../src/block-mapper.ts","../src/column-ratio.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 * An image's corner radius in px, or `undefined` for square corners.\n *\n * Accepts a percentage as well as a px length, because `border-radius: 50%` is\n * the idiomatic way to write a circular avatar and the block stores px only. A\n * percentage resolves against the shorter rendered side, which is what turns a\n * square image into a circle and a wide one into a pill.\n *\n * `width` and `height` must be the dimensions actually read from the source. A\n * percentage with neither known is dropped rather than resolved against the\n * caller's fallback width, which would invent a radius the template never\n * expressed. A non-positive result is dropped for the same reason a px `0` is:\n * both mean square, and only absence keeps it out of the exported JSON.\n */\nexport function parseImageBorderRadius(\n value: string | number | undefined,\n width: number | undefined,\n height: number | undefined,\n): number | undefined {\n if (value === undefined || value === null || value === \"\") return undefined;\n if (typeof value === \"number\") {\n return value > 0 ? Math.round(value) : undefined;\n }\n\n // Deliberately no `-?`: a negative percentage falls through to the px parse,\n // which rejects it too.\n const percent = value.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*%$/);\n if (percent) {\n const side = Math.min(width ?? Infinity, height ?? Infinity);\n if (!Number.isFinite(side)) return undefined;\n const radius = Math.round((parseFloat(percent[1]) / 100) * side);\n return radius > 0 ? radius : undefined;\n }\n\n const px = parsePxValue(value);\n return px > 0 ? px : undefined;\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 { isTag, isText } from \"domhandler\";\nimport type { Element, AnyNode } from \"domhandler\";\nimport {\n createTitleBlock,\n createParagraphBlock,\n createImageBlock,\n createButtonBlock,\n createDividerBlock,\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 parseImageBorderRadius,\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\n/**\n * Wrapper tags that carry layout rather than content. One of these is worth\n * descending into when it holds a table somewhere below it.\n */\nconst CONTAINER_TAGS = new Set([\"div\", \"center\", \"main\"]);\n\n/**\n * Decides whether an element is a layout container worth descending into: a\n * wrapper tag that holds a table somewhere below it.\n *\n * The table test is what keeps the descent from widening into \"descend every\n * div\". `div` is in `TEXT_TAGS` above, so a container holding only copy must\n * keep that mapping and become one paragraph rather than being split into a\n * block per child.\n *\n * Lives here, with the other predicates both traversal modules consult,\n * because the body walk and the cell walk have to agree on what a container\n * is. A second copy answers the question differently the moment either is\n * edited, and the divergence shows up as a table swallowed into a paragraph\n * on whichever surface was missed.\n */\nexport function isTableContainer($el: Cheerio<Element>, tag: string): boolean {\n return CONTAINER_TAGS.has(tag) && $el.find(\"table\").length > 0;\n}\n\n/**\n * Whether an element is laid out beside its siblings rather than stacked above\n * them: `display: inline-block`.\n *\n * This is the property that *makes* a set of divs a set of columns — an email\n * that wants side-by-side divs has no other way to say so, since a block-level\n * div stacks — so requiring it is the definition of the shape and not a\n * heuristic about it. Two plain divs stack vertically, and reading those as\n * columns would invent a layout the source never stated.\n */\nfunction isSideBySide($el: Cheerio<Element>): boolean {\n return (getStyles($el).display ?? \"\").trim().toLowerCase() === \"inline-block\";\n}\n\n/**\n * The sibling containers that make up a cell's column set, or `null` when the\n * cell is not one.\n *\n * This is the one column shape with no cell count to read: a single `<td>`\n * holding one inline-block `<div>` per column. Cerberus's hybrid template\n * states four layouts this way and compiled MJML states every one of them —\n * mjml puts a whole section's columns into one cell as sibling\n * `div.mj-column-per-*` — so a row's cell count reports one column for a\n * layout that has two or three. Counting the divs is still *counting*: the\n * number of columns comes from the number of elements, and a width is only\n * ever consulted afterwards to choose between layouts of that same count.\n *\n * Four conditions, each of them a hazard a relaxed version would reintroduce:\n *\n * - Every element child must be a container. A cell mixing a column set with\n * anything else has no column any other element belongs to.\n * - Every one must be laid out side by side (`isSideBySide`), which is what\n * distinguishes columns from stacked content.\n * - None may be blank. A multi-column section with an empty slot is worse\n * than the single column a cell count already gives, and the source's own\n * spacer chrome is exactly what would fill one.\n * - No text of the cell's own may survive, for the same reason as the first\n * condition — a bare sentence beside the columns belongs to none of them.\n *\n * Deliberately structural: it reads no width, so a column set with no declared\n * width still becomes one, and a `width:100%` — which every real column div\n * carries — can never make or break the decision.\n */\nexport function columnDivsOf(\n $cell: Cheerio<Element>,\n $: CheerioAPI,\n): Cheerio<Element>[] | null {\n const columns: Cheerio<Element>[] = [];\n let inlineText = \"\";\n\n for (const node of $cell.contents().toArray()) {\n if (isInlineContent(node)) {\n inlineText += $(node).text();\n continue;\n }\n // Comments carry no content, and MSO conditional comments sit between\n // every pair of column divs in both real sources — skipping them is what\n // keeps the siblings adjacent.\n if (!isTag(node)) continue;\n\n const tag = node.tagName.toLowerCase();\n if (!CONTAINER_TAGS.has(tag)) return null;\n\n const $child = $(node) as unknown as Cheerio<Element>;\n if (!isSideBySide($child)) return null;\n if (isBlankCell($child)) return null;\n columns.push($child);\n }\n\n if (columns.length < 2) return null;\n if (inlineText.trim() !== \"\") return null;\n return columns;\n}\n\n/**\n * Block-level elements a container may be unwrapped down to. Only a heading:\n * a container wrapping one is the single case where mapping the container\n * gets the block's *type* wrong.\n *\n * A tag qualifies only if unwrapping it loses nothing, and `p` is the one that\n * looks like it belongs and does not. `convertParagraph` reads an element's\n * inner HTML, so unwrapping `<div><p class=\"lead\">…</p></div>` drops the `<p>`\n * and every attribute on it, where mapping the container keeps that markup\n * inside the paragraph's content. Nothing is bought in exchange: a wrapped\n * `<p>` already maps to a paragraph, which is the right type. A heading has no\n * such cost, because `convertHeading` stores the level and the inner HTML —\n * so unwrapping routes it to the same converter a bare `<h3>` already reaches.\n *\n * `img`, `hr` and `table` stay out for the same \"nothing to fix\" reason: a\n * table belongs to the container descent the two traversals run, and the other\n * two would widen a heading-typing rule into image and divider mapping.\n *\n * The set is also what keeps the styling below correct — `convertHeading` is\n * the only converter reached with a container's styles, so admitting a tag it\n * does not handle would silently drop them.\n */\nconst UNWRAPPABLE_BLOCK_TAGS: ReadonlySet<string> = HEADING_TAGS;\n\n/**\n * Inline formatting tags, which carry no block of their own. One of these\n * reaching a block position means the parent's text extraction stopped short —\n * it does not mean the element has no mapping, so it must never fall through\n * to the html-fallback arm.\n *\n * The hazard that keeps them listed here: a cell's inline markup and the bare\n * text nodes around it are one run of rich text. Dispatching an inline element\n * on its own emits a block whose entire content is `<br>` AND deletes every\n * text node beside it, because a walk over element children never visits\n * those. That is silent content loss — text visible in the source email never\n * reaches the template.\n *\n * `a` is excluded on purpose: whether an anchor belongs to a run depends on\n * how the source styled it, so the cell walk asks `isProseAnchor` per anchor\n * instead. Reading every `<a>` as inline here would fold a styled call to\n * action into the sentence beside it and lose the button.\n */\nconst INLINE_FORMATTING_TAGS = new Set([\n \"br\",\n \"em\",\n \"strong\",\n \"i\",\n \"b\",\n \"u\",\n \"small\",\n \"sub\",\n \"sup\",\n]);\n\nfunction emptyPadding(): SpacingValue {\n return { top: 0, right: 0, bottom: 0, left: 0 };\n}\n\nfunction tagOf(el: Element | AnyNode): string {\n if (\"tagName\" in el && typeof el.tagName === \"string\")\n return el.tagName.toLowerCase();\n return \"\";\n}\n\nfunction getStyles($el: Cheerio<Element>): Record<string, string> {\n return parseStyleAttribute($el.attr(\"style\"));\n}\n\n/**\n * Whether an element carries anything a reader would see: text, or an element\n * that renders without text of its own.\n *\n * One rule with two readers, which is the point: it decides both whether a\n * text container is worth a block at all and whether a wrapper is worth\n * unwrapping. Two copies would let `<div><h3></h3></div>` be skipped by one\n * and turned into an empty title by the other.\n */\nfunction hasRenderedContent($el: Cheerio<Element>): boolean {\n if (($el.text() ?? \"\").trim() !== \"\") return true;\n return $el.find(\"img, a\").length > 0;\n}\n\n/**\n * The single element a container's whole content consists of, or `null` when\n * the container holds anything else.\n *\n * Whitespace and comments are incidental — the same reading `extractContentBlocks`\n * gives them — and `trim` counts `&nbsp;` among them, which is how the rest of\n * this module reads it (`normalizeCellText`, and through it `isBlankCell`).\n * Everything else is content: a second element, a bare word, or a rendering\n * `<br>` all mean the container holds more than one thing, and unwrapping it\n * would drop whatever was not unwrapped.\n */\nfunction soleElementChild($el: Cheerio<Element>): Element | null {\n let found: Element | null = null;\n\n for (const node of $el.contents().toArray()) {\n if (isText(node)) {\n if (node.data.trim() !== \"\") return null;\n continue;\n }\n if (!isTag(node)) continue;\n if (found) return null;\n found = node;\n }\n\n return found;\n}\n\n/**\n * The styles the innermost element of a wrapper chain renders with: each\n * container's own declarations, overridden by those of the element inside it.\n *\n * The container's styles have to travel, because the wrapper is where\n * table-based email puts the colour, size, font and alignment — a plain\n * `getStyles` on the unwrapped element trades a typing defect for a styling\n * loss.\n *\n * A declaration of `inherit` states nothing of its own, so it must not shadow\n * the container's value. That is load-bearing rather than pedantic: mjml@5\n * puts every visual property on the wrapper div and writes `color: inherit` on\n * the heading inside it, so honouring the keyword literally drops the colour\n * the email actually renders with.\n */\nfunction inheritedStyles(chain: Cheerio<Element>[]): Record<string, string> {\n const merged: Record<string, string> = { ...getStyles(chain[0]) };\n\n for (const $node of chain.slice(1)) {\n for (const [property, value] of Object.entries(getStyles($node))) {\n if (value.trim().toLowerCase() === \"inherit\") continue;\n merged[property] = value;\n }\n }\n\n return merged;\n}\n\n/**\n * The element that takes a container's place when the container's entire\n * meaningful content is one block-level element, together with the styles that\n * element renders with. Returns the element handed in when there is nothing to\n * unwrap.\n *\n * Generator-produced email wraps each text block in a plain `<div>` holding a\n * single block-level element — compiled MJML puts one `<h3>` inside an\n * `mj-text` body that rendered a heading. That `<div>` holds no table, so it\n * is correctly not a container to descend, and `div` is a text tag here:\n * mapping it emits a paragraph with the heading buried in its content, which\n * loses the heading's semantics, its own styling and any downstream treatment\n * of titles.\n *\n * Three constraints, each a hazard a relaxed version would reintroduce:\n *\n * - The chain must *end* on an `UNWRAPPABLE_BLOCK_TAGS` element. That is what\n * keeps a `div.mj-column-per-*` out: its sole child is a `<table>`, and\n * handing that to the dispatch below would html-fallback the whole column.\n * It also leaves a chain of containers bottoming out in bare text alone, so\n * `<div><div>copy</div></div>` keeps mapping as it did.\n * - A container holding a table is refused outright, through the same\n * `isTableContainer` predicate the two traversals use. This decides the one\n * case the tag test cannot — a sole child that *is* a heading, with the\n * table below it — and both traversals descend such a container, so the\n * subtree is theirs rather than this dispatch's.\n * - An empty wrapper is refused, so a container whose sole child renders\n * nothing stays skipped rather than becoming an empty title.\n *\n * Bounded by DOM depth: each step moves to a child.\n */\nfunction resolveWrappedBlock(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n): { $el: Cheerio<Element>; styles: Record<string, string> } {\n const unwrapped = { $el, styles: getStyles($el) };\n\n const chain: Cheerio<Element>[] = [$el];\n let $current = $el;\n\n for (;;) {\n const tag = tagOf($current[0]);\n if (!CONTAINER_TAGS.has(tag)) break;\n if (isTableContainer($current, tag)) break;\n\n const child = soleElementChild($current);\n if (!child) break;\n\n const childTag = tagOf(child);\n if (!CONTAINER_TAGS.has(childTag) && !UNWRAPPABLE_BLOCK_TAGS.has(childTag))\n break;\n\n $current = $(child) as unknown as Cheerio<Element>;\n chain.push($current);\n }\n\n if (chain.length === 1) return unwrapped;\n\n const $target = chain[chain.length - 1];\n if (!UNWRAPPABLE_BLOCK_TAGS.has(tagOf($target[0]))) return unwrapped;\n if (!hasRenderedContent($target)) return unwrapped;\n\n return { $el: $target, styles: inheritedStyles(chain) };\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 *\n * `styles` is the element's own by default and the wrapper chain's when the\n * heading was unwrapped out of a container — see `resolveWrappedBlock`.\n */\nfunction convertHeading(\n $el: Cheerio<Element>,\n styles: Record<string, string> = getStyles($el),\n): Block {\n const tag = tagOf($el[0]);\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 * Builds a Paragraph block from a fragment of inline markup, styled by the\n * element that supplied `styles`.\n */\nfunction buildParagraph(\n innerHtml: string,\n styles: Record<string, string>,\n): Block {\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 * Paragraph or block-level text container → Paragraph block.\n */\nfunction convertParagraph($el: Cheerio<Element>): Block {\n return buildParagraph(getInnerHtml($el), getStyles($el));\n}\n\n/**\n * Decides whether a child node of a table cell belongs to a run of inline\n * text rather than to a block of its own: a bare text node, or one of the\n * inline formatting tags.\n */\nexport function isInlineContent(node: AnyNode): boolean {\n if (isText(node)) return true;\n return isTag(node) && INLINE_FORMATTING_TAGS.has(node.tagName.toLowerCase());\n}\n\n/**\n * Converts a run of consecutive inline nodes lifted out of a table cell into\n * one Paragraph block, keeping their markup inside the paragraph's content.\n *\n * `$cell` supplies the styling: a bare run has no element of its own to read\n * a colour, size or alignment from, and table-based email puts all three on\n * the cell.\n *\n * Returns `null` for a run carrying no text — a cell holding nothing but\n * `&nbsp;` and `<br>` has no content, the same reading `convertElement`\n * gives an empty `<p>`.\n */\nexport function convertInlineRun(\n nodes: AnyNode[],\n $cell: Cheerio<Element>,\n $: CheerioAPI,\n): { block: Block; entry: ImportReportEntry } | null {\n const text = nodes.map((node) => $(node).text()).join(\"\");\n if (!text.trim()) return null;\n\n const html = nodes.map((node) => $.html(node)).join(\"\");\n\n return {\n block: buildParagraph(html, getStyles($cell)),\n entry: {\n sourceTag: tagOf($cell[0]),\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\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 // Kept separate from the `?? 600` fallback: a percentage radius has to\n // resolve against a width the template actually stated.\n const readWidth =\n parsePxValue(widthAttr) || parsePxValue(widthStyle) || undefined;\n const width = readWidth ?? 600;\n // No default, unlike width: an absent height is what keeps the aspect ratio,\n // and `height=\"auto\"` / `height: auto` parse to 0, which reads the same way.\n const height =\n parsePxValue($el.attr(\"height\")) ||\n parsePxValue(styles.height) ||\n undefined;\n\n return createImageBlock({\n src,\n alt,\n width,\n height,\n borderRadius: parseImageBorderRadius(\n styles[\"border-radius\"],\n readWidth,\n height,\n ),\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\n/**\n * Decides whether an `<a>` belongs to the run of prose around it rather than\n * to a block of its own: a link the source did not style as a button, whose\n * own text is what the reader sees.\n *\n * A link inside a sentence is prose, so folding it keeps the sentence in one\n * editable block — and keeps the anchor's markup, `href` included, which the\n * per-element path drops (`convertParagraph` reads inner HTML, so the element\n * itself never reaches the block).\n *\n * Two constraints, both hazards a relaxed version would reintroduce:\n *\n * - `looksLikeButton` is the same predicate `convertElement` and\n * `isButtonCell` use to tell a call to action from a link, so a styled\n * anchor is never absorbed into a sentence and keeps becoming a button.\n * - The anchor must carry text. `convertInlineRun` reads a run with no text\n * as empty and emits nothing, so an anchor whose content is an image has to\n * keep the block it already gets; folding it would delete the image.\n */\nexport function isProseAnchor($el: Cheerio<Element>): boolean {\n if (looksLikeButton(getStyles($el))) return false;\n return ($el.text() ?? \"\").trim() !== \"\";\n}\n\n/**\n * Walks an element's child *nodes*, grouping consecutive inline content into\n * runs and handing every other element to the caller.\n *\n * A bare text node between two elements is content, and a walk over\n * `children()` never visits it — so `Hello<br>World` loses both words while\n * emitting a block holding nothing but `<br>`. Consecutive inline nodes\n * therefore accumulate into one run and become a single paragraph, which is\n * what makes a bare line agree with the same line wrapped in a `<p>`.\n *\n * One walker for all three content traversals — the body walk, the layout\n * container walk and the cell walk — because a bare text node is content\n * wherever it sits and the three have to read it identically. A second copy\n * of this classification is the hazard: fixing it on one surface leaves the\n * others silently dropping copy the source email displays, and nothing fails\n * to say so. What differs between the three is only what a *block-level*\n * element becomes, which is why that half is the caller's.\n *\n * `$host` is what a run reads its styling and source tag from: a bare run has\n * no element of its own, and the nearest enclosing element is the one carrying\n * the colour, size and alignment it renders with.\n *\n * A run never reaches across `$host`'s own children into a descendant's:\n * `onElement` is called with the run already flushed, so a caller that\n * recurses keeps the text before a block-level child ahead of it.\n */\nexport function walkContentNodes(\n $host: Cheerio<Element>,\n $: CheerioAPI,\n onRun: (converted: { block: Block; entry: ImportReportEntry }) => void,\n onElement: ($child: Cheerio<Element>, tag: string) => void,\n): void {\n let inlineRun: AnyNode[] = [];\n\n const flushInlineRun = () => {\n if (inlineRun.length === 0) return;\n const run = inlineRun;\n inlineRun = [];\n const converted = convertInlineRun(run, $host, $);\n if (converted) onRun(converted);\n };\n\n for (const node of $host.contents().toArray()) {\n if (isInlineContent(node)) {\n inlineRun.push(node);\n continue;\n }\n // Comments and processing instructions carry no content, and must not end\n // the run either: a merge-tag comment sitting mid-sentence would otherwise\n // split one line into two paragraphs.\n if (!isTag(node)) continue;\n\n const $child = $(node) as unknown as Cheerio<Element>;\n const tag = node.tagName.toLowerCase();\n\n // A link inside a sentence is part of that sentence, so it joins the run\n // rather than ending it: one rich-text block carries the whole line, with\n // the anchor's own markup inside it. The per-element path builds its\n // paragraph from inner HTML, so the `<a>` never reaches the block and the\n // `href` is lost.\n //\n // Asked before the run is flushed and before the caller sees the element,\n // which is what keeps a call to action out of a sentence — a styled anchor\n // is not a prose anchor, so it reaches `onElement` and becomes its button.\n if (tag === \"a\" && isProseAnchor($child)) {\n inlineRun.push(node);\n continue;\n }\n\n flushInlineRun();\n onElement($child, tag);\n }\n\n flushInlineRun();\n}\n\n/**\n * Reads a button's placement from the cell that wraps it. An anchor styled as\n * a button is sized to its own content, so its `text-align` says nothing about\n * where it sits — table-based email puts that on the containing `<td>`, as\n * either a `text-align` style or the legacy `align` attribute. Only the\n * immediate parent is consulted; walking further up would start reporting the\n * alignment of the surrounding layout rather than of the button.\n */\nfunction readButtonAlign($el: Cheerio<Element>): \"left\" | \"center\" | \"right\" {\n const parent = $el.parent();\n if (parent.length === 0) return \"center\";\n\n const fromStyle = parseStyleAttribute(parent.attr(\"style\"))[\"text-align\"];\n if (fromStyle) return parseAlignment(fromStyle, \"center\");\n\n return parseAlignment(parent.attr(\"align\"), \"center\");\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 align: readButtonAlign($el),\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 * 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>` / `<th>` carries nothing a reader would see: no\n * text once source whitespace and `&nbsp;` are collapsed, and no element that\n * renders on its own.\n *\n * Text alone is not the test. An image or a link carries no text and is\n * content all the same, so a cell holding one is never blank — reading it as\n * blank would make a picture-only column disappear.\n *\n * Lives here, with the other predicates both traversal modules consult,\n * because a spacer cell and a row's gutter cells are one fact read for two\n * purposes: `isSpacerCell` adds a stated height to it, and the section\n * builder reads a row's blank cells as chrome rather than as columns. A\n * second copy would let one cell be a spacer in one traversal and a column\n * in the other.\n */\nexport function isBlankCell($el: Cheerio<Element>): boolean {\n if (normalizeCellText($el.text() ?? \"\") !== \"\") return false;\n return $el.find(\"img, a, hr\").length === 0;\n}\n\n/**\n * Decides whether a `<td>` looks like a vertical spacer: blank, and carrying\n * an explicit height.\n */\nexport function isSpacerCell($el: Cheerio<Element>): boolean {\n if (!isBlankCell($el)) 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 * Collapses every run of whitespace — `&nbsp;` included — to one space and\n * trims. Source indentation and nested tags introduce whitespace that never\n * renders, so a text comparison has to normalise both sides.\n */\nfunction normalizeCellText(value: string): string {\n return value.replace(/[\\s\\u00a0]+/g, \" \").trim();\n}\n\n/**\n * Whether the anchor *is* the cell rather than sitting inside its content.\n *\n * The hazard this guards: `buildCellButton` labels the button with the\n * anchor's text and drops every other node in the cell, so classifying a\n * sentence that merely contains a link as a button deletes the sentence. The\n * constraint is that a cell only reads as a button when the link is its\n * entire content — and `find(\"a\")` matches at any depth, so an outer callout\n * cell wrapping a real CTA reaches the same test.\n */\nfunction isWholeCellAnchor(\n $el: Cheerio<Element>,\n $anchor: Cheerio<Element>,\n): boolean {\n return (\n normalizeCellText($el.text() ?? \"\") ===\n normalizeCellText($anchor.text() ?? \"\")\n );\n}\n\n/**\n * Decides whether a `<td>` is a button container — i.e. its entire content is\n * one `<a>`, styled as a button either on the anchor or on the cell.\n *\n * Both arms require the anchor to be the cell's whole content. The anchor's\n * own styling is the stronger signal that a link is *a button*, but it says\n * nothing about whether the link is *the cell*, and `find(\"a\")` matches at\n * any depth — so a callout cell holding a paragraph plus a self-styled CTA\n * satisfies the anchor arm exactly as it does the cell arm.\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 (!isWholeCellAnchor($el, anchor)) return { match: false };\n\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 * A container whose entire meaningful content is one block-level element is\n * unwrapped first, so the element inside is what gets mapped and named in the\n * report — see `resolveWrappedBlock`. Only a heading can come back from that,\n * which is why the heading branch is the only one taking the resolved styles:\n * for every other branch the resolved element is the one handed in, so its own\n * styles are what `styles` already holds.\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 { $el: $target, styles } = resolveWrappedBlock($el, $);\n const tag = tagOf($target[0]);\n if (!tag) return null;\n\n if (HEADING_TAGS.has(tag)) {\n return {\n block: convertHeading($target, styles),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"title\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"img\") {\n return {\n block: convertImage($target),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"a\") {\n if (looksLikeButton(styles)) {\n return {\n block: convertButton($target),\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($target),\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($target),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n }\n\n if (TEXT_TAGS.has(tag)) {\n if (!hasRenderedContent($target)) return null;\n return {\n block: convertParagraph($target),\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 $target,\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","import type { ColumnLayout } from \"@templatical/types\";\n\n/**\n * The share of a section's width each column of a layout occupies.\n *\n * These are the percentages `@templatical/renderer` emits for the same layout\n * (`getWidthPercentages`), and they have to stay equal to them: this table is\n * what a declared ratio is snapped *to*, so a divergence would have the\n * importer choose `2-1` for a row the renderer then renders at some other\n * split — a silent disagreement with no failing surface between the two\n * packages. `@templatical/renderer` is a devDependency of this package and\n * must stay one (the importer has no runtime need of it), so the values are\n * copied rather than imported, and the agreement is asserted by the\n * renderer-agreement test in `column-ratio.test.ts`.\n *\n * Keyed by every `ColumnLayout`, so a new layout has to appear here before it\n * can be snapped to — and the candidate list below is derived from this table\n * rather than from a hand-written map of count to layouts, so adding one is a\n * single edit.\n *\n * Exported for that agreement test alone, which compares it against\n * `getWidthPercentages` value by value. Going through `resolveColumnRatio`\n * instead cannot do the job: a drift smaller than the snap tolerance still\n * snaps to the same layout, so a 50/50 target quietly moved to 55/45 passes\n * every behavioural case.\n */\nexport const LAYOUT_SHARES: Record<ColumnLayout, readonly number[]> = {\n \"1\": [100],\n \"2\": [50, 50],\n \"3\": [33.33, 33.33, 33.34],\n \"1-2\": [33.33, 66.67],\n \"2-1\": [66.67, 33.33],\n};\n\n/**\n * How far, in percentage points on the worst column, a declared ratio may sit\n * from a layout and still be read as that layout.\n *\n * Measured against the corpus rather than chosen for roundness. The widest\n * real match is mailchimp's `width=\"350\"` / `width=\"190\"` sidebar row at\n * 64.8 / 35.2, **1.9pp** from `2-1`; the nearest declared ratio that must\n * *not* match is an 80 / 20 split at **13.3pp** from the same layout. Six sits\n * 3.2x above the first and 2.2x below the second.\n *\n * It also has to stay under 8.33, which is half the 16.67pp gap between the\n * two closest two-column layouts (`2` at 50/50 and `2-1` at 66.67/33.33):\n * above that the snap windows overlap and one ratio matches two layouts.\n */\nconst SNAP_TOLERANCE_PP = 6;\n\n/** A width a column declared, with the unit it declared it in. */\nexport interface ColumnWidth {\n unit: \"px\" | \"%\";\n value: number;\n}\n\n/** The layout a row is imported with, and what the choice lost. */\nexport interface ColumnRatio {\n layout: ColumnLayout;\n note?: string;\n}\n\n/**\n * `mj-column-per-66-67` → 66.67%, `mj-column-per-50` → 50%. Compiled MJML\n * carries a column's share in its class name and nowhere else — every column\n * div also gets `style=\"width:100%\"` — so this signal has to outrank the\n * inline style or every MJML layout reads as an equal split.\n */\nconst MJ_COLUMN_PERCENT = /(?:^|\\s)mj-column-per-(\\d+)(?:-(\\d+))?(?=\\s|$)/;\n\n/** `mj-column-px-350` → 350px, MJML's fixed-width column variant. */\nconst MJ_COLUMN_PIXELS = /(?:^|\\s)mj-column-px-(\\d+)(?=\\s|$)/;\n\nfunction parseColumnClass(className: string | undefined): ColumnWidth | null {\n if (!className) return null;\n\n const percent = className.match(MJ_COLUMN_PERCENT);\n if (percent) {\n const fraction = percent[2] ? `.${percent[2]}` : \"\";\n return usableWidth({\n unit: \"%\",\n value: parseFloat(`${percent[1]}${fraction}`),\n });\n }\n\n const pixels = className.match(MJ_COLUMN_PIXELS);\n if (pixels) return usableWidth({ unit: \"px\", value: parseFloat(pixels[1]) });\n\n return null;\n}\n\n/**\n * A length that states a share of its row, or `null`.\n *\n * A bare number is px, which is what a legacy `width=\"350\"` attribute means.\n * Anything else — `auto`, a calc, an em length — states no share.\n */\nfunction parseWidth(raw: string | undefined): ColumnWidth | null {\n if (raw === undefined) return null;\n const match = raw.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*(px|%)?$/);\n if (!match) return null;\n return usableWidth({\n unit: match[2] === \"%\" ? \"%\" : \"px\",\n value: parseFloat(match[1]),\n });\n}\n\n/**\n * Drops a width that cannot express a share of its row.\n *\n * `100%` is the trap this exists for: every compiled-MJML column div and every\n * Cerberus stack column carries `width:100%` beside its real cap, and a cell\n * claiming the whole row while sharing it with another cell is stating \"fill\n * what is left\" rather than a ratio. Normalising it reads two such columns as\n * an equal split, and one such column beside a 200px one as 33/67 — a ratio\n * the source never declared. A `100px` width is a real one, so the rule is on\n * the percentage unit alone.\n */\nfunction usableWidth(width: ColumnWidth): ColumnWidth | null {\n if (!(width.value > 0)) return null;\n if (width.unit === \"%\" && width.value >= 100) return null;\n return width;\n}\n\n/**\n * The width a column host declares, read from the strongest signal it carries.\n *\n * Priority is by how specifically each signal states a *column's* share: an\n * `mj-column-*` class names it outright; `max-width` is the cap that decides\n * an inline-block column's rendered width and so wins over the `width:100%`\n * sitting beside it; a `width` style and the legacy `width` attribute come\n * last, style before attribute because CSS beats a presentational attribute in\n * every browser.\n *\n * Takes primitives rather than a Cheerio node so the whole ratio decision is\n * testable without a DOM.\n */\nexport function readColumnWidth(\n className: string | undefined,\n styles: Record<string, string>,\n widthAttr: string | undefined,\n): ColumnWidth | null {\n return (\n parseColumnClass(className) ??\n parseWidth(styles[\"max-width\"]) ??\n parseWidth(styles.width) ??\n parseWidth(widthAttr)\n );\n}\n\n/**\n * The declared widths as percentages of their row, or `null` when the row\n * declares no ratio at all.\n *\n * Every column must carry a width, in one unit: a single share of an unknown\n * total states no ratio, and neither does a px width beside a percentage. Both\n * are refused rather than guessed, because the fallback — the layout the\n * column count already gives — is correct, and a guess is not.\n */\nfunction normalizeShares(widths: (ColumnWidth | null)[]): number[] | null {\n if (widths.length === 0) return null;\n const first = widths[0];\n if (!first) return null;\n if (widths.some((width) => !width || width.unit !== first.unit)) return null;\n\n const values = widths.map((width) => (width as ColumnWidth).value);\n const total = values.reduce((sum, value) => sum + value, 0);\n if (!(total > 0)) return null;\n return values.map((value) => (value / total) * 100);\n}\n\n/** The layouts that hold exactly this many columns. */\nfunction candidateLayouts(count: number): ColumnLayout[] {\n return (Object.keys(LAYOUT_SHARES) as ColumnLayout[]).filter(\n (layout) => LAYOUT_SHARES[layout].length === count,\n );\n}\n\n/** How far the worst column of `shares` sits from `layout`, in points. */\nfunction deviation(shares: number[], layout: ColumnLayout): number {\n const target = LAYOUT_SHARES[layout];\n return Math.max(...shares.map((share, i) => Math.abs(share - target[i])));\n}\n\n/** The layout `shares` states, or `null` when none is within tolerance. */\nfunction snapToLayout(shares: number[]): ColumnLayout | null {\n let best: ColumnLayout | null = null;\n let bestDeviation = Number.POSITIVE_INFINITY;\n for (const layout of candidateLayouts(shares.length)) {\n const distance = deviation(shares, layout);\n if (distance < bestDeviation) {\n best = layout;\n bestDeviation = distance;\n }\n }\n return best !== null && bestDeviation <= SNAP_TOLERANCE_PP ? best : null;\n}\n\nfunction formatShare(share: number): string {\n return `${Math.round(share * 10) / 10}%`;\n}\n\n/**\n * What snapping could not express, named as the reader sees it.\n *\n * \"equal columns\" is true because the layouts a count alone produces — `2` and\n * `3` — are both equal splits; `resolveColumnRatio` is the only caller and\n * never reaches here for any other. Naming the layout's own percentages\n * instead would leak the renderer's 33.34 rounding column into a report.\n */\nfunction describeRatioLoss(shares: number[]): string {\n return (\n `Column widths ${shares.map(formatShare).join(\" / \")} have no ` +\n `Templatical equivalent. The section was imported as ${shares.length} ` +\n \"equal columns.\"\n );\n}\n\n/**\n * The layout a row's declared widths choose, given the layout its column count\n * already produced.\n *\n * Widths decide the *ratio* and never the count: `counted` fixes how many\n * columns there are, and the only layouts considered are the ones holding\n * exactly that many. A row declaring nothing, declaring only some of its\n * columns, or mixing units keeps `counted` and reports nothing — there is no\n * observed ratio to have lost. A ratio outside tolerance keeps `counted` too,\n * and names itself in a note, which is the only outcome that is a downgrade.\n *\n * Call it only for a `counted` that has columns to choose between. A merged\n * row (`\"1\"`) has none, and describing the ratio of columns the section no\n * longer has would contradict the merge note that row already carries.\n */\nexport function resolveColumnRatio(\n widths: (ColumnWidth | null)[],\n counted: ColumnLayout,\n): ColumnRatio {\n const shares = normalizeShares(widths);\n if (!shares) return { layout: counted };\n\n const snapped = snapToLayout(shares);\n if (snapped) return { layout: snapped };\n\n return { layout: counted, note: describeRatioLoss(shares) };\n}\n","import type { CheerioAPI, Cheerio } from \"cheerio\";\nimport { isTag } from \"domhandler\";\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 columnDivsOf,\n convertElement,\n convertHtmlFallback,\n isBlankCell,\n isButtonCell,\n isInlineContent,\n isSpacerCell,\n isTableContainer,\n looksLikeButton,\n walkContentNodes,\n} from \"./block-mapper\";\nimport { readColumnWidth, resolveColumnRatio } from \"./column-ratio\";\nimport type { ColumnWidth } from \"./column-ratio\";\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\n/**\n * The one cell a row's content sits in, when every other cell of that row is\n * chrome rather than a column — or `null` when the row is a layout row in its\n * own right.\n *\n * Table-based email centres a fixed-width body by flanking it with blank\n * cells, and Foundation-derived markup pads a row out with a blank `expander`\n * cell. Counting cells reads both as columns: leemunroe's template, the\n * most-copied table email there is, imported as a three-column section with\n * the entire email crushed into the middle third and two empty columns beside\n * it. That is worse than the single column a cell count could never have\n * produced, so it is the one place a row's cell count is not the column count.\n *\n * The signal is content, never width: a cell is chrome when it holds nothing\n * a reader sees (`isBlankCell`), which covers a `&nbsp;` gutter and an empty\n * `expander` alike, and covers a blank cell stating a height — a horizontal\n * gutter's height says nothing about the row.\n *\n * Two constraints, both hazards a relaxed version would reintroduce:\n *\n * - Exactly one cell may carry content. Two filled cells and a blank third\n * is a grid with an empty slot, and collapsing it would re-flow the filled\n * columns from thirds to halves.\n * - A row with content in no cell at all is left alone. Its cells sit side by\n * side, so it states one gap per column rather than a stack of them, and\n * merging them would add their heights and invent vertical space.\n *\n * This rule is coupled to the container descent in `packagingTablesOf`, and\n * neither is complete without the other: the descent reaches Foundation's\n * layout rows, whose blank `expander` cell then reads as a second column\n * holding nothing but a spacer. Removing this rule turns every one of those\n * rows into a phantom two-column section.\n */\nfunction centringCells(cells: Cheerio<Element>[]): Cheerio<Element>[] | null {\n if (cells.length < 2) return null;\n const withContent = cells.filter(($cell) => !isBlankCell($cell));\n return withContent.length === 1 ? withContent : null;\n}\n\n/**\n * Whether the markup below a layout container states columns anywhere: a row\n * with two or more cells carrying content.\n *\n * This gates the container descent below, and only there — the two content\n * walks descend a container unconditionally, which is right for them because\n * they convert its children in place. The packaging descent *promotes* the\n * rows it reaches to sections of their own, so it needs evidence that those\n * rows are layout rather than stacked content.\n *\n * Without the evidence test the descent shatters a section into one section\n * per block, wherever a single cell holds one container per column instead of\n * one cell per column. Measured on compiled MJML — a `div.mj-column-per-*`\n * per column inside one `<td>` — a five-section email imported as thirteen\n * one-block sections, and Cerberus's hybrid template went from 14 sections to\n * 24. Neither loses text; both lose the grouping the source stated, and a\n * cell holding parallel containers has no column count below it to recover in\n * exchange.\n *\n * Content-bearing cells, not cells: a blank-flanked row is one column, which\n * is what `centringCells` reads it as. Counting bare cells here would make\n * this the second answer in the file to \"is this row a set of columns?\".\n */\nfunction declaresColumnsBelow($el: Cheerio<Element>, $: CheerioAPI): boolean {\n let found = false;\n $el.find(\"tr\").each((_, row) => {\n if (found) return;\n const cells = getDirectCells($(row) as unknown as Cheerio<Element>, $);\n if (cells.filter(($cell) => !isBlankCell($cell)).length > 1) found = true;\n });\n return found;\n}\n\n/**\n * The tables that make up a cell's entire meaningful content, or `null` when\n * the cell holds anything else.\n *\n * Anything that is not a table has to leave nothing behind for the cell to\n * count as packaging: whitespace, comments, and inline formatting carrying no\n * text all produce no block, so a cell holding tables and a bare `<br>`\n * qualifies while one holding a heading beside its table does not.\n *\n * A layout container is descended rather than refused, through the same\n * `isTableContainer` predicate the two content walks use — plus the evidence\n * test above, which is what keeps the descent from shattering a section whose\n * columns are sibling containers in one cell. Refusing a container outright\n * made a `<div>` or a `<center>` between the cell and the layout table enough\n * to defeat the descent, and the cell walk then flattened the whole subtree\n * into one column: ZURB Inky's output, which wraps every email in a\n * `<center>`, imported as a single one-column section.\n *\n * The recursion is what keeps the guard below intact through the wrapper: a\n * container holding prose beside its table answers `null`, which propagates,\n * and the row keeps the section that carries that prose.\n *\n * Bounded by DOM depth — a container is descended only when it holds a table,\n * and each step moves to a child.\n */\nfunction packagingTablesOf(\n $cell: Cheerio<Element>,\n $: CheerioAPI,\n): Cheerio<Element>[] | null {\n const tables: Cheerio<Element>[] = [];\n let inlineText = \"\";\n\n for (const node of $cell.contents().toArray()) {\n if (isInlineContent(node)) {\n inlineText += $(node).text();\n continue;\n }\n // Comments and processing instructions carry no content.\n if (!isTag(node)) continue;\n\n const tag = node.tagName.toLowerCase();\n const $child = $(node) as unknown as Cheerio<Element>;\n if (tag === \"table\") {\n tables.push($child);\n continue;\n }\n if (isTableContainer($child, tag) && declaresColumnsBelow($child, $)) {\n const nested = packagingTablesOf($child, $);\n if (nested === null) return null;\n tables.push(...nested);\n continue;\n }\n return null;\n }\n\n if (tables.length === 0) return null;\n if (inlineText.trim() !== \"\") return null;\n return tables;\n}\n\n/**\n * The tables a row is merely packaging for, or `null` when the row is layout\n * in its own right.\n *\n * Table-based email buries the row that states the real column count under\n * one-cell wrapper tables, and a section emitted for a wrapper resolves\n * `columns` from that single cell — reporting one column for a row that has\n * two or three. Descending to the table inside reads the count off the row\n * that actually declares it, which is counting cells rather than inferring a\n * layout from widths or class names.\n *\n * Two conditions keep the descent from losing anything, and both are hazards\n * a future edit would reintroduce by relaxing them:\n *\n * - The cell's meaningful content must *be* the tables. Descending discards\n * the row, so a heading or an image beside the table would be dropped.\n * - The row must carry no background and no padding. The section it emits is\n * the only carrier for those, so descending past a styled row would drop\n * the band it paints.\n */\nfunction packagingRowTables(\n $row: Cheerio<Element>,\n cells: Cheerio<Element>[],\n $: CheerioAPI,\n): Cheerio<Element>[] | null {\n if (cells.length !== 1) return null;\n\n const rowStyles = getStyles($row);\n if (\n parseColor(rowStyles[\"background-color\"]) ||\n parseColor(rowStyles.background)\n )\n return null;\n const padding = readPaddingFromStyles(rowStyles);\n if (padding.top || padding.right || padding.bottom || padding.left)\n return null;\n\n return packagingTablesOf(cells[0], $);\n}\n\n/**\n * The report entry for the section a layout row produces.\n *\n * Whether the row was downgraded is read off the slots that were actually\n * built: one slot per cell means every cell kept its own column, while fewer\n * slots than cells means `resolveColumnLayout` merged them. Deciding it by\n * comparing the cell count against the column ceiling instead would be a\n * second source of truth for that ceiling and would start lying the moment\n * the resolver changed. The ceiling appears only in the note's wording, where\n * it explains the merge to a reader rather than driving the branch.\n *\n * A faithful row gets no `note` at all. Attaching one unconditionally makes\n * \"nothing was lost\" indistinguishable from a downgrade for a caller that\n * filters on `note`, which is the whole reason the field is optional.\n *\n * `cellCount` is the row's column-bearing cells, not every cell it has: a\n * centring row's gutters were never columns, so counting them would report a\n * three-into-one merge for a row that always stated one column.\n */\nfunction sectionEntry(\n cellCount: number,\n slotCount: number,\n ratioNote: string | undefined,\n): ImportReportEntry {\n if (slotCount !== cellCount) {\n return {\n sourceTag: \"tr\",\n templaticalBlockType: \"section\",\n status: \"approximated\",\n note: `Row of ${cellCount} cells was merged into a single column. Templatical sections hold at most 3 columns.`,\n };\n }\n if (ratioNote) {\n return {\n sourceTag: \"tr\",\n templaticalBlockType: \"section\",\n status: \"approximated\",\n note: ratioNote,\n };\n }\n return {\n sourceTag: \"tr\",\n templaticalBlockType: \"section\",\n status: \"converted\",\n };\n}\n\n/**\n * The report entry for a nested row whose section wrapper was dropped, or\n * `null` when dropping it lost nothing.\n *\n * `packages/core/src/editor.ts` forbids a section inside a column because MJML\n * forbids `mj-section` there, so a layout table reached from a cell has to\n * flatten — the columns are gone regardless. One cell has no columns to lose,\n * and reporting that as a downgrade would fill the report with entries for a\n * non-event.\n *\n * The count is the row's column-bearing cells, for the same reason\n * `sectionEntry`'s is: a centring row flattened into a parent column lost\n * nothing, so it must report nothing.\n */\nfunction flattenedRowEntry(cellCount: number): ImportReportEntry | null {\n if (cellCount <= 1) return null;\n return {\n sourceTag: \"tr\",\n templaticalBlockType: null,\n status: \"approximated\",\n note: `Nested row of ${cellCount} cells lost its columns. A Templatical section cannot nest inside a column, so its cells were merged into the surrounding column.`,\n };\n}\n\n/**\n * One column's content host, and which kind of element it is.\n *\n * Normally a row's columns are its `<td>`s. The exception is a single cell\n * holding one inline-block `<div>` per column, which `columnDivsOf` reads —\n * so a host is either a cell or one of those column containers, and the two\n * are extracted differently.\n */\ninterface ColumnHost {\n $el: Cheerio<Element>;\n kind: \"cell\" | \"container\";\n}\n\n/**\n * The elements that each carry one of the row's columns.\n *\n * A cell holding a column set is replaced by that set, so the count comes from\n * the divs rather than from the one cell around them. Only a single layout\n * cell is considered: columns inside a column are not representable, so a\n * multi-cell row keeps its cells and can never have its count *reduced* by\n * this rule.\n *\n * Asked after `centringCells`, which is what lets a gutter-flanked row whose\n * middle cell holds a column set still be read as that set.\n */\nfunction columnHostsOf(\n layoutCells: Cheerio<Element>[],\n $: CheerioAPI,\n): ColumnHost[] {\n if (layoutCells.length === 1) {\n const containers = columnDivsOf(layoutCells[0], $);\n if (containers)\n return containers.map(($el) => ({ $el, kind: \"container\" as const }));\n }\n return layoutCells.map(($el) => ({ $el, kind: \"cell\" as const }));\n}\n\n/**\n * The blocks a column host contributes.\n *\n * A promoted container takes the content walk rather than the cell walk,\n * which is the same walk a container reached from inside a cell already gets\n * — so promoting one changes which slot its blocks land in and nothing about\n * how they convert. The two early returns the cell walk adds are about cells\n * specifically — `isSpacerCell` reads a `<td height>`, and `isButtonCell`\n * reads the cell-level styling table-based email wraps a call to action in —\n * and `looksLikeButton` answers true for `display: inline-block`, which every\n * column container carries. Handing a container to the cell walk would turn a\n * column whose content is one link into a single button block and drop\n * everything the column's own table holds.\n */\nfunction extractHostBlocks(\n host: ColumnHost,\n $: CheerioAPI,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n return host.kind === \"cell\"\n ? extractCellBlocks(host.$el, $, entries, warnings)\n : extractContentBlocks(host.$el, $, entries, warnings);\n}\n\n/** The width an element declares, from the strongest signal it carries. */\nfunction readDeclaredWidth($el: Cheerio<Element>): ColumnWidth | null {\n return readColumnWidth($el.attr(\"class\"), getStyles($el), $el.attr(\"width\"));\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 // A cell whose only content is text has no element children, and the walk\n // below is what reads it: the text node becomes an inline run and then one\n // rich-text block styled from the cell. Handing the `<td>` to\n // `convertElement` instead matches no mapping there and comes back as an\n // html block, so an early return for this case intercepts the good path.\n return extractContentBlocks($cell, $, entries, warnings);\n}\n\n/**\n * The blocks an element's child nodes produce, for an element that holds\n * content rather than being content itself: a table cell, or a layout\n * container descended from one.\n *\n * Node classification — bare text and inline markup grouped into runs, a\n * prose anchor folded into the run around it, comments passed over without\n * splitting one — is `walkContentNodes`' half, shared with the body walk in\n * `converter.ts`. What is left here is the half that differs: inside a cell a\n * nested table flattens into the surrounding column, where at body level it\n * becomes a section of its own.\n */\nfunction extractContentBlocks(\n $host: Cheerio<Element>,\n $: CheerioAPI,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n const blocks: Block[] = [];\n\n walkContentNodes(\n $host,\n $,\n ({ block, entry }) => {\n entries.push(entry);\n blocks.push(block);\n },\n ($child, tag) => {\n if (tag === \"table\") {\n const inner = processTable($child, $, entries, warnings, true);\n blocks.push(...inner);\n return;\n }\n\n // A container contributes no block of its own; the content below it\n // takes its place. `div` is a text tag in the block mapper, so handing a\n // container to `convertElement` emits one paragraph whose content is the\n // entire table subtree as raw markup — the table's blocks never exist.\n //\n // Recursing here rather than passing a flag is what keeps the descent\n // consistent with the cell's own walk: a table found below a container\n // reaches the `table` branch above and flattens, which it must, because\n // Templatical forbids a section inside a column however many wrappers\n // deep the table sits. Bounded by DOM depth — a container is descended\n // only when it holds a table, and each step moves to a child.\n if (isTableContainer($child, tag)) {\n blocks.push(...extractContentBlocks($child, $, entries, warnings));\n return;\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 return;\n }\n\n const r = convertElement($child, $);\n if (r) {\n entries.push(r.entry);\n blocks.push(r.block);\n }\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 // A wrapper row contributes no section of its own; its tables take its\n // place. Bounded by DOM depth: each step descends to a table strictly\n // inside this row. `flattenInline` is carried through unchanged, because\n // Templatical forbids a section inside a column — a wrapper reached from\n // a parent cell must keep flattening.\n const packaging = packagingRowTables($row, cells, $);\n if (packaging) {\n for (const $inner of packaging) {\n sections.push(\n ...processTable($inner, $, entries, warnings, flattenInline),\n );\n }\n continue;\n }\n\n // A row's gutters are not columns, so the cells that state the layout are\n // what the hosts are drawn from, and the hosts are what everything below\n // reads — the column count, the blocks, the declared widths and both\n // report entries. Reading `cells.length` for the report instead would\n // claim a three-into-one merge for a row that always had one column.\n const layoutCells = centringCells(cells) ?? cells;\n const hosts = columnHostsOf(layoutCells, $);\n const countedLayout = resolveColumnLayout(hosts.length, warnings);\n\n let columnsBlocks: Block[][];\n if (countedLayout === \"1\") {\n const merged: Block[] = [];\n for (const host of hosts) {\n merged.push(...extractHostBlocks(host, $, entries, warnings));\n }\n columnsBlocks = [merged];\n } else {\n columnsBlocks = hosts.map((host) =>\n extractHostBlocks(host, $, entries, warnings),\n );\n }\n\n if (flattenInline) {\n const dropped = flattenedRowEntry(hosts.length);\n if (dropped) entries.push(dropped);\n for (const col of columnsBlocks) sections.push(...col);\n continue;\n }\n\n // The count is settled; the declared widths only choose which layout of\n // that count. `\"1\"` is skipped because a single column holds the whole\n // row by definition, so there is no ratio to choose and none to report —\n // and because that is also the layout a merged row lands on, whose own\n // note is the loss worth naming.\n const ratio =\n countedLayout === \"1\"\n ? { layout: countedLayout, note: undefined }\n : resolveColumnRatio(\n hosts.map((host) => readDeclaredWidth(host.$el)),\n countedLayout,\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 entries.push(sectionEntry(hosts.length, columnsBlocks.length, ratio.note));\n sections.push(\n createSectionBlock({\n columns: ratio.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 {\n convertElement,\n isTableContainer,\n walkContentNodes,\n} 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 textColor: \"#1a1a1a\",\n linkUnderline: false,\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, and report the section.\n *\n * The section corresponds to no source element, so the report names `body` and\n * says the section is synthetic — otherwise a caller counting sections against\n * the rows it can see in the source finds one it cannot account for. Nothing is\n * lost on this path: every loose block keeps its order inside the one column,\n * which is why the status is `converted` rather than an approximation.\n */\nfunction wrapInSection(blocks: Block[], entries: ImportReportEntry[]): Block {\n entries.push({\n sourceTag: \"body\",\n templaticalBlockType: \"section\",\n status: \"converted\",\n note: \"Loose top-level content was grouped into a synthetic single-column section.\",\n });\n return createSectionBlock({\n columns: \"1\",\n children: [blocks],\n styles: {\n padding: emptyPadding(),\n },\n });\n}\n\n/**\n * Walk the body's child nodes. Tables become sections; loose content is\n * accumulated and wrapped in a single one-column section.\n *\n * Both walks below go through `walkContentNodes`, the same node\n * classification the cell walk uses, so bare text and inline markup at body\n * level and inside a layout container reach a rich-text block. A walk over\n * `children()` visits neither, which drops copy the source email displays —\n * `Lead<h2>H</h2>Trailing` imported as the heading alone.\n */\nfunction processBody(\n $: CheerioAPI,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n const blocks: Block[] = [];\n const $body = $(\"body\");\n\n let pendingLoose: Block[] = [];\n\n const flushLoose = () => {\n if (pendingLoose.length > 0) {\n blocks.push(wrapInSection(pendingLoose, entries));\n pendingLoose = [];\n }\n };\n\n const collectLoose = ({\n block,\n entry,\n }: {\n block: Block;\n entry: ImportReportEntry;\n }) => {\n entries.push(entry);\n pendingLoose.push(block);\n };\n\n /**\n * Descend a layout container, taking its tables as sections and everything\n * else as loose content. A container holding another container descends\n * again, so a table reaches `processTable` at whatever depth the wrapper\n * markup buries it — MJML nests an outer body div around one div per\n * section around the section's table, and a single-level walk sees only the\n * middle div, which the block mapper turns into one paragraph swallowing\n * the entire table subtree.\n *\n * Bounded by DOM depth: a container is descended only when it holds a\n * table, and each step moves to a child.\n *\n * Declared here so every depth shares `pendingLoose` and `flushLoose`. A\n * per-level accumulator flushes at the wrong point and reorders the\n * document: content sitting before a nested table lands after it.\n */\n const walkContainer = ($container: Cheerio<Element>): void => {\n walkContentNodes($container, $, collectLoose, ($inner, innerTag) => {\n if (innerTag === \"table\") {\n // Flush loose content accumulated BEFORE this table so it keeps its\n // source position, mirroring the top-level walk. Without this, the\n // table is appended immediately while leading siblings are flushed\n // only after the walk — reordering the document.\n flushLoose();\n blocks.push(...processTable($inner, $, entries, warnings, false));\n return;\n }\n\n if (isTableContainer($inner, innerTag)) {\n walkContainer($inner);\n return;\n }\n\n const r = convertElement($inner, $);\n if (r) {\n entries.push(r.entry);\n pendingLoose.push(r.block);\n }\n });\n };\n\n walkContentNodes($body, $, collectLoose, ($child, tag) => {\n if (tag === \"table\") {\n flushLoose();\n blocks.push(...processTable($child, $, entries, warnings, false));\n return;\n }\n\n // Skip hidden preheader divs — already captured in settings.\n const childStyles = parseStyleAttribute($child.attr(\"style\"));\n if ((childStyles.display ?? \"\").toLowerCase() === \"none\") return;\n\n // Containers like a wrapping <div> with table children: descend.\n if (isTableContainer($child, tag)) {\n flushLoose();\n walkContainer($child);\n flushLoose();\n return;\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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"}
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@templatical/import-html",
3
3
  "description": "Convert HTML email templates to Templatical format",
4
- "version": "0.32.0",
4
+ "version": "0.34.0",
5
5
  "bugs": "https://github.com/templatical/sdk/issues",
6
6
  "dependencies": {
7
- "@templatical/types": "0.32.0",
7
+ "@templatical/types": "0.34.0",
8
8
  "cheerio": "^1.2.0",
9
9
  "domhandler": "^6.0.1"
10
10
  },
11
11
  "devDependencies": {
12
+ "@templatical/renderer": "0.34.0",
12
13
  "@types/node": "^25.9.5",
14
+ "mjml": "^5.4.0",
13
15
  "typescript": "^6.0.3",
14
16
  "vitest": "^5.0.0"
15
17
  },