From 4470af39268fa8160cb33dfd88871139526a84c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Fri, 6 Mar 2026 12:56:16 +0100 Subject: [PATCH] Add proper impl for templating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/ast/mod.rs | 13 ++++++++--- src/emit/mod.rs | 2 +- src/eval/mod.rs | 10 ++++---- src/lib.rs | 58 +++++++++++++++++++++++++++++++++++++++++------ src/parser/mod.rs | 29 +++++++++++++++--------- 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 313bab4..e5d9d82 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1,3 +1,4 @@ +use thiserror::Error; use winnow::Parser; use winnow::RecoverableParser; use winnow::combinator::alt; @@ -113,9 +114,15 @@ impl ParserError> for AstError { } } -#[derive(Debug)] +#[derive(Debug, Error)] pub struct AstFailure {} +impl std::fmt::Display for AstFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("TODO") + } +} + impl AstFailure { fn from_errors(_errors: Vec, _input: &[TemplateToken]) -> AstFailure { AstFailure {} @@ -217,7 +224,7 @@ mod tests { fn check_only_content() { let input = "Hello World"; - let parsed = crate::parser::parse(input).unwrap(); + let parsed = crate::parser::parse(input.into()).unwrap(); let ast = parse(parsed.tokens()).unwrap(); @@ -239,7 +246,7 @@ mod tests { fn check_simple_variable_interpolation() { let input = "Hello {{= world }}"; - let parsed = crate::parser::parse(input).unwrap(); + let parsed = crate::parser::parse(input.into()).unwrap(); let ast = parse(parsed.tokens()).unwrap(); diff --git a/src/emit/mod.rs b/src/emit/mod.rs index 6c5dd9e..207866b 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -122,7 +122,7 @@ mod tests { fn check_simple_variable_interpolation() { let input = "Hello {{= world }}"; - let parsed = crate::parser::parse(input).unwrap(); + let parsed = crate::parser::parse(input.into()).unwrap(); let ast = crate::ast::parse(parsed.tokens()).unwrap(); diff --git a/src/eval/mod.rs b/src/eval/mod.rs index a52f148..8984e2b 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -8,14 +8,14 @@ use crate::emit::Instruction; use crate::input::TempleInput; #[derive(Debug, Error, Display)] -enum EvalError { +pub enum EvaluationError { /// An unknown variable was encountered: .0 UnknownVariable(TempleInput), /// An explicit abort was requested ExplicitAbort, } -fn execute(instructions: &[Instruction], global_context: &Context) -> Result { +pub fn execute(instructions: &[Instruction], global_context: &Context) -> Result { let mut output = String::new(); let mut scopes: HashMap = HashMap::new(); @@ -27,7 +27,7 @@ fn execute(instructions: &[Instruction], global_context: &Context) -> Result Result todo!(), - Instruction::Abort => return Err(EvalError::ExplicitAbort), + Instruction::Abort => return Err(EvaluationError::ExplicitAbort), } } @@ -52,7 +52,7 @@ mod tests { fn check_simple_variable_interpolation() { let input = "Hello {{= world }}"; - let parsed = crate::parser::parse(input).unwrap(); + let parsed = crate::parser::parse(input.into()).unwrap(); let ast = crate::ast::parse(parsed.tokens()).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 9601158..d72e899 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,16 +5,36 @@ use displaydoc::Display; use serde::Serialize; use thiserror::Error; +use crate::emit::Instruction; +use crate::input::TempleInput; + pub mod ast; pub mod emit; pub mod eval; -pub mod parser; mod input; +pub mod parser; #[derive(Debug, Error, Display)] pub enum TempleError { /// Could not parse the given template - ParseError {}, + ParseError { + #[from] + source: parser::ParseFailure, + }, + /// Invalid Template + AstError { + #[from] + source: ast::AstFailure, + }, + + /// An error occurred while evaluating + EvaluationError { + #[from] + source: eval::EvaluationError, + }, + + /// The template '{0}' could not be found + UnknownTemplate(String), } pub struct Temple { @@ -37,17 +57,41 @@ impl Temple { pub fn add_template( &mut self, name: impl Into, - value: impl AsRef, + value: impl Into, ) -> Result<(), TempleError> { + let source = value.into(); + let parse = parser::parse(source.clone())?; + let ast = ast::parse(parse.tokens())?; + + let instructions = emit::emit_machine(ast); + + self.templates.insert( + name.into(), + Template { + source, + instructions, + }, + ); + Ok(()) } - fn render(&self, arg: &str, ctx: &Context) -> Result { - Ok(String::new()) + pub fn render(&self, name: &str, ctx: &Context) -> Result { + let template = self + .templates + .get(name) + .ok_or_else(|| TempleError::UnknownTemplate(name.to_string()))?; + + let res = eval::execute(&template.instructions, ctx)?; + + Ok(res) } } -struct Template {} +struct Template { + source: TempleInput, + instructions: Vec, +} pub struct Context { values: BTreeMap, @@ -155,6 +199,6 @@ mod tests { let rendered = temp.render("base", &ctx).unwrap(); - insta::assert_snapshot!(rendered, @"") + insta::assert_snapshot!(rendered, @"Hello World") } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ecf86d2..ce51f9f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4,6 +4,7 @@ use annotate_snippets::AnnotationKind; use annotate_snippets::Level; use annotate_snippets::Renderer; use annotate_snippets::Snippet; +use thiserror::Error; use winnow::LocatingSlice; use winnow::Parser; use winnow::RecoverableParser; @@ -37,16 +38,22 @@ use crate::resume_after_cut; type Input<'input> = Recoverable, ParseError>; type PResult<'input, T> = Result; -#[derive(Debug)] +#[derive(Debug, Error)] pub struct ParseFailure { - source: Arc, + input: Arc, errors: Vec, } +impl std::fmt::Display for ParseFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_report()) + } +} + impl ParseFailure { - fn from_errors(errors: Vec, input: &str) -> ParseFailure { + fn from_errors(errors: Vec, input: TempleInput) -> ParseFailure { ParseFailure { - source: Arc::from(input.to_string()), + input: Arc::from(input.to_string()), errors, } } @@ -64,7 +71,7 @@ impl ParseFailure { .unwrap_or("An error occurred while parsing"), ) .element( - Snippet::source(self.source.as_ref()).annotation( + Snippet::source(self.input.as_ref()).annotation( AnnotationKind::Primary .span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)), ), @@ -294,9 +301,9 @@ impl TemplateToken { } } -pub fn parse(input: &str) -> Result { +pub fn parse(input: TempleInput) -> Result { let (_remaining, val, errors) = - parse_tokens.recoverable_parse(LocatingSlice::new(TempleInput::from(input))); + parse_tokens.recoverable_parse(LocatingSlice::new(input.clone())); if errors.is_empty() && let Some(val) = val @@ -418,7 +425,7 @@ mod tests { #[test] fn parse_simple() { let input = "Hello There"; - let output = parse(input); + let output = parse(input.into()); insta::assert_debug_snapshot!(output, @r#" Ok( @@ -437,7 +444,7 @@ mod tests { #[test] fn parse_interpolate() { let input = "Hello {{ there }}"; - let output = parse(input); + let output = parse(input.into()); insta::assert_debug_snapshot!(output, @r#" Ok( @@ -480,12 +487,12 @@ mod tests { #[test] fn parse_interpolate_bad() { let input = "Hello {{ the2re }} {{ the@re }}"; - let output = parse(input); + let output = parse(input.into()); insta::assert_debug_snapshot!(output, @r#" Err( ParseFailure { - source: "Hello {{ the2re }} {{ the@re }}", + input: "Hello {{ the2re }} {{ the@re }}", errors: [ ParseError { message: Some(