Skip to content

Commit 9c3cf98

Browse files
Copilotsawka
andauthored
Make frontend CSS color validation Chromium-only (remove DOM style fallback) (#2946)
The new color validator is used exclusively in the Electron/Chromium frontend, so fallback parsing via temporary DOM elements is unnecessary. This update tightens the implementation to rely on the browser-native CSS capability check only. - **Scope** - Keep `validateCssColor(color: string): string` behavior unchanged (returns normalized type for valid colors, throws on invalid). - Remove non-Chromium fallback logic from validation path. - **Implementation** - **`frontend/util/color-validator.ts`** - `isValidCssColor` now exclusively uses: - `CSS.supports("color", color)` - Removed fallback using `document.createElement(...).style.color` assignment/parsing. - **Behavioral contract (unchanged)** - Valid values still return specific type strings (`hex`, `hex8`, `rgb`, `rgba`, `hsl`, `keyword`, etc.). - Invalid values still throw `Error("Invalid CSS color: ...")`. ```ts function isValidCssColor(color: string): boolean { if (typeof CSS == "undefined" || typeof CSS.supports != "function") { return false; } return CSS.supports("color", color); } ``` <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/wavetermdev/waveterm/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> Co-authored-by: sawka <mike@commandline.dev>
1 parent b1d11f7 commit 9c3cf98

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

Taskfile.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2024, Command Line Inc.
1+
# Copyright 2026, Command Line Inc.
22
# SPDX-License-Identifier: Apache-2.0
33

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

0 commit comments

Comments
 (0)