Compare commits

..

No commits in common. "main" and "v0.0.1" have entirely different histories.
main ... v0.0.1

25 changed files with 236 additions and 942 deletions

17
Cargo.lock generated
View file

@ -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",

View file

@ -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"]

View file

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

View file

@ -1,5 +1,3 @@
#![allow(missing_docs)]
use criterion::BenchmarkId;
use criterion::Criterion;
use criterion::criterion_group;

2
fuzz/Cargo.lock generated
View file

@ -112,7 +112,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "nomo"
version = "0.0.1"
version = "0.1.0"
dependencies = [
"annotate-snippets",
"displaydoc",

View file

@ -12,7 +12,6 @@ libfuzzer-sys = "0.4"
[dependencies.nomo]
path = ".."
features = ["unstable-pub"]
[[bin]]
name = "fuzz_target_1"

View file

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

View file

@ -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<AstError>,
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<AstError>, 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<usize>,
) -> std::ops::Range<usize> {
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<str>,
errors: Vec<ParseError>,
}
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<ParseError>, 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::<Vec<_>>();
let renderer =
Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
renderer.render(&reports)
}
}

View file

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

View file

@ -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<dyn std::error::Error + Send + Sync>,
},
}
/// A function that can be used inside a template
pub trait NomoFunction<T>: 'static + Send + Sync {
/// Call the function with the given arguments
fn call(&self, args: Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError>;
}
#[cfg(feature = "unstable-pub")]
#[derive(Default)]
#[expect(missing_docs)]
pub struct FunctionMap {
funcs: HashMap<String, ErasedNomoFunction>,
}
#[cfg(not(feature = "unstable-pub"))]
#[derive(Default)]
pub(crate) struct FunctionMap {
funcs: HashMap<String, ErasedNomoFunction>,
}
impl FunctionMap {
#[expect(missing_docs)]
pub fn register<NF: NomoFunction<T>, T>(&mut self, name: impl Into<String>, func: NF) {
self.funcs
.insert(name.into(), ErasedNomoFunction::erase(func));
}
pub(crate) fn get(&self, name: impl AsRef<str>) -> Option<&ErasedNomoFunction> {
pub fn get(&self, name: impl AsRef<str>) -> Option<&ErasedNomoFunction> {
self.funcs.get(name.as_ref())
}
}
pub(crate) struct ErasedNomoFunction {
pub struct ErasedNomoFunction {
func: Box<dyn Any + Send + Sync>,
call_fn: fn(&dyn Any, Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError>,
}

View file

@ -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<str>,
@ -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<str>, range: Range<usize>) -> NomoInput {
NomoInput { backing, range }
}
/// Turn the input into its parts
pub fn into_parts(self) -> (Arc<str>, Range<usize>) {
(self.backing, self.range)
}
/// Get the range this input covers
pub fn get_range(&self) -> Range<usize> {
self.range.clone()
}
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct NomoInputCheckpoint {
range: Range<usize>,
@ -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,

View file

@ -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<LocatingSlice<NomoInput>, ParseError>;
type PResult<'input, T> = Result<T, ParseError>;
#[derive(Debug, Error)]
pub struct ParseFailure {
input: Arc<str>,
errors: Vec<ParseError>,
}
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<ParseError>, 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::<Vec<_>>();
let renderer =
Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
renderer.render(&reports)
}
}
#[derive(Debug, Clone)]
pub struct ParseError {
pub(crate) message: Option<String>,

View file

@ -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<dyn std::error::Error>> {
//! let mut templates = nomo::Nomo::new();
//! templates.add_template("index.html", "<!DOCTYPE html><h1>Hello {{= name }}!</h1>");
//!
//! let mut context = nomo::Context::new();
//! context.insert("name", "World");
//! let result = templates.render("index.html", &context)?;
//!
//! assert_eq!(result, "<!DOCTYPE html><h1>Hello World!</h1>");
//! # 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:
//!
//! - `{{= <expr> }}` Interpolations
//! - `{{ <control> }}` 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 <expr> }}
//! {{ else if <expr> }}
//! {{ else }}
//! {{ end }}
//! ```
//!
//! **Loops `for .. in`**
//!
//! ```nomo
//! {{ for <identifier> in <expr> }}
//! {{ 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<String, Template>,
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<String>,
@ -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<A>(&mut self, name: impl Into<String>, f: impl NomoFunction<A>) {
self.function_map.register(name, f);
}
/// List of currently available templates
pub fn templates(&self) -> Vec<String> {
self.templates.keys().cloned().collect()
}
/// Render a specific template
pub fn render(&self, name: &str, ctx: &Context) -> Result<String, NomoError> {
let template = self
.templates
@ -242,7 +92,6 @@ struct Template {
instructions: VMInstructions,
}
/// The context for a given render call
pub struct Context {
values: HashMap<String, NomoValue>,
}
@ -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<String>,
@ -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<String>, value: impl Into<NomoValue>) {
self.values.insert(key.into(), value.into());
}
/// Access the values inside this context
pub fn values(&self) -> &HashMap<String, NomoValue> {
&self.values
}
}
#[derive(Debug, Clone)]
struct SourceSpan {
pub struct SourceSpan {
pub range: std::ops::Range<usize>,
}

View file

@ -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<String>,
pub(crate) help: Option<String>,
pub(crate) span: Option<crate::SourceSpan>,
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<Input<'_>, 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<Input<'_>, 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<Input<'_>> for AstError {
}
}
#[derive(Debug, Error)]
pub struct AstFailure {
errors: Vec<AstError>,
}
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<AstError>) -> 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::<Vec<_>>();
let renderer = annotate_snippets::Renderer::styled()
.decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
renderer.render(&reports)
}
}
type Input<'input> = Recoverable<TokenSlice<'input, TemplateToken>, AstError>;
impl<'input> Parser<Input<'input>, TemplateToken, AstError> for TokenKind {
@ -172,7 +210,7 @@ impl<'input> Parser<Input<'input>, TemplateToken, AstError> for TokenKind {
}
}
pub fn parse(source: NomoInput, input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, 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<TemplateAst<'
{
Ok(TemplateAst { root: val })
} else {
Err(AstFailure::from_errors(errors, source))
Err(AstFailure::from_errors(errors))
}
}
@ -223,7 +261,6 @@ pub enum TemplateAstExpr<'input> {
ElseConditional {
expression: Option<Box<TemplateAstExpr<'input>>>,
},
#[allow(unused)]
Invalid(&'input [TemplateToken]),
MathOperation {
op: TokenOperator,
@ -319,42 +356,26 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'in
alt((
parse_conditional_chain,
parse_for_chain,
parse_block(parse_unknown_action)
(parse_block(
cut_err(not(repeat_till(
0..,
any,
peek((ws, TokenKind::RightDelim)),
)
.map(|((), _)| ())))
.context(
AstError::ctx()
.msg("Standlone action block")
.help("If you want to output this expression, add a '=' to the block"),
)
.take()
.map(TemplateAstExpr::Invalid),
)),
)),
)
.parse_next(input)
}
fn parse_unknown_action<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<
let (content, taken) = resume_after_cut(
repeat_till(0.., parse_ast, loop_end),
repeat_till(0.., parse_ast, parse_end).map(|((), _)| ()),
repeat_till(0.., any, parse_end).map(|((), _)| ()),
)
.with_taken()
.parse_next(input)?;
@ -394,34 +415,24 @@ fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<
fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, 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 {
.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<TemplateAstExpr<'input>, 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<TemplateToken, AstError> {
alt((
TokenKind::ConditionalIf,
TokenKind::ConditionalElse,
TokenKind::For,
TokenKind::End,
TokenKind::In,
))
.parse_next(input)
}
fn parse_keywords_fail<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAst<'a>, AstFailure>) -> TemplateAst<'a> {
fn panic_pretty<'a>(
input: &'_ str,
tokens: Result<TemplateAst<'a>, 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);
}

