1 | //! Mdbook's configuration system. |
2 | //! |
3 | //! The main entrypoint of the `config` module is the `Config` struct. This acts |
4 | //! essentially as a bag of configuration information, with a couple |
5 | //! pre-determined tables ([`BookConfig`] and [`BuildConfig`]) as well as support |
6 | //! for arbitrary data which is exposed to plugins and alternative backends. |
7 | //! |
8 | //! |
9 | //! # Examples |
10 | //! |
11 | //! ```rust |
12 | //! # use mdbook::errors::*; |
13 | //! use std::path::PathBuf; |
14 | //! use std::str::FromStr; |
15 | //! use mdbook::Config; |
16 | //! use toml::Value; |
17 | //! |
18 | //! # fn run() -> Result<()> { |
19 | //! let src = r#" |
20 | //! [book] |
21 | //! title = "My Book" |
22 | //! authors = ["Michael-F-Bryan"] |
23 | //! |
24 | //! [build] |
25 | //! src = "out" |
26 | //! |
27 | //! [other-table.foo] |
28 | //! bar = 123 |
29 | //! "# ; |
30 | //! |
31 | //! // load the `Config` from a toml string |
32 | //! let mut cfg = Config::from_str(src)?; |
33 | //! |
34 | //! // retrieve a nested value |
35 | //! let bar = cfg.get("other-table.foo.bar" ).cloned(); |
36 | //! assert_eq!(bar, Some(Value::Integer(123))); |
37 | //! |
38 | //! // Set the `output.html.theme` directory |
39 | //! assert!(cfg.get("output.html" ).is_none()); |
40 | //! cfg.set("output.html.theme" , "./themes" ); |
41 | //! |
42 | //! // then load it again, automatically deserializing to a `PathBuf`. |
43 | //! let got: Option<PathBuf> = cfg.get_deserialized_opt("output.html.theme" )?; |
44 | //! assert_eq!(got, Some(PathBuf::from("./themes" ))); |
45 | //! # Ok(()) |
46 | //! # } |
47 | //! # run().unwrap() |
48 | //! ``` |
49 | |
50 | #![deny (missing_docs)] |
51 | |
52 | use log::{debug, trace, warn}; |
53 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; |
54 | use std::collections::HashMap; |
55 | use std::env; |
56 | use std::fs::File; |
57 | use std::io::Read; |
58 | use std::path::{Path, PathBuf}; |
59 | use std::str::FromStr; |
60 | use toml::value::Table; |
61 | use toml::{self, Value}; |
62 | |
63 | use crate::errors::*; |
64 | use crate::utils::{self, toml_ext::TomlExt}; |
65 | |
66 | /// The overall configuration object for MDBook, essentially an in-memory |
67 | /// representation of `book.toml`. |
68 | #[derive (Debug, Clone, PartialEq)] |
69 | pub struct Config { |
70 | /// Metadata about the book. |
71 | pub book: BookConfig, |
72 | /// Information about the build environment. |
73 | pub build: BuildConfig, |
74 | /// Information about Rust language support. |
75 | pub rust: RustConfig, |
76 | rest: Value, |
77 | } |
78 | |
79 | impl FromStr for Config { |
80 | type Err = Error; |
81 | |
82 | /// Load a `Config` from some string. |
83 | fn from_str(src: &str) -> Result<Self> { |
84 | toml::from_str(src).with_context(|| "Invalid configuration file" ) |
85 | } |
86 | } |
87 | |
88 | impl Config { |
89 | /// Load the configuration file from disk. |
90 | pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> { |
91 | let mut buffer = String::new(); |
92 | File::open(config_file) |
93 | .with_context(|| "Unable to open the configuration file" )? |
94 | .read_to_string(&mut buffer) |
95 | .with_context(|| "Couldn't read the file" )?; |
96 | |
97 | Config::from_str(&buffer) |
98 | } |
99 | |
100 | /// Updates the `Config` from the available environment variables. |
101 | /// |
102 | /// Variables starting with `MDBOOK_` are used for configuration. The key is |
103 | /// created by removing the `MDBOOK_` prefix and turning the resulting |
104 | /// string into `kebab-case`. Double underscores (`__`) separate nested |
105 | /// keys, while a single underscore (`_`) is replaced with a dash (`-`). |
106 | /// |
107 | /// For example: |
108 | /// |
109 | /// - `MDBOOK_foo` -> `foo` |
110 | /// - `MDBOOK_FOO` -> `foo` |
111 | /// - `MDBOOK_FOO__BAR` -> `foo.bar` |
112 | /// - `MDBOOK_FOO_BAR` -> `foo-bar` |
113 | /// - `MDBOOK_FOO_bar__baz` -> `foo-bar.baz` |
114 | /// |
115 | /// So by setting the `MDBOOK_BOOK__TITLE` environment variable you can |
116 | /// override the book's title without needing to touch your `book.toml`. |
117 | /// |
118 | /// > **Note:** To facilitate setting more complex config items, the value |
119 | /// > of an environment variable is first parsed as JSON, falling back to a |
120 | /// > string if the parse fails. |
121 | /// > |
122 | /// > This means, if you so desired, you could override all book metadata |
123 | /// > when building the book with something like |
124 | /// > |
125 | /// > ```text |
126 | /// > $ export MDBOOK_BOOK='{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}' |
127 | /// > $ mdbook build |
128 | /// > ``` |
129 | /// |
130 | /// The latter case may be useful in situations where `mdbook` is invoked |
131 | /// from a script or CI, where it sometimes isn't possible to update the |
132 | /// `book.toml` before building. |
133 | pub fn update_from_env(&mut self) { |
134 | debug!("Updating the config from environment variables" ); |
135 | |
136 | let overrides = |
137 | env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value))); |
138 | |
139 | for (key, value) in overrides { |
140 | trace!(" {} => {}" , key, value); |
141 | let parsed_value = serde_json::from_str(&value) |
142 | .unwrap_or_else(|_| serde_json::Value::String(value.to_string())); |
143 | |
144 | if key == "book" || key == "build" { |
145 | if let serde_json::Value::Object(ref map) = parsed_value { |
146 | // To `set` each `key`, we wrap them as `prefix.key` |
147 | for (k, v) in map { |
148 | let full_key = format!(" {}. {}" , key, k); |
149 | self.set(&full_key, v).expect("unreachable" ); |
150 | } |
151 | return; |
152 | } |
153 | } |
154 | |
155 | self.set(key, parsed_value).expect("unreachable" ); |
156 | } |
157 | } |
158 | |
159 | /// Fetch an arbitrary item from the `Config` as a `toml::Value`. |
160 | /// |
161 | /// You can use dotted indices to access nested items (e.g. |
162 | /// `output.html.playground` will fetch the "playground" out of the html output |
163 | /// table). |
164 | pub fn get(&self, key: &str) -> Option<&Value> { |
165 | self.rest.read(key) |
166 | } |
167 | |
168 | /// Fetch a value from the `Config` so you can mutate it. |
169 | pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> { |
170 | self.rest.read_mut(key) |
171 | } |
172 | |
173 | /// Convenience method for getting the html renderer's configuration. |
174 | /// |
175 | /// # Note |
176 | /// |
177 | /// This is for compatibility only. It will be removed completely once the |
178 | /// HTML renderer is refactored to be less coupled to `mdbook` internals. |
179 | #[doc (hidden)] |
180 | pub fn html_config(&self) -> Option<HtmlConfig> { |
181 | match self |
182 | .get_deserialized_opt("output.html" ) |
183 | .with_context(|| "Parsing configuration [output.html]" ) |
184 | { |
185 | Ok(Some(config)) => Some(config), |
186 | Ok(None) => None, |
187 | Err(e) => { |
188 | utils::log_backtrace(&e); |
189 | None |
190 | } |
191 | } |
192 | } |
193 | |
194 | /// Deprecated, use get_deserialized_opt instead. |
195 | #[deprecated = "use get_deserialized_opt instead" ] |
196 | pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> { |
197 | let name = name.as_ref(); |
198 | match self.get_deserialized_opt(name)? { |
199 | Some(value) => Ok(value), |
200 | None => bail!("Key not found, {:?}" , name), |
201 | } |
202 | } |
203 | |
204 | /// Convenience function to fetch a value from the config and deserialize it |
205 | /// into some arbitrary type. |
206 | pub fn get_deserialized_opt<'de, T: Deserialize<'de>, S: AsRef<str>>( |
207 | &self, |
208 | name: S, |
209 | ) -> Result<Option<T>> { |
210 | let name = name.as_ref(); |
211 | self.get(name) |
212 | .map(|value| { |
213 | value |
214 | .clone() |
215 | .try_into() |
216 | .with_context(|| "Couldn't deserialize the value" ) |
217 | }) |
218 | .transpose() |
219 | } |
220 | |
221 | /// Set a config key, clobbering any existing values along the way. |
222 | /// |
223 | /// The only way this can fail is if we can't serialize `value` into a |
224 | /// `toml::Value`. |
225 | pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> { |
226 | let index = index.as_ref(); |
227 | |
228 | let value = Value::try_from(value) |
229 | .with_context(|| "Unable to represent the item as a JSON Value" )?; |
230 | |
231 | if let Some(key) = index.strip_prefix("book." ) { |
232 | self.book.update_value(key, value); |
233 | } else if let Some(key) = index.strip_prefix("build." ) { |
234 | self.build.update_value(key, value); |
235 | } else { |
236 | self.rest.insert(index, value); |
237 | } |
238 | |
239 | Ok(()) |
240 | } |
241 | |
242 | /// Get the table associated with a particular renderer. |
243 | pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> { |
244 | let key = format!("output. {}" , index.as_ref()); |
245 | self.get(&key).and_then(Value::as_table) |
246 | } |
247 | |
248 | /// Get the table associated with a particular preprocessor. |
249 | pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> { |
250 | let key = format!("preprocessor. {}" , index.as_ref()); |
251 | self.get(&key).and_then(Value::as_table) |
252 | } |
253 | |
254 | fn from_legacy(mut table: Value) -> Config { |
255 | let mut cfg = Config::default(); |
256 | |
257 | // we use a macro here instead of a normal loop because the $out |
258 | // variable can be different types. This way we can make type inference |
259 | // figure out what try_into() deserializes to. |
260 | macro_rules! get_and_insert { |
261 | ($table:expr, $key:expr => $out:expr) => { |
262 | let got = $table |
263 | .as_table_mut() |
264 | .and_then(|t| t.remove($key)) |
265 | .and_then(|v| v.try_into().ok()); |
266 | if let Some(value) = got { |
267 | $out = value; |
268 | } |
269 | }; |
270 | } |
271 | |
272 | get_and_insert!(table, "title" => cfg.book.title); |
273 | get_and_insert!(table, "authors" => cfg.book.authors); |
274 | get_and_insert!(table, "source" => cfg.book.src); |
275 | get_and_insert!(table, "description" => cfg.book.description); |
276 | |
277 | if let Some(dest) = table.delete("output.html.destination" ) { |
278 | if let Ok(destination) = dest.try_into() { |
279 | cfg.build.build_dir = destination; |
280 | } |
281 | } |
282 | |
283 | cfg.rest = table; |
284 | cfg |
285 | } |
286 | } |
287 | |
288 | impl Default for Config { |
289 | fn default() -> Config { |
290 | Config { |
291 | book: BookConfig::default(), |
292 | build: BuildConfig::default(), |
293 | rust: RustConfig::default(), |
294 | rest: Value::Table(Table::default()), |
295 | } |
296 | } |
297 | } |
298 | |
299 | impl<'de> serde::Deserialize<'de> for Config { |
300 | fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> { |
301 | let raw = Value::deserialize(de)?; |
302 | |
303 | if is_legacy_format(&raw) { |
304 | warn!("It looks like you are using the legacy book.toml format." ); |
305 | warn!("We'll parse it for now, but you should probably convert to the new format." ); |
306 | warn!("See the mdbook documentation for more details, although as a rule of thumb" ); |
307 | warn!("just move all top level configuration entries like `title`, `author` and" ); |
308 | warn!("`description` under a table called `[book]`, move the `destination` entry" ); |
309 | warn!("from `[output.html]`, renamed to `build-dir`, under a table called" ); |
310 | warn!("`[build]`, and it should all work." ); |
311 | warn!("Documentation: https://rust-lang.github.io/mdBook/format/config.html" ); |
312 | return Ok(Config::from_legacy(raw)); |
313 | } |
314 | |
315 | use serde::de::Error; |
316 | let mut table = match raw { |
317 | Value::Table(t) => t, |
318 | _ => { |
319 | return Err(D::Error::custom( |
320 | "A config file should always be a toml table" , |
321 | )); |
322 | } |
323 | }; |
324 | |
325 | let book: BookConfig = table |
326 | .remove("book" ) |
327 | .map(|book| book.try_into().map_err(D::Error::custom)) |
328 | .transpose()? |
329 | .unwrap_or_default(); |
330 | |
331 | let build: BuildConfig = table |
332 | .remove("build" ) |
333 | .map(|build| build.try_into().map_err(D::Error::custom)) |
334 | .transpose()? |
335 | .unwrap_or_default(); |
336 | |
337 | let rust: RustConfig = table |
338 | .remove("rust" ) |
339 | .map(|rust| rust.try_into().map_err(D::Error::custom)) |
340 | .transpose()? |
341 | .unwrap_or_default(); |
342 | |
343 | Ok(Config { |
344 | book, |
345 | build, |
346 | rust, |
347 | rest: Value::Table(table), |
348 | }) |
349 | } |
350 | } |
351 | |
352 | impl Serialize for Config { |
353 | fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> { |
354 | // TODO: This should probably be removed and use a derive instead. |
355 | let mut table: Value = self.rest.clone(); |
356 | |
357 | let book_config: Value = Value::try_from(&self.book).expect(msg:"should always be serializable" ); |
358 | table.insert(key:"book" , value:book_config); |
359 | |
360 | if self.build != BuildConfig::default() { |
361 | let build_config: Value = Value::try_from(&self.build).expect(msg:"should always be serializable" ); |
362 | table.insert(key:"build" , value:build_config); |
363 | } |
364 | |
365 | if self.rust != RustConfig::default() { |
366 | let rust_config: Value = Value::try_from(&self.rust).expect(msg:"should always be serializable" ); |
367 | table.insert(key:"rust" , value:rust_config); |
368 | } |
369 | |
370 | table.serialize(serializer:s) |
371 | } |
372 | } |
373 | |
374 | fn parse_env(key: &str) -> Option<String> { |
375 | keyOption<&str>.strip_prefix("MDBOOK_" ) |
376 | .map(|key: &str| key.to_lowercase().replace("__" , "." ).replace(from:'_' , to:"-" )) |
377 | } |
378 | |
379 | fn is_legacy_format(table: &Value) -> bool { |
380 | let legacy_items: [&str; 5] = [ |
381 | "title" , |
382 | "authors" , |
383 | "source" , |
384 | "description" , |
385 | "output.html.destination" , |
386 | ]; |
387 | |
388 | for item: &&str in &legacy_items { |
389 | if table.read(key:item).is_some() { |
390 | return true; |
391 | } |
392 | } |
393 | |
394 | false |
395 | } |
396 | |
397 | /// Configuration options which are specific to the book and required for |
398 | /// loading it from disk. |
399 | #[derive (Debug, Clone, PartialEq, Serialize, Deserialize)] |
400 | #[serde(default, rename_all = "kebab-case" )] |
401 | pub struct BookConfig { |
402 | /// The book's title. |
403 | pub title: Option<String>, |
404 | /// The book's authors. |
405 | pub authors: Vec<String>, |
406 | /// An optional description for the book. |
407 | pub description: Option<String>, |
408 | /// Location of the book source relative to the book's root directory. |
409 | pub src: PathBuf, |
410 | /// Does this book support more than one language? |
411 | pub multilingual: bool, |
412 | /// The main language of the book. |
413 | pub language: Option<String>, |
414 | /// The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL). |
415 | /// When not specified, the text direction is derived from [`BookConfig::language`]. |
416 | pub text_direction: Option<TextDirection>, |
417 | } |
418 | |
419 | impl Default for BookConfig { |
420 | fn default() -> BookConfig { |
421 | BookConfig { |
422 | title: None, |
423 | authors: Vec::new(), |
424 | description: None, |
425 | src: PathBuf::from("src" ), |
426 | multilingual: false, |
427 | language: Some(String::from("en" )), |
428 | text_direction: None, |
429 | } |
430 | } |
431 | } |
432 | |
433 | impl BookConfig { |
434 | /// Gets the realized text direction, either from [`BookConfig::text_direction`] |
435 | /// or derived from [`BookConfig::language`], to be used by templating engines. |
436 | pub fn realized_text_direction(&self) -> TextDirection { |
437 | if let Some(direction: TextDirection) = self.text_direction { |
438 | direction |
439 | } else { |
440 | TextDirection::from_lang_code(self.language.as_deref().unwrap_or_default()) |
441 | } |
442 | } |
443 | } |
444 | |
445 | /// Text direction to use for HTML output |
446 | #[derive (Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] |
447 | pub enum TextDirection { |
448 | /// Left to right. |
449 | #[serde(rename = "ltr" )] |
450 | LeftToRight, |
451 | /// Right to left |
452 | #[serde(rename = "rtl" )] |
453 | RightToLeft, |
454 | } |
455 | |
456 | impl TextDirection { |
457 | /// Gets the text direction from language code |
458 | pub fn from_lang_code(code: &str) -> Self { |
459 | match code { |
460 | // list sourced from here: https://github.com/abarrak/rtl/blob/master/lib/rtl/core.rb#L16 |
461 | "ar" | "ara" | "arc" | "ae" | "ave" | "egy" | "he" | "heb" | "nqo" | "pal" | "phn" |
462 | | "sam" | "syc" | "syr" | "fa" | "per" | "fas" | "ku" | "kur" | "ur" | "urd" |
463 | | "pus" | "ps" | "yi" | "yid" => TextDirection::RightToLeft, |
464 | _ => TextDirection::LeftToRight, |
465 | } |
466 | } |
467 | } |
468 | |
469 | /// Configuration for the build procedure. |
470 | #[derive (Debug, Clone, PartialEq, Serialize, Deserialize)] |
471 | #[serde(default, rename_all = "kebab-case" )] |
472 | pub struct BuildConfig { |
473 | /// Where to put built artefacts relative to the book's root directory. |
474 | pub build_dir: PathBuf, |
475 | /// Should non-existent markdown files specified in `SUMMARY.md` be created |
476 | /// if they don't exist? |
477 | pub create_missing: bool, |
478 | /// Should the default preprocessors always be used when they are |
479 | /// compatible with the renderer? |
480 | pub use_default_preprocessors: bool, |
481 | /// Extra directories to trigger rebuild when watching/serving |
482 | pub extra_watch_dirs: Vec<PathBuf>, |
483 | } |
484 | |
485 | impl Default for BuildConfig { |
486 | fn default() -> BuildConfig { |
487 | BuildConfig { |
488 | build_dir: PathBuf::from("book" ), |
489 | create_missing: true, |
490 | use_default_preprocessors: true, |
491 | extra_watch_dirs: Vec::new(), |
492 | } |
493 | } |
494 | } |
495 | |
496 | /// Configuration for the Rust compiler(e.g., for playground) |
497 | #[derive (Debug, Default, Clone, PartialEq, Serialize, Deserialize)] |
498 | #[serde(default, rename_all = "kebab-case" )] |
499 | pub struct RustConfig { |
500 | /// Rust edition used in playground |
501 | pub edition: Option<RustEdition>, |
502 | } |
503 | |
504 | #[derive (Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] |
505 | /// Rust edition to use for the code. |
506 | pub enum RustEdition { |
507 | /// The 2021 edition of Rust |
508 | #[serde(rename = "2021" )] |
509 | E2021, |
510 | /// The 2018 edition of Rust |
511 | #[serde(rename = "2018" )] |
512 | E2018, |
513 | /// The 2015 edition of Rust |
514 | #[serde(rename = "2015" )] |
515 | E2015, |
516 | } |
517 | |
518 | /// Configuration for the HTML renderer. |
519 | #[derive (Debug, Clone, PartialEq, Serialize, Deserialize)] |
520 | #[serde(default, rename_all = "kebab-case" )] |
521 | pub struct HtmlConfig { |
522 | /// The theme directory, if specified. |
523 | pub theme: Option<PathBuf>, |
524 | /// The default theme to use, defaults to 'light' |
525 | pub default_theme: Option<String>, |
526 | /// The theme to use if the browser requests the dark version of the site. |
527 | /// Defaults to 'navy'. |
528 | pub preferred_dark_theme: Option<String>, |
529 | /// Use "smart quotes" instead of the usual `"` character. |
530 | pub curly_quotes: bool, |
531 | /// Should mathjax be enabled? |
532 | pub mathjax_support: bool, |
533 | /// Whether to fonts.css and respective font files to the output directory. |
534 | pub copy_fonts: bool, |
535 | /// An optional google analytics code. |
536 | pub google_analytics: Option<String>, |
537 | /// Additional CSS stylesheets to include in the rendered page's `<head>`. |
538 | pub additional_css: Vec<PathBuf>, |
539 | /// Additional JS scripts to include at the bottom of the rendered page's |
540 | /// `<body>`. |
541 | pub additional_js: Vec<PathBuf>, |
542 | /// Fold settings. |
543 | pub fold: Fold, |
544 | /// Playground settings. |
545 | #[serde(alias = "playpen" )] |
546 | pub playground: Playground, |
547 | /// Code settings. |
548 | pub code: Code, |
549 | /// Print settings. |
550 | pub print: Print, |
551 | /// Don't render section labels. |
552 | pub no_section_label: bool, |
553 | /// Search settings. If `None`, the default will be used. |
554 | pub search: Option<Search>, |
555 | /// Git repository url. If `None`, the git button will not be shown. |
556 | pub git_repository_url: Option<String>, |
557 | /// FontAwesome icon class to use for the Git repository link. |
558 | /// Defaults to `fa-github` if `None`. |
559 | pub git_repository_icon: Option<String>, |
560 | /// Input path for the 404 file, defaults to 404.md, set to "" to disable 404 file output |
561 | pub input_404: Option<String>, |
562 | /// Absolute url to site, used to emit correct paths for the 404 page, which might be accessed in a deeply nested directory |
563 | pub site_url: Option<String>, |
564 | /// The DNS subdomain or apex domain at which your book will be hosted. This |
565 | /// string will be written to a file named CNAME in the root of your site, |
566 | /// as required by GitHub Pages (see [*Managing a custom domain for your |
567 | /// GitHub Pages site*][custom domain]). |
568 | /// |
569 | /// [custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site |
570 | pub cname: Option<String>, |
571 | /// Edit url template, when set shows a "Suggest an edit" button for |
572 | /// directly jumping to editing the currently viewed page. |
573 | /// Contains {path} that is replaced with chapter source file path |
574 | pub edit_url_template: Option<String>, |
575 | /// Endpoint of websocket, for livereload usage. Value loaded from .toml |
576 | /// file is ignored, because our code overrides this field with an |
577 | /// internal value (`LIVE_RELOAD_ENDPOINT) |
578 | /// |
579 | /// This config item *should not be edited* by the end user. |
580 | #[doc (hidden)] |
581 | pub live_reload_endpoint: Option<String>, |
582 | /// The mapping from old pages to new pages/URLs to use when generating |
583 | /// redirects. |
584 | pub redirect: HashMap<String, String>, |
585 | } |
586 | |
587 | impl Default for HtmlConfig { |
588 | fn default() -> HtmlConfig { |
589 | HtmlConfig { |
590 | theme: None, |
591 | default_theme: None, |
592 | preferred_dark_theme: None, |
593 | curly_quotes: false, |
594 | mathjax_support: false, |
595 | copy_fonts: true, |
596 | google_analytics: None, |
597 | additional_css: Vec::new(), |
598 | additional_js: Vec::new(), |
599 | fold: Fold::default(), |
600 | playground: Playground::default(), |
601 | code: Code::default(), |
602 | print: Print::default(), |
603 | no_section_label: false, |
604 | search: None, |
605 | git_repository_url: None, |
606 | git_repository_icon: None, |
607 | edit_url_template: None, |
608 | input_404: None, |
609 | site_url: None, |
610 | cname: None, |
611 | live_reload_endpoint: None, |
612 | redirect: HashMap::new(), |
613 | } |
614 | } |
615 | } |
616 | |
617 | impl HtmlConfig { |
618 | /// Returns the directory of theme from the provided root directory. If the |
619 | /// directory is not present it will append the default directory of "theme" |
620 | pub fn theme_dir(&self, root: &Path) -> PathBuf { |
621 | match self.theme { |
622 | Some(ref d: &PathBuf) => root.join(path:d), |
623 | None => root.join(path:"theme" ), |
624 | } |
625 | } |
626 | } |
627 | |
628 | /// Configuration for how to render the print icon, print.html, and print.css. |
629 | #[derive (Debug, Clone, PartialEq, Serialize, Deserialize)] |
630 | #[serde(default, rename_all = "kebab-case" )] |
631 | pub struct Print { |
632 | /// Whether print support is enabled. |
633 | pub enable: bool, |
634 | /// Insert page breaks between chapters. Default: `true`. |
635 | pub page_break: bool, |
636 | } |
637 | |
638 | impl Default for Print { |
639 | fn default() -> Self { |
640 | Self { |
641 | enable: true, |
642 | page_break: true, |
643 | } |
644 | } |
645 | } |
646 | |
647 | /// Configuration for how to fold chapters of sidebar. |
648 | #[derive (Default, Debug, Clone, PartialEq, Serialize, Deserialize)] |
649 | #[serde(default, rename_all = "kebab-case" )] |
650 | pub struct Fold { |
651 | /// When off, all folds are open. Default: `false`. |
652 | pub enable: bool, |
653 | /// The higher the more folded regions are open. When level is 0, all folds |
654 | /// are closed. |
655 | /// Default: `0`. |
656 | pub level: u8, |
657 | } |
658 | |
659 | /// Configuration for tweaking how the the HTML renderer handles the playground. |
660 | #[derive (Debug, Clone, PartialEq, Serialize, Deserialize)] |
661 | #[serde(default, rename_all = "kebab-case" )] |
662 | pub struct Playground { |
663 | /// Should playground snippets be editable? Default: `false`. |
664 | pub editable: bool, |
665 | /// Display the copy button. Default: `true`. |
666 | pub copyable: bool, |
667 | /// Copy JavaScript files for the editor to the output directory? |
668 | /// Default: `true`. |
669 | pub copy_js: bool, |
670 | /// Display line numbers on playground snippets. Default: `false`. |
671 | pub line_numbers: bool, |
672 | /// Display the run button. Default: `true` |
673 | pub runnable: bool, |
674 | } |
675 | |
676 | impl Default for Playground { |
677 | fn default() -> Playground { |
678 | Playground { |
679 | editable: false, |
680 | copyable: true, |
681 | copy_js: true, |
682 | line_numbers: false, |
683 | runnable: true, |
684 | } |
685 | } |
686 | } |
687 | |
688 | /// Configuration for tweaking how the the HTML renderer handles code blocks. |
689 | #[derive (Debug, Clone, PartialEq, Serialize, Deserialize)] |
690 | #[serde(default, rename_all = "kebab-case" )] |
691 | pub struct Code { |
692 | /// A prefix string to hide lines per language (one or more chars). |
693 | pub hidelines: HashMap<String, String>, |
694 | } |
695 | |
696 | impl Default for Code { |
697 | fn default() -> Code { |
698 | Code { |
699 | hidelines: HashMap::new(), |
700 | } |
701 | } |
702 | } |
703 | |
704 | /// Configuration of the search functionality of the HTML renderer. |
705 | #[derive (Debug, Clone, PartialEq, Serialize, Deserialize)] |
706 | #[serde(default, rename_all = "kebab-case" )] |
707 | pub struct Search { |
708 | /// Enable the search feature. Default: `true`. |
709 | pub enable: bool, |
710 | /// Maximum number of visible results. Default: `30`. |
711 | pub limit_results: u32, |
712 | /// The number of words used for a search result teaser. Default: `30`. |
713 | pub teaser_word_count: u32, |
714 | /// Define the logical link between multiple search words. |
715 | /// If true, all search words must appear in each result. Default: `false`. |
716 | pub use_boolean_and: bool, |
717 | /// Boost factor for the search result score if a search word appears in the header. |
718 | /// Default: `2`. |
719 | pub boost_title: u8, |
720 | /// Boost factor for the search result score if a search word appears in the hierarchy. |
721 | /// The hierarchy contains all titles of the parent documents and all parent headings. |
722 | /// Default: `1`. |
723 | pub boost_hierarchy: u8, |
724 | /// Boost factor for the search result score if a search word appears in the text. |
725 | /// Default: `1`. |
726 | pub boost_paragraph: u8, |
727 | /// True if the searchword `micro` should match `microwave`. Default: `true`. |
728 | pub expand: bool, |
729 | /// Documents are split into smaller parts, separated by headings. This defines, until which |
730 | /// level of heading documents should be split. Default: `3`. (`### This is a level 3 heading`) |
731 | pub heading_split_level: u8, |
732 | /// Copy JavaScript files for the search functionality to the output directory? |
733 | /// Default: `true`. |
734 | pub copy_js: bool, |
735 | } |
736 | |
737 | impl Default for Search { |
738 | fn default() -> Search { |
739 | // Please update the documentation of `Search` when changing values! |
740 | Search { |
741 | enable: true, |
742 | limit_results: 30, |
743 | teaser_word_count: 30, |
744 | use_boolean_and: false, |
745 | boost_title: 2, |
746 | boost_hierarchy: 1, |
747 | boost_paragraph: 1, |
748 | expand: true, |
749 | heading_split_level: 3, |
750 | copy_js: true, |
751 | } |
752 | } |
753 | } |
754 | |
755 | /// Allows you to "update" any arbitrary field in a struct by round-tripping via |
756 | /// a `toml::Value`. |
757 | /// |
758 | /// This is definitely not the most performant way to do things, which means you |
759 | /// should probably keep it away from tight loops... |
760 | trait Updateable<'de>: Serialize + Deserialize<'de> { |
761 | fn update_value<S: Serialize>(&mut self, key: &str, value: S) { |
762 | let mut raw: Value = Value::try_from(&self).expect(msg:"unreachable" ); |
763 | |
764 | if let Ok(value: Value) = Value::try_from(value) { |
765 | raw.insert(key, value); |
766 | } else { |
767 | return; |
768 | } |
769 | |
770 | if let Ok(updated: Self) = raw.try_into() { |
771 | *self = updated; |
772 | } |
773 | } |
774 | } |
775 | |
776 | impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {} |
777 | |
778 | #[cfg (test)] |
779 | mod tests { |
780 | use super::*; |
781 | use crate::utils::fs::get_404_output_file; |
782 | use serde_json::json; |
783 | |
784 | const COMPLEX_CONFIG: &str = r#" |
785 | [book] |
786 | title = "Some Book" |
787 | authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"] |
788 | description = "A completely useless book" |
789 | multilingual = true |
790 | src = "source" |
791 | language = "ja" |
792 | |
793 | [build] |
794 | build-dir = "outputs" |
795 | create-missing = false |
796 | use-default-preprocessors = true |
797 | |
798 | [output.html] |
799 | theme = "./themedir" |
800 | default-theme = "rust" |
801 | curly-quotes = true |
802 | google-analytics = "123456" |
803 | additional-css = ["./foo/bar/baz.css"] |
804 | git-repository-url = "https://foo.com/" |
805 | git-repository-icon = "fa-code-fork" |
806 | |
807 | [output.html.playground] |
808 | editable = true |
809 | editor = "ace" |
810 | |
811 | [output.html.redirect] |
812 | "index.html" = "overview.html" |
813 | "nexted/page.md" = "https://rust-lang.org/" |
814 | |
815 | [preprocessor.first] |
816 | |
817 | [preprocessor.second] |
818 | "# ; |
819 | |
820 | #[test ] |
821 | fn load_a_complex_config_file() { |
822 | let src = COMPLEX_CONFIG; |
823 | |
824 | let book_should_be = BookConfig { |
825 | title: Some(String::from("Some Book" )), |
826 | authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>" )], |
827 | description: Some(String::from("A completely useless book" )), |
828 | multilingual: true, |
829 | src: PathBuf::from("source" ), |
830 | language: Some(String::from("ja" )), |
831 | text_direction: None, |
832 | }; |
833 | let build_should_be = BuildConfig { |
834 | build_dir: PathBuf::from("outputs" ), |
835 | create_missing: false, |
836 | use_default_preprocessors: true, |
837 | extra_watch_dirs: Vec::new(), |
838 | }; |
839 | let rust_should_be = RustConfig { edition: None }; |
840 | let playground_should_be = Playground { |
841 | editable: true, |
842 | copyable: true, |
843 | copy_js: true, |
844 | line_numbers: false, |
845 | runnable: true, |
846 | }; |
847 | let html_should_be = HtmlConfig { |
848 | curly_quotes: true, |
849 | google_analytics: Some(String::from("123456" )), |
850 | additional_css: vec![PathBuf::from("./foo/bar/baz.css" )], |
851 | theme: Some(PathBuf::from("./themedir" )), |
852 | default_theme: Some(String::from("rust" )), |
853 | playground: playground_should_be, |
854 | git_repository_url: Some(String::from("https://foo.com/" )), |
855 | git_repository_icon: Some(String::from("fa-code-fork" )), |
856 | redirect: vec![ |
857 | (String::from("index.html" ), String::from("overview.html" )), |
858 | ( |
859 | String::from("nexted/page.md" ), |
860 | String::from("https://rust-lang.org/" ), |
861 | ), |
862 | ] |
863 | .into_iter() |
864 | .collect(), |
865 | ..Default::default() |
866 | }; |
867 | |
868 | let got = Config::from_str(src).unwrap(); |
869 | |
870 | assert_eq!(got.book, book_should_be); |
871 | assert_eq!(got.build, build_should_be); |
872 | assert_eq!(got.rust, rust_should_be); |
873 | assert_eq!(got.html_config().unwrap(), html_should_be); |
874 | } |
875 | |
876 | #[test ] |
877 | fn disable_runnable() { |
878 | let src = r#" |
879 | [book] |
880 | title = "Some Book" |
881 | description = "book book book" |
882 | authors = ["Shogo Takata"] |
883 | |
884 | [output.html.playground] |
885 | runnable = false |
886 | "# ; |
887 | |
888 | let got = Config::from_str(src).unwrap(); |
889 | assert!(!got.html_config().unwrap().playground.runnable); |
890 | } |
891 | |
892 | #[test ] |
893 | fn edition_2015() { |
894 | let src = r#" |
895 | [book] |
896 | title = "mdBook Documentation" |
897 | description = "Create book from markdown files. Like Gitbook but implemented in Rust" |
898 | authors = ["Mathieu David"] |
899 | src = "./source" |
900 | [rust] |
901 | edition = "2015" |
902 | "# ; |
903 | |
904 | let book_should_be = BookConfig { |
905 | title: Some(String::from("mdBook Documentation" )), |
906 | description: Some(String::from( |
907 | "Create book from markdown files. Like Gitbook but implemented in Rust" , |
908 | )), |
909 | authors: vec![String::from("Mathieu David" )], |
910 | src: PathBuf::from("./source" ), |
911 | ..Default::default() |
912 | }; |
913 | |
914 | let got = Config::from_str(src).unwrap(); |
915 | assert_eq!(got.book, book_should_be); |
916 | |
917 | let rust_should_be = RustConfig { |
918 | edition: Some(RustEdition::E2015), |
919 | }; |
920 | let got = Config::from_str(src).unwrap(); |
921 | assert_eq!(got.rust, rust_should_be); |
922 | } |
923 | |
924 | #[test ] |
925 | fn edition_2018() { |
926 | let src = r#" |
927 | [book] |
928 | title = "mdBook Documentation" |
929 | description = "Create book from markdown files. Like Gitbook but implemented in Rust" |
930 | authors = ["Mathieu David"] |
931 | src = "./source" |
932 | [rust] |
933 | edition = "2018" |
934 | "# ; |
935 | |
936 | let rust_should_be = RustConfig { |
937 | edition: Some(RustEdition::E2018), |
938 | }; |
939 | |
940 | let got = Config::from_str(src).unwrap(); |
941 | assert_eq!(got.rust, rust_should_be); |
942 | } |
943 | |
944 | #[test ] |
945 | fn edition_2021() { |
946 | let src = r#" |
947 | [book] |
948 | title = "mdBook Documentation" |
949 | description = "Create book from markdown files. Like Gitbook but implemented in Rust" |
950 | authors = ["Mathieu David"] |
951 | src = "./source" |
952 | [rust] |
953 | edition = "2021" |
954 | "# ; |
955 | |
956 | let rust_should_be = RustConfig { |
957 | edition: Some(RustEdition::E2021), |
958 | }; |
959 | |
960 | let got = Config::from_str(src).unwrap(); |
961 | assert_eq!(got.rust, rust_should_be); |
962 | } |
963 | |
964 | #[test ] |
965 | fn load_arbitrary_output_type() { |
966 | #[derive (Debug, Deserialize, PartialEq)] |
967 | struct RandomOutput { |
968 | foo: u32, |
969 | bar: String, |
970 | baz: Vec<bool>, |
971 | } |
972 | |
973 | let src = r#" |
974 | [output.random] |
975 | foo = 5 |
976 | bar = "Hello World" |
977 | baz = [true, true, false] |
978 | "# ; |
979 | |
980 | let should_be = RandomOutput { |
981 | foo: 5, |
982 | bar: String::from("Hello World" ), |
983 | baz: vec![true, true, false], |
984 | }; |
985 | |
986 | let cfg = Config::from_str(src).unwrap(); |
987 | let got: RandomOutput = cfg.get_deserialized_opt("output.random" ).unwrap().unwrap(); |
988 | |
989 | assert_eq!(got, should_be); |
990 | |
991 | let got_baz: Vec<bool> = cfg |
992 | .get_deserialized_opt("output.random.baz" ) |
993 | .unwrap() |
994 | .unwrap(); |
995 | let baz_should_be = vec![true, true, false]; |
996 | |
997 | assert_eq!(got_baz, baz_should_be); |
998 | } |
999 | |
1000 | #[test ] |
1001 | fn mutate_some_stuff() { |
1002 | // really this is just a sanity check to make sure the borrow checker |
1003 | // is happy... |
1004 | let src = COMPLEX_CONFIG; |
1005 | let mut config = Config::from_str(src).unwrap(); |
1006 | let key = "output.html.playground.editable" ; |
1007 | |
1008 | assert_eq!(config.get(key).unwrap(), &Value::Boolean(true)); |
1009 | *config.get_mut(key).unwrap() = Value::Boolean(false); |
1010 | assert_eq!(config.get(key).unwrap(), &Value::Boolean(false)); |
1011 | } |
1012 | |
1013 | /// The config file format has slightly changed (metadata stuff is now under |
1014 | /// the `book` table instead of being at the top level) so we're adding a |
1015 | /// **temporary** compatibility check. You should be able to still load the |
1016 | /// old format, emitting a warning. |
1017 | #[test ] |
1018 | fn can_still_load_the_previous_format() { |
1019 | let src = r#" |
1020 | title = "mdBook Documentation" |
1021 | description = "Create book from markdown files. Like Gitbook but implemented in Rust" |
1022 | authors = ["Mathieu David"] |
1023 | source = "./source" |
1024 | |
1025 | [output.html] |
1026 | destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book` |
1027 | theme = "my-theme" |
1028 | curly-quotes = true |
1029 | google-analytics = "123456" |
1030 | additional-css = ["custom.css", "custom2.css"] |
1031 | additional-js = ["custom.js"] |
1032 | "# ; |
1033 | |
1034 | let book_should_be = BookConfig { |
1035 | title: Some(String::from("mdBook Documentation" )), |
1036 | description: Some(String::from( |
1037 | "Create book from markdown files. Like Gitbook but implemented in Rust" , |
1038 | )), |
1039 | authors: vec![String::from("Mathieu David" )], |
1040 | src: PathBuf::from("./source" ), |
1041 | ..Default::default() |
1042 | }; |
1043 | |
1044 | let build_should_be = BuildConfig { |
1045 | build_dir: PathBuf::from("my-book" ), |
1046 | create_missing: true, |
1047 | use_default_preprocessors: true, |
1048 | extra_watch_dirs: Vec::new(), |
1049 | }; |
1050 | |
1051 | let html_should_be = HtmlConfig { |
1052 | theme: Some(PathBuf::from("my-theme" )), |
1053 | curly_quotes: true, |
1054 | google_analytics: Some(String::from("123456" )), |
1055 | additional_css: vec![PathBuf::from("custom.css" ), PathBuf::from("custom2.css" )], |
1056 | additional_js: vec![PathBuf::from("custom.js" )], |
1057 | ..Default::default() |
1058 | }; |
1059 | |
1060 | let got = Config::from_str(src).unwrap(); |
1061 | assert_eq!(got.book, book_should_be); |
1062 | assert_eq!(got.build, build_should_be); |
1063 | assert_eq!(got.html_config().unwrap(), html_should_be); |
1064 | } |
1065 | |
1066 | #[test ] |
1067 | fn set_a_config_item() { |
1068 | let mut cfg = Config::default(); |
1069 | let key = "foo.bar.baz" ; |
1070 | let value = "Something Interesting" ; |
1071 | |
1072 | assert!(cfg.get(key).is_none()); |
1073 | cfg.set(key, value).unwrap(); |
1074 | |
1075 | let got: String = cfg.get_deserialized_opt(key).unwrap().unwrap(); |
1076 | assert_eq!(got, value); |
1077 | } |
1078 | |
1079 | #[test ] |
1080 | fn parse_env_vars() { |
1081 | let inputs = vec![ |
1082 | ("FOO" , None), |
1083 | ("MDBOOK_foo" , Some("foo" )), |
1084 | ("MDBOOK_FOO__bar__baz" , Some("foo.bar.baz" )), |
1085 | ("MDBOOK_FOO_bar__baz" , Some("foo-bar.baz" )), |
1086 | ]; |
1087 | |
1088 | for (src, should_be) in inputs { |
1089 | let got = parse_env(src); |
1090 | let should_be = should_be.map(ToString::to_string); |
1091 | |
1092 | assert_eq!(got, should_be); |
1093 | } |
1094 | } |
1095 | |
1096 | fn encode_env_var(key: &str) -> String { |
1097 | format!( |
1098 | "MDBOOK_ {}" , |
1099 | key.to_uppercase().replace('.' , "__" ).replace('-' , "_" ) |
1100 | ) |
1101 | } |
1102 | |
1103 | #[test ] |
1104 | fn update_config_using_env_var() { |
1105 | let mut cfg = Config::default(); |
1106 | let key = "foo.bar" ; |
1107 | let value = "baz" ; |
1108 | |
1109 | assert!(cfg.get(key).is_none()); |
1110 | |
1111 | let encoded_key = encode_env_var(key); |
1112 | env::set_var(encoded_key, value); |
1113 | |
1114 | cfg.update_from_env(); |
1115 | |
1116 | assert_eq!( |
1117 | cfg.get_deserialized_opt::<String, _>(key).unwrap().unwrap(), |
1118 | value |
1119 | ); |
1120 | } |
1121 | |
1122 | #[test ] |
1123 | fn update_config_using_env_var_and_complex_value() { |
1124 | let mut cfg = Config::default(); |
1125 | let key = "foo-bar.baz" ; |
1126 | let value = json!({"array" : [1, 2, 3], "number" : 13.37}); |
1127 | let value_str = serde_json::to_string(&value).unwrap(); |
1128 | |
1129 | assert!(cfg.get(key).is_none()); |
1130 | |
1131 | let encoded_key = encode_env_var(key); |
1132 | env::set_var(encoded_key, value_str); |
1133 | |
1134 | cfg.update_from_env(); |
1135 | |
1136 | assert_eq!( |
1137 | cfg.get_deserialized_opt::<serde_json::Value, _>(key) |
1138 | .unwrap() |
1139 | .unwrap(), |
1140 | value |
1141 | ); |
1142 | } |
1143 | |
1144 | #[test ] |
1145 | fn update_book_title_via_env() { |
1146 | let mut cfg = Config::default(); |
1147 | let should_be = "Something else" .to_string(); |
1148 | |
1149 | assert_ne!(cfg.book.title, Some(should_be.clone())); |
1150 | |
1151 | env::set_var("MDBOOK_BOOK__TITLE" , &should_be); |
1152 | cfg.update_from_env(); |
1153 | |
1154 | assert_eq!(cfg.book.title, Some(should_be)); |
1155 | } |
1156 | |
1157 | #[test ] |
1158 | fn file_404_default() { |
1159 | let src = r#" |
1160 | [output.html] |
1161 | destination = "my-book" |
1162 | "# ; |
1163 | |
1164 | let got = Config::from_str(src).unwrap(); |
1165 | let html_config = got.html_config().unwrap(); |
1166 | assert_eq!(html_config.input_404, None); |
1167 | assert_eq!(&get_404_output_file(&html_config.input_404), "404.html" ); |
1168 | } |
1169 | |
1170 | #[test ] |
1171 | fn file_404_custom() { |
1172 | let src = r#" |
1173 | [output.html] |
1174 | input-404= "missing.md" |
1175 | output-404= "missing.html" |
1176 | "# ; |
1177 | |
1178 | let got = Config::from_str(src).unwrap(); |
1179 | let html_config = got.html_config().unwrap(); |
1180 | assert_eq!(html_config.input_404, Some("missing.md" .to_string())); |
1181 | assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html" ); |
1182 | } |
1183 | |
1184 | #[test ] |
1185 | fn text_direction_ltr() { |
1186 | let src = r#" |
1187 | [book] |
1188 | text-direction = "ltr" |
1189 | "# ; |
1190 | |
1191 | let got = Config::from_str(src).unwrap(); |
1192 | assert_eq!(got.book.text_direction, Some(TextDirection::LeftToRight)); |
1193 | } |
1194 | |
1195 | #[test ] |
1196 | fn text_direction_rtl() { |
1197 | let src = r#" |
1198 | [book] |
1199 | text-direction = "rtl" |
1200 | "# ; |
1201 | |
1202 | let got = Config::from_str(src).unwrap(); |
1203 | assert_eq!(got.book.text_direction, Some(TextDirection::RightToLeft)); |
1204 | } |
1205 | |
1206 | #[test ] |
1207 | fn text_direction_none() { |
1208 | let src = r#" |
1209 | [book] |
1210 | "# ; |
1211 | |
1212 | let got = Config::from_str(src).unwrap(); |
1213 | assert_eq!(got.book.text_direction, None); |
1214 | } |
1215 | |
1216 | #[test ] |
1217 | fn test_text_direction() { |
1218 | let mut cfg = BookConfig::default(); |
1219 | |
1220 | // test deriving the text direction from language codes |
1221 | cfg.language = Some("ar" .into()); |
1222 | assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft); |
1223 | |
1224 | cfg.language = Some("he" .into()); |
1225 | assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft); |
1226 | |
1227 | cfg.language = Some("en" .into()); |
1228 | assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight); |
1229 | |
1230 | cfg.language = Some("ja" .into()); |
1231 | assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight); |
1232 | |
1233 | // test forced direction |
1234 | cfg.language = Some("ar" .into()); |
1235 | cfg.text_direction = Some(TextDirection::LeftToRight); |
1236 | assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight); |
1237 | |
1238 | cfg.language = Some("ar" .into()); |
1239 | cfg.text_direction = Some(TextDirection::RightToLeft); |
1240 | assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft); |
1241 | |
1242 | cfg.language = Some("en" .into()); |
1243 | cfg.text_direction = Some(TextDirection::LeftToRight); |
1244 | assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight); |
1245 | |
1246 | cfg.language = Some("en" .into()); |
1247 | cfg.text_direction = Some(TextDirection::RightToLeft); |
1248 | assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft); |
1249 | } |
1250 | |
1251 | #[test ] |
1252 | #[should_panic (expected = "Invalid configuration file" )] |
1253 | fn invalid_language_type_error() { |
1254 | let src = r#" |
1255 | [book] |
1256 | title = "mdBook Documentation" |
1257 | language = ["en", "pt-br"] |
1258 | description = "Create book from markdown files. Like Gitbook but implemented in Rust" |
1259 | authors = ["Mathieu David"] |
1260 | src = "./source" |
1261 | "# ; |
1262 | |
1263 | Config::from_str(src).unwrap(); |
1264 | } |
1265 | |
1266 | #[test ] |
1267 | #[should_panic (expected = "Invalid configuration file" )] |
1268 | fn invalid_title_type() { |
1269 | let src = r#" |
1270 | [book] |
1271 | title = 20 |
1272 | language = "en" |
1273 | description = "Create book from markdown files. Like Gitbook but implemented in Rust" |
1274 | authors = ["Mathieu David"] |
1275 | src = "./source" |
1276 | "# ; |
1277 | |
1278 | Config::from_str(src).unwrap(); |
1279 | } |
1280 | |
1281 | #[test ] |
1282 | #[should_panic (expected = "Invalid configuration file" )] |
1283 | fn invalid_build_dir_type() { |
1284 | let src = r#" |
1285 | [build] |
1286 | build-dir = 99 |
1287 | create-missing = false |
1288 | "# ; |
1289 | |
1290 | Config::from_str(src).unwrap(); |
1291 | } |
1292 | |
1293 | #[test ] |
1294 | #[should_panic (expected = "Invalid configuration file" )] |
1295 | fn invalid_rust_edition() { |
1296 | let src = r#" |
1297 | [rust] |
1298 | edition = "1999" |
1299 | "# ; |
1300 | |
1301 | Config::from_str(src).unwrap(); |
1302 | } |
1303 | |
1304 | #[test ] |
1305 | fn print_config() { |
1306 | let src = r#" |
1307 | [output.html.print] |
1308 | enable = false |
1309 | "# ; |
1310 | let got = Config::from_str(src).unwrap(); |
1311 | let html_config = got.html_config().unwrap(); |
1312 | assert!(!html_config.print.enable); |
1313 | assert!(html_config.print.page_break); |
1314 | let src = r#" |
1315 | [output.html.print] |
1316 | page-break = false |
1317 | "# ; |
1318 | let got = Config::from_str(src).unwrap(); |
1319 | let html_config = got.html_config().unwrap(); |
1320 | assert!(html_config.print.enable); |
1321 | assert!(!html_config.print.page_break); |
1322 | } |
1323 | } |
1324 | |