참고소스 수정본
This commit is contained in:
194
참고/instructor-main/scripts/check_links.py
Normal file
194
참고/instructor-main/scripts/check_links.py
Normal file
@@ -0,0 +1,194 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user