Initial upload: man-card CLI tool with PDF/PNG generation, templates, and tests
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
193
man_card/templates.py
Normal file
193
man_card/templates.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user