1
0
mirror of https://github.com/d3/d3.git synced 2025-12-08 19:46:24 +00:00
d3/test/docs-test.js
2023-06-13 16:15:07 -07:00

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;
}
}