Files
markitect-main/markitect/themes/__init__.py
tegwick d1e129c9b8 feat: implement modular theme system with file-based theme organization
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>
2025-11-10 11:43:25 +01:00

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']