492 lines
17 KiB
Python
492 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
HTML Documentation Generator
|
|
Generates beautiful HTML documentation from project analysis.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Dict, List, Any, Optional
|
|
|
|
# Try to import yaml
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
yaml = None
|
|
|
|
|
|
def load_template(template_path: Path) -> str:
|
|
"""Load the HTML template."""
|
|
with open(template_path, 'r', encoding='utf-8') as f:
|
|
return f.read()
|
|
|
|
|
|
def load_analysis(analysis_path: Path) -> Dict[str, Any]:
|
|
"""Load project analysis from YAML or JSON."""
|
|
with open(analysis_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
if yaml and (analysis_path.suffix in ['.yml', '.yaml']):
|
|
return yaml.safe_load(content)
|
|
return json.loads(content)
|
|
|
|
|
|
def escape_html(text: str) -> str:
|
|
"""Escape HTML special characters."""
|
|
if not text:
|
|
return ''
|
|
return (str(text)
|
|
.replace('&', '&')
|
|
.replace('<', '<')
|
|
.replace('>', '>')
|
|
.replace('"', '"')
|
|
.replace("'", '''))
|
|
|
|
|
|
def generate_capabilities_html(capabilities: List[Dict]) -> str:
|
|
"""Generate HTML for capabilities cards."""
|
|
icons = ['✨', '⚡', '🔐', '📊', '🚀', '💡', '🎯', '🔧']
|
|
html_parts = []
|
|
|
|
for i, cap in enumerate(capabilities[:8]):
|
|
icon = icons[i % len(icons)]
|
|
html_parts.append(f'''
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-icon">{icon}</div>
|
|
<div class="card-title">{escape_html(cap.get('capability', cap.get('name', 'Feature')))}</div>
|
|
</div>
|
|
<p>{escape_html(cap.get('description', ''))}</p>
|
|
</div>''')
|
|
|
|
return '\n'.join(html_parts)
|
|
|
|
|
|
def generate_prerequisites_html(prerequisites: List[Dict]) -> str:
|
|
"""Generate HTML for prerequisites table rows."""
|
|
html_parts = []
|
|
|
|
for prereq in prerequisites:
|
|
tool = prereq.get('tool', prereq.get('name', ''))
|
|
purpose = prereq.get('purpose', prereq.get('description', ''))
|
|
html_parts.append(f'''
|
|
<tr>
|
|
<td><code>{escape_html(tool)}</code></td>
|
|
<td>{escape_html(purpose)}</td>
|
|
</tr>''')
|
|
|
|
return '\n'.join(html_parts) if html_parts else '''
|
|
<tr>
|
|
<td><code>Node.js</code></td>
|
|
<td>JavaScript runtime environment</td>
|
|
</tr>'''
|
|
|
|
|
|
def generate_tech_stack_html(tech_stack: Dict) -> str:
|
|
"""Generate HTML for technology stack table rows."""
|
|
html_parts = []
|
|
|
|
stack_items = [
|
|
('Language', tech_stack.get('language')),
|
|
('Framework', tech_stack.get('framework')),
|
|
('Database', tech_stack.get('database')),
|
|
('UI Framework', tech_stack.get('ui_framework')),
|
|
]
|
|
|
|
purposes = {
|
|
'TypeScript': 'Type-safe JavaScript for better code quality',
|
|
'JavaScript': 'Programming language for web applications',
|
|
'Python': 'General-purpose programming language',
|
|
'Next.js': 'Full-stack React framework with SSR',
|
|
'React': 'Component-based UI library',
|
|
'Vue.js': 'Progressive JavaScript framework',
|
|
'Express': 'Minimal web server framework',
|
|
'Prisma': 'Type-safe database ORM',
|
|
'MongoDB': 'NoSQL document database',
|
|
'PostgreSQL': 'Relational database',
|
|
'Tailwind CSS': 'Utility-first CSS framework',
|
|
'Material UI': 'React component library',
|
|
}
|
|
|
|
for layer, tech in stack_items:
|
|
if tech:
|
|
purpose = purposes.get(tech, f'{tech} for {layer.lower()}')
|
|
html_parts.append(f'''
|
|
<tr>
|
|
<td>{escape_html(layer)}</td>
|
|
<td><span class="badge badge-primary">{escape_html(tech)}</span></td>
|
|
<td>{escape_html(purpose)}</td>
|
|
</tr>''')
|
|
|
|
# Add key dependencies
|
|
for dep in tech_stack.get('key_dependencies', [])[:5]:
|
|
html_parts.append(f'''
|
|
<tr>
|
|
<td>Dependency</td>
|
|
<td><span class="badge badge-info">{escape_html(dep.get('name', ''))}</span></td>
|
|
<td>{escape_html(dep.get('purpose', ''))}</td>
|
|
</tr>''')
|
|
|
|
return '\n'.join(html_parts)
|
|
|
|
|
|
def generate_directory_structure(structure: Dict) -> str:
|
|
"""Generate directory structure text."""
|
|
lines = ['project/']
|
|
|
|
for i, dir_info in enumerate(structure.get('directories', [])[:10]):
|
|
prefix = '└── ' if i == len(structure.get('directories', [])) - 1 else '├── '
|
|
path = dir_info.get('path', '')
|
|
purpose = dir_info.get('purpose', '')
|
|
lines.append(f"{prefix}{path}/ # {purpose}")
|
|
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def generate_features_html(features: List[Dict]) -> str:
|
|
"""Generate HTML for features section."""
|
|
icons = ['🔐', '👤', '🔌', '💾', '📁', '🔍', '📧', '⚙️']
|
|
html_parts = []
|
|
|
|
for i, feature in enumerate(features[:8]):
|
|
icon = icons[i % len(icons)]
|
|
name = feature.get('name', 'Feature')
|
|
description = feature.get('description', '')
|
|
technical_notes = feature.get('technical_notes', '')
|
|
files = feature.get('files', [])
|
|
|
|
files_html = '\n'.join([f'<li><code>{escape_html(f)}</code></li>' for f in files[:3]])
|
|
|
|
html_parts.append(f'''
|
|
<div class="feature-item">
|
|
<div class="feature-icon">{icon}</div>
|
|
<div class="feature-content">
|
|
<h4>{escape_html(name)}</h4>
|
|
<p>{escape_html(description)}</p>
|
|
|
|
<details>
|
|
<summary>
|
|
🔧 Technical Details
|
|
<span class="tech-badge">For Engineers</span>
|
|
</summary>
|
|
<div>
|
|
<p>{escape_html(technical_notes)}</p>
|
|
<p><strong>Key Files:</strong></p>
|
|
<ul>
|
|
{files_html}
|
|
</ul>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>''')
|
|
|
|
return '\n'.join(html_parts) if html_parts else '''
|
|
<div class="feature-item">
|
|
<div class="feature-icon">✨</div>
|
|
<div class="feature-content">
|
|
<h4>Core Functionality</h4>
|
|
<p>Main features of the application.</p>
|
|
</div>
|
|
</div>'''
|
|
|
|
|
|
def generate_api_endpoints_html(endpoints: List[Dict]) -> str:
|
|
"""Generate HTML for API endpoints."""
|
|
if not endpoints:
|
|
return '''
|
|
<p>No API endpoints detected. This project may use a different API pattern or may not have an API layer.</p>'''
|
|
|
|
html_parts = ['<table>', '<thead>', '<tr>', '<th>Method</th>', '<th>Endpoint</th>', '<th>Description</th>', '</tr>', '</thead>', '<tbody>']
|
|
|
|
for endpoint in endpoints[:15]:
|
|
method = endpoint.get('method', 'GET')
|
|
method_class = f'method-{method.lower()}'
|
|
path = endpoint.get('path', '')
|
|
description = endpoint.get('description', '')
|
|
|
|
html_parts.append(f'''
|
|
<tr>
|
|
<td><span class="method {method_class}">{escape_html(method)}</span></td>
|
|
<td><code>{escape_html(path)}</code></td>
|
|
<td>{escape_html(description)}</td>
|
|
</tr>''')
|
|
|
|
html_parts.extend(['</tbody>', '</table>'])
|
|
return '\n'.join(html_parts)
|
|
|
|
|
|
def generate_components_html(components: List[Dict]) -> str:
|
|
"""Generate HTML for component catalog."""
|
|
if not components:
|
|
return '''
|
|
<p>No UI components detected. This project may not have a frontend layer or uses a different component pattern.</p>'''
|
|
|
|
html_parts = []
|
|
|
|
for comp in components[:10]:
|
|
name = comp.get('name', 'Component')
|
|
description = comp.get('description', f'{name} component')
|
|
path = comp.get('path', '')
|
|
props = comp.get('props', 'See source file')
|
|
|
|
html_parts.append(f'''
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-icon">🧩</div>
|
|
<div class="card-title">{escape_html(name)}</div>
|
|
</div>
|
|
<p>{escape_html(description)}</p>
|
|
<p><code>{escape_html(path)}</code></p>
|
|
|
|
<details>
|
|
<summary>
|
|
🔧 Props & Usage
|
|
<span class="tech-badge">Technical</span>
|
|
</summary>
|
|
<div>
|
|
<p><strong>Props:</strong> {escape_html(props)}</p>
|
|
|
|
<h4>Usage Example</h4>
|
|
<pre><code><{escape_html(name)} /></code></pre>
|
|
</div>
|
|
</details>
|
|
</div>''')
|
|
|
|
return '\n'.join(html_parts)
|
|
|
|
|
|
def generate_data_models_html(models: List[Dict]) -> str:
|
|
"""Generate HTML for data models."""
|
|
if not models:
|
|
return '''
|
|
<p>No data models detected. This project may not use a database or uses a different data pattern.</p>'''
|
|
|
|
html_parts = []
|
|
|
|
for model in models[:10]:
|
|
name = model.get('name', 'Model')
|
|
description = model.get('description', f'{name} data model')
|
|
fields = model.get('fields', [])
|
|
|
|
fields_html = ''
|
|
if fields:
|
|
fields_html = '<table><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody>'
|
|
for field in fields[:10]:
|
|
field_name = field.get('name', '')
|
|
field_type = field.get('type', 'unknown')
|
|
field_desc = field.get('description', '')
|
|
fields_html += f'''
|
|
<tr>
|
|
<td><code>{escape_html(field_name)}</code></td>
|
|
<td><code>{escape_html(field_type)}</code></td>
|
|
<td>{escape_html(field_desc)}</td>
|
|
</tr>'''
|
|
fields_html += '</tbody></table>'
|
|
|
|
html_parts.append(f'''
|
|
<h3>{escape_html(name)}</h3>
|
|
<p><strong>What it represents:</strong> {escape_html(description)}</p>
|
|
{fields_html}''')
|
|
|
|
return '\n'.join(html_parts)
|
|
|
|
|
|
def generate_er_diagram(models: List[Dict]) -> str:
|
|
"""Generate ASCII ER diagram."""
|
|
if not models:
|
|
return '''┌─────────────────────────────────────┐
|
|
│ No data models detected │
|
|
└─────────────────────────────────────┘'''
|
|
|
|
lines = []
|
|
for model in models[:4]:
|
|
name = model.get('name', 'Model')
|
|
fields = model.get('fields', [])[:4]
|
|
|
|
width = max(len(name) + 4, max([len(f.get('name', '')) + len(f.get('type', '')) + 5 for f in fields] or [20]))
|
|
|
|
lines.append('┌' + '─' * width + '┐')
|
|
lines.append('│' + f' {name} '.center(width) + '│')
|
|
lines.append('├' + '─' * width + '┤')
|
|
|
|
for field in fields:
|
|
field_str = f" {field.get('name', '')} : {field.get('type', '')}"
|
|
lines.append('│' + field_str.ljust(width) + '│')
|
|
|
|
lines.append('└' + '─' * width + '┘')
|
|
lines.append('')
|
|
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def generate_glossary_html(terms: List[Dict]) -> str:
|
|
"""Generate HTML for glossary."""
|
|
html_parts = []
|
|
|
|
for term in terms:
|
|
word = term.get('term', '')
|
|
definition = term.get('definition', '')
|
|
|
|
html_parts.append(f'''
|
|
<div class="glossary-term">
|
|
<span class="glossary-word">{escape_html(word)}</span>
|
|
<span class="glossary-definition">{escape_html(definition)}</span>
|
|
</div>''')
|
|
|
|
return '\n'.join(html_parts)
|
|
|
|
|
|
def generate_system_diagram(tech_stack: Dict, structure: Dict) -> str:
|
|
"""Generate ASCII system architecture diagram."""
|
|
framework = tech_stack.get('framework', 'Application')
|
|
database = tech_stack.get('database', '')
|
|
ui = tech_stack.get('ui_framework', 'UI')
|
|
|
|
diagram = f'''┌─────────────────────────────────────────────────────────────┐
|
|
│ Application Architecture │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ Client │───▶│ API │───▶│ Database │ │
|
|
│ │ ({ui or 'UI'}) │ │ ({framework or 'Server'}) │ │ ({database or 'Storage'}) │ │
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────┘'''
|
|
return diagram
|
|
|
|
|
|
def generate_html(analysis: Dict, template: str) -> str:
|
|
"""Generate final HTML from analysis and template."""
|
|
project = analysis.get('project', {})
|
|
tech_stack = analysis.get('tech_stack', {})
|
|
structure = analysis.get('structure', {})
|
|
features = analysis.get('features', [])
|
|
components = analysis.get('components', [])
|
|
endpoints = analysis.get('api_endpoints', [])
|
|
models = analysis.get('data_models', [])
|
|
glossary = analysis.get('glossary_terms', [])
|
|
|
|
# Basic replacements
|
|
replacements = {
|
|
'{{PROJECT_NAME}}': escape_html(project.get('name', 'Project')),
|
|
'{{VERSION}}': escape_html(project.get('version', '1.0.0')),
|
|
'{{TAGLINE}}': escape_html(project.get('description', 'Project documentation')),
|
|
'{{DESCRIPTION}}': escape_html(project.get('description', 'This project provides various features and capabilities.')),
|
|
'{{AUDIENCE}}': 'Developers, stakeholders, and anyone interested in understanding this project.',
|
|
'{{GENERATED_DATE}}': datetime.now().strftime('%Y-%m-%d'),
|
|
}
|
|
|
|
# Generate complex sections
|
|
html = template
|
|
|
|
# Replace simple placeholders
|
|
for key, value in replacements.items():
|
|
html = html.replace(key, value)
|
|
|
|
# Replace capabilities section
|
|
capabilities = [{'capability': cap.get('name'), 'description': cap.get('description')}
|
|
for cap in features[:4]] if features else [
|
|
{'capability': 'Core Features', 'description': 'Main application functionality'},
|
|
{'capability': 'Easy Integration', 'description': 'Simple setup and configuration'}
|
|
]
|
|
|
|
# Find and replace the capabilities placeholder section
|
|
cap_html = generate_capabilities_html(capabilities)
|
|
html = re.sub(
|
|
r'<!-- CAPABILITIES_PLACEHOLDER -->.*?</div>\s*</div>',
|
|
f'<!-- Generated Capabilities -->\n{cap_html}',
|
|
html,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
# Replace technology stack
|
|
html = re.sub(
|
|
r'<!-- TECH_STACK_PLACEHOLDER -->.*?</tr>',
|
|
generate_tech_stack_html(tech_stack),
|
|
html,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
# Generate and replace diagrams
|
|
html = html.replace('{{SYSTEM_DIAGRAM}}', generate_system_diagram(tech_stack, structure))
|
|
html = html.replace('{{DIRECTORY_STRUCTURE}}', generate_directory_structure(structure))
|
|
html = html.replace('{{ER_DIAGRAM}}', generate_er_diagram(models))
|
|
|
|
# Replace features section
|
|
html = re.sub(
|
|
r'<!-- FEATURES_PLACEHOLDER -->.*?</div>\s*</div>\s*</div>',
|
|
f'<!-- Generated Features -->\n{generate_features_html(features)}',
|
|
html,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
# Replace API endpoints
|
|
html = re.sub(
|
|
r'<!-- API_ENDPOINTS_PLACEHOLDER -->.*?</details>',
|
|
f'<h3>API Endpoints</h3>\n{generate_api_endpoints_html(endpoints)}',
|
|
html,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
# Replace components
|
|
html = re.sub(
|
|
r'<!-- COMPONENTS_PLACEHOLDER -->.*?</div>\s*</div>',
|
|
f'<!-- Generated Components -->\n{generate_components_html(components)}',
|
|
html,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
# Replace data models
|
|
html = re.sub(
|
|
r'<!-- DATA_MODELS_PLACEHOLDER -->.*?</table>',
|
|
f'<!-- Generated Data Models -->\n{generate_data_models_html(models)}',
|
|
html,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
# Replace glossary
|
|
html = re.sub(
|
|
r'<!-- GLOSSARY_PLACEHOLDER -->.*?</div>',
|
|
f'<!-- Generated Glossary -->\n{generate_glossary_html(glossary)}\n</div>',
|
|
html,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
# Clean up remaining placeholders
|
|
html = re.sub(r'\{\{[A-Z_]+\}\}', '', html)
|
|
|
|
return html
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
if len(sys.argv) < 3:
|
|
print("Usage: generate_html.py <analysis.yml> <template.html> [output.html]")
|
|
sys.exit(1)
|
|
|
|
analysis_path = Path(sys.argv[1])
|
|
template_path = Path(sys.argv[2])
|
|
output_path = Path(sys.argv[3]) if len(sys.argv) > 3 else Path('documentation.html')
|
|
|
|
if not analysis_path.exists():
|
|
print(f"Error: Analysis file not found: {analysis_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if not template_path.exists():
|
|
print(f"Error: Template file not found: {template_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
analysis = load_analysis(analysis_path)
|
|
template = load_template(template_path)
|
|
html = generate_html(analysis, template)
|
|
|
|
output_path.write_text(html, encoding='utf-8')
|
|
print(f"HTML documentation generated: {output_path}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|