""" 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 }