askama_derive/
config.rs

1use std::collections::{BTreeMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::{env, fs};
4
5#[cfg(feature = "serde")]
6use serde::Deserialize;
7
8use crate::CompileError;
9use parser::node::Whitespace;
10use parser::Syntax;
11
12#[derive(Debug)]
13pub(crate) struct Config<'a> {
14    pub(crate) dirs: Vec<PathBuf>,
15    pub(crate) syntaxes: BTreeMap<String, Syntax<'a>>,
16    pub(crate) default_syntax: &'a str,
17    pub(crate) escapers: Vec<(HashSet<String>, String)>,
18    pub(crate) whitespace: WhitespaceHandling,
19}
20
21impl<'a> Config<'a> {
22    pub(crate) fn new(
23        s: &'a str,
24        template_whitespace: Option<&str>,
25    ) -> std::result::Result<Config<'a>, CompileError> {
26        let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
27        let default_dirs = vec![root.join("templates")];
28
29        let mut syntaxes = BTreeMap::new();
30        syntaxes.insert(DEFAULT_SYNTAX_NAME.to_string(), Syntax::default());
31
32        let raw = if s.is_empty() {
33            RawConfig::default()
34        } else {
35            RawConfig::from_toml_str(s)?
36        };
37
38        let (dirs, default_syntax, mut whitespace) = match raw.general {
39            Some(General {
40                dirs,
41                default_syntax,
42                whitespace,
43            }) => (
44                dirs.map_or(default_dirs, |v| {
45                    v.into_iter().map(|dir| root.join(dir)).collect()
46                }),
47                default_syntax.unwrap_or(DEFAULT_SYNTAX_NAME),
48                whitespace,
49            ),
50            None => (
51                default_dirs,
52                DEFAULT_SYNTAX_NAME,
53                WhitespaceHandling::default(),
54            ),
55        };
56        if let Some(template_whitespace) = template_whitespace {
57            whitespace = match template_whitespace {
58                "suppress" => WhitespaceHandling::Suppress,
59                "minimize" => WhitespaceHandling::Minimize,
60                "preserve" => WhitespaceHandling::Preserve,
61                s => return Err(format!("invalid value for `whitespace`: \"{s}\"").into()),
62            };
63        }
64
65        if let Some(raw_syntaxes) = raw.syntax {
66            for raw_s in raw_syntaxes {
67                let name = raw_s.name;
68
69                if syntaxes
70                    .insert(name.to_string(), raw_s.try_into()?)
71                    .is_some()
72                {
73                    return Err(format!("syntax \"{name}\" is already defined").into());
74                }
75            }
76        }
77
78        if !syntaxes.contains_key(default_syntax) {
79            return Err(format!("default syntax \"{default_syntax}\" not found").into());
80        }
81
82        let mut escapers = Vec::new();
83        if let Some(configured) = raw.escaper {
84            for escaper in configured {
85                escapers.push((
86                    escaper
87                        .extensions
88                        .iter()
89                        .map(|ext| (*ext).to_string())
90                        .collect(),
91                    escaper.path.to_string(),
92                ));
93            }
94        }
95        for (extensions, path) in DEFAULT_ESCAPERS {
96            escapers.push((str_set(extensions), (*path).to_string()));
97        }
98
99        Ok(Config {
100            dirs,
101            syntaxes,
102            default_syntax,
103            escapers,
104            whitespace,
105        })
106    }
107
108    pub(crate) fn find_template(
109        &self,
110        path: &str,
111        start_at: Option<&Path>,
112    ) -> std::result::Result<PathBuf, CompileError> {
113        if let Some(root) = start_at {
114            let relative = root.with_file_name(path);
115            if relative.exists() {
116                return Ok(relative);
117            }
118        }
119
120        for dir in &self.dirs {
121            let rooted = dir.join(path);
122            if rooted.exists() {
123                return Ok(rooted);
124            }
125        }
126
127        Err(format!(
128            "template {:?} not found in directories {:?}",
129            path, self.dirs
130        )
131        .into())
132    }
133}
134
135impl<'a> TryInto<Syntax<'a>> for RawSyntax<'a> {
136    type Error = CompileError;
137
138    fn try_into(self) -> Result<Syntax<'a>, Self::Error> {
139        let default = Syntax::default();
140        let syntax = Syntax {
141            block_start: self.block_start.unwrap_or(default.block_start),
142            block_end: self.block_end.unwrap_or(default.block_end),
143            expr_start: self.expr_start.unwrap_or(default.expr_start),
144            expr_end: self.expr_end.unwrap_or(default.expr_end),
145            comment_start: self.comment_start.unwrap_or(default.comment_start),
146            comment_end: self.comment_end.unwrap_or(default.comment_end),
147        };
148
149        for s in [
150            syntax.block_start,
151            syntax.block_end,
152            syntax.expr_start,
153            syntax.expr_end,
154            syntax.comment_start,
155            syntax.comment_end,
156        ] {
157            if s.len() < 2 {
158                return Err(
159                    format!("delimiters must be at least two characters long: {s:?}").into(),
160                );
161            } else if s.chars().any(|c| c.is_whitespace()) {
162                return Err(format!("delimiters may not contain white spaces: {s:?}").into());
163            }
164        }
165
166        for (s1, s2) in [
167            (syntax.block_start, syntax.expr_start),
168            (syntax.block_start, syntax.comment_start),
169            (syntax.expr_start, syntax.comment_start),
170        ] {
171            if s1.starts_with(s2) || s2.starts_with(s1) {
172                return Err(format!(
173                    "a delimiter may not be the prefix of another delimiter: {s1:?} vs {s2:?}",
174                )
175                .into());
176            }
177        }
178
179        Ok(syntax)
180    }
181}
182
183#[cfg_attr(feature = "serde", derive(Deserialize))]
184#[derive(Default)]
185struct RawConfig<'a> {
186    #[cfg_attr(feature = "serde", serde(borrow))]
187    general: Option<General<'a>>,
188    syntax: Option<Vec<RawSyntax<'a>>>,
189    escaper: Option<Vec<RawEscaper<'a>>>,
190}
191
192impl RawConfig<'_> {
193    #[cfg(feature = "config")]
194    fn from_toml_str(s: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
195        basic_toml::from_str(s)
196            .map_err(|e| format!("invalid TOML in {CONFIG_FILE_NAME}: {e}").into())
197    }
198
199    #[cfg(not(feature = "config"))]
200    fn from_toml_str(_: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
201        Err("TOML support not available".into())
202    }
203}
204
205#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
206#[cfg_attr(feature = "serde", derive(Deserialize))]
207#[cfg_attr(feature = "serde", serde(field_identifier, rename_all = "lowercase"))]
208pub(crate) enum WhitespaceHandling {
209    /// The default behaviour. It will leave the whitespace characters "as is".
210    #[default]
211    Preserve,
212    /// It'll remove all the whitespace characters before and after the jinja block.
213    Suppress,
214    /// It'll remove all the whitespace characters except one before and after the jinja blocks.
215    /// If there is a newline character, the preserved character in the trimmed characters, it will
216    /// the one preserved.
217    Minimize,
218}
219
220impl From<WhitespaceHandling> for Whitespace {
221    fn from(ws: WhitespaceHandling) -> Self {
222        match ws {
223            WhitespaceHandling::Suppress => Whitespace::Suppress,
224            WhitespaceHandling::Preserve => Whitespace::Preserve,
225            WhitespaceHandling::Minimize => Whitespace::Minimize,
226        }
227    }
228}
229
230#[cfg_attr(feature = "serde", derive(Deserialize))]
231struct General<'a> {
232    #[cfg_attr(feature = "serde", serde(borrow))]
233    dirs: Option<Vec<&'a str>>,
234    default_syntax: Option<&'a str>,
235    #[cfg_attr(feature = "serde", serde(default))]
236    whitespace: WhitespaceHandling,
237}
238
239#[cfg_attr(feature = "serde", derive(Deserialize))]
240struct RawSyntax<'a> {
241    name: &'a str,
242    block_start: Option<&'a str>,
243    block_end: Option<&'a str>,
244    expr_start: Option<&'a str>,
245    expr_end: Option<&'a str>,
246    comment_start: Option<&'a str>,
247    comment_end: Option<&'a str>,
248}
249
250#[cfg_attr(feature = "serde", derive(Deserialize))]
251struct RawEscaper<'a> {
252    path: &'a str,
253    extensions: Vec<&'a str>,
254}
255
256pub(crate) fn read_config_file(
257    config_path: Option<&str>,
258) -> std::result::Result<String, CompileError> {
259    let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
260    let filename = match config_path {
261        Some(config_path) => root.join(config_path),
262        None => root.join(CONFIG_FILE_NAME),
263    };
264
265    if filename.exists() {
266        fs::read_to_string(&filename)
267            .map_err(|_| format!("unable to read {:?}", filename.to_str().unwrap()).into())
268    } else if config_path.is_some() {
269        Err(format!("`{}` does not exist", root.display()).into())
270    } else {
271        Ok("".to_string())
272    }
273}
274
275fn str_set<T>(vals: &[T]) -> HashSet<String>
276where
277    T: ToString,
278{
279    vals.iter().map(|s| s.to_string()).collect()
280}
281
282#[allow(clippy::match_wild_err_arm)]
283pub(crate) fn get_template_source(tpl_path: &Path) -> std::result::Result<String, CompileError> {
284    match fs::read_to_string(tpl_path) {
285        Err(_) => Err(format!(
286            "unable to open template file '{}'",
287            tpl_path.to_str().unwrap()
288        )
289        .into()),
290        Ok(mut source) => {
291            if source.ends_with('\n') {
292                let _ = source.pop();
293            }
294            Ok(source)
295        }
296    }
297}
298
299static CONFIG_FILE_NAME: &str = "askama.toml";
300static DEFAULT_SYNTAX_NAME: &str = "default";
301static DEFAULT_ESCAPERS: &[(&[&str], &str)] = &[
302    (&["html", "htm", "svg", "xml"], "::askama::Html"),
303    (&["md", "none", "txt", "yml", ""], "::askama::Text"),
304    (&["j2", "jinja", "jinja2"], "::askama::Html"),
305];
306
307#[cfg(test)]
308mod tests {
309    use std::env;
310    use std::path::{Path, PathBuf};
311
312    use super::*;
313
314    #[test]
315    fn get_source() {
316        let path = Config::new("", None)
317            .and_then(|config| config.find_template("b.html", None))
318            .unwrap();
319        assert_eq!(get_template_source(&path).unwrap(), "bar");
320    }
321
322    #[test]
323    fn test_default_config() {
324        let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
325        root.push("templates");
326        let config = Config::new("", None).unwrap();
327        assert_eq!(config.dirs, vec![root]);
328    }
329
330    #[cfg(feature = "config")]
331    #[test]
332    fn test_config_dirs() {
333        let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
334        root.push("tpl");
335        let config = Config::new("[general]\ndirs = [\"tpl\"]", None).unwrap();
336        assert_eq!(config.dirs, vec![root]);
337    }
338
339    fn assert_eq_rooted(actual: &Path, expected: &str) {
340        let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
341        root.push("templates");
342        let mut inner = PathBuf::new();
343        inner.push(expected);
344        assert_eq!(actual.strip_prefix(root).unwrap(), inner);
345    }
346
347    #[test]
348    fn find_absolute() {
349        let config = Config::new("", None).unwrap();
350        let root = config.find_template("a.html", None).unwrap();
351        let path = config.find_template("sub/b.html", Some(&root)).unwrap();
352        assert_eq_rooted(&path, "sub/b.html");
353    }
354
355    #[test]
356    #[should_panic]
357    fn find_relative_nonexistent() {
358        let config = Config::new("", None).unwrap();
359        let root = config.find_template("a.html", None).unwrap();
360        config.find_template("c.html", Some(&root)).unwrap();
361    }
362
363    #[test]
364    fn find_relative() {
365        let config = Config::new("", None).unwrap();
366        let root = config.find_template("sub/b.html", None).unwrap();
367        let path = config.find_template("c.html", Some(&root)).unwrap();
368        assert_eq_rooted(&path, "sub/c.html");
369    }
370
371    #[test]
372    fn find_relative_sub() {
373        let config = Config::new("", None).unwrap();
374        let root = config.find_template("sub/b.html", None).unwrap();
375        let path = config.find_template("sub1/d.html", Some(&root)).unwrap();
376        assert_eq_rooted(&path, "sub/sub1/d.html");
377    }
378
379    #[cfg(feature = "config")]
380    #[test]
381    fn add_syntax() {
382        let raw_config = r#"
383        [general]
384        default_syntax = "foo"
385
386        [[syntax]]
387        name = "foo"
388        block_start = "{<"
389
390        [[syntax]]
391        name = "bar"
392        expr_start = "{!"
393        "#;
394
395        let default_syntax = Syntax::default();
396        let config = Config::new(raw_config, None).unwrap();
397        assert_eq!(config.default_syntax, "foo");
398
399        let foo = config.syntaxes.get("foo").unwrap();
400        assert_eq!(foo.block_start, "{<");
401        assert_eq!(foo.block_end, default_syntax.block_end);
402        assert_eq!(foo.expr_start, default_syntax.expr_start);
403        assert_eq!(foo.expr_end, default_syntax.expr_end);
404        assert_eq!(foo.comment_start, default_syntax.comment_start);
405        assert_eq!(foo.comment_end, default_syntax.comment_end);
406
407        let bar = config.syntaxes.get("bar").unwrap();
408        assert_eq!(bar.block_start, default_syntax.block_start);
409        assert_eq!(bar.block_end, default_syntax.block_end);
410        assert_eq!(bar.expr_start, "{!");
411        assert_eq!(bar.expr_end, default_syntax.expr_end);
412        assert_eq!(bar.comment_start, default_syntax.comment_start);
413        assert_eq!(bar.comment_end, default_syntax.comment_end);
414    }
415
416    #[cfg(feature = "config")]
417    #[test]
418    fn add_syntax_two() {
419        let raw_config = r#"
420        syntax = [{ name = "foo", block_start = "{<" },
421                  { name = "bar", expr_start = "{!" } ]
422
423        [general]
424        default_syntax = "foo"
425        "#;
426
427        let default_syntax = Syntax::default();
428        let config = Config::new(raw_config, None).unwrap();
429        assert_eq!(config.default_syntax, "foo");
430
431        let foo = config.syntaxes.get("foo").unwrap();
432        assert_eq!(foo.block_start, "{<");
433        assert_eq!(foo.block_end, default_syntax.block_end);
434        assert_eq!(foo.expr_start, default_syntax.expr_start);
435        assert_eq!(foo.expr_end, default_syntax.expr_end);
436        assert_eq!(foo.comment_start, default_syntax.comment_start);
437        assert_eq!(foo.comment_end, default_syntax.comment_end);
438
439        let bar = config.syntaxes.get("bar").unwrap();
440        assert_eq!(bar.block_start, default_syntax.block_start);
441        assert_eq!(bar.block_end, default_syntax.block_end);
442        assert_eq!(bar.expr_start, "{!");
443        assert_eq!(bar.expr_end, default_syntax.expr_end);
444        assert_eq!(bar.comment_start, default_syntax.comment_start);
445        assert_eq!(bar.comment_end, default_syntax.comment_end);
446    }
447
448    #[cfg(feature = "config")]
449    #[test]
450    fn longer_delimiters() {
451        let raw_config = r#"
452        [[syntax]]
453        name = "emoji"
454        block_start = "👉🙂👉"
455        block_end = "👈🙃👈"
456        expr_start = "🤜🤜"
457        expr_end = "🤛🤛"
458        comment_start = "👎_(ツ)_👎"
459        comment_end = "👍:D👍"
460
461        [general]
462        default_syntax = "emoji"
463        "#;
464
465        let config = Config::new(raw_config, None).unwrap();
466        assert_eq!(config.default_syntax, "emoji");
467
468        let foo = config.syntaxes.get("emoji").unwrap();
469        assert_eq!(foo.block_start, "👉🙂👉");
470        assert_eq!(foo.block_end, "👈🙃👈");
471        assert_eq!(foo.expr_start, "🤜🤜");
472        assert_eq!(foo.expr_end, "🤛🤛");
473        assert_eq!(foo.comment_start, "👎_(ツ)_👎");
474        assert_eq!(foo.comment_end, "👍:D👍");
475    }
476
477    #[cfg(feature = "config")]
478    #[test]
479    fn illegal_delimiters() {
480        let raw_config = r#"
481        [[syntax]]
482        name = "too_short"
483        block_start = "<"
484        "#;
485        let config = Config::new(raw_config, None);
486        assert_eq!(
487            config.unwrap_err().msg,
488            r#"delimiters must be at least two characters long: "<""#,
489        );
490
491        let raw_config = r#"
492        [[syntax]]
493        name = "contains_ws"
494        block_start = " {{ "
495        "#;
496        let config = Config::new(raw_config, None);
497        assert_eq!(
498            config.unwrap_err().msg,
499            r#"delimiters may not contain white spaces: " {{ ""#,
500        );
501
502        let raw_config = r#"
503        [[syntax]]
504        name = "is_prefix"
505        block_start = "{{"
506        expr_start = "{{$"
507        comment_start = "{{#"
508        "#;
509        let config = Config::new(raw_config, None);
510        assert_eq!(
511            config.unwrap_err().msg,
512            r#"a delimiter may not be the prefix of another delimiter: "{{" vs "{{$""#,
513        );
514    }
515
516    #[cfg(feature = "toml")]
517    #[should_panic]
518    #[test]
519    fn use_default_at_syntax_name() {
520        let raw_config = r#"
521        syntax = [{ name = "default" }]
522        "#;
523
524        let _config = Config::new(raw_config, None).unwrap();
525    }
526
527    #[cfg(feature = "toml")]
528    #[should_panic]
529    #[test]
530    fn duplicated_syntax_name_on_list() {
531        let raw_config = r#"
532        syntax = [{ name = "foo", block_start = "~<" },
533                  { name = "foo", block_start = "%%" } ]
534        "#;
535
536        let _config = Config::new(raw_config, None).unwrap();
537    }
538
539    #[cfg(feature = "toml")]
540    #[should_panic]
541    #[test]
542    fn is_not_exist_default_syntax() {
543        let raw_config = r#"
544        [general]
545        default_syntax = "foo"
546        "#;
547
548        let _config = Config::new(raw_config, None).unwrap();
549    }
550
551    #[cfg(feature = "config")]
552    #[test]
553    fn escape_modes() {
554        let config = Config::new(
555            r#"
556            [[escaper]]
557            path = "::askama::Js"
558            extensions = ["js"]
559        "#,
560            None,
561        )
562        .unwrap();
563        assert_eq!(
564            config.escapers,
565            vec![
566                (str_set(&["js"]), "::askama::Js".into()),
567                (
568                    str_set(&["html", "htm", "svg", "xml"]),
569                    "::askama::Html".into()
570                ),
571                (
572                    str_set(&["md", "none", "txt", "yml", ""]),
573                    "::askama::Text".into()
574                ),
575                (str_set(&["j2", "jinja", "jinja2"]), "::askama::Html".into()),
576            ]
577        );
578    }
579
580    #[cfg(feature = "config")]
581    #[test]
582    fn test_whitespace_parsing() {
583        let config = Config::new(
584            r#"
585            [general]
586            whitespace = "suppress"
587            "#,
588            None,
589        )
590        .unwrap();
591        assert_eq!(config.whitespace, WhitespaceHandling::Suppress);
592
593        let config = Config::new(r#""#, None).unwrap();
594        assert_eq!(config.whitespace, WhitespaceHandling::Preserve);
595
596        let config = Config::new(
597            r#"
598            [general]
599            whitespace = "preserve"
600            "#,
601            None,
602        )
603        .unwrap();
604        assert_eq!(config.whitespace, WhitespaceHandling::Preserve);
605
606        let config = Config::new(
607            r#"
608            [general]
609            whitespace = "minimize"
610            "#,
611            None,
612        )
613        .unwrap();
614        assert_eq!(config.whitespace, WhitespaceHandling::Minimize);
615    }
616
617    #[cfg(feature = "toml")]
618    #[test]
619    fn test_whitespace_in_template() {
620        // Checking that template arguments have precedence over general configuration.
621        // So in here, in the template arguments, there is `whitespace = "minimize"` so
622        // the `WhitespaceHandling` should be `Minimize` as well.
623        let config = Config::new(
624            r#"
625            [general]
626            whitespace = "suppress"
627            "#,
628            Some(&"minimize".to_owned()),
629        )
630        .unwrap();
631        assert_eq!(config.whitespace, WhitespaceHandling::Minimize);
632
633        let config = Config::new(r#""#, Some(&"minimize".to_owned())).unwrap();
634        assert_eq!(config.whitespace, WhitespaceHandling::Minimize);
635    }
636
637    #[test]
638    fn test_config_whitespace_error() {
639        let config = Config::new(r#""#, Some("trim"));
640        if let Err(err) = config {
641            assert_eq!(err.msg, "invalid value for `whitespace`: \"trim\"");
642        } else {
643            panic!("Config::new should have return an error");
644        }
645    }
646}