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
144 lines
4.0 KiB
Python
Executable File
144 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Validate heading structure in documentation files.
|
|
|
|
Checks for:
|
|
- Multiple H1 tags (should only have one)
|
|
- Heading hierarchy violations (e.g., H1 → H3 skipping H2)
|
|
- Missing H1 tags
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
from collections import defaultdict
|
|
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_headings(content: str) -> list[tuple[int, str, int]]:
|
|
"""
|
|
Extract all headings from markdown content.
|
|
|
|
Returns:
|
|
List of (level, text, line_number) tuples
|
|
"""
|
|
headings = []
|
|
lines = content.split("\n")
|
|
|
|
for line_num, line in enumerate(lines, 1):
|
|
# Match markdown headings: # Title, ## Title, etc.
|
|
match = re.match(r"^(#{1,6})\s+(.+)$", line)
|
|
if match:
|
|
level = len(match.group(1))
|
|
text = match.group(2).strip()
|
|
headings.append((level, text, line_num))
|
|
|
|
return headings
|
|
|
|
|
|
def validate_headings(headings: list[tuple[int, str, int]]) -> dict[str, list[str]]:
|
|
"""Validate heading structure."""
|
|
issues = {}
|
|
|
|
if not headings:
|
|
issues["no_headings"] = ["No headings found in file"]
|
|
return issues
|
|
|
|
# Check for H1
|
|
h1_headings = [h for h in headings if h[0] == 1]
|
|
if not h1_headings:
|
|
issues["missing_h1"] = ["No H1 heading found"]
|
|
elif len(h1_headings) > 1:
|
|
issues["multiple_h1"] = [
|
|
f"Line {line}: {text}" for level, text, line in h1_headings
|
|
]
|
|
|
|
# Check heading hierarchy
|
|
prev_level = 0
|
|
hierarchy_violations = []
|
|
for level, text, line_num in headings:
|
|
if prev_level > 0 and level > prev_level + 1:
|
|
hierarchy_violations.append(
|
|
f"Line {line_num}: Skipped from H{prev_level} to H{level}: {text[:50]}"
|
|
)
|
|
prev_level = level
|
|
|
|
if hierarchy_violations:
|
|
issues["hierarchy_violations"] = hierarchy_violations
|
|
|
|
return issues
|
|
|
|
|
|
def process_file(file_path: Path) -> dict[str, list[str]]:
|
|
"""Process a single file and return issues."""
|
|
try:
|
|
content = file_path.read_text(encoding="utf-8")
|
|
headings = extract_headings(content)
|
|
return validate_headings(headings)
|
|
except Exception as e:
|
|
return {"error": [str(e)]}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate heading structure in documentation files"
|
|
)
|
|
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="Validate a single file instead of all files",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.file:
|
|
files = [args.file]
|
|
else:
|
|
files = find_markdown_files(args.docs_dir)
|
|
|
|
all_issues = {}
|
|
total_counts = defaultdict(int)
|
|
|
|
for file_path in files:
|
|
issues = process_file(file_path)
|
|
if issues:
|
|
all_issues[str(file_path)] = issues
|
|
for issue_type, messages in issues.items():
|
|
total_counts[issue_type] += len(messages)
|
|
|
|
if args.summary:
|
|
print("Summary Statistics:")
|
|
print("=" * 60)
|
|
for issue_type, count in sorted(total_counts.items()):
|
|
print(f" {issue_type.replace('_', ' ').title()}: {count}")
|
|
else:
|
|
# Detailed report
|
|
for file_path, issues in sorted(all_issues.items()):
|
|
print(f"\n{file_path}:")
|
|
for issue_type, messages in issues.items():
|
|
print(f" {issue_type.replace('_', ' ').title()}:")
|
|
for message in messages:
|
|
print(f" {message}")
|
|
|
|
print(f"\nTotal files checked: {len(files)}")
|
|
print(f"Files with issues: {len(all_issues)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|