From a050d9c4ad116e6ccf8e83645aa4a7a016074847 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Fri, 11 Sep 2015 20:52:55 +0200 Subject: [PATCH] Big refactoring, now using enum for different book items (Chapter, Affix, Spacer, ...) Closes #9 --- book-example/src/SUMMARY.md | 2 + src/book/bookitem.rs | 51 +++-- src/book/mdbook.rs | 34 ++-- src/parse/summary.rs | 194 ++++++++++++++----- src/renderer/html_handlebars/hbs_renderer.rs | 137 +++++++------ src/renderer/html_handlebars/helpers/toc.rs | 29 ++- src/theme/book.css | 11 ++ 7 files changed, 300 insertions(+), 158 deletions(-) diff --git a/book-example/src/SUMMARY.md b/book-example/src/SUMMARY.md index bceb4eb3..a25059a7 100644 --- a/book-example/src/SUMMARY.md +++ b/book-example/src/SUMMARY.md @@ -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) diff --git a/src/book/bookitem.rs b/src/book/bookitem.rs index 117e913e..d438324e 100644 --- a/src/book/bookitem.rs +++ b/src/book/bookitem.rs @@ -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, - 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 = 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) } } } diff --git a/src/book/mdbook.rs b/src/book/mdbook.rs index f5ee7838..6fda6205 100644 --- a/src/book/mdbook.rs +++ b/src/book/mdbook.rs @@ -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> { - // 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(()) } diff --git a/src/parse/summary.rs b/src/parse/summary.rs index fcd3582b..be3cf7e0 100644 --- a/src/parse/summary.rs +++ b/src/parse/summary.rs @@ -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> { debug!("[fn]: construct_bookitems"); @@ -9,36 +9,106 @@ pub fn construct_bookitems(path: &PathBuf) -> Result> { 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> { +fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec) -> Result> { debug!("[fn]: parse_level"); let mut items: Vec = 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 { debug!("[fn]: level"); let mut spaces = 0; @@ -74,49 +144,33 @@ fn level(line: &str, spaces_in_tab: i32) -> Result { fn parse_line(l: &str) -> Option { 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 { 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)) +} diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 93285385..615afd3e 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -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, Box> { 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); diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs index a2c3d46d..7c8d9ecf 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/src/renderer/html_handlebars/helpers/toc.rs @@ -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("
  • ".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("
  • ".as_bytes())); try!(rc.writer.write("
      ".as_bytes())); @@ -42,7 +49,11 @@ impl HelperDef for RenderToc { try!(rc.writer.write("
    • ".as_bytes())); } else { - try!(rc.writer.write("
    • ".as_bytes())); + try!(rc.writer.write("".as_bytes())); } // Link @@ -74,10 +85,16 @@ impl HelperDef for RenderToc { false }; - try!(rc.writer.write("".as_bytes())); - try!(rc.writer.write(item.get("section").expect("Error: section should be Some(_)").as_bytes())); - try!(rc.writer.write(" ".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("".as_bytes())); + try!(rc.writer.write(section.as_bytes())); + try!(rc.writer.write(" ".as_bytes())); + } + + if let Some(name) = item.get("name") { + try!(rc.writer.write(name.as_bytes())); + } if path_exists { try!(rc.writer.write("".as_bytes())); diff --git a/src/theme/book.css b/src/theme/book.css index d2f6a7cf..31b874f5 100644 --- a/src/theme/book.css +++ b/src/theme/book.css @@ -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;