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