1use crate::utils::{TomlExt, fs, log_backtrace};
47use anyhow::{Context, Error, Result, bail};
48use serde::{Deserialize, Serialize};
49use std::collections::{BTreeMap, HashMap};
50use std::env;
51use std::path::{Path, PathBuf};
52use std::str::FromStr;
53use toml::Value;
54use toml::value::Table;
55use tracing::{debug, trace};
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(default, deny_unknown_fields)]
61#[non_exhaustive]
62pub struct Config {
63 pub book: BookConfig,
65 #[serde(skip_serializing_if = "is_default")]
67 pub build: BuildConfig,
68 #[serde(skip_serializing_if = "is_default")]
70 pub rust: RustConfig,
71 #[serde(skip_serializing_if = "toml_is_empty")]
73 output: Value,
74 #[serde(skip_serializing_if = "toml_is_empty")]
76 preprocessor: Value,
77}
78
79fn is_default<T: Default + PartialEq>(t: &T) -> bool {
81 t == &T::default()
82}
83
84fn toml_is_empty(table: &Value) -> bool {
86 table.as_table().unwrap().is_empty()
87}
88
89impl FromStr for Config {
90 type Err = Error;
91
92 fn from_str(src: &str) -> Result<Self> {
94 toml::from_str(src).with_context(|| "Invalid configuration file")
95 }
96}
97
98impl Default for Config {
99 fn default() -> Config {
100 Config {
101 book: BookConfig::default(),
102 build: BuildConfig::default(),
103 rust: RustConfig::default(),
104 output: Value::Table(Table::default()),
105 preprocessor: Value::Table(Table::default()),
106 }
107 }
108}
109
110impl Config {
111 pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
113 let cfg = fs::read_to_string(config_file)?;
114 Config::from_str(&cfg)
115 }
116
117 pub fn update_from_env(&mut self) -> Result<()> {
150 debug!("Updating the config from environment variables");
151
152 let overrides =
153 env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));
154
155 for (key, value) in overrides {
156 if key == "log" {
157 continue;
159 }
160 trace!("{} => {}", key, value);
161 let parsed_value = serde_json::from_str(&value)
162 .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
163
164 self.set(key, parsed_value)?;
165 }
166 Ok(())
167 }
168
169 pub fn get<'de, T: Deserialize<'de>>(&self, name: &str) -> Result<Option<T>> {
181 let (key, table) = if let Some(key) = name.strip_prefix("output.") {
182 (key, &self.output)
183 } else if let Some(key) = name.strip_prefix("preprocessor.") {
184 (key, &self.preprocessor)
185 } else {
186 bail!(
187 "unable to get `{name}`, only `output` and `preprocessor` table entries are allowed"
188 );
189 };
190 table
191 .read(key)
192 .map(|value| {
193 value
194 .clone()
195 .try_into()
196 .with_context(|| format!("Failed to deserialize `{name}`"))
197 })
198 .transpose()
199 }
200
201 pub fn contains_key(&self, name: &str) -> bool {
208 if let Some(key) = name.strip_prefix("output.") {
209 self.output.read(key)
210 } else if let Some(key) = name.strip_prefix("preprocessor.") {
211 self.preprocessor.read(key)
212 } else {
213 panic!("invalid key `{name}`");
214 }
215 .is_some()
216 }
217
218 pub fn preprocessors<'de, T: Deserialize<'de>>(&self) -> Result<BTreeMap<String, T>> {
220 self.preprocessor
221 .clone()
222 .try_into()
223 .with_context(|| "Failed to read preprocessors")
224 }
225
226 pub fn outputs<'de, T: Deserialize<'de>>(&self) -> Result<BTreeMap<String, T>> {
228 self.output
229 .clone()
230 .try_into()
231 .with_context(|| "Failed to read renderers")
232 }
233
234 #[doc(hidden)]
241 pub fn html_config(&self) -> Option<HtmlConfig> {
242 match self.get("output.html") {
243 Ok(Some(config)) => Some(config),
244 Ok(None) => None,
245 Err(e) => {
246 log_backtrace(&e);
247 None
248 }
249 }
250 }
251
252 pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
266 let index = index.as_ref();
267
268 let value = Value::try_from(value)
269 .with_context(|| "Unable to represent the item as a JSON Value")?;
270
271 if index == "book" {
272 self.book = value.try_into()?;
273 } else if index == "build" {
274 self.build = value.try_into()?;
275 } else if index == "rust" {
276 self.rust = value.try_into()?;
277 } else if index == "output" {
278 self.output = value;
279 } else if index == "preprocessor" {
280 self.preprocessor = value;
281 } else if let Some(key) = index.strip_prefix("book.") {
282 self.book.update_value(key, value)?;
283 } else if let Some(key) = index.strip_prefix("build.") {
284 self.build.update_value(key, value)?;
285 } else if let Some(key) = index.strip_prefix("rust.") {
286 self.rust.update_value(key, value)?;
287 } else if let Some(key) = index.strip_prefix("output.") {
288 self.output.update_value(key, value)?;
289 } else if let Some(key) = index.strip_prefix("preprocessor.") {
290 self.preprocessor.update_value(key, value)?;
291 } else {
292 bail!("invalid key `{index}`");
293 }
294
295 Ok(())
296 }
297}
298
299fn parse_env(key: &str) -> Option<String> {
300 key.strip_prefix("MDBOOK_")
301 .map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
302}
303
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
308#[non_exhaustive]
309pub struct BookConfig {
310 pub title: Option<String>,
312 pub authors: Vec<String>,
314 pub description: Option<String>,
316 #[serde(skip_serializing_if = "is_default_src")]
318 pub src: PathBuf,
319 pub language: Option<String>,
321 pub text_direction: Option<TextDirection>,
324}
325
326fn is_default_src(src: &PathBuf) -> bool {
328 src == Path::new("src")
329}
330
331impl Default for BookConfig {
332 fn default() -> BookConfig {
333 BookConfig {
334 title: None,
335 authors: Vec::new(),
336 description: None,
337 src: PathBuf::from("src"),
338 language: Some(String::from("en")),
339 text_direction: None,
340 }
341 }
342}
343
344impl BookConfig {
345 pub fn realized_text_direction(&self) -> TextDirection {
348 if let Some(direction) = self.text_direction {
349 direction
350 } else {
351 TextDirection::from_lang_code(self.language.as_deref().unwrap_or_default())
352 }
353 }
354}
355
356#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
358#[non_exhaustive]
359pub enum TextDirection {
360 #[serde(rename = "ltr")]
362 LeftToRight,
363 #[serde(rename = "rtl")]
365 RightToLeft,
366}
367
368impl TextDirection {
369 pub fn from_lang_code(code: &str) -> Self {
371 match code {
372 "ar" | "ara" | "arc" | "ae" | "ave" | "egy" | "he" | "heb" | "nqo" | "pal" | "phn"
374 | "sam" | "syc" | "syr" | "fa" | "per" | "fas" | "ku" | "kur" | "ur" | "urd"
375 | "pus" | "ps" | "yi" | "yid" => TextDirection::RightToLeft,
376 _ => TextDirection::LeftToRight,
377 }
378 }
379}
380
381#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
384#[non_exhaustive]
385pub struct BuildConfig {
386 pub build_dir: PathBuf,
388 pub create_missing: bool,
391 pub use_default_preprocessors: bool,
394 pub extra_watch_dirs: Vec<PathBuf>,
396}
397
398impl Default for BuildConfig {
399 fn default() -> BuildConfig {
400 BuildConfig {
401 build_dir: PathBuf::from("book"),
402 create_missing: true,
403 use_default_preprocessors: true,
404 extra_watch_dirs: Vec::new(),
405 }
406 }
407}
408
409#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
411#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
412#[non_exhaustive]
413pub struct RustConfig {
414 pub edition: Option<RustEdition>,
416}
417
418#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
420#[non_exhaustive]
421pub enum RustEdition {
422 #[serde(rename = "2024")]
424 E2024,
425 #[serde(rename = "2021")]
427 E2021,
428 #[serde(rename = "2018")]
430 E2018,
431 #[serde(rename = "2015")]
433 E2015,
434}
435
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
438#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
439#[non_exhaustive]
440pub struct HtmlConfig {
441 pub theme: Option<PathBuf>,
443 pub default_theme: Option<String>,
445 pub preferred_dark_theme: Option<String>,
448 pub smart_punctuation: bool,
450 pub definition_lists: bool,
452 pub admonitions: bool,
454 pub mathjax_support: bool,
456 pub additional_css: Vec<PathBuf>,
458 pub additional_js: Vec<PathBuf>,
461 pub fold: Fold,
463 #[serde(alias = "playpen")]
465 pub playground: Playground,
466 pub code: Code,
468 pub print: Print,
470 pub no_section_label: bool,
472 pub search: Option<Search>,
474 pub git_repository_url: Option<String>,
476 pub git_repository_icon: Option<String>,
479 pub input_404: Option<String>,
481 pub site_url: Option<String>,
483 pub cname: Option<String>,
490 pub edit_url_template: Option<String>,
494 #[doc(hidden)]
500 pub live_reload_endpoint: Option<String>,
501 pub redirect: HashMap<String, String>,
504 pub hash_files: bool,
509 pub sidebar_header_nav: bool,
512}
513
514impl Default for HtmlConfig {
515 fn default() -> HtmlConfig {
516 HtmlConfig {
517 theme: None,
518 default_theme: None,
519 preferred_dark_theme: None,
520 smart_punctuation: true,
521 definition_lists: true,
522 admonitions: true,
523 mathjax_support: false,
524 additional_css: Vec::new(),
525 additional_js: Vec::new(),
526 fold: Fold::default(),
527 playground: Playground::default(),
528 code: Code::default(),
529 print: Print::default(),
530 no_section_label: false,
531 search: None,
532 git_repository_url: None,
533 git_repository_icon: None,
534 input_404: None,
535 site_url: None,
536 cname: None,
537 edit_url_template: None,
538 live_reload_endpoint: None,
539 redirect: HashMap::new(),
540 hash_files: true,
541 sidebar_header_nav: true,
542 }
543 }
544}
545
546impl HtmlConfig {
547 pub fn theme_dir(&self, root: &Path) -> PathBuf {
550 match self.theme {
551 Some(ref d) => root.join(d),
552 None => root.join("theme"),
553 }
554 }
555
556 pub fn get_404_output_file(&self) -> String {
558 self.input_404
559 .as_ref()
560 .unwrap_or(&"404.md".to_string())
561 .replace(".md", ".html")
562 }
563}
564
565#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
567#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
568#[non_exhaustive]
569pub struct Print {
570 pub enable: bool,
572 pub page_break: bool,
574}
575
576impl Default for Print {
577 fn default() -> Self {
578 Self {
579 enable: true,
580 page_break: true,
581 }
582 }
583}
584
585#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
587#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
588#[non_exhaustive]
589pub struct Fold {
590 pub enable: bool,
592 pub level: u8,
596}
597
598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
600#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
601#[non_exhaustive]
602pub struct Playground {
603 pub editable: bool,
605 pub copyable: bool,
607 pub copy_js: bool,
610 pub line_numbers: bool,
612 pub runnable: bool,
614}
615
616impl Default for Playground {
617 fn default() -> Playground {
618 Playground {
619 editable: false,
620 copyable: true,
621 copy_js: true,
622 line_numbers: false,
623 runnable: true,
624 }
625 }
626}
627
628#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
630#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
631#[non_exhaustive]
632pub struct Code {
633 pub hidelines: HashMap<String, String>,
635}
636
637#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
639#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
640#[non_exhaustive]
641pub struct Search {
642 pub enable: bool,
644 pub limit_results: u32,
646 pub teaser_word_count: u32,
648 pub use_boolean_and: bool,
651 pub boost_title: u8,
654 pub boost_hierarchy: u8,
658 pub boost_paragraph: u8,
661 pub expand: bool,
663 pub heading_split_level: u8,
666 pub copy_js: bool,
669 pub chapter: HashMap<String, SearchChapterSettings>,
674}
675
676impl Default for Search {
677 fn default() -> Search {
678 Search {
680 enable: true,
681 limit_results: 30,
682 teaser_word_count: 30,
683 use_boolean_and: false,
684 boost_title: 2,
685 boost_hierarchy: 1,
686 boost_paragraph: 1,
687 expand: true,
688 heading_split_level: 3,
689 copy_js: true,
690 chapter: HashMap::new(),
691 }
692 }
693}
694
695#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
697#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
698#[non_exhaustive]
699pub struct SearchChapterSettings {
700 pub enable: Option<bool>,
702}
703
704trait Updateable<'de>: Serialize + Deserialize<'de> {
710 fn update_value<S: Serialize>(&mut self, key: &str, value: S) -> Result<()> {
711 let mut raw = Value::try_from(&self).expect("unreachable");
712 let value = Value::try_from(value)?;
713 raw.insert(key, value);
714 let updated = raw.try_into()?;
715 *self = updated;
716 Ok(())
717 }
718}
719
720impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 const COMPLEX_CONFIG: &str = r#"
727 [book]
728 title = "Some Book"
729 authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
730 description = "A completely useless book"
731 src = "source"
732 language = "ja"
733
734 [build]
735 build-dir = "outputs"
736 create-missing = false
737 use-default-preprocessors = true
738
739 [output.html]
740 theme = "./themedir"
741 default-theme = "rust"
742 smart-punctuation = true
743 additional-css = ["./foo/bar/baz.css"]
744 git-repository-url = "https://foo.com/"
745 git-repository-icon = "fa-code-fork"
746
747 [output.html.playground]
748 editable = true
749
750 [output.html.redirect]
751 "index.html" = "overview.html"
752 "nexted/page.md" = "https://rust-lang.org/"
753
754 [preprocessor.first]
755
756 [preprocessor.second]
757 "#;
758
759 #[test]
760 fn load_a_complex_config_file() {
761 let src = COMPLEX_CONFIG;
762
763 let book_should_be = BookConfig {
764 title: Some(String::from("Some Book")),
765 authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
766 description: Some(String::from("A completely useless book")),
767 src: PathBuf::from("source"),
768 language: Some(String::from("ja")),
769 text_direction: None,
770 };
771 let build_should_be = BuildConfig {
772 build_dir: PathBuf::from("outputs"),
773 create_missing: false,
774 use_default_preprocessors: true,
775 extra_watch_dirs: Vec::new(),
776 };
777 let rust_should_be = RustConfig { edition: None };
778 let playground_should_be = Playground {
779 editable: true,
780 copyable: true,
781 copy_js: true,
782 line_numbers: false,
783 runnable: true,
784 };
785 let html_should_be = HtmlConfig {
786 smart_punctuation: true,
787 additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
788 theme: Some(PathBuf::from("./themedir")),
789 default_theme: Some(String::from("rust")),
790 playground: playground_should_be,
791 git_repository_url: Some(String::from("https://foo.com/")),
792 git_repository_icon: Some(String::from("fa-code-fork")),
793 redirect: vec![
794 (String::from("index.html"), String::from("overview.html")),
795 (
796 String::from("nexted/page.md"),
797 String::from("https://rust-lang.org/"),
798 ),
799 ]
800 .into_iter()
801 .collect(),
802 ..Default::default()
803 };
804
805 let got = Config::from_str(src).unwrap();
806
807 assert_eq!(got.book, book_should_be);
808 assert_eq!(got.build, build_should_be);
809 assert_eq!(got.rust, rust_should_be);
810 assert_eq!(got.html_config().unwrap(), html_should_be);
811 }
812
813 #[test]
814 fn disable_runnable() {
815 let src = r#"
816 [book]
817 title = "Some Book"
818 description = "book book book"
819 authors = ["Shogo Takata"]
820
821 [output.html.playground]
822 runnable = false
823 "#;
824
825 let got = Config::from_str(src).unwrap();
826 assert!(!got.html_config().unwrap().playground.runnable);
827 }
828
829 #[test]
830 fn edition_2015() {
831 let src = r#"
832 [book]
833 title = "mdBook Documentation"
834 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
835 authors = ["Mathieu David"]
836 src = "./source"
837 [rust]
838 edition = "2015"
839 "#;
840
841 let book_should_be = BookConfig {
842 title: Some(String::from("mdBook Documentation")),
843 description: Some(String::from(
844 "Create book from markdown files. Like Gitbook but implemented in Rust",
845 )),
846 authors: vec![String::from("Mathieu David")],
847 src: PathBuf::from("./source"),
848 ..Default::default()
849 };
850
851 let got = Config::from_str(src).unwrap();
852 assert_eq!(got.book, book_should_be);
853
854 let rust_should_be = RustConfig {
855 edition: Some(RustEdition::E2015),
856 };
857 let got = Config::from_str(src).unwrap();
858 assert_eq!(got.rust, rust_should_be);
859 }
860
861 #[test]
862 fn edition_2018() {
863 let src = r#"
864 [book]
865 title = "mdBook Documentation"
866 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
867 authors = ["Mathieu David"]
868 src = "./source"
869 [rust]
870 edition = "2018"
871 "#;
872
873 let rust_should_be = RustConfig {
874 edition: Some(RustEdition::E2018),
875 };
876
877 let got = Config::from_str(src).unwrap();
878 assert_eq!(got.rust, rust_should_be);
879 }
880
881 #[test]
882 fn edition_2021() {
883 let src = r#"
884 [book]
885 title = "mdBook Documentation"
886 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
887 authors = ["Mathieu David"]
888 src = "./source"
889 [rust]
890 edition = "2021"
891 "#;
892
893 let rust_should_be = RustConfig {
894 edition: Some(RustEdition::E2021),
895 };
896
897 let got = Config::from_str(src).unwrap();
898 assert_eq!(got.rust, rust_should_be);
899 }
900
901 #[test]
902 fn load_arbitrary_output_type() {
903 #[derive(Debug, Deserialize, PartialEq)]
904 struct RandomOutput {
905 foo: u32,
906 bar: String,
907 baz: Vec<bool>,
908 }
909
910 let src = r#"
911 [output.random]
912 foo = 5
913 bar = "Hello World"
914 baz = [true, true, false]
915 "#;
916
917 let should_be = RandomOutput {
918 foo: 5,
919 bar: String::from("Hello World"),
920 baz: vec![true, true, false],
921 };
922
923 let cfg = Config::from_str(src).unwrap();
924 let got: RandomOutput = cfg.get("output.random").unwrap().unwrap();
925
926 assert_eq!(got, should_be);
927
928 let got_baz: Vec<bool> = cfg.get("output.random.baz").unwrap().unwrap();
929 let baz_should_be = vec![true, true, false];
930
931 assert_eq!(got_baz, baz_should_be);
932 }
933
934 #[test]
935 fn set_special_tables() {
936 let mut cfg = Config::default();
937 assert_eq!(cfg.book.title, None);
938 cfg.set("book.title", "my title").unwrap();
939 assert_eq!(cfg.book.title, Some("my title".to_string()));
940
941 assert_eq!(&cfg.build.build_dir, Path::new("book"));
942 cfg.set("build.build-dir", "some-directory").unwrap();
943 assert_eq!(&cfg.build.build_dir, Path::new("some-directory"));
944
945 assert_eq!(cfg.rust.edition, None);
946 cfg.set("rust.edition", "2024").unwrap();
947 assert_eq!(cfg.rust.edition, Some(RustEdition::E2024));
948
949 cfg.set("output.foo.value", "123").unwrap();
950 let got: String = cfg.get("output.foo.value").unwrap().unwrap();
951 assert_eq!(got, "123");
952
953 cfg.set("preprocessor.bar.value", "456").unwrap();
954 let got: String = cfg.get("preprocessor.bar.value").unwrap().unwrap();
955 assert_eq!(got, "456");
956 }
957
958 #[test]
959 fn set_invalid_keys() {
960 let mut cfg = Config::default();
961 let err = cfg.set("foo", "test").unwrap_err();
962 assert!(err.to_string().contains("invalid key `foo`"));
963 }
964
965 #[test]
966 fn parse_env_vars() {
967 let inputs = vec![
968 ("FOO", None),
969 ("MDBOOK_foo", Some("foo")),
970 ("MDBOOK_FOO__bar__baz", Some("foo.bar.baz")),
971 ("MDBOOK_FOO_bar__baz", Some("foo-bar.baz")),
972 ];
973
974 for (src, should_be) in inputs {
975 let got = parse_env(src);
976 let should_be = should_be.map(ToString::to_string);
977
978 assert_eq!(got, should_be);
979 }
980 }
981
982 #[test]
983 fn file_404_default() {
984 let src = r#"
985 [output.html]
986 "#;
987
988 let got = Config::from_str(src).unwrap();
989 let html_config = got.html_config().unwrap();
990 assert_eq!(html_config.input_404, None);
991 assert_eq!(html_config.get_404_output_file(), "404.html");
992 }
993
994 #[test]
995 fn file_404_custom() {
996 let src = r#"
997 [output.html]
998 input-404= "missing.md"
999 "#;
1000
1001 let got = Config::from_str(src).unwrap();
1002 let html_config = got.html_config().unwrap();
1003 assert_eq!(html_config.input_404, Some("missing.md".to_string()));
1004 assert_eq!(html_config.get_404_output_file(), "missing.html");
1005 }
1006
1007 #[test]
1008 fn text_direction_ltr() {
1009 let src = r#"
1010 [book]
1011 text-direction = "ltr"
1012 "#;
1013
1014 let got = Config::from_str(src).unwrap();
1015 assert_eq!(got.book.text_direction, Some(TextDirection::LeftToRight));
1016 }
1017
1018 #[test]
1019 fn text_direction_rtl() {
1020 let src = r#"
1021 [book]
1022 text-direction = "rtl"
1023 "#;
1024
1025 let got = Config::from_str(src).unwrap();
1026 assert_eq!(got.book.text_direction, Some(TextDirection::RightToLeft));
1027 }
1028
1029 #[test]
1030 fn text_direction_none() {
1031 let src = r#"
1032 [book]
1033 "#;
1034
1035 let got = Config::from_str(src).unwrap();
1036 assert_eq!(got.book.text_direction, None);
1037 }
1038
1039 #[test]
1040 fn test_text_direction() {
1041 let mut cfg = BookConfig::default();
1042
1043 cfg.language = Some("ar".into());
1045 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1046
1047 cfg.language = Some("he".into());
1048 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1049
1050 cfg.language = Some("en".into());
1051 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1052
1053 cfg.language = Some("ja".into());
1054 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1055
1056 cfg.language = Some("ar".into());
1058 cfg.text_direction = Some(TextDirection::LeftToRight);
1059 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1060
1061 cfg.language = Some("ar".into());
1062 cfg.text_direction = Some(TextDirection::RightToLeft);
1063 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1064
1065 cfg.language = Some("en".into());
1066 cfg.text_direction = Some(TextDirection::LeftToRight);
1067 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1068
1069 cfg.language = Some("en".into());
1070 cfg.text_direction = Some(TextDirection::RightToLeft);
1071 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1072 }
1073
1074 #[test]
1075 #[should_panic(expected = "Invalid configuration file")]
1076 fn invalid_language_type_error() {
1077 let src = r#"
1078 [book]
1079 title = "mdBook Documentation"
1080 language = ["en", "pt-br"]
1081 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
1082 authors = ["Mathieu David"]
1083 src = "./source"
1084 "#;
1085
1086 Config::from_str(src).unwrap();
1087 }
1088
1089 #[test]
1090 #[should_panic(expected = "Invalid configuration file")]
1091 fn invalid_title_type() {
1092 let src = r#"
1093 [book]
1094 title = 20
1095 language = "en"
1096 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
1097 authors = ["Mathieu David"]
1098 src = "./source"
1099 "#;
1100
1101 Config::from_str(src).unwrap();
1102 }
1103
1104 #[test]
1105 #[should_panic(expected = "Invalid configuration file")]
1106 fn invalid_build_dir_type() {
1107 let src = r#"
1108 [build]
1109 build-dir = 99
1110 create-missing = false
1111 "#;
1112
1113 Config::from_str(src).unwrap();
1114 }
1115
1116 #[test]
1117 #[should_panic(expected = "Invalid configuration file")]
1118 fn invalid_rust_edition() {
1119 let src = r#"
1120 [rust]
1121 edition = "1999"
1122 "#;
1123
1124 Config::from_str(src).unwrap();
1125 }
1126
1127 #[test]
1128 #[should_panic(
1129 expected = "unknown variant `1999`, expected one of `2024`, `2021`, `2018`, `2015`\n"
1130 )]
1131 fn invalid_rust_edition_expected() {
1132 let src = r#"
1133 [rust]
1134 edition = "1999"
1135 "#;
1136
1137 Config::from_str(src).unwrap();
1138 }
1139
1140 #[test]
1141 fn print_config() {
1142 let src = r#"
1143 [output.html.print]
1144 enable = false
1145 "#;
1146 let got = Config::from_str(src).unwrap();
1147 let html_config = got.html_config().unwrap();
1148 assert!(!html_config.print.enable);
1149 assert!(html_config.print.page_break);
1150 let src = r#"
1151 [output.html.print]
1152 page-break = false
1153 "#;
1154 let got = Config::from_str(src).unwrap();
1155 let html_config = got.html_config().unwrap();
1156 assert!(html_config.print.enable);
1157 assert!(!html_config.print.page_break);
1158 }
1159
1160 #[test]
1161 fn test_json_direction() {
1162 use serde_json::json;
1163 assert_eq!(json!(TextDirection::RightToLeft), json!("rtl"));
1164 assert_eq!(json!(TextDirection::LeftToRight), json!("ltr"));
1165 }
1166
1167 #[test]
1168 fn get_deserialize_error() {
1169 let src = r#"
1170 [preprocessor.foo]
1171 x = 123
1172 "#;
1173 let cfg = Config::from_str(src).unwrap();
1174 let err = cfg.get::<String>("preprocessor.foo.x").unwrap_err();
1175 assert_eq!(
1176 err.to_string(),
1177 "Failed to deserialize `preprocessor.foo.x`"
1178 );
1179 }
1180
1181 #[test]
1182 fn contains_key() {
1183 let src = r#"
1184 [preprocessor.foo]
1185 x = 123
1186 [output.foo.sub]
1187 y = 'x'
1188 "#;
1189 let cfg = Config::from_str(src).unwrap();
1190 assert!(cfg.contains_key("preprocessor.foo"));
1191 assert!(cfg.contains_key("preprocessor.foo.x"));
1192 assert!(!cfg.contains_key("preprocessor.bar"));
1193 assert!(!cfg.contains_key("preprocessor.foo.y"));
1194 assert!(cfg.contains_key("output.foo"));
1195 assert!(cfg.contains_key("output.foo.sub"));
1196 assert!(cfg.contains_key("output.foo.sub.y"));
1197 assert!(!cfg.contains_key("output.bar"));
1198 assert!(!cfg.contains_key("output.foo.sub.z"));
1199 }
1200}