Initial work on the parser for the Hornbeam grammar

This commit is contained in:
Olivier 'reivilibre' 2023-02-26 00:08:41 +00:00
commit 0e3b3ea96d
28 changed files with 1607 additions and 0 deletions

2
.envrc Normal file
View File

@ -0,0 +1,2 @@
use nix

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.idea

386
Cargo.lock generated Normal file
View File

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

17
Cargo.toml Normal file
View File

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

16
README.md Normal file
View File

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

9
hornbeam/Cargo.toml Normal file
View File

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

14
hornbeam/src/lib.rs Normal file
View File

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

View File

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

142
hornbeam_grammar/src/ast.rs Normal file
View File

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

View File

@ -0,0 +1,16 @@
TestComponent
:main
div
''
This isn't half bad... ${1 + 1}
''
if 1 + 1 = 2
div
span
''
woohoo
''
:header
''
Example 01!
''

View File

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

View File

@ -0,0 +1,2 @@
pub mod ast;
mod parser;

View File

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

View File

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

View File

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

View File

@ -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: []

View File

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

View File

@ -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: []

View File

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

View File

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

View File

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

View File

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

View File

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

8
hornbeam_ir/Cargo.toml Normal file
View File

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

14
hornbeam_ir/src/lib.rs Normal file
View File

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

View File

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

View File

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

47
shell.nix Normal file
View File

@ -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/''
];
}