Files
archivebox/bin/lychee_preprocess.py

44 lines
2.1 KiB
Python
Executable File

#!/usr/bin/env -S uv run --no-project --script
import pathlib
import re
import sys
from urllib.parse import unquote, urlsplit
p = pathlib.Path(sys.argv[1])
text = p.read_text()
if p.suffix not in (".md", ".html", ".rst"):
# Preserve source line numbers, but feed only literal documentation URLs to Lychee.
pattern = r'https?://(?:github\.com/(?:ArchiveBox|pirate)/ArchiveBox(?:/wiki|/blob/|/?#)|raw\.githubusercontent\.com/wiki/ArchiveBox/ArchiveBox/|docs\.archivebox\.io)[^\s<>\)\]"\x27`\\\[]*'
text = "\n".join(" ".join(u for u in re.findall(pattern, line) if not re.search(r"[{}]", u)) for line in text.splitlines())
if p.suffix == ".md" and p.parent.name == "docs":
pages = {page.stem.lower(): page.stem for page in p.parent.glob("*.md")}
def wikilink(match):
label, _, target = match[1].partition("|")
page, sep, fragment = (target or label).replace(" ", "-").partition("#")
page = pages.get(page.lower(), page)
return f"[{label}]({page}.md{sep}{fragment})"
text = re.sub(r"\[\[([^]\n]+)\]\]", wikilink, text)
if p.suffix == ".md":
# Lychee folds local Markdown fragments to lowercase; GitHub heading IDs are lowercase.
# Preserve explicitly declared mixed-case HTML IDs, but reject heading case typos.
errors = []
for match in re.finditer(r'\]\(([^\s)]+)\)|href=["\']([^"\']+)', text):
url = urlsplit(match[1] or match[2])
fragment = unquote(url.fragment)
if url.scheme or url.netloc or fragment == fragment.lower():
continue
target = p.parent / unquote(url.path) if url.path else p
if not target.suffix:
target = target.with_suffix(".md")
if target.suffix != ".md" or not target.is_file():
continue # Lychee handles missing files and non-Markdown targets.
ids = re.findall(r'(?:id|name)=["\']([^"\']+)', target.read_text())
if fragment not in ids:
line = text[: match.start()].count("\n") + 1
errors.append(f"{p}:{line}: heading fragment #{fragment} must match lowercase GitHub IDs")
if errors:
sys.exit("\n".join(errors))
print(text)