## Issue #65 - Template Engine Foundation (COMPLETED) - Implement complete TDD8 methodology with 30 comprehensive tests (100% passing) - Add template variable parser with Unicode and dot notation support - Add template rendering engine with strict/lenient modes - Add business document generation (invoices, reports) - Add CLI integration with `markitect template-render` command - Add performance optimization (1000+ variables in <0.1s) ## Critical CLI Regression Fix - Fix broken `markitect --help` due to import path issues in markitect/issues/base.py - Add proper path resolution for domain module accessibility - Add 12 comprehensive CLI integration tests to prevent future regressions - Restore full CLI functionality with 35+ working commands ## Template Engine Architecture - markitect/template/parser.py - Variable parsing with comprehensive validation - markitect/template/engine.py - Template rendering with business logic - markitect/template/__init__.py - Structured package exports - Comprehensive exception hierarchy for robust error handling ## Test Coverage Excellence - 30 Issue #65 tests: parser (9), substitution (14), integration (7) - 12 CLI integration tests for regression prevention - Business scenario validation with real invoice/report generation - Performance benchmarking and error handling validation ## CLI Professional Enhancement - Add template-render command with comprehensive options - Fix import path issues preventing CLI access - Add validation, data checking, output options - Support JSON/YAML data formats with auto-detection ## Business Impact - Transform MarkiTect from document analysis to business automation platform - Enable professional invoice and report generation - Provide robust CLI interface for document workflows - Establish foundation for Epic #64 advanced template features 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
"""
|
|
Template engine for rendering templates with data.
|
|
|
|
This module provides the core template rendering functionality,
|
|
building on the parser module for variable extraction and substitution.
|
|
"""
|
|
|
|
import re
|
|
from typing import Dict, Any, Optional, Union
|
|
from .parser import TemplateParser, TemplateParsingError
|
|
|
|
|
|
class TemplateRenderError(TemplateParsingError):
|
|
"""Exception raised during template rendering."""
|
|
pass
|
|
|
|
|
|
class VariableNotFoundError(TemplateRenderError):
|
|
"""Raised when required variable is missing from data."""
|
|
pass
|
|
|
|
|
|
class TemplateEngine:
|
|
"""Template rendering engine for dynamic document generation."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the template engine."""
|
|
self.parser = TemplateParser()
|
|
|
|
def render(self, template_text: str, data: Dict[str, Any], strict: bool = True) -> str:
|
|
"""
|
|
Render a template with the provided data.
|
|
|
|
Args:
|
|
template_text: The template content to render
|
|
data: Dictionary containing data for variable substitution
|
|
strict: If True, raise error for missing variables. If False, preserve placeholders.
|
|
|
|
Returns:
|
|
Rendered template with variables substituted
|
|
|
|
Raises:
|
|
TemplateRenderError: When variables are missing in strict mode
|
|
TypeError: When data is not a dictionary
|
|
"""
|
|
if not isinstance(data, dict):
|
|
raise TypeError("Data must be a dictionary")
|
|
|
|
if not template_text:
|
|
return template_text
|
|
|
|
# Use the parser's regex pattern to find and replace variables
|
|
def replace_variable(match):
|
|
variable_name = match.group(1)
|
|
try:
|
|
value = self._get_nested_value(data, variable_name)
|
|
return str(value) if value is not None else "None"
|
|
except (KeyError, TypeError, AttributeError) as e:
|
|
if strict:
|
|
raise VariableNotFoundError(f"Variable '{variable_name}' not found in data", context=str(e))
|
|
else:
|
|
# Return the original placeholder in lenient mode
|
|
return match.group(0)
|
|
|
|
# Perform the substitution
|
|
result = self.parser.VARIABLE_PATTERN.sub(replace_variable, template_text)
|
|
return result
|
|
|
|
def _get_nested_value(self, data: Dict[str, Any], key: str) -> Any:
|
|
"""
|
|
Get nested value using dot notation.
|
|
|
|
Args:
|
|
data: Dictionary containing the data
|
|
key: Key with dot notation (e.g., "nested.category")
|
|
|
|
Returns:
|
|
Value at the specified key path
|
|
|
|
Raises:
|
|
KeyError: When the key path is not found
|
|
"""
|
|
keys = key.split('.')
|
|
current = data
|
|
|
|
path_so_far = []
|
|
for k in keys:
|
|
path_so_far.append(k)
|
|
if isinstance(current, dict) and k in current:
|
|
current = current[k]
|
|
else:
|
|
available_keys = list(current.keys()) if isinstance(current, dict) else "not a dictionary"
|
|
raise KeyError(f"Key '{k}' not found in path '{key}'. Available keys at '{'.'.join(path_so_far[:-1])}': {available_keys}")
|
|
|
|
return current
|
|
|
|
def validate_template(self, template_text: str) -> list:
|
|
"""
|
|
Validate template syntax and return any errors.
|
|
|
|
Args:
|
|
template_text: The template content to validate
|
|
|
|
Returns:
|
|
List of validation errors (empty if template is valid)
|
|
"""
|
|
return self.parser.validate_variable_syntax(template_text)
|
|
|
|
def get_required_variables(self, template_text: str) -> list:
|
|
"""
|
|
Get list of variables required by the template.
|
|
|
|
Args:
|
|
template_text: The template content to analyze
|
|
|
|
Returns:
|
|
List of variable names required by the template
|
|
"""
|
|
return self.parser.extract_variables(template_text)
|
|
|
|
def check_data_completeness(self, template_text: str, data: Dict[str, Any]) -> Dict[str, list]:
|
|
"""
|
|
Check if provided data contains all required variables.
|
|
|
|
Args:
|
|
template_text: The template content to check
|
|
data: Data dictionary to validate
|
|
|
|
Returns:
|
|
Dictionary with 'missing' and 'available' variable lists
|
|
"""
|
|
required_vars = self.get_required_variables(template_text)
|
|
missing_vars = []
|
|
available_vars = []
|
|
|
|
for var in required_vars:
|
|
try:
|
|
self._get_nested_value(data, var)
|
|
available_vars.append(var)
|
|
except (KeyError, TypeError, AttributeError):
|
|
missing_vars.append(var)
|
|
|
|
return {
|
|
'missing': missing_vars,
|
|
'available': available_vars,
|
|
'completeness': len(available_vars) / len(required_vars) if required_vars else 1.0
|
|
} |