diff --git a/scaffoldforge/templates/engine.py b/scaffoldforge/templates/engine.py new file mode 100644 index 0000000..05680a7 --- /dev/null +++ b/scaffoldforge/templates/engine.py @@ -0,0 +1,224 @@ +"""Template engine for ScaffoldForge.""" + +import os +import re +from pathlib import Path +from typing import Any, Dict, List, Optional + +from jinja2 import ( + BaseLoader, + Environment, + FileSystemLoader, + PackageLoader, + TemplateSyntaxError, +) + +from scaffoldforge.parsers import IssueData + + +class TemplateEngine: + """Template rendering engine using Jinja2.""" + + def __init__(self, template_dir: Optional[str] = None): + """Initialize the template engine. + + Args: + template_dir: Path to custom template directory. + """ + self.template_dir = template_dir + self.env = self._create_environment() + self._loaded_templates: Dict[str, Dict[str, Any]] = {} + + def _create_environment(self) -> Environment: + """Create Jinja2 environment with appropriate loader.""" + if self.template_dir and Path(self.template_dir).exists(): + loader = FileSystemLoader(self.template_dir) + else: + loader = PackageLoader("scaffoldforge", "templates") + + return Environment( + loader=loader, + autoescape=True, + trim_blocks=True, + lstrip_blocks=True, + ) + + def load_templates( + self, language: str, template_name: str = "default" + ) -> Dict[str, str]: + """Load templates for a specific language and template type. + + Args: + language: Programming language. + template_name: Template variant name. + + Returns: + Dictionary mapping template names to rendered content. + """ + key = f"{language}/{template_name}" + if key in self._loaded_templates: + return self._loaded_templates[key] + + templates = {} + template_dir = Path(__file__) / language / template_name + + if template_dir.exists(): + for template_file in template_dir.glob("*.j2"): + template_name_only = template_file.stem + templates[template_name_only] = self._load_template( + language, template_name, template_file.name + ) + + self._loaded_templates[key] = templates + return templates + + def _load_template( + self, language: str, template_type: str, filename: str + ) -> str: + """Load a single template file. + + Args: + language: Programming language. + template_type: Template variant. + filename: Template filename. + + Returns: + Template content as string. + """ + template_path = f"{language}/{template_type}/{filename}" + try: + template = self.env.get_template(template_path) + return template.filename + except Exception: + return "" + + def render( + self, + template_name: str, + context: Dict[str, Any], + language: str = "python", + ) -> str: + """Render a template with the given context. + + Args: + template_name: Name of the template to render. + context: Dictionary of variables to pass to the template. + language: Programming language for template lookup. + + Returns: + Rendered template string. + """ + try: + full_name = f"{language}/{template_name}" + template = self.env.get_template(f"{full_name}.j2") + return template.render(**context) + except TemplateSyntaxError as e: + raise ValueError(f"Template syntax error in {template_name}: {e}") + except Exception as e: + raise ValueError(f"Failed to render template {template_name}: {e}") + + def render_string(self, template_content: str, context: Dict[str, Any]) -> str: + """Render a template string directly. + + Args: + template_content: Template content as string. + context: Dictionary of variables. + + Returns: + Rendered string. + """ + template = self.env.from_string(template_content) + return template.render(**context) + + @staticmethod + def list_available_templates(language: str) -> List[str]: + """List available templates for a language. + + Args: + language: Programming language. + + Returns: + List of template names. + """ + templates_dir = Path(__file__).parent / language + if not templates_dir.exists(): + return [] + + templates = [] + for item in templates_dir.iterdir(): + if item.is_dir(): + templates.append(item.name) + return sorted(templates) + + @staticmethod + def list_available_languages() -> List[str]: + """List all available programming languages. + + Returns: + List of language identifiers. + """ + templates_dir = Path(__file__).parent + if not templates_dir.exists(): + return [] + + languages = [] + for item in templates_dir.iterdir(): + if item.is_dir() and not item.name.startswith("_"): + languages.append(item.name) + return sorted(languages) + + def get_template_context(self, issue_data: IssueData) -> Dict[str, Any]: + """Generate template context from issue data. + + Args: + issue_data: IssueData object. + + Returns: + Dictionary of template variables. + """ + project_name = self._generate_project_name(issue_data) + + return { + "project_name": project_name, + "project_name_kebab": self._to_kebab_case(project_name), + "project_name_snake": self._to_snake_case(project_name), + "project_name_pascal": self._to_pascal_case(project_name), + "issue_number": issue_data.number, + "issue_title": issue_data.title, + "issue_url": issue_data.url, + "repository": issue_data.repository, + "author": issue_data.author, + "created_date": issue_data.created_at[:10] if issue_data.created_at else "", + "todo_items": issue_data.get_todo_items(), + "completed_items": issue_data.get_completed_items(), + "requirements": issue_data.requirements, + "acceptance_criteria": issue_data.acceptance_criteria, + "checklist": issue_data.checklist, + } + + def _generate_project_name(self, issue_data: IssueData) -> str: + """Generate a project name from issue title. + + Args: + issue_data: IssueData object. + + Returns: + Project name string. + """ + title = issue_data.title + title = re.sub(r"[^a-zA-Z0-9\s]", "", title) + title = re.sub(r"\s+", "_", title.strip()) + return title.lower()[:50] + + def _to_kebab_case(self, text: str) -> str: + """Convert text to kebab-case.""" + return re.sub(r"[^a-zA-Z0-9]+", "-", text).strip("-").lower() + + def _to_snake_case(self, text: str) -> str: + """Convert text to snake_case.""" + return re.sub(r"[^a-zA-Z0-9]+", "_", text).strip("_").lower() + + def _to_pascal_case(self, text: str) -> str: + """Convert text to PascalCase.""" + words = re.findall(r"[a-zA-Z0-9]+", text) + return "".join(word.title() for word in words)