Rewrote summary parser from a state machine to use recursive descent

This commit is contained in:
Michael Bryan 2017-12-11 15:17:20 +11:00
parent 4f4e86db3a
commit f5e9b857de
No known key found for this signature in database
GPG Key ID: E9C602B0D9A998DC
4 changed files with 412 additions and 367 deletions

View File

@ -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<Link> 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,194 +194,190 @@ 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<Summary> {
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")?;
Ok(Summary {
title,
prefix_chapters,
numbered_chapters,
suffix_chapters,
})
}
while self.state != State::End {
self.step()?;
}
/// 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<Vec<SummaryItem>> {
let mut items = Vec::new();
debug!(
"[*] Parsing {} items",
if is_prefix { "prefix" } else { "suffix" }
);
Ok(self.summary)
}
fn step(&mut self) -> Result<()> {
if let Some(next_event) = self.stream.next() {
trace!("[*] Current state: {:?}, next event: {:?}", self.state, next_event);
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;
}
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<SummaryItem> {
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;
} else {
trace!("[*] Nesting level decreased to {}", n - 1);
self.state = State::NumberedChapters(n - 1);
}
}
},
Event::End(Tag::Item) => { /* Ignore */ },
other => {
trace!("[*] ignoring token: {:?}", other);
},
}
Ok(())
}
/// Parse a single item (`[Some Chapter Name](./path/to/chapter.md)`).
fn parse_item(&mut self) -> Result<Link> {
loop {
let next = self.stream.next();
trace!("Parsing an item, next event is {:?}", next);
trace!("[*] Next event: {:?}", next);
if let Some(Event::Start(Tag::Link(dest, _))) = next {
let content = collect_events!(self.stream, Tag::Link(..));
Ok(Link::new(stringify_events(content), dest.as_ref()))
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 {
bail!("Expected a link, got {:?}", next)
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
}
}
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(items)
}
fn parse_link(&mut self, href: String) -> Result<Link> {
let link_content = collect_events!(self.stream, end Tag::Link(..));
let name = stringify_events(link_content);
Ok(Link {
name: name,
location: PathBuf::from(href.to_string()),
number: None,
nested_items: Vec::new(),
})
}
/// Parse the numbered chapters. This assumes the opening list tag has
/// already been consumed by a previous parser.
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
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<Vec<SummaryItem>> {
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<D: Display>(&self, msg: D) -> Error {
let (line, col) = self.current_location();
ErrorKind::ParseError(line, col, msg.to_string()).into()
}
/// Try to parse the title line.
fn parse_title(&mut self) -> Option<String> {
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<SummaryItem>, mut item: SummaryItem, level: usize, mut section_number: SectionNumber)
-> Result<SectionNumber> {
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,11 +421,15 @@ pub struct SectionNumber(pub Vec<u32>);
impl Display for SectionNumber {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if self.0.is_empty() {
write!(f, "0")
} else {
for item in &self.0 {
write!(f, "{}.", item)?;
}
Ok(())
}
}
}
impl Deref for SectionNumber {
@ -516,6 +445,12 @@ impl DerefMut for SectionNumber {
}
}
impl FromIterator<u32> for SectionNumber {
fn from_iter<I: IntoIterator<Item = u32>>(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);
}
}

View File

@ -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)
}
}
}

View File

@ -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 Librarys 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<T>` 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<T>`, the Reference Counted Smart Pointer](ch15-04-rc.md)
- [`RefCell<T>` 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)

View File

@ -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");