From e30cad33fd28c0d2546fbd70afa6834bea195f9e Mon Sep 17 00:00:00 2001
From: René Kijewski <kijewski@library.vetmed.fu-berlin.de>
Date: Wed, 18 May 2022 17:13:52 +0200
Subject: Move configuration into its own module

---
 askama_shared/src/config.rs    | 536 +++++++++++++++++++++++++++++++++++++++++
 askama_shared/src/generator.rs |   5 +-
 askama_shared/src/heritage.rs  |   3 +-
 askama_shared/src/input.rs     |   3 +-
 askama_shared/src/lib.rs       | 532 +---------------------------------------
 askama_shared/src/parser.rs    |   5 +-
 6 files changed, 551 insertions(+), 533 deletions(-)
 create mode 100644 askama_shared/src/config.rs

(limited to 'askama_shared/src')

diff --git a/askama_shared/src/config.rs b/askama_shared/src/config.rs
new file mode 100644
index 0000000..01f81a2
--- /dev/null
+++ b/askama_shared/src/config.rs
@@ -0,0 +1,536 @@
+use std::collections::{BTreeMap, HashSet};
+use std::convert::TryFrom;
+use std::path::{Path, PathBuf};
+use std::{env, fs};
+
+#[cfg(feature = "serde")]
+use serde::Deserialize;
+
+use crate::CompileError;
+
+#[derive(Debug)]
+pub(crate) struct Config<'a> {
+    pub(crate) dirs: Vec<PathBuf>,
+    pub(crate) syntaxes: BTreeMap<String, Syntax<'a>>,
+    pub(crate) default_syntax: &'a str,
+    pub(crate) escapers: Vec<(HashSet<String>, String)>,
+    pub(crate) whitespace: WhitespaceHandling,
+}
+
+impl Config<'_> {
+    pub(crate) fn new(s: &str) -> std::result::Result<Config<'_>, CompileError> {
+        let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+        let default_dirs = vec![root.join("templates")];
+
+        let mut syntaxes = BTreeMap::new();
+        syntaxes.insert(DEFAULT_SYNTAX_NAME.to_string(), Syntax::default());
+
+        let raw = if s.is_empty() {
+            RawConfig::default()
+        } else {
+            RawConfig::from_toml_str(s)?
+        };
+
+        let (dirs, default_syntax, whitespace) = match raw.general {
+            Some(General {
+                dirs,
+                default_syntax,
+                whitespace,
+            }) => (
+                dirs.map_or(default_dirs, |v| {
+                    v.into_iter().map(|dir| root.join(dir)).collect()
+                }),
+                default_syntax.unwrap_or(DEFAULT_SYNTAX_NAME),
+                whitespace,
+            ),
+            None => (
+                default_dirs,
+                DEFAULT_SYNTAX_NAME,
+                WhitespaceHandling::default(),
+            ),
+        };
+
+        if let Some(raw_syntaxes) = raw.syntax {
+            for raw_s in raw_syntaxes {
+                let name = raw_s.name;
+
+                if syntaxes
+                    .insert(name.to_string(), Syntax::try_from(raw_s)?)
+                    .is_some()
+                {
+                    return Err(format!("syntax \"{}\" is already defined", name).into());
+                }
+            }
+        }
+
+        if !syntaxes.contains_key(default_syntax) {
+            return Err(format!("default syntax \"{}\" not found", default_syntax).into());
+        }
+
+        let mut escapers = Vec::new();
+        if let Some(configured) = raw.escaper {
+            for escaper in configured {
+                escapers.push((
+                    escaper
+                        .extensions
+                        .iter()
+                        .map(|ext| (*ext).to_string())
+                        .collect(),
+                    escaper.path.to_string(),
+                ));
+            }
+        }
+        for (extensions, path) in DEFAULT_ESCAPERS {
+            escapers.push((str_set(extensions), (*path).to_string()));
+        }
+
+        Ok(Config {
+            dirs,
+            syntaxes,
+            default_syntax,
+            escapers,
+            whitespace,
+        })
+    }
+
+    pub(crate) fn find_template(
+        &self,
+        path: &str,
+        start_at: Option<&Path>,
+    ) -> std::result::Result<PathBuf, CompileError> {
+        if let Some(root) = start_at {
+            let relative = root.with_file_name(path);
+            if relative.exists() {
+                return Ok(relative);
+            }
+        }
+
+        for dir in &self.dirs {
+            let rooted = dir.join(path);
+            if rooted.exists() {
+                return Ok(rooted);
+            }
+        }
+
+        Err(format!(
+            "template {:?} not found in directories {:?}",
+            path, self.dirs
+        )
+        .into())
+    }
+}
+
+#[derive(Debug)]
+pub(crate) struct Syntax<'a> {
+    pub(crate) block_start: &'a str,
+    pub(crate) block_end: &'a str,
+    pub(crate) expr_start: &'a str,
+    pub(crate) expr_end: &'a str,
+    pub(crate) comment_start: &'a str,
+    pub(crate) comment_end: &'a str,
+}
+
+impl Default for Syntax<'_> {
+    fn default() -> Self {
+        Self {
+            block_start: "{%",
+            block_end: "%}",
+            expr_start: "{{",
+            expr_end: "}}",
+            comment_start: "{#",
+            comment_end: "#}",
+        }
+    }
+}
+
+impl<'a> TryFrom<RawSyntax<'a>> for Syntax<'a> {
+    type Error = CompileError;
+
+    fn try_from(raw: RawSyntax<'a>) -> std::result::Result<Self, Self::Error> {
+        let default = Self::default();
+        let syntax = Self {
+            block_start: raw.block_start.unwrap_or(default.block_start),
+            block_end: raw.block_end.unwrap_or(default.block_end),
+            expr_start: raw.expr_start.unwrap_or(default.expr_start),
+            expr_end: raw.expr_end.unwrap_or(default.expr_end),
+            comment_start: raw.comment_start.unwrap_or(default.comment_start),
+            comment_end: raw.comment_end.unwrap_or(default.comment_end),
+        };
+
+        if syntax.block_start.len() != 2
+            || syntax.block_end.len() != 2
+            || syntax.expr_start.len() != 2
+            || syntax.expr_end.len() != 2
+            || syntax.comment_start.len() != 2
+            || syntax.comment_end.len() != 2
+        {
+            return Err("length of delimiters must be two".into());
+        }
+
+        let bs = syntax.block_start.as_bytes()[0];
+        let be = syntax.block_start.as_bytes()[1];
+        let cs = syntax.comment_start.as_bytes()[0];
+        let ce = syntax.comment_start.as_bytes()[1];
+        let es = syntax.expr_start.as_bytes()[0];
+        let ee = syntax.expr_start.as_bytes()[1];
+        if !((bs == cs && bs == es) || (be == ce && be == ee)) {
+            return Err(format!("bad delimiters block_start: {}, comment_start: {}, expr_start: {}, needs one of the two characters in common", syntax.block_start, syntax.comment_start, syntax.expr_start).into());
+        }
+
+        Ok(syntax)
+    }
+}
+
+#[cfg_attr(feature = "serde", derive(Deserialize))]
+#[derive(Default)]
+struct RawConfig<'d> {
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    general: Option<General<'d>>,
+    syntax: Option<Vec<RawSyntax<'d>>>,
+    escaper: Option<Vec<RawEscaper<'d>>>,
+}
+
+impl RawConfig<'_> {
+    #[cfg(feature = "config")]
+    fn from_toml_str(s: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
+        toml::from_str(s).map_err(|e| format!("invalid TOML in {}: {}", CONFIG_FILE_NAME, e).into())
+    }
+
+    #[cfg(not(feature = "config"))]
+    fn from_toml_str(_: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
+        Err("TOML support not available".into())
+    }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+#[cfg_attr(feature = "serde", derive(Deserialize))]
+#[cfg_attr(feature = "serde", serde(field_identifier, rename_all = "lowercase"))]
+pub(crate) enum WhitespaceHandling {
+    /// The default behaviour. It will leave the whitespace characters "as is".
+    Preserve,
+    /// It'll remove all the whitespace characters before and after the jinja block.
+    Suppress,
+    /// It'll remove all the whitespace characters except one before and after the jinja blocks.
+    /// If there is a newline character, the preserved character in the trimmed characters, it will
+    /// the one preserved.
+    Minimize,
+}
+
+impl Default for WhitespaceHandling {
+    fn default() -> Self {
+        WhitespaceHandling::Preserve
+    }
+}
+
+#[cfg_attr(feature = "serde", derive(Deserialize))]
+struct General<'a> {
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    dirs: Option<Vec<&'a str>>,
+    default_syntax: Option<&'a str>,
+    #[cfg_attr(feature = "serde", serde(default))]
+    whitespace: WhitespaceHandling,
+}
+
+#[cfg_attr(feature = "serde", derive(Deserialize))]
+struct RawSyntax<'a> {
+    name: &'a str,
+    block_start: Option<&'a str>,
+    block_end: Option<&'a str>,
+    expr_start: Option<&'a str>,
+    expr_end: Option<&'a str>,
+    comment_start: Option<&'a str>,
+    comment_end: Option<&'a str>,
+}
+
+#[cfg_attr(feature = "serde", derive(Deserialize))]
+struct RawEscaper<'a> {
+    path: &'a str,
+    extensions: Vec<&'a str>,
+}
+
+pub(crate) fn read_config_file(
+    config_path: &Option<String>,
+) -> std::result::Result<String, CompileError> {
+    let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+    let filename = match config_path {
+        Some(config_path) => root.join(config_path),
+        None => root.join(CONFIG_FILE_NAME),
+    };
+
+    if filename.exists() {
+        fs::read_to_string(&filename)
+            .map_err(|_| format!("unable to read {:?}", filename.to_str().unwrap()).into())
+    } else if config_path.is_some() {
+        Err(format!("`{}` does not exist", root.display()).into())
+    } else {
+        Ok("".to_string())
+    }
+}
+
+fn str_set<T>(vals: &[T]) -> HashSet<String>
+where
+    T: ToString,
+{
+    vals.iter().map(|s| s.to_string()).collect()
+}
+
+#[allow(clippy::match_wild_err_arm)]
+pub(crate) fn get_template_source(tpl_path: &Path) -> std::result::Result<String, CompileError> {
+    match fs::read_to_string(tpl_path) {
+        Err(_) => Err(format!(
+            "unable to open template file '{}'",
+            tpl_path.to_str().unwrap()
+        )
+        .into()),
+        Ok(mut source) => {
+            if source.ends_with('\n') {
+                let _ = source.pop();
+            }
+            Ok(source)
+        }
+    }
+}
+
+static CONFIG_FILE_NAME: &str = "askama.toml";
+static DEFAULT_SYNTAX_NAME: &str = "default";
+static DEFAULT_ESCAPERS: &[(&[&str], &str)] = &[
+    (&["html", "htm", "xml"], "::askama::Html"),
+    (&["md", "none", "txt", "yml", ""], "::askama::Text"),
+    (&["j2", "jinja", "jinja2"], "::askama::Html"),
+];
+
+#[cfg(test)]
+#[allow(clippy::blacklisted_name)]
+mod tests {
+    use std::env;
+    use std::path::{Path, PathBuf};
+
+    use super::*;
+
+    #[test]
+    fn get_source() {
+        let path = Config::new("")
+            .and_then(|config| config.find_template("b.html", None))
+            .unwrap();
+        assert_eq!(get_template_source(&path).unwrap(), "bar");
+    }
+
+    #[test]
+    fn test_default_config() {
+        let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+        root.push("templates");
+        let config = Config::new("").unwrap();
+        assert_eq!(config.dirs, vec![root]);
+    }
+
+    #[cfg(feature = "config")]
+    #[test]
+    fn test_config_dirs() {
+        let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+        root.push("tpl");
+        let config = Config::new("[general]\ndirs = [\"tpl\"]").unwrap();
+        assert_eq!(config.dirs, vec![root]);
+    }
+
+    fn assert_eq_rooted(actual: &Path, expected: &str) {
+        let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+        root.push("templates");
+        let mut inner = PathBuf::new();
+        inner.push(expected);
+        assert_eq!(actual.strip_prefix(root).unwrap(), inner);
+    }
+
+    #[test]
+    fn find_absolute() {
+        let config = Config::new("").unwrap();
+        let root = config.find_template("a.html", None).unwrap();
+        let path = config.find_template("sub/b.html", Some(&root)).unwrap();
+        assert_eq_rooted(&path, "sub/b.html");
+    }
+
+    #[test]
+    #[should_panic]
+    fn find_relative_nonexistent() {
+        let config = Config::new("").unwrap();
+        let root = config.find_template("a.html", None).unwrap();
+        config.find_template("c.html", Some(&root)).unwrap();
+    }
+
+    #[test]
+    fn find_relative() {
+        let config = Config::new("").unwrap();
+        let root = config.find_template("sub/b.html", None).unwrap();
+        let path = config.find_template("c.html", Some(&root)).unwrap();
+        assert_eq_rooted(&path, "sub/c.html");
+    }
+
+    #[test]
+    fn find_relative_sub() {
+        let config = Config::new("").unwrap();
+        let root = config.find_template("sub/b.html", None).unwrap();
+        let path = config.find_template("sub1/d.html", Some(&root)).unwrap();
+        assert_eq_rooted(&path, "sub/sub1/d.html");
+    }
+
+    #[cfg(feature = "config")]
+    #[test]
+    fn add_syntax() {
+        let raw_config = r#"
+        [general]
+        default_syntax = "foo"
+
+        [[syntax]]
+        name = "foo"
+        block_start = "{<"
+
+        [[syntax]]
+        name = "bar"
+        expr_start = "{!"
+        "#;
+
+        let default_syntax = Syntax::default();
+        let config = Config::new(raw_config).unwrap();
+        assert_eq!(config.default_syntax, "foo");
+
+        let foo = config.syntaxes.get("foo").unwrap();
+        assert_eq!(foo.block_start, "{<");
+        assert_eq!(foo.block_end, default_syntax.block_end);
+        assert_eq!(foo.expr_start, default_syntax.expr_start);
+        assert_eq!(foo.expr_end, default_syntax.expr_end);
+        assert_eq!(foo.comment_start, default_syntax.comment_start);
+        assert_eq!(foo.comment_end, default_syntax.comment_end);
+
+        let bar = config.syntaxes.get("bar").unwrap();
+        assert_eq!(bar.block_start, default_syntax.block_start);
+        assert_eq!(bar.block_end, default_syntax.block_end);
+        assert_eq!(bar.expr_start, "{!");
+        assert_eq!(bar.expr_end, default_syntax.expr_end);
+        assert_eq!(bar.comment_start, default_syntax.comment_start);
+        assert_eq!(bar.comment_end, default_syntax.comment_end);
+    }
+
+    #[cfg(feature = "config")]
+    #[test]
+    fn add_syntax_two() {
+        let raw_config = r#"
+        syntax = [{ name = "foo", block_start = "{<" },
+                  { name = "bar", expr_start = "{!" } ]
+
+        [general]
+        default_syntax = "foo"
+        "#;
+
+        let default_syntax = Syntax::default();
+        let config = Config::new(raw_config).unwrap();
+        assert_eq!(config.default_syntax, "foo");
+
+        let foo = config.syntaxes.get("foo").unwrap();
+        assert_eq!(foo.block_start, "{<");
+        assert_eq!(foo.block_end, default_syntax.block_end);
+        assert_eq!(foo.expr_start, default_syntax.expr_start);
+        assert_eq!(foo.expr_end, default_syntax.expr_end);
+        assert_eq!(foo.comment_start, default_syntax.comment_start);
+        assert_eq!(foo.comment_end, default_syntax.comment_end);
+
+        let bar = config.syntaxes.get("bar").unwrap();
+        assert_eq!(bar.block_start, default_syntax.block_start);
+        assert_eq!(bar.block_end, default_syntax.block_end);
+        assert_eq!(bar.expr_start, "{!");
+        assert_eq!(bar.expr_end, default_syntax.expr_end);
+        assert_eq!(bar.comment_start, default_syntax.comment_start);
+        assert_eq!(bar.comment_end, default_syntax.comment_end);
+    }
+
+    #[cfg(feature = "toml")]
+    #[should_panic]
+    #[test]
+    fn use_default_at_syntax_name() {
+        let raw_config = r#"
+        syntax = [{ name = "default" }]
+        "#;
+
+        let _config = Config::new(raw_config).unwrap();
+    }
+
+    #[cfg(feature = "toml")]
+    #[should_panic]
+    #[test]
+    fn duplicated_syntax_name_on_list() {
+        let raw_config = r#"
+        syntax = [{ name = "foo", block_start = "~<" },
+                  { name = "foo", block_start = "%%" } ]
+        "#;
+
+        let _config = Config::new(raw_config).unwrap();
+    }
+
+    #[cfg(feature = "toml")]
+    #[should_panic]
+    #[test]
+    fn is_not_exist_default_syntax() {
+        let raw_config = r#"
+        [general]
+        default_syntax = "foo"
+        "#;
+
+        let _config = Config::new(raw_config).unwrap();
+    }
+
+    #[cfg(feature = "config")]
+    #[test]
+    fn escape_modes() {
+        let config = Config::new(
+            r#"
+            [[escaper]]
+            path = "::askama::Js"
+            extensions = ["js"]
+        "#,
+        )
+        .unwrap();
+        assert_eq!(
+            config.escapers,
+            vec![
+                (str_set(&["js"]), "::askama::Js".into()),
+                (str_set(&["html", "htm", "xml"]), "::askama::Html".into()),
+                (
+                    str_set(&["md", "none", "txt", "yml", ""]),
+                    "::askama::Text".into()
+                ),
+                (str_set(&["j2", "jinja", "jinja2"]), "::askama::Html".into()),
+            ]
+        );
+    }
+
+    #[test]
+    fn test_whitespace_parsing() {
+        let config = Config::new(
+            r#"
+            [general]
+            whitespace = "suppress"
+            "#,
+        )
+        .unwrap();
+        assert_eq!(config.whitespace, WhitespaceHandling::Suppress);
+
+        let config = Config::new(r#""#).unwrap();
+        assert_eq!(config.whitespace, WhitespaceHandling::Preserve);
+
+        let config = Config::new(
+            r#"
+            [general]
+            whitespace = "preserve"
+            "#,
+        )
+        .unwrap();
+        assert_eq!(config.whitespace, WhitespaceHandling::Preserve);
+
+        let config = Config::new(
+            r#"
+            [general]
+            whitespace = "minimize"
+            "#,
+        )
+        .unwrap();
+        assert_eq!(config.whitespace, WhitespaceHandling::Minimize);
+    }
+}
diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs
index 9174a77..ea95bd3 100644
--- a/askama_shared/src/generator.rs
+++ b/askama_shared/src/generator.rs
@@ -1,9 +1,8 @@
+use crate::config::{get_template_source, read_config_file, Config, WhitespaceHandling};
 use crate::heritage::{Context, Heritage};
 use crate::input::{Print, Source, TemplateInput};
 use crate::parser::{parse, Cond, CondTest, Expr, Loop, Node, Target, When, Whitespace, Ws};
-use crate::{
-    filters, get_template_source, read_config_file, CompileError, Config, WhitespaceHandling,
-};
+use crate::{filters, CompileError};
 
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
diff --git a/askama_shared/src/heritage.rs b/askama_shared/src/heritage.rs
index 49599af..52c14a2 100644
--- a/askama_shared/src/heritage.rs
+++ b/askama_shared/src/heritage.rs
@@ -1,8 +1,9 @@
 use std::collections::HashMap;
 use std::path::{Path, PathBuf};
 
+use crate::config::Config;
 use crate::parser::{Expr, Loop, Macro, Node};
-use crate::{CompileError, Config};
+use crate::CompileError;
 
 pub(crate) struct Heritage<'a> {
     pub(crate) root: &'a Context<'a>,
diff --git a/askama_shared/src/input.rs b/askama_shared/src/input.rs
index 350fc01..1f367fd 100644
--- a/askama_shared/src/input.rs
+++ b/askama_shared/src/input.rs
@@ -1,5 +1,6 @@
+use crate::config::{Config, Syntax};
 use crate::generator::TemplateArgs;
-use crate::{CompileError, Config, Syntax};
+use crate::CompileError;
 
 use std::path::{Path, PathBuf};
 use std::str::FromStr;
diff --git a/askama_shared/src/lib.rs b/askama_shared/src/lib.rs
index a94c509..a3ee4c1 100644
--- a/askama_shared/src/lib.rs
+++ b/askama_shared/src/lib.rs
@@ -4,19 +4,14 @@
 #![deny(unreachable_pub)]
 
 use std::borrow::Cow;
-use std::collections::{BTreeMap, HashSet};
-use std::convert::TryFrom;
-use std::path::{Path, PathBuf};
-use std::{env, fmt, fs};
-
-use proc_macro2::{Span, TokenStream};
-#[cfg(feature = "serde")]
-use serde::Deserialize;
+use std::fmt;
 
 pub use crate::generator::derive_template;
 pub use crate::input::extension_to_mime_type;
 pub use askama_escape::MarkupDisplay;
+use proc_macro2::{Span, TokenStream};
 
+mod config;
 mod error;
 pub use crate::error::{Error, Result};
 pub mod filters;
@@ -112,295 +107,6 @@ impl fmt::Display for dyn DynTemplate {
     }
 }
 
