tour_parser/
codegen.rs

1//! Code generation.
2use proc_macro2::TokenStream;
3use quote::{ToTokens, quote};
4use syn::*;
5
6use crate::{
7    common::{TemplWrite, path},
8    config::Config,
9    data::Template,
10    file::File,
11    metadata::{Metadata, TemplKind},
12};
13
14mod body;
15mod sizehint;
16
17/// Generate code from [`DeriveInput`].
18pub fn derive(input: &DeriveInput) -> Result<TokenStream> {
19    let conf = Config::default();
20    let meta = Metadata::from_attrs(&input.attrs, &conf)?;
21    let file = File::from_meta(&meta)?;
22    let templ = Template::new(input.ident.clone(), meta, file)?;
23    let mut root = quote! { const _: () = };
24
25    brace(&mut root, |tokens| {
26        generate_templ(&templ, input, tokens);
27    });
28
29    <Token![;]>::default().to_tokens(&mut root);
30
31    Ok(root)
32}
33
34fn generate_templ(templ: &Template, input: &DeriveInput, root: &mut TokenStream) {
35    let ident = &input.ident;
36    let (g1, g2, g3) = input.generics.split_for_impl();
37
38    // ===== trait Template =====
39
40    let cwd = templ.meta().path();
41    if std::path::Path::new(cwd).is_file() {
42        root.extend(quote! {
43            #[doc = concat!(" ",include_str!(#cwd))]
44        });
45    } else if let Some(src) = templ.meta().inline() {
46        root.extend(quote! {
47            #[doc = concat!(" ",#src)]
48        });
49    }
50
51    root.extend(quote! {
52        #[automatically_derived]
53        impl #g1 ::tour::Template for #ident #g2 #g3
54    });
55
56    brace(root, |trait_tokens| {
57
58        // ===== render_into() =====
59
60        trait_tokens.extend(quote! {
61            fn render_into(&self, writer: &mut impl #TemplWrite) -> ::tour::Result<()>
62        });
63
64        brace(trait_tokens, |render_into| {
65            body::Visitor::generate(templ, input, render_into);
66        });
67
68        // ===== render_block_into() =====
69
70        let blocks = templ.file().blocks();
71        let prefix = quote! {
72            fn render_block_into(&self, block: &str, writer: &mut impl #TemplWrite) -> ::tour::Result<()>
73        };
74
75        brace_if(!blocks.is_empty(), prefix, trait_tokens, |tokens| {
76            tokens.extend(quote! { match block });
77            brace(tokens, |tokens| {
78                for block in blocks {
79                    let name = &block.templ.name;
80                    let name_str = name.to_string();
81                    tokens.extend(quote! { #name_str => });
82                    brace(tokens, |tokens| {
83                        body::Visitor::generate_block(templ, name, input, tokens);
84                    });
85                }
86                tokens.extend(quote! { _ => Err(::tour::Error::NoBlock), });
87            });
88        });
89
90        // ===== contains_block() =====
91
92        let is_ok = matches!(templ.meta().kind(), TemplKind::Main) && !blocks.is_empty();
93        let prefix = quote! {
94            fn contains_block(&self, block: &str) -> bool
95        };
96
97        brace_if(is_ok, prefix, trait_tokens, |tokens| {
98            let blocks = blocks
99                .iter()
100                .map(|block|{
101                    block.templ.name.to_string()
102                });
103
104            tokens.extend(quote! {
105                matches!(block, #(#blocks)|*)
106            });
107        });
108
109        // ===== size_hint() =====
110
111        let is_skip = !matches!(templ.meta().kind(), TemplKind::Main);
112        let size = if is_skip {
113            (0,None)
114        } else {
115            sizehint::Visitor::new(templ).calculate()
116        };
117
118        let is_sized = !sizehint::is_empty(size);
119        let prefix = quote! {
120            fn size_hint(&self) -> (usize,Option<usize>)
121        };
122
123        brace_if(is_sized, prefix, trait_tokens, |tokens| {
124            sizehint::generate(size, tokens);
125        });
126
127        // ===== size_hint_block() =====
128
129        let is_ok = matches!(templ.meta().kind(), TemplKind::Main) && !blocks.is_empty();
130        let blocks = if is_ok {
131            blocks
132                .iter()
133                .map(|block|{
134                    let block_name = &block.templ.name;
135                    (
136                        sizehint::Visitor::new(templ).calculate_block(block_name),
137                        block.templ.name.to_string()
138                    )
139                })
140                .filter(|e|sizehint::is_empty(e.0))
141                .collect()
142
143        } else {
144            vec![]
145        };
146
147        let is_sized = !blocks.is_empty();
148        let prefix = quote! {
149            fn size_hint_block(&self, block: &str) -> (usize,Option<usize>)
150        };
151
152        brace_if(is_sized, prefix, trait_tokens, |tokens| {
153            tokens.extend(quote! { match block });
154            brace(tokens, |tokens| {
155                for (size,name) in blocks {
156                    tokens.extend(quote! { #name => });
157                    brace(tokens, |tokens| {
158                        sizehint::generate(size, tokens);
159                    });
160                }
161                tokens.extend(quote! { _ => (0,None), });
162            });
163        });
164    });
165
166    // ===== trait TemplDisplay =====
167
168    if matches!(templ.meta().kind(), TemplKind::Main) {
169        root.extend(quote! {
170            #[automatically_derived]
171            impl #g1 ::tour::TemplDisplay for #ident #g2 #g3 {
172                fn display(&self, f: &mut impl ::tour::TemplWrite) -> ::tour::Result<()> {
173                    self.render_into(f)
174                }
175            }
176        });
177    }
178
179    // ===== imports =====
180
181    for import in templ.file().imports() {
182        let name = import.alias();
183        let path = import
184            .templ()
185            .meta()
186            .path()
187            .trim_start_matches(path::cwd().to_str().unwrap_or(""))
188            .trim_start_matches("/");
189        let doc = if path.is_empty() {
190            quote! { }
191        } else {
192            quote! { #[doc = concat!(" ",#path)] }
193        };
194
195        let mut generics = input.generics.clone();
196        if !generics.lifetimes().any(|e|e.lifetime.ident=="tour_ref") {
197            generics.params.push(syn::parse_quote!('tour_ref));
198        }
199        let (t1,t2,_) = generics.split_for_impl();
200
201        let input: DeriveInput = syn::parse_quote! {
202            #doc
203            struct #name #t1 (&'tour_ref #ident #g2) #g3;
204        };
205        input.to_tokens(root);
206
207        generate_templ(import.templ(), &input, root);
208
209        root.extend(quote! {
210            #[automatically_derived]
211            impl #t1 ::std::ops::Deref for #name #t2 #g3 {
212                type Target = #ident #g2;
213                fn deref(&self) -> &Self::Target {
214                    self.0
215                }
216            }
217        });
218    }
219}
220
221fn brace<F>(tokens: &mut TokenStream, call: F)
222where
223    F: FnOnce(&mut TokenStream)
224{
225    token::Brace::default().surround(tokens, call);
226}
227
228fn brace_if<F>(cond: bool, prefix: impl ToTokens, tokens: &mut TokenStream, call: F)
229where
230    F: FnOnce(&mut TokenStream)
231{
232    if cond {
233        prefix.to_tokens(tokens);
234        token::Brace::default().surround(tokens, call);
235    }
236}
237
238fn paren<F>(tokens: &mut TokenStream, call: F)
239where
240    F: FnOnce(&mut TokenStream)
241{
242    token::Paren::default().surround(tokens, call);
243}
244