Compare commits

..

No commits in common. "main" and "hornbeam_macros@0.0.2" have entirely different histories.

68 changed files with 972 additions and 3928 deletions

2
.envrc
View File

@ -1,2 +1,2 @@
use flake --impure
use nix

2
.gitignore vendored
View File

@ -1,3 +1 @@
/.idea
/.devenv
/.direnv

607
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,10 @@ members = [
"hornbeam_interpreter",
"hornbeam_macros",
"hornbeam",
"demo_hornbeam_project", "formbeam", "formbeam_derive",
"demo_hornbeam_project",
]
[workspace.dependencies]
bevy_reflect = "0.14.0"
# Enable optimisation for testing helpers
[profile.dev.package.insta]

View File

@ -15,17 +15,3 @@ WIP. Will be: a lightweight and nimble HTML templating engine, designed for comp
Currently under the AGPL 3 or later, but this is relatively likely to be changed at a later date.
See `LICENCE.txt`.
## Development
### Releasing
This is the command used to cut a release:
```shell-commands
cargo ws publish patch --all --force '*'
```
* `patch` could be `major` or `minor` instead.
* `--force '*'` means all packages are bumped, even though they have no changes. This keeps the version numbers in sync.
* `--all` means `demo_hornbeam_project` will be bumped even though it is not published.

View File

@ -1,22 +1,16 @@
[package]
name = "demo_hornbeam_project"
version = "0.0.5"
version = "0.0.2"
edition = "2021"
private = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.8.4"
bevy_reflect.workspace = true
axum = "0.6.9"
eyre = "0.6.8"
color-eyre = "0.6.2"
tokio = { version = "1.25.0", features = ["full"] }
tracing = "0.1.37"
hornbeam = { version = "0.0.5", path = "../hornbeam", features = ["formbeam"] }
hornbeam = { version = "0.0.2", path = "../hornbeam" }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
async-trait = "0.1.81"
formbeam = { version = "0.0.5", path = "../formbeam" }
formbeam_derive = { version = "0.0.5", path = "../formbeam_derive" }
serde = { version = "1.0.204", features = ["derive"] }

View File

@ -1,23 +1,14 @@
use axum::extract::rejection::FormRejection;
use axum::extract::{self, Path};
use axum::extract::Path;
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use eyre::Context;
use formbeam::traits::FormValidation;
use formbeam::FormPartial;
use hornbeam::{
initialise_template_manager, make_template_manager, render_template_string, ReflectedForm,
};
use hornbeam::{initialise_template_manager, make_template_manager, render_template_string};
use std::net::SocketAddr;
use tokio::net::TcpListener;
use tracing::debug;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use formbeam_derive::Form;
make_template_manager! {
static ref TEMPLATING = {
default_locale: "en",
@ -37,16 +28,13 @@ async fn main() -> eyre::Result<()> {
initialise_template_manager!(TEMPLATING);
let app = Router::new()
.route("/:lang/hello/:name", get(say_hello))
.route("/:lang/formdemo", get(form_demo));
let app = Router::new().route("/:lang/hello/:name", get(say_hello));
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
let listener = TcpListener::bind(addr)
.await
.context("Failed to listen on :8080")?;
debug!("Listening on http://{}", addr);
axum::serve(listener, app).await?;
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
Ok(())
}
@ -72,38 +60,3 @@ impl IntoResponse for Rendered {
}
}
}
#[derive(Debug, Form)]
pub struct MyForm {
#[form(min_chars(2), max_chars(10), regex(r"^[^_:]+$"))]
name: String,
#[form(email)]
email: Option<String>,
}
async fn form_demo(
Path(lang): Path<String>,
form_in: Result<extract::Form<MyFormRaw>, FormRejection>,
) -> impl IntoResponse {
let (form, valid_submission) = match form_in {
Ok(extract::Form(form_raw)) => {
let validation = form_raw.validate().await.unwrap();
if validation.is_valid() {
let submitted = form_raw.form().unwrap();
let MyForm { name, email } = submitted;
(
Default::default(),
Some(format!("hello {name} ({email:?})")),
)
} else {
(ReflectedForm::new(form_raw, validation), None)
}
}
Err(_) => (Default::default(), None),
};
Rendered(render_template_string!(TEMPLATING, form_demo, lang, {
form, valid_submission
}))
}

View File

@ -1,6 +0,0 @@
if $errors.len() != 0
"errors on this field (or 'form-wide' section):"
ul
for $e in $errors
li
"${$e.error_code()}"

View File

@ -1,19 +0,0 @@
"validators on this field:"
ul
for $fv in $form.info.field_validators($name)
li
match $fv
MinLength($l) =>
"minimum length: $l"
MaxLength($l) =>
"maximum length: $l"
Required =>
"this field is required"
Email =>
"e-mail address"
Regex($pat) =>
"matches regex: $pat"
Custom($cust) =>
"(custom) $cust"
_ =>
"(other)"

View File

@ -1,48 +0,0 @@
html
head
title
"Hello!"
body
h1
"Form demo!"
match $valid_submission
Some($vs) =>
"great, you managed to submit a form! $vs"
br
"You can send another one if you want :)."
None =>
"Why don't you try submitting a form?"
hr
form {method="GET", action=""}
ShowErrors {errors=$form.errors.form_wide}
b
"Name"
input {type="text", name="name", value=$form.raw.name.unwrap_or("")}
br
ShowValidators {$form, name="name"}
br
ShowErrors {errors=$form.errors.name}
hr
b
"E-mail address"
input {type="email", name="email", value=$form.raw.email.unwrap_or("")}
br
ShowValidators {$form, name="email"}
br
ShowErrors {errors=$form.errors.email}
hr
button {type="submit"}
"Submit!"

291
flake.lock generated
View File

@ -1,291 +0,0 @@
{
"nodes": {
"devenv": {
"inputs": {
"flake-compat": "flake-compat",
"nix": "nix",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1688058187,
"narHash": "sha256-ipDcc7qrucpJ0+0eYNlwnE+ISTcq4m03qW+CWUshRXI=",
"owner": "cachix",
"repo": "devenv",
"rev": "c8778e3dc30eb9043e218aaa3861d42d4992de77",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "v0.6.3",
"repo": "devenv",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1752907304,
"narHash": "sha256-rSw0b/ahoZebcp+AZG7uoScB5Q59TYEE5Kx8k0pZp9E=",
"owner": "nix-community",
"repo": "fenix",
"rev": "e91719882d0e4366202cc9058eb21df74c0bdb92",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"lowdown-src": {
"flake": false,
"locked": {
"lastModified": 1633514407,
"narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
"owner": "kristapsdz",
"repo": "lowdown",
"rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
"type": "github"
},
"original": {
"owner": "kristapsdz",
"repo": "lowdown",
"type": "github"
}
},
"nix": {
"inputs": {
"lowdown-src": "lowdown-src",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1676545802,
"narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=",
"owner": "domenkozar",
"repo": "nix",
"rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "relaxed-flakes",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1678875422,
"narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1678872516,
"narHash": "sha256-/E1YwtMtFAu2KUQKV/1+KFuReYPANM2Rzehk84VxVoc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9b8e5abb18324c7fe9f07cb100c3cd4a29cda8b8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1752620740,
"narHash": "sha256-f3pO+9lg66mV7IMmmIqG4PL3223TYMlnlw+pnpelbss=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "32a4e87942101f1c9f9865e04dc3ddb175f5f32e",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-25.05",
"type": "indirect"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-utils": "flake-utils",
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1686050334,
"narHash": "sha256-R0mczWjDzBpIvM3XXhO908X5e2CQqjyh/gFbwZk/7/Q=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "6881eb2ae5d8a3516e34714e7a90d9d95914c4dc",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"fenix": "fenix",
"nixpkgs": "nixpkgs_2",
"utils": "utils"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1752817855,
"narHash": "sha256-YnG3d44oX+g2ooUsNWT+Ii24w6T+b0dj86k0HkIFUj4=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "330c4ed11c4e1eef0999a2cd629703a601da1436",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,84 +0,0 @@
{
description = "Hornbeam";
inputs = {
utils.url = "github:numtide/flake-utils";
# Current Rust in nixpkgs is too old unfortunately — let's use the Fenix overlay's packages...
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs.url = "nixpkgs/nixos-25.05";
devenv.url = "github:cachix/devenv/v0.6.3";
};
outputs = inputs @ { self, nixpkgs, utils, fenix, devenv }:
utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages."${system}";
fenixRustToolchain =
fenix.packages."${system}".stable.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
"rust-analyzer"
];
in rec {
# `nix develop`
devShell = devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
{
# Configure packages to install.
# Search for package names at https://search.nixos.org/packages?channel=unstable
packages = [
fenixRustToolchain
pkgs.gcc
# Snapshot testing
pkgs.cargo-insta
# Releasing a full workspace of packages
pkgs.cargo-workspaces
# Macro debugging
pkgs.cargo-expand
pkgs.grass-sass
pkgs.entr
# TODO Future pkgs.mdbook
pkgs.pkg-config
];
env = {
# Needed for bindgen when binding to avahi
LIBCLANG_PATH="${pkgs.llvmPackages_latest.libclang.lib}/lib";
# Sometimes useful for reference.
RUST_SRC_PATH = "${fenixRustToolchain}/lib/rustlib/src/rust/library";
# 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"''
];
};
}
];
};
});
}

View File

@ -1,12 +0,0 @@
[package]
name = "formbeam"
description = "Form system for the Hornbeam template engine"
license = "AGPL-3.0-or-later"
version = "0.0.5"
edition = "2021"
[dependencies]
async-trait = "0.1.81"
regex = "1.10.5"
bevy_reflect.workspace = true
static_assertions = "1.1.0"

View File

@ -1,81 +0,0 @@
use std::collections::BTreeMap;
use std::fmt::{Debug, Display};
use bevy_reflect::{FromReflect, Reflect, TypePath};
use static_assertions::assert_impl_all;
#[derive(Clone, Reflect)]
// This makes the struct opaque to the reflection engine, meaning it will
// be cloned absolutely instead of being converted to a DynamicEnum.
// However it won't implement Enum. I still think that's preferable, so `error_code()` will work etc.`
#[reflect_value]
pub enum FieldError {
Missing,
TooShort {
current: u32,
min_length: u32,
max_length: u32,
unit: FieldUnit,
},
TooLong {
current: u32,
min_length: u32,
max_length: u32,
unit: FieldUnit,
},
Custom {
code: String,
description: String,
values: BTreeMap<String, String>,
},
}
impl FieldError {
pub fn error_code(&self) -> &str {
match self {
FieldError::Missing => "missing",
FieldError::TooShort {
unit: FieldUnit::Characters,
..
} => "too_short_chars",
FieldError::TooLong {
unit: FieldUnit::Characters,
..
} => "too_long_chars",
FieldError::Custom { code, .. } => code,
}
}
}
#[derive(Copy, Clone, Reflect)]
pub enum FieldUnit {
Characters,
}
impl Display for FieldUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FieldUnit::Characters => write!(f, "characters"),
}
}
}
impl Debug for FieldError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FieldError::Missing => write!(f, "The field is missing"),
FieldError::TooShort { current, min_length, max_length, unit } => write!(f, "The field is too short ({current}); it should be between {min_length} and {max_length} {unit}."),
FieldError::TooLong { current, min_length, max_length, unit } => write!(f, "The field is too long ({current}); it should be between {min_length} and {max_length} {unit}."),
FieldError::Custom {
code,
description,
values,
} => write!(f, "[{code}] {description} {values:?}"),
}
}
}
pub type FieldErrors = Vec<FieldError>;
assert_impl_all!(FieldErrors: Reflect, FromReflect, TypePath);

View File

@ -1,9 +0,0 @@
pub mod errors;
pub mod traits;
pub mod validators;
pub use errors::{FieldError, FieldErrors};
pub use traits::{
FieldInfo, FieldValidator, FieldValidatorInfo, Form, FormPartial, FormPartialInfo,
FormValidator,
};

View File

@ -1,98 +0,0 @@
use async_trait::async_trait;
use crate::errors::FieldErrors;
#[async_trait]
pub trait FormValidator<F: Form, C: Send + 'static> {
type Error: Send + 'static;
async fn validate(&self, form: &mut F, context: &mut C) -> Result<(), Self::Error>;
}
#[async_trait]
pub trait FieldValidator<C: Send + 'static> {
type Error: Send + 'static;
async fn validate(
&self,
value: &str,
errors: &mut FieldErrors,
context: &mut C,
) -> Result<(), Self::Error>;
}
/// The type of a realised, fully validated and populated, form.
pub trait Form: Sized + 'static {
type Partial: FormPartial<Form = Self>;
}
/// The type of a partially populated and as-yet-unvalidated form.
/// Structs for and implementations of this trait can be generated via derive macro.
#[async_trait]
pub trait FormPartial {
type Form: Form<Partial = Self>;
type Validation: FormValidation<Partial = Self>;
type Error: Send + 'static;
/// Converts the partial into a form.
///
/// # Preconditions
///
/// - The form should already have been validated.
///
/// # Errors
///
/// Only structural/type errors will be returned here, with only the name of the field
/// to be returned.
///
/// No other validation is performed here.
fn form(&self) -> Result<Self::Form, &'static str>
where
Self: Sized;
/// Runs all the validators on the form and calculates errors.
///
/// Should only be called once.
///
/// # Errors
///
/// Returns direct errors from validators if one was thrown.
async fn validate(&self) -> Result<Self::Validation, Self::Error>
where
Self: Sized;
const INFO: &'static FormPartialInfo;
fn validator_info(&self) -> &'static FormPartialInfo {
Self::INFO
}
}
/// The result of validation.
pub trait FormValidation {
type Partial: FormPartial<Validation = Self>;
/// Returns true if the form is valid.
fn is_valid(&self) -> bool;
}
pub struct FormPartialInfo {
pub form_validators: &'static [&'static str],
pub fields: &'static [FieldInfo],
}
pub struct FieldInfo {
pub name: &'static str,
pub validators: &'static [FieldValidatorInfo],
}
pub enum FieldValidatorInfo {
MinLength(u32),
MaxLength(u32),
MinValue(i64),
MaxValue(i64),
Required,
Email,
Regex(&'static str),
Custom(&'static str),
}

View File

@ -1,102 +0,0 @@
use std::{collections::BTreeMap, convert::Infallible, sync::LazyLock};
use async_trait::async_trait;
pub use regex::Regex;
use crate::{
errors::{FieldError, FieldErrors, FieldUnit},
traits::FieldValidator,
};
/// Constrains the minimum and maximum length, counted in `char`s, of a text input.
pub struct LengthInChars {
pub min_length: u32,
pub max_length: u32,
}
#[async_trait]
impl FieldValidator<()> for LengthInChars {
type Error = Infallible;
async fn validate(
&self,
value: &str,
errors: &mut FieldErrors,
_context: &mut (),
) -> Result<(), Self::Error> {
// Clamp to u32::MAX
let length = value.chars().count().min(u32::MAX as usize) as u32;
if length < self.min_length {
errors.push(FieldError::TooShort {
current: length,
min_length: self.min_length,
max_length: self.max_length,
unit: FieldUnit::Characters,
});
}
if length > self.max_length {
errors.push(FieldError::TooLong {
current: length,
min_length: self.min_length,
max_length: self.max_length,
unit: FieldUnit::Characters,
});
}
Ok(())
}
}
/// Regex for an e-mail address according to
/// <https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address>
///
/// This is likely representative of what browsers accept, but note that
/// it intentionally deviates from RFC 5322 which is not considered practical.
static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$").expect("email regex")
});
pub struct Email;
#[async_trait]
impl FieldValidator<()> for Email {
type Error = Infallible;
async fn validate(
&self,
value: &str,
errors: &mut FieldErrors,
_context: &mut (),
) -> Result<(), Self::Error> {
if !EMAIL_REGEX.is_match_at(value, 0) {
errors.push(FieldError::Custom {
code: "invalid_email".to_owned(),
description: "Not a valid e-mail address".to_owned(),
values: BTreeMap::new(),
})
}
Ok(())
}
}
#[async_trait]
impl FieldValidator<()> for Regex {
type Error = Infallible;
async fn validate(
&self,
value: &str,
errors: &mut FieldErrors,
_context: &mut (),
) -> Result<(), Self::Error> {
if !self.is_match_at(value, 0) {
errors.push(FieldError::Custom {
code: "regex_unmatched".to_owned(),
description: "Field did not match the intended pattern".to_owned(),
values: BTreeMap::new(),
})
}
Ok(())
}
}

