diff --git a/Cargo.lock b/Cargo.lock index a59a65b..21b08d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "annotate-snippets" version = "0.12.13" @@ -31,6 +40,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -107,6 +126,19 @@ dependencies = [ "wasip3", ] +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -153,9 +185,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" dependencies = [ "console", + "globset", "once_cell", "similar", "tempfile", + "walkdir", ] [[package]] @@ -234,6 +268,23 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rustix" version = "1.1.4" @@ -247,6 +298,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "semver" version = "1.0.27" @@ -260,6 +320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -334,6 +395,7 @@ dependencies = [ "insta", "serde", "serde_json", + "temple", "thiserror", "winnow", ] @@ -376,6 +438,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -428,6 +500,15 @@ dependencies = [ "semver", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 7d99808..521eacb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,19 @@ edition = "2024" [dependencies] annotate-snippets = "0.12.13" displaydoc = "0.2.5" -serde = "1.0.228" +serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" thiserror = "2.0.18" winnow = { version = "0.7.14", features = ["unstable-recover"] } [dev-dependencies] annotate-snippets = { version = "0.12.13", features = ["testing-colors"] } -insta = "1.46.3" +insta = { version = "1.46.3", features = ["glob"] } +temple = { path = ".", features = ["serialize"] } + +[profile.dev.package] +insta.opt-level = 3 +similar.opt-level = 3 + +[features] +serialize = [] diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 8984e2b..f2097c6 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -15,7 +15,10 @@ pub enum EvaluationError { ExplicitAbort, } -pub fn execute(instructions: &[Instruction], global_context: &Context) -> Result { +pub fn execute( + instructions: &[Instruction], + global_context: &Context, +) -> Result { let mut output = String::new(); let mut scopes: HashMap = HashMap::new(); diff --git a/src/input.rs b/src/input.rs index ff8fa36..955c6c2 100644 --- a/src/input.rs +++ b/src/input.rs @@ -14,12 +14,31 @@ pub struct TempleInput { range: Range, } +impl TempleInput { + pub fn from_parts(backing: Arc, range: Range) -> TempleInput { + TempleInput { backing, range } + } + + pub fn into_parts(self) -> (Arc, Range) { + (self.backing, self.range) + } +} + impl std::fmt::Debug for TempleInput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "\"{}\"", self.as_str()) } } +impl From for TempleInput { + fn from(value: String) -> Self { + let range = 0..value.len(); + let backing = Arc::from(value); + + TempleInput { backing, range } + } +} + impl From<&str> for TempleInput { fn from(value: &str) -> Self { let backing = Arc::from(value.to_string()); diff --git a/src/lib.rs b/src/lib.rs index d72e899..fd23589 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ use crate::input::TempleInput; pub mod ast; pub mod emit; pub mod eval; -mod input; +pub mod input; pub mod parser; #[derive(Debug, Error, Display)] diff --git a/tests/cases/ast@interpolation.snap b/tests/cases/ast@interpolation.snap new file mode 100644 index 0000000..dd6aab3 --- /dev/null +++ b/tests/cases/ast@interpolation.snap @@ -0,0 +1,42 @@ +--- +source: tests/file_tests.rs +expression: ast +input_file: tests/cases/interpolation.temple +--- +TemplateAst { + root: [ + StaticContent( + TemplateToken { + kind: Content, + source: "Hello! I'm", + }, + ), + Interpolation { + prev_whitespace: Some( + TemplateToken { + kind: Whitespace, + source: " ", + }, + ), + wants_output: Some( + TemplateToken { + kind: WantsOutput, + source: "=", + }, + ), + expression: VariableAccess( + TemplateToken { + kind: Ident, + source: "name", + }, + ), + post_whitespace: Some( + TemplateToken { + kind: Whitespace, + source: " + ", + }, + ), + }, + ], +} diff --git a/tests/cases/ast@simple.snap b/tests/cases/ast@simple.snap new file mode 100644 index 0000000..cdfef72 --- /dev/null +++ b/tests/cases/ast@simple.snap @@ -0,0 +1,16 @@ +--- +source: tests/file_tests.rs +expression: ast +input_file: tests/cases/simple.temple +--- +TemplateAst { + root: [ + StaticContent( + TemplateToken { + kind: Content, + source: "Hello World! + ", + }, + ), + ], +} diff --git a/tests/cases/instructions@interpolation.snap b/tests/cases/instructions@interpolation.snap new file mode 100644 index 0000000..0dbff6d --- /dev/null +++ b/tests/cases/instructions@interpolation.snap @@ -0,0 +1,28 @@ +--- +source: tests/file_tests.rs +expression: emit +input_file: tests/cases/interpolation.temple +--- +[ + AppendContent { + content: "Hello! I'm", + }, + AppendContent { + content: " ", + }, + LoadFromContextToSlot { + name: "name", + slot: VariableSlot { + index: 0, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 0, + }, + }, + AppendContent { + content: " + ", + }, +] diff --git a/tests/cases/instructions@simple.snap b/tests/cases/instructions@simple.snap new file mode 100644 index 0000000..be760cf --- /dev/null +++ b/tests/cases/instructions@simple.snap @@ -0,0 +1,11 @@ +--- +source: tests/file_tests.rs +expression: emit +input_file: tests/cases/simple.temple +--- +[ + AppendContent { + content: "Hello World! + ", + }, +] diff --git a/tests/cases/interpolation.temple b/tests/cases/interpolation.temple new file mode 100644 index 0000000..5d0cfc9 --- /dev/null +++ b/tests/cases/interpolation.temple @@ -0,0 +1,5 @@ +{ + "name": "Hemera" +} +--- +Hello! I'm {{= name }} diff --git a/tests/cases/output@interpolation.snap b/tests/cases/output@interpolation.snap new file mode 100644 index 0000000..f607ac7 --- /dev/null +++ b/tests/cases/output@interpolation.snap @@ -0,0 +1,6 @@ +--- +source: tests/file_tests.rs +expression: output +input_file: tests/cases/interpolation.temple +--- +"Hello! I'm Hemera\n" diff --git a/tests/cases/output@simple.snap b/tests/cases/output@simple.snap new file mode 100644 index 0000000..7e78743 --- /dev/null +++ b/tests/cases/output@simple.snap @@ -0,0 +1,6 @@ +--- +source: tests/file_tests.rs +expression: output +input_file: tests/cases/simple.temple +--- +"Hello World!\n" diff --git a/tests/cases/parsed@interpolation.snap b/tests/cases/parsed@interpolation.snap new file mode 100644 index 0000000..c0ea910 --- /dev/null +++ b/tests/cases/parsed@interpolation.snap @@ -0,0 +1,46 @@ +--- +source: tests/file_tests.rs +expression: parsed +input_file: tests/cases/interpolation.temple +--- +ParsedTemplate { + tokens: [ + TemplateToken { + kind: Content, + source: "Hello! I'm", + }, + TemplateToken { + kind: Whitespace, + source: " ", + }, + TemplateToken { + kind: LeftDelim, + source: "{{", + }, + TemplateToken { + kind: WantsOutput, + source: "=", + }, + TemplateToken { + kind: Whitespace, + source: " ", + }, + TemplateToken { + kind: Ident, + source: "name", + }, + TemplateToken { + kind: Whitespace, + source: " ", + }, + TemplateToken { + kind: RightDelim, + source: "}}", + }, + TemplateToken { + kind: Whitespace, + source: " + ", + }, + ], +} diff --git a/tests/cases/parsed@simple.snap b/tests/cases/parsed@simple.snap new file mode 100644 index 0000000..a227f96 --- /dev/null +++ b/tests/cases/parsed@simple.snap @@ -0,0 +1,14 @@ +--- +source: tests/file_tests.rs +expression: parsed +input_file: tests/cases/simple.temple +--- +ParsedTemplate { + tokens: [ + TemplateToken { + kind: Content, + source: "Hello World! + ", + }, + ], +} diff --git a/tests/cases/simple.temple b/tests/cases/simple.temple new file mode 100644 index 0000000..980a0d5 --- /dev/null +++ b/tests/cases/simple.temple @@ -0,0 +1 @@ +Hello World! diff --git a/tests/file_tests.rs b/tests/file_tests.rs new file mode 100644 index 0000000..49e87b4 --- /dev/null +++ b/tests/file_tests.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; + +use temple::Context; + +#[test] +fn check_cases() { + insta::glob!("cases/*.temple", |path| { + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_path("cases"); + settings.set_snapshot_suffix(path.file_stem().unwrap().display().to_string()); + settings.set_prepend_module_to_snapshot(false); + let _guard = settings.bind_to_scope(); + + let input = std::fs::read_to_string(path).unwrap(); + + let (context, input) = input.split_once("\n---\n").unwrap_or_else(|| ("", &input)); + + let map = if !context.is_empty() { + serde_json::from_str::>(context).unwrap() + } else { + HashMap::new() + }; + + let mut context = Context::new(); + + for (k, v) in map { + context.insert(k, v); + } + + let parsed = temple::parser::parse(input.into()).unwrap(); + + insta::assert_debug_snapshot!("parsed", parsed); + + let ast = temple::ast::parse(parsed.tokens()).unwrap(); + + insta::assert_debug_snapshot!("ast", ast); + + let emit = temple::emit::emit_machine(ast); + + insta::assert_debug_snapshot!("instructions", emit); + + let output = temple::eval::execute(&emit, &context).unwrap(); + + insta::assert_debug_snapshot!("output", output); + }); +}