"""Template engine using Jinja2 for project scaffolding.""" import os from pathlib import Path from typing import Any, Dict, Optional from jinja2 import ( BaseLoader, Environment, FileSystemLoader, TemplateSyntaxError, ) from .config import Config class TemplateEngine: """Jinja2 template rendering engine for project scaffolding.""" SUPPORTED_LANGUAGES = ["python", "nodejs", "go", "rust"] def __init__(self, template_path: Optional[str] = None, config: Optional[Config] = None): self.template_path = template_path self.config = config or Config() self._env: Optional[Environment] = None def _get_environment(self) -> Environment: """Get or create the Jinja2 environment.""" if self._env is not None: return self._env if self.template_path and not os.path.isabs(self.template_path): search_dirs = self.config.get_template_dirs() search_dirs.insert(0, str(Path.cwd())) self._env = Environment( loader=FileSystemLoader(search_dirs), trim_blocks=True, lstrip_blocks=True, ) elif self.template_path: template_dir = Path(self.template_path).parent self._env = Environment( loader=FileSystemLoader([str(template_dir)]), trim_blocks=True, lstrip_blocks=True, ) else: project_root = self._get_project_root() self._env = Environment( loader=FileSystemLoader(str(project_root)), trim_blocks=True, lstrip_blocks=True, ) return self._env def _get_project_root(self) -> Path: """Get the project root directory for built-in templates.""" return Path(__file__).parent / "templates" def _validate_template(self, template_name: str) -> bool: """Validate that a template exists.""" env = self._get_environment() try: env.get_template(template_name) return True except Exception: return False def _render_template( self, template_name: str, context: Dict[str, Any] ) -> str: """Render a single template with context.""" env = self._get_environment() template = env.get_template(template_name) return template.render(**context) def render_language_template( self, language: str, context: Dict[str, Any], output_dir: Path ) -> None: """Render all templates for a language.""" if language not in self.SUPPORTED_LANGUAGES: raise ValueError( f"Unsupported language: {language}. " f"Supported: {', '.join(self.SUPPORTED_LANGUAGES)}" ) env = self._get_environment() language_dir = language try: project_root = self._get_project_root() / language_dir for root, dirs, files in os.walk(project_root): rel_path = Path(root).relative_to(project_root) rel_path_str = rel_path.as_posix() rel_path_str = rel_path_str.replace("{{project_slug}}", context.get("project_slug", "")) rel_path_str = rel_path_str.replace("{{ project_name }}", context.get("project_name", "")) dest_dir = output_dir / rel_path_str dest_dir.mkdir(parents=True, exist_ok=True) for file in files: if file.endswith((".j2", ".jinja2")): template_path = ( Path(root) / file ).relative_to(project_root).as_posix() dest_file = Path(dest_dir) / file[:-3] try: content = self._render_template( language_dir + "/" + template_path, context ) dest_file.write_text(content) except TemplateSyntaxError as e: raise ValueError( f"Template syntax error in {template_path}: {e}" ) else: src_file = Path(root) / file dest_file = Path(dest_dir) / file dest_file.write_bytes(src_file.read_bytes()) except Exception as e: raise RuntimeError(f"Error rendering language template: {e}") def render_project( self, context: Dict[str, Any], output_dir: Path, template: str, ) -> None: """Render a custom project template.""" if not self._validate_template(template): raise ValueError(f"Template not found: {template}") try: for root, dirs, files in os.walk( self._get_environment().loader.searchpath[0] ): for pattern in self._get_environment().loader.get_files( template ): pass env = self._get_environment() base_template = env.get_template(template) for item in base_template.list_templates(): pass except Exception as e: raise ValueError(f"Error rendering project template: {e}") def render_ci_template( self, ci_provider: str, context: Dict[str, Any], output_dir: Path, ) -> None: """Render CI/CD template for a provider.""" ci_templates = { "github": "ci/.github/workflows/ci.yml.j2", "gitlab": "ci/.gitlab-ci.yml.j2", } if ci_provider not in ci_templates: raise ValueError( f"Unsupported CI provider: {ci_provider}. " f"Supported: {', '.join(ci_templates.keys())}" ) env = self._get_environment() template_name = ci_templates[ci_provider] try: content = self._render_template(template_name, context) if ci_provider == "github": workflow_dir = output_dir / ".github" / "workflows" workflow_dir.mkdir(parents=True, exist_ok=True) output_file = workflow_dir / "ci.yml" else: output_file = output_dir / ".gitlab-ci.yml" output_file.write_text(content) except Exception as e: raise RuntimeError(f"Error rendering CI template: {e}") def get_template_list(self) -> list[str]: """Get list of available templates.""" env = self._get_environment() return env.list_templates() def validate_context(self, context: Dict[str, Any]) -> list[str]: """Validate that all required context variables are provided.""" required = ["project_name", "author"] missing = [var for var in required if var not in context or not context[var]] return missing