feat(email-renderer): compile safe semantic styles
Quality / quality (ubuntu-latest) (push) Failing after 10m18s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 11:11:02 +05:30
1 parent 353cadda60
commit 2428df6e78
3 files changed
+49 -2

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/email-renderer",
"version": "0.1.2",
"version": "0.1.3",
"type": "module",
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
"exports": {
+31 -1
View File
@@ -30,8 +30,38 @@ const html = (value: unknown) =>
.replaceAll('"', """)
.replaceAll("'", "'");
const attrs = (node: BlockNode) => node.props as Record<string, unknown>;
const styleBase = (node: BlockNode, key: string) => {
const value = (node.styles as Record<string, unknown>)[key];
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>).base
: undefined;
};
const safeCssValue = (value: unknown, pattern: RegExp) => {
const candidate = String(value ?? "").trim();
return candidate.length <= 80 && pattern.test(candidate) ? candidate : "";
};
const semanticStyle = (node: BlockNode) => {
const declarations: string[] = [];
const color = safeCssValue(styleBase(node, "color"), /^(#[0-9a-f]{3,8}|rgba?\([0-9., %]+\))$/i);
const background = safeCssValue(styleBase(node, "backgroundColor"), /^(#[0-9a-f]{3,8}|rgba?\([0-9., %]+\))$/i);
const padding = safeCssValue(styleBase(node, "padding"), /^\d+(?:\.\d+)?(?:px|rem|em|%)?(?:\s+\d+(?:\.\d+)?(?:px|rem|em|%)?){0,3}$/);
const margin = safeCssValue(styleBase(node, "margin"), /^\d+(?:\.\d+)?(?:px|rem|em|%)?(?:\s+\d+(?:\.\d+)?(?:px|rem|em|%)?){0,3}$/);
const fontSize = safeCssValue(styleBase(node, "fontSize"), /^\d+(?:\.\d+)?(?:px|rem|em|%)$/);
const radius = safeCssValue(styleBase(node, "borderRadius"), /^\d+(?:\.\d+)?(?:px|rem|em|%)$/);
const align = safeCssValue(styleBase(node, "textAlign"), /^(left|center|right|start|end)$/);
const weight = safeCssValue(styleBase(node, "fontWeight"), /^(normal|bold|[1-9]00)$/);
if (color) declarations.push(`color:${color}`);
if (background) declarations.push(`background-color:${background}`);
if (padding) declarations.push(`padding:${padding}`);
if (margin) declarations.push(`margin:${margin}`);
if (fontSize) declarations.push(`font-size:${fontSize}`);
if (radius) declarations.push(`border-radius:${radius}`);
if (align) declarations.push(`text-align:${align}`);
if (weight) declarations.push(`font-weight:${weight}`);
return declarations.join(";");
};
const emailCell = (body: string, node: BlockNode) =>
`<tr><td data-block-id="${html(node.id)}" style="padding:0;">${body}</td></tr>`;
`<tr><td data-block-id="${html(node.id)}" style="padding:0;${semanticStyle(node)}">${body}</td></tr>`;
export const emailDocumentBlock = defineBlock({
manifest: {
@@ -73,6 +73,23 @@ describe("email renderer", () => {
expect((await validateAmpEmail("<html></html>")).valid).toBe(false);
});
test("compiles only allowlisted semantic styles", async () => {
const styled = structuredClone(document);
styled.root.slots.content[0]!.slots.content[0]!.styles = {
color: { base: "#123456" },
textAlign: { base: "center" },
padding: { base: "12px 8px" },
backgroundColor: { base: "red;position:fixed" },
};
const rendered = await renderEmail(styled);
expect(rendered.ok).toBe(true);
if (!rendered.ok) return;
expect(rendered.value.parts.html.content).toContain(
"color:#123456;padding:12px 8px;text-align:center",
);
expect(rendered.value.parts.html.content).not.toContain("position:fixed");
});
test("rejects unsafe MJML and imports supported content", () => {
expect(
importMjml("<mjml><mj-body><mj-raw>x</mj-raw></mj-body></mjml>", {