240 lines
8.9 KiB
Python

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