Copied across the summary parser and Book structure (doesn't compile)
This commit is contained in:
parent
4619ab60b0
commit
4c6c696c87
|
@ -0,0 +1,392 @@
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use super::summary::{parse_summary, Summary, Link, SummaryItem, SectionNumber};
|
||||||
|
use errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
/// Load a book into memory from its `src/` directory.
|
||||||
|
pub fn load_book<P: AsRef<Path>>(src_dir: P, create_if_not_present: bool) -> Result<Book> {
|
||||||
|
let src_dir = src_dir.as_ref();
|
||||||
|
let summary_md = src_dir.join("SUMMARY.md");
|
||||||
|
|
||||||
|
let mut summary_content = String::new();
|
||||||
|
File::open(summary_md)
|
||||||
|
.chain_err(|| "Couldn't open SUMMARY.md")?
|
||||||
|
.read_to_string(&mut summary_content)?;
|
||||||
|
|
||||||
|
let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?;
|
||||||
|
|
||||||
|
load_book_from_disk(&summary, src_dir, create_if_not_present)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// A dumb tree structure representing a book.
|
||||||
|
///
|
||||||
|
/// For the moment a book is just a collection of `BookItems`.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Book {
|
||||||
|
/// The sections in this book.
|
||||||
|
pub sections: Vec<BookItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Book {
|
||||||
|
/// Create an empty book.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a depth-first iterator over the items in the book.
|
||||||
|
pub fn iter(&self) -> BookItems {
|
||||||
|
BookItems { items: self.sections.iter().collect() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum representing any type of item which can be added to a book.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum BookItem {
|
||||||
|
/// A nested chapter.
|
||||||
|
Chapter(Chapter),
|
||||||
|
/// A section separator.
|
||||||
|
Separator,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The representation of a "chapter", usually mapping to a single file on
|
||||||
|
/// disk however it may contain multiple sub-chapters.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Chapter {
|
||||||
|
/// The chapter's name.
|
||||||
|
pub name: String,
|
||||||
|
/// The chapter's contents.
|
||||||
|
pub content: String,
|
||||||
|
/// The chapter's section number, if it has one.
|
||||||
|
pub number: Option<SectionNumber>,
|
||||||
|
/// Nested items.
|
||||||
|
pub sub_items: Vec<BookItem>,
|
||||||
|
/// 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<P: Into<PathBuf>>(name: &str, content: String, path: P) -> Chapter {
|
||||||
|
Chapter {
|
||||||
|
name: name.to_string(),
|
||||||
|
content: content,
|
||||||
|
path: path.into(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use the provided `Summary` to load a `Book` from disk.
|
||||||
|
///
|
||||||
|
/// You need to pass in the book's source directory because all the links in
|
||||||
|
/// `SUMMARY.md` give the chapter locations relative to it.
|
||||||
|
fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P, create_if_not_present: bool) -> Result<Book> {
|
||||||
|
debug!("[*] Loading the book from disk");
|
||||||
|
let src_dir = src_dir.as_ref();
|
||||||
|
|
||||||
|
let prefix = summary.prefix_chapters.iter();
|
||||||
|
let numbered = summary.numbered_chapters.iter();
|
||||||
|
let suffix = summary.suffix_chapters.iter();
|
||||||
|
|
||||||
|
let summary_items = prefix.chain(numbered).chain(suffix);
|
||||||
|
|
||||||
|
let mut chapters = Vec::new();
|
||||||
|
|
||||||
|
for summary_item in summary_items {
|
||||||
|
let chapter = load_summary_item(summary_item, src_dir, create_if_not_present)?;
|
||||||
|
chapters.push(chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Book { sections: chapters })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_summary_item<P: AsRef<Path>>(item: &SummaryItem, src_dir: P, create_if_not_present: bool) -> Result<BookItem> {
|
||||||
|
match *item {
|
||||||
|
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||||
|
SummaryItem::Link(ref link) => {
|
||||||
|
let file = src_dir.as_ref().join(&link.location);
|
||||||
|
|
||||||
|
if create_if_not_present && !file.exists() {
|
||||||
|
let text = format!("# {}", link.name);
|
||||||
|
File::create(&file)?.write_all(text.as_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_chapter(link, src_dir).map(|c| BookItem::Chapter(c))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_chapter<P: AsRef<Path>>(link: &Link, src_dir: P) -> Result<Chapter> {
|
||||||
|
debug!("[*] Loading {} ({})", link.name, link.location.display());
|
||||||
|
let src_dir = src_dir.as_ref();
|
||||||
|
|
||||||
|
let location = if link.location.is_absolute() {
|
||||||
|
link.location.clone()
|
||||||
|
} else {
|
||||||
|
src_dir.join(&link.location)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut f = File::open(&location).chain_err(|| {
|
||||||
|
format!("Chapter file not found, {}", link.location.display())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut content = String::new();
|
||||||
|
f.read_to_string(&mut content)?;
|
||||||
|
|
||||||
|
let stripped = location.strip_prefix(&src_dir).expect("Chapters are always inside a book");
|
||||||
|
|
||||||
|
let mut ch = Chapter::new(&link.name, content, stripped);
|
||||||
|
ch.number = link.number.clone();
|
||||||
|
|
||||||
|
let sub_items = link.nested_items
|
||||||
|
.iter()
|
||||||
|
.map(|i| load_summary_item(i, src_dir, false))
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
ch.sub_items = sub_items;
|
||||||
|
|
||||||
|
Ok(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A depth-first iterator over the items in a book.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This struct shouldn't be created directly, instead prefer the
|
||||||
|
/// [`Book::iter()`] method.
|
||||||
|
///
|
||||||
|
/// [`Book::iter()`]: struct.Book.html#method.iter
|
||||||
|
pub struct BookItems<'a> {
|
||||||
|
items: VecDeque<&'a BookItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for BookItems<'a> {
|
||||||
|
type Item = &'a BookItem;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let item = self.items.pop_front();
|
||||||
|
|
||||||
|
if let Some(&BookItem::Chapter(ref ch)) = item {
|
||||||
|
// if we wanted a breadth-first iterator we'd `extend()` here
|
||||||
|
for sub_item in ch.sub_items.iter().rev() {
|
||||||
|
self.items.push_front(sub_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Chapter {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
if let Some(ref section_number) = self.number {
|
||||||
|
write!(f, "{} ", section_number)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "{}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempdir::TempDir;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
const DUMMY_SRC: &'static str = "
|
||||||
|
# Dummy Chapter
|
||||||
|
|
||||||
|
this is some dummy text.
|
||||||
|
|
||||||
|
And here is some more text.
|
||||||
|
";
|
||||||
|
|
||||||
|
/// Create a dummy `Link` in a temporary directory.
|
||||||
|
fn dummy_link() -> (Link, TempDir) {
|
||||||
|
let temp = TempDir::new("book").unwrap();
|
||||||
|
|
||||||
|
let chapter_path = temp.path().join("chapter_1.md");
|
||||||
|
File::create(&chapter_path)
|
||||||
|
.unwrap()
|
||||||
|
.write(DUMMY_SRC.as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let link = Link::new("Chapter 1", chapter_path);
|
||||||
|
|
||||||
|
(link, temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a nested `Link` written to a temporary directory.
|
||||||
|
fn nested_links() -> (Link, TempDir) {
|
||||||
|
let (mut root, temp_dir) = dummy_link();
|
||||||
|
|
||||||
|
let second_path = temp_dir.path().join("second.md");
|
||||||
|
|
||||||
|
File::create(&second_path)
|
||||||
|
.unwrap()
|
||||||
|
.write_all("Hello World!".as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
let mut second = Link::new("Nested Chapter 1", &second_path);
|
||||||
|
second.number = Some(SectionNumber(vec![1, 2]));
|
||||||
|
|
||||||
|
root.nested_items.push(second.clone().into());
|
||||||
|
root.nested_items.push(SummaryItem::Separator);
|
||||||
|
root.nested_items.push(second.clone().into());
|
||||||
|
|
||||||
|
(root, temp_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_a_single_chapter_from_disk() {
|
||||||
|
let (link, temp_dir) = dummy_link();
|
||||||
|
let should_be = Chapter::new("Chapter 1", DUMMY_SRC.to_string(), "chapter_1.md");
|
||||||
|
|
||||||
|
let got = load_chapter(&link, temp_dir.path()).unwrap();
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cant_load_a_nonexistent_chapter() {
|
||||||
|
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
|
||||||
|
|
||||||
|
let got = load_chapter(&link, "");
|
||||||
|
assert!(got.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_recursive_link_with_separators() {
|
||||||
|
let (root, temp) = nested_links();
|
||||||
|
|
||||||
|
let nested = Chapter {
|
||||||
|
name: String::from("Nested Chapter 1"),
|
||||||
|
content: String::from("Hello World!"),
|
||||||
|
number: Some(SectionNumber(vec![1, 2])),
|
||||||
|
path: PathBuf::from("second.md"),
|
||||||
|
sub_items: Vec::new(),
|
||||||
|
};
|
||||||
|
let should_be = BookItem::Chapter(Chapter {
|
||||||
|
name: String::from("Chapter 1"),
|
||||||
|
content: String::from(DUMMY_SRC),
|
||||||
|
number: None,
|
||||||
|
path: PathBuf::from("chapter_1.md"),
|
||||||
|
sub_items: vec![
|
||||||
|
BookItem::Chapter(nested.clone()),
|
||||||
|
BookItem::Separator,
|
||||||
|
BookItem::Chapter(nested.clone()),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), false).unwrap();
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_a_book_with_a_single_chapter() {
|
||||||
|
let (link, temp) = dummy_link();
|
||||||
|
let summary = Summary {
|
||||||
|
numbered_chapters: vec![SummaryItem::Link(link)],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let should_be = Book {
|
||||||
|
sections: vec![
|
||||||
|
BookItem::Chapter(Chapter {
|
||||||
|
name: String::from("Chapter 1"),
|
||||||
|
content: String::from(DUMMY_SRC),
|
||||||
|
path: PathBuf::from("chapter_1.md"),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let got = load_book_from_disk(&summary, temp.path(), false).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn book_iter_iterates_over_sequential_items() {
|
||||||
|
let book = Book {
|
||||||
|
sections: vec![
|
||||||
|
BookItem::Chapter(Chapter {
|
||||||
|
name: String::from("Chapter 1"),
|
||||||
|
content: String::from(DUMMY_SRC),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
BookItem::Separator,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_be: Vec<_> = book.sections.iter().collect();
|
||||||
|
|
||||||
|
let got: Vec<_> = book.iter().collect();
|
||||||
|
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iterate_over_nested_book_items() {
|
||||||
|
let book = Book {
|
||||||
|
sections: vec![
|
||||||
|
BookItem::Chapter(Chapter {
|
||||||
|
name: String::from("Chapter 1"),
|
||||||
|
content: String::from(DUMMY_SRC),
|
||||||
|
number: None,
|
||||||
|
path: PathBuf::from("Chapter_1/index.md"),
|
||||||
|
sub_items: vec![
|
||||||
|
BookItem::Chapter(Chapter::new("Hello World", String::new(), "Chapter_1/hello.md")),
|
||||||
|
BookItem::Separator,
|
||||||
|
BookItem::Chapter(Chapter::new("Goodbye World", String::new(), "Chapter_1/goodbye.md")),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
BookItem::Separator,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let got: Vec<_> = book.iter().collect();
|
||||||
|
|
||||||
|
assert_eq!(got.len(), 5);
|
||||||
|
|
||||||
|
// checking the chapter names are in the order should be sufficient here...
|
||||||
|
let chapter_names: Vec<String> = got.into_iter()
|
||||||
|
.filter_map(|i| match *i {
|
||||||
|
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let should_be: Vec<_> = vec![
|
||||||
|
String::from("Chapter 1"),
|
||||||
|
String::from("Hello World"),
|
||||||
|
String::from("Goodbye World"),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(chapter_names, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_missing_book_items() {
|
||||||
|
let (link, temp) = dummy_link();
|
||||||
|
let summary = Summary {
|
||||||
|
numbered_chapters: vec![SummaryItem::Link(link)],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let chapter_1 = temp.path().join("chapter_1.md");
|
||||||
|
fs::remove_file(&chapter_1).unwrap();
|
||||||
|
assert!(!chapter_1.exists());
|
||||||
|
|
||||||
|
load_book_from_disk(&summary, temp.path(), true).unwrap();
|
||||||
|
|
||||||
|
assert!(chapter_1.exists());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
use serde::{Serialize, Serializer};
|
|
||||||
use serde::ser::SerializeStruct;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum BookItem {
|
|
||||||
Chapter(String, Chapter), // String = section
|
|
||||||
Affix(Chapter),
|
|
||||||
Spacer,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Chapter {
|
|
||||||
pub name: String,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub sub_items: Vec<BookItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,718 @@
|
||||||
|
use std::fmt::{self, Formatter, Display};
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use pulldown_cmark::{self, Event, Tag};
|
||||||
|
use errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
|
||||||
|
/// used when loading a book from disk.
|
||||||
|
///
|
||||||
|
/// # Summary Format
|
||||||
|
///
|
||||||
|
/// **Title:** It's common practice to begin with a title, generally
|
||||||
|
/// "# Summary". But it is not mandatory, the parser just ignores it. So you
|
||||||
|
/// can too if you feel like it.
|
||||||
|
///
|
||||||
|
/// **Prefix Chapter:** Before the main numbered chapters you can add a couple
|
||||||
|
/// of elements that will not be numbered. This is useful for forewords,
|
||||||
|
/// introductions, etc. There are however some constraints. You can not nest
|
||||||
|
/// prefix chapters, they should all be on the root level. And you can not add
|
||||||
|
/// prefix chapters once you have added numbered chapters.
|
||||||
|
///
|
||||||
|
/// ```markdown
|
||||||
|
/// [Title of prefix element](relative/path/to/markdown.md)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// **Numbered Chapter:** Numbered chapters are the main content of the book,
|
||||||
|
/// they
|
||||||
|
/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
|
||||||
|
/// sub-chapters, etc.)
|
||||||
|
///
|
||||||
|
/// ```markdown
|
||||||
|
/// - [Title of the Chapter](relative/path/to/markdown.md)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// You can either use - or * to indicate a numbered chapter.
|
||||||
|
///
|
||||||
|
/// **Suffix Chapter:** After the numbered chapters you can add a couple of
|
||||||
|
/// non-numbered chapters. They are the same as prefix chapters but come after
|
||||||
|
/// the numbered chapters instead of before.
|
||||||
|
///
|
||||||
|
/// All other elements are unsupported and will be ignored at best or result in
|
||||||
|
/// an error.
|
||||||
|
pub fn parse_summary(summary: &str) -> Result<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, Serialize, Deserialize)]
|
||||||
|
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, Serialize, Deserialize)]
|
||||||
|
pub struct Link {
|
||||||
|
/// The name of the chapter.
|
||||||
|
pub name: String,
|
||||||
|
/// The location of the chapter's source file, taking the book's `src`
|
||||||
|
/// directory as the root.
|
||||||
|
pub location: PathBuf,
|
||||||
|
/// The section number, if this chapter is in the numbered section.
|
||||||
|
pub number: Option<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, Serialize, Deserialize)]
|
||||||
|
pub enum SummaryItem {
|
||||||
|
/// A link to a chapter.
|
||||||
|
Link(Link),
|
||||||
|
/// A separator (`---`).
|
||||||
|
Separator,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SummaryItem {
|
||||||
|
fn maybe_link_mut(&mut self) -> Option<&mut Link> {
|
||||||
|
match *self {
|
||||||
|
SummaryItem::Link(ref mut l) => Some(l),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Link> for SummaryItem {
|
||||||
|
fn from(other: Link) -> SummaryItem {
|
||||||
|
SummaryItem::Link(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
enum State {
|
||||||
|
Begin,
|
||||||
|
PrefixChapters,
|
||||||
|
/// Numbered chapters, including the nesting level.
|
||||||
|
NumberedChapters(u32),
|
||||||
|
SuffixChapters,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A state machine parser for parsing a `SUMMARY.md` file.
|
||||||
|
///
|
||||||
|
/// The parser has roughly 5 states,
|
||||||
|
///
|
||||||
|
/// - **Begin:** the initial state
|
||||||
|
/// - **Prefix Chapters:** Parsing the prefix chapters
|
||||||
|
/// - **Numbered Chapters:** Parsing the numbered chapters, using a `usize` to
|
||||||
|
/// indicate the nesting level (because chapters can have sub-chapters)
|
||||||
|
/// - **Suffix Chapters:** pretty much identical to the Prefix Chapters
|
||||||
|
/// - **End:** The final state
|
||||||
|
///
|
||||||
|
/// The `parse()` method then continually invokes `step()` until it reaches the
|
||||||
|
/// `End` state. Parsing is guaranteed to (eventually) finish because the next
|
||||||
|
/// `Event` is read from the underlying `pulldown_cmark::Parser` and passed
|
||||||
|
/// into the current state's associated method.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// # Grammar
|
||||||
|
///
|
||||||
|
/// The `SUMMARY.md` file has a grammar which looks something like this:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// summary ::= title prefix_chapters numbered_chapters
|
||||||
|
/// suffix_chapters
|
||||||
|
/// title ::= "# " TEXT
|
||||||
|
/// | EPSILON
|
||||||
|
/// prefix_chapters ::= item*
|
||||||
|
/// suffix_chapters ::= item*
|
||||||
|
/// numbered_chapters ::= dotted_item+
|
||||||
|
/// dotted_item ::= INDENT* DOT_POINT item
|
||||||
|
/// item ::= link
|
||||||
|
/// | separator
|
||||||
|
/// separator ::= "---"
|
||||||
|
/// link ::= "[" TEXT "]" "(" TEXT ")"
|
||||||
|
/// DOT_POINT ::= "-"
|
||||||
|
/// | "*"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly)
|
||||||
|
/// > match the following regex: "[^<>\n[]]+".
|
||||||
|
struct SummaryParser<'a> {
|
||||||
|
stream: pulldown_cmark::Parser<'a>,
|
||||||
|
summary: Summary,
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads `Events` from the provided stream until the corresponding
|
||||||
|
/// `Event::End` is encountered which matches the `$delimiter` pattern.
|
||||||
|
///
|
||||||
|
/// This is the equivalent of doing
|
||||||
|
/// `$stream.take_while(|e| e != $delimeter).collect()` but it allows you to
|
||||||
|
/// use pattern matching and you won't get errors because `take_while()`
|
||||||
|
/// moves `$stream` out of self.
|
||||||
|
macro_rules! collect_events {
|
||||||
|
($stream:expr, $delimiter:pat) => {
|
||||||
|
{
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let event = $stream.next();
|
||||||
|
match event {
|
||||||
|
Some(Event::End($delimiter)) => break,
|
||||||
|
Some(other) => events.push(other),
|
||||||
|
None => {
|
||||||
|
debug!("Reached end of stream without finding the closing pattern, {}", stringify!($delimiter));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SummaryParser<'a> {
|
||||||
|
fn new(text: &str) -> SummaryParser {
|
||||||
|
let pulldown_parser = pulldown_cmark::Parser::new(text);
|
||||||
|
let intermediate_summary = Summary::default();
|
||||||
|
|
||||||
|
SummaryParser {
|
||||||
|
stream: pulldown_parser,
|
||||||
|
summary: intermediate_summary,
|
||||||
|
state: State::Begin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the text the `SummaryParser` was created with.
|
||||||
|
fn parse(mut self) -> Result<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 | State::SuffixChapters => self.step_affix(next_event)?,
|
||||||
|
State::NumberedChapters(_) => self.step_numbered(next_event)?,
|
||||||
|
State::End => {},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trace!("[*] Reached end of SUMMARY.md");
|
||||||
|
self.state = State::End;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The very first state, we should see a `Begin Paragraph` token or
|
||||||
|
/// it's an error...
|
||||||
|
fn step_start(&mut self, event: Event<'a>) -> Result<()> {
|
||||||
|
match event {
|
||||||
|
Event::Start(Tag::Paragraph) => self.state = State::PrefixChapters,
|
||||||
|
Event::Start(Tag::List(_)) => self.state = State::NumberedChapters(0),
|
||||||
|
other => bail!("Expected a start of paragraph but got {:?}", other),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to step through an "affix" section (recognising prefix and suffix
|
||||||
|
/// chapters).
|
||||||
|
///
|
||||||
|
/// If we encounter a link or horizontal line, it'll get added to the
|
||||||
|
/// section. If we encounter a list, we'll either change to
|
||||||
|
/// `State::NumberedChapter` (for prefix) or throw an error (suffix chapters).
|
||||||
|
///
|
||||||
|
/// Anything else will be ignored.
|
||||||
|
fn step_affix(&mut self, event: Event<'a>) -> Result<()> {
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Start(tag) => self.handle_start_tag_in_affix_chapter(tag)?,
|
||||||
|
Event::End(Tag::Rule) => {
|
||||||
|
debug!("[*] Found an affix chapter separator");
|
||||||
|
self.affix_chapter_list().push(SummaryItem::Separator);
|
||||||
|
},
|
||||||
|
other => {
|
||||||
|
trace!("[*] Skipping unexpected token in summary: {:?}", other);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper function to get the `SummaryItem` list we should add items to
|
||||||
|
/// when parsing an affix chapter (i.e. prefix or suffix chapters).
|
||||||
|
fn affix_chapter_list(&mut self) -> &mut Vec<SummaryItem> {
|
||||||
|
match self.state {
|
||||||
|
State::PrefixChapters => &mut self.summary.prefix_chapters,
|
||||||
|
State::SuffixChapters => &mut self.summary.suffix_chapters,
|
||||||
|
other => panic!("affix_chapter_list() called with invalid state: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_start_tag_in_affix_chapter(&mut self, tag: Tag) -> Result<()> {
|
||||||
|
match tag {
|
||||||
|
Tag::Link(location, _) => {
|
||||||
|
let content = collect_events!(self.stream, Tag::Link(_, _));
|
||||||
|
let text = stringify_events(content);
|
||||||
|
let link = Link::new(text, location.as_ref());
|
||||||
|
|
||||||
|
debug!("[*] Found an affix chapter: {:?}", link.name);
|
||||||
|
self.affix_chapter_list().push(SummaryItem::Link(link));
|
||||||
|
},
|
||||||
|
Tag::List(_) => {
|
||||||
|
match self.state {
|
||||||
|
State::PrefixChapters => {
|
||||||
|
debug!("[*] Changing from prefix chapters to numbered chapters");
|
||||||
|
self.state = State::NumberedChapters(0);
|
||||||
|
},
|
||||||
|
State::SuffixChapters => bail!("Suffix chapters can't be followed by a list"),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
other => trace!("[*] Skipping unknown start tag while parsing affix chapters: {:?}", other),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the numbered chapters.
|
||||||
|
///
|
||||||
|
/// If the event is the start of a list item, consume the entire item and
|
||||||
|
/// add a new link to the summary with `push_numbered_section`.
|
||||||
|
///
|
||||||
|
/// If the event is the start of a new list, bump the nesting level.
|
||||||
|
///
|
||||||
|
/// If the event is the end of a list, decrement the nesting level. When
|
||||||
|
/// the nesting level would go negative, we've finished the numbered
|
||||||
|
/// section and need to parse the suffix section.
|
||||||
|
///
|
||||||
|
/// Otherwise, ignore the event.
|
||||||
|
fn step_numbered(&mut self, event: Event) -> Result<()> {
|
||||||
|
match event {
|
||||||
|
Event::Start(Tag::Item) => {
|
||||||
|
let it = self.parse_item().chain_err(
|
||||||
|
|| "List items should only contain links",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
debug!("[*] Found a chapter: {:?} ({})", it.name, it.location.display());
|
||||||
|
let section_number = self.push_numbered_section(SummaryItem::Link(it));
|
||||||
|
trace!("[*] Section number is {}", section_number);
|
||||||
|
},
|
||||||
|
Event::End(Tag::Rule) => {
|
||||||
|
debug!("[*] Found a numbered chapter separator");
|
||||||
|
self.summary.numbered_chapters.push(SummaryItem::Separator);
|
||||||
|
self.state = State::NumberedChapters(0);
|
||||||
|
},
|
||||||
|
Event::Start(Tag::List(_)) => {
|
||||||
|
if let State::NumberedChapters(n) = self.state {
|
||||||
|
self.state = State::NumberedChapters(n + 1);
|
||||||
|
trace!("[*] Nesting level increased to {}", n + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Event::End(Tag::List(_)) => {
|
||||||
|
if let State::NumberedChapters(n) = self.state {
|
||||||
|
if n == 0 {
|
||||||
|
trace!("[*] Finished parsing the numbered chapters");
|
||||||
|
self.state = State::SuffixChapters;
|
||||||
|
} else {
|
||||||
|
trace!("[*] Nesting level decreased to {}", n - 1);
|
||||||
|
self.state = State::NumberedChapters(n - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
other => {
|
||||||
|
trace!("[*] skipping unexpected token: {:?}", other);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a single item (`[Some Chapter Name](./path/to/chapter.md)`).
|
||||||
|
fn parse_item(&mut self) -> Result<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 (index, last_link) = get_last_link(links).chain_err(|| {
|
||||||
|
format!("The list of links needs to be {} levels deeper (current position {})",
|
||||||
|
level, section_number)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
section_number.push(index as u32 + 1);
|
||||||
|
push_item_at_nesting_level(&mut last_link.nested_items, item, level - 1, section_number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a pointer to the last `Link` in a list of `SummaryItem`s, and its
|
||||||
|
/// index.
|
||||||
|
fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
|
||||||
|
// TODO: This should probably be integrated into `Link::push_item()`
|
||||||
|
links
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
|
||||||
|
.rev()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| "The list of SummaryItems doesn't contain any Links".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Removes the styling from a list of Markdown events and returns just the
|
||||||
|
/// plain text.
|
||||||
|
fn stringify_events(events: Vec<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, Serialize, Deserialize)]
|
||||||
|
pub struct SectionNumber(pub Vec<u32>);
|
||||||
|
|
||||||
|
impl Display for SectionNumber {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
for item in &self.0 {
|
||||||
|
write!(f, "{}.", item)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SectionNumber {
|
||||||
|
type Target = Vec<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).to_string();
|
||||||
|
assert_eq!(section_number, should_be);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_initial_title() {
|
||||||
|
let src = "# Summary";
|
||||||
|
let should_be = String::from("Summary");
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
let got = parser.parse_title().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_title_with_styling() {
|
||||||
|
let src = "# My **Awesome** Summary";
|
||||||
|
let should_be = String::from("My Awesome Summary");
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
let got = parser.parse_title().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_a_single_item() {
|
||||||
|
let src = "[A Chapter](./path/to/chapter)";
|
||||||
|
let should_be = Link {
|
||||||
|
name: String::from("A Chapter"),
|
||||||
|
location: PathBuf::from("./path/to/chapter"),
|
||||||
|
number: None,
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
let _ = parser.stream.next(); // skip the opening paragraph tag
|
||||||
|
let got = parser.parse_item().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_markdown_events_to_a_string() {
|
||||||
|
let src = "Hello *World*, `this` is some text [and a link](./path/to/link)";
|
||||||
|
let should_be = "Hello World, this is some text and a link";
|
||||||
|
|
||||||
|
let events = pulldown_cmark::Parser::new(src).collect();
|
||||||
|
let got = stringify_events(events);
|
||||||
|
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_step_past_first_token() {
|
||||||
|
let src = "hello world";
|
||||||
|
let should_be = State::PrefixChapters;
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
assert_eq!(parser.state, State::Begin);
|
||||||
|
parser.step().unwrap();
|
||||||
|
assert_eq!(parser.state, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_token_must_be_open_paragraph() {
|
||||||
|
let src = "hello world";
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
let _ = parser.stream.next(); // manually step past the Start Paragraph
|
||||||
|
assert!(parser.step().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_parse_prefix_chapter_links() {
|
||||||
|
let src = "[Hello World](./foo/bar/baz)";
|
||||||
|
let should_be = Link {
|
||||||
|
name: String::from("Hello World"),
|
||||||
|
location: PathBuf::from("./foo/bar/baz"),
|
||||||
|
number: None,
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
parser.state = State::PrefixChapters;
|
||||||
|
assert!(parser.summary.prefix_chapters.is_empty());
|
||||||
|
|
||||||
|
let _ = parser.stream.next(); // manually step past the Start Paragraph
|
||||||
|
parser.step().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(parser.summary.prefix_chapters.len(), 1);
|
||||||
|
assert_eq!(parser.summary.prefix_chapters[0], SummaryItem::Link(should_be));
|
||||||
|
assert_eq!(parser.state, State::PrefixChapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_parse_prefix_chapter_horizontal_rules() {
|
||||||
|
let src = "---";
|
||||||
|
let should_be = SummaryItem::Separator;
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
parser.state = State::PrefixChapters;
|
||||||
|
assert!(parser.summary.prefix_chapters.is_empty());
|
||||||
|
|
||||||
|
let _ = parser.stream.next(); // manually step past the Start Paragraph
|
||||||
|
parser.step().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(parser.summary.prefix_chapters.len(), 1);
|
||||||
|
assert_eq!(parser.summary.prefix_chapters[0], should_be);
|
||||||
|
assert_eq!(parser.state, State::PrefixChapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn step_from_prefix_chapters_to_numbered() {
|
||||||
|
let src = "- foo";
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
parser.state = State::PrefixChapters;
|
||||||
|
|
||||||
|
// let _ = parser.stream.next(); // manually step past the Start Paragraph
|
||||||
|
parser.step().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(parser.state, State::NumberedChapters(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn push_item_onto_empty_link() {
|
||||||
|
let root = Link::new("First", "/");
|
||||||
|
let mut links = vec![SummaryItem::Link(root)];
|
||||||
|
|
||||||
|
assert_eq!(links[0].maybe_link_mut().unwrap().nested_items.len(), 0);
|
||||||
|
let got = push_item_at_nesting_level(&mut links, SummaryItem::Separator, 1, SectionNumber::default()).unwrap();
|
||||||
|
assert_eq!(links[0].maybe_link_mut().unwrap().nested_items.len(), 1);
|
||||||
|
assert_eq!(*got, vec![1, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn push_item_onto_complex_link() {
|
||||||
|
let mut root = Link::new("First", "/first");
|
||||||
|
root.nested_items.push(SummaryItem::Separator);
|
||||||
|
|
||||||
|
let mut child = Link::new("Second", "/first/second");
|
||||||
|
child.nested_items.push(SummaryItem::Link(
|
||||||
|
Link::new("Third", "/first/second/third"),
|
||||||
|
));
|
||||||
|
root.nested_items.push(SummaryItem::Link(child));
|
||||||
|
root.nested_items.push(SummaryItem::Separator);
|
||||||
|
|
||||||
|
let mut links = vec![SummaryItem::Link(root)];
|
||||||
|
|
||||||
|
// FIXME: This crap for getting a deeply nested member is just plain ugly :(
|
||||||
|
assert_eq!(links[0].maybe_link_mut().unwrap()
|
||||||
|
.nested_items[1].maybe_link_mut()
|
||||||
|
.unwrap()
|
||||||
|
.nested_items[0].maybe_link_mut()
|
||||||
|
.unwrap()
|
||||||
|
.nested_items.len() , 0);
|
||||||
|
let got = push_item_at_nesting_level(
|
||||||
|
&mut links,
|
||||||
|
SummaryItem::Link(Link::new("Dummy", "")),
|
||||||
|
3,
|
||||||
|
SectionNumber::default(),
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(links[0].maybe_link_mut().unwrap()
|
||||||
|
.nested_items[1].maybe_link_mut()
|
||||||
|
.unwrap()
|
||||||
|
.nested_items[0].maybe_link_mut()
|
||||||
|
.unwrap()
|
||||||
|
.nested_items.len() , 1);
|
||||||
|
println!("{:#?}", links);
|
||||||
|
assert_eq!(*got, vec![1, 2, 1, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_a_numbered_chapter() {
|
||||||
|
let src = "- [First](./second)";
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
let _ = parser.stream.next();
|
||||||
|
|
||||||
|
assert_eq!(parser.summary.numbered_chapters.len(), 0);
|
||||||
|
|
||||||
|
parser.state = State::NumberedChapters(0);
|
||||||
|
parser.step().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(parser.summary.numbered_chapters.len(), 1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -89,7 +89,6 @@ extern crate serde_json;
|
||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
extern crate toml;
|
extern crate toml;
|
||||||
|
|
||||||
mod parse;
|
|
||||||
mod preprocess;
|
mod preprocess;
|
||||||
pub mod book;
|
pub mod book;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub use self::summary::construct_bookitems;
|
|
||||||
|
|
||||||
pub mod summary;
|
|
|
@ -1,239 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{Error, ErrorKind, Read, Result};
|
|
||||||
use book::bookitem::{BookItem, Chapter};
|
|
||||||
|
|
||||||
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
|
|
||||||
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<i32>)
|
|
||||||
-> Result<Vec<BookItem>> {
|
|
||||||
debug!("[fn]: parse_level");
|
|
||||||
let mut items: Vec<BookItem> = 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<i32> {
|
|
||||||
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<BookItem> {
|
|
||||||
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))
|
|
||||||
}
|
|
Loading…
Reference in New Issue