Added the prefix chapter parsing step and a failing test for separators

This commit is contained in:
Michael Bryan 2017-06-28 00:19:38 +08:00
parent dcc8368543
commit df2ee64b2e
2 changed files with 96 additions and 13 deletions

View File

@ -3,9 +3,9 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::error::Error;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use errors::*;
mod summary; mod summary;
@ -26,7 +26,7 @@ impl Loader {
} }
/// Parse the `SUMMARY.md` file. /// Parse the `SUMMARY.md` file.
pub fn parse_summary(&self) -> Result<Summary, Box<Error>> { pub fn parse_summary(&self) -> Result<Summary> {
let path = self.source_directory.join("SUMMARY.md"); let path = self.source_directory.join("SUMMARY.md");
let mut summary_content = String::new(); let mut summary_content = String::new();

View File

@ -1,10 +1,12 @@
use std::error::Error; #![allow(dead_code, unused_variables)]
use std::fmt::{self, Formatter, Display}; use std::fmt::{self, Formatter, Display};
use std::io::{Error as IoError, ErrorKind};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::path::PathBuf; use std::path::PathBuf;
use pulldown_cmark::{self, Event, Tag}; use pulldown_cmark::{self, Event, Tag};
use errors::*;
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be /// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
/// used when loading a book from disk. /// used when loading a book from disk.
@ -42,7 +44,7 @@ use pulldown_cmark::{self, Event, Tag};
/// ///
/// All other elements are unsupported and will be ignored at best or result in /// All other elements are unsupported and will be ignored at best or result in
/// an error. /// an error.
pub fn parse_summary(summary: &str) -> Result<Summary, Box<Error>> { pub fn parse_summary(summary: &str) -> Result<Summary> {
let parser = SummaryParser::new(summary); let parser = SummaryParser::new(summary);
parser.parse() parser.parse()
} }
@ -51,6 +53,9 @@ pub fn parse_summary(summary: &str) -> Result<Summary, Box<Error>> {
#[derive(Debug, Clone, Default, PartialEq)] #[derive(Debug, Clone, Default, PartialEq)]
pub struct Summary { pub struct Summary {
title: Option<String>, title: Option<String>,
prefix_chapters: Vec<SummaryItem>,
numbered_chapters: Vec<SummaryItem>,
suffix_chapters: Vec<SummaryItem>,
} }
/// A struct representing an entry in the `SUMMARY.md`, possibly with nested /// A struct representing an entry in the `SUMMARY.md`, possibly with nested
@ -61,6 +66,7 @@ pub struct Summary {
struct Link { struct Link {
name: String, name: String,
location: PathBuf, location: PathBuf,
number: Option<SectionNumber>,
nested_items: Vec<SummaryItem>, nested_items: Vec<SummaryItem>,
} }
@ -153,28 +159,55 @@ impl<'a> SummaryParser<'a> {
} }
/// Parse the text the `SummaryParser` was created with. /// Parse the text the `SummaryParser` was created with.
fn parse(mut self) -> Result<Summary, Box<Error>> { fn parse(mut self) -> Result<Summary> {
self.summary.title = self.parse_title(); self.summary.title = self.parse_title();
Ok(self.summary) Ok(self.summary)
} }
fn step(&mut self) -> Result<(), Box<Error>> { fn step(&mut self) -> Result<()> {
let next_event = self.stream.next().expect("TODO: error-chain"); let next_event = self.stream.next().expect("TODO: error-chain");
trace!("[*] Current state = {:?}, Next Event = {:?}", self.state, next_event); trace!("[*] Current state = {:?}, Next Event = {:?}", self.state, next_event);
match self.state { match self.state {
State::Begin => self.step_start(next_event), State::Begin => self.step_start(next_event)?,
other => unimplemented!() State::PrefixChapters => self.step_prefix(next_event)?,
_ => unimplemented!()
} }
Ok(())
} }
/// The very first state, we should see a `BeginParagraph` token or /// The very first state, we should see a `BeginParagraph` token or
/// it's an error... /// it's an error...
fn step_start(&mut self, event: Event<'a>) -> Result<(), Box<Error>> { fn step_start(&mut self, event: Event<'a>) -> Result<()> {
match event { match event {
Event::Start(Tag::Paragraph) => self.state = State::PrefixChapters, Event::Start(Tag::Paragraph) => self.state = State::PrefixChapters,
other => panic!("Unexpected tag! {:?}", other), other => bail!("Unexpected tag! {:?}", other),
}
Ok(())
}
/// In the second step we look out for links and horizontal rules to add
/// to the prefix.
fn step_prefix(&mut self, event: Event<'a>) -> Result<()> {
match event {
Event::Start(Tag::Link(location, _)) => {
let content = collect_events!(self.stream, Tag::Link(_, _));
let text = stringify_events(content);
let link = Link {
name: text,
location: PathBuf::from(location.as_ref()),
number: None,
nested_items: Vec::new(),
};
self.summary.prefix_chapters.push(SummaryItem::Link(link));
}
other => {
debug!("[*] Skipping unexpected token in summary: {:?}", other);
}
} }
Ok(()) Ok(())
@ -196,7 +229,7 @@ impl<'a> SummaryParser<'a> {
} }
/// Parse a single item (`[Some Chapter Name](./path/to/chapter.md)`). /// Parse a single item (`[Some Chapter Name](./path/to/chapter.md)`).
fn parse_item(&mut self) -> Result<Link, Box<Error>> { fn parse_item(&mut self) -> Result<Link> {
let next = self.stream.next(); let next = self.stream.next();
if let Some(Event::Start(Tag::Link(dest, _))) = next { if let Some(Event::Start(Tag::Link(dest, _))) = next {
@ -205,10 +238,11 @@ impl<'a> SummaryParser<'a> {
Ok(Link { Ok(Link {
name: stringify_events(content), name: stringify_events(content),
location: PathBuf::from(dest.to_string()), location: PathBuf::from(dest.to_string()),
number: None,
nested_items: Vec::new(), nested_items: Vec::new(),
}) })
} else { } else {
Err(Box::new(IoError::new(ErrorKind::Other, format!("Expected a link, got {:?}", next)))) bail!("Expected a link, got {:?}", next)
} }
} }
} }
@ -301,6 +335,7 @@ mod tests {
let should_be = Link { let should_be = Link {
name: String::from("A Chapter"), name: String::from("A Chapter"),
location: PathBuf::from("./path/to/chapter"), location: PathBuf::from("./path/to/chapter"),
number: None,
nested_items: Vec::new(), nested_items: Vec::new(),
}; };
@ -333,4 +368,52 @@ mod tests {
parser.step().unwrap(); parser.step().unwrap();
assert_eq!(parser.state, should_be); assert_eq!(parser.state, should_be);
} }
#[test]
fn first_token_must_be_open_paragraph() {
let src = "hello world";
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // manually step past the Start Paragraph
assert!(parser.step().is_err());
}
#[test]
fn can_parse_prefix_chapter_links() {
let src = "[Hello World](./foo/bar/baz)";
let should_be = Link {
name: String::from("Hello World"),
location: PathBuf::from("./foo/bar/baz"),
number: None,
nested_items: Vec::new(),
};
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], 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);
}
} }