Add `--auto-summary` option.

Add a `Summary::from_source` function to generate the book's
summary from the sources directory structure.
This commit is contained in:
Timothée Haudebourg 2021-06-25 21:51:43 +02:00
parent ffa8284743
commit f767167808
16 changed files with 266 additions and 98 deletions

View File

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::fs::{self, File}; use std::fs::{self, File};
@ -11,19 +12,27 @@ use crate::errors::*;
/// Load a book into memory from its `src/` directory. /// Load a book into memory from its `src/` directory.
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> { pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
let src_dir = src_dir.as_ref(); let src_dir = src_dir.as_ref();
let summary_md = src_dir.join("SUMMARY.md");
let mut summary_content = String::new(); let summary = if !cfg.auto_summary {
File::open(&summary_md) let summary_md = src_dir.join("SUMMARY.md");
.with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))?
.read_to_string(&mut summary_content)?;
let summary = parse_summary(&summary_content) let mut summary_content = String::new();
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?; File::open(&summary_md)
.with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))?
.read_to_string(&mut summary_content)?;
if cfg.create_missing { let summary = parse_summary(&summary_content)
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?; .with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
}
if cfg.create_missing {
create_missing(src_dir, &summary)
.with_context(|| "Unable to create missing chapters")?;
}
summary
} else {
Summary::from_sources(src_dir)?
};
load_book_from_disk(&summary, src_dir) load_book_from_disk(&summary, src_dir)
} }
@ -53,7 +62,10 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
let mut f = File::create(&filename).with_context(|| { let mut f = File::create(&filename).with_context(|| {
format!("Unable to create missing file: {}", filename.display()) format!("Unable to create missing file: {}", filename.display())
})?; })?;
writeln!(f, "# {}", link.name)?;
if let Some(name) = &link.name {
writeln!(f, "# {}", name)?;
}
} }
} }
@ -253,7 +265,7 @@ fn load_chapter<P: AsRef<Path>>(
let src_dir = src_dir.as_ref(); let src_dir = src_dir.as_ref();
let mut ch = if let Some(ref link_location) = link.location { let mut ch = if let Some(ref link_location) = link.location {
debug!("Loading {} ({})", link.name, link_location.display()); debug!("Loading {}", link);
let location = if link_location.is_absolute() { let location = if link_location.is_absolute() {
link_location.clone() link_location.clone()
@ -265,9 +277,8 @@ fn load_chapter<P: AsRef<Path>>(
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?; .with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
let mut content = String::new(); let mut content = String::new();
f.read_to_string(&mut content).with_context(|| { f.read_to_string(&mut content)
format!("Unable to read \"{}\" ({})", link.name, location.display()) .with_context(|| format!("Unable to read {}", link))?;
})?;
if content.as_bytes().starts_with(b"\xef\xbb\xbf") { if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
content.replace_range(..3, ""); content.replace_range(..3, "");
@ -277,16 +288,21 @@ fn load_chapter<P: AsRef<Path>>(
.strip_prefix(&src_dir) .strip_prefix(&src_dir)
.expect("Chapters are always inside a book"); .expect("Chapters are always inside a book");
Chapter::new(&link.name, content, stripped, parent_names.clone()) let name = match &link.name {
Some(name) => Cow::Borrowed(name.as_str()),
None => Cow::Owned(read_chapter_title(&content)),
};
Chapter::new(&name, content, stripped, parent_names.clone())
} else { } else {
Chapter::new_draft(&link.name, parent_names.clone()) Chapter::new_draft(link.name.as_ref().unwrap().as_str(), parent_names.clone())
}; };
let mut sub_item_parents = parent_names; let mut sub_item_parents = parent_names;
ch.number = link.number.clone(); ch.number = link.number.clone();
sub_item_parents.push(link.name.clone()); sub_item_parents.push(ch.name.clone());
let sub_items = link let sub_items = link
.nested_items .nested_items
.iter() .iter()
@ -298,6 +314,23 @@ fn load_chapter<P: AsRef<Path>>(
Ok(ch) Ok(ch)
} }
/// Read a chapter title from its source file.
fn read_chapter_title(content: &str) -> String {
let mut pulldown_parser = pulldown_cmark::Parser::new(content);
while let Some(event) = pulldown_parser.next() {
if let pulldown_cmark::Event::Start(pulldown_cmark::Tag::Heading(1)) = event {
if let Some(pulldown_cmark::Event::Text(title)) = pulldown_parser.next() {
return title.to_string();
} else {
break;
}
}
}
"Untitled".to_string()
}
/// A depth-first iterator over the items in a book. /// A depth-first iterator over the items in a book.
/// ///
/// # Note /// # Note
@ -604,7 +637,7 @@ And here is some \
let (_, temp) = dummy_link(); let (_, temp) = dummy_link();
let summary = Summary { let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link { numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("Empty"), name: Some(String::from("Empty")),
location: Some(PathBuf::from("")), location: Some(PathBuf::from("")),
..Default::default() ..Default::default()
})], })],
@ -624,7 +657,7 @@ And here is some \
let summary = Summary { let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link { numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("nested"), name: Some(String::from("nested")),
location: Some(dir), location: Some(dir),
..Default::default() ..Default::default()
})], })],

