Bound style components and unify button style validation
This commit is contained in:
1 parent
14669bb4f3
commit
1f90e01e6e
4 files changed
+38
-9
No files matched your search
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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("");
|
||||
});
|
||||
Reference in new issue
Block a user