Initial upload: ScaffoldForge CLI tool with full codebase, tests, and CI/CD

This commit is contained in:
2026-02-04 05:37:12 +00:00
parent 97adeb8783
commit d32b668aa8

View 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)