diff --git a/hypertext-macros/src/component.rs b/hypertext-macros/src/component.rs index 04dcd7b..b1032c7 100644 --- a/hypertext-macros/src/component.rs +++ b/hypertext-macros/src/component.rs @@ -4,6 +4,13 @@ use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, Visibility, parse::Parse}; use crate::html::generate::Generator; +#[derive(Default, Clone, Copy)] +pub enum ComponentInstantiationMode { + #[default] + StructLiteral, + Builder, +} + pub struct ComponentArgs { visibility: Visibility, ident: Option, diff --git a/hypertext-macros/src/derive.rs b/hypertext-macros/src/derive.rs index e023353..10104be 100644 --- a/hypertext-macros/src/derive.rs +++ b/hypertext-macros/src/derive.rs @@ -1,9 +1,9 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::{DeriveInput, Error, spanned::Spanned}; +use syn::{Data, DeriveInput, Error, spanned::Spanned}; use crate::{ - AttributeValueNode, Context, Document, Maud, Nodes, Rsx, + AttributeValueNode, ComponentInstantiationMode, Context, Document, Maud, Nodes, Rsx, html::{self, generate::Generator}, }; @@ -35,13 +35,45 @@ fn renderable_element(input: &DeriveInput) -> syn::Result> { Some(( attr, html::generate::lazy::> - as fn(TokenStream, bool) -> syn::Result, + as fn( + TokenStream, + bool, + Option, + ) -> syn::Result, + Some(ComponentInstantiationMode::StructLiteral), + )) + } else if attr.path().is_ident("maud_cb") { + Some(( + attr, + html::generate::lazy::> + as fn( + TokenStream, + bool, + Option, + ) -> syn::Result, + Some(ComponentInstantiationMode::Builder), )) } else if attr.path().is_ident("rsx") { Some(( attr, html::generate::lazy::> - as fn(TokenStream, bool) -> syn::Result, + as fn( + TokenStream, + bool, + Option, + ) -> syn::Result, + Some(ComponentInstantiationMode::StructLiteral), + )) + } else if attr.path().is_ident("rsx_cb") { + Some(( + attr, + html::generate::lazy::> + as fn( + TokenStream, + bool, + Option, + ) -> syn::Result, + Some(ComponentInstantiationMode::Builder), )) } else { None @@ -49,14 +81,14 @@ fn renderable_element(input: &DeriveInput) -> syn::Result> { }) .peekable(); - let (lazy_fn, tokens) = match (attrs.next(), attrs.peek()) { - (Some((attr, f)), None) => (f, attr.meta.require_list()?.tokens.clone()), - (Some((attr, _)), Some(_)) => { + let (lazy_fn, tokens, instantiation_mode) = match (attrs.next(), attrs.peek()) { + (Some((attr, f, mode)), None) => (f, attr.meta.require_list()?.tokens.clone(), mode), + (Some((attr, _, _)), Some(_)) => { let mut error = Error::new( attr.span(), "cannot have multiple `maud` or `rsx` attributes", ); - for (attr, _) in attrs { + for (attr, _, _) in attrs { error.combine(syn::Error::new( attr.span(), "cannot have multiple `maud` or `rsx` attributes", @@ -69,7 +101,7 @@ fn renderable_element(input: &DeriveInput) -> syn::Result> { } }; - let lazy = lazy_fn(tokens, true)?; + let lazy = lazy_fn(tokens, true, instantiation_mode)?; let name = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); @@ -112,7 +144,7 @@ fn attribute_renderable(input: &DeriveInput) -> syn::Result> } }; - let lazy = html::generate::lazy::>(tokens, true)?; + let lazy = html::generate::lazy::>(tokens, true, None)?; let name = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let buffer_ident = Generator::buffer_ident(); @@ -135,3 +167,63 @@ fn attribute_renderable(input: &DeriveInput) -> syn::Result> Ok(Some(output)) } + +#[allow(clippy::needless_pass_by_value)] +pub fn builder(input: DeriveInput) -> syn::Result { + let Data::Struct(data_struct) = &input.data else { + return Err(syn::Error::new( + input.span(), + "#[derive(Builder)] may only be used on structs", + )); + }; + + let struct_name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let mut methods = Vec::new(); + for field in &data_struct.fields { + if let Some(name) = &field.ident { + let vis = &field.vis; + let ty = &field.ty; + + let is_skipped = field + .attrs + .iter() + .find(|attr| attr.path().is_ident("builder")) + .map_or(Ok(false), |builder_attr| { + builder_attr + .parse_nested_meta(|meta| { + if meta.path.is_ident("skip") { + return Ok(()); + } + + Err(meta.error("unrecognized builder")) + }) + .map(|()| true) + })?; + + if !is_skipped { + methods.push(quote! { + #[must_use] + #vis fn #name(mut self, #name: #ty) -> Self { + self.#name = #name; + self + } + }); + } + } + } + + let output = if methods.is_empty() { + quote! {} + } else { + quote! { + #[automatically_derived] + impl #impl_generics #struct_name #ty_generics #where_clause { + #(#methods)* + } + } + }; + + Ok(output) +} diff --git a/hypertext-macros/src/html/component.rs b/hypertext-macros/src/html/component.rs index d37d758..c47eed1 100644 --- a/hypertext-macros/src/html/component.rs +++ b/hypertext-macros/src/html/component.rs @@ -8,7 +8,7 @@ use syn::{ }; use super::{ElementBody, Generate, Generator, Literal, ParenExpr, Syntax}; -use crate::{AttributeValueNode, Context}; +use crate::{AttributeValueNode, Context, component::ComponentInstantiationMode}; pub struct Component { pub name: Ident, @@ -21,11 +21,16 @@ impl Generate for Component { const CONTEXT: Context = Context::Node; fn generate(&self, g: &mut Generator) { + let instantiation_mode = g.instantiation_mode().unwrap_or_default(); + let fields = self.attrs.iter().map(|attr| { let name = &attr.name; let value = &attr.value_expr(); - quote!(#name: #value,) + match instantiation_mode { + ComponentInstantiationMode::StructLiteral => quote!(#name: #value,), + ComponentInstantiationMode::Builder => quote!(.#name(#value)), + } }); let children = match &self.body { @@ -45,9 +50,12 @@ impl Generate for Component { let children_ident = Ident::new("children", self.name.span()); - quote!( - #children_ident: #lazy, - ) + match instantiation_mode { + ComponentInstantiationMode::StructLiteral => quote!( + #children_ident: #lazy, + ), + ComponentInstantiationMode::Builder => quote!(.#children_ident(#lazy)), + } } ElementBody::Void => quote!(), }; @@ -60,12 +68,19 @@ impl Generate for Component { .map(|dotdot| quote_spanned!(dotdot.span()=> ..::core::default::Default::default())) .unwrap_or_default(); - let init = quote! { - #name { - #(#fields)* - #children - #default - } + let init = match instantiation_mode { + ComponentInstantiationMode::StructLiteral => quote! { + #name { + #(#fields)* + #children + #default + } + }, + ComponentInstantiationMode::Builder => quote! { + #name::default() + #(#fields)* + #children + }, }; g.push_expr(Paren::default(), Self::CONTEXT, &init); diff --git a/hypertext-macros/src/html/generate.rs b/hypertext-macros/src/html/generate.rs index 08e40cd..c7d5a05 100644 --- a/hypertext-macros/src/html/generate.rs +++ b/hypertext-macros/src/html/generate.rs @@ -12,9 +12,17 @@ use syn::{ }; use super::UnquotedName; +use crate::component::ComponentInstantiationMode; -pub fn lazy(tokens: TokenStream, move_: bool) -> syn::Result { +pub fn lazy( + tokens: TokenStream, + move_: bool, + instantiation_mode: Option, +) -> syn::Result { let mut g = Generator::new_closure(T::CONTEXT); + if let Some(mode) = instantiation_mode { + g.set_instantiation_mode(mode); + } g.push(syn::parse2::(tokens)?); @@ -56,6 +64,7 @@ pub struct Generator { brace_token: Brace, parts: Vec, checks: Checks, + instantiation_mode: Option, } impl Generator { @@ -78,9 +87,18 @@ impl Generator { brace_token, parts: Vec::new(), checks: Checks::new(), + instantiation_mode: None, } } + const fn set_instantiation_mode(&mut self, instantiation_mode: ComponentInstantiationMode) { + self.instantiation_mode = Some(instantiation_mode); + } + + pub const fn instantiation_mode(&self) -> Option { + self.instantiation_mode + } + fn finish(self) -> AnyBlock { let render = if self.lazy { let buffer_ident = Self::buffer_ident(); diff --git a/hypertext-macros/src/lib.rs b/hypertext-macros/src/lib.rs index 2b2d3f7..3dad74c 100644 --- a/hypertext-macros/src/lib.rs +++ b/hypertext-macros/src/lib.rs @@ -10,26 +10,49 @@ use proc_macro::TokenStream; use syn::{DeriveInput, ItemFn, parse::Parse, parse_macro_input}; use self::html::{Document, Maud, Rsx, Syntax}; -use crate::{component::ComponentArgs, html::generate::Context}; +use crate::{ + component::{ComponentArgs, ComponentInstantiationMode}, + html::generate::Context, +}; #[proc_macro] pub fn maud(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { - lazy::(tokens, true) + lazy::(tokens, true, ComponentInstantiationMode::StructLiteral) } #[proc_macro] pub fn maud_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { - lazy::(tokens, false) + lazy::(tokens, false, ComponentInstantiationMode::StructLiteral) +} + +#[proc_macro] +pub fn maud_cb(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { + lazy::(tokens, true, ComponentInstantiationMode::Builder) +} + +#[proc_macro] +pub fn maud_cb_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { + lazy::(tokens, false, ComponentInstantiationMode::Builder) } #[proc_macro] pub fn rsx(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { - lazy::(tokens, true) + lazy::(tokens, true, ComponentInstantiationMode::StructLiteral) } #[proc_macro] pub fn rsx_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { - lazy::(tokens, false) + lazy::(tokens, false, ComponentInstantiationMode::StructLiteral) +} + +#[proc_macro] +pub fn rsx_cb(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { + lazy::(tokens, true, ComponentInstantiationMode::Builder) +} + +#[proc_macro] +pub fn rsx_cb_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { + lazy::(tokens, false, ComponentInstantiationMode::Builder) } #[proc_macro] @@ -42,11 +65,15 @@ pub fn rsx_static(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { static_::(tokens) } -fn lazy(tokens: proc_macro::TokenStream, move_: bool) -> proc_macro::TokenStream +fn lazy( + tokens: proc_macro::TokenStream, + move_: bool, + instantiation_mode: ComponentInstantiationMode, +) -> proc_macro::TokenStream where Document: Parse, { - html::generate::lazy::>(tokens.into(), move_) + html::generate::lazy::>(tokens.into(), move_, Some(instantiation_mode)) .unwrap_or_else(|err| err.to_compile_error()) .into() } @@ -71,7 +98,7 @@ pub fn attribute_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStr } fn attribute_lazy(tokens: proc_macro::TokenStream, move_: bool) -> proc_macro::TokenStream { - html::generate::lazy::>(tokens.into(), move_) + html::generate::lazy::>(tokens.into(), move_, None) .unwrap_or_else(|err| err.to_compile_error()) .into() } @@ -83,7 +110,7 @@ pub fn attribute_static(tokens: proc_macro::TokenStream) -> proc_macro::TokenStr .into() } -#[proc_macro_derive(Renderable, attributes(maud, rsx, attribute))] +#[proc_macro_derive(Renderable, attributes(maud, maud_cb, rsx, rsx_cb, attribute))] pub fn derive_renderable(input: proc_macro::TokenStream) -> proc_macro::TokenStream { derive::renderable(parse_macro_input!(input as DeriveInput)) .unwrap_or_else(|err| err.to_compile_error()) @@ -99,3 +126,10 @@ pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream { .unwrap_or_else(|err| err.to_compile_error()) .into() } + +#[proc_macro_derive(Builder, attributes(builder))] +pub fn derive_builder(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + derive::builder(parse_macro_input!(input as DeriveInput)) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/hypertext/tests/main.rs b/hypertext/tests/main.rs index 9d93de0..e63d758 100644 --- a/hypertext/tests/main.rs +++ b/hypertext/tests/main.rs @@ -4,6 +4,7 @@ use std::fmt::{self, Display, Formatter}; use hypertext::{Buffer, Lazy, Raw, maud_borrow, maud_static, prelude::*, rsx_borrow, rsx_static}; +use hypertext_macros::{Builder, maud_cb, rsx_cb}; #[test] fn readme() { @@ -754,9 +755,9 @@ fn toggles() { fn derive_default() { #[derive(Default)] struct Element<'a> { - pub id: &'a str, - pub tabindex: u32, - pub children: Lazy, + id: &'a str, + tabindex: u32, + children: Lazy, } impl<'a> Renderable for Element<'a> { @@ -770,25 +771,150 @@ fn derive_default() { } } - let with_children = rsx! { + let maud_with_children = maud! { + Element .. { + h1 { "hello" } + } + } + .render(); + + let rsx_with_children = rsx! {

