Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42698bb219 | |||
| 7f7bf5c98d | |||
| d6ac7af36b | |||
| 79a037b749 | |||
| 560d37f633 | |||
| 058e6be516 | |||
| 4c8938e4ff |
25 changed files with 942 additions and 236 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
|
@ -240,6 +240,15 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
|
|
@ -420,6 +429,12 @@ version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
|
|
@ -439,7 +454,9 @@ dependencies = [
|
||||||
"annotate-snippets",
|
"annotate-snippets",
|
||||||
"criterion",
|
"criterion",
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
|
"document-features",
|
||||||
"insta",
|
"insta",
|
||||||
|
"nomo",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"test_each_file",
|
"test_each_file",
|
||||||
|
|
|
||||||
13
Cargo.toml
13
Cargo.toml
|
|
@ -16,9 +16,14 @@ harness = false
|
||||||
[profile.bench]
|
[profile.bench]
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
missing_docs = "warn"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
annotate-snippets = "0.12.13"
|
annotate-snippets = "0.12.13"
|
||||||
displaydoc = "0.2.5"
|
displaydoc = "0.2.5"
|
||||||
|
document-features = { version = "0.2.12", optional = true }
|
||||||
serde_json = { version = "1.0.149", optional = true }
|
serde_json = { version = "1.0.149", optional = true }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
winnow = { version = "0.7.14", features = ["unstable-recover"] }
|
winnow = { version = "0.7.14", features = ["unstable-recover"] }
|
||||||
|
|
@ -30,11 +35,19 @@ insta = { version = "1.46.3", features = ["glob", "serde"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
test_each_file = "0.3.7"
|
test_each_file = "0.3.7"
|
||||||
|
nomo = { path = ".", features = ["unstable-pub"] }
|
||||||
|
|
||||||
[profile.dev.package]
|
[profile.dev.package]
|
||||||
insta.opt-level = 3
|
insta.opt-level = 3
|
||||||
similar.opt-level = 3
|
similar.opt-level = 3
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
features = ["document-features"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["serde_json"]
|
default = ["serde_json"]
|
||||||
|
## Add support for inserting [`serde_json::Value`]s into [`Context`] objects
|
||||||
serde_json = ["dep:serde_json"]
|
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"]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
use criterion::BenchmarkId;
|
use criterion::BenchmarkId;
|
||||||
use criterion::Criterion;
|
use criterion::Criterion;
|
||||||
use criterion::criterion_group;
|
use criterion::criterion_group;
|
||||||
|
|
@ -20,7 +22,7 @@ fn asting_benchmark(c: &mut Criterion) {
|
||||||
parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| {
|
parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let tokens = nomo::lexer::parse(input.clone()).unwrap();
|
let tokens = nomo::lexer::parse(input.clone()).unwrap();
|
||||||
let _ast = nomo::parser::parse(tokens.tokens()).unwrap();
|
let _ast = nomo::parser::parse(input.clone(), tokens.tokens()).unwrap();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +46,7 @@ fn asting_nested(c: &mut Criterion) {
|
||||||
parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| {
|
parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let tokens = nomo::lexer::parse(input.clone()).unwrap();
|
let tokens = nomo::lexer::parse(input.clone()).unwrap();
|
||||||
let _ast = nomo::parser::parse(tokens.tokens()).unwrap();
|
let _ast = nomo::parser::parse(input.clone(), tokens.tokens()).unwrap();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
use criterion::BenchmarkId;
|
use criterion::BenchmarkId;
|
||||||
use criterion::Criterion;
|
use criterion::Criterion;
|
||||||
use criterion::criterion_group;
|
use criterion::criterion_group;
|
||||||
|
|
|
||||||
2
fuzz/Cargo.lock
generated
2
fuzz/Cargo.lock
generated
|
|
@ -112,7 +112,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nomo"
|
name = "nomo"
|
||||||
version = "0.1.0"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"annotate-snippets",
|
"annotate-snippets",
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ libfuzzer-sys = "0.4"
|
||||||
|
|
||||||
[dependencies.nomo]
|
[dependencies.nomo]
|
||||||
path = ".."
|
path = ".."
|
||||||
|
features = ["unstable-pub"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "fuzz_target_1"
|
name = "fuzz_target_1"
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ pub enum Instruction {
|
||||||
slot: VariableSlot,
|
slot: VariableSlot,
|
||||||
},
|
},
|
||||||
PushScope {
|
PushScope {
|
||||||
|
#[allow(unused)]
|
||||||
inherit_parent: bool,
|
inherit_parent: bool,
|
||||||
},
|
},
|
||||||
Abort,
|
Abort,
|
||||||
|
|
@ -93,6 +94,7 @@ pub enum Instruction {
|
||||||
value_slot: VariableSlot,
|
value_slot: VariableSlot,
|
||||||
},
|
},
|
||||||
LoadLiteralToSlot {
|
LoadLiteralToSlot {
|
||||||
|
#[allow(unused)]
|
||||||
source: TemplateToken,
|
source: TemplateToken,
|
||||||
value: NomoValue,
|
value: NomoValue,
|
||||||
slot: VariableSlot,
|
slot: VariableSlot,
|
||||||
|
|
@ -577,14 +579,15 @@ fn emit_expr_load(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::compiler::emit_machine;
|
use crate::compiler::emit_machine;
|
||||||
|
use crate::input::NomoInput;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_simple_variable_interpolation() {
|
fn check_simple_variable_interpolation() {
|
||||||
let input = "Hello {{= world }}";
|
let input = NomoInput::from("Hello {{= world }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = crate::parser::parse(parsed.tokens()).unwrap();
|
let ast = crate::parser::parse(input, parsed.tokens()).unwrap();
|
||||||
|
|
||||||
let emit = emit_machine(ast);
|
let emit = emit_machine(ast);
|
||||||
|
|
||||||
|
|
@ -617,11 +620,12 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_if_else_if() {
|
fn check_if_else_if() {
|
||||||
let input = "{{ if foo }} foo {{ else if bar }} bar {{ else }} foobar {{ end }}";
|
let input =
|
||||||
|
NomoInput::from("{{ if foo }} foo {{ else if bar }} bar {{ else }} foobar {{ end }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = crate::parser::parse(parsed.tokens()).unwrap();
|
let ast = crate::parser::parse(input, parsed.tokens()).unwrap();
|
||||||
|
|
||||||
let emit = emit_machine(ast);
|
let emit = emit_machine(ast);
|
||||||
|
|
||||||
|
|
@ -630,11 +634,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_function_call() {
|
fn check_function_call() {
|
||||||
let input = "{{ if foo(23) }} bar {{ else }} foobar {{ end }}";
|
let input = NomoInput::from("{{ if foo(23) }} bar {{ else }} foobar {{ end }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = crate::parser::parse(parsed.tokens()).unwrap();
|
let ast = crate::parser::parse(input, parsed.tokens()).unwrap();
|
||||||
|
|
||||||
let emit = emit_machine(ast);
|
let emit = emit_machine(ast);
|
||||||
|
|
||||||
|
|
|
||||||
153
src/errors.rs
Normal file
153
src/errors.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -297,14 +297,15 @@ mod tests {
|
||||||
use crate::eval::execute;
|
use crate::eval::execute;
|
||||||
use crate::functions::FunctionMap;
|
use crate::functions::FunctionMap;
|
||||||
use crate::functions::NomoFunctionError;
|
use crate::functions::NomoFunctionError;
|
||||||
|
use crate::input::NomoInput;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_simple_variable_interpolation() {
|
fn check_simple_variable_interpolation() {
|
||||||
let input = "Hello {{= world }}";
|
let input = NomoInput::from("Hello {{= world }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = crate::parser::parse(parsed.tokens()).unwrap();
|
let ast = crate::parser::parse(input, parsed.tokens()).unwrap();
|
||||||
|
|
||||||
let emit = crate::compiler::emit_machine(ast);
|
let emit = crate::compiler::emit_machine(ast);
|
||||||
|
|
||||||
|
|
@ -321,11 +322,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_method_call() {
|
fn check_method_call() {
|
||||||
let input = "Hello {{= foo(world) }}";
|
let input = NomoInput::from("Hello {{= foo(world) }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = crate::parser::parse(parsed.tokens()).unwrap();
|
let ast = crate::parser::parse(input, parsed.tokens()).unwrap();
|
||||||
|
|
||||||
let emit = crate::compiler::emit_machine(ast);
|
let emit = crate::compiler::emit_machine(ast);
|
||||||
|
|
||||||
|
|
@ -348,11 +349,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_conditional_access() {
|
fn check_conditional_access() {
|
||||||
let input = "Hello {{= unknown? }}";
|
let input = NomoInput::from("Hello {{= unknown? }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = crate::parser::parse(parsed.tokens()).unwrap();
|
let ast = crate::parser::parse(input, parsed.tokens()).unwrap();
|
||||||
|
|
||||||
let emit = crate::compiler::emit_machine(ast);
|
let emit = crate::compiler::emit_machine(ast);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,42 +7,57 @@ use thiserror::Error;
|
||||||
use crate::NomoValueError;
|
use crate::NomoValueError;
|
||||||
use crate::value::NomoValue;
|
use crate::value::NomoValue;
|
||||||
|
|
||||||
|
/// Possible errors while executing a function
|
||||||
#[derive(Debug, Error, Display)]
|
#[derive(Debug, Error, Display)]
|
||||||
pub enum NomoFunctionError {
|
pub enum NomoFunctionError {
|
||||||
/// Received {received} arguments, but this function only takes {expected}
|
/// Received {received} arguments, but this function only takes {expected}
|
||||||
|
#[expect(missing_docs)]
|
||||||
WrongArgumentCount { received: usize, expected: usize },
|
WrongArgumentCount { received: usize, expected: usize },
|
||||||
|
|
||||||
/// The argument at this position is of the wrong type
|
/// The argument at this position is of the wrong type
|
||||||
|
#[expect(missing_docs)]
|
||||||
InvalidArgumentType { index: usize },
|
InvalidArgumentType { index: usize },
|
||||||
|
|
||||||
/// A user-provided error
|
/// A user-provided error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
CustomError {
|
CustomError {
|
||||||
|
#[expect(missing_docs)]
|
||||||
custom: Box<dyn std::error::Error + Send + Sync>,
|
custom: Box<dyn std::error::Error + Send + Sync>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A function that can be used inside a template
|
||||||
pub trait NomoFunction<T>: 'static + Send + Sync {
|
pub trait NomoFunction<T>: 'static + Send + Sync {
|
||||||
|
/// Call the function with the given arguments
|
||||||
fn call(&self, args: Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError>;
|
fn call(&self, args: Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "unstable-pub")]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
#[expect(missing_docs)]
|
||||||
pub struct FunctionMap {
|
pub struct FunctionMap {
|
||||||
funcs: HashMap<String, ErasedNomoFunction>,
|
funcs: HashMap<String, ErasedNomoFunction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "unstable-pub"))]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct FunctionMap {
|
||||||
|
funcs: HashMap<String, ErasedNomoFunction>,
|
||||||
|
}
|
||||||
|
|
||||||
impl FunctionMap {
|
impl FunctionMap {
|
||||||
|
#[expect(missing_docs)]
|
||||||
pub fn register<NF: NomoFunction<T>, T>(&mut self, name: impl Into<String>, func: NF) {
|
pub fn register<NF: NomoFunction<T>, T>(&mut self, name: impl Into<String>, func: NF) {
|
||||||
self.funcs
|
self.funcs
|
||||||
.insert(name.into(), ErasedNomoFunction::erase(func));
|
.insert(name.into(), ErasedNomoFunction::erase(func));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, name: impl AsRef<str>) -> Option<&ErasedNomoFunction> {
|
pub(crate) fn get(&self, name: impl AsRef<str>) -> Option<&ErasedNomoFunction> {
|
||||||
self.funcs.get(name.as_ref())
|
self.funcs.get(name.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ErasedNomoFunction {
|
pub(crate) struct ErasedNomoFunction {
|
||||||
func: Box<dyn Any + Send + Sync>,
|
func: Box<dyn Any + Send + Sync>,
|
||||||
call_fn: fn(&dyn Any, Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError>,
|
call_fn: fn(&dyn Any, Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
src/input.rs
11
src/input.rs
|
|
@ -8,6 +8,7 @@ use winnow::stream::Offset;
|
||||||
use winnow::stream::Stream;
|
use winnow::stream::Stream;
|
||||||
use winnow::stream::StreamIsPartial;
|
use winnow::stream::StreamIsPartial;
|
||||||
|
|
||||||
|
/// The input for templates in [nomo](crate)
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
pub struct NomoInput {
|
pub struct NomoInput {
|
||||||
backing: Arc<str>,
|
backing: Arc<str>,
|
||||||
|
|
@ -15,19 +16,26 @@ pub struct NomoInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn from_parts(backing: Arc<str>, range: Range<usize>) -> NomoInput {
|
||||||
NomoInput { backing, range }
|
NomoInput { backing, range }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn the input into its parts
|
||||||
pub fn into_parts(self) -> (Arc<str>, Range<usize>) {
|
pub fn into_parts(self) -> (Arc<str>, Range<usize>) {
|
||||||
(self.backing, self.range)
|
(self.backing, self.range)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the range this input covers
|
||||||
pub fn get_range(&self) -> Range<usize> {
|
pub fn get_range(&self) -> Range<usize> {
|
||||||
self.range.clone()
|
self.range.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct NomoInputCheckpoint {
|
pub struct NomoInputCheckpoint {
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
|
|
@ -78,6 +86,7 @@ impl Deref for NomoInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NomoInput {
|
impl NomoInput {
|
||||||
|
/// Get the input as a [`str`]
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
self.deref()
|
self.deref()
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +110,8 @@ impl Offset for NomoInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
pub struct NomoInputIter {
|
pub struct NomoInputIter {
|
||||||
idx: usize,
|
idx: usize,
|
||||||
input: NomoInput,
|
input: NomoInput,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,3 @@
|
||||||
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::LocatingSlice;
|
||||||
use winnow::Parser;
|
use winnow::Parser;
|
||||||
use winnow::RecoverableParser;
|
use winnow::RecoverableParser;
|
||||||
|
|
@ -39,60 +32,13 @@ use winnow::token::take_until;
|
||||||
use winnow::token::take_while;
|
use winnow::token::take_while;
|
||||||
|
|
||||||
use crate::SourceSpan;
|
use crate::SourceSpan;
|
||||||
|
use crate::errors::ParseFailure;
|
||||||
use crate::input::NomoInput;
|
use crate::input::NomoInput;
|
||||||
use crate::resume_after_cut;
|
use crate::resume_after_cut;
|
||||||
|
|
||||||
type Input<'input> = Recoverable<LocatingSlice<NomoInput>, ParseError>;
|
type Input<'input> = Recoverable<LocatingSlice<NomoInput>, ParseError>;
|
||||||
type PResult<'input, T> = Result<T, 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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ParseError {
|
pub struct ParseError {
|
||||||
pub(crate) message: Option<String>,
|
pub(crate) message: Option<String>,
|
||||||
|
|
|
||||||
173
src/lib.rs
173
src/lib.rs
|
|
@ -1,3 +1,102 @@
|
||||||
|
//! # 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 std::collections::HashMap;
|
||||||
|
|
||||||
use displaydoc::Display;
|
use displaydoc::Display;
|
||||||
|
|
@ -5,34 +104,71 @@ use thiserror::Error;
|
||||||
|
|
||||||
use crate::compiler::VMInstructions;
|
use crate::compiler::VMInstructions;
|
||||||
use crate::functions::FunctionMap;
|
use crate::functions::FunctionMap;
|
||||||
|
use crate::functions::NomoFunction;
|
||||||
use crate::input::NomoInput;
|
use crate::input::NomoInput;
|
||||||
use crate::value::NomoValue;
|
use crate::value::NomoValue;
|
||||||
use crate::value::NomoValueError;
|
use crate::value::NomoValueError;
|
||||||
|
|
||||||
pub mod compiler;
|
macro_rules! unstable_pub {
|
||||||
pub mod eval;
|
($(#[$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 functions;
|
pub mod functions;
|
||||||
|
/// Input for nomo
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod lexer;
|
/// Values used in Nomo
|
||||||
pub mod parser;
|
|
||||||
pub mod value;
|
pub mod value;
|
||||||
|
|
||||||
|
/// Errors related to parsing and evaluating templates
|
||||||
#[derive(Debug, Error, Display)]
|
#[derive(Debug, Error, Display)]
|
||||||
pub enum NomoError {
|
pub enum NomoError {
|
||||||
/// Could not parse the given template
|
/// Could not parse the given template
|
||||||
ParseError {
|
ParseError {
|
||||||
#[from]
|
#[from]
|
||||||
source: lexer::ParseFailure,
|
#[expect(missing_docs)]
|
||||||
|
source: errors::ParseFailure,
|
||||||
},
|
},
|
||||||
/// Invalid Template
|
/// Invalid Template
|
||||||
AstError {
|
AstError {
|
||||||
#[from]
|
#[from]
|
||||||
source: parser::AstFailure,
|
#[expect(missing_docs)]
|
||||||
|
source: errors::AstFailure,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// An error occurred while evaluating
|
/// An error occurred while evaluating
|
||||||
EvaluationError {
|
EvaluationError {
|
||||||
#[from]
|
#[from]
|
||||||
|
#[expect(missing_docs)]
|
||||||
source: eval::EvaluationError,
|
source: eval::EvaluationError,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -40,6 +176,7 @@ pub enum NomoError {
|
||||||
UnknownTemplate(String),
|
UnknownTemplate(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The main struct and entry point for the [`nomo`](crate)
|
||||||
pub struct Nomo {
|
pub struct Nomo {
|
||||||
templates: HashMap<String, Template>,
|
templates: HashMap<String, Template>,
|
||||||
function_map: FunctionMap,
|
function_map: FunctionMap,
|
||||||
|
|
@ -52,6 +189,7 @@ impl Default for Nomo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Nomo {
|
impl Nomo {
|
||||||
|
/// Create a new Nomo Instance
|
||||||
pub fn new() -> Nomo {
|
pub fn new() -> Nomo {
|
||||||
Nomo {
|
Nomo {
|
||||||
templates: HashMap::new(),
|
templates: HashMap::new(),
|
||||||
|
|
@ -59,6 +197,7 @@ impl Nomo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a new template
|
||||||
pub fn add_template(
|
pub fn add_template(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: impl Into<String>,
|
name: impl Into<String>,
|
||||||
|
|
@ -66,7 +205,7 @@ impl Nomo {
|
||||||
) -> Result<(), NomoError> {
|
) -> Result<(), NomoError> {
|
||||||
let source = value.into();
|
let source = value.into();
|
||||||
let parse = lexer::parse(source.clone())?;
|
let parse = lexer::parse(source.clone())?;
|
||||||
let ast = parser::parse(parse.tokens())?;
|
let ast = parser::parse(source.clone(), parse.tokens())?;
|
||||||
|
|
||||||
let instructions = compiler::emit_machine(ast);
|
let instructions = compiler::emit_machine(ast);
|
||||||
|
|
||||||
|
|
@ -76,6 +215,17 @@ impl Nomo {
|
||||||
Ok(())
|
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> {
|
pub fn render(&self, name: &str, ctx: &Context) -> Result<String, NomoError> {
|
||||||
let template = self
|
let template = self
|
||||||
.templates
|
.templates
|
||||||
|
|
@ -92,6 +242,7 @@ struct Template {
|
||||||
instructions: VMInstructions,
|
instructions: VMInstructions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The context for a given render call
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
values: HashMap<String, NomoValue>,
|
values: HashMap<String, NomoValue>,
|
||||||
}
|
}
|
||||||
|
|
@ -103,12 +254,16 @@ impl Default for Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
|
/// Create new Context
|
||||||
pub fn new() -> Context {
|
pub fn new() -> Context {
|
||||||
Context {
|
Context {
|
||||||
values: HashMap::new(),
|
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(
|
pub fn try_insert(
|
||||||
&mut self,
|
&mut self,
|
||||||
key: impl Into<String>,
|
key: impl Into<String>,
|
||||||
|
|
@ -119,17 +274,19 @@ impl Context {
|
||||||
Ok(())
|
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>) {
|
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<NomoValue>) {
|
||||||
self.values.insert(key.into(), value.into());
|
self.values.insert(key.into(), value.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the values inside this context
|
||||||
pub fn values(&self) -> &HashMap<String, NomoValue> {
|
pub fn values(&self) -> &HashMap<String, NomoValue> {
|
||||||
&self.values
|
&self.values
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SourceSpan {
|
struct SourceSpan {
|
||||||
pub range: std::ops::Range<usize>,
|
pub range: std::ops::Range<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use thiserror::Error;
|
|
||||||
use winnow::Parser;
|
use winnow::Parser;
|
||||||
use winnow::RecoverableParser;
|
use winnow::RecoverableParser;
|
||||||
use winnow::combinator::Infix::Left;
|
use winnow::combinator::Infix::Left;
|
||||||
|
|
@ -21,6 +20,7 @@ use winnow::error::AddContext;
|
||||||
use winnow::error::FromRecoverableError;
|
use winnow::error::FromRecoverableError;
|
||||||
use winnow::error::ModalError;
|
use winnow::error::ModalError;
|
||||||
use winnow::error::ParserError;
|
use winnow::error::ParserError;
|
||||||
|
use winnow::stream::Location;
|
||||||
use winnow::stream::Offset;
|
use winnow::stream::Offset;
|
||||||
use winnow::stream::Recoverable;
|
use winnow::stream::Recoverable;
|
||||||
use winnow::stream::Stream;
|
use winnow::stream::Stream;
|
||||||
|
|
@ -28,6 +28,8 @@ use winnow::stream::TokenSlice;
|
||||||
use winnow::token::any;
|
use winnow::token::any;
|
||||||
|
|
||||||
use crate::SourceSpan;
|
use crate::SourceSpan;
|
||||||
|
use crate::errors::AstFailure;
|
||||||
|
use crate::input::NomoInput;
|
||||||
use crate::lexer::TemplateToken;
|
use crate::lexer::TemplateToken;
|
||||||
use crate::lexer::TokenKind;
|
use crate::lexer::TokenKind;
|
||||||
use crate::lexer::TokenOperator;
|
use crate::lexer::TokenOperator;
|
||||||
|
|
@ -50,6 +52,7 @@ pub struct AstError {
|
||||||
pub(crate) message: Option<String>,
|
pub(crate) message: Option<String>,
|
||||||
pub(crate) help: Option<String>,
|
pub(crate) help: Option<String>,
|
||||||
pub(crate) span: Option<crate::SourceSpan>,
|
pub(crate) span: Option<crate::SourceSpan>,
|
||||||
|
pub(crate) replacement: Option<(SourceSpan, String)>,
|
||||||
|
|
||||||
is_fatal: bool,
|
is_fatal: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +63,7 @@ impl AstError {
|
||||||
message: None,
|
message: None,
|
||||||
help: None,
|
help: None,
|
||||||
span: None,
|
span: None,
|
||||||
|
replacement: None,
|
||||||
|
|
||||||
is_fatal: false,
|
is_fatal: false,
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +78,11 @@ impl AstError {
|
||||||
self.help = Some(help.to_string());
|
self.help = Some(help.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replacement(mut self, span: SourceSpan, replacement: &str) -> Self {
|
||||||
|
self.replacement = Some((span, replacement.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalError for AstError {
|
impl ModalError for AstError {
|
||||||
|
|
@ -104,13 +113,14 @@ impl FromRecoverableError<Input<'_>, AstError> for AstError {
|
||||||
.filter(|t| t.kind() != TokenKind::Whitespace);
|
.filter(|t| t.kind() != TokenKind::Whitespace);
|
||||||
let last = tokens.next();
|
let last = tokens.next();
|
||||||
let first = tokens.last();
|
let first = tokens.last();
|
||||||
|
|
||||||
match (last, first) {
|
match (last, first) {
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
(None, Some(single)) | (Some(single), None) => Some(SourceSpan {
|
(None, Some(single)) | (Some(single), None) => Some(SourceSpan {
|
||||||
range: single.source().get_range(),
|
range: single.source().get_range(),
|
||||||
}),
|
}),
|
||||||
(Some(last), Some(first)) => {
|
(Some(last), Some(first)) => {
|
||||||
let start = first.source().get_range().start;
|
let start = first.source().get_range().end;
|
||||||
let end = last.source().get_range().end;
|
let end = last.source().get_range().end;
|
||||||
|
|
||||||
Some(SourceSpan { range: start..end })
|
Some(SourceSpan { range: start..end })
|
||||||
|
|
@ -131,6 +141,7 @@ impl AddContext<Input<'_>, AstError> for AstError {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.message = context.message.or(self.message);
|
self.message = context.message.or(self.message);
|
||||||
self.help = context.help.or(self.help);
|
self.help = context.help.or(self.help);
|
||||||
|
self.replacement = context.replacement.or(self.replacement);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,55 +162,6 @@ 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>;
|
type Input<'input> = Recoverable<TokenSlice<'input, TemplateToken>, AstError>;
|
||||||
|
|
||||||
impl<'input> Parser<Input<'input>, TemplateToken, AstError> for TokenKind {
|
impl<'input> Parser<Input<'input>, TemplateToken, AstError> for TokenKind {
|
||||||
|
|
@ -210,7 +172,7 @@ impl<'input> Parser<Input<'input>, TemplateToken, AstError> for TokenKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
|
pub fn parse(source: NomoInput, input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
|
||||||
let (_remaining, val, errors) = parse_asts.recoverable_parse(TokenSlice::new(input));
|
let (_remaining, val, errors) = parse_asts.recoverable_parse(TokenSlice::new(input));
|
||||||
|
|
||||||
if errors.is_empty()
|
if errors.is_empty()
|
||||||
|
|
@ -218,7 +180,7 @@ pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
|
||||||
{
|
{
|
||||||
Ok(TemplateAst { root: val })
|
Ok(TemplateAst { root: val })
|
||||||
} else {
|
} else {
|
||||||
Err(AstFailure::from_errors(errors))
|
Err(AstFailure::from_errors(errors, source))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,6 +223,7 @@ pub enum TemplateAstExpr<'input> {
|
||||||
ElseConditional {
|
ElseConditional {
|
||||||
expression: Option<Box<TemplateAstExpr<'input>>>,
|
expression: Option<Box<TemplateAstExpr<'input>>>,
|
||||||
},
|
},
|
||||||
|
#[allow(unused)]
|
||||||
Invalid(&'input [TemplateToken]),
|
Invalid(&'input [TemplateToken]),
|
||||||
MathOperation {
|
MathOperation {
|
||||||
op: TokenOperator,
|
op: TokenOperator,
|
||||||
|
|
@ -356,26 +319,42 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'in
|
||||||
alt((
|
alt((
|
||||||
parse_conditional_chain,
|
parse_conditional_chain,
|
||||||
parse_for_chain,
|
parse_for_chain,
|
||||||
(parse_block(
|
parse_block(parse_unknown_action)
|
||||||
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()
|
.take()
|
||||||
.map(TemplateAstExpr::Invalid),
|
.map(TemplateAstExpr::Invalid),
|
||||||
)),
|
)),
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.parse_next(input)
|
.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> {
|
fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
|
||||||
trace("for_loop", |input: &mut Input<'input>| {
|
trace("for_loop", |input: &mut Input<'input>| {
|
||||||
let for_block = parse_for_loop.map(Box::new).parse_next(input)?;
|
let for_block = parse_for_loop.map(Box::new).parse_next(input)?;
|
||||||
|
|
@ -390,7 +369,7 @@ fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<
|
||||||
|
|
||||||
let (content, taken) = resume_after_cut(
|
let (content, taken) = resume_after_cut(
|
||||||
repeat_till(0.., parse_ast, loop_end),
|
repeat_till(0.., parse_ast, loop_end),
|
||||||
repeat_till(0.., any, parse_end).map(|((), _)| ()),
|
repeat_till(0.., parse_ast, parse_end).map(|((), _)| ()),
|
||||||
)
|
)
|
||||||
.with_taken()
|
.with_taken()
|
||||||
.parse_next(input)?;
|
.parse_next(input)?;
|
||||||
|
|
@ -415,24 +394,34 @@ fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<
|
||||||
fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
|
fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
|
||||||
trace(
|
trace(
|
||||||
"for_loop_inner",
|
"for_loop_inner",
|
||||||
parse_block(
|
parse_block(|input: &mut Input<'input>| {
|
||||||
(
|
let _ignored = surrounded(ws, TokenKind::For).parse_next(input)?;
|
||||||
ws,
|
|
||||||
TokenKind::For,
|
let value_ident =
|
||||||
ws,
|
cut_err(TokenKind::Ident.context(AstError::ctx().msg("Missing ident here")))
|
||||||
TokenKind::Ident,
|
.parse_next(input)?;
|
||||||
ws,
|
|
||||||
TokenKind::In,
|
let _ignored = cut_err(
|
||||||
ws,
|
preceded(ws, TokenKind::In).context(
|
||||||
parse_expression.map(Box::new),
|
AstError::ctx()
|
||||||
|
.msg("Missing `in` in `for .. in ` loop")
|
||||||
|
.replacement(
|
||||||
|
SourceSpan {
|
||||||
|
range: input.current_token_start()..(input.current_token_start()),
|
||||||
|
},
|
||||||
|
" in",
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.map(|(_, _for, _, value_ident, _, _in, _, value_expression)| {
|
.parse_next(input)?;
|
||||||
TemplateAstExpr::For {
|
|
||||||
|
let value_expression = parse_expression.map(Box::new).parse_next(input)?;
|
||||||
|
|
||||||
|
Ok(TemplateAstExpr::For {
|
||||||
value_ident,
|
value_ident,
|
||||||
value_expression,
|
value_expression,
|
||||||
}
|
})
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.parse_next(input)
|
.parse_next(input)
|
||||||
}
|
}
|
||||||
|
|
@ -649,11 +638,37 @@ where
|
||||||
fn parse_operand<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
|
fn parse_operand<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
|
||||||
trace(
|
trace(
|
||||||
"operand",
|
"operand",
|
||||||
alt((parse_function, parse_variable_access, parse_literal)),
|
alt((
|
||||||
|
parse_keywords_fail,
|
||||||
|
parse_function,
|
||||||
|
parse_variable_access,
|
||||||
|
parse_literal,
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.parse_next(input)
|
.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> {
|
fn parse_literal<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
|
||||||
trace(
|
trace(
|
||||||
"literal",
|
"literal",
|
||||||
|
|
@ -757,6 +772,7 @@ mod tests {
|
||||||
use winnow::combinator::fail;
|
use winnow::combinator::fail;
|
||||||
use winnow::stream::TokenSlice;
|
use winnow::stream::TokenSlice;
|
||||||
|
|
||||||
|
use crate::input::NomoInput;
|
||||||
use crate::lexer::TokenKind;
|
use crate::lexer::TokenKind;
|
||||||
use crate::parser::AstError;
|
use crate::parser::AstError;
|
||||||
use crate::parser::AstFailure;
|
use crate::parser::AstFailure;
|
||||||
|
|
@ -766,25 +782,22 @@ mod tests {
|
||||||
use crate::parser::parse_block;
|
use crate::parser::parse_block;
|
||||||
use crate::parser::parse_end;
|
use crate::parser::parse_end;
|
||||||
|
|
||||||
fn panic_pretty<'a>(
|
fn panic_pretty<'a>(tokens: Result<TemplateAst<'a>, AstFailure>) -> TemplateAst<'a> {
|
||||||
input: &'_ str,
|
|
||||||
tokens: Result<TemplateAst<'a>, AstFailure>,
|
|
||||||
) -> TemplateAst<'a> {
|
|
||||||
match tokens {
|
match tokens {
|
||||||
Ok(ast) => ast,
|
Ok(ast) => ast,
|
||||||
Err(failure) => {
|
Err(failure) => {
|
||||||
panic!("{}", failure.to_report(input));
|
panic!("{}", failure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_only_content() {
|
fn check_only_content() {
|
||||||
let input = "Hello World";
|
let input = NomoInput::from("Hello World");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = parse(parsed.tokens()).unwrap();
|
let ast = parse(input, parsed.tokens()).unwrap();
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast, @r#"
|
insta::assert_debug_snapshot!(ast, @r#"
|
||||||
TemplateAst {
|
TemplateAst {
|
||||||
|
|
@ -799,11 +812,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_simple_variable_interpolation() {
|
fn check_simple_variable_interpolation() {
|
||||||
let input = "Hello {{= world }}";
|
let input = NomoInput::from("Hello {{= world }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = parse(parsed.tokens()).unwrap();
|
let ast = parse(input, parsed.tokens()).unwrap();
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast, @r#"
|
insta::assert_debug_snapshot!(ast, @r#"
|
||||||
TemplateAst {
|
TemplateAst {
|
||||||
|
|
@ -827,11 +840,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_simple_if() {
|
fn check_simple_if() {
|
||||||
let input = "{{ if foo }} Hiii {{ end }}";
|
let input = NomoInput::from("{{ if foo }} Hiii {{ end }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast, @r#"
|
insta::assert_debug_snapshot!(ast, @r#"
|
||||||
TemplateAst {
|
TemplateAst {
|
||||||
|
|
@ -872,35 +885,39 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_invalid_action() {
|
fn check_invalid_action() {
|
||||||
let input = r#"{{ value }}
|
let input = NomoInput::from(
|
||||||
|
r#"{{ value }}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
{{ value }}"#;
|
{{ value }}"#,
|
||||||
|
);
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = parse(parsed.tokens()).unwrap_err();
|
let ast = parse(input, parsed.tokens()).unwrap_err();
|
||||||
|
|
||||||
insta::assert_snapshot!(ast.to_report(input));
|
insta::assert_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_nested_simple_if() {
|
fn check_nested_simple_if() {
|
||||||
let input = r#"{{ if foo }}
|
let input = NomoInput::from(
|
||||||
|
r#"{{ if foo }}
|
||||||
{{ if bar }}
|
{{ if bar }}
|
||||||
Hiii
|
Hiii
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{= value }}
|
{{= value }}
|
||||||
"#;
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
insta::assert_debug_snapshot!("simple_if_tokens", parsed);
|
insta::assert_debug_snapshot!("simple_if_tokens", parsed);
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!("simple_if_ast", ast);
|
insta::assert_debug_snapshot!("simple_if_ast", ast);
|
||||||
}
|
}
|
||||||
|
|
@ -909,9 +926,9 @@ mod tests {
|
||||||
fn check_parsing_block() {
|
fn check_parsing_block() {
|
||||||
use winnow::RecoverableParser;
|
use winnow::RecoverableParser;
|
||||||
|
|
||||||
let input = "{{ foo }}";
|
let input = NomoInput::from("{{ foo }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input).unwrap();
|
||||||
|
|
||||||
let result = alt((
|
let result = alt((
|
||||||
parse_end,
|
parse_end,
|
||||||
|
|
@ -943,9 +960,10 @@ mod tests {
|
||||||
help: None,
|
help: None,
|
||||||
span: Some(
|
span: Some(
|
||||||
SourceSpan {
|
SourceSpan {
|
||||||
range: 0..6,
|
range: 2..6,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
replacement: None,
|
||||||
is_fatal: false,
|
is_fatal: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -955,11 +973,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_empty_if_output() {
|
fn check_empty_if_output() {
|
||||||
let input = "{{ if foo }}{{ end }}";
|
let input = NomoInput::from("{{ if foo }}{{ end }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast, @r#"
|
insta::assert_debug_snapshot!(ast, @r#"
|
||||||
TemplateAst {
|
TemplateAst {
|
||||||
|
|
@ -992,99 +1010,101 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_if_else() {
|
fn check_if_else() {
|
||||||
let input = "{{ if foo }} foo {{ else }} bar {{ end }}";
|
let input = NomoInput::from("{{ if foo }} foo {{ else }} bar {{ end }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_if_else_if() {
|
fn check_if_else_if() {
|
||||||
let input = "{{ if foo }} foo {{ else if bar }} bar {{ end }}";
|
let input = NomoInput::from("{{ if foo }} foo {{ else if bar }} bar {{ end }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_trim_whitespace() {
|
fn check_trim_whitespace() {
|
||||||
let input = "{{ if foo -}} foo {{- else if bar -}} bar {{- end }}";
|
let input = NomoInput::from("{{ if foo -}} foo {{- else if bar -}} bar {{- end }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_for_loop() {
|
fn check_for_loop() {
|
||||||
let input = "{{ for value in array }} Hi: {{= value }} {{ else }} No Content :C {{ end }}";
|
let input = NomoInput::from(
|
||||||
|
"{{ for value in array }} Hi: {{= value }} {{ else }} No Content :C {{ end }}",
|
||||||
|
);
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_math_expression() {
|
fn check_math_expression() {
|
||||||
let input = "{{= 5 * 3 + 2 / 3 }}";
|
let input = NomoInput::from("{{= 5 * 3 + 2 / 3 }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_logical_expression() {
|
fn check_logical_expression() {
|
||||||
let input = "{{= true && false || 3 >= 2 && 5 == 2 }}";
|
let input = NomoInput::from("{{= true && false || 3 >= 2 && 5 == 2 }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_function_call() {
|
fn check_function_call() {
|
||||||
let input = "{{= foo(2 * 3, bar(2 + baz)) }}";
|
let input = NomoInput::from("{{= foo(2 * 3, bar(2 + baz)) }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_conditional_access() {
|
fn check_conditional_access() {
|
||||||
let input = "{{= foo? }}";
|
let input = NomoInput::from("{{= foo? }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_access_operator() {
|
fn check_access_operator() {
|
||||||
let input = "{{= foo?.bar }}";
|
let input = NomoInput::from("{{= foo?.bar }}");
|
||||||
|
|
||||||
let parsed = crate::lexer::parse(input.into()).unwrap();
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let ast = panic_pretty(input, parse(parsed.tokens()));
|
let ast = panic_pretty(parse(input, parsed.tokens()));
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: src/parser/mod.rs
|
source: src/parser/mod.rs
|
||||||
expression: ast.to_report(input)
|
expression: ast
|
||||||
---
|
---
|
||||||
[1m[91merror[0m[1m: Standlone action block[0m
|
[1m[91merror[0m[1m: Standlone action block[0m
|
||||||
[1m[94m ╭▸ [0m
|
[1m[94m ╭▸ [0m
|
||||||
|
|
@ -8,23 +8,31 @@ expression: ast.to_report(input)
|
||||||
[1m[94m│[0m [1m[91m━━━━━[0m
|
[1m[94m│[0m [1m[91m━━━━━[0m
|
||||||
[1m[94m│[0m
|
[1m[94m│[0m
|
||||||
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
||||||
|
|
||||||
[1m[91merror[0m[1m: Standlone action block[0m
|
[1m[91merror[0m[1m: Standlone action block[0m
|
||||||
[1m[94m ╭▸ [0m
|
[1m[94m ╭▸ [0m
|
||||||
[1m[94m2[0m [1m[94m│[0m {{ value }}
|
[1m[94m2[0m [1m[94m│[0m {{ value }}
|
||||||
[1m[94m│[0m [1m[91m━━━━━[0m
|
[1m[94m│[0m [1m[91m━━━━━[0m
|
||||||
|
[1m[94m│[0m
|
||||||
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
||||||
|
|
||||||
[1m[91merror[0m[1m: Standlone action block[0m
|
[1m[91merror[0m[1m: Standlone action block[0m
|
||||||
[1m[94m ╭▸ [0m
|
[1m[94m ╭▸ [0m
|
||||||
[1m[94m3[0m [1m[94m│[0m {{ value }}
|
[1m[94m3[0m [1m[94m│[0m {{ value }}
|
||||||
[1m[94m│[0m [1m[91m━━━━━[0m
|
[1m[94m│[0m [1m[91m━━━━━[0m
|
||||||
|
[1m[94m│[0m
|
||||||
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
||||||
|
|
||||||
[1m[91merror[0m[1m: Standlone action block[0m
|
[1m[91merror[0m[1m: Standlone action block[0m
|
||||||
[1m[94m ╭▸ [0m
|
[1m[94m ╭▸ [0m
|
||||||
[1m[94m4[0m [1m[94m│[0m {{ value }}
|
[1m[94m4[0m [1m[94m│[0m {{ value }}
|
||||||
[1m[94m│[0m [1m[91m━━━━━[0m
|
[1m[94m│[0m [1m[91m━━━━━[0m
|
||||||
|
[1m[94m│[0m
|
||||||
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
||||||
|
|
||||||
[1m[91merror[0m[1m: Standlone action block[0m
|
[1m[91merror[0m[1m: Standlone action block[0m
|
||||||
[1m[94m ╭▸ [0m
|
[1m[94m ╭▸ [0m
|
||||||
[1m[94m5[0m [1m[94m│[0m {{ value }}
|
[1m[94m5[0m [1m[94m│[0m {{ value }}
|
||||||
[1m[94m│[0m [1m[91m━━━━━[0m
|
[1m[94m│[0m [1m[91m━━━━━[0m
|
||||||
|
[1m[94m│[0m
|
||||||
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
[1m[94m╰ [0m[1mhelp[0m: If you want to output this expression, add a '=' to the block
|
||||||
|
|
|
||||||
17
src/value.rs
17
src/value.rs
|
|
@ -1,11 +1,12 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[cfg(feature = "serde_json")]
|
|
||||||
use displaydoc::Display;
|
use displaydoc::Display;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Values to be used inside templates
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
#[expect(missing_docs)]
|
||||||
pub enum NomoValue {
|
pub enum NomoValue {
|
||||||
String {
|
String {
|
||||||
value: Cow<'static, str>,
|
value: Cow<'static, str>,
|
||||||
|
|
@ -35,6 +36,7 @@ pub enum NomoValue {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NomoValue {
|
impl NomoValue {
|
||||||
|
/// Return a str if there is one inside
|
||||||
pub fn as_str(&self) -> Option<&str> {
|
pub fn as_str(&self) -> Option<&str> {
|
||||||
if let Self::String { value } = self {
|
if let Self::String { value } = self {
|
||||||
Some(value)
|
Some(value)
|
||||||
|
|
@ -43,6 +45,7 @@ impl NomoValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return an arry if there is one inside
|
||||||
pub fn as_array(&self) -> Option<&[NomoValue]> {
|
pub fn as_array(&self) -> Option<&[NomoValue]> {
|
||||||
if let Self::Array { value } = self {
|
if let Self::Array { value } = self {
|
||||||
Some(value)
|
Some(value)
|
||||||
|
|
@ -51,6 +54,7 @@ impl NomoValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a bool if there is one inside
|
||||||
pub fn as_bool(&self) -> Option<bool> {
|
pub fn as_bool(&self) -> Option<bool> {
|
||||||
if let Self::Bool { value } = self {
|
if let Self::Bool { value } = self {
|
||||||
Some(*value)
|
Some(*value)
|
||||||
|
|
@ -61,6 +65,7 @@ impl NomoValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return an object if there is one inside
|
||||||
pub fn as_object(&self) -> Option<&BTreeMap<String, NomoValue>> {
|
pub fn as_object(&self) -> Option<&BTreeMap<String, NomoValue>> {
|
||||||
if let Self::Object { value } = self {
|
if let Self::Object { value } = self {
|
||||||
Some(value)
|
Some(value)
|
||||||
|
|
@ -69,6 +74,7 @@ impl NomoValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return an integer if there is one inside
|
||||||
pub fn as_integer(&self) -> Option<u64> {
|
pub fn as_integer(&self) -> Option<u64> {
|
||||||
if let Self::Integer { value } = self {
|
if let Self::Integer { value } = self {
|
||||||
Some(*value)
|
Some(*value)
|
||||||
|
|
@ -77,6 +83,7 @@ impl NomoValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a float if there is one inside
|
||||||
pub fn as_float(&self) -> Option<f64> {
|
pub fn as_float(&self) -> Option<f64> {
|
||||||
if let Self::Float { value } = self {
|
if let Self::Float { value } = self {
|
||||||
Some(*value)
|
Some(*value)
|
||||||
|
|
@ -85,6 +92,7 @@ impl NomoValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the iterator if there is one inside
|
||||||
pub fn as_iterator(&self) -> Option<&dyn CloneIterator<Item = NomoValue>> {
|
pub fn as_iterator(&self) -> Option<&dyn CloneIterator<Item = NomoValue>> {
|
||||||
if let Self::Iterator { value } = self {
|
if let Self::Iterator { value } = self {
|
||||||
Some(value)
|
Some(value)
|
||||||
|
|
@ -93,6 +101,7 @@ impl NomoValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the iterator mutably if there is one inside
|
||||||
pub fn as_iterator_mut(&mut self) -> Option<&mut dyn CloneIterator<Item = NomoValue>> {
|
pub fn as_iterator_mut(&mut self) -> Option<&mut dyn CloneIterator<Item = NomoValue>> {
|
||||||
if let Self::Iterator { value } = self {
|
if let Self::Iterator { value } = self {
|
||||||
Some(value)
|
Some(value)
|
||||||
|
|
@ -237,19 +246,21 @@ impl NomoValue {
|
||||||
pub(crate) fn try_to_string(&self) -> Option<String> {
|
pub(crate) fn try_to_string(&self) -> Option<String> {
|
||||||
match self {
|
match self {
|
||||||
NomoValue::String { value } => Some(value.to_string()),
|
NomoValue::String { value } => Some(value.to_string()),
|
||||||
NomoValue::Array { .. } => None,
|
|
||||||
NomoValue::Bool { value } => Some(value.to_string()),
|
NomoValue::Bool { value } => Some(value.to_string()),
|
||||||
NomoValue::Object { .. } => None,
|
|
||||||
NomoValue::Integer { value } => Some(value.to_string()),
|
NomoValue::Integer { value } => Some(value.to_string()),
|
||||||
NomoValue::SignedInteger { value } => Some(value.to_string()),
|
NomoValue::SignedInteger { value } => Some(value.to_string()),
|
||||||
NomoValue::Float { value } => Some(value.to_string()),
|
NomoValue::Float { value } => Some(value.to_string()),
|
||||||
|
NomoValue::Array { .. } => None,
|
||||||
|
NomoValue::Object { .. } => None,
|
||||||
NomoValue::Iterator { .. } => None,
|
NomoValue::Iterator { .. } => None,
|
||||||
NomoValue::Undefined => None,
|
NomoValue::Undefined => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marker trait for iterators that can be cloned
|
||||||
pub trait CloneIterator: Iterator<Item = NomoValue> {
|
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>>;
|
fn clone_box(&self) -> Box<dyn CloneIterator<Item = NomoValue>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
51
src/winnow_ext.rs
Normal file
51
src/winnow_ext.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_files() {
|
fn check_files() {
|
||||||
let files = std::fs::read_dir("tests/checks/").unwrap();
|
let files = std::fs::read_dir("tests/checks/").unwrap();
|
||||||
|
|
@ -5,11 +7,13 @@ fn check_files() {
|
||||||
for file in files {
|
for file in files {
|
||||||
let input = std::fs::read_to_string(file.unwrap().path()).unwrap();
|
let input = std::fs::read_to_string(file.unwrap().path()).unwrap();
|
||||||
|
|
||||||
let Ok(parsed) = nomo::lexer::parse(input.into()) else {
|
let input = nomo::input::NomoInput::from(input);
|
||||||
|
|
||||||
|
let Ok(parsed) = nomo::lexer::parse(input.clone()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(ast) = nomo::parser::parse(parsed.tokens()) else {
|
let Ok(ast) = nomo::parser::parse(input, parsed.tokens()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
119
tests/errors/invalid_controls.1-parsed.snap
Normal file
119
tests/errors/invalid_controls.1-parsed.snap
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
---
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
}
|
||||||
51
tests/errors/invalid_controls.2-ast.snap
Normal file
51
tests/errors/invalid_controls.2-ast.snap
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
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: {}
|
||||||
|
---
|
||||||
|
[1m[91merror[0m[1m: Expected an expression after 'if'[0m
|
||||||
|
[1m[94m ╭▸ [0m
|
||||||
|
[1m[94m1[0m [1m[94m│[0m {{ if }} {{ end }}
|
||||||
|
[1m[94m╰╴[0m [1m[91m━━[0m
|
||||||
|
|
||||||
|
[1m[91merror[0m[1m: Expected an expression after 'if'[0m
|
||||||
|
[1m[94m ╭▸ [0m
|
||||||
|
[1m[94m2[0m [1m[94m│[0m {{ if if }} {{ end }}
|
||||||
|
[1m[94m╰╴[0m [1m[91m━━[0m
|
||||||
|
|
||||||
|
[1m[91merror[0m[1m: Expected an expression after 'if'[0m
|
||||||
|
[1m[94m ╭▸ [0m
|
||||||
|
[1m[94m3[0m [1m[94m│[0m {{ if if }} {{ for foo in bar }} {{ end }} {{ end }}
|
||||||
|
[1m[94m╰╴[0m [1m[91m━━[0m
|
||||||
|
|
||||||
|
[1m[91merror[0m[1m: Missing ident here[0m
|
||||||
|
[1m[94m ╭▸ [0m
|
||||||
|
[1m[94m4[0m [1m[94m│[0m {{ for in bar }} {{ end }}
|
||||||
|
[1m[94m╰╴[0m [1m[91m━━━━━━[0m
|
||||||
|
|
||||||
|
[1m[91merror[0m[1m: Missing `in` in `for .. in ` loop[0m
|
||||||
|
[1m[94m ╭▸ [0m
|
||||||
|
[1m[94m5[0m [1m[94m│[0m {{ for blah bar }} {{ end }}
|
||||||
|
[1m[94m│[0m [1m[91m━━━━━━━━[0m
|
||||||
|
[1m[94m╰╴[0m
|
||||||
|
[1m[92mnote[0m: Try adding it
|
||||||
|
[1m[94m╭╴[0m
|
||||||
|
[1m[94m5[0m [1m[94m│ [0m{{ for blah[92m in[0m bar }} {{ end }}
|
||||||
|
[1m[94m╰╴[0m [92m++[0m
|
||||||
|
|
||||||
|
[1m[91merror[0m[1m: Missing `in` in `for .. in ` loop[0m
|
||||||
|
[1m[94m ╭▸ [0m
|
||||||
|
[1m[94m6[0m [1m[94m│[0m {{ for blah}} {{ end }}
|
||||||
|
[1m[94m│[0m [1m[91m━━━━[0m
|
||||||
|
[1m[94m╰╴[0m
|
||||||
|
[1m[92mnote[0m: Try adding it
|
||||||
|
[1m[94m╭╴[0m
|
||||||
|
[1m[94m6[0m [1m[94m│ [0m{{ for blah[92m in[0m}} {{ end }}
|
||||||
|
[1m[94m╰╴[0m [92m++[0m
|
||||||
|
|
||||||
|
[1m[91merror[0m[1m: Unexpected keyword[0m
|
||||||
|
[1m[94m ╭▸ [0m
|
||||||
|
[1m[94m7[0m [1m[94m│[0m {{ else }}
|
||||||
|
[1m[94m╰╴[0m [1m[91m━━━━[0m
|
||||||
9
tests/errors/invalid_controls.nomo
Normal file
9
tests/errors/invalid_controls.nomo
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{}
|
||||||
|
---
|
||||||
|
{{ 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 }}
|
||||||
24
tests/errors/invalid_if.1-parsed.snap
Normal file
24
tests/errors/invalid_if.1-parsed.snap
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
}
|
||||||
11
tests/errors/invalid_if.2-ast.snap
Normal file
11
tests/errors/invalid_if.2-ast.snap
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
source: tests/file_tests.rs
|
||||||
|
expression: ast
|
||||||
|
info:
|
||||||
|
input: "{{ if if }} {{ end }}"
|
||||||
|
context: {}
|
||||||
|
---
|
||||||
|
[1m[91merror[0m[1m: Expected an expression after 'if'[0m
|
||||||
|
[1m[94m ╭▸ [0m
|
||||||
|
[1m[94m1[0m [1m[94m│[0m {{ if if }} {{ end }}
|
||||||
|
[1m[94m╰╴[0m [1m[91m━━[0m
|
||||||
3
tests/errors/invalid_if.nomo
Normal file
3
tests/errors/invalid_if.nomo
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{}
|
||||||
|
---
|
||||||
|
{{ if if }} {{ end }}
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use nomo::Context;
|
use nomo::Context;
|
||||||
use nomo::functions::FunctionMap;
|
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/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)]
|
#[derive(serde::Serialize)]
|
||||||
struct Info {
|
struct Info {
|
||||||
input: String,
|
input: String,
|
||||||
|
|
@ -39,16 +44,18 @@ fn check_for_input([path]: [&Path; 1]) {
|
||||||
context.try_insert(k, v).unwrap();
|
context.try_insert(k, v).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed = nomo::lexer::parse(input.into()).unwrap();
|
let input = NomoInput::from(input);
|
||||||
|
|
||||||
|
let parsed = nomo::lexer::parse(input.clone()).unwrap();
|
||||||
|
|
||||||
let _guard = settings.bind_to_scope();
|
let _guard = settings.bind_to_scope();
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(format!("{basename}.1-parsed"), parsed);
|
insta::assert_debug_snapshot!(format!("{basename}.1-parsed"), parsed);
|
||||||
|
|
||||||
let ast = match nomo::parser::parse(parsed.tokens()) {
|
let ast = match nomo::parser::parse(input, parsed.tokens()) {
|
||||||
Ok(ast) => ast,
|
Ok(ast) => ast,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("{}", err.to_report(input));
|
eprintln!("{}", err);
|
||||||
panic!("Could not evaluate ast");
|
panic!("Could not evaluate ast");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -63,3 +70,69 @@ fn check_for_input([path]: [&Path; 1]) {
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(format!("{basename}.4-output"), output);
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue