1 | #![deny (elided_lifetimes_in_paths)] |
2 | #![deny (unreachable_pub)] |
3 | |
4 | use std::fmt; |
5 | use std::{borrow::Cow, collections::HashMap}; |
6 | |
7 | use proc_macro::TokenStream; |
8 | use proc_macro2::Span; |
9 | |
10 | use parser::ParseError; |
11 | |
12 | mod config; |
13 | use config::Config; |
14 | mod generator; |
15 | use generator::{Generator, MapChain}; |
16 | mod heritage; |
17 | use heritage::{Context, Heritage}; |
18 | mod input; |
19 | use input::{Print, TemplateArgs, TemplateInput}; |
20 | |
21 | #[proc_macro_derive (Template, attributes(template))] |
22 | pub fn derive_template(input: TokenStream) -> TokenStream { |
23 | let ast: DeriveInput = syn::parse::<syn::DeriveInput>(tokens:input).unwrap(); |
24 | match build_template(&ast) { |
25 | Ok(source: String) => source.parse().unwrap(), |
26 | Err(e: CompileError) => e.into_compile_error(), |
27 | } |
28 | } |
29 | |
30 | /// Takes a `syn::DeriveInput` and generates source code for it |
31 | /// |
32 | /// Reads the metadata from the `template()` attribute to get the template |
33 | /// metadata, then fetches the source from the filesystem. The source is |
34 | /// parsed, and the parse tree is fed to the code generator. Will print |
35 | /// the parse tree and/or generated source according to the `print` key's |
36 | /// value as passed to the `template()` attribute. |
37 | pub(crate) fn build_template(ast: &syn::DeriveInput) -> Result<String, CompileError> { |
38 | let template_args = TemplateArgs::new(ast)?; |
39 | let toml = template_args.config()?; |
40 | let config = Config::new(&toml, template_args.whitespace.as_deref())?; |
41 | let input = TemplateInput::new(ast, &config, &template_args)?; |
42 | |
43 | let mut templates = HashMap::new(); |
44 | input.find_used_templates(&mut templates)?; |
45 | |
46 | let mut contexts = HashMap::new(); |
47 | for (path, parsed) in &templates { |
48 | contexts.insert( |
49 | path.as_path(), |
50 | Context::new(input.config, path, parsed.nodes())?, |
51 | ); |
52 | } |
53 | |
54 | let ctx = &contexts[input.path.as_path()]; |
55 | let heritage = if !ctx.blocks.is_empty() || ctx.extends.is_some() { |
56 | Some(Heritage::new(ctx, &contexts)) |
57 | } else { |
58 | None |
59 | }; |
60 | |
61 | if input.print == Print::Ast || input.print == Print::All { |
62 | eprintln!(" {:?}" , templates[input.path.as_path()].nodes()); |
63 | } |
64 | |
65 | let code = Generator::new(&input, &contexts, heritage.as_ref(), MapChain::default()) |
66 | .build(&contexts[input.path.as_path()])?; |
67 | if input.print == Print::Code || input.print == Print::All { |
68 | eprintln!(" {code}" ); |
69 | } |
70 | Ok(code) |
71 | } |
72 | |
73 | #[derive (Debug, Clone)] |
74 | struct CompileError { |
75 | msg: Cow<'static, str>, |
76 | span: Span, |
77 | } |
78 | |
79 | impl CompileError { |
80 | fn new<S: Into<Cow<'static, str>>>(s: S, span: Span) -> Self { |
81 | Self { |
82 | msg: s.into(), |
83 | span, |
84 | } |
85 | } |
86 | |
87 | fn into_compile_error(self) -> TokenStream { |
88 | synTokenStream::Error::new(self.span, self.msg) |
89 | .to_compile_error() |
90 | .into() |
91 | } |
92 | } |
93 | |
94 | impl std::error::Error for CompileError {} |
95 | |
96 | impl fmt::Display for CompileError { |
97 | #[inline ] |
98 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { |
99 | fmt.write_str(&self.msg) |
100 | } |
101 | } |
102 | |
103 | impl From<ParseError> for CompileError { |
104 | #[inline ] |
105 | fn from(e: ParseError) -> Self { |
106 | Self::new(s:e.to_string(), Span::call_site()) |
107 | } |
108 | } |
109 | |
110 | impl From<&'static str> for CompileError { |
111 | #[inline ] |
112 | fn from(s: &'static str) -> Self { |
113 | Self::new(s, Span::call_site()) |
114 | } |
115 | } |
116 | |
117 | impl From<String> for CompileError { |
118 | #[inline ] |
119 | fn from(s: String) -> Self { |
120 | Self::new(s, Span::call_site()) |
121 | } |
122 | } |
123 | |
124 | // This is used by the code generator to decide whether a named filter is part of |
125 | // Askama or should refer to a local `filters` module. It should contain all the |
126 | // filters shipped with Askama, even the optional ones (since optional inclusion |
127 | // in the const vector based on features seems impossible right now). |
128 | const BUILT_IN_FILTERS: &[&str] = &[ |
129 | "abs" , |
130 | "capitalize" , |
131 | "center" , |
132 | "e" , |
133 | "escape" , |
134 | "filesizeformat" , |
135 | "fmt" , |
136 | "format" , |
137 | "indent" , |
138 | "into_f64" , |
139 | "into_isize" , |
140 | "join" , |
141 | "linebreaks" , |
142 | "linebreaksbr" , |
143 | "paragraphbreaks" , |
144 | "lower" , |
145 | "lowercase" , |
146 | "safe" , |
147 | "trim" , |
148 | "truncate" , |
149 | "upper" , |
150 | "uppercase" , |
151 | "urlencode" , |
152 | "urlencode_strict" , |
153 | "wordcount" , |
154 | // optional features, reserve the names anyway: |
155 | "json" , |
156 | "markdown" , |
157 | "yaml" , |
158 | ]; |
159 | |