From 4c6c696c87b0c0ac1e99b27e94c50189bb6e83f2 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 18 Nov 2017 19:50:47 +0800 Subject: [PATCH 01/20] Copied across the summary parser and Book structure (doesn't compile) --- src/book/book.rs | 392 +++++++++++++++++++++++ src/book/bookitem.rs | 86 ------ src/book/summary.rs | 718 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 +- src/parse/mod.rs | 3 - src/parse/summary.rs | 239 -------------- 6 files changed, 1112 insertions(+), 331 deletions(-) create mode 100644 src/book/book.rs delete mode 100644 src/book/bookitem.rs create mode 100644 src/book/summary.rs delete mode 100644 src/parse/mod.rs delete mode 100644 src/parse/summary.rs 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)) -} From cafb8b75e7778e58a9688a83a269686869ff2b99 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 18 Nov 2017 20:01:50 +0800 Subject: [PATCH 02/20] The library not compiles (probably completely broken) --- src/book/book.rs | 53 +--- src/book/mod.rs | 316 ++++++++++--------- src/renderer/html_handlebars/hbs_renderer.rs | 29 +- 3 files changed, 187 insertions(+), 211 deletions(-) diff --git a/src/book/book.rs b/src/book/book.rs index 09914271..0824bd06 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -4,12 +4,12 @@ use std::collections::VecDeque; use std::fs::File; use std::io::{Read, Write}; -use super::summary::{parse_summary, Summary, Link, SummaryItem, SectionNumber}; +use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; use errors::*; /// Load a book into memory from its `src/` directory. -pub fn load_book>(src_dir: P, create_if_not_present: bool) -> Result { +pub fn load_book>(src_dir: P) -> Result { let src_dir = src_dir.as_ref(); let summary_md = src_dir.join("SUMMARY.md"); @@ -20,7 +20,7 @@ pub fn load_book>(src_dir: P, create_if_not_present: bool) -> Res let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?; - load_book_from_disk(&summary, src_dir, create_if_not_present) + load_book_from_disk(&summary, src_dir) } @@ -41,7 +41,9 @@ impl Book { /// Get a depth-first iterator over the items in the book. pub fn iter(&self) -> BookItems { - BookItems { items: self.sections.iter().collect() } + BookItems { + items: self.sections.iter().collect(), + } } } @@ -86,7 +88,7 @@ impl Chapter { /// /// 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 { +fn load_book_from_disk>(summary: &Summary, src_dir: P) -> Result { debug!("[*] Loading the book from disk"); let src_dir = src_dir.as_ref(); @@ -99,24 +101,18 @@ fn load_book_from_disk>(summary: &Summary, src_dir: P, create_if_ let mut chapters = Vec::new(); for summary_item in summary_items { - let chapter = load_summary_item(summary_item, src_dir, create_if_not_present)?; + let chapter = load_summary_item(summary_item, src_dir)?; chapters.push(chapter); } Ok(Book { sections: chapters }) } -fn load_summary_item>(item: &SummaryItem, src_dir: P, create_if_not_present: bool) -> Result { +fn load_summary_item>(item: &SummaryItem, src_dir: P) -> 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)) }, } @@ -132,21 +128,21 @@ fn load_chapter>(link: &Link, src_dir: P) -> Result { src_dir.join(&link.location) }; - let mut f = File::open(&location).chain_err(|| { - format!("Chapter file not found, {}", link.location.display()) - })?; + 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 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)) + .map(|i| load_summary_item(i, src_dir)) .collect::>>()?; ch.sub_items = sub_items; @@ -286,7 +282,7 @@ And here is some more text. ], }); - let got = load_summary_item(&SummaryItem::Link(root), temp.path(), false).unwrap(); + let got = load_summary_item(&SummaryItem::Link(root), temp.path()).unwrap(); assert_eq!(got, should_be); } @@ -308,7 +304,7 @@ And here is some more text. ], }; - let got = load_book_from_disk(&summary, temp.path(), false).unwrap(); + let got = load_book_from_disk(&summary, temp.path()).unwrap(); assert_eq!(got, should_be); } @@ -372,21 +368,4 @@ And here is some more text. 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/mod.rs b/src/book/mod.rs index 05d45d9c..a7db403e 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -1,6 +1,7 @@ -pub mod bookitem; +mod summary; +mod book; -pub use self::bookitem::{BookItem, BookItems}; +pub use self::book::{Book, BookItem, BookItems, Chapter}; use std::path::{Path, PathBuf}; use std::fs::{self, File}; @@ -8,7 +9,7 @@ use std::io::Write; use std::process::Command; use tempdir::TempDir; -use {parse, theme, utils}; +use {theme, utils}; use renderer::{HtmlHandlebars, Renderer}; use preprocess; use errors::*; @@ -19,57 +20,81 @@ pub struct MDBook { pub root: PathBuf, pub config: Config, - pub content: Vec, + book: Book, renderer: Box, pub livereload: Option, } impl MDBook { - /// Create a new `MDBook` struct with root directory `root` - /// - /// # Examples - /// - /// ```no_run - /// # extern crate mdbook; - /// # use mdbook::MDBook; - /// # #[allow(unused_variables)] - /// # fn main() { - /// let book = MDBook::new("root_dir"); - /// # } - /// ``` - /// - /// In this example, `root_dir` will be the root directory of our book - /// and is specified in function of the current working directory - /// by using a relative path instead of an - /// absolute path. - /// - /// Default directory paths: - /// - /// - source: `root/src` - /// - output: `root/book` - /// - theme: `root/theme` - /// - /// They can both be changed by using [`set_src()`](#method.set_src) and - /// [`set_dest()`](#method.set_dest) + /// Load a book from its root directory on disk. + pub fn load>(book_root: P) -> Result { + let book_root = book_root.into(); + let config_location = book_root.join("book.toml"); - pub fn new>(root: P) -> MDBook { - let root = root.into(); - if !root.exists() || !root.is_dir() { - warn!("{:?} No directory with that name", root); - } + let config = if config_location.exists() { + Config::from_disk(&config_location)? + } else { + Config::default() + }; - MDBook { - root: root, - config: Config::default(), + let src_dir = book_root.join(&config.book.src); + let book = book::load_book(&src_dir)?; - content: vec![], + let md = MDBook { + root: book_root, + config: config, + book: book, renderer: Box::new(HtmlHandlebars::new()), - livereload: None, } } + // /// Create a new `MDBook` struct with root directory `root` + // /// + // /// # Examples + // /// + // /// ```no_run + // /// # extern crate mdbook; + // /// # use mdbook::MDBook; + // /// # #[allow(unused_variables)] + // /// # fn main() { + // /// let book = MDBook::new("root_dir"); + // /// # } + // /// ``` + // /// + // /// In this example, `root_dir` will be the root directory of our book + // /// and is specified in function of the current working directory + // /// by using a relative path instead of an + // /// absolute path. + // /// + // /// Default directory paths: + // /// + // /// - source: `root/src` + // /// - output: `root/book` + // /// - theme: `root/theme` + // /// + // /// They can both be changed by using [`set_src()`](#method.set_src) and + // /// [`set_dest()`](#method.set_dest) + + // pub fn new>(root: P) -> MDBook { + // let root = root.into(); + // if !root.exists() || !root.is_dir() { + // warn!("{:?} No directory with that name", root); + // } + + // MDBook { + // root: root, + // config: Config::default(), + + // content: vec![], + // renderer: Box::new(HtmlHandlebars::new()), + + // livereload: None, + // create_missing: true, + // } + // } + /// Returns a flat depth-first iterator over the elements of the book, /// it returns an [BookItem enum](bookitem.html): /// `(section: String, bookitem: &BookItem)` @@ -77,15 +102,14 @@ impl MDBook { /// ```no_run /// # extern crate mdbook; /// # use mdbook::MDBook; - /// # use mdbook::BookItem; + /// # use mdbook::book::BookItem; /// # #[allow(unused_variables)] /// # fn main() { /// # let book = MDBook::new("mybook"); /// for item in book.iter() { - /// match item { - /// &BookItem::Chapter(ref section, ref chapter) => {}, - /// &BookItem::Affix(ref chapter) => {}, - /// &BookItem::Spacer => {}, + /// match *item { + /// BookItem::Chapter(ref chapter) => {}, + /// BookItem::Spacer => {}, /// } /// } /// @@ -100,11 +124,7 @@ impl MDBook { /// ``` pub fn iter(&self) -> BookItems { - BookItems { - items: &self.content[..], - current_index: 0, - stack: Vec::new(), - } + self.book.iter() } /// `init()` creates some boilerplate files and directories @@ -122,104 +142,106 @@ impl MDBook { /// and adds a `SUMMARY.md` and a /// `chapter_1.md` to the source directory. - pub fn init(&mut self) -> Result<()> { - debug!("[fn]: init"); + pub fn init>(book_root: P) -> Result { + unimplemented!() + // debug!("[fn]: init"); - if !self.root.exists() { - fs::create_dir_all(&self.root).unwrap(); - info!("{:?} created", self.root.display()); - } + // if !self.root.exists() { + // fs::create_dir_all(&self.root).unwrap(); + // info!("{:?} created", self.root.display()); + // } - { - let dest = self.get_destination(); - if !dest.exists() { - debug!("[*]: {} does not exist, trying to create directory", dest.display()); - fs::create_dir_all(dest)?; - } + // { + // let dest = self.get_destination(); + // if !dest.exists() { + // debug!("[*]: {} does not exist, trying to create directory", + // dest.display()); fs::create_dir_all(dest)?; + // } - let src = self.get_source(); - if !src.exists() { - debug!("[*]: {} does not exist, trying to create directory", src.display()); - fs::create_dir_all(&src)?; - } + // let src = self.get_source(); + // if !src.exists() { + // debug!("[*]: {} does not exist, trying to create directory", + // src.display()); fs::create_dir_all(&src)?; + // } - let summary = src.join("SUMMARY.md"); + // let summary = src.join("SUMMARY.md"); - if !summary.exists() { - // Summary does not exist, create it - debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", - &summary); - let mut f = File::create(&summary)?; + // if !summary.exists() { + // // Summary does not exist, create it + // debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", + // &summary); let mut f = File::create(&summary)?; - debug!("[*]: Writing to SUMMARY.md"); + // debug!("[*]: Writing to SUMMARY.md"); - writeln!(f, "# Summary")?; - writeln!(f, "")?; - writeln!(f, "- [Chapter 1](./chapter_1.md)")?; - } - } + // writeln!(f, "# Summary")?; + // writeln!(f, "")?; + // writeln!(f, "- [Chapter 1](./chapter_1.md)")?; + // } + // } - // parse SUMMARY.md, and create the missing item related file - self.parse_summary()?; + // // parse SUMMARY.md, and create the missing item related file + // self.parse_summary()?; - debug!("[*]: constructing paths for missing files"); - for item in self.iter() { - debug!("[*]: item: {:?}", item); - let ch = match *item { - BookItem::Spacer => continue, - BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => ch, - }; - if !ch.path.as_os_str().is_empty() { - let path = self.get_source().join(&ch.path); + // debug!("[*]: constructing paths for missing files"); + // for item in self.iter() { + // debug!("[*]: item: {:?}", item); + // let ch = match *item { + // BookItem::Spacer => continue, + // BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => ch, + // }; + // if !ch.path.as_os_str().is_empty() { + // let path = self.get_source().join(&ch.path); - if !path.exists() { - if !self.config.build.create_missing { - return Err( - format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy()).into(), - ); - } - debug!("[*]: {:?} does not exist, trying to create file", path); - ::std::fs::create_dir_all(path.parent().unwrap())?; - let mut f = File::create(path)?; + // if !path.exists() { + // if !self.create_missing { + // return Err( + // format!("'{}' referenced from SUMMARY.md does not + // exist.", path.to_string_lossy()).into(), ); + // } + // debug!("[*]: {:?} does not exist, trying to create file", path); + // ::std::fs::create_dir_all(path.parent().unwrap())?; + // let mut f = File::create(path)?; - // debug!("[*]: Writing to {:?}", path); - writeln!(f, "# {}", ch.name)?; - } - } - } + // // debug!("[*]: Writing to {:?}", path); + // writeln!(f, "# {}", ch.name)?; + // } + // } + // } - debug!("[*]: init done"); - Ok(()) + // debug!("[*]: init done"); + // Ok(()) } - pub fn create_gitignore(&self) { - let gitignore = self.get_gitignore(); + // pub fn create_gitignore(&self) { + // let gitignore = self.get_gitignore(); - let destination = self.get_destination(); + // let destination = self.get_destination(); - // Check that the gitignore does not extist and that the destination path - // begins with the root path - // We assume tha if it does begin with the root path it is contained within. - // This assumption - // will not hold true for paths containing double dots to go back up e.g. - // `root/../destination` - if !gitignore.exists() && destination.starts_with(&self.root) { - let relative = destination - .strip_prefix(&self.root) - .expect("Could not strip the root prefix, path is not relative to root") - .to_str() - .expect("Could not convert to &str"); + // // Check that the gitignore does not extist and that the destination path + // // begins with the root path + // // We assume tha if it does begin with the root path it is contained + // within. // This assumption + // // will not hold true for paths containing double dots to go back up e.g. + // // `root/../destination` + // if !gitignore.exists() && destination.starts_with(&self.root) { + // let relative = destination + // .strip_prefix(&self.root) + // .expect("Could not strip the root prefix, path is not relative + // to root") .to_str() + // .expect("Could not convert to &str"); - debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore); + // debug!("[*]: {:?} does not exist, trying to create .gitignore", + // gitignore); - let mut f = File::create(&gitignore).expect("Could not create file."); + // let mut f = File::create(&gitignore).expect("Could not create + // file."); - debug!("[*]: Writing to .gitignore"); + // debug!("[*]: Writing to .gitignore"); - writeln!(f, "/{}", relative).expect("Could not write to file."); - } - } + // writeln!(f, "/{}", relative).expect("Could not write to file."); + // } + // } /// The `build()` method is the one where everything happens. /// First it parses `SUMMARY.md` to construct the book's structure @@ -230,8 +252,6 @@ impl MDBook { pub fn build(&mut self) -> Result<()> { debug!("[fn]: build"); - self.init()?; - // Clean output directory utils::fs::remove_dir_content(&self.get_destination())?; @@ -239,18 +259,13 @@ impl MDBook { } - pub fn get_gitignore(&self) -> PathBuf { - self.root.join(".gitignore") - } - pub fn copy_theme(&self) -> Result<()> { debug!("[fn]: copy_theme"); let themedir = self.theme_dir(); if !themedir.exists() { - debug!("[*]: {:?} does not exist, trying to create directory", - themedir); + debug!("[*]: {:?} does not exist, trying to create directory", themedir); fs::create_dir(&themedir)?; } @@ -288,8 +303,9 @@ impl MDBook { pub fn write_file>(&self, filename: P, content: &[u8]) -> Result<()> { let path = self.get_destination().join(filename); - utils::fs::create_file(&path)?.write_all(content) - .map_err(|e| e.into()) + utils::fs::create_file(&path)? + .write_all(content) + .map_err(|e| e.into()) } /// Parses the `book.json` file (if it exists) to extract @@ -340,8 +356,6 @@ impl MDBook { } pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { - // read in the chapters - self.parse_summary().chain_err(|| "Couldn't parse summary")?; let library_args: Vec<&str> = (0..library_paths.len()) .map(|_| "-L") .zip(library_paths.into_iter()) @@ -349,11 +363,11 @@ impl MDBook { .collect(); let temp_dir = TempDir::new("mdbook")?; for item in self.iter() { - if let BookItem::Chapter(_, ref ch) = *item { + if let BookItem::Chapter(ref ch) = *item { if !ch.path.as_os_str().is_empty() { let path = self.get_source().join(&ch.path); let base = path.parent() - .ok_or_else(|| String::from("Invalid bookitem path!"))?; + .ok_or_else(|| String::from("Invalid bookitem path!"))?; let content = utils::fs::file_to_string(&path)?; // Parse and expand links let content = preprocess::links::replace_all(&content, base)?; @@ -364,14 +378,14 @@ impl MDBook { let mut tmpf = utils::fs::create_file(&path)?; tmpf.write_all(content.as_bytes())?; - let output = Command::new("rustdoc").arg(&path) - .arg("--test") - .args(&library_args) - .output()?; + let output = Command::new("rustdoc") + .arg(&path) + .arg("--test") + .args(&library_args) + .output()?; if !output.status.success() { - bail!(ErrorKind::Subprocess("Rustdoc returned an error".to_string(), - output)); + bail!(ErrorKind::Subprocess("Rustdoc returned an error".to_string(), output)); } } } @@ -379,14 +393,6 @@ impl MDBook { Ok(()) } - // Construct book - fn parse_summary(&mut self) -> Result<()> { - // When append becomes stable, use self.content.append() ... - let summary = self.get_source().join("SUMMARY.md"); - self.content = parse::construct_bookitems(&summary)?; - Ok(()) - } - pub fn get_destination(&self) -> PathBuf { self.root.join(&self.config.build.build_dir) } diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 14e45c23..342aef76 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -2,7 +2,7 @@ use renderer::html_handlebars::helpers; use preprocess; use renderer::Renderer; use book::MDBook; -use book::bookitem::{BookItem, Chapter}; +use book::{BookItem, Chapter}; use config::{Config, Playpen, HtmlConfig}; use {utils, theme}; use theme::{Theme, playpen_editor}; @@ -35,13 +35,10 @@ impl HtmlHandlebars { -> Result<()> { // FIXME: This should be made DRY-er and rely less on mutable state match *item { - BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) - if !ch.path.as_os_str().is_empty() => + BookItem::Chapter(ref ch) => { - let path = ctx.book.get_source().join(&ch.path); - let content = utils::fs::file_to_string(&path)?; - let base = path.parent() - .ok_or_else(|| String::from("Invalid bookitem path!"))?; + let content = ch.content.clone(); + let base = ch.path.parent().expect("All chapters must have a parent directory"); // Parse and expand links let content = preprocess::links::replace_all(&content, base)?; @@ -397,7 +394,11 @@ fn make_data(book: &MDBook, config: &Config) -> Result { + BookItem::Chapter(ref ch) => { + if let Some(ref section) = ch.number { + chapter.insert("section".to_owned(), json!(section.to_string())); + } + chapter.insert("name".to_owned(), json!(ch.name)); let path = ch.path.to_str().ok_or_else(|| { io::Error::new(io::ErrorKind::Other, @@ -406,17 +407,7 @@ fn make_data(book: &MDBook, config: &Config) -> Result { - chapter.insert("section".to_owned(), json!(s)); - chapter.insert("name".to_owned(), json!(ch.name)); - let path = ch.path.to_str().ok_or_else(|| { - io::Error::new(io::ErrorKind::Other, - "Could not convert path \ - to str") - })?; - chapter.insert("path".to_owned(), json!(path)); - } - BookItem::Spacer => { + BookItem::Separator => { chapter.insert("spacer".to_owned(), json!("_spacer_")); } } From 47eb4788cb7294b7617c2972832d82c2b85232e8 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 18 Nov 2017 20:41:04 +0800 Subject: [PATCH 03/20] Introduced the `BookBuilder`. - You now use a `BookBuilder` for creating a book directory tree - This also removes the `--no-create` argument --- src/bin/build.rs | 21 ++-- src/bin/init.rs | 33 ++---- src/bin/serve.rs | 9 +- src/bin/test.rs | 2 +- src/bin/watch.rs | 2 +- src/book/init.rs | 156 ++++++++++++++++++++++++++++ src/book/mod.rs | 214 ++++----------------------------------- tests/init.rs | 29 +++--- tests/rendered_output.rs | 16 +-- tests/testing.rs | 4 +- 10 files changed, 225 insertions(+), 261 deletions(-) create mode 100644 src/book/init.rs diff --git a/src/bin/build.rs b/src/bin/build.rs index 54dffbd1..bc784ea9 100644 --- a/src/bin/build.rs +++ b/src/bin/build.rs @@ -1,5 +1,5 @@ use std::path::PathBuf; -use clap::{ArgMatches, SubCommand, App}; +use clap::{App, ArgMatches, SubCommand}; use mdbook::MDBook; use mdbook::errors::Result; use {get_book_dir, open}; @@ -10,32 +10,23 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { .about("Build the book from the markdown files") .arg_from_usage("-o, --open 'Open the compiled book in a web browser'") .arg_from_usage( - "-d, --dest-dir=[dest-dir] 'The output directory for your \ - book{n}(Defaults to ./book when omitted)'", - ) - .arg_from_usage( - "--no-create 'Will not create non-existent files linked from SUMMARY.md (deprecated: use book.toml instead)'", - ) - .arg_from_usage( - "[dir] 'A directory for your book{n}(Defaults to Current Directory \ + "-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book \ when omitted)'", ) + .arg_from_usage( + "[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'", + ) } // Build command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir).read_config()?; + let mut book = MDBook::load(&book_dir)?; if let Some(dest_dir) = args.value_of("dest-dir") { book.config.build.build_dir = PathBuf::from(dest_dir); } - // This flag is deprecated in favor of being set via `book.toml`. - if args.is_present("no-create") { - book.config.build.create_missing = false; - } - book.build()?; if args.is_present("open") { diff --git a/src/bin/init.rs b/src/bin/init.rs index 83ac54be..cbc08291 100644 --- a/src/bin/init.rs +++ b/src/bin/init.rs @@ -3,6 +3,7 @@ use std::io::Write; use clap::{App, ArgMatches, SubCommand}; use mdbook::MDBook; use mdbook::errors::Result; +use mdbook::config::Config; use get_book_dir; // Create clap subcommand arguments @@ -19,45 +20,31 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { // Init command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir); - - // Call the function that does the initialization - book.init()?; + let mut builder = MDBook::init(&book_dir); // If flag `--theme` is present, copy theme to src if args.is_present("theme") { // Skip this if `--force` is present if !args.is_present("force") { // Print warning - print!("\nCopying the default theme to {:?}", book.get_source()); - println!("could potentially overwrite files already present in that directory."); + print!("\nCopying the default theme to {}", builder.config().book.src.display()); + println!("This could potentially overwrite files already present in that directory."); print!("\nAre you sure you want to continue? (y/n) "); // Read answer from user and exit if it's not 'yes' - if !confirm() { - println!("\nSkipping...\n"); - println!("All done, no errors..."); - ::std::process::exit(0); + if confirm() { + builder.copy_theme(true); } } - - // Call the function that copies the theme - book.copy_theme()?; - println!("\nTheme copied."); } - // Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root` - let is_dest_inside_root = book.get_destination().starts_with(&book.root); + println!("\nDo you want a .gitignore to be created? (y/n)"); - if !args.is_present("force") && is_dest_inside_root { - println!("\nDo you want a .gitignore to be created? (y/n)"); - - if confirm() { - book.create_gitignore(); - println!("\n.gitignore created."); - } + if confirm() { + builder.create_gitignore(true); } + builder.build()?; println!("\nAll done, no errors..."); Ok(()) diff --git a/src/bin/serve.rs b/src/bin/serve.rs index ac1ba144..2768e466 100644 --- a/src/bin/serve.rs +++ b/src/bin/serve.rs @@ -49,7 +49,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { const RELOAD_COMMAND: &'static str = "reload"; let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir).read_config()?; + let mut book = MDBook::load(&book_dir)?; if let Some(dest_dir) = args.value_of("dest-dir") { book.config.build.build_dir = PathBuf::from(dest_dir); @@ -64,7 +64,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> { let address = format!("{}:{}", interface, port); let ws_address = format!("{}:{}", interface, ws_port); - book.livereload = Some(format!(r#" + book.livereload = Some(format!( + r#"