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) {
151 debug!("Updating the config from environment variables");
152
153 let overrides =
154 env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));
155
156 for (key, value) in overrides {
157 if key == "log" {
158 continue;
160 }
161 trace!("{} => {}", key, value);
162 let parsed_value = serde_json::from_str(&value)
163 .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
164
165 if key == "book" || key == "build" {
166 if let serde_json::Value::Object(ref map) = parsed_value {
167 for (k, v) in map {
169 let full_key = format!("{key}.{k}");
170 self.set(&full_key, v).expect("unreachable");
171 }
172 return;
173 }
174 }
175
176 self.set(key, parsed_value).expect("unreachable");
177 }
178 }
179
180 pub fn get<'de, T: Deserialize<'de>>(&self, name: &str) -> Result<Option<T>> {
192 let (key, table) = if let Some(key) = name.strip_prefix("output.") {
193 (key, &self.output)
194 } else if let Some(key) = name.strip_prefix("preprocessor.") {
195 (key, &self.preprocessor)
196 } else {
197 bail!(
198 "unable to get `{name}`, only `output` and `preprocessor` table entries are allowed"
199 );
200 };
201 table
202 .read(key)
203 .map(|value| {
204 value
205 .clone()
206 .try_into()
207 .with_context(|| format!("Failed to deserialize `{name}`"))
208 })
209 .transpose()
210 }
211
212 pub fn contains_key(&self, name: &str) -> bool {
219 if let Some(key) = name.strip_prefix("output.") {
220 self.output.read(key)
221 } else if let Some(key) = name.strip_prefix("preprocessor.") {
222 self.preprocessor.read(key)
223 } else {
224 panic!("invalid key `{name}`");
225 }
226 .is_some()
227 }
228
229 pub fn preprocessors<'de, T: Deserialize<'de>>(&self) -> Result<BTreeMap<String, T>> {
231 self.preprocessor
232 .clone()
233 .try_into()
234 .with_context(|| "Failed to read preprocessors")
235 }
236
237 pub fn outputs<'de, T: Deserialize<'de>>(&self) -> Result<BTreeMap<String, T>> {
239 self.output
240 .clone()
241 .try_into()
242 .with_context(|| "Failed to read renderers")
243 }
244
245 #[doc(hidden)]
252 pub fn html_config(&self) -> Option<HtmlConfig> {
253 match self.get("output.html") {
254 Ok(Some(config)) => Some(config),
255 Ok(None) => None,
256 Err(e) => {
257 log_backtrace(&e);
258 None
259 }
260 }
261 }
262
263 pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
272 let index = index.as_ref();
273
274 let value = Value::try_from(value)
275 .with_context(|| "Unable to represent the item as a JSON Value")?;
276
277 if let Some(key) = index.strip_prefix("book.") {
278 self.book.update_value(key, value);
279 } else if let Some(key) = index.strip_prefix("build.") {
280 self.build.update_value(key, value);
281 } else if let Some(key) = index.strip_prefix("rust.") {
282 self.rust.update_value(key, value);
283 } else if let Some(key) = index.strip_prefix("output.") {
284 self.output.update_value(key, value);
285 } else if let Some(key) = index.strip_prefix("preprocessor.") {
286 self.preprocessor.update_value(key, value);
287 } else {
288 bail!("invalid key `{index}`");
289 }
290
291 Ok(())
292 }
293}
294
295fn parse_env(key: &str) -> Option<String> {
296 key.strip_prefix("MDBOOK_")
297 .map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
298}
299
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
303#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
304#[non_exhaustive]
305pub struct BookConfig {
306 pub title: Option<String>,
308 pub authors: Vec<String>,
310 pub description: Option<String>,
312 #[serde(skip_serializing_if = "is_default_src")]
314 pub src: PathBuf,
315 pub language: Option<String>,
317 pub text_direction: Option<TextDirection>,
320}
321
322fn is_default_src(src: &PathBuf) -> bool {
324 src == Path::new("src")
325}
326
327impl Default for BookConfig {
328 fn default() -> BookConfig {
329 BookConfig {
330 title: None,
331 authors: Vec::new(),
332 description: None,
333 src: PathBuf::from("src"),
334 language: Some(String::from("en")),
335 text_direction: None,
336 }
337 }
338}
339
340impl BookConfig {
341 pub fn realized_text_direction(&self) -> TextDirection {
344 if let Some(direction) = self.text_direction {
345 direction
346 } else {
347 TextDirection::from_lang_code(self.language.as_deref().unwrap_or_default())
348 }
349 }
350}
351
352#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
354#[non_exhaustive]
355pub enum TextDirection {
356 #[serde(rename = "ltr")]
358 LeftToRight,
359 #[serde(rename = "rtl")]
361 RightToLeft,
362}
363
364impl TextDirection {
365 pub fn from_lang_code(code: &str) -> Self {
367 match code {
368 "ar" | "ara" | "arc" | "ae" | "ave" | "egy" | "he" | "heb" | "nqo" | "pal" | "phn"
370 | "sam" | "syc" | "syr" | "fa" | "per" | "fas" | "ku" | "kur" | "ur" | "urd"
371 | "pus" | "ps" | "yi" | "yid" => TextDirection::RightToLeft,
372 _ => TextDirection::LeftToRight,
373 }
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
379#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
380#[non_exhaustive]
381pub struct BuildConfig {
382 pub build_dir: PathBuf,
384 pub create_missing: bool,
387 pub use_default_preprocessors: bool,
390 pub extra_watch_dirs: Vec<PathBuf>,
392}
393
394impl Default for BuildConfig {
395 fn default() -> BuildConfig {
396 BuildConfig {
397 build_dir: PathBuf::from("book"),
398 create_missing: true,
399 use_default_preprocessors: true,
400 extra_watch_dirs: Vec::new(),
401 }
402 }
403}
404
405#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
407#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
408#[non_exhaustive]
409pub struct RustConfig {
410 pub edition: Option<RustEdition>,
412}
413
414#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
416#[non_exhaustive]
417pub enum RustEdition {
418 #[serde(rename = "2024")]
420 E2024,
421 #[serde(rename = "2021")]
423 E2021,
424 #[serde(rename = "2018")]
426 E2018,
427 #[serde(rename = "2015")]
429 E2015,
430}
431
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
434#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
435#[non_exhaustive]
436pub struct HtmlConfig {
437 pub theme: Option<PathBuf>,
439 pub default_theme: Option<String>,
441 pub preferred_dark_theme: Option<String>,
444 pub smart_punctuation: bool,
446 pub definition_lists: bool,
448 pub admonitions: bool,
450 pub mathjax_support: bool,
452 pub additional_css: Vec<PathBuf>,
454 pub additional_js: Vec<PathBuf>,
457 pub fold: Fold,
459 #[serde(alias = "playpen")]
461 pub playground: Playground,
462 pub code: Code,
464 pub print: Print,
466 pub no_section_label: bool,
468 pub search: Option<Search>,
470 pub git_repository_url: Option<String>,
472 pub git_repository_icon: Option<String>,
475 pub input_404: Option<String>,
477 pub site_url: Option<String>,
479 pub cname: Option<String>,
486 pub edit_url_template: Option<String>,
490 #[doc(hidden)]
496 pub live_reload_endpoint: Option<String>,
497 pub redirect: HashMap<String, String>,
500 pub hash_files: bool,
505 pub sidebar_header_nav: bool,
508}
509
510impl Default for HtmlConfig {
511 fn default() -> HtmlConfig {
512 HtmlConfig {
513 theme: None,
514 default_theme: None,
515 preferred_dark_theme: None,
516 smart_punctuation: true,
517 definition_lists: true,
518 admonitions: true,
519 mathjax_support: false,
520 additional_css: Vec::new(),
521 additional_js: Vec::new(),
522 fold: Fold::default(),
523 playground: Playground::default(),
524 code: Code::default(),
525 print: Print::default(),
526 no_section_label: false,
527 search: None,
528 git_repository_url: None,
529 git_repository_icon: None,
530 input_404: None,
531 site_url: None,
532 cname: None,
533 edit_url_template: None,
534 live_reload_endpoint: None,
535 redirect: HashMap::new(),
536 hash_files: true,
537 sidebar_header_nav: true,
538 }
539 }
540}
541
542impl HtmlConfig {
543 pub fn theme_dir(&self, root: &Path) -> PathBuf {
546 match self.theme {
547 Some(ref d) => root.join(d),
548 None => root.join("theme"),
549 }
550 }
551
552 pub fn get_404_output_file(&self) -> String {
554 self.input_404
555 .as_ref()
556 .unwrap_or(&"404.md".to_string())
557 .replace(".md", ".html")
558 }
559}
560
561#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
563#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
564#[non_exhaustive]
565pub struct Print {
566 pub enable: bool,
568 pub page_break: bool,
570}
571
572impl Default for Print {
573 fn default() -> Self {
574 Self {
575 enable: true,
576 page_break: true,
577 }
578 }
579}
580
581#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
583#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
584#[non_exhaustive]
585pub struct Fold {
586 pub enable: bool,
588 pub level: u8,
592}
593
594#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
596#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
597#[non_exhaustive]
598pub struct Playground {
599 pub editable: bool,
601 pub copyable: bool,
603 pub copy_js: bool,
606 pub line_numbers: bool,
608 pub runnable: bool,
610}
611
612impl Default for Playground {
613 fn default() -> Playground {
614 Playground {
615 editable: false,
616 copyable: true,
617 copy_js: true,
618 line_numbers: false,
619 runnable: true,
620 }
621 }
622}
623
624#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
626#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
627#[non_exhaustive]
628pub struct Code {
629 pub hidelines: HashMap<String, String>,
631}
632
633#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
635#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
636#[non_exhaustive]
637pub struct Search {
638 pub enable: bool,
640 pub limit_results: u32,
642 pub teaser_word_count: u32,
644 pub use_boolean_and: bool,
647 pub boost_title: u8,
650 pub boost_hierarchy: u8,
654 pub boost_paragraph: u8,
657 pub expand: bool,
659 pub heading_split_level: u8,
662 pub copy_js: bool,
665 pub chapter: HashMap<String, SearchChapterSettings>,
670}
671
672impl Default for Search {
673 fn default() -> Search {
674 Search {
676 enable: true,
677 limit_results: 30,
678 teaser_word_count: 30,
679 use_boolean_and: false,
680 boost_title: 2,
681 boost_hierarchy: 1,
682 boost_paragraph: 1,
683 expand: true,
684 heading_split_level: 3,
685 copy_js: true,
686 chapter: HashMap::new(),
687 }
688 }
689}
690
691#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
693#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
694#[non_exhaustive]
695pub struct SearchChapterSettings {
696 pub enable: Option<bool>,
698}
699
700trait Updateable<'de>: Serialize + Deserialize<'de> {
706 fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
707 let mut raw = Value::try_from(&self).expect("unreachable");
708
709 if let Ok(value) = Value::try_from(value) {
710 raw.insert(key, value);
711 } else {
712 return;
713 }
714
715 if let Ok(updated) = raw.try_into() {
716 *self = updated;
717 }
718 }
719}
720
721impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726
727 const COMPLEX_CONFIG: &str = r#"
728 [book]
729 title = "Some Book"
730 authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
731 description = "A completely useless book"
732 src = "source"
733 language = "ja"
734
735 [build]
736 build-dir = "outputs"
737 create-missing = false
738 use-default-preprocessors = true
739
740 [output.html]
741 theme = "./themedir"
742 default-theme = "rust"
743 smart-punctuation = true
744 additional-css = ["./foo/bar/baz.css"]
745 git-repository-url = "https://foo.com/"
746 git-repository-icon = "fa-code-fork"
747
748 [output.html.playground]
749 editable = true
750
751 [output.html.redirect]
752 "index.html" = "overview.html"
753 "nexted/page.md" = "https://rust-lang.org/"
754
755 [preprocessor.first]
756
757 [preprocessor.second]
758 "#;
759
760 #[test]
761 fn load_a_complex_config_file() {
762 let src = COMPLEX_CONFIG;
763
764 let book_should_be = BookConfig {
765 title: Some(String::from("Some Book")),
766 authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
767 description: Some(String::from("A completely useless book")),
768 src: PathBuf::from("source"),
769 language: Some(String::from("ja")),
770 text_direction: None,
771 };
772 let build_should_be = BuildConfig {
773 build_dir: PathBuf::from("outputs"),
774 create_missing: false,
775 use_default_preprocessors: true,
776 extra_watch_dirs: Vec::new(),
777 };
778 let rust_should_be = RustConfig { edition: None };
779 let playground_should_be = Playground {
780 editable: true,
781 copyable: true,
782 copy_js: true,
783 line_numbers: false,
784 runnable: true,
785 };
786 let html_should_be = HtmlConfig {
787 smart_punctuation: true,
788 additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
789 theme: Some(PathBuf::from("./themedir")),
790 default_theme: Some(String::from("rust")),
791 playground: playground_should_be,
792 git_repository_url: Some(String::from("https://foo.com/")),
793 git_repository_icon: Some(String::from("fa-code-fork")),
794 redirect: vec![
795 (String::from("index.html"), String::from("overview.html")),
796 (
797 String::from("nexted/page.md"),
798 String::from("https://rust-lang.org/"),
799 ),
800 ]
801 .into_iter()
802 .collect(),
803 ..Default::default()
804 };
805
806 let got = Config::from_str(src).unwrap();
807
808 assert_eq!(got.book, book_should_be);
809 assert_eq!(got.build, build_should_be);
810 assert_eq!(got.rust, rust_should_be);
811 assert_eq!(got.html_config().unwrap(), html_should_be);
812 }
813
814 #[test]
815 fn disable_runnable() {
816 let src = r#"
817 [book]
818 title = "Some Book"
819 description = "book book book"
820 authors = ["Shogo Takata"]
821
822 [output.html.playground]
823 runnable = false
824 "#;
825
826 let got = Config::from_str(src).unwrap();
827 assert!(!got.html_config().unwrap().playground.runnable);
828 }
829
830 #[test]
831 fn edition_2015() {
832 let src = r#"
833 [book]
834 title = "mdBook Documentation"
835 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
836 authors = ["Mathieu David"]
837 src = "./source"
838 [rust]
839 edition = "2015"
840 "#;
841
842 let book_should_be = BookConfig {
843 title: Some(String::from("mdBook Documentation")),
844 description: Some(String::from(
845 "Create book from markdown files. Like Gitbook but implemented in Rust",
846 )),
847 authors: vec![String::from("Mathieu David")],
848 src: PathBuf::from("./source"),
849 ..Default::default()
850 };
851
852 let got = Config::from_str(src).unwrap();
853 assert_eq!(got.book, book_should_be);
854
855 let rust_should_be = RustConfig {
856 edition: Some(RustEdition::E2015),
857 };
858 let got = Config::from_str(src).unwrap();
859 assert_eq!(got.rust, rust_should_be);
860 }
861
862 #[test]
863 fn edition_2018() {
864 let src = r#"
865 [book]
866 title = "mdBook Documentation"
867 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
868 authors = ["Mathieu David"]
869 src = "./source"
870 [rust]
871 edition = "2018"
872 "#;
873
874 let rust_should_be = RustConfig {
875 edition: Some(RustEdition::E2018),
876 };
877
878 let got = Config::from_str(src).unwrap();
879 assert_eq!(got.rust, rust_should_be);
880 }
881
882 #[test]
883 fn edition_2021() {
884 let src = r#"
885 [book]
886 title = "mdBook Documentation"
887 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
888 authors = ["Mathieu David"]
889 src = "./source"
890 [rust]
891 edition = "2021"
892 "#;
893
894 let rust_should_be = RustConfig {
895 edition: Some(RustEdition::E2021),
896 };
897
898 let got = Config::from_str(src).unwrap();
899 assert_eq!(got.rust, rust_should_be);
900 }
901
902 #[test]
903 fn load_arbitrary_output_type() {
904 #[derive(Debug, Deserialize, PartialEq)]
905 struct RandomOutput {
906 foo: u32,
907 bar: String,
908 baz: Vec<bool>,
909 }
910
911 let src = r#"
912 [output.random]
913 foo = 5
914 bar = "Hello World"
915 baz = [true, true, false]
916 "#;
917
918 let should_be = RandomOutput {
919 foo: 5,
920 bar: String::from("Hello World"),
921 baz: vec![true, true, false],
922 };
923
924 let cfg = Config::from_str(src).unwrap();
925 let got: RandomOutput = cfg.get("output.random").unwrap().unwrap();
926
927 assert_eq!(got, should_be);
928
929 let got_baz: Vec<bool> = cfg.get("output.random.baz").unwrap().unwrap();
930 let baz_should_be = vec![true, true, false];
931
932 assert_eq!(got_baz, baz_should_be);
933 }
934
935 #[test]
936 fn set_special_tables() {
937 let mut cfg = Config::default();
938 assert_eq!(cfg.book.title, None);
939 cfg.set("book.title", "my title").unwrap();
940 assert_eq!(cfg.book.title, Some("my title".to_string()));
941
942 assert_eq!(&cfg.build.build_dir, Path::new("book"));
943 cfg.set("build.build-dir", "some-directory").unwrap();
944 assert_eq!(&cfg.build.build_dir, Path::new("some-directory"));
945
946 assert_eq!(cfg.rust.edition, None);
947 cfg.set("rust.edition", "2024").unwrap();
948 assert_eq!(cfg.rust.edition, Some(RustEdition::E2024));
949
950 cfg.set("output.foo.value", "123").unwrap();
951 let got: String = cfg.get("output.foo.value").unwrap().unwrap();
952 assert_eq!(got, "123");
953
954 cfg.set("preprocessor.bar.value", "456").unwrap();
955 let got: String = cfg.get("preprocessor.bar.value").unwrap().unwrap();
956 assert_eq!(got, "456");
957 }
958
959 #[test]
960 fn set_invalid_keys() {
961 let mut cfg = Config::default();
962 let err = cfg.set("foo", "test").unwrap_err();
963 assert!(err.to_string().contains("invalid key `foo`"));
964 }
965
966 #[test]
967 fn parse_env_vars() {
968 let inputs = vec![
969 ("FOO", None),
970 ("MDBOOK_foo", Some("foo")),
971 ("MDBOOK_FOO__bar__baz", Some("foo.bar.baz")),
972 ("MDBOOK_FOO_bar__baz", Some("foo-bar.baz")),
973 ];
974
975 for (src, should_be) in inputs {
976 let got = parse_env(src);
977 let should_be = should_be.map(ToString::to_string);
978
979 assert_eq!(got, should_be);
980 }
981 }
982
983 #[test]
984 fn file_404_default() {
985 let src = r#"
986 [output.html]
987 "#;
988
989 let got = Config::from_str(src).unwrap();
990 let html_config = got.html_config().unwrap();
991 assert_eq!(html_config.input_404, None);
992 assert_eq!(html_config.get_404_output_file(), "404.html");
993 }
994
995 #[test]
996 fn file_404_custom() {
997 let src = r#"
998 [output.html]
999 input-404= "missing.md"
1000 "#;
1001
1002 let got = Config::from_str(src).unwrap();
1003 let html_config = got.html_config().unwrap();
1004 assert_eq!(html_config.input_404, Some("missing.md".to_string()));
1005 assert_eq!(html_config.get_404_output_file(), "missing.html");
1006 }
1007
1008 #[test]
1009 fn text_direction_ltr() {
1010 let src = r#"
1011 [book]
1012 text-direction = "ltr"
1013 "#;
1014
1015 let got = Config::from_str(src).unwrap();
1016 assert_eq!(got.book.text_direction, Some(TextDirection::LeftToRight));
1017 }
1018
1019 #[test]
1020 fn text_direction_rtl() {
1021 let src = r#"
1022 [book]
1023 text-direction = "rtl"
1024 "#;
1025
1026 let got = Config::from_str(src).unwrap();
1027 assert_eq!(got.book.text_direction, Some(TextDirection::RightToLeft));
1028 }
1029
1030 #[test]
1031 fn text_direction_none() {
1032 let src = r#"
1033 [book]
1034 "#;
1035
1036 let got = Config::from_str(src).unwrap();
1037 assert_eq!(got.book.text_direction, None);
1038 }
1039
1040 #[test]
1041 fn test_text_direction() {
1042 let mut cfg = BookConfig::default();
1043
1044 cfg.language = Some("ar".into());
1046 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1047
1048 cfg.language = Some("he".into());
1049 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1050
1051 cfg.language = Some("en".into());
1052 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1053
1054 cfg.language = Some("ja".into());
1055 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1056
1057 cfg.language = Some("ar".into());
1059 cfg.text_direction = Some(TextDirection::LeftToRight);
1060 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1061
1062 cfg.language = Some("ar".into());
1063 cfg.text_direction = Some(TextDirection::RightToLeft);
1064 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1065
1066 cfg.language = Some("en".into());
1067 cfg.text_direction = Some(TextDirection::LeftToRight);
1068 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1069
1070 cfg.language = Some("en".into());
1071 cfg.text_direction = Some(TextDirection::RightToLeft);
1072 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1073 }
1074
1075 #[test]
1076 #[should_panic(expected = "Invalid configuration file")]
1077 fn invalid_language_type_error() {
1078 let src = r#"
1079 [book]
1080 title = "mdBook Documentation"
1081 language = ["en", "pt-br"]
1082 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
1083 authors = ["Mathieu David"]
1084 src = "./source"
1085 "#;
1086
1087 Config::from_str(src).unwrap();
1088 }
1089
1090 #[test]
1091 #[should_panic(expected = "Invalid configuration file")]
1092 fn invalid_title_type() {
1093 let src = r#"
1094 [book]
1095 title = 20
1096 language = "en"
1097 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
1098 authors = ["Mathieu David"]
1099 src = "./source"
1100 "#;
1101
1102 Config::from_str(src).unwrap();
1103 }
1104
1105 #[test]
1106 #[should_panic(expected = "Invalid configuration file")]
1107 fn invalid_build_dir_type() {
1108 let src = r#"
1109 [build]
1110 build-dir = 99
1111 create-missing = false
1112 "#;
1113
1114 Config::from_str(src).unwrap();
1115 }
1116
1117 #[test]
1118 #[should_panic(expected = "Invalid configuration file")]
1119 fn invalid_rust_edition() {
1120 let src = r#"
1121 [rust]
1122 edition = "1999"
1123 "#;
1124
1125 Config::from_str(src).unwrap();
1126 }
1127
1128 #[test]
1129 #[should_panic(
1130 expected = "unknown variant `1999`, expected one of `2024`, `2021`, `2018`, `2015`\n"
1131 )]
1132 fn invalid_rust_edition_expected() {
1133 let src = r#"
1134 [rust]
1135 edition = "1999"
1136 "#;
1137
1138 Config::from_str(src).unwrap();
1139 }
1140
1141 #[test]
1142 fn print_config() {
1143 let src = r#"
1144 [output.html.print]
1145 enable = false
1146 "#;
1147 let got = Config::from_str(src).unwrap();
1148 let html_config = got.html_config().unwrap();
1149 assert!(!html_config.print.enable);
1150 assert!(html_config.print.page_break);
1151 let src = r#"
1152 [output.html.print]
1153 page-break = false
1154 "#;
1155 let got = Config::from_str(src).unwrap();
1156 let html_config = got.html_config().unwrap();
1157 assert!(html_config.print.enable);
1158 assert!(!html_config.print.page_break);
1159 }
1160
1161 #[test]
1162 fn test_json_direction() {
1163 use serde_json::json;
1164 assert_eq!(json!(TextDirection::RightToLeft), json!("rtl"));
1165 assert_eq!(json!(TextDirection::LeftToRight), json!("ltr"));
1166 }
1167
1168 #[test]
1169 fn get_deserialize_error() {
1170 let src = r#"
1171 [preprocessor.foo]
1172 x = 123
1173 "#;
1174 let cfg = Config::from_str(src).unwrap();
1175 let err = cfg.get::<String>("preprocessor.foo.x").unwrap_err();
1176 assert_eq!(
1177 err.to_string(),
1178 "Failed to deserialize `preprocessor.foo.x`"
1179 );
1180 }
1181
1182 #[test]
1183 fn contains_key() {
1184 let src = r#"
1185 [preprocessor.foo]
1186 x = 123
1187 [output.foo.sub]
1188 y = 'x'
1189 "#;
1190 let cfg = Config::from_str(src).unwrap();
1191 assert!(cfg.contains_key("preprocessor.foo"));
1192 assert!(cfg.contains_key("preprocessor.foo.x"));
1193 assert!(!cfg.contains_key("preprocessor.bar"));
1194 assert!(!cfg.contains_key("preprocessor.foo.y"));
1195 assert!(cfg.contains_key("output.foo"));
1196 assert!(cfg.contains_key("output.foo.sub"));
1197 assert!(cfg.contains_key("output.foo.sub.y"));
1198 assert!(!cfg.contains_key("output.bar"));
1199 assert!(!cfg.contains_key("output.foo.sub.z"));
1200 }
1201}