implement support for book parts

This commit is contained in:
mark 2020-03-20 21:18:07 -05:00
parent 1b3b10d2ae
commit 5dd2a5bff4
6 changed files with 260 additions and 98 deletions

View File

@ -31,7 +31,12 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
let mut items: Vec<_> = summary let mut items: Vec<_> = summary
.prefix_chapters .prefix_chapters
.iter() .iter()
.chain(summary.numbered_chapters.iter()) .chain(
summary
.parts
.iter()
.flat_map(|part| part.numbered_chapters.iter()),
)
.chain(summary.suffix_chapters.iter()) .chain(summary.suffix_chapters.iter())
.collect(); .collect();
@ -133,6 +138,8 @@ pub enum BookItem {
Chapter(Chapter), Chapter(Chapter),
/// A section separator. /// A section separator.
Separator, Separator,
/// A part title.
PartTitle(String),
} }
impl From<Chapter> for BookItem { impl From<Chapter> for BookItem {
@ -205,17 +212,24 @@ pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P)
debug!("Loading the book from disk"); debug!("Loading the book from disk");
let src_dir = src_dir.as_ref(); let src_dir = src_dir.as_ref();
let prefix = summary.prefix_chapters.iter();
let numbered = summary.numbered_chapters.iter();
let suffix = summary.suffix_chapters.iter();
let summary_items = prefix.chain(numbered).chain(suffix);
let mut chapters = Vec::new(); let mut chapters = Vec::new();
for summary_item in summary_items { for prefix_chapter in &summary.prefix_chapters {
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?; chapters.push(load_summary_item(prefix_chapter, src_dir, Vec::new())?);
chapters.push(chapter); }
for part in &summary.parts {
if let Some(title) = &part.title {
chapters.push(BookItem::PartTitle(title.clone()));
}
for numbered_chapter in &part.numbered_chapters {
chapters.push(load_summary_item(numbered_chapter, src_dir, Vec::new())?);
}
}
for suffix_chapter in &summary.suffix_chapters {
chapters.push(load_summary_item(suffix_chapter, src_dir, Vec::new())?);
} }
Ok(Book { Ok(Book {
@ -327,6 +341,7 @@ impl Display for Chapter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::book::summary::Part;
use std::io::Write; use std::io::Write;
use tempfile::{Builder as TempFileBuilder, TempDir}; use tempfile::{Builder as TempFileBuilder, TempDir};
@ -430,7 +445,10 @@ And here is some \
fn load_a_book_with_a_single_chapter() { fn load_a_book_with_a_single_chapter() {
let (link, temp) = dummy_link(); let (link, temp) = dummy_link();
let summary = Summary { let summary = Summary {
parts: vec![Part {
title: None,
numbered_chapters: vec![SummaryItem::Link(link)], numbered_chapters: vec![SummaryItem::Link(link)],
}],
..Default::default() ..Default::default()
}; };
let should_be = Book { let should_be = Book {
@ -564,11 +582,14 @@ And here is some \
fn cant_load_chapters_with_an_empty_path() { fn cant_load_chapters_with_an_empty_path() {
let (_, temp) = dummy_link(); let (_, temp) = dummy_link();
let summary = Summary { let summary = Summary {
parts: vec![Part {
title: None,
numbered_chapters: vec![SummaryItem::Link(Link { numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("Empty"), name: String::from("Empty"),
location: Some(PathBuf::from("")), location: Some(PathBuf::from("")),
..Default::default() ..Default::default()
})], })],
}],
..Default::default() ..Default::default()
}; };
@ -583,11 +604,14 @@ And here is some \
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
let summary = Summary { let summary = Summary {
parts: vec![Part {
title: None,
numbered_chapters: vec![SummaryItem::Link(Link { numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("nested"), name: String::from("nested"),
location: Some(dir), location: Some(dir),
..Default::default() ..Default::default()
})], })],
}],
..Default::default() ..Default::default()
}; };

View File

@ -132,6 +132,7 @@ impl MDBook {
/// match *item { /// match *item {
/// BookItem::Chapter(ref chapter) => {}, /// BookItem::Chapter(ref chapter) => {},
/// BookItem::Separator => {}, /// BookItem::Separator => {},
/// BookItem::PartTitle(ref title) => {}
/// } /// }
/// } /// }
/// ///

View File

@ -25,12 +25,17 @@ use std::path::{Path, PathBuf};
/// [Title of prefix element](relative/path/to/markdown.md) /// [Title of prefix element](relative/path/to/markdown.md)
/// ``` /// ```
/// ///
/// **Part Title:** An optional title for the next collect of numbered chapters. The numbered
/// chapters can be broken into as many parts as desired.
///
/// **Numbered Chapter:** Numbered chapters are the main content of the book, /// **Numbered Chapter:** Numbered chapters are the main content of the book,
/// they /// they
/// will be numbered and can be nested, resulting in a nice hierarchy (chapters, /// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
/// sub-chapters, etc.) /// sub-chapters, etc.)
/// ///
/// ```markdown /// ```markdown
/// # Title of Part
///
/// - [Title of the Chapter](relative/path/to/markdown.md) /// - [Title of the Chapter](relative/path/to/markdown.md)
/// ``` /// ```
/// ///
@ -55,12 +60,23 @@ pub struct Summary {
pub title: Option<String>, pub title: Option<String>,
/// Chapters before the main text (e.g. an introduction). /// Chapters before the main text (e.g. an introduction).
pub prefix_chapters: Vec<SummaryItem>, pub prefix_chapters: Vec<SummaryItem>,
/// The main chapters in the document. /// The main numbered chapters of the book, broken into one or more possibly named parts.
pub numbered_chapters: Vec<SummaryItem>, pub parts: Vec<Part>,
/// Items which come after the main document (e.g. a conclusion). /// Items which come after the main document (e.g. a conclusion).
pub suffix_chapters: Vec<SummaryItem>, pub suffix_chapters: Vec<SummaryItem>,
} }
/// A struct representing a "part" in the `SUMMARY.md`. This is a possibly-titled section with
/// numbered chapters in it.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Part {
/// An optional title for the `SUMMARY.md`, currently just ignored.
pub title: Option<String>,
/// The main chapters in the document.
pub numbered_chapters: Vec<SummaryItem>,
}
/// 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.
/// ///
@ -139,7 +155,8 @@ impl From<Link> for SummaryItem {
/// | EPSILON /// | EPSILON
/// prefix_chapters ::= item* /// prefix_chapters ::= item*
/// suffix_chapters ::= item* /// suffix_chapters ::= item*
/// numbered_chapters ::= dotted_item+ /// numbered_chapters ::= part+
/// part ::= title dotted_item+
/// dotted_item ::= INDENT* DOT_POINT item /// dotted_item ::= INDENT* DOT_POINT item
/// item ::= link /// item ::= link
/// | separator /// | separator
@ -155,6 +172,10 @@ struct SummaryParser<'a> {
src: &'a str, src: &'a str,
stream: pulldown_cmark::OffsetIter<'a>, stream: pulldown_cmark::OffsetIter<'a>,
offset: usize, offset: usize,
/// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
/// here until somebody calls `next_event` again.
back: Option<Event<'a>>,
} }
/// Reads `Events` from the provided stream until the corresponding /// Reads `Events` from the provided stream until the corresponding
@ -203,6 +224,7 @@ impl<'a> SummaryParser<'a> {
src: text, src: text,
stream: pulldown_parser, stream: pulldown_parser,
offset: 0, offset: 0,
back: None,
} }
} }
@ -224,8 +246,8 @@ impl<'a> SummaryParser<'a> {
let prefix_chapters = self let prefix_chapters = self
.parse_affix(true) .parse_affix(true)
.chain_err(|| "There was an error parsing the prefix chapters")?; .chain_err(|| "There was an error parsing the prefix chapters")?;
let numbered_chapters = self let parts = self
.parse_numbered() .parse_parts()
.chain_err(|| "There was an error parsing the numbered chapters")?; .chain_err(|| "There was an error parsing the numbered chapters")?;
let suffix_chapters = self let suffix_chapters = self
.parse_affix(false) .parse_affix(false)
@ -234,13 +256,12 @@ impl<'a> SummaryParser<'a> {
Ok(Summary { Ok(Summary {
title, title,
prefix_chapters, prefix_chapters,
numbered_chapters, parts,
suffix_chapters, suffix_chapters,
}) })
} }
/// Parse the affix chapters. This expects the first event (start of /// Parse the affix chapters.
/// paragraph) to have already been consumed by the previous parser.
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> { fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new(); let mut items = Vec::new();
debug!( debug!(
@ -250,10 +271,12 @@ impl<'a> SummaryParser<'a> {
loop { loop {
match self.next_event() { match self.next_event() {
Some(Event::Start(Tag::List(..))) => { Some(ev @ Event::Start(Tag::List(..)))
| Some(ev @ Event::Start(Tag::Heading(1))) => {
if is_prefix { if is_prefix {
// we've finished prefix chapters and are at the start // we've finished prefix chapters and are at the start
// of the numbered section. // of the numbered section.
self.back(ev);
break; break;
} else { } else {
bail!(self.parse_error("Suffix chapters cannot be followed by a list")); bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
@ -272,6 +295,52 @@ impl<'a> SummaryParser<'a> {
Ok(items) Ok(items)
} }
fn parse_parts(&mut self) -> Result<Vec<Part>> {
let mut parts = vec![];
// We want the section numbers to be continues through all parts.
let mut root_number = SectionNumber::default();
let mut root_items = 0;
loop {
// Possibly match a title or the end of the "numbered chapters part".
let title = match self.next_event() {
Some(ev @ Event::Start(Tag::Paragraph)) => {
// we're starting the suffix chapters
self.back(ev);
break;
}
Some(Event::Start(Tag::Heading(1))) => {
debug!("Found a h1 in the SUMMARY");
let tags = collect_events!(self.stream, end Tag::Heading(1));
Some(stringify_events(tags))
}
Some(ev) => {
self.back(ev);
None
}
None => break, // EOF, bail...
};
// Parse the rest of the part.
let numbered_chapters = self
.parse_numbered(&mut root_items, &mut root_number)
.chain_err(|| "There was an error parsing the numbered chapters")?;
parts.push(Part {
title,
numbered_chapters,
});
}
Ok(parts)
}
/// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
fn parse_link(&mut self, href: String) -> Link { fn parse_link(&mut self, href: String) -> Link {
let link_content = collect_events!(self.stream, end Tag::Link(..)); let link_content = collect_events!(self.stream, end Tag::Link(..));
let name = stringify_events(link_content); let name = stringify_events(link_content);
@ -290,35 +359,43 @@ impl<'a> SummaryParser<'a> {
} }
} }
/// Parse the numbered chapters. This assumes the opening list tag has /// Parse the numbered chapters.
/// already been consumed by a previous parser. fn parse_numbered(
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> { &mut self,
root_items: &mut u32,
root_number: &mut SectionNumber,
) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new(); let mut items = Vec::new();
let mut root_items = 0;
let root_number = SectionNumber::default();
// we need to do this funny loop-match-if-let dance because a rule will // For the first iteration, we want to just skip any opening paragraph tags, as that just
// close off any currently running list. Therefore we try to read the // marks the start of the list. But after that, another opening paragraph indicates that we
// list items before the rule, then if we encounter a rule we'll add a // have started a new part or the suffix chapters.
// separator and try to resume parsing numbered chapters if we start a let mut first = true;
// list immediately afterwards.
//
// If you can think of a better way to do this then please make a PR :)
loop { loop {
match self.next_event() {
Some(ev @ Event::Start(Tag::Paragraph)) if !first => {
// we're starting the suffix chapters
self.back(ev);
break;
}
// The expectation is that pulldown cmark will terminate a paragraph before a new
// heading, so we can always count on this to return without skipping headings.
Some(ev @ Event::Start(Tag::Heading(1))) => {
// we're starting a new part
self.back(ev);
break;
}
Some(ev @ Event::Start(Tag::List(..))) => {
self.back(ev);
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?; let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
// if we've resumed after something like a rule the root sections // if we've resumed after something like a rule the root sections
// will be numbered from 1. We need to manually go back and update // will be numbered from 1. We need to manually go back and update
// them // them
update_section_numbers(&mut bunch_of_items, 0, root_items); update_section_numbers(&mut bunch_of_items, 0, *root_items);
root_items += bunch_of_items.len() as u32; *root_items += bunch_of_items.len() as u32;
items.extend(bunch_of_items); items.extend(bunch_of_items);
match self.next_event() {
Some(Event::Start(Tag::Paragraph)) => {
// we're starting the suffix chapters
break;
} }
Some(Event::Start(other_tag)) => { Some(Event::Start(other_tag)) => {
trace!("Skipping contents of {:?}", other_tag); trace!("Skipping contents of {:?}", other_tag);
@ -329,40 +406,42 @@ impl<'a> SummaryParser<'a> {
break; break;
} }
} }
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
continue;
} else {
break;
}
} }
Some(Event::Rule) => { Some(Event::Rule) => {
items.push(SummaryItem::Separator); items.push(SummaryItem::Separator);
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
continue;
} else {
break;
} }
}
Some(_) => {
// something else... ignore // something else... ignore
continue; Some(_) => {}
}
None => {
// EOF, bail... // EOF, bail...
None => {
break; break;
} }
} }
// From now on, we cannot accept any new paragraph opening tags.
first = false;
} }
Ok(items) Ok(items)
} }
/// Push an event back to the tail of the stream.
fn back(&mut self, ev: Event<'a>) {
assert!(self.back.is_none());
trace!("Back: {:?}", ev);
self.back = Some(ev);
}
fn next_event(&mut self) -> Option<Event<'a>> { fn next_event(&mut self) -> Option<Event<'a>> {
let next = self.stream.next().map(|(ev, range)| { let next = self.back.take().or_else(|| {
self.stream.next().map(|(ev, range)| {
self.offset = range.start; self.offset = range.start;
ev ev
})
}); });
trace!("Next event: {:?}", next); trace!("Next event: {:?}", next);
next next
@ -448,13 +527,14 @@ impl<'a> SummaryParser<'a> {
/// Try to parse the title line. /// Try to parse the title line.
fn parse_title(&mut self) -> Option<String> { fn parse_title(&mut self) -> Option<String> {
if let Some(Event::Start(Tag::Heading(1))) = self.next_event() { match self.next_event() {
Some(Event::Start(Tag::Heading(1))) => {
debug!("Found a h1 in the SUMMARY"); debug!("Found a h1 in the SUMMARY");
let tags = collect_events!(self.stream, end Tag::Heading(1)); let tags = collect_events!(self.stream, end Tag::Heading(1));
Some(stringify_events(tags)) Some(stringify_events(tags))
} else { }
None _ => None,
} }
} }
} }
@ -604,7 +684,6 @@ mod tests {
}), }),
]; ];
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(true).unwrap(); let got = parser.parse_affix(true).unwrap();
assert_eq!(got, should_be); assert_eq!(got, should_be);
@ -615,7 +694,6 @@ mod tests {
let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n"; let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
let mut parser = SummaryParser::new(src); let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(true).unwrap(); let got = parser.parse_affix(true).unwrap();
assert_eq!(got.len(), 3); assert_eq!(got.len(), 3);
@ -627,7 +705,6 @@ mod tests {
let src = "[First](./first.md)\n- [Second](./second.md)\n"; let src = "[First](./first.md)\n- [Second](./second.md)\n";
let mut parser = SummaryParser::new(src); let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(false); let got = parser.parse_affix(false);
assert!(got.is_err()); assert!(got.is_err());
@ -643,7 +720,7 @@ mod tests {
}; };
let mut parser = SummaryParser::new(src); let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // skip past start of paragraph let _ = parser.stream.next(); // Discard opening paragraph
let href = match parser.stream.next() { let href = match parser.stream.next() {
Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(), Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(),
@ -666,9 +743,9 @@ mod tests {
let should_be = vec![SummaryItem::Link(link)]; let should_be = vec![SummaryItem::Link(link)];
let mut parser = SummaryParser::new(src); let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
let got = parser.parse_numbered().unwrap(); .unwrap();
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }
@ -698,9 +775,9 @@ mod tests {
]; ];
let mut parser = SummaryParser::new(src); let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
let got = parser.parse_numbered().unwrap(); .unwrap();
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }
@ -725,9 +802,54 @@ mod tests {
]; ];
let mut parser = SummaryParser::new(src); let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
.unwrap();
let got = parser.parse_numbered().unwrap(); assert_eq!(got, should_be);
}
#[test]
fn parse_titled_parts() {
let src = "- [First](./first.md)\n- [Second](./second.md)\n\
# Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)";
let should_be = vec![
Part {
title: None,
numbered_chapters: vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(),
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
],
},
Part {
title: Some(String::from("Title 2")),
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("Third"),
location: Some(PathBuf::from("./third.md")),
number: Some(SectionNumber(vec![3])),
nested_items: vec![SummaryItem::Link(Link {
name: String::from("Fourth"),
location: Some(PathBuf::from("./fourth.md")),
number: Some(SectionNumber(vec![3, 1])),
nested_items: Vec::new(),
})],
})],
},
];
let mut parser = SummaryParser::new(src);
let got = parser.parse_parts().unwrap();
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }
@ -755,9 +877,9 @@ mod tests {
]; ];
let mut parser = SummaryParser::new(src); let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
let got = parser.parse_numbered().unwrap(); .unwrap();
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }
@ -766,9 +888,8 @@ mod tests {
fn an_empty_link_location_is_a_draft_chapter() { fn an_empty_link_location_is_a_draft_chapter() {
let src = "- [Empty]()\n"; let src = "- [Empty]()\n";
let mut parser = SummaryParser::new(src); let mut parser = SummaryParser::new(src);
parser.stream.next();
let got = parser.parse_numbered(); 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: String::from("Empty"),
location: None, location: None,
@ -810,9 +931,9 @@ mod tests {
]; ];
let mut parser = SummaryParser::new(src); let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
let got = parser.parse_numbered().unwrap(); .unwrap();
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }

View File

@ -514,6 +514,9 @@ fn make_data(
let mut chapter = BTreeMap::new(); let mut chapter = BTreeMap::new();
match *item { match *item {
BookItem::PartTitle(ref title) => {
chapter.insert("part".to_owned(), json!(title));
}
BookItem::Chapter(ref ch) => { BookItem::Chapter(ref ch) => {
if let Some(ref section) = ch.number { if let Some(ref section) = ch.number {
chapter.insert("section".to_owned(), json!(section.to_string())); chapter.insert("section".to_owned(), json!(section.to_string()));

View File

@ -99,6 +99,14 @@ impl HelperDef for RenderToc {
write_li_open_tag(out, is_expanded, item.get("section").is_none())?; write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
} }
// Part title
if let Some(title) = item.get("part") {
out.write("<li class=\"part-title active\">")?;
out.write(title)?;
out.write("</li>")?;
continue;
}
// Link // Link
let path_exists = if let Some(path) = item.get("path") { let path_exists = if let Some(path) = item.get("path") {
if !path.is_empty() { if !path.is_empty() {

View File

@ -166,3 +166,8 @@ blockquote {
.tooltipped .tooltiptext { .tooltipped .tooltiptext {
visibility: visible; visibility: visible;
} }
.chapter li.part-title {
color: var(--sidebar-fg);
margin: 5px 0px;
}