Transform theme system from large inline dictionaries to maintainable YAML files:
**Architecture:**
- File-based themes organized by scope: mode/, ui/, document/, branding/
- Dynamic theme loading with automatic discovery
- Hybrid system maintaining 100% backward compatibility
- Rich metadata support with theme documentation
**Implementation:**
- Created markitect/themes/ directory with organized structure
- Added ThemeRegistry for dynamic YAML theme loading
- Extracted ChatGPT and Substack themes to separate files
- Added mode themes (light.yaml, dark.yaml) as examples
- Integrated with existing LAYERED_THEMES system seamlessly
**Benefits:**
- Improved maintainability: each theme is a separate file
- Better collaboration: multiple contributors can work simultaneously
- Enhanced discoverability: clear organization shows available themes
- Rich documentation: each theme file includes design notes and metadata
- Schema validation potential with YAML format
**Quality Assurance:**
- Comprehensive 12-test suite for modular system (12/12 passing)
- Backward compatibility verified with existing 15 theme tests (15/15 passing)
- CLI integration tested and working with file-based themes
- Theme combination and scoping functionality preserved
**Files Created:**
- markitect/themes/__init__.py - Theme registry and dynamic loader
- markitect/themes/README.md - Complete documentation and usage guide
- markitect/themes/document/{chatgpt,substack}.yaml - Modular theme files
- markitect/themes/mode/{light,dark}.yaml - Mode theme examples
- tests/test_modular_theme_system.py - Comprehensive test coverage
Addresses maintainability concerns while preserving all existing functionality.
No breaking changes - all existing code, CLI commands, and API calls work unchanged.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
146 lines
4.9 KiB
Python
146 lines
4.9 KiB
Python
"""
|
|
Modular Theme System for Markitect
|
|
|
|
This module provides a dynamic theme loading system that loads themes from
|
|
separate YAML files organized by scope (mode, ui, document, branding).
|
|
|
|
Architecture:
|
|
themes/
|
|
mode/ # Light/dark color schemes
|
|
ui/ # Interface styling
|
|
document/ # Document formatting
|
|
branding/ # Company/personal styling
|
|
|
|
Each theme file is a YAML file with metadata and properties.
|
|
"""
|
|
|
|
import os
|
|
import yaml
|
|
from pathlib import Path
|
|
from typing import Dict, Any, List
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ThemeRegistry:
|
|
"""Registry for dynamically loaded themes."""
|
|
|
|
def __init__(self):
|
|
self._themes = {}
|
|
self._loaded = False
|
|
|
|
def load_themes(self) -> Dict[str, Dict[str, Any]]:
|
|
"""Load all themes from YAML files."""
|
|
if self._loaded:
|
|
return self._themes
|
|
|
|
themes_dir = Path(__file__).parent
|
|
|
|
# Scan all scope directories
|
|
for scope_dir in themes_dir.iterdir():
|
|
if scope_dir.is_dir() and not scope_dir.name.startswith('__'):
|
|
scope = scope_dir.name
|
|
self._load_scope_themes(scope_dir, scope)
|
|
|
|
self._loaded = True
|
|
return self._themes
|
|
|
|
def _load_scope_themes(self, scope_dir: Path, scope: str) -> None:
|
|
"""Load themes from a specific scope directory."""
|
|
for theme_file in scope_dir.glob('*.yaml'):
|
|
theme_name = theme_file.stem
|
|
try:
|
|
with open(theme_file, 'r', encoding='utf-8') as f:
|
|
theme_data = yaml.safe_load(f)
|
|
|
|
# Validate theme structure
|
|
if not self._validate_theme(theme_data, theme_name, theme_file):
|
|
continue
|
|
|
|
# Store theme in registry
|
|
self._themes[theme_name] = {
|
|
'scope': theme_data.get('scope', scope),
|
|
'properties': theme_data.get('properties', {}),
|
|
'metadata': {
|
|
'name': theme_data.get('name', theme_name),
|
|
'description': theme_data.get('description', ''),
|
|
'author': theme_data.get('author', ''),
|
|
'version': theme_data.get('version', '1.0.0'),
|
|
'file': str(theme_file)
|
|
}
|
|
}
|
|
|
|
logger.debug(f"Loaded theme '{theme_name}' from {theme_file}")
|
|
|
|
except yaml.YAMLError as e:
|
|
logger.error(f"Failed to parse YAML in {theme_file}: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to load theme from {theme_file}: {e}")
|
|
|
|
def _validate_theme(self, theme_data: Dict[str, Any], theme_name: str, theme_file: Path) -> bool:
|
|
"""Validate theme structure."""
|
|
if not isinstance(theme_data, dict):
|
|
logger.error(f"Theme {theme_file} must be a dictionary")
|
|
return False
|
|
|
|
if 'properties' not in theme_data:
|
|
logger.error(f"Theme {theme_file} missing 'properties' section")
|
|
return False
|
|
|
|
if not isinstance(theme_data['properties'], dict):
|
|
logger.error(f"Theme {theme_file} 'properties' must be a dictionary")
|
|
return False
|
|
|
|
return True
|
|
|
|
def get_theme(self, name: str) -> Dict[str, Any]:
|
|
"""Get a specific theme by name."""
|
|
if not self._loaded:
|
|
self.load_themes()
|
|
return self._themes.get(name)
|
|
|
|
def list_themes(self, scope: str = None) -> List[str]:
|
|
"""List available theme names, optionally filtered by scope."""
|
|
if not self._loaded:
|
|
self.load_themes()
|
|
|
|
if scope:
|
|
return [name for name, data in self._themes.items()
|
|
if data.get('scope') == scope]
|
|
return list(self._themes.keys())
|
|
|
|
def get_themes_by_scope(self, scope: str) -> Dict[str, Dict[str, Any]]:
|
|
"""Get all themes for a specific scope."""
|
|
if not self._loaded:
|
|
self.load_themes()
|
|
|
|
return {name: data for name, data in self._themes.items()
|
|
if data.get('scope') == scope}
|
|
|
|
# Global theme registry instance
|
|
theme_registry = ThemeRegistry()
|
|
|
|
def get_layered_themes() -> Dict[str, Dict[str, Any]]:
|
|
"""
|
|
Get all themes in the format expected by the existing system.
|
|
|
|
Returns a dictionary compatible with the old LAYERED_THEMES structure.
|
|
"""
|
|
return theme_registry.load_themes()
|
|
|
|
def get_theme(name: str) -> Dict[str, Any]:
|
|
"""Get a specific theme by name."""
|
|
return theme_registry.get_theme(name)
|
|
|
|
def list_themes(scope: str = None) -> List[str]:
|
|
"""List available theme names."""
|
|
return theme_registry.list_themes(scope)
|
|
|
|
def reload_themes() -> None:
|
|
"""Force reload of all themes."""
|
|
theme_registry._loaded = False
|
|
theme_registry._themes.clear()
|
|
theme_registry.load_themes()
|
|
|
|
# Export main functions
|
|
__all__ = ['get_layered_themes', 'get_theme', 'list_themes', 'reload_themes', 'theme_registry'] |