diff --git a/man_card/templates.py b/man_card/templates.py new file mode 100644 index 0000000..3c2aeac --- /dev/null +++ b/man_card/templates.py @@ -0,0 +1,193 @@ +"""Template system for card generation.""" + +import json +import os +from pathlib import Path +from typing import Any, Optional +from dataclasses import dataclass, field + + +@dataclass +class Template: + """Card template configuration.""" + name: str + layout: dict[str, Any] = field(default_factory=dict) + colors: dict[str, str] = field(default_factory=dict) + fonts: dict[str, str] = field(default_factory=dict) + spacing: dict[str, int] = field(default_factory=dict) + + +class TemplateLoader: + """Loader and validator for card templates.""" + + DEFAULT_TEMPLATES = { + "default": { + "layout": { + "header_height": 60, + "section_spacing": 20, + "option_spacing": 10, + "margin": 30, + "columns": 2 + }, + "colors": { + "header_bg": "#2E3440", + "header_text": "#FFFFFF", + "section_title": "#88C0D0", + "option_flag": "#A3BE8C", + "option_desc": "#ECEFF4", + "background": "#FFFFFF", + "border": "#4C566A" + }, + "fonts": { + "header": ("Helvetica", 24, "B"), + "section": ("Helvetica", 14, "B"), + "option_flag": ("Courier", 10, "B"), + "option_desc": ("Helvetica", 9, ""), + "synopsis": ("Courier", 11, ""), + "body": ("Helvetica", 10, "") + }, + "spacing": { + "line_height": 14, + "margin": 20, + "section_padding": 15 + } + }, + "minimal": { + "layout": { + "header_height": 40, + "section_spacing": 15, + "option_spacing": 8, + "margin": 20, + "columns": 2 + }, + "colors": { + "header_bg": "#FFFFFF", + "header_text": "#000000", + "section_title": "#666666", + "option_flag": "#000000", + "option_desc": "#333333", + "background": "#FFFFFF", + "border": "#CCCCCC" + }, + "fonts": { + "header": ("Helvetica", 18, ""), + "section": ("Helvetica", 11, "B"), + "option_flag": ("Courier", 9, ""), + "option_desc": ("Helvetica", 8, ""), + "synopsis": ("Courier", 9, ""), + "body": ("Helvetica", 8, "") + }, + "spacing": { + "line_height": 12, + "margin": 15, + "section_padding": 10 + } + }, + "compact": { + "layout": { + "header_height": 35, + "section_spacing": 10, + "option_spacing": 5, + "margin": 15, + "columns": 3 + }, + "colors": { + "header_bg": "#1A1A2E", + "header_text": "#FFFFFF", + "section_title": "#00D9FF", + "option_flag": "#00FF88", + "option_desc": "#AAAAAA", + "background": "#16213E", + "border": "#0F3460" + }, + "fonts": { + "header": ("Helvetica", 14, "B"), + "section": ("Helvetica", 10, "B"), + "option_flag": ("Courier", 7, "B"), + "option_desc": ("Helvetica", 7, ""), + "synopsis": ("Courier", 8, ""), + "body": ("Helvetica", 7, "") + }, + "spacing": { + "line_height": 10, + "margin": 10, + "section_padding": 8 + } + }, + "dark": { + "layout": { + "header_height": 50, + "section_spacing": 18, + "option_spacing": 12, + "margin": 25, + "columns": 2 + }, + "colors": { + "header_bg": "#1E1E1E", + "header_text": "#61AFEF", + "section_title": "#C678DD", + "option_flag": "#98C379", + "option_desc": "#ABB2BF", + "background": "#282C34", + "border": "#3E4451" + }, + "fonts": { + "header": ("Helvetica", 20, "B"), + "section": ("Helvetica", 12, "B"), + "option_flag": ("Courier", 9, "B"), + "option_desc": ("Helvetica", 8, ""), + "synopsis": ("Courier", 10, ""), + "body": ("Helvetica", 9, "") + }, + "spacing": { + "line_height": 13, + "margin": 18, + "section_padding": 12 + } + } + } + + def __init__(self, template_dir: Optional[str] = None): + self.template_dir = Path(template_dir) if template_dir else Path(__file__).parent.parent / "templates" + + def list_templates(self) -> list[str]: + """List available template names.""" + templates = list(self.DEFAULT_TEMPLATES.keys()) + if self.template_dir.exists(): + for f in self.template_dir.glob("*.json"): + if f.stem not in templates: + templates.append(f.stem) + return sorted(templates) + + def load(self, name: str) -> Template: + """Load a template by name.""" + if name in self.DEFAULT_TEMPLATES: + data = self.DEFAULT_TEMPLATES[name] + return Template(name=name, **data) + + template_path = self.template_dir / f"{name}.json" + if template_path.exists(): + with open(template_path, 'r') as f: + data = json.load(f) + if "fonts" in data: + for key in data["fonts"]: + if isinstance(data["fonts"][key], list): + data["fonts"][key] = tuple(data["fonts"][key]) + return Template(name=name, **data) + + raise ValueError(f"Template '{name}' not found. Available: {', '.join(self.list_templates())}") + + def load_custom(self, path: str) -> Template: + """Load a custom template from file path.""" + with open(path, 'r') as f: + data = json.load(f) + return Template(name=Path(path).stem, **data) + + def get_template_path(self, name: str) -> Optional[Path]: + """Get the file path for a template if it exists.""" + if name in self.DEFAULT_TEMPLATES: + return None + template_path = self.template_dir / f"{name}.json" + if template_path.exists(): + return template_path + return None