website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
18b36a2
— pyrossh
2026-11-06T15:29:20+05:30
Improve markdown readability in light and dark themes
- assets/css/markdown.css +40 -16
- assets/css/shared.css +9 -9
- tests/pages.test.js +53 -0
assets/css/markdown.css
CHANGED
|
@@ -10,9 +10,32 @@
|
|
|
10
10
|
between markdown content and the file-viewer, so it stays global
|
|
11
11
|
rather than duplicated here). */
|
|
12
12
|
.markdown-body {
|
|
13
|
+
/* Reading colors, redefined here so long documents get them without changing
|
|
14
|
+
the rest of the site. Targets are about 12:1 for body text, since the
|
|
15
|
+
site-wide near-white on near-black (18:1) is harsh for long reading, and at
|
|
16
|
+
least 4.5:1 for everything else, including syntax tokens in light mode. */
|
|
17
|
+
--color-text: light-dark(hsl(0, 0%, 20%), hsl(210, 14%, 86%));
|
|
18
|
+
--color-heading: light-dark(hsl(0, 0%, 12%), hsl(210, 17%, 95%));
|
|
19
|
+
--color-link: light-dark(hsl(211, 100%, 36%), hsl(211, 100%, 72%));
|
|
20
|
+
--color-pre-bg: light-dark(hsl(0, 0%, 98%), hsl(210, 13%, 6%));
|
|
21
|
+
--color-code-fg: light-dark(hsl(348, 80%, 34%), hsl(348, 75%, 80%));
|
|
22
|
+
--color-code-bg: light-dark(hsl(0, 0%, 90%), hsl(210, 11%, 17%));
|
|
23
|
+
--color-markdown-border: light-dark(hsl(0, 0%, 80%), hsl(210, 10%, 26%));
|
|
24
|
+
|
|
25
|
+
color: var(--color-text);
|
|
13
26
|
font-size: 1.1rem;
|
|
14
27
|
line-height: 1.5;
|
|
15
28
|
|
|
29
|
+
h1,
|
|
30
|
+
h2,
|
|
31
|
+
h3,
|
|
32
|
+
h4,
|
|
33
|
+
h5,
|
|
34
|
+
h6,
|
|
35
|
+
strong {
|
|
36
|
+
color: var(--color-heading);
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
h1 {
|
|
17
40
|
font-size: var(--fs-heading-md);
|
|
18
41
|
font-weight: 700;
|
|
@@ -74,25 +97,28 @@
|
|
|
74
97
|
margin-bottom: 0;
|
|
75
98
|
}
|
|
76
99
|
|
|
77
|
-
|
|
100
|
+
/* Inline code anywhere (paragraphs, lists, tables, headings), not fenced
|
|
101
|
+
blocks. Sized relative to its surroundings so it stays in proportion. */
|
|
78
|
-
|
|
102
|
+
:not(pre) > code {
|
|
79
|
-
color: var(--color-code-fg)
|
|
103
|
+
color: var(--color-code-fg);
|
|
80
|
-
background: var(--color-code-bg)
|
|
104
|
+
background: var(--color-code-bg);
|
|
81
105
|
word-wrap: break-word;
|
|
82
106
|
box-decoration-break: clone;
|
|
83
107
|
padding: 2px 4px;
|
|
84
108
|
border-radius: 0.2rem;
|
|
85
109
|
font-weight: 400;
|
|
86
|
-
font-size: 0.
|
|
110
|
+
font-size: 0.85em;
|
|
87
111
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
88
112
|
}
|
|
89
113
|
|
|
90
|
-
/* Fenced code blocks in a README (markdown-it output — no language-
|
|
91
|
-
class on the <pre> itself, unlike Prism's blog-post output, which is
|
|
92
|
-
how this targets only the README case) get a subtle border + corner
|
|
93
|
-
|
|
114
|
+
/* Code inside a link has to still read as a link. */
|
|
115
|
+
a[href] > code {
|
|
94
|
-
|
|
116
|
+
color: var(--color-link);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pre {
|
|
120
|
+
font-size: 0.9rem;
|
|
95
|
-
border: 1px solid var(--color-
|
|
121
|
+
border: 1px solid var(--color-markdown-border);
|
|
96
122
|
border-radius: 0.4rem;
|
|
97
123
|
}
|
|
98
124
|
|
|
@@ -113,7 +139,7 @@
|
|
|
113
139
|
|
|
114
140
|
th,
|
|
115
141
|
td {
|
|
116
|
-
border: 1px solid var(--color-
|
|
142
|
+
border: 1px solid var(--color-markdown-border);
|
|
117
143
|
padding: 0.4rem 0.8rem;
|
|
118
144
|
text-align: left;
|
|
119
145
|
}
|
|
@@ -128,10 +154,8 @@
|
|
|
128
154
|
}
|
|
129
155
|
|
|
130
156
|
blockquote {
|
|
131
|
-
font-size: 1.2rem;
|
|
132
|
-
font-weight: 600;
|
|
133
|
-
|
|
157
|
+
border-left: 3px solid var(--color-markdown-border);
|
|
134
|
-
padding: 0.
|
|
158
|
+
padding: 0.2rem 0 0.2rem 0.9rem;
|
|
135
159
|
margin: 16px 0px;
|
|
136
160
|
|
|
137
161
|
p {
|
assets/css/shared.css
CHANGED
|
@@ -77,15 +77,15 @@ table { border-collapse: collapse; }
|
|
|
77
77
|
--color-code-bg: light-dark(hsl(0, 0%, 89%), hsl(210, 13%, 6%));
|
|
78
78
|
--color-pre-bg: light-dark(hsl(0, 0%, 89%), hsl(210, 13%, 6%));
|
|
79
79
|
--color-tag: light-dark(hsl(152, 96%, 35%), hsl(152, 96%, 44%));
|
|
80
|
-
--token-comment: light-dark(hsl(220, 10%,
|
|
80
|
+
--token-comment: light-dark(hsl(220, 10%, 38%), hsl(220, 10%, 60%));
|
|
81
|
-
--token-keyword: light-dark(hsl(310, 60%,
|
|
81
|
+
--token-keyword: light-dark(hsl(310, 60%, 36%), hsl(310, 70%, 72%));
|
|
82
|
-
--token-string: light-dark(hsl(95,
|
|
82
|
+
--token-string: light-dark(hsl(95, 50%, 25%), hsl(95, 55%, 65%));
|
|
83
|
-
--token-function: light-dark(hsl(210, 70%,
|
|
83
|
+
--token-function: light-dark(hsl(210, 70%, 36%), hsl(210, 80%, 72%));
|
|
84
|
-
--token-number: light-dark(hsl(25,
|
|
84
|
+
--token-number: light-dark(hsl(25, 85%, 30%), hsl(25, 90%, 65%));
|
|
85
|
-
--token-builtin: light-dark(hsl(175,
|
|
85
|
+
--token-builtin: light-dark(hsl(175, 60%, 24%), hsl(175, 60%, 60%));
|
|
86
|
-
--token-punctuation: light-dark(hsl(220, 8%,
|
|
86
|
+
--token-punctuation: light-dark(hsl(220, 8%, 36%), hsl(220, 8%, 70%));
|
|
87
|
-
--token-tag: light-dark(hsl(0, 60%,
|
|
87
|
+
--token-tag: light-dark(hsl(0, 60%, 38%), hsl(0, 70%, 72%));
|
|
88
|
-
--token-attr-name: light-dark(hsl(35,
|
|
88
|
+
--token-attr-name: light-dark(hsl(35, 80%, 26%), hsl(35, 80%, 65%));
|
|
89
89
|
color-scheme: light dark;
|
|
90
90
|
--btn-light: none;
|
|
91
91
|
--btn-dark: block;
|
tests/pages.test.js
CHANGED
|
@@ -251,6 +251,59 @@ describe("repo pages", () => {
|
|
|
251
251
|
assert.notEqual(style.color, style.parentColor);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
+
for (const colorScheme of ["dark", "light"]) {
|
|
255
|
+
test(`markdown text is comfortably readable in ${colorScheme} mode`, async () => {
|
|
256
|
+
await page.emulateMedia({ colorScheme });
|
|
257
|
+
try {
|
|
258
|
+
await goto(`/repos/${repoId}/files/README.md/`);
|
|
259
|
+
await page.locator("#file-content > .markdown-body").waitFor();
|
|
260
|
+
const ratios = await page.evaluate(() => {
|
|
261
|
+
const channels = (color) => color.match(/[\d.]+/g).slice(0, 3).map(Number);
|
|
262
|
+
const luminance = (color) => {
|
|
263
|
+
const [r, g, b] = channels(color).map((v) => {
|
|
264
|
+
const c = v / 255;
|
|
265
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
266
|
+
});
|
|
267
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
268
|
+
};
|
|
269
|
+
const contrast = (a, b) => {
|
|
270
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
271
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
272
|
+
};
|
|
273
|
+
const style = (selector) => getComputedStyle(document.querySelector(selector));
|
|
274
|
+
const article = style("#file-content .markdown-body");
|
|
275
|
+
const pre = style("#file-content .markdown-body pre");
|
|
276
|
+
const inlineCode = style("#file-content .markdown-body :not(pre) > code");
|
|
277
|
+
const tokenColor = (name) => {
|
|
278
|
+
const probe = document.createElement("span");
|
|
279
|
+
probe.style.color = `var(--token-${name})`;
|
|
280
|
+
document.querySelector("#file-content .markdown-body pre").append(probe);
|
|
281
|
+
const { color } = getComputedStyle(probe);
|
|
282
|
+
probe.remove();
|
|
283
|
+
return color;
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
body: contrast(article.color, article.backgroundColor),
|
|
287
|
+
link: contrast(style("#file-content .markdown-body a[href]").color, article.backgroundColor),
|
|
288
|
+
inlineCode: contrast(inlineCode.color, inlineCode.backgroundColor),
|
|
289
|
+
codeBlock: contrast(pre.color, pre.backgroundColor),
|
|
290
|
+
tokens: ["comment", "keyword", "string", "function", "number", "builtin", "punctuation", "tag", "attr-name"].map(
|
|
291
|
+
(name) => contrast(tokenColor(name), pre.backgroundColor),
|
|
292
|
+
),
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Not just a floor: near-white on near-black is hard to read for long.
|
|
297
|
+
assert.ok(ratios.body >= 7 && ratios.body <= 14, `body text is ${ratios.body}:1`);
|
|
298
|
+
for (const [part, ratio] of Object.entries({ ...ratios, tokens: Math.min(...ratios.tokens) })) {
|
|
299
|
+
assert.ok(ratio >= 4.5, `${part} is ${ratio}:1`);
|
|
300
|
+
}
|
|
301
|
+
} finally {
|
|
302
|
+
await page.emulateMedia({ colorScheme: null });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
254
307
|
test("repo files index renders the tree and a default file", async () => {
|
|
255
308
|
const response = await goto(`/repos/${repoId}/files/`);
|
|
256
309
|
assert.equal(response.status(), 200);
|