rshtml_core 0.5.0

RsHtml: A Template Engine for Seamless HTML and Rust Integration.
Documentation
mod block;
mod component;
mod inner_text;
mod match_expr;
mod raw_block;
mod rust_block;
mod rust_expr;
mod rust_expr_paren;
mod rust_expr_simple;
mod template;
mod template_params;
mod text;
mod use_directive;

use crate::{
    config::Config,
    error::{E, rename_rules},
    node::*,
    parser::{
        block::BlockParser, component::ComponentParser, inner_text::InnerTextParser,
        match_expr::MatchExprParser, raw_block::RawBlockParser, rust_block::RustBlockParser,
        rust_expr::RustExprParser, rust_expr_paren::RustExprParenParser,
        rust_expr_simple::RustExprSimpleParser, template::TemplateParser,
        template_params::TemplateParamsParser, text::TextParser, use_directive::UseDirectiveParser,
    },
};
use pest::{
    Parser, Span,
    error::Error,
    iterators::{Pair, Pairs},
};
use pest_derive::Parser;
use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

#[derive(Parser)]
#[grammar = "rshtml.pest"]
pub struct RsHtmlParser {
    config: Config,
    files: Vec<PathBuf>,
    pub sources: HashMap<PathBuf, String>,
    fns: Vec<Function>,
}

impl RsHtmlParser {
    pub fn new() -> Self {
        Self {
            config: Config::default(),
            files: Vec::new(),
            sources: HashMap::new(),
            fns: Vec::new(),
        }
    }

    fn build_nodes_from_pairs(
        &mut self,
        pairs: Pairs<Rule>,
    ) -> Result<Vec<Node>, Box<Error<Rule>>> {
        let mut nodes = Vec::new();
        for pair in pairs {
            match pair.as_rule() {
                Rule::template_content => {
                    let inner_nodes = self.build_nodes_from_pairs(pair.into_inner())?;
                    nodes.extend(inner_nodes);
                }
                Rule::template_params | Rule::block | Rule::text | Rule::inner_text => {
                    nodes.push(self.build_ast_node(pair)?);
                }
                // skip other rules (EOI, WHITESPACE, etc.)
                _ => {}
            }
        }
        Ok(nodes)
    }

    fn build_ast_node(&mut self, pair: Pair<Rule>) -> Result<Node, Box<Error<Rule>>> {
        match pair.as_rule() {
            Rule::template => TemplateParser::parse(self, pair),
            Rule::text => TextParser::parse(self, pair),
            Rule::inner_text => InnerTextParser::parse(self, pair),
            Rule::template_params => TemplateParamsParser::parse(self, pair),
            Rule::block => BlockParser::parse(self, pair),
            Rule::rust_block => RustBlockParser::parse(self, pair),
            Rule::rust_expr_simple => RustExprSimpleParser::parse(self, pair),
            Rule::rust_expr_paren => RustExprParenParser::parse(self, pair),
            Rule::rust_expr => RustExprParser::parse(self, pair),
            Rule::match_expr => MatchExprParser::parse(self, pair),
            Rule::component => ComponentParser::parse(self, pair),
            Rule::child_content_directive => Ok(Node::ChildContent),
            Rule::raw_block => RawBlockParser::parse(self, pair),
            Rule::use_directive => UseDirectiveParser::parse(self, pair),
            Rule::continue_directive => Ok(Node::ContinueDirective),
            Rule::break_directive => Ok(Node::BreakDirective),
            rule => Err(E::mes(format!("Error: Unknown rule: {rule:?}")).span(pair.as_span())),
        }
    }

    fn parse_template(&mut self, path: &Path) -> Result<Node, Box<Error<Rule>>> {
        let input = self.read_template(path).map_err(|err| {
            E::mes(format!(
                "Error reading template: {err:?}, path: {}",
                path.display()
            ))
            .span(Span::new(path.to_string_lossy().to_string().as_str(), 0, 0).unwrap())
        })?;

        let mut pairs = Self::parse(Rule::template, &input)?;
        let template_pair = pairs.next().ok_or(
            E::mes("Error: Empty template").position(pest::Position::new("Template", 0).unwrap()),
        )?;

        if template_pair.as_rule() == Rule::template {
            if self.files.contains(&path.to_owned()) {
                return Err(E::mes(format!(
                    "Error: Circular call detected for '{}'",
                    path.display()
                ))
                .span(template_pair.as_span()));
            }

            self.sources
                .entry(path.to_owned())
                .or_insert_with(|| input.clone());
            self.files.push(path.to_owned());

            let ast = self.build_ast_node(template_pair)?;

            self.files.pop();

            Ok(ast)
        } else {
            Err(E::pos(Rule::template).span(template_pair.as_span()))
        }
    }

    fn read_template(&self, path: &Path) -> Result<String, String> {
        let view_path = self.config.base_path.join(path);
        let template = std::fs::read_to_string(&view_path).map_err(|err| {
            format!(
                "Error reading template: {:?}, path: {}",
                err,
                view_path.to_string_lossy()
            )
        })?;

        Ok(template)
    }

    pub fn run(&mut self, path: &str, config: Config) -> Result<Node, Box<Error<Rule>>> {
        self.config = config;
        let path = PathBuf::from(path);

        self.parse_template(&path).map_err(|err| rename_rules(*err))
    }

    fn extract_component_name(&self, path: &Path) -> Option<String> {
        let filename = path.file_name().and_then(|n| n.to_str())?;
        let component_name = filename.strip_suffix(".rs.html").unwrap_or(filename);
        Some(component_name.to_owned())
    }
}

pub trait IParser {
    fn parse(parser: &mut RsHtmlParser, pair: Pair<Rule>) -> Result<Node, Box<Error<Rule>>>;
}