katana-document-viewer 0.1.4

KatanA document viewer artifact, render evaluation, and export foundation.
Documentation
use crate::export_surface_span::{SurfaceTextSpan, SurfaceTextStyle};
use image::Rgba;
use std::sync::LazyLock;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, Theme, ThemeSet};
use syntect::parsing::{SyntaxReference, SyntaxSet};
use syntect::util::LinesWithEndings;

const SYNTAX_THEME: &str = "InspiredGitHub";

pub(crate) struct SurfaceCodeHighlighter;

impl SurfaceCodeHighlighter {
    pub(crate) fn highlight(language: Option<&str>, body: &str) -> Vec<Vec<SurfaceTextSpan>> {
        match language.filter(|value| !value.is_empty()) {
            Some(language) => Self::highlight_language(language, body),
            None => body.lines().map(Self::plain_line).collect(),
        }
    }

    fn highlight_language(language: &str, body: &str) -> Vec<Vec<SurfaceTextSpan>> {
        let syntax = syntax(language);
        let mut highlighter = HighlightLines::new(syntax, theme());
        LinesWithEndings::from(body)
            .map(|line| Self::highlight_line(&mut highlighter, line))
            .collect()
    }

    fn highlight_line(highlighter: &mut HighlightLines<'_>, line: &str) -> Vec<SurfaceTextSpan> {
        let line = line.trim_end_matches(['\r', '\n']);
        let fallback = vec![(Style::default(), line)];
        let ranges = highlighter
            .highlight_line(line, syntax_set())
            .unwrap_or(fallback);
        ranges
            .into_iter()
            .map(|(style, text)| SurfaceTextSpan::styled(text, span_style(style)))
            .collect()
    }

    fn plain_line(line: &str) -> Vec<SurfaceTextSpan> {
        let line = line.trim_end_matches(['\r', '\n']);
        vec![SurfaceTextSpan::styled(
            line,
            SurfaceTextStyle::default().monospace(),
        )]
    }
}

fn span_style(style: Style) -> SurfaceTextStyle {
    SurfaceTextStyle::default().monospace().with_color(Rgba([
        style.foreground.r,
        style.foreground.g,
        style.foreground.b,
        style.foreground.a,
    ]))
}

fn syntax(language: &str) -> &'static SyntaxReference {
    syntax_set()
        .find_syntax_by_token(language)
        .or_else(|| syntax_set().find_syntax_by_extension(language))
        .unwrap_or_else(|| syntax_set().find_syntax_plain_text())
}

fn syntax_set() -> &'static SyntaxSet {
    static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
    &SYNTAX_SET
}

fn theme_set() -> &'static ThemeSet {
    static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
    &THEME_SET
}

fn theme() -> &'static Theme {
    &theme_set().themes[SYNTAX_THEME]
}

#[cfg(test)]
#[path = "export_surface_code_tests.rs"]
mod tests;