]> git.proxmox.com Git - rustc.git/blame - vendor/mdbook/src/config.rs
New upstream version 1.56.0~beta.4+dfsg1
[rustc.git] / vendor / mdbook / src / config.rs
CommitLineData
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 52use serde::{Deserialize, Deserializer, Serialize, Serializer};
f035d41b 53use std::collections::HashMap;
9fa01778 54use std::env;
2c00a5a8
XL
55use std::fs::File;
56use std::io::Read;
9fa01778 57use std::path::{Path, PathBuf};
dc9dc135 58use std::str::FromStr;
2c00a5a8 59use toml::value::Table;
9fa01778 60use toml::{self, Value};
2c00a5a8 61
dc9dc135 62use crate::errors::*;
f035d41b 63use 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)]
68pub 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
78impl 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 87impl 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
287impl 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 298impl<'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
351impl 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
373fn 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
385fn 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")]
407pub 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
422impl 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")]
438pub 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
449impl 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")]
462pub 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.
469pub 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")]
484pub 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
549impl 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
578impl 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")]
592pub struct Print {
593 /// Whether print support is enabled.
594 pub enable: bool,
595}
596
597impl 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")]
606pub 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
618pub 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
630impl 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")]
644pub 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
674impl 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...
697trait 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 713impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
2c00a5a8
XL
714
715#[cfg(test)]
716mod 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}