Import supported MJML header metadata and typography
Quality / quality (ubuntu-latest) (push) Failing after 1s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 17:10:25 +05:30
1 parent 69db4bd62e
commit f5139f2c82
3 files changed
+68 -5

No files matched your search

+42 -3
View File
@@ -67,7 +67,7 @@ export interface MjmlDiagnostic extends Diagnostic {
export function importMjml(
source: string,
options: { id(): string; subject: string; preheader?: string },
options: { id(): string; subject?: string; preheader?: string },
): Result<BlockDocument, MjmlDiagnostic[]> {
if (new TextEncoder().encode(source).byteLength > DEFAULT_LIMITS.maxBytes)
return {
@@ -111,6 +111,44 @@ export function importMjml(
}
for (const child of node.children ?? []) pending.push({ node: child, depth: depth + 1 });
}
const decode = (value: string) => value.replace(/&(?:#(x[0-9a-f]+|[0-9]+)|(amp|lt|gt|quot|apos));/gi, (original, numeric: string | undefined, name: string | undefined) => {
if (numeric) {
const point = numeric[0]!.toLowerCase() === "x" ? Number.parseInt(numeric.slice(1), 16) : Number(numeric);
return point > 0 && point <= 0x10ffff && !(point >= 0xd800 && point <= 0xdfff) ? String.fromCodePoint(point) : original;
}
return ({ amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" } as Record<string, string>)[name!.toLowerCase()]!;
});
const brand: Record<string, string> = {};
let importedSubject = "", importedPreheader = "";
const attributeError = (node: MjmlAst, attribute?: string) => errors.push({ code: "mjml.header", message: `Unsupported MJML header setting ${node.tagName}${attribute ? `.${attribute}` : ""}.`, severity: "error", element: node.tagName, attribute });
const onlyAttributes = (node: MjmlAst, allowed: string[]) => {
for (const key of Object.keys(node.attributes ?? {})) if (!allowed.includes(key)) attributeError(node, key);
};
for (const head of (ast.children ?? []).filter(node => node.tagName === "mj-head")) {
onlyAttributes(head, []);
for (const node of head.children ?? []) {
if (node.tagName === "mj-title" || node.tagName === "mj-preview") {
onlyAttributes(node, []);
if (node.tagName === "mj-title") importedSubject = decode(node.content ?? "");
else importedPreheader = decode(node.content ?? "");
} else if (node.tagName === "mj-attributes") {
onlyAttributes(node, []);
for (const defaults of node.children ?? []) {
if (defaults.tagName === "mj-all") {
onlyAttributes(defaults, ["font-family"]);
if (defaults.attributes?.["font-family"]) brand.fontFamily = decode(defaults.attributes["font-family"]);
} else if (defaults.tagName === "mj-text") {
onlyAttributes(defaults, ["color"]);
if (defaults.attributes?.color) brand.textColor = decode(defaults.attributes.color);
} else attributeError(defaults);
}
} else if (node.tagName === "mj-breakpoint") {
onlyAttributes(node, ["width"]);
if (node.attributes?.width !== "480px") attributeError(node, "width");
} else attributeError(node);
}
}
const bodyAttributes = (ast.children ?? []).find(node => node.tagName === "mj-body")?.attributes ?? {};
const wrapColumns = (nodes: BlockNode[]): BlockNode[] => {
const output: BlockNode[] = [];
for (let index = 0; index < nodes.length;) {
@@ -139,6 +177,7 @@ export function importMjml(
return output;
};
const convert = (node: MjmlAst): BlockNode[] => {
if (node.tagName === "mj-head") return [];
if (!supported.has(node.tagName)) {
errors.push({
code: "mjml.component",
@@ -243,7 +282,7 @@ export function importMjml(
id: options.id(),
type: "layout.email-document",
version: 1,
props: { width: "600", background: "#f3f4f6" },
props: { width: (bodyAttributes.width ?? "600").replace(/px$/, ""), background: bodyAttributes["background-color"] ?? "#f3f4f6", ...brand },
styles: {},
slots: {
content: content.map((node) =>
@@ -263,7 +302,7 @@ export function importMjml(
const document: BlockDocument = {
schemaVersion: 1,
kind: "email",
meta: { subject: options.subject, preheader: options.preheader ?? "", lang: "en" },
meta: { subject: options.subject ?? (importedSubject || "Imported email"), preheader: options.preheader ?? importedPreheader, lang: "en" },
theme: { tokens: {} },
root,
};