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<SummaryItem>`) - 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
This commit is contained in:
parent
a6d4881e00
commit
6d0d4bf379
|
@ -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"
|
||||
|
||||
|
|
32
src/lib.rs
32
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<BookItems>` and you get
|
||||
//! If you make a renderer, you get the book constructed in form of
|
||||
//! `Vec<BookItems>` 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;
|
||||
|
|
|
@ -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<P: AsRef<Path>>(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<Summary> {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<Summary> {
|
||||
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<String>,
|
||||
/// Chapters before the main text (e.g. an introduction).
|
||||
pub prefix_chapters: Vec<SummaryItem>,
|
||||
/// The main chapters in the document.
|
||||
pub numbered_chapters: Vec<SummaryItem>,
|
||||
/// Items which come after the main document (e.g. a conclusion).
|
||||
pub suffix_chapters: Vec<SummaryItem>,
|
||||
}
|
||||
|
||||
/// 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<SectionNumber>,
|
||||
/// Any nested items this chapter may contain.
|
||||
pub nested_items: Vec<SummaryItem>,
|
||||
}
|
||||
|
||||
impl Link {
|
||||
/// Create a new link with no nested items.
|
||||
pub fn new<S: Into<String>, P: AsRef<Path>>(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<Summary> {
|
||||
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<Link> {
|
||||
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<String> {
|
||||
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<SummaryItem>, mut item: SummaryItem, level: usize, mut section_number: SectionNumber)
|
||||
-> Result<SectionNumber> {
|
||||
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<Event>) -> 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<u32>` with
|
||||
/// a pretty `Display` impl.
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub struct SectionNumber(pub Vec<u32>);
|
||||
|
||||
impl Display for SectionNumber {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let dotted_number: String = self.0
|
||||
.iter()
|
||||
.map(|i| format!("{}", i))
|
||||
.collect::<Vec<String>>()
|
||||
.join(".");
|
||||
|
||||
write!(f, "{}", dotted_number)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SectionNumber {
|
||||
type Target = Vec<u32>;
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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![],
|
||||
}),
|
||||
],
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue