- Forum topics and classifieds now handle NULL views_count gracefully - Prevents TypeError when incrementing view counter
255 lines
8.7 KiB
Python
255 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Verify all Mermaid diagrams in architecture documentation.
|
||
|
||
Checks for:
|
||
- Proper Mermaid code block syntax
|
||
- Common syntax errors
|
||
- Readability issues (too long lines, etc.)
|
||
- Missing diagram titles/descriptions
|
||
"""
|
||
|
||
import re
|
||
import os
|
||
from pathlib import Path
|
||
from typing import List, Dict, Tuple
|
||
|
||
class DiagramVerifier:
|
||
def __init__(self):
|
||
self.errors = []
|
||
self.warnings = []
|
||
self.info = []
|
||
self.diagrams_found = 0
|
||
|
||
def verify_file(self, filepath: Path) -> Dict:
|
||
"""Verify a single markdown file."""
|
||
with open(filepath, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
file_result = {
|
||
'file': str(filepath),
|
||
'diagrams': 0,
|
||
'errors': [],
|
||
'warnings': [],
|
||
'info': []
|
||
}
|
||
|
||
# Find all Mermaid code blocks
|
||
mermaid_pattern = r'```mermaid\n(.*?)```'
|
||
matches = re.finditer(mermaid_pattern, content, re.DOTALL)
|
||
|
||
for idx, match in enumerate(matches, 1):
|
||
self.diagrams_found += 1
|
||
file_result['diagrams'] += 1
|
||
diagram_content = match.group(1)
|
||
|
||
# Check for common issues
|
||
issues = self.check_diagram(diagram_content, idx)
|
||
file_result['errors'].extend(issues['errors'])
|
||
file_result['warnings'].extend(issues['warnings'])
|
||
file_result['info'].extend(issues['info'])
|
||
|
||
return file_result
|
||
|
||
def check_diagram(self, diagram: str, diagram_num: int) -> Dict:
|
||
"""Check a single diagram for issues."""
|
||
issues = {'errors': [], 'warnings': [], 'info': []}
|
||
|
||
lines = diagram.strip().split('\n')
|
||
if not lines:
|
||
issues['errors'].append(f"Diagram {diagram_num}: Empty diagram")
|
||
return issues
|
||
|
||
first_line = lines[0].strip()
|
||
|
||
# Check for valid diagram type
|
||
valid_types = [
|
||
'graph', 'flowchart', 'sequenceDiagram', 'classDiagram',
|
||
'stateDiagram', 'erDiagram', 'gantt', 'pie', 'journey',
|
||
'gitGraph', 'C4Context', 'C4Container', 'C4Component'
|
||
]
|
||
|
||
# Also accept diagrams starting with comments or init blocks
|
||
valid_prefixes = ['%%', '%%{init:']
|
||
|
||
# Find the first non-comment line to check for diagram type
|
||
diagram_type_line = first_line
|
||
for line in lines:
|
||
stripped = line.strip()
|
||
if not stripped.startswith('%%'):
|
||
diagram_type_line = stripped
|
||
break
|
||
|
||
diagram_type = diagram_type_line.split()[0] if diagram_type_line else ''
|
||
is_comment_or_init = any(first_line.startswith(p) for p in valid_prefixes)
|
||
has_valid_type = any(diagram_type_line.startswith(t) for t in valid_types)
|
||
|
||
if not has_valid_type and not is_comment_or_init:
|
||
issues['errors'].append(
|
||
f"Diagram {diagram_num}: Invalid or missing diagram type. "
|
||
f"First line: '{first_line[:50]}...'"
|
||
)
|
||
|
||
# Check for extremely long lines (readability)
|
||
for line_num, line in enumerate(lines, 1):
|
||
if len(line) > 200:
|
||
issues['warnings'].append(
|
||
f"Diagram {diagram_num}, Line {line_num}: "
|
||
f"Very long line ({len(line)} chars) may affect readability"
|
||
)
|
||
|
||
# Check for common syntax errors
|
||
|
||
# 1. Unmatched quotes
|
||
for line_num, line in enumerate(lines, 1):
|
||
# Count quotes (ignoring escaped quotes)
|
||
quote_count = line.count('"') - line.count('\\"')
|
||
if quote_count % 2 != 0:
|
||
issues['warnings'].append(
|
||
f"Diagram {diagram_num}, Line {line_num}: "
|
||
f"Unmatched quotes detected"
|
||
)
|
||
|
||
# 2. Unclosed brackets/parentheses
|
||
for line_num, line in enumerate(lines, 1):
|
||
open_count = line.count('[') + line.count('(') + line.count('{')
|
||
close_count = line.count(']') + line.count(')') + line.count('}')
|
||
# Note: This is a simple check, might have false positives for multi-line
|
||
# statements, but it's good for catching obvious errors
|
||
|
||
# 3. Check for common Mermaid syntax patterns
|
||
diagram_str = '\n'.join(lines)
|
||
|
||
# For sequence diagrams, check for proper participant declarations
|
||
if 'sequenceDiagram' in first_line:
|
||
if 'participant' not in diagram_str and 'actor' not in diagram_str:
|
||
issues['info'].append(
|
||
f"Diagram {diagram_num}: Sequence diagram without explicit "
|
||
f"participant/actor declarations (auto-generated)"
|
||
)
|
||
|
||
# For flowcharts, check for proper node definitions
|
||
if 'flowchart' in first_line or 'graph' in first_line:
|
||
# Check for at least one node definition
|
||
if not re.search(r'\w+\[.*?\]|\w+\(.*?\)|\w+\{.*?\}', diagram_str):
|
||
issues['warnings'].append(
|
||
f"Diagram {diagram_num}: Flowchart might be missing node definitions"
|
||
)
|
||
|
||
# For ERD, check for entity definitions
|
||
if 'erDiagram' in first_line:
|
||
if '{' not in diagram_str or '}' not in diagram_str:
|
||
issues['warnings'].append(
|
||
f"Diagram {diagram_num}: ERD might be missing entity attribute blocks"
|
||
)
|
||
|
||
# Check diagram size (too many lines might be hard to render)
|
||
if len(lines) > 500:
|
||
issues['warnings'].append(
|
||
f"Diagram {diagram_num}: Very large diagram ({len(lines)} lines) "
|
||
f"might have rendering issues"
|
||
)
|
||
elif len(lines) > 200:
|
||
issues['info'].append(
|
||
f"Diagram {diagram_num}: Large diagram ({len(lines)} lines)"
|
||
)
|
||
|
||
return issues
|
||
|
||
def verify_all(self, base_path: Path) -> List[Dict]:
|
||
"""Verify all markdown files in the directory."""
|
||
results = []
|
||
|
||
# Find all .md files
|
||
md_files = sorted(base_path.rglob('*.md'))
|
||
|
||
for md_file in md_files:
|
||
result = self.verify_file(md_file)
|
||
results.append(result)
|
||
|
||
return results
|
||
|
||
def print_report(self, results: List[Dict]):
|
||
"""Print a formatted report."""
|
||
print("=" * 80)
|
||
print("MERMAID DIAGRAM VERIFICATION REPORT")
|
||
print("=" * 80)
|
||
print()
|
||
|
||
total_files = len(results)
|
||
total_diagrams = sum(r['diagrams'] for r in results)
|
||
total_errors = sum(len(r['errors']) for r in results)
|
||
total_warnings = sum(len(r['warnings']) for r in results)
|
||
total_info = sum(len(r['info']) for r in results)
|
||
|
||
print(f"📊 Summary:")
|
||
print(f" - Files checked: {total_files}")
|
||
print(f" - Diagrams found: {total_diagrams}")
|
||
print(f" - Errors: {total_errors}")
|
||
print(f" - Warnings: {total_warnings}")
|
||
print(f" - Info messages: {total_info}")
|
||
print()
|
||
|
||
# Print details for each file
|
||
for result in results:
|
||
if result['diagrams'] == 0:
|
||
continue
|
||
|
||
# Handle both relative and absolute paths
|
||
try:
|
||
filename = Path(result['file']).relative_to(Path.cwd())
|
||
except ValueError:
|
||
filename = result['file']
|
||
print(f"\n{'─' * 80}")
|
||
print(f"📄 {filename}")
|
||
print(f" Diagrams: {result['diagrams']}")
|
||
|
||
if result['errors']:
|
||
print(f"\n ❌ ERRORS ({len(result['errors'])}):")
|
||
for error in result['errors']:
|
||
print(f" • {error}")
|
||
|
||
if result['warnings']:
|
||
print(f"\n ⚠️ WARNINGS ({len(result['warnings'])}):")
|
||
for warning in result['warnings']:
|
||
print(f" • {warning}")
|
||
|
||
if result['info']:
|
||
print(f"\n ℹ️ INFO ({len(result['info'])}):")
|
||
for info in result['info']:
|
||
print(f" • {info}")
|
||
|
||
print("\n" + "=" * 80)
|
||
|
||
if total_errors == 0 and total_warnings == 0:
|
||
print("✅ All diagrams passed verification!")
|
||
elif total_errors == 0:
|
||
print("✅ No errors found (warnings are informational)")
|
||
else:
|
||
print("❌ Errors found - please review and fix")
|
||
|
||
print("=" * 80)
|
||
|
||
return total_errors == 0
|
||
|
||
|
||
def main():
|
||
"""Main entry point."""
|
||
base_path = Path('./docs/architecture')
|
||
|
||
if not base_path.exists():
|
||
print(f"❌ Directory not found: {base_path}")
|
||
return False
|
||
|
||
verifier = DiagramVerifier()
|
||
results = verifier.verify_all(base_path)
|
||
success = verifier.print_report(results)
|
||
|
||
return success
|
||
|
||
|
||
if __name__ == '__main__':
|
||
import sys
|
||
success = main()
|
||
sys.exit(0 if success else 1)
|