Skip to content

Commit f153a4e

Browse files
committed
fix broken links, and validate
1 parent ca2cb0a commit f153a4e

File tree

3 files changed

+73
-45
lines changed

3 files changed

+73
-45
lines changed

docs/components/links.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {readdir, readFile, stat} from "fs/promises";
2+
3+
// Anchors can be derived from headers, or explicitly written as {#names}.
4+
export function getAnchors(text) {
5+
const anchors = [];
6+
for (const [, header] of text.matchAll(/^#+ ([*\w][*().,\w\d -]+)\n/gm)) {
7+
anchors.push(
8+
header
9+
.replaceAll(/[^\w\d\s]+/g, " ")
10+
.trim()
11+
.replaceAll(/ +/g, "-")
12+
.toLowerCase()
13+
);
14+
}
15+
for (const [, anchor] of text.matchAll(/ \{#([\w\d-]+)\}/g)) {
16+
anchors.push(anchor);
17+
}
18+
return anchors;
19+
}
20+
21+
// Internal links.
22+
export function getLinks(file, text) {
23+
const links = [];
24+
for (const match of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
25+
const [, link] = match;
26+
if (/^\w+:/.test(link)) continue; // absolute link with protocol
27+
const {pathname, hash} = new URL(link, new URL(file, "https://example.com/"));
28+
links.push({pathname, hash});
29+
}
30+
return links;
31+
}
32+
33+
// In source files, ignore comments.
34+
export async function readMarkdownSource(f) {
35+
return (await readFile(f, "utf8")).replaceAll(/<!-- .*? -->/gs, "");
36+
}
37+
38+
// Recursively find all md files in the directory.
39+
export async function* readMarkdownFiles(root, subpath = "/") {
40+
for (const fname of await readdir(root + subpath)) {
41+
if (fname.startsWith(".") || fname.endsWith(".js")) continue; // ignore .vitepress etc.
42+
if ((await stat(root + subpath + fname)).isDirectory()) yield* readMarkdownFiles(root, subpath + fname + "/");
43+
else if (fname.endsWith(".md")) yield subpath + fname;
44+
}
45+
}

docs/data/api.data.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {rollup, sort} from "d3";
22
import {FunctionDeclaration, Node, Project, VariableStatement} from "ts-morph";
3+
import {readMarkdownFiles, readMarkdownSource, getAnchors} from "../components/links.js";
34

45
// These interfaces tend to represent things that Plot constructs internally,
56
// rather than objects that the user is expected to provide.
@@ -43,8 +44,12 @@ function getHref(name: string, path: string): string {
4344
case "features/curve":
4445
case "features/format":
4546
case "features/mark":
47+
case "features/marker":
4648
case "features/plot":
49+
case "features/projection":
4750
return `${path}s`;
51+
case "features/inset":
52+
return "features/scales";
4853
case "features/options":
4954
return "features/transforms";
5055
case "marks/axis": {
@@ -88,6 +93,7 @@ function getInterfaceName(name: string, path: string): string {
8893
export default {
8994
watch: [],
9095
async load() {
96+
// Parse the TypeScript declarations to get exported symbols.
9197
const project = new Project({tsConfigFilePath: "tsconfig.json"});
9298
const allMethods: {name: string; comment: string; href: string}[] = [];
9399
const allOptions: {name: string; context: {name: string; href: string}}[] = [];
@@ -117,6 +123,27 @@ export default {
117123
}
118124
}
119125
}
126+
// Parse the Markdown files to get all known anchors.
127+
const root = "docs";
128+
const anchors = new Map();
129+
for await (const file of readMarkdownFiles(root)) {
130+
const text = await readMarkdownSource(root + file);
131+
anchors.set(file, getAnchors(text));
132+
}
133+
// Cross-reference the generated links.
134+
for (const {name, href} of allMethods) {
135+
if (!anchors.has(`/${href}.md`)) {
136+
throw new Error(`file not found: ${href}`);
137+
}
138+
if (!anchors.get(`/${href}.md`).includes(name)) {
139+
throw new Error(`anchor not found: ${href}#${name}`);
140+
}
141+
}
142+
for (const {context: {href}} of allOptions) {
143+
if (!anchors.has(`/${href}.md`)) {
144+
throw new Error(`file not found: ${href}`);
145+
}
146+
}
120147
return {
121148
methods: sort(allMethods, ({name}) => name),
122149
options: sort(

test/docs-test.js

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from "assert";
2-
import {readdir, readFile, stat} from "fs/promises";
2+
import {readMarkdownFiles, readMarkdownSource, getAnchors, getLinks} from "../docs/components/links.js";
33

44
it("documentation links point to existing internal anchors", async () => {
55
const root = "docs";
@@ -28,47 +28,3 @@ it("documentation links point to existing internal anchors", async () => {
2828
}
2929
assert(errors.length === 0, new Error(`${errors.length} broken links:\n${errors.join("\n")}`));
3030
});
31-
32-
// Anchors can be derived from headers, or explicitly written as {#names}.
33-
function getAnchors(text) {
34-
const anchors = [];
35-
for (const [, header] of text.matchAll(/^#+ ([*\w][*().,\w\d -]+)\n/gm)) {
36-
anchors.push(
37-
header
38-
.replaceAll(/[^\w\d\s]+/g, " ")
39-
.trim()
40-
.replaceAll(/ +/g, "-")
41-
.toLowerCase()
42-
);
43-
}
44-
for (const [, anchor] of text.matchAll(/ \{#([\w\d-]+)\}/g)) {
45-
anchors.push(anchor);
46-
}
47-
return anchors;
48-
}
49-
50-
// Internal links.
51-
function getLinks(file, text) {
52-
const links = [];
53-
for (const match of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
54-
const [, link] = match;
55-
if (/^\w+:/.test(link)) continue; // absolute link with protocol
56-
const {pathname, hash} = new URL(link, new URL(file, "https://example.com/"));
57-
links.push({pathname, hash});
58-
}
59-
return links;
60-
}
61-
62-
// In source files, ignore comments.
63-
async function readMarkdownSource(f) {
64-
return (await readFile(f, "utf8")).replaceAll(/<!-- .*? -->/gs, "");
65-
}
66-
67-
// Recursively find all md files in the directory.
68-
async function* readMarkdownFiles(root, subpath = "/") {
69-
for (const fname of await readdir(root + subpath)) {
70-
if (fname.startsWith(".") || fname.endsWith(".js")) continue; // ignore .vitepress etc.
71-
if ((await stat(root + subpath + fname)).isDirectory()) yield* readMarkdownFiles(root, subpath + fname + "/");
72-
else if (fname.endsWith(".md")) yield subpath + fname;
73-
}
74-
}

0 commit comments

Comments
 (0)