@templatical/import-html 0.42.1 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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(\n $el: Cheerio<Element>,\n link?: { url: string; openInNewTab: boolean },\n): 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 const href = (link?.url ?? \"\").trim();\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 ...(href ? { linkUrl: href } : {}),\n ...(href && link?.openInNewTab ? { linkOpenInNewTab: true } : {}),\n });\n}\n\n/**\n * Whether an `<a>` wraps an image. An anchor wrapping only an image carries\n * no text, so a button built from it is labelled by the factory default and\n * discards the image entirely. Asked before both button paths — `convertElement`\n * and `isButtonCell` — and before a mixed image-plus-text anchor can fold\n * into a prose run.\n */\nexport function isImageAnchor($el: Cheerio<Element>): boolean {\n return $el.find(\"img\").length > 0;\n}\n\nfunction isImageOnlyAnchor($el: Cheerio<Element>): boolean {\n return isImageAnchor($el) && ($el.text() ?? \"\").trim() === \"\";\n}\n\nfunction convertLinkedImage(\n $anchor: Cheerio<Element>,\n $img: Cheerio<Element>,\n): { block: Block; entry: ImportReportEntry } {\n return {\n block: convertImage($img, {\n url: $anchor.attr(\"href\") ?? \"\",\n openInNewTab: $anchor.attr(\"target\") === \"_blank\",\n }),\n entry: {\n sourceTag: \"a\",\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * An `<a>` that wraps both an image and text cannot become one block without\n * dropping one of them: `convertElement` returns a single block. The walker\n * emits the image with `linkUrl` and a sibling paragraph that keeps the\n * remaining `<a>`, so neither is lost.\n */\nfunction splitMixedImageAnchor(\n $anchor: Cheerio<Element>,\n $host: Cheerio<Element>,\n $: CheerioAPI,\n): { block: Block; entry: ImportReportEntry }[] {\n const results: { block: Block; entry: ImportReportEntry }[] = [];\n\n for (const img of $anchor.find(\"img\").toArray()) {\n results.push(\n convertLinkedImage($anchor, $(img) as unknown as Cheerio<Element>),\n );\n }\n\n const $clone = $anchor.clone();\n $clone.find(\"img\").remove();\n if (($clone.text() ?? \"\").trim() === \"\") return results;\n\n results.push({\n block: buildParagraph($.html($clone) ?? \"\", getStyles($host)),\n entry: {\n sourceTag: tagOf($host[0]),\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n });\n return results;\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 * Three constraints, each a hazard 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 * - The anchor must not wrap an image. An image-plus-text `<a>` folded into\n * the run would keep the image as raw markup; it is a first-class image\n * with `linkUrl` and a sibling paragraph instead.\n */\nexport function isProseAnchor($el: Cheerio<Element>): boolean {\n if (looksLikeButton(getStyles($el))) return false;\n if (isImageAnchor($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 //\n // An image-plus-text `<a>` is not one block: `convertElement` would drop\n // either the image or the text. Split it here so every walk — cell, body,\n // container — keeps both.\n if (tag === \"a\" && isImageAnchor($child) && ($child.text() ?? \"\").trim()) {\n flushInlineRun();\n for (const converted of splitMixedImageAnchor($child, $host, $)) {\n onRun(converted);\n }\n continue;\n }\n\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 // An image-only `<a>` is `\"\" === \"\"` on the whole-cell text test, and a\n // button labelled from that empty text is the factory default — the image\n // is discarded. Mixed image-plus-text is the same loss of the image.\n if (isImageAnchor(anchor)) return { match: false };\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 // Image-only is decided before looksLikeButton: an image-only anchor\n // carries no text, so a button built from it is labelled by the factory\n // default and discards the image entirely.\n if (isImageOnlyAnchor($target)) {\n return convertLinkedImage(\n $target,\n $($target.find(\"img\")[0]) as unknown as Cheerio<Element>,\n );\n }\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 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 column means every column kept its own slot, while\n * fewer slots than columns means `resolveColumnLayout` merged them. Deciding\n * it by comparing the column count against the column ceiling instead would\n * be a second source of truth for that ceiling and would start lying the\n * moment the resolver changed. The ceiling appears only in the note's\n * wording, where it explains the merge to a reader rather than driving the\n * 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 * `columnCount` is the row's column hosts — layout cells, or the sibling\n * column `<div>`s a single cell holds — not every cell the row 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 columnCount: number,\n slotCount: number,\n ratioNote: string | undefined,\n): ImportReportEntry {\n if (slotCount !== columnCount) {\n return {\n sourceTag: \"tr\",\n templaticalBlockType: \"section\",\n status: \"approximated\",\n note: `Row of ${columnCount} columns 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 hosts, for the same reason `sectionEntry`'s\n * is: a centring row flattened into a parent column lost nothing, so it must\n * report nothing.\n */\nfunction flattenedRowEntry(columnCount: number): ImportReportEntry | null {\n if (columnCount <= 1) return null;\n return {\n sourceTag: \"tr\",\n templaticalBlockType: null,\n status: \"approximated\",\n note: `Nested row of ${columnCount} columns lost its columns. A Templatical section cannot nest inside a column, so its columns 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 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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"}
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 * Normalizes a legacy `bgcolor` attribute value the way `parseColor` does a\n * CSS one, also accepting hex without its `#` (`bgcolor=\"f4f4f4\"`), which\n * browsers render and CSS rejects.\n */\nexport function parseLegacyColor(value: string | undefined): string {\n const color = parseColor(value);\n if (color) return color;\n const bare = (value ?? \"\").trim();\n return /^(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(bare)\n ? parseColor(`#${bare}`)\n : \"\";\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 * Whether the document underlines its links, from a `<style>` rule whose\n * selector is a bare `a`, or `undefined` when no such rule sets\n * `text-decoration`.\n *\n * Only a rule that reaches every link states the document's default: a\n * pseudo-class, a scoped selector and an `@media` rule each cover some links\n * some of the time. A later rule wins, as in the cascade. Read it before\n * `resolveCssStyles`, which removes the `<style>` tags.\n */\nexport function readLinkUnderline($: CheerioAPI): boolean | undefined {\n let underline: boolean | undefined;\n $(\"style\").each((_, el) => {\n for (const rule of parseStyleSheet($(el).text())) {\n if (!rule.selectors.some((selector) => selector.toLowerCase() === \"a\"))\n continue;\n const decoration =\n rule.declarations[\"text-decoration\"] ??\n rule.declarations[\"text-decoration-line\"];\n if (decoration === undefined) continue;\n underline = /\\bunderline\\b/i.test(decoration);\n }\n });\n return underline;\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 {\n Block,\n DividerBlock,\n HeadingLevel,\n SpacingValue,\n} 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 * Whether a `style` attribute states an alignment of its own. `inherit` does\n * not: it takes the container's.\n */\nfunction statesOwnAlignment(style: string): boolean {\n const own = parseStyleAttribute(style)[\"text-align\"];\n return own !== undefined && own.trim().toLowerCase() !== \"inherit\";\n}\n\n/**\n * Apply a container-level `text-align` to every `<p>` opening tag in `html`\n * that states no alignment of its own, merging into an existing `style=\"…\"`\n * attribute when present. Any other attribute on the `<p>` (class/id/dir/…)\n * is kept, and does not stop the alignment from applying.\n *\n * A `<p>`'s own `text-align` wins, as it does in a browser, where the\n * container's value only reaches a paragraph by inheritance.\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 if (statesOwnAlignment(styleMatch[1])) return match;\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 *\n * `padding` defaults to that element's own. A caller building from a host's\n * styles passes none: a host insets every block it holds, and the cell walk\n * applies that inset once, to all of them.\n */\nfunction buildParagraph(\n innerHtml: string,\n styles: Record<string, string>,\n padding: SpacingValue = readPaddingFromStyles(styles),\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,\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. Its padding stays out: the cell walk insets every block the cell\n * holds by it, so reading it here too would count it twice.\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), emptyPadding()),\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(\n $el: Cheerio<Element>,\n link?: { url: string; openInNewTab: boolean },\n): 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 const href = (link?.url ?? \"\").trim();\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 ...(href ? { linkUrl: href } : {}),\n ...(href && link?.openInNewTab ? { linkOpenInNewTab: true } : {}),\n });\n}\n\n/**\n * Whether an `<a>` wraps an image. An anchor wrapping only an image carries\n * no text, so a button built from it is labelled by the factory default and\n * discards the image entirely. Asked before both button paths — `convertElement`\n * and `isButtonCell` — and before a mixed image-plus-text anchor can fold\n * into a prose run.\n */\nexport function isImageAnchor($el: Cheerio<Element>): boolean {\n return $el.find(\"img\").length > 0;\n}\n\nfunction isImageOnlyAnchor($el: Cheerio<Element>): boolean {\n return isImageAnchor($el) && ($el.text() ?? \"\").trim() === \"\";\n}\n\nfunction convertLinkedImage(\n $anchor: Cheerio<Element>,\n $img: Cheerio<Element>,\n): { block: Block; entry: ImportReportEntry } {\n return {\n block: convertImage($img, {\n url: $anchor.attr(\"href\") ?? \"\",\n openInNewTab: $anchor.attr(\"target\") === \"_blank\",\n }),\n entry: {\n sourceTag: \"a\",\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * An `<a>` that wraps both an image and text cannot become one block without\n * dropping one of them: `convertElement` returns a single block. The walker\n * emits the image with `linkUrl` and a sibling paragraph that keeps the\n * remaining `<a>`, so neither is lost.\n */\nfunction splitMixedImageAnchor(\n $anchor: Cheerio<Element>,\n $host: Cheerio<Element>,\n $: CheerioAPI,\n): { block: Block; entry: ImportReportEntry }[] {\n const results: { block: Block; entry: ImportReportEntry }[] = [];\n\n for (const img of $anchor.find(\"img\").toArray()) {\n results.push(\n convertLinkedImage($anchor, $(img) as unknown as Cheerio<Element>),\n );\n }\n\n const $clone = $anchor.clone();\n $clone.find(\"img\").remove();\n if (($clone.text() ?? \"\").trim() === \"\") return results;\n\n results.push({\n block: buildParagraph(\n $.html($clone) ?? \"\",\n getStyles($host),\n emptyPadding(),\n ),\n entry: {\n sourceTag: tagOf($host[0]),\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n });\n return results;\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 * Three constraints, each a hazard 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 * - The anchor must not wrap an image. An image-plus-text `<a>` folded into\n * the run would keep the image as raw markup; it is a first-class image\n * with `linkUrl` and a sibling paragraph instead.\n */\nexport function isProseAnchor($el: Cheerio<Element>): boolean {\n if (looksLikeButton(getStyles($el))) return false;\n if (isImageAnchor($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 //\n // An image-plus-text `<a>` is not one block: `convertElement` would drop\n // either the image or the text. Split it here so every walk — cell, body,\n // container — keeps both.\n if (tag === \"a\" && isImageAnchor($child) && ($child.text() ?? \"\").trim()) {\n flushInlineRun();\n for (const converted of splitMixedImageAnchor($child, $host, $)) {\n onRun(converted);\n }\n continue;\n }\n\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/** A width as a number and an optional unit; a unitless value is px. */\nconst DIVIDER_WIDTH = /^(-?\\d+(?:\\.\\d+)?)\\s*(%|px)?$/i;\n\n/**\n * The width an `<hr>` spans, from its `width` style, then its `width`\n * attribute, with a note in `notes` for whatever the result does not keep.\n *\n * No width, `auto` and `100%` are the whole column, which is how a\n * block-level `<hr>` renders. Any other percentage stays a share of the\n * column, clamped to 0–100% and rounded to two decimals, which keeps it inside\n * `DividerPercentWidth`'s pattern. A px width stays px until it reaches\n * `room` less the divider's own side padding, which is the width `mj-divider`\n * draws 100% across; past that it is the whole column, and it keeps narrowing\n * with the column on a phone. An unknown `room` keeps every px width.\n */\nfunction readDividerWidth(\n $el: Cheerio<Element>,\n room: number | undefined,\n notes: string[],\n): DividerBlock[\"width\"] {\n const styles = getStyles($el);\n const raw = (styles.width ?? $el.attr(\"width\") ?? \"\").trim();\n if (raw === \"\" || raw.toLowerCase() === \"auto\") return \"full\";\n\n const match = raw.match(DIVIDER_WIDTH);\n if (!match) {\n notes.push(\n `Divider width \"${raw}\" could not be read; imported as full width.`,\n );\n return \"full\";\n }\n\n const value = parseFloat(match[1]);\n if (match[2] === \"%\") {\n const percent = Math.min(100, Math.max(0, value));\n if (percent !== value) {\n notes.push(`Divider width ${raw} was clamped to ${percent}%.`);\n }\n const share = Math.round(percent * 100) / 100;\n return share === 100 ? \"full\" : `${share}%`;\n }\n\n const px = Math.max(0, Math.round(value));\n if (value < 0) notes.push(`Divider width ${raw} was clamped to 0px.`);\n if (room === undefined) return px;\n\n const own = readPaddingFromStyles(styles);\n return px >= room - own.left - own.right ? \"full\" : px;\n}\n\n/**\n * Where an `<hr>` sits in its column. Each side margin starts at the\n * browser's `auto`, the legacy `align` attribute sets them next, and CSS\n * margins override both, side by side. Auto on both sides centres the line;\n * auto on the left alone pushes it right; anything else leaves it at the\n * start of the column.\n */\nfunction dividerAlign($el: Cheerio<Element>): \"left\" | \"center\" | \"right\" {\n let left = \"auto\";\n let right = \"auto\";\n\n const align = ($el.attr(\"align\") ?? \"\").trim().toLowerCase();\n if (align === \"left\") left = \"0\";\n if (align === \"right\") right = \"0\";\n\n const styles = getStyles($el);\n const margin = (styles.margin ?? \"\").trim().split(/\\s+/).filter(Boolean);\n if (margin.length > 0) {\n right = margin[1] ?? margin[0];\n left = margin.length === 4 ? margin[3] : right;\n }\n left = styles[\"margin-left\"] ?? left;\n right = styles[\"margin-right\"] ?? right;\n\n const isAuto = (value: string) => value.trim().toLowerCase() === \"auto\";\n if (isAuto(left) && isAuto(right)) return \"center\";\n return isAuto(left) ? \"right\" : \"left\";\n}\n\n/**\n * <hr> → Divider block, with a note for each thing the block does not keep.\n *\n * Templatical centres every divider, so a partial one the source places at\n * either side is reported. A full-width line has no placement to lose.\n */\nfunction convertDivider(\n $el: Cheerio<Element>,\n room: number | undefined,\n): { block: Block; notes: string[] } {\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 const notes: string[] = [];\n const width = readDividerWidth($el, room, notes);\n const align = dividerAlign($el);\n if (width !== \"full\" && align !== \"center\") {\n notes.push(\n `The source aligns this divider ${align}; Templatical centres every divider.`,\n );\n }\n\n return {\n block: createDividerBlock({\n lineStyle: lineStyle as \"solid\" | \"dashed\" | \"dotted\",\n color: border.color || \"#e5e7eb\",\n thickness: border.width || 1,\n width,\n styles: {\n padding: readPaddingFromStyles(styles),\n },\n }),\n notes,\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 // An image-only `<a>` is `\"\" === \"\"` on the whole-cell text test, and a\n // button labelled from that empty text is the factory default — the image\n // is discarded. Mixed image-plus-text is the same loss of the image.\n if (isImageAnchor(anchor)) return { match: false };\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 * `room` is the width, in px, that a line in the element's column can span\n * before the element's own padding, when the caller knows it. Only a divider\n * reads it.\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 room?: number,\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 // Image-only is decided before looksLikeButton: an image-only anchor\n // carries no text, so a button built from it is labelled by the factory\n // default and discards the image entirely.\n if (isImageOnlyAnchor($target)) {\n return convertLinkedImage(\n $target,\n $($target.find(\"img\")[0]) as unknown as Cheerio<Element>,\n );\n }\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 const { block, notes } = convertDivider($target, room);\n return {\n block,\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"divider\",\n status: notes.length > 0 ? \"approximated\" : \"converted\",\n ...(notes.length > 0 ? { note: notes.join(\" \") } : {}),\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, SpacingValue } from \"@templatical/types\";\nimport {\n columnDivsOf,\n convertElement,\n convertHtmlFallback,\n isBlankCell,\n isButtonCell,\n isInlineContent,\n isSpacerCell,\n isTableContainer,\n walkContentNodes,\n} from \"./block-mapper\";\nimport {\n LAYOUT_SHARES,\n readColumnWidth,\n resolveColumnRatio,\n} from \"./column-ratio\";\nimport type { ColumnWidth } from \"./column-ratio\";\nimport {\n parseColor,\n parseLegacyColor,\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\n/**\n * The padding an element insets its content by.\n *\n * A table cell falls back to its table's `cellpadding`, side by side: the\n * attribute pads every cell of the table, and a side the cell's own CSS\n * declares overrides it, as in a browser.\n */\nfunction readHostPadding($el: Cheerio<Element>): SpacingValue {\n const styles = getStyles($el);\n const own = readPaddingFromStyles(styles);\n if (!$el.is(\"td, th\")) return own;\n\n const cellpadding = parsePxValue($el.closest(\"table\").attr(\"cellpadding\"));\n if (!(cellpadding > 0)) return own;\n\n const declares = (side: keyof SpacingValue) =>\n styles.padding !== undefined || styles[`padding-${side}`] !== undefined;\n return {\n top: declares(\"top\") ? own.top : cellpadding,\n right: declares(\"right\") ? own.right : cellpadding,\n bottom: declares(\"bottom\") ? own.bottom : cellpadding,\n left: declares(\"left\") ? own.left : cellpadding,\n };\n}\n\n/**\n * The colour an element paints behind its content: its CSS background, then\n * its legacy `bgcolor` attribute, which CSS overrides.\n */\nfunction fillOf($el: Cheerio<Element>): string {\n const styles = getStyles($el);\n return (\n parseColor(styles[\"background-color\"]) ||\n parseColor(styles.background) ||\n parseLegacyColor($el.attr(\"bgcolor\"))\n );\n}\n\n/**\n * Insets columns by the padding of the element that holds them, where that\n * padding places them: the top on each column's first block, the bottom on\n * each column's last, the left on the first column and the right on the\n * last. A single column takes all four sides.\n *\n * Blocks are the carrier because nothing else is: a column has no padding of\n * its own, and a cell's blocks flatten into whichever column the cell lands\n * in.\n */\nfunction insetColumns(columns: Block[][], padding: SpacingValue): void {\n if (!padding.top && !padding.right && !padding.bottom && !padding.left)\n return;\n\n columns.forEach((column, columnIndex) => {\n const left = columnIndex === 0 ? padding.left : 0;\n const right = columnIndex === columns.length - 1 ? padding.right : 0;\n column.forEach((block, blockIndex) => {\n insetBlock(block, {\n top: blockIndex === 0 ? padding.top : 0,\n right,\n bottom: blockIndex === column.length - 1 ? padding.bottom : 0,\n left,\n });\n });\n });\n}\n\n/**\n * `room` less the side padding of something inside it, or `undefined` while\n * the room is unknown.\n */\nfunction narrow(\n room: number | undefined,\n padding: SpacingValue,\n): number | undefined {\n return room === undefined ? undefined : room - padding.left - padding.right;\n}\n\n/**\n * Adds `by` to a block's own padding.\n *\n * A spacer renders at its height and ignores its padding, in the canvas and\n * the export alike, so the top and bottom it is inset by are added to its\n * height instead. Held as padding, the space a spacer at a cell's edge\n * carries would vanish on export.\n */\nfunction insetBlock(block: Block, by: SpacingValue): void {\n if (block.type === \"spacer\") {\n block.height += by.top + by.bottom;\n return;\n }\n const own = block.styles.padding;\n block.styles = {\n ...block.styles,\n padding: {\n top: own.top + by.top,\n right: own.right + by.right,\n bottom: own.bottom + by.bottom,\n left: own.left + by.left,\n },\n };\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 // The cell's `bgcolor` is the button's colour, not the section's, which\n // is why a section's background skips a button cell.\n backgroundColor:\n parseColor(merged[\"background-color\"]) ||\n parseColor(merged.background) ||\n parseLegacyColor($cell.attr(\"bgcolor\")) ||\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's style must set no background and no padding. The section it\n * emits is the carrier for those, so descending past a styled row would\n * drop the band it paints.\n *\n * A fill the descent does pass — the row's `bgcolor`, its cell's, its\n * table's — is not lost: `processTable` hands it to the tables below, whose\n * sections take it when nothing nearer paints them. Section counts depend on\n * this gate, so a fill is carried down rather than made a reason to stop.\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 column means every column kept its own slot, while\n * fewer slots than columns means `resolveColumnLayout` merged them. Deciding\n * it by comparing the column count against the column ceiling instead would\n * be a second source of truth for that ceiling and would start lying the\n * moment the resolver changed. The ceiling appears only in the note's\n * wording, where it explains the merge to a reader rather than driving the\n * 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 * `columnCount` is the row's column hosts — layout cells, or the sibling\n * column `<div>`s a single cell holds — not every cell the row 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 *\n * A background the section could not keep adds its own note after the\n * layout's, so one entry names every loss the row had.\n */\nfunction sectionEntry(\n columnCount: number,\n slotCount: number,\n ratioNote: string | undefined,\n fillNote: string | undefined,\n): ImportReportEntry {\n const notes: string[] = [];\n if (slotCount !== columnCount) {\n notes.push(\n `Row of ${columnCount} columns was merged into a single column. Templatical sections hold at most 3 columns.`,\n );\n } else if (ratioNote) {\n notes.push(ratioNote);\n }\n if (fillNote) notes.push(fillNote);\n\n if (notes.length === 0) {\n return {\n sourceTag: \"tr\",\n templaticalBlockType: \"section\",\n status: \"converted\",\n };\n }\n return {\n sourceTag: \"tr\",\n templaticalBlockType: \"section\",\n status: \"approximated\",\n note: notes.join(\" \"),\n };\n}\n\n/**\n * The background a row's section takes, with a note when a cell renders on\n * a colour the section does not keep.\n *\n * Nearest first: the `<tr>`, then its layout cells, then the tables around\n * it (`tableFill`, which a descended wrapper hands down). The cells count\n * when they share one fill, which covers the row's only cell as well as a\n * row painted alike across its columns. Cells that differ leave the section\n * to the table's fill, and the note names what each cell rendered on.\n *\n * A button cell is left out: its colour is the button's own, which\n * `buildCellButton` carries, and read as the section's it would band the\n * whole row in the button's colour.\n */\nfunction sectionFill(\n $row: Cheerio<Element>,\n layoutCells: Cheerio<Element>[],\n tableFill: string,\n $: CheerioAPI,\n): { color: string; note?: string } {\n const rowFill = fillOf($row);\n const cellFills = layoutCells\n .filter(($cell) => !isButtonCell($cell, $).match)\n .map(fillOf);\n const shared =\n cellFills.length > 0 && cellFills.every((fill) => fill === cellFills[0])\n ? cellFills[0]\n : \"\";\n const color = rowFill || shared || tableFill;\n\n // What each cell renders on: its own fill, or what shows through it.\n const rendered = cellFills.map((fill) => fill || rowFill || tableFill);\n if (rendered.every((fill) => fill === color)) return { color };\n\n const cells = rendered.map((fill) => fill || \"none\").join(\" / \");\n const section = color\n ? `the section background ${color}`\n : \"the section, which has no background\";\n return {\n color,\n note: `Cell backgrounds ${cells} differ from ${section}. A Templatical section has one background colour.`,\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 hosts, for the same reason `sectionEntry`'s\n * is: a centring row flattened into a parent column lost nothing, so it must\n * report nothing.\n */\nfunction flattenedRowEntry(columnCount: number): ImportReportEntry | null {\n if (columnCount <= 1) return null;\n return {\n sourceTag: \"tr\",\n templaticalBlockType: null,\n status: \"approximated\",\n note: `Nested row of ${columnCount} columns lost its columns. A Templatical section cannot nest inside a column, so its columns 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 room: number | undefined,\n): Block[] {\n return host.kind === \"cell\"\n ? extractCellBlocks(host.$el, $, entries, warnings, room)\n : extractContentBlocks(host.$el, $, entries, warnings, room);\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 room: number | undefined,\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, room);\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 *\n * `room` is the width a line has across the host's column, before the host's\n * own padding narrows it for everything inside.\n */\nfunction extractContentBlocks(\n $host: Cheerio<Element>,\n $: CheerioAPI,\n entries: ImportReportEntry[],\n warnings: string[],\n room: number | undefined,\n): Block[] {\n const blocks: Block[] = [];\n const padding = readHostPadding($host);\n const inner = narrow(room, padding);\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 blocks.push(\n ...processTable($child, $, entries, warnings, true, \"\", inner),\n );\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(\n ...extractContentBlocks($child, $, entries, warnings, inner),\n );\n return;\n }\n\n const r = convertElement($child, $, inner);\n if (r) {\n entries.push(r.entry);\n blocks.push(r.block);\n }\n },\n );\n\n // The host's padding insets everything it holds, bare text and blocks\n // alike. Nested hosts have inset their own blocks already, so the padding\n // of every cell and container around a block adds up.\n insetColumns([blocks], padding);\n return blocks;\n}\n\n/**\n * The room each of a row's hosts has, from the room across the row.\n *\n * `layout` is the one the hosts land in, `\"1\"` when they stack, and each\n * host takes its column's share of the row. A column set's cell pads the\n * row's edges, so its left side narrows the first column and its right the\n * last; stacked, every host sits between both.\n */\nfunction hostRooms(\n rowRoom: number | undefined,\n layout: ColumnLayout,\n hostCount: number,\n setPadding: SpacingValue | undefined,\n): (number | undefined)[] {\n const shares = LAYOUT_SHARES[layout];\n const edges = setPadding ?? emptyPadding();\n return Array.from({ length: hostCount }, (_, index) => {\n if (rowRoom === undefined) return undefined;\n if (layout === \"1\") return rowRoom - edges.left - edges.right;\n const share = (rowRoom * shares[index]) / 100;\n const left = index === 0 ? edges.left : 0;\n const right = index === hostCount - 1 ? edges.right : 0;\n return share - left - right;\n });\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 * @param enclosingFill - The fill of the wrapper rows, cells and tables this\n * table was reached through, which its sections take when nothing nearer\n * paints them.\n * @param room - The width, in px, a line can span where the table's blocks\n * land: the template body's width for a table whose rows become sections,\n * the surrounding column's for a flattened one. Unknown, a divider keeps a\n * px width as stated.\n */\nexport function processTable(\n $table: Cheerio<Element>,\n $: CheerioAPI,\n entries: ImportReportEntry[],\n warnings: string[],\n flattenInline = false,\n enclosingFill = \"\",\n room?: number,\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 tableFill = fillOf($table) || enclosingFill;\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 //\n // The wrapper's fill goes down with the descent, so the sections below\n // take it when nothing nearer paints them.\n const packaging = packagingRowTables($row, cells, $);\n if (packaging) {\n const carried = fillOf($row) || fillOf(cells[0]) || tableFill;\n // Flattened, the descended blocks land in the parent column, so the\n // wrapper cell's padding insets them as it would have had the walk\n // stopped at the cell, and narrows their room with it.\n const wrapperPadding = readHostPadding(cells[0]);\n const innerRoom = flattenInline ? narrow(room, wrapperPadding) : room;\n const descended: Block[] = [];\n for (const $inner of packaging) {\n descended.push(\n ...processTable(\n $inner,\n $,\n entries,\n warnings,\n flattenInline,\n carried,\n innerRoom,\n ),\n );\n }\n if (flattenInline) insetColumns([descended], wrapperPadding);\n sections.push(...descended);\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 // 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. A flattened row has no section to lay\n // out.\n const ratio =\n countedLayout === \"1\" || flattenInline\n ? { layout: countedLayout, note: undefined }\n : resolveColumnRatio(\n hosts.map((host) => readDeclaredWidth(host.$el)),\n countedLayout,\n );\n\n const padding = readPaddingFromStyles(getStyles($row));\n // A column set's cell pads the row around its columns, and none of the\n // hosts is that cell, so this row applies its padding: to the hosts'\n // room here and to their blocks below. Merged or flattened, the columns\n // stack into one.\n const setPadding =\n hosts[0]?.kind === \"container\"\n ? readHostPadding(layoutCells[0])\n : undefined;\n const stacked = countedLayout === \"1\" || flattenInline;\n const rooms = hostRooms(\n flattenInline ? room : narrow(room, padding),\n stacked ? \"1\" : ratio.layout,\n hosts.length,\n setPadding,\n );\n\n let columnsBlocks: Block[][];\n if (countedLayout === \"1\") {\n const merged: Block[] = [];\n hosts.forEach((host, index) => {\n merged.push(\n ...extractHostBlocks(host, $, entries, warnings, rooms[index]),\n );\n });\n columnsBlocks = [merged];\n } else {\n columnsBlocks = hosts.map((host, index) =>\n extractHostBlocks(host, $, entries, warnings, rooms[index]),\n );\n }\n\n if (setPadding) {\n insetColumns(\n flattenInline ? [columnsBlocks.flat()] : columnsBlocks,\n setPadding,\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 const fill = sectionFill($row, layoutCells, tableFill, $);\n\n entries.push(\n sectionEntry(hosts.length, columnsBlocks.length, ratio.note, fill.note),\n );\n sections.push(\n createSectionBlock({\n columns: ratio.layout,\n children: columnsBlocks,\n styles: {\n padding,\n ...(fill.color ? { backgroundColor: fill.color } : {}),\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 { readLinkUnderline, 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\n/**\n * `linkUnderline` is what the source's stylesheet states for every link, as\n * `readLinkUnderline` reads it. A source stating nothing underlines its links,\n * which is the browser's default.\n */\nfunction extractSettings(\n $: CheerioAPI,\n linkUnderline: boolean | undefined,\n): 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: linkUnderline ?? true,\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 *\n * `bodyWidth` is the room a line has across a top-level section, which is\n * what a divider's px width is measured against.\n */\nfunction processBody(\n $: CheerioAPI,\n entries: ImportReportEntry[],\n warnings: string[],\n bodyWidth: number,\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(\n ...processTable($inner, $, entries, warnings, false, \"\", bodyWidth),\n );\n return;\n }\n\n if (isTableContainer($inner, innerTag)) {\n walkContainer($inner);\n return;\n }\n\n const r = convertElement($inner, $, bodyWidth);\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(\n ...processTable($child, $, entries, warnings, false, \"\", bodyWidth),\n );\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, $, bodyWidth);\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 const linkUnderline = readLinkUnderline($);\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 settings = extractSettings($, linkUnderline);\n const blocks = processBody($, entries, warnings, settings.width);\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,\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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"}