Big refactoring, now using enum for different book items (Chapter, Affix, Spacer, ...) Closes #9

This commit is contained in:
Mathieu David 2015-09-11 20:52:55 +02:00
parent 6962731474
commit a050d9c4ad
7 changed files with 300 additions and 158 deletions

View File

@ -11,3 +11,5 @@
- [index.hbs](format/theme/index-hbs.md) - [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md) - [Syntax highlighting](format/theme/syntax-highlighting.md)
- [Rust Library](lib/lib.md) - [Rust Library](lib/lib.md)
-----------
[Contributors](misc/contributors.md)

View File

@ -5,11 +5,17 @@ use std::path::PathBuf;
use std::collections::BTreeMap; use std::collections::BTreeMap;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BookItem { pub enum BookItem {
Chapter(String, Chapter), // String = section
Affix(Chapter),
Spacer,
}
#[derive(Debug, Clone)]
pub struct Chapter {
pub name: String, pub name: String,
pub path: PathBuf, pub path: PathBuf,
pub sub_items: Vec<BookItem>, pub sub_items: Vec<BookItem>,
spacer: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -20,30 +26,21 @@ pub struct BookItems<'a> {
} }
impl BookItem { impl Chapter {
pub fn new(name: String, path: PathBuf) -> Self { pub fn new(name: String, path: PathBuf) -> Self {
BookItem { Chapter {
name: name, name: name,
path: path, path: path,
sub_items: vec![], sub_items: vec![],
spacer: false,
}
}
fn _spacer() -> Self {
BookItem {
name: String::from("SPACER"),
path: PathBuf::new(),
sub_items: vec![],
spacer: true,
} }
} }
} }
impl ToJson for BookItem { impl ToJson for Chapter {
fn to_json(&self) -> Json { fn to_json(&self) -> Json {
let mut m: BTreeMap<String, Json> = BTreeMap::new(); let mut m: BTreeMap<String, Json> = BTreeMap::new();
m.insert("name".to_string(), self.name.to_json()); m.insert("name".to_string(), self.name.to_json());
@ -59,9 +56,9 @@ impl ToJson for BookItem {
// Shamelessly copied from Rustbook // Shamelessly copied from Rustbook
// (https://github.com/rust-lang/rust/blob/master/src/rustbook/book.rs) // (https://github.com/rust-lang/rust/blob/master/src/rustbook/book.rs)
impl<'a> Iterator for BookItems<'a> { impl<'a> Iterator for BookItems<'a> {
type Item = (String, &'a BookItem); type Item = &'a BookItem;
fn next(&mut self) -> Option<(String, &'a BookItem)> { fn next(&mut self) -> Option<&'a BookItem> {
loop { loop {
if self.current_index >= self.items.len() { if self.current_index >= self.items.len() {
match self.stack.pop() { match self.stack.pop() {
@ -74,18 +71,18 @@ impl<'a> Iterator for BookItems<'a> {
} else { } else {
let cur = self.items.get(self.current_index).unwrap(); let cur = self.items.get(self.current_index).unwrap();
let mut section = "".to_string(); match cur {
for &(_, idx) in &self.stack { &BookItem::Chapter(_, ref ch) | &BookItem::Affix(ref ch) => {
section.push_str(&(idx + 1).to_string()[..]); self.stack.push((self.items, self.current_index));
section.push('.'); self.items = &ch.sub_items[..];
self.current_index = 0;
},
&BookItem::Spacer => {
self.current_index += 1;
}
} }
section.push_str(&(self.current_index + 1).to_string()[..]);
section.push('.');
self.stack.push((self.items, self.current_index)); return Some(cur)
self.items = &cur.sub_items[..];
self.current_index = 0;
return Some((section, cur))
} }
} }
} }

View File

@ -126,21 +126,29 @@ impl MDBook {
// parse SUMMARY.md, and create the missing item related file // parse SUMMARY.md, and create the missing item related file
try!(self.parse_summary()); try!(self.parse_summary());
for (_, item) in self.iter() { debug!("[*]: constructing paths for missing files");
if item.path != PathBuf::new() { for item in self.iter() {
let path = self.config.get_src().join(&item.path); debug!("[*]: item: {:?}", item);
match item {
&BookItem::Spacer => continue,
&BookItem::Chapter(_, ref ch) | &BookItem::Affix(ref ch) => {
if ch.path != PathBuf::new() {
let path = self.config.get_src().join(&ch.path);
if !path.exists() { if !path.exists() {
debug!("[*]: {:?} does not exist, trying to create file", path); debug!("[*]: {:?} does not exist, trying to create file", path);
try!(::std::fs::create_dir_all(path.parent().unwrap())); try!(::std::fs::create_dir_all(path.parent().unwrap()));
let mut f = try!(File::create(path)); let mut f = try!(File::create(path));
debug!("[*]: Writing to {:?}", path); //debug!("[*]: Writing to {:?}", path);
try!(writeln!(f, "# {}", item.name)); try!(writeln!(f, "# {}", ch.name));
}
}
} }
} }
} }
debug!("[*]: init done");
return Ok(()); return Ok(());
} }
@ -300,14 +308,8 @@ impl MDBook {
// Construct book // Construct book
fn parse_summary(&mut self) -> Result<(), Box<Error>> { fn parse_summary(&mut self) -> Result<(), Box<Error>> {
// When append becomes stable, use self.content.append() ... // When append becomes stable, use self.content.append() ...
let book_items = try!(parse::construct_bookitems(&self.config.get_src().join("SUMMARY.md"))); self.content = try!(parse::construct_bookitems(&self.config.get_src().join("SUMMARY.md")));
for item in book_items {
self.content.push(item)
}
Ok(()) Ok(())
} }

View File

@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::fs::File; use std::fs::File;
use std::io::{Read, Result, Error, ErrorKind}; use std::io::{Read, Result, Error, ErrorKind};
use book::bookitem::BookItem; use book::bookitem::{BookItem, Chapter};
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> { pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
debug!("[fn]: construct_bookitems"); debug!("[fn]: construct_bookitems");
@ -9,36 +9,106 @@ pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
try!(try!(File::open(path)).read_to_string(&mut summary)); try!(try!(File::open(path)).read_to_string(&mut summary));
debug!("[*]: Parse SUMMARY.md"); debug!("[*]: Parse SUMMARY.md");
let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0)); let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0, vec![0]));
debug!("[*]: Done parsing SUMMARY.md");
Ok(top_items) Ok(top_items)
} }
fn parse_level(summary: &mut Vec<&str>, current_level: i32) -> Result<Vec<BookItem>> { fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32>) -> Result<Vec<BookItem>> {
debug!("[fn]: parse_level"); debug!("[fn]: parse_level");
let mut items: Vec<BookItem> = vec![]; let mut items: Vec<BookItem> = vec![];
loop { // Construct the book recursively
if summary.len() <= 0 { break } while summary.len() > 0 {
let item: BookItem;
// Indentation level of the line to parse
let level = try!(level(summary[0], 4)); let level = try!(level(summary[0], 4));
if current_level > level { break } // if level < current_level we remove the last digit of section, exit the current function,
else if current_level < level { // and return the parsed level to the calling function.
items.last_mut().unwrap().sub_items = try!(parse_level(summary, level)) if level < current_level { break }
}
else {
// Do the thing
if let Some(item) = parse_line(summary[0].clone()) {
items.push(item);
}
summary.remove(0);
}
}
// 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(1);
let last = items.pop().expect("There should be at least one item since this can't be the root level");
item = if let BookItem::Chapter(ref s, ref ch) = last {
let mut ch = ch.clone();
ch.sub_items = try!(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, format!(
"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, format!(
"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, format!(
"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) Ok(items)
} }
fn level(line: &str, spaces_in_tab: i32) -> Result<i32> { fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
debug!("[fn]: level"); debug!("[fn]: level");
let mut spaces = 0; let mut spaces = 0;
@ -74,49 +144,33 @@ fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
fn parse_line(l: &str) -> Option<BookItem> { fn parse_line(l: &str) -> Option<BookItem> {
debug!("[fn]: parse_line"); debug!("[fn]: parse_line");
let mut name;
let mut path;
// Remove leading and trailing spaces or tabs // Remove leading and trailing spaces or tabs
let line = l.trim_matches(|c: char| { c == ' ' || c == '\t' }); 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) { if let Some(c) = line.chars().nth(0) {
match c { match c {
// List item // List item
'-' | '*' => { '-' | '*' => {
debug!("[*]: Line is list element"); debug!("[*]: Line is list element");
let mut start_delimitor;
let mut end_delimitor;
// In the future, support for list item that is not a link if let Some((name, path)) = read_link(line) {
// Not sure if I should error on line I can't parse or just ignore them... return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path)))
if let Some(i) = line.find('[') { start_delimitor = i; } } else { return None }
else { },
debug!("[*]: '[' not found, this line is not a link. Ignoring..."); // Non-list element
return None '[' => {
} debug!("[*]: Line is a link element");
if let Some(i) = line[start_delimitor..].find("](") { if let Some((name, path)) = read_link(line) {
end_delimitor = start_delimitor +i; return Some(BookItem::Affix(Chapter::new(name, path)))
} } else { return None }
else {
debug!("[*]: '](' not found, this line is not a link. Ignoring...");
return None
}
name = line[start_delimitor + 1 .. end_delimitor].to_string();
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
}
path = PathBuf::from(line[start_delimitor + 1 .. end_delimitor].to_string());
return Some(BookItem::new(name, path))
} }
_ => {} _ => {}
} }
@ -124,3 +178,39 @@ fn parse_line(l: &str) -> Option<BookItem> {
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_string();
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_string());
Some((name, path))
}

View File

@ -5,6 +5,7 @@ extern crate pulldown_cmark;
use renderer::html_handlebars::helpers; use renderer::html_handlebars::helpers;
use renderer::Renderer; use renderer::Renderer;
use book::MDBook; use book::MDBook;
use book::bookitem::BookItem;
use {utils, theme}; use {utils, theme};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -57,64 +58,69 @@ impl Renderer for HtmlHandlebars {
// Render a file for every entry in the book // Render a file for every entry in the book
let mut index = true; let mut index = true;
for (_, item) in book.iter() { for item in book.iter() {
if item.path != PathBuf::new() { match item {
&BookItem::Chapter(_, ref ch) | &BookItem::Affix(ref ch) => {
if ch.path != PathBuf::new() {
let path = book.get_src().join(&item.path); let path = book.get_src().join(&ch.path);
debug!("[*]: Opening file: {:?}", path); debug!("[*]: Opening file: {:?}", path);
let mut f = try!(File::open(&path)); let mut f = try!(File::open(&path));
let mut content: String = String::new(); let mut content: String = String::new();
debug!("[*]: Reading file"); debug!("[*]: Reading file");
try!(f.read_to_string(&mut content)); try!(f.read_to_string(&mut content));
// Render markdown using the pulldown-cmark crate // Render markdown using the pulldown-cmark crate
content = render_html(&content); content = render_html(&content);
print_content.push_str(&content); print_content.push_str(&content);
// Remove content from previous file and render content for this one // Remove content from previous file and render content for this one
data.remove("path"); data.remove("path");
match item.path.to_str() { match ch.path.to_str() {
Some(p) => { data.insert("path".to_string(), p.to_json()); }, Some(p) => { data.insert("path".to_string(), p.to_json()); },
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))), None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
} }
// Remove content from previous file and render content for this one // Remove content from previous file and render content for this one
data.remove("content"); data.remove("content");
data.insert("content".to_string(), content.to_json()); data.insert("content".to_string(), content.to_json());
// Remove path to root from previous file and render content for this one // Remove path to root from previous file and render content for this one
data.remove("path_to_root"); data.remove("path_to_root");
data.insert("path_to_root".to_string(), utils::path_to_root(&item.path).to_json()); data.insert("path_to_root".to_string(), utils::path_to_root(&ch.path).to_json());
// Rendere the handlebars template with the data // Rendere the handlebars template with the data
debug!("[*]: Render template"); debug!("[*]: Render template");
let rendered = try!(handlebars.render("index", &data)); let rendered = try!(handlebars.render("index", &data));
debug!("[*]: Create file {:?}", &book.get_dest().join(&item.path).with_extension("html")); debug!("[*]: Create file {:?}", &book.get_dest().join(&ch.path).with_extension("html"));
// Write to file // Write to file
let mut file = try!(utils::create_file(&book.get_dest().join(&item.path).with_extension("html"))); let mut file = try!(utils::create_file(&book.get_dest().join(&ch.path).with_extension("html")));
output!("[*] Creating {:?} ✓", &book.get_dest().join(&item.path).with_extension("html")); output!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html"));
try!(file.write_all(&rendered.into_bytes())); try!(file.write_all(&rendered.into_bytes()));
// Create an index.html from the first element in SUMMARY.md // Create an index.html from the first element in SUMMARY.md
if index { if index {
debug!("[*]: index.html"); debug!("[*]: index.html");
try!(fs::copy( try!(fs::copy(
book.get_dest().join(&item.path.with_extension("html")), book.get_dest().join(&ch.path.with_extension("html")),
book.get_dest().join("index.html") book.get_dest().join("index.html")
)); ));
output!( output!(
"[*] Creating index.html from {:?} ✓", "[*] Creating index.html from {:?} ✓",
book.get_dest().join(&item.path.with_extension("html")) book.get_dest().join(&ch.path.with_extension("html"))
); );
index = false; index = false;
}
}
} }
_ => {}
} }
} }
@ -169,13 +175,30 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
let mut chapters = vec![]; let mut chapters = vec![];
for (section, item) in book.iter() { for item in book.iter() {
// Create the data to inject in the template
let mut chapter = BTreeMap::new(); let mut chapter = BTreeMap::new();
chapter.insert("section".to_string(), section.to_json());
chapter.insert("name".to_string(), item.name.to_json()); match item {
match item.path.to_str() { &BookItem::Affix(ref ch) => {
Some(p) => { chapter.insert("path".to_string(), p.to_json()); }, chapter.insert("name".to_string(), ch.name.to_json());
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))), match ch.path.to_str() {
Some(p) => { chapter.insert("path".to_string(), p.to_json()); },
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
}
},
&BookItem::Chapter(ref s, ref ch) => {
chapter.insert("section".to_string(), s.to_json());
chapter.insert("name".to_string(), ch.name.to_json());
match ch.path.to_str() {
Some(p) => { chapter.insert("path".to_string(), p.to_json()); },
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
}
},
&BookItem::Spacer => {
chapter.insert("spacer".to_string(), "_spacer_".to_json());
}
} }
chapters.push(chapter); chapters.push(chapter);

View File

@ -28,7 +28,14 @@ impl HelperDef for RenderToc {
for item in decoded { for item in decoded {
let level = item.get("section").expect("Error: section should be Some(_)").len() / 2; // Spacer
if let Some(_) = item.get("spacer") {
try!(rc.writer.write("<li class=\"spacer\"></li>".as_bytes()));
continue
}
let level = if let Some(s) = item.get("section") { s.len() / 2 } else { 1 };
if level > current_level { if level > current_level {
try!(rc.writer.write("<li>".as_bytes())); try!(rc.writer.write("<li>".as_bytes()));
try!(rc.writer.write("<ul class=\"section\">".as_bytes())); try!(rc.writer.write("<ul class=\"section\">".as_bytes()));
@ -42,7 +49,11 @@ impl HelperDef for RenderToc {
try!(rc.writer.write("<li>".as_bytes())); try!(rc.writer.write("<li>".as_bytes()));
} }
else { else {
try!(rc.writer.write("<li>".as_bytes())); try!(rc.writer.write("<li".as_bytes()));
if let None = item.get("section") {
try!(rc.writer.write(" class=\"affix\"".as_bytes()));
}
try!(rc.writer.write(">".as_bytes()));
} }
// Link // Link
@ -74,10 +85,16 @@ impl HelperDef for RenderToc {
false false
}; };
try!(rc.writer.write("<strong>".as_bytes())); // Section does not necessarily exist
try!(rc.writer.write(item.get("section").expect("Error: section should be Some(_)").as_bytes())); if let Some(section) = item.get("section") {
try!(rc.writer.write("</strong> ".as_bytes())); try!(rc.writer.write("<strong>".as_bytes()));
try!(rc.writer.write(item.get("name").expect("Error: name should be Some(_)").as_bytes())); try!(rc.writer.write(section.as_bytes()));
try!(rc.writer.write("</strong> ".as_bytes()));
}
if let Some(name) = item.get("name") {
try!(rc.writer.write(name.as_bytes()));
}
if path_exists { if path_exists {
try!(rc.writer.write("</a>".as_bytes())); try!(rc.writer.write("</a>".as_bytes()));

View File

@ -102,6 +102,17 @@ html, body {
text-decoration: none; text-decoration: none;
} }
.chapter .affix {
}
.chapter .spacer {
width: 100%;
height: 3px;
background-color: #f4f4f4;
margin: 10px 0px;
}
.menu-bar { .menu-bar {
position: relative; position: relative;
height: 50px; height: 50px;