Share base style syntax between rendering and editor validation
Quality / quality (ubuntu-latest) (push) Failing after 1s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 19:06:09 +05:30
1 parent 16d804e16f
commit 14669bb4f3
4 files changed
+45 -10

No files matched your search

+3 -2
View File
@@ -1,12 +1,13 @@
{
"name": "@wrnexus/email-renderer",
"version": "0.1.41",
"version": "0.1.42",
"type": "module",
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
"exports": {
".": "./src/index.ts",
"./blocks": "./src/blocks.ts",
"./create": "./src/create.ts"
"./create": "./src/create.ts",
"./style-values": "./src/style-values.ts"
},
"files": [
"src",
+9 -8
View File
@@ -12,6 +12,7 @@ import {
type ValueSchema,
} from "@wrnexus/blocks";
import { brandColor, brandFont, brandWidth } from "./brand.ts";
import { emailStyleValue } from "./style-values.ts";
const styles = record(json, { maxKeys: 50 }) as never;
const label = (key: string, fallback: string) => ({ key, fallback });
@@ -87,14 +88,14 @@ const safeCssValue = (value: unknown, pattern: RegExp) => {
};
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)$/);
const color = emailStyleValue("color", styleBase(node, "color"));
const background = emailStyleValue("backgroundColor", styleBase(node, "backgroundColor"));
const padding = emailStyleValue("padding", styleBase(node, "padding"));
const margin = emailStyleValue("margin", styleBase(node, "margin"));
const fontSize = emailStyleValue("fontSize", styleBase(node, "fontSize"));
const radius = emailStyleValue("borderRadius", styleBase(node, "borderRadius"));
const align = emailStyleValue("textAlign", styleBase(node, "textAlign"));
const weight = emailStyleValue("fontWeight", styleBase(node, "fontWeight"));
if (color) declarations.push(`color:${color}`);
if (background) declarations.push(`background-color:${background}`);
if (padding) declarations.push(`padding:${padding}`);
@@ -0,0 +1,20 @@
const color = /^(#[0-9a-f]{3}|#[0-9a-f]{4}|#[0-9a-f]{6}|#[0-9a-f]{8}|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,
fontSize: /^\d+(?:\.\d+)?(?:px|rem|em|%)$/,
borderRadius: /^(?:0|\d+(?:\.\d+)?(?:px|rem|em|%))$/,
textAlign: /^(left|center|right|start|end)$/,
fontWeight: /^(normal|bold|[1-9]00)$/,
};
/** 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 : "";
}
export function validateEmailStyleValue(key: string, value: string): void {
if (!emailStyleValue(key, value)) throw new Error(`styles.${key}: Unsupported style value. Use an email-safe value with units where required.`);
}
@@ -0,0 +1,13 @@
import { expect, test } from "bun:test";
import { emailStyleValue, validateEmailStyleValue } from "../src/style-values.ts";
test("base style syntax rejects ignored or injected values and accepts supported units", () => {
for (const [key, value] of [["padding","12"],["padding","1px 2px 3px 4px 5px"],["color","#12345"],["fontWeight","heavy"],["fontSize","12px;color:red"],["position","fixed"],["constructor","x"]]) {
expect(emailStyleValue(key!, value)).toBe("");
expect(() => validateEmailStyleValue(key!, value!)).toThrow();
}
for (const [key, value] of [["padding","0 12px"],["margin","1rem 2%"],["color","#1234"],["fontWeight","700"],["fontSize","16px"],["borderRadius","0"],["textAlign","end"]]) {
expect(emailStyleValue(key!, value)).toBe(value!);
expect(() => validateEmailStyleValue(key!, value!)).not.toThrow();
}
});