diff --git a/src/auto_readme/templates/__init__.py b/src/auto_readme/templates/__init__.py new file mode 100644 index 0000000..75d6db9 --- /dev/null +++ b/src/auto_readme/templates/__init__.py @@ -0,0 +1,313 @@ +"""Template system for README generation.""" + +import os +from pathlib import Path +from typing import Optional +from jinja2 import Environment, FileSystemLoader, BaseLoader + +from ..models import Project + + +class TemplateRenderer: + """Renders README templates with project context.""" + + BUILTIN_TEMPLATES = { + "base": """# {{ title }} + +{% if description %} +{{ description }} +{% endif %} + +{% if badges %} +{{ badges }} +{% endif %} + +{% if table_of_contents %} +## Table of Contents + +- [Overview](#overview) +{% if installation_steps %}- [Installation](#installation){% endif %} +{% if usage_examples %}- [Usage](#usage){% endif %} +{% if features %}- [Features](#features){% endif %} +{% if api_functions %}- [API Reference](#api-reference){% endif %} +{% if contributing_guidelines %}- [Contributing](#contributing){% endif %} +{% if license_info %}- [License](#license){% endif %} + +{% endif %} + +## Overview + +{{ project_overview }} + +{% if tech_stack %} +## Supported Languages + +This project uses: +{% for tech in tech_stack %} +- **{{ tech }}** +{% endfor %} +{% endif %} + +{% if installation_steps %} +## Installation + +```bash +{{ installation_steps|join('\\n') }} +``` + +{% if dependencies %} +### Dependencies + +{% for dep in dependencies %} +- `{{ dep.name }}`{% if dep.version %} v{{ dep.version }}{% endif %} +{% endfor %} +{% endif %} +{% endif %} + +{% if usage_examples %} +## Usage + +### Basic Usage + +```python +{{ usage_examples[0] }} +``` + +{% endif %} + +{% if features %} +## Features + +{% for feature in features %} +- {{ feature }} +{% endfor %} +{% endif %} + +{% if api_functions %} +## API Reference + +{% for func in api_functions %} +### `{{ func.name }}` + +{% if func.docstring %} +{{ func.docstring }} +{% endif %} + +**Parameters:** `{{ func.parameters|join(', ') }}` + +{% endfor %} +{% endif %} + +{% if contributing_guidelines %} +## Contributing + +{{ contributing_guidelines }} + +{% endif %} + +{% if license_info %} +## License + +{{ license_info }} +{% endif %} + +--- + +*Generated by Auto README Generator on {{ generated_at }}* +""", + } + + def __init__(self, custom_template_dir: Optional[Path] = None): + """Initialize the template renderer.""" + self.env = Environment( + loader=FileSystemLoader(str(custom_template_dir)) if custom_template_dir else BaseLoader(), + trim_blocks=True, + lstrip_blocks=True, + ) + self.custom_template_dir = custom_template_dir + + def render( + self, + project: Project, + template_name: str = "base", + **extra_context, + ) -> str: + """Render a README template with project context.""" + context = self._build_context(project, **extra_context) + + if template_name in self.BUILTIN_TEMPLATES: + template = self.env.from_string(self.BUILTIN_TEMPLATES[template_name]) + elif self.custom_template_dir: + try: + template = self.env.get_template(template_name) + except Exception: + template = self.env.from_string(self.BUILTIN_TEMPLATES["base"]) + else: + template = self.env.from_string(self.BUILTIN_TEMPLATES["base"]) + + return template.render(**context) + + def _build_context(self, project: Project, **extra_context) -> dict: + """Build the template context from project data.""" + context = { + "title": project.config.name if project.config else project.root_path.name, + "description": project.config.description if project.config else None, + "project_overview": self._generate_overview(project), + "badges": None, + "tech_stack": self._get_tech_stack(project), + "installation_steps": project.installation_steps or self._generate_installation_steps(project), + "usage_examples": project.usage_examples or self._generate_usage_examples(project), + "features": project.features or self._detect_features(project), + "api_functions": project.all_functions()[:20], + "contributing_guidelines": self._generate_contributing_guidelines(project), + "license_info": project.config.license if project.config else None, + "generated_at": project.generated_at.strftime("%Y-%m-%d"), + "table_of_contents": True, + "dependencies": project.dependencies, + } + + context.update(extra_context) + return context + + def _generate_overview(self, project: Project) -> str: + """Generate a project overview.""" + lines = [ + f"A project of type **{project.project_type.value}** located at `{project.root_path}`.", + ] + lines.append(f"Contains {len(project.files)} files.") + + if project.config and project.config.description: + lines.insert(0, project.config.description) + + return " ".join(lines) + + def _get_tech_stack(self, project: Project) -> list[str]: + """Extract technology stack from dependencies.""" + tech = [project.project_type.value.title()] + + dep_names = {dep.name.lower() for dep in project.dependencies} + + framework_map = { + "fastapi": "FastAPI", + "flask": "Flask", + "django": "Django", + "click": "Click", + "express": "Express.js", + "react": "React", + "vue": "Vue.js", + "angular": "Angular", + "gin": "Gin", + "echo": "Echo", + "actix-web": "Actix Web", + "rocket": "Rocket", + } + + for dep, framework in framework_map.items(): + if dep in dep_names: + tech.append(framework) + + return list(dict.fromkeys(tech)) + + def _generate_installation_steps(self, project: Project) -> list[str]: + """Generate installation steps based on project type.""" + steps = [] + + if project.project_type.value == "python": + steps = [ + "pip install -r requirements.txt", + "pip install -e .", + ] + elif project.project_type.value in ("javascript", "typescript"): + steps = ["npm install", "npm run build"] + elif project.project_type.value == "go": + steps = ["go mod download", "go build"] + elif project.project_type.value == "rust": + steps = ["cargo build --release"] + + if project.git_info and project.git_info.is_repo: + steps.insert(0, f"git clone {project.git_info.remote_url or 'https://github.com/user/repo.git'}") + + return steps + + def _generate_usage_examples(self, project: Project) -> list[str]: + """Generate usage examples based on project type.""" + if project.project_type.value == "python": + return ["from project_name import main", "main()"] + elif project.project_type.value in ("javascript", "typescript"): + return ["import { main } from 'project-name'", "main()"] + elif project.project_type.value == "go": + return ["go run main.go"] + elif project.project_type.value == "rust": + return ["cargo run --release"] + return ["# Check the docs for usage information"] + + def _detect_features(self, project: Project) -> list[str]: + """Detect project features from structure.""" + features = [] + + if any(f.file_type.name == "TEST" for f in project.files): + features.append("Test suite included") + + if project.git_info and project.git_info.is_repo: + features.append("Git repository") + + if project.dependencies: + features.append(f"Uses {len(project.dependencies)} dependencies") + + if project.all_classes(): + features.append(f"Contains {len(project.all_classes())} classes") + + if project.all_functions(): + features.append(f"Contains {len(project.all_functions())} functions") + + return features if features else ["Auto-generated documentation"] + + def _generate_contributing_guidelines(self, project: Project) -> str: + """Generate contributing guidelines.""" + guidelines = [ + "1. Fork the repository", + "2. Create a feature branch (`git checkout -b feature/amazing-feature`)", + "3. Commit your changes (`git commit -m 'Add some amazing feature'`)", + "4. Push to the branch (`git push origin feature/amazing-feature`)", + "5. Open a Pull Request", + ] + + if project.project_type.value == "python": + guidelines.extend([ + "", + "For Python development:", + "- Run tests with `pytest`", + "- Format code with `black` and `isort`", + "- Check types with `mypy`", + ]) + elif project.project_type.value in ("javascript", "typescript"): + guidelines.extend([ + "", + "For JavaScript/TypeScript development:", + "- Run tests with `npm test`", + "- Format code with `npm run format`", + "- Lint with `npm run lint`", + ]) + + return "\n".join(guidelines) + + +class TemplateManager: + """Manages template operations.""" + + def __init__(self, template_dir: Optional[Path] = None): + """Initialize the template manager.""" + self.template_dir = template_dir + self.renderer = TemplateRenderer(custom_template_dir=template_dir) + + def list_templates(self) -> list[str]: + """List available templates.""" + return list(self.renderer.BUILTIN_TEMPLATES.keys()) + + def get_template_path(self, name: str) -> Optional[Path]: + """Get the path to a template.""" + if name in self.renderer.BUILTIN_TEMPLATES: + return None + if self.template_dir: + return self.template_dir / name + return None