feat(email-renderer): compile safe semantic styles
This commit is contained in:
1 parent
353cadda60
commit
2428df6e78
3 files changed
+49
-2
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/email-renderer",
|
"name": "@wrnexus/email-renderer",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -30,8 +30,38 @@ const html = (value: unknown) =>
|
|||||||
.replaceAll('"', """)
|
.replaceAll('"', """)
|
||||||
.replaceAll("'", "'");
|
.replaceAll("'", "'");
|
||||||
const attrs = (node: BlockNode) => node.props as Record<string, unknown>;
|
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) =>
|
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({
|
export const emailDocumentBlock = defineBlock({
|
||||||
manifest: {
|
manifest: {
|
||||||
|
|||||||
@@ -73,6 +73,23 @@ describe("email renderer", () => {
|
|||||||
expect((await validateAmpEmail("<html></html>")).valid).toBe(false);
|
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", () => {
|
test("rejects unsafe MJML and imports supported content", () => {
|
||||||
expect(
|
expect(
|
||||||
importMjml("<mjml><mj-body><mj-raw>x</mj-raw></mj-body></mjml>", {
|
importMjml("<mjml><mj-body><mj-raw>x</mj-raw></mj-body></mjml>", {
|
||||||
|
|||||||
Reference in new issue
Block a user