1
0
mirror of https://github.com/d3/d3.git synced 2025-12-08 19:46:24 +00:00
d3/test/docs-test.js
Philippe Rivière cd09a142c2
test documentation links (#3673)
* test documentation links

* fix hard errors (non-existing files)

* more links (force is now defined in d3-force/simulation.md)

* this version is a bit more complicated but it doesn't crawl, and it works with the plot project too

* move eslint env

* async test

---------

Co-authored-by: Mike Bostock <mbostock@gmail.com>
2023-06-13 16:57:28 +00:00

75 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 (!hash || 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 = [];
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;
}
}