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