Created
September 25, 2025 16:02
-
-
Save MohamedElashri/b8bdf3611ac64582a6b8607a384c879d to your computer and use it in GitHub Desktop.
Revisions
-
MohamedElashri created this gist
Sep 25, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,446 @@ #!/usr/bin/env python3 """ Syntax Highlighting CSS Generator for Zola from Pygments Themes Description: Generate a combined light/dark mode syntax highlighting CSS file for Zola. This script takes one light theme and one dark theme from Pygments and produces a CSS file in the same format as the existing highlight.css file. Author: Mohamed Elashri Date: 2025-09-10 Usage: python syntax_highlight.py --light github --dark monokai --output highlight.css Requirements: - Python 3.x - Pygments library (install via `pip install pygments`) License: MIT """ import sys import argparse from pathlib import Path try: from pygments.styles import get_all_styles, get_style_by_name from pygments.formatters import HtmlFormatter except ImportError: print("Error: Pygments is required. Install it with: pip install pygments") sys.exit(1) class CombinedThemeGenerator: """Generate combined light/dark theme CSS for Zola.""" # Mapping from Pygments token types to Zola semantic classes PYGMENTS_TO_ZOLA_MAPPING = { # Keywords 'k': 'z-keyword', # Keyword 'kc': 'z-keyword', # Keyword.Constant 'kd': 'z-keyword', # Keyword.Declaration 'kn': 'z-keyword', # Keyword.Namespace 'kp': 'z-keyword', # Keyword.Pseudo 'kr': 'z-keyword', # Keyword.Reserved 'kt': 'z-storage.z-type', # Keyword.Type # Control keywords 'k.control': 'z-keyword.z-control', # Names and Functions 'n': 'z-entity.z-name', # Name 'na': 'z-entity.z-name', # Name.Attribute 'nb': 'z-entity.z-name', # Name.Builtin 'bp': 'z-entity.z-name', # Name.Builtin.Pseudo 'nc': 'z-entity.z-name', # Name.Class 'no': 'z-entity.z-name', # Name.Constant 'nd': 'z-entity.z-name', # Name.Decorator 'ni': 'z-entity.z-name', # Name.Entity 'ne': 'z-entity.z-name', # Name.Exception 'nf': 'z-entity.z-name.z-function', # Name.Function 'fm': 'z-entity.z-name.z-function', # Name.Function.Magic 'nl': 'z-entity.z-name', # Name.Label 'nn': 'z-entity.z-name', # Name.Namespace 'nx': 'z-entity.z-name', # Name.Other 'py': 'z-entity.z-name', # Name.Property 'nt': 'z-entity.z-name', # Name.Tag 'nv': 'z-variable', # Name.Variable 'vc': 'z-variable', # Name.Variable.Class 'vg': 'z-variable', # Name.Variable.Global 'vi': 'z-variable', # Name.Variable.Instance 'vm': 'z-variable', # Name.Variable.Magic # Literals 'l': 'z-constant', # Literal 'm': 'z-constant.z-numeric', # Literal.Number 'mb': 'z-constant.z-numeric', # Literal.Number.Bin 'mf': 'z-constant.z-numeric', # Literal.Number.Float 'mh': 'z-constant.z-numeric', # Literal.Number.Hex 'mi': 'z-constant.z-numeric', # Literal.Number.Integer 'il': 'z-constant.z-numeric', # Literal.Number.Integer.Long 'mo': 'z-constant.z-numeric', # Literal.Number.Oct # Strings 's': 'z-string', # Literal.String 'sa': 'z-string', # Literal.String.Affix 'sb': 'z-string', # Literal.String.Backtick 'sc': 'z-string', # Literal.String.Char 'dl': 'z-string', # Literal.String.Delimiter 'sd': 'z-string', # Literal.String.Doc 's2': 'z-string', # Literal.String.Double 'se': 'z-string', # Literal.String.Escape 'sh': 'z-string', # Literal.String.Heredoc 'si': 'z-string', # Literal.String.Interpol 'sx': 'z-string', # Literal.String.Other 'sr': 'z-string', # Literal.String.Regex 's1': 'z-string', # Literal.String.Single 'ss': 'z-string', # Literal.String.Symbol # Comments 'c': 'z-comment', # Comment 'ch': 'z-comment', # Comment.Hashbang 'cm': 'z-comment', # Comment.Multiline 'c1': 'z-comment', # Comment.Single 'cs': 'z-comment', # Comment.Special 'cp': 'z-comment', # Comment.Preproc 'cpf': 'z-comment', # Comment.PreprocFile # Operators 'o': 'z-keyword.z-operator', # Operator 'ow': 'z-keyword.z-operator', # Operator.Word # Punctuation 'p': 'z-punctuation', # Punctuation # Other 'g': 'z-meta', # Generic 'gd': 'z-meta', # Generic.Deleted 'ge': 'z-meta', # Generic.Emph 'gr': 'z-meta', # Generic.Error 'gh': 'z-meta', # Generic.Heading 'gi': 'z-meta', # Generic.Inserted 'go': 'z-meta', # Generic.Output 'gp': 'z-meta', # Generic.Prompt 'gs': 'z-meta', # Generic.Strong 'gu': 'z-meta', # Generic.Subheading 'gt': 'z-meta', # Generic.Traceback 'w': 'z-meta', # Text.Whitespace } def __init__(self): self.available_styles = list(get_all_styles()) def _get_theme_colors(self, theme_name: str): """Extract background and foreground colors from a Pygments theme.""" try: style_class = get_style_by_name(theme_name) background = getattr(style_class, 'background_color', '#ffffff') # Find a reasonable foreground color from the theme foreground = '#000000' # default for token_type, style in style_class.styles.items(): if style and '#' in style: # Extract color from style string parts = style.split() for part in parts: if part.startswith('#') and len(part) == 7: foreground = part break if foreground != '#000000': break return background, foreground except: return '#ffffff', '#000000' def _adjust_color_brightness(self, hex_color: str, factor: float) -> str: """Adjust brightness of a hex color for border generation.""" if not hex_color.startswith('#'): return hex_color try: # Remove # and convert to RGB hex_color = hex_color[1:] r = int(hex_color[0:2], 16) g = int(hex_color[2:4], 16) b = int(hex_color[4:6], 16) # Adjust brightness if factor > 0: # Lighten r = min(255, max(0, int(r + (255 - r) * factor))) g = min(255, max(0, int(g + (255 - g) * factor))) b = min(255, max(0, int(b + (255 - b) * factor))) else: # Darken factor = abs(factor) r = max(0, int(r * (1 - factor))) g = max(0, int(g * (1 - factor))) b = max(0, int(b * (1 - factor))) return f"#{r:02x}{g:02x}{b:02x}" except: return hex_color def _parse_pygments_css(self, css_content: str): """Parse Pygments CSS and extract style rules.""" rules = {} lines = css_content.split('\n') current_selector = None current_styles = {} in_rule = False for line in lines: line = line.strip() if line.startswith('.highlight .') and '{' in line: # Handle single-line rule parts = line.split('{', 1) if len(parts) == 2: selector = parts[0].strip() styles_part = parts[1].replace('}', '').strip() # Extract class name from selector class_name = selector.replace('.highlight .', '').strip() # Parse styles styles = {} if styles_part: for style in styles_part.split(';'): if ':' in style: prop, value = style.split(':', 1) styles[prop.strip()] = value.strip() if styles: rules[class_name] = styles elif line.startswith('.highlight .') and line.endswith('{'): # Start of multi-line rule current_selector = line.replace('{', '').strip() current_styles = {} in_rule = True elif in_rule and line == '}': # End of multi-line rule if current_selector and current_styles: class_name = current_selector.replace('.highlight .', '').strip() rules[class_name] = current_styles current_selector = None current_styles = {} in_rule = False elif in_rule and ':' in line: # Style property in multi-line rule prop, value = line.split(':', 1) prop = prop.strip() value = value.replace(';', '').strip() if prop and value: current_styles[prop] = value return rules def _generate_zola_classes(self, pygments_rules: dict, is_dark_mode: bool = False) -> str: """Convert Pygments rules to Zola classes.""" zola_styles = {} # Group Pygments classes by their Zola equivalents for pygments_class, styles in pygments_rules.items(): if pygments_class in self.PYGMENTS_TO_ZOLA_MAPPING: zola_class = self.PYGMENTS_TO_ZOLA_MAPPING[pygments_class] if zola_class in zola_styles: # Merge styles (later ones override earlier ones) zola_styles[zola_class].update(styles) else: zola_styles[zola_class] = styles.copy() # Generate CSS css_parts = [] for zola_class, styles in sorted(zola_styles.items()): if not styles: continue css_props = [] for prop, value in styles.items(): css_props.append(f" {prop}: {value};") if css_props: css_parts.append(f" .{zola_class} {{\n" + "\n".join(css_props) + "\n }\n") return "\n".join(css_parts) def _generate_pygments_classes(self, pygments_rules: dict) -> str: """Generate Pygments classes CSS.""" css_parts = [] # Group similar classes together for cleaner output grouped_classes = {} for class_name, styles in pygments_rules.items(): if not styles: continue style_key = tuple(sorted(styles.items())) if style_key not in grouped_classes: grouped_classes[style_key] = [] grouped_classes[style_key].append(class_name) for style_items, class_names in grouped_classes.items(): if not class_names: continue # Create selector selectors = [f".highlight .{class_name}" for class_name in class_names] selector = ", ".join(selectors) # Create CSS properties css_props = [] for prop, value in style_items: css_props.append(f" {prop}: {value};") if css_props: css_parts.append(f" {selector} {{\n" + "\n".join(css_props) + "\n }") return "\n".join(css_parts) def generate_combined_css(self, light_theme: str, dark_theme: str) -> str: """Generate combined CSS with light and dark themes.""" if light_theme not in self.available_styles: raise ValueError(f"Light theme '{light_theme}' not found. Available: {', '.join(sorted(self.available_styles))}") if dark_theme not in self.available_styles: raise ValueError(f"Dark theme '{dark_theme}' not found. Available: {', '.join(sorted(self.available_styles))}") # Generate Pygments CSS for both themes light_formatter = HtmlFormatter(style=light_theme, cssclass='highlight') dark_formatter = HtmlFormatter(style=dark_theme, cssclass='highlight') light_css = light_formatter.get_style_defs() dark_css = dark_formatter.get_style_defs() # Parse CSS rules light_rules = self._parse_pygments_css(light_css) dark_rules = self._parse_pygments_css(dark_css) # Get theme colors light_bg, light_fg = self._get_theme_colors(light_theme) dark_bg, dark_fg = self._get_theme_colors(dark_theme) # Generate the combined CSS css_content = f"""/* Syntax highlighting styles - {light_theme.title()} Light / {dark_theme.title()} Dark */ .highlight, pre.z-code {{ background: {light_bg}; color: {light_fg}; padding: 20px; border-radius: 6px; border: 1px solid {self._adjust_color_brightness(light_bg, 0.1)}; margin: 2rem 0; overflow-x: auto; }} .highlight pre code {{ margin-top: 0; margin-bottom: 0; }} /* Light mode Zola classes */ {self._generate_zola_classes(light_rules, is_dark_mode=False)} /* Dark mode styles */ @media (prefers-color-scheme: dark) {{ .highlight, pre.z-code {{ background-color: {dark_bg}; color: {dark_fg}; border: 1px solid {self._adjust_color_brightness(dark_bg, 0.1) if dark_bg.startswith('#') else 'rgba(255,255,255,0.07)'}; }} {self._generate_zola_classes(dark_rules, is_dark_mode=True)} }} /* Pygments classes for compatibility */ @media (prefers-color-scheme: dark) {{ .highlight {{ background-color: {dark_bg}; color: {dark_fg}; border: 1px solid rgba(255,255,255,0.07); }} {self._generate_pygments_classes(dark_rules)} }}""" return css_content def main(): parser = argparse.ArgumentParser( description="Generate combined light/dark syntax highlighting CSS for Zola", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s --light github --dark monokai %(prog)s --light vs --dark gruvbox-dark --output my_highlight.css %(prog)s --list-themes """ ) parser.add_argument("--light", type=str, default="github", help="Light theme name (default: github)") parser.add_argument("--dark", type=str, default="monokai", help="Dark theme name (default: monokai)") parser.add_argument("--output", type=str, default="highlight.css", help="Output CSS file (default: highlight.css)") parser.add_argument("--list-themes", action="store_true", help="List all available Pygments themes") args = parser.parse_args() generator = CombinedThemeGenerator() if args.list_themes: print("Available Pygments themes:") themes = sorted(generator.available_styles) # Categorize themes for better display light_themes = [] dark_themes = [] other_themes = [] for theme in themes: if any(word in theme.lower() for word in ['light', 'github', 'vs', 'default', 'friendly']): light_themes.append(theme) elif any(word in theme.lower() for word in ['dark', 'monokai', 'material', 'native', 'gruvbox-dark']): dark_themes.append(theme) else: other_themes.append(theme) if light_themes: print("\nπ LIGHT THEMES:") for theme in light_themes: print(f" {theme}") if dark_themes: print("\nπ DARK THEMES:") for theme in dark_themes: print(f" {theme}") if other_themes: print("\nπ OTHER THEMES:") for theme in other_themes: print(f" {theme}") return try: css_content = generator.generate_combined_css(args.light, args.dark) with open(args.output, 'w', encoding='utf-8') as f: f.write(css_content) print(f"β Generated combined CSS:") print(f" Light theme: {args.light}") print(f" Dark theme: {args.dark}") print(f" Output file: {args.output}") except Exception as e: print(f"Error: {e}") sys.exit(1) if __name__ == "__main__": main()