Merge pull request #1171 from mark-i-m/master

implement support for book parts
This commit is contained in:
Eric Huss 2020-05-20 12:17:45 -07:00 committed by GitHub
commit 5d5c55e619
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 221 additions and 80 deletions

View File

@ -22,15 +22,27 @@ allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
[Title of prefix element](relative/path/to/markdown.md) [Title of prefix element](relative/path/to/markdown.md)
``` ```
3. ***Numbered Chapter*** Numbered chapters are the main content of the book, 3. ***Part Title:*** Headers can be used as a title for the following numbered
chapters. This can be used to logically separate different sections
of book. The title is rendered as unclickable text.
Titles are optional, and the numbered chapters can be broken into as many
parts as desired.
4. ***Numbered Chapter*** Numbered chapters are the main content of the book,
they will be numbered and can be nested, resulting in a nice hierarchy they will be numbered and can be nested, resulting in a nice hierarchy
(chapters, sub-chapters, etc.) (chapters, 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)
# Title of Another Part
- [More Chapters](relative/path/to/markdown2.md)
``` ```
You can either use `-` or `*` to indicate a numbered chapter. You can either use `-` or `*` to indicate a numbered chapter.
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of 5. ***Suffix Chapter*** After the numbered chapters you can add a couple of
non-numbered chapters. They are the same as prefix chapters but come after non-numbered chapters. They are the same as prefix chapters but come after
the numbered chapters instead of before. the numbered chapters instead of before.

View File

@ -133,6 +133,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 {
@ -229,11 +231,12 @@ fn load_summary_item<P: AsRef<Path> + Clone>(
src_dir: P, src_dir: P,
parent_names: Vec<String>, parent_names: Vec<String>,
) -> Result<BookItem> { ) -> Result<BookItem> {
match *item { match item {
SummaryItem::Separator => Ok(BookItem::Separator), SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(ref link) => { SummaryItem::Link(ref link) => {
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter) load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
} }
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
} }
} }
@ -569,6 +572,7 @@ And here is some \
location: Some(PathBuf::from("")), location: Some(PathBuf::from("")),
..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,7 +60,7 @@ 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 numbered_chapters: Vec<SummaryItem>,
/// 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>,
@ -108,6 +113,8 @@ pub enum SummaryItem {
Link(Link), Link(Link),
/// A separator (`---`). /// A separator (`---`).
Separator, Separator,
/// A part title.
PartTitle(String),
} }
impl SummaryItem { impl SummaryItem {
@ -139,7 +146,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 +163,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 +215,7 @@ impl<'a> SummaryParser<'a> {
src: text, src: text,
stream: pulldown_parser, stream: pulldown_parser,
offset: 0, offset: 0,
back: None,
} }
} }
@ -225,7 +238,7 @@ impl<'a> SummaryParser<'a> {
.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 numbered_chapters = 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)
@ -239,8 +252,7 @@ impl<'a> SummaryParser<'a> {
}) })
} }
/// 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 +262,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 +286,52 @@ impl<'a> SummaryParser<'a> {
Ok(items) Ok(items)
} }
fn parse_parts(&mut self) -> Result<Vec<SummaryItem>> {
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")?;
if let Some(title) = title {
parts.push(SummaryItem::PartTitle(title));
}
parts.extend(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 +350,45 @@ 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 +399,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 +520,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 +677,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 +687,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 +698,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 +713,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 +736,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 +768,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 +795,47 @@ 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![
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(),
}),
SummaryItem::PartTitle(String::from("Title 2")),
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 +863,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 +874,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 +917,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

@ -532,6 +532,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,9 @@ blockquote {
.tooltipped .tooltiptext { .tooltipped .tooltiptext {
visibility: visible; visibility: visible;
} }
.chapter li.part-title {
color: var(--sidebar-fg);
margin: 5px 0px;
font-weight: bold;
}