diff --git a/Cargo.lock b/Cargo.lock
index 969eb27..f15b3b3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -291,7 +291,7 @@ dependencies = [
[[package]]
name = "attribute-derive"
-version = "0.9.1"
+version = "0.9.2"
dependencies = [
"attribute-derive-macro",
"derive-where",
@@ -303,7 +303,7 @@ dependencies = [
[[package]]
name = "attribute-derive-macro"
-version = "0.9.1"
+version = "0.9.2"
dependencies = [
"collection_literals",
"interpolator",
@@ -1196,6 +1196,17 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
+[[package]]
+name = "ghost"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0e085ded9f1267c32176b40921b9754c474f7dd96f7e808d4a982e48aa1e854"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.53",
+]
+
[[package]]
name = "gimli"
version = "0.28.1"
@@ -1448,9 +1459,9 @@ dependencies = [
"chrono",
"derive_more 1.0.0-beta.6",
"forr",
+ "ghost",
"html",
"html-escape",
- "htmx",
"htmx-macros",
"insta",
"serde",
@@ -1465,7 +1476,9 @@ dependencies = [
"attribute-derive",
"derive_more 1.0.0-beta.6",
"forr",
+ "html-escape",
"htmx-script",
+ "ident_case",
"manyhow 0.9.0",
"proc-macro-utils 0.10.0",
"proc-macro2",
diff --git a/Cargo.toml b/Cargo.toml
index 63ac9c0..5504d7e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,8 +14,7 @@ repository = "https://github.com/ModProg/htmx"
documentation = "https://docs.rs/htmx"
[features]
-default = ["axum", "actix-web"]
-sorted_attributes = []
+# default = ["axum", "actix-web"]
axum = ["dep:axum-core"]
[dependencies]
@@ -30,13 +29,10 @@ serde = "1.0.188"
serde_json = "1.0.107"
typed-builder = {git = "https://github.com/ModProg/rust-typed-builder", branch = "mutators"}
chrono = "0.4.31"
+ghost = "0.1.17"
[dev-dependencies]
insta = "1.31.0"
-# Enables `sorted_attributes` for tests, to make assertions stable.
-htmx = { path = ".", default-features = false, features = [
- "sorted_attributes",
-] }
serde = { version = "1.0.188", features = ["derive"] }
[profile.dev.package.insta]
diff --git a/example/Cargo.toml b/example/Cargo.toml
index 4f375ab..d0dfdba 100644
--- a/example/Cargo.toml
+++ b/example/Cargo.toml
@@ -11,7 +11,7 @@ repository = "https://github.com/ModProg/htmx"
documentation = "https://docs.rs/htmx"
[features]
-default = ["axum"]
+default = ["axum", "tauri", "actix"]
axum = ["htmx/axum", "dep:tokio", "dep:axum"]
tauri = ["dep:tauri-runtime", "dep:tauri-runtime-wry", "dep:url"]
actix = ["htmx/actix-web", "dep:actix-web"]
diff --git a/example/axum.rs b/example/axum.rs
index b4b0bb9..d0723e8 100644
--- a/example/axum.rs
+++ b/example/axum.rs
@@ -8,18 +8,17 @@ use std::error::Error;
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::{Form, Router};
-use htmx::{html, HtmxSrc};
+use htmx::{html, HtmlPage, HtmxSrc};
async fn index() -> impl IntoResponse {
html! {
-
-
-
- "Axum Demo"
-
+
+ "Axum Demo"
+
+
}
}
@@ -39,6 +38,7 @@ async fn main() -> Result<(), Box> {
Router::new()
.route("/", get(index))
.route("/greet", post(greet))
+ .route("/htmx", get(HtmxSrc))
.into_make_service(),
)
.await
diff --git a/example/tauri.rs b/example/tauri.rs
index 9b72193..855907c 100644
--- a/example/tauri.rs
+++ b/example/tauri.rs
@@ -8,7 +8,7 @@ use std::borrow::Cow;
use std::error::Error;
use std::fmt::Display;
-use htmx::{html, Html, HtmxSrc};
+use htmx::{html, HtmxSrc};
use tauri_runtime::http::ResponseBuilder;
use tauri_runtime::webview::{WebviewAttributes, WindowBuilder};
use tauri_runtime::window::PendingWindow;
@@ -16,7 +16,7 @@ use tauri_runtime::Runtime;
use tauri_runtime_wry::Wry;
use url::Url;
-fn index() -> Html {
+fn index() -> String {
html! {
"Tauri Demo"
@@ -25,14 +25,16 @@ fn index() -> Html {
}
+ .into_string()
}
-fn greet(name: impl Display) -> Html {
+fn greet(name: impl Display) -> String {
html! {
"Hello "
{format!("{name}! ")}
":D"
}
+ .into_string()
}
fn get_param<'a>(key: &str, url: &'a Url) -> Result, String> {
@@ -61,7 +63,6 @@ fn main() -> Result<(), Box> {
"/greet" => greet(get_param("name", &url)?),
path => return Err(format!("Unknown path `{path}`").into()),
}
- .to_string()
.into_bytes(),
)
.unwrap())
diff --git a/htmx-macros/Cargo.toml b/htmx-macros/Cargo.toml
index 4ceccf1..4bef215 100644
--- a/htmx-macros/Cargo.toml
+++ b/htmx-macros/Cargo.toml
@@ -19,7 +19,9 @@ proc-macro = true
attribute-derive.path = "../../../Rust/attribute-derive"
derive_more = { version = "1.0.0-beta.6", features = ["display"] }
forr = "0.2.2"
+html-escape = "0.2.13"
htmx-script = { version = "0.1.0", path = "../htmx-script" }
+ident_case = "1.0.1"
manyhow = "0.9"
proc-macro-utils = "0.10"
proc-macro2 = "1.0.66"
diff --git a/htmx-macros/src/component.rs b/htmx-macros/src/component.rs
index d7388d8..dacf2e2 100644
--- a/htmx-macros/src/component.rs
+++ b/htmx-macros/src/component.rs
@@ -1,200 +1,426 @@
use attribute_derive::{FlagOrValue, FromAttr};
-use manyhow::{bail, ensure, error_message, Result};
-use quote::ToTokens;
+use manyhow::{bail, ensure, Result};
+use proc_macro2::{Span, TokenStream};
+use quote::{format_ident, ToTokens, TokenStreamExt};
+use syn::punctuated::Punctuated;
+use syn::token::{Brace, Paren};
use syn::{
- parse2, Attribute, Expr, FnArg, Ident, ItemFn, ItemStruct, Pat, PatIdent, PatTupleStruct,
- PatType, Signature, Type,
+ AssocType, Attribute, Expr, FnArg, GenericArgument, Generics, Ident, Lifetime, Pat, PatIdent,
+ PatTupleStruct, PatType, PathArguments, ReturnType, Token, Type, TypeImplTrait, TypeParamBound,
+ Visibility,
};
+use syn_derive::ToTokens;
use crate::*;
-pub fn component(_input: TokenStream, item: TokenStream) -> Result {
- Ok(if let Ok(mut item) = parse2::(item.clone()) {
- // Generate
- // #[derive(typed_builder::TypedBuilder)]
- // #[builder(crate_module_path=htmx::__private::typed_builder)]
- // #[builder(build_method(into = Html))]
-
- // Set some defaults
- for field in item.fields.iter_mut() {
- type_attrs(
- field.ident.as_ref().ok_or_else(|| {
- error_message!("Only structs with named fields are supported")
- })?,
- &field.ty,
- &mut field.attrs,
- )?;
+enum Arg {
+ Body(Ident),
+ Field(Field),
+}
+
+#[derive(Debug)]
+struct Field {
+ name: Ident,
+ ty: Type,
+ pat: Pat,
+ default: FlagOrValue,
+ default_type: Option,
+ doc_attrs: TokenStream,
+}
+
+impl Field {
+ fn generic(&self) -> Ident {
+ Ident::new(
+ &ident_case::RenameRule::PascalCase.apply_to_field(self.name.to_string()),
+ self.name.span(),
+ )
+ }
+
+ fn field(&self) -> TokenStream {
+ let name = &self.name;
+ let generic = self.generic();
+ quote!(#name: #generic)
+ }
+
+ fn name(&self) -> &Ident {
+ &self.name
+ }
+
+ fn unset(&self) -> TokenStream {
+ if let Some(default_type) = &self.default_type {
+ return quote!(#default_type);
}
- quote! {
- #use ::htmx::Html;
- #use ::htmx::__private::typed_builder::{self, TypedBuilder};
- #[derive(TypedBuilder)]
- #[builder(crate_module_path = typed_builder)]
- #[builder(build_method(into = Html))]
- #item
+ if self.is_optional() {
+ if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = &self.ty {
+ for t in bounds {
+ if let TypeParamBound::Trait(t) = t {
+ let t = t.path.segments.last().unwrap();
+ if t.ident == "IntoIterator" {
+ if let PathArguments::AngleBracketed(t) = &t.arguments {
+ for t in &t.args {
+ if let GenericArgument::AssocType(t) = &t {
+ if t.ident == "Item" {
+ let t = &t.ty;
+ return quote!(::htmx::__private::Empty::<#t>);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
- } else if let Ok(ItemFn {
- attrs,
- vis,
- sig:
- Signature {
- ident,
- generics,
- inputs,
- output,
- ..
- },
- block,
- }) = parse2::(item.clone())
- {
- // #[derive(typed_builder::TypedBuilder)]
- // #[builder(crate_module_path=::typed_builder)]
- // #[builder(build_method(into = Html))]
- // struct MyFnComponent {
- // a: bool,
- // b: String,
- // }
- //
- // impl Into for MyFnComponent {
- // fn into(self) -> Html {
- // let Self { a, b } = self;
- // htmx! {crate
- //
- // }
- // }
- // }
- ensure!(generics.params.is_empty(), "generics are not supported");
- let output = match output {
- syn::ReturnType::Default => quote!(::htmx::Html),
- syn::ReturnType::Type(_, ty) => ty.to_token_stream(),
- };
+ quote!(::htmx::__private::Unset)
+ }
- let (fields, fieldpats): (Vec<_>, Vec<_>) = inputs
- .into_iter()
- .map(|arg| {
- // hi
- ensure!(let FnArg::Typed(PatType { mut attrs, pat, ty, .. }) = arg,
- arg, "`self` is not supported");
-
- let ident = match &*pat {
- Pat::Ident(PatIdent { ident, .. }) => ident,
- // On tuples with a single field, take its ident
- Pat::TupleStruct(PatTupleStruct { elems, .. })
- if elems.len() == 1 && matches!(elems.first().unwrap(), Pat::Ident(_)) =>
- {
- let Some(Pat::Ident(PatIdent { ident, .. })) = elems.first() else {
- unreachable!("pat should contain a single ident")
- };
- ident
+ fn unset_value(&self) -> TokenStream {
+ if let Some(default_type) = &self.default_type {
+ quote!(<#default_type>::default())
+ } else {
+ self.unset()
+ }
+ }
+
+ fn is_optional(&self) -> bool {
+ if let Type::Path(path) = &self.ty {
+ path.path.is_ident("bool")
+ || path.path.segments.len() == 1
+ && path.path.segments.first().unwrap().ident == "Option"
+ || !self.default.is_none()
+ } else {
+ !self.default.is_none()
+ }
+ }
+
+ fn destructure(&self) -> TokenStream {
+ let name = &self.name;
+ let pat = &self.pat;
+ match &self.default {
+ FlagOrValue::Value(default) => quote!(let #pat = #name.get_or_else(|| #default);),
+ _ if self.is_impl_trait() && self.is_optional() => quote! {},
+ _ if self.is_impl_trait() => quote!(let ::htmx::__private::Set(#name) = #name;),
+ _ if self.is_optional() => quote!(let #pat = #name.get_or_default();),
+ _ => quote!(let ::htmx::__private::Set(#name) = #name;),
+ }
+ }
+
+ fn get_generics(&self, base: &Ident) -> Option {
+ if let Type::ImplTrait(ty) = &self.ty {
+ let mut tokens = TokenStream::new();
+ desugar_impl(&mut tokens, ty.clone(), base);
+ Some(tokens)
+ } else {
+ None
+ }
+ }
+
+ fn is_impl_trait(&self) -> bool {
+ matches!(self.ty, Type::ImplTrait(_))
+ }
+}
+
+fn desugar_impl(tokens: &mut TokenStream, ty: TypeImplTrait, base: &Ident) {
+ let mut count = 0;
+ let mut bounds = ty.bounds;
+ for bound in &mut bounds {
+ if let TypeParamBound::Trait(bound) = bound {
+ for segment in &mut bound.path.segments {
+ if let PathArguments::AngleBracketed(arguments) = &mut segment.arguments {
+ for argument in &mut arguments.args {
+ if let GenericArgument::Type(ty)
+ | GenericArgument::AssocType(AssocType { ty, .. }) = argument
+ {
+ if let Type::ImplTrait(tr) = ty {
+ let gen_ident = format_ident!("{base}_{count}");
+ count += 1;
+ desugar_impl(tokens, tr.clone(), &gen_ident);
+ tokens.extend(quote!(,));
+ *ty = parse_quote!(#gen_ident);
+ };
+ }
}
- pat => bail!(pat, "only named arguments and new type patterns are allowed";
- help = "use `ident @ {}`", pat.into_token_stream();),
+ }
+ }
+ }
+ }
+ tokens.extend(quote!(#base: #bounds));
+}
+
+impl TryFrom for Arg {
+ type Error = manyhow::Error;
+
+ fn try_from(arg: FnArg) -> std::result::Result {
+ // hi
+ ensure!(let FnArg::Typed(PatType { mut attrs, pat, ty, .. }) = arg,
+ arg, "`self` is not supported");
+
+ let ident = match &*pat {
+ Pat::Ident(PatIdent { ident, .. }) => ident,
+ // On tuples with a single field, take its ident
+ Pat::TupleStruct(PatTupleStruct { elems, .. })
+ if elems.len() == 1 && matches!(elems.first().unwrap(), Pat::Ident(_)) =>
+ {
+ let Some(Pat::Ident(PatIdent { ident, .. })) = elems.first() else {
+ unreachable!("pat should contain a single ident")
};
+ ident
+ }
+ pat => bail!(
+ pat, "only named arguments and new type patterns are allowed";
+ help = "use `ident @ {}`", pat.into_token_stream();
+ ),
+ };
+
+ if ident == "body" {
+ return Ok(Arg::Body(ident.clone()))
+ }
+
+ let DefaultAttr(mut default) = DefaultAttr::remove_attributes(&mut attrs)?;
+ let DefaultType(default_type) = DefaultType::remove_attributes(&mut attrs)?;
+ // let ChildrenAttr(children) = ChildrenAttr::remove_attributes(attrs)?;
- type_attrs(ident, &ty, &mut attrs)?;
+ if default_type.is_some() && default.is_none() {
+ default = FlagOrValue::Flag;
+ }
- Ok((quote!(#(#attrs)* pub #ident: #ty,), quote!(#ident: #pat,)))
- })
- .collect::>>()?
+ let doc_attrs = attrs
.into_iter()
- .unzip();
-
- // #attrs #vis struct
- quote! {
- #use ::htmx::__private::typed_builder::{self, TypedBuilder};
- #[derive(TypedBuilder)]
- #[builder(crate_module_path=typed_builder)]
- #[builder(build_method(into=#output))]
- #(#attrs)*
- #vis struct #ident {
- #(#fields)*
+ .filter(|a| a.path().is_ident("doc"))
+ .map(ToTokens::into_token_stream)
+ .collect();
+
+ Ok(Arg::Field(Field {
+ name: ident.clone(),
+ pat: *pat,
+ ty: *ty,
+ default,
+ default_type,
+ doc_attrs,
+ }))
+ // Ok((quote!(#(#attrs)* pub #ident: #ty,), quote!(#ident: #pat,)))
+ }
+}
+
+#[derive(syn_derive::Parse, ToTokens)]
+pub struct Component {
+ #[parse(Attribute::parse_outer)]
+ #[to_tokens(|ts, t| ts.append_all(t))]
+ attrs: Vec,
+ vis: Visibility,
+ fn_token: Token![fn],
+ name: Ident,
+ generics: Generics,
+ #[syn(parenthesized)]
+ paren_token: Paren,
+ #[syn(in = paren_token)]
+ #[parse(Punctuated::parse_terminated)]
+ inputs: Punctuated,
+ output: ReturnType,
+ #[syn(braced)]
+ brace_token: Brace,
+ #[syn(in = brace_token)]
+ body: TokenStream,
+}
+
+pub fn component(
+ _input: TokenStream,
+ Component {
+ attrs,
+ vis,
+ name: struct_name,
+ generics,
+ inputs,
+ output,
+ body: fn_body,
+ ..
+ }: Component,
+) -> Result {
+ ensure!(generics.params.is_empty(), "generics are not supported");
+ if let ReturnType::Type(_, t) = &output {
+ if let Type::Tuple(t) = &**t {
+ if !t.elems.is_empty() {
+ bail!(output, "expected `()` return type");
}
+ } else {
+ bail!(output, "expected `()` return type");
+ }
+ }
+
+ let (body, args) = inputs.into_iter().map(Arg::try_from).try_fold(
+ Default::default(),
+ |mut acc, arg| -> Result<(Option, Vec)> {
+ match arg? {
+ Arg::Body(body) => {
+ ensure!(acc.0.is_none(), body, "multiple `body` arguments");
+ acc.0 = Some(body);
+ }
+ Arg::Field(field) => acc.1.push(field),
+ };
+ Ok(acc)
+ },
+ )?;
+
+ let body = body.unwrap_or_else(|| Ident::new("body", Span::call_site()));
+
+ let html_lt = Lifetime::new("'html", Span::call_site());
+
+ let fields = args.iter().map(Field::field);
+ let generics = args.iter().map(Field::generic);
+ let unsets_types: Vec<_> = args.iter().map(Field::unset).collect();
+ let unset_values: Vec<_> = args.iter().map(Field::unset_value).collect();
+ let field_names: Vec<_> = args.iter().map(Field::name).collect();
+
+ let optional_gens = args
+ .iter()
+ .filter(|&f| (f.is_optional() && !f.is_impl_trait()))
+ .map(|f| {
+ let g = f.generic();
+ let ty = &f.ty;
+ quote!(#g: ::htmx::__private::Settable<#ty>)
+ })
+ .chain(args.iter().filter_map(|f| f.get_generics(&f.generic())));
- impl From<#ident> for #output {
- #[allow(non_shorthand_field_patterns)]
- fn from(#ident{ #(#fieldpats)* }: #ident) -> #output #block
+ let mandatory_gens = args.iter().map(|f| {
+ if f.is_optional() {
+ f.generic().into_token_stream()
+ } else if f.is_impl_trait() {
+ let gen = f.generic();
+ quote!(::htmx::__private::Set<#gen>)
+ } else {
+ let ty = &f.ty;
+ quote!(::htmx::__private::Set<#ty>)
+ }
+ });
+
+ let field_destructure = args.iter().map(Field::destructure);
+
+ let mut setters = vec![];
+ for i in 0..args.len() {
+ let mut impl_gens = vec![];
+ let mut unset_gens = vec![];
+ let mut set_gens = vec![];
+ let mut destructure = vec![];
+ let mut structure = vec![];
+
+ let field @ Field {
+ name: field_name,
+ doc_attrs,
+ ..
+ } = &args[i];
+ let gen = field.generic();
+
+ let mut fn_gen = None;
+
+ for (idx, field @ Field { ty, name, .. }) in args.iter().enumerate() {
+ let generic = field.generic().to_token_stream();
+ if idx == i {
+ unset_gens.push(field.unset());
+ destructure.push(quote!(#name: _));
+ if let Some(bounds) = field.get_generics(&gen) {
+ fn_gen = Some(quote!(#bounds));
+ set_gens.push(quote!(::htmx::__private::Set<#generic>));
+ structure.push(quote!(#name: ::htmx::__private::Set(#name)));
+ } else {
+ fn_gen = Some(quote!(#gen: Into<#ty>));
+ set_gens.push(quote!(::htmx::__private::Set<#ty>));
+ structure.push(quote!(#name: ::htmx::__private::Set(#name.into())));
+ };
+ } else {
+ impl_gens.push(quote!(#generic));
+ unset_gens.push(quote!(#generic));
+ set_gens.push(quote!(#generic));
+ destructure.push(quote!(#name));
+ structure.push(quote!(#name));
}
}
- } else {
- bail!(item, "only functions and structs are supported")
+
+ let already_set_msg = format!("{field_name} was alredy set");
+ let already_set_ty = format_ident!("{field_name}_was_alredy_set");
+
+ let extra_gen = field.is_impl_trait().then_some(&gen).into_iter();
+
+ setters.push(quote! {
+ impl<#html_lt, #(#impl_gens),*> #struct_name<#html_lt, #(#unset_gens),*> {
+ #doc_attrs
+ pub fn #field_name<#fn_gen>(self, #field_name: #gen)
+ -> #struct_name<#html_lt, #(#set_gens),*> {
+ let Self {
+ html,
+ #(#destructure),*
+ } = self;
+ #struct_name {
+ html,
+ #(#structure),*
+ }
+ }
+ }
+
+ #[allow(non_camel_case_types)]
+ pub struct #already_set_ty;
+
+ impl<#html_lt, #(#extra_gen,)* #(#impl_gens),*> #struct_name<#html_lt, #(#set_gens),*> {
+ #[doc(hidden)]
+ #[deprecated = #already_set_msg]
+ #[allow(unused)]
+ pub fn #field_name<__Gen>(
+ self,
+ #field_name: __Gen, _: #already_set_ty
+ ) -> Self {
+ self
+ }
+ }
+ });
+ }
+
+ // #attrs #vis struct
+ Ok(quote! {
+ #use ::htmx::__private::{Set};
+
+ #(#attrs)*
+ #[must_use = "call body or close"]
+ #vis struct #struct_name<#html_lt, #(#generics),*> {
+ html: ::core::marker::PhantomData<html_lt ()>,
+ #(#fields),*
+ }
+ const _: () = {
+ use ::core::default::Default as _;
+ impl<#html_lt> #struct_name<#html_lt, #(#unsets_types),*> {
+ pub fn new(_: &mut ::htmx::Html) -> Self {
+ Self {
+ html: ::core::marker::PhantomData,
+ #(#field_names: #unset_values),*
+ }
+ }
+ }
+
+ #(#setters)*
+
+ impl<#html_lt, #(#optional_gens),*> #struct_name<#html_lt, #(#mandatory_gens),*> {
+ pub fn body(self, #body: impl ::htmx::IntoHtml + #html_lt) -> impl ::htmx::IntoHtml + #html_lt {
+ let Self {
+ html: _,
+ #(#field_names),*
+ } = self;
+
+ #(#field_destructure;)*
+
+
+ ::htmx::Fragment(move |__html: &mut ::htmx::Html|(||{#fn_body})().into_html(__html))
+ }
+
+ pub fn close(self) -> impl ::htmx::IntoHtml + #html_lt {
+ self.body(::htmx::Fragment::EMPTY)
+ }
+ }
+ };
})
}
-// todo derive macro
-// #[component]
-// fn MyFnComponent(a: bool, b: String) -> Html {
-// htmx! {crate
-//
-// }
-// }
-//
-// // Generates
-//
-// #[derive(typed_builder::TypedBuilder)]
-// #[builder(crate_module_path=::typed_builder)]
-// #[builder(build_method(into = Html))]
-// struct MyFnComponent {
-// a: bool,
-// b: String,
-// }
-//
-// impl Into for MyFnComponent {
-// fn into(self) -> Html {
-// let Self { a, b } = self;
-// htmx! {crate
-//
-// }
-// }
-// }
-//
-// // Using only struct
-// #[derive(Component)]
-// struct MyStructComponent {
-// a: bool,
-// b: String,
-// }
-// impl Into for MyStructComponent {
-// fn into(self) -> Html {
-// let Self { a, b } = self;
-// htmx! {crate
-//
-// }
-// }
-// }
-//
-//
#[derive(FromAttr)]
#[attribute(ident = default)]
struct DefaultAttr(FlagOrValue);
#[derive(FromAttr)]
-#[attribute(ident = default)]
-struct ChildrenAttr(bool);
-
-fn type_attrs(name: &Ident, ty: &Type, attrs: &mut Vec) -> Result<()> {
- let DefaultAttr(default) = DefaultAttr::remove_attributes(attrs)?;
- let ChildrenAttr(children) = ChildrenAttr::remove_attributes(attrs)?;
- if let Type::Path(path) = ty {
- // TODO strip Option
- if default.is_flag() || path.path.is_ident("bool") || path.path.is_ident("Option") {
- attrs.push(parse_quote!(#[builder(default)]))
- }
- if let Some(default) = default.as_value() {
- attrs.push(parse_quote!(#[builder(default = #default)]))
- }
- if children
- || path.path.is_ident("Children")
- || path.path == parse_quote!(htmx::Children)
- || path.path == parse_quote!(::htmx::Children)
- {
- attrs.push(parse_quote! {#[builder(via_mutators, mutators(
- pub fn child(&mut self, __child: impl ::htmx::ToHtml) {
- self.#name.push(__child);
- }
- ))]});
- attrs.push(parse_quote!(#[allow(missing_docs)]));
- }
- }
- attrs.push(parse_quote!(#[builder(setter(into))]));
- Ok(())
-}
+#[attribute(ident = default_type)]
+struct DefaultType(Option);
diff --git a/htmx-macros/src/htmx/html.rs b/htmx-macros/src/htmx/html.rs
index 29f6b40..13cd079 100644
--- a/htmx-macros/src/htmx/html.rs
+++ b/htmx-macros/src/htmx/html.rs
@@ -1,29 +1,21 @@
use htmx_script::{Script, ToJs};
-use manyhow::{ensure, ErrorMessage, Result};
+use manyhow::{ensure, Error, ErrorMessage, Result};
use proc_macro2::TokenStream;
-use proc_macro_utils::TokenStream2Ext;
use quote::ToTokens;
use rstml::atoms::{CloseTag, OpenTag};
use rstml::node::{
AttributeValueExpr, KeyedAttribute, KeyedAttributeValue, NodeAttribute, NodeBlock, NodeElement,
- NodeName,
+ NodeFragment, NodeName,
};
use rstml::recoverable::Recoverable;
use syn::spanned::Spanned;
use syn::{parse2, Expr, ExprLit, ExprPath, Lit, LitStr, Stmt};
use super::special_components::{Node, Special};
+use super::try_into_iter;
use crate::*;
pub fn html(input: TokenStream) -> Result {
- let mut parser = input.clone().parser();
- let (input, expr) =
- if let ( Some(expr), Some(_)) = (parser.next_expression(), parser.next_tt_fat_arrow()) {
- (parser.into_token_stream(), quote!(&mut #expr))
- } else {
- (input, quote!(::htmx::Html::new()))
- };
-
let nodes = rstml::Parser::new(
rstml::ParserConfig::new()
.recover_block(true)
@@ -32,19 +24,161 @@ pub fn html(input: TokenStream) -> Result {
.raw_text_elements(["script"].into()),
)
// TODO parse_recoverable
- .parse_simple(input)?
- .into_iter()
- .map(expand_node)
- .collect::()?;
+ .parse_simple(input)?;
- Ok(quote! {
- {
- use ::htmx::native::*;
- let mut __html = #expr;
- #nodes
- __html
+ super::expand_nodes(nodes)
+}
+
+impl TryFrom for super::Node {
+ type Error = Error;
+
+ fn try_from(value: Node) -> std::result::Result {
+ match value {
+ Node::Comment(comment) => bail!(comment, "html comments are not supported"),
+ Node::Doctype(doc_type) => bail!(doc_type, "doc typ is set automatically"),
+ Node::Fragment(NodeFragment { tag_open, .. }) => bail!(tag_open, "missing tag name"),
+ Node::Element(element) => Ok(super::Node::Element(element.try_into()?)),
+ Node::Block(block) => Ok(super::Node::Block(block.into_token_stream())),
+ Node::Text(text) => Ok(super::Node::String(text.value)),
+ Node::RawText(text) => bail!(
+ text.into_token_stream().into_iter().next(),
+ "expected `<`, `{{` or `\"`"
+ ),
+ Node::Custom(special) => special.try_into(),
}
- })
+ }
+}
+
+impl TryFrom> for super::Element {
+ type Error = Error;
+
+ fn try_from(value: NodeElement) -> std::result::Result {
+ let NodeElement {
+ open_tag,
+ children,
+ close_tag,
+ } = value;
+ Ok(super::Element {
+ close_tag: close_tag.and_then(|ct| match ct.name {
+ NodeName::Path(p) if !ct.name.is_wildcard() => Some(p.into_token_stream()),
+ _ => None,
+ }),
+ attributes: try_into_iter(open_tag.attributes)?,
+ body: if !children.is_empty()
+ && matches!(&open_tag.name, NodeName::Path(p) if p.path.is_ident("script"))
+ {
+ let Some(Node::RawText(script)) = children.first() else {
+ unreachable!("script always raw text")
+ };
+ let script = script.into_token_stream();
+ if let Ok(script) = parse2::(script.clone()) {
+ super::ElementBody::Script(super::ScriptBody::String(script))
+ } else if let Ok(block) =
+ parse2::>(script.clone()).map(Recoverable::inner)
+ {
+ super::ElementBody::Script(super::ScriptBody::Expr(block.into_token_stream()))
+ } else {
+ let script: Script = parse2(script)?;
+ let script = script.to_java_script();
+ // quote!(__html.body(#script);)
+ super::ElementBody::Script(super::ScriptBody::Expr(script.into_token_stream()))
+ }
+ } else {
+ super::ElementBody::Children(try_into_iter(children)?)
+ },
+ open_tag: open_tag.name.try_into()?,
+ })
+ }
+}
+
+fn string_from_block(block: &syn::Block) -> Option<&LitStr> {
+ if let [
+ Stmt::Expr(
+ Expr::Lit(ExprLit {
+ lit: Lit::Str(lit), ..
+ }),
+ None,
+ ),
+ ] = &block.stmts[..]
+ {
+ Some(lit)
+ } else {
+ None
+ }
+}
+
+impl TryFrom for super::OpenTag {
+ type Error = Error;
+
+ fn try_from(value: NodeName) -> std::result::Result {
+ Ok(match value {
+ NodeName::Path(path) => super::OpenTag::Path(path.into_token_stream()),
+ name @ NodeName::Punctuated(_) => {
+ super::OpenTag::from_str(name.to_string(), name.span())?
+ }
+ NodeName::Block(name) => {
+ if let Some(name) = string_from_block(&name) {
+ super::OpenTag::from_str(name.value(), name.span())?
+ } else {
+ super::OpenTag::Expr(name.into_token_stream())
+ }
+ }
+ })
+ }
+}
+
+impl TryFrom for super::Attribute {
+ type Error = Error;
+
+ fn try_from(value: NodeAttribute) -> std::result::Result {
+ Ok(match value {
+ NodeAttribute::Block(name) => super::Attribute {
+ key: if let Some(name) = name.try_block().and_then(string_from_block) {
+ super::AttributeKey::from_str(name.value(), name.span())?
+ } else {
+ super::AttributeKey::Expr(name.into_token_stream())
+ },
+ value: None,
+ },
+ NodeAttribute::Attribute(attribute) => super::Attribute {
+ value: attribute.value().map(ToTokens::into_token_stream),
+ key: attribute.key.try_into()?,
+ },
+ })
+ }
+}
+
+impl TryFrom for super::AttributeKey {
+ type Error = Error;
+
+ fn try_from(value: NodeName) -> std::result::Result {
+ Ok(match value {
+ NodeName::Path(p) if p.path.get_ident().is_some() => {
+ super::AttributeKey::Fn(p.into_token_stream())
+ }
+ NodeName::Path(p) if p.path.segments.first().is_some_and(|hx| hx.ident == "hx") => {
+ let sident = p
+ .path
+ .segments
+ .iter()
+ .map(|i| i.ident.to_string().replace('_', "-"))
+ // hx::swap::oob
+ .collect::>()
+ .join("-");
+ super::AttributeKey::from_str(sident, p.span())?
+ }
+ key @ (NodeName::Punctuated(_) | NodeName::Path(_)) => {
+ super::AttributeKey::from_str(key.to_string(), key.span())?
+ }
+ NodeName::Block(block) => {
+ if let Some(key) = string_from_block(&block) {
+ super::AttributeKey::from_str(key.value(), key.span())?
+ } else {
+ super::AttributeKey::Expr(block.into_token_stream())
+ }
+ }
+ })
+ }
}
pub fn expand_node(node: Node) -> Result {
@@ -61,7 +195,7 @@ pub fn expand_node(node: Node) -> Result {
..
}) => {
let script = name.to_string() == "script";
- let (name, custom) = name_to_struct(name)?;
+ let (name, node_type) = name_to_struct(name)?;
let attributes = attributes
.into_iter()
.map(|attribute| match attribute {
@@ -72,9 +206,11 @@ pub fn expand_node(node: Node) -> Result {
}) => match possible_value {
KeyedAttributeValue::Binding(_) => todo!("{}", line!()),
KeyedAttributeValue::Value(AttributeValueExpr { value, .. }) => {
- attribute_key_to_fn(key, value, custom)
+ attribute_key_to_fn(key, value, matches!(node_type, NodeType::Custom))
+ }
+ KeyedAttributeValue::None => {
+ attribute_key_to_fn(key, true, matches!(node_type, NodeType::Custom))
}
- KeyedAttributeValue::None => attribute_key_to_fn(key, true, custom),
},
})
.collect::>>()?;
@@ -87,21 +223,33 @@ pub fn expand_node(node: Node) -> Result {
};
let script = script.into_token_stream();
if let Ok(script) = parse2::(script.clone()) {
+ // quote!(__html.body(#script);)
quote!(::htmx::ToScript::to_script(script, &mut __html);)
} else if let Ok(block) =
parse2::>(script.clone()).map(Recoverable::inner)
{
- quote!(::htmx::ToScript::to_script(&{#[allow(unused_braces)] #block}, &mut __html);)
+ // quote!(__html.body({#[allow(unused_braces)] #block});)
+ quote!(::htmx::ToScript::to_script(&{# [allow(unused_braces)] #block}, &mut __html);)
} else {
let script: Script = parse2(script)?;
let script = script.to_java_script();
- quote!(::htmx::ToScript::to_script(#script, &mut __html);)
+ // quote!(__html.body(#script);)
+ quote!(::htmx::ToScript::to_script(script, &mut __html);)
}
} else {
expand_nodes(children)?
};
- let body = (!children.is_empty()).then(|| quote!(let mut __html = __html.body(|mut __html| {#children});));
- let main = quote!({let mut __html = #name #(.#attributes)*; #body;});
+ let close_arg = if matches!(node_type, NodeType::Component) {
+ quote!(&mut __html)
+ } else {
+ quote!()
+ };
+ let body = if children.is_empty() {
+ quote!(.close(#close_arg))
+ } else {
+ quote!(.body(::htmx::Fragment(|mut __html: &mut ::htmx::Html| {#children}), #close_arg))
+ };
+ let main = quote!({{let mut __html = #name #(.#attributes)*; __html}#body;});
match close_tag {
Some(CloseTag {
@@ -115,7 +263,7 @@ pub fn expand_node(node: Node) -> Result {
}
}
Node::Block(_) | Node::Text(_) => {
- quote!(::htmx::ToHtml::to_html(&{#[allow(unused_braces)] #node}, &mut __html);)
+ quote!(::htmx::IntoHtml::into_html({#[allow(unused_braces)] #node}, &mut __html);)
}
Node::RawText(_) => todo!("{}", line!()),
Node::Custom(c) => c.expand_node()?,
@@ -137,14 +285,28 @@ pub fn ensure_tag_name(name: String, span: impl ToTokens) -> Result Result<(TokenStream, bool)> {
+enum NodeType {
+ Native,
+ Component,
+ Custom,
+}
+
+fn name_to_struct(name: NodeName) -> Result<(TokenStream, NodeType)> {
match name {
- NodeName::Path(path) => Ok((quote!(#path::new(&mut __html)), false)),
+ NodeName::Path(path)
+ if path
+ .path
+ .get_ident()
+ .is_some_and(|i| !i.to_string().contains(char::is_uppercase)) =>
+ {
+ Ok((quote!(#path::new(&mut __html)), NodeType::Native))
+ }
+ NodeName::Path(path) => Ok((quote!(#path::new()), NodeType::Component)),
name @ NodeName::Punctuated(_) => {
let name = ensure_tag_name(name.to_string(), name)?;
Ok((
quote!(::htmx::CustomElement::new_unchecked(&mut __html, #name)),
- true,
+ NodeType::Custom,
))
}
// This {...}
@@ -162,12 +324,12 @@ fn name_to_struct(name: NodeName) -> Result<(TokenStream, bool)> {
let name = ensure_tag_name(name.value(), name)?;
Ok((
quote!(::htmx::CustomElement::new_unchecked(&mut __html, #name)),
- true,
+ NodeType::Custom,
))
} else {
Ok((
quote!(::htmx::CustomElement::new(&mut __html, #name)),
- true,
+ NodeType::Custom,
))
}
}
diff --git a/htmx-macros/src/htmx/mod.rs b/htmx-macros/src/htmx/mod.rs
index 3785f42..d6a370a 100644
--- a/htmx-macros/src/htmx/mod.rs
+++ b/htmx-macros/src/htmx/mod.rs
@@ -1,10 +1,16 @@
-mod html;
+#![allow(unused)]
+pub mod html;
mod special_components;
-mod rusty;
+pub mod rusty;
-pub use html::html;
-pub use rusty::html as rtml;
+use html_escape::{encode_safe, encode_script};
+use manyhow::ensure;
+use proc_macro2::{Literal, Span};
+use syn::spanned::Spanned;
+use syn::LitStr;
+
+use super::*;
// pub fn html(input: TokenStream) -> Result {
// if input.is_empty() {
@@ -26,3 +32,301 @@ pub use rusty::html as rtml;
// rusty::html(input.collect())
// }
// }
+
+fn try_into_iter(
+ input: impl IntoIterator- >,
+) -> Result> {
+ input.into_iter().map(TryInto::try_into).collect()
+}
+
+fn expand_nodes(
+ nodes: impl IntoIterator
- >,
+) -> Result {
+ let nodes = nodes
+ .into_iter()
+ .map(TryInto::try_into)
+ .collect::>>()?;
+ Ok(quote! {
+ ::htmx::Fragment(move |mut __html: &mut ::htmx::Html| {
+ #[allow(unused_braces)]
+ {
+ use ::htmx::native::*;
+ use ::htmx::IntoHtml as _;
+ #(#nodes)*
+ };
+ })
+ })
+}
+
+enum Node {
+ String(LitStr),
+ Block(TokenStream),
+ If(If),
+ For(For),
+ While(While),
+ FunctionCall(FunctionCall),
+ Element(Element),
+}
+
+impl ToTokens for Node {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ match self {
+ Node::String(lit) => {
+ let value = lit.value();
+ let value = encode_safe(&value);
+ let mut value = Literal::string(&value);
+ value.set_span(lit.span());
+ quote!(::htmx::IntoHtml::into_html(::htmx::RawSrc::new(#value), &mut __html);).to_tokens(tokens)
+ }
+ Node::Block(block) => {
+ quote!(::htmx::IntoHtml::into_html({#[allow(unused_braces)] {#block}}, &mut __html);).to_tokens(tokens)
+ }
+ Node::If(if_) => if_.to_tokens(tokens),
+ Node::For(for_) => for_.to_tokens(tokens),
+ Node::While(while_) => while_.to_tokens(tokens),
+ Node::FunctionCall(call) => call.to_tokens(tokens),
+ Node::Element(element) => element.to_tokens(tokens)
+ }
+ }
+}
+
+struct If {
+ condition: TokenStream,
+ then_branch: Vec,
+ else_branch: ElseBranch,
+}
+
+impl ToTokens for If {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self {
+ condition,
+ then_branch,
+ else_branch,
+ } = self;
+ quote! {
+ if #condition {
+ #(#then_branch)*
+ } #else_branch
+ }
+ .to_tokens(tokens)
+ }
+}
+
+enum ElseBranch {
+ None,
+ Else(Vec),
+ ElseIf(Box),
+}
+
+impl ToTokens for ElseBranch {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ match self {
+ ElseBranch::None => {}
+ ElseBranch::Else(nodes) => quote!(else {#(#nodes)*}).to_tokens(tokens),
+ ElseBranch::ElseIf(if_) => quote!(else #if_).to_tokens(tokens),
+ }
+ }
+}
+
+struct For {
+ pat: TokenStream,
+ expr: TokenStream,
+ body: Vec,
+}
+
+impl ToTokens for For {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self { pat, expr, body } = self;
+ quote! {
+ for #pat in #expr {
+ #(#body)*
+ }
+ }
+ .to_tokens(tokens)
+ }
+}
+
+struct While {
+ expr: TokenStream,
+ body: Vec,
+}
+
+impl ToTokens for While {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self { expr, body } = self;
+ quote! {
+ while #expr {
+ #(#body)*
+ }
+ }
+ .to_tokens(tokens)
+ }
+}
+
+struct FunctionCall {
+ function: TokenStream,
+ args: Vec,
+}
+
+impl ToTokens for FunctionCall {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self { function, args } = self;
+ quote!(::htmx::IntoHtml::into_html(#function(#(Into::into(#args),)*), &mut __html);)
+ .to_tokens(tokens)
+ }
+}
+
+struct Element {
+ open_tag: OpenTag,
+ close_tag: Option,
+ attributes: Vec,
+ body: ElementBody,
+}
+
+impl ToTokens for Element {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self {
+ open_tag,
+ close_tag,
+ attributes,
+ body,
+ } = self;
+
+ let close_tag = close_tag.iter();
+
+ let mut attributes = attributes.clone();
+
+ if !matches!(open_tag, OpenTag::Path(_)) {
+ for attribute in &mut attributes {
+ if let AttributeKey::Fn(name) = &attribute.key {
+ attribute.key = AttributeKey::from_str(name.to_string(), name.span())
+ .expect("idents should be valid attribute keys")
+ }
+ }
+ };
+
+ quote! {
+ {{
+ #( use ::htmx::__private::Unused; #close_tag::unused(); )*
+ #open_tag #(#attributes)* #body
+ }.into_html(&mut __html)}
+ }
+ .to_tokens(tokens)
+ }
+}
+
+enum OpenTag {
+ Path(TokenStream),
+ String(String, Span),
+ Expr(TokenStream),
+}
+
+impl OpenTag {
+ fn from_str(name: String, span: Span) -> Result {
+ ensure!(
+ name.to_ascii_lowercase().chars()
+ .all(|c| matches!(c, '-' | '.' | '0'..='9' | '_' | 'a'..='z' | '\u{B7}' | '\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{37D}' | '\u{37F}'..='\u{1FFF}' | '\u{200C}'..='\u{200D}' | '\u{203F}'..='\u{2040}' | '\u{2070}'..='\u{218F}' | '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}' | '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' | '\u{10000}'..='\u{EFFFF}')),
+ span,
+ "invalid tag name `{name}`, https://html.spec.whatwg.org/multipage/custom-elements.html#prod-potentialcustomelementname"
+ // TODO similar function but with css error: https://drafts.csswg.org/css-syntax-3/#non-ascii-ident-code-point
+ );
+ Ok(OpenTag::String(name, span))
+ }
+}
+
+impl ToTokens for OpenTag {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ match self {
+ OpenTag::Path(path) => quote!(#path::new(&mut __html)),
+ OpenTag::String(name, span) => {
+ let name = quote_spanned!(*span=> #name);
+ quote!(::htmx::CustomElement::new_unchecked(&mut __html, #name))
+ }
+ OpenTag::Expr(name) => quote!(quote!(::htmx::CustomElement::new(&mut __html, #name)),),
+ }
+ .to_tokens(tokens)
+ }
+}
+
+#[derive(Clone)]
+struct Attribute {
+ key: AttributeKey,
+ // TODO value encoding
+ value: Option,
+}
+
+impl ToTokens for Attribute {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self { key, value } = self;
+ let value = value.clone().unwrap_or_else(|| quote!(true));
+ match key {
+ AttributeKey::Fn(fun) => quote!(.#fun(#value)),
+ AttributeKey::String(key, span) => {
+ let key = quote_spanned!(*span => #key);
+ quote!(.custom_attr_unchecked(#key, #value))
+ }
+ AttributeKey::Expr(key) => quote!(.custom_attr(#key, #value)),
+ }
+ .to_tokens(tokens);
+ }
+}
+
+#[derive(Clone)]
+enum AttributeKey {
+ Fn(TokenStream),
+ String(String, Span),
+ Expr(TokenStream),
+}
+
+impl AttributeKey {
+ fn from_str(value: String, span: Span) -> Result {
+ ensure!(
+ !value.to_string().chars().any(|c| c.is_whitespace()
+ || c.is_control()
+ || matches!(c, '\0' | '"' | '\'' | '>' | '/' | '=')),
+ span,
+ "invalid key `{value}`, https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0"
+ );
+ Ok(AttributeKey::String(value, span))
+ }
+}
+
+enum ElementBody {
+ Script(ScriptBody),
+ Children(Vec),
+}
+
+impl ToTokens for ElementBody {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ match self {
+ ElementBody::Script(script) => quote!(.body(#script)),
+ ElementBody::Children(children) if children.is_empty() => {
+ quote!(.close())
+ }
+ ElementBody::Children(children) => {
+ quote!(.body(::htmx::Fragment(|mut __html: &mut ::htmx::Html| {#(#children)*})))
+ }
+ }
+ .to_tokens(tokens)
+ }
+}
+
+enum ScriptBody {
+ String(LitStr),
+ Expr(TokenStream),
+}
+
+impl ToTokens for ScriptBody {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ match self {
+ ScriptBody::String(lit) => {
+ let value = lit.value();
+ let value = encode_script(&value);
+ let mut value = Literal::string(&value);
+ value.set_span(lit.span());
+ quote!(RawSrc(#value)).to_tokens(tokens)
+ }
+ ScriptBody::Expr(expr) => expr.to_tokens(tokens),
+ }
+ }
+}
diff --git a/htmx-macros/src/htmx/rusty.rs b/htmx-macros/src/htmx/rusty.rs
index 7af5d38..de92eab 100644
--- a/htmx-macros/src/htmx/rusty.rs
+++ b/htmx-macros/src/htmx/rusty.rs
@@ -2,6 +2,7 @@ use std::mem;
use manyhow::{bail, ensure, Result};
use proc_macro2::{TokenStream, TokenTree};
+use proc_macro_utils::TokenStream2Ext;
use quote::{format_ident, ToTokens};
use syn::ext::IdentExt;
use syn::parse::discouraged::Speculative;
@@ -14,17 +15,14 @@ use syn_derive::{Parse, ToTokens};
use super::html::ensure_tag_name;
use crate::*;
-pub fn html(input: TokenStream) -> Result {
+pub fn rtml(input: TokenStream) -> Result {
let nodes = expand_nodes(Punctuated::::parse_terminated.parse2(input)?);
Ok(quote! {
- #use ::htmx::{ToHtml, Html, IntoHtmlElements};
- {
+ ::htmx::Fragment(|mut __html: &mut ::htmx::Html| {
use ::htmx::native::*;
- let mut __html = Html::new();
#(#nodes)*
- __html
- }
+ })
})
}
@@ -63,8 +61,8 @@ impl Node {
quote!(::htmx::ToHtml::to_html(lit, &mut __html);)
}
Node::Block(block) => {
- quote!(::htmx::ToHtml::to_html(block, &mut __html);)
- },
+ quote!(::htmx::IntoHtml::into_html({#[allow(unused_braces)] #block}, &mut __html);)
+ }
Node::Element(element) => element.expand(),
Node::If(node) => node.expand(),
Node::For(node) => node.expand(),
@@ -387,6 +385,7 @@ struct Element {
impl Element {
fn expand(self) -> TokenStream {
let mut attrs = self.attrs.unwrap_or_default();
+ let mut close_arg = quote!();
let name = match self.path {
ElementName::String(name) => {
quote!(::htmx::CustomElement::new_unchecked(&mut __html, #name);)
@@ -396,7 +395,17 @@ impl Element {
attrs.attrs.push(Attr::Classes(classes));
quote!(div::new(&mut __html))
}
- ElementName::Path(path) => quote!(#path::new(&mut __html);),
+ ElementName::Path(path)
+ if path
+ .get_ident()
+ .is_some_and(|i| !i.to_string().contains(char::is_uppercase)) =>
+ {
+ quote!(#path::new(&mut __html);)
+ }
+ ElementName::Path(path) => {
+ close_arg = quote!(&mut __html);
+ quote!(#path::new();)
+ }
};
let mut children = expand_nodes(
@@ -410,13 +419,16 @@ impl Element {
let attrs = attrs.attrs.into_iter().map(Attr::expand);
- let body = children.peek().is_some().then(|| quote!(__html.body(|mut __html| {#(#children)*})));
+ let body = children
+ .peek()
+ .is_some()
+ .then(|| quote!(.body(::htmx::Fragment(|mut __html: &mut ::htmx::Html| {#(#children)*}), #close_arg))).unwrap_or_else(|| quote!(.close(#close_arg)));
- quote!({
- let mut __html = #name
- #(__html #attrs;)*
- #body;
- })
+ quote!({{
+ let mut __html = #name;
+ #(let __html = __html #attrs;)*
+ __html
+ }#body;})
}
}
diff --git a/htmx-macros/src/htmx/special_components.rs b/htmx-macros/src/htmx/special_components.rs
index 4ed6657..25ad02c 100644
--- a/htmx-macros/src/htmx/special_components.rs
+++ b/htmx-macros/src/htmx/special_components.rs
@@ -74,6 +74,37 @@ pub enum Special {
FunctionCall(FunctionCall),
}
+fn map_vec(value: Vec) -> Result> {
+ value.into_iter().map(super::Node::try_from).collect()
+}
+
+impl TryFrom for super::Node {
+ type Error = manyhow::Error;
+
+ fn try_from(value: Special) -> std::result::Result {
+ Ok(match value {
+ Special::If(if_) => super::Node::If(if_.try_into()?),
+ Special::For(For {
+ pat, expr, body, ..
+ }) => super::Node::For(super::For {
+ pat: pat.into_token_stream(),
+ expr: expr.into_token_stream(),
+ body: map_vec(body)?,
+ }),
+ Special::While(While { expr, body, .. }) => super::Node::While(super::While {
+ expr: expr.into_token_stream(),
+ body: map_vec(body)?,
+ }),
+ Special::FunctionCall(FunctionCall { function, args, .. }) => {
+ super::Node::FunctionCall(super::FunctionCall {
+ function: function.into_token_stream(),
+ args: args.into_iter().map(ToTokens::into_token_stream).collect(),
+ })
+ }
+ })
+ }
+}
+
impl Special {
pub(crate) fn expand_node(self) -> Result {
match self {
@@ -123,6 +154,31 @@ pub struct If {
pub else_branch: ElseBranch,
}
+impl TryFrom for super::If {
+ type Error = manyhow::Error;
+
+ fn try_from(
+ If {
+ condition,
+ then_branch,
+ else_branch,
+ ..
+ }: If,
+ ) -> std::result::Result {
+ Ok(super::If {
+ condition: condition.into_token_stream(),
+ then_branch: map_vec(then_branch)?,
+ else_branch: match else_branch {
+ ElseBranch::None => super::ElseBranch::None,
+ ElseBranch::Else { body, .. } => super::ElseBranch::Else(map_vec(body)?),
+ ElseBranch::ElseIf { body, .. } => {
+ super::ElseBranch::ElseIf(Box::new((*body).try_into()?))
+ }
+ },
+ })
+ }
+}
+
impl If {
fn expand_node(self) -> Result {
let If {
diff --git a/htmx-macros/src/lib.rs b/htmx-macros/src/lib.rs
index 58902a3..11e42d1 100644
--- a/htmx-macros/src/lib.rs
+++ b/htmx-macros/src/lib.rs
@@ -21,9 +21,9 @@ mod htmx;
// more rusty kind of typst syntax:
// rtml! {div(attr: "hello") [ {rust block}, "literals"]}
#[manyhow(proc_macro)]
-pub use htmx::html;
+pub use htmx::html::html;
#[manyhow(proc_macro)]
-pub use htmx::rtml;
+pub use htmx::rusty::rtml;
// js!{ }
diff --git a/src/actix.rs b/src/actix.rs
index af43ce3..5436119 100644
--- a/src/actix.rs
+++ b/src/actix.rs
@@ -7,7 +7,7 @@ use actix_web::http::header::ContentType;
use actix_web::web::Bytes;
use actix_web::{HttpResponse, Responder};
-use crate::{Css, Html, HtmxSrc};
+use crate::{Css, Html, HtmxSrc, Fragment};
impl Responder for Html {
type Body = BoxBody;
@@ -19,6 +19,16 @@ impl Responder for Html {
}
}
+impl Responder for Fragment {
+ type Body = BoxBody;
+
+ fn respond_to(self, _req: &actix_web::HttpRequest) -> HttpResponse {
+ HttpResponse::Ok()
+ .content_type(ContentType::html())
+ .body(Html::from(self))
+ }
+}
+
impl MessageBody for Html {
type Error = ::Error;
diff --git a/src/attributes.rs b/src/attributes.rs
index 04f9a58..37bacbc 100644
--- a/src/attributes.rs
+++ b/src/attributes.rs
@@ -6,7 +6,7 @@ use std::num::{NonZeroU64, NonZeroU8};
use derive_more::Display;
use forr::forr;
-use crate::WriteHtml;
+use crate::Html;
/// An attribute that accepts an attribute value or a flag.
pub struct FlagOrValue(PhantomData);
@@ -36,19 +36,19 @@ pub enum ValueOrFlag {
/// [`Number`].
pub trait ToAttribute