97 lines
2.6 KiB
JavaScript
97 lines
2.6 KiB
JavaScript
export const Fragment = Symbol.for("email-template.fragment");
|
|
const VOID_ELEMENTS = new Set([
|
|
"area",
|
|
"base",
|
|
"br",
|
|
"col",
|
|
"embed",
|
|
"hr",
|
|
"img",
|
|
"input",
|
|
"link",
|
|
"meta",
|
|
"param",
|
|
"source",
|
|
"track",
|
|
"wbr"
|
|
]);
|
|
export function h(type, props, ...children) {
|
|
const normalizedProps = props ?? {};
|
|
if (type === Fragment) {
|
|
return children;
|
|
}
|
|
if (typeof type === "function") {
|
|
return type({
|
|
...normalizedProps,
|
|
children
|
|
});
|
|
}
|
|
return {
|
|
type,
|
|
props: normalizedProps,
|
|
children
|
|
};
|
|
}
|
|
export function renderEmail(node) {
|
|
return minifyHtml(`<!DOCTYPE html>${renderNode(node)}`);
|
|
}
|
|
function renderNode(node) {
|
|
if (node === null || node === undefined || typeof node === "boolean") {
|
|
return "";
|
|
}
|
|
if (Array.isArray(node)) {
|
|
return node.map((child) => renderNode(child)).join("");
|
|
}
|
|
if (typeof node === "string" || typeof node === "number") {
|
|
return escapeHtml(String(node));
|
|
}
|
|
const attributes = renderAttributes(node.props);
|
|
if (VOID_ELEMENTS.has(node.type)) {
|
|
return `<${node.type}${attributes}>`;
|
|
}
|
|
const content = node.children.map((child) => renderNode(child)).join("");
|
|
return `<${node.type}${attributes}>${content}</${node.type}>`;
|
|
}
|
|
function renderAttributes(props) {
|
|
return Object.entries(props)
|
|
.filter(([key, value]) => key !== "children" && value !== null && value !== undefined && value !== false)
|
|
.map(([key, value]) => {
|
|
const attrName = key === "className" ? "class" : key;
|
|
if (value === true) {
|
|
return ` ${attrName}`;
|
|
}
|
|
if (attrName === "style") {
|
|
return ` style="${escapeAttribute(serializeStyle(value))}"`;
|
|
}
|
|
return ` ${attrName}="${escapeAttribute(String(value))}"`;
|
|
})
|
|
.join("");
|
|
}
|
|
function serializeStyle(style) {
|
|
if (typeof style === "string") {
|
|
return style.trim();
|
|
}
|
|
return Object.entries(style)
|
|
.filter(([, value]) => value !== null && value !== undefined)
|
|
.map(([key, value]) => `${toKebabCase(key)}:${String(value)}`)
|
|
.join(";");
|
|
}
|
|
function toKebabCase(value) {
|
|
return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
}
|
|
function escapeHtml(value) {
|
|
return value
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">");
|
|
}
|
|
function escapeAttribute(value) {
|
|
return escapeHtml(value).replaceAll("\"", """);
|
|
}
|
|
function minifyHtml(html) {
|
|
return html
|
|
.replace(/>\s+</g, "><")
|
|
.replace(/\s{2,}/g, " ")
|
|
.trim();
|
|
}
|