#![warn(missing_docs)]
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::{
braced, parenthesized,
parse::{Parse, ParseStream, Parser},
parse_macro_input,
punctuated::Punctuated,
spanned::Spanned,
token::{Brace, Comma, Paren},
DeriveInput, Error, Expr, FieldValue, FnArg, GenericParam, Generics, Ident, ItemFn, ItemStruct,
Lifetime, Lit, Member, Pat, Result, Token, Type, TypePath, WhereClause, WherePredicate,
};
use uuid::Uuid;
enum ParsedElementChild {
Element(ParsedElement),
Expr(Expr),
}
struct ParsedElement {
ty: TypePath,
props: Punctuated<FieldValue, Comma>,
children: Vec<ParsedElementChild>,
}
impl Parse for ParsedElement {
fn parse(input: ParseStream) -> Result<Self> {
let ty: TypePath = input.parse()?;
let props = if input.peek(Paren) {
let props_input;
parenthesized!(props_input in input);
Punctuated::parse_terminated(&props_input)?
} else {
Punctuated::new()
};
let mut children = Vec::new();
if input.peek(Brace) {
let children_input;
braced!(children_input in input);
while !children_input.is_empty() {
if children_input.peek(Token![#]) {
children_input.parse::<Token![#]>()?;
let child_input;
parenthesized!(child_input in children_input);
children.push(ParsedElementChild::Expr(child_input.parse()?));
} else {
children.push(ParsedElementChild::Element(children_input.parse()?));
}
}
}
Ok(Self {
props,
ty,
children,
})
}
}
impl ToTokens for ParsedElement {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let ty = &self.ty;
let decl_key = Uuid::new_v4().as_u128();
let key = self
.props
.iter()
.find_map(|FieldValue { member, expr, .. }| match member {
Member::Named(ident) if ident == "key" => Some(quote!((#decl_key, #expr))),
_ => None,
})
.unwrap_or_else(|| quote!(#decl_key));
let prop_assignments = self
.props
.iter()
.filter_map(|FieldValue { member, expr, .. }| match member {
Member::Named(ident) if ident == "key" => None,
_ => Some(match expr {
Expr::Lit(lit) => match &lit.lit {
Lit::Int(lit) if lit.suffix() == "pct" => {
let value = lit.base10_parse::<f32>().unwrap();
quote!(_iocraft_props.#member = ::iocraft::Percent(#value).into())
}
Lit::Float(lit) if lit.suffix() == "pct" => {
let value = lit.base10_parse::<f32>().unwrap();
quote!(_iocraft_props.#member = ::iocraft::Percent(#value).into())
}
_ => quote!(_iocraft_props.#member = (#expr).into()),
},
_ => quote!(_iocraft_props.#member = (#expr).into()),
}),
})
.collect::<Vec<_>>();
let set_children = if !self.children.is_empty() {
let children = self.children.iter().map(|child| match child {
ParsedElementChild::Element(child) => quote!(#child),
ParsedElementChild::Expr(expr) => quote!(#expr),
});
Some(quote! {
#(::iocraft::extend_with_elements(&mut _iocraft_element.props.children, #children);)*
})
} else {
None
};
tokens.extend(quote! {
{
type Props<'a> = <#ty as ::iocraft::ElementType>::Props<'a>;
let mut _iocraft_props: Props = Default::default();
#(#prop_assignments;)*
let mut _iocraft_element = ::iocraft::Element::<#ty>{
key: ::iocraft::ElementKey::new(#key),
props: _iocraft_props,
};
#set_children
_iocraft_element
}
});
}
}
#[proc_macro]
pub fn element(input: TokenStream) -> TokenStream {
let element = parse_macro_input!(input as ParsedElement);
quote!(#element).into()
}
struct ParsedProps {
def: ItemStruct,
}
impl Parse for ParsedProps {
fn parse(input: ParseStream) -> Result<Self> {
let def: ItemStruct = input.parse()?;
for field in &def.fields {
if let Some(ident) = &field.ident {
if ident == "key" {
return Err(Error::new(
ident.span(),
"the `key` property name is reserved",
));
}
}
}
Ok(Self { def })
}
}
impl ToTokens for ParsedProps {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let def = &self.def;
let name = &def.ident;
let where_clause = &def.generics.where_clause;
let has_generics = !def.generics.params.is_empty();
let lifetime_generic_count = def
.generics
.params
.iter()
.filter(|param| matches!(param, GenericParam::Lifetime(_)))
.count();
let generics = &def.generics;
let generics_names = def.generics.params.iter().map(|param| match param {
GenericParam::Type(ty) => {
let name = &ty.ident;
quote!(#name)
}
GenericParam::Lifetime(lt) => {
let name = <.lifetime;
quote!(#name)
}
GenericParam::Const(c) => {
let name = &c.ident;
quote!(#name)
}
});
let bracketed_generic_names = match has_generics {
true => quote!(<#(#generics_names),*>),
false => quote!(),
};
if lifetime_generic_count > 0 {
let generic_decls = {
let mut lifetime_index = 0;
def.generics.params.iter().map(move |param| match param {
GenericParam::Lifetime(_) => {
let a = Lifetime::new(
format!("'a{}", lifetime_index).as_str(),
Span::call_site(),
);
let b = Lifetime::new(
format!("'b{}", lifetime_index).as_str(),
Span::call_site(),
);
lifetime_index += 1;
quote!(#a, #b: #a)
}
_ => quote!(#param),
})
};
let test_args = ["a", "b"].iter().map(|arg| {
let mut lifetime_index = 0;
let generic_params = def.generics.params.iter().map(|param| match param {
GenericParam::Type(ty) => {
let name = &ty.ident;
quote!(#name)
}
GenericParam::Lifetime(_) => {
let lt = Lifetime::new(
format!("'{}{}", arg, lifetime_index).as_str(),
Span::call_site(),
);
lifetime_index += 1;
quote!(#lt)
}
GenericParam::Const(c) => {
let name = &c.ident;
quote!(#name)
}
});
let arg_ident = Ident::new(arg, Span::call_site());
quote!(#arg_ident: &#name<#(#generic_params),*>)
});
tokens.extend(quote! {
const _: () = {
fn take_two<T>(_a: T, _b: T) {}
fn test_type_covariance<#(#generic_decls),*>(#(#test_args),*) {
take_two(a, b)
}
};
});
}
tokens.extend(quote! {
unsafe impl #generics ::iocraft::Props for #name #bracketed_generic_names #where_clause {}
});
}
}
#[proc_macro_derive(Props)]
pub fn derive_props(item: TokenStream) -> TokenStream {
let props = parse_macro_input!(item as ParsedProps);
quote!(#props).into()
}
struct ParsedComponent {
f: ItemFn,
props_type: Option<Box<Type>>,
impl_args: Vec<proc_macro2::TokenStream>,
}
impl Parse for ParsedComponent {
fn parse(input: ParseStream) -> Result<Self> {
let f: ItemFn = input.parse()?;
let mut props_type = None;
let mut impl_args = Vec::new();
for arg in &f.sig.inputs {
match arg {
FnArg::Typed(arg) => {
let name = match &*arg.pat {
Pat::Ident(arg) => arg.ident.to_string(),
_ => return Err(Error::new(arg.pat.span(), "invalid argument")),
};
match name.as_str() {
"props" | "_props" => {
if props_type.is_some() {
return Err(Error::new(arg.span(), "duplicate `props` argument"));
}
match &*arg.ty {
Type::Reference(r) => {
props_type = Some(r.elem.clone());
impl_args.push(quote!(props));
}
_ => {
return Err(Error::new(
arg.ty.span(),
"invalid `props` type (must be a reference)",
))
}
}
}
"hooks" | "_hooks" => match &*arg.ty {
Type::Reference(_) => {
impl_args.push(quote!(&mut hooks));
}
Type::Path(_) => {
impl_args.push(quote!(hooks));
}
_ => {
return Err(Error::new(
arg.ty.span(),
"invalid `hooks` type (must be a reference or a value)",
))
}
},
_ => {
return Err(Error::new(
arg.span(),
"unexpected argument (must be named `props` or `hooks`)",
))
}
}
}
_ => return Err(Error::new(arg.span(), "invalid argument")),
}
}
Ok(Self {
f,
props_type,
impl_args,
})
}
}
impl ToTokens for ParsedComponent {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let attrs = &self.f.attrs;
let vis = &self.f.vis;
let name = &self.f.sig.ident;
let args = &self.f.sig.inputs;
let block = &self.f.block;
let output = &self.f.sig.output;
let generics = &self.f.sig.generics;
let lifetime_generics = {
Generics {
params: generics
.params
.iter()
.filter(|param| matches!(param, GenericParam::Lifetime(_)))
.cloned()
.collect(),
where_clause: generics
.where_clause
.as_ref()
.map(|where_clause| WhereClause {
where_token: where_clause.where_token,
predicates: where_clause
.predicates
.iter()
.filter(|predicate| matches!(predicate, WherePredicate::Lifetime(_)))
.cloned()
.collect(),
}),
..generics.clone()
}
};
let (lifetime_impl_generics, _lifetime_ty_generics, lifetime_where_clause) =
lifetime_generics.split_for_impl();
let type_generics = {
Generics {
params: generics
.params
.iter()
.filter(|param| !matches!(param, GenericParam::Lifetime(_)))
.cloned()
.collect(),
where_clause: generics
.where_clause
.as_ref()
.map(|where_clause| WhereClause {
where_token: where_clause.where_token,
predicates: where_clause
.predicates
.iter()
.filter(|predicate| !matches!(predicate, WherePredicate::Lifetime(_)))
.cloned()
.collect(),
}),
..generics.clone()
}
};
let (impl_generics, ty_generics, where_clause) = type_generics.split_for_impl();
let ty_generic_names = type_generics.params.iter().filter_map(|param| match param {
GenericParam::Type(ty) => {
let name = &ty.ident;
Some(quote!(#name))
}
_ => None,
});
let impl_args = &self.impl_args;
let props_type_name = self
.props_type
.as_ref()
.map(|ty| quote!(#ty))
.unwrap_or_else(|| quote!(::iocraft::NoProps));
tokens.extend(quote! {
#(#attrs)*
#vis struct #name #impl_generics {
_marker: std::marker::PhantomData<fn(#(#ty_generic_names),*)>,
}
impl #impl_generics #name #ty_generics #where_clause {
fn implementation #lifetime_impl_generics (#args) #output #lifetime_where_clause #block
}
impl #impl_generics ::iocraft::Component for #name #ty_generics #where_clause {
type Props<'a> = #props_type_name;
fn new(_props: &Self::Props<'_>) -> Self {
Self{
_marker: std::marker::PhantomData,
}
}
fn update(&mut self, props: &mut Self::Props<'_>, mut hooks: ::iocraft::Hooks, updater: &mut ::iocraft::ComponentUpdater) {
let mut e = {
let mut hooks = hooks.with_context_stack(updater.component_context_stack());
Self::implementation(#(#impl_args),*).into()
};
updater.set_transparent_layout(true);
updater.update_children([&mut e], None);
}
}
});
}
}
#[proc_macro_attribute]
pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
let component = parse_macro_input!(item as ParsedComponent);
quote!(#component).into()
}
#[doc(hidden)]
#[proc_macro_attribute]
pub fn with_layout_style_props(_attr: TokenStream, item: TokenStream) -> TokenStream {
let layout_style_fields = [
quote! {
pub display: ::iocraft::Display
},
quote! {
pub width: ::iocraft::Size
},
quote! {
pub height: ::iocraft::Size
},
quote! {
pub min_width: ::iocraft::Size
},
quote! {
pub min_height: ::iocraft::Size
},
quote! {
pub max_width: ::iocraft::Size
},
quote! {
pub max_height: ::iocraft::Size
},
quote! {
pub gap: ::iocraft::Gap
},
quote! {
pub column_gap: ::iocraft::Gap
},
quote! {
pub row_gap: ::iocraft::Gap
},
quote! {
pub padding: ::iocraft::Padding
},
quote! {
pub padding_top: ::iocraft::Padding
},
quote! {
pub padding_right: ::iocraft::Padding
},
quote! {
pub padding_bottom: ::iocraft::Padding
},
quote! {
pub padding_left: ::iocraft::Padding
},
quote! {
pub position: ::iocraft::Position
},
quote! {
pub inset: ::iocraft::Inset
},
quote! {
pub top: ::iocraft::Inset
},
quote! {
pub right: ::iocraft::Inset
},
quote! {
pub bottom: ::iocraft::Inset
},
quote! {
pub left: ::iocraft::Inset
},
quote! {
pub margin: ::iocraft::Margin
},
quote! {
pub margin_top: ::iocraft::Margin
},
quote! {
pub margin_right: ::iocraft::Margin
},
quote! {
pub margin_bottom: ::iocraft::Margin
},
quote! {
pub margin_left: ::iocraft::Margin
},
quote! {
pub flex_direction: ::iocraft::FlexDirection
},
quote! {
pub flex_wrap: ::iocraft::FlexWrap
},
quote! {
pub flex_basis: ::iocraft::FlexBasis
},
quote! {
pub flex_grow: f32
},
quote! {
pub flex_shrink: Option<f32>
},
quote! {
pub align_items: Option<::iocraft::AlignItems>
},
quote! {
pub align_content: Option<::iocraft::AlignContent>
},
quote! {
pub justify_content: Option<::iocraft::JustifyContent>
},
quote! {
pub overflow: Option<::iocraft::Overflow>
},
quote! {
pub overflow_x: Option<::iocraft::Overflow>
},
quote! {
pub overflow_y: Option<::iocraft::Overflow>
},
]
.map(|tokens| syn::Field::parse_named.parse2(tokens).unwrap());
let mut ast = parse_macro_input!(item as DeriveInput);
match &mut ast.data {
syn::Data::Struct(ref mut struct_data) => {
if let syn::Fields::Named(fields) = &mut struct_data.fields {
fields.named.extend(layout_style_fields.iter().cloned());
}
let struct_name = &ast.ident;
let field_assignments = layout_style_fields.iter().map(|field| {
let field_name = &field.ident;
quote! { ret.#field_name = self.#field_name; }
});
let where_clause = &ast.generics.where_clause;
let has_generics = !ast.generics.params.is_empty();
let generics = &ast.generics;
let generics_names = ast.generics.params.iter().map(|param| match param {
GenericParam::Type(ty) => {
let name = &ty.ident;
quote!(#name)
}
GenericParam::Lifetime(lt) => {
let name = <.lifetime;
quote!(#name)
}
GenericParam::Const(c) => {
let name = &c.ident;
quote!(#name)
}
});
let bracketed_generic_names = match has_generics {
true => quote!(<#(#generics_names),*>),
false => quote!(),
};
quote! {
#ast
impl #generics #struct_name #bracketed_generic_names #where_clause {
pub fn layout_style(&self) -> ::iocraft::LayoutStyle {
let mut ret: ::iocraft::LayoutStyle = Default::default();
#(#field_assignments)*
ret
}
}
}
.into()
}
_ => panic!("`with_layout_style_props` can only be used with structs "),
}
}