"""
Template service for applying HTML/CSS templates to generated content
"""
import json
import random
from pathlib import Path
from typing import Optional, Dict
from src.core.config import get_config
from src.database.repositories import GeneratedContentRepository
class TemplateService:
"""Service for loading, selecting, and applying HTML templates"""
def __init__(self, content_repo: Optional[GeneratedContentRepository] = None):
self.templates_dir = Path(__file__).parent / "templates"
self._template_cache: Dict[str, str] = {}
self.content_repo = content_repo
def get_available_templates(self) -> list[str]:
"""
Get list of available template names
Returns:
List of template names without .html extension
"""
templates = []
for template_file in self.templates_dir.glob("*.html"):
templates.append(template_file.stem)
return sorted(templates)
def load_template(self, template_name: str) -> str:
"""
Load a template file by name
Args:
template_name: Name of template (without .html extension)
Returns:
Template content as string
Raises:
FileNotFoundError: If template doesn't exist
"""
if template_name in self._template_cache:
return self._template_cache[template_name]
template_path = self.templates_dir / f"{template_name}.html"
if not template_path.exists():
available = self.get_available_templates()
raise FileNotFoundError(
f"Template '{template_name}' not found. Available templates: {', '.join(available)}"
)
with open(template_path, 'r', encoding='utf-8') as f:
template_content = f.read()
self._template_cache[template_name] = template_content
return template_content
def select_template_for_content(
self,
site_deployment_id: Optional[int] = None,
site_deployment_repo=None
) -> str:
"""
Select appropriate template based on site deployment
Args:
site_deployment_id: Optional site deployment ID
site_deployment_repo: Optional repository for querying site deployments
Returns:
Template name to use
Logic:
1. If site_deployment_id exists:
- Query custom_hostname from SiteDeployment
- Check config mappings for hostname
- If mapping exists, use it
- If no mapping, randomly select and persist to config
2. If site_deployment_id is null: randomly select (don't persist)
"""
config = get_config()
if site_deployment_id and site_deployment_repo:
site_deployment = site_deployment_repo.get_by_id(site_deployment_id)
if site_deployment:
hostname = site_deployment.custom_hostname or site_deployment.pull_zone_bcdn_hostname
if hostname in config.templates.mappings:
return config.templates.mappings[hostname]
template_name = self._select_random_template()
self._persist_template_mapping(hostname, template_name)
return template_name
return self._select_random_template()
def _select_random_template(self) -> str:
"""
Randomly select a template from available templates
Returns:
Template name
"""
available = self.get_available_templates()
if not available:
return "basic"
return random.choice(available)
def _persist_template_mapping(self, hostname: str, template_name: str) -> None:
"""
Save template mapping to master.config.json
Args:
hostname: Custom hostname to map
template_name: Template to assign
"""
config_path = Path("master.config.json")
try:
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
if "templates" not in config_data:
config_data["templates"] = {"default": "basic", "mappings": {}}
if "mappings" not in config_data["templates"]:
config_data["templates"]["mappings"] = {}
config_data["templates"]["mappings"][hostname] = template_name
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config_data, f, indent=2)
except Exception as e:
print(f"Warning: Failed to persist template mapping: {e}")
def format_content(
self,
content: str,
title: str,
meta_description: str,
template_name: str
) -> str:
"""
Format content using specified template
Args:
content: Raw HTML content (h2, h3, p tags)
title: Article title
meta_description: Meta description for SEO
template_name: Name of template to use
Returns:
Complete formatted HTML document
Raises:
FileNotFoundError: If template doesn't exist
"""
config = get_config()
try:
template = self.load_template(template_name)
except FileNotFoundError:
fallback_template = config.templates.default
print(f"Warning: Template '{template_name}' not found, using '{fallback_template}'")
template = self.load_template(fallback_template)
formatted_html = template.replace("{{ title }}", self._escape_html(title))
formatted_html = formatted_html.replace("{{ meta_description }}", self._escape_html(meta_description))
formatted_html = formatted_html.replace("{{ content }}", content)
return formatted_html
def _escape_html(self, text: str) -> str:
"""
Escape HTML special characters in text
Args:
text: Text to escape
Returns:
Escaped text
"""
replacements = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
for char, escaped in replacements.items():
text = text.replace(char, escaped)
return text