From 6d0d4bf37942c9a5eaf57ca2d6e52d096a27314c Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 24 Jun 2017 22:47:03 +0800 Subject: [PATCH 01/21] Created a SUMMARY.md parser and basic Loader From the [pull request comment][pr], here's a rough summary of what was done in the squashed commits. --- \# Summary Parser - Added a private submodule called `mdbook::loader::summary` which contains all the code for parsing `SUMMARY.md` - A `Summary` contains a title (optional), then some prefix, numbered, and suffix chapters (technically `Vec`) - A `SummaryItem` is either a `Link` (i.e. link to a chapter), or a separator - A `Link` contains the chapter name, its location relative to the book's `src/` directory, and a list of nested `SummaryItems` - The `SummaryParser` (a state machine-based parser) uses `pulldown_cmark` to turn the `SUMMARY.md` string into a stream of `Events`, it then iterates over those events changing its behaviour depending on the current state, - The states are `Start`, `PrefixChapters`, `NestedChapters(u32)` (the `u32` represents your nesting level, because lists can contain lists), `SuffixChapters`, and `End` - Each state will read the appropriate link and build up the `Summary`, skipping any events which aren't a link, horizontal rule (separator), or a list \# Loader - Created a basic loader which can be used to load the `SUMMARY.md` in a directory. \# Tests - Added a couple unit tests for each state in the parser's state machine - Added integration tests for parsing a dummy SUMMARY.md then asserting the result is exactly what we expected [pr]: https://github.com/azerupi/mdBook/pull/371#issuecomment-312636102 --- Cargo.toml | 5 + src/lib.rs | 32 +- src/loader/mod.rs | 84 +++++ src/loader/summary.rs | 714 ++++++++++++++++++++++++++++++++++++++++++ tests/loading.rs | 109 +++++++ 5 files changed, 935 insertions(+), 9 deletions(-) create mode 100644 src/loader/mod.rs create mode 100644 src/loader/summary.rs create mode 100644 tests/loading.rs diff --git a/Cargo.toml b/Cargo.toml index ca9e9d3df3..c1be46bef5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,11 @@ iron = { version = "0.5", optional = true } staticfile = { version = "0.4", optional = true } ws = { version = "0.7", optional = true} +# Tests +[dev-dependencies] +tempdir = "0.3.4" +pretty_assertions = "0.2.1" + [build-dependencies] error-chain = "0.11" diff --git a/src/lib.rs b/src/lib.rs index fff77bbd72..9a01bb9346 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,8 @@ //! **mdBook** is similar to Gitbook but implemented in Rust. //! It offers a command line interface, but can also be used as a regular crate. //! -//! This is the API doc, but you can find a [less "low-level" documentation here](../index.html) that +//! This is the API doc, but you can find a [less "low-level" documentation +//! here](../index.html) that //! contains information about the command line tool, format, structure etc. //! It is also rendered with mdBook to showcase the features and default theme. //! @@ -25,10 +26,14 @@ //! # #[allow(unused_variables)] //! fn main() { //! let mut book = MDBook::new("my-book") // Path to root -//! .with_source("src") // Path from root to source directory -//! .with_destination("book") // Path from root to output directory -//! .read_config() // Parse book.toml configuration file -//! .expect("I don't handle configuration file errors, but you should!"); +//! .with_source("src") // Path from root to +//! source directory +//! .with_destination("book") // Path from root to +//! output directory +//! .read_config() // Parse book.toml +//! configuration file +//! .expect("I don't handle configuration file errors, +//! but you should!"); //! //! book.build().unwrap(); // Render the book //! } @@ -36,7 +41,8 @@ //! //! ## Implementing a new Renderer //! -//! If you want to create a new renderer for mdBook, the only thing you have to do is to implement +//! If you want to create a new renderer for mdBook, the only thing you have to +//! do is to implement //! the [Renderer trait](renderer/renderer/trait.Renderer.html) //! //! And then you can swap in your renderer like this: @@ -54,14 +60,17 @@ //! let book = MDBook::new("my-book").set_renderer(Box::new(your_renderer)); //! # } //! ``` -//! If you make a renderer, you get the book constructed in form of `Vec` and you get +//! If you make a renderer, you get the book constructed in form of +//! `Vec` and you get //! the book config in a `BookConfig` struct. //! -//! It's your responsability to create the necessary files in the correct directories. +//! It's your responsability to create the necessary files in the correct +//! directories. //! //! ## utils //! -//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the +//! I have regrouped some useful functions in the [utils](utils/index.html) +//! module, like the //! following function [`utils::fs::create_file(path: //! &Path)`](utils/fs/fn.create_file.html) //! @@ -87,6 +96,10 @@ extern crate serde; extern crate serde_json; extern crate tempdir; +#[cfg(test)] +#[macro_use] +extern crate pretty_assertions; + mod parse; mod preprocess; pub mod book; @@ -94,6 +107,7 @@ pub mod config; pub mod renderer; pub mod theme; pub mod utils; +pub mod loader; pub use book::MDBook; pub use book::BookItem; diff --git a/src/loader/mod.rs b/src/loader/mod.rs new file mode 100644 index 0000000000..3c597d4b90 --- /dev/null +++ b/src/loader/mod.rs @@ -0,0 +1,84 @@ +//! Functionality for loading the internal book representation from disk. +//! +//! The typical use case is to create a `Loader` pointing at the correct +//! source directory then call the `load()` method. Internally this will +//! search for the `SUMMARY.md` file, parse it, then use the parsed +//! `Summary` to construct an in-memory representation of the entire book. +//! +//! # Examples +//! +//! ```rust,no_run +//! # fn run() -> mdbook::errors::Result<()> { +//! use mdbook::loader::Loader; +//! let loader = Loader::new("./src/"); +//! let book = loader.load()?; +//! # Ok(()) +//! # } +//! # fn main() { run().unwrap() } +//! ``` +//! +//! Alternatively, if you are using the `mdbook` crate as a library and +//! only want to read the `SUMMARY.md` file without having to load the +//! entire book from disk, you can use the `parse_summary()` function. +//! +//! ```rust +//! # fn run() -> mdbook::errors::Result<()> { +//! use mdbook::loader::parse_summary; +//! let src = "# Book Summary +//! +//! [Introduction](./index.md) +//! - [First Chapter](./first/index.md) +//! - [Sub-Section](./first/subsection.md) +//! - [Second Chapter](./second/index.md) +//! "; +//! let summary = parse_summary(src)?; +//! println!("{:#?}", summary); +//! # Ok(()) +//! # } +//! # fn main() { run().unwrap() } +//! ``` + +#![deny(missing_docs)] + +use std::path::{Path, PathBuf}; +use std::fs::File; +use std::io::Read; +use errors::*; + +mod summary; + +pub use self::summary::{Summary, Link, SummaryItem, parse_summary, SectionNumber}; + + +/// The object in charge of parsing the source directory into a usable +/// `Book` struct. +#[derive(Debug, Clone, PartialEq)] +pub struct Loader { + source_directory: PathBuf, +} + +impl Loader { + /// Create a new loader which uses the provided source directory. + pub fn new>(source_directory: P) -> Loader { + Loader { source_directory: source_directory.as_ref().to_path_buf() } + } + + /// Parse the summary file and use it to load a book from disk. + pub fn load(&self) -> Result<()> { + let summary = self.parse_summary().chain_err( + || "Couldn't parse `SUMMARY.md`", + )?; + + unimplemented!() + } + + /// Parse the `SUMMARY.md` file. + pub fn parse_summary(&self) -> Result { + let path = self.source_directory.join("SUMMARY.md"); + + let mut summary_content = String::new(); + File::open(&path)?.read_to_string(&mut summary_content)?; + + summary::parse_summary(&summary_content) + } +} diff --git a/src/loader/summary.rs b/src/loader/summary.rs new file mode 100644 index 0000000000..6510fe9727 --- /dev/null +++ b/src/loader/summary.rs @@ -0,0 +1,714 @@ +#![allow(dead_code, unused_variables)] + +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)] +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)] +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)] +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, + } + } +} + +#[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 => self.step_prefix(next_event)?, + State::NumberedChapters(n) => self.step_numbered(next_event, n)?, + State::SuffixChapters => self.step_suffix(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, + other => bail!("Expected a start of paragraph but got {:?}", other), + } + + Ok(()) + } + + /// In the second step we look out for links and horizontal rules to add + /// to the prefix. + /// + /// This state should only progress when it encounters a list. All other + /// events will either be separators (horizontal rule), prefix chapters + /// (the links), or skipped. + fn step_prefix(&mut self, event: Event<'a>) -> Result<()> { + match event { + Event::Start(Tag::Link(location, _)) => { + let content = collect_events!(self.stream, Tag::Link(_, _)); + let text = stringify_events(content); + let link = Link::new(text, location.as_ref()); + + debug!("[*] Found a prefix chapter: {:?}", link.name); + self.summary.prefix_chapters.push(SummaryItem::Link(link)); + }, + Event::End(Tag::Rule) => { + debug!("[*] Found a prefix chapter separator"); + self.summary.prefix_chapters.push(SummaryItem::Separator); + }, + Event::Start(Tag::List(_)) => { + debug!("[*] Changing from prefix chapters to numbered chapters"); + self.state = State::NumberedChapters(0); + }, + + other => { + trace!("[*] Skipping unexpected token in summary: {:?}", 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, nesting: u32) -> 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::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(()) + } + + fn step_suffix(&mut self, event: Event<'a>) -> Result<()> { + // FIXME: This has been copy/pasted from step_prefix. make DRY. + match event { + Event::Start(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 a suffix chapter: {:?}", link.name); + self.summary.suffix_chapters.push(SummaryItem::Link(link)); + }, + Event::End(Tag::Rule) => { + debug!("[*] Found a suffix chapter separator"); + self.summary.suffix_chapters.push(SummaryItem::Separator); + }, + other => { + trace!("[*] Skipping unexpected token in summary: {:?}", 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 next_level = level - 1; + let index_for_item = links.len() + 1; + + // FIXME: This bit needs simplifying! + 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)> { + 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)] +pub struct SectionNumber(pub Vec); + +impl Display for SectionNumber { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let dotted_number: String = self.0 + .iter() + .map(|i| format!("{}", i)) + .collect::>() + .join("."); + + write!(f, "{}", dotted_number) + } +} + +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); + let string_repr = format!("{}", section_number); + + assert_eq!(string_repr, 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/tests/loading.rs b/tests/loading.rs new file mode 100644 index 0000000000..581478b4e1 --- /dev/null +++ b/tests/loading.rs @@ -0,0 +1,109 @@ +//! Integration tests for loading a book into memory + +#[macro_use] +extern crate pretty_assertions; +extern crate mdbook; +extern crate env_logger; +extern crate tempdir; + +use std::path::PathBuf; +use std::fs::File; +use std::io::Write; + +use mdbook::loader::{parse_summary, Link, SummaryItem, SectionNumber, Summary, Loader}; +use tempdir::TempDir; + + +const SUMMARY: &'static str = " +# Summary + +[Introduction](/intro.md) + +--- + +[A Prefix Chapter](/some_prefix.md) + +- [First Chapter](/chapter_1/index.md) + - [Some Subsection](/chapter_1/subsection.md) + +--- + +[Conclusion](/conclusion.md) +"; + +#[test] +fn parse_summary_md() { + env_logger::init().ok(); + + let should_be = expected_summary(); + let got = parse_summary(SUMMARY).unwrap(); + + println!("{:#?}", got); + assert_eq!(got, should_be); +} + +#[test] +fn parse_summary_using_loader() { + env_logger::init().ok(); + + let temp = TempDir::new("book").unwrap(); + let summary_md = temp.path().join("SUMMARY.md"); + + File::create(&summary_md).unwrap().write_all(SUMMARY.as_bytes()).unwrap(); + + let loader = Loader::new(temp.path()); + + let got = loader.parse_summary().unwrap(); + let should_be = expected_summary(); + + assert_eq!(got, should_be); +} + +/// This is what the SUMMARY should be parsed as +fn expected_summary() -> Summary { + Summary { + title: Some(String::from("Summary")), + + prefix_chapters: vec![ + SummaryItem::Link(Link { + name: String::from("Introduction"), + location: PathBuf::from("/intro.md"), + number: None, + nested_items: vec![], + }), + SummaryItem::Separator, + SummaryItem::Link(Link { + name: String::from("A Prefix Chapter"), + location: PathBuf::from("/some_prefix.md"), + number: None, + nested_items: vec![], + }), + ], + + numbered_chapters: vec![ + SummaryItem::Link(Link { + name: String::from("First Chapter"), + location: PathBuf::from("/chapter_1/index.md"), + number: Some(SectionNumber(vec![1])), + nested_items: vec![ + SummaryItem::Link(Link { + name: String::from("Some Subsection"), + location: PathBuf::from("/chapter_1/subsection.md"), + number: Some(SectionNumber(vec![1, 1])), + nested_items: vec![], + }), + ], + }), + ], + + suffix_chapters: vec![ + SummaryItem::Separator, + SummaryItem::Link(Link { + name: String::from("Conclusion"), + location: PathBuf::from("/conclusion.md"), + number: None, + nested_items: vec![], + }), + ], + } +} From 1b5a58902f6b8fc13a235eb76d3b7bfb253818c7 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 3 Jul 2017 07:34:03 +0800 Subject: [PATCH 02/21] Created a Loader for loading a book using the Summary This is a squashed commit. It roughly encompasses the following changes. --- \# Book - Created another private submodule, mdbook::loader::book - This submodule contains the data types representing a Book - For now the Book just contains a list of BookItems (either chapters or separators) - A Chapter contains its name, contents (as one long string), an optional section number (only numbered chapters have numbers, obviously), and any nested chapters - There's a function for loading a single Chapter from disk using it's associated Link entry from the SUMMARY.md - Another function builds up the Book by recursively visiting all Links and separators in the Summary and joining them into a single Vec. This is the only non-dumb-data-type item which is actually exported from the book module \# Loader - Made the loader use the book::load_book_from_disk function for loading a book in the loader's directory. \# Tests - Made sure you can load from disk by writing some files to a temporary directory - Made sure the Loader can load the entire example-book from disk and doesn't crash or hit an error - Increased test coverage from 34.4% to 47.7% (as reported by cargo kcov) --- src/lib.rs | 29 ++---- src/loader/book.rs | 234 ++++++++++++++++++++++++++++++++++++++++++ src/loader/mod.rs | 30 ++++-- src/loader/summary.rs | 24 +++-- tests/loading.rs | 18 +++- 5 files changed, 301 insertions(+), 34 deletions(-) create mode 100644 src/loader/book.rs diff --git a/src/lib.rs b/src/lib.rs index 9a01bb9346..2b4c5dd3a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,7 @@ //! **mdBook** is similar to Gitbook but implemented in Rust. //! It offers a command line interface, but can also be used as a regular crate. //! -//! This is the API doc, but you can find a [less "low-level" documentation -//! here](../index.html) that +//! This is the API doc, but you can find a [less "low-level" documentation here](../index.html) that //! contains information about the command line tool, format, structure etc. //! It is also rendered with mdBook to showcase the features and default theme. //! @@ -26,23 +25,17 @@ //! # #[allow(unused_variables)] //! fn main() { //! let mut book = MDBook::new("my-book") // Path to root -//! .with_source("src") // Path from root to -//! source directory -//! .with_destination("book") // Path from root to -//! output directory -//! .read_config() // Parse book.toml -//! configuration file -//! .expect("I don't handle configuration file errors, -//! but you should!"); -//! +//! .with_source("src") // Path from root to source directory +//! .with_destination("book") // Path from root to output directory +//! .read_config() // Parse book.toml configuration file +//! .expect("I don't handle configuration file errors, but you should!"); //! book.build().unwrap(); // Render the book //! } //! ``` //! //! ## Implementing a new Renderer //! -//! If you want to create a new renderer for mdBook, the only thing you have to -//! do is to implement +//! If you want to create a new renderer for mdBook, the only thing you have to do is to implement //! the [Renderer trait](renderer/renderer/trait.Renderer.html) //! //! And then you can swap in your renderer like this: @@ -60,17 +53,15 @@ //! let book = MDBook::new("my-book").set_renderer(Box::new(your_renderer)); //! # } //! ``` -//! If you make a renderer, you get the book constructed in form of -//! `Vec` and you get +//! If you make a renderer, you get the book constructed in form of `Vec` and you get //! the book config in a `BookConfig` struct. //! -//! It's your responsability to create the necessary files in the correct +//! It's your responsibility to create the necessary files in the correct //! directories. //! //! ## utils //! -//! I have regrouped some useful functions in the [utils](utils/index.html) -//! module, like the +//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the //! following function [`utils::fs::create_file(path: //! &Path)`](utils/fs/fn.create_file.html) //! @@ -99,6 +90,8 @@ extern crate tempdir; #[cfg(test)] #[macro_use] extern crate pretty_assertions; +#[cfg(test)] +extern crate tempdir; mod parse; mod preprocess; diff --git a/src/loader/book.rs b/src/loader/book.rs new file mode 100644 index 0000000000..7721b44b0c --- /dev/null +++ b/src/loader/book.rs @@ -0,0 +1,234 @@ +#![allow(missing_docs, unused_variables, unused_imports, dead_code)] + +use std::path::Path; +use std::fs::File; +use std::io::Read; + +use loader::summary::{Summary, Link, SummaryItem, SectionNumber}; +use errors::*; + + +/// 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() + } +} + +/// 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, +} + +impl Chapter { + /// Create a new chapter with the provided content. + pub fn new(name: &str, content: String) -> Chapter { + Chapter { + name: name.to_string(), + content: content, + ..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. +pub fn load_book_from_disk>(summary: &Summary, src_dir: P) -> 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 chapters = summary_items + .map(|i| load_summary_item(i, src_dir)) + .collect::>() + .chain_err(|| "Couldn't load chapters from disk")?; + + Ok(Book { sections: chapters }) +} + +fn load_summary_item>(item: &SummaryItem, src_dir: P) -> Result { + match *item { + SummaryItem::Separator => Ok(BookItem::Separator), + SummaryItem::Link(ref link) => 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 mut ch = Chapter::new(&link.name, content); + ch.number = link.number.clone(); + + let sub_items = link.nested_items + .iter() + .map(|i| load_summary_item(i, src_dir)) + .collect::>>()?; + + ch.sub_items = sub_items; + + Ok(ch) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempdir::TempDir; + use std::io::Write; + + 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.push_item(second.clone()); + root.push_item(SummaryItem::Separator); + root.push_item(second.clone()); + + (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()); + + 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])), + sub_items: Vec::new(), + }; + let should_be = BookItem::Chapter(Chapter { + name: String::from("Chapter 1"), + content: String::from(DUMMY_SRC), + number: None, + sub_items: vec![ + BookItem::Chapter(nested.clone()), + BookItem::Separator, + BookItem::Chapter(nested.clone()), + ], + }); + + let got = load_summary_item(&SummaryItem::Link(root), "").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), + ..Default::default() + }), + ], + }; + + let got = load_book_from_disk(&summary, "").unwrap(); + + assert_eq!(got, should_be); + } +} \ No newline at end of file diff --git a/src/loader/mod.rs b/src/loader/mod.rs index 3c597d4b90..eff82505b0 100644 --- a/src/loader/mod.rs +++ b/src/loader/mod.rs @@ -46,8 +46,10 @@ use std::io::Read; use errors::*; mod summary; +mod book; pub use self::summary::{Summary, Link, SummaryItem, parse_summary, SectionNumber}; +pub use self::book::{Book, load_book_from_disk, BookItem, Chapter}; /// The object in charge of parsing the source directory into a usable @@ -64,21 +66,33 @@ impl Loader { } /// Parse the summary file and use it to load a book from disk. - pub fn load(&self) -> Result<()> { - let summary = self.parse_summary().chain_err( + pub fn load(&self) -> Result { + let summary_md = self.find_summary().chain_err( + || "Couldn't find `SUMMARY.md`", + )?; + + let summary = self.parse_summary(&summary_md).chain_err( || "Couldn't parse `SUMMARY.md`", )?; - unimplemented!() + let src_dir = match summary_md.parent() { + Some(parent) => parent, + None => bail!("SUMMARY.md doesn't have a parent... wtf?"), + }; + load_book_from_disk(&summary, src_dir) } - /// Parse the `SUMMARY.md` file. - pub fn parse_summary(&self) -> Result { - let path = self.source_directory.join("SUMMARY.md"); - + /// Parse a `SUMMARY.md` file. + pub fn parse_summary>(&self, summary_md: P) -> Result { let mut summary_content = String::new(); - File::open(&path)?.read_to_string(&mut summary_content)?; + File::open(summary_md)?.read_to_string(&mut summary_content)?; summary::parse_summary(&summary_content) } + + fn find_summary(&self) -> Result { + // TODO: use Piston's find_folder to make locating SUMMARY.md easier. + // https://github.com/PistonDevelopers/find_folder + Ok(self.source_directory.join("SUMMARY.md")) + } } diff --git a/src/loader/summary.rs b/src/loader/summary.rs index 6510fe9727..45cb1674db 100644 --- a/src/loader/summary.rs +++ b/src/loader/summary.rs @@ -50,7 +50,7 @@ pub fn parse_summary(summary: &str) -> Result { } /// The parsed `SUMMARY.md`, specifying how the book should be laid out. -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Summary { /// An optional title for the `SUMMARY.md`, currently just ignored. pub title: Option, @@ -66,7 +66,7 @@ pub struct Summary { /// entries. /// /// This is roughly the equivalent of `[Some section](./path/to/file.md)`. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Link { /// The name of the chapter. pub name: String, @@ -89,6 +89,11 @@ impl Link { nested_items: Vec::new(), } } + + /// Add an item to this link's `nested_items`. + pub fn push_item>(&mut self, item: I) { + self.nested_items.push(item.into()); + } } impl Default for Link { @@ -103,7 +108,7 @@ impl Default for Link { } /// An item in `SUMMARY.md` which could be either a separator or a `Link`. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SummaryItem { /// A link to a chapter. Link(Link), @@ -120,6 +125,12 @@ impl SummaryItem { } } +impl From for SummaryItem { + fn from(other: Link) -> SummaryItem { + SummaryItem::Link(other) + } +} + #[derive(Debug, Copy, Clone, PartialEq)] enum State { Begin, @@ -310,8 +321,9 @@ impl<'a> SummaryParser<'a> { fn step_numbered(&mut self, event: Event, nesting: u32) -> Result<()> { match event { Event::Start(Tag::Item) => { - let it = self.parse_item() - .chain_err(|| "List items should only contain links")?; + 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)); @@ -479,7 +491,7 @@ fn stringify_events(events: Vec) -> String { /// A section number like "1.2.3", basically just a newtype'd `Vec` with /// a pretty `Display` impl. -#[derive(Debug, PartialEq, Clone, Default)] +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] pub struct SectionNumber(pub Vec); impl Display for SectionNumber { diff --git a/tests/loading.rs b/tests/loading.rs index 581478b4e1..4ae35108e5 100644 --- a/tests/loading.rs +++ b/tests/loading.rs @@ -49,11 +49,14 @@ fn parse_summary_using_loader() { let temp = TempDir::new("book").unwrap(); let summary_md = temp.path().join("SUMMARY.md"); - File::create(&summary_md).unwrap().write_all(SUMMARY.as_bytes()).unwrap(); + File::create(&summary_md) + .unwrap() + .write_all(SUMMARY.as_bytes()) + .unwrap(); let loader = Loader::new(temp.path()); - let got = loader.parse_summary().unwrap(); + let got = loader.parse_summary(&summary_md).unwrap(); let should_be = expected_summary(); assert_eq!(got, should_be); @@ -107,3 +110,14 @@ fn expected_summary() -> Summary { ], } } + +#[test] +fn load_the_example_book() { + let example_src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("book-example") + .join("src"); + let loader = Loader::new(example_src_dir); + + let book = loader.load().unwrap(); + println!("{:#?}", book); +} From b925c7c41cf266805547c6120589f53b902ecc23 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Fri, 7 Jul 2017 21:00:15 +0800 Subject: [PATCH 03/21] Added a depth-first chapter iterator --- src/loader/book.rs | 88 ++++++++++++++++++++++++++++++++++++++++++++++ src/loader/mod.rs | 2 +- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/loader/book.rs b/src/loader/book.rs index 7721b44b0c..4c2e6ba24f 100644 --- a/src/loader/book.rs +++ b/src/loader/book.rs @@ -1,6 +1,7 @@ #![allow(missing_docs, unused_variables, unused_imports, dead_code)] use std::path::Path; +use std::collections::VecDeque; use std::fs::File; use std::io::Read; @@ -22,6 +23,11 @@ impl 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. @@ -117,6 +123,29 @@ fn load_chapter>(link: &Link, src_dir: P) -> Result { Ok(ch) } +/// A depth-first iterator over the items in a book. +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 + } +} + + #[cfg(test)] mod tests { use super::*; @@ -231,4 +260,63 @@ And here is some more text. 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, + sub_items: vec![ + BookItem::Chapter(Chapter::new("Hello World", String::new())), + BookItem::Separator, + BookItem::Chapter(Chapter::new("Goodbye World", String::new())), + ], + }), + 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); + } } \ No newline at end of file diff --git a/src/loader/mod.rs b/src/loader/mod.rs index eff82505b0..145aa8b0e7 100644 --- a/src/loader/mod.rs +++ b/src/loader/mod.rs @@ -49,7 +49,7 @@ mod summary; mod book; pub use self::summary::{Summary, Link, SummaryItem, parse_summary, SectionNumber}; -pub use self::book::{Book, load_book_from_disk, BookItem, Chapter}; +pub use self::book::{Book, BookItems, load_book_from_disk, BookItem, Chapter}; /// The object in charge of parsing the source directory into a usable From 4202ead962bd7cc54a5158257b7f22d986a19e38 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 8 Jul 2017 14:28:47 +0800 Subject: [PATCH 04/21] Started integrating some of the feedback from budziq --- src/loader/book.rs | 2 - src/loader/mod.rs | 83 +++++++-------------------------- src/loader/summary.rs | 2 - tests/loading.rs | 105 +----------------------------------------- 4 files changed, 19 insertions(+), 173 deletions(-) diff --git a/src/loader/book.rs b/src/loader/book.rs index 4c2e6ba24f..f253eea486 100644 --- a/src/loader/book.rs +++ b/src/loader/book.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs, unused_variables, unused_imports, dead_code)] - use std::path::Path; use std::collections::VecDeque; use std::fs::File; diff --git a/src/loader/mod.rs b/src/loader/mod.rs index 145aa8b0e7..bf3e8ae250 100644 --- a/src/loader/mod.rs +++ b/src/loader/mod.rs @@ -9,30 +9,8 @@ //! //! ```rust,no_run //! # fn run() -> mdbook::errors::Result<()> { -//! use mdbook::loader::Loader; -//! let loader = Loader::new("./src/"); -//! let book = loader.load()?; -//! # Ok(()) -//! # } -//! # fn main() { run().unwrap() } -//! ``` -//! -//! Alternatively, if you are using the `mdbook` crate as a library and -//! only want to read the `SUMMARY.md` file without having to load the -//! entire book from disk, you can use the `parse_summary()` function. -//! -//! ```rust -//! # fn run() -> mdbook::errors::Result<()> { -//! use mdbook::loader::parse_summary; -//! let src = "# Book Summary -//! -//! [Introduction](./index.md) -//! - [First Chapter](./first/index.md) -//! - [Sub-Section](./first/subsection.md) -//! - [Second Chapter](./second/index.md) -//! "; -//! let summary = parse_summary(src)?; -//! println!("{:#?}", summary); +//! use mdbook::loader::load_book; +//! let book = load_book("./src/")?; //! # Ok(()) //! # } //! # fn main() { run().unwrap() } @@ -48,51 +26,24 @@ use errors::*; mod summary; mod book; -pub use self::summary::{Summary, Link, SummaryItem, parse_summary, SectionNumber}; -pub use self::book::{Book, BookItems, load_book_from_disk, BookItem, Chapter}; - - -/// The object in charge of parsing the source directory into a usable -/// `Book` struct. -#[derive(Debug, Clone, PartialEq)] -pub struct Loader { - source_directory: PathBuf, -} - -impl Loader { - /// Create a new loader which uses the provided source directory. - pub fn new>(source_directory: P) -> Loader { - Loader { source_directory: source_directory.as_ref().to_path_buf() } - } - - /// Parse the summary file and use it to load a book from disk. - pub fn load(&self) -> Result { - let summary_md = self.find_summary().chain_err( - || "Couldn't find `SUMMARY.md`", - )?; +pub use self::book::{Book, BookItems, BookItem, Chapter}; - let summary = self.parse_summary(&summary_md).chain_err( - || "Couldn't parse `SUMMARY.md`", - )?; +use self::book::load_book_from_disk; +use self::summary::parse_summary; - let src_dir = match summary_md.parent() { - Some(parent) => parent, - None => bail!("SUMMARY.md doesn't have a parent... wtf?"), - }; - load_book_from_disk(&summary, src_dir) - } +/// Load a book into memory from its `src/` directory. +pub fn load_book>(src_dir: P) -> Result { + let src_dir = src_dir.as_ref(); + let summary_md = src_dir.join("SUMMARY.md"); - /// Parse a `SUMMARY.md` file. - pub fn parse_summary>(&self, summary_md: P) -> Result { - let mut summary_content = String::new(); - File::open(summary_md)?.read_to_string(&mut summary_content)?; + let mut summary_content = String::new(); + File::open(summary_md) + .chain_err(|| "Couldn't open SUMMARY.md")? + .read_to_string(&mut summary_content)?; - summary::parse_summary(&summary_content) - } + let summary = parse_summary(&summary_content).chain_err( + || "Summary parsing failed", + )?; - fn find_summary(&self) -> Result { - // TODO: use Piston's find_folder to make locating SUMMARY.md easier. - // https://github.com/PistonDevelopers/find_folder - Ok(self.source_directory.join("SUMMARY.md")) - } + load_book_from_disk(&summary, src_dir) } diff --git a/src/loader/summary.rs b/src/loader/summary.rs index 45cb1674db..88ad7d0244 100644 --- a/src/loader/summary.rs +++ b/src/loader/summary.rs @@ -1,5 +1,3 @@ -#![allow(dead_code, unused_variables)] - use std::fmt::{self, Formatter, Display}; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; diff --git a/tests/loading.rs b/tests/loading.rs index 4ae35108e5..62a5361ee6 100644 --- a/tests/loading.rs +++ b/tests/loading.rs @@ -7,117 +7,16 @@ extern crate env_logger; extern crate tempdir; use std::path::PathBuf; -use std::fs::File; -use std::io::Write; -use mdbook::loader::{parse_summary, Link, SummaryItem, SectionNumber, Summary, Loader}; -use tempdir::TempDir; +use mdbook::loader::load_book; -const SUMMARY: &'static str = " -# Summary - -[Introduction](/intro.md) - ---- - -[A Prefix Chapter](/some_prefix.md) - -- [First Chapter](/chapter_1/index.md) - - [Some Subsection](/chapter_1/subsection.md) - ---- - -[Conclusion](/conclusion.md) -"; - -#[test] -fn parse_summary_md() { - env_logger::init().ok(); - - let should_be = expected_summary(); - let got = parse_summary(SUMMARY).unwrap(); - - println!("{:#?}", got); - assert_eq!(got, should_be); -} - -#[test] -fn parse_summary_using_loader() { - env_logger::init().ok(); - - let temp = TempDir::new("book").unwrap(); - let summary_md = temp.path().join("SUMMARY.md"); - - File::create(&summary_md) - .unwrap() - .write_all(SUMMARY.as_bytes()) - .unwrap(); - - let loader = Loader::new(temp.path()); - - let got = loader.parse_summary(&summary_md).unwrap(); - let should_be = expected_summary(); - - assert_eq!(got, should_be); -} - -/// This is what the SUMMARY should be parsed as -fn expected_summary() -> Summary { - Summary { - title: Some(String::from("Summary")), - - prefix_chapters: vec![ - SummaryItem::Link(Link { - name: String::from("Introduction"), - location: PathBuf::from("/intro.md"), - number: None, - nested_items: vec![], - }), - SummaryItem::Separator, - SummaryItem::Link(Link { - name: String::from("A Prefix Chapter"), - location: PathBuf::from("/some_prefix.md"), - number: None, - nested_items: vec![], - }), - ], - - numbered_chapters: vec![ - SummaryItem::Link(Link { - name: String::from("First Chapter"), - location: PathBuf::from("/chapter_1/index.md"), - number: Some(SectionNumber(vec![1])), - nested_items: vec![ - SummaryItem::Link(Link { - name: String::from("Some Subsection"), - location: PathBuf::from("/chapter_1/subsection.md"), - number: Some(SectionNumber(vec![1, 1])), - nested_items: vec![], - }), - ], - }), - ], - - suffix_chapters: vec![ - SummaryItem::Separator, - SummaryItem::Link(Link { - name: String::from("Conclusion"), - location: PathBuf::from("/conclusion.md"), - number: None, - nested_items: vec![], - }), - ], - } -} - #[test] fn load_the_example_book() { let example_src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("book-example") .join("src"); - let loader = Loader::new(example_src_dir); - let book = loader.load().unwrap(); + let book = load_book(example_src_dir).unwrap(); println!("{:#?}", book); } From 02d1d9317db70fe2df85f39f2f3bc9395277ea18 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 8 Jul 2017 17:55:00 +0800 Subject: [PATCH 05/21] Started cleaning up some of the lints and warnings --- Cargo.toml | 1 - src/lib.rs | 3 --- src/loader/book.rs | 9 ++++++++- src/loader/mod.rs | 3 ++- src/loader/summary.rs | 10 +++------- tests/loading.rs | 5 ++--- 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1be46bef5..f5d39a1c01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ ws = { version = "0.7", optional = true} # Tests [dev-dependencies] tempdir = "0.3.4" -pretty_assertions = "0.2.1" [build-dependencies] error-chain = "0.11" diff --git a/src/lib.rs b/src/lib.rs index 2b4c5dd3a3..97af9a4fc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,9 +87,6 @@ extern crate serde; extern crate serde_json; extern crate tempdir; -#[cfg(test)] -#[macro_use] -extern crate pretty_assertions; #[cfg(test)] extern crate tempdir; diff --git a/src/loader/book.rs b/src/loader/book.rs index f253eea486..803c402045 100644 --- a/src/loader/book.rs +++ b/src/loader/book.rs @@ -122,6 +122,13 @@ fn load_chapter>(link: &Link, src_dir: P) -> Result { } /// 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>, } @@ -239,7 +246,7 @@ And here is some more text. #[test] fn load_a_book_with_a_single_chapter() { - let (link, temp) = dummy_link(); + let (link, _temp) = dummy_link(); let summary = Summary { numbered_chapters: vec![SummaryItem::Link(link)], ..Default::default() diff --git a/src/loader/mod.rs b/src/loader/mod.rs index bf3e8ae250..0209497feb 100644 --- a/src/loader/mod.rs +++ b/src/loader/mod.rs @@ -18,7 +18,7 @@ #![deny(missing_docs)] -use std::path::{Path, PathBuf}; +use std::path::Path; use std::fs::File; use std::io::Read; use errors::*; @@ -27,6 +27,7 @@ mod summary; mod book; pub use self::book::{Book, BookItems, BookItem, Chapter}; +pub use self::summary::SectionNumber; use self::book::load_book_from_disk; use self::summary::parse_summary; diff --git a/src/loader/summary.rs b/src/loader/summary.rs index 88ad7d0244..c2dfe03f3b 100644 --- a/src/loader/summary.rs +++ b/src/loader/summary.rs @@ -2,7 +2,6 @@ use std::fmt::{self, Formatter, Display}; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use pulldown_cmark::{self, Event, Tag}; - use errors::*; @@ -248,7 +247,7 @@ impl<'a> SummaryParser<'a> { match self.state { State::Begin => self.step_start(next_event)?, State::PrefixChapters => self.step_prefix(next_event)?, - State::NumberedChapters(n) => self.step_numbered(next_event, n)?, + State::NumberedChapters(_) => self.step_numbered(next_event)?, State::SuffixChapters => self.step_suffix(next_event)?, State::End => {}, } @@ -316,7 +315,7 @@ impl<'a> SummaryParser<'a> { /// section and need to parse the suffix section. /// /// Otherwise, ignore the event. - fn step_numbered(&mut self, event: Event, nesting: u32) -> Result<()> { + fn step_numbered(&mut self, event: Event) -> Result<()> { match event { Event::Start(Tag::Item) => { let it = self.parse_item().chain_err( @@ -448,10 +447,6 @@ fn push_item_at_nesting_level(links: &mut Vec, mut item: SummaryIte links.push(item); Ok(section_number) } else { - let next_level = level - 1; - let index_for_item = links.len() + 1; - - // FIXME: This bit needs simplifying! 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) @@ -465,6 +460,7 @@ fn push_item_at_nesting_level(links: &mut Vec, mut item: SummaryIte /// 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() diff --git a/tests/loading.rs b/tests/loading.rs index 62a5361ee6..50ff4f1006 100644 --- a/tests/loading.rs +++ b/tests/loading.rs @@ -1,10 +1,7 @@ //! Integration tests for loading a book into memory -#[macro_use] -extern crate pretty_assertions; extern crate mdbook; extern crate env_logger; -extern crate tempdir; use std::path::PathBuf; @@ -13,6 +10,8 @@ use mdbook::loader::load_book; #[test] fn load_the_example_book() { + env_logger::init().ok(); + let example_src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("book-example") .join("src"); From ce6dbd6736d85b9ecac5d68f826334bb58ca4e2e Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 21 Aug 2017 10:19:23 +0800 Subject: [PATCH 06/21] Deleted previous bookitem.md and moved loader contents across --- src/{loader => book}/book.rs | 20 ++- src/book/mod.rs | 4 +- src/{loader => book}/summary.rs | 0 src/lib.rs | 4 +- src/loader/mod.rs | 50 ------- src/parse/mod.rs | 3 - src/parse/summary.rs | 231 -------------------------------- 7 files changed, 23 insertions(+), 289 deletions(-) rename src/{loader => book}/book.rs (93%) rename src/{loader => book}/summary.rs (100%) delete mode 100644 src/loader/mod.rs delete mode 100644 src/parse/mod.rs delete mode 100644 src/parse/summary.rs diff --git a/src/loader/book.rs b/src/book/book.rs similarity index 93% rename from src/loader/book.rs rename to src/book/book.rs index 803c402045..6bbb47bfd3 100644 --- a/src/loader/book.rs +++ b/src/book/book.rs @@ -3,10 +3,28 @@ use std::collections::VecDeque; use std::fs::File; use std::io::Read; -use loader::summary::{Summary, Link, SummaryItem, SectionNumber}; +use super::summary::{Summary, Link, SummaryItem, SectionNumber}; use errors::*; +/// Load a book into memory from its `src/` directory. +pub fn load_book>(src_dir: P) -> 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) +} + + /// A dumb tree structure representing a book. /// /// For the moment a book is just a collection of `BookItems`. diff --git a/src/book/mod.rs b/src/book/mod.rs index e8896e0aca..35a5cfb7b4 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -1,6 +1,8 @@ pub mod bookitem; +pub mod book; +pub mod summary; -pub use self::bookitem::{BookItem, BookItems}; +use self::book::{parse_book, Book, BookItem, BookItems}; use std::path::{Path, PathBuf}; use std::fs::{self, File}; diff --git a/src/loader/summary.rs b/src/book/summary.rs similarity index 100% rename from src/loader/summary.rs rename to src/book/summary.rs diff --git a/src/lib.rs b/src/lib.rs index 97af9a4fc7..ec6e76b52f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,17 +90,15 @@ extern crate tempdir; #[cfg(test)] extern crate tempdir; -mod parse; mod preprocess; pub mod book; pub mod config; pub mod renderer; pub mod theme; pub mod utils; -pub mod loader; pub use book::MDBook; -pub use book::BookItem; +pub use book::Book; pub use renderer::Renderer; /// The error types used through out this crate. diff --git a/src/loader/mod.rs b/src/loader/mod.rs deleted file mode 100644 index 0209497feb..0000000000 --- a/src/loader/mod.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Functionality for loading the internal book representation from disk. -//! -//! The typical use case is to create a `Loader` pointing at the correct -//! source directory then call the `load()` method. Internally this will -//! search for the `SUMMARY.md` file, parse it, then use the parsed -//! `Summary` to construct an in-memory representation of the entire book. -//! -//! # Examples -//! -//! ```rust,no_run -//! # fn run() -> mdbook::errors::Result<()> { -//! use mdbook::loader::load_book; -//! let book = load_book("./src/")?; -//! # Ok(()) -//! # } -//! # fn main() { run().unwrap() } -//! ``` - -#![deny(missing_docs)] - -use std::path::Path; -use std::fs::File; -use std::io::Read; -use errors::*; - -mod summary; -mod book; - -pub use self::book::{Book, BookItems, BookItem, Chapter}; -pub use self::summary::SectionNumber; - -use self::book::load_book_from_disk; -use self::summary::parse_summary; - -/// Load a book into memory from its `src/` directory. -pub fn load_book>(src_dir: P) -> 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) -} diff --git a/src/parse/mod.rs b/src/parse/mod.rs deleted file mode 100644 index c8c8aab7d4..0000000000 --- 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 cc8452d441..0000000000 --- a/src/parse/summary.rs +++ /dev/null @@ -1,231 +0,0 @@ -use std::path::PathBuf; -use std::fs::File; -use std::io::{Read, Result, Error, ErrorKind}; -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 7ca198a7009cfbc91a033df43d3eaf533e6f0f26 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 21 Aug 2017 10:23:52 +0800 Subject: [PATCH 07/21] Moved most of MDBook over to using the new Book format --- src/book/book.rs | 2 +- src/book/mod.rs | 28 ++++++++++++---------------- src/lib.rs | 2 +- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/book/book.rs b/src/book/book.rs index 6bbb47bfd3..210a8ae499 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -3,7 +3,7 @@ use std::collections::VecDeque; use std::fs::File; use std::io::Read; -use super::summary::{Summary, Link, SummaryItem, SectionNumber}; +use super::summary::{parse_summary, Summary, Link, SummaryItem, SectionNumber}; use errors::*; diff --git a/src/book/mod.rs b/src/book/mod.rs index 35a5cfb7b4..77987b4099 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -2,7 +2,7 @@ pub mod bookitem; pub mod book; pub mod summary; -use self::book::{parse_book, Book, BookItem, BookItems}; +use self::book::{load_book, Book, BookItem, BookItems}; use std::path::{Path, PathBuf}; use std::fs::{self, File}; @@ -10,7 +10,7 @@ use std::io::{Read, Write}; use std::process::Command; use tempdir::TempDir; -use {theme, parse, utils}; +use {theme, utils}; use renderer::{Renderer, HtmlHandlebars}; use preprocess; use errors::*; @@ -23,7 +23,7 @@ use config::jsonconfig::JsonConfig; pub struct MDBook { config: BookConfig, - pub content: Vec, + pub content: Option, renderer: Box, livereload: Option, @@ -71,7 +71,7 @@ impl MDBook { MDBook { config: BookConfig::new(root), - content: vec![], + content: None, renderer: Box::new(HtmlHandlebars::new()), livereload: None, @@ -109,11 +109,8 @@ impl MDBook { /// ``` pub fn iter(&self) -> BookItems { - BookItems { - items: &self.content[..], - current_index: 0, - stack: Vec::new(), - } + self.content.expect("Trying to iterate over a book before it is loaded. This is a bug") + .iter() } /// `init()` creates some boilerplate files and directories @@ -176,9 +173,8 @@ impl MDBook { for item in self.iter() { debug!("[*]: item: {:?}", item); let ch = match *item { - BookItem::Spacer => continue, - BookItem::Chapter(_, ref ch) | - BookItem::Affix(ref ch) => ch, + BookItem::Separator => continue, + BookItem::Chapter(ref ch) => ch, }; if !ch.path.as_os_str().is_empty() { let path = self.config.get_source().join(&ch.path); @@ -365,8 +361,8 @@ impl MDBook { let temp_dir = TempDir::new("mdbook")?; for item in self.iter() { - if let BookItem::Chapter(_, ref ch) = *item { - if !ch.path.as_os_str().is_empty() { + if let BookItem::Chapter(ref ch) = *item { + if ch.path != PathBuf::new() { let path = self.get_source().join(&ch.path); let base = path.parent().ok_or_else( @@ -517,8 +513,8 @@ impl MDBook { // Construct book fn parse_summary(&mut self) -> Result<()> { - // When append becomes stable, use self.content.append() ... - self.content = parse::construct_bookitems(&self.get_source().join("SUMMARY.md"))?; + let book = load_book(self.get_source().join("SUMMARY.md"))?; + self.content = Some(book); Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index ec6e76b52f..c8a684a448 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,7 +98,7 @@ pub mod theme; pub mod utils; pub use book::MDBook; -pub use book::Book; +pub use book::book::Book; pub use renderer::Renderer; /// The error types used through out this crate. From c4da845974c3219319b2841d6acdb917e43b9745 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 21 Aug 2017 10:25:50 +0800 Subject: [PATCH 08/21] Removed old bookitem.md, now everyone uses the correct BookItem --- src/book/bookitem.rs | 87 -------------------- src/book/mod.rs | 1 - src/renderer/html_handlebars/hbs_renderer.rs | 30 ++++--- 3 files changed, 19 insertions(+), 99 deletions(-) delete mode 100644 src/book/bookitem.rs diff --git a/src/book/bookitem.rs b/src/book/bookitem.rs deleted file mode 100644 index 7fe7ab5528..0000000000 --- a/src/book/bookitem.rs +++ /dev/null @@ -1,87 +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/mod.rs b/src/book/mod.rs index 77987b4099..a85c2257e9 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -1,4 +1,3 @@ -pub mod bookitem; pub mod book; pub mod summary; diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index d2b4677921..b845786868 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -2,10 +2,11 @@ use renderer::html_handlebars::helpers; use preprocess; use renderer::Renderer; use book::MDBook; -use book::bookitem::{BookItem, Chapter}; use config::PlaypenConfig; -use {utils, theme}; use theme::{Theme, playpen_editor}; +use book::book::{BookItem, Chapter}; +use utils; +use theme::{self, Theme}; use errors::*; use regex::{Regex, Captures}; @@ -32,8 +33,22 @@ impl HtmlHandlebars { -> Result<()> { // FIXME: This should be made DRY-er and rely less on mutable state match *item { +<<<<<<< HEAD BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) if !ch.path.as_os_str().is_empty() => { +======= + BookItem::Chapter(ref ch) => { + if ch.path != PathBuf::new() { + + let path = ctx.book.get_source().join(&ch.path); + + debug!("[*]: Opening file: {:?}", path); + let mut f = File::open(&path)?; + let mut content: String = String::new(); + + debug!("[*]: Reading file"); + f.read_to_string(&mut content)?; +>>>>>>> Removed old bookitem.md, now everyone uses the correct BookItem let path = ctx.book.get_source().join(&ch.path); let content = utils::fs::file_to_string(&path)?; @@ -395,14 +410,7 @@ fn make_data(book: &MDBook) -> Result let mut chapter = BTreeMap::new(); match *item { - BookItem::Affix(ref ch) => { - 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::Chapter(ref s, ref ch) => { + BookItem::Chapter(ref ch) => { chapter.insert("section".to_owned(), json!(s)); chapter.insert("name".to_owned(), json!(ch.name)); let path = ch.path.to_str().ok_or_else(|| { @@ -410,7 +418,7 @@ fn make_data(book: &MDBook) -> Result })?; chapter.insert("path".to_owned(), json!(path)); }, - BookItem::Spacer => { + BookItem::Separator => { chapter.insert("spacer".to_owned(), json!("_spacer_")); }, From d39352aa45e30d28e23f396fb0561ffdeac50cb0 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 21 Aug 2017 10:46:29 +0800 Subject: [PATCH 09/21] Unit tests now all pass --- Cargo.toml | 7 ++--- src/book/book.rs | 32 +++++++++++++------- src/book/mod.rs | 2 +- src/lib.rs | 3 ++ src/renderer/html_handlebars/hbs_renderer.rs | 2 +- tests/loading.rs | 2 +- 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f5d39a1c01..bf4bded078 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,10 +40,6 @@ iron = { version = "0.5", optional = true } staticfile = { version = "0.4", optional = true } ws = { version = "0.7", optional = true} -# Tests -[dev-dependencies] -tempdir = "0.3.4" - [build-dependencies] error-chain = "0.11" @@ -59,3 +55,6 @@ serve = ["iron", "staticfile", "ws"] doc = false name = "mdbook" path = "src/bin/mdbook.rs" + +[dev-dependencies] +pretty_assertions = "0.2.1" \ No newline at end of file diff --git a/src/book/book.rs b/src/book/book.rs index 210a8ae499..6e4cecaccf 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use std::collections::VecDeque; use std::fs::File; use std::io::Read; @@ -67,14 +67,17 @@ pub struct Chapter { 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) -> Chapter { + pub fn new>(name: &str, content: String, path: P) -> Chapter { Chapter { name: name.to_string(), content: content, + path: path.into(), ..Default::default() } } @@ -119,14 +122,17 @@ fn load_chapter>(link: &Link, src_dir: P) -> Result { src_dir.join(&link.location) }; - let mut f = File::open(location).chain_err(|| { + 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 mut ch = Chapter::new(&link.name, content); + let stripped = location.strip_prefix(&src_dir).expect("Chapters are always inside a book"); + println!("{} {} => {}", src_dir.display(), location.display(), stripped.display()); + + let mut ch = Chapter::new(&link.name, content, stripped); ch.number = link.number.clone(); let sub_items = link.nested_items @@ -223,7 +229,7 @@ And here is some more text. #[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()); + 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); @@ -239,18 +245,20 @@ And here is some more text. #[test] fn load_recursive_link_with_separators() { - let (root, _temp) = nested_links(); + 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, @@ -258,13 +266,13 @@ And here is some more text. ], }); - let got = load_summary_item(&SummaryItem::Link(root), "").unwrap(); + let got = load_summary_item(&SummaryItem::Link(root), temp.path()).unwrap(); assert_eq!(got, should_be); } #[test] fn load_a_book_with_a_single_chapter() { - let (link, _temp) = dummy_link(); + let (link, temp) = dummy_link(); let summary = Summary { numbered_chapters: vec![SummaryItem::Link(link)], ..Default::default() @@ -274,12 +282,13 @@ And here is some more text. 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, "").unwrap(); + let got = load_book_from_disk(&summary, temp.path()).unwrap(); assert_eq!(got, should_be); } @@ -312,10 +321,11 @@ And here is some more text. 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())), + BookItem::Chapter(Chapter::new("Hello World", String::new(), "Chapter_1/hello.md")), BookItem::Separator, - BookItem::Chapter(Chapter::new("Goodbye World", String::new())), + BookItem::Chapter(Chapter::new("Goodbye World", String::new(), "Chapter_1/goodbye.md")), ], }), BookItem::Separator, diff --git a/src/book/mod.rs b/src/book/mod.rs index a85c2257e9..2fc71ff85c 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -108,7 +108,7 @@ impl MDBook { /// ``` pub fn iter(&self) -> BookItems { - self.content.expect("Trying to iterate over a book before it is loaded. This is a bug") + self.content.as_ref().expect("Trying to iterate over a book before it is loaded. This is a bug") .iter() } diff --git a/src/lib.rs b/src/lib.rs index c8a684a448..ef94b0b430 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,9 @@ extern crate tempdir; #[cfg(test)] extern crate tempdir; +#[cfg(test)] +#[macro_use] +extern crate pretty_assertions; mod preprocess; pub mod book; diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index b845786868..81d06dc1b4 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -411,7 +411,7 @@ fn make_data(book: &MDBook) -> Result match *item { BookItem::Chapter(ref ch) => { - chapter.insert("section".to_owned(), json!(s)); + chapter.insert("section".to_owned(), json!(ch.number.clone())); 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") diff --git a/tests/loading.rs b/tests/loading.rs index 50ff4f1006..f3ce86a311 100644 --- a/tests/loading.rs +++ b/tests/loading.rs @@ -5,7 +5,7 @@ extern crate env_logger; use std::path::PathBuf; -use mdbook::loader::load_book; +use mdbook::book::book::load_book; #[test] From 3a51e4a27cd81f86d51e6197289aff2fe008c0c4 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 21 Aug 2017 11:34:34 +0800 Subject: [PATCH 10/21] Everything compiles and all the tests pass. --- src/book/book.rs | 12 ++- src/book/mod.rs | 82 +++++++------------- src/book/summary.rs | 1 + src/lib.rs | 2 - src/renderer/html_handlebars/hbs_renderer.rs | 45 ++++------- 5 files changed, 56 insertions(+), 86 deletions(-) diff --git a/src/book/book.rs b/src/book/book.rs index 6e4cecaccf..3acb697f81 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -1,3 +1,4 @@ +use std::fmt::{self, Display, Formatter}; use std::path::{Path, PathBuf}; use std::collections::VecDeque; use std::fs::File; @@ -130,7 +131,6 @@ fn load_chapter>(link: &Link, src_dir: P) -> Result { f.read_to_string(&mut content)?; let stripped = location.strip_prefix(&src_dir).expect("Chapters are always inside a book"); - println!("{} {} => {}", src_dir.display(), location.display(), stripped.display()); let mut ch = Chapter::new(&link.name, content, stripped); ch.number = link.number.clone(); @@ -174,6 +174,16 @@ impl<'a> Iterator for BookItems<'a> { } } +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 { diff --git a/src/book/mod.rs b/src/book/mod.rs index 2fc71ff85c..e8f583439a 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -85,17 +85,17 @@ impl MDBook { /// ```no_run /// # extern crate mdbook; /// # use mdbook::MDBook; - /// # use mdbook::BookItem; + /// # use mdbook::book::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) => println!("{}", chapter), + /// BookItem::Separator => {}, /// } /// } + /// panic!(); /// /// // would print something like this: /// // 1. Chapter 1 @@ -131,68 +131,44 @@ impl MDBook { debug!("[fn]: init"); - if !self.config.get_root().exists() { - fs::create_dir_all(&self.config.get_root()).unwrap(); - info!("{:?} created", &self.config.get_root()); - } - { - - if !self.get_destination().exists() { - debug!("[*]: {:?} does not exist, trying to create directory", self.get_destination()); - fs::create_dir_all(self.get_destination())?; - } - - - if !self.config.get_source().exists() { - debug!("[*]: {:?} does not exist, trying to create directory", self.config.get_source()); - fs::create_dir_all(self.config.get_source())?; + let root = self.config.get_root(); + let dest = self.get_destination(); + let src = self.config.get_source(); + + let necessary_folders = &[root, dest, src]; + + for folder in necessary_folders { + if !folder.exists() { + fs::create_dir_all(folder)?; + debug!("{} created", folder.display()); + } } - let summary = self.config.get_source().join("SUMMARY.md"); + let summary = src.join("SUMMARY.md"); if !summary.exists() { + debug!("[*]: Creating SUMMARY.md"); - // 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"); - writeln!(f, "# Summary")?; - writeln!(f, "")?; + writeln!(f)?; writeln!(f, "- [Chapter 1](./chapter_1.md)")?; } - } - - // 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::Separator => continue, - BookItem::Chapter(ref ch) => ch, - }; - if !ch.path.as_os_str().is_empty() { - let path = self.config.get_source().join(&ch.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)?; + let ch_1 = src.join("chapter_1.md"); + if !ch_1.exists() { + debug!("[*] Creating {}", ch_1.display()); - // debug!("[*]: Writing to {:?}", path); - writeln!(f, "# {}", ch.name)?; - } + let mut f = File::create(&ch_1)?; + writeln!(f, "# Chapter 1")?; } } + // parse SUMMARY.md and load the newly created files into memory + self.parse_summary().chain_err(|| "Couldn't parse the SUMMARY.md file")?; + debug!("[*]: init done"); Ok(()) } @@ -512,7 +488,9 @@ impl MDBook { // Construct book fn parse_summary(&mut self) -> Result<()> { - let book = load_book(self.get_source().join("SUMMARY.md"))?; + let src = self.config.get_source(); + let book = load_book(&src)?; + self.content = Some(book); Ok(()) } diff --git a/src/book/summary.rs b/src/book/summary.rs index c2dfe03f3b..f02942fed4 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -264,6 +264,7 @@ impl<'a> SummaryParser<'a> { 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), } diff --git a/src/lib.rs b/src/lib.rs index ef94b0b430..b4fee9a94b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,8 +87,6 @@ extern crate serde; extern crate serde_json; extern crate tempdir; -#[cfg(test)] -extern crate tempdir; #[cfg(test)] #[macro_use] extern crate pretty_assertions; diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 81d06dc1b4..1286d3aab4 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -3,10 +3,9 @@ use preprocess; use renderer::Renderer; use book::MDBook; use config::PlaypenConfig; -use theme::{Theme, playpen_editor}; +use theme::{self, Theme, playpen_editor}; use book::book::{BookItem, Chapter}; use utils; -use theme::{self, Theme}; use errors::*; use regex::{Regex, Captures}; @@ -33,25 +32,9 @@ impl HtmlHandlebars { -> Result<()> { // FIXME: This should be made DRY-er and rely less on mutable state match *item { -<<<<<<< HEAD - BookItem::Chapter(_, ref ch) | - BookItem::Affix(ref ch) if !ch.path.as_os_str().is_empty() => { -======= BookItem::Chapter(ref ch) => { - if ch.path != PathBuf::new() { - - let path = ctx.book.get_source().join(&ch.path); - - debug!("[*]: Opening file: {:?}", path); - let mut f = File::open(&path)?; - let mut content: String = String::new(); - - debug!("[*]: Reading file"); - f.read_to_string(&mut content)?; ->>>>>>> Removed old bookitem.md, now everyone uses the correct BookItem - let path = ctx.book.get_source().join(&ch.path); - let content = utils::fs::file_to_string(&path)?; + let content = ch.content.clone(); let base = path.parent().ok_or_else( || String::from("Invalid bookitem path!"), )?; @@ -62,9 +45,7 @@ impl HtmlHandlebars { print_content.push_str(&content); // Update the context with data for this file - let path = ch.path.to_str().ok_or_else(|| { - io::Error::new(io::ErrorKind::Other, "Could not convert path to str") - })?; + let path = ch.path.to_str().ok_or_else(|| Error::from("Could not convert path to str"))?; // Non-lexical lifetimes needed :'( let title: String; @@ -407,24 +388,26 @@ fn make_data(book: &MDBook) -> Result for item in book.iter() { // Create the data to inject in the template - let mut chapter = BTreeMap::new(); + let mut chapter_data = BTreeMap::new(); match *item { BookItem::Chapter(ref ch) => { - chapter.insert("section".to_owned(), json!(ch.number.clone())); - 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)); + if let Some(ref section_number) = ch.number { + chapter_data.insert("section".to_owned(), json!(section_number.to_string())); + } + + chapter_data.insert("name".to_owned(), json!(ch.name)); + let path = ch.path.to_str() + .ok_or_else(|| Error::from("Could not convert path to str"))?; + chapter_data.insert("path".to_owned(), json!(path)); }, BookItem::Separator => { - chapter.insert("spacer".to_owned(), json!("_spacer_")); + chapter_data.insert("spacer".to_owned(), json!("_spacer_")); }, } - chapters.push(chapter); + chapters.push(chapter_data); } data.insert("chapters".to_owned(), json!(chapters)); From 782183512987ba948756c84186bd49ba447f3ca9 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 21 Aug 2017 12:35:18 +0800 Subject: [PATCH 11/21] Reverted some churn (cheers cargo-edit) and added myself to contributors --- Cargo.toml | 2 +- book-example/src/misc/contributors.md | 1 + src/lib.rs | 3 +++ src/renderer/html_handlebars/hbs_renderer.rs | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bf4bded078..66398ea3c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,4 +57,4 @@ name = "mdbook" path = "src/bin/mdbook.rs" [dev-dependencies] -pretty_assertions = "0.2.1" \ No newline at end of file +pretty_assertions = "0.2.1" diff --git a/book-example/src/misc/contributors.md b/book-example/src/misc/contributors.md index 36b50fad24..6c91f660fd 100644 --- a/book-example/src/misc/contributors.md +++ b/book-example/src/misc/contributors.md @@ -11,3 +11,4 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add - Wayne Nilsen ([waynenilsen](https://github.com/waynenilsen)) - [funnkill](https://github.com/funkill) - Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang)) +- Michael Bryan ([Michael-F-Bryan](https://github.com/Michael-F-Bryan)) diff --git a/src/lib.rs b/src/lib.rs index b4fee9a94b..b3c373dbe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,9 @@ pub use renderer::Renderer; /// The error types used through out this crate. pub mod errors { + // needed temporarily because of https://github.com/rust-lang-nursery/error-chain/issues/208 + #![allow(unknown_lints, unused_doc_comment)] + error_chain!{ foreign_links { Io(::std::io::Error); diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 1286d3aab4..0b2138bb74 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -12,7 +12,7 @@ use regex::{Regex, Captures}; use std::ascii::AsciiExt; use std::path::{Path, PathBuf}; use std::fs::{self, File}; -use std::io::{self, Read}; +use std::io::Read; use std::collections::BTreeMap; use std::collections::HashMap; From 1826fbd65e005ad6cb16dda19bc76fbf3b7ff272 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 21 Aug 2017 22:59:19 +0800 Subject: [PATCH 12/21] Removed an unused function --- src/book/book.rs | 8 ++++---- src/book/mod.rs | 14 ++++++++++---- src/book/summary.rs | 5 ----- src/lib.rs | 2 +- src/renderer/html_handlebars/hbs_renderer.rs | 2 +- tests/loading.rs | 2 +- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/book/book.rs b/src/book/book.rs index 3acb697f81..61e8990900 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -88,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. -pub fn load_book_from_disk>(summary: &Summary, src_dir: P) -> 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(); @@ -229,9 +229,9 @@ And here is some more text. let mut second = Link::new("Nested Chapter 1", &second_path); second.number = Some(SectionNumber(vec![1, 2])); - root.push_item(second.clone()); - root.push_item(SummaryItem::Separator); - root.push_item(second.clone()); + root.nested_items.push(second.clone().into()); + root.nested_items.push(SummaryItem::Separator); + root.nested_items.push(second.clone().into()); (root, temp_dir) } diff --git a/src/book/mod.rs b/src/book/mod.rs index e8f583439a..b78fc2639e 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -1,7 +1,10 @@ -pub mod book; -pub mod summary; +//! The internal representation of a `Book`. -use self::book::{load_book, Book, BookItem, BookItems}; +mod book; +mod summary; + +pub use self::book::{load_book, Book, BookItem, BookItems, Chapter}; +pub use self::summary::SectionNumber; use std::path::{Path, PathBuf}; use std::fs::{self, File}; @@ -19,6 +22,9 @@ use config::tomlconfig::TomlConfig; use config::htmlconfig::HtmlConfig; use config::jsonconfig::JsonConfig; + +/// A helper for managing the `Book`, its configuration, and the rendering +/// process. pub struct MDBook { config: BookConfig, @@ -85,7 +91,7 @@ impl MDBook { /// ```no_run /// # extern crate mdbook; /// # use mdbook::MDBook; - /// # use mdbook::book::book::BookItem; + /// # use mdbook::book::BookItem; /// # #[allow(unused_variables)] /// # fn main() { /// # let book = MDBook::new("mybook"); diff --git a/src/book/summary.rs b/src/book/summary.rs index f02942fed4..009b33e134 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -86,11 +86,6 @@ impl Link { nested_items: Vec::new(), } } - - /// Add an item to this link's `nested_items`. - pub fn push_item>(&mut self, item: I) { - self.nested_items.push(item.into()); - } } impl Default for Link { diff --git a/src/lib.rs b/src/lib.rs index b3c373dbe4..242437d0ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,7 +99,7 @@ pub mod theme; pub mod utils; pub use book::MDBook; -pub use book::book::Book; +pub use book::Book; pub use renderer::Renderer; /// The error types used through out this crate. diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 0b2138bb74..d90ad75458 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -4,7 +4,7 @@ use renderer::Renderer; use book::MDBook; use config::PlaypenConfig; use theme::{self, Theme, playpen_editor}; -use book::book::{BookItem, Chapter}; +use book::{BookItem, Chapter}; use utils; use errors::*; use regex::{Regex, Captures}; diff --git a/tests/loading.rs b/tests/loading.rs index f3ce86a311..356c7e81ba 100644 --- a/tests/loading.rs +++ b/tests/loading.rs @@ -5,7 +5,7 @@ extern crate env_logger; use std::path::PathBuf; -use mdbook::book::book::load_book; +use mdbook::book::load_book; #[test] From b530b673c9f6a7eca98de6ada20626fe07e81394 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Fri, 25 Aug 2017 22:39:59 +0800 Subject: [PATCH 13/21] Fixed up Cargo.toml --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 66398ea3c9..6218d5f74b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,9 @@ ws = { version = "0.7", optional = true} [build-dependencies] error-chain = "0.11" +[dev-dependencies] +pretty_assertions = "0.2.1" + [features] default = ["output", "watch", "serve"] debug = [] @@ -55,6 +58,3 @@ serve = ["iron", "staticfile", "ws"] doc = false name = "mdbook" path = "src/bin/mdbook.rs" - -[dev-dependencies] -pretty_assertions = "0.2.1" From af8a5483b8a59c460d27f39033654b9601bb97e3 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 28 Aug 2017 03:09:47 +0800 Subject: [PATCH 14/21] Added a flag to create missing files --- src/book/book.rs | 61 ++++++++++++++++++++++++++++++++++-------------- src/book/mod.rs | 2 +- tests/loading.rs | 2 +- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/book/book.rs b/src/book/book.rs index 61e8990900..0991427193 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -2,14 +2,14 @@ use std::fmt::{self, Display, Formatter}; use std::path::{Path, PathBuf}; use std::collections::VecDeque; use std::fs::File; -use std::io::Read; +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) -> Result { +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"); @@ -18,11 +18,9 @@ pub fn load_book>(src_dir: P) -> Result { .chain_err(|| "Couldn't open SUMMARY.md")? .read_to_string(&mut summary_content)?; - let summary = parse_summary(&summary_content).chain_err( - || "Summary parsing failed", - )?; + let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?; - load_book_from_disk(&summary, src_dir) + load_book_from_disk(&summary, src_dir, create_if_not_present) } @@ -88,7 +86,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) -> Result { +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(); @@ -98,18 +96,29 @@ fn load_book_from_disk>(summary: &Summary, src_dir: P) -> Result< let summary_items = prefix.chain(numbered).chain(suffix); - let chapters = summary_items - .map(|i| load_summary_item(i, src_dir)) - .collect::>() - .chain_err(|| "Couldn't load chapters from disk")?; + 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) -> Result { +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) => load_chapter(link, src_dir).map(|c| BookItem::Chapter(c)), + 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)) + }, } } @@ -137,7 +146,7 @@ fn load_chapter>(link: &Link, src_dir: P) -> Result { let sub_items = link.nested_items .iter() - .map(|i| load_summary_item(i, src_dir)) + .map(|i| load_summary_item(i, src_dir, false)) .collect::>>()?; ch.sub_items = sub_items; @@ -190,6 +199,7 @@ mod tests { use super::*; use tempdir::TempDir; use std::io::Write; + use std::fs; const DUMMY_SRC: &'static str = " # Dummy Chapter @@ -276,7 +286,7 @@ And here is some more text. ], }); - let got = load_summary_item(&SummaryItem::Link(root), temp.path()).unwrap(); + let got = load_summary_item(&SummaryItem::Link(root), temp.path(), false).unwrap(); assert_eq!(got, should_be); } @@ -298,7 +308,7 @@ And here is some more text. ], }; - let got = load_book_from_disk(&summary, temp.path()).unwrap(); + let got = load_book_from_disk(&summary, temp.path(), false).unwrap(); assert_eq!(got, should_be); } @@ -362,4 +372,21 @@ And here is some more text. assert_eq!(chapter_names, should_be); } -} \ No newline at end of file + + #[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 b78fc2639e..60b583dd75 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -495,7 +495,7 @@ impl MDBook { // Construct book fn parse_summary(&mut self) -> Result<()> { let src = self.config.get_source(); - let book = load_book(&src)?; + let book = load_book(&src, self.create_missing)?; self.content = Some(book); Ok(()) diff --git a/tests/loading.rs b/tests/loading.rs index 356c7e81ba..beaa219774 100644 --- a/tests/loading.rs +++ b/tests/loading.rs @@ -16,6 +16,6 @@ fn load_the_example_book() { .join("book-example") .join("src"); - let book = load_book(example_src_dir).unwrap(); + let book = load_book(example_src_dir, false).unwrap(); println!("{:#?}", book); } From 98a8ce934b102def15b14ae759996a6bf0e0ee7e Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 28 Aug 2017 03:15:11 +0800 Subject: [PATCH 15/21] Made non-existent file creation opt-in instead of opt-out --- src/bin/build.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bin/build.rs b/src/bin/build.rs index 1a296ee543..0fe950f095 100644 --- a/src/bin/build.rs +++ b/src/bin/build.rs @@ -9,7 +9,7 @@ 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'") + .arg_from_usage("--create 'Will create non-existent files linked from SUMMARY.md'") .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'") } @@ -24,8 +24,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> { None => book, }; - if args.is_present("no-create") { - book.create_missing = false; + if args.is_present("create") { + book.create_missing = true; } if args.is_present("curly-quotes") { From f8cec4cef345b72a0b90a83a5d745772cbd02de5 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Thu, 31 Aug 2017 18:00:21 +0800 Subject: [PATCH 16/21] Incorporated Budziq's feedback --- Cargo.toml | 2 +- src/bin/build.rs | 4 +- src/book/mod.rs | 132 ++++++++++++++++++++++++-------------------- src/book/summary.rs | 5 ++ 4 files changed, 79 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6218d5f74b..1c74008a13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ ws = { version = "0.7", optional = true} error-chain = "0.11" [dev-dependencies] -pretty_assertions = "0.2.1" +pretty_assertions = "0.2" [features] default = ["output", "watch", "serve"] diff --git a/src/bin/build.rs b/src/bin/build.rs index 0fe950f095..6e311e519c 100644 --- a/src/bin/build.rs +++ b/src/bin/build.rs @@ -24,9 +24,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { None => book, }; - if args.is_present("create") { - book.create_missing = true; - } + book.create_missing = args.is_present("create"); if args.is_present("curly-quotes") { book = book.with_curly_quotes(true); diff --git a/src/book/mod.rs b/src/book/mod.rs index 60b583dd75..75319b0248 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -13,7 +13,7 @@ use std::process::Command; use tempdir::TempDir; use {theme, utils}; -use renderer::{Renderer, HtmlHandlebars}; +use renderer::{HtmlHandlebars, Renderer}; use preprocess; use errors::*; @@ -23,7 +23,10 @@ use config::htmlconfig::HtmlConfig; use config::jsonconfig::JsonConfig; -/// A helper for managing the `Book`, its configuration, and the rendering +const STUB_SUMMARY_CONTENTS: &'static str = "# Summary\n\n- [Chapter 1](./chapter_1.md)]"; +const STUB_CHAPTER_1: &'static str = "# Chapter 1\n"; + +/// A helper for managing the `Book`, its configuration, and the rendering /// process. pub struct MDBook { config: BookConfig, @@ -67,7 +70,6 @@ impl MDBook { /// [`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); @@ -114,8 +116,10 @@ impl MDBook { /// ``` pub fn iter(&self) -> BookItems { - self.content.as_ref().expect("Trying to iterate over a book before it is loaded. This is a bug") - .iter() + self.content + .as_ref() + .expect("Trying to iterate over a book before it is loaded. This is a bug") + .iter() } /// `init()` creates some boilerplate files and directories @@ -132,64 +136,73 @@ impl MDBook { /// It uses the paths given as source and output directories /// and adds a `SUMMARY.md` and a /// `chapter_1.md` to the source directory. - pub fn init(&mut self) -> Result<()> { - debug!("[fn]: init"); - { - let root = self.config.get_root(); - let dest = self.get_destination(); - let src = self.config.get_source(); - - let necessary_folders = &[root, dest, src]; - - for folder in necessary_folders { - if !folder.exists() { - fs::create_dir_all(folder)?; - debug!("{} created", folder.display()); - } - } + self.create_book_directories()?; + self.create_stub_files()?; - let summary = src.join("SUMMARY.md"); + self.parse_summary() + .chain_err(|| "Couldn't parse the SUMMARY.md file")?; - if !summary.exists() { - debug!("[*]: Creating SUMMARY.md"); + debug!("[*]: init done"); + Ok(()) + } - let mut f = File::create(&summary)?; + fn create_book_directories(&self) -> Result<()> { + debug!("[*] Creating directories"); - writeln!(f, "# Summary")?; - writeln!(f)?; - writeln!(f, "- [Chapter 1](./chapter_1.md)")?; - } + let root = self.config.get_root(); + let dest = self.get_destination(); + let src = self.config.get_source(); - let ch_1 = src.join("chapter_1.md"); - if !ch_1.exists() { - debug!("[*] Creating {}", ch_1.display()); + let necessary_folders = &[root, dest, src]; - let mut f = File::create(&ch_1)?; - writeln!(f, "# Chapter 1")?; + for folder in necessary_folders { + if !folder.exists() { + fs::create_dir_all(folder)?; + debug!("{} created", folder.display()); } } - // parse SUMMARY.md and load the newly created files into memory - self.parse_summary().chain_err(|| "Couldn't parse the SUMMARY.md file")?; + Ok(()) + } + + fn create_stub_files(&self) -> Result<()> { + debug!("[*] Creating stub files"); + + let src = self.config.get_source(); + let summary = src.join("SUMMARY.md"); + + if !summary.exists() { + debug!("[*]: Creating SUMMARY.md"); + let mut f = File::create(&summary)?; + writeln!(f, "{}", STUB_SUMMARY_CONTENTS)?; + } + + let ch_1 = src.join("chapter_1.md"); + if !ch_1.exists() { + debug!("[*] Creating {}", ch_1.display()); + + let mut f = File::create(&ch_1)?; + writeln!(f, "{}", STUB_CHAPTER_1)?; + } - debug!("[*]: init done"); Ok(()) } pub fn create_gitignore(&self) { let gitignore = self.get_gitignore(); - let destination = self.config.get_html_config() - .get_destination(); + let destination = self.config.get_html_config().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` + // Check that the gitignore does not exist 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.config.get_root()) { - let relative = destination .strip_prefix(self.config.get_root()) .expect("Could not strip the root prefix, path is not relative to root") @@ -223,7 +236,6 @@ impl MDBook { self.renderer.render(self) } - pub fn get_gitignore(&self) -> PathBuf { self.config.get_root().join(".gitignore") } @@ -265,8 +277,7 @@ impl MDBook { } pub fn write_file>(&self, filename: P, content: &[u8]) -> Result<()> { - let path = self.get_destination() - .join(filename); + let path = self.get_destination().join(filename); utils::fs::create_file(&path)? .write_all(content) @@ -279,7 +290,6 @@ impl MDBook { /// The root directory is the one specified when creating a new `MDBook` pub fn read_config(mut self) -> Result { - let toml = self.get_root().join("book.toml"); let json = self.get_root().join("book.json"); @@ -335,31 +345,33 @@ 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()) - .flat_map(|x| vec![x.0, x.1]) - .collect(); + let library_args: Vec<&str> = (0..library_paths.len()) + .map(|_| "-L") + .zip(library_paths.into_iter()) + .flat_map(|x| vec![x.0, x.1]) + .collect(); let temp_dir = TempDir::new("mdbook")?; for item in self.iter() { - if let BookItem::Chapter(ref ch) = *item { if ch.path != PathBuf::new() { - let path = self.get_source().join(&ch.path); - let base = path.parent().ok_or_else( - || String::from("Invalid bookitem path!"), - )?; + let base = path.parent() + .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)?; println!("[*]: Testing file: {:?}", path); - //write preprocessed file to tempdir + // write preprocessed file to tempdir let path = temp_dir.path().join(&ch.path); 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)); @@ -377,15 +389,15 @@ impl MDBook { pub fn with_destination>(mut self, destination: T) -> Self { let root = self.config.get_root().to_owned(); - self.config.get_mut_html_config() + self.config + .get_mut_html_config() .set_destination(&root, &destination.into()); self } pub fn get_destination(&self) -> &Path { - self.config.get_html_config() - .get_destination() + self.config.get_html_config().get_destination() } pub fn with_source>(mut self, source: T) -> Self { diff --git a/src/book/summary.rs b/src/book/summary.rs index 009b33e134..79f4e0f3b4 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -322,6 +322,11 @@ impl<'a> SummaryParser<'a> { 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); From e89e6a04cd2316333e20446c9256c670116f5716 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Thu, 31 Aug 2017 21:20:09 +0800 Subject: [PATCH 17/21] `mdbook init` will stub out chapters if SUMMARY already exists --- src/book/mod.rs | 93 ++++++++++++++++++++++++++++++---------- tests/rendered_output.rs | 24 ++++++++++- 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index 75319b0248..8e666a4ba5 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -14,6 +14,7 @@ use tempdir::TempDir; use {theme, utils}; use renderer::{HtmlHandlebars, Renderer}; +use self::summary::SummaryItem; use preprocess; use errors::*; @@ -174,12 +175,19 @@ impl MDBook { let src = self.config.get_source(); let summary = src.join("SUMMARY.md"); - if !summary.exists() { - debug!("[*]: Creating SUMMARY.md"); - let mut f = File::create(&summary)?; - writeln!(f, "{}", STUB_SUMMARY_CONTENTS)?; + if summary.exists() && self.create_missing { + // As a special case, if we run "mdbook init" on a book which + // already has a summary we'll read that summary and create + // stubs for any files which don't already exist (@azerupi likes + // having access to this shortcut). + return create_files_from_summary(&src, &summary); } + // We need to create the summary file + debug!("[*]: Creating SUMMARY.md"); + let mut f = File::create(&summary)?; + writeln!(f, "{}", STUB_SUMMARY_CONTENTS)?; + let ch_1 = src.join("chapter_1.md"); if !ch_1.exists() { debug!("[*] Creating {}", ch_1.display()); @@ -443,61 +451,56 @@ impl MDBook { pub fn with_theme_path>(mut self, theme_path: T) -> Self { let root = self.config.get_root().to_owned(); - self.config.get_mut_html_config() + self.config + .get_mut_html_config() .set_theme(&root, &theme_path.into()); self } pub fn get_theme_path(&self) -> &Path { - self.config.get_html_config() - .get_theme() + self.config.get_html_config().get_theme() } pub fn with_curly_quotes(mut self, curly_quotes: bool) -> Self { - self.config.get_mut_html_config() + self.config + .get_mut_html_config() .set_curly_quotes(curly_quotes); self } pub fn get_curly_quotes(&self) -> bool { - self.config.get_html_config() - .get_curly_quotes() + self.config.get_html_config().get_curly_quotes() } pub fn with_mathjax_support(mut self, mathjax_support: bool) -> Self { - self.config.get_mut_html_config() + self.config + .get_mut_html_config() .set_mathjax_support(mathjax_support); self } pub fn get_mathjax_support(&self) -> bool { - self.config.get_html_config() - .get_mathjax_support() + self.config.get_html_config().get_mathjax_support() } pub fn get_google_analytics_id(&self) -> Option { - self.config.get_html_config() - .get_google_analytics_id() + self.config.get_html_config().get_google_analytics_id() } pub fn has_additional_js(&self) -> bool { - self.config.get_html_config() - .has_additional_js() + self.config.get_html_config().has_additional_js() } pub fn get_additional_js(&self) -> &[PathBuf] { - self.config.get_html_config() - .get_additional_js() + self.config.get_html_config().get_additional_js() } pub fn has_additional_css(&self) -> bool { - self.config.get_html_config() - .has_additional_css() + self.config.get_html_config().has_additional_css() } pub fn get_additional_css(&self) -> &[PathBuf] { - self.config.get_html_config() - .get_additional_css() + self.config.get_html_config().get_additional_css() } pub fn get_html_config(&self) -> &HtmlConfig { @@ -513,3 +516,47 @@ impl MDBook { Ok(()) } } + +fn create_files_from_summary(src: &Path, summary_path: &Path) -> Result<()> { + debug!("[fn]: create_files_from_summary"); + let summary = summary::parse_summary(&utils::fs::file_to_string(summary_path)?)?; + debug!("[*]: parsed existing summary"); + trace!("[*]: {:#?}", summary); + + create_list_of_stub_chapters(&summary.prefix_chapters, src)?; + create_list_of_stub_chapters(&summary.numbered_chapters, src)?; + create_list_of_stub_chapters(&summary.suffix_chapters, src)?; + + debug!("[*]: Finished creating stub chapters using for a SUMMARY.md"); + Ok(()) +} + +fn create_list_of_stub_chapters(chapters: &[SummaryItem], src_dir: &Path) -> Result<()> { + for summary_item in chapters { + if let SummaryItem::Link(ref ch) = *summary_item { + let location = src_dir.join(&ch.location); + debug!("{} + {} => {}", src_dir.display(), ch.location.display(), location.display()); + create_stub_chapter(&ch.name, &location)?; + create_list_of_stub_chapters(&ch.nested_items, src_dir)?; + } + } + + Ok(()) +} + +fn create_stub_chapter(name: &str, path: &Path) -> Result<()> { + debug!("[*]: Creating stub for \"{}\" at {}", name, path.display()); + + if let Some(parent) = path.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + } + + if !path.exists() { + let mut f = File::create(path)?; + writeln!(f, "# {}", name)?; + } + + Ok(()) +} diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 7de0838fff..c588c9f9c5 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -1,3 +1,4 @@ +extern crate env_logger; extern crate mdbook; extern crate tempdir; @@ -77,14 +78,14 @@ fn check_correct_cross_links_in_nested_dir() { assert_contains_strings( first.join("index.html"), &[ - r##"href="./first/index.html#some-section" id="some-section""## + r##"href="./first/index.html#some-section" id="some-section""##, ], ); assert_contains_strings( first.join("nested.html"), &[ - r##"href="./first/nested.html#some-section" id="some-section""## + r##"href="./first/nested.html#some-section" id="some-section""##, ], ); } @@ -106,6 +107,8 @@ fn rendered_code_has_playpen_stuff() { #[test] fn chapter_content_appears_in_rendered_document() { + env_logger::init().ok(); + let content = vec![ ("index.html", "Here's some interesting text"), ("second.html", "Second Chapter"), @@ -125,3 +128,20 @@ fn chapter_content_appears_in_rendered_document() { assert_contains_strings(path, &[text]); } } + +#[test] +fn chapter_1_file_not_created_if_summary_already_exists() { + let temp = DummyBook::default().build(); + + let src = temp.path().join("src"); + let summary = src.join("SUMMARY.md"); + let chapter_1 = src.join("chapter_1.md"); + + assert!(summary.exists()); + assert!(!chapter_1.exists()); + + let mut md = MDBook::new(temp.path()); + md.build().unwrap(); + + assert!(!chapter_1.exists()); +} From 227f40679e350d70b1c6c379c4ebd79c25fd15af Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 2 Sep 2017 18:21:37 +0800 Subject: [PATCH 18/21] Reduced some of the code duplication --- src/book/summary.rs | 104 ++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/book/summary.rs b/src/book/summary.rs index 79f4e0f3b4..a8a16623a3 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -241,9 +241,8 @@ impl<'a> SummaryParser<'a> { match self.state { State::Begin => self.step_start(next_event)?, - State::PrefixChapters => self.step_prefix(next_event)?, + State::PrefixChapters | State::SuffixChapters => self.step_affix(next_event)?, State::NumberedChapters(_) => self.step_numbered(next_event)?, - State::SuffixChapters => self.step_suffix(next_event)?, State::End => {}, } } else { @@ -266,31 +265,22 @@ impl<'a> SummaryParser<'a> { Ok(()) } - /// In the second step we look out for links and horizontal rules to add - /// to the prefix. + /// Try to step through an "affix" section (recognising prefix and suffix + /// chapters). /// - /// This state should only progress when it encounters a list. All other - /// events will either be separators (horizontal rule), prefix chapters - /// (the links), or skipped. - fn step_prefix(&mut self, event: Event<'a>) -> Result<()> { - match event { - Event::Start(Tag::Link(location, _)) => { - let content = collect_events!(self.stream, Tag::Link(_, _)); - let text = stringify_events(content); - let link = Link::new(text, location.as_ref()); + /// 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<()> { - debug!("[*] Found a prefix chapter: {:?}", link.name); - self.summary.prefix_chapters.push(SummaryItem::Link(link)); - }, + match event { + Event::Start(tag) => self.handle_start_tag_in_affix_chapter(tag)?, Event::End(Tag::Rule) => { - debug!("[*] Found a prefix chapter separator"); - self.summary.prefix_chapters.push(SummaryItem::Separator); + debug!("[*] Found an affix chapter separator"); + self.affix_chapter_list().push(SummaryItem::Separator); }, - Event::Start(Tag::List(_)) => { - debug!("[*] Changing from prefix chapters to numbered chapters"); - self.state = State::NumberedChapters(0); - }, - other => { trace!("[*] Skipping unexpected token in summary: {:?}", other); }, @@ -299,6 +289,42 @@ impl<'a> SummaryParser<'a> { 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 @@ -352,30 +378,6 @@ impl<'a> SummaryParser<'a> { Ok(()) } - fn step_suffix(&mut self, event: Event<'a>) -> Result<()> { - // FIXME: This has been copy/pasted from step_prefix. make DRY. - match event { - Event::Start(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 a suffix chapter: {:?}", link.name); - self.summary.suffix_chapters.push(SummaryItem::Link(link)); - }, - Event::End(Tag::Rule) => { - debug!("[*] Found a suffix chapter separator"); - self.summary.suffix_chapters.push(SummaryItem::Separator); - }, - other => { - trace!("[*] Skipping unexpected token in summary: {:?}", other); - }, - } - - Ok(()) - } - - /// Parse a single item (`[Some Chapter Name](./path/to/chapter.md)`). fn parse_item(&mut self) -> Result { let next = self.stream.next(); @@ -493,7 +495,7 @@ impl Display for SectionNumber { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let dotted_number: String = self.0 .iter() - .map(|i| format!("{}", i)) + .map(|i| i.to_string()) .collect::>() .join("."); @@ -527,10 +529,8 @@ mod tests { ]; for (input, should_be) in inputs { - let section_number = SectionNumber(input); - let string_repr = format!("{}", section_number); - - assert_eq!(string_repr, should_be); + let section_number = SectionNumber(input).to_string(); + assert_eq!(section_number, should_be); } } From 1bd26fb2eee0b1d6da9595da5def0cfd333b67df Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 2 Sep 2017 18:52:11 +0800 Subject: [PATCH 19/21] Fixed the rendering bug --- src/book/summary.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/book/summary.rs b/src/book/summary.rs index a8a16623a3..46f477e527 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -493,13 +493,10 @@ pub struct SectionNumber(pub Vec); impl Display for SectionNumber { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let dotted_number: String = self.0 - .iter() - .map(|i| i.to_string()) - .collect::>() - .join("."); - - write!(f, "{}", dotted_number) + for item in &self.0 { + write!(f, "{}.", item)?; + } + Ok(()) } } @@ -523,9 +520,9 @@ mod tests { #[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"), + (vec![0], "0."), + (vec![1, 3], "1.3."), + (vec![1, 2, 3], "1.2.3."), ]; for (input, should_be) in inputs { From 2bdca9e720c23dc769ed680037fda786c51514f2 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 24 Sep 2017 23:32:21 +0800 Subject: [PATCH 20/21] Fixed `mdbook build` overwriting your book with the `mdbook init` stuff --- src/bin/build.rs | 3 ++- src/book/mod.rs | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/bin/build.rs b/src/bin/build.rs index 6e311e519c..123fccc1b6 100644 --- a/src/bin/build.rs +++ b/src/bin/build.rs @@ -17,7 +17,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { // Build command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); - let book = MDBook::new(&book_dir).read_config()?; + let book = MDBook::new(&book_dir); let mut book = match args.value_of("dest-dir") { Some(dest_dir) => book.with_destination(dest_dir), @@ -30,6 +30,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { book = book.with_curly_quotes(true); } + book = book.read_config()?; book.build()?; if args.is_present("open") { diff --git a/src/book/mod.rs b/src/book/mod.rs index 8e666a4ba5..77bd6ed188 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -83,7 +83,7 @@ impl MDBook { renderer: Box::new(HtmlHandlebars::new()), livereload: None, - create_missing: true, + create_missing: false, } } @@ -175,15 +175,18 @@ impl MDBook { let src = self.config.get_source(); let summary = src.join("SUMMARY.md"); - if summary.exists() && self.create_missing { + if summary.exists() { + if self.create_missing { // As a special case, if we run "mdbook init" on a book which // already has a summary we'll read that summary and create // stubs for any files which don't already exist (@azerupi likes // having access to this shortcut). return create_files_from_summary(&src, &summary); + } else { + return Ok(()); + } } - // We need to create the summary file debug!("[*]: Creating SUMMARY.md"); let mut f = File::create(&summary)?; writeln!(f, "{}", STUB_SUMMARY_CONTENTS)?; From e9370c91697727225dc0c3f1e4b60d2a10f4104b Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 30 Sep 2017 16:14:12 +0800 Subject: [PATCH 21/21] Updated the hrefs in the integration tests --- tests/rendered_output.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index c588c9f9c5..83d2a0e220 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -40,10 +40,10 @@ fn make_sure_bottom_level_files_contain_links_to_chapters() { let dest = temp.path().join("book"); let links = vec![ r#"href="intro.html""#, - r#"href="./first/index.html""#, - r#"href="./first/nested.html""#, - r#"href="./second.html""#, - r#"href="./conclusion.html""#, + r#"href="first/index.html""#, + r#"href="first/nested.html""#, + r#"href="second.html""#, + r#"href="conclusion.html""#, ]; let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"]; @@ -63,10 +63,10 @@ fn check_correct_cross_links_in_nested_dir() { let links = vec![ r#""#, r#"href="intro.html""#, - r#"href="./first/index.html""#, - r#"href="./first/nested.html""#, - r#"href="./second.html""#, - r#"href="./conclusion.html""#, + r#"href="first/index.html""#, + r#"href="first/nested.html""#, + r#"href="second.html""#, + r#"href="conclusion.html""#, ]; let files_in_nested_dir = vec!["index.html", "nested.html"]; @@ -78,14 +78,14 @@ fn check_correct_cross_links_in_nested_dir() { assert_contains_strings( first.join("index.html"), &[ - r##"href="./first/index.html#some-section" id="some-section""##, + r##"href="first/index.html#some-section" id="some-section""##, ], ); assert_contains_strings( first.join("nested.html"), &[ - r##"href="./first/nested.html#some-section" id="some-section""##, + r##"href="first/nested.html#some-section" id="some-section""##, ], ); }