1 | use std::collections::hash_map::HashMap; |
2 | use std::path::{Path, PathBuf}; |
3 | use std::str::FromStr; |
4 | |
5 | use mime::Mime; |
6 | use quote::ToTokens; |
7 | use syn::punctuated::Punctuated; |
8 | |
9 | use crate::config::{get_template_source, read_config_file, Config}; |
10 | use crate::CompileError; |
11 | use parser::{Node, Parsed, Syntax}; |
12 | |
13 | pub(crate) struct TemplateInput<'a> { |
14 | pub(crate) ast: &'a syn::DeriveInput, |
15 | pub(crate) config: &'a Config<'a>, |
16 | pub(crate) syntax: &'a Syntax<'a>, |
17 | pub(crate) source: &'a Source, |
18 | pub(crate) print: Print, |
19 | pub(crate) escaper: &'a str, |
20 | pub(crate) ext: Option<&'a str>, |
21 | pub(crate) mime_type: String, |
22 | pub(crate) path: PathBuf, |
23 | } |
24 | |
25 | impl TemplateInput<'_> { |
26 | /// Extract the template metadata from the `DeriveInput` structure. This |
27 | /// mostly recovers the data for the `TemplateInput` fields from the |
28 | /// `template()` attribute list fields. |
29 | pub(crate) fn new<'n>( |
30 | ast: &'n syn::DeriveInput, |
31 | config: &'n Config<'_>, |
32 | args: &'n TemplateArgs, |
33 | ) -> Result<TemplateInput<'n>, CompileError> { |
34 | let TemplateArgs { |
35 | source, |
36 | print, |
37 | escaping, |
38 | ext, |
39 | syntax, |
40 | .. |
41 | } = args; |
42 | |
43 | // Validate the `source` and `ext` value together, since they are |
44 | // related. In case `source` was used instead of `path`, the value |
45 | // of `ext` is merged into a synthetic `path` value here. |
46 | let source = source |
47 | .as_ref() |
48 | .expect("template path or source not found in attributes" ); |
49 | let path = match (&source, &ext) { |
50 | (Source::Path(path), _) => config.find_template(path, None)?, |
51 | (&Source::Source(_), Some(ext)) => PathBuf::from(format!(" {}. {}" , ast.ident, ext)), |
52 | (&Source::Source(_), None) => { |
53 | return Err("must include 'ext' attribute when using 'source' attribute" .into()) |
54 | } |
55 | }; |
56 | |
57 | // Validate syntax |
58 | let syntax = syntax.as_deref().map_or_else( |
59 | || Ok(config.syntaxes.get(config.default_syntax).unwrap()), |
60 | |s| { |
61 | config |
62 | .syntaxes |
63 | .get(s) |
64 | .ok_or_else(|| CompileError::from(format!("attribute syntax {s} not exist" ))) |
65 | }, |
66 | )?; |
67 | |
68 | // Match extension against defined output formats |
69 | |
70 | let escaping = escaping |
71 | .as_deref() |
72 | .unwrap_or_else(|| path.extension().map(|s| s.to_str().unwrap()).unwrap_or("" )); |
73 | |
74 | let mut escaper = None; |
75 | for (extensions, path) in &config.escapers { |
76 | if extensions.contains(escaping) { |
77 | escaper = Some(path); |
78 | break; |
79 | } |
80 | } |
81 | |
82 | let escaper = escaper.ok_or_else(|| { |
83 | CompileError::from(format!("no escaper defined for extension ' {escaping}'" )) |
84 | })?; |
85 | |
86 | let mime_type = |
87 | extension_to_mime_type(ext_default_to_path(ext.as_deref(), &path).unwrap_or("txt" )) |
88 | .to_string(); |
89 | |
90 | Ok(TemplateInput { |
91 | ast, |
92 | config, |
93 | syntax, |
94 | source, |
95 | print: *print, |
96 | escaper, |
97 | ext: ext.as_deref(), |
98 | mime_type, |
99 | path, |
100 | }) |
101 | } |
102 | |
103 | pub(crate) fn find_used_templates( |
104 | &self, |
105 | map: &mut HashMap<PathBuf, Parsed>, |
106 | ) -> Result<(), CompileError> { |
107 | let source = match &self.source { |
108 | Source::Source(s) => s.clone(), |
109 | Source::Path(_) => get_template_source(&self.path)?, |
110 | }; |
111 | |
112 | let mut dependency_graph = Vec::new(); |
113 | let mut check = vec![(self.path.clone(), source)]; |
114 | while let Some((path, source)) = check.pop() { |
115 | let parsed = Parsed::new(source, self.syntax)?; |
116 | |
117 | let mut top = true; |
118 | let mut nested = vec![parsed.nodes()]; |
119 | while let Some(nodes) = nested.pop() { |
120 | for n in nodes { |
121 | let mut add_to_check = |path: PathBuf| -> Result<(), CompileError> { |
122 | if !map.contains_key(&path) { |
123 | // Add a dummy entry to `map` in order to prevent adding `path` |
124 | // multiple times to `check`. |
125 | map.insert(path.clone(), Parsed::default()); |
126 | let source = get_template_source(&path)?; |
127 | check.push((path, source)); |
128 | } |
129 | Ok(()) |
130 | }; |
131 | |
132 | use Node::*; |
133 | match n { |
134 | Extends(extends) if top => { |
135 | let extends = self.config.find_template(extends.path, Some(&path))?; |
136 | let dependency_path = (path.clone(), extends.clone()); |
137 | if dependency_graph.contains(&dependency_path) { |
138 | return Err(format!( |
139 | "cyclic dependency in graph {:#?}" , |
140 | dependency_graph |
141 | .iter() |
142 | .map(|e| format!(" {:#?} --> {:#?}" , e.0, e.1)) |
143 | .collect::<Vec<String>>() |
144 | ) |
145 | .into()); |
146 | } |
147 | dependency_graph.push(dependency_path); |
148 | add_to_check(extends)?; |
149 | } |
150 | Macro(m) if top => { |
151 | nested.push(&m.nodes); |
152 | } |
153 | Import(import) if top => { |
154 | let import = self.config.find_template(import.path, Some(&path))?; |
155 | add_to_check(import)?; |
156 | } |
157 | Include(include) => { |
158 | let include = self.config.find_template(include.path, Some(&path))?; |
159 | add_to_check(include)?; |
160 | } |
161 | BlockDef(b) => { |
162 | nested.push(&b.nodes); |
163 | } |
164 | If(i) => { |
165 | for cond in &i.branches { |
166 | nested.push(&cond.nodes); |
167 | } |
168 | } |
169 | Loop(l) => { |
170 | nested.push(&l.body); |
171 | nested.push(&l.else_nodes); |
172 | } |
173 | Match(m) => { |
174 | for arm in &m.arms { |
175 | nested.push(&arm.nodes); |
176 | } |
177 | } |
178 | Lit(_) |
179 | | Comment(_) |
180 | | Expr(_, _) |
181 | | Call(_) |
182 | | Extends(_) |
183 | | Let(_) |
184 | | Import(_) |
185 | | Macro(_) |
186 | | Raw(_) |
187 | | Continue(_) |
188 | | Break(_) => {} |
189 | } |
190 | } |
191 | top = false; |
192 | } |
193 | map.insert(path, parsed); |
194 | } |
195 | Ok(()) |
196 | } |
197 | |
198 | #[inline ] |
199 | pub(crate) fn extension(&self) -> Option<&str> { |
200 | ext_default_to_path(self.ext, &self.path) |
201 | } |
202 | } |
203 | |
204 | #[derive (Debug, Default)] |
205 | pub(crate) struct TemplateArgs { |
206 | source: Option<Source>, |
207 | print: Print, |
208 | escaping: Option<String>, |
209 | ext: Option<String>, |
210 | syntax: Option<String>, |
211 | config: Option<String>, |
212 | pub(crate) whitespace: Option<String>, |
213 | } |
214 | |
215 | impl TemplateArgs { |
216 | pub(crate) fn new(ast: &'_ syn::DeriveInput) -> Result<Self, CompileError> { |
217 | // Check that an attribute called `template()` exists once and that it is |
218 | // the proper type (list). |
219 | let mut template_args = None; |
220 | for attr in &ast.attrs { |
221 | if !attr.path().is_ident("template" ) { |
222 | continue; |
223 | } |
224 | |
225 | match attr.parse_args_with(Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated) { |
226 | Ok(args) if template_args.is_none() => template_args = Some(args), |
227 | Ok(_) => return Err("duplicated 'template' attribute" .into()), |
228 | Err(e) => return Err(format!("unable to parse template arguments: {e}" ).into()), |
229 | }; |
230 | } |
231 | |
232 | let template_args = |
233 | template_args.ok_or_else(|| CompileError::from("no attribute 'template' found" ))?; |
234 | |
235 | let mut args = Self::default(); |
236 | // Loop over the meta attributes and find everything that we |
237 | // understand. Return a CompileError if something is not right. |
238 | // `source` contains an enum that can represent `path` or `source`. |
239 | for item in template_args { |
240 | let pair = match item { |
241 | syn::Meta::NameValue(pair) => pair, |
242 | _ => { |
243 | return Err(format!( |
244 | "unsupported attribute argument {:?}" , |
245 | item.to_token_stream() |
246 | ) |
247 | .into()) |
248 | } |
249 | }; |
250 | |
251 | let ident = match pair.path.get_ident() { |
252 | Some(ident) => ident, |
253 | None => unreachable!("not possible in syn::Meta::NameValue(…)" ), |
254 | }; |
255 | |
256 | let value = match pair.value { |
257 | syn::Expr::Lit(lit) => lit, |
258 | syn::Expr::Group(group) => match *group.expr { |
259 | syn::Expr::Lit(lit) => lit, |
260 | _ => { |
261 | return Err(format!("unsupported argument value type for {ident:?}" ).into()) |
262 | } |
263 | }, |
264 | _ => return Err(format!("unsupported argument value type for {ident:?}" ).into()), |
265 | }; |
266 | |
267 | if ident == "path" { |
268 | if let syn::Lit::Str(s) = value.lit { |
269 | if args.source.is_some() { |
270 | return Err("must specify 'source' or 'path', not both" .into()); |
271 | } |
272 | args.source = Some(Source::Path(s.value())); |
273 | } else { |
274 | return Err("template path must be string literal" .into()); |
275 | } |
276 | } else if ident == "source" { |
277 | if let syn::Lit::Str(s) = value.lit { |
278 | if args.source.is_some() { |
279 | return Err("must specify 'source' or 'path', not both" .into()); |
280 | } |
281 | args.source = Some(Source::Source(s.value())); |
282 | } else { |
283 | return Err("template source must be string literal" .into()); |
284 | } |
285 | } else if ident == "print" { |
286 | if let syn::Lit::Str(s) = value.lit { |
287 | args.print = s.value().parse()?; |
288 | } else { |
289 | return Err("print value must be string literal" .into()); |
290 | } |
291 | } else if ident == "escape" { |
292 | if let syn::Lit::Str(s) = value.lit { |
293 | args.escaping = Some(s.value()); |
294 | } else { |
295 | return Err("escape value must be string literal" .into()); |
296 | } |
297 | } else if ident == "ext" { |
298 | if let syn::Lit::Str(s) = value.lit { |
299 | args.ext = Some(s.value()); |
300 | } else { |
301 | return Err("ext value must be string literal" .into()); |
302 | } |
303 | } else if ident == "syntax" { |
304 | if let syn::Lit::Str(s) = value.lit { |
305 | args.syntax = Some(s.value()) |
306 | } else { |
307 | return Err("syntax value must be string literal" .into()); |
308 | } |
309 | } else if ident == "config" { |
310 | if let syn::Lit::Str(s) = value.lit { |
311 | args.config = Some(s.value()); |
312 | } else { |
313 | return Err("config value must be string literal" .into()); |
314 | } |
315 | } else if ident == "whitespace" { |
316 | if let syn::Lit::Str(s) = value.lit { |
317 | args.whitespace = Some(s.value()) |
318 | } else { |
319 | return Err("whitespace value must be string literal" .into()); |
320 | } |
321 | } else { |
322 | return Err(format!("unsupported attribute key {ident:?} found" ).into()); |
323 | } |
324 | } |
325 | |
326 | Ok(args) |
327 | } |
328 | |
329 | pub(crate) fn config(&self) -> Result<String, CompileError> { |
330 | read_config_file(self.config.as_deref()) |
331 | } |
332 | } |
333 | |
334 | #[inline ] |
335 | fn ext_default_to_path<'a>(ext: Option<&'a str>, path: &'a Path) -> Option<&'a str> { |
336 | ext.or_else(|| extension(path)) |
337 | } |
338 | |
339 | fn extension(path: &Path) -> Option<&str> { |
340 | let ext: &str = path.extension().map(|s: &OsStr| s.to_str().unwrap())?; |
341 | |
342 | const JINJA_EXTENSIONS: [&str; 3] = ["j2" , "jinja" , "jinja2" ]; |
343 | if JINJA_EXTENSIONS.contains(&ext) { |
344 | Path::new(path.file_stem().unwrap()) |
345 | .extension() |
346 | .map(|s| s.to_str().unwrap()) |
347 | .or(optb:Some(ext)) |
348 | } else { |
349 | Some(ext) |
350 | } |
351 | } |
352 | |
353 | #[derive (Debug)] |
354 | pub(crate) enum Source { |
355 | Path(String), |
356 | Source(String), |
357 | } |
358 | |
359 | #[derive (Clone, Copy, Debug, PartialEq)] |
360 | pub(crate) enum Print { |
361 | All, |
362 | Ast, |
363 | Code, |
364 | None, |
365 | } |
366 | |
367 | impl FromStr for Print { |
368 | type Err = CompileError; |
369 | |
370 | fn from_str(s: &str) -> Result<Print, Self::Err> { |
371 | use self::Print::*; |
372 | Ok(match s { |
373 | "all" => All, |
374 | "ast" => Ast, |
375 | "code" => Code, |
376 | "none" => None, |
377 | v: &str => return Err(format!("invalid value for print option: {v}" ,).into()), |
378 | }) |
379 | } |
380 | } |
381 | |
382 | impl Default for Print { |
383 | fn default() -> Self { |
384 | Self::None |
385 | } |
386 | } |
387 | |
388 | pub(crate) fn extension_to_mime_type(ext: &str) -> Mime { |
389 | let basic_type: Mime = mime_guess::from_ext(ext).first_or_octet_stream(); |
390 | for (simple: &Mime, utf_8: &Mime) in &TEXT_TYPES { |
391 | if &basic_type == simple { |
392 | return utf_8.clone(); |
393 | } |
394 | } |
395 | basic_type |
396 | } |
397 | |
398 | const TEXT_TYPES: [(Mime, Mime); 7] = [ |
399 | (mime::TEXT_PLAIN, mime::TEXT_PLAIN_UTF_8), |
400 | (mime::TEXT_HTML, mime::TEXT_HTML_UTF_8), |
401 | (mime::TEXT_CSS, mime::TEXT_CSS_UTF_8), |
402 | (mime::TEXT_CSV, mime::TEXT_CSV_UTF_8), |
403 | ( |
404 | mime::TEXT_TAB_SEPARATED_VALUES, |
405 | mime::TEXT_TAB_SEPARATED_VALUES_UTF_8, |
406 | ), |
407 | ( |
408 | mime::APPLICATION_JAVASCRIPT, |
409 | mime::APPLICATION_JAVASCRIPT_UTF_8, |
410 | ), |
411 | (mime::IMAGE_SVG, mime::IMAGE_SVG), |
412 | ]; |
413 | |
414 | #[cfg (test)] |
415 | mod tests { |
416 | use super::*; |
417 | |
418 | #[test ] |
419 | fn test_ext() { |
420 | assert_eq!(extension(Path::new("foo-bar.txt" )), Some("txt" )); |
421 | assert_eq!(extension(Path::new("foo-bar.html" )), Some("html" )); |
422 | assert_eq!(extension(Path::new("foo-bar.unknown" )), Some("unknown" )); |
423 | assert_eq!(extension(Path::new("foo-bar.svg" )), Some("svg" )); |
424 | |
425 | assert_eq!(extension(Path::new("foo/bar/baz.txt" )), Some("txt" )); |
426 | assert_eq!(extension(Path::new("foo/bar/baz.html" )), Some("html" )); |
427 | assert_eq!(extension(Path::new("foo/bar/baz.unknown" )), Some("unknown" )); |
428 | assert_eq!(extension(Path::new("foo/bar/baz.svg" )), Some("svg" )); |
429 | } |
430 | |
431 | #[test ] |
432 | fn test_double_ext() { |
433 | assert_eq!(extension(Path::new("foo-bar.html.txt" )), Some("txt" )); |
434 | assert_eq!(extension(Path::new("foo-bar.txt.html" )), Some("html" )); |
435 | assert_eq!(extension(Path::new("foo-bar.txt.unknown" )), Some("unknown" )); |
436 | |
437 | assert_eq!(extension(Path::new("foo/bar/baz.html.txt" )), Some("txt" )); |
438 | assert_eq!(extension(Path::new("foo/bar/baz.txt.html" )), Some("html" )); |
439 | assert_eq!( |
440 | extension(Path::new("foo/bar/baz.txt.unknown" )), |
441 | Some("unknown" ) |
442 | ); |
443 | } |
444 | |
445 | #[test ] |
446 | fn test_skip_jinja_ext() { |
447 | assert_eq!(extension(Path::new("foo-bar.html.j2" )), Some("html" )); |
448 | assert_eq!(extension(Path::new("foo-bar.html.jinja" )), Some("html" )); |
449 | assert_eq!(extension(Path::new("foo-bar.html.jinja2" )), Some("html" )); |
450 | |
451 | assert_eq!(extension(Path::new("foo/bar/baz.txt.j2" )), Some("txt" )); |
452 | assert_eq!(extension(Path::new("foo/bar/baz.txt.jinja" )), Some("txt" )); |
453 | assert_eq!(extension(Path::new("foo/bar/baz.txt.jinja2" )), Some("txt" )); |
454 | } |
455 | |
456 | #[test ] |
457 | fn test_only_jinja_ext() { |
458 | assert_eq!(extension(Path::new("foo-bar.j2" )), Some("j2" )); |
459 | assert_eq!(extension(Path::new("foo-bar.jinja" )), Some("jinja" )); |
460 | assert_eq!(extension(Path::new("foo-bar.jinja2" )), Some("jinja2" )); |
461 | } |
462 | } |
463 | |