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));
}
}