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 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 {