giallo 0.5.0

A code highlighter giving the same output as VSCode
Documentation
use std::fmt::Write;

use crate::themes::compiled::CompiledTheme;

pub(crate) const LIGHT_SUFFIX: &str = "l-";
pub(crate) const DARK_SUFFIX: &str = "d-";

/// Generates the CSS content for a textmate theme
///
/// This is used with the HTML renderer, typically to switch highlighting scheme in light/dark
/// mode which is something that cannot be done inline.
pub(crate) fn generate_css(theme: &CompiledTheme, prefix: &str) -> String {
    let mut css = String::new();

    // Add header comment
    writeln!(css, "/*").unwrap();
    writeln!(css, " * theme \"{}\" generated by giallo", theme.name).unwrap();
    writeln!(css, " */").unwrap();
    writeln!(css).unwrap();

    // Generate base code class with default theme colors
    writeln!(css, ".{prefix}code {{").unwrap();
    writeln!(
        css,
        "  {}",
        theme.default_style.foreground.as_css_color_property()
    )
    .unwrap();
    writeln!(
        css,
        "  {}",
        theme.default_style.background.as_css_bg_color_property()
    )
    .unwrap();
    writeln!(css, "}}").unwrap();
    writeln!(css).unwrap();

    if let Some(ref highlight_bg) = theme.highlight_background_color {
        writeln!(css, ".{prefix}hl {{").unwrap();
        writeln!(css, "  {}", highlight_bg.as_css_bg_color_property()).unwrap();
        writeln!(css, "}}").unwrap();
        writeln!(css).unwrap();
    }

    // Generate line number color class if theme has one
    if let Some(ref line_number_fg) = theme.line_number_foreground {
        writeln!(css, ".giallo-ln {{").unwrap();
        writeln!(css, "  {}", line_number_fg.as_css_color_property()).unwrap();
        writeln!(css, "}}").unwrap();
        writeln!(css).unwrap();
    }

    // And then the unique colours fg and bg
    let mut fg_entries: Vec<_> = theme.style_map.fg.iter().collect();
    fg_entries.sort_by_key(|(_, id)| *id);
    for (color, id) in fg_entries {
        writeln!(css, ".{prefix}{id} {{ {} }}", color.as_css_color_property()).unwrap();
    }

    let mut bg_entries: Vec<_> = theme.style_map.bg.iter().collect();
    bg_entries.sort_by_key(|(_, id)| *id);
    for (color, id) in bg_entries {
        writeln!(
            css,
            ".{prefix}bg{id} {{ {} }}",
            color.as_css_bg_color_property()
        )
        .unwrap();
    }

    writeln!(css, ".{prefix}b {{ font-weight: bold; }}").unwrap();
    writeln!(css, ".{prefix}i {{ font-style: italic; }}").unwrap();
    writeln!(css, ".{prefix}u {{ text-decoration: underline; }}").unwrap();
    writeln!(css, ".{prefix}s {{ text-decoration: line-through; }}").unwrap();
    writeln!(
        css,
        ".{prefix}us {{ text-decoration: underline line-through; }}"
    )
    .unwrap();

    if !theme.default_style.font_style.is_empty() {
        writeln!(
            css,
            ".{prefix}fr {{ font-style: normal; font-weight: normal; text-decoration: none; }}"
        )
        .unwrap();
    }

    css
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::themes::RawTheme;
    use insta::assert_snapshot;

    #[test]
    fn can_generate_css_for_theme() {
        let theme = RawTheme::load_from_file(
            "grammars-themes/packages/tm-themes/themes/vitesse-black.json",
        )
        .unwrap()
        .compile()
        .unwrap();
        assert!(theme.style_map.fg.len() < theme.rules.len());
        assert_snapshot!(generate_css(&theme, "g-"));
    }
}