-#[derive(Debug)]
-struct Config<'a> {
-    dirs: Vec<PathBuf>,
-    syntaxes: BTreeMap<String, Syntax<'a>>,
-    default_syntax: &'a str,
-    escapers: Vec<(HashSet<String>, String)>,
-    whitespace: WhitespaceHandling,
-}
-
-impl Config<'_> {
-    fn new(s: &str) -> std::result::Result<Config<'_>, CompileError> {
-        let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
-        let default_dirs = vec![root.join("templates")];
-
-        let mut syntaxes = BTreeMap::new();
-        syntaxes.insert(DEFAULT_SYNTAX_NAME.to_string(), Syntax::default());
-
-        let raw = if s.is_empty() {
-            RawConfig::default()
-        } else {
-            RawConfig::from_toml_str(s)?
-        };
-
-        let (dirs, default_syntax, whitespace) = match raw.general {
-            Some(General {
-                dirs,
-                default_syntax,
-                whitespace,
-            }) => (
-                dirs.map_or(default_dirs, |v| {
-                    v.into_iter().map(|dir| root.join(dir)).collect()
-                }),
-                default_syntax.unwrap_or(DEFAULT_SYNTAX_NAME),
-                whitespace,
-            ),
-            None => (
-                default_dirs,
-                DEFAULT_SYNTAX_NAME,
-                WhitespaceHandling::default(),
-            ),
-        };
-
-        if let Some(raw_syntaxes) = raw.syntax {
-            for raw_s in raw_syntaxes {
-                let name = raw_s.name;
-
-                if syntaxes
-                    .insert(name.to_string(), Syntax::try_from(raw_s)?)
-                    .is_some()
-                {
-                    return Err(format!("syntax \"{}\" is already defined", name).into());
-                }
-            }
-        }
-
-        if !syntaxes.contains_key(default_syntax) {
-            return Err(format!("default syntax \"{}\" not found", default_syntax).into());
-        }
-
-        let mut escapers = Vec::new();
-        if let Some(configured) = raw.escaper {
-            for escaper in configured {
-                escapers.push((
-                    escaper
-                        .extensions
-                        .iter()
-                        .map(|ext| (*ext).to_string())
-                        .collect(),
-                    escaper.path.to_string(),
-                ));
-            }
-        }
-        for (extensions, path) in DEFAULT_ESCAPERS {
-            escapers.push((str_set(extensions), (*path).to_string()));
-        }
-
-        Ok(Config {
-            dirs,
-            syntaxes,
-            default_syntax,
-            escapers,
-            whitespace,
-        })
-    }
-
-    fn find_template(
-        &self,
-        path: &str,
-        start_at: Option<&Path>,
-    ) -> std::result::Result<PathBuf, CompileError> {
-        if let Some(root) = start_at {
-            let relative = root.with_file_name(path);
-            if relative.exists() {
-                return Ok(relative);
-            }
-        }
-
-        for dir in &self.dirs {
-            let rooted = dir.join(path);
-            if rooted.exists() {
-                return Ok(rooted);
-            }
-        }
-
-        Err(format!(
-            "template {:?} not found in directories {:?}",
-            path, self.dirs
-        )
-        .into())
-    }
-}
-
-#[derive(Debug)]
-struct Syntax<'a> {
-    block_start: &'a str,
-    block_end: &'a str,
-    expr_start: &'a str,
-    expr_end: &'a str,
-    comment_start: &'a str,
-    comment_end: &'a str,
-}
-
-impl Default for Syntax<'_> {
-    fn default() -> Self {
-        Self {
-            block_start: "{%",
-            block_end: "%}",
-            expr_start: "{{",
-            expr_end: "}}",
-            comment_start: "{#",
-            comment_end: "#}",
-        }
-    }
-}
-
-impl<'a> TryFrom<RawSyntax<'a>> for Syntax<'a> {
-    type Error = CompileError;
-
-    fn try_from(raw: RawSyntax<'a>) -> std::result::Result<Self, Self::Error> {
-        let default = Self::default();
-        let syntax = Self {
-            block_start: raw.block_start.unwrap_or(default.block_start),
-            block_end: raw.block_end.unwrap_or(default.block_end),
-            expr_start: raw.expr_start.unwrap_or(default.expr_start),
-            expr_end: raw.expr_end.unwrap_or(default.expr_end),
-            comment_start: raw.comment_start.unwrap_or(default.comment_start),
-            comment_end: raw.comment_end.unwrap_or(default.comment_end),
-        };
-
-        if syntax.block_start.len() != 2
-            || syntax.block_end.len() != 2
-            || syntax.expr_start.len() != 2
-            || syntax.expr_end.len() != 2
-            || syntax.comment_start.len() != 2
-            || syntax.comment_end.len() != 2
-        {
-            return Err("length of delimiters must be two".into());
-        }
-
-        let bs = syntax.block_start.as_bytes()[0];
-        let be = syntax.block_start.as_bytes()[1];
-        let cs = syntax.comment_start.as_bytes()[0];
-        let ce = syntax.comment_start.as_bytes()[1];
-        let es = syntax.expr_start.as_bytes()[0];
-        let ee = syntax.expr_start.as_bytes()[1];
-        if !((bs == cs && bs == es) || (be == ce && be == ee)) {
-            return Err(format!("bad delimiters block_start: {}, comment_start: {}, expr_start: {}, needs one of the two characters in common", syntax.block_start, syntax.comment_start, syntax.expr_start).into());
-        }
-
-        Ok(syntax)
-    }
-}
-
-#[cfg_attr(feature = "serde", derive(Deserialize))]
-#[derive(Default)]
-struct RawConfig<'d> {
-    #[cfg_attr(feature = "serde", serde(borrow))]
-    general: Option<General<'d>>,
-    syntax: Option<Vec<RawSyntax<'d>>>,
-    escaper: Option<Vec<RawEscaper<'d>>>,
-}
-
-impl RawConfig<'_> {
-    #[cfg(feature = "config")]
-    fn from_toml_str(s: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
-        toml::from_str(s).map_err(|e| format!("invalid TOML in {}: {}", CONFIG_FILE_NAME, e).into())
-    }
-
-    #[cfg(not(feature = "config"))]
-    fn from_toml_str(_: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
-        Err("TOML support not available".into())
-    }
-}
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug)]
-#[cfg_attr(feature = "serde", derive(Deserialize))]
-#[cfg_attr(feature = "serde", serde(field_identifier, rename_all = "lowercase"))]
-pub(crate) enum WhitespaceHandling {
-    /// The default behaviour. It will leave the whitespace characters "as is".
-    Preserve,
-    /// It'll remove all the whitespace characters before and after the jinja block.
-    Suppress,
-    /// It'll remove all the whitespace characters except one before and after the jinja blocks.
-    /// If there is a newline character, the preserved character in the trimmed characters, it will
-    /// the one preserved.
-    Minimize,
-}
-
-impl Default for WhitespaceHandling {
-    fn default() -> Self {
-        WhitespaceHandling::Preserve
-    }
-}
-
-#[cfg_attr(feature = "serde", derive(Deserialize))]
-struct General<'a> {
-    #[cfg_attr(feature = "serde", serde(borrow))]
-    dirs: Option<Vec<&'a str>>,
-    default_syntax: Option<&'a str>,
-    #[cfg_attr(feature = "serde", serde(default))]
-    whitespace: WhitespaceHandling,
-}
-
-#[cfg_attr(feature = "serde", derive(Deserialize))]
-struct RawSyntax<'a> {
-    name: &'a str,
-    block_start: Option<&'a str>,
-    block_end: Option<&'a str>,
-    expr_start: Option<&'a str>,
-    expr_end: Option<&'a str>,
-    comment_start: Option<&'a str>,
-    comment_end: Option<&'a str>,
-}
-
-#[cfg_attr(feature = "serde", derive(Deserialize))]
-struct RawEscaper<'a> {
-    path: &'a str,
-    extensions: Vec<&'a str>,
-}
-
-fn read_config_file(config_path: &Option<String>) -> std::result::Result<String, CompileError> {
-    let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
-    let filename = match config_path {
-        Some(config_path) => root.join(config_path),
-        None => root.join(CONFIG_FILE_NAME),
-    };
-
-    if filename.exists() {
-        fs::read_to_string(&filename)
-            .map_err(|_| format!("unable to read {:?}", filename.to_str().unwrap()).into())
-    } else if config_path.is_some() {
-        Err(format!("`{}` does not exist", root.display()).into())
-    } else {
-        Ok("".to_string())
-    }
-}
-
-fn str_set<T>(vals: &[T]) -> HashSet<String>
-where
-    T: ToString,
-{
-    vals.iter().map(|s| s.to_string()).collect()
-}
-
-#[allow(clippy::match_wild_err_arm)]
-fn get_template_source(tpl_path: &Path) -> std::result::Result<String, CompileError> {
-    match fs::read_to_string(tpl_path) {
-        Err(_) => Err(format!(
-            "unable to open template file '{}'",
-            tpl_path.to_str().unwrap()
-        )
-        .into()),
-        Ok(mut source) => {
-            if source.ends_with('\n') {
-                let _ = source.pop();
-            }
-            Ok(source)
-        }
-    }
-}
-
-static CONFIG_FILE_NAME: &str = "askama.toml";
-static DEFAULT_SYNTAX_NAME: &str = "default";
-static DEFAULT_ESCAPERS: &[(&[&str], &str)] = &[
-    (&["html", "htm", "xml"], "::askama::Html"),
-    (&["md", "none", "txt", "yml", ""], "::askama::Text"),
-    (&["j2", "jinja", "jinja2"], "::askama::Html"),
-];
-
 #[derive(Debug, Clone)]
 struct CompileError {
     msg: Cow<'static, str>,
@@ -446,203 +152,10 @@ impl From<String> for CompileError {
 #[cfg(test)]
 #[allow(clippy::blacklisted_name)]
 mod tests {
-    use super::*;
-    use std::env;
-    use std::path::{Path, PathBuf};
-
-    #[test]
-    fn get_source() {
-        let path = Config::new("")
-            .and_then(|config| config.find_template("b.html", None))
-            .unwrap();
-        assert_eq!(get_template_source(&path).unwrap(), "bar");
-    }
-
-    #[test]
-    fn test_default_config() {
-        let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
-        root.push("templates");
-        let config = Config::new("").unwrap();
-        assert_eq!(config.dirs, vec![root]);
-    }
-
-    #[cfg(feature = "config")]
-    #[test]
-    fn test_config_dirs() {
-        let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
-        root.push("tpl");
-        let config = Config::new("[general]\ndirs = [\"tpl\"]").unwrap();
-        assert_eq!(config.dirs, vec![root]);
-    }
-
-    fn assert_eq_rooted(actual: &Path, expected: &str) {
-        let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
-        root.push("templates");
-        let mut inner = PathBuf::new();
-        inner.push(expected);
-        assert_eq!(actual.strip_prefix(root).unwrap(), inner);
-    }
-
-    #[test]
-    fn find_absolute() {
-        let config = Config::new("").unwrap();
-        let root = config.find_template("a.html", None).unwrap();
-        let path = config.find_template("sub/b.html", Some(&root)).unwrap();
-        assert_eq_rooted(&path, "sub/b.html");
-    }
-
-    #[test]
-    #[should_panic]
-    fn find_relative_nonexistent() {
-        let config = Config::new("").unwrap();
-        let root = config.find_template("a.html", None).unwrap();
-        config.find_template("c.html", Some(&root)).unwrap();
-    }
-
-    #[test]
-    fn find_relative() {
-        let config = Config::new("").unwrap();
-        let root = config.find_template("sub/b.html", None).unwrap();
-        let path = config.find_template("c.html", Some(&root)).unwrap();
-        assert_eq_rooted(&path, "sub/c.html");
-    }
-
-    #[test]
-    fn find_relative_sub() {
-        let config = Config::new("").unwrap();
-        let root = config.find_template("sub/b.html", None).unwrap();
-        let path = config.find_template("sub1/d.html", Some(&root)).unwrap();
-        assert_eq_rooted(&path, "sub/sub1/d.html");
-    }
-
-    #[cfg(feature = "config")]
-    #[test]
-    fn add_syntax() {
-        let raw_config = r#"
-        [general]
-        default_syntax = "foo"
-
-        [[syntax]]
-        name = "foo"
-        block_start = "{<"
-
-        [[syntax]]
-        name = "bar"
-        expr_start = "{!"
-        "#;
-
-        let default_syntax = Syntax::default();
-        let config = Config::new(raw_config).unwrap();
-        assert_eq!(config.default_syntax, "foo");
-
-        let foo = config.syntaxes.get("foo").unwrap();
-        assert_eq!(foo.block_start, "{<");
-        assert_eq!(foo.block_end, default_syntax.block_end);
-        assert_eq!(foo.expr_start, default_syntax.expr_start);
-        assert_eq!(foo.expr_end, default_syntax.expr_end);
-        assert_eq!(foo.comment_start, default_syntax.comment_start);
-        assert_eq!(foo.comment_end, default_syntax.comment_end);
-
-        let bar = config.syntaxes.get("bar").unwrap();
-        assert_eq!(bar.block_start, default_syntax.block_start);
-        assert_eq!(bar.block_end, default_syntax.block_end);
-        assert_eq!(bar.expr_start, "{!");
-        assert_eq!(bar.expr_end, default_syntax.expr_end);
-        assert_eq!(bar.comment_start, default_syntax.comment_start);
-        assert_eq!(bar.comment_end, default_syntax.comment_end);
-    }
-
-    #[cfg(feature = "config")]
-    #[test]
-    fn add_syntax_two() {
-        let raw_config = r#"
-        syntax = [{ name = "foo", block_start = "{<" },
-                  { name = "bar", expr_start = "{!" } ]
-
-        [general]
-        default_syntax = "foo"
-        "#;
-
-        let default_syntax = Syntax::default();
-        let config = Config::new(raw_config).unwrap();
-        assert_eq!(config.default_syntax, "foo");
-
-        let foo = config.syntaxes.get("foo").unwrap();
-        assert_eq!(foo.block_start, "{<");
-        assert_eq!(foo.block_end, default_syntax.block_end);
-        assert_eq!(foo.expr_start, default_syntax.expr_start);
-        assert_eq!(foo.expr_end, default_syntax.expr_end);
-        assert_eq!(foo.comment_start, default_syntax.comment_start);
-        assert_eq!(foo.comment_end, default_syntax.comment_end);
-
-        let bar = config.syntaxes.get("bar").unwrap();
-        assert_eq!(bar.block_start, default_syntax.block_start);
-        assert_eq!(bar.block_end, default_syntax.block_end);
-        assert_eq!(bar.expr_start, "{!");
-        assert_eq!(bar.expr_end, default_syntax.expr_end);
-        assert_eq!(bar.comment_start, default_syntax.comment_start);
-        assert_eq!(bar.comment_end, default_syntax.comment_end);
-    }
-
-    #[cfg(feature = "toml")]
-    #[should_panic]
-    #[test]
-    fn use_default_at_syntax_name() {
-        let raw_config = r#"
-        syntax = [{ name = "default" }]
-        "#;
-
-        let _config = Config::new(raw_config).unwrap();
-    }
+    use std::fmt;
 
-    #[cfg(feature = "toml")]
-    #[should_panic]
-    #[test]
-    fn duplicated_syntax_name_on_list() {
-        let raw_config = r#"
-        syntax = [{ name = "foo", block_start = "~<" },
-                  { name = "foo", block_start = "%%" } ]
-        "#;
-
-        let _config = Config::new(raw_config).unwrap();
-    }
-
-    #[cfg(feature = "toml")]
-    #[should_panic]
-    #[test]
-    fn is_not_exist_default_syntax() {
-        let raw_config = r#"
-        [general]
-        default_syntax = "foo"
-        "#;
-
-        let _config = Config::new(raw_config).unwrap();
-    }
-
-    #[cfg(feature = "config")]
-    #[test]
-    fn escape_modes() {
-        let config = Config::new(
-            r#"
-            [[escaper]]
-            path = "::askama::Js"
-            extensions = ["js"]
-        "#,
-        )
-        .unwrap();
-        assert_eq!(
-            config.escapers,
-            vec![
-                (str_set(&["js"]), "::askama::Js".into()),
-                (str_set(&["html", "htm", "xml"]), "::askama::Html".into()),
-                (
-                    str_set(&["md", "none", "txt", "yml", ""]),
-                    "::askama::Text".into()
-                ),
-                (str_set(&["j2", "jinja", "jinja2"]), "::askama::Html".into()),
-            ]
-        );
-    }
+    use super::*;
+    use crate::{DynTemplate, Template};
 
     #[test]
     fn dyn_template() {
@@ -682,37 +195,4 @@ mod tests {
         test.dyn_write_into(&mut vec).unwrap();
         assert_eq!(vec, vec![b't', b'e', b's', b't']);
     }
-
-    #[test]
-    fn test_whitespace_parsing() {
-        let config = Config::new(
-            r#"
-            [general]
-            whitespace = "suppress"
-            "#,
-        )
-        .unwrap();
-        assert_eq!(config.whitespace, WhitespaceHandling::Suppress);
-
-        let config = Config::new(r#""#).unwrap();
-        assert_eq!(config.whitespace, WhitespaceHandling::Preserve);
-
-        let config = Config::new(
-            r#"
-            [general]
-            whitespace = "preserve"
-            "#,
-        )
-        .unwrap();
-        assert_eq!(config.whitespace, WhitespaceHandling::Preserve);
-
-        let config = Config::new(
-            r#"
-            [general]
-            whitespace = "minimize"
-            "#,
-        )
-        .unwrap();
-        assert_eq!(config.whitespace, WhitespaceHandling::Minimize);
-    }
 }
diff --git a/askama_shared/src/parser.rs b/askama_shared/src/parser.rs
index 19df795..efcad73 100644
--- a/askama_shared/src/parser.rs
+++ b/askama_shared/src/parser.rs
@@ -10,7 +10,8 @@ use nom::multi::{fold_many0, many0, many1, separated_list0, separated_list1};
 use nom::sequence::{delimited, pair, preceded, terminated, tuple};
 use nom::{self, error_position, AsChar, IResult, InputTakeAtPosition};
 
-use crate::{CompileError, Syntax};
+use crate::config::Syntax;
+use crate::CompileError;
 
 #[derive(Debug, PartialEq)]
 pub(crate) enum Node<'a> {
@@ -1224,7 +1225,7 @@ pub(crate) fn parse<'a>(
 #[cfg(test)]
 mod tests {
     use super::{Expr, Node, Whitespace, Ws};
-    use crate::Syntax;
+    use crate::config::Syntax;
 
     fn check_ws_split(s: &str, res: &(&str, &str, &str)) {
         match super::split_ws_parts(s) {
-- 
cgit