mod edit; use convert_case::{Case, Casing}; use proc_macro::TokenStream; use quote::{__private::TokenStream as TokenStream2, format_ident, quote}; use syn::{token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Path, Type}; #[derive(Debug)] enum Documentation { Title(String), Description(String), None, } impl Documentation { fn parse(attr: &Attribute) -> Documentation { if !attr.path.is_ident("doc") { return Documentation::None; } let text = attr.tokens.to_string(); let text = text.trim_matches(|c: char| c == '\"' || c == '=' || c.is_whitespace()); match text.get(0..1) { Some("#") => Documentation::Title(text.trim_matches('#').trim().into()), Some(&_) => Documentation::Description(text.trim().into()), None => Documentation::None, } } } fn get_title_description(attrs: &[Attribute]) -> (Option, Option) { let docs: Vec<_> = attrs.iter().map(Documentation::parse).collect(); let mut title = None; let mut description: Option = None; for doc in docs { match doc { Documentation::Title(t) => title = Some(t), Documentation::Description(d) => { if description.is_some() { description.as_mut().unwrap().push(' '); } else { let _ = description.insert(String::new()); } description.as_mut().unwrap().push_str(&d); } _ => {} } } (title, description) } struct ItemProps { name: Ident, title: String, description: Option, } struct StructField { name: Ident, name_str: String, title: Option, description: Option, } struct EnumVariant { variant: Ident, variant_lower: Ident, inner: Type, title: Option, description: Option, } fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 { let ItemProps { name, title, description, } = props; let fields = s.fields.iter().map(|f| { let name = f .ident .as_ref() .expect("each struct field must be named") .clone(); let name_str = name.to_string(); let (title, description) = get_title_description(&f.attrs); StructField { name_str, name, title, description, } }); let fields_view = fields.clone().map(|f| { let title = f.title.unwrap_or(f.name_str.to_case(Case::Title)); let description = if let Some(d) = f.description { quote! { span(class="description") { " " #d } } } else { quote! {} }; let name = f.name; quote! { p { label { (#title) br() #description } (#name.edit(cx)) } } }); let signals = fields.clone().map(|f| { let name = f.name; quote! { let #name = create_signal(cx, state.get_untracked().#name.clone()); } }); let effect_fields = fields.clone().map(|f| { let name = f.name; quote! { #name: #name.get().as_ref().clone() } }); quote! { let state = props.state; #(#signals)* create_effect(cx, || { state.set(#name { #(#effect_fields,)* ..Default::default() }); }); view! { cx, Block(title=#title.to_string()) { p(class="description") { #description } #(#fields_view)* } } } } fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 { let ItemProps { name, title, description, } = props; let variants = e.variants.iter().map(|v| { let variant = v.ident.clone(); let inner = match &v.fields { Fields::Unnamed(u) => u.unnamed.first().expect("the should be a field").ty.clone(), _ => unimplemented!(), }; let (title, description) = get_title_description(&v.attrs); let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake)); EnumVariant { variant_lower, variant, inner, title, description, } }); let first = variants.clone().next().unwrap().variant_lower; let first_str = first.to_string(); let options = variants.clone().map(|v| { let lower = v.variant_lower; let title = v .title .unwrap_or(v.variant.to_string().to_case(Case::Title)); let selected = first == lower; quote! { option(value={stringify!(#lower)}, selected=#selected) { (#title) } } }); let view_match = variants.clone().map(|v| { let lower = v.variant_lower; let lower_str = format!("{}", lower); quote! { #lower_str => #lower.edit(cx) } }); let view_description = variants.clone().map(|v| { let lower = v.variant_lower; let lower_str = format!("{}", lower); let description = if let Some(d) = v.description { quote! { span(class="description") { " " #d } } } else { quote! {} }; quote! { #lower_str => view! { cx, #description } } }); let signals = variants.clone().map( |EnumVariant { variant, variant_lower, inner, .. }| { quote! { let #variant_lower = if let #name::#variant(v) = state.get_untracked().as_ref().clone() { create_signal(cx, v.clone()) } else { create_signal(cx, <#inner>::default()) }; } }, ); let effect_match = variants.clone().map(|v| { let lower = v.variant_lower; let variant = v.variant; let lower_str = format!("{}", lower); quote! { #lower_str => state.set(#name::#variant(#lower.get().as_ref().clone())) } }); quote! { let state = props.state; let selected = create_signal(cx, String::from(#first_str)); #(#signals)* create_effect(cx, || { match selected.get().as_str() { #(#effect_match,)* _ => {} } }); view! { cx, Block(title=#title.to_string()) { p(class="description") { #description } select(bind:value=selected) { #(#options)* } (match selected.get().as_str() { #(#view_description,)* _ => view! { cx, } }) (match selected.get().as_str() { #(#view_match,)* _ => view! { cx, } }) } } } } fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 { let ItemProps { name, title, description, } = props; let fields = s.fields.iter().map(|f| { let name = f .ident .as_ref() .expect("each struct field must be named") .clone(); let name_str = name.to_string(); let (title, description) = get_title_description(&f.attrs); StructField { name_str, name, title, description, } }); let refs = fields.clone().map(|f| { let name = f.name; quote! { let #name = create_ref(cx, state.#name.clone()); } }); let fields_view = fields.clone().map(|f| { let title = f.title.unwrap_or(f.name_str.to_case(Case::Title)); let description = if let Some(d) = f.description { quote! { span(class="description") { " " #d } } } else { quote! {} }; let name = f.name; quote! { p { label { (#title) br() #description } (#name.view(cx)) } } }); quote! { let state = props.state; #(#refs)* view! { cx, Block(title=#title.to_string()) { p(class="description") { #description } #(#fields_view)* } } } } fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 { let ItemProps { name, title, description, } = props; let variants = e.variants.iter().map(|v| { let variant = v.ident.clone(); let inner = match &v.fields { Fields::Unnamed(u) => u.unnamed.first().expect("the should be a field").ty.clone(), _ => unimplemented!(), }; let (title, description) = get_title_description(&v.attrs); let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake)); EnumVariant { variant_lower, variant, inner, title, description, } }); let view_match = variants.clone().map(|v| { let variant = v.variant; quote! { #name::#variant(x) => x.view(cx) } }); let view_description = variants.clone().map(|v| { let variant = v.variant; let description = if let Some(d) = v.description { quote! { span(class="description") { " " #d } } } else { quote! {} }; quote! { #name::#variant(_) => view! { cx, #description } } }); quote! { let state = props.state; view! { cx, Block(title=#title.to_string()) { p(class="description") { #description } (match state { #(#view_description,)* _ => view! { cx, } }) (match state { #(#view_match,)* _ => view! { cx, } }) } } } } #[proc_macro_derive(WebEdit)] pub fn web_edit(tokens: TokenStream) -> TokenStream { let input: syn::DeriveInput = syn::parse(tokens).unwrap(); let name = input.ident; let edit_ident = format_ident!("{}Edit", name); let (t, d) = get_title_description(&input.attrs); let title = t.unwrap_or(name.to_string()); let description = d; let props = ItemProps { name: name.clone(), title: title.clone(), description: description.clone(), }; let inner = match input.data { syn::Data::Struct(s) => struct_edit(&props, s), syn::Data::Enum(e) => enum_edit(&props, e), _ => unimplemented!(), }; let res = quote! { pub struct #edit_ident; impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident { fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View { #inner } } impl<'a, G: Html> Editable<'a, G> for #name { type Editor = #edit_ident; } }; TokenStream::from(res) } #[proc_macro_derive(WebView)] pub fn web_view(tokens: TokenStream) -> TokenStream { let input: syn::DeriveInput = syn::parse(tokens).unwrap(); let name = input.ident; let view_ident = format_ident!("{}View", name); let (t, d) = get_title_description(&input.attrs); let title = t.unwrap_or(name.to_string()); let description = d; let props = ItemProps { name: name.clone(), title: title.clone(), description: description.clone(), }; let inner = match input.data { syn::Data::Struct(s) => struct_view(&props, s), syn::Data::Enum(e) => enum_view(&props, e), _ => unimplemented!(), }; let res = quote! { pub struct #view_ident; impl<'a, G: Html> Viewer<'a, G, #name> for #view_ident { fn view(cx: Scope<'a>, props: ViewProps<'a, #name>) -> View { #inner } } impl<'a, G: Html> Viewable<'a, G> for #name { type Viewer = #view_ident; } }; TokenStream::from(res) }