roxy_syntect 0.1.0

Roxy plugin for providing syntax highlighting via Syntect
Documentation
use std::{io::Write, ops::Deref};

use once_cell::sync::Lazy;
use regex::{Captures, Regex, RegexBuilder};
use roxy_core::roxy::Parse;
use syntect::{highlighting::ThemeSet, html::highlighted_html_for_string, parsing::SyntaxSet};

const CODE_BLOCK_RE: Lazy<Regex> = Lazy::new(|| {
    RegexBuilder::new(r"^```(.+?)\n(.+?)\n```")
        .multi_line(true)
        .dot_matches_new_line(true)
        .build()
        .unwrap()
});

pub enum MaybeOwned<'a, T: 'a> {
    Owned(T),
    Borrowed(&'a T),
}

impl<'a, T> Deref for MaybeOwned<'a, T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        match self {
            Self::Owned(v) => v,
            Self::Borrowed(v) => v,
        }
    }
}

impl<'a, T> From<T> for MaybeOwned<'a, T> {
    fn from(value: T) -> Self {
        Self::Owned(value)
    }
}

impl<'a, T> From<&'a T> for MaybeOwned<'a, T> {
    fn from(value: &'a T) -> Self {
        Self::Borrowed(value)
    }
}

pub struct SyntectParser<'a> {
    syntax_set: MaybeOwned<'a, SyntaxSet>,
    theme_set: MaybeOwned<'a, ThemeSet>,
    theme: &'a str,
}

impl<'a> SyntectParser<'a> {
    pub fn new<S: Into<MaybeOwned<'a, SyntaxSet>>, T: Into<MaybeOwned<'a, ThemeSet>>>(
        syntax_set: S,
        theme_set: T,
        theme: &'a str,
    ) -> Self {
        Self {
            syntax_set: syntax_set.into(),
            theme_set: theme_set.into(),
            theme,
        }
    }

    pub fn with_theme(theme: &'a str) -> Self {
        Self {
            theme,
            ..Default::default()
        }
    }
}

impl<'a> Default for SyntectParser<'a> {
    fn default() -> Self {
        Self::new(
            SyntaxSet::load_defaults_newlines(),
            ThemeSet::load_defaults(),
            "base16-ocean.dark",
        )
    }
}

impl<'a> Parse for SyntectParser<'a> {
    fn parse(
        &mut self,
        _path: &str,
        src: &[u8],
        dst: &mut Vec<u8>,
    ) -> Result<(), roxy_core::error::Error> {
        let file = String::from_utf8_lossy(src).to_string();
        let result = CODE_BLOCK_RE
            .replace_all(file.as_str(), |captures: &Captures| {
                let syntax = self
                    .syntax_set
                    .find_syntax_by_token(&captures[1])
                    .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());

                highlighted_html_for_string(
                    &captures[2],
                    &self.syntax_set,
                    &syntax,
                    &self
                        .theme_set
                        .themes
                        .get(self.theme)
                        .expect(format!("couldn't find theme \"{}\"", self.theme).as_str()),
                )
                .unwrap()
            })
            .to_string();
        dst.write_all(result.as_bytes())?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use roxy_core::roxy::Parse;

    use crate::SyntectParser;

    #[test]
    fn invalid_syntax() {
        let mut parser = SyntectParser::default();
        let data = b"```toml\n[roxy]\nslug_word_limit = 8\n[syntect]\ntheme = \"base16-ocean.dark\"\ntheme_dir = \"./themes\"```";
        let mut dst = Vec::new();
        parser.parse("test.html", data, &mut dst);
        println!("{:?}", String::from_utf8(dst));
    }
}