From 1f90e01e6e255da220e19946df9a45ad1bd8d7c6 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sat, 5 Sep 2026 19:08:47 +0530 Subject: [PATCH] Bound style components and unify button style validation --- packages/email-renderer/package.json | 2 +- packages/email-renderer/src/blocks.ts | 12 +++++----- packages/email-renderer/src/style-values.ts | 23 +++++++++++++++++-- .../email-renderer/test/style-values.test.ts | 10 ++++++++ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/email-renderer/package.json b/packages/email-renderer/package.json index 346d71e0..7210be0c 100644 --- a/packages/email-renderer/package.json +++ b/packages/email-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/email-renderer", - "version": "0.1.42", + "version": "0.1.43", "type": "module", "description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.", "exports": { diff --git a/packages/email-renderer/src/blocks.ts b/packages/email-renderer/src/blocks.ts index 40258f9b..83fc242c 100644 --- a/packages/email-renderer/src/blocks.ts +++ b/packages/email-renderer/src/blocks.ts @@ -107,22 +107,22 @@ const semanticStyle = (node: BlockNode) => { return declarations.join(";"); }; const buttonBackground = (node: BlockNode, context: unknown) => { - const override = safeCssValue(styleBase(node, "backgroundColor"), /^(#[0-9a-f]{3,8}|rgba?\([0-9., %]+\))$/i); + const override = emailStyleValue("backgroundColor", styleBase(node, "backgroundColor")); const brand = (context as { brandLinkColor?: string } | undefined)?.brandLinkColor; return override || (brand && brandColor("#2563eb").parse(brand).ok ? brand : "#2563eb"); }; const buttonVisuals = (node: BlockNode) => ({ - color: safeCssValue(styleBase(node, "color"), /^(#[0-9a-f]{3,8}|rgba?\([0-9., %]+\))$/i) || "#ffffff", - fontSize: safeCssValue(styleBase(node, "fontSize"), /^\d+(?:\.\d+)?(?:px|rem|em|%)$/) || "16px", - fontWeight: safeCssValue(styleBase(node, "fontWeight"), /^(normal|bold|[1-9]00)$/) || "700", - radius: safeCssValue(styleBase(node, "borderRadius"), /^\d+(?:\.\d+)?(?:px|rem|em|%)$/) || "6px", + color: emailStyleValue("color", styleBase(node, "color")) || "#ffffff", + fontSize: emailStyleValue("fontSize", styleBase(node, "fontSize")) || "16px", + fontWeight: emailStyleValue("fontWeight", styleBase(node, "fontWeight")) || "700", + radius: emailStyleValue("borderRadius", styleBase(node, "borderRadius")) || "6px", }); const buttonVisualCss = (node: BlockNode) => { const values = buttonVisuals(node); return `color:${values.color};font-size:${values.fontSize};font-weight:${values.fontWeight};border-radius:${values.radius}`; }; const buttonAlignment = (node: BlockNode, context: unknown) => { - const value = safeCssValue(styleBase(node, "textAlign"), /^(left|center|right|start|end)$/) || "start"; + const value = emailStyleValue("textAlign", styleBase(node, "textAlign")) || "start"; const rtl = (context as { direction?: string } | undefined)?.direction === "rtl"; return value === "start" ? (rtl ? "right" : "left") : value === "end" ? (rtl ? "left" : "right") : value; }; diff --git a/packages/email-renderer/src/style-values.ts b/packages/email-renderer/src/style-values.ts index 3de1fc2a..4e6c3a5e 100644 --- a/packages/email-renderer/src/style-values.ts +++ b/packages/email-renderer/src/style-values.ts @@ -1,4 +1,4 @@ -const color = /^(#[0-9a-f]{3}|#[0-9a-f]{4}|#[0-9a-f]{6}|#[0-9a-f]{8}|rgba?\([0-9., %]+\))$/i; +const color = /^(#[0-9a-f]{3}|#[0-9a-f]{4}|#[0-9a-f]{6}|#[0-9a-f]{8}|transparent|rgba?\([0-9., %]+\))$/i; const spacing = /^(?:0|\d+(?:\.\d+)?(?:px|rem|em|%))(?:\s+(?:0|\d+(?:\.\d+)?(?:px|rem|em|%))){0,3}$/; const patterns: Record = { color, backgroundColor: color, padding: spacing, margin: spacing, @@ -8,11 +8,30 @@ const patterns: Record = { fontWeight: /^(normal|bold|[1-9]00)$/, }; +function validColor(value: string): boolean { + if (!/^rgba?\(/i.test(value)) return true; + const match = /^(rgb|rgba)\((.*)\)$/i.exec(value)!; + const channels = match[2]!.split(",").map(part => part.trim()); + if (channels.length !== (match[1]!.toLowerCase() === "rgba" ? 4 : 3)) return false; + const numeric = /^(?:\d+(?:\.\d+)?|\.\d+)%?$/; + if (!channels.every(channel => numeric.test(channel))) return false; + const percentages = channels.slice(0, 3).filter(channel => channel.endsWith("%")).length; + if (percentages !== 0 && percentages !== 3) return false; + return channels.every((channel, index) => { + const maximum = channel.endsWith("%") ? 100 : index === 3 ? 1 : 255; + return Number.parseFloat(channel) <= maximum; + }); +} + /** Accepted base-value syntax shared by the inspector and semantic style compiler. */ export function emailStyleValue(key: string, value: unknown): string { if (typeof value !== "string") return ""; const candidate = value.trim(); - return candidate.length <= 80 && Object.prototype.hasOwnProperty.call(patterns, key) && patterns[key]!.test(candidate) ? candidate : ""; + if (candidate.length > 80 || !Object.prototype.hasOwnProperty.call(patterns, key) || !patterns[key]!.test(candidate)) return ""; + if ((key === "color" || key === "backgroundColor") && !validColor(candidate)) return ""; + if (["padding", "margin", "fontSize", "borderRadius"].includes(key) && + candidate.split(/\s+/).some(part => Number.parseFloat(part) > 4000)) return ""; + return candidate; } export function validateEmailStyleValue(key: string, value: string): void { diff --git a/packages/email-renderer/test/style-values.test.ts b/packages/email-renderer/test/style-values.test.ts index ff9e6623..328f1a69 100644 --- a/packages/email-renderer/test/style-values.test.ts +++ b/packages/email-renderer/test/style-values.test.ts @@ -11,3 +11,13 @@ test("base style syntax rejects ignored or injected values and accepts supported expect(() => validateEmailStyleValue(key!, value!)).not.toThrow(); } }); + +test("RGB functions require correct channel counts, units and bounded components", () => { + for (const value of ["rgb(1)", "rgb(1,2,3,4)", "rgba(1,2,3)", "rgb(256,0,0)", "rgb(100%,0,0)", "rgba(1,2,3,1.1)", "rgb(1..2,2,3)", "rgba(0,0,0,101%)"]) + expect(emailStyleValue("color", value)).toBe(""); + for (const value of ["rgb(255, 0, 12)", "rgb(100%, 0%, 50%)", "rgba(0,0,0,.5)", "rgba(0,0,0,50%)", "transparent"]) + expect(emailStyleValue("backgroundColor", value)).toBe(value); + expect(emailStyleValue("padding", "0 4000px")).toBe("0 4000px"); + expect(emailStyleValue("padding", "0 4001px")).toBe(""); + expect(emailStyleValue("fontSize", "999999999999999999999px")).toBe(""); +});