diff options
-rw-r--r-- | askama_shared/src/config.rs | 536 | ||||
-rw-r--r-- | askama_shared/src/generator.rs | 5 | ||||
-rw-r--r-- | askama_shared/src/heritage.rs | 3 | ||||
-rw-r--r-- | askama_shared/src/input.rs | 3 | ||||
-rw-r--r-- | askama_shared/src/lib.rs | 532 | ||||
-rw-r--r-- | askama_shared/src/parser.rs | 5 |
6 files changed, 551 insertions, 533 deletions
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) { |