Coverage Report

Created: 2026-10-28 06:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/askama/askama_derive/src/config.rs
Line
Count
Source
1
use std::borrow::{Borrow, Cow};
2
use std::collections::hash_map::Entry;
3
use std::mem::ManuallyDrop;
4
use std::ops::Deref;
5
use std::path::{Path, PathBuf, absolute};
6
use std::sync::{Arc, OnceLock};
7
use std::{env, fs};
8
9
use parser::node::Whitespace;
10
use parser::{ParseError, Parsed, Syntax, SyntaxBuilder};
11
use proc_macro2::Span;
12
#[cfg(feature = "config")]
13
use serde_derive::Deserialize;
14
15
use crate::{CompileError, FileInfo, HashMap, OnceMap};
16
17
#[derive(Debug)]
18
pub(crate) struct Config {
19
    pub(crate) dirs: Vec<PathBuf>,
20
    pub(crate) syntaxes: HashMap<String, SyntaxAndCache>,
21
    pub(crate) default_syntax: &'static str,
22
    pub(crate) escapers: Vec<(Vec<Cow<'static, str>>, Cow<'static, str>)>,
23
    pub(crate) whitespace: Whitespace,
24
    pub(crate) full_config_path: Option<PathBuf>,
25
    // `Config` is self referential and `_key` owns it data, so it must come last
26
    _key: OwnedConfigKey,
27
}
28
29
impl Drop for Config {
30
    #[track_caller]
31
0
    fn drop(&mut self) {
32
0
        panic!();
33
    }
34
}
35
36
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
37
struct OwnedConfigKey(&'static ConfigKey<'static>);
38
39
#[derive(Debug, PartialEq, Eq, Hash)]
40
struct ConfigKey<'a> {
41
    root: Cow<'a, Path>,
42
    source: Cow<'a, str>,
43
    config_path: Option<Cow<'a, str>>,
44
    template_whitespace: Option<Whitespace>,
45
}
46
47
impl ToOwned for ConfigKey<'_> {
48
    type Owned = OwnedConfigKey;
49
50
1
    fn to_owned(&self) -> Self::Owned {
51
1
        let owned_key = ConfigKey {
52
1
            root: Cow::Owned(self.root.as_ref().to_owned()),
53
1
            source: Cow::Owned(self.source.as_ref().to_owned()),
54
1
            config_path: self
55
1
                .config_path
56
1
                .as_ref()
57
1
                .map(|s| Cow::Owned(s.as_ref().to_owned())),
58
1
            template_whitespace: self.template_whitespace,
59
        };
60
1
        OwnedConfigKey(Box::leak(Box::new(owned_key)))
61
1
    }
62
}
63
64
impl<'a> Borrow<ConfigKey<'a>> for OwnedConfigKey {
65
    #[inline]
66
17.8k
    fn borrow(&self) -> &ConfigKey<'a> {
67
17.8k
        self.0
68
17.8k
    }
69
}
70
71
impl Config {
72
17.8k
    pub(crate) fn new(
73
17.8k
        source: &str,
74
17.8k
        config_path: Option<&str>,
75
17.8k
        template_whitespace: Option<Whitespace>,
76
17.8k
        config_span: Option<Span>,
77
17.8k
        full_config_path: Option<PathBuf>,
78
17.8k
    ) -> Result<&'static Config, CompileError> {
79
        static CACHE: ManuallyDrop<OnceLock<OnceMap<OwnedConfigKey, &'static Config>>> =
80
            ManuallyDrop::new(OnceLock::new());
81
17.8k
        CACHE.get_or_init(OnceMap::default).get_or_try_insert(
82
17.8k
            &ConfigKey {
83
17.8k
                root: Cow::Owned(manifest_root()),
84
17.8k
                source: source.into(),
85
17.8k
                config_path: config_path.map(Cow::Borrowed),
86
17.8k
                template_whitespace,
87
17.8k
            },
88
1
            |key| {
89
1
                let config = Config::new_uncached(key.to_owned(), config_span, full_config_path)?;
90
1
                let config = &*Box::leak(Box::new(config));
91
1
                Ok((config._key, config))
92
1
            },
93
            |config| *config,
94
        )
95
17.8k
    }
96
97
1
    fn new_uncached(
98
1
        key: OwnedConfigKey,
99
1
        config_span: Option<Span>,
100
1
        full_config_path: Option<PathBuf>,
101
1
    ) -> Result<Config, CompileError> {
102
1
        let s = key.0.source.as_ref();
103
1
        let config_path = key.0.config_path.as_deref();
104
1
        let root = key.0.root.as_ref();
105
106
1
        let default_dirs = vec![root.join("templates")];
107
108
1
        let mut syntaxes = HashMap::default();
109
1
        syntaxes.insert(DEFAULT_SYNTAX_NAME.to_string(), SyntaxAndCache::default());
110
111
1
        let raw = if s.is_empty() {
112
1
            RawConfig::default()
113
        } else {
114
0
            RawConfig::from_toml_str(s)?
115
        };
116
117
1
        let (dirs, default_syntax, whitespace) = match raw.general {
118
            Some(General {
119
0
                dirs,
120
0
                default_syntax,
121
0
                whitespace,
122
            }) => (
123
0
                dirs.map_or(default_dirs, |v| {
124
0
                    v.into_iter()
125
0
                        .flat_map(|dir| get_config_dirs(root, dir))
126
0
                        .collect()
127
0
                }),
128
0
                default_syntax.unwrap_or(DEFAULT_SYNTAX_NAME),
129
0
                whitespace,
130
            ),
131
1
            None => (default_dirs, DEFAULT_SYNTAX_NAME, Whitespace::default()),
132
        };
133
1
        let file_info = config_path.map(|path| FileInfo::new(Path::new(path), None, None));
134
1
        let whitespace = key.0.template_whitespace.unwrap_or(whitespace);
135
136
1
        if let Some(raw_syntaxes) = raw.syntax {
137
0
            for raw_s in raw_syntaxes {
138
0
                let name = raw_s.name;
139
0
                match syntaxes.entry(name.to_string()) {
140
0
                    Entry::Vacant(entry) => {
141
0
                        entry.insert(raw_s.to_syntax().map(SyntaxAndCache::new).map_err(
142
0
                            |err| CompileError::new_with_span_stable(err, file_info, config_span),
143
0
                        )?);
144
                    }
145
                    Entry::Occupied(_) => {
146
0
                        return Err(CompileError::new(
147
0
                            format_args!("syntax {name:?} is already defined"),
148
0
                            file_info,
149
0
                        ));
150
                    }
151
                }
152
            }
153
1
        }
154
155
1
        if !syntaxes.contains_key(default_syntax) {
156
0
            return Err(CompileError::new(
157
0
                format_args!("default syntax \"{default_syntax}\" not found"),
158
0
                file_info,
159
0
            ));
160
1
        }
161
162
1
        let mut escapers = Vec::new();
163
1
        if let Some(configured) = raw.escaper {
164
0
            for escaper in configured {
165
0
                escapers.push((str_set(&escaper.extensions), escaper.path.into()));
166
0
            }
167
1
        }
168
3
        for (extensions, name) in DEFAULT_ESCAPERS {
169
2
            escapers.push((
170
2
                str_set(extensions),
171
2
                format!("askama::filters::{name}").into(),
172
2
            ));
173
2
        }
174
175
1
        Ok(Config {
176
1
            dirs,
177
1
            syntaxes,
178
1
            default_syntax,
179
1
            escapers,
180
1
            whitespace,
181
1
            full_config_path,
182
1
            _key: key,
183
1
        })
184
1
    }
185
186
0
    pub(crate) fn find_template(
187
0
        &self,
188
0
        path: &str,
189
0
        start_at: Option<&Path>,
190
0
        file_info: Option<FileInfo<'_>>,
191
0
        span: Option<proc_macro2::Span>,
192
0
    ) -> Result<Arc<Path>, CompileError> {
193
0
        let path = 'find_path: {
194
0
            if let Some(root) = start_at {
195
0
                let relative = root.with_file_name(path);
196
0
                if relative.exists() {
197
0
                    break 'find_path relative;
198
0
                }
199
0
            }
200
0
            for dir in &self.dirs {
201
0
                let rooted = dir.join(path);
202
0
                if rooted.exists() {
203
0
                    break 'find_path rooted;
204
0
                }
205
            }
206
0
            return Err(CompileError::new_with_span(
207
0
                format_args!(
208
0
                    "template {:?} not found in directories {:?}",
209
0
                    path, self.dirs,
210
0
                ),
211
0
                file_info,
212
0
                span,
213
0
            ));
214
        };
215
0
        match absolute(&path) {
216
0
            Ok(path) => Ok(path.into()),
217
0
            Err(err) => Err(CompileError::new_with_span(
218
0
                format_args!("could not get absolute path for {path:?}: {err}"),
219
0
                file_info,
220
0
                span,
221
0
            )),
222
        }
223
0
    }
224
}
225
226
#[cfg(not(feature = "config"))]
227
0
fn get_config_dirs(_root: &Path, _dir: &str) -> impl Iterator<Item = PathBuf> {
228
0
    std::iter::empty()
229
0
}
230
231
#[cfg(feature = "config")]
232
fn get_config_dirs(root: &Path, dir: &str) -> impl Iterator<Item = PathBuf> {
233
    let path = root.join(dir);
234
    if dir.contains('*')
235
        && let Some(path) = path.to_str()
236
        && let Ok(matches) = glob::glob(path)
237
    {
238
        PathOrPaths::Paths(matches)
239
    } else {
240
        PathOrPaths::Path(Some(path))
241
    }
242
}
243
244
#[cfg(feature = "config")]
245
enum PathOrPaths {
246
    Paths(glob::Paths),
247
    Path(Option<PathBuf>),
248
}
249
250
#[cfg(feature = "config")]
251
impl Iterator for PathOrPaths {
252
    type Item = PathBuf;
253
254
    fn next(&mut self) -> Option<Self::Item> {
255
        match self {
256
            Self::Paths(paths) => loop {
257
                if let Ok(path) = paths.next()?
258
                    && path.is_dir()
259
                {
260
                    return Some(path);
261
                }
262
            },
263
            Self::Path(path) => path.take(),
264
        }
265
    }
266
}
267
268
#[derive(Debug, Default)]
269
pub(crate) struct SyntaxAndCache {
270
    syntax: Syntax<'static>,
271
    cache: OnceMap<OwnedSyntaxAndCacheKey, Arc<Parsed>>,
272
}
273
274
impl Deref for SyntaxAndCache {
275
    type Target = Syntax<'static>;
276
277
    #[inline]
278
    fn deref(&self) -> &Self::Target {
279
        &self.syntax
280
    }
281
}
282
283
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
284
struct OwnedSyntaxAndCacheKey(SyntaxAndCacheKey<'static>);
285
286
impl Deref for OwnedSyntaxAndCacheKey {
287
    type Target = SyntaxAndCacheKey<'static>;
288
289
    #[inline]
290
2.37k
    fn deref(&self) -> &Self::Target {
291
2.37k
        &self.0
292
2.37k
    }
293
}
294
295
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
296
struct SyntaxAndCacheKey<'a> {
297
    source: Cow<'a, Arc<str>>,
298
    source_path: Option<Cow<'a, Arc<Path>>>,
299
}
300
301
impl<'a> Borrow<SyntaxAndCacheKey<'a>> for OwnedSyntaxAndCacheKey {
302
17.1k
    fn borrow(&self) -> &SyntaxAndCacheKey<'a> {
303
17.1k
        &self.0
304
17.1k
    }
305
}
306
307
impl SyntaxAndCache {
308
0
    fn new(syntax: Syntax<'static>) -> Self {
309
0
        Self {
310
0
            syntax,
311
0
            cache: OnceMap::default(),
312
0
        }
313
0
    }
314
315
17.2k
    pub(crate) fn parse(
316
17.2k
        &self,
317
17.2k
        source: Arc<str>,
318
17.2k
        source_path: Option<Arc<Path>>,
319
17.2k
    ) -> Result<Arc<Parsed>, ParseError> {
320
17.2k
        self.cache.get_or_try_insert(
321
17.2k
            &SyntaxAndCacheKey {
322
17.2k
                source: Cow::Owned(source),
323
17.2k
                source_path: source_path.map(Cow::Owned),
324
17.2k
            },
325
1.18k
            |key| {
326
1.18k
                let key = OwnedSyntaxAndCacheKey(SyntaxAndCacheKey {
327
1.18k
                    source: Cow::Owned(Arc::clone(key.source.as_ref())),
328
1.18k
                    source_path: key
329
1.18k
                        .source_path
330
1.18k
                        .as_deref()
331
1.18k
                        .map(|v| Cow::Owned(Arc::clone(v))),
332
                });
333
1.18k
                let parsed = Parsed::new(
334
1.18k
                    Arc::clone(key.source.as_ref()),
335
1.18k
                    key.source_path.as_deref().map(Arc::clone),
336
1.18k
                    &self.syntax,
337
462
                )?;
338
726
                Ok((key, Arc::new(parsed)))
339
1.18k
            },
340
            Arc::clone,
341
        )
342
17.2k
    }
343
}
344
345
#[cfg_attr(feature = "config", derive(Deserialize))]
346
#[derive(Default)]
347
struct RawConfig<'a> {
348
    #[cfg_attr(feature = "config", serde(borrow))]
