#!/usr/bin/env python3 """Validate public documentation and its bilingual test checklist.""" import re import sys from pathlib import Path REPO = Path(__file__).resolve().parent.parent PUBLIC_DOCS = REPO / "public-docs" # A page is capped so it stays readable in one sitting, not to make authors # compress sentences: a contents table, one-fact lines, and a diagram all cost # lines that a reader gets back. MAX_LINES = 300 # API is the contract page; the complete test checklists also need room for # every case and the commands needed to run them. LINE_LIMIT_EXEMPT = frozenset({ "reference/api.md", "development/testing.md", "development/testing.ko.md", }) KOREAN_ALLOWED = frozenset({"development/testing.ko.md"}) LINK_RE = re.compile(r"\]\(\s*\s]+)>?(?:\s+\"[^\"]*\")?\s*\)") PUBLIC_PATH_RE = re.compile(r"public-docs/[\w./-]+\.(?:md|py|sh)") LEGACY_PATH_RE = re.compile(r"(? bool: return any(part in IGNORED_PARTS for part in path.parts) def check_public_docs() -> list[str]: problems: list[str] = [] root = PUBLIC_DOCS.resolve() for doc in sorted(PUBLIC_DOCS.rglob("*.md")): relative = doc.relative_to(REPO) public_path = doc.relative_to(PUBLIC_DOCS).as_posix() if doc.is_symlink(): target = doc.resolve() if not target.is_file(): problems.append(f"{relative}: broken symbolic link") elif not target.is_relative_to(root): problems.append(f"{relative}: symbolic link leaves public-docs") continue text = doc.read_text(encoding="utf-8") lines = text.splitlines() if public_path not in LINE_LIMIT_EXEMPT and len(lines) > MAX_LINES: problems.append(f"{relative}: {len(lines)} lines exceeds {MAX_LINES}") for line_number, line in enumerate(lines, start=1): if public_path not in KOREAN_ALLOWED and KOREAN_RE.search(line): problems.append( f"{relative}:{line_number}: Korean text is only allowed in " "development/testing.ko.md" ) in_fenced_code = False for line_number, line in enumerate(lines, start=1): if re.match(r"^\s*(`{3,}|~{3,})", line): in_fenced_code = not in_fenced_code continue if in_fenced_code: continue for target in LINK_RE.findall(line): if target.startswith(SKIP_PREFIXES): continue path_part = target.split("#", 1)[0] if not path_part: continue if not (doc.parent / path_part).resolve().exists(): problems.append(f"{relative}:{line_number}: broken link {target}") return problems def check_source_references() -> list[str]: problems: list[str] = [] seen: set[Path] = set() for pattern in SOURCE_GLOBS: for path in sorted(REPO.glob(pattern)): if is_ignored(path) or path in seen: continue seen.add(path) try: text = path.read_text(encoding="utf-8") except UnicodeDecodeError: continue for line_number, line in enumerate(text.splitlines(), start=1): for reference in LEGACY_PATH_RE.findall(line): relative = path.relative_to(REPO) problems.append(f"{relative}:{line_number}: private docs reference {reference}") for reference in PUBLIC_PATH_RE.findall(line): if not (REPO / reference).exists(): relative = path.relative_to(REPO) problems.append(f"{relative}:{line_number}: broken reference {reference}") return problems def main() -> int: problems = check_public_docs() + check_source_references() if problems: print(f"Public documentation check failed with {len(problems)} problem(s).") for problem in problems: print(f" {problem}") return 1 print( "Public documentation links and language rules pass; files are at most " f"{MAX_LINES} lines except {', '.join(sorted(LINE_LIMIT_EXEMPT))}." ) return 0 if __name__ == "__main__": sys.exit(main())