Initial work on the parser for the Hornbeam grammar
This commit is contained in:
commit
0e3b3ea96d
|
@ -0,0 +1 @@
|
|||
/.idea
|
|
@ -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",
|
||||
]
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -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" }
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"] }
|
|
@ -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<Block<'a>>,
|
||||
}
|
||||
|
||||
#[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<Block<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct ComponentElement<'a> {
|
||||
pub name: Cow<'a, str>,
|
||||
pub slots: BTreeMap<Cow<'a, str>, Vec<Block<'a>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct IfBlock<'a> {
|
||||
pub condition: Expression<'a>,
|
||||
pub blocks: Vec<Block<'a>>,
|
||||
pub else_blocks: Vec<Block<'a>>,
|
||||
}
|
||||
|
||||
#[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<Block<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct StringExpr<'a> {
|
||||
pub pieces: Vec<StringPiece<'a>>,
|
||||
}
|
||||
|
||||
#[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<Cow<'a, str>, Expression<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub enum Expression<'a> {
|
||||
// Arithmetic Operators
|
||||
Add {
|
||||
left: Box<Expression<'a>>,
|
||||
right: Box<Expression<'a>>,
|
||||
},
|
||||
Sub {
|
||||
left: Box<Expression<'a>>,
|
||||
right: Box<Expression<'a>>,
|
||||
},
|
||||
Mul {
|
||||
left: Box<Expression<'a>>,
|
||||
right: Box<Expression<'a>>,
|
||||
},
|
||||
Div {
|
||||
left: Box<Expression<'a>>,
|
||||
right: Box<Expression<'a>>,
|
||||
},
|
||||
Negate {
|
||||
sub: Box<Expression<'a>>,
|
||||
},
|
||||
|
||||
// Boolean Operators
|
||||
BAnd {
|
||||
left: Box<Expression<'a>>,
|
||||
right: Box<Expression<'a>>,
|
||||
},
|
||||
BOr {
|
||||
left: Box<Expression<'a>>,
|
||||
right: Box<Expression<'a>>,
|
||||
},
|
||||
BNot {
|
||||
sub: Box<Expression<'a>>,
|
||||
},
|
||||
|
||||
// Comparators
|
||||
Equals {
|
||||
left: Box<Expression<'a>>,
|
||||
right: Box<Expression<'a>>,
|
||||
},
|
||||
|
||||
// Other Operators
|
||||
ListAdd {
|
||||
left: Box<Expression<'a>>,
|
||||
right: Box<Expression<'a>>,
|
||||
},
|
||||
|
||||
// Literals
|
||||
List {
|
||||
elements: Vec<Expression<'a>>,
|
||||
},
|
||||
IntLiteral {
|
||||
val: i64,
|
||||
},
|
||||
StringExpr(StringExpr<'a>),
|
||||
|
||||
// Relatives
|
||||
FieldLookup {
|
||||
obj: Box<Expression<'a>>,
|
||||
ident: Cow<'a, str>,
|
||||
},
|
||||
MethodCall {
|
||||
obj: Box<Expression<'a>>,
|
||||
ident: Cow<'a, str>,
|
||||
args: Vec<Expression<'a>>,
|
||||
},
|
||||
|
||||
// Other Primaries
|
||||
Variable {
|
||||
name: Cow<'a, str>,
|
||||
},
|
||||
FunctionCall {
|
||||
name: Cow<'a, str>,
|
||||
args: Vec<Expression<'a>>,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
TestComponent
|
||||
:main
|
||||
div
|
||||
''
|
||||
This isn't half bad... ${1 + 1}
|
||||
''
|
||||
if 1 + 1 = 2
|
||||
div
|
||||
span
|
||||
''
|
||||
woohoo
|
||||
''
|
||||
:header
|
||||
''
|
||||
Example 01!
|
||||
''
|
|
@ -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) }
|
|
@ -0,0 +1,2 @@
|
|||
pub mod ast;
|
||||
mod parser;
|
|
@ -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<T> = Result<T, PCError<Rule>>;
|
||||
type Node<'i> = pest_consume::Node<'i, Rule, ()>;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar = "hornbeam.pest"]
|
||||
struct HornbeamParser;
|
||||
|
||||
fn error<R: Copy + Debug + Hash + Ord>(msg: &str, span: Span) -> PCError<R> {
|
||||
PCError::new_from_span(
|
||||
ErrorVariant::CustomError {
|
||||
message: msg.to_owned(),
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref PRATT_PARSER: PrattParser<Rule> = 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<Template> {
|
||||
let blocks = HornbeamParser::helper_blocks(input.into_children())?;
|
||||
Ok(Template { blocks })
|
||||
}
|
||||
|
||||
fn Element(input: Node) -> PCResult<Block> {
|
||||
Ok(match_nodes!(input.into_children();
|
||||
[ElementName(name)] => {
|
||||
if name.chars().next().unwrap().is_ascii_lowercase() {
|
||||
Block::HtmlElement(HtmlElement {
|
||||
name: name,
|
||||
children: Vec::new()
|
||||
})
|
||||
} else {
|
||||
Block::ComponentElement(ComponentElement {
|
||||
name: name,
|
||||
slots: BTreeMap::new()
|
||||
})
|
||||
}
|
||||
},
|
||||
[ElementName(name), SupplySlot(mut supply_slots)..] => {
|
||||
if name.chars().next().unwrap().is_ascii_lowercase() {
|
||||
let slot_node = supply_slots.next().unwrap();
|
||||
|
||||
return Err(error("You can't supply slots to HTML elements.", slot_node.2));
|
||||
} else {
|
||||
let mut slots = BTreeMap::new();
|
||||
for (slot_name, slot_content_blocks, _slot_span) in supply_slots {
|
||||
slots.insert(Cow::from(slot_name), slot_content_blocks);
|
||||
}
|
||||
|
||||
Block::ComponentElement(ComponentElement {
|
||||
name: name,
|
||||
slots,
|
||||
})
|
||||
}
|
||||
},
|
||||
[ElementName(name), blocks..] => {
|
||||
if name.chars().next().unwrap().is_ascii_lowercase() {
|
||||
Block::HtmlElement(HtmlElement {
|
||||
name: name,
|
||||
children: HornbeamParser::helper_blocks(blocks)?
|
||||
})
|
||||
} else {
|
||||
let mut slots = BTreeMap::new();
|
||||
slots.insert(Cow::from("main"), HornbeamParser::helper_blocks(blocks)?);
|
||||
Block::ComponentElement(ComponentElement {
|
||||
name: name,
|
||||
slots,
|
||||
})
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fn DefineFragment(input: Node) -> PCResult<Block> {
|
||||
Ok(match_nodes!(input.into_children();
|
||||
[Identifier(name), blocks..] => {
|
||||
Block::DefineFragment(DefineFragment {
|
||||
name, blocks: HornbeamParser::helper_blocks(blocks)?
|
||||
})
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fn DefineExpandSlot(input: Node) -> PCResult<Block> {
|
||||
let (optional, name) = match_nodes!(input.into_children();
|
||||
[SlotOptional(_), Identifier(name)] => {
|
||||
(true, name)
|
||||
},
|
||||
[Identifier(name)] => {
|
||||
(false, name)
|
||||
}
|
||||
);
|
||||
Ok(Block::DefineExpandSlot(DefineExpandSlot { name, optional }))
|
||||
}
|
||||
|
||||
fn SlotOptional(_input: Node) -> PCResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ElementName(input: Node) -> PCResult<Cow<str>> {
|
||||
Ok(Cow::from(input.as_str()))
|
||||
}
|
||||
|
||||
fn SupplySlot(input: Node) -> PCResult<(Cow<str>, Vec<Block>, Span)> {
|
||||
let in_span = input.as_span();
|
||||
match_nodes!(input.into_children();
|
||||
[Identifier(ident), blocks..] => {
|
||||
let blocks = HornbeamParser::helper_blocks(blocks)?;
|
||||
Ok((ident, blocks, in_span))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn Identifier(input: Node) -> PCResult<Cow<str>> {
|
||||
Ok(Cow::from(input.as_str()))
|
||||
}
|
||||
fn LocalisationIdentifier(input: Node) -> PCResult<Cow<str>> {
|
||||
Ok(Cow::from(input.as_str()))
|
||||
}
|
||||
|
||||
fn Text(input: Node) -> PCResult<Block> {
|
||||
Ok(Block::Text(HornbeamParser::String(
|
||||
input.into_children().single()?,
|
||||
)?))
|
||||
}
|
||||
|
||||
fn String(input: Node) -> PCResult<StringExpr> {
|
||||
let mut pieces = Vec::new();
|
||||
for node in input.into_children() {
|
||||
match node.as_rule() {
|
||||
Rule::SEscape => pieces.push(StringPiece::Literal(HornbeamParser::SEscape(node)?)),
|
||||
Rule::SInterpol => pieces.push(StringPiece::Interpolation(HornbeamParser::Expr(
|
||||
node.into_children().single()?,
|
||||
)?)),
|
||||
Rule::SingleStringContent
|
||||
| Rule::DoubleStringContent
|
||||
| Rule::BlockStringContent => {
|
||||
pieces.push(StringPiece::Literal(Cow::from(node.as_str())));
|
||||
}
|
||||
Rule::BlockStringNewline => {
|
||||
pieces.push(StringPiece::Literal(Cow::from("\n")));
|
||||
}
|
||||
Rule::ParameterisedLocalisation => {
|
||||
let (trans_key, parameters) = match_nodes!(node.into_children();
|
||||
[LocalisationIdentifier(name), MapLiteral(map_literal)] => (name, map_literal),
|
||||
[LocalisationIdentifier(name)] => (name, BTreeMap::new()),
|
||||
);
|
||||
pieces.push(StringPiece::Localise {
|
||||
trans_key,
|
||||
parameters,
|
||||
});
|
||||
}
|
||||
other => unimplemented!("Unknown string piece {other:?}"),
|
||||
}
|
||||
}
|
||||
Ok(StringExpr { pieces })
|
||||
}
|
||||
|
||||
fn KVPair(input: Node) -> PCResult<(Cow<str>, Expression)> {
|
||||
Ok(match_nodes!(input.into_children();
|
||||
[Identifier(key), Expr(value)] => (key, value),
|
||||
))
|
||||
}
|
||||
|
||||
fn MapLiteral(input: Node) -> PCResult<BTreeMap<Cow<str>, Expression>> {
|
||||
Ok(match_nodes!(input.into_children();
|
||||
[KVPair(kv_pair)..] => kv_pair.collect()
|
||||
))
|
||||
}
|
||||
|
||||
fn SEscape(input: Node) -> PCResult<Cow<str>> {
|
||||
let esc = input.as_str();
|
||||
Ok(match esc {
|
||||
"\\\\" | "\\'" | "\\\"" | "\\$" => Cow::from(&esc[1..2]),
|
||||
other => unimplemented!("Unimplemented escape sequence {other:?}! This is a bug."),
|
||||
})
|
||||
}
|
||||
|
||||
fn Variable(input: Node) -> PCResult<Expression> {
|
||||
let name = Cow::from(input.into_children().single()?.as_str());
|
||||
Ok(Expression::Variable { name })
|
||||
}
|
||||
|
||||
fn Expr(input: Node) -> PCResult<Expression> {
|
||||
PRATT_PARSER
|
||||
.map_primary(|primary| Ok(match primary.as_rule() {
|
||||
Rule::IntLiteral => Expression::IntLiteral { val: primary.as_str().parse().map_err(|e| error(&format!("can't parse int: {e:?}"), primary.as_span()))? },
|
||||
Rule::String => Expression::StringExpr(HornbeamParser::String(Node::new(primary))?),
|
||||
Rule::Variable => HornbeamParser::Variable(Node::new(primary))?,
|
||||
Rule::FunctionCall => HornbeamParser::FunctionCall(Node::new(primary))?,
|
||||
other => unimplemented!("unimp primary {other:?}!"),
|
||||
}))
|
||||
.map_prefix(|op, rhs| Ok(match op.as_rule() {
|
||||
Rule::negation => Expression::Negate { sub: Box::new(rhs?) },
|
||||
other => unimplemented!("unimp prefix {other:?}!"),
|
||||
}))
|
||||
.map_infix(|lhs, op, rhs| Ok(match op.as_rule() {
|
||||
Rule::add => Expression::Add { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||
Rule::sub => Expression::Sub { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||
Rule::mul => Expression::Mul { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||
Rule::div => Expression::Div { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||
Rule::equals => Expression::Equals { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||
Rule::bor => Expression::BOr { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||
Rule::band => Expression::BAnd { left: Box::new(lhs?), right: Box::new(rhs?) },
|
||||
other => unimplemented!("unimp infix {other:?}!"),
|
||||
}))
|
||||
.map_postfix(|lhs, op| Ok(match op.as_rule() {
|
||||
Rule::unwrap => unimplemented!("unimp unwrap"),
|
||||
Rule::FieldLookup => {
|
||||
let ident = Cow::from(Node::new(op).into_children().single()?.as_str());
|
||||
Expression::FieldLookup { obj: Box::new(lhs?), ident }
|
||||
},
|
||||
Rule::MethodCall => {
|
||||
match_nodes!(Node::new(op).into_children();
|
||||
[Identifier(ident), Expr(args)..] => {
|
||||
Expression::MethodCall { obj: Box::new(lhs?), ident, args: args.collect() }
|
||||
}
|
||||
)
|
||||
}
|
||||
other => unimplemented!("unimp postfix {other:?}!"),
|
||||
}))
|
||||
.parse(input.into_children().into_pairs())
|
||||
}
|
||||
|
||||
fn FunctionCall(input: Node) -> PCResult<Expression> {
|
||||
Ok(match_nodes!(input.into_children();
|
||||
[Identifier(name), Expr(args)..] => {
|
||||
Expression::FunctionCall { name, args: args.collect() }
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fn IfCondition(input: Node) -> PCResult<Expression> {
|
||||
Self::Expr(input.into_children().single()?)
|
||||
}
|
||||
|
||||
fn IfBlock(input: Node) -> PCResult<Block> {
|
||||
let (cond, blocks, else_blocks) = match_nodes!(input.into_children();
|
||||
[IfCondition(cond), blocks.., ElseBlock(else_blocks)] => {
|
||||
(cond, blocks, else_blocks)
|
||||
},
|
||||
[IfCondition(cond), blocks..] => {
|
||||
(cond, blocks, Vec::new())
|
||||
},
|
||||
);
|
||||
|
||||
Ok(Block::IfBlock(IfBlock {
|
||||
condition: cond,
|
||||
blocks: HornbeamParser::helper_blocks(blocks)?,
|
||||
else_blocks,
|
||||
}))
|
||||
}
|
||||
|
||||
fn ElseBlock(input: Node) -> PCResult<Vec<Block>> {
|
||||
HornbeamParser::helper_blocks(input.into_children())
|
||||
}
|
||||
|
||||
#[allow(unused)] // for some reason, the existence of this function is required.
|
||||
fn EOI(_input: Node) -> PCResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HornbeamParser {
|
||||
fn helper_blocks<'a>(input: impl Iterator<Item = Node<'a>>) -> PCResult<Vec<Block<'a>>> {
|
||||
let mut result = Vec::with_capacity(input.size_hint().0);
|
||||
for child in input {
|
||||
if let Some(block) = HornbeamParser::helper_block(child)? {
|
||||
result.push(block);
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn helper_block<'a>(input: Node) -> PCResult<Option<Block>> {
|
||||
Ok(match input.as_rule() {
|
||||
Rule::Element => Some(HornbeamParser::Element(input)?),
|
||||
Rule::Text => Some(HornbeamParser::Text(input)?),
|
||||
Rule::IfBlock => Some(HornbeamParser::IfBlock(input)?),
|
||||
Rule::DefineFragment => Some(HornbeamParser::DefineFragment(input)?),
|
||||
Rule::DefineExpandSlot => Some(HornbeamParser::DefineExpandSlot(input)?),
|
||||
Rule::EOI => None,
|
||||
other => unimplemented!("unimp rule {other:?}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_template(input: &str) -> PCResult<Template> {
|
||||
let res = HornbeamParser::parse(Rule::Hornbeam, input)?;
|
||||
let hornbeam = res.single()?;
|
||||
HornbeamParser::Hornbeam(hornbeam)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use insta::{assert_debug_snapshot, assert_yaml_snapshot};
|
||||
|
||||
#[test]
|
||||
fn simple_parses_correct() {
|
||||
assert_yaml_snapshot!(parse_template(
|
||||
r#"
|
||||
// This is a simple Hornbeam template that just shows a <div>
|
||||
div
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supply_slots_to_components_only() {
|
||||
assert_debug_snapshot!(parse_template(
|
||||
r#"
|
||||
div
|
||||
:someslot
|
||||
"Oops!"
|
||||
"#
|
||||
));
|
||||
|
||||
assert_yaml_snapshot!(parse_template(
|
||||
r#"
|
||||
MyComponent
|
||||
:someslot
|
||||
"That's better!"
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_interpolations_nested() {
|
||||
assert_yaml_snapshot!(parse_template(
|
||||
r#"
|
||||
MyComponent
|
||||
:someslot
|
||||
''
|
||||
${"abc" + "def${ 1 + 1 }"}
|
||||
Not too bad now.
|
||||
''
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_blocks() {
|
||||
assert_yaml_snapshot!(parse_template(
|
||||
r#"
|
||||
if 10 / 2 == 5 or $point.x == 42 // div for a div?
|
||||
div
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
|
||||
assert_yaml_snapshot!(parse_template(
|
||||
r#"
|
||||
if 1 + 1 == 2
|
||||
div
|
||||
"Phew, safe!"
|
||||
else if 1 + 2 - 1 == 3 // not too far off, I suppose
|
||||
Warning
|
||||
"Not quite, but fairly close. What kind of world is this?"
|
||||
else // peculiar.
|
||||
"Not even close, eh?"
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragments_and_slots() {
|
||||
assert_yaml_snapshot!(parse_template(
|
||||
r#"
|
||||
div
|
||||
fragment BobbyDazzler
|
||||
span
|
||||
"Wow!"
|
||||
fragment MainPart
|
||||
slot :main
|
||||
div
|
||||
optional slot :footer
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn localisation() {
|
||||
assert_yaml_snapshot!(parse_template(
|
||||
r#"
|
||||
div
|
||||
span
|
||||
@header-wow
|
||||
MainBody
|
||||
''
|
||||
@body-welcome
|
||||
@body-msg{count = $messages.len()}
|
||||
''
|
||||
Footer
|
||||
@footer-copyright{year = today().year}
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
source: hornbeam_grammar/src/parser.rs
|
||||
expression: "parse_template(r#\"\ndiv\n fragment BobbyDazzler\n span\n \"Wow!\"\n fragment MainPart\n slot :main\n div\n optional slot :footer\n \"#).unwrap()"
|
||||
---
|
||||
blocks:
|
||||
- HtmlElement:
|
||||
name: div
|
||||
children:
|
||||
- DefineFragment:
|
||||
name: BobbyDazzler
|
||||
blocks:
|
||||
- HtmlElement:
|
||||
name: span
|
||||
children:
|
||||
- Text:
|
||||
pieces:
|
||||
- Literal: Wow!
|
||||
- DefineFragment:
|
||||
name: MainPart
|
||||
blocks:
|
||||
- DefineExpandSlot:
|
||||
name: main
|
||||
optional: false
|
||||
- HtmlElement:
|
||||
name: div
|
||||
children:
|
||||
- DefineExpandSlot:
|
||||
name: footer
|
||||
optional: true
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
source: hornbeam_grammar/src/parser.rs
|
||||
expression: "parse_template(r#\"\nif 1 + 1 == 2\n div\n \"Phew, safe!\"\nelse if 1 + 2 - 1 == 3 // not too far off, I suppose\n Warning\n \"Not quite, but fairly close. What kind of world is this?\"\nelse // peculiar.\n \"Not even close, eh?\"\n \"#).unwrap()"
|
||||
---
|
||||
blocks:
|
||||
- IfBlock:
|
||||
condition:
|
||||
Equals:
|
||||
left:
|
||||
Add:
|
||||
left:
|
||||
IntLiteral:
|
||||
val: 1
|
||||
right:
|
||||
IntLiteral:
|
||||
val: 1
|
||||
right:
|
||||
IntLiteral:
|
||||
val: 2
|
||||
blocks:
|
||||
- HtmlElement:
|
||||
name: div
|
||||
children:
|
||||
- Text:
|
||||
pieces:
|
||||
- Literal: "Phew, safe!"
|
||||
else_blocks:
|
||||
- IfBlock:
|
||||
condition:
|
||||
Equals:
|
||||
left:
|
||||
Sub:
|
||||
left:
|
||||
Add:
|
||||
left:
|
||||
IntLiteral:
|
||||
val: 1
|
||||
right:
|
||||
IntLiteral:
|
||||
val: 2
|
||||
right:
|
||||
IntLiteral:
|
||||
val: 1
|
||||
right:
|
||||
IntLiteral:
|
||||
val: 3
|
||||
blocks:
|
||||
- ComponentElement:
|
||||
name: Warning
|
||||
slots:
|
||||
main:
|
||||
- Text:
|
||||
pieces:
|
||||
- Literal: "Not quite, but fairly close. What kind of world is this?"
|
||||
else_blocks:
|
||||
- Text:
|
||||
pieces:
|
||||
- Literal: "Not even close, eh?"
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
source: hornbeam_grammar/src/parser.rs
|
||||
expression: "parse_template(r#\"\nif 10 / 2 == 5 or $point.x == 42 // div for a div?\n div\n \"#).unwrap()"
|
||||
---
|
||||
blocks:
|
||||
- IfBlock:
|
||||
condition:
|
||||
BOr:
|
||||
left:
|
||||
Equals:
|
||||
left:
|
||||
Div:
|
||||
left:
|
||||
IntLiteral:
|
||||
val: 10
|
||||
right:
|
||||
IntLiteral:
|
||||
val: 2
|
||||
right:
|
||||
IntLiteral:
|
||||
val: 5
|
||||
right:
|
||||
Equals:
|
||||
left:
|
||||
FieldLookup:
|
||||
obj:
|
||||
Variable:
|
||||
name: point
|
||||
ident: x
|
||||
right:
|
||||
IntLiteral:
|
||||
val: 42
|
||||
blocks:
|
||||
- HtmlElement:
|
||||
name: div
|
||||
children: []
|
||||
else_blocks: []
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
source: hornbeam_grammar/src/parser.rs
|
||||
expression: "parse_template(r#\"\ndiv\n span\n @header-wow\n MainBody\n ''\n @body-welcome\n @body-msg{count = $messages.len()}\n ''\n Footer\n @footer-copyright{year = today().year}\n \"#).unwrap()"
|
||||
---
|
||||
blocks:
|
||||
- HtmlElement:
|
||||
name: div
|
||||
children:
|
||||
- HtmlElement:
|
||||
name: span
|
||||
children:
|
||||
- Text:
|
||||
pieces:
|
||||
- Localise:
|
||||
trans_key: header-wow
|
||||
parameters: {}
|
||||
- ComponentElement:
|
||||
name: MainBody
|
||||
slots:
|
||||
main:
|
||||
- Text:
|
||||
pieces:
|
||||
- Localise:
|
||||
trans_key: body-welcome
|
||||
parameters: {}
|
||||
- Literal: "\n"
|
||||
- Localise:
|
||||
trans_key: body-msg
|
||||
parameters:
|
||||
count:
|
||||
MethodCall:
|
||||
obj:
|
||||
Variable:
|
||||
name: messages
|
||||
ident: len
|
||||
args: []
|
||||
- ComponentElement:
|
||||
name: Footer
|
||||
slots:
|
||||
main:
|
||||
- Text:
|
||||
pieces:
|
||||
- Localise:
|
||||
trans_key: footer-copyright
|
||||
parameters:
|
||||
year:
|
||||
FieldLookup:
|
||||
obj:
|
||||
FunctionCall:
|
||||
name: today
|
||||
args: []
|
||||
ident: year
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
source: hornbeam_grammar/src/parser.rs
|
||||
expression: "parse_template(r#\"\n// This is a simple Hornbeam template that just shows a <div>\ndiv\n \"#).unwrap()"
|
||||
---
|
||||
blocks:
|
||||
- HtmlElement:
|
||||
name: div
|
||||
children: []
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
source: hornbeam_grammar/src/parser.rs
|
||||
expression: "parse_template(r#\"\nMyComponent\n :someslot\n ''\n ${\"abc\" + \"def${ 1 + 1 }\"}\n Not too bad now.\n ''\n \"#).unwrap()"
|
||||
---
|
||||
blocks:
|
||||
- ComponentElement:
|
||||
name: MyComponent
|
||||
slots:
|
||||
someslot:
|
||||
- Text:
|
||||
pieces:
|
||||
- Interpolation:
|
||||
Add:
|
||||
left:
|
||||
StringExpr:
|
||||
pieces:
|
||||
- Literal: abc
|
||||
right:
|
||||
StringExpr:
|
||||
pieces:
|
||||
- Literal: def
|
||||
- Interpolation:
|
||||
Add:
|
||||
left:
|
||||
IntLiteral:
|
||||
val: 1
|
||||
right:
|
||||
IntLiteral:
|
||||
val: 1
|
||||
- Literal: "\n"
|
||||
- Literal: Not too bad now.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
source: hornbeam_grammar/src/parser.rs
|
||||
expression: "parse_template(r#\"\nMyComponent\n :someslot\n \"That's better!\"\n \"#).unwrap()"
|
||||
---
|
||||
blocks:
|
||||
- ComponentElement:
|
||||
name: MyComponent
|
||||
slots:
|
||||
someslot:
|
||||
- Text:
|
||||
pieces:
|
||||
- Literal: "That's better!"
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
source: hornbeam_grammar/src/parser.rs
|
||||
expression: "parse_template(r#\"\ndiv\n :someslot\n \"Oops!\"\n \"#)"
|
||||
---
|
||||
Err(
|
||||
Error {
|
||||
variant: CustomError {
|
||||
message: "You can't supply slots to HTML elements.",
|
||||
},
|
||||
location: Span(
|
||||
(
|
||||
9,
|
||||
43,
|
||||
),
|
||||
),
|
||||
line_col: Span(
|
||||
(
|
||||
3,
|
||||
5,
|
||||
),
|
||||
(
|
||||
5,
|
||||
9,
|
||||
),
|
||||
),
|
||||
path: None,
|
||||
line: " :someslot",
|
||||
continued_line: Some(
|
||||
" ",
|
||||
),
|
||||
},
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "hornbeam_interpreter"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "hornbeam_ir"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "hornbeam_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
let
|
||||
# We may need some packages from nixpkgs-unstable
|
||||
#unstable = import <nixpkgs-unstable> {};
|
||||
|
||||
rust-toolchain = pkgs.symlinkJoin {
|
||||
name = "rust-toolchain";
|
||||
paths = [pkgs.rustc pkgs.cargo pkgs.rustfmt pkgs.rustPlatform.rustcSrc];
|
||||
};
|
||||
in
|
||||
|
||||
pkgs.mkShell {
|
||||
|
||||
buildInputs = [
|
||||
rust-toolchain
|
||||
|
||||
pkgs.pkg-config
|
||||
|
||||
#pkgs.libclang # ??
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.openssl
|
||||
];
|
||||
|
||||
# LIBCLANG_PATH="${pkgs.llvmPackages_latest.libclang.lib}/lib";
|
||||
|
||||
# Cargo culted:
|
||||
# Add to rustc search path
|
||||
# RUSTFLAGS = (builtins.map (a: ''-L ${a}/lib'') [
|
||||
# ]);
|
||||
# Add to bindgen search path
|
||||
BINDGEN_EXTRA_CLANG_ARGS =
|
||||
# Includes with normal include path
|
||||
(builtins.map (a: ''-I"${a}/include"'') [
|
||||
# pkgs.glibc.dev
|
||||
])
|
||||
# Includes with special directory paths
|
||||
++ [
|
||||
# ''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"''
|
||||
#''-I"${pkgs.glib.dev}/include/glib-2.0"''
|
||||
#''-I${pkgs.glib.out}/lib/glib-2.0/include/''
|
||||
];
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue