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(|| "Failed to deserialize `{name}`")
208 })
209 .transpose()
210 }
211
212 pub fn preprocessors<'de, T: Deserialize<'de>>(&self) -> Result<BTreeMap<String, T>> {
214 self.preprocessor
215 .clone()
216 .try_into()
217 .with_context(|| "Failed to read preprocessors")
218 }
219
220 pub fn outputs<'de, T: Deserialize<'de>>(&self) -> Result<BTreeMap<String, T>> {
222 self.output
223 .clone()
224 .try_into()
225 .with_context(|| "Failed to read renderers")
226 }
227
228 #[doc(hidden)]
235 pub fn html_config(&self) -> Option<HtmlConfig> {
236 match self.get("output.html") {
237 Ok(Some(config)) => Some(config),
238 Ok(None) => None,
239 Err(e) => {
240 log_backtrace(&e);
241 None
242 }
243 }
244 }
245
246 pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
255 let index = index.as_ref();
256
257 let value = Value::try_from(value)
258 .with_context(|| "Unable to represent the item as a JSON Value")?;
259
260 if let Some(key) = index.strip_prefix("book.") {
261 self.book.update_value(key, value);
262 } else if let Some(key) = index.strip_prefix("build.") {
263 self.build.update_value(key, value);
264 } else if let Some(key) = index.strip_prefix("rust.") {
265 self.rust.update_value(key, value);
266 } else if let Some(key) = index.strip_prefix("output.") {
267 self.output.update_value(key, value);
268 } else if let Some(key) = index.strip_prefix("preprocessor.") {
269 self.preprocessor.update_value(key, value);
270 } else {
271 bail!("invalid key `{index}`");
272 }
273
274 Ok(())
275 }
276}
277
278fn parse_env(key: &str) -> Option<String> {
279 key.strip_prefix("MDBOOK_")
280 .map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
287#[non_exhaustive]
288pub struct BookConfig {
289 pub title: Option<String>,
291 pub authors: Vec<String>,
293 pub description: Option<String>,
295 #[serde(skip_serializing_if = "is_default_src")]
297 pub src: PathBuf,
298 pub language: Option<String>,
300 pub text_direction: Option<TextDirection>,
303}
304
305fn is_default_src(src: &PathBuf) -> bool {
307 src == Path::new("src")
308}
309
310impl Default for BookConfig {
311 fn default() -> BookConfig {
312 BookConfig {
313 title: None,
314 authors: Vec::new(),
315 description: None,
316 src: PathBuf::from("src"),
317 language: Some(String::from("en")),
318 text_direction: None,
319 }
320 }
321}
322
323impl BookConfig {
324 pub fn realized_text_direction(&self) -> TextDirection {
327 if let Some(direction) = self.text_direction {
328 direction
329 } else {
330 TextDirection::from_lang_code(self.language.as_deref().unwrap_or_default())
331 }
332 }
333}
334
335#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
337#[non_exhaustive]
338pub enum TextDirection {
339 #[serde(rename = "ltr")]
341 LeftToRight,
342 #[serde(rename = "rtl")]
344 RightToLeft,
345}
346
347impl TextDirection {
348 pub fn from_lang_code(code: &str) -> Self {
350 match code {
351 "ar" | "ara" | "arc" | "ae" | "ave" | "egy" | "he" | "heb" | "nqo" | "pal" | "phn"
353 | "sam" | "syc" | "syr" | "fa" | "per" | "fas" | "ku" | "kur" | "ur" | "urd"
354 | "pus" | "ps" | "yi" | "yid" => TextDirection::RightToLeft,
355 _ => TextDirection::LeftToRight,
356 }
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
362#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
363#[non_exhaustive]
364pub struct BuildConfig {
365 pub build_dir: PathBuf,
367 pub create_missing: bool,
370 pub use_default_preprocessors: bool,
373 pub extra_watch_dirs: Vec<PathBuf>,
375}
376
377impl Default for BuildConfig {
378 fn default() -> BuildConfig {
379 BuildConfig {
380 build_dir: PathBuf::from("book"),
381 create_missing: true,
382 use_default_preprocessors: true,
383 extra_watch_dirs: Vec::new(),
384 }
385 }
386}
387
388#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
390#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
391#[non_exhaustive]
392pub struct RustConfig {
393 pub edition: Option<RustEdition>,
395}
396
397#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
399#[non_exhaustive]
400pub enum RustEdition {
401 #[serde(rename = "2024")]
403 E2024,
404 #[serde(rename = "2021")]
406 E2021,
407 #[serde(rename = "2018")]
409 E2018,
410 #[serde(rename = "2015")]
412 E2015,
413}
414
415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
417#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
418#[non_exhaustive]
419pub struct HtmlConfig {
420 pub theme: Option<PathBuf>,
422 pub default_theme: Option<String>,
424 pub preferred_dark_theme: Option<String>,
427 pub smart_punctuation: bool,
429 pub definition_lists: bool,
431 pub admonitions: bool,
433 pub mathjax_support: bool,
435 pub additional_css: Vec<PathBuf>,
437 pub additional_js: Vec<PathBuf>,
440 pub fold: Fold,
442 #[serde(alias = "playpen")]
444 pub playground: Playground,
445 pub code: Code,
447 pub print: Print,
449 pub no_section_label: bool,
451 pub search: Option<Search>,
453 pub git_repository_url: Option<String>,
455 pub git_repository_icon: Option<String>,
458 pub input_404: Option<String>,
460 pub site_url: Option<String>,
462 pub cname: Option<String>,
469 pub edit_url_template: Option<String>,
473 #[doc(hidden)]
479 pub live_reload_endpoint: Option<String>,
480 pub redirect: HashMap<String, String>,
483 pub hash_files: bool,
488 pub sidebar_header_nav: bool,
491}
492
493impl Default for HtmlConfig {
494 fn default() -> HtmlConfig {
495 HtmlConfig {
496 theme: None,
497 default_theme: None,
498 preferred_dark_theme: None,
499 smart_punctuation: true,
500 definition_lists: true,
501 admonitions: true,
502 mathjax_support: false,
503 additional_css: Vec::new(),
504 additional_js: Vec::new(),
505 fold: Fold::default(),
506 playground: Playground::default(),
507 code: Code::default(),
508 print: Print::default(),
509 no_section_label: false,
510 search: None,
511 git_repository_url: None,
512 git_repository_icon: None,
513 input_404: None,
514 site_url: None,
515 cname: None,
516 edit_url_template: None,
517 live_reload_endpoint: None,
518 redirect: HashMap::new(),
519 hash_files: true,
520 sidebar_header_nav: true,
521 }
522 }
523}
524
525impl HtmlConfig {
526 pub fn theme_dir(&self, root: &Path) -> PathBuf {
529 match self.theme {
530 Some(ref d) => root.join(d),
531 None => root.join("theme"),
532 }
533 }
534
535 pub fn get_404_output_file(&self) -> String {
537 self.input_404
538 .as_ref()
539 .unwrap_or(&"404.md".to_string())
540 .replace(".md", ".html")
541 }
542}
543
544#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
546#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
547#[non_exhaustive]
548pub struct Print {
549 pub enable: bool,
551 pub page_break: bool,
553}
554
555impl Default for Print {
556 fn default() -> Self {
557 Self {
558 enable: true,
559 page_break: true,
560 }
561 }
562}
563
564#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
566#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
567#[non_exhaustive]
568pub struct Fold {
569 pub enable: bool,
571 pub level: u8,
575}
576
577#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
579#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
580#[non_exhaustive]
581pub struct Playground {
582 pub editable: bool,
584 pub copyable: bool,
586 pub copy_js: bool,
589 pub line_numbers: bool,
591 pub runnable: bool,
593}
594
595impl Default for Playground {
596 fn default() -> Playground {
597 Playground {
598 editable: false,
599 copyable: true,
600 copy_js: true,
601 line_numbers: false,
602 runnable: true,
603 }
604 }
605}
606
607#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
609#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
610#[non_exhaustive]
611pub struct Code {
612 pub hidelines: HashMap<String, String>,
614}
615
616#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
618#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
619#[non_exhaustive]
620pub struct Search {
621 pub enable: bool,
623 pub limit_results: u32,
625 pub teaser_word_count: u32,
627 pub use_boolean_and: bool,
630 pub boost_title: u8,
633 pub boost_hierarchy: u8,
637 pub boost_paragraph: u8,
640 pub expand: bool,
642 pub heading_split_level: u8,
645 pub copy_js: bool,
648 pub chapter: HashMap<String, SearchChapterSettings>,
653}
654
655impl Default for Search {
656 fn default() -> Search {
657 Search {
659 enable: true,
660 limit_results: 30,
661 teaser_word_count: 30,
662 use_boolean_and: false,
663 boost_title: 2,
664 boost_hierarchy: 1,
665 boost_paragraph: 1,
666 expand: true,
667 heading_split_level: 3,
668 copy_js: true,
669 chapter: HashMap::new(),
670 }
671 }
672}
673
674#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
676#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
677#[non_exhaustive]
678pub struct SearchChapterSettings {
679 pub enable: Option<bool>,
681}
682
683trait Updateable<'de>: Serialize + Deserialize<'de> {
689 fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
690 let mut raw = Value::try_from(&self).expect("unreachable");
691
692 if let Ok(value) = Value::try_from(value) {
693 raw.insert(key, value);
694 } else {
695 return;
696 }
697
698 if let Ok(updated) = raw.try_into() {
699 *self = updated;
700 }
701 }
702}
703
704impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709
710 const COMPLEX_CONFIG: &str = r#"
711 [book]
712 title = "Some Book"
713 authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
714 description = "A completely useless book"
715 src = "source"
716 language = "ja"
717
718 [build]
719 build-dir = "outputs"
720 create-missing = false
721 use-default-preprocessors = true
722
723 [output.html]
724 theme = "./themedir"
725 default-theme = "rust"
726 smart-punctuation = true
727 additional-css = ["./foo/bar/baz.css"]
728 git-repository-url = "https://foo.com/"
729 git-repository-icon = "fa-code-fork"
730
731 [output.html.playground]
732 editable = true
733
734 [output.html.redirect]
735 "index.html" = "overview.html"
736 "nexted/page.md" = "https://rust-lang.org/"
737
738 [preprocessor.first]
739
740 [preprocessor.second]
741 "#;
742
743 #[test]
744 fn load_a_complex_config_file() {
745 let src = COMPLEX_CONFIG;
746
747 let book_should_be = BookConfig {
748 title: Some(String::from("Some Book")),
749 authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
750 description: Some(String::from("A completely useless book")),
751 src: PathBuf::from("source"),
752 language: Some(String::from("ja")),
753 text_direction: None,
754 };
755 let build_should_be = BuildConfig {
756 build_dir: PathBuf::from("outputs"),
757 create_missing: false,
758 use_default_preprocessors: true,
759 extra_watch_dirs: Vec::new(),
760 };
761 let rust_should_be = RustConfig { edition: None };
762 let playground_should_be = Playground {
763 editable: true,
764 copyable: true,
765 copy_js: true,
766 line_numbers: false,
767 runnable: true,
768 };
769 let html_should_be = HtmlConfig {
770 smart_punctuation: true,
771 additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
772 theme: Some(PathBuf::from("./themedir")),
773 default_theme: Some(String::from("rust")),
774 playground: playground_should_be,
775 git_repository_url: Some(String::from("https://foo.com/")),
776 git_repository_icon: Some(String::from("fa-code-fork")),
777 redirect: vec![
778 (String::from("index.html"), String::from("overview.html")),
779 (
780 String::from("nexted/page.md"),
781 String::from("https://rust-lang.org/"),
782 ),
783 ]
784 .into_iter()
785 .collect(),
786 ..Default::default()
787 };
788
789 let got = Config::from_str(src).unwrap();
790
791 assert_eq!(got.book, book_should_be);
792 assert_eq!(got.build, build_should_be);
793 assert_eq!(got.rust, rust_should_be);
794 assert_eq!(got.html_config().unwrap(), html_should_be);
795 }
796
797 #[test]
798 fn disable_runnable() {
799 let src = r#"
800 [book]
801 title = "Some Book"
802 description = "book book book"
803 authors = ["Shogo Takata"]
804
805 [output.html.playground]
806 runnable = false
807 "#;
808
809 let got = Config::from_str(src).unwrap();
810 assert!(!got.html_config().unwrap().playground.runnable);
811 }
812
813 #[test]
814 fn edition_2015() {
815 let src = r#"
816 [book]
817 title = "mdBook Documentation"
818 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
819 authors = ["Mathieu David"]
820 src = "./source"
821 [rust]
822 edition = "2015"
823 "#;
824
825 let book_should_be = BookConfig {
826 title: Some(String::from("mdBook Documentation")),
827 description: Some(String::from(
828 "Create book from markdown files. Like Gitbook but implemented in Rust",
829 )),
830 authors: vec![String::from("Mathieu David")],
831 src: PathBuf::from("./source"),
832 ..Default::default()
833 };
834
835 let got = Config::from_str(src).unwrap();
836 assert_eq!(got.book, book_should_be);
837
838 let rust_should_be = RustConfig {
839 edition: Some(RustEdition::E2015),
840 };
841 let got = Config::from_str(src).unwrap();
842 assert_eq!(got.rust, rust_should_be);
843 }
844
845 #[test]
846 fn edition_2018() {
847 let src = r#"
848 [book]
849 title = "mdBook Documentation"
850 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
851 authors = ["Mathieu David"]
852 src = "./source"
853 [rust]
854 edition = "2018"
855 "#;
856
857 let rust_should_be = RustConfig {
858 edition: Some(RustEdition::E2018),
859 };
860
861 let got = Config::from_str(src).unwrap();
862 assert_eq!(got.rust, rust_should_be);
863 }
864
865 #[test]
866 fn edition_2021() {
867 let src = r#"
868 [book]
869 title = "mdBook Documentation"
870 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
871 authors = ["Mathieu David"]
872 src = "./source"
873 [rust]
874 edition = "2021"
875 "#;
876
877 let rust_should_be = RustConfig {
878 edition: Some(RustEdition::E2021),
879 };
880
881 let got = Config::from_str(src).unwrap();
882 assert_eq!(got.rust, rust_should_be);
883 }
884
885 #[test]
886 fn load_arbitrary_output_type() {
887 #[derive(Debug, Deserialize, PartialEq)]
888 struct RandomOutput {
889 foo: u32,
890 bar: String,
891 baz: Vec<bool>,
892 }
893
894 let src = r#"
895 [output.random]
896 foo = 5
897 bar = "Hello World"
898 baz = [true, true, false]
899 "#;
900
901 let should_be = RandomOutput {
902 foo: 5,
903 bar: String::from("Hello World"),
904 baz: vec![true, true, false],
905 };
906
907 let cfg = Config::from_str(src).unwrap();
908 let got: RandomOutput = cfg.get("output.random").unwrap().unwrap();
909
910 assert_eq!(got, should_be);
911
912 let got_baz: Vec<bool> = cfg.get("output.random.baz").unwrap().unwrap();
913 let baz_should_be = vec![true, true, false];
914
915 assert_eq!(got_baz, baz_should_be);
916 }
917
918 #[test]
919 fn set_special_tables() {
920 let mut cfg = Config::default();
921 assert_eq!(cfg.book.title, None);
922 cfg.set("book.title", "my title").unwrap();
923 assert_eq!(cfg.book.title, Some("my title".to_string()));
924
925 assert_eq!(&cfg.build.build_dir, Path::new("book"));
926 cfg.set("build.build-dir", "some-directory").unwrap();
927 assert_eq!(&cfg.build.build_dir, Path::new("some-directory"));
928
929 assert_eq!(cfg.rust.edition, None);
930 cfg.set("rust.edition", "2024").unwrap();
931 assert_eq!(cfg.rust.edition, Some(RustEdition::E2024));
932
933 cfg.set("output.foo.value", "123").unwrap();
934 let got: String = cfg.get("output.foo.value").unwrap().unwrap();
935 assert_eq!(got, "123");
936
937 cfg.set("preprocessor.bar.value", "456").unwrap();
938 let got: String = cfg.get("preprocessor.bar.value").unwrap().unwrap();
939 assert_eq!(got, "456");
940 }
941
942 #[test]
943 fn set_invalid_keys() {
944 let mut cfg = Config::default();
945 let err = cfg.set("foo", "test").unwrap_err();
946 assert!(err.to_string().contains("invalid key `foo`"));
947 }
948
949 #[test]
950 fn parse_env_vars() {
951 let inputs = vec![
952 ("FOO", None),
953 ("MDBOOK_foo", Some("foo")),
954 ("MDBOOK_FOO__bar__baz", Some("foo.bar.baz")),
955 ("MDBOOK_FOO_bar__baz", Some("foo-bar.baz")),
956 ];
957
958 for (src, should_be) in inputs {
959 let got = parse_env(src);
960 let should_be = should_be.map(ToString::to_string);
961
962 assert_eq!(got, should_be);
963 }
964 }
965
966 #[test]
967 fn file_404_default() {
968 let src = r#"
969 [output.html]
970 "#;
971
972 let got = Config::from_str(src).unwrap();
973 let html_config = got.html_config().unwrap();
974 assert_eq!(html_config.input_404, None);
975 assert_eq!(html_config.get_404_output_file(), "404.html");
976 }
977
978 #[test]
979 fn file_404_custom() {
980 let src = r#"
981 [output.html]
982 input-404= "missing.md"
983 "#;
984
985 let got = Config::from_str(src).unwrap();
986 let html_config = got.html_config().unwrap();
987 assert_eq!(html_config.input_404, Some("missing.md".to_string()));
988 assert_eq!(html_config.get_404_output_file(), "missing.html");
989 }
990
991 #[test]
992 fn text_direction_ltr() {
993 let src = r#"
994 [book]
995 text-direction = "ltr"
996 "#;
997
998 let got = Config::from_str(src).unwrap();
999 assert_eq!(got.book.text_direction, Some(TextDirection::LeftToRight));
1000 }
1001
1002 #[test]
1003 fn text_direction_rtl() {
1004 let src = r#"
1005 [book]
1006 text-direction = "rtl"
1007 "#;
1008
1009 let got = Config::from_str(src).unwrap();
1010 assert_eq!(got.book.text_direction, Some(TextDirection::RightToLeft));
1011 }
1012
1013 #[test]
1014 fn text_direction_none() {
1015 let src = r#"
1016 [book]
1017 "#;
1018
1019 let got = Config::from_str(src).unwrap();
1020 assert_eq!(got.book.text_direction, None);
1021 }
1022
1023 #[test]
1024 fn test_text_direction() {
1025 let mut cfg = BookConfig::default();
1026
1027 cfg.language = Some("ar".into());
1029 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1030
1031 cfg.language = Some("he".into());
1032 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1033
1034 cfg.language = Some("en".into());
1035 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1036
1037 cfg.language = Some("ja".into());
1038 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1039
1040 cfg.language = Some("ar".into());
1042 cfg.text_direction = Some(TextDirection::LeftToRight);
1043 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1044
1045 cfg.language = Some("ar".into());
1046 cfg.text_direction = Some(TextDirection::RightToLeft);
1047 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1048
1049 cfg.language = Some("en".into());
1050 cfg.text_direction = Some(TextDirection::LeftToRight);
1051 assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1052
1053 cfg.language = Some("en".into());
1054 cfg.text_direction = Some(TextDirection::RightToLeft);
1055 assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1056 }
1057
1058 #[test]
1059 #[should_panic(expected = "Invalid configuration file")]
1060 fn invalid_language_type_error() {
1061 let src = r#"
1062 [book]
1063 title = "mdBook Documentation"
1064 language = ["en", "pt-br"]
1065 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
1066 authors = ["Mathieu David"]
1067 src = "./source"
1068 "#;
1069
1070 Config::from_str(src).unwrap();
1071 }
1072
1073 #[test]
1074 #[should_panic(expected = "Invalid configuration file")]
1075 fn invalid_title_type() {
1076 let src = r#"
1077 [book]
1078 title = 20
1079 language = "en"
1080 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
1081 authors = ["Mathieu David"]
1082 src = "./source"
1083 "#;
1084
1085 Config::from_str(src).unwrap();
1086 }
1087
1088 #[test]
1089 #[should_panic(expected = "Invalid configuration file")]
1090 fn invalid_build_dir_type() {
1091 let src = r#"
1092 [build]
1093 build-dir = 99
1094 create-missing = false
1095 "#;
1096
1097 Config::from_str(src).unwrap();
1098 }
1099
1100 #[test]
1101 #[should_panic(expected = "Invalid configuration file")]
1102 fn invalid_rust_edition() {
1103 let src = r#"
1104 [rust]
1105 edition = "1999"
1106 "#;
1107
1108 Config::from_str(src).unwrap();
1109 }
1110
1111 #[test]
1112 #[should_panic(
1113 expected = "unknown variant `1999`, expected one of `2024`, `2021`, `2018`, `2015`\n"
1114 )]
1115 fn invalid_rust_edition_expected() {
1116 let src = r#"
1117 [rust]
1118 edition = "1999"
1119 "#;
1120
1121 Config::from_str(src).unwrap();
1122 }
1123
1124 #[test]
1125 fn print_config() {
1126 let src = r#"
1127 [output.html.print]
1128 enable = false
1129 "#;
1130 let got = Config::from_str(src).unwrap();
1131 let html_config = got.html_config().unwrap();
1132 assert!(!html_config.print.enable);
1133 assert!(html_config.print.page_break);
1134 let src = r#"
1135 [output.html.print]
1136 page-break = false
1137 "#;
1138 let got = Config::from_str(src).unwrap();
1139 let html_config = got.html_config().unwrap();
1140 assert!(html_config.print.enable);
1141 assert!(!html_config.print.page_break);
1142 }
1143
1144 #[test]
1145 fn test_json_direction() {
1146 use serde_json::json;
1147 assert_eq!(json!(TextDirection::RightToLeft), json!("rtl"));
1148 assert_eq!(json!(TextDirection::LeftToRight), json!("ltr"));
1149 }
1150}