Can now parse a single item (link to a chapter)

This commit is contained in:
Michael Bryan 2017-06-25 18:26:19 +08:00
parent 588b444f06
commit dacb3e082e
1 changed files with 95 additions and 27 deletions

View File

@ -1,6 +1,8 @@
use std::error::Error; use std::error::Error;
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 pulldown_cmark::{self, Event, Tag}; use pulldown_cmark::{self, Event, Tag};
@ -50,6 +52,23 @@ pub struct Summary {
title: Option<String>, title: Option<String>,
} }
/// A struct representing an entry in the `SUMMARY.md`, possibly with nested
/// entries.
///
/// This is roughly the equivalent of `[Some section](./path/to/file.md)`.
#[derive(Debug, Clone, Default, PartialEq)]
struct Link {
name: String,
location: PathBuf,
nested_items: Vec<SummaryItem>,
}
#[derive(Debug, Clone, PartialEq)]
enum SummaryItem {
Link(Link),
Separator,
}
/// A stateful parser for parsing a `SUMMARY.md` file. /// A stateful parser for parsing a `SUMMARY.md` file.
/// ///
/// # Grammar /// # Grammar
@ -79,6 +98,35 @@ struct SummaryParser<'a> {
summary: Summary, summary: Summary,
} }
/// Reads `Events` from the provided stream until the corresponding
/// `Event::End` is encountered which matches the `$delimiter` pattern.
///
/// This is the equivalent of doing
/// `$stream.take_while(|e| e != $delimeter).collect()` but it allows you to
/// use pattern matching and you won't get errors because `take_while()`
/// moves `$stream` out of self.
macro_rules! collect_events {
($stream:expr, $delimiter:pat) => {
{
let mut events = Vec::new();
loop {
let event = $stream.next();
match event {
Some(Event::End($delimiter)) => break,
Some(other) => events.push(other),
None => {
debug!("Reached end of stream without finding the closing pattern, {}", stringify!($delimiter));
break;
}
}
}
events
}
}
}
impl<'a> SummaryParser<'a> impl<'a> SummaryParser<'a>
{ {
fn new(text: &str) -> SummaryParser { fn new(text: &str) -> SummaryParser {
@ -91,6 +139,7 @@ impl<'a> SummaryParser<'a>
} }
} }
/// Parse the text the `SummaryParser` was created with.
fn parse(mut self) -> Result<Summary, Box<Error>> { fn parse(mut self) -> Result<Summary, Box<Error>> {
self.summary.title = self.parse_title(); self.summary.title = self.parse_title();
@ -101,41 +150,44 @@ impl<'a> SummaryParser<'a>
if let Some(Event::Start(Tag::Header(1))) = self.stream.next() { if let Some(Event::Start(Tag::Header(1))) = self.stream.next() {
debug!("[*] Found a h1 in the SUMMARY"); debug!("[*] Found a h1 in the SUMMARY");
let mut tags = Vec::new(); let tags = collect_events!(self.stream, Tag::Header(1));
loop {
let next_event = self.stream.next();
match next_event {
Some(Event::End(Tag::Header(1))) => break,
Some(other) => tags.push(other),
None => {
// If we ever get here then changes are pulldown_cmark
// is seriously broken. It means there's an opening
// <h1> tag but not a closing one. It also means
// we've consumed the entire stream of events, so
// chances are any parsing after this will just hit
// EOF and end early :(
warn!("[*] No closing <h1> tag in the SUMMARY.md file");
break;
}
}
}
// TODO: How do we deal with headings like "# My **awesome** summary"? // TODO: How do we deal with headings like "# My **awesome** summary"?
// for now, I'm just going to scan through and concatenate the // for now, I'm just going to scan through and concatenate the
// Event::Text tags, skipping any styling. // Event::Text tags, skipping any styling.
let title: String = tags.into_iter() Some(stringify_events(tags))
.filter_map(|t| match t {
Event::Text(text) => Some(text),
_ => None,
})
.collect();
Some(title)
} else { } else {
None None
} }
} }
/// Parse a single item (`[Some Chapter Name](./path/to/chapter.md)`).
fn parse_item(&mut self) -> Result<Link, Box<Error>> {
let next = self.stream.next();
if let Some(Event::Start(Tag::Link(dest, _))) = next {
let content = collect_events!(self.stream, Tag::Link(..));
Ok(Link {
name: stringify_events(content),
location: PathBuf::from(dest.to_string()),
nested_items: Vec::new(),
})
} else {
Err(Box::new(IoError::new(ErrorKind::Other, format!("Expected a link, got {:?}", next))))
}
}
}
/// Extract just the text from a bunch of events and concatenate it into a
/// single string.
fn stringify_events<'a>(events: Vec<Event<'a>>) -> String {
events.into_iter()
.filter_map(|t| match t {
Event::Text(text) => Some(text),
_ => None,
})
.collect()
} }
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>`. /// A section number like "1.2.3", basically just a newtype'd `Vec<u32>`.
@ -206,4 +258,20 @@ mod tests {
assert_eq!(got, should_be); 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"),
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);
}
} }