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 {