feat: Complete Issue #65 Template Engine Foundation + Fix CLI Regression
## 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>
This commit is contained in:
147
markitect/template/engine.py
Normal file
147
markitect/template/engine.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user