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)
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [Rust Library](lib/lib.md)
-----------
[Contributors](misc/contributors.md)

View file

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

View file

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

View file

@ -1,7 +1,7 @@
use std::path::PathBuf;
use std::fs::File;
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>> {
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));
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)
}
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");
let mut items: Vec<BookItem> = vec![];
loop {
if summary.len() <= 0 { break }
// Construct the book recursively
while summary.len() > 0 {
let item: BookItem;
// Indentation level of the line to parse
let level = try!(level(summary[0], 4));
if current_level > level { break }
else if current_level < level {
items.last_mut().unwrap().sub_items = try!(parse_level(summary, level))
}
else {
// Do the thing
if let Some(item) = parse_line(summary[0].clone()) {
items.push(item);
}
summary.remove(0);
}
}
// 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(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)
}
fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
debug!("[fn]: level");
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> {
debug!("[fn]: parse_line");
let mut name;
let mut path;
// 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");
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((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(i) = line[start_delimitor..].find("](") {
end_delimitor = start_delimitor +i;
}
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))
if let Some((name, path)) = read_link(line) {
return Some(BookItem::Affix(Chapter::new(name, path)))
} else { return None }
}
_ => {}
}
@ -124,3 +178,39 @@ fn parse_line(l: &str) -> Option<BookItem> {
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::Renderer;
use book::MDBook;
use book::bookitem::BookItem;
use {utils, theme};
use std::path::{Path, PathBuf};
@ -57,64 +58,69 @@ impl Renderer for HtmlHandlebars {
// Render a file for every entry in the book
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);
let mut f = try!(File::open(&path));
let mut content: String = String::new();
debug!("[*]: Opening file: {:?}", path);
let mut f = try!(File::open(&path));
let mut content: String = String::new();
debug!("[*]: Reading file");
try!(f.read_to_string(&mut content));
debug!("[*]: Reading file");
try!(f.read_to_string(&mut content));
// Render markdown using the pulldown-cmark crate
content = render_html(&content);
print_content.push_str(&content);
// Render markdown using the pulldown-cmark crate
content = render_html(&content);
print_content.push_str(&content);
// Remove content from previous file and render content for this one
data.remove("path");
match item.path.to_str() {
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"))),
}
// Remove content from previous file and render content for this one
data.remove("content");
data.insert("content".to_string(), content.to_json());
// Remove path to root from previous file and render content for this one
data.remove("path_to_root");
data.insert("path_to_root".to_string(), utils::path_to_root(&item.path).to_json());
// Rendere the handlebars template with the data
debug!("[*]: Render template");
let rendered = try!(handlebars.render("index", &data));
debug!("[*]: Create file {:?}", &book.get_dest().join(&item.path).with_extension("html"));
// Write to file
let mut file = try!(utils::create_file(&book.get_dest().join(&item.path).with_extension("html")));
output!("[*] Creating {:?} ✓", &book.get_dest().join(&item.path).with_extension("html"));
try!(file.write_all(&rendered.into_bytes()));
// Create an index.html from the first element in SUMMARY.md
if index {
debug!("[*]: index.html");
try!(fs::copy(
book.get_dest().join(&item.path.with_extension("html")),
book.get_dest().join("index.html")
));
output!(
"[*] Creating index.html from {:?} ✓",
book.get_dest().join(&item.path.with_extension("html"))
);
index = false;
// Remove content from previous file and render content for this one
data.remove("path");
match ch.path.to_str() {
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"))),
}
// Remove content from previous file and render content for this one
data.remove("content");
data.insert("content".to_string(), content.to_json());
// Remove path to root from previous file and render content for this one
data.remove("path_to_root");
data.insert("path_to_root".to_string(), utils::path_to_root(&ch.path).to_json());
// Rendere the handlebars template with the data
debug!("[*]: Render template");
let rendered = try!(handlebars.render("index", &data));
debug!("[*]: Create file {:?}", &book.get_dest().join(&ch.path).with_extension("html"));
// Write to file
let mut file = try!(utils::create_file(&book.get_dest().join(&ch.path).with_extension("html")));
output!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html"));
try!(file.write_all(&rendered.into_bytes()));
// Create an index.html from the first element in SUMMARY.md
if index {
debug!("[*]: index.html");
try!(fs::copy(
book.get_dest().join(&ch.path.with_extension("html")),
book.get_dest().join("index.html")
));
output!(
"[*] Creating index.html from {:?} ✓",
book.get_dest().join(&ch.path.with_extension("html"))
);
index = false;
}
}
}
_ => {}
}
}
@ -169,13 +175,30 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
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();
chapter.insert("section".to_string(), section.to_json());
chapter.insert("name".to_string(), item.name.to_json());
match item.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"))),
match item {
&BookItem::Affix(ref ch) => {
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::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);

View file

@ -28,7 +28,14 @@ impl HelperDef for RenderToc {
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 {
try!(rc.writer.write("<li>".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()));
}
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
@ -74,10 +85,16 @@ impl HelperDef for RenderToc {
false
};
try!(rc.writer.write("<strong>".as_bytes()));
try!(rc.writer.write(item.get("section").expect("Error: section should be Some(_)").as_bytes()));
try!(rc.writer.write("</strong> ".as_bytes()));
try!(rc.writer.write(item.get("name").expect("Error: name should be Some(_)").as_bytes()));
// Section does not necessarily exist
if let Some(section) = item.get("section") {
try!(rc.writer.write("<strong>".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 {
try!(rc.writer.write("</a>".as_bytes()));

View file

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