website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
8940631
— pyrossh
2026-11-06T17:18:02+05:30
Add a reading page for markdown files under /docs/
- assets/css/repos.css +13 -0
- src/_data/repoFiles.js +3 -4
- src/_helpers/repoMarkdown.js +17 -8
- src/repos/doc.njk +28 -0
- tests/pages.test.js +15 -1
- tests/repoMarkdown.test.js +26 -14
assets/css/repos.css
CHANGED
|
@@ -446,6 +446,19 @@
|
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
/* The same strip above a doc page, with a way across to the code view. */
|
|
450
|
+
.doc-header {
|
|
451
|
+
margin-bottom: 1rem;
|
|
452
|
+
border: 1px solid var(--color-pre-bg);
|
|
453
|
+
border-radius: 0.4rem;
|
|
454
|
+
|
|
455
|
+
a {
|
|
456
|
+
flex-shrink: 0;
|
|
457
|
+
font-size: 0.9rem;
|
|
458
|
+
text-decoration: underline;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
449
462
|
/* diff2html ships light tokens on :root and a dark set it only applies under
|
|
450
463
|
a `.d2h-dark-color-scheme` class we never add. Redefining the base tokens
|
|
451
464
|
with light-dark() follows the same color-scheme as the rest of the site, so
|
src/_data/repoFiles.js
CHANGED
|
@@ -13,10 +13,9 @@ import {
|
|
|
13
13
|
} from "../_helpers/gitRepos.js";
|
|
14
14
|
import { highlightCode } from "../_helpers/highlightCode.js";
|
|
15
15
|
import { mapPool } from "../_helpers/pool.js";
|
|
16
|
-
import { renderRepoMarkdown } from "../_helpers/repoMarkdown.js";
|
|
16
|
+
import { isRenderedMarkdown, renderRepoMarkdown } from "../_helpers/repoMarkdown.js";
|
|
17
17
|
|
|
18
18
|
const CONCURRENCY = 8;
|
|
19
|
-
const MARKDOWN_EXTENSIONS = new Set(["md", "markdown"]);
|
|
20
19
|
|
|
21
20
|
const renderFile = (repo, file) => {
|
|
22
21
|
const full = path.join(repo.dir, file.path);
|
|
@@ -24,7 +23,7 @@ const renderFile = (repo, file) => {
|
|
|
24
23
|
if (file.size > LARGE_FILE_SIZE) return { kind: "large" };
|
|
25
24
|
if (IMAGE_EXTENSIONS.has(file.ext)) return { kind: "image", src: toDataUri(fs.readFileSync(full), file.ext) };
|
|
26
25
|
const source = fs.readFileSync(full, "utf8");
|
|
27
|
-
if (
|
|
26
|
+
if (isRenderedMarkdown(file)) {
|
|
28
27
|
const html = renderRepoMarkdown(source, { repoId: repo.id, files: repo.files, filePath: file.path });
|
|
29
28
|
return { kind: "markdown", html: inlineMarkdownImages(html, repo.dir, file.path) };
|
|
30
29
|
}
|
|
@@ -41,7 +40,7 @@ export default async () => {
|
|
|
41
40
|
// The version is part of the key because the cache is otherwise keyed by
|
|
42
41
|
// HEAD: bump it when renderFile's output changes, or unchanged repos keep
|
|
43
42
|
// serving the old render.
|
|
44
|
-
const cacheKey = `files-
|
|
43
|
+
const cacheKey = `files-v3-${repo.id}`;
|
|
45
44
|
|
|
46
45
|
let entries = cacheable ? readCache(cacheKey, sha) : null;
|
|
47
46
|
if (!entries) {
|
src/_helpers/repoMarkdown.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import MarkdownIt from "markdown-it";
|
|
3
|
-
import { decodePath } from "./gitRepos.js";
|
|
3
|
+
import { LARGE_FILE_SIZE, decodePath } from "./gitRepos.js";
|
|
4
4
|
import { highlightFenceCode } from "./highlightCode.js";
|
|
5
5
|
|
|
6
6
|
const md = new MarkdownIt({ html: true, linkify: true, highlight: highlightFenceCode });
|
|
@@ -36,19 +36,25 @@ const FRONT_MATTER = /^---\r?\n[\w-]+:[\s\S]*?\r?\n---\r?\n/;
|
|
|
36
36
|
|
|
37
37
|
const encodePath = (filePath) => filePath.split("/").map(encodeURIComponent).join("/");
|
|
38
38
|
|
|
39
|
+
/** Whether a repo file is rendered as a document, so has a page under /docs/. */
|
|
40
|
+
export const isRenderedMarkdown = (file) =>
|
|
41
|
+
/\.(md|markdown)$/i.test(file.path) && !(file.size > LARGE_FILE_SIZE);
|
|
42
|
+
|
|
39
43
|
// Folders have no page of their own, so a link to one opens its README or else
|
|
40
44
|
// its first file, which also expands that folder in the file tree.
|
|
41
45
|
const findLinkTarget = (files, target) => {
|
|
42
|
-
|
|
46
|
+
const exact = files.find((f) => f.path === target);
|
|
47
|
+
if (exact) return exact;
|
|
43
48
|
const inFolder = files.filter((f) => f.path.startsWith(`${target}/`));
|
|
44
49
|
const readme = inFolder.find((f) => f.path.toLowerCase() === `${target}/readme.md`.toLowerCase());
|
|
45
|
-
return
|
|
50
|
+
return readme ?? inFolder[0];
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
// Repo markdown links relative to its own file, as GitHub resolves them. Here
|
|
54
|
+
// those links need rewriting: documents open on their reading page under
|
|
49
|
-
//
|
|
55
|
+
// /repos/<id>/docs/<path>/ and anything else in the code view under
|
|
50
|
-
//
|
|
56
|
+
// /repos/<id>/files/<path>/. A leading `/` means the repo root. Targets not in
|
|
51
|
-
// left as written. `href` is still HTML-escaped, so `&` is `&`
|
|
57
|
+
// the repo are left as written. `href` is still HTML-escaped, so `&` is `&`.
|
|
52
58
|
const resolveLink = (href, repoId, files, filePath) => {
|
|
53
59
|
if (/^(#|\/\/|[a-z][a-z0-9+.-]*:)/i.test(href)) return href;
|
|
54
60
|
|
|
@@ -61,7 +67,10 @@ const resolveLink = (href, repoId, files, filePath) => {
|
|
|
61
67
|
if (target === ".") return `/repos/${repoId}/files/${suffix}`;
|
|
62
68
|
|
|
63
69
|
const file = findLinkTarget(files, target);
|
|
70
|
+
if (file) {
|
|
71
|
+
const route = isRenderedMarkdown(file) ? "docs" : "files";
|
|
64
|
-
|
|
72
|
+
return `/repos/${repoId}/${route}/${encodePath(file.path)}/${suffix}`;
|
|
73
|
+
}
|
|
65
74
|
|
|
66
75
|
// Scheme-less external links, e.g. [Teensy](www.pjrc.com/store/teensy3.html)
|
|
67
76
|
return /^www\./i.test(href) ? `https://${href}` : href;
|
|
@@ -69,7 +78,7 @@ const resolveLink = (href, repoId, files, filePath) => {
|
|
|
69
78
|
|
|
70
79
|
/**
|
|
71
80
|
* Render a markdown file from a repo, pointing its relative links at this
|
|
72
|
-
* site's file pages. Runs over the final HTML rather than a markdown-it link
|
|
81
|
+
* site's doc and file pages. Runs over the final HTML rather than a markdown-it link
|
|
73
82
|
* rule so raw <a> tags in the markdown are covered too; code blocks can't match
|
|
74
83
|
* because their `<` is already escaped.
|
|
75
84
|
*
|
src/repos/doc.njk
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---js
|
|
2
|
+
const pagination = {
|
|
3
|
+
data: "repoFiles",
|
|
4
|
+
size: 1,
|
|
5
|
+
alias: "item",
|
|
6
|
+
before: (entries) => entries.filter((entry) => entry.render.kind === "markdown"),
|
|
7
|
+
};
|
|
8
|
+
const permalink = "/repos/{{ item.repoId }}/docs/{{ item.file.path }}/";
|
|
9
|
+
const layout = "layout.njk";
|
|
10
|
+
const eleventyComputed = {
|
|
11
|
+
title: "{{ item.file.name }} | {{ item.repo.data.title }}",
|
|
12
|
+
description: "{{ item.file.path }} — document in {{ item.repo.data.title }}",
|
|
13
|
+
robots: "noindex, follow",
|
|
14
|
+
};
|
|
15
|
+
---
|
|
16
|
+
{% from "repo-layout.njk" import repoLayout %}
|
|
17
|
+
{# The reading view of a markdown file: the same render as the code view, at the
|
|
18
|
+
README's width without the file tree. Links between documents land here. #}
|
|
19
|
+
<div id="repo-page">
|
|
20
|
+
{{ repoLayout(item.repo.id, item.repo.data.title, item.repo.data.description, item.repo.data.tags, "Readme", item.repo.version) }}
|
|
21
|
+
<div id="repo-content">
|
|
22
|
+
<div class="file-layout doc-header">
|
|
23
|
+
<div class="title">{{ item.file.name | fileIcon(item.file.ext) | safe }}<span>{{ item.file.path }}</span></div>
|
|
24
|
+
<a href="/repos/{{ item.repoId }}/files/{{ item.file.path }}/">Open in Code</a>
|
|
25
|
+
</div>
|
|
26
|
+
<article class="markdown-body">{{ item.render.html | safe }}</article>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
tests/pages.test.js
CHANGED
|
@@ -251,6 +251,20 @@ describe("repo pages", () => {
|
|
|
251
251
|
assert.notEqual(style.color, style.parentColor);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
+
test("markdown files have a reading page without the file tree", async () => {
|
|
255
|
+
const response = await goto(`/repos/${repoId}/docs/README.md/`);
|
|
256
|
+
assert.equal(response.status(), 200);
|
|
257
|
+
await page.locator("#repo-content > .markdown-body").waitFor();
|
|
258
|
+
assert.equal(await page.locator(".file-explorer").count(), 0);
|
|
259
|
+
|
|
260
|
+
await page.click(".doc-header a");
|
|
261
|
+
await page.waitForURL(`${server.url}/repos/${repoId}/files/README.md/`);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("other files have no reading page", async () => {
|
|
265
|
+
assert.equal((await goto(`/repos/${repoId}/docs/package.json/`)).status(), 404);
|
|
266
|
+
});
|
|
267
|
+
|
|
254
268
|
for (const colorScheme of ["dark", "light"]) {
|
|
255
269
|
test(`markdown text is comfortably readable in ${colorScheme} mode`, async () => {
|
|
256
270
|
await page.emulateMedia({ colorScheme });
|
|
@@ -367,6 +381,6 @@ test("no unexpected console errors were logged across visited pages", () => {
|
|
|
367
381
|
// The deliberate 404 tests above trigger a browser-level "failed to load
|
|
368
382
|
// resource" console entry for their own navigation requests — that's the
|
|
369
383
|
// behavior under test there, not a bug, so they're excluded here.
|
|
370
|
-
const unexpected = consoleErrors.filter((msg) => !/this-page-does-not-exist|\/(history|blame)\/\]/.test(msg));
|
|
384
|
+
const unexpected = consoleErrors.filter((msg) => !/this-page-does-not-exist|\/(history|blame)\/\]|\/docs\/package\.json\/\]/.test(msg));
|
|
371
385
|
assert.deepEqual(unexpected, []);
|
|
372
386
|
});
|
tests/repoMarkdown.test.js
CHANGED
|
@@ -6,28 +6,40 @@ import path from "node:path";
|
|
|
6
6
|
import { LARGE_FILE_SIZE, inlineMarkdownImages } from "../src/_helpers/gitRepos.js";
|
|
7
7
|
import { renderRepoMarkdown } from "../src/_helpers/repoMarkdown.js";
|
|
8
8
|
|
|
9
|
-
const files = [
|
|
9
|
+
const files = [
|
|
10
|
+
...["README.md", "docs/language.md", "docs/std.md", "examples/arrays.plum", "examples/basics.plum", "guide/intro.md", "guide/README.md", "my docs/a b.md", "c#/q&a.md", "www.site/index.md", "UPPER.MD"].map((p) => ({ path: p })),
|
|
11
|
+
{ path: "docs/huge.md", size: LARGE_FILE_SIZE + 1 },
|
|
12
|
+
];
|
|
10
13
|
|
|
11
14
|
const render = (source, filePath = "README.md") => renderRepoMarkdown(source, { repoId: "plum", files, filePath });
|
|
12
15
|
const href = (source, filePath) => render(source, filePath).match(/href="([^"]*)"/)[1];
|
|
13
16
|
|
|
14
17
|
describe("renderRepoMarkdown links", () => {
|
|
15
|
-
test("
|
|
18
|
+
test("links to markdown files open their doc pages", () => {
|
|
16
|
-
assert.equal(href("[x](docs/language.md)"), "/repos/plum/
|
|
19
|
+
assert.equal(href("[x](docs/language.md)"), "/repos/plum/docs/docs/language.md/");
|
|
17
|
-
assert.equal(href("[x](./docs/language.md)"), "/repos/plum/
|
|
20
|
+
assert.equal(href("[x](./docs/language.md)"), "/repos/plum/docs/docs/language.md/");
|
|
21
|
+
assert.equal(href("[x](UPPER.MD)"), "/repos/plum/docs/UPPER.MD/");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("links to other files open the code view", () => {
|
|
25
|
+
assert.equal(href("[x](examples/basics.plum)"), "/repos/plum/files/examples/basics.plum/");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("markdown too large to render has no doc page, so opens the code view", () => {
|
|
29
|
+
assert.equal(href("[x](docs/huge.md)"), "/repos/plum/files/docs/huge.md/");
|
|
18
30
|
});
|
|
19
31
|
|
|
20
32
|
test("links resolve against the markdown file's own folder", () => {
|
|
21
|
-
assert.equal(href("[x](std.md)", "docs/language.md"), "/repos/plum/
|
|
33
|
+
assert.equal(href("[x](std.md)", "docs/language.md"), "/repos/plum/docs/docs/std.md/");
|
|
22
|
-
assert.equal(href("[x](../README.md)", "docs/language.md"), "/repos/plum/
|
|
34
|
+
assert.equal(href("[x](../README.md)", "docs/language.md"), "/repos/plum/docs/README.md/");
|
|
23
35
|
});
|
|
24
36
|
|
|
25
37
|
test("a section in another file keeps its fragment", () => {
|
|
26
|
-
assert.equal(href("[x](docs/std.md#strings)"), "/repos/plum/
|
|
38
|
+
assert.equal(href("[x](docs/std.md#strings)"), "/repos/plum/docs/docs/std.md/#strings");
|
|
27
39
|
});
|
|
28
40
|
|
|
29
41
|
test("folder links open the folder's README, else its first file", () => {
|
|
30
|
-
assert.equal(href("[x](guide/)"), "/repos/plum/
|
|
42
|
+
assert.equal(href("[x](guide/)"), "/repos/plum/docs/guide/README.md/");
|
|
31
43
|
assert.equal(href("[x](../examples)", "docs/language.md"), "/repos/plum/files/examples/arrays.plum/");
|
|
32
44
|
});
|
|
33
45
|
|
|
@@ -36,20 +48,20 @@ describe("renderRepoMarkdown links", () => {
|
|
|
36
48
|
});
|
|
37
49
|
|
|
38
50
|
test("paths with spaces stay encoded", () => {
|
|
39
|
-
assert.equal(href("[x](my%20docs/a%20b.md)"), "/repos/plum/
|
|
51
|
+
assert.equal(href("[x](my%20docs/a%20b.md)"), "/repos/plum/docs/my%20docs/a%20b.md/");
|
|
40
52
|
});
|
|
41
53
|
|
|
42
54
|
test("filenames with URL-special characters are encoded per segment", () => {
|
|
43
|
-
assert.equal(href("[x](c%23/q&a.md)"), "/repos/plum/
|
|
55
|
+
assert.equal(href("[x](c%23/q&a.md)"), "/repos/plum/docs/c%23/q%26a.md/");
|
|
44
56
|
});
|
|
45
57
|
|
|
46
58
|
test("a leading slash means the repo root", () => {
|
|
47
|
-
assert.equal(href("[x](/docs/std.md)", "guide/intro.md"), "/repos/plum/
|
|
59
|
+
assert.equal(href("[x](/docs/std.md)", "guide/intro.md"), "/repos/plum/docs/docs/std.md/");
|
|
48
60
|
});
|
|
49
61
|
|
|
50
62
|
test("raw HTML links are resolved too, with either quote style", () => {
|
|
51
|
-
assert.equal(href('<a href="docs/std.md">x</a>'), "/repos/plum/
|
|
63
|
+
assert.equal(href('<a href="docs/std.md">x</a>'), "/repos/plum/docs/docs/std.md/");
|
|
52
|
-
assert.match(render("<a href='docs/std.md'>x</a>"), /href='\/repos\/plum\/
|
|
64
|
+
assert.match(render("<a href='docs/std.md'>x</a>"), /href='\/repos\/plum\/docs\/docs\/std\.md\/'/);
|
|
53
65
|
});
|
|
54
66
|
|
|
55
67
|
test("data-href isn't treated as the link", () => {
|
|
@@ -64,7 +76,7 @@ describe("renderRepoMarkdown links", () => {
|
|
|
64
76
|
|
|
65
77
|
test("scheme-less www links become external, unless the repo has that path", () => {
|
|
66
78
|
assert.equal(href("[x](www.pjrc.com/store/teensy3.html)"), "https://www.pjrc.com/store/teensy3.html");
|
|
67
|
-
assert.equal(href("[x](www.site/index.md)"), "/repos/plum/
|
|
79
|
+
assert.equal(href("[x](www.site/index.md)"), "/repos/plum/docs/www.site/index.md/");
|
|
68
80
|
});
|
|
69
81
|
|
|
70
82
|
test("links shown in code blocks are not rewritten", () => {
|