Compare commits
	
		
			26 Commits
		
	
	
		
			hornbeam@0
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9585ec5a51 | |||
| 98de78aa72 | |||
| 02db5531d3 | |||
| 1cb196571f | |||
| 2281a9bb50 | |||
| 9098d72217 | |||
| 7cb653ab67 | |||
| 5a115b3a20 | |||
| 659f3a88e4 | |||
| 16f3488d20 | |||
| dc8447f08e | |||
| 845a05b720 | |||
| d08a088257 | |||
| c92ab5aaff | |||
| d9f008d5ed | |||
| d08c04de5b | |||
| 708f5d214d | |||
| 75121c2fb8 | |||
| f21500d5e8 | |||
| b69662cfdc | |||
| 5448888651 | |||
| 8a3cc0eeef | |||
| b6181eb204 | |||
| dd74768ec6 | |||
| cdc34d45f7 | |||
| b7818dddc1 | 
							
								
								
									
										597
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										597
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -5,10 +5,12 @@ members = [ | |||||||
|     "hornbeam_interpreter", |     "hornbeam_interpreter", | ||||||
|     "hornbeam_macros", |     "hornbeam_macros", | ||||||
|     "hornbeam", |     "hornbeam", | ||||||
|     "demo_hornbeam_project", |     "demo_hornbeam_project", "formbeam", "formbeam_derive", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | [workspace.dependencies] | ||||||
|  | bevy_reflect = "0.14.0" | ||||||
| 
 | 
 | ||||||
| # Enable optimisation for testing helpers | # Enable optimisation for testing helpers | ||||||
| [profile.dev.package.insta] | [profile.dev.package.insta] | ||||||
|  | |||||||
| @ -1,16 +1,22 @@ | |||||||
| [package] | [package] | ||||||
| name = "demo_hornbeam_project" | name = "demo_hornbeam_project" | ||||||
| version = "0.0.3" | version = "0.0.5" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| private = true | private = true | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| axum = "0.6.9" | axum = "0.8.4" | ||||||
|  | bevy_reflect.workspace = true | ||||||
| eyre = "0.6.8" | eyre = "0.6.8" | ||||||
| color-eyre = "0.6.2" | color-eyre = "0.6.2" | ||||||
| tokio = { version = "1.25.0", features = ["full"] } | tokio = { version = "1.25.0", features = ["full"] } | ||||||
| tracing = "0.1.37" | tracing = "0.1.37" | ||||||
| hornbeam = { version = "0.0.3", path = "../hornbeam" } | hornbeam = { version = "0.0.5", path = "../hornbeam", features = ["formbeam"] } | ||||||
| tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } | 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"] } | ||||||
|  | |||||||
| @ -1,14 +1,23 @@ | |||||||
| use axum::extract::Path; | use axum::extract::rejection::FormRejection; | ||||||
|  | use axum::extract::{self, Path}; | ||||||
| use axum::http::StatusCode; | use axum::http::StatusCode; | ||||||
| use axum::response::{Html, IntoResponse, Response}; | use axum::response::{Html, IntoResponse, Response}; | ||||||
| use axum::routing::get; | use axum::routing::get; | ||||||
| use axum::Router; | use axum::Router; | ||||||
| use hornbeam::{initialise_template_manager, make_template_manager, render_template_string}; | use eyre::Context; | ||||||
|  | use formbeam::traits::FormValidation; | ||||||
|  | use formbeam::FormPartial; | ||||||
|  | use hornbeam::{ | ||||||
|  |     initialise_template_manager, make_template_manager, render_template_string, ReflectedForm, | ||||||
|  | }; | ||||||
| use std::net::SocketAddr; | use std::net::SocketAddr; | ||||||
|  | use tokio::net::TcpListener; | ||||||
| use tracing::debug; | use tracing::debug; | ||||||
| use tracing_subscriber::layer::SubscriberExt; | use tracing_subscriber::layer::SubscriberExt; | ||||||
| use tracing_subscriber::util::SubscriberInitExt; | use tracing_subscriber::util::SubscriberInitExt; | ||||||
| 
 | 
 | ||||||
|  | use formbeam_derive::Form; | ||||||
|  | 
 | ||||||
| make_template_manager! { | make_template_manager! { | ||||||
|     static ref TEMPLATING = { |     static ref TEMPLATING = { | ||||||
|         default_locale: "en", |         default_locale: "en", | ||||||
| @ -28,13 +37,16 @@ async fn main() -> eyre::Result<()> { | |||||||
| 
 | 
 | ||||||
|     initialise_template_manager!(TEMPLATING); |     initialise_template_manager!(TEMPLATING); | ||||||
| 
 | 
 | ||||||
|     let app = Router::new().route("/:lang/hello/:name", get(say_hello)); |     let app = Router::new() | ||||||
|  |         .route("/:lang/hello/:name", get(say_hello)) | ||||||
|  |         .route("/:lang/formdemo", get(form_demo)); | ||||||
| 
 | 
 | ||||||
|     let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); |     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); |     debug!("Listening on http://{}", addr); | ||||||
|     axum::Server::bind(&addr) |     axum::serve(listener, app).await?; | ||||||
|         .serve(app.into_make_service()) |  | ||||||
|         .await?; |  | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -60,3 +72,38 @@ 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 | ||||||
|  |     })) | ||||||
|  | } | ||||||
|  | |||||||
| @ -0,0 +1,6 @@ | |||||||
|  | if $errors.len() != 0 | ||||||
|  |     "errors on this field (or 'form-wide' section):" | ||||||
|  |     ul | ||||||
|  |         for $e in $errors | ||||||
|  |             li | ||||||
|  |                 "${$e.error_code()}" | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | "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)" | ||||||
							
								
								
									
										48
									
								
								demo_hornbeam_project/templates/pages/form_demo.hnb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								demo_hornbeam_project/templates/pages/form_demo.hnb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | |||||||
|  | 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!" | ||||||
							
								
								
									
										26
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							| @ -30,11 +30,11 @@ | |||||||
|         "rust-analyzer-src": "rust-analyzer-src" |         "rust-analyzer-src": "rust-analyzer-src" | ||||||
|       }, |       }, | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1700979662, |         "lastModified": 1752907304, | ||||||
|         "narHash": "sha256-AlS2tefQT6fBxWGKKkQNXh1HAyzlwBQ8P6AUMmI1mxw=", |         "narHash": "sha256-rSw0b/ahoZebcp+AZG7uoScB5Q59TYEE5Kx8k0pZp9E=", | ||||||
|         "owner": "nix-community", |         "owner": "nix-community", | ||||||
|         "repo": "fenix", |         "repo": "fenix", | ||||||
|         "rev": "6f824c3d294716db51fbf8d555af8b9514cdbec4", |         "rev": "e91719882d0e4366202cc9058eb21df74c0bdb92", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
| @ -186,16 +186,16 @@ | |||||||
|     }, |     }, | ||||||
|     "nixpkgs_2": { |     "nixpkgs_2": { | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1700851152, |         "lastModified": 1752620740, | ||||||
|         "narHash": "sha256-3PWITNJZyA3jz5IGREJRfSykM6xSLmD8u5A3WpBCyDM=", |         "narHash": "sha256-f3pO+9lg66mV7IMmmIqG4PL3223TYMlnlw+pnpelbss=", | ||||||
|         "owner": "NixOS", |         "owner": "NixOS", | ||||||
|         "repo": "nixpkgs", |         "repo": "nixpkgs", | ||||||
|         "rev": "1216a5ba22a93a4a3a3bfdb4bff0f4727c576fcc", |         "rev": "32a4e87942101f1c9f9865e04dc3ddb175f5f32e", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|         "id": "nixpkgs", |         "id": "nixpkgs", | ||||||
|         "ref": "nixos-23.05", |         "ref": "nixos-25.05", | ||||||
|         "type": "indirect" |         "type": "indirect" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
| @ -238,11 +238,11 @@ | |||||||
|     "rust-analyzer-src": { |     "rust-analyzer-src": { | ||||||
|       "flake": false, |       "flake": false, | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1700937652, |         "lastModified": 1752817855, | ||||||
|         "narHash": "sha256-sHczfsDUYH1CXPqNztnWH0gFo7xU0AuqQD96qSVC2HI=", |         "narHash": "sha256-YnG3d44oX+g2ooUsNWT+Ii24w6T+b0dj86k0HkIFUj4=", | ||||||
|         "owner": "rust-lang", |         "owner": "rust-lang", | ||||||
|         "repo": "rust-analyzer", |         "repo": "rust-analyzer", | ||||||
|         "rev": "79ec2c584b990a533f7fde685a51d45d2a896103", |         "rev": "330c4ed11c4e1eef0999a2cd629703a601da1436", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
| @ -272,11 +272,11 @@ | |||||||
|         "systems": "systems" |         "systems": "systems" | ||||||
|       }, |       }, | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1694529238, |         "lastModified": 1710146030, | ||||||
|         "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", |         "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", | ||||||
|         "owner": "numtide", |         "owner": "numtide", | ||||||
|         "repo": "flake-utils", |         "repo": "flake-utils", | ||||||
|         "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", |         "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ | |||||||
|       url = "github:nix-community/fenix"; |       url = "github:nix-community/fenix"; | ||||||
|       inputs.nixpkgs.follows = "nixpkgs"; |       inputs.nixpkgs.follows = "nixpkgs"; | ||||||
|     }; |     }; | ||||||
|     nixpkgs.url = "nixpkgs/nixos-23.05"; |     nixpkgs.url = "nixpkgs/nixos-25.05"; | ||||||
| 
 | 
 | ||||||
|     devenv.url = "github:cachix/devenv/v0.6.3"; |     devenv.url = "github:cachix/devenv/v0.6.3"; | ||||||
|   }; |   }; | ||||||
| @ -44,6 +44,9 @@ | |||||||
|               # Releasing a full workspace of packages |               # Releasing a full workspace of packages | ||||||
|               pkgs.cargo-workspaces |               pkgs.cargo-workspaces | ||||||
| 
 | 
 | ||||||
|  |               # Macro debugging | ||||||
|  |               pkgs.cargo-expand | ||||||
|  | 
 | ||||||
|               pkgs.grass-sass |               pkgs.grass-sass | ||||||
|               pkgs.entr |               pkgs.entr | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								formbeam/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								formbeam/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | [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" | ||||||
							
								
								
									
										81
									
								
								formbeam/src/errors.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								formbeam/src/errors.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | 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); | ||||||
							
								
								
									
										9
									
								
								formbeam/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								formbeam/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | pub mod errors; | ||||||
|  | pub mod traits; | ||||||
|  | pub mod validators; | ||||||
|  | 
 | ||||||
|  | pub use errors::{FieldError, FieldErrors}; | ||||||
|  | pub use traits::{ | ||||||
|  |     FieldInfo, FieldValidator, FieldValidatorInfo, Form, FormPartial, FormPartialInfo, | ||||||
|  |     FormValidator, | ||||||
|  | }; | ||||||
							
								
								
									
										98
									
								
								formbeam/src/traits.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								formbeam/src/traits.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,98 @@ | |||||||
|  | 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), | ||||||
|  | } | ||||||
							
								
								
									
										102
									
								
								formbeam/src/validators.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								formbeam/src/validators.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,102 @@ | |||||||
|  | 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(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								formbeam_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								formbeam_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | [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" | ||||||
							
								
								
									
										540
									
								
								formbeam_derive/src/derive_form.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										540
									
								
								formbeam_derive/src/derive_form.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,540 @@ | |||||||
|  | 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()), | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								formbeam_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								formbeam_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | 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() | ||||||
|  | } | ||||||
| @ -2,22 +2,23 @@ | |||||||
| name = "hornbeam" | name = "hornbeam" | ||||||
| description = "Hornbeam template engine (high-level crate for use in applications)" | description = "Hornbeam template engine (high-level crate for use in applications)" | ||||||
| license = "AGPL-3.0-or-later" | license = "AGPL-3.0-or-later" | ||||||
| version = "0.0.3" | version = "0.0.5" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| hornbeam_interpreter = { version = "0.0.3", path = "../hornbeam_interpreter" } | hornbeam_interpreter = { version = "0.0.5", path = "../hornbeam_interpreter" } | ||||||
| 
 | 
 | ||||||
| arc-swap = "1.6.0" | arc-swap = "1.6.0" | ||||||
| notify = "5.1.0" | notify = "5.1.0" | ||||||
| lazy_static = "1.4.0" | lazy_static = "1.4.0" | ||||||
| 
 | 
 | ||||||
