Bound style components and unify button style validation
Quality / quality (ubuntu-latest) (push) Failing after 25m18s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 19:08:47 +05:30
1 parent 14669bb4f3
commit 1f90e01e6e
4 files changed
+38 -9

No files matched your search

+1 -1
View File
@@ -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": {
+6 -6
View File
@@ -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;
};
+21 -2
View File
@@ -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<string, RegExp> = {
color, backgroundColor: color, padding: spacing, margin: spacing,
@@ -8,11 +8,30 @@ const patterns: Record<string, RegExp> = {
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 {
@@ -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("");
});