From f5e9b857de9a38f88669c6e3a61a5d8ce3ae1089 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 11 Dec 2017 15:17:20 +1100 Subject: [PATCH] Rewrote summary parser from a state machine to use recursive descent --- src/book/summary.rs | 637 ++++++++---------- src/lib.rs | 9 + .../summary_md_files/the_book-2nd_edition.md | 130 ++++ tests/summary_parser.rs | 3 +- 4 files changed, 412 insertions(+), 367 deletions(-) create mode 100644 tests/summary_md_files/the_book-2nd_edition.md diff --git a/src/book/summary.rs b/src/book/summary.rs index 63581c59..430e9fa5 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -1,4 +1,5 @@ use std::fmt::{self, Display, Formatter}; +use std::iter::FromIterator; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use pulldown_cmark::{self, Event, Tag}; @@ -123,31 +124,7 @@ impl From for SummaryItem { } } -#[derive(Debug, Copy, Clone, PartialEq)] -enum State { - Begin, - PrefixChapters, - /// Numbered chapters, including the nesting level. - NumberedChapters(u32), - SuffixChapters, - End, -} - -/// A state machine parser for parsing a `SUMMARY.md` file. -/// -/// The parser has roughly 5 states, -/// -/// - **Begin:** the initial state -/// - **Prefix Chapters:** Parsing the prefix chapters -/// - **Numbered Chapters:** Parsing the numbered chapters, using a `usize` to -/// indicate the nesting level (because chapters can have sub-chapters) -/// - **Suffix Chapters:** pretty much identical to the Prefix Chapters -/// - **End:** The final state -/// -/// The `parse()` method then continually invokes `step()` until it reaches the -/// `End` state. Parsing is guaranteed to (eventually) finish because the next -/// `Event` is read from the underlying `pulldown_cmark::Parser` and passed -/// into the current state's associated method. +/// A recursive descent parser for a `SUMMARY.md`. /// /// /// # Grammar @@ -174,9 +151,8 @@ enum State { /// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly) /// > match the following regex: "[^<>\n[]]+". struct SummaryParser<'a> { + src: &'a str, stream: pulldown_cmark::Parser<'a>, - summary: Summary, - state: State, } /// Reads `Events` from the provided stream until the corresponding @@ -187,6 +163,12 @@ struct SummaryParser<'a> { /// use pattern matching and you won't get errors because `take_while()` /// moves `$stream` out of self. macro_rules! collect_events { + ($stream:expr, start $delimiter:pat) => { + collect_events!($stream, Event::Start($delimiter)) + }; + ($stream:expr, end $delimiter:pat) => { + collect_events!($stream, Event::End($delimiter)) + }; ($stream:expr, $delimiter:pat) => { { let mut events = Vec::new(); @@ -195,7 +177,7 @@ macro_rules! collect_events { let event = $stream.next(); trace!("Next event: {:?}", event); match event { - Some(Event::End($delimiter)) => break, + Some($delimiter) => break, Some(other) => events.push(other), None => { debug!("Reached end of stream without finding the closing pattern, {}", stringify!($delimiter)); @@ -212,186 +194,182 @@ macro_rules! collect_events { impl<'a> SummaryParser<'a> { fn new(text: &str) -> SummaryParser { let pulldown_parser = pulldown_cmark::Parser::new(text); - let intermediate_summary = Summary::default(); SummaryParser { + src: text, stream: pulldown_parser, - summary: intermediate_summary, - state: State::Begin, } } + fn current_location(&self) -> (usize, usize) { + let byte_offset = self.stream.get_offset(); + + let line = 0; + let col = 0; + (line, col) + } + /// Parse the text the `SummaryParser` was created with. fn parse(mut self) -> Result { - self.summary.title = self.parse_title(); + let title = self.parse_title(); - if let Some(ref title) = self.summary.title { - debug!("[*] Title is {:?}", title); - } + let prefix_chapters = self.parse_affix(true) + .chain_err(|| "There was an error parsing the prefix chapters")?; + let numbered_chapters = self.parse_numbered() + .chain_err(|| "There was an error parsing the numbered chapters")?; + let suffix_chapters = self.parse_affix(false) + .chain_err(|| "There was an error parsing the suffix chapters")?; - while self.state != State::End { - self.step()?; - } - - Ok(self.summary) + Ok(Summary { + title, + prefix_chapters, + numbered_chapters, + suffix_chapters, + }) } - fn step(&mut self) -> Result<()> { - if let Some(next_event) = self.stream.next() { - trace!("[*] Current state: {:?}, next event: {:?}", self.state, next_event); + /// Parse the affix chapters. This expects the first event (start of + /// paragraph) to have already been consumed by the previous parser. + fn parse_affix(&mut self, is_prefix: bool) -> Result> { + let mut items = Vec::new(); + debug!( + "[*] Parsing {} items", + if is_prefix { "prefix" } else { "suffix" } + ); - match self.state { - State::Begin => self.step_start(next_event)?, - State::PrefixChapters | State::SuffixChapters => self.step_affix(next_event)?, - State::NumberedChapters(_) => self.step_numbered(next_event)?, - State::End => {}, - } - } else { - trace!("[*] Reached end of SUMMARY.md"); - self.state = State::End; - } + loop { + let next = self.stream.next(); + trace!("[*] Next event: {:?}", next); - Ok(()) - } - - /// The very first state, we should see a `Begin Paragraph` token or - /// it's an error... - fn step_start(&mut self, event: Event<'a>) -> Result<()> { - match event { - Event::Start(Tag::Paragraph) => self.state = State::PrefixChapters, - Event::Start(Tag::List(_)) => self.state = State::NumberedChapters(0), - other => bail!("Expected a start of paragraph but got {:?}", other), - } - - Ok(()) - } - - /// Try to step through an "affix" section (recognising prefix and suffix - /// chapters). - /// - /// If we encounter a link or horizontal line, it'll get added to the - /// section. If we encounter a list, we'll either change to - /// `State::NumberedChapter` (for prefix) or throw an error (suffix chapters). - /// - /// Anything else will be ignored. - fn step_affix(&mut self, event: Event<'a>) -> Result<()> { - - match event { - Event::Start(tag) => self.handle_start_tag_in_affix_chapter(tag)?, - Event::End(Tag::Rule) => { - debug!("[*] Found an affix chapter separator"); - self.affix_chapter_list().push(SummaryItem::Separator); - }, - other => { - trace!("[*] Skipping unexpected token in summary: {:?}", other); - }, - } - - Ok(()) - } - - /// A helper function to get the `SummaryItem` list we should add items to - /// when parsing an affix chapter (i.e. prefix or suffix chapters). - fn affix_chapter_list(&mut self) -> &mut Vec { - match self.state { - State::PrefixChapters => &mut self.summary.prefix_chapters, - State::SuffixChapters => &mut self.summary.suffix_chapters, - other => panic!("affix_chapter_list() called with invalid state: {:?}", other), - } - } - - fn handle_start_tag_in_affix_chapter(&mut self, tag: Tag) -> Result<()> { - match tag { - Tag::Link(location, _) => { - let content = collect_events!(self.stream, Tag::Link(_, _)); - let text = stringify_events(content); - let link = Link::new(text, location.as_ref()); - - debug!("[*] Found an affix chapter: {:?}", link.name); - self.affix_chapter_list().push(SummaryItem::Link(link)); - }, - Tag::List(_) => { - match self.state { - State::PrefixChapters => { - debug!("[*] Changing from prefix chapters to numbered chapters"); - self.state = State::NumberedChapters(0); - }, - State::SuffixChapters => bail!("Suffix chapters can't be followed by a list"), - _ => unreachable!(), - } - }, - other => trace!("[*] Skipping unknown start tag while parsing affix chapters: {:?}", other), - } - - Ok(()) - } - - /// Parse the numbered chapters. - /// - /// If the event is the start of a list item, consume the entire item and - /// add a new link to the summary with `push_numbered_section`. - /// - /// If the event is the start of a new list, bump the nesting level. - /// - /// If the event is the end of a list, decrement the nesting level. When - /// the nesting level would go negative, we've finished the numbered - /// section and need to parse the suffix section. - /// - /// Otherwise, ignore the event. - fn step_numbered(&mut self, event: Event) -> Result<()> { - match event { - Event::Start(Tag::Item) => { - let it = self.parse_item().chain_err( - || "List items should only contain links", - )?; - - debug!("[*] Found a chapter: {:?} ({})", it.name, it.location.display()); - let section_number = self.push_numbered_section(SummaryItem::Link(it)); - trace!("[*] Section number is {}", section_number); - }, - Event::End(Tag::Rule) => { - debug!("[*] Found a numbered chapter separator"); - self.summary.numbered_chapters.push(SummaryItem::Separator); - self.state = State::NumberedChapters(0); - }, - Event::Start(Tag::List(_)) => { - if let State::NumberedChapters(n) = self.state { - self.state = State::NumberedChapters(n + 1); - trace!("[*] Nesting level increased to {}", n + 1); - } - }, - Event::End(Tag::List(_)) => { - if let State::NumberedChapters(n) = self.state { - if n == 0 { - trace!("[*] Finished parsing the numbered chapters"); - self.state = State::SuffixChapters; + match next { + Some(Event::Start(Tag::List(..))) => { + if is_prefix { + // we've finished prefix chapters and are at the start + // of the numbered section. + break; } else { - trace!("[*] Nesting level decreased to {}", n - 1); - self.state = State::NumberedChapters(n - 1); + bail!(self.parse_error("Suffix chapters cannot be followed by a list")); } } - }, - Event::End(Tag::Item) => { /* Ignore */ }, - other => { - trace!("[*] ignoring token: {:?}", other); - }, + Some(Event::Start(Tag::Link(href, _))) => { + let link = self.parse_link(href.to_string())?; + items.push(SummaryItem::Link(link)); + } + Some(Event::Start(Tag::Rule)) => items.push(SummaryItem::Separator), + Some(_) => {} + None => break, + } } - Ok(()) + Ok(items) } - /// Parse a single item (`[Some Chapter Name](./path/to/chapter.md)`). - fn parse_item(&mut self) -> Result { - let next = self.stream.next(); - trace!("Parsing an item, next event is {:?}", next); + fn parse_link(&mut self, href: String) -> Result { + let link_content = collect_events!(self.stream, end Tag::Link(..)); + let name = stringify_events(link_content); - if let Some(Event::Start(Tag::Link(dest, _))) = next { - let content = collect_events!(self.stream, Tag::Link(..)); + Ok(Link { + name: name, + location: PathBuf::from(href.to_string()), + number: None, + nested_items: Vec::new(), + }) + } - Ok(Link::new(stringify_events(content), dest.as_ref())) - } else { - bail!("Expected a link, got {:?}", next) + /// Parse the numbered chapters. This assumes the opening list tag has + /// already been consumed by a previous parser. + fn parse_numbered(&mut self) -> Result> { + let mut items = Vec::new(); + let root_number = SectionNumber::default(); + + // we need to do this funny loop-match-if-let dance because a rule will + // close off any currently running list. Therefore we try to read the + // list items before the rule, then if we encounter a rule we'll add a + // separator and try to resume parsing numbered chapters if we start a + // list immediately afterwards. + // + // If you can think of a better way to do this then please make a PR :) + + loop { + let bunch_of_items = self.parse_nested_numbered(&root_number)?; + items.extend(bunch_of_items); + + match self.stream.next() { + Some(Event::Start(Tag::Rule)) => { + items.push(SummaryItem::Separator); + + if let Some(Event::Start(Tag::List(..))) = self.stream.next() { + continue; + } else { + break; + } + } + _ => { + break; + } + } } + + Ok(items) + } + + fn parse_nested_numbered(&mut self, parent: &SectionNumber) -> Result> { + debug!("[*] Parsing numbered chapters at level {}", parent); + let mut items = Vec::new(); + + loop { + let next = self.stream.next(); + trace!("[*] Next event: {:?}", next); + + match next { + Some(Event::Start(Tag::Item)) => match self.stream.next() { + Some(Event::Start(Tag::Link(href, _))) => { + let mut link = self.parse_link(href.to_string())?; + + let mut number = parent.clone(); + number.0.push(items.len() as u32 + 1); + trace!( + "[*] Found chapter: {} {} ({})", + number, + link.name, + link.location.display() + ); + + link.number = Some(number); + items.push(SummaryItem::Link(link)); + } + other => { + warn!("Expected a start of a link, actually got {:?}", other); + bail!(self.parse_error( + "The link items for nested chapters must only contain a hyperlink" + )); + } + }, + Some(Event::Start(Tag::List(..))) => { + // recurse + 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; + } + Some(Event::End(Tag::List(..))) => break, + Some(_) => {} + None => break, + } + } + + Ok(items) + } + + fn parse_error(&self, msg: D) -> Error { + let (line, col) = self.current_location(); + + ErrorKind::ParseError(line, col, msg.to_string()).into() } /// Try to parse the title line. @@ -399,7 +377,7 @@ impl<'a> SummaryParser<'a> { if let Some(Event::Start(Tag::Header(1))) = self.stream.next() { debug!("[*] Found a h1 in the SUMMARY"); - let tags = collect_events!(self.stream, Tag::Header(1)); + let tags = collect_events!(self.stream, end Tag::Header(1)); // TODO: How do we deal with headings like "# My **awesome** summary"? // for now, I'm just going to scan through and concatenate the @@ -409,64 +387,11 @@ impl<'a> SummaryParser<'a> { None } } - - /// Push a new section at the end of the current nesting level. - fn push_numbered_section(&mut self, item: SummaryItem) -> SectionNumber { - if let State::NumberedChapters(level) = self.state { - push_item_at_nesting_level( - &mut self.summary.numbered_chapters, - item, - level as usize, - SectionNumber::default(), - ).chain_err(|| { - format!("The parser should always ensure we add the next \ - item at the correct level ({}:{})", module_path!(), line!()) - }) - .unwrap() - } else { - // this method should only ever be called when parsing a numbered - // section, therefore if we ever get here something has gone - // hideously wrong... - error!("Calling push_numbered_section() when not in a numbered section"); - error!("Current state: {:?}", self.state); - error!("Item: {:?}", item); - error!("Summary:"); - error!("{:#?}", self.summary); - panic!("Entered unreachable code, this is a bug"); - } - } -} - -/// Given a particular level (e.g. 3), go that many levels down the `Link`'s -/// nested items then append the provided item to the last `Link` in the -/// list. -fn push_item_at_nesting_level(links: &mut Vec, mut item: SummaryItem, level: usize, mut section_number: SectionNumber) - -> Result { - if level == 0 { - // set the section number, if applicable - section_number.push(links.len() as u32 + 1); - - if let SummaryItem::Link(ref mut l) = item { - l.number = Some(section_number.clone()); - } - - links.push(item); - Ok(section_number) - } else { - let (index, last_link) = get_last_link(links).chain_err(|| { - format!("The list of links needs to be {} levels deeper (current position {})", - level, section_number) - })?; - - section_number.push(index as u32 + 1); - push_item_at_nesting_level(&mut last_link.nested_items, item, level - 1, section_number) - } } /// 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)> { - // TODO: This should probably be integrated into `Link::push_item()` links .iter_mut() .enumerate() @@ -496,10 +421,14 @@ pub struct SectionNumber(pub Vec); impl Display for SectionNumber { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - for item in &self.0 { - write!(f, "{}.", item)?; + if self.0.is_empty() { + write!(f, "0") + } else { + for item in &self.0 { + write!(f, "{}.", item)?; + } + Ok(()) } - Ok(()) } } @@ -516,6 +445,12 @@ impl DerefMut for SectionNumber { } } +impl FromIterator for SectionNumber { + fn from_iter>(it: I) -> Self { + SectionNumber(it.into_iter().collect()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -556,23 +491,6 @@ mod tests { assert_eq!(got, should_be); } - #[test] - fn parse_a_single_item() { - let src = "[A Chapter](./path/to/chapter)"; - let should_be = Link { - name: String::from("A Chapter"), - location: PathBuf::from("./path/to/chapter"), - number: None, - nested_items: Vec::new(), - }; - - let mut parser = SummaryParser::new(src); - let _ = parser.stream.next(); // skip the opening paragraph tag - let got = parser.parse_item().unwrap(); - - assert_eq!(got, should_be); - } - #[test] fn convert_markdown_events_to_a_string() { let src = "Hello *World*, `this` is some text [and a link](./path/to/link)"; @@ -585,136 +503,123 @@ mod tests { } #[test] - fn can_step_past_first_token() { - let src = "hello world"; - let should_be = State::PrefixChapters; - + fn parse_some_prefix_items() { + let src = "[First](./first.md)\n[Second](./second.md)\n"; let mut parser = SummaryParser::new(src); - assert_eq!(parser.state, State::Begin); - parser.step().unwrap(); - assert_eq!(parser.state, should_be); + + let should_be = vec![ + SummaryItem::Link(Link { + name: String::from("First"), + location: PathBuf::from("./first.md"), + ..Default::default() + }), + SummaryItem::Link(Link { + name: String::from("Second"), + location: PathBuf::from("./second.md"), + ..Default::default() + }), + ]; + + let _ = parser.stream.next(); // step past first event + let got = parser.parse_affix(true).unwrap(); + + assert_eq!(got, should_be); } #[test] - fn first_token_must_be_open_paragraph() { - let src = "hello world"; - + fn parse_prefix_items_with_a_separator() { + let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n"; let mut parser = SummaryParser::new(src); - let _ = parser.stream.next(); // manually step past the Start Paragraph - assert!(parser.step().is_err()); + + let _ = parser.stream.next(); // step past first event + let got = parser.parse_affix(true).unwrap(); + + assert_eq!(got.len(), 3); + assert_eq!(got[1], SummaryItem::Separator); } #[test] - fn can_parse_prefix_chapter_links() { - let src = "[Hello World](./foo/bar/baz)"; + fn suffix_items_cannot_be_followed_by_a_list() { + let src = "[First](./first.md)\n- [Second](./second.md)\n"; + let mut parser = SummaryParser::new(src); + + let _ = parser.stream.next(); // step past first event + let got = parser.parse_affix(false); + + assert!(got.is_err()); + } + + #[test] + fn parse_a_link() { + let src = "[First](./first.md)"; let should_be = Link { - name: String::from("Hello World"), - location: PathBuf::from("./foo/bar/baz"), - number: None, - nested_items: Vec::new(), + name: String::from("First"), + location: PathBuf::from("./first.md"), + ..Default::default() }; let mut parser = SummaryParser::new(src); - parser.state = State::PrefixChapters; - assert!(parser.summary.prefix_chapters.is_empty()); + let _ = parser.stream.next(); // skip past start of paragraph - let _ = parser.stream.next(); // manually step past the Start Paragraph - parser.step().unwrap(); + let href = match parser.stream.next() { + Some(Event::Start(Tag::Link(href, _))) => href.to_string(), + other => panic!("Unreachable, {:?}", other), + }; - assert_eq!(parser.summary.prefix_chapters.len(), 1); - assert_eq!(parser.summary.prefix_chapters[0], SummaryItem::Link(should_be)); - assert_eq!(parser.state, State::PrefixChapters); - } - - #[test] - fn can_parse_prefix_chapter_horizontal_rules() { - let src = "---"; - let should_be = SummaryItem::Separator; - - let mut parser = SummaryParser::new(src); - parser.state = State::PrefixChapters; - assert!(parser.summary.prefix_chapters.is_empty()); - - let _ = parser.stream.next(); // manually step past the Start Paragraph - parser.step().unwrap(); - - assert_eq!(parser.summary.prefix_chapters.len(), 1); - assert_eq!(parser.summary.prefix_chapters[0], should_be); - assert_eq!(parser.state, State::PrefixChapters); - } - - #[test] - fn step_from_prefix_chapters_to_numbered() { - let src = "- foo"; - - let mut parser = SummaryParser::new(src); - parser.state = State::PrefixChapters; - - // let _ = parser.stream.next(); // manually step past the Start Paragraph - parser.step().unwrap(); - - assert_eq!(parser.state, State::NumberedChapters(0)); - } - - #[test] - fn push_item_onto_empty_link() { - let root = Link::new("First", "/"); - let mut links = vec![SummaryItem::Link(root)]; - - assert_eq!(links[0].maybe_link_mut().unwrap().nested_items.len(), 0); - let got = push_item_at_nesting_level(&mut links, SummaryItem::Separator, 1, SectionNumber::default()).unwrap(); - assert_eq!(links[0].maybe_link_mut().unwrap().nested_items.len(), 1); - assert_eq!(*got, vec![1, 1]); - } - - #[test] - fn push_item_onto_complex_link() { - let mut root = Link::new("First", "/first"); - root.nested_items.push(SummaryItem::Separator); - - let mut child = Link::new("Second", "/first/second"); - child.nested_items.push(SummaryItem::Link( - Link::new("Third", "/first/second/third"), - )); - root.nested_items.push(SummaryItem::Link(child)); - root.nested_items.push(SummaryItem::Separator); - - let mut links = vec![SummaryItem::Link(root)]; - - // FIXME: This crap for getting a deeply nested member is just plain ugly :( - assert_eq!(links[0].maybe_link_mut().unwrap() - .nested_items[1].maybe_link_mut() - .unwrap() - .nested_items[0].maybe_link_mut() - .unwrap() - .nested_items.len() , 0); - let got = push_item_at_nesting_level( - &mut links, - SummaryItem::Link(Link::new("Dummy", "")), - 3, - SectionNumber::default(), - ).unwrap(); - assert_eq!(links[0].maybe_link_mut().unwrap() - .nested_items[1].maybe_link_mut() - .unwrap() - .nested_items[0].maybe_link_mut() - .unwrap() - .nested_items.len() , 1); - println!("{:#?}", links); - assert_eq!(*got, vec![1, 2, 1, 1]); + let got = parser.parse_link(href).unwrap(); + assert_eq!(got, should_be); } #[test] fn parse_a_numbered_chapter() { - let src = "- [First](./second)"; + let src = "- [First](./first.md)\n"; + let link = Link { + name: String::from("First"), + location: PathBuf::from("./first.md"), + number: Some(SectionNumber(vec![1])), + ..Default::default() + }; + let should_be = vec![SummaryItem::Link(link)]; + let mut parser = SummaryParser::new(src); let _ = parser.stream.next(); - assert_eq!(parser.summary.numbered_chapters.len(), 0); + let got = parser.parse_numbered().unwrap(); - parser.state = State::NumberedChapters(0); - parser.step().unwrap(); + assert_eq!(got, should_be); + } - assert_eq!(parser.summary.numbered_chapters.len(), 1); + #[test] + fn parse_nested_numbered_chapters() { + let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)"; + + let should_be = vec![ + SummaryItem::Link(Link { + name: String::from("First"), + location: PathBuf::from("./first.md"), + number: Some(SectionNumber(vec![1])), + nested_items: vec![ + SummaryItem::Link(Link { + name: String::from("Nested"), + location: PathBuf::from("./nested.md"), + 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); } } diff --git a/src/lib.rs b/src/lib.rs index 450e7e9c..f6590088 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,10 @@ extern crate serde_json; extern crate tempdir; extern crate toml; +#[cfg(test)] +#[macro_use] +extern crate pretty_assertions; + mod preprocess; pub mod book; pub mod config; @@ -131,6 +135,11 @@ pub mod errors { description("A subprocess failed") display("{}: {}", message, String::from_utf8_lossy(&output.stdout)) } + + ParseError(line: usize, col: usize, message: String) { + description("A SUMMARY.md parsing error") + display("Error at line {}, column {}: {}", line, col, message) + } } } diff --git a/tests/summary_md_files/the_book-2nd_edition.md b/tests/summary_md_files/the_book-2nd_edition.md new file mode 100644 index 00000000..c7f83067 --- /dev/null +++ b/tests/summary_md_files/the_book-2nd_edition.md @@ -0,0 +1,130 @@ +# The Rust Programming Language + +## Getting started + +- [Introduction](ch01-00-introduction.md) + - [Installation](ch01-01-installation.md) + - [Hello, World!](ch01-02-hello-world.md) + +- [Guessing Game Tutorial](ch02-00-guessing-game-tutorial.md) + +- [Common Programming Concepts](ch03-00-common-programming-concepts.md) + - [Variables and Mutability](ch03-01-variables-and-mutability.md) + - [Data Types](ch03-02-data-types.md) + - [How Functions Work](ch03-03-how-functions-work.md) + - [Comments](ch03-04-comments.md) + - [Control Flow](ch03-05-control-flow.md) + +- [Understanding Ownership](ch04-00-understanding-ownership.md) + - [What is Ownership?](ch04-01-what-is-ownership.md) + - [References & Borrowing](ch04-02-references-and-borrowing.md) + - [Slices](ch04-03-slices.md) + +- [Using Structs to Structure Related Data](ch05-00-structs.md) + - [Defining and Instantiating Structs](ch05-01-defining-structs.md) + - [An Example Program Using Structs](ch05-02-example-structs.md) + - [Method Syntax](ch05-03-method-syntax.md) + +- [Enums and Pattern Matching](ch06-00-enums.md) + - [Defining an Enum](ch06-01-defining-an-enum.md) + - [The `match` Control Flow Operator](ch06-02-match.md) + - [Concise Control Flow with `if let`](ch06-03-if-let.md) + +## Basic Rust Literacy + +- [Modules](ch07-00-modules.md) + - [`mod` and the Filesystem](ch07-01-mod-and-the-filesystem.md) + - [Controlling Visibility with `pub`](ch07-02-controlling-visibility-with-pub.md) + - [Referring to Names in Different Modules](ch07-03-importing-names-with-use.md) + +- [Common Collections](ch08-00-common-collections.md) + - [Vectors](ch08-01-vectors.md) + - [Strings](ch08-02-strings.md) + - [Hash Maps](ch08-03-hash-maps.md) + +- [Error Handling](ch09-00-error-handling.md) + - [Unrecoverable Errors with `panic!`](ch09-01-unrecoverable-errors-with-panic.md) + - [Recoverable Errors with `Result`](ch09-02-recoverable-errors-with-result.md) + - [To `panic!` or Not To `panic!`](ch09-03-to-panic-or-not-to-panic.md) + +- [Generic Types, Traits, and Lifetimes](ch10-00-generics.md) + - [Generic Data Types](ch10-01-syntax.md) + - [Traits: Defining Shared Behavior](ch10-02-traits.md) + - [Validating References with Lifetimes](ch10-03-lifetime-syntax.md) + +- [Testing](ch11-00-testing.md) + - [Writing tests](ch11-01-writing-tests.md) + - [Running tests](ch11-02-running-tests.md) + - [Test Organization](ch11-03-test-organization.md) + +- [An I/O Project: Building a Command Line Program](ch12-00-an-io-project.md) + - [Accepting Command Line Arguments](ch12-01-accepting-command-line-arguments.md) + - [Reading a File](ch12-02-reading-a-file.md) + - [Refactoring to Improve Modularity and Error Handling](ch12-03-improving-error-handling-and-modularity.md) + - [Developing the Library’s Functionality with Test Driven Development](ch12-04-testing-the-librarys-functionality.md) + - [Working with Environment Variables](ch12-05-working-with-environment-variables.md) + - [Writing Error Messages to Standard Error Instead of Standard Output](ch12-06-writing-to-stderr-instead-of-stdout.md) + +## Thinking in Rust + +- [Functional Language Features: Iterators and Closures](ch13-00-functional-features.md) + - [Closures: Anonymous Functions that Can Capture Their Environment](ch13-01-closures.md) + - [Processing a Series of Items with Iterators](ch13-02-iterators.md) + - [Improving Our I/O Project](ch13-03-improving-our-io-project.md) + - [Comparing Performance: Loops vs. Iterators](ch13-04-performance.md) + +- [More about Cargo and Crates.io](ch14-00-more-about-cargo.md) + - [Customizing Builds with Release Profiles](ch14-01-release-profiles.md) + - [Publishing a Crate to Crates.io](ch14-02-publishing-to-crates-io.md) + - [Cargo Workspaces](ch14-03-cargo-workspaces.md) + - [Installing Binaries from Crates.io with `cargo install`](ch14-04-installing-binaries.md) + - [Extending Cargo with Custom Commands](ch14-05-extending-cargo.md) + +- [Smart Pointers](ch15-00-smart-pointers.md) + - [`Box` Points to Data on the Heap and Has a Known Size](ch15-01-box.md) + - [The `Deref` Trait Allows Access to the Data Through a Reference](ch15-02-deref.md) + - [The `Drop` Trait Runs Code on Cleanup](ch15-03-drop.md) + - [`Rc`, the Reference Counted Smart Pointer](ch15-04-rc.md) + - [`RefCell` and the Interior Mutability Pattern](ch15-05-interior-mutability.md) + - [Creating Reference Cycles and Leaking Memory is Safe](ch15-06-reference-cycles.md) + +- [Fearless Concurrency](ch16-00-concurrency.md) + - [Threads](ch16-01-threads.md) + - [Message Passing](ch16-02-message-passing.md) + - [Shared State](ch16-03-shared-state.md) + - [Extensible Concurrency: `Sync` and `Send`](ch16-04-extensible-concurrency-sync-and-send.md) + +- [Is Rust an Object-Oriented Programming Language?](ch17-00-oop.md) + - [What Does Object-Oriented Mean?](ch17-01-what-is-oo.md) + - [Trait Objects for Using Values of Different Types](ch17-02-trait-objects.md) + - [Object-Oriented Design Pattern Implementations](ch17-03-oo-design-patterns.md) + +## Advanced Topics + +- [Patterns Match the Structure of Values](ch18-00-patterns.md) + - [All the Places Patterns May be Used](ch18-01-all-the-places-for-patterns.md) + - [Refutability: Whether a Pattern Might Fail to Match](ch18-02-refutability.md) + - [All the Pattern Syntax](ch18-03-pattern-syntax.md) + +- [Advanced Features](ch19-00-advanced-features.md) + - [Unsafe Rust](ch19-01-unsafe-rust.md) + - [Advanced Lifetimes](ch19-02-advanced-lifetimes.md) + - [Advanced Traits](ch19-03-advanced-traits.md) + - [Advanced Types](ch19-04-advanced-types.md) + - [Advanced Functions & Closures](ch19-05-advanced-functions-and-closures.md) + +- [Final Project: Building a Multithreaded Web Server](ch20-00-final-project-a-web-server.md) + - [A Single Threaded Web Server](ch20-01-single-threaded.md) + - [How Slow Requests Affect Throughput](ch20-02-slow-requests.md) + - [Designing the Thread Pool Interface](ch20-03-designing-the-interface.md) + - [Creating the Thread Pool and Storing Threads](ch20-04-storing-threads.md) + - [Sending Requests to Threads Via Channels](ch20-05-sending-requests-via-channels.md) + - [Graceful Shutdown and Cleanup](ch20-06-graceful-shutdown-and-cleanup.md) + +- [Appendix](appendix-00.md) + - [A - Keywords](appendix-01-keywords.md) + - [B - Operators and Symbols](appendix-02-operators.md) + - [C - Derivable Traits](appendix-03-derivable-traits.md) + - [D - Macros](appendix-04-macros.md) + - [E - Translations](appendix-05-translation.md) + - [F - Newest Features](appendix-06-newest-features.md) diff --git a/tests/summary_parser.rs b/tests/summary_parser.rs index 730d4204..8f23ab05 100644 --- a/tests/summary_parser.rs +++ b/tests/summary_parser.rs @@ -1,8 +1,8 @@ //! Some integration tests to make sure the `SUMMARY.md` parser can deal with //! some real-life examples. -extern crate error_chain; extern crate env_logger; +extern crate error_chain; extern crate mdbook; use std::fs::File; @@ -46,3 +46,4 @@ macro_rules! summary_md_test { summary_md_test!(rust_by_example, "rust_by_example.md"); summary_md_test!(rust_ffi_guide, "rust_ffi_guide.md"); summary_md_test!(example_book, "example_book.md"); +summary_md_test!(the_book_2nd_edition, "the_book-2nd_edition.md");