From 6a62284fc507c7016914672ba21754fb55199c34 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Mon, 2 Dec 2019 23:31:05 +0100 Subject: [PATCH 1/2] Bring back draft chapters Draft chapters, chapters that do not have a content and only show up in the TOC, were previously part of mdBook. At some point they were inadvertently removed and never came back. This commit brings back draft chapters with a better, explicit implementation. --- src/book/book.rs | 123 ++++++++-- src/book/mod.rs | 1 + src/book/summary.rs | 216 +++++++++++++----- src/renderer/html_handlebars/hbs_renderer.rs | 144 ++++++++---- .../html_handlebars/helpers/navigation.rs | 9 +- src/renderer/html_handlebars/helpers/toc.rs | 3 +- 6 files changed, 377 insertions(+), 119 deletions(-) diff --git a/src/book/book.rs b/src/book/book.rs index 6a31c9e8c6..91754aaa66 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -4,7 +4,7 @@ use std::fs::{self, File}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; +use super::summary::{parse_summary, DraftLink, Link, SectionNumber, Summary, SummaryItem}; use crate::config::BuildConfig; use crate::errors::*; @@ -116,8 +116,10 @@ where I: IntoIterator, { for item in items { - if let BookItem::Chapter(ch) = item { - for_each_mut(func, &mut ch.sub_items); + match item { + BookItem::Chapter(ch) => for_each_mut(func, &mut ch.sub_items), + BookItem::DraftChapter(ch) => for_each_mut(func, &mut ch.sub_items), + _ => {} } func(item); @@ -129,6 +131,8 @@ where pub enum BookItem { /// A nested chapter. Chapter(Chapter), + /// A draft chapter that only shows in the summary + DraftChapter(DraftChapter), /// A section separator. Separator, } @@ -139,6 +143,40 @@ impl From for BookItem { } } +impl From for BookItem { + fn from(other: DraftChapter) -> BookItem { + BookItem::DraftChapter(other) + } +} + +impl BookItem { + /// Returns the name of the chapter if the BookItem is a chapter or draft chapter + pub(crate) fn get_name(&self) -> Option<&str> { + match self { + BookItem::Chapter(ch) => Some(&ch.name), + BookItem::DraftChapter(ch) => Some(&ch.name), + _ => None, + } + } + + /// Returns the section of the chapter if the BookItem is a chapter or draft chapter + pub(crate) fn get_section(&self) -> Option<&SectionNumber> { + match self { + BookItem::Chapter(ch) => ch.number.as_ref(), + BookItem::DraftChapter(ch) => ch.number.as_ref(), + _ => None, + } + } + + /// Returns true if the BookItem is a chapter or draft chapter, false otherwise + pub(crate) fn is_chapter(&self) -> bool { + match self { + BookItem::Chapter(_) | BookItem::DraftChapter(_) => true, + _ => false, + } + } +} + /// 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 +213,31 @@ impl Chapter { } } +/// The representation of a "draft chapter", it does not map to a file +/// but appears in the summary / TOC and can have nested chapters. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct DraftChapter { + /// 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, + /// An ordered list of the names of each chapter above this one, in the hierarchy. + pub parent_names: Vec, +} + +impl DraftChapter { + /// Create a new chapter with the provided content. + pub fn new(name: &str, parent_names: Vec) -> DraftChapter { + DraftChapter { + name: name.to_string(), + parent_names, + ..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 @@ -202,9 +265,9 @@ pub(crate) fn load_book_from_disk>(summary: &Summary, src_dir: P) }) } -fn load_summary_item>( +fn load_summary_item( item: &SummaryItem, - src_dir: P, + src_dir: &Path, parent_names: Vec, ) -> Result { match *item { @@ -212,16 +275,14 @@ fn load_summary_item>( SummaryItem::Link(ref link) => { load_chapter(link, src_dir, parent_names).map(BookItem::Chapter) } + SummaryItem::DraftLink(ref link) => { + load_draft_chapter(link, src_dir, parent_names).map(BookItem::DraftChapter) + } } } -fn load_chapter>( - link: &Link, - src_dir: P, - parent_names: Vec, -) -> Result { +fn load_chapter(link: &Link, src_dir: &Path, parent_names: Vec) -> Result { debug!("Loading {} ({})", link.name, link.location.display()); - let src_dir = src_dir.as_ref(); let location = if link.location.is_absolute() { link.location.clone() @@ -248,7 +309,28 @@ fn load_chapter>( let sub_items = link .nested_items .iter() - .map(|i| load_summary_item(i, src_dir, sub_item_parents.clone())) + .map(|i| load_summary_item(i, &src_dir, sub_item_parents.clone())) + .collect::>>()?; + + ch.sub_items = sub_items; + + Ok(ch) +} + +fn load_draft_chapter( + link: &DraftLink, + src_dir: &Path, + parent_names: Vec, +) -> Result { + let mut sub_item_parents = parent_names.clone(); + let mut ch = DraftChapter::new(&link.name, parent_names); + ch.number = link.number.clone(); + + sub_item_parents.push(link.name.clone()); + let sub_items = link + .nested_items + .iter() + .map(|i| load_summary_item(i, &src_dir, sub_item_parents.clone())) .collect::>>()?; ch.sub_items = sub_items; @@ -274,11 +356,18 @@ 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)) => { + for sub_item in ch.sub_items.iter().rev() { + self.items.push_front(sub_item); + } + } + Some(&BookItem::DraftChapter(ref ch)) => { + for sub_item in ch.sub_items.iter().rev() { + self.items.push_front(sub_item); + } } + _ => {} } item @@ -364,7 +453,7 @@ And here is some \ fn cant_load_a_nonexistent_chapter() { let link = Link::new("Chapter 1", "/foo/bar/baz.md"); - let got = load_chapter(&link, "", Vec::new()); + let got = load_chapter(&link, Path::new(""), Vec::new()); assert!(got.is_err()); } diff --git a/src/book/mod.rs b/src/book/mod.rs index 2015801c86..b3980a6ed7 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -132,6 +132,7 @@ impl MDBook { /// for item in book.iter() { /// match *item { /// BookItem::Chapter(ref chapter) => {}, + /// BookItem::DraftChapter(ref chapter) => {}, /// BookItem::Separator => {}, /// } /// } diff --git a/src/book/summary.rs b/src/book/summary.rs index 33e8126d08..b7bb418ede 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -101,20 +101,58 @@ impl Default for Link { } } +/// A struct representing an entry in the `SUMMARY.md`, possibly with nested +/// entries, but that doesn't have a source file associated with it. This indicates +/// a chapter that will be added in the future. +/// +/// This is roughly the equivalent of `[Some section]()`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DraftLink { + /// The name of the chapter. + pub name: String, + /// The section number, if this chapter is in the numbered section. + pub number: Option, + /// Any nested items this chapter may contain. + pub nested_items: Vec, +} + +impl DraftLink { + /// Create a new draft link with no nested items. + pub fn new>(name: S) -> DraftLink { + DraftLink { + name: name.into(), + number: None, + nested_items: Vec::new(), + } + } +} + +impl Default for DraftLink { + fn default() -> Self { + DraftLink { + name: String::new(), + number: None, + nested_items: Vec::new(), + } + } +} + /// An item in `SUMMARY.md` which could be either a separator or a `Link`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SummaryItem { /// A link to a chapter. Link(Link), + /// A draft link that has no content yet but appears in the summary + DraftLink(DraftLink), /// A separator (`---`). Separator, } impl SummaryItem { - fn maybe_link_mut(&mut self) -> Option<&mut Link> { - match *self { - SummaryItem::Link(ref mut l) => Some(l), - _ => None, + fn is_chapter(&self) -> bool { + match self { + SummaryItem::Link(_) | SummaryItem::DraftLink(_) => true, + _ => false, } } } @@ -125,6 +163,12 @@ impl From for SummaryItem { } } +impl From for SummaryItem { + fn from(other: DraftLink) -> SummaryItem { + SummaryItem::DraftLink(other) + } +} + /// A recursive descent (-ish) parser for a `SUMMARY.md`. /// /// @@ -260,8 +304,8 @@ impl<'a> SummaryParser<'a> { } } Some(Event::Start(Tag::Link(_type, href, _title))) => { - let link = self.parse_link(href.to_string())?; - items.push(SummaryItem::Link(link)); + let link = self.parse_link(href.to_string()); + items.push(link); } Some(Event::Rule) => items.push(SummaryItem::Separator), Some(_) => {} @@ -272,19 +316,14 @@ impl<'a> SummaryParser<'a> { Ok(items) } - fn parse_link(&mut self, href: String) -> Result { + fn parse_link(&mut self, href: String) -> SummaryItem { let link_content = collect_events!(self.stream, end Tag::Link(..)); let name = stringify_events(link_content); if href.is_empty() { - Err(self.parse_error("You can't have an empty link.")) + SummaryItem::DraftLink(DraftLink::new(name)) } else { - Ok(Link { - name, - location: PathBuf::from(href.to_string()), - number: None, - nested_items: Vec::new(), - }) + SummaryItem::Link(Link::new(name, href.to_string())) } } @@ -378,15 +417,28 @@ 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_link = get_last_link(&mut items)?; + match last_link { + SummaryItem::Link(ref mut last_item) => { + let last_item_number = last_item + .number + .as_ref() + .expect("All numbered chapters have numbers"); + + last_item.nested_items = + self.parse_nested_numbered(last_item_number)?; + } + SummaryItem::DraftLink(ref mut last_item) => { + let last_item_number = last_item + .number + .as_ref() + .expect("All numbered chapters have numbers"); + + last_item.nested_items = + self.parse_nested_numbered(last_item_number)?; + } + _ => unreachable!(), + }; } Some(Event::End(Tag::List(..))) => break, Some(_) => {} @@ -406,20 +458,31 @@ impl<'a> SummaryParser<'a> { match self.next_event() { Some(Event::Start(Tag::Paragraph)) => continue, Some(Event::Start(Tag::Link(_type, href, _title))) => { - let mut link = self.parse_link(href.to_string())?; + let mut link = self.parse_link(href.to_string()); 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 link { + SummaryItem::Link(ref mut l) => { + trace!( + "Found chapter: {} {} ({})", + number, + l.name, + l.location.display() + ); - return Ok(SummaryItem::Link(link)); + l.number = Some(number); + } + SummaryItem::DraftLink(ref mut l) => { + trace!("Found draft chapter: {} {}", number, l.name); + + l.number = Some(number); + } + _ => unreachable!(), + } + + return Ok(link); } other => { warn!("Expected a start of a link, actually got {:?}", other); @@ -452,23 +515,30 @@ impl<'a> SummaryParser<'a> { fn update_section_numbers(sections: &mut [SummaryItem], level: usize, by: u32) { for section in sections { - if let SummaryItem::Link(ref mut link) = *section { - if let Some(ref mut number) = link.number { - number.0[level] += by; + match *section { + SummaryItem::Link(ref mut link) => { + if let Some(ref mut number) = link.number { + number.0[level] += by; + } + update_section_numbers(&mut link.nested_items, level, by); } - - update_section_numbers(&mut link.nested_items, level, by); + SummaryItem::DraftLink(ref mut link) => { + if let Some(ref mut number) = link.number { + number.0[level] += by; + } + update_section_numbers(&mut link.nested_items, level, by); + } + _ => {} } } } /// 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)> { +/// index. This will only return chapters or draft chapters, never separators. +fn get_last_link(links: &mut [SummaryItem]) -> Result<&mut SummaryItem> { links .iter_mut() - .enumerate() - .filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l))) + .filter(|item| item.is_chapter()) .rev() .next() .ok_or_else(|| { @@ -627,11 +697,11 @@ mod tests { #[test] fn parse_a_link() { let src = "[First](./first.md)"; - let should_be = Link { + let should_be = SummaryItem::Link(Link { name: String::from("First"), location: PathBuf::from("./first.md"), ..Default::default() - }; + }); let mut parser = SummaryParser::new(src); let _ = parser.stream.next(); // skip past start of paragraph @@ -641,7 +711,7 @@ mod tests { other => panic!("Unreachable, {:?}", other), }; - let got = parser.parse_link(href).unwrap(); + let got = parser.parse_link(href); assert_eq!(got, should_be); } @@ -664,6 +734,24 @@ mod tests { assert_eq!(got, should_be); } + #[test] + fn parse_a_numbered_draft_chapter() { + let src = "- [First]()\n"; + let link = DraftLink { + name: String::from("First"), + number: Some(SectionNumber(vec![1])), + ..Default::default() + }; + let should_be = vec![SummaryItem::DraftLink(link)]; + + let mut parser = SummaryParser::new(src); + let _ = parser.stream.next(); + + let got = parser.parse_numbered().unwrap(); + + assert_eq!(got, should_be); + } + #[test] fn parse_nested_numbered_chapters() { let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)"; @@ -696,6 +784,36 @@ mod tests { assert_eq!(got, should_be); } + #[test] + fn parse_nested_numbered_draft_chapters() { + let src = "- [First]()\n - [Nested]()\n- [Second](./second.md)"; + + let should_be = vec![ + SummaryItem::DraftLink(DraftLink { + name: String::from("First"), + number: Some(SectionNumber(vec![1])), + nested_items: vec![SummaryItem::DraftLink(DraftLink { + name: String::from("Nested"), + number: Some(SectionNumber(vec![1, 1])), + nested_items: Vec::new(), + })], + }), + SummaryItem::Link(Link { + name: String::from("Second"), + location: PathBuf::from("./second.md"), + number: Some(SectionNumber(vec![2])), + nested_items: Vec::new(), + }), + ]; + + let mut parser = SummaryParser::new(src); + let _ = parser.stream.next(); + + let got = parser.parse_numbered().unwrap(); + + assert_eq!(got, should_be); + } + /// This test ensures the book will continue to pass because it breaks the /// `SUMMARY.md` up using level 2 headers ([example]). /// @@ -726,16 +844,6 @@ 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()); - } - /// Regression test for https://github.com/rust-lang/mdBook/issues/779 /// Ensure section numbers are correctly incremented after a horizontal separator. #[test] diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 0be925622a..1a752a7370 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -1,4 +1,4 @@ -use crate::book::{Book, BookItem}; +use crate::book::{Book, BookItem, SectionNumber}; use crate::config::{Config, HtmlConfig, Playpen}; use crate::errors::*; use crate::renderer::html_handlebars::helpers; @@ -29,18 +29,17 @@ impl HtmlHandlebars { mut ctx: RenderItemContext<'_>, print_content: &mut String, ) -> Result<()> { - // FIXME: This should be made DRY-er and rely less on mutable state - if let BookItem::Chapter(ref ch) = *item { - let content = ch.content.clone(); - let content = utils::render_markdown(&content, ctx.html_config.curly_quotes); + let (content, fixed_content) = + self.render_markdown_content(item, ctx.html_config.curly_quotes); + print_content.push_str(&fixed_content); - let fixed_content = utils::render_markdown_with_path( - &ch.content, - ctx.html_config.curly_quotes, - Some(&ch.path), - ); - print_content.push_str(&fixed_content); + let ch_name = item.get_name().expect("Chapter always have a name"); + let ch_section = item.get_section(); + let title = self.concat_with_book_title(&ctx, ch_name); + + self.insert_chapter_info_in_context(&mut ctx, ch_name, &content, &title, ch_section); + if let BookItem::Chapter(ref ch) = *item { // Update the context with data for this file let path = ch .path @@ -48,34 +47,13 @@ impl HtmlHandlebars { .chain_err(|| "Could not convert path to str")?; let filepath = Path::new(&ch.path).with_extension("html"); - // "print.html" is used for the print page. - if ch.path == Path::new("print.md") { - bail!(ErrorKind::ReservedFilenameError(ch.path.clone())); - }; - - // 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 = ch.name.clone() + " - " + book_title; - } + self.check_reserved_filenames(&ch.path)?; - ctx.data.insert("path".to_owned(), json!(path)); - ctx.data.insert("content".to_owned(), json!(content)); - ctx.data.insert("chapter_title".to_owned(), json!(ch.name)); - ctx.data.insert("title".to_owned(), json!(title)); + ctx.data.insert("path".to_string(), json!(path)); ctx.data.insert( - "path_to_root".to_owned(), - json!(utils::fs::path_to_root(&ch.path)), + "path_to_root".to_string(), + json!(utils::fs::path_to_root(&path)), ); - if let Some(ref section) = ch.number { - ctx.data - .insert("section".to_owned(), json!(section.to_string())); - } // Render the handlebars template with the data debug!("Render template"); @@ -86,21 +64,81 @@ impl HtmlHandlebars { // Write to file debug!("Creating {}", filepath.display()); utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?; + } - if ctx.is_index { - ctx.data.insert("path".to_owned(), json!("index.md")); - ctx.data.insert("path_to_root".to_owned(), json!("")); - ctx.data.insert("is_index".to_owned(), json!("true")); - let rendered_index = ctx.handlebars.render("index", &ctx.data)?; - let rendered_index = self.post_process(rendered_index, &ctx.html_config.playpen); - debug!("Creating index.html from {}", path); - utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?; - } + if ctx.is_index { + self.render_index_page(&mut ctx)?; + } + + Ok(()) + } + + fn render_index_page(&self, ctx: &mut RenderItemContext<'_>) -> Result<()> { + ctx.data.insert("path".to_owned(), json!("index.md")); + ctx.data.insert("path_to_root".to_owned(), json!("")); + ctx.data.insert("is_index".to_owned(), json!("true")); + + let rendered_index = ctx.handlebars.render("index", &ctx.data)?; + let rendered_index = self.post_process(rendered_index, &ctx.html_config.playpen); + + if let Some(path) = ctx.data.get("path") { + debug!("Creating index.html from {}", path); } + utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?; Ok(()) } + fn render_markdown_content(&self, item: &BookItem, curly_quotes: bool) -> (String, String) { + match item { + BookItem::Chapter(ch) => { + let content = ch.content.clone(); + let content = utils::render_markdown(&content, curly_quotes); + + let fixed_content = + utils::render_markdown_with_path(&ch.content, curly_quotes, Some(&ch.path)); + + (content, fixed_content) + } + BookItem::DraftChapter(_) => (String::new(), String::new()), + _ => unreachable!(), + } + } + + fn check_reserved_filenames(&self, path_to_check: &Path) -> Result<()> { + match path_to_check.to_str().unwrap_or("") { + "print.md" => bail!(ErrorKind::ReservedFilenameError(path_to_check.to_owned())), + _ => Ok(()), + } + } + + fn concat_with_book_title(&self, ctx: &RenderItemContext<'_>, name: &str) -> String { + let book_title = ctx + .data + .get("book_title") + .and_then(serde_json::Value::as_str) + .unwrap_or(""); + + name.to_string() + " - " + book_title + } + + fn insert_chapter_info_in_context( + &self, + ctx: &mut RenderItemContext<'_>, + name: &str, + content: &str, + title: &str, + section: Option<&SectionNumber>, + ) { + ctx.data.insert("content".to_string(), json!(content)); + ctx.data.insert("chapter_title".to_string(), json!(name)); + ctx.data.insert("title".to_string(), json!(title)); + + if let Some(ref s) = section { + ctx.data.insert("section".to_string(), json!(s.to_string())); + } + } + #[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))] fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String { let rendered = build_header_links(&rendered); @@ -331,7 +369,7 @@ impl Renderer for HtmlHandlebars { .chain_err(|| "Unexpected error when constructing destination path")?; let mut is_index = true; - for item in book.iter() { + for item in book.iter().filter(|x| x.is_chapter()) { let ctx = RenderItemContext { handlebars: &handlebars, destination: destination.to_path_buf(), @@ -511,12 +549,26 @@ fn make_data( ); chapter.insert("name".to_owned(), json!(ch.name)); + let path = ch .path .to_str() .chain_err(|| "Could not convert path to str")?; chapter.insert("path".to_owned(), json!(path)); } + BookItem::DraftChapter(ref ch) => { + if let Some(ref section) = ch.number { + chapter.insert("section".to_owned(), json!(section.to_string())); + } + + chapter.insert( + "has_sub_items".to_owned(), + json!((!ch.sub_items.is_empty()).to_string()), + ); + + chapter.insert("name".to_owned(), json!(ch.name)); + chapter.insert("draft".to_owned(), json!("true")); + } BookItem::Separator => { chapter.insert("spacer".to_owned(), json!("_spacer_")); } diff --git a/src/renderer/html_handlebars/helpers/navigation.rs b/src/renderer/html_handlebars/helpers/navigation.rs index 5914d42ff2..2a6060414e 100644 --- a/src/renderer/html_handlebars/helpers/navigation.rs +++ b/src/renderer/html_handlebars/helpers/navigation.rs @@ -64,6 +64,13 @@ fn find_chapter( .replace("\"", ""); if !rc.evaluate(ctx, "@root/is_index")?.is_missing() { + // If the first chapter is a draft, don't skip the first real chapter + // otherwise skip it because the index is a copy of it + let skip_n = if chapters[0].contains_key("draft") { + 0 + } else { + 1 + }; // Special case for index.md which may be a synthetic page. // Target::find won't match because there is no page with the path // "index.md" (unless there really is an index.md in SUMMARY.md). @@ -75,7 +82,7 @@ fn find_chapter( // Skip things like "spacer" chapter.contains_key("path") }) - .skip(1) + .skip(skip_n) .next() { Some(chapter) => return Ok(Some(chapter.clone())), diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs index 9d81039834..5a82312dab 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/src/renderer/html_handlebars/helpers/toc.rs @@ -139,7 +139,7 @@ impl HelperDef for RenderToc { if !self.no_section_label { // Section does not necessarily exist if let Some(section) = item.get("section") { - out.write("")?; + out.write("")?; out.write(§ion)?; out.write(" ")?; } @@ -160,6 +160,7 @@ impl HelperDef for RenderToc { // write to the handlebars template out.write(&markdown_parsed_name)?; + out.write("")?; } if path_exists { From 63a7d3585996ed70212e550f372a407dc90fa113 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Mon, 2 Dec 2019 23:46:42 +0100 Subject: [PATCH 2/2] Document draft chapters --- book-example/src/SUMMARY.md | 1 + book-example/src/format/summary.md | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/book-example/src/SUMMARY.md b/book-example/src/SUMMARY.md index 4aab90f3b1..3ae143fb87 100644 --- a/book-example/src/SUMMARY.md +++ b/book-example/src/SUMMARY.md @@ -10,6 +10,7 @@ - [clean](cli/clean.md) - [Format](format/README.md) - [SUMMARY.md](format/summary.md) + - [Draft chapter]() - [Configuration](format/config.md) - [Theme](format/theme/README.md) - [index.hbs](format/theme/index-hbs.md) diff --git a/book-example/src/format/summary.md b/book-example/src/format/summary.md index 71aa723bb3..61a2c6ec1e 100644 --- a/book-example/src/format/summary.md +++ b/book-example/src/format/summary.md @@ -7,7 +7,7 @@ are. Without this file, there is no book. Even though `SUMMARY.md` is a markdown file, the formatting is very strict to allow for easy parsing. Let's see how you should format your `SUMMARY.md` file. -#### Allowed elements +#### Structure 1. ***Title*** It's common practice to begin with a title, generally # Summary. But it is not mandatory, the @@ -36,3 +36,19 @@ allow for easy parsing. Let's see how you should format your `SUMMARY.md` file. All other elements are unsupported and will be ignored at best or result in an error. + +#### Other elements + +- ***Separators*** In between chapters you can add a separator. In the HTML renderer + this will result in a line being rendered in the table of contents. A separator is + a line containing exclusively dashes and at least three of them: `---`. +- ***Draft chapters*** Draft chapters are chapters without a file and thus content. + The purpose of a draft chapter is to signal future chapters still to be written. + Or when still laying out the structure of the book to avoid creating the files + while you are still changing the structure of the book a lot. + Draft chapters will be rendered in the HTML renderer as disabled links in the table + of contents, as you can see for the next chapter in the table of contents on the left. + Draft chapters are written like normal chapters but without writing the path to the file + ```markdown + - [Draft chapter]() + ``` \ No newline at end of file