Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions hypertext-macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ident>,
Expand Down
112 changes: 102 additions & 10 deletions hypertext-macros/src/derive.rs
Original file line number Diff line number Diff line change
@@ -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},
};

Expand Down Expand Up @@ -35,28 +35,60 @@ fn renderable_element(input: &DeriveInput) -> syn::Result<Option<TokenStream>> {
Some((
attr,
html::generate::lazy::<Document<Maud>>
as fn(TokenStream, bool) -> syn::Result<TokenStream>,
as fn(
TokenStream,
bool,
Option<ComponentInstantiationMode>,
) -> syn::Result<TokenStream>,
Some(ComponentInstantiationMode::StructLiteral),
))
} else if attr.path().is_ident("maud_cb") {
Some((
attr,
html::generate::lazy::<Document<Maud>>
as fn(
TokenStream,
bool,
Option<ComponentInstantiationMode>,
) -> syn::Result<TokenStream>,
Some(ComponentInstantiationMode::Builder),
))
} else if attr.path().is_ident("rsx") {
Some((
attr,
html::generate::lazy::<Document<Rsx>>
as fn(TokenStream, bool) -> syn::Result<TokenStream>,
as fn(
TokenStream,
bool,
Option<ComponentInstantiationMode>,
) -> syn::Result<TokenStream>,
Some(ComponentInstantiationMode::StructLiteral),
))
} else if attr.path().is_ident("rsx_cb") {
Some((
attr,
html::generate::lazy::<Document<Rsx>>
as fn(
TokenStream,
bool,
Option<ComponentInstantiationMode>,
) -> syn::Result<TokenStream>,
Some(ComponentInstantiationMode::Builder),
))
} else {
None
}
})
.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",
Expand All @@ -69,7 +101,7 @@ fn renderable_element(input: &DeriveInput) -> syn::Result<Option<TokenStream>> {
}
};

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();
Expand Down Expand Up @@ -112,7 +144,7 @@ fn attribute_renderable(input: &DeriveInput) -> syn::Result<Option<TokenStream>>
}
};

let lazy = html::generate::lazy::<Nodes<AttributeValueNode>>(tokens, true)?;
let lazy = html::generate::lazy::<Nodes<AttributeValueNode>>(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();
Expand All @@ -135,3 +167,63 @@ fn attribute_renderable(input: &DeriveInput) -> syn::Result<Option<TokenStream>>

Ok(Some(output))
}

#[allow(clippy::needless_pass_by_value)]
pub fn builder(input: DeriveInput) -> syn::Result<TokenStream> {
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)
}
37 changes: 26 additions & 11 deletions hypertext-macros/src/html/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<S: Syntax> {
pub name: Ident,
Expand All @@ -21,11 +21,16 @@ impl<S: Syntax> Generate for Component<S> {
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 {
Expand All @@ -45,9 +50,12 @@ impl<S: Syntax> Generate for Component<S> {

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!(),
};
Expand All @@ -60,12 +68,19 @@ impl<S: Syntax> Generate for Component<S> {
.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);
Expand Down
20 changes: 19 additions & 1 deletion hypertext-macros/src/html/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,17 @@ use syn::{
};

use super::UnquotedName;
use crate::component::ComponentInstantiationMode;

pub fn lazy<T: Parse + Generate>(tokens: TokenStream, move_: bool) -> syn::Result<TokenStream> {
pub fn lazy<T: Parse + Generate>(
tokens: TokenStream,
move_: bool,
instantiation_mode: Option<ComponentInstantiationMode>,
) -> syn::Result<TokenStream> {
let mut g = Generator::new_closure(T::CONTEXT);
if let Some(mode) = instantiation_mode {
g.set_instantiation_mode(mode);
}

g.push(syn::parse2::<T>(tokens)?);

Expand Down Expand Up @@ -56,6 +64,7 @@ pub struct Generator {
brace_token: Brace,
parts: Vec<Part>,
checks: Checks,
instantiation_mode: Option<ComponentInstantiationMode>,
}

impl Generator {
Expand All @@ -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<ComponentInstantiationMode> {
self.instantiation_mode
}

fn finish(self) -> AnyBlock {
let render = if self.lazy {
let buffer_ident = Self::buffer_ident();
Expand Down
52 changes: 43 additions & 9 deletions hypertext-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Maud>(tokens, true)
lazy::<Maud>(tokens, true, ComponentInstantiationMode::StructLiteral)
}

#[proc_macro]
pub fn maud_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
lazy::<Maud>(tokens, false)
lazy::<Maud>(tokens, false, ComponentInstantiationMode::StructLiteral)
}

#[proc_macro]
pub fn maud_cb(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
lazy::<Maud>(tokens, true, ComponentInstantiationMode::Builder)
}

#[proc_macro]
pub fn maud_cb_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
lazy::<Maud>(tokens, false, ComponentInstantiationMode::Builder)
}

#[proc_macro]
pub fn rsx(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
lazy::<Rsx>(tokens, true)
lazy::<Rsx>(tokens, true, ComponentInstantiationMode::StructLiteral)
}

#[proc_macro]
pub fn rsx_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
lazy::<Rsx>(tokens, false)
lazy::<Rsx>(tokens, false, ComponentInstantiationMode::StructLiteral)
}

#[proc_macro]
pub fn rsx_cb(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
lazy::<Rsx>(tokens, true, ComponentInstantiationMode::Builder)
}

#[proc_macro]
pub fn rsx_cb_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
lazy::<Rsx>(tokens, false, ComponentInstantiationMode::Builder)
}

#[proc_macro]
Expand All @@ -42,11 +65,15 @@ pub fn rsx_static(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
static_::<Rsx>(tokens)
}

fn lazy<S: Syntax>(tokens: proc_macro::TokenStream, move_: bool) -> proc_macro::TokenStream
fn lazy<S: Syntax>(
tokens: proc_macro::TokenStream,
move_: bool,
instantiation_mode: ComponentInstantiationMode,
) -> proc_macro::TokenStream
where
Document<S>: Parse,
{
html::generate::lazy::<Document<S>>(tokens.into(), move_)
html::generate::lazy::<Document<S>>(tokens.into(), move_, Some(instantiation_mode))
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
Expand All @@ -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::<Nodes<AttributeValueNode>>(tokens.into(), move_)
html::generate::lazy::<Nodes<AttributeValueNode>>(tokens.into(), move_, None)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
Expand All @@ -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())
Expand All @@ -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()
}
Loading