from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
UNVERIFIABLE = {
"a b c d e werden im deutschen energiemarkt verwendet": (
"a label inside a figure — pdftotext cannot see it; the neighbouring "
"§ 2.3 Wertegruppe F passage does verify"
),
}
QUOTE = re.compile(r'[*_]{1,2}"(.+?)"[*_]{1,2}', re.S)
GERMAN = re.compile(
r"[äöüßÄÖÜ]|\b(?:der|die|das|den|dem|des|eine|einer|einem|einen|und|oder|"
r"nicht|wird|werden|wurde|sind|ist|vom|zur|zum|dass|durch|sowie|soweit|"
r"jeweils|deutschen|nur|auch|einer|keine|kann|muss|soll)\b",
re.I,
)
def fold_math_alphanumerics(text: str) -> str:
out: list[str] = []
for ch in text:
cp = ord(ch)
if 0x1D400 <= cp <= 0x1D7CB:
index = (cp - 0x1D400) % 52
out.append(chr(ord("A") + index) if index < 26 else chr(ord("a") + index - 26))
elif 0x1D7CE <= cp <= 0x1D7FF:
out.append(chr(ord("0") + (cp - 0x1D7CE) % 10))
else:
out.append(ch)
return "".join(out)
def normalise(text: str) -> str:
text = fold_math_alphanumerics(text)
text = re.sub("\u00ad\\s*\n\\s*", "", text)
text = text.replace("", "") text = text.replace(" ", " ") text = re.sub(r"^[ \t]*(///|//!|//)[ \t]?", "", text, flags=re.M)
text = re.sub(r"-\s*\n\s*(?=[\dA-ZÄÖÜ])", "-", text)
text = re.sub(r"-\s*\n\s*", "", text) text = re.sub(r"(?<=[a-zäöüß])-(?=[a-zäöüß])", "", text)
text = text.replace("„", '"').replace("“", '"').replace("”", '"')
text = text.replace("’", "'").replace("‘", "'")
text = text.replace("–", "-").replace("—", "-").replace("−", "-")
text = re.sub(r"[*_`]", "", text)
text = text.replace("\\[", "[").replace("\\]", "]")
text = re.sub(r"\[\s*(?:…|\.\.\.)\s*\]", "…", text)
text = text.replace("...", "…")
return re.sub(r"\s+", " ", text).strip().lower()
def corpus(dirs: list[Path]) -> str:
parts: list[str] = []
for directory in dirs:
for pdf in sorted(directory.rglob("*.pdf")):
out = subprocess.run(
["pdftotext", "-layout", str(pdf), "-"],
capture_output=True,
text=True,
check=False,
)
parts.append(out.stdout)
return normalise("\n".join(parts))
def sources() -> list[Path]:
files = sorted((ROOT / "src").rglob("*.rs"))
files += sorted((ROOT / "site" / "content" / "docs").glob("*.md"))
files.append(ROOT / "README.md")
return [f for f in files if f.exists()]
def main() -> int:
dirs = [ROOT / "specs"] + [Path(a).expanduser() for a in sys.argv[1:]]
dirs = [d for d in dirs if d.is_dir()]
if not dirs:
print("no PDF directory found — run `just specs` first", file=sys.stderr)
return 1
haystack = corpus(dirs)
if not haystack:
print("empty corpus — is pdftotext installed?", file=sys.stderr)
return 1
checked = verified = skipped = 0
failures: list[tuple[str, str]] = []
for path in sources():
text = path.read_text(encoding="utf-8")
for raw in QUOTE.findall(text):
quote = normalise(raw)
if not GERMAN.search(quote):
continue
if len(quote.split(" ")) < 4:
continue
checked += 1
if any(marker in quote for marker in UNVERIFIABLE):
skipped += 1
continue
fragments = [f.strip() for f in quote.split("…") if len(f.strip()) > 12]
if fragments and all(f in haystack for f in fragments):
verified += 1
else:
missing = next((f for f in fragments if f not in haystack), quote)
failures.append((str(path.relative_to(ROOT)), missing))
print(f"{checked} German passages: {verified} verified, {skipped} known-unverifiable, "
f"{len(failures)} unmatched")
for path, fragment in failures:
print(f" {path}\n {fragment[:160]}")
if failures:
print(
"\nAn unmatched passage is a misquote until proven otherwise. Either the "
"wording is wrong, or the source is not in the corpus — add its directory "
"as an argument, or record it in UNVERIFIABLE with the reason.",
file=sys.stderr,
)
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())