506 lines
12 KiB
Rust
506 lines
12 KiB
Rust
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<String>, Option<String>) {
|
|
let docs: Vec<_> = attrs.iter().map(Documentation::parse).collect();
|
|
|
|
let mut title = None;
|
|
let mut description: Option<String> = 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<String>,
|
|
}
|
|
|
|
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,
|
|
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 {
|
|
#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 {
|
|
#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 signals = fields.clone().map(|f| {
|
|
let name = f.name;
|
|
quote! {
|
|
let #name = create_signal(cx, state.get().#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;
|
|
|
|
#(#signals)*
|
|
|
|
view! { cx,
|
|
Block(title=#title.to_string()) {
|
|
p {
|
|
#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) => create_signal(cx, 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 {
|
|
#description
|
|
}
|
|
(match state.get().as_ref().clone() {
|
|
#(#view_description,)*
|
|
_ => view! { cx, }
|
|
})
|
|
(match state.get().as_ref().clone() {
|
|
#(#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<G> {
|
|
#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<G> {
|
|
#inner
|
|
}
|
|
}
|
|
|
|
impl<'a, G: Html> Viewable<'a, G> for #name {
|
|
type Viewer = #view_ident;
|
|
}
|
|
};
|
|
|
|
TokenStream::from(res)
|
|
}
|