""" Template engine for generating Markdown documentation from templates. """ import os import re import sys from typing import Dict, Any, List, Optional class TemplateEngine: """Simple template engine for generating Markdown files.""" def __init__( self, templates_dir: str = "scripts/track_progress/templates" ): """Initialize the template engine with the templates directory.""" self.templates_dir = templates_dir def _load_template(self, template_name: str) -> str: """Load a template file.""" # Try multiple possible template locations possible_paths = [ # Original path os.path.join(self.templates_dir, template_name), # Path relative to current working directory os.path.join("templates", template_name), # Path relative to executable directory os.path.join( os.path.dirname(sys.executable), "templates", template_name ), # Path relative to script directory os.path.join( os.path.dirname(os.path.abspath(__file__)), "templates", template_name, ), # Path with 'scripts/track_progress' removed (for compiled version) template_name.replace("scripts/track_progress/", ""), # Just the filename template_name, ] # Try each path for template_path in possible_paths: if os.path.exists(template_path): with open(template_path, "r", encoding="utf-8") as f: return f.read() # If we get here, none of the paths worked error_message = f"Template not found: {template_name}\nTried paths: {possible_paths}" raise FileNotFoundError(error_message) def _replace_variables( self, template: str, context: Dict[str, Any] ) -> str: """Replace {{ variable }} with values from context.""" def replace_var(match): var_name = match.group(1).strip() # Handle nested variables with dot notation if "." in var_name: parts = var_name.split(".") value = context for part in parts: if isinstance(value, dict) and part in value: value = value[part] else: return match.group(0) # Return original if not found return str(value) if value is not None else "" # Handle simple variables if var_name in context: return ( str(context[var_name]) if context[var_name] is not None else "" ) return match.group(0) # Return original if not found # Replace {{ variable }} pattern = r"{{(.*?)}}" return re.sub(pattern, replace_var, template) def _process_conditionals( self, template: str, context: Dict[str, Any] ) -> str: """Process {% if condition %} ... {% endif %} blocks.""" def replace_conditional(match): condition_var = match.group(1).strip() content = match.group(2) # Handle nested conditions with dot notation if "." in condition_var: parts = condition_var.split(".") value = context for part in parts: if isinstance(value, dict) and part in value: value = value[part] else: return "" # Condition not met if value: return content return "" # Handle simple conditions if condition_var in context and context[condition_var]: return content return "" # Process {% if condition %} ... {% endif %} pattern = r"{%\s*if\s+(.*?)\s*%}(.*?){%\s*endif\s*%}" return re.sub(pattern, replace_conditional, template, flags=re.DOTALL) def _process_loops(self, template: str, context: Dict[str, Any]) -> str: """Process {% for item_var, item in items.items() %} ... {% endfor %} blocks.""" # First, process dictionary iteration with items() def replace_dict_loop(match): item_key_var = match.group(1).strip() item_val_var = match.group(2).strip() collection_var = match.group(3).strip() content_template = match.group(4) # Handle nested collections with dot notation if "." in collection_var: parts = collection_var.split(".") collection = context for part in parts: if isinstance(collection, dict) and part in collection: collection = collection[part] else: return "" # Collection not found else: collection = context.get(collection_var, {}) if not isinstance(collection, dict): return "" # Not a dictionary result = [] for key, value in collection.items(): # Create a new context for each iteration loop_context = context.copy() loop_context[item_key_var] = key loop_context[item_val_var] = value # Process the content template with the loop context item_content = self._replace_variables( content_template, loop_context ) item_content = self._process_conditionals( item_content, loop_context ) result.append(item_content) return "".join(result) # Process {% for key, value in dict.items() %} ... {% endfor %} dict_pattern = r"{%\s*for\s+(.*?),\s*(.*?)\s+in\s+(.*?)\.items\(\)\s*%}(.*?){%\s*endfor\s*%}" template = re.sub( dict_pattern, replace_dict_loop, template, flags=re.DOTALL ) # Then, process regular list iteration def replace_list_loop(match): item_var = match.group(1).strip() collection_var = match.group(2).strip() content_template = match.group(3) # Handle nested collections with dot notation if "." in collection_var: parts = collection_var.split(".") collection = context for part in parts: if isinstance(collection, dict) and part in collection: collection = collection[part] else: return "" # Collection not found else: collection = context.get(collection_var, []) if not isinstance(collection, (list, dict)): return "" # Not a collection result = [] if isinstance(collection, dict): # If it's a dict, iterate over keys for key in collection: # Create a new context for each iteration loop_context = context.copy() loop_context[item_var] = key # Process the content template with the loop context item_content = self._replace_variables( content_template, loop_context ) item_content = self._process_conditionals( item_content, loop_context ) result.append(item_content) else: # list for item in collection: # Create a new context for each iteration loop_context = context.copy() loop_context[item_var] = item # Process the content template with the loop context item_content = self._replace_variables( content_template, loop_context ) item_content = self._process_conditionals( item_content, loop_context ) result.append(item_content) return "".join(result) # Process {% for item in items %} ... {% endfor %} list_pattern = ( r"{%\s*for\s+(.*?)\s+in\s+(.*?)\s*%}(.*?){%\s*endfor\s*%}" ) return re.sub( list_pattern, replace_list_loop, template, flags=re.DOTALL ) def render(self, template_name: str, context: Dict[str, Any]) -> str: """Render a template with the given context.""" template = self._load_template(template_name) # Process template directives in order template = self._process_loops(template, context) template = self._process_conditionals(template, context) template = self._replace_variables(template, context) return template