View File

@ -1,14 +0,0 @@
[package]
name = "formbeam_derive"
description = "Form system for the Hornbeam template engine (derive macros)"
license = "AGPL-3.0-or-later"
version = "0.0.5"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.86"
quote = "1.0.36"
syn = "2.0.72"

View File

@ -1,540 +0,0 @@
use proc_macro2::Ident;
use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote;
use syn::parenthesized;
use syn::DeriveInput;
use syn::Field;
use syn::LitInt;
use syn::LitStr;
use syn::Type;
use syn::Visibility;
pub fn derive_form(input: DeriveInput) -> TokenStream {
let fields = match input.data {
syn::Data::Struct(strukt) => match strukt.fields {
syn::Fields::Named(named) => named,
syn::Fields::Unnamed(_) => panic!("cannot derive Form for a tuple struct"),
syn::Fields::Unit => panic!("cannot derive Form for a unit struct"),
},
syn::Data::Enum(_) => panic!("cannot derive Form for an enum"),
syn::Data::Union(_) => panic!("cannot derive Form for a union"),
};
let mut fields_vec = Vec::new();
for pair in fields.named.into_pairs() {
let field = pair.into_value();
let f_info = parse_field_attrs(&field);
fields_vec.push((field, f_info));
}
let partial_ident = format_ident!("{}Raw", input.ident);
let validation_ident = format_ident!("{}Validation", input.ident);
let partial_struct = write_form_partial_struct(&partial_ident, &fields_vec, &input.vis);
let validation_struct =
write_form_validation_struct(&partial_ident, &validation_ident, &fields_vec, &input.vis);
let partial_impl =
write_form_partial_impl(&input.ident, &partial_ident, &validation_ident, &fields_vec);
quote!(
#partial_struct
#validation_struct
#partial_impl
)
}
fn identify_base_type(unprefixed: &Ident) -> FieldType {
match unprefixed.to_string().as_str() {
"u8" | "i8" | "u16" | "i16" | "u32" | "i32" | "u64" | "i64" | "u128" | "i128" | "f32"
| "f64" => FieldType::Numeric,
"String" => FieldType::String,
"bool" => FieldType::Boolean,
other => {
panic!("unsupported field type: `{other}`")
}
}
}
fn identify_type(ty: &Type) -> (FieldType, bool) {
let (ftype, needed) = match ty {
syn::Type::Array(_) => panic!("unsupported field type: array"),
syn::Type::BareFn(_) => panic!("unsupported field type: bare fn"),
syn::Type::Group(_) => panic!("unsupported field type: Group"),
syn::Type::ImplTrait(_) => panic!("unsupported field type: impl Trait"),
syn::Type::Infer(_) => panic!("unsupported field type: Infer"),
syn::Type::Macro(_) => panic!("unsupported field type: macro"),
syn::Type::Never(_) => panic!("unsupported field type: Never"),
syn::Type::Paren(_) => panic!("unsupported field type: parenthesised"),
syn::Type::Path(type_path) => {
let path = &type_path.path;
if path.segments.len() != 1 {
panic!("unsupported field type syntax (can't deal with multiple path segments)");
}
let segment = path.segments.get(0).unwrap();
if segment.ident == "Option" {
match &segment.arguments {
syn::PathArguments::None => {
panic!("unsupported field type: Option without args")
}
syn::PathArguments::AngleBracketed(bracks) => {
if bracks.args.len() != 1 {
panic!("unsupported field type: Option with other than 1 arg");
}
let arg = bracks.args.get(0).unwrap();
match arg {
syn::GenericArgument::Lifetime(_) => {
panic!("unsupported field type with lifetime")
}
syn::GenericArgument::Type(ty) => {
let (base_type, check_needed) = identify_type(ty);
if base_type == FieldType::Boolean {
panic!(
"Option<bool> isn't supported (does it even make sense?)"
);
}
if !check_needed {
panic!("unsupported field type with nested Options or something like that");
}
(base_type, false)
}
syn::GenericArgument::Const(_) => {
panic!("unsupported field type with const")
}
syn::GenericArgument::AssocType(_) => {
panic!("unsupported field type with associated type")
}
syn::GenericArgument::AssocConst(_) => {
panic!("unsupported field type with associated const")
}
syn::GenericArgument::Constraint(_) => {
panic!("unsupported field type with constraint")
}
_ => panic!("other generic argument unsupported?"),
}
}
syn::PathArguments::Parenthesized(_) => {
panic!("unsupported field type with ( )s")
}
}
} else {
match &segment.arguments {
syn::PathArguments::None => {
let base_type = identify_base_type(&segment.ident);
let needed = base_type != FieldType::Boolean;
(base_type, needed)
}
syn::PathArguments::AngleBracketed(_) => {
panic!("unsupported field type: non-Option with args")
}
syn::PathArguments::Parenthesized(_) => {
panic!("unsupported field type with ( )s")
}
}
}
}
syn::Type::Ptr(_) => panic!("unsupported field type: pointer"),
syn::Type::Reference(_) => panic!("unsupported field type: ref"),
syn::Type::Slice(_) => panic!("unsupported field type: slice"),
syn::Type::TraitObject(_) => panic!("unsupported field type: trait object"),
syn::Type::Tuple(_) => panic!("unsupported field type: tuple"),
syn::Type::Verbatim(_) => panic!("verbatim?"),
_ => panic!("unsupported (unknown) field type"),
};
(ftype, needed)
}
fn parse_field_attrs(f: &Field) -> FieldInfo {
let (ftype, needed) = identify_type(&f.ty);
let mut out = FieldInfo {
ftype,
needed,
need_nonempty: true,
min_chars: None,
max_chars: None,
email: false,
regex: None,
};
for attr in &f.attrs {
if !attr.meta.path().is_ident("form") {
continue;
}
attr.parse_nested_meta(|meta| {
// #[form(allow_empty)]
if meta.path.is_ident("allow_empty") {
out.need_nonempty = false;
return Ok(());
}
// #[form(email)]
if meta.path.is_ident("email") {
if out.email {
panic!("duplicate email annotation");
}
out.email = true;
return Ok(());
}
// #[form(min_chars(2))]
if meta.path.is_ident("min_chars") {
let content;
parenthesized!(content in meta.input);
let lit: LitInt = content.parse()?;
let n: u32 = lit.base10_parse()?;
if out.min_chars.is_some() {
panic!("duplicate min_chars annotation");
}
out.min_chars = Some(n);
return Ok(());
}
// #[form(max_chars(2))]
if meta.path.is_ident("max_chars") {
let content;
parenthesized!(content in meta.input);
let lit: LitInt = content.parse()?;
let n: u32 = lit.base10_parse()?;
if out.max_chars.is_some() {
panic!("duplicate max_chars annotation");
}
out.max_chars = Some(n);
return Ok(());
}
// #[form(regex("\A[0-9]+\Z"))]
if meta.path.is_ident("regex") {
let content;
parenthesized!(content in meta.input);
let lit: LitStr = content.parse()?;
if out.regex.is_some() {
panic!("duplicate regex annotation");
}
out.regex = Some(lit.value());
return Ok(());
}
panic!(
"unrecognised field attribute: {}",
meta.path
.get_ident()
.map(|i| format!("`{i}`"))
.unwrap_or_else(|| "<???>".to_owned())
)
})
.unwrap();
}
out
}
fn write_form_partial_struct(
partial_ident: &Ident,
fields: &[(Field, FieldInfo)],
vis: &Visibility,
) -> TokenStream {
let field_idents: Vec<&Ident> = fields
.iter()
.map(|(f, _)| f.ident.as_ref().unwrap())
.collect();
quote!(
#[allow(missing_docs)]
#[derive(Default, Debug, ::bevy_reflect::Reflect, ::serde::Deserialize)]
#vis struct #partial_ident {
#(#field_idents: Option<String>),*
}
)
}
fn write_form_validation_struct(
partial_ident: &Ident,
validation_ident: &Ident,
fields: &[(Field, FieldInfo)],
vis: &Visibility,
) -> TokenStream {
let field_idents: Vec<&Ident> = fields
.iter()
.map(|(f, _)| f.ident.as_ref().unwrap())
.collect();
quote!(
#[allow(missing_docs)]
#[derive(Default, Debug, ::bevy_reflect::Reflect)]
#vis struct #validation_ident {
form_wide: ::formbeam::FieldErrors,
#(#field_idents: ::formbeam::FieldErrors),*
}
impl ::formbeam::traits::FormValidation for #validation_ident {
type Partial = #partial_ident;
fn is_valid(&self) -> bool {
self.form_wide.is_empty()
#(&& self.#field_idents.is_empty())*
}
}
)
}
fn write_form_partial_impl(
form_ident: &Ident,
partial_ident: &Ident,
validation_ident: &Ident,
fields: &[(Field, FieldInfo)],
) -> TokenStream {
let validate_body = write_partial_impl_validate(validation_ident, fields);
let form_method_body = write_partial_impl_form_method(form_ident, fields);
let form_info = write_form_info(fields);
quote!(
impl ::formbeam::Form for #form_ident {
type Partial = #partial_ident;
}
#[::async_trait::async_trait]
impl ::formbeam::FormPartial for #partial_ident {
type Form = #form_ident;
type Validation = #validation_ident;
type Error = ::core::convert::Infallible; // TODO
const INFO: &'static ::formbeam::FormPartialInfo = &#form_info;
async fn validate(&self) -> Result<Self::Validation, Self::Error> {
use ::formbeam::FieldValidator;
#validate_body
}
fn form(&self) -> Result<Self::Form, &'static str> {
#form_method_body
}
}
)
}
fn write_partial_impl_validate(
validation_ident: &Ident,
fields: &[(Field, FieldInfo)],
) -> TokenStream {
let field_statements = fields.iter().map(write_validate_statement);
quote!(
let mut errors = #validation_ident::default();
#(#field_statements)*
Ok(errors)
)
}
struct FieldInfo {
ftype: FieldType,
/// Whether the field is required to be present in the form.
/// This is true unless the type is `Option<T>`.
needed: bool,
/// Whether the field needs to be non-empty. By default this is true.
/// If `needed` is false and `need_nonempty` is true, then empty fields will be coerced to `None` instead.
/// If both `needed` and `need_nonempty` are false, then empty fields will be `Some("")` o.e.; only absent fields will be `None`.
/// If both `needed` and `need_nonempty` are true, then empty fields will not be accepted
/// and the `Required` validator is added to the field's info (this matches HTML form semantics).
need_nonempty: bool,
min_chars: Option<u32>,
max_chars: Option<u32>,
email: bool,
regex: Option<String>,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
enum FieldType {
Numeric,
String,
/// Boolean indicating the presence and non-emptiness of the field.
Boolean,
}
fn write_form_info(fields: &[(Field, FieldInfo)]) -> TokenStream {
let form_validators: Vec<TokenStream> = Vec::new();
let fields: Vec<TokenStream> = fields
.iter()
.map(|(f, fi)| write_form_info_for_field(f, fi))
.collect();
quote!(
::formbeam::FormPartialInfo {
form_validators: &[
#(#form_validators),*
],
fields: &[
#(#fields),*
]
}
)
}
fn write_form_info_for_field(field: &Field, f_info: &FieldInfo) -> TokenStream {
let name = field.ident.as_ref().unwrap().to_string();
let mut validators = Vec::new();
if f_info.needed && f_info.need_nonempty {
validators.push(quote!(::formbeam::FieldValidatorInfo::Required));
}
if let Some(l) = f_info.min_chars {
validators.push(quote!(::formbeam::FieldValidatorInfo::MinLength(#l)));
}
if let Some(l) = f_info.max_chars {
validators.push(quote!(::formbeam::FieldValidatorInfo::MaxLength(#l)));
}
if f_info.email {
validators.push(quote!(::formbeam::FieldValidatorInfo::Email));
}
if let Some(r) = &f_info.regex {
validators.push(quote!(::formbeam::FieldValidatorInfo::Regex(#r)));
}
quote!(
::formbeam::FieldInfo {
name: #name,
validators: &[#(#validators),*]
}
)
}
fn write_validate_statement((field, f_info): &(Field, FieldInfo)) -> TokenStream {
let f = field.ident.as_ref().unwrap();
let configured_validators = write_validators(field, f_info);
let check_nonempty = if f_info.need_nonempty {
quote!(
if field.is_empty() {
errors.#f.push(::formbeam::FieldError::Missing);
} else {
#(#configured_validators)*
}
)
} else {
quote!(#(#configured_validators)*)
};
let else_not_present = if f_info.needed {
quote!(
errors.#f.push(::formbeam::FieldError::Missing);
)
} else {
TokenStream::new()
};
quote!(
if let Some(field) = &self.#f {
#check_nonempty
} else {
#else_not_present
}
)
}
fn write_validators(field: &Field, f_info: &FieldInfo) -> Vec<TokenStream> {
let f = field.ident.as_ref().unwrap();
let mut out = Vec::new();
if f_info.min_chars.is_some() || f_info.max_chars.is_some() {
let effective_min = f_info.min_chars.unwrap_or(0);
let effective_max = f_info.max_chars.unwrap_or(u32::MAX);
out.push(quote!(
::formbeam::validators::LengthInChars { min_length: #effective_min, max_length: #effective_max }.validate(&field, &mut errors.#f, &mut ()).await?;
));
}
if let Some(regex) = &f_info.regex {
let regex_ident = format_ident!("REGEX_{}", f.to_string().to_uppercase());
out.push(quote!(
static #regex_ident: ::std::sync::LazyLock<::formbeam::validators::Regex> = ::std::sync::LazyLock::new(|| {
::formbeam::validators::Regex::new(#regex).expect("invalid regex")
});
#regex_ident.validate(&field, &mut errors.#f, &mut ()).await?;
));
}
if f_info.email {
out.push(quote!(
::formbeam::validators::Email.validate(&field, &mut errors.#f, &mut ()).await?;
));
}
out
}
fn write_partial_impl_form_method(
form_ident: &Ident,
fields: &[(Field, FieldInfo)],
) -> TokenStream {
let field_idents: Vec<&Ident> = fields
.iter()
.map(|(f, _)| f.ident.as_ref().unwrap())
.collect();
let mut statements = Vec::new();
for (field, field_info) in fields.iter() {
let f = field.ident.as_ref().unwrap();
let none_case = if field_info.needed {
let f_name = f.to_string();
quote!(return Err(#f_name);)
} else if field_info.ftype == FieldType::Boolean {
quote!(false)
} else {
quote!(None)
};
let some_case = {
let converter = converter_from_raw_to_field(f, field_info);
if field_info.needed || field_info.ftype == FieldType::Boolean {
converter
} else if field_info.need_nonempty {
// For not-needed fields that must not be empty,
// coerce empty fields into `None`
quote!(
if raw.is_empty() {
None
} else {
Some({#converter})
}
)
} else {
quote!(Some({#converter}))
}
};
statements.push(quote!(
let #f = match &self.#f {
Some(raw) => {
#some_case
},
None => {
#none_case
}
};
))
}
quote!(
#(#statements)*
Ok(#form_ident { #(#field_idents),* })
)
}
fn converter_from_raw_to_field(f: &Ident, field_info: &FieldInfo) -> TokenStream {
let f_name = f.to_string();
match field_info.ftype {
FieldType::Numeric => quote!(
raw.parse().map_err(|_| #f_name)?
),
FieldType::String => quote!(raw.to_owned()),
FieldType::Boolean => quote!(!raw.is_empty()),
}
}

View File

@ -1,10 +0,0 @@
use proc_macro::TokenStream;
use syn::parse_macro_input;
mod derive_form;
#[proc_macro_derive(Form, attributes(form))]
pub fn my_proc_macro(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
derive_form::derive_form(input).into()
}

View File

@ -2,23 +2,22 @@
name = "hornbeam"
description = "Hornbeam template engine (high-level crate for use in applications)"
license = "AGPL-3.0-or-later"
version = "0.0.5"
version = "0.0.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
hornbeam_interpreter = { version = "0.0.5", path = "../hornbeam_interpreter" }
hornbeam_interpreter = { version = "0.0.2", path = "../hornbeam_interpreter" }
arc-swap = "1.6.0"
notify = "5.1.0"
lazy_static = "1.4.0"
tokio = { version = "1.26.0", optional = true }
axum = { version = "0.8.4", optional = true }
axum = { version = "0.6.10", optional = true }
[features]
default = ["interpreted", "hot_reload"]
interpreted = []
hot_reload = ["tokio", "axum"]
formbeam = ["hornbeam_interpreter/formbeam"]

View File

@ -70,18 +70,14 @@ pub fn new_template_manager(
}
}
successful.unwrap_or_else(|| panic!("Could not find Hornbeam templates/translations: tried looking in {try_paths:?} and no HORNBEAM_BASE environment variable set!"))
successful.expect(&format!("Could not find Hornbeam templates/translations: tried looking in {try_paths:?} and no HORNBEAM_BASE environment variable set!"))
};
let template_sys = match load_new_template_system(
let template_sys = load_new_template_system(
default_locale,
&base_dir.join("templates"),
&base_dir.join("translations"),
) {
Ok(v) => v,
Err(err) => {
panic!("Failed to create Hornbeam environment! {err}");
}
};
)
.expect("Failed to create Hornbeam environment!");
let templates = Arc::new(ArcSwap::new(Arc::new(template_sys)));
#[cfg(feature = "hot_reload")]
@ -115,11 +111,6 @@ fn load_new_template_system(
#[cfg(feature = "hot_reload")]
pub fn is_hot_reload_enabled() -> bool {
#[cfg(debug_assertions)]
const DEFAULT_HOT_RELOAD: bool = true;
#[cfg(not(debug_assertions))]
const DEFAULT_HOT_RELOAD: bool = false;
std::env::var("HORNBEAM_HOT")
.map(|env_var| {
if let Ok(i) = env_var.parse::<u32>() {
@ -135,9 +126,9 @@ pub fn is_hot_reload_enabled() -> bool {
return false;
}
eprintln!("Not sure how to interpret HORNBEAM_HOT={env_var:?}, assuming yes.");
true
return true;
})
.unwrap_or(DEFAULT_HOT_RELOAD)
.unwrap_or(true)
}
#[cfg(not(feature = "hot_reload"))]

View File

@ -6,13 +6,11 @@ use axum::{Extension, Router};
use hornbeam_interpreter::localisation::fluent::FluentLocalisationSystem;
use hornbeam_interpreter::LoadedTemplates;
use notify::{Event, EventKind, RecursiveMode, Watcher};
use std::error::Error;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::mpsc::RecvTimeoutError;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::net::TcpListener;
use tokio::sync::oneshot;
pub(crate) fn start_hot_reloader(
@ -58,7 +56,7 @@ pub(crate) fn start_hot_reloader(
let default_locale = default_locale;
loop {
notif_rx.recv().unwrap();
let _ = notif_rx.recv().unwrap();
// Debounce, because editors often make a series of modifications and we don't
// want to reload before it's ready.
@ -102,17 +100,14 @@ pub(crate) fn start_auto_hot_reloader() -> WaiterList {
.layer(Extension(waiter_list.clone()));
let addr = SocketAddr::from(([127, 0, 0, 1], 7015));
eprintln!("Hornbeam Auto Hot Reload: Listening on http://{}", addr);
tokio::spawn(async move {
let result: Result<(), Box<dyn Error>> = async move {
let listener = TcpListener::bind(addr).await?;
eprintln!("Hornbeam Auto Hot Reload: Listening on http://{}", addr);
axum::serve(listener, app).await?;
Ok(())
}
.await;
if let Err(e) = result {
eprintln!("Hornbeam Auto Hot Reload failed: {e}");
if let Err(e) = axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
{
eprintln!("Hornbeam Auto Hot Reload failed: {e:?}");
}
});

View File

@ -17,7 +17,3 @@ pub use interpreted::{
is_hot_reload_enabled, lazy_static, new_template_manager, Params, TemplateError,
TemplateManager,
};
#[cfg(feature = "formbeam")]
#[cfg(feature = "interpreted")]
pub use hornbeam_interpreter::formbeam_integration::ReflectedForm;

View File

@ -2,7 +2,7 @@
name = "hornbeam_grammar"
description = "Grammar for the Hornbeam template language"
license = "AGPL-3.0-or-later"
version = "0.0.5"
version = "0.0.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -4,27 +4,17 @@ use std::collections::BTreeMap;
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct Template {
pub param_defs: Option<Vec<ParameterDefinition>>,
pub blocks: Vec<Block>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ParameterDefinition {
pub name: IStr,
pub loc: Locator,
pub default: Option<Expression>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum Block {
HtmlElement(HtmlElement),
ComponentElement(ComponentElement),
SetStatement(SetStatement),
IfBlock(IfBlock),
ForBlock(ForBlock),
MatchBlock(MatchBlock),
Text(StringExpr),
RawUnescapedHtml(StringExpr),
DefineExpandSlot(DefineExpandSlot),
DefineFragment(DefineFragment),
}
@ -35,15 +25,10 @@ pub struct HtmlElement {
pub children: Vec<Block>,
pub classes: Vec<IStr>,
pub dom_id: Option<IStr>,
pub attributes: BTreeMap<IStr, (Expression, ElementAttributeFlags)>,
pub attributes: BTreeMap<IStr, Expression>,
pub loc: Locator,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ElementAttributeFlags {
pub optional: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ComponentElement {
pub name: IStr,
@ -52,13 +37,6 @@ pub struct ComponentElement {
pub loc: Locator,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct SetStatement {
pub binding: Binding,
pub expression: Expression,
pub loc: Locator,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct IfBlock {
pub condition: Expression,
@ -91,11 +69,6 @@ pub enum MatchBinding {
/// `None =>`
UnitVariant { name: IStr },
/// `$var`
/// (A fallback case)
/// TODO this is not implemented in the grammar yet, but is used in generated rules
Variable { name: IStr },
/// `_ =>`
Ignore,
}
@ -177,14 +150,6 @@ pub enum Expression {
left: Box<Expression>,
right: Box<Expression>,
},
LessThan {
left: Box<Expression>,
right: Box<Expression>,
},
LessThanOrEquals {
left: Box<Expression>,
right: Box<Expression>,
},
// Other Operators
ListAdd {
@ -199,8 +164,6 @@ pub enum Expression {
IntLiteral {
val: i64,
},
BoolLiteral(bool),
NoneLiteral,
StringExpr(StringExpr),
// Relatives
@ -226,9 +189,4 @@ pub enum Expression {
args: Vec<Expression>,
loc: Locator,
},
Unwrap {
obj: Box<Expression>,
loc: Locator,
},
}

View File

@ -1,21 +1,5 @@
//
Hornbeam = { SOI ~ wsnl* ~ PreambleList? ~ wsnl* ~ HornbeamBlockList ~ ws* ~ EOI }
// Will eventually expand to other types of preamble content
PreambleList = {
// accept `declare` keyword. We expect `PEEK_ALL` to be empty, but future definitions might change this.
PEEK_ALL ~ "declare" ~ lineEnd ~
// then accept the first definition. We must have at least one definition.
// We accept the new indentation level with the `PUSH` here
PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ ParameterDefinition ~
// Now accept any number of extra definitions at the same indentation level.
(PEEK_ALL ~ ParameterDefinition)* ~
// Drop the indentation when exiting the preamble block
DROP
}
HornbeamBlockList = { BlockContent* }
Hornbeam = { SOI ~ wsnl* ~ BlockContent* ~ ws* ~ EOI }
NewBlock = _{
PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ BlockContent ~
@ -26,23 +10,14 @@ BlockContent = _{
Element |
IfBlock |
Text |
RawUnescapedHtml |
DefineExpandSlot |
SetStatement |
ForBlock |
MatchBlock |
DefineFragment
}
// `param $x`
// `param $x = 1 + 1`
// TODO: type annotation: `param $x: int`
ParameterDefinition = {
"param" ~ ws+ ~ "$" ~ Identifier ~ (ws* ~ "=" ~ ws* ~ Expr)? ~ lineEnd
}
Element = {
ElementName ~ cssClass* ~ domId? ~ (ws+ ~ AttrMapLiteral)? ~ lineEnd ~ (NewBlock | NewSlotBlock)?
ElementName ~ cssClass* ~ domId? ~ (ws+ ~ MapLiteral)? ~ lineEnd ~ (NewBlock | NewSlotBlock)?
}
cssClass = _{
@ -59,14 +34,6 @@ Text = {
String ~ lineEnd
}
RawUnescapedHtml = {
"raw" ~ ws+ ~ String ~ lineEnd
}
SetStatement = {
"set" ~ ws+ ~ Binding ~ ws+ ~ "=" ~ ws+ ~ Expr ~ lineEnd
}
IfBlock = {
"if" ~ ws+ ~ IfCondition ~ lineEnd ~ NewBlock ~
ElseBlock?
@ -158,21 +125,20 @@ lineEnd = _{ ws_nocnl* ~ nlOrEoi ~ (ws_nocnl* ~ NEWLINE)* ~ (ws_nocnl* ~ &EOI)?
// was: lineEnd = _{ ws_nocnl* ~ nlOrEoi ~ (!EOI ~ ws* ~ nlOrEoi)* }
SingleStringContent = { (!("'" | "\\" | "$") ~ ANY)+ }
singleString = _{ !("''") ~ "'" ~ (SingleStringContent | SEscape | SSimpleVarInterpol | SInterpol)* ~ "'" }
singleString = _{ !("''") ~ "'" ~ (SingleStringContent | SEscape | SInterpol)* ~ "'" }
DoubleStringContent = { (!("\"" | "\\" | "$") ~ ANY)+ }
doubleString = _{ "\"" ~ (DoubleStringContent | SEscape | SSimpleVarInterpol | SInterpol)* ~ "\"" }
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 | SSimpleVarInterpol | SInterpol | ParameterisedLocalisation | BlockStringNewline)* ~ blockStringEnd }
blockString = _{ blockStringStart ~ (BlockStringContent | SEscape | SInterpol | ParameterisedLocalisation | BlockStringNewline)* ~ blockStringEnd }
String = { blockString | singleString | doubleString | ParameterisedLocalisation }
SEscape = { "\\" ~ ("\\" | "'" | "\"" | "$" | "@") }
SSimpleVarInterpol = { "$" ~ Identifier }
SInterpol = { "${" ~ ws* ~ Expr ~ ws* ~ "}" }
@ -186,7 +152,7 @@ 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 | notEquals | greaterThanOrEqual | lessThanOrEqual | greaterThan | lessThan | band | bor }
infix = _{ add | sub | mul | div | pow | modulo | listAdd | equals | band | bor }
add = { "+" }
sub = { "-" }
mul = { "*" }
@ -195,11 +161,6 @@ pow = { "^" }
modulo = { "%" }
listAdd = { "++" }
equals = { "==" }
notEquals = { "!=" }
greaterThan = { ">" }
lessThan = { "<" }
greaterThanOrEqual = { ">=" }
lessThanOrEqual = { "<=" }
// BUG: these should have forced whitespace at the start!
// (lookbehind wouldn't be the worst feature in the world for a parser grammar!)
@ -213,43 +174,32 @@ bnot = { "not" ~ ws+ }
// POSTFIX
postfix = _{ unwrap | MethodCall | FieldLookup | Indexing }
unwrap = { "!" } // Not sure I'm convinced about this one, but we can think about it.
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 | NoneLiteral | TrueLiteral | FalseLiteral) }
Term = _{ (IntLiteral | bracketedTerm | FunctionCall | ListLiteral | MapLiteral | String | Variable) }
bracketedTerm = _{ "(" ~ Expr ~ ")" }
NoneLiteral = { "None" }
TrueLiteral = { "true" }
FalseLiteral = { "false" }
IntLiteral = @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) }
// `-` is important in identifiers for `kebab-case` HTML element attributes
// We could consider splitting this out into its own kind of identifier but let's not bother now.
Identifier = { (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* }
Identifier = { (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
commaSeparatedExprs = _{ wsnl* ~ (Expr ~ wsnl* ~ ("," ~ wsnl* ~ Expr ~ wsnl*)* ~ ("," ~ wsnl*)?)? }
FunctionCall = { Identifier ~ "(" ~ commaSeparatedExprs ~ ")" }
Variable = { "$" ~ Identifier }
ListLiteral = { "[" ~ commaSeparatedExprs ~ "]" }
// Basic key-value pairs forming a map literal {a = .., b = ..}.
KVPair = { Identifier ~ wsnl* ~ "=" ~ wsnl* ~ Expr }
KVarShorthand = { Variable }
commaSeparatedKVPairs = _{ wsnl* ~ ((KVPair | KVarShorthand) ~ wsnl* ~ ("," ~ wsnl* ~ (KVPair | KVarShorthand) ~ wsnl*)* ~ ("," ~ wsnl*)?)? }
MapLiteral = { "{" ~ commaSeparatedKVPairs ~ "}" }
// Element attribute key-value pairs
// These can have extra features not found in the basic map literals
AttrKVPairOptionalMarker = { "?" }
AttrKVPair = { Identifier ~ AttrKVPairOptionalMarker? ~ wsnl* ~ "=" ~ wsnl* ~ Expr }
AttrKVarShorthand = { Variable ~ AttrKVPairOptionalMarker? }
commaSeparatedAttrKVPairs = _{ wsnl* ~ ((AttrKVPair | AttrKVarShorthand) ~ wsnl* ~ ("," ~ wsnl* ~ (AttrKVPair | AttrKVarShorthand) ~ wsnl*)* ~ ("," ~ wsnl*)?)? }
AttrMapLiteral = { "{" ~ commaSeparatedAttrKVPairs ~ "}" }
// As in a let binding or for binding.

View File

@ -1,9 +1,8 @@
#![allow(non_snake_case, clippy::result_large_err)]
#![allow(non_snake_case)]
use crate::ast::{
Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, ElementAttributeFlags,
Expression, ForBlock, HtmlElement, IfBlock, MatchBinding, MatchBlock, ParameterDefinition,
SetStatement, StringExpr, StringPiece, Template,
Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, Expression, ForBlock,
HtmlElement, IfBlock, MatchBinding, MatchBlock, StringExpr, StringPiece, Template,
};
use crate::{intern, IStr, Locator};
use lazy_static::lazy_static;
@ -43,12 +42,7 @@ fn error<R: Copy + Debug + Hash + Ord>(msg: &str, span: Span) -> PCError<R> {
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::infix(Rule::notEquals, Assoc::Left)
| Op::infix(Rule::lessThan, Assoc::Left)
| Op::infix(Rule::greaterThan, Assoc::Left)
| Op::infix(Rule::greaterThanOrEqual, Assoc::Left)
| Op::infix(Rule::lessThanOrEqual, 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))
@ -61,51 +55,8 @@ lazy_static! {
#[pest_consume::parser]
impl HornbeamParser {
fn Hornbeam(input: Node) -> PCResult<Template> {
Ok(match_nodes!(input.into_children();
[PreambleList(param_defs), HornbeamBlockList(blocks), EOI(_)] => {
Template { param_defs, blocks }
},
[HornbeamBlockList(blocks), EOI(_)] => {
Template { param_defs: None, blocks }
}
))
}
fn PreambleList(input: Node) -> PCResult<Option<Vec<ParameterDefinition>>> {
let param_defs: Vec<ParameterDefinition> = match_nodes!(input.into_children();
[ParameterDefinition(param_defs)..] => param_defs.collect()
);
// Templates with no parameter definitions are treated as untyped.
// (Mostly for backwards compat)
Ok(if param_defs.is_empty() {
None
} else {
Some(param_defs)
})
}
fn ParameterDefinition(input: Node) -> PCResult<ParameterDefinition> {
let loc = nodeloc(&input);
Ok(match_nodes!(input.into_children();
[Identifier(name), Expr(default)] => {
ParameterDefinition {
name,
loc,
default: Some(default),
}
},
[Identifier(name)] => {
ParameterDefinition {
name,
loc,
default: None,
}
},
))
}
fn HornbeamBlockList(input: Node) -> PCResult<Vec<Block>> {
HornbeamParser::helper_blocks(input.into_children())
let blocks = HornbeamParser::helper_blocks(input.into_children())?;
Ok(Template { blocks })
}
fn Element(input: Node) -> PCResult<Block> {
@ -121,7 +72,7 @@ impl HornbeamParser {
let mut supply_slots = Vec::new();
let mut blocks = Vec::new();
for next in children {
while let Some(next) = children.next() {
match next.as_rule() {
Rule::CssClass => {
classes.push(HornbeamParser::CssClass(next)?);
@ -132,8 +83,8 @@ impl HornbeamParser {
Rule::SupplySlot => {
supply_slots.push(HornbeamParser::SupplySlot(next)?);
}
Rule::AttrMapLiteral => {
attributes = HornbeamParser::AttrMapLiteral(next)?;
Rule::MapLiteral => {
attributes = HornbeamParser::MapLiteral(next)?;
}
_ => {
if let Some(block) = HornbeamParser::helper_block(next)? {
@ -157,20 +108,6 @@ impl HornbeamParser {
loc,
})
} else {
let attributes: PCResult<BTreeMap<IStr, Expression>> = attributes
.into_iter()
.map(|(k, (v, attrs))| {
if attrs.optional {
Err(error(
"Optional arguments to components are currently unsupported",
span,
))
} else {
Ok((k, v))
}
})
.collect();
let attributes = attributes?;
if !supply_slots.is_empty() {
let mut slots = BTreeMap::new();
for (slot_name, slot_content_blocks, _slot_span) in supply_slots {
@ -260,26 +197,11 @@ impl HornbeamParser {
)?))
}
fn RawUnescapedHtml(input: Node) -> PCResult<Block> {
Ok(Block::RawUnescapedHtml(HornbeamParser::String(
input.into_children().single()?,
)?))
}
fn String(input: Node) -> PCResult<StringExpr> {
let mut pieces = Vec::new();
for node in input.into_children() {
let loc = nodeloc(&node);
match node.as_rule() {
Rule::SEscape => pieces.push(StringPiece::Literal(HornbeamParser::SEscape(node)?)),
Rule::SSimpleVarInterpol => {
let var_identifier =
HornbeamParser::Identifier(node.into_children().single()?)?;
pieces.push(StringPiece::Interpolation(Expression::Variable {
name: var_identifier,
loc,
}));
}
Rule::SInterpol => pieces.push(StringPiece::Interpolation(HornbeamParser::Expr(
node.into_children().single()?,
)?)),
@ -336,52 +258,6 @@ impl HornbeamParser {
.collect()
}
fn AttrKVPair(input: Node) -> PCResult<(IStr, (Expression, ElementAttributeFlags))> {
Ok(match_nodes!(input.into_children();
[Identifier(key), Expr(value)] => (key, (value, ElementAttributeFlags {
optional: false
})),
[Identifier(key), AttrKVPairOptionalMarker(_), Expr(value)] => (key, (value, ElementAttributeFlags {
optional: true
})),
))
}
fn AttrKVPairOptionalMarker(input: Node) -> PCResult<()> {
Ok(())
}
fn AttrKVarShorthand(input: Node) -> PCResult<(IStr, (Expression, ElementAttributeFlags))> {
let (var_expr, optional) = match_nodes!(input.into_children();
[Variable(var_expr), AttrKVPairOptionalMarker(_)] => {
(var_expr, true)
},
[Variable(var_expr)] => {
(var_expr, false)
},
);
if let Expression::Variable { name, .. } = &var_expr {
Ok((name.clone(), (var_expr, ElementAttributeFlags { optional })))
} else {
unreachable!("Variable should also be returned from Variable");
}
}
fn AttrMapLiteral(
input: Node,
) -> PCResult<BTreeMap<IStr, (Expression, ElementAttributeFlags)>> {
input
.into_children()
.map(|node| match node.as_rule() {
Rule::AttrKVPair => HornbeamParser::AttrKVPair(node),
Rule::AttrKVarShorthand => HornbeamParser::AttrKVarShorthand(node),
other => {
unimplemented!("unexpected {other:?} in AttrMapLiteral");
}
})
.collect()
}
fn SEscape(input: Node) -> PCResult<IStr> {
let esc = input.as_str();
Ok(match esc {
@ -403,9 +279,6 @@ impl HornbeamParser {
let node = Node::new_with_user_data(primary, ud.clone());
Ok(match node.as_rule() {
Rule::IntLiteral => Expression::IntLiteral { val: node.as_str().parse().map_err(|e| error(&format!("can't parse int: {e:?}"), node.as_span()))? },
Rule::NoneLiteral => Expression::NoneLiteral,
Rule::TrueLiteral => Expression::BoolLiteral(true),
Rule::FalseLiteral => Expression::BoolLiteral(false),
Rule::String => Expression::StringExpr(HornbeamParser::String(node)?),
Rule::Variable => HornbeamParser::Variable(node)?,
Rule::FunctionCall => HornbeamParser::FunctionCall(node)?,
@ -421,11 +294,6 @@ impl HornbeamParser {
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::notEquals => Expression::BNot { sub: Box::new(Expression::Equals { left: Box::new(lhs?), right: Box::new(rhs?) }) },
Rule::lessThan => Expression::LessThan { left: Box::new(lhs?), right: Box::new(rhs?) },
Rule::lessThanOrEqual => Expression::LessThanOrEquals { left: Box::new(lhs?), right: Box::new(rhs?) },
Rule::greaterThan => Expression::BNot { sub: Box::new(Expression::LessThanOrEquals { left: Box::new(lhs?), right: Box::new(rhs?) }) },
Rule::greaterThanOrEqual => Expression::BNot { sub: Box::new(Expression::LessThan { 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:?}!"),
@ -434,9 +302,7 @@ impl HornbeamParser {
let node = Node::new_with_user_data(op, ud.clone());
let loc = nodeloc(&node);
Ok(match node.as_rule() {
Rule::unwrap => {
Expression::Unwrap { obj: Box::new(lhs?), loc }
},
Rule::unwrap => unimplemented!("unimp unwrap"),
Rule::FieldLookup => {
let ident = intern(node.into_children().single()?.as_str());
Expression::FieldLookup { obj: Box::new(lhs?), ident, loc }
@ -463,22 +329,6 @@ impl HornbeamParser {
))
}
fn SetStatement(input: Node) -> PCResult<Block> {
let loc = nodeloc(&input);
let (binding, expression) = match_nodes!(input.into_children();
[Binding(binding), Expr(expression)] => {
(binding, expression)
},
);
Ok(Block::SetStatement(SetStatement {
binding,
expression,
loc,
}))
}
fn IfCondition(input: Node) -> PCResult<Expression> {
Self::Expr(input.into_children().single()?)
}
@ -618,12 +468,10 @@ impl HornbeamParser {
Ok(result)
}
fn helper_block(input: Node) -> PCResult<Option<Block>> {
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::RawUnescapedHtml => Some(HornbeamParser::RawUnescapedHtml(input)?),
Rule::SetStatement => Some(HornbeamParser::SetStatement(input)?),
Rule::IfBlock => Some(HornbeamParser::IfBlock(input)?),
Rule::ForBlock => Some(HornbeamParser::ForBlock(input)?),
Rule::MatchBlock => Some(HornbeamParser::MatchBlock(input)?),
@ -664,23 +512,6 @@ div
.unwrap());
}
#[test]
fn param_defs() {
assert_yaml_snapshot!(parse_template(
r#"
declare
// We need form information
param $form
// Also wouldn't hurt to have a default value on this other parameter
param $user = None
"#,
"inp"
)
.unwrap());
}
#[test]
fn supply_slots_to_components_only() {
assert_debug_snapshot!(parse_template(
@ -816,17 +647,4 @@ div
)
.unwrap());
}
#[test]
fn raw() {
assert_yaml_snapshot!(parse_template(
r#"
div
span
raw "<u>wow $x ${$x} @wowage{}</u>"
"#,
"inp"
)
.unwrap());
}
}

View File

@ -2,7 +2,6 @@
source: hornbeam_grammar/src/parser.rs
expression: "parse_template(r#\"\nfor $x in $xs\n for $y in $ys\n \"Woot\"\n empty\n \"no ys\"\nempty\n \"no xs\"\n \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- ForBlock:
binding:
@ -45,3 +44,4 @@ blocks:
filename: inp
line: 2
column: 1

View File

@ -2,7 +2,6 @@
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 \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- HtmlElement:
name: div
@ -65,3 +64,4 @@ blocks:
filename: inp
line: 2
column: 1

View File

@ -2,7 +2,6 @@
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 \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- IfBlock:
condition:
@ -77,3 +76,4 @@ blocks:
filename: inp
line: 2
column: 1

View File

@ -2,7 +2,6 @@
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 \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- IfBlock:
condition:
@ -55,3 +54,4 @@ blocks:
filename: inp
line: 2
column: 1

View File

@ -2,7 +2,6 @@
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 \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- HtmlElement:
name: div
@ -91,3 +90,4 @@ blocks:
filename: inp
line: 2
column: 1

View File

@ -2,7 +2,6 @@
source: hornbeam_grammar/src/parser.rs
expression: "parse_template(r#\"\nif $x\n match $y\n None =>\n \"None\"\n Some($z) =>\n \"Some(${$z})\"\n \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- IfBlock:
condition:
@ -51,3 +50,4 @@ blocks:
filename: inp
line: 2
column: 1

View File

@ -1,18 +0,0 @@
---
source: hornbeam_grammar/src/parser.rs
expression: "parse_template(r#\"\ndeclare\n // We need form information\n param $form\n\n // Also wouldn't hurt to have a default value on this other parameter\n\n param $user = None\n \"#,\n \"inp\").unwrap()"
---
param_defs:
- name: form
loc:
filename: inp
line: 4
column: 5
default: ~
- name: user
loc:
filename: inp
line: 8
column: 5
default: NoneLiteral
blocks: []

View File

@ -1,45 +0,0 @@
---
source: hornbeam_grammar/src/parser.rs
expression: "parse_template(r#\"\ndiv\n span\n raw \"<u>wow $x ${$x} @wowage{}</u>\"\n \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- HtmlElement:
name: div
children:
- HtmlElement:
name: span
children:
- RawUnescapedHtml:
pieces:
- Literal: "<u>wow "
- Interpolation:
Variable:
name: x
loc:
filename: inp
line: 4
column: 21
- Literal: " "
- Interpolation:
Variable:
name: x
loc:
filename: inp
line: 4
column: 26
- Literal: " @wowage{}</u>"
classes: []
dom_id: ~
attributes: {}
loc:
filename: inp
line: 3
column: 5
classes: []
dom_id: ~
attributes: {}
loc:
filename: inp
line: 2
column: 1

View File

@ -2,7 +2,6 @@
source: hornbeam_grammar/src/parser.rs
expression: "parse_template(r#\"\n// This is a simple Hornbeam template that just shows a <div>\ndiv\n \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- HtmlElement:
name: div
@ -14,3 +13,4 @@ blocks:
filename: inp
line: 3
column: 1

View File

@ -2,7 +2,6 @@
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 \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- ComponentElement:
name: MyComponent
@ -35,3 +34,4 @@ blocks:
filename: inp
line: 2
column: 1

View File

@ -2,7 +2,6 @@
source: hornbeam_grammar/src/parser.rs
expression: "parse_template(r#\"\nMyComponent\n :someslot\n \"That's better!\"\n \"#,\n \"inp\").unwrap()"
---
param_defs: ~
blocks:
- ComponentElement:
name: MyComponent
@ -16,3 +15,4 @@ blocks:
filename: inp
line: 2
column: 1

View File

@ -2,19 +2,18 @@
name = "hornbeam_interpreter"
description = "Interpreter for the Hornbeam template language. This is the low-level implementation crate; not advised for direct use in applications."
license = "AGPL-3.0-or-later"
version = "0.0.5"
version = "0.0.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
hornbeam_grammar = { version = "0.0.5", path = "../hornbeam_grammar" }
hornbeam_ir = { version = "0.0.5", path = "../hornbeam_ir" }
hornbeam_grammar = { version = "0.0.2", path = "../hornbeam_grammar" }
hornbeam_ir = { version = "0.0.2", path = "../hornbeam_ir" }
fluent-templates = { version = "0.8.0", optional = true }
bevy_reflect.workspace = true
bevy_reflect = { version = "0.11.0" }
html-escape = "0.2.13"
formbeam = { version = "0.0.5", path = "../formbeam", optional = true }
walkdir = "2.3.2"
@ -26,12 +25,7 @@ itertools = "0.10.5"
pollster = "0.3.0"
tracing = "0.1.37"
percent-encoding = "2.2.0"
[features]
default = ["fluent"]
fluent = ["dep:fluent-templates"]
formbeam = ["dep:formbeam"]
[dev-dependencies]
insta = "1.38.0"
fluent = ["fluent-templates"]

View File

@ -1,4 +1,3 @@
use crate::functions::TemplateAccessibleMethod;
use crate::interface::{LocalisationSystem, OutputSystem};
use crate::InterpreterError;
use async_recursion::async_recursion;
@ -6,7 +5,7 @@ use bevy_reflect::{FromReflect, Reflect, ReflectRef, VariantType};
use fluent_templates::lazy_static::lazy_static;
use hornbeam_grammar::ast::{Binding, MatchBinding};
use hornbeam_grammar::Locator;
use hornbeam_ir::ir::{Expression, Step, StepDef, StringPiece, TemplateFunction};
use hornbeam_ir::ir::{Expression, Step, StepDef, StringPiece};
use itertools::Itertools;
use std::any::TypeId;
use std::borrow::Cow;
@ -28,12 +27,11 @@ pub(crate) struct Scope<'a> {
pub(crate) struct Interpreter<'a, O, LS> {
pub(crate) entrypoint: String,
pub(crate) program: &'a BTreeMap<String, Arc<TemplateFunction>>,
pub(crate) program: &'a BTreeMap<String, Arc<Vec<Step>>>,
pub(crate) output: O,
pub(crate) localisation: Arc<LS>,
pub(crate) locale: String,
pub(crate) scopes: Vec<Scope<'a>>,
pub(crate) methods: &'a BTreeMap<String, TemplateAccessibleMethod>,
}
#[derive(Debug)]
@ -52,13 +50,7 @@ impl Value {
Value::Int(_) => "Int",
Value::Bool(_) => "Bool",
Value::List(_) => "List",
// TODO get rid of unwraps
Value::Reflective(reflective) => reflective
.get_represented_type_info()
.unwrap()
.type_path_table()
.ident()
.unwrap(),
Value::Reflective(reflective) => reflective.type_name(),
})
}
}
@ -67,7 +59,6 @@ lazy_static! {
static ref U8_TYPEID: TypeId = TypeId::of::<u8>();
static ref U16_TYPEID: TypeId = TypeId::of::<u16>();
static ref U32_TYPEID: TypeId = TypeId::of::<u32>();
static ref U64_TYPEID: TypeId = TypeId::of::<u64>();
static ref I8_TYPEID: TypeId = TypeId::of::<i8>();
static ref I16_TYPEID: TypeId = TypeId::of::<i16>();
static ref I32_TYPEID: TypeId = TypeId::of::<i32>();
@ -89,16 +80,6 @@ impl Value {
Value::Int(u16::from_reflect(reflect.deref()).unwrap() as i64)
} else if ti == *U32_TYPEID {
Value::Int(u32::from_reflect(reflect.deref()).unwrap() as i64)
} else if ti == *U64_TYPEID {
// If we can, convert it to a native i64 value
// If we can't (because it's bigger than i64::MAX),
// then we have no choice but to leave it as a reflective value... :/
// TODO I really don't like this but my hands are tied...
let value_u64 = u64::from_reflect(reflect.deref()).unwrap();
match i64::try_from(value_u64) {
Ok(value_i64) => Value::Int(value_i64),
Err(_cant) => Value::Reflective(reflect),
}
} else if ti == *I8_TYPEID {
Value::Int(i8::from_reflect(reflect.deref()).unwrap() as i64)
} else if ti == *I16_TYPEID {
@ -222,7 +203,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
} else {
self.output
.write(text)
.write(&text)
.await
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
}
@ -243,20 +224,6 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
.await
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
}
StepDef::Set {
expression,
binding,
} => {
let expr_evaled = self.evaluate_expression(scope_idx, expression, &step.locator)?;
// Bind the expression
// Unlike the other places where we bind variables, we never unbind this one,
// since `set` is not block-scoped.
// (Ideally we would have slightly more careful scoping rules, but `set` is intentionally being
// added to allow variables to escape their respective blocks...)
let mut binder = Binder::new();
binder.bind(&mut self.scopes[scope_idx].variables, binding, expr_evaled);
}
StepDef::If {
condition,
true_steps,
@ -294,7 +261,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
} else {
for val in list {
let mut binder = Binder::new();
binder.bind(&mut self.scopes[scope_idx].variables, binding, val);
binder.bind(&mut self.scopes[scope_idx].variables, &binding, val);
self.run_steps(scope_idx, body_steps).await?;
binder.unbind(&mut self.scopes[scope_idx].variables);
}
@ -312,7 +279,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
let mut binder = Binder::new();
binder.bind(
&mut self.scopes[scope_idx].variables,
binding,
&binding,
Value::from_reflect(val),
);
self.run_steps(scope_idx, body_steps).await?;
@ -363,10 +330,10 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
},
_ => {
// warn!(
// "trying to `match` non-reflective vs {name} at {}",
// step.locator
// );
warn!(
"trying to `match` non-reflective vs {name} at {}",
step.locator
);
continue;
}
}
@ -416,19 +383,14 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
},
_ => {
// warn!(
// "trying to `match` non-reflective vs {name}(...) at {}",
// step.locator
// );
warn!(
"trying to `match` non-reflective vs {name}(...) at {}",
step.locator
);
continue;
}
}
}
MatchBinding::Variable { name } => binder.bind(
&mut self.scopes[scope_idx].variables,
&Binding::Variable(name.clone()),
matchable_evaled,
),
MatchBinding::Ignore => {
// always matches: no variable to bind, no conditions to check!
}
@ -445,14 +407,6 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
}
StepDef::Call { name, args, slots } => {
let Some(module) = self.program.get(name as &str) else {
return Err(InterpreterError::TypeError {
context: "Call".to_string(),
conflict: format!("no entrypoint for {name:?}."),
location: step.locator.clone(),
});
};
let mut evaled_args = BTreeMap::new();
for (key, expr) in args {
evaled_args.insert(
@ -472,67 +426,23 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
);
}
// TODO check slots
// ...
// check params and evaluate defaults
if let Some(param_defs) = &module.param_defs {
for param_def in param_defs {
if evaled_args.contains_key(param_def.name.as_str()) {
continue;
}
let Some(default_expr) = &param_def.default else {
return Err(InterpreterError::TypeError {
context: "Call".to_string(),
conflict: format!(
"missing required parameter {} for {name:?}.",
param_def.name
),
location: step.locator.clone(),
});
};
// Push a temporary empty scope
// TODO in the future allow evaluation with some scope if desired.
self.scopes.push(Scope {
variables: BTreeMap::new(),
slots: BTreeMap::new(),
});
evaled_args.insert(
String::from(param_def.name.as_str()),
self.evaluate_expression(
scope_idx + 1,
default_expr,
&param_def.locator,
)?,
);
self.scopes.pop();
}
for supplied_param in evaled_args.keys() {
if !param_defs
.iter()
.any(|def| def.name.as_str() == supplied_param)
{
return Err(InterpreterError::TypeError {
context: "Call".to_string(),
conflict: format!(
"provided non-existent parameter {} for {name:?}.",
supplied_param
),
location: Locator::empty(),
});
}
}
}
self.scopes.push(Scope {
variables: evaled_args,
slots: filled_in_slots,
});
let next_scope_idx = self.scopes.len() - 1;
self.run_steps(next_scope_idx, &module.steps).await?;
let steps = if let Some(steps) = self.program.get(name as &str) {
steps
} else {
return Err(InterpreterError::TypeError {
context: "Call".to_string(),
conflict: format!("no entrypoint for {name:?}."),
location: step.locator.clone(),
});
};
self.run_steps(next_scope_idx, steps).await?;
self.scopes.pop();
assert_eq!(self.scopes.len(), next_scope_idx);
@ -545,7 +455,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
return if !optional {
Err(InterpreterError::TypeError {
context: format!("Required slot '{name}' not filled"),
conflict: "slot was left empty.".to_string(),
conflict: format!("slot was left empty."),
location: step.locator.clone(),
})
} else {
@ -570,8 +480,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
) -> Result<Value, InterpreterError<LS::Error, O::Error>> {
match expr {
Expression::Add { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
match (lval, rval) {
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint + rint)),
@ -583,8 +493,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
}
Expression::Sub { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
match (lval, rval) {
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint - rint)),
@ -596,8 +506,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
}
Expression::Mul { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
match (lval, rval) {
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint * rint)),
@ -609,8 +519,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
}
Expression::Div { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
match (lval, rval) {
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint / rint)),
@ -622,7 +532,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
}
Expression::Negate { sub } => {
let sval = self.evaluate_expression(scope_idx, sub, loc)?;
let sval = self.evaluate_expression(scope_idx, &sub, loc)?;
match sval {
Value::Int(sint) => Ok(Value::Int(-sint)),
@ -634,8 +544,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
}
Expression::BAnd { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
match (lval, rval) {
(Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool && rbool)),
@ -647,8 +557,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
}
Expression::BOr { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
match (lval, rval) {
(Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool || rbool)),
@ -660,7 +570,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
}
Expression::BNot { sub } => {
let sval = self.evaluate_expression(scope_idx, sub, loc)?;
let sval = self.evaluate_expression(scope_idx, &sub, loc)?;
match sval {
Value::Bool(sbool) => Ok(Value::Bool(!sbool)),
@ -672,8 +582,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
}
Expression::Equals { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
match (lval, rval) {
(Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool == rbool)),
@ -687,9 +597,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
location: loc.clone(),
})
}
}
(Value::Str(lstr), Value::Str(rstr)) => Ok(Value::Bool(lstr == rstr)),
// TODO List vs List. Not *that* useful but can be occasionally. But it does involve recursion.
},
(lother, rother) => Err(InterpreterError::TypeError {
context: "Equals".to_string(),
conflict: format!("can't test {lother:?} and {rother:?} for equality!"),
@ -697,37 +605,9 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}),
}
}
Expression::LessThan { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
match (lval, rval) {
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Bool(lint < rint)),
(Value::Str(lstr), Value::Str(rstr)) => Ok(Value::Bool(lstr < rstr)),
(lother, rother) => Err(InterpreterError::TypeError {
context: "LessThan".to_string(),
conflict: format!("can't test {lother:?} < {rother:?}!"),
location: loc.clone(),
}),
}
}
Expression::LessThanOrEquals { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
match (lval, rval) {
(Value::Int(lint), Value::Int(rint)) => Ok(Value::Bool(lint <= rint)),
(Value::Str(lstr), Value::Str(rstr)) => Ok(Value::Bool(lstr <= rstr)),
(lother, rother) => Err(InterpreterError::TypeError {
context: "LessThanOrEquals".to_string(),
conflict: format!("can't test {lother:?} <= {rother:?}!"),
location: loc.clone(),
}),
}
}
Expression::ListAdd { left, right } => {
let lval = self.evaluate_expression(scope_idx, left, loc)?;
let rval = self.evaluate_expression(scope_idx, right, loc)?;
let lval = self.evaluate_expression(scope_idx, &left, loc)?;
let rval = self.evaluate_expression(scope_idx, &right, loc)?;
match (lval, rval) {
(Value::List(mut llist), Value::List(rlist)) => {
@ -749,8 +629,6 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
Ok(Value::List(result))
}
Expression::IntLiteral { val } => Ok(Value::Int(*val)),
Expression::BoolLiteral(val) => Ok(Value::Bool(*val)),
Expression::NoneLiteral => Ok(Value::Reflective(Box::new(None::<()>))),
Expression::StringExpr(sexpr) => {
let mut output = String::new();
for piece in &sexpr.pieces {
@ -808,34 +686,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}),
}
}
Expression::MethodCall {
obj,
ident,
args,
loc,
} => {
let Some(method) = self.methods.get(ident.as_str()) else {
return Err(InterpreterError::TypeError {
context: format!("method call to {ident:?}"),
conflict: "No method by that name!".to_string(),
location: loc.clone(),
});
};
let obj_value = self.evaluate_expression(scope_idx, obj, loc)?;
let mut arg_values: Vec<Value> = Vec::with_capacity(args.len());
for arg in args {
arg_values.push(self.evaluate_expression(scope_idx, arg, loc)?);
}
match method.call(obj_value, arg_values) {
Ok(result_val) => Ok(result_val),
Err(method_err) => Err(InterpreterError::TypeError {
context: format!("method call to {ident:?}"),
conflict: format!("method error: {method_err}"),
location: loc.clone(),
}),
}
Expression::MethodCall { .. } => {
unimplemented!()
}
Expression::Variable { name, loc } => {
let locals = &self.scopes[scope_idx].variables;
@ -856,54 +708,6 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
Expression::FunctionCall { .. } => {
unimplemented!()
}
Expression::Unwrap { obj, loc } => {
let obj_value = self.evaluate_expression(scope_idx, obj, loc)?;
match &obj_value {
Value::Reflective(reflective) => match reflective.reflect_ref() {
ReflectRef::Enum(reflenum) => match reflenum.variant_name() {
"Some" => {
if reflenum.field_len() != 1 {
return Err(InterpreterError::TypeError {
context: "unwrap".to_owned(),
conflict: "wrong number of fields in Some".to_owned(),
location: loc.clone(),
});
}
if reflenum.variant_type() != VariantType::Tuple {
return Err(InterpreterError::TypeError {
context: "unwrap".to_owned(),
conflict: "Some is not a tuple variant".to_owned(),
location: loc.clone(),
});
}
Ok(Value::from_reflect(
reflenum.field_at(0).unwrap().clone_value(),
))
}
"None" => Err(InterpreterError::TypeError {
context: "unwrap".to_owned(),
conflict: "tried to unwrap None".to_owned(),
location: loc.clone(),
}),
_other => {
warn!("unnecessary unwrap (!) at {loc}");
Ok(obj_value)
}
},
_other => {
warn!("unnecessary unwrap (!) at {loc}");
Ok(obj_value)
}
},
_other => {
warn!("unnecessary unwrap (!) at {loc}");
Ok(obj_value)
}
}
}
}
}
@ -941,8 +745,9 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
// too dodgy! We might want to allow this in the future, but for now let's not.
Err(InterpreterError::TypeError {
context: "String Interpolation".to_string(),
conflict: "Don't know how to write List[...] as a sensible string output."
.to_string(),
conflict: format!(
"Don't know how to write List[...] as a sensible string output."
),
location: loc.clone(),
})
}
@ -958,7 +763,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
) -> Result<(), InterpreterError<LS::Error, O::Error>> {
match piece {
StringPiece::Literal(lit) => {
output.push_str(lit);
output.push_str(&lit);
Ok(())
}
StringPiece::Interpolation(expr) => {
@ -993,63 +798,16 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
}
pub async fn run(mut self) -> Result<(), InterpreterError<LS::Error, O::Error>> {
let Some(main) = self.program.get(&self.entrypoint) else {
let main = if let Some(main) = self.program.get(&self.entrypoint) {
main
} else {
return Err(InterpreterError::TypeError {
context: format!("No entrypoint called {:?}", self.entrypoint),
conflict: "".to_string(),
location: Locator::empty(),
});
};
// TODO deduplicate with `Call` step
if let Some(param_defs) = &main.param_defs {
for param_def in param_defs {
if self.scopes[0]
.variables
.contains_key(param_def.name.as_str())
{
continue;
}
let Some(default_expr) = &param_def.default else {
return Err(InterpreterError::TypeError {
context: "main entrypoint".to_string(),
conflict: format!(
"missing required parameter {} for entrypoint {:?}.",
param_def.name, self.entrypoint
),
location: Locator::empty(),
});
};
// Push a temporary empty scope
// TODO in the future allow evaluation with some scope if desired.
self.scopes.push(Scope {
variables: BTreeMap::new(),
slots: BTreeMap::new(),
});
let value = self.evaluate_expression(1, default_expr, &param_def.locator)?;
self.scopes[0]
.variables
.insert(String::from(param_def.name.as_str()), value);
self.scopes.pop();
}
for supplied_param in self.scopes[0].variables.keys() {
if !param_defs
.iter()
.any(|def| def.name.as_str() == supplied_param)
{
return Err(InterpreterError::TypeError {
context: "main entrypoint".to_string(),
conflict: format!(
"provided non-existent parameter {} for entrypoint {:?}.",
supplied_param, self.entrypoint
),
location: Locator::empty(),
});
}
}
}
self.run_steps(0, &main.steps).await?;
self.run_steps(0, main).await?;
Ok(())
}
}

View File

@ -1,62 +0,0 @@
use bevy_reflect::Reflect;
use formbeam::{FieldValidatorInfo, FormPartial, FormPartialInfo};
#[derive(Reflect)]
pub struct ReflectedForm<P: FormPartial> {
pub raw: P,
pub errors: P::Validation,
pub info: InfoWrapper,
}
impl<P: FormPartial> ReflectedForm<P> {
pub fn new(raw: P, errors: P::Validation) -> Self {
Self {
raw,
errors,
info: InfoWrapper(Some(P::INFO)),
}
}
}
impl<P: FormPartial + Default> Default for ReflectedForm<P>
where
P::Validation: Default,
{
fn default() -> Self {
Self::new(Default::default(), Default::default())
}
}
#[derive(Copy, Clone, Reflect)]
// This makes the struct opaque to the reflection engine, meaning it will
// be cloned absolutely instead of being converted to a DynamicTupleStruct.
// However it won't implement TupleStruct. But that's fine, that's actually what we want!
#[reflect_value]
pub struct InfoWrapper(#[reflect(ignore)] pub(crate) Option<&'static FormPartialInfo>);
#[derive(Clone, Debug, Reflect)]
pub enum ReflectFieldValidatorInfo {
MinLength(u32),
MaxLength(u32),
MinValue(i64),
MaxValue(i64),
Required,
Email,
Regex(String),
Custom(String),
}
impl From<&FieldValidatorInfo> for ReflectFieldValidatorInfo {
fn from(value: &FieldValidatorInfo) -> Self {
match value {
FieldValidatorInfo::MinLength(m) => ReflectFieldValidatorInfo::MinLength(*m),
FieldValidatorInfo::MaxLength(m) => ReflectFieldValidatorInfo::MaxLength(*m),
FieldValidatorInfo::MinValue(m) => ReflectFieldValidatorInfo::MinValue(*m),
FieldValidatorInfo::MaxValue(m) => ReflectFieldValidatorInfo::MaxValue(*m),
FieldValidatorInfo::Required => ReflectFieldValidatorInfo::Required,
FieldValidatorInfo::Email => ReflectFieldValidatorInfo::Email,
FieldValidatorInfo::Regex(s) => ReflectFieldValidatorInfo::Regex((*s).to_owned()),
FieldValidatorInfo::Custom(s) => ReflectFieldValidatorInfo::Custom((*s).to_owned()),
}
}
}

View File

@ -1,25 +0,0 @@
use crate::interface::Value;
pub(crate) mod defaults;
#[cfg(feature = "formbeam")]
pub(crate) mod formbeam_integration;
/// A method that can be accessed (called) by templates.
/// There is no dynamic dispatch for methods: the name of the method is the only thing that determines which one to call,
/// not the type of the self-parameter.
#[derive(Clone)]
pub struct TemplateAccessibleMethod {
/// Function pointer that implements the method.
/// Arguments are:
/// - the 'self' parameter
/// - list of any parameters
///
/// TODO Extend this to expose interpreter state.
function: fn(Value, Vec<Value>) -> Result<Value, String>,
}
impl TemplateAccessibleMethod {
pub(crate) fn call(&self, obj: Value, args: Vec<Value>) -> Result<Value, String> {
(self.function)(obj, args)
}
}

View File

@ -1,217 +0,0 @@
use std::{collections::BTreeMap, sync::Arc};
use bevy_reflect::{ReflectRef, VariantType};
use percent_encoding::NON_ALPHANUMERIC;
use crate::interface::Value;
use super::TemplateAccessibleMethod;
#[allow(clippy::type_complexity)]
const DEFAULT_TEMPLATE_ACCESSIBLE_METHODS: &[(
&str,
fn(Value, Vec<Value>) -> Result<Value, String>,
)] = &[
("leftpad", leftpad),
("urlencode", urlencode),
("len", len),
("split", split),
("unwrap_or", unwrap_or),
("__get", __get),
];
/// Return a map of the default suggested template-accessible methods.
pub fn default_template_accessible_methods() -> BTreeMap<String, TemplateAccessibleMethod> {
DEFAULT_TEMPLATE_ACCESSIBLE_METHODS
.iter()
.map(|(name, func)| {
(
(*name).to_owned(),
TemplateAccessibleMethod { function: *func },
)
})
.collect()
}
/// Left-pads a string to a given length using a given padding character.
///
/// `<Str>.leftpad(<Int>, <Str>) -> Str`
pub fn leftpad(obj: Value, args: Vec<Value>) -> Result<Value, String> {
let Value::Str(string_to_pad) = obj else {
return Err(format!("{obj:?} is not a string: can't leftpad!"));
};
if args.len() != 2 {
return Err(format!(
"leftpad takes 2 args (length, padding character), not {}",
args.len()
));
}
let Value::Int(pad_length) = &args[0] else {
return Err("leftpad's first arg should be an integer".to_owned());
};
let Value::Str(padding_character) = &args[1] else {
return Err(
"leftpad's second arg should be a string (usually a single character)".to_owned(),
);
};
if string_to_pad.len() as i64 >= *pad_length {
// zero-clone shortcut
// also required to prevent underflow!
return Ok(Value::Str(string_to_pad));
}
let repetitions = pad_length - string_to_pad.len() as i64;
let mut result = String::new();
for _ in 0..repetitions {
result.push_str(padding_character);
}
result.push_str(&string_to_pad);
Ok(Value::Str(Arc::new(result)))
}
/// URL-encodes sensitive characters in a string.
///
/// `<Str>.urlencode() -> Str`
pub fn urlencode(obj: Value, args: Vec<Value>) -> Result<Value, String> {
let Value::Str(string_to_encode) = obj else {
return Err(format!("{obj:?} is not a string: can't urlencode!"));
};
if !args.is_empty() {
return Err(format!("urlencode takes 0 args, not {}", args.len()));
}
Ok(Value::Str(Arc::new(
percent_encoding::utf8_percent_encode(&string_to_encode, NON_ALPHANUMERIC).to_string(),
)))
}
/// Returns the length of a given string or list.
///
/// - `<Str>.len() -> Int`
/// - `<List>.len() -> Int`
pub fn len(obj: Value, args: Vec<Value>) -> Result<Value, String> {
if !args.is_empty() {
return Err(format!("len takes 0 args, not {}", args.len()));
}
Ok(Value::Int(match obj {
Value::Str(string) => string.len() as i64,
Value::List(list) => list.len() as i64,
Value::Reflective(reflect) => match reflect.reflect_ref() {
ReflectRef::List(list) => list.len() as i64,
ReflectRef::Array(array) => array.len() as i64,
ReflectRef::Map(map) => map.len() as i64,
_ => {
return Err(format!(
"reflective {reflect:?} is not a list or map: can't get length!"
));
}
},
_ => {
return Err(format!(
"{obj:?} is not a string or list: can't get length!"
));
}
}))
}
/// Splits a string by given delimiters.
///
/// `<Str>.split(<Str>) -> List of Str`
pub fn split(obj: Value, args: Vec<Value>) -> Result<Value, String> {
if args.len() != 1 {
return Err(format!("split takes 1 arg, not {}", args.len()));
}
let Value::Str(string_to_split) = obj else {
return Err(format!("{obj:?} is not a string: can't split!"));
};
let Value::Str(delimiter) = &args[0] else {
return Err("first arg is not a string: can't split!".to_owned());
};
let result = string_to_split
.split(delimiter.as_str())
.map(|segment| Value::Str(Arc::new(segment.to_owned())))
.collect();
Ok(Value::List(result))
}
/// Unwraps an Option or returns the given default.
///
/// `<Option<T>>.unwrap_or(<T>) -> <T>`
pub fn unwrap_or(obj: Value, mut args: Vec<Value>) -> Result<Value, String> {
if args.len() != 1 {
return Err(format!("unwrap_or takes 1 arg, not {}", args.len()));
}
match obj {
Value::Reflective(reflect) => match reflect.reflect_ref() {
ReflectRef::Enum(reflenum) => match reflenum.variant_name() {
"Some" => {
if reflenum.field_len() != 1 {
return Err("wrong number of fields in Some".to_owned());
}
if reflenum.variant_type() != VariantType::Tuple {
return Err("Some is not a tuple variant".to_owned());
}
Ok(Value::from_reflect(
reflenum.field_at(0).unwrap().clone_value(),
))
}
"None" => Ok(args.pop().unwrap()),
other => Err(format!("`{other}` is not Some or None")),
},
_ => Err(format!("reflective {reflect:?} is not an Option")),
},
other => Err(format!("{other:?} is not an Option")),
}
}
/// Gets a field on an object by name.
///
/// `<Reflective>.__get(<Str>) -> <T>`
pub fn __get(obj: Value, args: Vec<Value>) -> Result<Value, String> {
if args.len() != 1 {
return Err(format!("__get takes 1 arg, not {}", args.len()));
}
let Value::Str(ident) = &args[0] else {
// TODO support ints for tuple structs in the future?
return Err("first arg is not a string: can't __get!".to_owned());
};
match obj {
Value::Reflective(reflective) => {
match reflective.reflect_ref() {
ReflectRef::Struct(ss) => {
if let Some(field) = ss.field(ident) {
Ok(Value::from_reflect(field.clone_value()))
} else {
Err(format!("__get Field Lookup for '{ident}': {reflective:?} is a reflective struct that does not have that field!"))
}
}
ReflectRef::TupleStruct(ts) => {
let field_id: Result<usize, _> = ident.parse();
if let Ok(field_id) = field_id {
if let Some(field) = ts.field(field_id) {
Ok(Value::from_reflect(field.clone_value()))
} else {
Err(format!("__get Field Lookup for '{ident}': {reflective:?} is a reflective tuple struct that does not have that field!"))
}
} else {
Err(format!("__get Field Lookup for '{ident}': {reflective:?} is a reflective tuple struct that does not have names for field!"))
}
}
_ => Err(format!("__get Field Lookup for '{ident}': {reflective:?} is of a reflective type that has no fields!"))
}
}
other => Err(format!("{other:?} is not Reflective")),
}
}

View File

@ -1,110 +0,0 @@
use std::{collections::BTreeMap, sync::Arc};
use formbeam::FieldError;
use crate::{
formbeam_integration::{InfoWrapper, ReflectFieldValidatorInfo},
interface::Value,
};
use super::TemplateAccessibleMethod;
#[allow(clippy::type_complexity)]
const FORMBEAM_TEMPLATE_ACCESSIBLE_METHODS: &[(
&str,
fn(Value, Vec<Value>) -> Result<Value, String>,
)] = &[
("field_validators", field_validators),
("error_code", error_code),
];
/// Return a map of the default suggested template-accessible methods.
pub fn formbeam_template_accessible_methods() -> BTreeMap<String, TemplateAccessibleMethod> {
FORMBEAM_TEMPLATE_ACCESSIBLE_METHODS
.iter()
.map(|(name, func)| {
(
(*name).to_owned(),
TemplateAccessibleMethod { function: *func },
)
})
.collect()
}
/// Returns the validators for the given-named field.
///
/// - `<InfoWrapper>.field_validators(<Str>) -> ...`
pub fn field_validators(obj: Value, args: Vec<Value>) -> Result<Value, String> {
if args.len() != 1 {
return Err(format!("field_validators takes 1 arg, not {}", args.len()));
}
let info_wrapper = match obj {
Value::Reflective(reflect) => match reflect.downcast::<InfoWrapper>() {
Ok(info_wrapper) => info_wrapper,
Err(not_an_info_wrapper) => {
return Err(format!(
"{not_an_info_wrapper:?} is reflective but not an InfoWrapper!"
));
}
},
_ => {
return Err(format!("{obj:?} is not an InfoWrapper!"));
}
};
let field_name = match args.first().unwrap() {
Value::Str(s) => s,
other => {
return Err(format!(
"{other:?} is not a string so cannot be a field name!"
));
}
};
let info = (*info_wrapper).0.as_ref().unwrap();
for field in info.fields {
if field.name == field_name.as_str() {
return Ok(Value::List(
field
.validators
.iter()
.map(|validator| {
Value::from_reflect(Box::new(ReflectFieldValidatorInfo::from(validator)))
})
.collect(),
));
}
}
Err(format!(
"No such field by the name of {field_name} on this form!"
))
}
/// Return the 'code' of an error, suitable for passing to the localisation engine as a key.
///
/// - `<FieldError>.error_code() -> Str`
pub fn error_code(obj: Value, args: Vec<Value>) -> Result<Value, String> {
if !args.is_empty() {
return Err(format!("error_code takes 0 args, not {}", args.len()));
}
let field_error = match obj {
Value::Reflective(reflect) => match reflect.downcast::<FieldError>() {
Ok(ferr) => ferr,
Err(not_an_info_wrapper) => {
return Err(format!(
"{not_an_info_wrapper:?} is reflective but not a FieldError!"
));
}
},
_ => {
return Err(format!("{obj:?} is not a FieldError!"));
}
};
Ok(Value::Str(Arc::new(field_error.error_code().to_owned())))
}
// TODO error_args

View File

@ -1,10 +1,9 @@
use crate::engine::{Interpreter, Scope};
use crate::functions::TemplateAccessibleMethod;
use async_trait::async_trait;
use bevy_reflect::Reflect;
use hornbeam_grammar::parse_template;
use hornbeam_ir::ast_to_optimised_ir;
use hornbeam_ir::ir::TemplateFunction;
use hornbeam_ir::ir::Step;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::convert::Infallible;
@ -39,16 +38,12 @@ pub trait OutputSystem {
// Value is currently used in the localisation system. We might pull it away later on...
pub use crate::engine::Value;
#[cfg(feature = "formbeam")]
use crate::formbeam_template_accessible_methods;
use crate::{default_template_accessible_methods, InterpreterError};
use crate::InterpreterError;
pub struct LoadedTemplates<LS> {
// todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent?
// or do we just staticify?
template_functions: BTreeMap<String, Arc<TemplateFunction>>,
methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>,
template_functions: BTreeMap<String, Arc<Vec<Step>>>,
localisation: Arc<LS>,
}
@ -68,26 +63,12 @@ impl Params {
impl<'a, LS> LoadedTemplates<LS> {
pub fn new(localisation_system: LS) -> Self {
#[allow(unused_mut)]
let mut methods = default_template_accessible_methods();
#[cfg(feature = "formbeam")]
methods.extend(formbeam_template_accessible_methods());
LoadedTemplates {
template_functions: Default::default(),
methods: Arc::new(methods),
localisation: Arc::new(localisation_system),
}
}
pub fn add_methods(
&mut self,
methods: impl IntoIterator<Item = (String, TemplateAccessibleMethod)>,
) {
let registered_methods = Arc::make_mut(&mut self.methods);
registered_methods.extend(methods);
}
pub fn unload_template(&mut self, template_name: &str) -> bool {
let was_removed = self.template_functions.remove(template_name).is_some();
@ -170,10 +151,8 @@ impl<'a, LS> LoadedTemplates<LS> {
params: Params,
locale: String,
) -> PreparedTemplate<LS> {
// TODO add support for running an `init` or `pre` fragment before running any fragment
// This would allow boilerplate `set` statements to be done ahead of time for example...
PreparedTemplate {
template_functions: Arc::new(self.template_functions.clone()),
all_instructions: Arc::new(self.template_functions.clone()),
entrypoint: if let Some(frag) = fragment_name {
format!("{template_name}__{frag}")
} else {
@ -182,14 +161,12 @@ impl<'a, LS> LoadedTemplates<LS> {
variables: params,
localisation: self.localisation.clone(),
locale,
methods: self.methods.clone(),
}
}
}
pub struct PreparedTemplate<LS> {
pub(crate) template_functions: Arc<BTreeMap<String, Arc<TemplateFunction>>>,
pub(crate) methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>,
pub(crate) all_instructions: Arc<BTreeMap<String, Arc<Vec<Step>>>>,
pub(crate) entrypoint: String,
pub(crate) variables: Params,
pub(crate) localisation: Arc<LS>,
@ -203,7 +180,7 @@ impl<LS: LocalisationSystem + Sync + Send> PreparedTemplate<LS> {
) -> Result<(), InterpreterError<LS::Error, O::Error>> {
let interpreter = Interpreter {
entrypoint: self.entrypoint,
program: &self.template_functions,
program: &self.all_instructions,
output,
localisation: self.localisation,
locale: self.locale,
@ -211,7 +188,6 @@ impl<LS: LocalisationSystem + Sync + Send> PreparedTemplate<LS> {
variables: self.variables.params,
slots: Default::default(),
}],
methods: &self.methods,
};
interpreter.run().await?;
Ok(())

View File

@ -1,9 +1,6 @@
use std::fmt::Debug;
mod engine;
#[cfg(feature = "formbeam")]
pub mod formbeam_integration;
mod functions;
pub(crate) mod interface;
pub mod localisation;
@ -29,18 +26,14 @@ pub enum InterpreterError<LE: Debug + Clone, OE: Debug> {
#[error("failed to write to output: {underlying:?}")]
OutputError { underlying: OE },
#[error("failed to parse template: {0}")]
#[error("failed to parse template: ")]
ParseError(#[from] ParseError),
#[error("failed to process parsed template: {0}")]
#[error("failed to process parsed template: ")]
AstToIrError(#[from] AstToIrError),
#[error("error finding templates to load: {0}")]
TemplateFindError(String),
}
pub use functions::defaults::default_template_accessible_methods;
#[cfg(feature = "formbeam")]
pub use functions::formbeam_integration::formbeam_template_accessible_methods;
pub use functions::TemplateAccessibleMethod;
pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate};

View File

@ -24,7 +24,7 @@ impl LocalisationSystem for NoLocalisation {
}
}
/// Localisation system that dumps debug output.
///
#[derive(Copy, Clone)]
pub struct DebugLocalisationSystem;

View File

@ -17,7 +17,7 @@ use thiserror::Error;
fn interpreter_value_to_fluent_value(v: &Value) -> Option<FluentValue> {
match v {
Value::Str(str) => Some(FluentValue::String(Cow::Borrowed(str))),
Value::Str(str) => Some(FluentValue::String(Cow::Borrowed(&str))),
Value::Int(int) => {
// This is an unstyled number
// TODO Support fancier numbers
@ -97,10 +97,12 @@ impl LocalisationSystem for FluentLocalisationSystem {
}
match self.fluent.lookup_with_args(&li, trans_key, &mapped_params) {
Some(val) => Ok(Cow::Owned(val)),
None => Err(FluentLocalisationError::NoTranslation {
trans_key: String::from(trans_key),
lang_id: li.to_string(),
}),
None => {
return Err(FluentLocalisationError::NoTranslation {
trans_key: String::from(trans_key),
lang_id: li.to_string(),
});
}
}
}
}

View File

@ -1,130 +0,0 @@
use bevy_reflect::Reflect;
use hornbeam_interpreter::{localisation::DebugLocalisationSystem, LoadedTemplates, Params};
use insta::assert_snapshot;
#[derive(Reflect)]
struct SimpleTestStruct {
wombat: u64,
apple: u64,
banana: String,
carrot: String,
}
fn simple_test_struct() -> SimpleTestStruct {
SimpleTestStruct {
wombat: 42,
apple: 78,
banana: "banana!!!".to_owned(),
carrot: "mmm CARROT!".to_owned(),
}
}
#[track_caller]
fn simple_render(template: &str) -> String {
let mut templates = LoadedTemplates::new(DebugLocalisationSystem);
templates
.load_template_from_str("main", template, "main.hnb")
.expect("failed to load template");
let params = Params::default()
.set("sts", simple_test_struct())
.set("five", 5);
let prepared = templates.prepare("main", None, params, "en".to_owned());
prepared.render_to_string().unwrap()
}
#[test]
fn snapshot_001() {
assert_snapshot!(simple_render(
r#"
html
body
"this was a triumph :>"
br
raw "<u>making a note here, huge success</u>"
if $five == 5
"FIVE!!! $five"
br
if $five < 10
"five is less than ten!"
br
if $five > 5
"weird..."
"#
))
}
#[test]
fn snapshot_002() {
assert_snapshot!(simple_render(
r#"
@localiseMe{x=5}
br
"$five"
"#
))
}
#[test]
fn snapshot_003() {
assert_snapshot!(simple_render(
r#"
"unpadded: ${$sts.carrot}"
br
"padded to 15: ${$sts.carrot.leftpad(15, 'M')}"
br
"urlencoded: ${$sts.carrot.urlencode()}"
br
"length: ${$sts.carrot.len()}"
br
"split on A: "
for $part in $sts.carrot.split("A")
"($part)"
br
"#
))
}
#[test]
fn snapshot_004() {
assert_snapshot!(simple_render(
r#"
for $part in $sts.carrot.split("A")
set $final_part = $part
"the final part was: $final_part"
"#
))
}
#[test]
fn snapshot_005() {
assert_snapshot!(simple_render(
r#"
set $unused_var = None
match $unused_var
Some($nope) =>
"UNEXPECTED $nope"
None =>
"indeed, var not used"
"#
))
}
#[test]
fn snapshot_006() {
assert_snapshot!(simple_render(
r#"
declare
param $default_param = "default value!"
param $five
param $sts
"$default_param"
"#
))
}

View File

@ -1,5 +0,0 @@
---
source: hornbeam_interpreter/tests/snapshots.rs
expression: "simple_render(r#\"\nhtml\n body\n \"this was a triumph :>\"\n br\n raw \"<u>making a note here, huge success</u>\"\n\n if $five == 5\n \"FIVE!!! $five\"\n br\n\n if $five < 10\n \"five is less than ten!\"\n br\n\n if $five > 5\n \"weird...\"\n \"#)"
---
<!DOCTYPE html><html><body>this was a triumph :&gt;<br><u>making a note here, huge success</u>FIVE!!! 5<br>five is less than ten!<br></body></html>

View File

@ -1,5 +0,0 @@
---
source: hornbeam_interpreter/tests/snapshots.rs
expression: "simple_render(r#\"\n@localiseMe{x=5}\nbr\n\"$five\"\n \"#)"
---
@localiseMe{{&quot;x&quot;: Int(5)}}<br>5

View File

@ -1,5 +0,0 @@
---
source: hornbeam_interpreter/tests/snapshots.rs
expression: "simple_render(r#\"\n\"unpadded: ${$sts.carrot}\"\nbr\n\"padded to 15: ${$sts.carrot.leftpad(15, 'M')}\"\nbr\n\"urlencoded: ${$sts.carrot.urlencode()}\"\nbr\n\"length: ${$sts.carrot.len()}\"\nbr\n\"split on A: \"\nfor $part in $sts.carrot.split(\"A\")\n \"($part)\"\n br\n \"#)"
---
unpadded: mmm CARROT!<br>padded to 15: MMMMmmm CARROT!<br>urlencoded: mmm%20CARROT%21<br>length: 11<br>split on A: (mmm C)<br>(RROT!)<br>

View File

@ -1,5 +0,0 @@
---
source: hornbeam_interpreter/tests/snapshots.rs
expression: "simple_render(r#\"\nfor $part in $sts.carrot.split(\"A\")\n set $final_part = $part\n\n\"the final part was: $final_part\"\n \"#)"
---
the final part was: RROT!

View File

@ -1,5 +0,0 @@
---
source: hornbeam_interpreter/tests/snapshots.rs
expression: "simple_render(r#\"\nset $unused_var = None\n\nmatch $unused_var\n Some($nope) =>\n \"UNEXPECTED $nope\"\n None =>\n \"indeed, var not used\"\n \"#)"
---
indeed, var not used

View File

@ -1,5 +0,0 @@
---
source: hornbeam_interpreter/tests/snapshots.rs
expression: "simple_render(r#\"\ndeclare\n param $default_param = \"default value!\"\n\n\"$default_param\"\n \"#)"
---
default value!

View File

@ -2,13 +2,13 @@
name = "hornbeam_ir"
description = "Intermediate representation for the Hornbeam template language"
license = "AGPL-3.0-or-later"
version = "0.0.5"
version = "0.0.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
hornbeam_grammar = { version = "0.0.5", path = "../hornbeam_grammar" }
hornbeam_grammar = { version = "0.0.2", path = "../hornbeam_grammar" }
thiserror = "1.0.38"
serde = { version = "1.0.152", features = ["derive"] }
itertools = "0.10.5"

View File

@ -1,8 +1,6 @@
use crate::ir::{ParamDef, Step, StepDef, TemplateFunction};
use hornbeam_grammar::ast::{
Binding, Block, Expression, HtmlElement, MatchBinding, StringExpr, StringPiece, Template,
};
use hornbeam_grammar::{intern, IStr, Locator};
use crate::ir::{Step, StepDef};
use hornbeam_grammar::ast::{Block, Expression, StringExpr, StringPiece, Template};
use hornbeam_grammar::{intern, Locator};
use itertools::Itertools;
use std::borrow::Cow;
use std::collections::btree_map::Entry;
@ -10,8 +8,9 @@ use std::collections::BTreeMap;
use std::ops::Deref;
use thiserror::Error;
// TODO use the void tags
/// List of all void (self-closing) HTML tags.
const VOID_TAGS: &[&str] = &[
const VOID_TAGS: &'static [&'static str] = &[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "menuitem", "meta",
"param", "source", "track", "wbr",
];
@ -33,10 +32,10 @@ pub enum AstToIrError {
///
/// Fragments are extracted to `{template_name}__{fragment_name}`.
/// The top-level template is extracted to `{template_name}`.
pub(crate) fn pull_out_entrypoints(
pub(crate) fn pull_out_entrypoints<'a>(
mut template: Template,
template_name: &str,
) -> Result<BTreeMap<String, Template>, AstToIrError> {
) -> Result<BTreeMap<String, Vec<Block>>, AstToIrError> {
let mut functions = BTreeMap::new();
for child in &mut template.blocks {
@ -45,7 +44,7 @@ pub(crate) fn pull_out_entrypoints(
match functions.entry(template_name.to_owned()) {
Entry::Vacant(ve) => {
ve.insert(template);
ve.insert(template.blocks);
}
Entry::Occupied(_) => {
return Err(AstToIrError::SemanticError {
@ -63,10 +62,10 @@ pub(crate) fn pull_out_entrypoints(
/// Extract entrypoints (template fragments) from a block (recursively), replacing them with
/// calls to that entrypoint.
fn pull_out_entrypoints_from_block(
fn pull_out_entrypoints_from_block<'a>(
block: &mut Block,
template_name: &str,
target: &mut BTreeMap<String, Template>,
target: &mut BTreeMap<String, Vec<Block>>,
) -> Result<(), AstToIrError> {
match block {
Block::HtmlElement(he) => {
@ -122,10 +121,7 @@ fn pull_out_entrypoints_from_block(
// function.
let mut blocks = Vec::new();
std::mem::swap(&mut blocks, &mut frag.blocks);
ve.insert(Template {
blocks,
param_defs: None,
});
ve.insert(blocks);
}
Entry::Occupied(_) => {
return Err(AstToIrError::SemanticError {
@ -136,40 +132,27 @@ fn pull_out_entrypoints_from_block(
}
}
}
Block::Text(_)
| Block::RawUnescapedHtml(_)
| Block::DefineExpandSlot(_)
| Block::SetStatement(_) => { /* nop */ }
Block::Text(_) | Block::DefineExpandSlot(_) => { /* nop */ }
}
Ok(())
}
/// Step 2. Compile the AST to IR steps.
pub(crate) fn compile_functions(
functions: &BTreeMap<String, Template>,
) -> Result<BTreeMap<String, TemplateFunction>, AstToIrError> {
pub(crate) fn compile_functions<'a>(
functions: &BTreeMap<String, Vec<Block>>,
) -> Result<BTreeMap<String, Vec<Step>>, AstToIrError> {
let mut result = BTreeMap::new();
for (func_name, func_template) in functions {
for (func_name, func_blocks) in functions {
let mut steps = Vec::new();
for block in &func_template.blocks {
for block in func_blocks {
compile_ast_block_to_steps(block, &mut steps)?;
}
let param_defs = func_template.param_defs.as_ref().map(|defs| {
defs.iter()
.map(|def| ParamDef {
name: def.name.clone(),
default: def.default.clone(),
locator: def.loc.clone(),
})
.collect()
});
result.insert(func_name.clone(), TemplateFunction { param_defs, steps });
result.insert(func_name.clone(), steps);
}
Ok(result)
}
fn compile_ast_block_to_steps(
fn compile_ast_block_to_steps<'a>(
block: &Block,
instructions: &mut Vec<Step>,
) -> Result<(), AstToIrError> {
@ -195,7 +178,7 @@ fn compile_ast_block_to_steps(
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: false,
text: intern(" id=\""),
text: intern(format!(" id=\"")),
},
locator: he.loc.clone(),
});
@ -218,89 +201,75 @@ fn compile_ast_block_to_steps(
// This is only handling the case where we are the exclusive owner of all the class
// names: see below for more...
if !he.classes.is_empty() && !he.attributes.contains_key(&intern("class")) {
let classes = he.classes.iter().map(|istr| istr.as_str()).join(" ");
gen_steps_to_write_literal_attribute(he, "class", &classes, instructions);
}
// Write attributes
for (attr_name, (attr_expr, attr_flags)) in &he.attributes {
let extra_inject = (attr_name.as_str() == "class" && !he.classes.is_empty())
.then(|| intern(he.classes.iter().map(|istr| istr.as_str()).join(" ") + " "));
if attr_flags.optional {
// For optional fields:
// - if it matches Some($x), unwrap to $x and emit $x
// - if it matches None, skip
// - if it matches neither, assume it's already unwrapped and emit directly
// A little bit ugly to say the least...
let mut some_stage = Vec::new();
let virtual_varname = intern("___attrval");
gen_steps_to_write_attribute(
he,
attr_name,
&Expression::Variable {
name: virtual_varname.clone(),
loc: he.loc.clone(),
},
extra_inject.clone(),
&mut some_stage,
);
let binding = MatchBinding::TupleVariant {
name: intern("Some"),
pieces: vec![Binding::Variable(virtual_varname.clone())],
};
let mut arms = vec![(binding, some_stage.clone())];
if let Some(extra_inject) = extra_inject {
let mut none_stage = Vec::new();
gen_steps_to_write_literal_attribute(
he,
attr_name,
extra_inject.trim_end_matches(' '),
&mut none_stage,
);
arms.push((
MatchBinding::UnitVariant {
name: intern("None"),
},
none_stage,
));
} else {
arms.push((
MatchBinding::UnitVariant {
name: intern("None"),
},
Vec::new(),
));
}
arms.push((
MatchBinding::Variable {
name: virtual_varname.clone(),
},
some_stage,
));
if !he.classes.is_empty() {
if !he.attributes.contains_key(&intern("class")) {
instructions.push(Step {
def: StepDef::Match {
matchable: attr_expr.clone(),
arms,
def: StepDef::WriteLiteral {
escape: false,
text: intern(format!(" class=\"")),
},
locator: he.loc.clone(),
});
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: true,
text: intern(he.classes.iter().map(|istr| istr.as_str()).join(" ")),
},
locator: he.loc.clone(),
});
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: false,
text: intern("\""),
},
locator: he.loc.clone(),
});
} else {
gen_steps_to_write_attribute(
he,
attr_name,
attr_expr,
extra_inject,
instructions,
);
}
}
// Write attributes
for (attr_name, attr_expr) in &he.attributes {
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: false,
text: intern(format!(" {attr_name}=\"")),
},
locator: he.loc.clone(),
});
if attr_name.as_str() == "class" && !he.classes.is_empty() {
// This handles the case where we need to merge class lists between an
// attribute and the shorthand form.
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: true,
text: intern(
he.classes.iter().map(|istr| istr.as_str()).join(" ") + " ",
),
},
locator: he.loc.clone(),
});
}
instructions.push(Step {
def: StepDef::WriteEval {
escape: true,
expr: attr_expr.clone(),
},
locator: he.loc.clone(),
});
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: false,
text: intern("\""),
},
locator: he.loc.clone(),
});
}
// Close the tag
instructions.push(Step {
def: StepDef::WriteLiteral {
@ -344,13 +313,6 @@ fn compile_ast_block_to_steps(
locator: ce.loc.clone(),
})
}
Block::SetStatement(ss) => instructions.push(Step {
def: StepDef::Set {
binding: ss.binding.clone(),
expression: ss.expression.clone(),
},
locator: ss.loc.clone(),
}),
Block::IfBlock(ifb) => {
let mut true_instrs = Vec::new();
let mut false_instrs = Vec::new();
@ -449,41 +411,6 @@ fn compile_ast_block_to_steps(
}
}
}
Block::RawUnescapedHtml(text) => {
for piece in &text.pieces {
match piece {
StringPiece::Literal(lit) => {
instructions.push(Step {
def: StepDef::WriteLiteral {
text: lit.clone(),
escape: false,
},
locator: Locator::empty(),
});
}
StringPiece::Interpolation(expr) => {
instructions.push(Step {
def: StepDef::WriteEval {
expr: expr.clone(),
escape: false,
},
locator: Locator::empty(),
});
}
piece @ StringPiece::Localise { .. } => {
instructions.push(Step {
def: StepDef::WriteEval {
expr: Expression::StringExpr(StringExpr {
pieces: vec![piece.clone()],
}),
escape: false,
},
locator: Locator::empty(),
});
}
}
}
}
Block::DefineExpandSlot(slot) => {
instructions.push(Step {
def: StepDef::CallSlotWithParentScope {
@ -508,81 +435,6 @@ fn compile_ast_block_to_steps(
Ok(())
}
fn gen_steps_to_write_literal_attribute(
he: &HtmlElement,
attr_name: &str,
attr_value: &str,
instructions: &mut Vec<Step>,
) {
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: false,
text: intern(format!(" {attr_name}=\"")),
},
locator: he.loc.clone(),
});
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: true,
text: intern(attr_value),
},
locator: he.loc.clone(),
});
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: false,
text: intern("\""),
},
locator: he.loc.clone(),
});
}
fn gen_steps_to_write_attribute(
he: &HtmlElement,
attr_name: &str,
attr_expr: &Expression,
extra_inject: Option<IStr>,
instructions: &mut Vec<Step>,
) {
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: false,
text: intern(format!(" {attr_name}=\"")),
},
locator: he.loc.clone(),
});
if let Some(extra_inject) = extra_inject {
// This handles the case where we need to merge class lists between an
// attribute and the shorthand form.
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: true,
text: extra_inject,
},
locator: he.loc.clone(),
});
}
instructions.push(Step {
def: StepDef::WriteEval {
escape: true,
expr: attr_expr.clone(),
},
locator: he.loc.clone(),
});
instructions.push(Step {
def: StepDef::WriteLiteral {
escape: false,
text: intern("\""),
},
locator: he.loc.clone(),
});
}
#[cfg(test)]
mod tests {
use super::*;
@ -648,24 +500,4 @@ div.stylish#myid {size=42, stringy="yup", arb=$ritrary}
)
.unwrap());
}
#[test]
fn test_compile_params() {
let template = parse_template(
r#"
declare
param $ritary
param $three = 3
div {arb=$ritrary}
OtherComponent {param3=$three}
"#,
"inp",
)
.unwrap();
assert_yaml_snapshot!(compile_functions(
&pull_out_entrypoints(template, "TemplateName").unwrap()
)
.unwrap());
}
}

View File

@ -11,21 +11,6 @@ pub struct Function {
pub steps: Vec<Step>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct TemplateFunction {
/// `None` if we don't have static parameter information.
pub param_defs: Option<Vec<ParamDef>>,
pub steps: Vec<Step>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ParamDef {
pub name: IStr,
// TODO type information
pub default: Option<Expression>,
pub locator: Locator,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct Step {
#[serde(flatten)]
@ -43,10 +28,6 @@ pub enum StepDef {
escape: bool,
expr: Expression,
},
Set {
expression: Expression,
binding: Binding,
},
If {
condition: Expression,
true_steps: Vec<Step>,

View File

@ -8,6 +8,7 @@
//! For using the IR, see `hornbeam_interpreter` (dynamic) or `hornbeam_macros` (code gen).
use crate::ast_to_ir::pull_out_entrypoints;
use crate::ir::Step;
use crate::peephole::apply_all_peephole_passes;
use hornbeam_grammar::ast::Template;
use std::collections::BTreeMap;
@ -18,16 +19,14 @@ mod peephole;
pub use ast_to_ir::AstToIrError;
use self::ir::TemplateFunction;
pub fn ast_to_optimised_ir(
template_name: &str,
template: Template,
) -> Result<BTreeMap<String, TemplateFunction>, AstToIrError> {
) -> Result<BTreeMap<String, Vec<Step>>, AstToIrError> {
let entrypoints = pull_out_entrypoints(template, template_name)?;
let mut compiled_funcs = ast_to_ir::compile_functions(&entrypoints)?;
for func in compiled_funcs.values_mut() {
apply_all_peephole_passes(&mut func.steps);
for steps in compiled_funcs.values_mut() {
apply_all_peephole_passes(steps);
}
Ok(compiled_funcs)

View File

@ -6,15 +6,15 @@ use crate::ir::{Step, StepDef};
use hornbeam_grammar::ast::{Expression, StringPiece};
use hornbeam_grammar::intern;
/// Peephole Machinery
//// Peephole Machinery
fn peephole<T, F: FnMut(&mut [T])>(steps: &mut [T], peephole_width: usize, mut f: F) {
fn peephole<T, F: FnMut(&mut [T]) -> ()>(steps: &mut [T], peephole_width: usize, mut f: F) {
for peephole_start in 0..(steps.len() - peephole_width + 1) {
f(&mut steps[peephole_start..peephole_start + peephole_width]);
}
}
fn peephole_opt<T, F: FnMut(&mut [Option<T>])>(
fn peephole_opt<T, F: FnMut(&mut [Option<T>]) -> ()>(
steps: &mut Vec<T>,
peephole_width: usize,
mut f: F,
@ -54,7 +54,6 @@ fn apply_peephole_pass<F: Fn(&mut Vec<Step>)>(steps: &mut Vec<Step>, pass: &F) {
match &mut step.def {
StepDef::WriteLiteral { .. } => {}
StepDef::WriteEval { .. } => {}
StepDef::Set { .. } => {}
StepDef::If {
true_steps,
false_steps,
@ -86,7 +85,7 @@ fn apply_peephole_pass<F: Fn(&mut Vec<Step>)>(steps: &mut Vec<Step>, pass: &F) {
}
}
///// Peephole Passes
//// Peephole Passes
/// Given a WriteEval step that just writes literals, convert it to a WriteLiteral step.
fn pass_write_eval_literal_to_write_literal(steps: &mut Vec<Step>) {
@ -157,7 +156,8 @@ fn pass_combine_write_literals(steps: &mut Vec<Step>) {
});
}
/// Apply all passes in the preferred order
//// Apply all passes in the preferred order
pub fn apply_all_peephole_passes(steps: &mut Vec<Step>) {
apply_peephole_pass(steps, &pass_write_eval_literal_to_write_literal);
apply_peephole_pass(steps, &pass_write_literal_preescape);
@ -167,21 +167,16 @@ pub fn apply_all_peephole_passes(steps: &mut Vec<Step>) {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ast_to_ir::{compile_functions, pull_out_entrypoints},
ir::TemplateFunction,
};
use crate::ast_to_ir::{compile_functions, pull_out_entrypoints};
use hornbeam_grammar::parse_template;
use insta::assert_yaml_snapshot;
use std::collections::BTreeMap;
fn parse_ir_and_peephole(text: &str) -> BTreeMap<String, TemplateFunction> {
fn parse_ir_and_peephole(text: &str) -> BTreeMap<String, Vec<Step>> {
let template = parse_template(text, "inp").unwrap();
let entrypoints = pull_out_entrypoints(template, "TemplateName").unwrap();
let mut compiled = compile_functions(&entrypoints).unwrap();
compiled
.values_mut()
.for_each(|func| apply_all_peephole_passes(&mut func.steps));
compiled.values_mut().for_each(apply_all_peephole_passes);
compiled
}

View File

@ -3,122 +3,115 @@ source: hornbeam_ir/src/ast_to_ir.rs
expression: "compile_functions(&pull_out_entrypoints(template,\n \"TemplateName\").unwrap()).unwrap()"
---
TemplateName:
param_defs: ~
steps:
- WriteLiteral:
escape: false
text: "<div"
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: ">"
locator:
filename: inp
line: 2
column: 1
- Call:
name: TemplateName__Frag1
args: {}
slots: {}
locator:
filename: inp
line: 3
column: 5
- Call:
name: TemplateName__Footer
args: {}
slots: {}
locator:
filename: inp
line: 9
column: 5
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "<div"
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: ">"
locator:
filename: inp
line: 2
column: 1
- Call:
name: TemplateName__Frag1
args: {}
slots: {}
locator:
filename: inp
line: 3
column: 5
- Call:
name: TemplateName__Footer
args: {}
slots: {}
locator:
filename: inp
line: 9
column: 5
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 2
column: 1
TemplateName__Footer:
param_defs: ~
steps:
- WriteLiteral:
escape: true
text: Or even adjacent ones
locator:
filename: ""
line: 0
column: 0
- WriteLiteral:
escape: true
text: Or even adjacent ones
locator:
filename: ""
line: 0
column: 0
TemplateName__Frag1:
param_defs: ~
steps:
- WriteLiteral:
escape: false
text: "<span"
locator:
filename: inp
line: 4
column: 9
- WriteLiteral:
escape: false
text: ">"
locator:
filename: inp
line: 4
column: 9
- WriteLiteral:
escape: true
text: This is a fragment!!
locator:
filename: ""
line: 0
column: 0
- WriteLiteral:
escape: false
text: "</span>"
locator:
filename: inp
line: 4
column: 9
- Call:
name: TemplateName__Frag2
args: {}
slots: {}
locator:
filename: inp
line: 6
column: 9
- WriteLiteral:
escape: false
text: "<span"
locator:
filename: inp
line: 4
column: 9
- WriteLiteral:
escape: false
text: ">"
locator:
filename: inp
line: 4
column: 9
- WriteLiteral:
escape: true
text: This is a fragment!!
locator:
filename: ""
line: 0
column: 0
- WriteLiteral:
escape: false
text: "</span>"
locator:
filename: inp
line: 4
column: 9
- Call:
name: TemplateName__Frag2
args: {}
slots: {}
locator:
filename: inp
line: 6
column: 9
TemplateName__Frag2:
param_defs: ~
steps:
- WriteLiteral:
escape: false
text: "<div"
locator:
filename: inp
line: 7
column: 13
- WriteLiteral:
escape: false
text: ">"
locator:
filename: inp
line: 7
column: 13
- WriteLiteral:
escape: true
text: "There's no problem having nested fragments!"
locator:
filename: ""
line: 0
column: 0
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 7
column: 13
- WriteLiteral:
escape: false
text: "<div"
locator:
filename: inp
line: 7
column: 13
- WriteLiteral:
escape: false
text: ">"
locator:
filename: inp
line: 7
column: 13
- WriteLiteral:
escape: true
text: "There's no problem having nested fragments!"
locator:
filename: ""
line: 0
column: 0
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 7
column: 13

View File

@ -3,172 +3,171 @@ source: hornbeam_ir/src/ast_to_ir.rs
expression: "compile_functions(&pull_out_entrypoints(template,\n \"TemplateName\").unwrap()).unwrap()"
---
TemplateName:
param_defs: ~
steps:
- WriteLiteral:
escape: false
text: "<div"
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " id=\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: true
text: myid
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " class=\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: true
text: stylish
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " arb=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
Variable:
name: ritrary
loc:
filename: inp
line: 2
column: 47
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " size=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
- WriteLiteral:
escape: false
text: "<div"
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " id=\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: true
text: myid
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " class=\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: true
text: stylish
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " arb=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
Variable:
name: ritrary
loc:
filename: inp
line: 2
column: 47
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " size=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
IntLiteral:
val: 42
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " stringy=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
StringExpr:
pieces:
- Literal: yup
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: ">"
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: true
text: This is a div with a few extras
locator:
filename: ""
line: 0
column: 0
- Call:
name: OtherComponent
args:
param1:
IntLiteral:
val: 42
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: " stringy=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
val: 1
param2:
StringExpr:
pieces:
- Literal: yup
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: ">"
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: true
text: This is a div with a few extras
locator:
filename: ""
line: 0
column: 0
- Call:
name: OtherComponent
args:
param1:
IntLiteral:
val: 1
param2:
StringExpr:
pieces:
- Literal: two
param3:
Variable:
name: three
loc:
filename: inp
line: 4
column: 52
slots:
main: []
locator:
filename: inp
line: 4
column: 5
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 2
column: 1
- Literal: two
param3:
Variable:
name: three
loc:
filename: inp
line: 4
column: 52
slots:
main: []
locator:
filename: inp
line: 4
column: 5
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 2
column: 1

View File

@ -1,85 +0,0 @@
---
source: hornbeam_ir/src/ast_to_ir.rs
expression: "compile_functions(&pull_out_entrypoints(template,\n \"TemplateName\").unwrap()).unwrap()"
---
TemplateName:
param_defs:
- name: ritary
default: ~
locator:
filename: inp
line: 3
column: 5
- name: three
default:
IntLiteral:
val: 3
locator:
filename: inp
line: 5
column: 5
steps:
- WriteLiteral:
escape: false
text: "<div"
locator:
filename: inp
line: 7
column: 1
- WriteLiteral:
escape: false
text: " arb=\""
locator:
filename: inp
line: 7
column: 1
- WriteEval:
escape: true
expr:
Variable:
name: ritrary
loc:
filename: inp
line: 7
column: 10
locator:
filename: inp
line: 7
column: 1
- WriteLiteral:
escape: false
text: "\""
locator:
filename: inp
line: 7
column: 1
- WriteLiteral:
escape: false
text: ">"
locator:
filename: inp
line: 7
column: 1
- Call:
name: OtherComponent
args:
param3:
Variable:
name: three
loc:
filename: inp
line: 8
column: 28
slots:
main: []
locator:
filename: inp
line: 8
column: 5
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 7
column: 1

View File

@ -3,74 +3,67 @@ source: hornbeam_ir/src/ast_to_ir.rs
expression: "pull_out_entrypoints(template, \"TemplateName\").unwrap()"
---
TemplateName:
param_defs: ~
blocks:
- HtmlElement:
name: div
children:
- DefineFragment:
name: TemplateName__Frag1
blocks: []
loc:
filename: inp
line: 3
column: 5
- DefineFragment:
name: TemplateName__Footer
blocks: []
loc:
filename: inp
line: 9
column: 5
classes: []
dom_id: ~
attributes: {}
loc:
filename: inp
line: 2
column: 1
- HtmlElement:
name: div
children:
- DefineFragment:
name: TemplateName__Frag1
blocks: []
loc:
filename: inp
line: 3
column: 5
- DefineFragment:
name: TemplateName__Footer
blocks: []
loc:
filename: inp
line: 9
column: 5
classes: []
dom_id: ~
attributes: {}
loc:
filename: inp
line: 2
column: 1
TemplateName__Footer:
param_defs: ~
blocks:
- Text:
pieces:
- Literal: Or even adjacent ones
- Text:
pieces:
- Literal: Or even adjacent ones
TemplateName__Frag1:
param_defs: ~
blocks:
- HtmlElement:
name: span
children:
- Text:
pieces:
- Literal: This is a fragment!!
classes: []
dom_id: ~
attributes: {}
loc:
filename: inp
line: 4
column: 9
- DefineFragment:
name: TemplateName__Frag2
blocks: []
loc:
filename: inp
line: 6
column: 9
- HtmlElement:
name: span
children:
- Text:
pieces:
- Literal: This is a fragment!!
classes: []
dom_id: ~
attributes: {}
loc:
filename: inp
line: 4
column: 9
- DefineFragment:
name: TemplateName__Frag2
blocks: []
loc:
filename: inp
line: 6
column: 9
TemplateName__Frag2:
param_defs: ~
blocks:
- HtmlElement:
name: div
children:
- Text:
pieces:
- Literal: "There's no problem having nested fragments!"
classes: []
dom_id: ~
attributes: {}
loc:
filename: inp
line: 7
column: 13
- HtmlElement:
name: div
children:
- Text:
pieces:
- Literal: "There's no problem having nested fragments!"
classes: []
dom_id: ~
attributes: {}
loc:
filename: inp
line: 7
column: 13

View File

@ -3,73 +3,66 @@ source: hornbeam_ir/src/peephole.rs
expression: "parse_ir_and_peephole(r#\"\ndiv\n fragment Frag1\n span\n \"This is a fragment!!\"\n fragment Frag2\n div\n \"There's no problem having <<nested>> fragments!\"\n fragment Footer\n \"Or even adjacent ones\"\n \"#)"
---
TemplateName:
param_defs: ~
steps:
- WriteLiteral:
escape: false
text: "<div>"
locator:
filename: inp
line: 2
column: 1
- Call:
name: TemplateName__Frag1
args: {}
slots: {}
locator:
filename: inp
line: 3
column: 5
- Call:
name: TemplateName__Footer
args: {}
slots: {}
locator:
filename: inp
line: 9
column: 5
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "<div>"
locator:
filename: inp
line: 2
column: 1
- Call:
name: TemplateName__Frag1
args: {}
slots: {}
locator:
filename: inp
line: 3
column: 5
- Call:
name: TemplateName__Footer
args: {}
slots: {}
locator:
filename: inp
line: 9
column: 5
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 2
column: 1
TemplateName__Footer:
param_defs: ~
steps:
- WriteLiteral:
escape: false
text: Or even adjacent ones
locator:
filename: ""
line: 0
column: 0
- WriteLiteral:
escape: false
text: Or even adjacent ones
locator:
filename: ""
line: 0
column: 0
TemplateName__Frag1:
param_defs: ~
steps:
- WriteLiteral:
escape: false
text: "<span>This is a fragment!!</span>"
locator:
filename: inp
line: 4
column: 9
- Call:
name: TemplateName__Frag2
args: {}
slots: {}
locator:
filename: inp
line: 6
column: 9
- WriteLiteral:
escape: false
text: "<span>This is a fragment!!</span>"
locator:
filename: inp
line: 4
column: 9
- Call:
name: TemplateName__Frag2
args: {}
slots: {}
locator:
filename: inp
line: 6
column: 9
TemplateName__Frag2:
param_defs: ~
steps:
- WriteLiteral:
escape: false
text: "<div>There&#x27;s no problem having &lt;&lt;nested&gt;&gt; fragments!</div>"
locator:
filename: inp
line: 7
column: 13
- WriteLiteral:
escape: false
text: "<div>There&#x27;s no problem having &lt;&lt;nested&gt;&gt; fragments!</div>"
locator:
filename: inp
line: 7
column: 13

View File

@ -3,78 +3,77 @@ source: hornbeam_ir/src/peephole.rs
expression: "parse_ir_and_peephole(r#\"\ndiv.stylish#myid {size=42, stringy=\"yup\", arb=$ritrary}\n \"This is a div with a few extras\"\n OtherComponent {param1=1, param2=\"two\", param3=$three}\n \"#)"
---
TemplateName:
param_defs: ~
steps:
- WriteLiteral:
escape: false
text: "<div id=\"myid\" class=\"stylish\" arb=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
- WriteLiteral:
escape: false
text: "<div id=\"myid\" class=\"stylish\" arb=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
Variable:
name: ritrary
loc:
filename: inp
line: 2
column: 47
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\" size=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
IntLiteral:
val: 42
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\" stringy=\"yup\">This is a div with a few extras"
locator:
filename: inp
line: 2
column: 1
- Call:
name: OtherComponent
args:
param1:
IntLiteral:
val: 1
param2:
StringExpr:
pieces:
- Literal: two
param3:
Variable:
name: ritrary
name: three
loc:
filename: inp
line: 2
column: 47
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\" size=\""
locator:
filename: inp
line: 2
column: 1
- WriteEval:
escape: true
expr:
IntLiteral:
val: 42
locator:
filename: inp
line: 2
column: 1
- WriteLiteral:
escape: false
text: "\" stringy=\"yup\">This is a div with a few extras"
locator:
filename: inp
line: 2
column: 1
- Call:
name: OtherComponent
args:
param1:
IntLiteral:
val: 1
param2:
StringExpr:
pieces:
- Literal: two
param3:
Variable:
name: three
loc:
filename: inp
line: 4
column: 52
slots:
main: []
locator:
filename: inp
line: 4
column: 5
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 2
column: 1
line: 4
column: 52
slots:
main: []
locator:
filename: inp
line: 4
column: 5
- WriteLiteral:
escape: false
text: "</div>"
locator:
filename: inp
line: 2
column: 1

View File

@ -2,7 +2,7 @@
name = "hornbeam_macros"
description = "Macros for the Hornbeam template system"
license = "AGPL-3.0-or-later"
version = "0.0.5"
version = "0.0.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html