Files
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Waiting to run
Test / Core Tests (push) Waiting to run
Test / Offline Coverage Tests (Python 3.10) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.11) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.12) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.13) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.9) (push) Waiting to run
Test / Full Coverage (Python 3.11) (push) Waiting to run
Test / Core Provider Tests (OpenAI) (push) Blocked by required conditions
Test / Core Provider Tests (Anthropic) (push) Blocked by required conditions
Test / Core Provider Tests (Google) (push) Blocked by required conditions
Test / Core Provider Tests (Other) (push) Blocked by required conditions
Test / Anthropic Tests (push) Blocked by required conditions
Test / Gemini Tests (push) Blocked by required conditions
Test / Google GenAI Tests (push) Blocked by required conditions
Test / Vertex AI Tests (push) Blocked by required conditions
Test / OpenAI Tests (push) Blocked by required conditions
Test / Writer Tests (push) Blocked by required conditions
Test / Auto Client Tests (push) Blocked by required conditions
ty / type-check (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

195 lines
5.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Check for broken internal links in documentation files.
Finds:
- Broken internal links (missing target files)
- Broken anchor links
- Orphaned pages (no incoming links)
"""
import argparse
import re
from pathlib import Path
def find_markdown_files(docs_dir: Path) -> list[Path]:
"""Find all markdown files in the docs directory."""
return list(docs_dir.rglob("*.md"))
def extract_links(content: str, file_path: Path) -> list[tuple[str, int]]: # noqa: ARG001
"""
Extract internal markdown links from content.
Returns:
List of (link_target, line_number) tuples
"""
links = []
# Match markdown links: [text](url)
for match in re.finditer(r"\[([^\]]+)\]\(([^)]+)\)", content):
link_text = match.group(1)
link_url = match.group(2)
line_num = content[: match.start()].count("\n") + 1
# Skip external links
if link_url.startswith(("http://", "https://", "mailto:", "#")):
continue
links.append((link_url, line_num))
return links
def resolve_link(link_url: str, source_file: Path, docs_dir: Path) -> tuple[bool, str]: # noqa: ARG001
"""
Resolve a relative link and check if target exists.
Returns:
(exists, resolved_path)
"""
# Split anchor if present
if "#" in link_url:
link_path, anchor = link_url.split("#", 1)
else:
link_path = link_url
anchor = None
# Resolve relative path
source_dir = source_file.parent
target_path = (source_dir / link_path).resolve()
# Check if file exists
exists = target_path.exists()
return exists, str(target_path)
def check_file(file_path: Path, docs_dir: Path) -> dict[str, list[tuple[str, int]]]:
"""Check all links in a file."""
issues = {}
try:
content = file_path.read_text(encoding="utf-8")
links = extract_links(content, file_path)
broken_links = []
for link_url, line_num in links:
exists, resolved_path = resolve_link(link_url, file_path, docs_dir)
if not exists:
broken_links.append((link_url, line_num))
if broken_links:
issues["broken_links"] = broken_links
return issues
except Exception as e:
return {"error": [(str(e), 0)]}
def find_orphaned_pages(files: list[Path], docs_dir: Path) -> set[Path]:
"""Find pages with no incoming links."""
all_files = set(files)
referenced_files = set()
for file_path in files:
try:
content = file_path.read_text(encoding="utf-8")
links = extract_links(content, file_path)
for link_url, _ in links:
exists, resolved_path = resolve_link(link_url, file_path, docs_dir)
if exists:
referenced_files.add(Path(resolved_path))
except Exception:
pass
# Files that are not referenced (orphaned)
orphaned = all_files - referenced_files
# Remove index pages and special files from orphaned list
orphaned = {
f
for f in orphaned
if not any(
part in str(f)
for part in ["index.md", "AGENT.md", "repository-overview.md"]
)
}
return orphaned
def main():
parser = argparse.ArgumentParser(
description="Check for broken internal links in documentation"
)
parser.add_argument(
"--docs-dir",
type=Path,
default=Path("docs"),
help="Directory containing documentation files (default: docs)",
)
parser.add_argument(
"--summary",
action="store_true",
help="Show only summary statistics",
)
parser.add_argument(
"--file",
type=Path,
help="Check a single file instead of all files",
)
parser.add_argument(
"--find-orphans",
action="store_true",
help="Find orphaned pages with no incoming links",
)
args = parser.parse_args()
if args.file:
files = [args.file]
else:
files = find_markdown_files(args.docs_dir)
all_issues = {}
total_broken = 0
for file_path in files:
issues = check_file(file_path, args.docs_dir)
if issues:
all_issues[str(file_path)] = issues
if "broken_links" in issues:
total_broken += len(issues["broken_links"])
if args.summary:
print("Summary Statistics:")
print("=" * 60)
print(f" Files with broken links: {len(all_issues)}")
print(f" Total broken links: {total_broken}")
else:
# Detailed report
for file_path, issues in sorted(all_issues.items()):
if "broken_links" in issues:
print(f"\n{file_path}:")
for link_url, line_num in issues["broken_links"]:
print(f" Line {line_num}: {link_url}")
if args.find_orphans:
orphaned = find_orphaned_pages(files, args.docs_dir)
if orphaned:
print("\n" + "=" * 60)
print("Orphaned Pages (no incoming links):")
print("=" * 60)
for file_path in sorted(orphaned):
print(f" {file_path}")
print(f"\nTotal orphaned pages: {len(orphaned)}")
print(f"\nTotal files checked: {len(files)}")
if __name__ == "__main__":
main()