349
    general: Option<General<'a>>,
350
    syntax: Option<Vec<SyntaxBuilder<'a>>>,
351
    escaper: Option<Vec<RawEscaper<'a>>>,
352
}
353
354
impl RawConfig<'_> {
355
    #[cfg(feature = "config")]
356
    fn from_toml_str(s: &str) -> Result<RawConfig<'_>, CompileError> {
357
        toml::from_str(s).map_err(|e| {
358
            CompileError::no_file_info(format!("invalid TOML in {CONFIG_FILE_NAME}: {e}"), None)
359
        })
360
    }
361
362
    #[cfg(not(feature = "config"))]
363
0
    fn from_toml_str(_: &str) -> Result<RawConfig<'_>, CompileError> {
364
0
        Err(CompileError::no_file_info(
365
0
            "TOML support not available",
366
0
            None,
367
0
        ))
368
0
    }
369
}
370
371
#[cfg_attr(feature = "config", derive(Deserialize))]
372
struct General<'a> {
373
    #[cfg_attr(feature = "config", serde(borrow))]
374
    dirs: Option<Vec<&'a str>>,
375
    default_syntax: Option<&'a str>,
376
    #[cfg_attr(feature = "config", serde(default))]
377
    whitespace: Whitespace,
378
}
379
380
#[cfg_attr(feature = "config", derive(Deserialize))]
381
struct RawEscaper<'a> {
382
    path: &'a str,
383
    extensions: Vec<&'a str>,
