use convert_case::{Case, Casing}; use proc_macro::TokenStream; use quote::{__private::TokenStream as TokenStream2, format_ident, quote}; use syn::{ parse::Parse, parse_str, token::Do, Attribute, DataEnum, DataStruct, Fields, Generics, Ident, LifetimeDef, Lit, MetaNameValue, Path, PredicateType, Type, TypeParam, }; enum ParsedAttribute { Documentation(Documentation), View(ViewAttribute), Serde(SerdeAttribute), None, } #[derive(Debug)] enum Documentation { Title(String), Description(String), None, } enum ViewAttribute { Title(Ident), None, } enum SerdeAttribute { Untagged, 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, } } } impl ViewAttribute { fn parse(attr: &Attribute) -> ViewAttribute { if !attr.path.is_ident("web_view_attr") { return Self::None; } let parsed: Result = parse_str( attr.tokens .to_string() .trim_matches(|c: char| c == '(' || c == ')'), ); match parsed { Ok(p) => match (p.path.get_ident().unwrap().to_string().as_str(), p.lit) { ("title", Lit::Str(ls)) => Self::Title(format_ident!("{}", ls.value())), _ => Self::None, }, Err(_) => Self::None, } } } impl SerdeAttribute { fn parse(attr: &Attribute) -> SerdeAttribute { if !attr.path.is_ident("serde") { return Self::None; } let parsed: Result = parse_str( attr.tokens .to_string() .trim_matches(|c: char| c == '(' || c == ')'), ); match parsed { Ok(p) => match p.to_string().as_str() { "untagged" => Self::Untagged, _ => Self::None, }, Err(_) => Self::None, } } } impl ParsedAttribute { fn parse(attr: &Attribute) -> ParsedAttribute { match attr.path.get_ident() { Some(i) if i.to_string() == "doc" => Self::Documentation(Documentation::parse(attr)), Some(i) if i.to_string() == "web_view_attr" => Self::View(ViewAttribute::parse(attr)), Some(i) if i.to_string() == "serde" => Self::Serde(SerdeAttribute::parse(attr)), _ => Self::None, } } } struct Attributes { title: Option, title_field: Option, description: Option, untagged: bool, } impl Attributes { pub fn parse(attrs: &[Attribute]) -> Self { let parsed: Vec<_> = attrs.iter().map(ParsedAttribute::parse).collect(); let mut title = None; let mut title_field = None; let mut description: Option = None; let mut untagged = false; for attr in parsed { match attr { ParsedAttribute::Documentation(doc) => 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); } _ => {} }, ParsedAttribute::View(v) => match v { ViewAttribute::Title(t) => title_field = Some(t), _ => {} }, ParsedAttribute::Serde(s) => match s { SerdeAttribute::Untagged => untagged = true, _ => {} }, _ => {} } } Self { title, description, title_field, untagged, } } } #[proc_macro_attribute] pub fn web_view_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { item } struct ItemProps { name: Ident, attributes: Attributes, generics: Generics, } 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, attributes: Attributes { title, description, .. }, generics, } = props; let title = title.clone().unwrap_or(name.to_string()); 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 Attributes { title, title_field: _, description, .. } = Attributes::parse(&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, attributes: Attributes { title, description, .. }, generics, } = props; let title = title.clone().unwrap_or(name.to_string()); 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 Attributes { title, title_field: _, description, .. } = Attributes::parse(&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, } }) br() (match selected.get().as_str() { #(#view_match,)* _ => view! { cx, } }) } } } } fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 { let ItemProps { name, attributes: Attributes { title, description, title_field, .. }, generics, } = props; let title = title.clone().unwrap_or(name.to_string()); 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 Attributes { title, title_field: _, description, .. } = Attributes::parse(&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)) } } }); let title = if let Some(title_field) = title_field { quote! { state.#title_field } } else { quote! { #title } }; quote! { let state = props.state; #(#refs)* view! { cx, Block(title=#title.to_string()) { p(class="description") { #description } #(#fields_view)* } } } } fn enum_fields<'a>(e: &'a DataEnum) -> impl Iterator + 'a { 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 Attributes { title, title_field: _, description, .. } = Attributes::parse(&v.attrs); let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake)); EnumVariant { variant_lower, variant, inner, title, description, } }) } fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 { let ItemProps { name, attributes: Attributes { title, description, .. }, generics, } = props; let title = title.clone().unwrap_or(name.to_string()); 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 Attributes { title, title_field: _, description, .. } = Attributes::parse(&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 attrs = Attributes::parse(&input.attrs); let props = ItemProps { name: name.clone(), attributes: attrs, generics: input.generics.clone(), }; let inner = match input.data { syn::Data::Struct(s) => struct_edit(&props, s), syn::Data::Enum(e) => enum_edit(&props, e), _ => unimplemented!(), }; let mut generics = input.generics.clone(); let input_generics = input.generics; //if generics.type_params().count() == 0 { generics .params .push(syn::GenericParam::Lifetime(syn::parse_str("'a").unwrap())); generics .params .push(syn::GenericParam::Type(syn::parse_str("G: Html").unwrap())); //} let (input_impl_generics, input_ty_generics, input_where_clause) = input_generics.split_for_impl(); for ty_param in input_generics.type_params() { generics.make_where_clause().predicates.push( syn::parse_str(&format!( "{}: for<'b> Editable<'b, G>", ty_param.ident.to_string() )) .unwrap(), ); } let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let res = quote! { pub struct #edit_ident; impl #impl_generics Editor<'a, G, #name #input_ty_generics> for #edit_ident #where_clause { fn edit(cx: Scope<'a>, props: EditProps<'a, #name #input_ty_generics>) -> View { #inner } } impl #impl_generics Editable<'a, G> for #name #input_ty_generics #where_clause { type Editor = #edit_ident; } }; println!("{}", &res.to_string()); 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 attrs = Attributes::parse(&input.attrs); let props = ItemProps { name: name.clone(), attributes: attrs, generics: input.generics.clone(), }; let inner = match input.data { syn::Data::Struct(s) => struct_view(&props, s), syn::Data::Enum(e) => enum_view(&props, e), _ => unimplemented!(), }; let mut generics = input.generics.clone(); let input_generics = input.generics; //if generics.type_params().count() == 0 { generics .params .push(syn::GenericParam::Lifetime(syn::parse_str("'a").unwrap())); generics .params .push(syn::GenericParam::Type(syn::parse_str("G: Html").unwrap())); //} let (input_impl_generics, input_ty_generics, input_where_clause) = input_generics.split_for_impl(); for ty_param in input_generics.type_params() { generics.make_where_clause().predicates.push( syn::parse_str(&format!( "{}: for<'b> Viewable<'b, G>", ty_param.ident.to_string() )) .unwrap(), ); } let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); //println!("{}", &inner.to_string()); let res = quote! { pub struct #view_ident; impl #impl_generics Viewer<'a, G, #name #input_ty_generics> for #view_ident #where_clause { fn view(cx: Scope<'a>, props: ViewProps<'a, #name #input_ty_generics>) -> View { #inner } } impl #impl_generics Viewable<'a, G> for #name #input_ty_generics #where_clause { type Viewer = #view_ident; } }; println!("{}", &res.to_string()); TokenStream::from(res) }