mirror of
https://github.com/d3/d3.git
synced 2025-12-08 19:46:24 +00:00
74 lines
2.4 KiB
JavaScript
74 lines
2.4 KiB
JavaScript
import assert from "assert";
|
|
import {readdir, readFile, stat} from "fs/promises";
|
|
|
|
it("documentation links point to existing internal anchors", async () => {
|
|
const root = "docs";
|
|
|
|
// Crawl all files, read their links and anchors.
|
|
const anchors = new Map();
|
|
const links = [];
|
|
for await (const file of readMarkdownFiles(root)) {
|
|
const text = await readMarkdownSource(root + file);
|
|
anchors.set(file, getAnchors(text));
|
|
for (const {pathname, hash} of getLinks(file, text)) {
|
|
links.push({source: file, target: pathname, hash});
|
|
}
|
|
}
|
|
|
|
// Check for broken links.
|
|
let errors = [];
|
|
for (let {source, target, hash} of links) {
|
|
if (!target.endsWith(".md")) {
|
|
errors.push(`- ${source} points to ${target} instead of ${target}.md.`);
|
|
target += ".md";
|
|
}
|
|
if (anchors.get(target)?.includes(hash.slice(1))) continue;
|
|
errors.push(`- ${source} points to missing ${target}${hash}.`);
|
|
}
|
|
assert(errors.length === 0, new Error(`${errors.length} broken links:\n${errors.join("\n")}`));
|
|
});
|
|
|
|
// Anchors can be derived from headers, or explicitly written as {#names}.
|
|
function getAnchors(text) {
|
|
const anchors = [""]; // empty string for non-fragment links
|
|
for (const [, header] of text.matchAll(/^#+ ([*\w][*().,\w\d -]+)\n/gm)) {
|
|
anchors.push(
|
|
header
|
|
.replaceAll(/[^\w\d\s]+/g, " ")
|
|
.trim()
|
|
.replaceAll(/ +/g, "-")
|
|
.toLowerCase()
|
|
);
|
|
}
|
|
for (const [, anchor] of text.matchAll(/ \{#([\w\d-]+)\}/g)) {
|
|
anchors.push(anchor);
|
|
}
|
|
return anchors;
|
|
}
|
|
|
|
// Internal links.
|
|
function getLinks(file, text) {
|
|
const links = [];
|
|
for (const match of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
|
|
const [, link] = match;
|
|
if (/^\w+:/.test(link)) continue; // absolute link with protocol
|
|
const {pathname, hash} = new URL(link, new URL(file, "https://example.com/"));
|
|
links.push({pathname, hash});
|
|
}
|
|
return links;
|
|
}
|
|
|
|
// In source files, ignore comments.
|
|
async function readMarkdownSource(f) {
|
|
return (await readFile(f, "utf8")).replaceAll(/<!-- .*? -->/gs, "");
|
|
}
|
|
|
|
// Recursively find all md files in the directory.
|
|
async function* readMarkdownFiles(root, subpath = "/") {
|
|
for (const fname of await readdir(root + subpath)) {
|
|
if (fname.startsWith(".")) continue; // ignore .vitepress etc.
|
|
if ((await stat(root + subpath + fname)).isDirectory()) yield* readMarkdownFiles(root, subpath + fname + "/");
|
|
else if (fname.endsWith(".md")) yield subpath + fname;
|
|
}
|
|
}
|