Public: anyone can read this; the crew can change it.

Every in-page link in the README lands on a heading that exists, and a test keeps it so #227

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/deal-clock:mason/215-every-in-page-link-in-the-readme-lands-o → main 4 commits fork 3 threads · 3 unresolved

  • Merged. mason/215-every-in-page-link-in-the-readme-lands-o was merged into main as 2283f4d.

Review

Sign in to review this change.
1 file changed +55 −0
src/readme.test.ts added +55
@@ -0,0 +1,55 @@
1 import { expect, test } from "bun:test";
2 import { readFileSync } from "node:fs";
3
4 // GitHub-style heading slug: lowercase, punctuation dropped, spaces to hyphens.
5 function slug(heading: string): string {
6 return heading
7 .trim()
8 .toLowerCase()
9 .replace(/[^\p{L}\p{N}\s_-]/gu, "")
10 .replace(/\s/g, "-");
11 }
12
13 function anchors(markdown: string): Set<string> {
14 const seen = new Map<string, number>();
15 const out = new Set<string>();
16 let fenced = false;
17 for (const line of markdown.split("\n")) {
18 if (/^\s*(```|~~~)/.test(line)) fenced = !fenced;
19 if (fenced) continue;
20 const m = /^#{1,6}\s+(.*?)\s*#*\s*$/.exec(line);
21 if (!m) continue;
22 const base = slug(m[1]!.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[`*]/g, ""));
23 const n = seen.get(base) ?? 0;
24 seen.set(base, n + 1);
25 out.add(n === 0 ? base : `${base}-${n}`);
26 }
27 return out;
28 }
29
30 export function brokenAnchors(markdown: string): string[] {
31 const have = anchors(markdown);
32 const broken: string[] = [];
33 for (const m of markdown.matchAll(/\]\(#([^)\s]*)\)/g)) {
34 let a: string;
35 try {
36 a = decodeURIComponent(m[1]!).toLowerCase();
37 } catch {
outdated

decodeURIComponent throws on malformed escapes like %zz. Catch it and treat the anchor as broken.

38 a = m[1]!.toLowerCase();
39 }
40 if (!have.has(a) && !broken.includes(a)) broken.push(a);
41 }
42 return broken;
43 }
44
45 test("every in-page link in README.md lands on a heading", () => {
46 const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8");
47 expect(brokenAnchors(readme)).toEqual([]);
48 });
49
50 test("a broken anchor is reported, a good one and repeated headings are not", () => {
51 const doc = "# Top\n\n## Make it yours!\n\n## Same\n\n## Same\n\n" +
52 "## See [the docs](http://x.y) now\n\n" +
53 "[a](#make-it-yours) [b](#same-1) [c](#nowhere) [d](#top) [e](#%zz) [f](#see-the-docs-now)\n";
54 expect(brokenAnchors(doc)).toEqual(["nowhere", "%zz"]);
55 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs