diff --git a/book-example/src/SUMMARY.md b/book-example/src/SUMMARY.md index 8d7324a68a..1472da6cfa 100644 --- a/book-example/src/SUMMARY.md +++ b/book-example/src/SUMMARY.md @@ -10,6 +10,7 @@ - [clean](cli/clean.md) - [Format](format/format.md) - [SUMMARY.md](format/summary.md) + - [Virtual Chapter]() - [Configuration](format/config.md) - [Theme](format/theme/theme.md) - [index.hbs](format/theme/index-hbs.md) diff --git a/src/bin/init.rs b/src/bin/init.rs index f22618ba46..0f66a647e2 100644 --- a/src/bin/init.rs +++ b/src/bin/init.rs @@ -4,7 +4,6 @@ use std::process::Command; use clap::{App, ArgMatches, SubCommand}; use mdbook::MDBook; use mdbook::errors::Result; -use mdbook::utils; use mdbook::config; use get_book_dir; diff --git a/src/book/book.rs b/src/book/book.rs index 7da28a6871..5c63c373aa 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -4,7 +4,7 @@ use std::collections::VecDeque; use std::fs::{self, File}; use std::io::{Read, Write}; -use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; +use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem, VirtualLink}; use config::BuildConfig; use errors::*; @@ -129,16 +129,27 @@ where pub enum BookItem { /// A nested chapter. Chapter(Chapter), + /// A nested virtual chapter. + VirtualChapter(VirtualChapter), /// A section separator. Separator, + // To make sure clients have a `_ =>` case + #[doc(hidden)] + __NonExhaustive, } impl From for BookItem { - fn from(other: Chapter) -> BookItem { + fn from(other: Chapter) -> Self { BookItem::Chapter(other) } } +impl From for BookItem { + fn from(other: VirtualChapter) -> Self { + BookItem::VirtualChapter(other) + } +} + /// The representation of a "chapter", usually mapping to a single file on /// disk however it may contain multiple sub-chapters. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -175,6 +186,28 @@ impl Chapter { } } +/// The representation of a "virtual chapter", available for namespacing +/// purposes and not mapping to a file on disk. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct VirtualChapter { + /// The chapter's name. + pub name: String, + /// The chapter's section number, if it has one. + pub number: Option, + /// Nested items. + pub sub_items: Vec, +} + +impl VirtualChapter { + /// Create a new virtual chapter with the given name. + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + ..Default::default() + } + } +} + /// Use the provided `Summary` to load a `Book` from disk. /// /// You need to pass in the book's source directory because all the links in @@ -212,6 +245,8 @@ fn load_summary_item>( SummaryItem::Link(ref link) => { load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c)) } + SummaryItem::VirtualLink(ref link) => + load_virtual_chapter(link, src_dir, parent_names).map(VirtualChapter::into), } } @@ -255,6 +290,23 @@ fn load_chapter>( Ok(ch) } +fn load_virtual_chapter>(link: &VirtualLink, src_dir: P, parent_names: Vec) -> Result { + debug!("Loading {}", link.name); + let src_dir = src_dir.as_ref(); + + let mut ch = VirtualChapter::new(&link.name); + ch.number = link.number.clone(); + + let sub_items = link.nested_items + .iter() + .map(|i| load_summary_item(i, src_dir, parent_names.clone())) + .collect::>>()?; + + ch.sub_items = sub_items; + + Ok(ch) +} + /// A depth-first iterator over the items in a book. /// /// # Note @@ -273,11 +325,20 @@ impl<'a> Iterator for BookItems<'a> { fn next(&mut self) -> Option { let item = self.items.pop_front(); - if let Some(&BookItem::Chapter(ref ch)) = item { - // if we wanted a breadth-first iterator we'd `extend()` here - for sub_item in ch.sub_items.iter().rev() { - self.items.push_front(sub_item); + match item { + Some(&BookItem::Chapter(ref ch)) => { + // if we wanted a breadth-first iterator we'd `extend()` here + for sub_item in ch.sub_items.iter().rev() { + self.items.push_front(sub_item); + } + }, + Some(&BookItem::VirtualChapter(ref ch)) => { + // if we wanted a breadth-first iterator we'd `extend()` here + for sub_item in ch.sub_items.iter().rev() { + self.items.push_front(sub_item); + } } + _ => {}, } item diff --git a/src/book/mod.rs b/src/book/mod.rs index 03871e7d11..780f206a2b 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -107,7 +107,9 @@ impl MDBook { /// for item in book.iter() { /// match *item { /// BookItem::Chapter(ref chapter) => {}, + /// BookItem::VirtualChapter(ref virtual_chapter) => {}, /// BookItem::Separator => {}, + /// _ => {}, /// } /// } /// diff --git a/src/book/summary.rs b/src/book/summary.rs index ceb38ebc49..ad5a83470d 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; use std::iter::FromIterator; use std::ops::{Deref, DerefMut}; @@ -61,10 +62,9 @@ pub struct Summary { pub suffix_chapters: Vec, } -/// A struct representing an entry in the `SUMMARY.md`, possibly with nested -/// entries. +/// A linked chapter in the `SUMMARY.md`, possibly with nested entries. /// -/// This is roughly the equivalent of `[Some section](./path/to/file.md)`. +/// This is roughly the equivalent of `- [Some section](./path/to/file.md)`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Link { /// The name of the chapter. @@ -80,8 +80,8 @@ pub struct Link { impl Link { /// Create a new link with no nested items. - pub fn new, P: AsRef>(name: S, location: P) -> Link { - Link { + pub fn new, P: AsRef>(name: S, location: P) -> Self { + Self { name: name.into(), location: location.as_ref().to_path_buf(), number: None, @@ -92,7 +92,7 @@ impl Link { impl Default for Link { fn default() -> Self { - Link { + Self { name: String::new(), location: PathBuf::new(), number: None, @@ -101,11 +101,48 @@ impl Default for Link { } } -/// An item in `SUMMARY.md` which could be either a separator or a `Link`. +/// A linked virtual chapter in the `SUMMARY.md`, possibly with nested entries. +/// +/// This is roughly the equivalent of `- Some virtual section`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct VirtualLink { + /// The name of the chapter. + pub name: String, + /// The section number, if this virtual chapter is in the numbered section. + pub number: Option, + /// Any nested items this virtual chapter may contain. + pub nested_items: Vec, +} + +impl VirtualLink { + /// Create a new virtual link with no nested items. + pub fn new>(name: S) -> Self { + Self { + name: name.into(), + number: None, + nested_items: Vec::new(), + } + } +} + +impl Default for VirtualLink { + fn default() -> Self { + Self { + name: String::new(), + number: None, + nested_items: Vec::new(), + } + } +} + +/// An entry in the `SUMMARY.md` which could be either a `Link`, a +/// `VirtualLink` or separator. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SummaryItem { /// A link to a chapter. Link(Link), + /// A link to a virtual chapter. + VirtualLink(VirtualLink), /// A separator (`---`). Separator, } @@ -117,14 +154,27 @@ impl SummaryItem { _ => None, } } + + fn maybe_virtual_link_mut(&mut self) -> Option<&mut VirtualLink> { + match *self { + SummaryItem::VirtualLink(ref mut l) => Some(l), + _ => None, + } + } } impl From for SummaryItem { - fn from(other: Link) -> SummaryItem { + fn from(other: Link) -> Self { SummaryItem::Link(other) } } +impl From for SummaryItem { + fn from(other: VirtualLink) -> Self { + SummaryItem::VirtualLink(other) + } +} + /// A recursive descent (-ish) parser for a `SUMMARY.md`. /// /// @@ -142,9 +192,11 @@ impl From for SummaryItem { /// numbered_chapters ::= dotted_item+ /// dotted_item ::= INDENT* DOT_POINT item /// item ::= link +/// | virtual_link /// | separator -/// separator ::= "---" /// link ::= "[" TEXT "]" "(" TEXT ")" +/// virtual_link ::= "[" TEXT "]" "()" +/// separator ::= "---" /// DOT_POINT ::= "-" /// | "*" /// ``` @@ -255,10 +307,7 @@ impl<'a> SummaryParser<'a> { bail!(self.parse_error("Suffix chapters cannot be followed by a list")); } } - Some(Event::Start(Tag::Link(href, _))) => { - let link = self.parse_link(href.to_string())?; - items.push(SummaryItem::Link(link)); - } + Some(Event::Start(Tag::Link(href, _))) => items.push(self.parse_linklike(href)), Some(Event::Start(Tag::Rule)) => items.push(SummaryItem::Separator), Some(_) => {} None => break, @@ -268,19 +317,18 @@ impl<'a> SummaryParser<'a> { Ok(items) } - fn parse_link(&mut self, href: String) -> Result { - let link_content = collect_events!(self.stream, end Tag::Link(..)); - let name = stringify_events(link_content); + fn parse_linklike(&mut self, href: Cow<'a, str>) -> SummaryItem { + let name = { + let link_content = collect_events!(self.stream, end Tag::Link(..)); + stringify_events(link_content) + }; if href.is_empty() { - Err(self.parse_error("You can't have an empty link.")) + let link = VirtualLink::new(name); + SummaryItem::VirtualLink(link) } else { - Ok(Link { - name: name, - location: PathBuf::from(href.to_string()), - number: None, - nested_items: Vec::new(), - }) + let link = Link::new(name, href.into_owned()); + SummaryItem::Link(link) } } @@ -364,15 +412,31 @@ impl<'a> SummaryParser<'a> { } Some(Event::Start(Tag::List(..))) => { // recurse to parse the nested list - let (_, last_item) = get_last_link(&mut items)?; - let last_item_number = last_item - .number - .as_ref() - .expect("All numbered chapters have numbers"); - - let sub_items = self.parse_nested_numbered(last_item_number)?; - - last_item.nested_items = sub_items; + let (_, last_item) = get_last_linklike(&mut items)?; + + match *last_item { + SummaryItem::Link(ref mut last_link) => { + let last_item_number = last_link + .number + .as_ref() + .expect("All numbered chapters have numbers"); + + let sub_items = self.parse_nested_numbered(last_item_number)?; + + last_link.nested_items = sub_items; + }, + SummaryItem::VirtualLink(ref mut last_link) => { + let last_item_number = last_link + .number + .as_ref() + .expect("All numbered chapters have numbers"); + + let sub_items = self.parse_nested_numbered(last_item_number)?; + + last_link.nested_items = sub_items; + }, + SummaryItem::Separator => unreachable!(), + }; } Some(Event::End(Tag::List(..))) => break, Some(_) => {} @@ -392,20 +456,35 @@ impl<'a> SummaryParser<'a> { match self.next_event() { Some(Event::Start(Tag::Paragraph)) => continue, Some(Event::Start(Tag::Link(href, _))) => { - let mut link = self.parse_link(href.to_string())?; + let mut item = self.parse_linklike(href); let mut number = parent.clone(); number.0.push(num_existing_items as u32 + 1); - trace!( - "Found chapter: {} {} ({})", - number, - link.name, - link.location.display() - ); - link.number = Some(number); + match item { + SummaryItem::Link(ref mut link) => { + trace!( + "Found chapter: {} {} ({})", + number, + link.name, + link.location.display(), + ); + + link.number = Some(number); + }, + SummaryItem::VirtualLink(ref mut link) => { + trace!( + "Found virtual chapter: {} {}", + number, + link.name, + ); + + link.number = Some(number); + }, + SummaryItem::Separator => panic!(), + } - return Ok(SummaryItem::Link(link)); + return Ok(item); } other => { warn!("Expected a start of a link, actually got {:?}", other); @@ -448,17 +527,23 @@ fn update_section_numbers(sections: &mut [SummaryItem], level: usize, by: u32) { } } -/// Gets a pointer to the last `Link` in a list of `SummaryItem`s, and its -/// index. -fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> { - links +/// Gets a pointer to the last `Link` or `VirtualLink` in a list of +/// `SummaryItem`s, and its index. +fn get_last_linklike(items: &mut [SummaryItem]) -> Result<(usize, &mut SummaryItem)> { + items .iter_mut() .enumerate() - .filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l))) + .filter(|&(_, ref item)| { + match **item { + SummaryItem::Link(_) => true, + SummaryItem::VirtualLink(_) => true, + SummaryItem::Separator => false, + } + }) .rev() .next() .ok_or_else(|| { - "Unable to get last link because the list of SummaryItems doesn't contain any Links" + "Unable to get last Link or VirtualLink because the list of SummaryItems doesn't contain any" .into() }) } @@ -612,22 +697,46 @@ mod tests { #[test] fn parse_a_link() { - let src = "[First](./first.md)"; - let should_be = Link { - name: String::from("First"), - location: PathBuf::from("./first.md"), - ..Default::default() + let src = "[Chapter](./chapter.md)"; + let should_be = SummaryItem::Link( + Link { + name: String::from("Chapter"), + location: PathBuf::from("./chapter.md"), + ..Default::default() + } + ); + + let mut parser = SummaryParser::new(src); + let _ = parser.stream.next(); // skip past start of paragraph + + let href = match parser.stream.next() { + Some(Event::Start(Tag::Link(href, _))) => href, + other => panic!("Unreachable, {:?}", other), }; + let got = parser.parse_linklike(href); + assert_eq!(got, should_be); + } + + #[test] + fn parse_a_virtual_link() { + let src = "[Virtual chapter]()"; + let should_be = SummaryItem::VirtualLink( + VirtualLink { + name: String::from("Virtual chapter"), + ..Default::default() + } + ); + let mut parser = SummaryParser::new(src); let _ = parser.stream.next(); // skip past start of paragraph let href = match parser.stream.next() { - Some(Event::Start(Tag::Link(href, _))) => href.to_string(), + Some(Event::Start(Tag::Link(href, _))) => href, other => panic!("Unreachable, {:?}", other), }; - let got = parser.parse_link(href).unwrap(); + let got = parser.parse_linklike(href); assert_eq!(got, should_be); } @@ -713,14 +822,4 @@ mod tests { assert_eq!(got, should_be); } - - #[test] - fn an_empty_link_location_is_an_error() { - let src = "- [Empty]()\n"; - let mut parser = SummaryParser::new(src); - parser.stream.next(); - - let got = parser.parse_numbered(); - assert!(got.is_err()); - } } diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index fbaad5239a..d8423e9250 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -87,6 +87,52 @@ impl HtmlHandlebars { self.render_index(ch, &ctx.destination)?; } } + // If the first chapter is a virtual chapter, create an empty index.html + BookItem::VirtualChapter(ref ch) => { + + if ctx.is_index { + + let content = String::new(); + + // Update the context with data for this file + let filepath = Path::new("index.md") + .with_extension("html"); + let filepathstr = filepath.to_str() + .chain_err(|| "Could not convert HTML path to str")?; + let filepathstr = utils::fs::normalize_path(filepathstr); + + // Non-lexical lifetimes needed :'( + let title: String; + { + let book_title = ctx.data + .get("book_title") + .and_then(serde_json::Value::as_str) + .unwrap_or(""); + title = book_title.to_string(); + } + + ctx.data.insert("path".to_owned(), json!("index.md".to_string())); + ctx.data.insert("content".to_owned(), json!(content)); + ctx.data.insert("chapter_title".to_owned(), json!("".to_string())); + ctx.data.insert("title".to_owned(), json!(title)); + ctx.data.insert("path_to_root".to_owned(), + json!(utils::fs::path_to_root(Path::new("index.md")))); + + // Render the handlebars template with the data + debug!("Render template"); + let rendered = ctx.handlebars.render("index", &ctx.data)?; + + let rendered = self.post_process( + rendered, + &filepathstr, + &ctx.html_config.playpen, + ); + + // Write to file + debug!("Creating {} ✓", filepathstr); + utils::fs::write_file(&ctx.destination, &filepath, &rendered.into_bytes())?; + } + } _ => {} } @@ -434,9 +480,17 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig .chain_err(|| "Could not convert path to str")?; chapter.insert("path".to_owned(), json!(path)); } + BookItem::VirtualChapter(ref ch) => { + if let Some(ref section) = ch.number { + chapter.insert("section".to_owned(), json!(section.to_string())); + } + + chapter.insert("name".to_owned(), json!(ch.name)); + } BookItem::Separator => { chapter.insert("spacer".to_owned(), json!("_spacer_")); } + _ => unimplemented!(), } chapters.push(chapter);