View file

@ -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

View file

@ -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<bool> {
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<String, NomoValue>> {
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<u64> {
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<f64> {
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<Item = NomoValue>> {
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<Item = NomoValue>> {
if let Self::Iterator { value } = self {
Some(value)
@ -246,21 +237,19 @@ impl NomoValue {
pub(crate) fn try_to_string(&self) -> Option<String> {
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<Item = NomoValue> {
/// Create a new instance of the iterator inside a [Box]
fn clone_box(&self) -> Box<dyn CloneIterator<Item = NomoValue>>;
}

View file

@ -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<I, O, E>: Parser<I, O, E> {
fn with_context<F, C>(self, context: F) -> WithContext<Self, I, O, E, F, C>
where
Self: Sized,
I: Stream,
E: AddContext<I, C> + ParserError<I>,
F: Fn(&<I as Stream>::Slice, &E) -> C,
{
WithContext {
parser: self,
context,
_pd: PhantomData,
}
}
}
pub struct WithContext<P, I, O, E, F, C> {
parser: P,
context: F,
_pd: PhantomData<(I, O, E, C)>,
}
impl<P, I, O, E, F, C> Parser<I, O, E> for WithContext<P, I, O, E, F, C>
where
P: Parser<I, O, E>,
I: Stream,
E: AddContext<I, C> + ParserError<I>,
F: Fn(&<I as Stream>::Slice, &E) -> C,
{
fn parse_next(&mut self, input: &mut I) -> winnow::Result<O, E> {
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)
})
}
}

View file

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

View file

@ -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),
],
}

View file

@ -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 }}
╰╴ ━━━━

View file

@ -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 }}

View file

@ -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),
],
}

View file

@ -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 }}
╰╴ ━━

View file

@ -1,3 +0,0 @@
{}
---
{{ if if }} {{ end }}

View file

@ -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::<HashMap<String, serde_json::Value>>(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),
}
}