384
}
385
386
17.2k
pub(crate) fn read_config_file(
387
17.2k
    config_path: Option<&str>,
388
17.2k
    span: Option<Span>,
389
17.2k
) -> Result<(String, Option<PathBuf>), CompileError> {
390
17.2k
    let root = manifest_root();
391
17.2k
    let filename = match config_path {
392
0
        Some(config_path) => root.join(config_path),
393
17.2k
        None => root.join(CONFIG_FILE_NAME),
394
    };
395
396
17.2k
    if filename.exists() {
397
0
        let content = fs::read_to_string(&filename).map_err(|err| {
398
0
            CompileError::no_file_info(
399
0
                format_args!("unable to read {}: {err}", filename.display()),
400
0
                span,
401
            )
402
0
        })?;
403
0
        Ok((content, filename.canonicalize().ok()))
404
17.2k
    } else if config_path.is_some() {
405
0
        Err(CompileError::no_file_info(
406
0
            format_args!("`{}` does not exist", filename.display()),
407
0
            span,
408
0
        ))
409
    } else {
410
17.2k
        Ok((String::new(), None))
411
    }
412
17.2k
}
413
414
35.1k
fn manifest_root() -> PathBuf {
415
35.1k
    env::var_os("CARGO_MANIFEST_DIR").map_or_else(|| PathBuf::from("."), PathBuf::from)
416
35.1k
}
417
418
2
fn str_set(vals: &[&'static str]) -> Vec<Cow<'static, str>> {
419
14
    vals.iter().map(|s| Cow::Borrowed(*s)).collect()
420
2
}
421
422
static CONFIG_FILE_NAME: &str = "askama.toml";
423
static DEFAULT_SYNTAX_NAME: &str = "default";
424
static DEFAULT_ESCAPERS: &[(&[&str], &str)] = &[
425
    (
426
        &[
427
            "askama", "html", "htm", "j2", "jinja", "jinja2", "rinja", "svg", "xml",
428
        ],
429
        "Html",
430
    ),
431
    (&["md", "none", "txt", "yml", ""], "Text"),
432
];
433
434
#[cfg(test)]
435
mod tests {
436
    use std::path::{Path, PathBuf, absolute};
437
438
    use super::*;
439
440
    #[test]
441
    fn test_default_config() {
442
        let mut root = manifest_root();
443
        root.push("templates");
444
        let config = Config::new("", None, None, None, None).unwrap();
445
        assert_eq!(config.dirs, vec![root]);
446
    }
447
448
    #[cfg(feature = "config")]
449
    #[test]
450
    fn test_config_dirs() {
451
        let mut root = manifest_root();
452
        root.push("tpl");
453
        let config = Config::new("[general]\ndirs = [\"tpl\"]", None, None, None, None).unwrap();
454
        assert_eq!(config.dirs, vec![root]);
455
    }
456
457
    #[cfg(feature = "config")]
458
    #[test]
459
    fn test_config_dirs_glob() {
460
        let root = manifest_root();
461
        let config = Config::new(
462
            "[general]\ndirs = [\"templates/*\"]",
463
            None,
464
            None,
465
            None,
466
            None,
467
        )
468
        .unwrap();
469
        // We ensure that it includes only top level folders.
470
        assert_eq!(config.dirs, vec![root.join("templates/sub")]);
471
472
        let config = Config::new(
473
            "[general]\ndirs = [\"templates/**\"]",
474
            None,
475
            None,
476
            None,
477
            None,
478
        )
479
        .unwrap();
480
        // We ensure that it includes top level and sub-folders.
481
        assert_eq!(
482
            config.dirs,
483
            vec![root.join("templates/sub"), root.join("templates/sub/sub1")]
484
        );
485
    }
486
487
    fn assert_eq_rooted(actual: &Path, expected: &str) {
488
        let mut root = absolute(manifest_root()).unwrap();
489
        root.push("templates");
490
        let mut inner = PathBuf::new();
491
        inner.push(expected);
492
        assert_eq!(actual.strip_prefix(root).unwrap(), inner);
493
    }
494
495
    #[test]
496
    fn find_absolute() {
497
        let config = Config::new("", None, None, None, None).unwrap();
498
        let root = config.find_template("a.html", None, None, None).unwrap();
499
        let path = config
500
            .find_template("sub/b.html", Some(&root), None, None)
501
            .unwrap();
502
        assert_eq_rooted(&path, "sub/b.html");
503
    }
504
505
    #[test]
506
    #[should_panic]
507
    fn find_relative_nonexistent() {
508
        let config = Config::new("", None, None, None, None).unwrap();
509
        let root = config.find_template("a.html", None, None, None).unwrap();
510
        config
511
            .find_template("c.html", Some(&root), None, None)
512
            .unwrap();
513
    }
514
515
    #[test]
516
    fn find_relative() {
517
        let config = Config::new("", None, None, None, None).unwrap();
518
        let root = config
519
            .find_template("sub/b.html", None, None, None)
520
            .unwrap();
521
        let path = config
522
            .find_template("c.html", Some(&root), None, None)
523
            .unwrap();
524
        assert_eq_rooted(&path, "sub/c.html");
525
    }
526
527
    #[test]
528
    fn find_relative_sub() {
529
        let config = Config::new("", None, None, None, None).unwrap();
530
        let root = config
531
            .find_template("sub/b.html", None, None, None)
532
            .unwrap();
533
        let path = config
534
            .find_template("sub1/d.html", Some(&root), None, None)
535
            .unwrap();
536
        assert_eq_rooted(&path, "sub/sub1/d.html");
537
    }
538
539
    #[cfg(feature = "config")]
540
    #[test]
541
    fn add_syntax() {
542
        let raw_config = r#"
543
        [general]
544
        default_syntax = "foo"
545
546
        [[syntax]]
547
        name = "foo"
548
        block_start = "{<"
549
550
        [[syntax]]
551
        name = "bar"
552
        expr_start = "{!"
553
        "#;
554
555
        let default_syntax = Syntax::default();
556
        let config = Config::new(raw_config, None, None, None, None).unwrap();
557
        assert_eq!(config.default_syntax, "foo");
558
559
        let foo = config.syntaxes.get("foo").unwrap();
560
        assert_eq!(foo.block_start, "{<");
561
        assert_eq!(foo.block_end, default_syntax.block_end);
562
        assert_eq!(foo.expr_start, default_syntax.expr_start);
563
        assert_eq!(foo.expr_end, default_syntax.expr_end);
564
        assert_eq!(foo.comment_start, default_syntax.comment_start);
565
        assert_eq!(foo.comment_end, default_syntax.comment_end);
566
567
        let bar = config.syntaxes.get("bar").unwrap();
568
        assert_eq!(bar.block_start, default_syntax.block_start);
569
        assert_eq!(bar.block_end, default_syntax.block_end);
570
        assert_eq!(bar.expr_start, "{!");
571
        assert_eq!(bar.expr_end, default_syntax.expr_end);
572
        assert_eq!(bar.comment_start, default_syntax.comment_start);
573
        assert_eq!(bar.comment_end, default_syntax.comment_end);
574
    }
575
576
    #[cfg(feature = "config")]
577
    #[test]
578
    fn add_syntax_two() {
579
        let raw_config = r#"
580
        syntax = [{ name = "foo", block_start = "{<" },
581
                  { name = "bar", expr_start = "{!" } ]
582
583
        [general]
584
        default_syntax = "foo"
585
        "#;
586
587
        let default_syntax = Syntax::default();
588
        let config = Config::new(raw_config, None, None, None, None).unwrap();
589
        assert_eq!(config.default_syntax, "foo");
590
591
        let foo = config.syntaxes.get("foo").unwrap();
592
        assert_eq!(foo.block_start, "{<");
593
        assert_eq!(foo.block_end, default_syntax.block_end);
594
        assert_eq!(foo.expr_start, default_syntax.expr_start);
595
        assert_eq!(foo.expr_end, default_syntax.expr_end);
596
        assert_eq!(foo.comment_start, default_syntax.comment_start);
597
        assert_eq!(foo.comment_end, default_syntax.comment_end);
598
599
        let bar = config.syntaxes.get("bar").unwrap();
600
        assert_eq!(bar.block_start, default_syntax.block_start);
601
        assert_eq!(bar.block_end, default_syntax.block_end);
602
        assert_eq!(bar.expr_start, "{!");
603
        assert_eq!(bar.expr_end, default_syntax.expr_end);
604
        assert_eq!(bar.comment_start, default_syntax.comment_start);
605
        assert_eq!(bar.comment_end, default_syntax.comment_end);
606
    }
607
608
    #[cfg(feature = "config")]
609
    #[test]
610
    fn longer_delimiters() {
611
        let raw_config = r#"
612
        [[syntax]]
613
        name = "emoji"
614
        block_start = "👉🙂👉"
615
        block_end = "👈🙃👈"
616
        expr_start = "🤜🤜"
617
        expr_end = "🤛🤛"
618
        comment_start = "👎_(ツ)_👎"
619
        comment_end = "👍:D👍"
620
621
        [general]
622
        default_syntax = "emoji"
623
        "#;
624
625
        let config = Config::new(raw_config, None, None, None, None).unwrap();
626
        assert_eq!(config.default_syntax, "emoji");
627
628
        let foo = config.syntaxes.get("emoji").unwrap();
629
        assert_eq!(foo.block_start, "👉🙂👉");
630
        assert_eq!(foo.block_end, "👈🙃👈");
631
        assert_eq!(foo.expr_start, "🤜🤜");
632
        assert_eq!(foo.expr_end, "🤛🤛");
633
        assert_eq!(foo.comment_start, "👎_(ツ)_👎");
634
        assert_eq!(foo.comment_end, "👍:D👍");
635
    }
636
637
    #[cfg(feature = "config")]
638
    #[test]
639
    fn illegal_delimiters() {
640
        #[track_caller]
641
        fn expect_err<T, E>(result: Result<T, E>) -> E {
642
            match result {
643
                Ok(_) => panic!("should have failed"),
644
                Err(err) => err,
645
            }
646
        }
647
648
        let raw_config = r#"
649
        [[syntax]]
650
        name = "too_short"
651
        block_start = "<"
652
        "#;
653
        let config = Config::new(raw_config, None, None, None, None);
654
        assert_eq!(
655
            expect_err(config).msg,
656
            r#"delimiters must be at least two characters long. The opening block delimiter ("<") is too short"#,
657
        );
658
659
        let raw_config = r#"
660
        [[syntax]]
661
        name = "contains_ws"
662
        block_start = " {{ "
663
        "#;
664
        let config = Config::new(raw_config, None, None, None, None);
665
        assert_eq!(
666
            expect_err(config).msg,
667
            r#"delimiters may not contain white spaces. The opening block delimiter (" {{ ") contains white spaces"#,
668
        );
669
670
        let raw_config = r#"
671
        [[syntax]]
672
        name = "is_prefix"
673
        block_start = "{{"
674
        expr_start = "{{$"
675
        comment_start = "{{#"
676
        "#;
677
        let config = Config::new(raw_config, None, None, None, None);
678
        assert_eq!(
679
            expect_err(config).msg,
680
            r#"an opening delimiter may not be the prefix of another delimiter. The block delimiter ("{{") clashes with the expression delimiter ("{{$")"#,
681
        );
682
    }
683
684
    #[cfg(feature = "config")]
685
    #[should_panic]
686
    #[test]
687
    fn use_default_at_syntax_name() {
688
        let raw_config = r#"
689
        syntax = [{ name = "default" }]
690
        "#;
691
692
        let _config = Config::new(raw_config, None, None, None, None).unwrap();
693
    }
694
695
    #[cfg(feature = "config")]
696
    #[should_panic]
697
    #[test]
698
    fn duplicated_syntax_name_on_list() {
699
        let raw_config = r#"
700
        syntax = [{ name = "foo", block_start = "~<" },
701
                  { name = "foo", block_start = "%%" } ]
702
        "#;
703
704
        let _config = Config::new(raw_config, None, None, None, None).unwrap();
705
    }
706
707
    #[cfg(feature = "config")]
708
    #[should_panic]
709
    #[test]
710
    fn is_not_exist_default_syntax() {
711
        let raw_config = r#"
712
        [general]
713
        default_syntax = "foo"
714
        "#;
715
716
        let _config = Config::new(raw_config, None, None, None, None).unwrap();
717
    }
718
719
    #[cfg(feature = "config")]
720
    #[test]
721
    fn escape_modes() {
722
        let config = Config::new(
723
            r#"
724
            [[escaper]]
725
            path = "::my_filters::Js"
726
            extensions = ["js"]
727
        "#,
728
            None,
729
            None,
730
            None,
731
            None,
732
        )
733
        .unwrap();
734
        assert_eq!(
735
            config.escapers,
736
            vec![
737
                (str_set(&["js"]), "::my_filters::Js".into()),
738
                (
739
                    str_set(&[
740
                        "askama", "html", "htm", "j2", "jinja", "jinja2", "rinja", "svg", "xml"
741
                    ]),
742
                    "askama::filters::Html".into()
743
                ),
744
                (
745
                    str_set(&["md", "none", "txt", "yml", ""]),
746
                    "askama::filters::Text".into()
747
                ),
748
            ]
749
        );
750
    }
751
752
    #[cfg(feature = "config")]
753
    #[test]
754
    fn test_whitespace_parsing() {
755
        let config = Config::new(
756
            r#"
757
            [general]
758
            whitespace = "suppress"
759
            "#,
760
            None,
761
            None,
762
            None,
763
            None,
764
        )
765
        .unwrap();
766
        assert_eq!(config.whitespace, Whitespace::Suppress);
767
768
        let config = Config::new(r#""#, None, None, None, None).unwrap();
769
        assert_eq!(config.whitespace, Whitespace::Preserve);
770
771
        let config = Config::new(
772
            r#"
773
            [general]
774
            whitespace = "preserve"
775
            "#,
776
            None,
777
            None,
778
            None,
779
            None,
780
        )
781
        .unwrap();
782
        assert_eq!(config.whitespace, Whitespace::Preserve);
783
784
        let config = Config::new(
785
            r#"
786
            [general]
787
            whitespace = "minimize"
788
            "#,
789
            None,
790
            None,
791
            None,
792
            None,
793
        )
794
        .unwrap();
795
        assert_eq!(config.whitespace, Whitespace::Minimize);
796
    }
797
798
    #[cfg(feature = "config")]
799
    #[test]
800
    fn test_whitespace_in_template() {
801
        // Checking that template arguments have precedence over general configuration.
802
        // So in here, in the template arguments, there is `whitespace = "minimize"` so
803
        // the `Whitespace` should be `Minimize` as well.
804
        let config = Config::new(
805
            r#"
806
            [general]
807
            whitespace = "suppress"
808
            "#,
809
            None,
810
            Some(Whitespace::Minimize),
811
            None,
812
            None,
813
        )
814
        .unwrap();
815
        assert_eq!(config.whitespace, Whitespace::Minimize);
816
817
        let config = Config::new(r#""#, None, Some(Whitespace::Minimize), None, None).unwrap();
818
        assert_eq!(config.whitespace, Whitespace::Minimize);
819
    }
820
}