diff --git a/Cargo.lock b/Cargo.lock index 60269ce..72dd9f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,15 +240,6 @@ dependencies = [ "syn", ] -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - [[package]] name = "either" version = "1.15.0" @@ -429,12 +420,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - [[package]] name = "log" version = "0.4.29" @@ -454,9 +439,7 @@ dependencies = [ "annotate-snippets", "criterion", "displaydoc", - "document-features", "insta", - "nomo", "serde", "serde_json", "test_each_file", diff --git a/Cargo.toml b/Cargo.toml index 6378b08..35f087d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,14 +16,9 @@ harness = false [profile.bench] debug = true -[lints.rust] -unsafe_code = "forbid" -missing_docs = "warn" - [dependencies] annotate-snippets = "0.12.13" displaydoc = "0.2.5" -document-features = { version = "0.2.12", optional = true } serde_json = { version = "1.0.149", optional = true } thiserror = "2.0.18" winnow = { version = "0.7.14", features = ["unstable-recover"] } @@ -35,19 +30,11 @@ insta = { version = "1.46.3", features = ["glob", "serde"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" test_each_file = "0.3.7" -nomo = { path = ".", features = ["unstable-pub"] } [profile.dev.package] insta.opt-level = 3 similar.opt-level = 3 -[package.metadata.docs.rs] -features = ["document-features"] - [features] default = ["serde_json"] -## Add support for inserting [`serde_json::Value`]s into [`Context`] objects serde_json = ["dep:serde_json"] -## Get access to the internals of the crate ⚠️ This is a perma-unstable feature ⚠️ -unstable-pub = [] -document-features = ["dep:document-features"] diff --git a/benches/asting.rs b/benches/asting.rs index 5cf1b43..3e8d755 100644 --- a/benches/asting.rs +++ b/benches/asting.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use criterion::BenchmarkId; use criterion::Criterion; use criterion::criterion_group; @@ -22,7 +20,7 @@ fn asting_benchmark(c: &mut Criterion) { parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| { b.iter(|| { let tokens = nomo::lexer::parse(input.clone()).unwrap(); - let _ast = nomo::parser::parse(input.clone(), tokens.tokens()).unwrap(); + let _ast = nomo::parser::parse(tokens.tokens()).unwrap(); }); }); } @@ -46,7 +44,7 @@ fn asting_nested(c: &mut Criterion) { parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| { b.iter(|| { let tokens = nomo::lexer::parse(input.clone()).unwrap(); - let _ast = nomo::parser::parse(input.clone(), tokens.tokens()).unwrap(); + let _ast = nomo::parser::parse(tokens.tokens()).unwrap(); }); }); } diff --git a/benches/parsing.rs b/benches/parsing.rs index 455a964..20548e6 100644 --- a/benches/parsing.rs +++ b/benches/parsing.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use criterion::BenchmarkId; use criterion::Criterion; use criterion::criterion_group; diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 2cf07f7..ee307ed 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -112,7 +112,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "nomo" -version = "0.0.1" +version = "0.1.0" dependencies = [ "annotate-snippets", "displaydoc", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index fe1acb7..e6af9c6 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -12,7 +12,6 @@ libfuzzer-sys = "0.4" [dependencies.nomo] path = ".." -features = ["unstable-pub"] [[bin]] name = "fuzz_target_1" diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index 8a18816..4fc3644 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -63,7 +63,6 @@ pub enum Instruction { slot: VariableSlot, }, PushScope { - #[allow(unused)] inherit_parent: bool, }, Abort, @@ -94,7 +93,6 @@ pub enum Instruction { value_slot: VariableSlot, }, LoadLiteralToSlot { - #[allow(unused)] source: TemplateToken, value: NomoValue, slot: VariableSlot, @@ -579,15 +577,14 @@ fn emit_expr_load( #[cfg(test)] mod tests { use crate::compiler::emit_machine; - use crate::input::NomoInput; #[test] fn check_simple_variable_interpolation() { - let input = NomoInput::from("Hello {{= world }}"); + let input = "Hello {{= world }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = crate::parser::parse(input, parsed.tokens()).unwrap(); + let ast = crate::parser::parse(parsed.tokens()).unwrap(); let emit = emit_machine(ast); @@ -620,12 +617,11 @@ mod tests { #[test] fn check_if_else_if() { - let input = - NomoInput::from("{{ if foo }} foo {{ else if bar }} bar {{ else }} foobar {{ end }}"); + let input = "{{ if foo }} foo {{ else if bar }} bar {{ else }} foobar {{ end }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = crate::parser::parse(input, parsed.tokens()).unwrap(); + let ast = crate::parser::parse(parsed.tokens()).unwrap(); let emit = emit_machine(ast); @@ -634,11 +630,11 @@ mod tests { #[test] fn check_function_call() { - let input = NomoInput::from("{{ if foo(23) }} bar {{ else }} foobar {{ end }}"); + let input = "{{ if foo(23) }} bar {{ else }} foobar {{ end }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = crate::parser::parse(input, parsed.tokens()).unwrap(); + let ast = crate::parser::parse(parsed.tokens()).unwrap(); let emit = emit_machine(ast); diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index 11df09a..0000000 --- a/src/errors.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::sync::Arc; - -use annotate_snippets::AnnotationKind; -use annotate_snippets::Level; -use annotate_snippets::Patch; -use annotate_snippets::Renderer; -use annotate_snippets::Snippet; -use thiserror::Error; -use winnow::stream::Offset; - -use crate::input::NomoInput; -use crate::lexer::ParseError; -use crate::parser::AstError; - -/// An error occurred while producing an Ast -#[derive(Debug, Error)] -pub struct AstFailure { - errors: Vec, - input: NomoInput, -} - -impl std::fmt::Display for AstFailure { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.render(f) - } -} - -impl AstFailure { - pub(crate) fn from_errors(errors: Vec, source: NomoInput) -> AstFailure { - AstFailure { - errors, - input: source, - } - } - - /// Create a CLI printable report - pub fn to_report(&self) -> String { - self.to_string() - } - - /// Render this failure to the given formatter - /// - /// Note, you can also use [`to_string`](ToString::to_string), as this type also implements - /// [`Display`](std::fmt::Display). - pub fn render(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let renderer = annotate_snippets::Renderer::styled() - .decor_style(annotate_snippets::renderer::DecorStyle::Unicode); - - for error in &self.errors { - let mut snippets = vec![ - annotate_snippets::Level::ERROR - .primary_title( - error - .message - .as_deref() - .unwrap_or("An error occurred while producing an Ast"), - ) - .element( - annotate_snippets::Snippet::source(self.input.as_str()).annotation( - annotate_snippets::AnnotationKind::Primary.span( - constrain_without_whitespace( - self.input.as_ref(), - error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0), - ), - ), - ), - ) - .elements( - error - .help - .as_ref() - .map(|help| annotate_snippets::Level::HELP.message(help)), - ), - ]; - if let Some((range, help)) = &error.replacement { - snippets.push( - annotate_snippets::Level::NOTE - .secondary_title("Try adding it") - .element( - Snippet::source(self.input.as_str()) - .patch(Patch::new(range.range.clone(), help.clone())), - ), - ); - } - - writeln!(f, "{}", renderer.render(&snippets))?; - writeln!(f)?; - } - - Ok(()) - } -} - -fn constrain_without_whitespace( - input: &str, - range: std::ops::Range, -) -> std::ops::Range { - let trimmed = input[range].trim(); - let start = trimmed.offset_from(&input); - let end = start + trimmed.len(); - - start..end -} - -/// An error occurred during lexing -#[derive(Debug, Error)] -pub struct ParseFailure { - 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 { - pub(crate) fn from_errors(errors: Vec, input: NomoInput) -> ParseFailure { - ParseFailure { - input: Arc::from(input.to_string()), - errors, - } - } - - /// Produce a CLi printable report - pub fn to_report(&self) -> String { - let reports = self - .errors - .iter() - .map(|error| { - Level::ERROR - .primary_title( - error - .message - .as_deref() - .unwrap_or("An error occurred while parsing"), - ) - .element( - Snippet::source(self.input.as_ref()).annotation( - AnnotationKind::Primary - .span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)), - ), - ) - .elements(error.help.as_ref().map(|help| Level::HELP.message(help))) - }) - .collect::>(); - - let renderer = - Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode); - renderer.render(&reports) - } -} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 7a0957b..283432a 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -297,15 +297,14 @@ mod tests { use crate::eval::execute; use crate::functions::FunctionMap; use crate::functions::NomoFunctionError; - use crate::input::NomoInput; #[test] fn check_simple_variable_interpolation() { - let input = NomoInput::from("Hello {{= world }}"); + let input = "Hello {{= world }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = crate::parser::parse(input, parsed.tokens()).unwrap(); + let ast = crate::parser::parse(parsed.tokens()).unwrap(); let emit = crate::compiler::emit_machine(ast); @@ -322,11 +321,11 @@ mod tests { #[test] fn check_method_call() { - let input = NomoInput::from("Hello {{= foo(world) }}"); + let input = "Hello {{= foo(world) }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = crate::parser::parse(input, parsed.tokens()).unwrap(); + let ast = crate::parser::parse(parsed.tokens()).unwrap(); let emit = crate::compiler::emit_machine(ast); @@ -349,11 +348,11 @@ mod tests { #[test] fn check_conditional_access() { - let input = NomoInput::from("Hello {{= unknown? }}"); + let input = "Hello {{= unknown? }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = crate::parser::parse(input, parsed.tokens()).unwrap(); + let ast = crate::parser::parse(parsed.tokens()).unwrap(); let emit = crate::compiler::emit_machine(ast); diff --git a/src/functions.rs b/src/functions.rs index 1a2883d..c15031c 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -7,57 +7,42 @@ use thiserror::Error; use crate::NomoValueError; use crate::value::NomoValue; -/// Possible errors while executing a function #[derive(Debug, Error, Display)] pub enum NomoFunctionError { /// Received {received} arguments, but this function only takes {expected} - #[expect(missing_docs)] WrongArgumentCount { received: usize, expected: usize }, /// The argument at this position is of the wrong type - #[expect(missing_docs)] InvalidArgumentType { index: usize }, /// A user-provided error #[error(transparent)] CustomError { - #[expect(missing_docs)] custom: Box, }, } -/// A function that can be used inside a template pub trait NomoFunction: 'static + Send + Sync { - /// Call the function with the given arguments fn call(&self, args: Vec) -> Result; } -#[cfg(feature = "unstable-pub")] #[derive(Default)] -#[expect(missing_docs)] pub struct FunctionMap { funcs: HashMap, } -#[cfg(not(feature = "unstable-pub"))] -#[derive(Default)] -pub(crate) struct FunctionMap { - funcs: HashMap, -} - impl FunctionMap { - #[expect(missing_docs)] pub fn register, T>(&mut self, name: impl Into, func: NF) { self.funcs .insert(name.into(), ErasedNomoFunction::erase(func)); } - pub(crate) fn get(&self, name: impl AsRef) -> Option<&ErasedNomoFunction> { + pub fn get(&self, name: impl AsRef) -> Option<&ErasedNomoFunction> { self.funcs.get(name.as_ref()) } } -pub(crate) struct ErasedNomoFunction { +pub struct ErasedNomoFunction { func: Box, call_fn: fn(&dyn Any, Vec) -> Result, } diff --git a/src/input.rs b/src/input.rs index 70c01b2..ba404eb 100644 --- a/src/input.rs +++ b/src/input.rs @@ -8,7 +8,6 @@ use winnow::stream::Offset; use winnow::stream::Stream; use winnow::stream::StreamIsPartial; -/// The input for templates in [nomo](crate) #[derive(Clone, PartialEq, Eq)] pub struct NomoInput { backing: Arc, @@ -16,26 +15,19 @@ pub struct NomoInput { } impl NomoInput { - /// Manually create an input - /// - /// While it is not unsafe to pass in mismatched informations, the outcome will most likely not - /// work and possibly even panic later pub fn from_parts(backing: Arc, range: Range) -> NomoInput { NomoInput { backing, range } } - /// Turn the input into its parts pub fn into_parts(self) -> (Arc, Range) { (self.backing, self.range) } - /// Get the range this input covers pub fn get_range(&self) -> Range { self.range.clone() } } -#[doc(hidden)] #[derive(Debug, Clone)] pub struct NomoInputCheckpoint { range: Range, @@ -86,7 +78,6 @@ impl Deref for NomoInput { } impl NomoInput { - /// Get the input as a [`str`] pub fn as_str(&self) -> &str { self.deref() } @@ -110,8 +101,6 @@ impl Offset for NomoInput { } } - -#[doc(hidden)] pub struct NomoInputIter { idx: usize, input: NomoInput, diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 94daf26..277f3e3 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -1,3 +1,10 @@ +use std::sync::Arc; + +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; @@ -32,13 +39,60 @@ use winnow::token::take_until; use winnow::token::take_while; use crate::SourceSpan; -use crate::errors::ParseFailure; use crate::input::NomoInput; use crate::resume_after_cut; type Input<'input> = Recoverable, ParseError>; type PResult<'input, T> = Result; +#[derive(Debug, Error)] +pub struct ParseFailure { + 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: NomoInput) -> ParseFailure { + ParseFailure { + input: Arc::from(input.to_string()), + errors, + } + } + + pub fn to_report(&self) -> String { + let reports = self + .errors + .iter() + .map(|error| { + Level::ERROR + .primary_title( + error + .message + .as_deref() + .unwrap_or("An error occurred while parsing"), + ) + .element( + Snippet::source(self.input.as_ref()).annotation( + AnnotationKind::Primary + .span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)), + ), + ) + .elements(error.help.as_ref().map(|help| Level::HELP.message(help))) + }) + .collect::>(); + + let renderer = + Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode); + renderer.render(&reports) + } +} + #[derive(Debug, Clone)] pub struct ParseError { pub(crate) message: Option, diff --git a/src/lib.rs b/src/lib.rs index abddcf5..7874d4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,102 +1,3 @@ -//! # Nomo, the templating library -//! -//! To get started, add the crate to your project: -//! -//! ```bash -//! cargo add nomo -//! ``` -//! -//! Then, load some templates: -//! -//! ```rust -//! # fn main() -> Result<(), Box> { -//! let mut templates = nomo::Nomo::new(); -//! templates.add_template("index.html", "

Hello {{= name }}!

"); -//! -//! let mut context = nomo::Context::new(); -//! context.insert("name", "World"); -//! let result = templates.render("index.html", &context)?; -//! -//! assert_eq!(result, "

Hello World!

"); -//! # Ok(()) } -//! ``` -//! -//! The crate has the following feature flags: -#![cfg_attr( - feature = "document-features", - cfg_attr(doc, doc = ::document_features::document_features!()) -)] -//! -//! ## Syntax -//! -//! Nomo tries to keep a consistent syntax across its features. -//! The main concepts are: -//! -//! [`nomo`](crate) uses `{{ }}` as its identifiers. Each of them forms a 'block'. -//! -//! There are two kinds of blocks: -//! -//! - `{{= }}` Interpolations -//! - `{{ }}` everything else -//! -//! ### Expressions -//! -//! An expression in [`nomo`](crate) is anything that produces a value. Notably, control structures -//! _do not_ create values. This is different to rust. -//! -//! So for example this does not work: -//! -//! ```nomo -//! {{= if is_active "Active" else "Inactive" end }} -//! ``` -//! -//! If you wish to conditionally output something, you would write: -//! -//! ```nomo -//! {{ if is_active }} -//! Active -//! {{ else }} -//! Inactive -//! {{ end }} -//! ``` -//! -//! In expressions you can write: -//! - Mathematical expressions (`+-*/`) -//! - Logical/Binary expressions (`&&`, `||`, `>`, ...) -//! - Literals (`12`, `292.21`, `"Hello"`) -//! -//! ### Interpolations -//! -//! Interpolations is how one prints out data. A [`NomoValue`] can be printed if it is a: -//! -//! - String -//! - Integer/SignedInteger -//! - Bool -//! - Float -//! -//! All other values will result in an error. -//! -//! ### Control Structures -//! -//! [`Nomo`](crate) supports several control structures: -//! -//! **Conditions `if/else`**: -//! -//! ```nomo -//! {{ if }} -//! {{ else if }} -//! {{ else }} -//! {{ end }} -//! ``` -//! -//! **Loops `for .. in`** -//! -//! ```nomo -//! {{ for in }} -//! {{ else }} -//! {{ end }} -//! ``` - use std::collections::HashMap; use displaydoc::Display; @@ -104,71 +5,34 @@ use thiserror::Error; use crate::compiler::VMInstructions; use crate::functions::FunctionMap; -use crate::functions::NomoFunction; use crate::input::NomoInput; use crate::value::NomoValue; use crate::value::NomoValueError; -macro_rules! unstable_pub { - ($(#[$m:meta])* mod $name:ident) => { - $(#[$m])* - #[cfg(feature = "unstable-pub")] - #[allow(missing_docs)] - pub mod $name; - #[cfg(not(feature = "unstable-pub"))] - mod $name; - }; -} - -unstable_pub!( - /// The compiler internals - mod compiler -); -unstable_pub!( - /// Evaluation internals - mod eval -); -unstable_pub!( - /// Lexer internals - mod lexer -); -unstable_pub!( - /// Parser internals - mod parser -); - -mod winnow_ext; - -/// Errors in this library -pub mod errors; - -/// Nomo Functions +pub mod compiler; +pub mod eval; pub mod functions; -/// Input for nomo pub mod input; -/// Values used in Nomo +pub mod lexer; +pub mod parser; pub mod value; -/// Errors related to parsing and evaluating templates #[derive(Debug, Error, Display)] pub enum NomoError { /// Could not parse the given template ParseError { #[from] - #[expect(missing_docs)] - source: errors::ParseFailure, + source: lexer::ParseFailure, }, /// Invalid Template AstError { #[from] - #[expect(missing_docs)] - source: errors::AstFailure, + source: parser::AstFailure, }, /// An error occurred while evaluating EvaluationError { #[from] - #[expect(missing_docs)] source: eval::EvaluationError, }, @@ -176,7 +40,6 @@ pub enum NomoError { UnknownTemplate(String), } -/// The main struct and entry point for the [`nomo`](crate) pub struct Nomo { templates: HashMap, function_map: FunctionMap, @@ -189,7 +52,6 @@ impl Default for Nomo { } impl Nomo { - /// Create a new Nomo Instance pub fn new() -> Nomo { Nomo { templates: HashMap::new(), @@ -197,7 +59,6 @@ impl Nomo { } } - /// Add a new template pub fn add_template( &mut self, name: impl Into, @@ -205,7 +66,7 @@ impl Nomo { ) -> Result<(), NomoError> { let source = value.into(); let parse = lexer::parse(source.clone())?; - let ast = parser::parse(source.clone(), parse.tokens())?; + let ast = parser::parse(parse.tokens())?; let instructions = compiler::emit_machine(ast); @@ -215,17 +76,6 @@ impl Nomo { Ok(()) } - /// Register a function to make it available when rendering - pub fn register_function(&mut self, name: impl Into, f: impl NomoFunction) { - self.function_map.register(name, f); - } - - /// List of currently available templates - pub fn templates(&self) -> Vec { - self.templates.keys().cloned().collect() - } - - /// Render a specific template pub fn render(&self, name: &str, ctx: &Context) -> Result { let template = self .templates @@ -242,7 +92,6 @@ struct Template { instructions: VMInstructions, } -/// The context for a given render call pub struct Context { values: HashMap, } @@ -254,16 +103,12 @@ impl Default for Context { } impl Context { - /// Create new Context pub fn new() -> Context { Context { values: HashMap::new(), } } - /// Add a value into the map - /// - /// If you enable the `serde_json` feature, you can insert [`serde_json::Value`]s pub fn try_insert( &mut self, key: impl Into, @@ -274,19 +119,17 @@ impl Context { Ok(()) } - /// Add a value into the map, panic if you can't pub fn insert(&mut self, key: impl Into, value: impl Into) { self.values.insert(key.into(), value.into()); } - /// Access the values inside this context pub fn values(&self) -> &HashMap { &self.values } } #[derive(Debug, Clone)] -struct SourceSpan { +pub struct SourceSpan { pub range: std::ops::Range, } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index decda96..43481a2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,3 +1,4 @@ +use thiserror::Error; use winnow::Parser; use winnow::RecoverableParser; use winnow::combinator::Infix::Left; @@ -20,7 +21,6 @@ use winnow::error::AddContext; use winnow::error::FromRecoverableError; use winnow::error::ModalError; use winnow::error::ParserError; -use winnow::stream::Location; use winnow::stream::Offset; use winnow::stream::Recoverable; use winnow::stream::Stream; @@ -28,8 +28,6 @@ use winnow::stream::TokenSlice; use winnow::token::any; use crate::SourceSpan; -use crate::errors::AstFailure; -use crate::input::NomoInput; use crate::lexer::TemplateToken; use crate::lexer::TokenKind; use crate::lexer::TokenOperator; @@ -52,7 +50,6 @@ pub struct AstError { pub(crate) message: Option, pub(crate) help: Option, pub(crate) span: Option, - pub(crate) replacement: Option<(SourceSpan, String)>, is_fatal: bool, } @@ -63,7 +60,6 @@ impl AstError { message: None, help: None, span: None, - replacement: None, is_fatal: false, } @@ -78,11 +74,6 @@ impl AstError { self.help = Some(help.to_string()); self } - - fn replacement(mut self, span: SourceSpan, replacement: &str) -> Self { - self.replacement = Some((span, replacement.to_string())); - self - } } impl ModalError for AstError { @@ -113,14 +104,13 @@ impl FromRecoverableError, AstError> for AstError { .filter(|t| t.kind() != TokenKind::Whitespace); let last = tokens.next(); let first = tokens.last(); - match (last, first) { (None, None) => None, (None, Some(single)) | (Some(single), None) => Some(SourceSpan { range: single.source().get_range(), }), (Some(last), Some(first)) => { - let start = first.source().get_range().end; + let start = first.source().get_range().start; let end = last.source().get_range().end; Some(SourceSpan { range: start..end }) @@ -141,7 +131,6 @@ impl AddContext, AstError> for AstError { ) -> Self { self.message = context.message.or(self.message); self.help = context.help.or(self.help); - self.replacement = context.replacement.or(self.replacement); self } } @@ -162,6 +151,55 @@ impl ParserError> for AstError { } } +#[derive(Debug, Error)] +pub struct AstFailure { + errors: Vec, +} + +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) -> AstFailure { + AstFailure { errors } + } + + pub fn to_report(&self, source: &str) -> String { + let reports = self + .errors + .iter() + .map(|error| { + annotate_snippets::Level::ERROR + .primary_title( + error + .message + .as_deref() + .unwrap_or("An error occurred while producing an Ast"), + ) + .element( + annotate_snippets::Snippet::source(source).annotation( + annotate_snippets::AnnotationKind::Primary + .span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)), + ), + ) + .elements( + error + .help + .as_ref() + .map(|help| annotate_snippets::Level::HELP.message(help)), + ) + }) + .collect::>(); + + let renderer = annotate_snippets::Renderer::styled() + .decor_style(annotate_snippets::renderer::DecorStyle::Unicode); + renderer.render(&reports) + } +} + type Input<'input> = Recoverable, AstError>; impl<'input> Parser, TemplateToken, AstError> for TokenKind { @@ -172,7 +210,7 @@ impl<'input> Parser, TemplateToken, AstError> for TokenKind { } } -pub fn parse(source: NomoInput, input: &[TemplateToken]) -> Result, AstFailure> { +pub fn parse(input: &[TemplateToken]) -> Result, AstFailure> { let (_remaining, val, errors) = parse_asts.recoverable_parse(TokenSlice::new(input)); if errors.is_empty() @@ -180,7 +218,7 @@ pub fn parse(source: NomoInput, input: &[TemplateToken]) -> Result { ElseConditional { expression: Option>>, }, - #[allow(unused)] Invalid(&'input [TemplateToken]), MathOperation { op: TokenOperator, @@ -319,42 +356,26 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result( - input: &mut Input<'input>, -) -> Result, AstError> { - let expression = peek(parse_expression).parse_next(input); - - let is_expr = match expression { - Ok(_) => true, - Err(err) if err.is_backtrack() => true, - _ => false, - }; - - if is_expr { - return Err(AstError::ctx() - .msg("Standlone action block") - .help("If you want to output this expression, add a '=' to the block") - .cut()); - } - - let keywords = peek(parse_keyword).parse_next(input); - - let is_keyword = keywords.is_ok(); - - if is_keyword { - return Err(AstError::ctx().msg("Unexpected keyword").cut()); - } - - Err(AstError::ctx().cut()) -} fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result, AstError> { trace("for_loop", |input: &mut Input<'input>| { let for_block = parse_for_loop.map(Box::new).parse_next(input)?; @@ -369,7 +390,7 @@ fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result(input: &mut Input<'input>) -> Result(input: &mut Input<'input>) -> Result, AstError> { trace( "for_loop_inner", - parse_block(|input: &mut Input<'input>| { - let _ignored = surrounded(ws, TokenKind::For).parse_next(input)?; - - let value_ident = - cut_err(TokenKind::Ident.context(AstError::ctx().msg("Missing ident here"))) - .parse_next(input)?; - - let _ignored = cut_err( - preceded(ws, TokenKind::In).context( - AstError::ctx() - .msg("Missing `in` in `for .. in ` loop") - .replacement( - SourceSpan { - range: input.current_token_start()..(input.current_token_start()), - }, - " in", - ), - ), + parse_block( + ( + ws, + TokenKind::For, + ws, + TokenKind::Ident, + ws, + TokenKind::In, + ws, + parse_expression.map(Box::new), ) - .parse_next(input)?; - - let value_expression = parse_expression.map(Box::new).parse_next(input)?; - - Ok(TemplateAstExpr::For { - value_ident, - value_expression, - }) - }), + .map(|(_, _for, _, value_ident, _, _in, _, value_expression)| { + TemplateAstExpr::For { + value_ident, + value_expression, + } + }), + ), ) .parse_next(input) } @@ -638,37 +649,11 @@ where fn parse_operand<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "operand", - alt(( - parse_keywords_fail, - parse_function, - parse_variable_access, - parse_literal, - )), + alt((parse_function, parse_variable_access, parse_literal)), ) .parse_next(input) } -fn parse_keyword<'input>(input: &mut Input<'input>) -> Result { - alt(( - TokenKind::ConditionalIf, - TokenKind::ConditionalElse, - TokenKind::For, - TokenKind::End, - TokenKind::In, - )) - .parse_next(input) -} - -fn parse_keywords_fail<'input>( - input: &mut Input<'input>, -) -> Result, AstError> { - let _value = parse_keyword.parse_next(input)?; - - Err(AstError::ctx() - .msg("Found literal, expected expression") - .cut()) -} - fn parse_literal<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "literal", @@ -772,7 +757,6 @@ mod tests { use winnow::combinator::fail; use winnow::stream::TokenSlice; - use crate::input::NomoInput; use crate::lexer::TokenKind; use crate::parser::AstError; use crate::parser::AstFailure; @@ -782,22 +766,25 @@ mod tests { use crate::parser::parse_block; use crate::parser::parse_end; - fn panic_pretty<'a>(tokens: Result, AstFailure>) -> TemplateAst<'a> { + fn panic_pretty<'a>( + input: &'_ str, + tokens: Result, AstFailure>, + ) -> TemplateAst<'a> { match tokens { Ok(ast) => ast, Err(failure) => { - panic!("{}", failure); + panic!("{}", failure.to_report(input)); } } } #[test] fn check_only_content() { - let input = NomoInput::from("Hello World"); + let input = "Hello World"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = parse(input, parsed.tokens()).unwrap(); + let ast = parse(parsed.tokens()).unwrap(); insta::assert_debug_snapshot!(ast, @r#" TemplateAst { @@ -812,11 +799,11 @@ mod tests { #[test] fn check_simple_variable_interpolation() { - let input = NomoInput::from("Hello {{= world }}"); + let input = "Hello {{= world }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = parse(input, parsed.tokens()).unwrap(); + let ast = parse(parsed.tokens()).unwrap(); insta::assert_debug_snapshot!(ast, @r#" TemplateAst { @@ -840,11 +827,11 @@ mod tests { #[test] fn check_simple_if() { - let input = NomoInput::from("{{ if foo }} Hiii {{ end }}"); + let input = "{{ if foo }} Hiii {{ end }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast, @r#" TemplateAst { @@ -885,39 +872,35 @@ mod tests { #[test] fn check_invalid_action() { - let input = NomoInput::from( - r#"{{ value }} + let input = r#"{{ value }} {{ value }} {{ value }} {{ value }} - {{ value }}"#, - ); + {{ value }}"#; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = parse(input, parsed.tokens()).unwrap_err(); + let ast = parse(parsed.tokens()).unwrap_err(); - insta::assert_snapshot!(ast); + insta::assert_snapshot!(ast.to_report(input)); } #[test] fn check_nested_simple_if() { - let input = NomoInput::from( - r#"{{ if foo }} + let input = r#"{{ if foo }} {{ if bar }} Hiii {{ end }} {{ end }} {{= value }} - "#, - ); + "#; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); insta::assert_debug_snapshot!("simple_if_tokens", parsed); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!("simple_if_ast", ast); } @@ -926,9 +909,9 @@ mod tests { fn check_parsing_block() { use winnow::RecoverableParser; - let input = NomoInput::from("{{ foo }}"); + let input = "{{ foo }}"; - let parsed = crate::lexer::parse(input).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); let result = alt(( parse_end, @@ -960,10 +943,9 @@ mod tests { help: None, span: Some( SourceSpan { - range: 2..6, + range: 0..6, }, ), - replacement: None, is_fatal: false, }, ], @@ -973,11 +955,11 @@ mod tests { #[test] fn check_empty_if_output() { - let input = NomoInput::from("{{ if foo }}{{ end }}"); + let input = "{{ if foo }}{{ end }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast, @r#" TemplateAst { @@ -1010,101 +992,99 @@ mod tests { #[test] fn check_if_else() { - let input = NomoInput::from("{{ if foo }} foo {{ else }} bar {{ end }}"); + let input = "{{ if foo }} foo {{ else }} bar {{ end }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_if_else_if() { - let input = NomoInput::from("{{ if foo }} foo {{ else if bar }} bar {{ end }}"); + let input = "{{ if foo }} foo {{ else if bar }} bar {{ end }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_trim_whitespace() { - let input = NomoInput::from("{{ if foo -}} foo {{- else if bar -}} bar {{- end }}"); + let input = "{{ if foo -}} foo {{- else if bar -}} bar {{- end }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_for_loop() { - let input = NomoInput::from( - "{{ for value in array }} Hi: {{= value }} {{ else }} No Content :C {{ end }}", - ); + let input = "{{ for value in array }} Hi: {{= value }} {{ else }} No Content :C {{ end }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_math_expression() { - let input = NomoInput::from("{{= 5 * 3 + 2 / 3 }}"); + let input = "{{= 5 * 3 + 2 / 3 }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_logical_expression() { - let input = NomoInput::from("{{= true && false || 3 >= 2 && 5 == 2 }}"); + let input = "{{= true && false || 3 >= 2 && 5 == 2 }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_function_call() { - let input = NomoInput::from("{{= foo(2 * 3, bar(2 + baz)) }}"); + let input = "{{= foo(2 * 3, bar(2 + baz)) }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_conditional_access() { - let input = NomoInput::from("{{= foo? }}"); + let input = "{{= foo? }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_access_operator() { - let input = NomoInput::from("{{= foo?.bar }}"); + let input = "{{= foo?.bar }}"; - let parsed = crate::lexer::parse(input.clone()).unwrap(); + let parsed = crate::lexer::parse(input.into()).unwrap(); - let ast = panic_pretty(parse(input, parsed.tokens())); + let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } diff --git a/src/parser/snapshots/nomo__parser__tests__check_invalid_action.snap b/src/parser/snapshots/nomo__parser__tests__check_invalid_action.snap index d4b9694..0587130 100644 --- a/src/parser/snapshots/nomo__parser__tests__check_invalid_action.snap +++ b/src/parser/snapshots/nomo__parser__tests__check_invalid_action.snap @@ -1,6 +1,6 @@ --- source: src/parser/mod.rs -expression: ast +expression: ast.to_report(input) --- error: Standlone action block  ╭▸  @@ -8,31 +8,23 @@ expression: ast │ ━━━━━ │ ╰ help: If you want to output this expression, add a '=' to the block - error: Standlone action block  ╭▸  2 │ {{ value }} │ ━━━━━ - │ ╰ help: If you want to output this expression, add a '=' to the block - error: Standlone action block  ╭▸  3 │ {{ value }} │ ━━━━━ - │ ╰ help: If you want to output this expression, add a '=' to the block - error: Standlone action block  ╭▸  4 │ {{ value }} │ ━━━━━ - │ ╰ help: If you want to output this expression, add a '=' to the block - error: Standlone action block  ╭▸  5 │ {{ value }} │ ━━━━━ - │ ╰ help: If you want to output this expression, add a '=' to the block diff --git a/src/value.rs b/src/value.rs index b652dfd..ab317bb 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,12 +1,11 @@ use std::borrow::Cow; use std::collections::BTreeMap; +#[cfg(feature = "serde_json")] use displaydoc::Display; use thiserror::Error; -/// Values to be used inside templates #[derive(Clone)] -#[expect(missing_docs)] pub enum NomoValue { String { value: Cow<'static, str>, @@ -36,7 +35,6 @@ pub enum NomoValue { } impl NomoValue { - /// Return a str if there is one inside pub fn as_str(&self) -> Option<&str> { if let Self::String { value } = self { Some(value) @@ -45,7 +43,6 @@ impl NomoValue { } } - /// Return an arry if there is one inside pub fn as_array(&self) -> Option<&[NomoValue]> { if let Self::Array { value } = self { Some(value) @@ -54,7 +51,6 @@ impl NomoValue { } } - /// Return a bool if there is one inside pub fn as_bool(&self) -> Option { if let Self::Bool { value } = self { Some(*value) @@ -65,7 +61,6 @@ impl NomoValue { } } - /// Return an object if there is one inside pub fn as_object(&self) -> Option<&BTreeMap> { if let Self::Object { value } = self { Some(value) @@ -74,7 +69,6 @@ impl NomoValue { } } - /// Return an integer if there is one inside pub fn as_integer(&self) -> Option { if let Self::Integer { value } = self { Some(*value) @@ -83,7 +77,6 @@ impl NomoValue { } } - /// Return a float if there is one inside pub fn as_float(&self) -> Option { if let Self::Float { value } = self { Some(*value) @@ -92,7 +85,6 @@ impl NomoValue { } } - /// Return the iterator if there is one inside pub fn as_iterator(&self) -> Option<&dyn CloneIterator> { if let Self::Iterator { value } = self { Some(value) @@ -101,7 +93,6 @@ impl NomoValue { } } - /// Return the iterator mutably if there is one inside pub fn as_iterator_mut(&mut self) -> Option<&mut dyn CloneIterator> { if let Self::Iterator { value } = self { Some(value) @@ -246,21 +237,19 @@ impl NomoValue { pub(crate) fn try_to_string(&self) -> Option { match self { NomoValue::String { value } => Some(value.to_string()), + NomoValue::Array { .. } => None, NomoValue::Bool { value } => Some(value.to_string()), + NomoValue::Object { .. } => None, NomoValue::Integer { value } => Some(value.to_string()), NomoValue::SignedInteger { value } => Some(value.to_string()), NomoValue::Float { value } => Some(value.to_string()), - NomoValue::Array { .. } => None, - NomoValue::Object { .. } => None, NomoValue::Iterator { .. } => None, NomoValue::Undefined => None, } } } -/// Marker trait for iterators that can be cloned pub trait CloneIterator: Iterator { - /// Create a new instance of the iterator inside a [Box] fn clone_box(&self) -> Box>; } diff --git a/src/winnow_ext.rs b/src/winnow_ext.rs deleted file mode 100644 index 4d7450a..0000000 --- a/src/winnow_ext.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::marker::PhantomData; - -use winnow::Parser; -use winnow::error::AddContext; -use winnow::error::ParserError; -use winnow::stream::Stream; - -pub trait ParserExt: Parser { - fn with_context(self, context: F) -> WithContext - where - Self: Sized, - I: Stream, - E: AddContext + ParserError, - F: Fn(&::Slice, &E) -> C, - { - WithContext { - parser: self, - context, - _pd: PhantomData, - } - } -} - -pub struct WithContext { - parser: P, - context: F, - _pd: PhantomData<(I, O, E, C)>, -} - -impl Parser for WithContext -where - P: Parser, - I: Stream, - E: AddContext + ParserError, - F: Fn(&::Slice, &E) -> C, -{ - fn parse_next(&mut self, input: &mut I) -> winnow::Result { - let start = input.checkpoint(); - - let res = self.parser.parse_next(input); - - res.map_err(|err| { - let offset = input.offset_from(&start); - input.reset(&start); - let taken = input.next_slice(offset); - - let context = (self.context)(&taken, &err); - err.add_context(input, &start, context) - }) - } -} diff --git a/tests/checks.rs b/tests/checks.rs index 29278b1..200ac20 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - #[test] fn check_files() { let files = std::fs::read_dir("tests/checks/").unwrap(); @@ -7,13 +5,11 @@ fn check_files() { for file in files { let input = std::fs::read_to_string(file.unwrap().path()).unwrap(); - let input = nomo::input::NomoInput::from(input); - - let Ok(parsed) = nomo::lexer::parse(input.clone()) else { + let Ok(parsed) = nomo::lexer::parse(input.into()) else { continue; }; - let Ok(ast) = nomo::parser::parse(input, parsed.tokens()) else { + let Ok(ast) = nomo::parser::parse(parsed.tokens()) else { continue; }; diff --git a/tests/errors/invalid_controls.1-parsed.snap b/tests/errors/invalid_controls.1-parsed.snap deleted file mode 100644 index 1ae070d..0000000 --- a/tests/errors/invalid_controls.1-parsed.snap +++ /dev/null @@ -1,119 +0,0 @@ ---- -source: tests/file_tests.rs -expression: parsed -info: - input: "{{ if }} {{ end }}\n{{ if if }} {{ end }}\n{{ if if }} {{ for foo in bar }} {{ end }} {{ end }}\n{{ for in bar }} {{ end }}\n{{ for blah bar }} {{ end }}\n{{ for blah}} {{ end }}\n{{ else }}" - context: {} ---- -ParsedTemplate { - tokens: [ - [LeftDelim]"{{" (0..2), - [Whitespace]" " (2..3), - [ConditionalIf]"if" (3..5), - [Whitespace]" " (5..6), - [RightDelim]"}}" (6..8), - [Whitespace]" " (8..9), - [LeftDelim]"{{" (9..11), - [Whitespace]" " (11..12), - [End]"end" (12..15), - [Whitespace]" " (15..16), - [RightDelim]"}}" (16..18), - [Whitespace]"\n" (18..19), - [LeftDelim]"{{" (19..21), - [Whitespace]" " (21..22), - [ConditionalIf]"if" (22..24), - [Whitespace]" " (24..25), - [ConditionalIf]"if" (25..27), - [Whitespace]" " (27..28), - [RightDelim]"}}" (28..30), - [Whitespace]" " (30..31), - [LeftDelim]"{{" (31..33), - [Whitespace]" " (33..34), - [End]"end" (34..37), - [Whitespace]" " (37..38), - [RightDelim]"}}" (38..40), - [Whitespace]"\n" (40..41), - [LeftDelim]"{{" (41..43), - [Whitespace]" " (43..44), - [ConditionalIf]"if" (44..46), - [Whitespace]" " (46..47), - [ConditionalIf]"if" (47..49), - [Whitespace]" " (49..50), - [RightDelim]"}}" (50..52), - [Whitespace]" " (52..53), - [LeftDelim]"{{" (53..55), - [Whitespace]" " (55..56), - [For]"for" (56..59), - [Whitespace]" " (59..60), - [Ident]"foo" (60..63), - [Whitespace]" " (63..64), - [In]"in" (64..66), - [Whitespace]" " (66..67), - [Ident]"bar" (67..70), - [Whitespace]" " (70..71), - [RightDelim]"}}" (71..73), - [Whitespace]" " (73..74), - [LeftDelim]"{{" (74..76), - [Whitespace]" " (76..77), - [End]"end" (77..80), - [Whitespace]" " (80..81), - [RightDelim]"}}" (81..83), - [Whitespace]" " (83..84), - [LeftDelim]"{{" (84..86), - [Whitespace]" " (86..87), - [End]"end" (87..90), - [Whitespace]" " (90..91), - [RightDelim]"}}" (91..93), - [Whitespace]"\n" (93..94), - [LeftDelim]"{{" (94..96), - [Whitespace]" " (96..97), - [For]"for" (97..100), - [Whitespace]" " (100..101), - [In]"in" (101..103), - [Whitespace]" " (103..104), - [Ident]"bar" (104..107), - [Whitespace]" " (107..108), - [RightDelim]"}}" (108..110), - [Whitespace]" " (110..111), - [LeftDelim]"{{" (111..113), - [Whitespace]" " (113..114), - [End]"end" (114..117), - [Whitespace]" " (117..118), - [RightDelim]"}}" (118..120), - [Whitespace]"\n" (120..121), - [LeftDelim]"{{" (121..123), - [Whitespace]" " (123..124), - [For]"for" (124..127), - [Whitespace]" " (127..128), - [Ident]"blah" (128..132), - [Whitespace]" " (132..133), - [Ident]"bar" (133..136), - [Whitespace]" " (136..137), - [RightDelim]"}}" (137..139), - [Whitespace]" " (139..140), - [LeftDelim]"{{" (140..142), - [Whitespace]" " (142..143), - [End]"end" (143..146), - [Whitespace]" " (146..147), - [RightDelim]"}}" (147..149), - [Whitespace]"\n" (149..150), - [LeftDelim]"{{" (150..152), - [Whitespace]" " (152..153), - [For]"for" (153..156), - [Whitespace]" " (156..157), - [Ident]"blah" (157..161), - [RightDelim]"}}" (161..163), - [Whitespace]" " (163..164), - [LeftDelim]"{{" (164..166), - [Whitespace]" " (166..167), - [End]"end" (167..170), - [Whitespace]" " (170..171), - [RightDelim]"}}" (171..173), - [Whitespace]"\n" (173..174), - [LeftDelim]"{{" (174..176), - [Whitespace]" " (176..177), - [ConditionalElse]"else" (177..181), - [Whitespace]" " (181..182), - [RightDelim]"}}" (182..184), - ], -} diff --git a/tests/errors/invalid_controls.2-ast.snap b/tests/errors/invalid_controls.2-ast.snap deleted file mode 100644 index e3d5494..0000000 --- a/tests/errors/invalid_controls.2-ast.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: tests/file_tests.rs -expression: ast -info: - input: "{{ if }} {{ end }}\n{{ if if }} {{ end }}\n{{ if if }} {{ for foo in bar }} {{ end }} {{ end }}\n{{ for in bar }} {{ end }}\n{{ for blah bar }} {{ end }}\n{{ for blah}} {{ end }}\n{{ else }}" - context: {} ---- -error: Expected an expression after 'if' -  ╭▸  -1 │ {{ if }} {{ end }} - ╰╴ ━━ - -error: Expected an expression after 'if' -  ╭▸  -2 │ {{ if if }} {{ end }} - ╰╴ ━━ - -error: Expected an expression after 'if' -  ╭▸  -3 │ {{ if if }} {{ for foo in bar }} {{ end }} {{ end }} - ╰╴ ━━ - -error: Missing ident here -  ╭▸  -4 │ {{ for in bar }} {{ end }} - ╰╴ ━━━━━━ - -error: Missing `in` in `for .. in ` loop -  ╭▸  -5 │ {{ for blah bar }} {{ end }} - │ ━━━━━━━━ - ╰╴ -note: Try adding it - ╭╴ -5 │ {{ for blah in bar }} {{ end }} - ╰╴ ++ - -error: Missing `in` in `for .. in ` loop -  ╭▸  -6 │ {{ for blah}} {{ end }} - │ ━━━━ - ╰╴ -note: Try adding it - ╭╴ -6 │ {{ for blah in}} {{ end }} - ╰╴ ++ - -error: Unexpected keyword -  ╭▸  -7 │ {{ else }} - ╰╴ ━━━━ diff --git a/tests/errors/invalid_controls.nomo b/tests/errors/invalid_controls.nomo deleted file mode 100644 index a9d079f..0000000 --- a/tests/errors/invalid_controls.nomo +++ /dev/null @@ -1,9 +0,0 @@ -{} ---- -{{ if }} {{ end }} -{{ if if }} {{ end }} -{{ if if }} {{ for foo in bar }} {{ end }} {{ end }} -{{ for in bar }} {{ end }} -{{ for blah bar }} {{ end }} -{{ for blah}} {{ end }} -{{ else }} \ No newline at end of file diff --git a/tests/errors/invalid_if.1-parsed.snap b/tests/errors/invalid_if.1-parsed.snap deleted file mode 100644 index 7c8ec1a..0000000 --- a/tests/errors/invalid_if.1-parsed.snap +++ /dev/null @@ -1,24 +0,0 @@ ---- -source: tests/file_tests.rs -expression: parsed -info: - input: "{{ if if }} {{ end }}" - context: {} ---- -ParsedTemplate { - tokens: [ - [LeftDelim]"{{" (0..2), - [Whitespace]" " (2..3), - [ConditionalIf]"if" (3..5), - [Whitespace]" " (5..6), - [ConditionalIf]"if" (6..8), - [Whitespace]" " (8..9), - [RightDelim]"}}" (9..11), - [Whitespace]" " (11..12), - [LeftDelim]"{{" (12..14), - [Whitespace]" " (14..15), - [End]"end" (15..18), - [Whitespace]" " (18..19), - [RightDelim]"}}" (19..21), - ], -} diff --git a/tests/errors/invalid_if.2-ast.snap b/tests/errors/invalid_if.2-ast.snap deleted file mode 100644 index b1781a4..0000000 --- a/tests/errors/invalid_if.2-ast.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: tests/file_tests.rs -expression: ast -info: - input: "{{ if if }} {{ end }}" - context: {} ---- -error: Expected an expression after 'if' -  ╭▸  -1 │ {{ if if }} {{ end }} - ╰╴ ━━ diff --git a/tests/errors/invalid_if.nomo b/tests/errors/invalid_if.nomo deleted file mode 100644 index 7a492ca..0000000 --- a/tests/errors/invalid_if.nomo +++ /dev/null @@ -1,3 +0,0 @@ -{} ---- -{{ if if }} {{ end }} \ No newline at end of file diff --git a/tests/file_tests.rs b/tests/file_tests.rs index bd3334e..b26c356 100644 --- a/tests/file_tests.rs +++ b/tests/file_tests.rs @@ -1,16 +1,11 @@ -#![allow(missing_docs)] - use std::collections::HashMap; use std::path::Path; use nomo::Context; use nomo::functions::FunctionMap; -use nomo::input::NomoInput; test_each_file::test_each_path! { for ["nomo"] in "./tests/cases/" as cases => check_for_input } -test_each_file::test_each_path! { for ["nomo"] in "./tests/errors/" as error_cases => check_errors } - #[derive(serde::Serialize)] struct Info { input: String, @@ -44,18 +39,16 @@ fn check_for_input([path]: [&Path; 1]) { context.try_insert(k, v).unwrap(); } - let input = NomoInput::from(input); - - let parsed = nomo::lexer::parse(input.clone()).unwrap(); + let parsed = nomo::lexer::parse(input.into()).unwrap(); let _guard = settings.bind_to_scope(); insta::assert_debug_snapshot!(format!("{basename}.1-parsed"), parsed); - let ast = match nomo::parser::parse(input, parsed.tokens()) { + let ast = match nomo::parser::parse(parsed.tokens()) { Ok(ast) => ast, Err(err) => { - eprintln!("{}", err); + eprintln!("{}", err.to_report(input)); panic!("Could not evaluate ast"); } }; @@ -70,69 +63,3 @@ fn check_for_input([path]: [&Path; 1]) { insta::assert_debug_snapshot!(format!("{basename}.4-output"), output); } - -fn check_errors([path]: [&Path; 1]) { - let mut settings = insta::Settings::clone_current(); - settings.set_snapshot_path("errors"); - settings.set_prepend_module_to_snapshot(false); - - let basename = path.file_stem().unwrap().to_string_lossy(); - let input = std::fs::read_to_string(path).unwrap(); - - let (context, input) = input.split_once("\n---\n").unwrap_or_else(|| ("", &input)); - - let map = if !context.is_empty() { - serde_json::from_str::>(context).unwrap() - } else { - HashMap::new() - }; - - settings.set_info(&Info { - input: input.to_string(), - context: map.clone(), - }); - - let mut context = Context::new(); - - for (k, v) in map { - context.try_insert(k, v).unwrap(); - } - - let _guard = settings.bind_to_scope(); - - let input = NomoInput::from(input); - - let parsed = nomo::lexer::parse(input.clone()).map_err(|err| err.to_report()); - - match &parsed { - Ok(parsed) => insta::assert_debug_snapshot!(format!("{basename}.1-parsed"), parsed), - Err(parsed) => insta::assert_snapshot!(format!("{basename}.1-parsed"), parsed), - } - - let Ok(parsed) = parsed else { - return; - }; - - let ast = nomo::parser::parse(input, parsed.tokens()).map_err(|err| err.to_string()); - - match &ast { - Ok(ast) => insta::assert_debug_snapshot!(format!("{basename}.2-ast"), ast), - Err(ast) => insta::assert_snapshot!(format!("{basename}.2-ast"), ast), - } - - let Ok(ast) = ast else { - return; - }; - - let emit = nomo::compiler::emit_machine(ast); - - insta::assert_debug_snapshot!(format!("{basename}.3-instructions"), emit); - - let output = nomo::eval::execute(&FunctionMap::default(), &emit, &context) - .map_err(|err| err.to_string()); - - match &output { - Ok(output) => insta::assert_debug_snapshot!(format!("{basename}.4-output"), output), - Err(output) => insta::assert_snapshot!(format!("{basename}.4-output"), output), - } -}