diff --git a/proc/Cargo.toml b/proc/Cargo.toml index 2462993..aa2cd24 100644 --- a/proc/Cargo.toml +++ b/proc/Cargo.toml @@ -8,6 +8,6 @@ edition = { workspace = true } proc-macro = true [dependencies] -syn = { version = "2", features = ["full"] } +syn = { version = "2", features = ["full", "extra-traits"] } quote = { version = "1" } proc-macro2 = { version = "1", features = ["span-locations"] } diff --git a/proc/src/proc_view.rs b/proc/src/proc_view.rs index b9737d1..7095fce 100644 --- a/proc/src/proc_view.rs +++ b/proc/src/proc_view.rs @@ -1,80 +1,299 @@ use proc_macro::TokenStream; -use proc_macro2::{TokenStream as TokenStream2}; +use proc_macro2::{ + TokenStream as TokenStream2, TokenTree, + Ident, Span, Punct, Spacing, Group, Delimiter, Literal +}; +use syn::{parse, parse_macro_input, braced, Token}; +use syn::{Expr, Attribute, Meta, MetaList, Path, PathSegment, PathArguments, ImplItem, LitStr}; +use syn::parse::{Parse, ParseStream, Result}; +use syn::token::{PathSep, Brace}; +use syn::punctuated::Punctuated; +use quote::{quote, TokenStreamExt, ToTokens}; -pub(crate) fn view_impl (meta: TokenStream, item: TokenStream) -> TokenStream { - let ViewMeta { output, define, attrs } = syn::parse_macro_input!(meta as ViewMeta); - let ViewItem { target, mapped, items } = syn::parse_macro_input!(item as ViewItem); - quote::quote! { - #attrs - impl #target { - #items - } - impl ::tengri::Content<#output> for #target { - fn content (&self) -> impl Render<#output> { - self.size.of(::tengri::View(self, #define)) - } - } - impl<'a> ::tengri::ViewContext<'a, #output> for #target { - fn get_content_sym (&'a self, value: &Value<'a>) -> Option> { - match value { - #mapped - _ => panic!("expected Sym(content), got: {value:?}") - } - //if let Value::Sym(s) = value { - //match *s { - //$($sym => Some($body.boxed()),)* - //_ => None - //} - //} else { - //panic!("expected Sym(content), got: {value:?}") - //} - } - } - }.into() +pub(crate) fn view_impl (meta: TokenStream, data: TokenStream) -> TokenStream { + let mut out = TokenStream2::new(); + ViewDefinition { + meta: parse_macro_input!(meta as ViewMeta), + data: parse_macro_input!(data as ViewImpl), + }.to_tokens(&mut out); + out.into() } +#[derive(Debug, Clone)] +struct ViewDefinition { + meta: ViewMeta, + data: ViewImpl, +} + +#[derive(Debug, Clone)] struct ViewMeta { - attrs: &'static str, - output: &'static str, - define: &'static str, + output: Ident, + //attrs: Vec, } -impl syn::parse::Parse for ViewMeta { - fn parse (input: syn::parse::ParseStream) -> syn::parse::Result { - Ok(Self { - attrs: "", - output: "", - define: "", - }) - } +#[derive(Debug, Clone)] +struct ViewImpl { + target: Ident, + items: Vec, + syms: Vec, } +#[derive(Debug, Clone)] struct ViewItem { - items: &'static str, - target: &'static str, - mapped: &'static str, + item: ImplItem, + expose: Option, } -impl syn::parse::Parse for ViewItem { - fn parse (input: syn::parse::ParseStream) -> syn::parse::Result { +#[derive(Debug, Clone)] +struct ViewSym { + symbol: Literal, + name: Ident, +} + +impl Parse for ViewDefinition { + fn parse (input: ParseStream) -> Result { Ok(Self { - items: "", - target: "", - mapped: "", + meta: input.parse::()?, + data: input.parse::()?, }) } } -#[cfg(test)] #[test] fn test_view () { +impl Parse for ViewMeta { + fn parse (input: ParseStream) -> Result { + Ok(Self { + output: input.parse::()?, + }) + } +} - let _: syn::ItemImpl = syn::parse_quote! { - #[tengri::view(Tui)] - impl SomeView { - #[tengri::view(":view")] - fn view (&self) -> impl Content + use<'_> { - "view" +impl Parse for ViewImpl { + fn parse (input: ParseStream) -> Result { + let _ = input.parse::()?; + let mut syms = vec![]; + Ok(Self { + target: input.parse::()?, + items: { + let group; + let brace = braced!(group in input); + let mut items = vec![]; + while !group.is_empty() { + let item = group.parse::()?; + if let Some(expose) = &item.expose { + if let ImplItem::Fn(ref item) = item.item { + let symbol = expose.clone(); + let name = item.sig.ident.clone(); + syms.push(ViewSym { symbol, name }) + } else { + return Err( + input.error("only fn items can be exposed to #[tengri::view]") + ) + } + } + items.push(item); + } + items + }, + syms, + }) + } +} + + +impl Parse for ViewItem { + fn parse (input: ParseStream) -> Result { + let mut expose = None; + Ok(Self { + item: { + let mut item = input.parse::()?; + if let ImplItem::Fn(ref mut item) = item { + item.attrs = item.attrs.iter().filter(|attr| { + if let Attribute { + meta: Meta::List(MetaList { path, tokens, .. }), .. + } = attr + && path.segments.len() == 2 + && nth_segment_is(&path.segments, 0, "tengri") + && nth_segment_is(&path.segments, 1, "view") + && let Some(TokenTree::Literal(name)) = tokens.clone().into_iter().next() + { + expose = Some(name); + return false + } + true + }).map(|x|x.clone()).collect(); + }; + item + }, + expose, + }) + } +} + +impl ToTokens for ViewSym { + fn to_tokens (&self, out: &mut TokenStream2) { + use Spacing::*; + out.append(Punct::new(':', Joint)); + out.append(Punct::new(':', Alone)); + out.append(Ident::new("tengri", Span::call_site())); + out.append(Punct::new(':', Joint)); + out.append(Punct::new(':', Alone)); + out.append(Ident::new("dsl", Span::call_site())); + out.append(Punct::new(':', Joint)); + out.append(Punct::new(':', Alone)); + out.append(Ident::new("Value", Span::call_site())); + out.append(Punct::new(':', Joint)); + out.append(Punct::new(':', Alone)); + out.append(Ident::new("Sym", Span::call_site())); + out.append(Group::new(Delimiter::Parenthesis, { + let mut out = TokenStream2::new(); + out.append(self.symbol.clone()); + out + })); + out.append(Punct::new('=', Joint)); + out.append(Punct::new('>', Alone)); + out.append(Ident::new("Some", Span::call_site())); + out.append(Group::new(Delimiter::Parenthesis, { + let mut out = TokenStream2::new(); + out.append(Ident::new("self", Span::call_site())); + out.append(Punct::new('.', Alone)); + out.append(self.name.clone()); + out.append(Group::new(Delimiter::Parenthesis, TokenStream2::new())); + out.append(Punct::new('.', Alone)); + out.append(Ident::new("boxed", Span::call_site())); + out.append(Group::new(Delimiter::Parenthesis, TokenStream2::new())); + out + })); + out.append(Punct::new(',', Alone)); + } +} + +impl ToTokens for ViewItem { + fn to_tokens (&self, out: &mut TokenStream2) { + self.item.to_tokens(out) + } +} + +impl ToTokens for ViewDefinition { + fn to_tokens (&self, out: &mut TokenStream2) { + let Self { + meta: ViewMeta { output }, + data: ViewImpl { target, syms, items }, + } = self; + for token in quote! { + /// Augmented by [tengri_proc]. + impl #target { + #(#items)* } + /// Generated by [tengri_proc]. + impl ::tengri::output::Content<#output> for #target { + fn content (&self) -> impl Render<#output> { + self.size.of(::tengri::output::View(self, self.config.view)) + } + } + /// Generated by [tengri_proc]. + impl<'a> ::tengri::output::ViewContext<'a, #output> for #target { + fn get_content_sym (&'a self, value: &Value<'a>) -> Option> { + match value { + #(#syms)* + _ => panic!("expected Sym(content), got: {value:?}") + } + } + } + } { + out.append(token) + } + } +} + +fn nth_segment_is (segments: &Punctuated, n: usize, x: &str) -> bool { + if let Some(PathSegment { arguments: PathArguments::None, ident, .. }) = segments.get(n) { + if format!("{ident}") == x { + return true + } + } + return false +} + +impl std::cmp::PartialEq for ViewItem { + fn eq (&self, other: &Self) -> bool { + self.item == other.item && (format!("{:?}", self.expose) == format!("{:?}", other.expose)) + } +} + +impl std::cmp::PartialEq for ViewSym { + fn eq (&self, other: &Self) -> bool { + self.name == other.name && (format!("{}", self.symbol) == format!("{}", other.symbol)) + } +} + +#[cfg(test)] use syn::{ItemImpl, parse_quote as pq}; + +#[cfg(test)] #[test] fn test_view_meta () { + let x: ViewMeta = pq! { SomeOutput }; + let output: Ident = pq! { SomeOutput }; + assert_eq!(x.output, output); +} + +#[cfg(test)] #[test] fn test_view_impl () { + let x: ViewImpl = pq! { + impl Foo { + /// docstring1 + #[tengri::view(":view1")] #[bar] fn a_view () {} + + #[baz] + /// docstring2 + #[baz] fn is_not_view () {} } }; - + let expected_target: Ident = pq! { Foo }; + assert_eq!(x.target, expected_target); + assert_eq!(x.items.len(), 2); + assert_eq!(x.items[0].item, pq! { + /// docstring1 + #[bar] fn a_view () {} + }); + assert_eq!(x.items[1].item, pq! { + #[baz] + /// docstring2 + #[baz] fn is_not_view () {} + }); + assert_eq!(x.syms, vec![ + ViewSym { symbol: pq! { ":view1" }, name: pq! { a_view }, }, + ]); +} + +#[cfg(test)] #[test] fn test_view_definition () { + // FIXME + //let parsed: ViewDefinition = pq! { + //#[tengri_proc::view(SomeOutput)] + //impl SomeView { + //#[tengri::view(":view-1")] + //fn view_1 (&self) -> impl Content + use<'_> { + //"view-1" + //} + //} + //}; + //let written = quote! { #parsed }; + //assert_eq!(format!("{written}"), format!("{}", quote! { + //impl SomeView { + //fn view_1 (&self) -> impl Content + use<'_> { + //"view-1" + //} + //} + ///// Generated by [tengri_proc]. + //impl ::tengri::output::Content for SomeView { + //fn content (&self) -> impl Render { + //self.size.of(::tengri::output::View(self, self.config.view)) + //} + //} + ///// Generated by [tengri_proc]. + //impl<'a> ::tengri::dsl::ViewContext<'a, SomeOutput> for SomeView { + //fn get_content_sym (&'a self, value: &Value<'a>) -> Option> { + //match value { + //::tengri::dsl::Value::Sym(":view-1") => self.view_1().boxed(), + //_ => panic!("expected Sym(content), got: {value:?}") + //} + //} + //} + //})); }