diff --git a/src/book/book.rs b/src/book/book.rs new file mode 100644 index 00000000..09914271 --- /dev/null +++ b/src/book/book.rs @@ -0,0 +1,392 @@ +use std::fmt::{self, Display, Formatter}; +use std::path::{Path, PathBuf}; +use std::collections::VecDeque; +use std::fs::File; +use std::io::{Read, Write}; + +use super::summary::{parse_summary, Summary, Link, SummaryItem, SectionNumber}; +use errors::*; + + +/// Load a book into memory from its `src/` directory. +pub fn load_book>(src_dir: P, create_if_not_present: bool) -> Result { + let src_dir = src_dir.as_ref(); + let summary_md = src_dir.join("SUMMARY.md"); + + let mut summary_content = String::new(); + File::open(summary_md) + .chain_err(|| "Couldn't open SUMMARY.md")? + .read_to_string(&mut summary_content)?; + + let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?; + + load_book_from_disk(&summary, src_dir, create_if_not_present) +} + + +/// A dumb tree structure representing a book. +/// +/// For the moment a book is just a collection of `BookItems`. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Book { + /// The sections in this book. + pub sections: Vec, +} + +impl Book { + /// Create an empty book. + pub fn new() -> Self { + Default::default() + } + + /// Get a depth-first iterator over the items in the book. + pub fn iter(&self) -> BookItems { + BookItems { items: self.sections.iter().collect() } + } +} + +/// Enum representing any type of item which can be added to a book. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum BookItem { + /// A nested chapter. + Chapter(Chapter), + /// A section separator. + Separator, +} + +/// The representation of a "chapter", usually mapping to a single file on +/// disk however it may contain multiple sub-chapters. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Chapter { + /// The chapter's name. + pub name: String, + /// The chapter's contents. + pub content: String, + /// The chapter's section number, if it has one. + pub number: Option, + /// Nested items. + pub sub_items: Vec, + /// The chapter's location, relative to the `SUMMARY.md` file. + pub path: PathBuf, +} + +impl Chapter { + /// Create a new chapter with the provided content. + pub fn new>(name: &str, content: String, path: P) -> Chapter { + Chapter { + name: name.to_string(), + content: content, + path: path.into(), + ..Default::default() + } + } +} + +/// Use the provided `Summary` to load a `Book` from disk. +/// +/// You need to pass in the book's source directory because all the links in +/// `SUMMARY.md` give the chapter locations relative to it. +fn load_book_from_disk>(summary: &Summary, src_dir: P, create_if_not_present: bool) -> Result { + debug!("[*] Loading the book from disk"); + let src_dir = src_dir.as_ref(); + + let prefix = summary.prefix_chapters.iter(); + let numbered = summary.numbered_chapters.iter(); + let suffix = summary.suffix_chapters.iter(); + + let summary_items = prefix.chain(numbered).chain(suffix); + + let mut chapters = Vec::new(); + + for summary_item in summary_items { + let chapter = load_summary_item(summary_item, src_dir, create_if_not_present)?; + chapters.push(chapter); + } + + Ok(Book { sections: chapters }) +} + +fn load_summary_item>(item: &SummaryItem, src_dir: P, create_if_not_present: bool) -> Result { + match *item { + SummaryItem::Separator => Ok(BookItem::Separator), + SummaryItem::Link(ref link) => { + let file = src_dir.as_ref().join(&link.location); + + if create_if_not_present && !file.exists() { + let text = format!("# {}", link.name); + File::create(&file)?.write_all(text.as_bytes())?; + } + + load_chapter(link, src_dir).map(|c| BookItem::Chapter(c)) + }, + } +} + +fn load_chapter>(link: &Link, src_dir: P) -> Result { + debug!("[*] Loading {} ({})", link.name, link.location.display()); + let src_dir = src_dir.as_ref(); + + let location = if link.location.is_absolute() { + link.location.clone() + } else { + src_dir.join(&link.location) + }; + + let mut f = File::open(&location).chain_err(|| { + format!("Chapter file not found, {}", link.location.display()) + })?; + + let mut content = String::new(); + f.read_to_string(&mut content)?; + + let stripped = location.strip_prefix(&src_dir).expect("Chapters are always inside a book"); + + let mut ch = Chapter::new(&link.name, content, stripped); + ch.number = link.number.clone(); + + let sub_items = link.nested_items + .iter() + .map(|i| load_summary_item(i, src_dir, false)) + .collect::>>()?; + + ch.sub_items = sub_items; + + Ok(ch) +} + +/// A depth-first iterator over the items in a book. +/// +/// # Note +/// +/// This struct shouldn't be created directly, instead prefer the +/// [`Book::iter()`] method. +/// +/// [`Book::iter()`]: struct.Book.html#method.iter +pub struct BookItems<'a> { + items: VecDeque<&'a BookItem>, +} + +impl<'a> Iterator for BookItems<'a> { + type Item = &'a BookItem; + + fn next(&mut self) -> Option { + let item = self.items.pop_front(); + + if let Some(&BookItem::Chapter(ref ch)) = item { + // if we wanted a breadth-first iterator we'd `extend()` here + for sub_item in ch.sub_items.iter().rev() { + self.items.push_front(sub_item); + } + } + + item + } +} + +impl Display for Chapter { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if let Some(ref section_number) = self.number { + write!(f, "{} ", section_number)?; + } + + write!(f, "{}", self.name) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use tempdir::TempDir; + use std::io::Write; + use std::fs; + + const DUMMY_SRC: &'static str = " +# Dummy Chapter + +this is some dummy text. + +And here is some more text. +"; + + /// Create a dummy `Link` in a temporary directory. + fn dummy_link() -> (Link, TempDir) { + let temp = TempDir::new("book").unwrap(); + + let chapter_path = temp.path().join("chapter_1.md"); + File::create(&chapter_path) + .unwrap() + .write(DUMMY_SRC.as_bytes()) + .unwrap(); + + let link = Link::new("Chapter 1", chapter_path); + + (link, temp) + } + + /// Create a nested `Link` written to a temporary directory. + fn nested_links() -> (Link, TempDir) { + let (mut root, temp_dir) = dummy_link(); + + let second_path = temp_dir.path().join("second.md"); + + File::create(&second_path) + .unwrap() + .write_all("Hello World!".as_bytes()) + .unwrap(); + + + let mut second = Link::new("Nested Chapter 1", &second_path); + second.number = Some(SectionNumber(vec![1, 2])); + + root.nested_items.push(second.clone().into()); + root.nested_items.push(SummaryItem::Separator); + root.nested_items.push(second.clone().into()); + + (root, temp_dir) + } + + #[test] + fn load_a_single_chapter_from_disk() { + let (link, temp_dir) = dummy_link(); + let should_be = Chapter::new("Chapter 1", DUMMY_SRC.to_string(), "chapter_1.md"); + + let got = load_chapter(&link, temp_dir.path()).unwrap(); + assert_eq!(got, should_be); + } + + #[test] + fn cant_load_a_nonexistent_chapter() { + let link = Link::new("Chapter 1", "/foo/bar/baz.md"); + + let got = load_chapter(&link, ""); + assert!(got.is_err()); + } + + #[test] + fn load_recursive_link_with_separators() { + let (root, temp) = nested_links(); + + let nested = Chapter { + name: String::from("Nested Chapter 1"), + content: String::from("Hello World!"), + number: Some(SectionNumber(vec![1, 2])), + path: PathBuf::from("second.md"), + sub_items: Vec::new(), + }; + let should_be = BookItem::Chapter(Chapter { + name: String::from("Chapter 1"), + content: String::from(DUMMY_SRC), + number: None, + path: PathBuf::from("chapter_1.md"), + sub_items: vec![ + BookItem::Chapter(nested.clone()), + BookItem::Separator, + BookItem::Chapter(nested.clone()), + ], + }); + + let got = load_summary_item(&SummaryItem::Link(root), temp.path(), false).unwrap(); + assert_eq!(got, should_be); + } + + #[test] + fn load_a_book_with_a_single_chapter() { + let (link, temp) = dummy_link(); + let summary = Summary { + numbered_chapters: vec![SummaryItem::Link(link)], + ..Default::default() + }; + let should_be = Book { + sections: vec![ + BookItem::Chapter(Chapter { + name: String::from("Chapter 1"), + content: String::from(DUMMY_SRC), + path: PathBuf::from("chapter_1.md"), + ..Default::default() + }), + ], + }; + + let got = load_book_from_disk(&summary, temp.path(), false).unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn book_iter_iterates_over_sequential_items() { + let book = Book { + sections: vec![ + BookItem::Chapter(Chapter { + name: String::from("Chapter 1"), + content: String::from(DUMMY_SRC), + ..Default::default() + }), + BookItem::Separator, + ], + }; + + let should_be: Vec<_> = book.sections.iter().collect(); + + let got: Vec<_> = book.iter().collect(); + + assert_eq!(got, should_be); + } + + #[test] + fn iterate_over_nested_book_items() { + let book = Book { + sections: vec![ + BookItem::Chapter(Chapter { + name: String::from("Chapter 1"), + content: String::from(DUMMY_SRC), + number: None, + path: PathBuf::from("Chapter_1/index.md"), + sub_items: vec![ + BookItem::Chapter(Chapter::new("Hello World", String::new(), "Chapter_1/hello.md")), + BookItem::Separator, + BookItem::Chapter(Chapter::new("Goodbye World", String::new(), "Chapter_1/goodbye.md")), + ], + }), + BookItem::Separator, + ], + }; + + + let got: Vec<_> = book.iter().collect(); + + assert_eq!(got.len(), 5); + + // checking the chapter names are in the order should be sufficient here... + let chapter_names: Vec = got.into_iter() + .filter_map(|i| match *i { + BookItem::Chapter(ref ch) => Some(ch.name.clone()), + _ => None, + }) + .collect(); + let should_be: Vec<_> = vec![ + String::from("Chapter 1"), + String::from("Hello World"), + String::from("Goodbye World"), + ]; + + assert_eq!(chapter_names, should_be); + } + + #[test] + fn create_missing_book_items() { + let (link, temp) = dummy_link(); + let summary = Summary { + numbered_chapters: vec![SummaryItem::Link(link)], + ..Default::default() + }; + + let chapter_1 = temp.path().join("chapter_1.md"); + fs::remove_file(&chapter_1).unwrap(); + assert!(!chapter_1.exists()); + + load_book_from_disk(&summary, temp.path(), true).unwrap(); + + assert!(chapter_1.exists()); + } +} diff --git a/src/book/bookitem.rs b/src/book/bookitem.rs deleted file mode 100644 index a2ec2cb0..00000000 --- a/src/book/bookitem.rs +++ /dev/null @@ -1,86 +0,0 @@ -use serde::{Serialize, Serializer}; -use serde::ser::SerializeStruct; -use std::path::PathBuf; - - -#[derive(Debug, Clone)] -pub enum BookItem { - Chapter(String, Chapter), // String = section - Affix(Chapter), - Spacer, -} - -#[derive(Debug, Clone)] -pub struct Chapter { - pub name: String, - pub path: PathBuf, - pub sub_items: Vec, -} - -#[derive(Debug, Clone)] -pub struct BookItems<'a> { - pub items: &'a [BookItem], - pub current_index: usize, - pub stack: Vec<(&'a [BookItem], usize)>, -} - - -impl Chapter { - pub fn new(name: String, path: PathBuf) -> Self { - Chapter { - name: name, - path: path, - sub_items: vec![], - } - } -} - - -impl Serialize for Chapter { - fn serialize(&self, serializer: S) -> ::std::result::Result - where - S: Serializer, - { - let mut struct_ = serializer.serialize_struct("Chapter", 2)?; - struct_.serialize_field("name", &self.name)?; - struct_.serialize_field("path", &self.path)?; - struct_.end() - } -} - - - -// Shamelessly copied from Rustbook -// (https://github.com/rust-lang/rust/blob/master/src/rustbook/book.rs) -impl<'a> Iterator for BookItems<'a> { - type Item = &'a BookItem; - - fn next(&mut self) -> Option<&'a BookItem> { - loop { - if self.current_index >= self.items.len() { - match self.stack.pop() { - None => return None, - Some((parent_items, parent_idx)) => { - self.items = parent_items; - self.current_index = parent_idx + 1; - } - } - } else { - let cur = &self.items[self.current_index]; - - match *cur { - BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => { - self.stack.push((self.items, self.current_index)); - self.items = &ch.sub_items[..]; - self.current_index = 0; - } - BookItem::Spacer => { - self.current_index += 1; - } - } - - return Some(cur); - } - } - } -} diff --git a/src/book/summary.rs b/src/book/summary.rs new file mode 100644 index 00000000..46f477e5 --- /dev/null +++ b/src/book/summary.rs @@ -0,0 +1,718 @@ +use std::fmt::{self, Formatter, Display}; +use std::ops::{Deref, DerefMut}; +use std::path::{Path, PathBuf}; +use pulldown_cmark::{self, Event, Tag}; +use errors::*; + + +/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be +/// used when loading a book from disk. +/// +/// # Summary Format +/// +/// **Title:** It's common practice to begin with a title, generally +/// "# Summary". But it is not mandatory, the parser just ignores it. So you +/// can too if you feel like it. +/// +/// **Prefix Chapter:** Before the main numbered chapters you can add a couple +/// of elements that will not be numbered. This is useful for forewords, +/// introductions, etc. There are however some constraints. You can not nest +/// prefix chapters, they should all be on the root level. And you can not add +/// prefix chapters once you have added numbered chapters. +/// +/// ```markdown +/// [Title of prefix element](relative/path/to/markdown.md) +/// ``` +/// +/// **Numbered Chapter:** Numbered chapters are the main content of the book, +/// they +/// will be numbered and can be nested, resulting in a nice hierarchy (chapters, +/// sub-chapters, etc.) +/// +/// ```markdown +/// - [Title of the Chapter](relative/path/to/markdown.md) +/// ``` +/// +/// You can either use - or * to indicate a numbered chapter. +/// +/// **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 +/// the numbered chapters instead of before. +/// +/// All other elements are unsupported and will be ignored at best or result in +/// an error. +pub fn parse_summary(summary: &str) -> Result { + let parser = SummaryParser::new(summary); + parser.parse() +} + +/// The parsed `SUMMARY.md`, specifying how the book should be laid out. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Summary { + /// An optional title for the `SUMMARY.md`, currently just ignored. + pub title: Option, + /// Chapters before the main text (e.g. an introduction). + pub prefix_chapters: Vec, + /// The main chapters in the document. + pub numbered_chapters: Vec, + /// Items which come after the main document (e.g. a conclusion). + pub suffix_chapters: Vec, +} + +/// 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, PartialEq, Serialize, Deserialize)] +pub struct Link { + /// The name of the chapter. + pub name: String, + /// The location of the chapter's source file, taking the book's `src` + /// directory as the root. + pub location: PathBuf, + /// The section number, if this chapter is in the numbered section. + pub number: Option, + /// Any nested items this chapter may contain. + pub nested_items: Vec, +} + +impl Link { + /// Create a new link with no nested items. + pub fn new, P: AsRef>(name: S, location: P) -> Link { + Link { + name: name.into(), + location: location.as_ref().to_path_buf(), + number: None, + nested_items: Vec::new(), + } + } +} + +impl Default for Link { + fn default() -> Self { + Link { + name: String::new(), + location: PathBuf::new(), + number: None, + nested_items: Vec::new(), + } + } +} + +/// An item in `SUMMARY.md` which could be either a separator or a `Link`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum SummaryItem { + /// A link to a chapter. + Link(Link), + /// A separator (`---`). + Separator, +} + +impl SummaryItem { + fn maybe_link_mut(&mut self) -> Option<&mut Link> { + match *self { + SummaryItem::Link(ref mut l) => Some(l), + _ => None, + } + } +} + +impl From for SummaryItem { + fn from(other: Link) -> SummaryItem { + SummaryItem::Link(other) + } +} + +#[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. +/// +/// +/// # Grammar +/// +/// The `SUMMARY.md` file has a grammar which looks something like this: +/// +/// ```text +/// summary ::= title prefix_chapters numbered_chapters +/// suffix_chapters +/// title ::= "# " TEXT +/// | EPSILON +/// prefix_chapters ::= item* +/// suffix_chapters ::= item* +/// numbered_chapters ::= dotted_item+ +/// dotted_item ::= INDENT* DOT_POINT item +/// item ::= link +/// | separator +/// separator ::= "---" +/// link ::= "[" TEXT "]" "(" TEXT ")" +/// DOT_POINT ::= "-" +/// | "*" +/// ``` +/// +/// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly) +/// > match the following regex: "[^<>\n[]]+". +struct SummaryParser<'a> { + stream: pulldown_cmark::Parser<'a>, + summary: Summary, + state: State, +} + +/// 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> { + fn new(text: &str) -> SummaryParser { + let pulldown_parser = pulldown_cmark::Parser::new(text); + let intermediate_summary = Summary::default(); + + SummaryParser { + stream: pulldown_parser, + summary: intermediate_summary, + state: State::Begin, + } + } + + /// Parse the text the `SummaryParser` was created with. + fn parse(mut self) -> Result { + self.summary.title = self.parse_title(); + + if let Some(ref title) = self.summary.title { + debug!("[*] Title is {:?}", title); + } + + while self.state != State::End { + self.step()?; + } + + 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 { + 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); + } + } + }, + other => { + trace!("[*] skipping unexpected token: {:?}", other); + }, + } + + Ok(()) + } + + /// Parse a single item (`[Some Chapter Name](./path/to/chapter.md)`). + fn parse_item(&mut self) -> Result { + let next = self.stream.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())) + } else { + bail!("Expected a link, got {:?}", next) + } + } + + /// Try to parse the title line. + fn parse_title(&mut self) -> Option { + 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)); + + // TODO: How do we deal with headings like "# My **awesome** summary"? + // for now, I'm just going to scan through and concatenate the + // Event::Text tags, skipping any styling. + Some(stringify_events(tags)) + } else { + 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() + .filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l))) + .rev() + .next() + .ok_or_else(|| "The list of SummaryItems doesn't contain any Links".into()) +} + + +/// Removes the styling from a list of Markdown events and returns just the +/// plain text. +fn stringify_events(events: Vec) -> String { + events + .into_iter() + .filter_map(|t| match t { + Event::Text(text) => Some(text.into_owned()), + _ => None, + }) + .collect() +} + +/// A section number like "1.2.3", basically just a newtype'd `Vec` with +/// a pretty `Display` impl. +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] +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)?; + } + Ok(()) + } +} + +impl Deref for SectionNumber { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SectionNumber { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn section_number_has_correct_dotted_representation() { + let inputs = vec![ + (vec![0], "0."), + (vec![1, 3], "1.3."), + (vec![1, 2, 3], "1.2.3."), + ]; + + for (input, should_be) in inputs { + let section_number = SectionNumber(input).to_string(); + assert_eq!(section_number, should_be); + } + } + + #[test] + fn parse_initial_title() { + let src = "# Summary"; + let should_be = String::from("Summary"); + + let mut parser = SummaryParser::new(src); + let got = parser.parse_title().unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn parse_title_with_styling() { + let src = "# My **Awesome** Summary"; + let should_be = String::from("My Awesome Summary"); + + let mut parser = SummaryParser::new(src); + let got = parser.parse_title().unwrap(); + + 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)"; + let should_be = "Hello World, this is some text and a link"; + + let events = pulldown_cmark::Parser::new(src).collect(); + let got = stringify_events(events); + + assert_eq!(got, should_be); + + } + + #[test] + fn can_step_past_first_token() { + let src = "hello world"; + let should_be = State::PrefixChapters; + + let mut parser = SummaryParser::new(src); + assert_eq!(parser.state, State::Begin); + parser.step().unwrap(); + 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); + } + + #[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]); + } + + #[test] + fn parse_a_numbered_chapter() { + let src = "- [First](./second)"; + let mut parser = SummaryParser::new(src); + let _ = parser.stream.next(); + + assert_eq!(parser.summary.numbered_chapters.len(), 0); + + parser.state = State::NumberedChapters(0); + parser.step().unwrap(); + + assert_eq!(parser.summary.numbered_chapters.len(), 1); + } +} diff --git a/src/lib.rs b/src/lib.rs index 3991ae95..647f5b24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,13 +25,13 @@ //! //! fn main() { //! let mut md = MDBook::new("my-book"); -//! +//! //! // tweak the book configuration a bit //! md.config.book.src = PathBuf::from("source"); //! md.config.build.build_dir = PathBuf::from("book"); //! //! // Render the book -//! md.build().unwrap(); +//! md.build().unwrap(); //! } //! ``` //! @@ -89,7 +89,6 @@ extern crate serde_json; extern crate tempdir; extern crate toml; -mod parse; mod preprocess; pub mod book; pub mod config; diff --git a/src/parse/mod.rs b/src/parse/mod.rs deleted file mode 100644 index c8c8aab7..00000000 --- a/src/parse/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use self::summary::construct_bookitems; - -pub mod summary; diff --git a/src/parse/summary.rs b/src/parse/summary.rs deleted file mode 100644 index 1193b6f5..00000000 --- a/src/parse/summary.rs +++ /dev/null @@ -1,239 +0,0 @@ -use std::path::PathBuf; -use std::fs::File; -use std::io::{Error, ErrorKind, Read, Result}; -use book::bookitem::{BookItem, Chapter}; - -pub fn construct_bookitems(path: &PathBuf) -> Result> { - debug!("[fn]: construct_bookitems"); - let mut summary = String::new(); - File::open(path)?.read_to_string(&mut summary)?; - - debug!("[*]: Parse SUMMARY.md"); - let top_items = parse_level(&mut summary.split('\n').collect(), 0, vec![0])?; - debug!("[*]: Done parsing SUMMARY.md"); - Ok(top_items) -} - -fn parse_level(summary: &mut Vec<&str>, - current_level: i32, - mut section: Vec) - -> Result> { - debug!("[fn]: parse_level"); - let mut items: Vec = vec![]; - - // Construct the book recursively - while !summary.is_empty() { - let item: BookItem; - // Indentation level of the line to parse - let level = level(summary[0], 4)?; - - // if level < current_level we remove the last digit of section, - // exit the current function, - // and return the parsed level to the calling function. - if level < current_level { - break; - } - - // if level > current_level we call ourselves to go one level deeper - if level > current_level { - // Level can not be root level !! - // Add a sub-number to section - section.push(0); - let last = items.pop().expect( - "There should be at least one item since this can't be the root level", - ); - - if let BookItem::Chapter(ref s, ref ch) = last { - let mut ch = ch.clone(); - ch.sub_items = parse_level(summary, level, section.clone())?; - items.push(BookItem::Chapter(s.clone(), ch)); - - // Remove the last number from the section, because we got back to our level.. - section.pop(); - continue; - } else { - return Err(Error::new( - ErrorKind::Other, - "Your summary.md is messed up\n\n - Prefix, \ - Suffix and Spacer elements can only exist \ - on the root level.\n - \ - Prefix elements can only exist before \ - any chapter and there can be \ - no chapters after suffix elements.", - )); - }; - } else { - // level and current_level are the same, parse the line - item = if let Some(parsed_item) = parse_line(summary[0]) { - // Eliminate possible errors and set section to -1 after suffix - match parsed_item { - // error if level != 0 and BookItem is != Chapter - BookItem::Affix(_) | BookItem::Spacer if level > 0 => { - return Err(Error::new( - ErrorKind::Other, - "Your summary.md is messed up\n\n - \ - Prefix, Suffix and Spacer elements \ - can only exist on the root level.\n - Prefix \ - elements can only exist before any chapter and \ - there can be no chapters after suffix elements.", - )) - } - - // error if BookItem == Chapter and section == -1 - BookItem::Chapter(_, _) if section[0] == -1 => { - return Err(Error::new( - ErrorKind::Other, - "Your summary.md is messed up\n\n - \ - Prefix, Suffix and Spacer elements can only \ - exist on the root level.\n - Prefix \ - elements can only exist before any chapter and \ - there can be no chapters after suffix elements.", - )) - } - - // Set section = -1 after suffix - BookItem::Affix(_) if section[0] > 0 => { - section[0] = -1; - } - - _ => {} - } - - match parsed_item { - BookItem::Chapter(_, ch) => { - // Increment section - let len = section.len() - 1; - section[len] += 1; - let s = section.iter() - .fold("".to_owned(), |s, i| s + &i.to_string() + "."); - BookItem::Chapter(s, ch) - } - _ => parsed_item, - } - } else { - // If parse_line does not return Some(_) continue... - summary.remove(0); - continue; - }; - } - - summary.remove(0); - items.push(item) - } - debug!("[*]: Level: {:?}", items); - Ok(items) -} - - -fn level(line: &str, spaces_in_tab: i32) -> Result { - debug!("[fn]: level"); - let mut spaces = 0; - let mut level = 0; - - for ch in line.chars() { - match ch { - ' ' => spaces += 1, - '\t' => level += 1, - _ => break, - } - if spaces >= spaces_in_tab { - level += 1; - spaces = 0; - } - } - - // If there are spaces left, there is an indentation error - if spaces > 0 { - debug!("[SUMMARY.md]:"); - debug!("\t[line]: {}", line); - debug!("[*]: There is an indentation error on this line. Indentation should be {} spaces", - spaces_in_tab); - return Err(Error::new(ErrorKind::Other, - format!("Indentation error on line:\n\n{}", line))); - } - - Ok(level) -} - - -fn parse_line(l: &str) -> Option { - debug!("[fn]: parse_line"); - - // Remove leading and trailing spaces or tabs - let line = l.trim_matches(|c: char| c == ' ' || c == '\t'); - - // Spacers are "------" - if line.starts_with("--") { - debug!("[*]: Line is spacer"); - return Some(BookItem::Spacer); - } - - if let Some(c) = line.chars().nth(0) { - match c { - // List item - '-' | '*' => { - debug!("[*]: Line is list element"); - - if let Some((name, path)) = read_link(line) { - return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path))); - } else { - return None; - } - } - // Non-list element - '[' => { - debug!("[*]: Line is a link element"); - - if let Some((name, path)) = read_link(line) { - return Some(BookItem::Affix(Chapter::new(name, path))); - } else { - return None; - } - } - _ => {} - } - } - - None -} - -fn read_link(line: &str) -> Option<(String, PathBuf)> { - let mut start_delimitor; - let mut end_delimitor; - - // In the future, support for list item that is not a link - // Not sure if I should error on line I can't parse or just ignore them... - if let Some(i) = line.find('[') { - start_delimitor = i; - } else { - debug!("[*]: '[' not found, this line is not a link. Ignoring..."); - return None; - } - - if let Some(i) = line[start_delimitor..].find("](") { - end_delimitor = start_delimitor + i; - } else { - debug!("[*]: '](' not found, this line is not a link. Ignoring..."); - return None; - } - - let name = line[start_delimitor + 1..end_delimitor].to_owned(); - - start_delimitor = end_delimitor + 1; - if let Some(i) = line[start_delimitor..].find(')') { - end_delimitor = start_delimitor + i; - } else { - debug!("[*]: ')' not found, this line is not a link. Ignoring..."); - return None; - } - - let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned()); - - Some((name, path)) -}