| tokio = { version = "1.26.0", optional = true } | tokio = { version = "1.26.0", optional = true } | ||||||
| axum = { version = "0.6.10", optional = true } | axum = { version = "0.8.4", optional = true } | ||||||
| 
 | 
 | ||||||
| [features] | [features] | ||||||
| default = ["interpreted", "hot_reload"] | default = ["interpreted", "hot_reload"] | ||||||
| interpreted = [] | interpreted = [] | ||||||
| hot_reload = ["tokio", "axum"] | hot_reload = ["tokio", "axum"] | ||||||
|  | formbeam = ["hornbeam_interpreter/formbeam"] | ||||||
|  | |||||||
| @ -70,14 +70,18 @@ pub fn new_template_manager( | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         successful.expect(&format!("Could not find Hornbeam templates/translations: tried looking in {try_paths:?} and no HORNBEAM_BASE environment variable set!")) |         successful.unwrap_or_else(|| panic!("Could not find Hornbeam templates/translations: tried looking in {try_paths:?} and no HORNBEAM_BASE environment variable set!")) | ||||||
|     }; |     }; | ||||||
|     let template_sys = load_new_template_system( |     let template_sys = match load_new_template_system( | ||||||
|         default_locale, |         default_locale, | ||||||
|         &base_dir.join("templates"), |         &base_dir.join("templates"), | ||||||
|         &base_dir.join("translations"), |         &base_dir.join("translations"), | ||||||
|     ) |     ) { | ||||||
|     .expect("Failed to create Hornbeam environment!"); |         Ok(v) => v, | ||||||
|  |         Err(err) => { | ||||||
|  |             panic!("Failed to create Hornbeam environment! {err}"); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|     let templates = Arc::new(ArcSwap::new(Arc::new(template_sys))); |     let templates = Arc::new(ArcSwap::new(Arc::new(template_sys))); | ||||||
| 
 | 
 | ||||||
|     #[cfg(feature = "hot_reload")] |     #[cfg(feature = "hot_reload")] | ||||||
| @ -111,6 +115,11 @@ fn load_new_template_system( | |||||||
| 
 | 
 | ||||||
| #[cfg(feature = "hot_reload")] | #[cfg(feature = "hot_reload")] | ||||||
| pub fn is_hot_reload_enabled() -> bool { | 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") |     std::env::var("HORNBEAM_HOT") | ||||||
|         .map(|env_var| { |         .map(|env_var| { | ||||||
|             if let Ok(i) = env_var.parse::<u32>() { |             if let Ok(i) = env_var.parse::<u32>() { | ||||||
| @ -126,9 +135,9 @@ pub fn is_hot_reload_enabled() -> bool { | |||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
|             eprintln!("Not sure how to interpret HORNBEAM_HOT={env_var:?}, assuming yes."); |             eprintln!("Not sure how to interpret HORNBEAM_HOT={env_var:?}, assuming yes."); | ||||||
|             return true; |             true | ||||||
|         }) |         }) | ||||||
|         .unwrap_or(true) |         .unwrap_or(DEFAULT_HOT_RELOAD) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg(not(feature = "hot_reload"))] | #[cfg(not(feature = "hot_reload"))] | ||||||
|  | |||||||
| @ -6,11 +6,13 @@ use axum::{Extension, Router}; | |||||||
| use hornbeam_interpreter::localisation::fluent::FluentLocalisationSystem; | use hornbeam_interpreter::localisation::fluent::FluentLocalisationSystem; | ||||||
| use hornbeam_interpreter::LoadedTemplates; | use hornbeam_interpreter::LoadedTemplates; | ||||||
| use notify::{Event, EventKind, RecursiveMode, Watcher}; | use notify::{Event, EventKind, RecursiveMode, Watcher}; | ||||||
|  | use std::error::Error; | ||||||
| use std::net::SocketAddr; | use std::net::SocketAddr; | ||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
| use std::sync::mpsc::RecvTimeoutError; | use std::sync::mpsc::RecvTimeoutError; | ||||||
| use std::sync::{Arc, Mutex}; | use std::sync::{Arc, Mutex}; | ||||||
| use std::time::{Duration, Instant}; | use std::time::{Duration, Instant}; | ||||||
|  | use tokio::net::TcpListener; | ||||||
| use tokio::sync::oneshot; | use tokio::sync::oneshot; | ||||||
| 
 | 
 | ||||||
| pub(crate) fn start_hot_reloader( | pub(crate) fn start_hot_reloader( | ||||||
| @ -56,7 +58,7 @@ pub(crate) fn start_hot_reloader( | |||||||
|             let default_locale = default_locale; |             let default_locale = default_locale; | ||||||
| 
 | 
 | ||||||
|             loop { |             loop { | ||||||
|                 let _ = notif_rx.recv().unwrap(); |                 notif_rx.recv().unwrap(); | ||||||
| 
 | 
 | ||||||
|                 // Debounce, because editors often make a series of modifications and we don't
 |                 // Debounce, because editors often make a series of modifications and we don't
 | ||||||
|                 // want to reload before it's ready.
 |                 // want to reload before it's ready.
 | ||||||
| @ -100,14 +102,17 @@ pub(crate) fn start_auto_hot_reloader() -> WaiterList { | |||||||
|         .layer(Extension(waiter_list.clone())); |         .layer(Extension(waiter_list.clone())); | ||||||
| 
 | 
 | ||||||
|     let addr = SocketAddr::from(([127, 0, 0, 1], 7015)); |     let addr = SocketAddr::from(([127, 0, 0, 1], 7015)); | ||||||
|     eprintln!("Hornbeam Auto Hot Reload: Listening on http://{}", addr); |  | ||||||
| 
 | 
 | ||||||
|     tokio::spawn(async move { |     tokio::spawn(async move { | ||||||
|         if let Err(e) = axum::Server::bind(&addr) |         let result: Result<(), Box<dyn Error>> = async move { | ||||||
|             .serve(app.into_make_service()) |             let listener = TcpListener::bind(addr).await?; | ||||||
|             .await |             eprintln!("Hornbeam Auto Hot Reload: Listening on http://{}", addr); | ||||||
|         { |             axum::serve(listener, app).await?; | ||||||
|             eprintln!("Hornbeam Auto Hot Reload failed: {e:?}"); |             Ok(()) | ||||||
|  |         } | ||||||
|  |         .await; | ||||||
|  |         if let Err(e) = result { | ||||||
|  |             eprintln!("Hornbeam Auto Hot Reload failed: {e}"); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -17,3 +17,7 @@ pub use interpreted::{ | |||||||
|     is_hot_reload_enabled, lazy_static, new_template_manager, Params, TemplateError, |     is_hot_reload_enabled, lazy_static, new_template_manager, Params, TemplateError, | ||||||
|     TemplateManager, |     TemplateManager, | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | #[cfg(feature = "formbeam")] | ||||||
|  | #[cfg(feature = "interpreted")] | ||||||
|  | pub use hornbeam_interpreter::formbeam_integration::ReflectedForm; | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| name = "hornbeam_grammar" | name = "hornbeam_grammar" | ||||||
| description = "Grammar for the Hornbeam template language" | description = "Grammar for the Hornbeam template language" | ||||||
| license = "AGPL-3.0-or-later" | license = "AGPL-3.0-or-later" | ||||||
| version = "0.0.3" | version = "0.0.5" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  | |||||||
| @ -4,13 +4,22 @@ use std::collections::BTreeMap; | |||||||
| 
 | 
 | ||||||
| #[derive(Clone, Debug, Eq, PartialEq, Serialize)] | #[derive(Clone, Debug, Eq, PartialEq, Serialize)] | ||||||
| pub struct Template { | pub struct Template { | ||||||
|  |     pub param_defs: Option<Vec<ParameterDefinition>>, | ||||||
|     pub blocks: Vec<Block>, |     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)] | #[derive(Clone, Debug, Eq, PartialEq, Serialize)] | ||||||
| pub enum Block { | pub enum Block { | ||||||
|     HtmlElement(HtmlElement), |     HtmlElement(HtmlElement), | ||||||
|     ComponentElement(ComponentElement), |     ComponentElement(ComponentElement), | ||||||
|  |     SetStatement(SetStatement), | ||||||
|     IfBlock(IfBlock), |     IfBlock(IfBlock), | ||||||
|     ForBlock(ForBlock), |     ForBlock(ForBlock), | ||||||
|     MatchBlock(MatchBlock), |     MatchBlock(MatchBlock), | ||||||
| @ -26,10 +35,15 @@ pub struct HtmlElement { | |||||||
|     pub children: Vec<Block>, |     pub children: Vec<Block>, | ||||||
|     pub classes: Vec<IStr>, |     pub classes: Vec<IStr>, | ||||||
|     pub dom_id: Option<IStr>, |     pub dom_id: Option<IStr>, | ||||||
|     pub attributes: BTreeMap<IStr, Expression>, |     pub attributes: BTreeMap<IStr, (Expression, ElementAttributeFlags)>, | ||||||
|     pub loc: Locator, |     pub loc: Locator, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] | ||||||
|  | pub struct ElementAttributeFlags { | ||||||
|  |     pub optional: bool, | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[derive(Clone, Debug, Eq, PartialEq, Serialize)] | #[derive(Clone, Debug, Eq, PartialEq, Serialize)] | ||||||
| pub struct ComponentElement { | pub struct ComponentElement { | ||||||
|     pub name: IStr, |     pub name: IStr, | ||||||
| @ -38,6 +52,13 @@ pub struct ComponentElement { | |||||||
|     pub loc: Locator, |     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)] | #[derive(Clone, Debug, Eq, PartialEq, Serialize)] | ||||||
| pub struct IfBlock { | pub struct IfBlock { | ||||||
|     pub condition: Expression, |     pub condition: Expression, | ||||||
| @ -70,6 +91,11 @@ pub enum MatchBinding { | |||||||
|     /// `None =>`
 |     /// `None =>`
 | ||||||
|     UnitVariant { name: IStr }, |     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, |     Ignore, | ||||||
| } | } | ||||||
| @ -173,6 +199,8 @@ pub enum Expression { | |||||||
|     IntLiteral { |     IntLiteral { | ||||||
|         val: i64, |         val: i64, | ||||||
|     }, |     }, | ||||||
|  |     BoolLiteral(bool), | ||||||
|  |     NoneLiteral, | ||||||
|     StringExpr(StringExpr), |     StringExpr(StringExpr), | ||||||
| 
 | 
 | ||||||
|     // Relatives
 |     // Relatives
 | ||||||
| @ -198,4 +226,9 @@ pub enum Expression { | |||||||
|         args: Vec<Expression>, |         args: Vec<Expression>, | ||||||
|         loc: Locator, |         loc: Locator, | ||||||
|     }, |     }, | ||||||
|  | 
 | ||||||
|  |     Unwrap { | ||||||
|  |         obj: Box<Expression>, | ||||||
|  |         loc: Locator, | ||||||
|  |     }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,5 +1,21 @@ | |||||||
| // | // | ||||||
| Hornbeam = { SOI ~ wsnl* ~ BlockContent* ~ ws* ~ EOI } | 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* } | ||||||
| 
 | 
 | ||||||
| NewBlock = _{ | NewBlock = _{ | ||||||
| 	PEEK_ALL ~ PUSH("  "+ | "\t"+) ~ BlockContent ~ | 	PEEK_ALL ~ PUSH("  "+ | "\t"+) ~ BlockContent ~ | ||||||
| @ -12,13 +28,21 @@ BlockContent = _{ | |||||||
| 	Text | | 	Text | | ||||||
|     RawUnescapedHtml | |     RawUnescapedHtml | | ||||||
| 	DefineExpandSlot | | 	DefineExpandSlot | | ||||||
|  | 	SetStatement | | ||||||
| 	ForBlock | | 	ForBlock | | ||||||
| 	MatchBlock | | 	MatchBlock | | ||||||
| 	DefineFragment | 	DefineFragment | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // `param $x` | ||||||
|  | // `param $x = 1 + 1` | ||||||
|  | // TODO: type annotation: `param $x: int` | ||||||
|  | ParameterDefinition = { | ||||||
|  |     "param" ~ ws+ ~ "$" ~ Identifier ~ (ws* ~ "=" ~ ws* ~ Expr)? ~ lineEnd | ||||||
|  | } | ||||||
|  | 
 | ||||||
| Element = { | Element = { | ||||||
| 	ElementName ~ cssClass* ~ domId? ~ (ws+ ~ MapLiteral)? ~ lineEnd ~ (NewBlock | NewSlotBlock)? | 	ElementName ~ cssClass* ~ domId? ~ (ws+ ~ AttrMapLiteral)? ~ lineEnd ~ (NewBlock | NewSlotBlock)? | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| cssClass = _{ | cssClass = _{ | ||||||
| @ -39,6 +63,10 @@ RawUnescapedHtml = { | |||||||
|     "raw" ~ ws+ ~ String ~ lineEnd |     "raw" ~ ws+ ~ String ~ lineEnd | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | SetStatement = { | ||||||
|  |     "set" ~ ws+ ~ Binding ~ ws+ ~ "=" ~ ws+ ~ Expr ~ lineEnd | ||||||
|  | } | ||||||
|  | 
 | ||||||
| IfBlock = { | IfBlock = { | ||||||
| 	"if" ~ ws+ ~ IfCondition ~ lineEnd ~ NewBlock ~ | 	"if" ~ ws+ ~ IfCondition ~ lineEnd ~ NewBlock ~ | ||||||
| 	ElseBlock? | 	ElseBlock? | ||||||
| @ -185,32 +213,43 @@ bnot = { "not" ~ ws+ } | |||||||
| 
 | 
 | ||||||
| // POSTFIX | // POSTFIX | ||||||
| postfix =  _{ unwrap | MethodCall | FieldLookup | Indexing } | 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. | // 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. | // This is probably for the best since we might have multiple backends for the templating. | ||||||
| MethodCall = { "." ~ ws* ~ Identifier ~ "(" ~ commaSeparatedExprs ~ ")" } | MethodCall = { "." ~ ws* ~ Identifier ~ "(" ~ commaSeparatedExprs ~ ")" } | ||||||
| FieldLookup = { "." ~ ws* ~ Identifier } | FieldLookup = { "." ~ ws* ~ Identifier } | ||||||
| Indexing = { "[" ~ ws* ~ Expr ~ ws* ~ "]" } | Indexing = { "[" ~ ws* ~ Expr ~ ws* ~ "]" } | ||||||
| 
 | 
 | ||||||
| Term =  _{ (IntLiteral | bracketedTerm | FunctionCall | ListLiteral | MapLiteral | String | Variable) } | Term =  _{ (IntLiteral | bracketedTerm | FunctionCall | ListLiteral | MapLiteral | String | Variable | NoneLiteral | TrueLiteral | FalseLiteral) } | ||||||
| 
 | 
 | ||||||
| bracketedTerm = _{ "(" ~ Expr ~ ")" } | bracketedTerm = _{ "(" ~ Expr ~ ")" } | ||||||
| 
 | 
 | ||||||
| 
 | NoneLiteral = { "None" } | ||||||
|  | TrueLiteral = { "true" } | ||||||
|  | FalseLiteral = { "false" } | ||||||
| IntLiteral =  @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) } | IntLiteral =  @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) } | ||||||
| Identifier = { (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } | // `-` 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 | "_" | "-")* } | ||||||
| commaSeparatedExprs = _{ wsnl* ~ (Expr ~ wsnl* ~ ("," ~ wsnl* ~ Expr ~ wsnl*)* ~ ("," ~ wsnl*)?)? } | commaSeparatedExprs = _{ wsnl* ~ (Expr ~ wsnl* ~ ("," ~ wsnl* ~ Expr ~ wsnl*)* ~ ("," ~ wsnl*)?)? } | ||||||
| FunctionCall = { Identifier ~ "(" ~ commaSeparatedExprs ~ ")" } | FunctionCall = { Identifier ~ "(" ~ commaSeparatedExprs ~ ")" } | ||||||
| Variable = { "$" ~ Identifier } | Variable = { "$" ~ Identifier } | ||||||
| 
 | 
 | ||||||
| ListLiteral = { "[" ~ commaSeparatedExprs ~  "]" } | ListLiteral = { "[" ~ commaSeparatedExprs ~  "]" } | ||||||
| 
 | 
 | ||||||
|  | // Basic key-value pairs forming a map literal {a = .., b = ..}. | ||||||
| KVPair = { Identifier ~ wsnl* ~ "=" ~ wsnl* ~ Expr } | KVPair = { Identifier ~ wsnl* ~ "=" ~ wsnl* ~ Expr } | ||||||
| KVarShorthand = { Variable } | KVarShorthand = { Variable } | ||||||
| commaSeparatedKVPairs = _{ wsnl* ~ ((KVPair | KVarShorthand) ~ wsnl* ~ ("," ~ wsnl* ~ (KVPair | KVarShorthand) ~ wsnl*)* ~ ("," ~ wsnl*)?)? } | commaSeparatedKVPairs = _{ wsnl* ~ ((KVPair | KVarShorthand) ~ wsnl* ~ ("," ~ wsnl* ~ (KVPair | KVarShorthand) ~ wsnl*)* ~ ("," ~ wsnl*)?)? } | ||||||
| MapLiteral = { "{" ~ commaSeparatedKVPairs ~ "}" } | 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. | // As in a let binding or for binding. | ||||||
|  | |||||||
| @ -1,8 +1,9 @@ | |||||||
| #![allow(non_snake_case)] | #![allow(non_snake_case, clippy::result_large_err)] | ||||||
| 
 | 
 | ||||||
| use crate::ast::{ | use crate::ast::{ | ||||||
|     Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, Expression, ForBlock, |     Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, ElementAttributeFlags, | ||||||
|     HtmlElement, IfBlock, MatchBinding, MatchBlock, StringExpr, StringPiece, Template, |     Expression, ForBlock, HtmlElement, IfBlock, MatchBinding, MatchBlock, ParameterDefinition, | ||||||
|  |     SetStatement, StringExpr, StringPiece, Template, | ||||||
| }; | }; | ||||||
| use crate::{intern, IStr, Locator}; | use crate::{intern, IStr, Locator}; | ||||||
| use lazy_static::lazy_static; | use lazy_static::lazy_static; | ||||||
| @ -60,8 +61,51 @@ lazy_static! { | |||||||
| #[pest_consume::parser] | #[pest_consume::parser] | ||||||
| impl HornbeamParser { | impl HornbeamParser { | ||||||
|     fn Hornbeam(input: Node) -> PCResult<Template> { |     fn Hornbeam(input: Node) -> PCResult<Template> { | ||||||
|         let blocks = HornbeamParser::helper_blocks(input.into_children())?; |         Ok(match_nodes!(input.into_children(); | ||||||
|         Ok(Template { blocks }) |             [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()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn Element(input: Node) -> PCResult<Block> { |     fn Element(input: Node) -> PCResult<Block> { | ||||||
| @ -77,7 +121,7 @@ impl HornbeamParser { | |||||||
|         let mut supply_slots = Vec::new(); |         let mut supply_slots = Vec::new(); | ||||||
|         let mut blocks = Vec::new(); |         let mut blocks = Vec::new(); | ||||||
| 
 | 
 | ||||||
|         while let Some(next) = children.next() { |         for next in children { | ||||||
|             match next.as_rule() { |             match next.as_rule() { | ||||||
|                 Rule::CssClass => { |                 Rule::CssClass => { | ||||||
|                     classes.push(HornbeamParser::CssClass(next)?); |                     classes.push(HornbeamParser::CssClass(next)?); | ||||||
| @ -88,8 +132,8 @@ impl HornbeamParser { | |||||||
|                 Rule::SupplySlot => { |                 Rule::SupplySlot => { | ||||||
|                     supply_slots.push(HornbeamParser::SupplySlot(next)?); |                     supply_slots.push(HornbeamParser::SupplySlot(next)?); | ||||||
|                 } |                 } | ||||||
|                 Rule::MapLiteral => { |                 Rule::AttrMapLiteral => { | ||||||
|                     attributes = HornbeamParser::MapLiteral(next)?; |                     attributes = HornbeamParser::AttrMapLiteral(next)?; | ||||||
|                 } |                 } | ||||||
|                 _ => { |                 _ => { | ||||||
|                     if let Some(block) = HornbeamParser::helper_block(next)? { |                     if let Some(block) = HornbeamParser::helper_block(next)? { | ||||||
| @ -113,6 +157,20 @@ impl HornbeamParser { | |||||||
|                 loc, |                 loc, | ||||||
|             }) |             }) | ||||||
|         } else { |         } 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() { |             if !supply_slots.is_empty() { | ||||||
|                 let mut slots = BTreeMap::new(); |                 let mut slots = BTreeMap::new(); | ||||||
|                 for (slot_name, slot_content_blocks, _slot_span) in supply_slots { |                 for (slot_name, slot_content_blocks, _slot_span) in supply_slots { | ||||||
| @ -278,6 +336,52 @@ impl HornbeamParser { | |||||||
|             .collect() |             .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> { |     fn SEscape(input: Node) -> PCResult<IStr> { | ||||||
|         let esc = input.as_str(); |         let esc = input.as_str(); | ||||||
|         Ok(match esc { |         Ok(match esc { | ||||||
| @ -299,6 +403,9 @@ impl HornbeamParser { | |||||||
|                 let node = Node::new_with_user_data(primary, ud.clone()); |                 let node = Node::new_with_user_data(primary, ud.clone()); | ||||||
|                 Ok(match node.as_rule() { |                 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::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::String => Expression::StringExpr(HornbeamParser::String(node)?), | ||||||
|                 Rule::Variable => HornbeamParser::Variable(node)?, |                 Rule::Variable => HornbeamParser::Variable(node)?, | ||||||
|                 Rule::FunctionCall => HornbeamParser::FunctionCall(node)?, |                 Rule::FunctionCall => HornbeamParser::FunctionCall(node)?, | ||||||
| @ -327,7 +434,9 @@ impl HornbeamParser { | |||||||
|                 let node = Node::new_with_user_data(op, ud.clone()); |                 let node = Node::new_with_user_data(op, ud.clone()); | ||||||
|                 let loc = nodeloc(&node); |                 let loc = nodeloc(&node); | ||||||
|                 Ok(match node.as_rule() { |                 Ok(match node.as_rule() { | ||||||
|                 Rule::unwrap => unimplemented!("unimp unwrap"), |                 Rule::unwrap => { | ||||||
|  |                     Expression::Unwrap { obj: Box::new(lhs?), loc } | ||||||
|  |                 }, | ||||||
|                 Rule::FieldLookup => { |                 Rule::FieldLookup => { | ||||||
|                     let ident = intern(node.into_children().single()?.as_str()); |                     let ident = intern(node.into_children().single()?.as_str()); | ||||||
|                     Expression::FieldLookup { obj: Box::new(lhs?), ident, loc } |                     Expression::FieldLookup { obj: Box::new(lhs?), ident, loc } | ||||||
| @ -354,6 +463,22 @@ 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> { |     fn IfCondition(input: Node) -> PCResult<Expression> { | ||||||
|         Self::Expr(input.into_children().single()?) |         Self::Expr(input.into_children().single()?) | ||||||
|     } |     } | ||||||
| @ -493,11 +618,12 @@ impl HornbeamParser { | |||||||
|         Ok(result) |         Ok(result) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn helper_block<'a>(input: Node) -> PCResult<Option<Block>> { |     fn helper_block(input: Node) -> PCResult<Option<Block>> { | ||||||
|         Ok(match input.as_rule() { |         Ok(match input.as_rule() { | ||||||
|             Rule::Element => Some(HornbeamParser::Element(input)?), |             Rule::Element => Some(HornbeamParser::Element(input)?), | ||||||
|             Rule::Text => Some(HornbeamParser::Text(input)?), |             Rule::Text => Some(HornbeamParser::Text(input)?), | ||||||
|             Rule::RawUnescapedHtml => Some(HornbeamParser::RawUnescapedHtml(input)?), |             Rule::RawUnescapedHtml => Some(HornbeamParser::RawUnescapedHtml(input)?), | ||||||
|  |             Rule::SetStatement => Some(HornbeamParser::SetStatement(input)?), | ||||||
|             Rule::IfBlock => Some(HornbeamParser::IfBlock(input)?), |             Rule::IfBlock => Some(HornbeamParser::IfBlock(input)?), | ||||||
|             Rule::ForBlock => Some(HornbeamParser::ForBlock(input)?), |             Rule::ForBlock => Some(HornbeamParser::ForBlock(input)?), | ||||||
|             Rule::MatchBlock => Some(HornbeamParser::MatchBlock(input)?), |             Rule::MatchBlock => Some(HornbeamParser::MatchBlock(input)?), | ||||||
| @ -538,6 +664,23 @@ div | |||||||
|         .unwrap()); |         .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] |     #[test] | ||||||
|     fn supply_slots_to_components_only() { |     fn supply_slots_to_components_only() { | ||||||
|         assert_debug_snapshot!(parse_template( |         assert_debug_snapshot!(parse_template( | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | 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()" | 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: | blocks: | ||||||
|   - ForBlock: |   - ForBlock: | ||||||
|       binding: |       binding: | ||||||
| @ -44,4 +45,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | 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()" | 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: | blocks: | ||||||
|   - HtmlElement: |   - HtmlElement: | ||||||
|       name: div |       name: div | ||||||
| @ -64,4 +65,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | 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()" | 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: | blocks: | ||||||
|   - IfBlock: |   - IfBlock: | ||||||
|       condition: |       condition: | ||||||
| @ -76,4 +77,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | 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()" | 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: | blocks: | ||||||
|   - IfBlock: |   - IfBlock: | ||||||
|       condition: |       condition: | ||||||
| @ -54,4 +55,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | 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()" | 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: | blocks: | ||||||
|   - HtmlElement: |   - HtmlElement: | ||||||
|       name: div |       name: div | ||||||
| @ -90,4 +91,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | 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()" | 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: | blocks: | ||||||
|   - IfBlock: |   - IfBlock: | ||||||
|       condition: |       condition: | ||||||
| @ -50,4 +51,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -0,0 +1,18 @@ | |||||||
|  | --- | ||||||
|  | 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: [] | ||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | 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()" | expression: "parse_template(r#\"\ndiv\n    span\n        raw \"<u>wow $x ${$x} @wowage{}</u>\"\n            \"#,\n        \"inp\").unwrap()" | ||||||
| --- | --- | ||||||
|  | param_defs: ~ | ||||||
| blocks: | blocks: | ||||||
|   - HtmlElement: |   - HtmlElement: | ||||||
|       name: div |       name: div | ||||||
| @ -42,4 +43,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | 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()" | expression: "parse_template(r#\"\n// This is a simple Hornbeam template that just shows a <div>\ndiv\n        \"#,\n        \"inp\").unwrap()" | ||||||
| --- | --- | ||||||
|  | param_defs: ~ | ||||||
| blocks: | blocks: | ||||||
|   - HtmlElement: |   - HtmlElement: | ||||||
|       name: div |       name: div | ||||||
| @ -13,4 +14,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 3 |         line: 3 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | 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()" | 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: | blocks: | ||||||
|   - ComponentElement: |   - ComponentElement: | ||||||
|       name: MyComponent |       name: MyComponent | ||||||
| @ -34,4 +35,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| source: hornbeam_grammar/src/parser.rs | source: hornbeam_grammar/src/parser.rs | ||||||
| expression: "parse_template(r#\"\nMyComponent\n    :someslot\n        \"That's better!\"\n        \"#,\n        \"inp\").unwrap()" | expression: "parse_template(r#\"\nMyComponent\n    :someslot\n        \"That's better!\"\n        \"#,\n        \"inp\").unwrap()" | ||||||
| --- | --- | ||||||
|  | param_defs: ~ | ||||||
| blocks: | blocks: | ||||||
|   - ComponentElement: |   - ComponentElement: | ||||||
|       name: MyComponent |       name: MyComponent | ||||||
| @ -15,4 +16,3 @@ blocks: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,18 +2,19 @@ | |||||||
| name = "hornbeam_interpreter" | name = "hornbeam_interpreter" | ||||||
| description = "Interpreter for the Hornbeam template language. This is the low-level implementation crate; not advised for direct use in applications." | 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" | license = "AGPL-3.0-or-later" | ||||||
| version = "0.0.3" | version = "0.0.5" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| hornbeam_grammar = { version = "0.0.3", path = "../hornbeam_grammar" } | hornbeam_grammar = { version = "0.0.5", path = "../hornbeam_grammar" } | ||||||
| hornbeam_ir = { version = "0.0.3", path = "../hornbeam_ir" } | hornbeam_ir = { version = "0.0.5", path = "../hornbeam_ir" } | ||||||
| 
 | 
 | ||||||
| fluent-templates = { version = "0.8.0", optional = true } | fluent-templates = { version = "0.8.0", optional = true } | ||||||
| bevy_reflect = { version = "0.11.0" } | bevy_reflect.workspace = true | ||||||
| html-escape = "0.2.13" | html-escape = "0.2.13" | ||||||
|  | formbeam = { version = "0.0.5", path = "../formbeam", optional = true } | ||||||
| 
 | 
 | ||||||
| walkdir = "2.3.2" | walkdir = "2.3.2" | ||||||
| 
 | 
 | ||||||
| @ -29,7 +30,8 @@ percent-encoding = "2.2.0" | |||||||
| 
 | 
 | ||||||
| [features] | [features] | ||||||
| default = ["fluent"] | default = ["fluent"] | ||||||
| fluent = ["fluent-templates"] | fluent = ["dep:fluent-templates"] | ||||||
|  | formbeam = ["dep:formbeam"] | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| insta = "1.38.0" | insta = "1.38.0" | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ use bevy_reflect::{FromReflect, Reflect, ReflectRef, VariantType}; | |||||||
| use fluent_templates::lazy_static::lazy_static; | use fluent_templates::lazy_static::lazy_static; | ||||||
| use hornbeam_grammar::ast::{Binding, MatchBinding}; | use hornbeam_grammar::ast::{Binding, MatchBinding}; | ||||||
| use hornbeam_grammar::Locator; | use hornbeam_grammar::Locator; | ||||||
| use hornbeam_ir::ir::{Expression, Step, StepDef, StringPiece}; | use hornbeam_ir::ir::{Expression, Step, StepDef, StringPiece, TemplateFunction}; | ||||||
| use itertools::Itertools; | use itertools::Itertools; | ||||||
| use std::any::TypeId; | use std::any::TypeId; | ||||||
| use std::borrow::Cow; | use std::borrow::Cow; | ||||||
| @ -28,7 +28,7 @@ pub(crate) struct Scope<'a> { | |||||||
| 
 | 
 | ||||||
| pub(crate) struct Interpreter<'a, O, LS> { | pub(crate) struct Interpreter<'a, O, LS> { | ||||||
|     pub(crate) entrypoint: String, |     pub(crate) entrypoint: String, | ||||||
|     pub(crate) program: &'a BTreeMap<String, Arc<Vec<Step>>>, |     pub(crate) program: &'a BTreeMap<String, Arc<TemplateFunction>>, | ||||||
|     pub(crate) output: O, |     pub(crate) output: O, | ||||||
|     pub(crate) localisation: Arc<LS>, |     pub(crate) localisation: Arc<LS>, | ||||||
|     pub(crate) locale: String, |     pub(crate) locale: String, | ||||||
| @ -52,7 +52,13 @@ impl Value { | |||||||
|             Value::Int(_) => "Int", |             Value::Int(_) => "Int", | ||||||
|             Value::Bool(_) => "Bool", |             Value::Bool(_) => "Bool", | ||||||
|             Value::List(_) => "List", |             Value::List(_) => "List", | ||||||
|             Value::Reflective(reflective) => reflective.type_name(), |             // TODO get rid of unwraps
 | ||||||
|  |             Value::Reflective(reflective) => reflective | ||||||
|  |                 .get_represented_type_info() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .type_path_table() | ||||||
|  |                 .ident() | ||||||
|  |                 .unwrap(), | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -216,7 +222,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                         .map_err(|underlying| InterpreterError::OutputError { underlying })?; |                         .map_err(|underlying| InterpreterError::OutputError { underlying })?; | ||||||
|                 } else { |                 } else { | ||||||
|                     self.output |                     self.output | ||||||
|                         .write(&text) |                         .write(text) | ||||||
|                         .await |                         .await | ||||||
|                         .map_err(|underlying| InterpreterError::OutputError { underlying })?; |                         .map_err(|underlying| InterpreterError::OutputError { underlying })?; | ||||||
|                 } |                 } | ||||||
| @ -237,6 +243,20 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                     .await |                     .await | ||||||
|                     .map_err(|underlying| InterpreterError::OutputError { underlying })?; |                     .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 { |             StepDef::If { | ||||||
|                 condition, |                 condition, | ||||||
|                 true_steps, |                 true_steps, | ||||||
| @ -274,7 +294,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                         } else { |                         } else { | ||||||
|                             for val in list { |                             for val in list { | ||||||
|                                 let mut binder = Binder::new(); |                                 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?; |                                 self.run_steps(scope_idx, body_steps).await?; | ||||||
|                                 binder.unbind(&mut self.scopes[scope_idx].variables); |                                 binder.unbind(&mut self.scopes[scope_idx].variables); | ||||||
|                             } |                             } | ||||||
| @ -292,7 +312,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                                     let mut binder = Binder::new(); |                                     let mut binder = Binder::new(); | ||||||
|                                     binder.bind( |                                     binder.bind( | ||||||
|                                         &mut self.scopes[scope_idx].variables, |                                         &mut self.scopes[scope_idx].variables, | ||||||
|                                         &binding, |                                         binding, | ||||||
|                                         Value::from_reflect(val), |                                         Value::from_reflect(val), | ||||||
|                                     ); |                                     ); | ||||||
|                                     self.run_steps(scope_idx, body_steps).await?; |                                     self.run_steps(scope_idx, body_steps).await?; | ||||||
| @ -343,10 +363,10 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                                     } |                                     } | ||||||
|                                 }, |                                 }, | ||||||
|                                 _ => { |                                 _ => { | ||||||
|                                     warn!( |                                     // warn!(
 | ||||||
|                                         "trying to `match` non-reflective vs {name} at {}", |                                     //     "trying to `match` non-reflective vs {name} at {}",
 | ||||||
|                                         step.locator |                                     //     step.locator
 | ||||||
|                                     ); |                                     // );
 | ||||||
|                                     continue; |                                     continue; | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
| @ -396,14 +416,19 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                                     } |                                     } | ||||||
|                                 }, |                                 }, | ||||||
|                                 _ => { |                                 _ => { | ||||||
|                                     warn!( |                                     // warn!(
 | ||||||
|                                         "trying to `match` non-reflective vs {name}(...) at {}", |                                     //     "trying to `match` non-reflective vs {name}(...) at {}",
 | ||||||
|                                         step.locator |                                     //     step.locator
 | ||||||
|                                     ); |                                     // );
 | ||||||
|                                     continue; |                                     continue; | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  |                         MatchBinding::Variable { name } => binder.bind( | ||||||
|  |                             &mut self.scopes[scope_idx].variables, | ||||||
|  |                             &Binding::Variable(name.clone()), | ||||||
|  |                             matchable_evaled, | ||||||
|  |                         ), | ||||||
|                         MatchBinding::Ignore => { |                         MatchBinding::Ignore => { | ||||||
|                             // always matches: no variable to bind, no conditions to check!
 |                             // always matches: no variable to bind, no conditions to check!
 | ||||||
|                         } |                         } | ||||||
| @ -420,6 +445,14 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             StepDef::Call { name, args, slots } => { |             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(); |                 let mut evaled_args = BTreeMap::new(); | ||||||
|                 for (key, expr) in args { |                 for (key, expr) in args { | ||||||
|                     evaled_args.insert( |                     evaled_args.insert( | ||||||
| @ -439,23 +472,67 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                     ); |                     ); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 self.scopes.push(Scope { |                 // TODO check slots
 | ||||||
|                     variables: evaled_args, |                 // ...
 | ||||||
|                     slots: filled_in_slots, |  | ||||||
|                 }); |  | ||||||
|                 let next_scope_idx = self.scopes.len() - 1; |  | ||||||
| 
 | 
 | ||||||
|                 let steps = if let Some(steps) = self.program.get(name as &str) { |                 // check params and evaluate defaults
 | ||||||
|                     steps |                 if let Some(param_defs) = &module.param_defs { | ||||||
|                 } else { |                     for param_def in param_defs { | ||||||
|  |                         if evaled_args.contains_key(param_def.name.as_str()) { | ||||||
|  |                             continue; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         let Some(default_expr) = ¶m_def.default else { | ||||||
|                             return Err(InterpreterError::TypeError { |                             return Err(InterpreterError::TypeError { | ||||||
|                                 context: "Call".to_string(), |                                 context: "Call".to_string(), | ||||||
|                         conflict: format!("no entrypoint for {name:?}."), |                                 conflict: format!( | ||||||
|  |                                     "missing required parameter {} for {name:?}.", | ||||||
|  |                                     param_def.name | ||||||
|  |                                 ), | ||||||
|                                 location: step.locator.clone(), |                                 location: step.locator.clone(), | ||||||
|                             }); |                             }); | ||||||
|                         }; |                         }; | ||||||
| 
 | 
 | ||||||
|                 self.run_steps(next_scope_idx, steps).await?; |                         // 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, | ||||||
|  |                                 ¶m_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?; | ||||||
| 
 | 
 | ||||||
|                 self.scopes.pop(); |                 self.scopes.pop(); | ||||||
|                 assert_eq!(self.scopes.len(), next_scope_idx); |                 assert_eq!(self.scopes.len(), next_scope_idx); | ||||||
| @ -468,7 +545,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                         return if !optional { |                         return if !optional { | ||||||
|                             Err(InterpreterError::TypeError { |                             Err(InterpreterError::TypeError { | ||||||
|                                 context: format!("Required slot '{name}' not filled"), |                                 context: format!("Required slot '{name}' not filled"), | ||||||
|                                 conflict: format!("slot was left empty."), |                                 conflict: "slot was left empty.".to_string(), | ||||||
|                                 location: step.locator.clone(), |                                 location: step.locator.clone(), | ||||||
|                             }) |                             }) | ||||||
|                         } else { |                         } else { | ||||||
| @ -493,8 +570,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|     ) -> Result<Value, InterpreterError<LS::Error, O::Error>> { |     ) -> Result<Value, InterpreterError<LS::Error, O::Error>> { | ||||||
|         match expr { |         match expr { | ||||||
|             Expression::Add { left, right } => { |             Expression::Add { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint + rint)), |                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint + rint)), | ||||||
| @ -506,8 +583,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::Sub { left, right } => { |             Expression::Sub { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint - rint)), |                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint - rint)), | ||||||
| @ -519,8 +596,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::Mul { left, right } => { |             Expression::Mul { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint * rint)), |                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint * rint)), | ||||||
| @ -532,8 +609,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::Div { left, right } => { |             Expression::Div { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint / rint)), |                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint / rint)), | ||||||
| @ -545,7 +622,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::Negate { sub } => { |             Expression::Negate { sub } => { | ||||||
|                 let sval = self.evaluate_expression(scope_idx, &sub, loc)?; |                 let sval = self.evaluate_expression(scope_idx, sub, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match sval { |                 match sval { | ||||||
|                     Value::Int(sint) => Ok(Value::Int(-sint)), |                     Value::Int(sint) => Ok(Value::Int(-sint)), | ||||||
| @ -557,8 +634,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::BAnd { left, right } => { |             Expression::BAnd { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool && rbool)), |                     (Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool && rbool)), | ||||||
| @ -570,8 +647,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::BOr { left, right } => { |             Expression::BOr { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool || rbool)), |                     (Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool || rbool)), | ||||||
| @ -583,7 +660,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::BNot { sub } => { |             Expression::BNot { sub } => { | ||||||
|                 let sval = self.evaluate_expression(scope_idx, &sub, loc)?; |                 let sval = self.evaluate_expression(scope_idx, sub, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match sval { |                 match sval { | ||||||
|                     Value::Bool(sbool) => Ok(Value::Bool(!sbool)), |                     Value::Bool(sbool) => Ok(Value::Bool(!sbool)), | ||||||
| @ -595,8 +672,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::Equals { left, right } => { |             Expression::Equals { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool == rbool)), |                     (Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool == rbool)), | ||||||
| @ -621,8 +698,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::LessThan { left, right } => { |             Expression::LessThan { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Bool(lint < rint)), |                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Bool(lint < rint)), | ||||||
| @ -635,8 +712,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::LessThanOrEquals { left, right } => { |             Expression::LessThanOrEquals { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Bool(lint <= rint)), |                     (Value::Int(lint), Value::Int(rint)) => Ok(Value::Bool(lint <= rint)), | ||||||
| @ -649,8 +726,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Expression::ListAdd { left, right } => { |             Expression::ListAdd { left, right } => { | ||||||
|                 let lval = self.evaluate_expression(scope_idx, &left, loc)?; |                 let lval = self.evaluate_expression(scope_idx, left, loc)?; | ||||||
|                 let rval = self.evaluate_expression(scope_idx, &right, loc)?; |                 let rval = self.evaluate_expression(scope_idx, right, loc)?; | ||||||
| 
 | 
 | ||||||
|                 match (lval, rval) { |                 match (lval, rval) { | ||||||
|                     (Value::List(mut llist), Value::List(rlist)) => { |                     (Value::List(mut llist), Value::List(rlist)) => { | ||||||
| @ -672,6 +749,8 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 Ok(Value::List(result)) |                 Ok(Value::List(result)) | ||||||
|             } |             } | ||||||
|             Expression::IntLiteral { val } => Ok(Value::Int(*val)), |             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) => { |             Expression::StringExpr(sexpr) => { | ||||||
|                 let mut output = String::new(); |                 let mut output = String::new(); | ||||||
|                 for piece in &sexpr.pieces { |                 for piece in &sexpr.pieces { | ||||||
| @ -738,7 +817,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|                 let Some(method) = self.methods.get(ident.as_str()) else { |                 let Some(method) = self.methods.get(ident.as_str()) else { | ||||||
|                     return Err(InterpreterError::TypeError { |                     return Err(InterpreterError::TypeError { | ||||||
|                         context: format!("method call to {ident:?}"), |                         context: format!("method call to {ident:?}"), | ||||||
|                         conflict: format!("No method by that name!"), |                         conflict: "No method by that name!".to_string(), | ||||||
|                         location: loc.clone(), |                         location: loc.clone(), | ||||||
|                     }); |                     }); | ||||||
|                 }; |                 }; | ||||||
| @ -777,6 +856,54 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|             Expression::FunctionCall { .. } => { |             Expression::FunctionCall { .. } => { | ||||||
|                 unimplemented!() |                 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) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -814,9 +941,8 @@ 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.
 |                 // too dodgy! We might want to allow this in the future, but for now let's not.
 | ||||||
|                 Err(InterpreterError::TypeError { |                 Err(InterpreterError::TypeError { | ||||||
|                     context: "String Interpolation".to_string(), |                     context: "String Interpolation".to_string(), | ||||||
|                     conflict: format!( |                     conflict: "Don't know how to write List[...] as a sensible string output." | ||||||
|                         "Don't know how to write List[...] as a sensible string output." |                         .to_string(), | ||||||
|                     ), |  | ||||||
|                     location: loc.clone(), |                     location: loc.clone(), | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
| @ -832,7 +958,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|     ) -> Result<(), InterpreterError<LS::Error, O::Error>> { |     ) -> Result<(), InterpreterError<LS::Error, O::Error>> { | ||||||
|         match piece { |         match piece { | ||||||
|             StringPiece::Literal(lit) => { |             StringPiece::Literal(lit) => { | ||||||
|                 output.push_str(&lit); |                 output.push_str(lit); | ||||||
|                 Ok(()) |                 Ok(()) | ||||||
|             } |             } | ||||||
|             StringPiece::Interpolation(expr) => { |             StringPiece::Interpolation(expr) => { | ||||||
| @ -867,16 +993,63 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn run(mut self) -> Result<(), InterpreterError<LS::Error, O::Error>> { |     pub async fn run(mut self) -> Result<(), InterpreterError<LS::Error, O::Error>> { | ||||||
|         let main = if let Some(main) = self.program.get(&self.entrypoint) { |         let Some(main) = self.program.get(&self.entrypoint) else { | ||||||
|             main |  | ||||||
|         } else { |  | ||||||
|             return Err(InterpreterError::TypeError { |             return Err(InterpreterError::TypeError { | ||||||
|                 context: format!("No entrypoint called {:?}", self.entrypoint), |                 context: format!("No entrypoint called {:?}", self.entrypoint), | ||||||
|                 conflict: "".to_string(), |                 conflict: "".to_string(), | ||||||
|                 location: Locator::empty(), |                 location: Locator::empty(), | ||||||
|             }); |             }); | ||||||
|         }; |         }; | ||||||
|         self.run_steps(0, main).await?; |         // 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) = ¶m_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, ¶m_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?; | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										62
									
								
								hornbeam_interpreter/src/formbeam_integration.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								hornbeam_interpreter/src/formbeam_integration.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | |||||||
|  | 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()), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,6 +1,8 @@ | |||||||
| use crate::interface::Value; | use crate::interface::Value; | ||||||
| 
 | 
 | ||||||
| pub(crate) mod defaults; | pub(crate) mod defaults; | ||||||
|  | #[cfg(feature = "formbeam")] | ||||||
|  | pub(crate) mod formbeam_integration; | ||||||
| 
 | 
 | ||||||
| /// A method that can be accessed (called) by templates.
 | /// 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,
 | /// There is no dynamic dispatch for methods: the name of the method is the only thing that determines which one to call,
 | ||||||
|  | |||||||
| @ -1,19 +1,23 @@ | |||||||
| use std::{collections::BTreeMap, sync::Arc}; | use std::{collections::BTreeMap, sync::Arc}; | ||||||
| 
 | 
 | ||||||
|  | use bevy_reflect::{ReflectRef, VariantType}; | ||||||
| use percent_encoding::NON_ALPHANUMERIC; | use percent_encoding::NON_ALPHANUMERIC; | ||||||
| 
 | 
 | ||||||
| use crate::interface::Value; | use crate::interface::Value; | ||||||
| 
 | 
 | ||||||
| use super::TemplateAccessibleMethod; | use super::TemplateAccessibleMethod; | ||||||
| 
 | 
 | ||||||
| const DEFAULT_TEMPLATE_ACCESSIBLE_METHODS: &'static [( | #[allow(clippy::type_complexity)] | ||||||
|     &'static str, | const DEFAULT_TEMPLATE_ACCESSIBLE_METHODS: &[( | ||||||
|  |     &str, | ||||||
|     fn(Value, Vec<Value>) -> Result<Value, String>, |     fn(Value, Vec<Value>) -> Result<Value, String>, | ||||||
| )] = &[ | )] = &[ | ||||||
|     ("leftpad", leftpad), |     ("leftpad", leftpad), | ||||||
|     ("urlencode", urlencode), |     ("urlencode", urlencode), | ||||||
|     ("len", len), |     ("len", len), | ||||||
|     ("split", split), |     ("split", split), | ||||||
|  |     ("unwrap_or", unwrap_or), | ||||||
|  |     ("__get", __get), | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| /// Return a map of the default suggested template-accessible methods.
 | /// Return a map of the default suggested template-accessible methods.
 | ||||||
| @ -43,12 +47,12 @@ pub fn leftpad(obj: Value, args: Vec<Value>) -> Result<Value, String> { | |||||||
|         )); |         )); | ||||||
|     } |     } | ||||||
|     let Value::Int(pad_length) = &args[0] else { |     let Value::Int(pad_length) = &args[0] else { | ||||||
|         return Err(format!("leftpad's first arg should be an integer")); |         return Err("leftpad's first arg should be an integer".to_owned()); | ||||||
|     }; |     }; | ||||||
|     let Value::Str(padding_character) = &args[1] else { |     let Value::Str(padding_character) = &args[1] else { | ||||||
|         return Err(format!( |         return Err( | ||||||
|             "leftpad's second arg should be a string (usually a single character)" |             "leftpad's second arg should be a string (usually a single character)".to_owned(), | ||||||
|         )); |         ); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     if string_to_pad.len() as i64 >= *pad_length { |     if string_to_pad.len() as i64 >= *pad_length { | ||||||
| @ -61,7 +65,7 @@ pub fn leftpad(obj: Value, args: Vec<Value>) -> Result<Value, String> { | |||||||
| 
 | 
 | ||||||
|     let mut result = String::new(); |     let mut result = String::new(); | ||||||
|     for _ in 0..repetitions { |     for _ in 0..repetitions { | ||||||
|         result.push_str(&padding_character); |         result.push_str(padding_character); | ||||||
|     } |     } | ||||||
|     result.push_str(&string_to_pad); |     result.push_str(&string_to_pad); | ||||||
| 
 | 
 | ||||||
| @ -75,7 +79,7 @@ pub fn urlencode(obj: Value, args: Vec<Value>) -> Result<Value, String> { | |||||||
|     let Value::Str(string_to_encode) = obj else { |     let Value::Str(string_to_encode) = obj else { | ||||||
|         return Err(format!("{obj:?} is not a string: can't urlencode!")); |         return Err(format!("{obj:?} is not a string: can't urlencode!")); | ||||||
|     }; |     }; | ||||||
|     if args.len() != 0 { |     if !args.is_empty() { | ||||||
|         return Err(format!("urlencode takes 0 args, not {}", args.len())); |         return Err(format!("urlencode takes 0 args, not {}", args.len())); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -89,19 +93,29 @@ pub fn urlencode(obj: Value, args: Vec<Value>) -> Result<Value, String> { | |||||||
| /// - `<Str>.len() -> Int`
 | /// - `<Str>.len() -> Int`
 | ||||||
| /// - `<List>.len() -> Int`
 | /// - `<List>.len() -> Int`
 | ||||||
| pub fn len(obj: Value, args: Vec<Value>) -> Result<Value, String> { | pub fn len(obj: Value, args: Vec<Value>) -> Result<Value, String> { | ||||||
|     if args.len() != 0 { |     if !args.is_empty() { | ||||||
|         return Err(format!("len takes 0 args, not {}", args.len())); |         return Err(format!("len takes 0 args, not {}", args.len())); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     match obj { |     Ok(Value::Int(match obj { | ||||||
|         Value::Str(string) => Ok(Value::Int(string.len() as i64)), |         Value::Str(string) => string.len() as i64, | ||||||
|         Value::List(list) => Ok(Value::Int(list.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!( |             return Err(format!( | ||||||
|                 "{obj:?} is not a string or list: can't get length!" |                 "{obj:?} is not a string or list: can't get length!" | ||||||
|             )); |             )); | ||||||
|         } |         } | ||||||
|     } |     })) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Splits a string by given delimiters.
 | /// Splits a string by given delimiters.
 | ||||||
| @ -117,7 +131,7 @@ pub fn split(obj: Value, args: Vec<Value>) -> Result<Value, String> { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let Value::Str(delimiter) = &args[0] else { |     let Value::Str(delimiter) = &args[0] else { | ||||||
|         return Err(format!("first arg is not a string: can't split!")); |         return Err("first arg is not a string: can't split!".to_owned()); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let result = string_to_split |     let result = string_to_split | ||||||
| @ -127,3 +141,77 @@ pub fn split(obj: Value, args: Vec<Value>) -> Result<Value, String> { | |||||||
| 
 | 
 | ||||||
|     Ok(Value::List(result)) |     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")), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										110
									
								
								hornbeam_interpreter/src/functions/formbeam_integration.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								hornbeam_interpreter/src/functions/formbeam_integration.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,110 @@ | |||||||
|  | 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
 | ||||||
| @ -4,7 +4,7 @@ use async_trait::async_trait; | |||||||
| use bevy_reflect::Reflect; | use bevy_reflect::Reflect; | ||||||
| use hornbeam_grammar::parse_template; | use hornbeam_grammar::parse_template; | ||||||
| use hornbeam_ir::ast_to_optimised_ir; | use hornbeam_ir::ast_to_optimised_ir; | ||||||
| use hornbeam_ir::ir::Step; | use hornbeam_ir::ir::TemplateFunction; | ||||||
| use std::borrow::Cow; | use std::borrow::Cow; | ||||||
| use std::collections::BTreeMap; | use std::collections::BTreeMap; | ||||||
| use std::convert::Infallible; | use std::convert::Infallible; | ||||||
| @ -39,12 +39,14 @@ pub trait OutputSystem { | |||||||
| 
 | 
 | ||||||
| // Value is currently used in the localisation system. We might pull it away later on...
 | // Value is currently used in the localisation system. We might pull it away later on...
 | ||||||
| pub use crate::engine::Value; | pub use crate::engine::Value; | ||||||
|  | #[cfg(feature = "formbeam")] | ||||||
|  | use crate::formbeam_template_accessible_methods; | ||||||
| use crate::{default_template_accessible_methods, InterpreterError}; | use crate::{default_template_accessible_methods, InterpreterError}; | ||||||
| 
 | 
 | ||||||
| pub struct LoadedTemplates<LS> { | pub struct LoadedTemplates<LS> { | ||||||
|     // todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent?
 |     // todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent?
 | ||||||
|     // or do we just staticify?
 |     // or do we just staticify?
 | ||||||
|     template_functions: BTreeMap<String, Arc<Vec<Step>>>, |     template_functions: BTreeMap<String, Arc<TemplateFunction>>, | ||||||
| 
 | 
 | ||||||
|     methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>, |     methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>, | ||||||
| 
 | 
 | ||||||
| @ -66,13 +68,26 @@ impl Params { | |||||||
| 
 | 
 | ||||||
| impl<'a, LS> LoadedTemplates<LS> { | impl<'a, LS> LoadedTemplates<LS> { | ||||||
|     pub fn new(localisation_system: LS) -> Self { |     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 { |         LoadedTemplates { | ||||||
|             template_functions: Default::default(), |             template_functions: Default::default(), | ||||||
|             methods: Arc::new(default_template_accessible_methods()), |             methods: Arc::new(methods), | ||||||
|             localisation: Arc::new(localisation_system), |             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 { |     pub fn unload_template(&mut self, template_name: &str) -> bool { | ||||||
|         let was_removed = self.template_functions.remove(template_name).is_some(); |         let was_removed = self.template_functions.remove(template_name).is_some(); | ||||||
| 
 | 
 | ||||||
| @ -155,8 +170,10 @@ impl<'a, LS> LoadedTemplates<LS> { | |||||||
|         params: Params, |         params: Params, | ||||||
|         locale: String, |         locale: String, | ||||||
|     ) -> PreparedTemplate<LS> { |     ) -> 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 { |         PreparedTemplate { | ||||||
|             all_instructions: Arc::new(self.template_functions.clone()), |             template_functions: Arc::new(self.template_functions.clone()), | ||||||
|             entrypoint: if let Some(frag) = fragment_name { |             entrypoint: if let Some(frag) = fragment_name { | ||||||
|                 format!("{template_name}__{frag}") |                 format!("{template_name}__{frag}") | ||||||
|             } else { |             } else { | ||||||
| @ -171,7 +188,7 @@ impl<'a, LS> LoadedTemplates<LS> { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct PreparedTemplate<LS> { | pub struct PreparedTemplate<LS> { | ||||||
|     pub(crate) all_instructions: Arc<BTreeMap<String, Arc<Vec<Step>>>>, |     pub(crate) template_functions: Arc<BTreeMap<String, Arc<TemplateFunction>>>, | ||||||
|     pub(crate) methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>, |     pub(crate) methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>, | ||||||
|     pub(crate) entrypoint: String, |     pub(crate) entrypoint: String, | ||||||
|     pub(crate) variables: Params, |     pub(crate) variables: Params, | ||||||
| @ -186,7 +203,7 @@ impl<LS: LocalisationSystem + Sync + Send> PreparedTemplate<LS> { | |||||||
|     ) -> Result<(), InterpreterError<LS::Error, O::Error>> { |     ) -> Result<(), InterpreterError<LS::Error, O::Error>> { | ||||||
|         let interpreter = Interpreter { |         let interpreter = Interpreter { | ||||||
|             entrypoint: self.entrypoint, |             entrypoint: self.entrypoint, | ||||||
|             program: &self.all_instructions, |             program: &self.template_functions, | ||||||
|             output, |             output, | ||||||
|             localisation: self.localisation, |             localisation: self.localisation, | ||||||
|             locale: self.locale, |             locale: self.locale, | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| use std::fmt::Debug; | use std::fmt::Debug; | ||||||
| 
 | 
 | ||||||
| mod engine; | mod engine; | ||||||
|  | #[cfg(feature = "formbeam")] | ||||||
|  | pub mod formbeam_integration; | ||||||
| mod functions; | mod functions; | ||||||
| pub(crate) mod interface; | pub(crate) mod interface; | ||||||
| 
 | 
 | ||||||
| @ -27,10 +29,10 @@ pub enum InterpreterError<LE: Debug + Clone, OE: Debug> { | |||||||
|     #[error("failed to write to output: {underlying:?}")] |     #[error("failed to write to output: {underlying:?}")] | ||||||
|     OutputError { underlying: OE }, |     OutputError { underlying: OE }, | ||||||
| 
 | 
 | ||||||
|     #[error("failed to parse template: ")] |     #[error("failed to parse template: {0}")] | ||||||
|     ParseError(#[from] ParseError), |     ParseError(#[from] ParseError), | ||||||
| 
 | 
 | ||||||
|     #[error("failed to process parsed template: ")] |     #[error("failed to process parsed template: {0}")] | ||||||
|     AstToIrError(#[from] AstToIrError), |     AstToIrError(#[from] AstToIrError), | ||||||
| 
 | 
 | ||||||
|     #[error("error finding templates to load: {0}")] |     #[error("error finding templates to load: {0}")] | ||||||
| @ -38,5 +40,7 @@ pub enum InterpreterError<LE: Debug + Clone, OE: Debug> { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub use functions::defaults::default_template_accessible_methods; | 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 functions::TemplateAccessibleMethod; | ||||||
| pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate}; | pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate}; | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ impl LocalisationSystem for NoLocalisation { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ///
 | /// Localisation system that dumps debug output.
 | ||||||
| #[derive(Copy, Clone)] | #[derive(Copy, Clone)] | ||||||
| pub struct DebugLocalisationSystem; | pub struct DebugLocalisationSystem; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ use thiserror::Error; | |||||||
| 
 | 
 | ||||||
| fn interpreter_value_to_fluent_value(v: &Value) -> Option<FluentValue> { | fn interpreter_value_to_fluent_value(v: &Value) -> Option<FluentValue> { | ||||||
|     match v { |     match v { | ||||||
|         Value::Str(str) => Some(FluentValue::String(Cow::Borrowed(&str))), |         Value::Str(str) => Some(FluentValue::String(Cow::Borrowed(str))), | ||||||
|         Value::Int(int) => { |         Value::Int(int) => { | ||||||
|             // This is an unstyled number
 |             // This is an unstyled number
 | ||||||
|             // TODO Support fancier numbers
 |             // TODO Support fancier numbers
 | ||||||
| @ -97,12 +97,10 @@ impl LocalisationSystem for FluentLocalisationSystem { | |||||||
|         } |         } | ||||||
|         match self.fluent.lookup_with_args(&li, trans_key, &mapped_params) { |         match self.fluent.lookup_with_args(&li, trans_key, &mapped_params) { | ||||||
|             Some(val) => Ok(Cow::Owned(val)), |             Some(val) => Ok(Cow::Owned(val)), | ||||||
|             None => { |             None => Err(FluentLocalisationError::NoTranslation { | ||||||
|                 return Err(FluentLocalisationError::NoTranslation { |  | ||||||
|                 trans_key: String::from(trans_key), |                 trans_key: String::from(trans_key), | ||||||
|                 lang_id: li.to_string(), |                 lang_id: li.to_string(), | ||||||
|                 }); |             }), | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ fn simple_test_struct() -> SimpleTestStruct { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[track_caller] | ||||||
| fn simple_render(template: &str) -> String { | fn simple_render(template: &str) -> String { | ||||||
|     let mut templates = LoadedTemplates::new(DebugLocalisationSystem); |     let mut templates = LoadedTemplates::new(DebugLocalisationSystem); | ||||||
|     templates |     templates | ||||||
| @ -85,3 +86,45 @@ for $part in $sts.carrot.split("A") | |||||||
|         "#
 |         "#
 | ||||||
|     )) |     )) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[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" | ||||||
|  |         "#
 | ||||||
|  |     )) | ||||||
|  | } | ||||||
|  | |||||||
| @ -0,0 +1,5 @@ | |||||||
|  | --- | ||||||
|  | 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! | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | --- | ||||||
|  | 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 | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | --- | ||||||
|  | source: hornbeam_interpreter/tests/snapshots.rs | ||||||
|  | expression: "simple_render(r#\"\ndeclare\n    param $default_param = \"default value!\"\n\n\"$default_param\"\n        \"#)" | ||||||
|  | --- | ||||||
|  | default value! | ||||||
| @ -2,13 +2,13 @@ | |||||||
| name = "hornbeam_ir" | name = "hornbeam_ir" | ||||||
| description = "Intermediate representation for the Hornbeam template language" | description = "Intermediate representation for the Hornbeam template language" | ||||||
| license = "AGPL-3.0-or-later" | license = "AGPL-3.0-or-later" | ||||||
| version = "0.0.3" | version = "0.0.5" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| hornbeam_grammar = { version = "0.0.3", path = "../hornbeam_grammar" } | hornbeam_grammar = { version = "0.0.5", path = "../hornbeam_grammar" } | ||||||
| thiserror = "1.0.38" | thiserror = "1.0.38" | ||||||
| serde = { version = "1.0.152", features = ["derive"] } | serde = { version = "1.0.152", features = ["derive"] } | ||||||
| itertools = "0.10.5" | itertools = "0.10.5" | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| use crate::ir::{Step, StepDef}; | use crate::ir::{ParamDef, Step, StepDef, TemplateFunction}; | ||||||
| use hornbeam_grammar::ast::{Block, Expression, StringExpr, StringPiece, Template}; | use hornbeam_grammar::ast::{ | ||||||
| use hornbeam_grammar::{intern, Locator}; |     Binding, Block, Expression, HtmlElement, MatchBinding, StringExpr, StringPiece, Template, | ||||||
|  | }; | ||||||
|  | use hornbeam_grammar::{intern, IStr, Locator}; | ||||||
| use itertools::Itertools; | use itertools::Itertools; | ||||||
| use std::borrow::Cow; | use std::borrow::Cow; | ||||||
| use std::collections::btree_map::Entry; | use std::collections::btree_map::Entry; | ||||||
| @ -8,9 +10,8 @@ use std::collections::BTreeMap; | |||||||
| use std::ops::Deref; | use std::ops::Deref; | ||||||
| use thiserror::Error; | use thiserror::Error; | ||||||
| 
 | 
 | ||||||
| // TODO use the void tags
 |  | ||||||
| /// List of all void (self-closing) HTML tags.
 | /// List of all void (self-closing) HTML tags.
 | ||||||
| const VOID_TAGS: &'static [&'static str] = &[ | const VOID_TAGS: &[&str] = &[ | ||||||
|     "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "menuitem", "meta", |     "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "menuitem", "meta", | ||||||
|     "param", "source", "track", "wbr", |     "param", "source", "track", "wbr", | ||||||
| ]; | ]; | ||||||
| @ -32,10 +33,10 @@ pub enum AstToIrError { | |||||||
| ///
 | ///
 | ||||||
| /// Fragments are extracted to `{template_name}__{fragment_name}`.
 | /// Fragments are extracted to `{template_name}__{fragment_name}`.
 | ||||||
| /// The top-level template is extracted to `{template_name}`.
 | /// The top-level template is extracted to `{template_name}`.
 | ||||||
| pub(crate) fn pull_out_entrypoints<'a>( | pub(crate) fn pull_out_entrypoints( | ||||||
|     mut template: Template, |     mut template: Template, | ||||||
|     template_name: &str, |     template_name: &str, | ||||||
| ) -> Result<BTreeMap<String, Vec<Block>>, AstToIrError> { | ) -> Result<BTreeMap<String, Template>, AstToIrError> { | ||||||
|     let mut functions = BTreeMap::new(); |     let mut functions = BTreeMap::new(); | ||||||
| 
 | 
 | ||||||
|     for child in &mut template.blocks { |     for child in &mut template.blocks { | ||||||
| @ -44,7 +45,7 @@ pub(crate) fn pull_out_entrypoints<'a>( | |||||||
| 
 | 
 | ||||||
|     match functions.entry(template_name.to_owned()) { |     match functions.entry(template_name.to_owned()) { | ||||||
|         Entry::Vacant(ve) => { |         Entry::Vacant(ve) => { | ||||||
|             ve.insert(template.blocks); |             ve.insert(template); | ||||||
|         } |         } | ||||||
|         Entry::Occupied(_) => { |         Entry::Occupied(_) => { | ||||||
|             return Err(AstToIrError::SemanticError { |             return Err(AstToIrError::SemanticError { | ||||||
| @ -62,10 +63,10 @@ pub(crate) fn pull_out_entrypoints<'a>( | |||||||
| 
 | 
 | ||||||
| /// Extract entrypoints (template fragments) from a block (recursively), replacing them with
 | /// Extract entrypoints (template fragments) from a block (recursively), replacing them with
 | ||||||
| /// calls to that entrypoint.
 | /// calls to that entrypoint.
 | ||||||
| fn pull_out_entrypoints_from_block<'a>( | fn pull_out_entrypoints_from_block( | ||||||
|     block: &mut Block, |     block: &mut Block, | ||||||
|     template_name: &str, |     template_name: &str, | ||||||
|     target: &mut BTreeMap<String, Vec<Block>>, |     target: &mut BTreeMap<String, Template>, | ||||||
| ) -> Result<(), AstToIrError> { | ) -> Result<(), AstToIrError> { | ||||||
|     match block { |     match block { | ||||||
|         Block::HtmlElement(he) => { |         Block::HtmlElement(he) => { | ||||||
| @ -121,7 +122,10 @@ fn pull_out_entrypoints_from_block<'a>( | |||||||
|                     // function.
 |                     // function.
 | ||||||
|                     let mut blocks = Vec::new(); |                     let mut blocks = Vec::new(); | ||||||
|                     std::mem::swap(&mut blocks, &mut frag.blocks); |                     std::mem::swap(&mut blocks, &mut frag.blocks); | ||||||
|                     ve.insert(blocks); |                     ve.insert(Template { | ||||||
|  |                         blocks, | ||||||
|  |                         param_defs: None, | ||||||
|  |                     }); | ||||||
|                 } |                 } | ||||||
|                 Entry::Occupied(_) => { |                 Entry::Occupied(_) => { | ||||||
|                     return Err(AstToIrError::SemanticError { |                     return Err(AstToIrError::SemanticError { | ||||||
| @ -132,27 +136,40 @@ fn pull_out_entrypoints_from_block<'a>( | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         Block::Text(_) | Block::RawUnescapedHtml(_) | Block::DefineExpandSlot(_) => { /* nop */ } |         Block::Text(_) | ||||||
|  |         | Block::RawUnescapedHtml(_) | ||||||
|  |         | Block::DefineExpandSlot(_) | ||||||
|  |         | Block::SetStatement(_) => { /* nop */ } | ||||||
|     } |     } | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Step 2. Compile the AST to IR steps.
 | /// Step 2. Compile the AST to IR steps.
 | ||||||
| pub(crate) fn compile_functions<'a>( | pub(crate) fn compile_functions( | ||||||
|     functions: &BTreeMap<String, Vec<Block>>, |     functions: &BTreeMap<String, Template>, | ||||||
| ) -> Result<BTreeMap<String, Vec<Step>>, AstToIrError> { | ) -> Result<BTreeMap<String, TemplateFunction>, AstToIrError> { | ||||||
|     let mut result = BTreeMap::new(); |     let mut result = BTreeMap::new(); | ||||||
|     for (func_name, func_blocks) in functions { |     for (func_name, func_template) in functions { | ||||||
|         let mut steps = Vec::new(); |         let mut steps = Vec::new(); | ||||||
|         for block in func_blocks { |         for block in &func_template.blocks { | ||||||
|             compile_ast_block_to_steps(block, &mut steps)?; |             compile_ast_block_to_steps(block, &mut steps)?; | ||||||
|         } |         } | ||||||
|         result.insert(func_name.clone(), 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 }); | ||||||
|     } |     } | ||||||
|     Ok(result) |     Ok(result) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn compile_ast_block_to_steps<'a>( | fn compile_ast_block_to_steps( | ||||||
|     block: &Block, |     block: &Block, | ||||||
|     instructions: &mut Vec<Step>, |     instructions: &mut Vec<Step>, | ||||||
| ) -> Result<(), AstToIrError> { | ) -> Result<(), AstToIrError> { | ||||||
| @ -178,7 +195,7 @@ fn compile_ast_block_to_steps<'a>( | |||||||
|                 instructions.push(Step { |                 instructions.push(Step { | ||||||
|                     def: StepDef::WriteLiteral { |                     def: StepDef::WriteLiteral { | ||||||
|                         escape: false, |                         escape: false, | ||||||
|                         text: intern(format!(" id=\"")), |                         text: intern(" id=\""), | ||||||
|                     }, |                     }, | ||||||
|                     locator: he.loc.clone(), |                     locator: he.loc.clone(), | ||||||
|                 }); |                 }); | ||||||
| @ -201,73 +218,87 @@ fn compile_ast_block_to_steps<'a>( | |||||||
| 
 | 
 | ||||||
|             // This is only handling the case where we are the exclusive owner of all the class
 |             // This is only handling the case where we are the exclusive owner of all the class
 | ||||||
|             // names: see below for more...
 |             // names: see below for more...
 | ||||||
|             if !he.classes.is_empty() { |             if !he.classes.is_empty() && !he.attributes.contains_key(&intern("class")) { | ||||||
|                 if !he.attributes.contains_key(&intern("class")) { |                 let classes = he.classes.iter().map(|istr| istr.as_str()).join(" "); | ||||||
|                     instructions.push(Step { |                 gen_steps_to_write_literal_attribute(he, "class", &classes, instructions); | ||||||
|                         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(), |  | ||||||
|                     }); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Write attributes
 |             // Write attributes
 | ||||||
|             for (attr_name, attr_expr) in &he.attributes { |             for (attr_name, (attr_expr, attr_flags)) in &he.attributes { | ||||||
|                 instructions.push(Step { |                 let extra_inject = (attr_name.as_str() == "class" && !he.classes.is_empty()) | ||||||
|                     def: StepDef::WriteLiteral { |                     .then(|| intern(he.classes.iter().map(|istr| istr.as_str()).join(" ") + " ")); | ||||||
|                         escape: false, |                 if attr_flags.optional { | ||||||
|                         text: intern(format!(" {attr_name}=\"")), |                     // For optional fields:
 | ||||||
|                     }, |                     // - if it matches Some($x), unwrap to $x and emit $x
 | ||||||
|                     locator: he.loc.clone(), |                     // - 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"); | ||||||
| 
 | 
 | ||||||
|                 if attr_name.as_str() == "class" && !he.classes.is_empty() { |                     gen_steps_to_write_attribute( | ||||||
|                     // This handles the case where we need to merge class lists between an
 |                         he, | ||||||
|                     // attribute and the shorthand form.
 |                         attr_name, | ||||||
|                     instructions.push(Step { |                         &Expression::Variable { | ||||||
|                         def: StepDef::WriteLiteral { |                             name: virtual_varname.clone(), | ||||||
|                             escape: true, |                             loc: he.loc.clone(), | ||||||
|                             text: intern( |  | ||||||
|                                 he.classes.iter().map(|istr| istr.as_str()).join(" ") + " ", |  | ||||||
|                             ), |  | ||||||
|                         }, |                         }, | ||||||
|                         locator: 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(), | ||||||
|  |                         )); | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                 instructions.push(Step { |                     arms.push(( | ||||||
|                     def: StepDef::WriteEval { |                         MatchBinding::Variable { | ||||||
|                         escape: true, |                             name: virtual_varname.clone(), | ||||||
|                         expr: attr_expr.clone(), |  | ||||||
|                         }, |                         }, | ||||||
|                     locator: he.loc.clone(), |                         some_stage, | ||||||
|                 }); |                     )); | ||||||
| 
 | 
 | ||||||
|                     instructions.push(Step { |                     instructions.push(Step { | ||||||
|                     def: StepDef::WriteLiteral { |                         def: StepDef::Match { | ||||||
|                         escape: false, |                             matchable: attr_expr.clone(), | ||||||
|                         text: intern("\""), |                             arms, | ||||||
|                         }, |                         }, | ||||||
|                         locator: he.loc.clone(), |                         locator: he.loc.clone(), | ||||||
|                     }); |                     }); | ||||||
|  |                 } else { | ||||||
|  |                     gen_steps_to_write_attribute( | ||||||
|  |                         he, | ||||||
|  |                         attr_name, | ||||||
|  |                         attr_expr, | ||||||
|  |                         extra_inject, | ||||||
|  |                         instructions, | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Close the tag
 |             // Close the tag
 | ||||||
| @ -313,6 +344,13 @@ fn compile_ast_block_to_steps<'a>( | |||||||
|                 locator: ce.loc.clone(), |                 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) => { |         Block::IfBlock(ifb) => { | ||||||
|             let mut true_instrs = Vec::new(); |             let mut true_instrs = Vec::new(); | ||||||
|             let mut false_instrs = Vec::new(); |             let mut false_instrs = Vec::new(); | ||||||
| @ -470,6 +508,81 @@ fn compile_ast_block_to_steps<'a>( | |||||||
|     Ok(()) |     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)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use super::*; |     use super::*; | ||||||
| @ -535,4 +648,24 @@ div.stylish#myid {size=42, stringy="yup", arb=$ritrary} | |||||||
|         ) |         ) | ||||||
|         .unwrap()); |         .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()); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -11,6 +11,21 @@ pub struct Function { | |||||||
|     pub steps: Vec<Step>, |     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)] | #[derive(Clone, Debug, Eq, PartialEq, Serialize)] | ||||||
| pub struct Step { | pub struct Step { | ||||||
|     #[serde(flatten)] |     #[serde(flatten)] | ||||||
| @ -28,6 +43,10 @@ pub enum StepDef { | |||||||
|         escape: bool, |         escape: bool, | ||||||
|         expr: Expression, |         expr: Expression, | ||||||
|     }, |     }, | ||||||
|  |     Set { | ||||||
|  |         expression: Expression, | ||||||
|  |         binding: Binding, | ||||||
|  |     }, | ||||||
|     If { |     If { | ||||||
|         condition: Expression, |         condition: Expression, | ||||||
|         true_steps: Vec<Step>, |         true_steps: Vec<Step>, | ||||||
|  | |||||||
| @ -8,7 +8,6 @@ | |||||||
| //! For using the IR, see `hornbeam_interpreter` (dynamic) or `hornbeam_macros` (code gen).
 | //! For using the IR, see `hornbeam_interpreter` (dynamic) or `hornbeam_macros` (code gen).
 | ||||||
| 
 | 
 | ||||||
| use crate::ast_to_ir::pull_out_entrypoints; | use crate::ast_to_ir::pull_out_entrypoints; | ||||||
| use crate::ir::Step; |  | ||||||
| use crate::peephole::apply_all_peephole_passes; | use crate::peephole::apply_all_peephole_passes; | ||||||
| use hornbeam_grammar::ast::Template; | use hornbeam_grammar::ast::Template; | ||||||
| use std::collections::BTreeMap; | use std::collections::BTreeMap; | ||||||
| @ -19,14 +18,16 @@ mod peephole; | |||||||
| 
 | 
 | ||||||
| pub use ast_to_ir::AstToIrError; | pub use ast_to_ir::AstToIrError; | ||||||
| 
 | 
 | ||||||
|  | use self::ir::TemplateFunction; | ||||||
|  | 
 | ||||||
| pub fn ast_to_optimised_ir( | pub fn ast_to_optimised_ir( | ||||||
|     template_name: &str, |     template_name: &str, | ||||||
|     template: Template, |     template: Template, | ||||||
| ) -> Result<BTreeMap<String, Vec<Step>>, AstToIrError> { | ) -> Result<BTreeMap<String, TemplateFunction>, AstToIrError> { | ||||||
|     let entrypoints = pull_out_entrypoints(template, template_name)?; |     let entrypoints = pull_out_entrypoints(template, template_name)?; | ||||||
|     let mut compiled_funcs = ast_to_ir::compile_functions(&entrypoints)?; |     let mut compiled_funcs = ast_to_ir::compile_functions(&entrypoints)?; | ||||||
|     for steps in compiled_funcs.values_mut() { |     for func in compiled_funcs.values_mut() { | ||||||
|         apply_all_peephole_passes(steps); |         apply_all_peephole_passes(&mut func.steps); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Ok(compiled_funcs) |     Ok(compiled_funcs) | ||||||
|  | |||||||
| @ -6,15 +6,15 @@ use crate::ir::{Step, StepDef}; | |||||||
| use hornbeam_grammar::ast::{Expression, StringPiece}; | use hornbeam_grammar::ast::{Expression, StringPiece}; | ||||||
| use hornbeam_grammar::intern; | 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) { |     for peephole_start in 0..(steps.len() - peephole_width + 1) { | ||||||
|         f(&mut steps[peephole_start..peephole_start + peephole_width]); |         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>, |     steps: &mut Vec<T>, | ||||||
|     peephole_width: usize, |     peephole_width: usize, | ||||||
|     mut f: F, |     mut f: F, | ||||||
| @ -54,6 +54,7 @@ fn apply_peephole_pass<F: Fn(&mut Vec<Step>)>(steps: &mut Vec<Step>, pass: &F) { | |||||||
|         match &mut step.def { |         match &mut step.def { | ||||||
|             StepDef::WriteLiteral { .. } => {} |             StepDef::WriteLiteral { .. } => {} | ||||||
|             StepDef::WriteEval { .. } => {} |             StepDef::WriteEval { .. } => {} | ||||||
|  |             StepDef::Set { .. } => {} | ||||||
|             StepDef::If { |             StepDef::If { | ||||||
|                 true_steps, |                 true_steps, | ||||||
|                 false_steps, |                 false_steps, | ||||||
| @ -85,7 +86,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.
 | /// 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>) { | fn pass_write_eval_literal_to_write_literal(steps: &mut Vec<Step>) { | ||||||
| @ -156,8 +157,7 @@ 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>) { | 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_eval_literal_to_write_literal); | ||||||
|     apply_peephole_pass(steps, &pass_write_literal_preescape); |     apply_peephole_pass(steps, &pass_write_literal_preescape); | ||||||
| @ -167,16 +167,21 @@ pub fn apply_all_peephole_passes(steps: &mut Vec<Step>) { | |||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use super::*; |     use super::*; | ||||||
|     use crate::ast_to_ir::{compile_functions, pull_out_entrypoints}; |     use crate::{ | ||||||
|  |         ast_to_ir::{compile_functions, pull_out_entrypoints}, | ||||||
|  |         ir::TemplateFunction, | ||||||
|  |     }; | ||||||
|     use hornbeam_grammar::parse_template; |     use hornbeam_grammar::parse_template; | ||||||
|     use insta::assert_yaml_snapshot; |     use insta::assert_yaml_snapshot; | ||||||
|     use std::collections::BTreeMap; |     use std::collections::BTreeMap; | ||||||
| 
 | 
 | ||||||
|     fn parse_ir_and_peephole(text: &str) -> BTreeMap<String, Vec<Step>> { |     fn parse_ir_and_peephole(text: &str) -> BTreeMap<String, TemplateFunction> { | ||||||
|         let template = parse_template(text, "inp").unwrap(); |         let template = parse_template(text, "inp").unwrap(); | ||||||
|         let entrypoints = pull_out_entrypoints(template, "TemplateName").unwrap(); |         let entrypoints = pull_out_entrypoints(template, "TemplateName").unwrap(); | ||||||
|         let mut compiled = compile_functions(&entrypoints).unwrap(); |         let mut compiled = compile_functions(&entrypoints).unwrap(); | ||||||
|         compiled.values_mut().for_each(apply_all_peephole_passes); |         compiled | ||||||
|  |             .values_mut() | ||||||
|  |             .for_each(|func| apply_all_peephole_passes(&mut func.steps)); | ||||||
|         compiled |         compiled | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ source: hornbeam_ir/src/ast_to_ir.rs | |||||||
| expression: "compile_functions(&pull_out_entrypoints(template,\n                    \"TemplateName\").unwrap()).unwrap()" | expression: "compile_functions(&pull_out_entrypoints(template,\n                    \"TemplateName\").unwrap()).unwrap()" | ||||||
| --- | --- | ||||||
| TemplateName: | TemplateName: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: false |         escape: false | ||||||
|         text: "<div" |         text: "<div" | ||||||
| @ -41,6 +43,8 @@ TemplateName: | |||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| TemplateName__Footer: | TemplateName__Footer: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: true |         escape: true | ||||||
|         text: Or even adjacent ones |         text: Or even adjacent ones | ||||||
| @ -49,6 +53,8 @@ TemplateName__Footer: | |||||||
|         line: 0 |         line: 0 | ||||||
|         column: 0 |         column: 0 | ||||||
| TemplateName__Frag1: | TemplateName__Frag1: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: false |         escape: false | ||||||
|         text: "<span" |         text: "<span" | ||||||
| @ -86,6 +92,8 @@ TemplateName__Frag1: | |||||||
|         line: 6 |         line: 6 | ||||||
|         column: 9 |         column: 9 | ||||||
| TemplateName__Frag2: | TemplateName__Frag2: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: false |         escape: false | ||||||
|         text: "<div" |         text: "<div" | ||||||
| @ -114,4 +122,3 @@ TemplateName__Frag2: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 7 |         line: 7 | ||||||
|         column: 13 |         column: 13 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ source: hornbeam_ir/src/ast_to_ir.rs | |||||||
| expression: "compile_functions(&pull_out_entrypoints(template,\n                    \"TemplateName\").unwrap()).unwrap()" | expression: "compile_functions(&pull_out_entrypoints(template,\n                    \"TemplateName\").unwrap()).unwrap()" | ||||||
| --- | --- | ||||||
| TemplateName: | TemplateName: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: false |         escape: false | ||||||
|         text: "<div" |         text: "<div" | ||||||
| @ -170,4 +172,3 @@ TemplateName: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -0,0 +1,85 @@ | |||||||
|  | --- | ||||||
|  | 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 | ||||||
| @ -3,6 +3,8 @@ source: hornbeam_ir/src/ast_to_ir.rs | |||||||
| expression: "pull_out_entrypoints(template, \"TemplateName\").unwrap()" | expression: "pull_out_entrypoints(template, \"TemplateName\").unwrap()" | ||||||
| --- | --- | ||||||
| TemplateName: | TemplateName: | ||||||
|  |   param_defs: ~ | ||||||
|  |   blocks: | ||||||
|     - HtmlElement: |     - HtmlElement: | ||||||
|         name: div |         name: div | ||||||
|         children: |         children: | ||||||
| @ -28,10 +30,14 @@ TemplateName: | |||||||
|           line: 2 |           line: 2 | ||||||
|           column: 1 |           column: 1 | ||||||
| TemplateName__Footer: | TemplateName__Footer: | ||||||
|  |   param_defs: ~ | ||||||
|  |   blocks: | ||||||
|     - Text: |     - Text: | ||||||
|         pieces: |         pieces: | ||||||
|           - Literal: Or even adjacent ones |           - Literal: Or even adjacent ones | ||||||
| TemplateName__Frag1: | TemplateName__Frag1: | ||||||
|  |   param_defs: ~ | ||||||
|  |   blocks: | ||||||
|     - HtmlElement: |     - HtmlElement: | ||||||
|         name: span |         name: span | ||||||
|         children: |         children: | ||||||
| @ -53,6 +59,8 @@ TemplateName__Frag1: | |||||||
|           line: 6 |           line: 6 | ||||||
|           column: 9 |           column: 9 | ||||||
| TemplateName__Frag2: | TemplateName__Frag2: | ||||||
|  |   param_defs: ~ | ||||||
|  |   blocks: | ||||||
|     - HtmlElement: |     - HtmlElement: | ||||||
|         name: div |         name: div | ||||||
|         children: |         children: | ||||||
| @ -66,4 +74,3 @@ TemplateName__Frag2: | |||||||
|           filename: inp |           filename: inp | ||||||
|           line: 7 |           line: 7 | ||||||
|           column: 13 |           column: 13 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ 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        \"#)" | 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: | TemplateName: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: false |         escape: false | ||||||
|         text: "<div>" |         text: "<div>" | ||||||
| @ -34,6 +36,8 @@ TemplateName: | |||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| TemplateName__Footer: | TemplateName__Footer: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: false |         escape: false | ||||||
|         text: Or even adjacent ones |         text: Or even adjacent ones | ||||||
| @ -42,6 +46,8 @@ TemplateName__Footer: | |||||||
|         line: 0 |         line: 0 | ||||||
|         column: 0 |         column: 0 | ||||||
| TemplateName__Frag1: | TemplateName__Frag1: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: false |         escape: false | ||||||
|         text: "<span>This is a fragment!!</span>" |         text: "<span>This is a fragment!!</span>" | ||||||
| @ -58,6 +64,8 @@ TemplateName__Frag1: | |||||||
|         line: 6 |         line: 6 | ||||||
|         column: 9 |         column: 9 | ||||||
| TemplateName__Frag2: | TemplateName__Frag2: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: false |         escape: false | ||||||
|         text: "<div>There's no problem having <<nested>> fragments!</div>" |         text: "<div>There's no problem having <<nested>> fragments!</div>" | ||||||
| @ -65,4 +73,3 @@ TemplateName__Frag2: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 7 |         line: 7 | ||||||
|         column: 13 |         column: 13 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ 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        \"#)" | 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: | TemplateName: | ||||||
|  |   param_defs: ~ | ||||||
|  |   steps: | ||||||
|     - WriteLiteral: |     - WriteLiteral: | ||||||
|         escape: false |         escape: false | ||||||
|         text: "<div id=\"myid\" class=\"stylish\" arb=\"" |         text: "<div id=\"myid\" class=\"stylish\" arb=\"" | ||||||
| @ -76,4 +78,3 @@ TemplateName: | |||||||
|         filename: inp |         filename: inp | ||||||
|         line: 2 |         line: 2 | ||||||
|         column: 1 |         column: 1 | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| name = "hornbeam_macros" | name = "hornbeam_macros" | ||||||
| description = "Macros for the Hornbeam template system" | description = "Macros for the Hornbeam template system" | ||||||
| license = "AGPL-3.0-or-later" | license = "AGPL-3.0-or-later" | ||||||
| version = "0.0.3" | version = "0.0.5" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user