View File

@ -81,7 +81,7 @@ impl BookBuilder {
self.write_book_toml()?; self.write_book_toml()?;
match MDBook::load(&self.root) { match MDBook::load(&self.root, false) {
Ok(book) => Ok(book), Ok(book) => Ok(book),
Err(e) => { Err(e) => {
error!("{}", e); error!("{}", e);

View File

@ -47,7 +47,10 @@ pub struct MDBook {
impl MDBook { impl MDBook {
/// Load a book from its root directory on disk. /// Load a book from its root directory on disk.
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> { ///
/// If `auto_summary` is set, the book's summary is automatically generated
/// from the root directory structure.
pub fn load<P: Into<PathBuf>>(book_root: P, auto_summary: bool) -> Result<MDBook> {
let book_root = book_root.into(); let book_root = book_root.into();
let config_location = book_root.join("book.toml"); let config_location = book_root.join("book.toml");
@ -68,6 +71,10 @@ impl MDBook {
Config::default() Config::default()
}; };
if auto_summary {
config.build.auto_summary = true;
}
config.update_from_env(); config.update_from_env();
if log_enabled!(log::Level::Trace) { if log_enabled!(log::Level::Trace) {
@ -128,7 +135,7 @@ impl MDBook {
/// ```no_run /// ```no_run
/// # use mdbook::MDBook; /// # use mdbook::MDBook;
/// # use mdbook::book::BookItem; /// # use mdbook::book::BookItem;
/// # let book = MDBook::load("mybook").unwrap(); /// # let book = MDBook::load("mybook", false).unwrap();
/// for item in book.iter() { /// for item in book.iter() {
/// match *item { /// match *item {
/// BookItem::Chapter(ref chapter) => {}, /// BookItem::Chapter(ref chapter) => {},

View File

@ -66,6 +66,91 @@ pub struct Summary {
pub suffix_chapters: Vec<SummaryItem>, pub suffix_chapters: Vec<SummaryItem>,
} }
impl Summary {
/// Create a summary from the book's sources directory.
///
/// Each file is imported as a book chapter.
/// Each folder is imported as a book chapter and must contain
/// a `README.md` file defining the chapter's title and content.
/// Any file/folder inside the directory is imported as a sub-chapter.
/// The file/folder name is used to compose the chapter's link.
///
/// Chapters are added to the book in alphabetical order, using the file/folder name.
pub fn from_sources<P: AsRef<Path>>(src_dir: P) -> std::io::Result<Summary> {
let mut summary = Summary {
title: None,
prefix_chapters: Vec::new(),
numbered_chapters: Vec::new(),
suffix_chapters: Vec::new(),
};
// Checks if the given path must be considered.
fn include_path(path: &Path) -> bool {
if let Some(name) = path.file_name() {
if name == "README.md" || name == "SUMMARY.md" {
return false;
}
}
true
}
// Read a directory recursively and return the found summary items.
fn read_dir<P: AsRef<Path>>(dir: P) -> std::io::Result<Vec<SummaryItem>> {
let mut links = Vec::new();
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let entry_path = entry.path();
if include_path(&entry_path) {
let metadata = std::fs::metadata(&entry_path)?;
if metadata.is_file() {
links.push(Link::new_unnamed(entry_path))
} else {
let chapter_path = entry_path.join("README.md");
if chapter_path.is_file() {
let mut link = Link::new_unnamed(chapter_path);
link.nested_items = read_dir(entry_path)?;
links.push(link)
}
}
}
}
// Items are sorted by name.
links.sort_by(|a, b| {
a.location
.as_ref()
.unwrap()
.cmp(b.location.as_ref().unwrap())
});
Ok(links.into_iter().map(SummaryItem::Link).collect())
}
// Associate the correct section number to each summary item.
fn number_items(items: &mut [SummaryItem], number: &[u32]) {
let mut n = 1;
for item in items {
if let SummaryItem::Link(link) = item {
let mut entry_number = number.to_vec();
entry_number.push(n);
n += 1;
number_items(&mut link.nested_items, &entry_number);
link.number = Some(SectionNumber(entry_number))
}
}
}
summary.numbered_chapters = read_dir(src_dir)?;
number_items(&mut summary.numbered_chapters, &[]);
Ok(summary)
}
}
/// A struct representing an entry in the `SUMMARY.md`, possibly with nested /// A struct representing an entry in the `SUMMARY.md`, possibly with nested
/// entries. /// entries.
/// ///
@ -73,7 +158,7 @@ pub struct Summary {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Link { pub struct Link {
/// The name of the chapter. /// The name of the chapter.
pub name: String, pub name: Option<String>,
/// The location of the chapter's source file, taking the book's `src` /// The location of the chapter's source file, taking the book's `src`
/// directory as the root. /// directory as the root.
pub location: Option<PathBuf>, pub location: Option<PathBuf>,
@ -87,7 +172,17 @@ impl Link {
/// Create a new link with no nested items. /// Create a new link with no nested items.
pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link { pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
Link { Link {
name: name.into(), name: Some(name.into()),
location: Some(location.as_ref().to_path_buf()),
number: None,
nested_items: Vec::new(),
}
}
/// Create a new unnamed link with no nested items.
pub fn new_unnamed<P: AsRef<Path>>(location: P) -> Link {
Link {
name: None,
location: Some(location.as_ref().to_path_buf()), location: Some(location.as_ref().to_path_buf()),
number: None, number: None,
nested_items: Vec::new(), nested_items: Vec::new(),
@ -98,7 +193,7 @@ impl Link {
impl Default for Link { impl Default for Link {
fn default() -> Self { fn default() -> Self {
Link { Link {
name: String::new(), name: None,
location: Some(PathBuf::new()), location: Some(PathBuf::new()),
number: None, number: None,
nested_items: Vec::new(), nested_items: Vec::new(),
@ -106,6 +201,22 @@ impl Default for Link {
} }
} }
impl fmt::Display for Link {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.name {
Some(name) => write!(f, "\"{}\"", name)?,
None => write!(f, "unnamed chapter")?,
}
match &self.location {
Some(location) => write!(f, " ({})", location.display())?,
None => write!(f, " [draft]")?,
}
Ok(())
}
}
/// An item in `SUMMARY.md` which could be either a separator or a `Link`. /// An item in `SUMMARY.md` which could be either a separator or a `Link`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SummaryItem { pub enum SummaryItem {
@ -344,7 +455,7 @@ impl<'a> SummaryParser<'a> {
}; };
Link { Link {
name, name: Some(name),
location: path, location: path,
number: None, number: None,
nested_items: Vec::new(), nested_items: Vec::new(),
@ -489,15 +600,7 @@ impl<'a> SummaryParser<'a> {
let mut number = parent.clone(); let mut number = parent.clone();
number.0.push(num_existing_items as u32 + 1); number.0.push(num_existing_items as u32 + 1);
trace!( trace!("Found chapter: {} {}", number, link);
"Found chapter: {} {} ({})",
number,
link.name,
link.location
.as_ref()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("[draft]")
);
link.number = Some(number); link.number = Some(number);
@ -676,12 +779,12 @@ mod tests {
let should_be = vec![ let should_be = vec![
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("First"), name: Some(String::from("First")),
location: Some(PathBuf::from("./first.md")), location: Some(PathBuf::from("./first.md")),
..Default::default() ..Default::default()
}), }),
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("Second"), name: Some(String::from("Second")),
location: Some(PathBuf::from("./second.md")), location: Some(PathBuf::from("./second.md")),
..Default::default() ..Default::default()
}), }),
@ -717,7 +820,7 @@ mod tests {
fn parse_a_link() { fn parse_a_link() {
let src = "[First](./first.md)"; let src = "[First](./first.md)";
let should_be = Link { let should_be = Link {
name: String::from("First"), name: Some(String::from("First")),
location: Some(PathBuf::from("./first.md")), location: Some(PathBuf::from("./first.md")),
..Default::default() ..Default::default()
}; };
@ -738,7 +841,7 @@ mod tests {
fn parse_a_numbered_chapter() { fn parse_a_numbered_chapter() {
let src = "- [First](./first.md)\n"; let src = "- [First](./first.md)\n";
let link = Link { let link = Link {
name: String::from("First"), name: Some(String::from("First")),
location: Some(PathBuf::from("./first.md")), location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])), number: Some(SectionNumber(vec![1])),
..Default::default() ..Default::default()
@ -759,18 +862,18 @@ mod tests {
let should_be = vec![ let should_be = vec![
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("First"), name: Some(String::from("First")),
location: Some(PathBuf::from("./first.md")), location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])), number: Some(SectionNumber(vec![1])),
nested_items: vec![SummaryItem::Link(Link { nested_items: vec![SummaryItem::Link(Link {
name: String::from("Nested"), name: Some(String::from("Nested")),
location: Some(PathBuf::from("./nested.md")), location: Some(PathBuf::from("./nested.md")),
number: Some(SectionNumber(vec![1, 1])), number: Some(SectionNumber(vec![1, 1])),
nested_items: Vec::new(), nested_items: Vec::new(),
})], })],
}), }),
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("Second"), name: Some(String::from("Second")),
location: Some(PathBuf::from("./second.md")), location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])), number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(), nested_items: Vec::new(),
@ -791,13 +894,13 @@ mod tests {
let should_be = vec![ let should_be = vec![
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("First"), name: Some(String::from("First")),
location: Some(PathBuf::from("./first.md")), location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])), number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(), nested_items: Vec::new(),
}), }),
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("Second"), name: Some(String::from("Second")),
location: Some(PathBuf::from("./second.md")), location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])), number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(), nested_items: Vec::new(),
@ -819,24 +922,24 @@ mod tests {
let should_be = vec![ let should_be = vec![
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("First"), name: Some(String::from("First")),
location: Some(PathBuf::from("./first.md")), location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])), number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(), nested_items: Vec::new(),
}), }),
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("Second"), name: Some(String::from("Second")),
location: Some(PathBuf::from("./second.md")), location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])), number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(), nested_items: Vec::new(),
}), }),
SummaryItem::PartTitle(String::from("Title 2")), SummaryItem::PartTitle(String::from("Title 2")),
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("Third"), name: Some(String::from("Third")),
location: Some(PathBuf::from("./third.md")), location: Some(PathBuf::from("./third.md")),
number: Some(SectionNumber(vec![3])), number: Some(SectionNumber(vec![3])),
nested_items: vec![SummaryItem::Link(Link { nested_items: vec![SummaryItem::Link(Link {
name: String::from("Fourth"), name: Some(String::from("Fourth")),
location: Some(PathBuf::from("./fourth.md")), location: Some(PathBuf::from("./fourth.md")),
number: Some(SectionNumber(vec![3, 1])), number: Some(SectionNumber(vec![3, 1])),
nested_items: Vec::new(), nested_items: Vec::new(),
@ -859,13 +962,13 @@ mod tests {
let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.md)\n"; let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.md)\n";
let should_be = vec![ let should_be = vec![
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("First"), name: Some(String::from("First")),
location: Some(PathBuf::from("./first.md")), location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])), number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(), nested_items: Vec::new(),
}), }),
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("Second"), name: Some(String::from("Second")),
location: Some(PathBuf::from("./second.md")), location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])), number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(), nested_items: Vec::new(),
@ -887,7 +990,7 @@ mod tests {
let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default()); let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default());
let should_be = vec![SummaryItem::Link(Link { let should_be = vec![SummaryItem::Link(Link {
name: String::from("Empty"), name: Some(String::from("Empty")),
location: None, location: None,
number: Some(SectionNumber(vec![1])), number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(), nested_items: Vec::new(),
@ -905,21 +1008,21 @@ mod tests {
"- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n"; "- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
let should_be = vec![ let should_be = vec![
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("First"), name: Some(String::from("First")),
location: Some(PathBuf::from("./first.md")), location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])), number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(), nested_items: Vec::new(),
}), }),
SummaryItem::Separator, SummaryItem::Separator,
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("Second"), name: Some(String::from("Second")),
location: Some(PathBuf::from("./second.md")), location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])), number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(), nested_items: Vec::new(),
}), }),
SummaryItem::Separator, SummaryItem::Separator,
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("Third"), name: Some(String::from("Third")),
location: Some(PathBuf::from("./third.md")), location: Some(PathBuf::from("./third.md")),
number: Some(SectionNumber(vec![3])), number: Some(SectionNumber(vec![3])),
nested_items: Vec::new(), nested_items: Vec::new(),
@ -940,7 +1043,7 @@ mod tests {
fn add_space_for_multi_line_chapter_names() { fn add_space_for_multi_line_chapter_names() {
let src = "- [Chapter\ntitle](./chapter.md)"; let src = "- [Chapter\ntitle](./chapter.md)";
let should_be = vec![SummaryItem::Link(Link { let should_be = vec![SummaryItem::Link(Link {
name: String::from("Chapter title"), name: Some(String::from("Chapter title")),
location: Some(PathBuf::from("./chapter.md")), location: Some(PathBuf::from("./chapter.md")),
number: Some(SectionNumber(vec![1])), number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(), nested_items: Vec::new(),
@ -959,13 +1062,13 @@ mod tests {
let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)"; let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)";
let should_be = vec![ let should_be = vec![
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("test1"), name: Some(String::from("test1")),
location: Some(PathBuf::from("./test link1.md")), location: Some(PathBuf::from("./test link1.md")),
number: Some(SectionNumber(vec![1])), number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(), nested_items: Vec::new(),
}), }),
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from("test2"), name: Some(String::from("test2")),
location: Some(PathBuf::from("./test link2.md")), location: Some(PathBuf::from("./test link2.md")),
number: Some(SectionNumber(vec![2])), number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(), nested_items: Vec::new(),
@ -1030,7 +1133,7 @@ mod tests {
let new_affix_item = |name, location| { let new_affix_item = |name, location| {
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from(name), name: Some(String::from(name)),
location: Some(PathBuf::from(location)), location: Some(PathBuf::from(location)),
..Default::default() ..Default::default()
}) })
@ -1048,7 +1151,7 @@ mod tests {
let new_numbered_item = |name, location, numbers: &[u32], nested_items| { let new_numbered_item = |name, location, numbers: &[u32], nested_items| {
SummaryItem::Link(Link { SummaryItem::Link(Link {
name: String::from(name), name: Some(String::from(name)),
location: Some(PathBuf::from(location)), location: Some(PathBuf::from(location)),
number: Some(SectionNumber(numbers.to_vec())), number: Some(SectionNumber(numbers.to_vec())),
nested_items, nested_items,

View File

@ -17,12 +17,16 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)'",
) )
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'") .arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
.arg_from_usage(
"--auto-summary 'Automatically generate the book's summary{n}\
from the sources directory structure.'",
)
} }
// Build command implementation // Build command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(&book_dir, args.is_present("auto-summary"))?;
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into(); book.config.build.build_dir = dest_dir.into();

View File

@ -18,12 +18,16 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
"[dir] 'Root directory for the book{n}\ "[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)'",
) )
.arg_from_usage(
"--auto-summary 'Automatically generate the book's summary{n}\
from the sources directory structure.'",
)
} }
// Clean command implementation // Clean command implementation
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> { pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let book = MDBook::load(&book_dir)?; let book = MDBook::load(&book_dir, args.is_present("auto-summary"))?;
let dir_to_remove = match args.value_of("dest-dir") { let dir_to_remove = match args.value_of("dest-dir") {
Some(dest_dir) => dest_dir.into(), Some(dest_dir) => dest_dir.into(),

View File

@ -49,12 +49,16 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.help("Port to use for HTTP connections"), .help("Port to use for HTTP connections"),
) )
.arg_from_usage("-o, --open 'Opens the book server in a web browser'") .arg_from_usage("-o, --open 'Opens the book server in a web browser'")
.arg_from_usage(
"--auto-summary 'Automatically generate the book's summary{n}\
from the sources directory structure.'",
)
} }
// Serve command implementation // Serve command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(&book_dir, args.is_present("auto-summary"))?;
let port = args.value_of("port").unwrap(); let port = args.value_of("port").unwrap();
let hostname = args.value_of("hostname").unwrap(); let hostname = args.value_of("hostname").unwrap();
@ -110,7 +114,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
info!("Building book..."); info!("Building book...");
// FIXME: This area is really ugly because we need to re-set livereload :( // FIXME: This area is really ugly because we need to re-set livereload :(
let result = MDBook::load(&book_dir).and_then(|mut b| { let result = MDBook::load(&book_dir, args.is_present("auto-summary")).and_then(|mut b| {
update_config(&mut b); update_config(&mut b);
b.build() b.build()
}); });

View File

@ -25,6 +25,10 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.multiple(true) .multiple(true)
.empty_values(false) .empty_values(false)
.help("A comma-separated list of directories to add to {n}the crate search path when building tests")) .help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
.arg_from_usage(
"--auto-summary 'Automatically generate the book's summary{n}\
from the sources directory structure.'"
)
} }
// test command implementation // test command implementation
@ -34,7 +38,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
.map(std::iter::Iterator::collect) .map(std::iter::Iterator::collect)
.unwrap_or_default(); .unwrap_or_default();
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(&book_dir, args.is_present("auto-summary"))?;
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into(); book.config.build.build_dir = dest_dir.into();

