mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-23 11:17:13 +00:00
docs: a page for every tool and every library, with real output
This commit is contained in:
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that every link between the markdown files goes somewhere.
|
||||
|
||||
Documentation that links to a page that has been renamed is worse than
|
||||
documentation that does not link at all, because the reader trusts it. Run
|
||||
from the repository root:
|
||||
|
||||
python3 tools/check-links.py
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
|
||||
|
||||
|
||||
def prose(text: str) -> str:
|
||||
"""The document with its fenced code blocks taken out.
|
||||
|
||||
An example that happens to contain a link is an example, not a link.
|
||||
"""
|
||||
out, fenced = [], False
|
||||
for line in text.split("\n"):
|
||||
if line.startswith("```"):
|
||||
fenced = not fenced
|
||||
continue
|
||||
if not fenced:
|
||||
out.append(line)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def anchors(text: str) -> set[str]:
|
||||
"""The headings of a document, as the fragments that reach them."""
|
||||
found = set()
|
||||
for line in prose(text).split("\n"):
|
||||
if line.startswith("#"):
|
||||
title = line.lstrip("#").strip().lower()
|
||||
found.add(re.sub(r"[^a-z0-9 -]", "", title).replace(" ", "-"))
|
||||
return found
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = pathlib.Path(".").resolve()
|
||||
# Fixtures are not documentation: a parity case is deliberately malformed
|
||||
# markdown, and a link in one is part of what is being tested.
|
||||
skip = {"target", "cases", "golden"}
|
||||
files = sorted(
|
||||
path
|
||||
for path in root.rglob("*.md")
|
||||
if skip.isdisjoint(path.parts) and not path.name.startswith(".")
|
||||
)
|
||||
headings = {path: anchors(path.read_text(encoding="utf-8")) for path in files}
|
||||
|
||||
broken = []
|
||||
for path in files:
|
||||
for target in LINK.findall(prose(path.read_text(encoding="utf-8"))):
|
||||
if target.startswith(("http://", "https://", "mailto:")):
|
||||
continue
|
||||
where, _, fragment = target.partition("#")
|
||||
dest = (path.parent / where).resolve() if where else path
|
||||
if not dest.exists():
|
||||
broken.append(f"{path.relative_to(root)} -> {target}: no such file")
|
||||
elif fragment and dest in headings and fragment not in headings[dest]:
|
||||
broken.append(f"{path.relative_to(root)} -> {target}: no such heading")
|
||||
|
||||
for line in broken:
|
||||
print(f"broken link: {line}", file=sys.stderr)
|
||||
print(f"{len(files)} files, {len(broken)} broken links")
|
||||
return 1 if broken else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user