hello

} .render(); - assert_eq!( - with_children.as_inner(), - r#"

hello

"# - ); + let expected_result = r#"

hello

"#; + assert_eq!(maud_with_children.as_inner(), expected_result); + assert_eq!(rsx_with_children.as_inner(), expected_result); - let without_children = rsx! { + let maud_without_children = maud! { + Element ..; + } + .render(); + + let rsx_without_children = rsx! { } .render(); - assert_eq!( - without_children.as_inner(), - r#"
"# - ); + let expected_result = r#"
"#; + assert_eq!(maud_without_children.as_inner(), expected_result); + assert_eq!(rsx_without_children.as_inner(), expected_result); +} + +#[test] +fn component_builder() { + #[derive(Default, Builder)] + struct Element<'a> { + id: &'a str, + tabindex: u32, + children: Lazy, + + #[builder(skip)] + _skipped: (), + } + + impl<'a> Renderable for Element<'a> { + fn render_to(&self, buf: &mut Buffer) { + rsx! { +
+ (self.children) +
+ } + .render_to(buf) + } + } + + let maud_cb_result = maud_cb! { + Element; + } + .render(); + + let rsx_cb_result = rsx_cb! { + + } + .render(); + + let expected_result = r#"
"#; + assert_eq!(maud_cb_result.as_inner(), expected_result); + assert_eq!(rsx_cb_result.as_inner(), expected_result); + + let maud_without_children = maud_cb! { + Element id="test"; + } + .render(); + + let rsx_without_children = rsx_cb! { + + } + .render(); + + let expected_result = r#"
"#; + assert_eq!(maud_without_children.as_inner(), expected_result); + assert_eq!(rsx_without_children.as_inner(), expected_result); + + let maud_cb_result = maud_cb! { + Element { + h1 { "hello" } + } + } + .render(); + + let rsx_cb_result = rsx_cb! { + +

hello

+
+ } + .render(); + + let expected_result = r#"

hello

"#; + assert_eq!(maud_cb_result.as_inner(), expected_result); + assert_eq!(rsx_cb_result.as_inner(), expected_result); + + let maud_cb_result = maud_cb! { + Element tabindex=2 id="element-cb" { + h1 { "hello" } + } + } + .render(); + + let rsx_cb_result = rsx_cb! { + +

hello

+
+ } + .render(); + + let expected_result = r#"

hello

"#; + assert_eq!(maud_cb_result.as_inner(), expected_result); + assert_eq!(rsx_cb_result.as_inner(), expected_result); + + let maud_result = maud! { + Element .. {( + maud_cb! { + Element id="nested-cb" { + h1 { "hello" } + } + } + )} + } + .render(); + + let rsx_result = rsx! { + ( + rsx_cb! { + +

"hello"

+
+ } + )
+ } + .render(); + + let expected_result = + r#"

hello

"#; + assert_eq!(maud_result.as_inner(), expected_result); + assert_eq!(rsx_result.as_inner(), expected_result); }