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

@templatical/import-html

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@templatical/import-html - npm Package Compare versions

Comparing version
0.10.0
to
0.10.1
+22
-21
dist/index.d.ts

@@ -1,3 +0,2 @@

import * as _templatical_types from '@templatical/types';
//#region src/types.d.ts
/**

@@ -14,8 +13,8 @@ * Type definitions for the HTML email importer.

interface ImportReportEntry {
/** The source HTML element tag name (e.g. "h1", "img", "table"). */
sourceTag: string;
/** The Templatical block type produced, or null if skipped. */
templaticalBlockType: string | null;
status: ConversionStatus;
note?: string;
/** The source HTML element tag name (e.g. "h1", "img", "table"). */
sourceTag: string;
/** The Templatical block type produced, or null if skipped. */
templaticalBlockType: string | null;
status: ConversionStatus;
note?: string;
}

@@ -26,11 +25,11 @@ /**

interface ImportReport {
entries: ImportReportEntry[];
warnings: string[];
summary: {
total: number;
converted: number;
approximated: number;
htmlFallback: number;
skipped: number;
};
entries: ImportReportEntry[];
warnings: string[];
summary: {
total: number;
converted: number;
approximated: number;
htmlFallback: number;
skipped: number;
};
}

@@ -41,6 +40,7 @@ /**

interface ImportResult {
content: _templatical_types.TemplateContent;
report: ImportReport;
content: import("@templatical/types").TemplateContent;
report: ImportReport;
}
//#endregion
//#region src/converter.d.ts
/**

@@ -70,3 +70,4 @@ * Converts an HTML email template to Templatical TemplateContent.

declare function convertHtmlTemplate(html: string): ImportResult;
//#endregion
export { type ConversionStatus, type ImportReport, type ImportReportEntry, type ImportResult, convertHtmlTemplate };
//# sourceMappingURL=index.d.ts.map

@@ -1,868 +0,960 @@

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

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

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

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

"domhandler": "^6.0.1",
"@templatical/types": "0.10.0"
"@templatical/types": "0.10.1"
},
"devDependencies": {
"tsup": "^8.5.1",
"@types/node": "^25.9.1",
"typescript": "^6.0.3",

@@ -49,3 +49,3 @@ "vitest": "^4.1.7"

"scripts": {
"build": "tsup",
"build": "tsdown",
"test": "vitest run --config vitest.config.ts",

@@ -52,0 +52,0 @@ "typecheck": "tsc --noEmit"