cryosnap-core 0.2.11

Core rendering library for cryosnap.
Documentation
use crate::{Error, Line, Result, Span, TextStyle};
use once_cell::sync::Lazy;
use std::path::Path;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Color, FontStyle, ThemeSet};
use syntect::parsing::SyntaxSet;

pub(crate) fn highlight_code(
    text: &str,
    path: Option<&Path>,
    language: Option<&str>,
    theme_name: &str,
) -> Result<(Vec<Line>, String)> {
    static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
    static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
    static CHARM_THEME: Lazy<syntect::highlighting::Theme> = Lazy::new(charm_theme);

    let ps = &*SYNTAX_SET;
    let ts = &*THEME_SET;

    let theme = if theme_name.eq_ignore_ascii_case("charm") {
        &*CHARM_THEME
    } else if let Some(theme) = ts.themes.get(theme_name) {
        theme
    } else if let Some(theme) = ts.themes.get("base16-ocean.dark") {
        theme
    } else if let Some(theme) = ts.themes.values().next() {
        theme
    } else {
        return Err(Error::Render("no themes available".to_string()));
    };

    let syntax = match language {
        Some(lang) => ps
            .find_syntax_by_token(lang)
            .or_else(|| ps.find_syntax_by_extension(lang))
            .unwrap_or_else(|| ps.find_syntax_plain_text()),
        None => {
            if let Some(path) = path {
                ps.find_syntax_for_file(path)
                    .ok()
                    .flatten()
                    .unwrap_or_else(|| ps.find_syntax_plain_text())
            } else {
                let first_line = text.lines().next().unwrap_or("");
                ps.find_syntax_by_first_line(first_line)
                    .unwrap_or_else(|| ps.find_syntax_plain_text())
            }
        }
    };

    let default_fg = theme.settings.foreground.unwrap_or(Color::WHITE);
    let default_fg_hex = color_to_hex(default_fg);

    let mut highlighter = HighlightLines::new(syntax, theme);
    let mut lines = Vec::new();

    let raw_lines: Vec<&str> = text.split('\n').collect();
    for (idx, raw) in raw_lines.iter().enumerate() {
        let mut line_with_end = raw.to_string();
        if idx + 1 < raw_lines.len() {
            line_with_end.push('\n');
        }

        let regions = highlighter
            .highlight_line(&line_with_end, ps)
            .map_err(|err| Error::Render(format!("highlight: {err}")))?;
        let mut line = Line::default();

        for (style, piece) in regions {
            let mut text_piece = piece.to_string();
            if text_piece.ends_with('\n') {
                text_piece.pop();
                if text_piece.ends_with('\r') {
                    text_piece.pop();
                }
            }
            if text_piece.is_empty() {
                continue;
            }

            let mut span_style = TextStyle::default();
            if style.foreground.a > 0 {
                span_style.fg = Some(color_to_hex(style.foreground));
            }
            if style.background.a > 0 {
                span_style.bg = Some(color_to_hex(style.background));
            }
            if style.font_style.contains(FontStyle::BOLD) {
                span_style.bold = true;
            }
            if style.font_style.contains(FontStyle::ITALIC) {
                span_style.italic = true;
            }
            if style.font_style.contains(FontStyle::UNDERLINE) {
                span_style.underline = true;
            }

            push_span(&mut line.spans, text_piece, span_style);
        }
        lines.push(line);
    }

    Ok((lines, default_fg_hex))
}

fn push_span(spans: &mut Vec<Span>, text: String, style: TextStyle) {
    if let Some(last) = spans.last_mut() {
        if last.style == style {
            last.text.push_str(&text);
            return;
        }
    }
    spans.push(Span { text, style });
}

fn color_to_hex(color: syntect::highlighting::Color) -> String {
    format!("#{:02X}{:02X}{:02X}", color.r, color.g, color.b)
}

fn charm_theme() -> syntect::highlighting::Theme {
    use std::str::FromStr;
    use syntect::highlighting::{
        Color, FontStyle, ScopeSelectors, Theme, ThemeItem, ThemeSettings,
    };

    let mut theme = Theme {
        name: Some("charm".to_string()),
        author: Some("cryosnap".to_string()),
        settings: ThemeSettings {
            foreground: Some(Color {
                r: 0xC4,
                g: 0xC4,
                b: 0xC4,
                a: 0xFF,
            }),
            background: Some(Color {
                r: 0x17,
                g: 0x17,
                b: 0x17,
                a: 0xFF,
            }),
            ..ThemeSettings::default()
        },
        scopes: Vec::new(),
    };

    let mut push = |scope: &str, fg: &str, style: FontStyle| {
        let scope = ScopeSelectors::from_str(scope)
            .unwrap_or_else(|_| ScopeSelectors::from_str("text").unwrap());
        let color = hex_to_color(fg);
        theme.scopes.push(ThemeItem {
            scope,
            style: syntect::highlighting::StyleModifier {
                foreground: Some(color),
                background: None,
                font_style: Some(style),
            },
        });
    };

    push("comment", "#676767", FontStyle::empty());
    push("comment.preproc", "#FF875F", FontStyle::empty());
    push("keyword", "#00AAFF", FontStyle::empty());
    push("keyword.reserved", "#FF48DD", FontStyle::empty());
    push("keyword.namespace", "#FF5F87", FontStyle::empty());
    push("storage.type", "#635ADF", FontStyle::empty());
    push("operator", "#FF7F83", FontStyle::empty());
    push("punctuation", "#E8E8A8", FontStyle::empty());
    push("constant.numeric", "#6EEFC0", FontStyle::empty());
    push("string", "#E38356", FontStyle::empty());
    push("string.escape", "#AFFFD7", FontStyle::empty());
    push("entity.name.function", "#00DC7F", FontStyle::empty());
    push("entity.name.tag", "#B083EA", FontStyle::empty());
    push("entity.name.attribute", "#7A7AE6", FontStyle::empty());
    push(
        "entity.name.class",
        "#F1F1F1",
        FontStyle::BOLD | FontStyle::UNDERLINE,
    );
    push("entity.name.decorator", "#FFFF87", FontStyle::empty());

    theme
}

fn hex_to_color(hex: &str) -> syntect::highlighting::Color {
    let hex = hex.trim_start_matches('#');
    if hex.len() != 6 {
        return syntect::highlighting::Color::WHITE;
    }
    let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
    let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
    let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
    syntect::highlighting::Color { r, g, b, a: 0xFF }
}