Initial upload: ScaffoldForge CLI tool with full codebase, tests, and CI/CD
This commit is contained in:
224
scaffoldforge/templates/engine.py
Normal file
224
scaffoldforge/templates/engine.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user