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