1use crate::{CompileError, Config, Syntax};
2
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5
6use mime::Mime;
7use quote::ToTokens;
8
9pub struct TemplateInput<'a> {
10 pub ast: &'a syn::DeriveInput,
11 pub config: &'a Config<'a>,
12 pub syntax: &'a Syntax<'a>,
13 pub source: Source,
14 pub print: Print,
15 pub escaper: &'a str,
16 pub ext: Option<String>,
17 pub mime_type: String,
18 pub parent: Option<&'a syn::Type>,
19 pub path: PathBuf,
20}
21
22impl TemplateInput<'_> {
23 pub fn new<'n>(
28 ast: &'n syn::DeriveInput,
29 config: &'n Config<'_>,
30 ) -> Result<TemplateInput<'n>, CompileError> {
31 let mut template_args = None;
34 for attr in &ast.attrs {
35 let ident = match attr.path.get_ident() {
36 Some(ident) => ident,
37 None => continue,
38 };
39
40 if ident == "template" {
41 if template_args.is_some() {
42 return Err("duplicated 'template' attribute".into());
43 }
44
45 match attr.parse_meta() {
46 Ok(syn::Meta::List(syn::MetaList { nested, .. })) => {
47 template_args = Some(nested);
48 }
49 Ok(_) => return Err("'template' attribute must be a list".into()),
50 Err(e) => return Err(format!("unable to parse attribute: {}", e).into()),
51 }
52 }
53 }
54 let template_args =
55 template_args.ok_or_else(|| CompileError::from("no attribute 'template' found"))?;
56
57 let mut source = None;
61 let mut print = Print::None;
62 let mut escaping = None;
63 let mut ext = None;
64 let mut syntax = None;
65 for item in template_args {
66 let pair = match item {
67 syn::NestedMeta::Meta(syn::Meta::NameValue(ref pair)) => pair,
68 _ => {
69 return Err(format!(
70 "unsupported attribute argument {:?}",
71 item.to_token_stream()
72 )
73 .into())
74 }
75 };
76 let ident = match pair.path.get_ident() {
77 Some(ident) => ident,
78 None => unreachable!("not possible in syn::Meta::NameValue(…)"),
79 };
80
81 if ident == "path" {
82 if let syn::Lit::Str(ref s) = pair.lit {
83 if source.is_some() {
84 return Err("must specify 'source' or 'path', not both".into());
85 }
86 source = Some(Source::Path(s.value()));
87 } else {
88 return Err("template path must be string literal".into());
89 }
90 } else if ident == "source" {
91 if let syn::Lit::Str(ref s) = pair.lit {
92 if source.is_some() {
93 return Err("must specify 'source' or 'path', not both".into());
94 }
95 source = Some(Source::Source(s.value()));
96 } else {
97 return Err("template source must be string literal".into());
98 }
99 } else if ident == "print" {
100 if let syn::Lit::Str(ref s) = pair.lit {
101 print = s.value().parse()?;
102 } else {
103 return Err("print value must be string literal".into());
104 }
105 } else if ident == "escape" {
106 if let syn::Lit::Str(ref s) = pair.lit {
107 escaping = Some(s.value());
108 } else {
109 return Err("escape value must be string literal".into());
110 }
111 } else if ident == "ext" {
112 if let syn::Lit::Str(ref s) = pair.lit {
113 ext = Some(s.value());
114 } else {
115 return Err("ext value must be string literal".into());
116 }
117 } else if ident == "syntax" {
118 if let syn::Lit::Str(ref s) = pair.lit {
119 syntax = Some(s.value())
120 } else {
121 return Err("syntax value must be string literal".into());
122 }
123 } else {
124 return Err(format!("unsupported attribute key {:?} found", ident).into());
125 }
126 }
127
128 let source = source.expect("template path or source not found in attributes");
132 let path = match (&source, &ext) {
133 (&Source::Path(ref path), _) => config.find_template(path, None)?,
134 (&Source::Source(_), Some(ext)) => PathBuf::from(format!("{}.{}", ast.ident, ext)),
135 (&Source::Source(_), None) => {
136 return Err("must include 'ext' attribute when using 'source' attribute".into())
137 }
138 };
139
140 let parent = match ast.data {
143 syn::Data::Struct(syn::DataStruct {
144 fields: syn::Fields::Named(ref fields),
145 ..
146 }) => fields
147 .named
148 .iter()
149 .find(|f| f.ident.as_ref().filter(|name| *name == "_parent").is_some())
150 .map(|f| &f.ty),
151 _ => None,
152 };
153
154 if parent.is_some() {
155 eprint!(
156 " --> in struct {}\n = use of deprecated field '_parent'\n",
157 ast.ident
158 );
159 }
160
161 let syntax = syntax.map_or_else(
163 || Ok(config.syntaxes.get(config.default_syntax).unwrap()),
164 |s| {
165 config
166 .syntaxes
167 .get(&s)
168 .ok_or_else(|| CompileError::from(format!("attribute syntax {} not exist", s)))
169 },
170 )?;
171
172 let escaping = escaping.unwrap_or_else(|| {
175 path.extension()
176 .map(|s| s.to_str().unwrap())
177 .unwrap_or("")
178 .to_string()
179 });
180
181 let mut escaper = None;
182 for (extensions, path) in &config.escapers {
183 if extensions.contains(&escaping) {
184 escaper = Some(path);
185 break;
186 }
187 }
188
189 let escaper = escaper.ok_or_else(|| {
190 CompileError::from(format!("no escaper defined for extension '{}'", escaping))
191 })?;
192
193 let mime_type =
194 extension_to_mime_type(ext_default_to_path(ext.as_deref(), &path).unwrap_or("txt"))
195 .to_string();
196
197 Ok(TemplateInput {
198 ast,
199 config,
200 syntax,
201 source,
202 print,
203 escaper,
204 ext,
205 mime_type,
206 parent,
207 path,
208 })
209 }
210
211 #[inline]
212 pub fn extension(&self) -> Option<&str> {
213 ext_default_to_path(self.ext.as_deref(), &self.path)
214 }
215}
216
217#[inline]
218pub fn ext_default_to_path<'a>(ext: Option<&'a str>, path: &'a Path) -> Option<&'a str> {
219 ext.or_else(|| extension(path))
220}
221
222fn extension(path: &Path) -> Option<&str> {
223 let ext = path.extension().map(|s| s.to_str().unwrap())?;
224
225 const JINJA_EXTENSIONS: [&str; 3] = ["j2", "jinja", "jinja2"];
226 if JINJA_EXTENSIONS.contains(&ext) {
227 Path::new(path.file_stem().unwrap())
228 .extension()
229 .map(|s| s.to_str().unwrap())
230 .or(Some(ext))
231 } else {
232 Some(ext)
233 }
234}
235
236pub enum Source {
237 Path(String),
238 Source(String),
239}
240
241#[derive(PartialEq)]
242pub enum Print {
243 All,
244 Ast,
245 Code,
246 None,
247}
248
249impl FromStr for Print {
250 type Err = CompileError;
251
252 fn from_str(s: &str) -> Result<Print, Self::Err> {
253 use self::Print::*;
254 Ok(match s {
255 "all" => All,
256 "ast" => Ast,
257 "code" => Code,
258 "none" => None,
259 v => return Err(format!("invalid value for print option: {}", v,).into()),
260 })
261 }
262}
263
264#[doc(hidden)]
265pub fn extension_to_mime_type(ext: &str) -> Mime {
266 let basic_type = mime_guess::from_ext(ext).first_or_octet_stream();
267 for (simple, utf_8) in &TEXT_TYPES {
268 if &basic_type == simple {
269 return utf_8.clone();
270 }
271 }
272 basic_type
273}
274
275const TEXT_TYPES: [(Mime, Mime); 6] = [
276 (mime::TEXT_PLAIN, mime::TEXT_PLAIN_UTF_8),
277 (mime::TEXT_HTML, mime::TEXT_HTML_UTF_8),
278 (mime::TEXT_CSS, mime::TEXT_CSS_UTF_8),
279 (mime::TEXT_CSV, mime::TEXT_CSV_UTF_8),
280 (
281 mime::TEXT_TAB_SEPARATED_VALUES,
282 mime::TEXT_TAB_SEPARATED_VALUES_UTF_8,
283 ),
284 (
285 mime::APPLICATION_JAVASCRIPT,
286 mime::APPLICATION_JAVASCRIPT_UTF_8,
287 ),
288];
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_ext() {
296 assert_eq!(extension(Path::new("foo-bar.txt")), Some("txt"));
297 assert_eq!(extension(Path::new("foo-bar.html")), Some("html"));
298 assert_eq!(extension(Path::new("foo-bar.unknown")), Some("unknown"));
299
300 assert_eq!(extension(Path::new("foo/bar/baz.txt")), Some("txt"));
301 assert_eq!(extension(Path::new("foo/bar/baz.html")), Some("html"));
302 assert_eq!(extension(Path::new("foo/bar/baz.unknown")), Some("unknown"));
303 }
304
305 #[test]
306 fn test_double_ext() {
307 assert_eq!(extension(Path::new("foo-bar.html.txt")), Some("txt"));
308 assert_eq!(extension(Path::new("foo-bar.txt.html")), Some("html"));
309 assert_eq!(extension(Path::new("foo-bar.txt.unknown")), Some("unknown"));
310
311 assert_eq!(extension(Path::new("foo/bar/baz.html.txt")), Some("txt"));
312 assert_eq!(extension(Path::new("foo/bar/baz.txt.html")), Some("html"));
313 assert_eq!(
314 extension(Path::new("foo/bar/baz.txt.unknown")),
315 Some("unknown")
316 );
317 }
318
319 #[test]
320 fn test_skip_jinja_ext() {
321 assert_eq!(extension(Path::new("foo-bar.html.j2")), Some("html"));
322 assert_eq!(extension(Path::new("foo-bar.html.jinja")), Some("html"));
323 assert_eq!(extension(Path::new("foo-bar.html.jinja2")), Some("html"));
324
325 assert_eq!(extension(Path::new("foo/bar/baz.txt.j2")), Some("txt"));
326 assert_eq!(extension(Path::new("foo/bar/baz.txt.jinja")), Some("txt"));
327 assert_eq!(extension(Path::new("foo/bar/baz.txt.jinja2")), Some("txt"));
328 }
329
330 #[test]
331 fn test_only_jinja_ext() {
332 assert_eq!(extension(Path::new("foo-bar.j2")), Some("j2"));
333 assert_eq!(extension(Path::new("foo-bar.jinja")), Some("jinja"));
334 assert_eq!(extension(Path::new("foo-bar.jinja2")), Some("jinja2"));
335 }
336}