View File

@ -23,12 +23,16 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)'",
) )
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'") .arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.arg_from_usage(
"--auto-summary 'Automatically generate the book's summary{n}\
from the sources directory structure.'",
)
} }
// Watch command implementation // Watch command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(&book_dir, args.is_present("auto-summary"))?;
let update_config = |book: &mut MDBook| { let update_config = |book: &mut MDBook| {
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
@ -44,7 +48,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
trigger_on_change(&book, |paths, book_dir| { trigger_on_change(&book, |paths, book_dir| {
info!("Files changed: {:?}\nBuilding book...\n", paths); info!("Files changed: {:?}\nBuilding book...\n", paths);
let result = MDBook::load(&book_dir).and_then(|mut b| { let result = MDBook::load(&book_dir, args.is_present("auto-summary")).and_then(|mut b| {
update_config(&mut b); update_config(&mut b);
b.build() b.build()
}); });

View File

@ -444,6 +444,8 @@ pub struct BuildConfig {
/// Should the default preprocessors always be used when they are /// Should the default preprocessors always be used when they are
/// compatible with the renderer? /// compatible with the renderer?
pub use_default_preprocessors: bool, pub use_default_preprocessors: bool,
/// Automatically build the book's summary from the directory structure.
pub auto_summary: bool,
} }
impl Default for BuildConfig { impl Default for BuildConfig {
@ -452,6 +454,7 @@ impl Default for BuildConfig {
build_dir: PathBuf::from("book"), build_dir: PathBuf::from("book"),
create_missing: true, create_missing: true,
use_default_preprocessors: true, use_default_preprocessors: true,
auto_summary: false,
} }
} }
} }
@ -769,6 +772,7 @@ mod tests {
build_dir: PathBuf::from("outputs"), build_dir: PathBuf::from("outputs"),
create_missing: false, create_missing: false,
use_default_preprocessors: true, use_default_preprocessors: true,
auto_summary: false,
}; };
let rust_should_be = RustConfig { edition: None }; let rust_should_be = RustConfig { edition: None };
let playground_should_be = Playground { let playground_should_be = Playground {
@ -962,6 +966,7 @@ mod tests {
build_dir: PathBuf::from("my-book"), build_dir: PathBuf::from("my-book"),
create_missing: true, create_missing: true,
use_default_preprocessors: true, use_default_preprocessors: true,
auto_summary: false,
}; };
let html_should_be = HtmlConfig { let html_should_be = HtmlConfig {

View File

@ -52,7 +52,7 @@
//! //!
//! let root_dir = "/path/to/book/root"; //! let root_dir = "/path/to/book/root";
//! //!
//! let mut md = MDBook::load(root_dir) //! let mut md = MDBook::load(root_dir, false)
//! .expect("Unable to load the book"); //! .expect("Unable to load the book");
//! md.build().expect("Building failed"); //! md.build().expect("Building failed");
//! ``` //! ```

View File

@ -183,7 +183,7 @@ mod tests {
fn guide() -> MDBook { fn guide() -> MDBook {
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide"); let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
MDBook::load(example).unwrap() MDBook::load(example, false).unwrap()
} }
#[test] #[test]

View File

@ -33,7 +33,7 @@ fn example_doesnt_support_not_supported() {
fn ask_the_preprocessor_to_blow_up() { fn ask_the_preprocessor_to_blow_up() {
let dummy_book = DummyBook::new(); let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap(); let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap(); let mut md = MDBook::load(temp.path(), false).unwrap();
md.with_preprocessor(example()); md.with_preprocessor(example());
md.config md.config
@ -49,7 +49,7 @@ fn ask_the_preprocessor_to_blow_up() {
fn process_the_dummy_book() { fn process_the_dummy_book() {
let dummy_book = DummyBook::new(); let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap(); let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap(); let mut md = MDBook::load(temp.path(), false).unwrap();
md.with_preprocessor(example()); md.with_preprocessor(example());
md.build().unwrap(); md.build().unwrap();

View File

@ -95,7 +95,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap(); let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
assert_eq!( assert_eq!(
contents, contents,
"[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"in\"\n\n[build]\nbuild-dir = \"out\"\ncreate-missing = true\nuse-default-preprocessors = true\n" "[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"in\"\n\n[build]\nauto-summary = false\nbuild-dir = \"out\"\ncreate-missing = true\nuse-default-preprocessors = true\n"
); );
} }

View File

@ -42,7 +42,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
#[test] #[test]
fn build_the_dummy_book() { fn build_the_dummy_book() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
} }
@ -50,7 +50,7 @@ fn build_the_dummy_book() {
#[test] #[test]
fn by_default_mdbook_generates_rendered_content_in_the_book_directory() { fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
assert!(!temp.path().join("book").exists()); assert!(!temp.path().join("book").exists());
md.build().unwrap(); md.build().unwrap();
@ -63,7 +63,7 @@ fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
#[test] #[test]
fn make_sure_bottom_level_files_contain_links_to_chapters() { fn make_sure_bottom_level_files_contain_links_to_chapters() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let dest = temp.path().join("book"); let dest = temp.path().join("book");
@ -85,7 +85,7 @@ fn make_sure_bottom_level_files_contain_links_to_chapters() {
#[test] #[test]
fn check_correct_cross_links_in_nested_dir() { fn check_correct_cross_links_in_nested_dir() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let first = temp.path().join("book").join("first"); let first = temp.path().join("book").join("first");
@ -117,7 +117,7 @@ fn check_correct_cross_links_in_nested_dir() {
#[test] #[test]
fn check_correct_relative_links_in_print_page() { fn check_correct_relative_links_in_print_page() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let first = temp.path().join("book"); let first = temp.path().join("book");
@ -138,7 +138,7 @@ fn check_correct_relative_links_in_print_page() {
#[test] #[test]
fn rendered_code_has_playground_stuff() { fn rendered_code_has_playground_stuff() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let nested = temp.path().join("book/first/nested.html"); let nested = temp.path().join("book/first/nested.html");
@ -153,7 +153,7 @@ fn rendered_code_has_playground_stuff() {
#[test] #[test]
fn anchors_include_text_between_but_not_anchor_comments() { fn anchors_include_text_between_but_not_anchor_comments() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let nested = temp.path().join("book/first/nested.html"); let nested = temp.path().join("book/first/nested.html");
@ -167,7 +167,7 @@ fn anchors_include_text_between_but_not_anchor_comments() {
#[test] #[test]
fn rustdoc_include_hides_the_unspecified_part_of_the_file() { fn rustdoc_include_hides_the_unspecified_part_of_the_file() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let nested = temp.path().join("book/first/nested.html"); let nested = temp.path().join("book/first/nested.html");
@ -191,7 +191,7 @@ fn chapter_content_appears_in_rendered_document() {
]; ];
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let destination = temp.path().join("book"); let destination = temp.path().join("book");
@ -251,7 +251,7 @@ fn root_index_html() -> Result<Document> {
let temp = DummyBook::new() let temp = DummyBook::new()
.build() .build()
.with_context(|| "Couldn't create the dummy book")?; .with_context(|| "Couldn't create the dummy book")?;
MDBook::load(temp.path())? MDBook::load(temp.path(), false)?
.build() .build()
.with_context(|| "Book building failed")?; .with_context(|| "Book building failed")?;
@ -350,7 +350,7 @@ fn create_missing_file_with_config() {
#[test] #[test]
fn able_to_include_playground_files_in_chapters() { fn able_to_include_playground_files_in_chapters() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let second = temp.path().join("book/second.html"); let second = temp.path().join("book/second.html");
@ -368,7 +368,7 @@ fn able_to_include_playground_files_in_chapters() {
#[test] #[test]
fn able_to_include_files_in_chapters() { fn able_to_include_files_in_chapters() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let includes = temp.path().join("book/first/includes.html"); let includes = temp.path().join("book/first/includes.html");
@ -386,7 +386,7 @@ fn able_to_include_files_in_chapters() {
#[test] #[test]
fn recursive_includes_are_capped() { fn recursive_includes_are_capped() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let recursive = temp.path().join("book/first/recursive.html"); let recursive = temp.path().join("book/first/recursive.html");
@ -400,7 +400,7 @@ Around the world, around the world"];
fn example_book_can_build() { fn example_book_can_build() {
let example_book_dir = dummy_book::new_copy_of_example_book().unwrap(); let example_book_dir = dummy_book::new_copy_of_example_book().unwrap();
let md = MDBook::load(example_book_dir.path()).unwrap(); let md = MDBook::load(example_book_dir.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
} }
@ -418,7 +418,7 @@ fn book_with_a_reserved_filename_does_not_build() {
let mut summary_file = fs::File::create(summary_path).unwrap(); let mut summary_file = fs::File::create(summary_path).unwrap();
writeln!(summary_file, "[print](print.md)").unwrap(); writeln!(summary_file, "[print](print.md)").unwrap();
let md = MDBook::load(tmp_dir.path()).unwrap(); let md = MDBook::load(tmp_dir.path(), false).unwrap();
let got = md.build(); let got = md.build();
assert!(got.is_err()); assert!(got.is_err());
} }
@ -457,7 +457,7 @@ fn theme_dir_overrides_work_correctly() {
write_file(&theme_dir, "index.hbs", &index).unwrap(); write_file(&theme_dir, "index.hbs", &index).unwrap();
let md = MDBook::load(book_dir).unwrap(); let md = MDBook::load(book_dir, false).unwrap();
md.build().unwrap(); md.build().unwrap();
let built_index = book_dir.join("book").join("index.html"); let built_index = book_dir.join("book").join("index.html");
@ -467,7 +467,7 @@ fn theme_dir_overrides_work_correctly() {
#[test] #[test]
fn no_index_for_print_html() { fn no_index_for_print_html() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let print_html = temp.path().join("book/print.html"); let print_html = temp.path().join("book/print.html");
@ -480,7 +480,7 @@ fn no_index_for_print_html() {
#[test] #[test]
fn markdown_options() { fn markdown_options() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let path = temp.path().join("book/first/markdown.html"); let path = temp.path().join("book/first/markdown.html");
@ -516,7 +516,7 @@ fn markdown_options() {
#[test] #[test]
fn redirects_are_emitted_correctly() { fn redirects_are_emitted_correctly() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap(); let mut md = MDBook::load(temp.path(), false).unwrap();
// override the "outputs.html.redirect" table // override the "outputs.html.redirect" table
let redirects: HashMap<PathBuf, String> = vec![ let redirects: HashMap<PathBuf, String> = vec![
@ -555,7 +555,7 @@ fn edit_url_has_default_src_dir_edit_url() {
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap(); write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let index_html = temp.path().join("book").join("index.html"); let index_html = temp.path().join("book").join("index.html");
@ -581,7 +581,7 @@ fn edit_url_has_configured_src_dir_edit_url() {
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap(); write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let index_html = temp.path().join("book").join("index.html"); let index_html = temp.path().join("book").join("index.html");
@ -619,7 +619,7 @@ mod search {
#[allow(clippy::float_cmp)] #[allow(clippy::float_cmp)]
fn book_creates_reasonable_search_index() { fn book_creates_reasonable_search_index() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let index = read_book_index(temp.path()); let index = read_book_index(temp.path());
@ -671,7 +671,7 @@ mod search {
fn get_fixture() -> serde_json::Value { fn get_fixture() -> serde_json::Value {
if GENERATE_FIXTURE { if GENERATE_FIXTURE {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let src = read_book_index(temp.path()); let src = read_book_index(temp.path());
@ -699,7 +699,7 @@ mod search {
#[test] #[test]
fn search_index_hasnt_changed_accidentally() { fn search_index_hasnt_changed_accidentally() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path(), false).unwrap();
md.build().unwrap(); md.build().unwrap();
let book_index = read_book_index(temp.path()); let book_index = read_book_index(temp.path());

View File

@ -7,7 +7,7 @@ use mdbook::MDBook;
#[test] #[test]
fn mdbook_can_correctly_test_a_passing_book() { fn mdbook_can_correctly_test_a_passing_book() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap(); let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap(); let mut md = MDBook::load(temp.path(), false).unwrap();
let result = md.test(vec![]); let result = md.test(vec![]);
assert!( assert!(
@ -20,7 +20,7 @@ fn mdbook_can_correctly_test_a_passing_book() {
#[test] #[test]
fn mdbook_detects_book_with_failing_tests() { fn mdbook_detects_book_with_failing_tests() {
let temp = DummyBook::new().with_passing_test(false).build().unwrap(); let temp = DummyBook::new().with_passing_test(false).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap(); let mut md = MDBook::load(temp.path(), false).unwrap();
assert!(md.test(vec![]).is_err()); assert!(md.test(vec![]).is_err());
} }