Merge pull request #1171 from mark-i-m/master
implement support for book parts
This commit is contained in:
commit
5d5c55e619
|
@ -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.
|
||||||
|
|
||||||
|
@ -50,5 +62,5 @@ error.
|
||||||
of contents, as you can see for the next chapter in the table of contents on the left.
|
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
|
Draft chapters are written like normal chapters but without writing the path to the file
|
||||||
```markdown
|
```markdown
|
||||||
- [Draft chapter]()
|
- [Draft chapter]()
|
||||||
```
|
```
|
||||||
|
|
|
@ -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()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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) => {}
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -134,12 +141,13 @@ impl From<Link> for SummaryItem {
|
||||||
///
|
///
|
||||||
/// ```text
|
/// ```text
|
||||||
/// summary ::= title prefix_chapters numbered_chapters
|
/// summary ::= title prefix_chapters numbered_chapters
|
||||||
/// suffix_chapters
|
/// suffix_chapters
|
||||||
/// title ::= "# " TEXT
|
/// title ::= "# " TEXT
|
||||||
/// | 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,36 +350,46 @@ 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 {
|
||||||
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// them
|
|
||||||
update_section_numbers(&mut bunch_of_items, 0, root_items);
|
|
||||||
root_items += bunch_of_items.len() as u32;
|
|
||||||
items.extend(bunch_of_items);
|
|
||||||
|
|
||||||
match self.next_event() {
|
match self.next_event() {
|
||||||
Some(Event::Start(Tag::Paragraph)) => {
|
Some(ev @ Event::Start(Tag::Paragraph)) => {
|
||||||
// we're starting the suffix chapters
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
Some(ev @ Event::Start(Tag::List(..))) => {
|
||||||
|
self.back(ev);
|
||||||
|
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// them
|
||||||
|
update_section_numbers(&mut bunch_of_items, 0, *root_items);
|
||||||
|
*root_items += bunch_of_items.len() as u32;
|
||||||
|
items.extend(bunch_of_items);
|
||||||
|
}
|
||||||
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
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// something else... ignore
|
||||||
|
Some(_) => {}
|
||||||
|
|
||||||
|
// EOF, bail...
|
||||||
None => {
|
None => {
|
||||||
// EOF, bail...
|
|
||||||
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.offset = range.start;
|
self.stream.next().map(|(ev, range)| {
|
||||||
ev
|
self.offset = range.start;
|
||||||
|
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() {
|
||||||
debug!("Found a h1 in the SUMMARY");
|
Some(Event::Start(Tag::Heading(1))) => {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()));
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue