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