Skip to content

Instantly share code, notes, and snippets.

@MohamedElashri
Created September 25, 2025 16:02
Show Gist options
  • Save MohamedElashri/b8bdf3611ac64582a6b8607a384c879d to your computer and use it in GitHub Desktop.
Save MohamedElashri/b8bdf3611ac64582a6b8607a384c879d to your computer and use it in GitHub Desktop.
Syntax Highlighting CSS Generator for Zola from Pygments Themes
#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment