Merge pull request #500 from cspiegel/create-missing
WIP: Add a create-missing option to book.toml.
This commit is contained in:
commit
e735bc6d3e
|
@ -10,8 +10,11 @@ title = "Example book"
|
||||||
author = "John Doe"
|
author = "John Doe"
|
||||||
description = "The example book covers examples."
|
description = "The example book covers examples."
|
||||||
|
|
||||||
|
[build]
|
||||||
|
build-dir = "my-example-book"
|
||||||
|
create-missing = false
|
||||||
|
|
||||||
[output.html]
|
[output.html]
|
||||||
destination = "my-example-book"
|
|
||||||
additional-css = ["custom.css"]
|
additional-css = ["custom.css"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -32,8 +35,6 @@ This is general information about your book.
|
||||||
- **src:** By default, the source directory is found in the directory named
|
- **src:** By default, the source directory is found in the directory named
|
||||||
`src` directly under the root folder. But this is configurable with the `src`
|
`src` directly under the root folder. But this is configurable with the `src`
|
||||||
key in the configuration file.
|
key in the configuration file.
|
||||||
- **build-dir:** The directory to put the rendered book in. By default this is
|
|
||||||
`book/` in the book's root directory.
|
|
||||||
|
|
||||||
**book.toml**
|
**book.toml**
|
||||||
```toml
|
```toml
|
||||||
|
@ -42,7 +43,24 @@ title = "Example book"
|
||||||
authors = ["John Doe", "Jane Doe"]
|
authors = ["John Doe", "Jane Doe"]
|
||||||
description = "The example book covers examples."
|
description = "The example book covers examples."
|
||||||
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build options
|
||||||
|
|
||||||
|
This controls the build process of your book.
|
||||||
|
|
||||||
|
- **build-dir:** The directory to put the rendered book in. By default this is
|
||||||
|
`book/` in the book's root directory.
|
||||||
|
- **create-missing:** By default, any missing files specified in `SUMMARY.md`
|
||||||
|
will be created when the book is built (i.e. `create-missing = true`). If this
|
||||||
|
is `false` then the build process will instead exit with an error if any files
|
||||||
|
do not exist.
|
||||||
|
|
||||||
|
**book.toml**
|
||||||
|
```toml
|
||||||
|
[build]
|
||||||
build-dir = "build"
|
build-dir = "build"
|
||||||
|
create-missing = false
|
||||||
```
|
```
|
||||||
|
|
||||||
### HTML renderer options
|
### HTML renderer options
|
||||||
|
|
|
@ -12,3 +12,4 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
|
||||||
- [funnkill](https://github.com/funkill)
|
- [funnkill](https://github.com/funkill)
|
||||||
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))
|
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))
|
||||||
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
|
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
|
||||||
|
- [Chris Spiegel](https://github.com/cspiegel)
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||||
book{n}(Defaults to ./book when omitted)'",
|
book{n}(Defaults to ./book when omitted)'",
|
||||||
)
|
)
|
||||||
.arg_from_usage(
|
.arg_from_usage(
|
||||||
"--no-create 'Will not create non-existent files linked from SUMMARY.md'",
|
"--no-create 'Will not create non-existent files linked from SUMMARY.md (deprecated: use book.toml instead)'",
|
||||||
)
|
)
|
||||||
.arg_from_usage(
|
.arg_from_usage(
|
||||||
"[dir] 'A directory for your book{n}(Defaults to Current Directory \
|
"[dir] 'A directory for your book{n}(Defaults to Current Directory \
|
||||||
|
@ -28,11 +28,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||||
|
|
||||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||||
book.config.book.build_dir = PathBuf::from(dest_dir);
|
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This flag is deprecated in favor of being set via `book.toml`.
|
||||||
if args.is_present("no-create") {
|
if args.is_present("no-create") {
|
||||||
book.create_missing = false;
|
book.config.build.create_missing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
book.build()?;
|
book.build()?;
|
||||||
|
|
|
@ -52,7 +52,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||||
|
|
||||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||||
book.config.book.build_dir = PathBuf::from(dest_dir);
|
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
let port = args.value_of("port").unwrap_or("3000");
|
let port = args.value_of("port").unwrap_or("3000");
|
||||||
|
|
|
@ -30,7 +30,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||||
|
|
||||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||||
book.config.book.build_dir = PathBuf::from(dest_dir);
|
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.is_present("open") {
|
if args.is_present("open") {
|
||||||
|
|
|
@ -23,10 +23,6 @@ pub struct MDBook {
|
||||||
renderer: Box<Renderer>,
|
renderer: Box<Renderer>,
|
||||||
|
|
||||||
pub livereload: Option<String>,
|
pub livereload: Option<String>,
|
||||||
|
|
||||||
/// Should `mdbook build` create files referenced from SUMMARY.md if they
|
|
||||||
/// don't exist
|
|
||||||
pub create_missing: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MDBook {
|
impl MDBook {
|
||||||
|
@ -71,7 +67,6 @@ impl MDBook {
|
||||||
renderer: Box::new(HtmlHandlebars::new()),
|
renderer: Box::new(HtmlHandlebars::new()),
|
||||||
|
|
||||||
livereload: None,
|
livereload: None,
|
||||||
create_missing: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +174,7 @@ impl MDBook {
|
||||||
let path = self.get_source().join(&ch.path);
|
let path = self.get_source().join(&ch.path);
|
||||||
|
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
if !self.create_missing {
|
if !self.config.build.create_missing {
|
||||||
return Err(
|
return Err(
|
||||||
format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy()).into(),
|
format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy()).into(),
|
||||||
);
|
);
|
||||||
|
@ -388,7 +383,7 @@ impl MDBook {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_destination(&self) -> PathBuf {
|
pub fn get_destination(&self) -> PathBuf {
|
||||||
self.root.join(&self.config.book.build_dir)
|
self.root.join(&self.config.build.build_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_source(&self) -> PathBuf {
|
pub fn get_source(&self) -> PathBuf {
|
||||||
|
|
|
@ -12,6 +12,7 @@ use errors::*;
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Metadata about the book.
|
/// Metadata about the book.
|
||||||
pub book: BookConfig,
|
pub book: BookConfig,
|
||||||
|
pub build: BuildConfig,
|
||||||
rest: Table,
|
rest: Table,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@ impl Config {
|
||||||
get_and_insert!(table, "description" => cfg.book.description);
|
get_and_insert!(table, "description" => cfg.book.description);
|
||||||
|
|
||||||
// This complicated chain of and_then's is so we can move
|
// This complicated chain of and_then's is so we can move
|
||||||
// "output.html.destination" to "book.build_dir" and parse it into a
|
// "output.html.destination" to "build.build_dir" and parse it into a
|
||||||
// PathBuf.
|
// PathBuf.
|
||||||
let destination: Option<PathBuf> = table.get_mut("output")
|
let destination: Option<PathBuf> = table.get_mut("output")
|
||||||
.and_then(|output| output.as_table_mut())
|
.and_then(|output| output.as_table_mut())
|
||||||
|
@ -101,7 +102,7 @@ impl Config {
|
||||||
.and_then(|dest| dest.try_into().ok());
|
.and_then(|dest| dest.try_into().ok());
|
||||||
|
|
||||||
if let Some(dest) = destination {
|
if let Some(dest) = destination {
|
||||||
cfg.book.build_dir = dest;
|
cfg.build.build_dir = dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.rest = table;
|
cfg.rest = table;
|
||||||
|
@ -162,8 +163,10 @@ impl<'de> Deserialize<'de> for Config {
|
||||||
warn!("It looks like you are using the legacy book.toml format.");
|
warn!("It looks like you are using the legacy book.toml format.");
|
||||||
warn!("We'll parse it for now, but you should probably convert to the new format.");
|
warn!("We'll parse it for now, but you should probably convert to the new format.");
|
||||||
warn!("See the mdbook documentation for more details, although as a rule of thumb");
|
warn!("See the mdbook documentation for more details, although as a rule of thumb");
|
||||||
warn!("just move all top level configuration entries like `title`, `author` and ");
|
warn!("just move all top level configuration entries like `title`, `author` and");
|
||||||
warn!("`description` under a table called `[book]` and it should all work.");
|
warn!("`description` under a table called `[book]`, move the `destination` entry");
|
||||||
|
warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
|
||||||
|
warn!("`[build]`, and it should all work.");
|
||||||
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
|
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
|
||||||
return Ok(Config::from_legacy(table));
|
return Ok(Config::from_legacy(table));
|
||||||
}
|
}
|
||||||
|
@ -171,8 +174,14 @@ impl<'de> Deserialize<'de> for Config {
|
||||||
let book: BookConfig = table.remove("book")
|
let book: BookConfig = table.remove("book")
|
||||||
.and_then(|value| value.try_into().ok())
|
.and_then(|value| value.try_into().ok())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let build: BuildConfig = table.remove("build")
|
||||||
|
.and_then(|value| value.try_into().ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
book: book,
|
book: book,
|
||||||
|
build: build,
|
||||||
rest: table,
|
rest: table,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -198,8 +207,6 @@ pub struct BookConfig {
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
/// Location of the book source relative to the book's root directory.
|
/// Location of the book source relative to the book's root directory.
|
||||||
pub src: PathBuf,
|
pub src: PathBuf,
|
||||||
/// Where to put built artefacts relative to the book's root directory.
|
|
||||||
pub build_dir: PathBuf,
|
|
||||||
/// Does this book support more than one language?
|
/// Does this book support more than one language?
|
||||||
pub multilingual: bool,
|
pub multilingual: bool,
|
||||||
}
|
}
|
||||||
|
@ -211,12 +218,31 @@ impl Default for BookConfig {
|
||||||
authors: Vec::new(),
|
authors: Vec::new(),
|
||||||
description: None,
|
description: None,
|
||||||
src: PathBuf::from("src"),
|
src: PathBuf::from("src"),
|
||||||
build_dir: PathBuf::from("book"),
|
|
||||||
multilingual: false,
|
multilingual: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration for the build procedure.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default, rename_all = "kebab-case")]
|
||||||
|
pub struct BuildConfig {
|
||||||
|
/// Where to put built artefacts relative to the book's root directory.
|
||||||
|
pub build_dir: PathBuf,
|
||||||
|
/// Should non-existent markdown files specified in `SETTINGS.md` be created
|
||||||
|
/// if they don't exist?
|
||||||
|
pub create_missing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BuildConfig {
|
||||||
|
fn default() -> BuildConfig {
|
||||||
|
BuildConfig {
|
||||||
|
build_dir: PathBuf::from("book"),
|
||||||
|
create_missing: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case")]
|
||||||
pub struct HtmlConfig {
|
pub struct HtmlConfig {
|
||||||
|
@ -248,7 +274,10 @@ mod tests {
|
||||||
description = "A completely useless book"
|
description = "A completely useless book"
|
||||||
multilingual = true
|
multilingual = true
|
||||||
src = "source"
|
src = "source"
|
||||||
|
|
||||||
|
[build]
|
||||||
build-dir = "outputs"
|
build-dir = "outputs"
|
||||||
|
create-missing = false
|
||||||
|
|
||||||
[output.html]
|
[output.html]
|
||||||
theme = "./themedir"
|
theme = "./themedir"
|
||||||
|
@ -271,9 +300,12 @@ mod tests {
|
||||||
description: Some(String::from("A completely useless book")),
|
description: Some(String::from("A completely useless book")),
|
||||||
multilingual: true,
|
multilingual: true,
|
||||||
src: PathBuf::from("source"),
|
src: PathBuf::from("source"),
|
||||||
build_dir: PathBuf::from("outputs"),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
let build_should_be = BuildConfig {
|
||||||
|
build_dir: PathBuf::from("outputs"),
|
||||||
|
create_missing: false,
|
||||||
|
};
|
||||||
let playpen_should_be = Playpen {
|
let playpen_should_be = Playpen {
|
||||||
editable: true,
|
editable: true,
|
||||||
editor: PathBuf::from("ace"),
|
editor: PathBuf::from("ace"),
|
||||||
|
@ -290,6 +322,7 @@ mod tests {
|
||||||
let got = Config::from_str(src).unwrap();
|
let got = Config::from_str(src).unwrap();
|
||||||
|
|
||||||
assert_eq!(got.book, book_should_be);
|
assert_eq!(got.book, book_should_be);
|
||||||
|
assert_eq!(got.build, build_should_be);
|
||||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,11 +399,15 @@ mod tests {
|
||||||
"Create book from markdown files. Like Gitbook but implemented in Rust",
|
"Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||||
)),
|
)),
|
||||||
authors: vec![String::from("Mathieu David")],
|
authors: vec![String::from("Mathieu David")],
|
||||||
build_dir: PathBuf::from("my-book"),
|
|
||||||
src: PathBuf::from("./source"),
|
src: PathBuf::from("./source"),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let build_should_be = BuildConfig {
|
||||||
|
build_dir: PathBuf::from("my-book"),
|
||||||
|
create_missing: true,
|
||||||
|
};
|
||||||
|
|
||||||
let html_should_be = HtmlConfig {
|
let html_should_be = HtmlConfig {
|
||||||
theme: Some(PathBuf::from("my-theme")),
|
theme: Some(PathBuf::from("my-theme")),
|
||||||
curly_quotes: true,
|
curly_quotes: true,
|
||||||
|
@ -382,6 +419,7 @@ mod tests {
|
||||||
|
|
||||||
let got = Config::from_str(src).unwrap();
|
let got = Config::from_str(src).unwrap();
|
||||||
assert_eq!(got.book, book_should_be);
|
assert_eq!(got.book, book_should_be);
|
||||||
|
assert_eq!(got.build, build_should_be);
|
||||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
//!
|
//!
|
||||||
//! // tweak the book configuration a bit
|
//! // tweak the book configuration a bit
|
||||||
//! md.config.book.src = PathBuf::from("source");
|
//! md.config.book.src = PathBuf::from("source");
|
||||||
//! md.config.book.build_dir = PathBuf::from("book");
|
//! md.config.build.build_dir = PathBuf::from("book");
|
||||||
//!
|
//!
|
||||||
//! // Render the book
|
//! // Render the book
|
||||||
//! md.build().unwrap();
|
//! md.build().unwrap();
|
||||||
|
|
|
@ -42,7 +42,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||||
|
|
||||||
let mut md = MDBook::new(temp.path());
|
let mut md = MDBook::new(temp.path());
|
||||||
md.config.book.src = PathBuf::from("in");
|
md.config.book.src = PathBuf::from("in");
|
||||||
md.config.book.build_dir = PathBuf::from("out");
|
md.config.build.build_dir = PathBuf::from("out");
|
||||||
|
|
||||||
md.init().unwrap();
|
md.init().unwrap();
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,21 @@ extern crate mdbook;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate pretty_assertions;
|
extern crate pretty_assertions;
|
||||||
extern crate select;
|
extern crate select;
|
||||||
|
extern crate tempdir;
|
||||||
extern crate walkdir;
|
extern crate walkdir;
|
||||||
|
|
||||||
mod dummy_book;
|
mod dummy_book;
|
||||||
|
|
||||||
use dummy_book::{assert_contains_strings, DummyBook};
|
use dummy_book::{assert_contains_strings, DummyBook};
|
||||||
|
|
||||||
|
use std::fs::{File, remove_file};
|
||||||
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use walkdir::{DirEntry, WalkDir, WalkDirIterator};
|
use walkdir::{DirEntry, WalkDir, WalkDirIterator};
|
||||||
use select::document::Document;
|
use select::document::Document;
|
||||||
use select::predicate::{Class, Name, Predicate};
|
use select::predicate::{Class, Name, Predicate};
|
||||||
|
use tempdir::TempDir;
|
||||||
use mdbook::errors::*;
|
use mdbook::errors::*;
|
||||||
use mdbook::utils::fs::file_to_string;
|
use mdbook::utils::fs::file_to_string;
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
|
@ -227,3 +231,50 @@ fn check_spacers() {
|
||||||
doc.find(Class("chapter").descendant(Name("li").and(Class("spacer")))).count();
|
doc.find(Class("chapter").descendant(Name("li").and(Class("spacer")))).count();
|
||||||
assert_eq!(num_spacers, should_be);
|
assert_eq!(num_spacers, should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure building fails if `create-missing` is false and one of the files does
|
||||||
|
/// not exist.
|
||||||
|
#[test]
|
||||||
|
fn failure_on_missing_file() {
|
||||||
|
let (md, _temp) = create_missing_setup(Some(false));
|
||||||
|
|
||||||
|
// On failure, `build()` does not return a specific error, so assume
|
||||||
|
// any error is a failure due to a missing file.
|
||||||
|
assert!(md.read_config().unwrap().build().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure a missing file is created if `create-missing` is true.
|
||||||
|
#[test]
|
||||||
|
fn create_missing_file_with_config() {
|
||||||
|
let (md, temp) = create_missing_setup(Some(true));
|
||||||
|
|
||||||
|
md.read_config().unwrap().build().unwrap();
|
||||||
|
assert!(temp.path().join("src").join("intro.md").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure a missing file is created if `create-missing` is not set (the default
|
||||||
|
/// is true).
|
||||||
|
#[test]
|
||||||
|
fn create_missing_file_without_config() {
|
||||||
|
let (md, temp) = create_missing_setup(None);
|
||||||
|
|
||||||
|
md.read_config().unwrap().build().unwrap();
|
||||||
|
assert!(temp.path().join("src").join("intro.md").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_missing_setup(create_missing: Option<bool>) -> (MDBook, TempDir) {
|
||||||
|
let temp = DummyBook::new().build().unwrap();
|
||||||
|
let md = MDBook::new(temp.path());
|
||||||
|
|
||||||
|
let mut file = File::create(temp.path().join("book.toml")).unwrap();
|
||||||
|
match create_missing {
|
||||||
|
Some(true) => file.write_all(b"[build]\ncreate-missing = true\n").unwrap(),
|
||||||
|
Some(false) => file.write_all(b"[build]\ncreate-missing = false\n").unwrap(),
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
file.flush().unwrap();
|
||||||
|
|
||||||
|
remove_file(temp.path().join("src").join("intro.md")).unwrap();
|
||||||
|
|
||||||
|
(md, temp)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue