roxy_tera_parser 0.1.0

Roxy plugin for parsing Tera templates
Documentation
use std::{fs, path::Path};

use once_cell::sync::Lazy;
use regex::Regex;
use roxy_core::roxy::Parse;

const DEFAULT_CONTEXT: Lazy<tera::Context> = Lazy::new(|| tera::Context::default());
const EXPANSION_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r#"\{% (extends|include|import) "?(.+?)"? .*?%\}"#).expect("couldn't load regex")
});

#[derive(Debug)]
pub struct TeraParserOptions {
    auto_load_layouts: bool,
}

impl Default for TeraParserOptions {
    fn default() -> Self {
        Self {
            auto_load_layouts: true,
        }
    }
}

#[derive(Debug)]
pub struct TeraParser<'a> {
    pub tera: &'a mut tera::Tera,
    context: Option<&'a tera::Context>,
    options: TeraParserOptions,
}

impl<'a> TeraParser<'a> {
    pub fn new(tera: &'a mut tera::Tera, options: TeraParserOptions) -> Self {
        Self {
            tera,
            context: None,
            options,
        }
    }

    pub fn add_context(&mut self, context: &'a tera::Context) {
        self.context = Some(context);
    }

    fn load_template<P: AsRef<Path>>(&mut self, path: &P, src: &[u8]) -> Result<(), tera::Error> {
        let path = path.as_ref().canonicalize()?;
        let str = String::from_utf8_lossy(src).to_string();
        for (_, [_, layout_path]) in EXPANSION_RE
            .captures_iter(&str.as_str())
            .map(|c| c.extract())
        {
            let path = path
                .canonicalize()?
                .parent()
                .map(|p| p.join(layout_path))
                .unwrap();

            let next_template = fs::read(&path)?;
            self.load_template(&path, &next_template)?;

            self.tera.add_template_file(&path, Some(layout_path))?;
        }

        Ok(())
    }
}

impl<'a> Parse for TeraParser<'a> {
    fn parse(
        &mut self,
        path: &str,
        src: &[u8],
        dst: &mut Vec<u8>,
    ) -> Result<(), roxy_core::error::Error> {
        // TODO: This error is a hack
        let err = |e: tera::Error| {
            println!("{e:?}");
            std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())
        };

        if self.options.auto_load_layouts {
            self.load_template(&path, src).map_err(err)?;
        }

        let template = String::from_utf8_lossy(src).to_string();

        self.tera
            .add_raw_template(path, template.as_str())
            .map_err(err)?;

        self.tera
            .render_to(path, self.context.unwrap_or(&DEFAULT_CONTEXT), dst)
            .map_err(err)?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use roxy_core::roxy::Parse;

    use crate::{TeraParser, TeraParserOptions};

    #[test]
    fn capture_test() {
        let mut tera = tera::Tera::default();
        let mut parser = TeraParser::new(&mut tera, TeraParserOptions::default());
        let data = fs::read("./tests/index.md").unwrap();
        let mut dst = Vec::new();
        parser.parse("./tests/index.md", &data, &mut dst).unwrap();
        let expect = [
            10, 32, 32, 10, 32, 32, 10, 60, 112, 62, 104, 105, 32, 102, 114, 111, 109, 32, 97, 32,
            109, 97, 99, 114, 111, 33, 32, 110, 119, 110, 60, 47, 112, 62, 10, 10, 32, 32, 60, 112,
            62, 102, 114, 111, 109, 32, 97, 110, 32, 105, 110, 99, 108, 117, 100, 101, 33, 33, 33,
            60, 47, 112, 62, 10, 10, 10, 10, 32, 32, 10, 32, 32, 32, 32, 10, 32, 32, 10, 32, 32,
            32, 32, 58, 51, 10, 10, 10, 10,
        ];
        assert_eq!(expect, dst.as_slice());
    }
}