Skip to content

Commit c46da47

Browse files
Copilotsawka
andcommitted
Add CSS color validator utility and tests
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
1 parent 3e82a19 commit c46da47

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
import { validateCssColor } from "./color-validator";
4+
5+
describe("validateCssColor", () => {
6+
beforeEach(() => {
7+
vi.stubGlobal("CSS", {
8+
supports: (_property: string, value: string) => {
9+
return [
10+
"red",
11+
"#aabbcc",
12+
"#aabbccdd",
13+
"rgb(255, 0, 0)",
14+
"rgba(255, 0, 0, 0.5)",
15+
"hsl(120 100% 50%)",
16+
"transparent",
17+
"currentColor",
18+
].includes(value);
19+
},
20+
});
21+
});
22+
23+
afterEach(() => {
24+
vi.unstubAllGlobals();
25+
});
26+
27+
it("returns type for supported CSS color formats", () => {
28+
expect(validateCssColor("red")).toBe("keyword");
29+
expect(validateCssColor("#aabbcc")).toBe("hex");
30+
expect(validateCssColor("#aabbccdd")).toBe("hex8");
31+
expect(validateCssColor("rgb(255, 0, 0)")).toBe("rgb");
32+
expect(validateCssColor("rgba(255, 0, 0, 0.5)")).toBe("rgba");
33+
expect(validateCssColor("hsl(120 100% 50%)")).toBe("hsl");
34+
expect(validateCssColor("transparent")).toBe("transparent");
35+
expect(validateCssColor("currentColor")).toBe("currentcolor");
36+
});
37+
38+
it("throws for invalid CSS colors", () => {
39+
expect(() => validateCssColor(":not-a-color:" as any)).toThrow("Invalid CSS color");
40+
expect(() => validateCssColor("#12")).toThrow("Invalid CSS color");
41+
expect(() => validateCssColor("rgb(255, 0)")).toThrow("Invalid CSS color");
42+
});
43+
});

frontend/util/color-validator.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const HexColorRegex = /^#([\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i;
2+
const FunctionalColorRegex = /^([a-z-]+)\(/i;
3+
const NamedColorRegex = /^[a-z]+$/i;
4+
5+
function isValidCssColor(color: string): boolean {
6+
if (typeof CSS != "undefined" && typeof CSS.supports == "function") {
7+
return CSS.supports("color", color);
8+
}
9+
if (typeof document == "undefined") {
10+
return false;
11+
}
12+
const temp = document.createElement("div");
13+
temp.style.color = "";
14+
temp.style.color = color;
15+
return temp.style.color != "";
16+
}
17+
18+
function getCssColorType(color: string): string {
19+
const normalizedColor = color.toLowerCase();
20+
if (HexColorRegex.test(normalizedColor)) {
21+
if (normalizedColor.length === 4) {
22+
return "hex3";
23+
}
24+
if (normalizedColor.length === 5) {
25+
return "hex4";
26+
}
27+
if (normalizedColor.length === 9) {
28+
return "hex8";
29+
}
30+
return "hex";
31+
}
32+
if (normalizedColor === "transparent") {
33+
return "transparent";
34+
}
35+
if (normalizedColor === "currentcolor") {
36+
return "currentcolor";
37+
}
38+
const functionMatch = normalizedColor.match(FunctionalColorRegex);
39+
if (functionMatch) {
40+
return functionMatch[1];
41+
}
42+
if (NamedColorRegex.test(normalizedColor)) {
43+
return "keyword";
44+
}
45+
return "color";
46+
}
47+
48+
export function validateCssColor(color: string): string {
49+
if (typeof color != "string") {
50+
throw new Error(`Invalid CSS color: ${String(color)}`);
51+
}
52+
const normalizedColor = color.trim();
53+
if (normalizedColor == "" || !isValidCssColor(normalizedColor)) {
54+
throw new Error(`Invalid CSS color: ${color}`);
55+
}
56+
return getCssColorType(normalizedColor);
57+
}

0 commit comments

Comments
 (0)