Repository navigation
Expand file tree
/
Copy pathreadableSyntaxTheme.ts
More file actions
76 lines (66 loc) · 2.99 KB
/
Copy pathreadableSyntaxTheme.ts
File metadata and controls
76 lines (66 loc) · 2.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import { getResolvedOrResolveTheme, registerCustomTheme, type DiffsTheme } from "@pierre/diffs";
/** pierre-light with every syntax color darkened until it reads on every light code surface. */
export const READABLE_LIGHT_THEME_NAME = "t3-light";
const MIN_CONTRAST = 4.5;
// The darkest light code surface is a changed word on a deleted line (#f8d3d4, luminance
// 0.71), rounded down for margin.
const DARKEST_SURFACE_LUMINANCE = 0.69;
function channelLuminance(channel: number): number {
const value = channel / 255;
return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
}
function contrastOnLightCode(rgb: readonly [number, number, number]): number {
const [r, g, b] = rgb.map(channelLuminance) as [number, number, number];
return (DARKEST_SURFACE_LUMINANCE + 0.05) / (0.2126 * r + 0.7152 * g + 0.0722 * b + 0.05);
}
/** Darkens a #rrggbb(aa) color in steps that keep its hue until it reads on light code. */
export function readableOnLightCode(color: string, minContrast = MIN_CONTRAST): string {
const match = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(color);
if (!match) return color;
let rgb = [0, 2, 4].map((start) => Number.parseInt(match[1]!.slice(start, start + 2), 16)) as [
number,
number,
number,
];
if (contrastOnLightCode(rgb) >= minContrast) return color;
while (contrastOnLightCode(rgb) < minContrast) {
rgb = rgb.map((channel) => Math.floor(channel * 0.96)) as [number, number, number];
}
return `#${rgb.map((channel) => channel.toString(16).padStart(2, "0")).join("")}${match[2] ?? ""}`;
}
type ZedTheme = NonNullable<DiffsTheme["zed"]>;
// Pairs that differ only in lightness would merge at one floor. The darker color of each pair
// goes further, keeping pierre-light's order: numbers stay darker than operators, punctuation
// darker than comments, and decorators darker than property names.
const FURTHER_COLORS = new Set(["#1ca1c7", "#636363", "#1a85d4"]);
const FURTHER_MIN_CONTRAST = 6;
/** The t3-light color for a pierre-light syntax color. */
export function readableSyntaxColor(color: string): string {
return readableOnLightCode(
color,
FURTHER_COLORS.has(color.toLowerCase()) ? FURTHER_MIN_CONTRAST : MIN_CONTRAST,
);
}
function withReadableSyntax(theme: ZedTheme): ZedTheme {
const syntax = Object.fromEntries(
Object.entries(theme.style.syntax ?? {}).map(([scope, style]) => [
scope,
typeof style === "string"
? readableSyntaxColor(style)
: style.color
? { ...style, color: readableSyntaxColor(style.color) }
: style,
]),
);
return { ...theme, name: READABLE_LIGHT_THEME_NAME, style: { ...theme.style, syntax } };
}
// Themes resolve on the main thread and reach the diff workers already resolved.
registerCustomTheme(
READABLE_LIGHT_THEME_NAME,
async () => {
const base = await getResolvedOrResolveTheme("pierre-light", "highlights");
if (!base.zed) throw new Error("pierre-light has no Highlights palette");
return withReadableSyntax(base.zed);
},
"zed",
);