askama_shared/
input.rs

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    /// Extract the template metadata from the `DeriveInput` structure. This
24    /// mostly recovers the data for the `TemplateInput` fields from the
25    /// `template()` attribute list fields; it also finds the of the `_parent`
26    /// field, if any.
27    pub fn new<'n>(
28        ast: &'n syn::DeriveInput,
29        config: &'n Config<'_>,
30    ) -> Result<TemplateInput<'n>, CompileError> {
31        // Check that an attribute called `template()` exists once and that it is
32        // the proper type (list).
33        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        // Loop over the meta attributes and find everything that we
58        // understand. Return a CompileError if something is not right.
59        // `source` contains an enum that can represent `path` or `source`.
60        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        // Validate the `source` and `ext` value together, since they are
129        // related. In case `source` was used instead of `path`, the value
130        // of `ext` is merged into a synthetic `path` value here.
131        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        // Check to see if a `_parent` field was defined on the context
141        // struct, and store the type for it for use in the code generator.
142        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        // Validate syntax
162        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        // Match extension against defined output formats
173
174        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}