738 lines
19 KiB
Rust
738 lines
19 KiB
Rust
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<MetaNameValue, _> = 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<Ident, _> = 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<String>,
|
|
title_field: Option<Ident>,
|
|
description: Option<String>,
|
|
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<String> = 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<String>,
|
|
description: Option<String>,
|
|
}
|
|
|
|
struct EnumVariant {
|
|
variant: Ident,
|
|
variant_lower: Ident,
|
|
inner: Type,
|
|
title: Option<String>,
|
|
description: Option<String>,
|
|
}
|
|
|
|
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<Item = EnumVariant> + '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<G> {
|
|
#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<G> {
|
|
#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)
|
|
}
|