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