feat: implement sophisticated layered theme system for md-render
MAJOR FEATURES: - **Layered Theme Architecture**: Combine themes across UI, document, and branding scopes - **Advanced Theme Combinations**: Support complex themes like "dark,academic" or "light,github,corporate" - **Legacy Compatibility**: Existing --template usage continues to work seamlessly - **Enhanced CLI Validation**: Proper theme validation with helpful error messages TECHNICAL IMPROVEMENTS: - Replace DocumentManager with CleanDocumentManager throughout codebase - Add ThemeType custom click parameter with comprehensive validation - Implement parse_theme_string() and combine_theme_properties() functions - Add _get_template_css() and _generate_layered_css() methods - Support for UI themes (light/dark), document themes (basic/github/academic), and branding themes (corporate/startup) THEME CAPABILITIES: - **Single themes**: basic, github, dark, academic, light, corporate, startup - **Layered themes**: dark,academic combines dark UI with academic typography - **Complex combinations**: light,github,corporate for branded GitHub-style documents - **Intelligent property merging**: Later themes override earlier theme properties QUALITY ASSURANCE: - All template system tests passing (12/12) - Fixed import errors and missing dependencies - Updated test expectations for new validation messages - Comprehensive validation prevents unknown theme usage Breaking Change: --template parameter renamed to --theme with enhanced functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,202 @@ class CleanDocumentManager:
|
||||
|
||||
return version_info
|
||||
|
||||
def _get_template_css(self, template: str = None) -> str:
|
||||
"""Generate layered theme CSS styles."""
|
||||
# Import layered theme functions
|
||||
from markitect.plugins.builtin.markdown_commands import (
|
||||
parse_theme_string, combine_theme_properties, TEMPLATE_STYLES
|
||||
)
|
||||
|
||||
# Handle layered themes or fall back to legacy
|
||||
if template and ',' in template:
|
||||
# New layered theme system
|
||||
theme_list = parse_theme_string(template)
|
||||
combined_props = combine_theme_properties(theme_list)
|
||||
return self._generate_layered_css(combined_props)
|
||||
else:
|
||||
# Legacy single theme or fallback
|
||||
if not template or template not in TEMPLATE_STYLES:
|
||||
# Use default layered themes
|
||||
theme_list = parse_theme_string('basic')
|
||||
combined_props = combine_theme_properties(theme_list)
|
||||
return self._generate_layered_css(combined_props)
|
||||
else:
|
||||
# Legacy theme - convert to layered
|
||||
theme_list = parse_theme_string(template)
|
||||
combined_props = combine_theme_properties(theme_list)
|
||||
return self._generate_layered_css(combined_props)
|
||||
|
||||
def _generate_layered_css(self, properties: dict) -> str:
|
||||
"""Generate CSS from combined theme properties."""
|
||||
|
||||
# Set defaults for missing properties (properties override defaults)
|
||||
defaults = {
|
||||
'body_background': '#ffffff',
|
||||
'body_color': '#333333',
|
||||
'font_family': '-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif',
|
||||
'max_width': '800px',
|
||||
'heading_color': '#333333', # Use same as body color by default
|
||||
'heading_style': 'simple',
|
||||
'text_align': 'left',
|
||||
'code_background': '#f6f8fa',
|
||||
'code_color': '#333333',
|
||||
'border_color': '#d0d7de',
|
||||
'blockquote_border': '#dfe2e5',
|
||||
'blockquote_color': '#6a737d',
|
||||
'table_border': '#d0d7de',
|
||||
'table_header_bg': '#f6f8fa',
|
||||
'accent_color': None,
|
||||
'secondary_color': None
|
||||
}
|
||||
|
||||
# Merge defaults first, then override with theme properties
|
||||
props = {**defaults, **properties}
|
||||
|
||||
# Base CSS
|
||||
base_css = f"""
|
||||
body {{
|
||||
font-family: {props['font_family']};
|
||||
max-width: {props['max_width']};
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
color: {props['body_color']};
|
||||
background-color: {props['body_background']};
|
||||
}}
|
||||
#markdown-content {{
|
||||
min-height: 200px;
|
||||
}}"""
|
||||
|
||||
# Heading styles
|
||||
heading_css = ""
|
||||
if props['heading_style'] == 'underlined':
|
||||
heading_css = f"""
|
||||
h1, h2, h3, h4, h5, h6 {{
|
||||
color: {props['heading_color']};
|
||||
border-bottom: 1px solid {props['border_color']};
|
||||
padding-bottom: 0.3em;
|
||||
}}"""
|
||||
elif props['heading_style'] == 'centered':
|
||||
heading_css = f"""
|
||||
h1, h2, h3, h4, h5, h6 {{
|
||||
color: {props['heading_color']};
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}}
|
||||
h1 {{
|
||||
text-align: center;
|
||||
font-size: 2.2em;
|
||||
border-bottom: 2px solid {props['heading_color']};
|
||||
padding-bottom: 0.5rem;
|
||||
}}"""
|
||||
else: # simple
|
||||
heading_css = f"""
|
||||
h1, h2, h3, h4, h5, h6 {{
|
||||
color: {props['heading_color']};
|
||||
}}"""
|
||||
|
||||
# Text alignment
|
||||
text_css = ""
|
||||
if props['text_align'] == 'justify':
|
||||
text_css = """
|
||||
p {
|
||||
text-align: justify;
|
||||
margin-bottom: 1.2rem;
|
||||
}"""
|
||||
|
||||
# Element styling
|
||||
element_css = f"""
|
||||
pre {{
|
||||
background-color: {props['code_background']};
|
||||
color: {props['code_color']};
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid {props['border_color']};
|
||||
}}
|
||||
code {{
|
||||
background-color: {props['code_background']};
|
||||
color: {props['code_color']};
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
pre code {{
|
||||
background: none;
|
||||
padding: 0;
|
||||
}}
|
||||
blockquote {{
|
||||
border-left: 4px solid {props['blockquote_border']};
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
color: {props['blockquote_color']};
|
||||
}}
|
||||
table {{
|
||||
font-size: 0.85em;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
width: 100%;
|
||||
border: 1px solid {props['table_border']};
|
||||
}}
|
||||
th, td {{
|
||||
font-size: inherit;
|
||||
border: 1px solid {props['table_border']};
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
}}
|
||||
th {{
|
||||
background-color: {props['table_header_bg']};
|
||||
font-weight: 600;
|
||||
}}"""
|
||||
|
||||
# Branding accents (if specified)
|
||||
accent_css = ""
|
||||
if props.get('accent_color'):
|
||||
accent_css = f"""
|
||||
a {{
|
||||
color: {props['accent_color']};
|
||||
}}
|
||||
a:hover {{
|
||||
opacity: 0.8;
|
||||
}}"""
|
||||
|
||||
return f"<style>{base_css}{heading_css}{text_css}{element_css}{accent_css}</style>"
|
||||
|
||||
def _get_legacy_template_css(self, template: str) -> str:
|
||||
"""Legacy CSS generation - kept for backward compatibility."""
|
||||
# Import template styles
|
||||
from markitect.plugins.builtin.markdown_commands import TEMPLATE_STYLES
|
||||
|
||||
# Use basic as default if no template specified
|
||||
if not template or template not in TEMPLATE_STYLES:
|
||||
template = 'basic'
|
||||
|
||||
style_config = TEMPLATE_STYLES[template]
|
||||
|
||||
# Base CSS that's common to all templates
|
||||
base_css = f"""
|
||||
body {{
|
||||
font-family: {style_config['font_family']};
|
||||
max-width: {style_config['max_width']};
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
color: {style_config['body_color']};
|
||||
}}
|
||||
#markdown-content {{
|
||||
min-height: 200px;
|
||||
}}"""
|
||||
|
||||
# Convert legacy template config to layered format
|
||||
legacy_config = TEMPLATE_STYLES[template]
|
||||
layered_props = {
|
||||
'font_family': legacy_config['font_family'],
|
||||
'max_width': legacy_config['max_width'],
|
||||
'body_color': legacy_config['body_color'],
|
||||
}
|
||||
return self._generate_layered_css(layered_props)
|
||||
|
||||
def _generate_html_template(self, markdown_content: str, title: str, css: str = None, template: str = None,
|
||||
edit_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None) -> str:
|
||||
"""Generate clean HTML template."""
|
||||
@@ -138,60 +334,8 @@ class CleanDocumentManager:
|
||||
except Exception:
|
||||
css_content = f'<link rel="stylesheet" href="{css}">'
|
||||
|
||||
# Default CSS for basic styling
|
||||
default_css = """
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
#markdown-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
pre {
|
||||
background: #f6f8fa;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
code {
|
||||
background: #f6f8fa;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid #dfe2e5;
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
color: #6a737d;
|
||||
}
|
||||
table {
|
||||
font-size: 0.85em;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
th, td {
|
||||
font-size: inherit;
|
||||
border: 1px solid #dfe2e5;
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
# Generate template-specific CSS
|
||||
default_css = self._get_template_css(template)
|
||||
|
||||
# Load clean editor JavaScript files
|
||||
editor_scripts = ""
|
||||
|
||||
Reference in New Issue
Block a user