From 0e3b3ea96d37c7ae723b5ad0956aa86b27133e5b Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Sun, 26 Feb 2023 00:08:41 +0000 Subject: [PATCH] Initial work on the parser for the Hornbeam grammar --- .envrc | 2 + .gitignore | 1 + Cargo.lock | 386 ++++++++++++++++ Cargo.toml | 17 + README.md | 16 + hornbeam/Cargo.toml | 9 + hornbeam/src/lib.rs | 14 + hornbeam_grammar/Cargo.toml | 16 + hornbeam_grammar/src/ast.rs | 142 ++++++ hornbeam_grammar/src/examples/example01.hnb | 16 + hornbeam_grammar/src/hornbeam.pest | 173 +++++++ hornbeam_grammar/src/lib.rs | 2 + hornbeam_grammar/src/parser.rs | 434 ++++++++++++++++++ ...r__parser__tests__fragments_and_slots.snap | 30 ++ ...m_grammar__parser__tests__if_blocks-2.snap | 59 +++ ...eam_grammar__parser__tests__if_blocks.snap | 38 ++ ..._grammar__parser__tests__localisation.snap | 53 +++ ..._parser__tests__simple_parses_correct.snap | 9 + ...__tests__string_interpolations_nested.snap | 32 ++ ...ts__supply_slots_to_components_only-2.snap | 13 + ...ests__supply_slots_to_components_only.snap | 32 ++ hornbeam_interpreter/Cargo.toml | 8 + hornbeam_interpreter/src/lib.rs | 14 + hornbeam_ir/Cargo.toml | 8 + hornbeam_ir/src/lib.rs | 14 + hornbeam_macros/Cargo.toml | 8 + hornbeam_macros/src/lib.rs | 14 + shell.nix | 47 ++ 28 files changed, 1607 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 hornbeam/Cargo.toml create mode 100644 hornbeam/src/lib.rs create mode 100644 hornbeam_grammar/Cargo.toml create mode 100644 hornbeam_grammar/src/ast.rs create mode 100644 hornbeam_grammar/src/examples/example01.hnb create mode 100644 hornbeam_grammar/src/hornbeam.pest create mode 100644 hornbeam_grammar/src/lib.rs create mode 100644 hornbeam_grammar/src/parser.rs create mode 100644 hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__fragments_and_slots.snap create mode 100644 hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks-2.snap create mode 100644 hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks.snap create mode 100644 hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__localisation.snap create mode 100644 hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__simple_parses_correct.snap create mode 100644 hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__string_interpolations_nested.snap create mode 100644 hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__supply_slots_to_components_only-2.snap create mode 100644 hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__supply_slots_to_components_only.snap create mode 100644 hornbeam_interpreter/Cargo.toml create mode 100644 hornbeam_interpreter/src/lib.rs create mode 100644 hornbeam_ir/Cargo.toml create mode 100644 hornbeam_ir/src/lib.rs create mode 100644 hornbeam_macros/Cargo.toml create mode 100644 hornbeam_macros/src/lib.rs create mode 100644 shell.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..c9293c0 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +use nix + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a09c56d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..602d8f8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,386 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys", +] + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hornbeam" +version = "0.1.0" +dependencies = [ + "hornbeam_grammar", +] + +[[package]] +name = "hornbeam_grammar" +version = "0.1.0" +dependencies = [ + "insta", + "lazy_static", + "pest", + "pest_consume", + "pest_derive", + "serde", +] + +[[package]] +name = "hornbeam_interpreter" +version = "0.1.0" + +[[package]] +name = "hornbeam_ir" +version = "0.1.0" + +[[package]] +name = "hornbeam_macros" +version = "0.1.0" + +[[package]] +name = "insta" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea5b3894afe466b4bcf0388630fc15e11938a6074af0cd637c825ba2ec8a099" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", + "yaml-rust", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "pest" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028accff104c4e513bad663bbcd2ad7cfd5304144404c31ed0a77ac103d00660" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_consume" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79447402d15d18e7142e14c72f2e63fa3d155be1bc5b70b3ccbb610ac55f536b" +dependencies = [ + "pest", + "pest_consume_macros", + "pest_derive", +] + +[[package]] +name = "pest_consume_macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8630a7a899cb344ec1c16ba0a6b24240029af34bdc0a21f84e411d7f793f29" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_derive" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac3922aac69a40733080f53c1ce7f91dcf57e1a5f6c52f421fadec7fbdc4b69" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06646e185566b5961b4058dd107e0a7f56e77c3f484549fb119867773c0f202" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f60b2ba541577e2a0c307c8f39d1439108120eb7903adeb6497fa880c59616" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9d4c108 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[workspace] +members = [ + "hornbeam_grammar", + "hornbeam_ir", + "hornbeam_interpreter", + "hornbeam_macros", + "hornbeam" +] + + + +# Enable optimisation for testing helpers +[profile.dev.package.insta] +opt-level = 3 + +[profile.dev.package.similar] +opt-level = 3 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..da25649 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Hornbeam Template Engine + +WIP. Will be: a lightweight and nimble HTML templating engine, designed for component reuse and ease of use with HTMX, whilst not harming iterative development time. + +## Crates + +* `hornbeam` — usual point of entry to the engine, useful in applications. +* `hornbeam_grammar` — grammar definition for Hornbeam templates. +* `hornbeam_ir` — intermediate representation for Hornbeam templates. +* `hornbeam_interpreter` — interpreter for Hornbeam templates, using `bevy_reflect` and being hot-reloadable. Useful in debug builds. +* `hornbeam_macros` — macros for compile-time template compilation to Rust. Useful in release builds. + +## Licence + +TODO. Currently All Rights Reserved. + diff --git a/hornbeam/Cargo.toml b/hornbeam/Cargo.toml new file mode 100644 index 0000000..e5eb475 --- /dev/null +++ b/hornbeam/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "hornbeam" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hornbeam_grammar = { version = "0.1.0", path = "../hornbeam_grammar" } diff --git a/hornbeam/src/lib.rs b/hornbeam/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/hornbeam/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/hornbeam_grammar/Cargo.toml b/hornbeam_grammar/Cargo.toml new file mode 100644 index 0000000..9a3886a --- /dev/null +++ b/hornbeam_grammar/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "hornbeam_grammar" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +pest = "2.5.5" +pest_derive = "2.5.5" +pest_consume = "1.1.3" +lazy_static = "1.4.0" +serde = { version = "1.0.152", features = ["derive"] } + +[dev-dependencies] +insta = { version = "1.28.0", features = ["yaml"] } diff --git a/hornbeam_grammar/src/ast.rs b/hornbeam_grammar/src/ast.rs new file mode 100644 index 0000000..69ae6ae --- /dev/null +++ b/hornbeam_grammar/src/ast.rs @@ -0,0 +1,142 @@ +use serde::Serialize; +use std::borrow::Cow; +use std::collections::BTreeMap; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct Template<'a> { + pub blocks: Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub enum Block<'a> { + HtmlElement(HtmlElement<'a>), + ComponentElement(ComponentElement<'a>), + IfBlock(IfBlock<'a>), + Text(StringExpr<'a>), + DefineExpandSlot(DefineExpandSlot<'a>), + DefineFragment(DefineFragment<'a>), +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct HtmlElement<'a> { + pub name: Cow<'a, str>, + pub children: Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct ComponentElement<'a> { + pub name: Cow<'a, str>, + pub slots: BTreeMap, Vec>>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct IfBlock<'a> { + pub condition: Expression<'a>, + pub blocks: Vec>, + pub else_blocks: Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct DefineExpandSlot<'a> { + pub name: Cow<'a, str>, + pub optional: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct DefineFragment<'a> { + pub name: Cow<'a, str>, + pub blocks: Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct StringExpr<'a> { + pub pieces: Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub enum StringPiece<'a> { + Literal(Cow<'a, str>), + Interpolation(Expression<'a>), + Localise { + trans_key: Cow<'a, str>, + parameters: BTreeMap, Expression<'a>>, + }, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub enum Expression<'a> { + // Arithmetic Operators + Add { + left: Box>, + right: Box>, + }, + Sub { + left: Box>, + right: Box>, + }, + Mul { + left: Box>, + right: Box>, + }, + Div { + left: Box>, + right: Box>, + }, + Negate { + sub: Box>, + }, + + // Boolean Operators + BAnd { + left: Box>, + right: Box>, + }, + BOr { + left: Box>, + right: Box>, + }, + BNot { + sub: Box>, + }, + + // Comparators + Equals { + left: Box>, + right: Box>, + }, + + // Other Operators + ListAdd { + left: Box>, + right: Box>, + }, + + // Literals + List { + elements: Vec>, + }, + IntLiteral { + val: i64, + }, + StringExpr(StringExpr<'a>), + + // Relatives + FieldLookup { + obj: Box>, + ident: Cow<'a, str>, + }, + MethodCall { + obj: Box>, + ident: Cow<'a, str>, + args: Vec>, + }, + + // Other Primaries + Variable { + name: Cow<'a, str>, + }, + FunctionCall { + name: Cow<'a, str>, + args: Vec>, + }, +} diff --git a/hornbeam_grammar/src/examples/example01.hnb b/hornbeam_grammar/src/examples/example01.hnb new file mode 100644 index 0000000..96588bc --- /dev/null +++ b/hornbeam_grammar/src/examples/example01.hnb @@ -0,0 +1,16 @@ +TestComponent + :main + div + '' + This isn't half bad... ${1 + 1} + '' + if 1 + 1 = 2 + div + span + '' + woohoo + '' + :header + '' + Example 01! + '' diff --git a/hornbeam_grammar/src/hornbeam.pest b/hornbeam_grammar/src/hornbeam.pest new file mode 100644 index 0000000..a76eb20 --- /dev/null +++ b/hornbeam_grammar/src/hornbeam.pest @@ -0,0 +1,173 @@ +// +Hornbeam = { SOI ~ wsnl* ~ BlockContent* ~ ws* ~ EOI } + +NewBlock = _{ + PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ BlockContent ~ + (PEEK_ALL ~ BlockContent)* ~ DROP +} + +BlockContent = _{ + Element | + IfBlock | + Text | + DefineExpandSlot | + ForBlock | + DefineFragment +} + +Element = { + ElementName ~ lineEnd ~ (NewBlock | NewSlotBlock)? +} + +Text = { + String ~ lineEnd +} + +IfBlock = { + "if" ~ ws+ ~ IfCondition ~ lineEnd ~ NewBlock ~ + ElseBlock? +} + +ElseBlock = { + "else" ~ (ws+ ~ IfBlock | lineEnd ~ NewBlock) +} + +IfCondition = { + Expr +} + +ForBlock = { + "for" ~ ws+ ~ Binding ~ ws+ ~ "in" ~ ws+ ~ Expr ~ lineEnd ~ NewBlock ~ + EmptyForBlock? +} +// Optional part of a for block: triggered if there were no items to show. +EmptyForBlock = { + "empty" ~ ws+ ~ lineEnd ~ NewBlock +} + +DefineExpandSlot = { + SlotOptional? ~ "slot" ~ ws+ ~ ":" ~ Identifier ~ lineEnd +} +SlotOptional = { "optional" ~ ws+ } + +DefineFragment = { + "fragment" ~ ws+ ~ Identifier ~ lineEnd ~ NewBlock +} + +NewSlotBlock = _{ + PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ SupplySlot ~ + (PEEK_ALL ~ SupplySlot)* ~ DROP +} +SupplySlot = { + ":" ~ Identifier ~ lineEnd ~ NewBlock +} + +ElementName = { componentName | htmlBuiltin } + +// PascalCase +componentName = _{ ASCII_ALPHA_UPPER ~ ASCII_ALPHANUMERIC+ } + +// Built-in tag names +htmlBuiltin = _{ "html" | "head" | "body" | "x" | "div" | "span" } + +// We don't actually want to consume the EOI because then we'll get an annoying extra node in our parse... +nlOrEoi = _{ NEWLINE | &EOI } +comment = _{ "//" ~ (!NEWLINE ~ ANY)* ~ nlOrEoi } +comment_nonl = _{ "//" ~ (!NEWLINE ~ ANY)* ~ &nlOrEoi } + +// I don't like the implicit WHITESPACE thing in Pest, it doesn't work well, +// atomicity of rules (@) propagates inwards and it's all very messy. +// Instead, define our own `ws` and use it how we like. +ws = _{ " " | "\t" | comment } +wsnl = _{ " " | "\t" | NEWLINE | comment } +wsnc = _{ " " | "\t" } +ws_nocnl = _{ " " | "\t" | comment_nonl } + +// Line ending, with allowed blank lines. +lineEnd = _{ ws_nocnl* ~ nlOrEoi ~ (!EOI ~ ws* ~ nlOrEoi)* } + +SingleStringContent = { (!("'" | "\\" | "$") ~ ANY)+ } +singleString = _{ !("''") ~ "'" ~ (SingleStringContent | SEscape | SInterpol)* ~ "'" } + +DoubleStringContent = { (!("\"" | "\\" | "$") ~ ANY)+ } +doubleString = _{ "\"" ~ (DoubleStringContent | SEscape | SInterpol)* ~ "\"" } + +blockStringStart = _{ "''" ~ NEWLINE ~ PEEK_ALL } +blockStringEnd = _{ NEWLINE ~ (" " | "\t")* ~ "''" } +BlockStringContent = { (!(NEWLINE | blockStringEnd | "\\" | "$" | "@") ~ ANY)+ } +// This rule becomes just \n later on, so it effectively strips leading indentation! +BlockStringNewline = { !blockStringEnd ~ NEWLINE ~ PEEK_ALL } +blockString = _{ blockStringStart ~ (BlockStringContent | SEscape | SInterpol | ParameterisedLocalisation | BlockStringNewline)* ~ blockStringEnd } +String = { blockString | singleString | doubleString | ParameterisedLocalisation } + +SEscape = { "\\" ~ ("\\" | "'" | "\"" | "$" | "@") } +SInterpol = { "${" ~ ws* ~ Expr ~ ws* ~ "}" } + + + +// We'll use a Pratt parser rather than encode operator precedence in PEG. + +// wshack: needed to prevent the final newline after a comment being devoured +// when it's needed for an explicit line change, but there's no Expr after the comment. +wshack = _{ (wsnc | (comment ~ &(ws* ~ Expr)))* } + +Expr = { prefix* ~ ws* ~ Term ~ ws* ~ postfix* ~ (ws* ~ infix ~ ws* ~ prefix* ~ ws* ~ Term ~ wshack ~ postfix*)* } + +// INFIX +infix = _{ add | sub | mul | div | pow | modulo | listAdd | equals | band | bor } +add = { "+" } +sub = { "-" } +mul = { "*" } +div = { "/" } +pow = { "^" } +modulo = { "%" } +listAdd = { "++" } +equals = { "==" } + +// BUG: these should have forced whitespace at the start! +// (lookbehind wouldn't be the worst feature in the world for a parser grammar!) +band = { "and" ~ ws+ } +bor = { "or" ~ ws+ } + +// PREFIX +prefix = _{ negation | bnot } +negation = { "-" } // Negation +bnot = { "not" ~ ws+ } + +// POSTFIX +postfix = _{ unwrap | MethodCall | FieldLookup | Indexing } +unwrap = { "?" } // Not sure I'm convinced about this one, but we can think about it. +// Note that functions aren't first-class; we don't allow you to 'call' an arbitrary term. +// This is probably for the best since we might have multiple backends for the templating. +MethodCall = { "." ~ ws* ~ Identifier ~ "(" ~ commaSeparatedExprs ~ ")" } +FieldLookup = { "." ~ ws* ~ Identifier } +Indexing = { "[" ~ ws* ~ Expr ~ ws* ~ "]" } + +Term = _{ (IntLiteral | bracketedTerm | FunctionCall | ListLiteral | MapLiteral | String | Variable) } + +bracketedTerm = _{ "(" ~ Expr ~ ")" } + + +IntLiteral = @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) } +Identifier = { (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } +commaSeparatedExprs = _{ wsnl* ~ (Expr ~ wsnl* ~ ("," ~ wsnl* ~ Expr ~ wsnl*)* ~ ("," ~ wsnl*)?)? } +FunctionCall = { Identifier ~ "(" ~ commaSeparatedExprs ~ ")" } +Variable = { "$" ~ Identifier } + +ListLiteral = { "[" ~ commaSeparatedExprs ~ "]" } + +KVPair = { Identifier ~ wsnl* ~ "=" ~ wsnl* ~ Expr } +commaSeparatedKVPairs = _{ wsnl* ~ (KVPair ~ wsnl* ~ ("," ~ wsnl* ~ KVPair ~ wsnl*)* ~ ("," ~ wsnl*)?)? } +MapLiteral = { "{" ~ commaSeparatedKVPairs ~ "}" } + + + + +// As in a let binding or for binding. +// More options in the future, but for now you just get one identifier and that's it! +Binding = { + Identifier +} + +LocalisationIdentifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_" | "-")+ } +ParameterisedLocalisation = { "@" ~ LocalisationIdentifier ~ (!"{" | MapLiteral) } diff --git a/hornbeam_grammar/src/lib.rs b/hornbeam_grammar/src/lib.rs new file mode 100644 index 0000000..f4e2acf --- /dev/null +++ b/hornbeam_grammar/src/lib.rs @@ -0,0 +1,2 @@ +pub mod ast; +mod parser; diff --git a/hornbeam_grammar/src/parser.rs b/hornbeam_grammar/src/parser.rs new file mode 100644 index 0000000..25bd399 --- /dev/null +++ b/hornbeam_grammar/src/parser.rs @@ -0,0 +1,434 @@ +#![allow(non_snake_case)] + +use crate::ast::{ + Block, ComponentElement, DefineExpandSlot, DefineFragment, Expression, HtmlElement, IfBlock, + StringExpr, StringPiece, Template, +}; +use lazy_static::lazy_static; +use pest::error::ErrorVariant; +use pest::pratt_parser::{Assoc, Op, PrattParser}; +use pest::Span; +use pest_consume::{match_nodes, Error as PCError, Parser}; +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::fmt::Debug; +use std::hash::Hash; + +type PCResult = Result>; +type Node<'i> = pest_consume::Node<'i, Rule, ()>; + +#[derive(Parser)] +#[grammar = "hornbeam.pest"] +struct HornbeamParser; + +fn error(msg: &str, span: Span) -> PCError { + PCError::new_from_span( + ErrorVariant::CustomError { + message: msg.to_owned(), + }, + span, + ) +} + +lazy_static! { + static ref PRATT_PARSER: PrattParser = PrattParser::new() + .op(Op::infix(Rule::band, Assoc::Left) | Op::infix(Rule::bor, Assoc::Left)) + .op(Op::infix(Rule::equals, Assoc::Left)) + .op(Op::infix(Rule::add, Assoc::Left) | Op::infix(Rule::sub, Assoc::Left)) + .op(Op::infix(Rule::mul, Assoc::Left) | Op::infix(Rule::div, Assoc::Left)) + .op(Op::infix(Rule::pow, Assoc::Right)) + .op(Op::postfix(Rule::unwrap) + | Op::postfix(Rule::FieldLookup) + | Op::postfix(Rule::MethodCall)) + .op(Op::prefix(Rule::negation)); +} + +#[pest_consume::parser] +impl HornbeamParser { + fn Hornbeam(input: Node) -> PCResult