240 lines
8.9 KiB
Python
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
|