website

#astro#js#html#css

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 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 (MARKDOWN_EXTENSIONS.has(file.ext)) {
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-v2-${repo.id}`;
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
- if (files.some((f) => f.path === target)) return target;
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 (readme ?? inFolder[0])?.path;
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
- // that file's content lives at /repos/<id>/files/<path>/, so those links need
55
+ // /repos/<id>/docs/<path>/ and anything else in the code view under
50
- // rewriting, and a leading `/` means the repo root. Targets not in the repo are
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 `&amp;` here.
57
+ // the repo are left as written. `href` is still HTML-escaped, so `&` is `&amp;`.
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
- if (file) return `/repos/${repoId}/files/${encodePath(file)}/${suffix}`;
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 = ["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"].map((p) => ({ path: p }));
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("relative file links point at the repo's file pages", () => {
18
+ test("links to markdown files open their doc pages", () => {
16
- assert.equal(href("[x](docs/language.md)"), "/repos/plum/files/docs/language.md/");
19
+ assert.equal(href("[x](docs/language.md)"), "/repos/plum/docs/docs/language.md/");
17
- assert.equal(href("[x](./docs/language.md)"), "/repos/plum/files/docs/language.md/");
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/files/docs/std.md/");
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/files/README.md/");
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/files/docs/std.md/#strings");
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/files/guide/README.md/");
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/files/my%20docs/a%20b.md/");
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/files/c%23/q%26a.md/");
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/files/docs/std.md/");
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/files/docs/std.md/");
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\/files\/docs\/std\.md\/'/);
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/files/www.site/index.md/");
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", () => {