First big step for the html renderer, it reads an handlebars template and creates the files from SUMMARY.md respecting the source folder structure

This commit is contained in:
Mathieu David 2015-07-19 00:08:38 +02:00
parent 35be20da8b
commit 4d4f35ecba
12 changed files with 289 additions and 53 deletions

View File

@ -5,3 +5,5 @@ authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
[dependencies]
getopts = "*"
handlebars = "*"
rustc-serialize = "*"

View File

@ -1,6 +1,9 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct BookConfig {
title: String,
author: String,
dest: PathBuf,
src: PathBuf,
indent_spaces: i32,
@ -11,6 +14,8 @@ pub struct BookConfig {
impl BookConfig {
pub fn new() -> Self {
BookConfig {
title: String::new(),
author: String::new(),
dest: PathBuf::from("book"),
src: PathBuf::from("src"),
indent_spaces: 4,
@ -18,21 +23,31 @@ impl BookConfig {
}
}
pub fn dest(&self) -> PathBuf {
self.dest.clone()
pub fn dest(&self) -> &Path {
&self.dest
}
pub fn set_dest(mut self, dest: PathBuf) -> Self {
self.dest = dest;
pub fn set_dest(&mut self, dest: &Path) -> &mut Self {
self.dest = dest.to_owned();
self
}
pub fn src(&self) -> PathBuf {
self.src.clone()
pub fn src(&self) -> &Path {
&self.src
}
pub fn set_src(mut self, src: PathBuf) -> Self {
self.src = src;
pub fn set_src(&mut self, src: &Path) -> &mut Self {
self.src = src.to_owned();
self
}
pub fn set_title(&mut self, title: &str) -> &mut Self {
self.title = title.to_owned();
self
}
pub fn set_author(&mut self, author: &str) -> &mut Self {
self.author = author.to_owned();
self
}
}

View File

@ -1,5 +1,10 @@
use std::path::PathBuf;
extern crate rustc_serialize;
use self::rustc_serialize::json::{Json, ToJson};
use std::path::PathBuf;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct BookItem {
pub name: String,
pub path: PathBuf,
@ -7,12 +12,14 @@ pub struct BookItem {
spacer: bool,
}
pub enum ItemType {
Pre,
Chapter,
Post
#[derive(Debug, Clone)]
pub struct BookItems<'a> {
pub items: &'a [BookItem],
pub current_index: usize,
pub stack: Vec<(&'a [BookItem], usize)>,
}
impl BookItem {
pub fn new(name: String, path: PathBuf) -> Self {
@ -33,9 +40,53 @@ impl BookItem {
spacer: true,
}
}
fn push(&mut self, item: BookItem) {
self.sub_items.push(item);
}
impl ToJson for BookItem {
fn to_json(&self) -> Json {
let mut m: BTreeMap<String, Json> = BTreeMap::new();
m.insert("name".to_string(), self.name.to_json());
m.insert("path".to_string(),self.path.to_str()
.expect("Json conversion failed for path").to_json()
);
m.to_json()
}
}
// 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);
fn next(&mut self) -> Option<(String, &'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.get(self.current_index).unwrap();
let mut section = "".to_string();
for &(_, idx) in &self.stack {
section.push_str(&(idx + 1).to_string()[..]);
section.push('.');
}
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))
}
}
}
}

View File

@ -1,21 +1,23 @@
use std::path::PathBuf;
use std::path::Path;
use std::fs::{self, File, metadata};
use std::io::{Write, Result};
use std::io::{self, Write};
use std::error::Error;
use book::bookconfig::BookConfig;
use book::bookitem::BookItem;
use {BookConfig, BookItem};
use book::BookItems;
use parse;
use renderer::Renderer;
use renderer::HtmlHandlebars;
pub struct MDBook {
title: String,
author: String,
config: BookConfig,
pub content: Vec<BookItem>
pub content: Vec<BookItem>,
renderer: Box<Renderer>,
}
impl MDBook {
pub fn new(path: &PathBuf) -> Self {
pub fn new(path: &Path) -> MDBook {
// Hacky way to check if the path exists... Until PathExt moves to stable
match metadata(path) {
@ -28,16 +30,24 @@ impl MDBook {
}
MDBook {
title: String::from(""),
author: String::from(""),
content: vec![],
config: BookConfig::new()
.set_src(path.join("src"))
.set_dest(path.join("book")),
.set_src(&path.join("src"))
.set_dest(&path.join("book"))
.to_owned(),
renderer: Box::new(HtmlHandlebars::new()),
}
}
pub fn init(&self) -> Result<()> {
pub fn iter(&self) -> BookItems {
BookItems {
items: &self.content[..],
current_index: 0,
stack: Vec::new(),
}
}
pub fn init(&self) -> Result<(), Box<Error>> {
let dest = self.config.dest();
let src = self.config.src();
@ -87,38 +97,43 @@ impl MDBook {
return Ok(());
}
pub fn build(&mut self) -> Result<()> {
pub fn build(&mut self) -> Result<(), Box<Error>> {
try!(self.parse_summary());
try!(self.renderer.render(
self.iter(),
&self.config,
));
Ok(())
}
// Builder functions
pub fn set_dest(mut self, dest: PathBuf) -> Self {
self.config = self.config.set_dest(dest);
pub fn set_dest(mut self, dest: &Path) -> Self {
self.config.set_dest(dest);
self
}
pub fn set_src(mut self, src: PathBuf) -> Self {
self.config = self.config.set_src(src);
pub fn set_src(mut self, src: &Path) -> Self {
self.config.set_src(src);
self
}
pub fn set_title(mut self, title: String) -> Self {
self.title = title;
pub fn set_title(mut self, title: &str) -> Self {
self.config.set_title(title);
self
}
pub fn set_author(mut self, author: String) -> Self {
self.author = author;
pub fn set_author(mut self, author: &str) -> Self {
self.config.set_author(author);
self
}
// Construct book
fn parse_summary(&mut self) -> Result<()> {
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
// When append becomes stale, use self.content.append() ...
let book_items = try!(parse::construct_bookitems(&self.config.src().join("SUMMARY.md")));
@ -127,11 +142,6 @@ impl MDBook {
self.content.push(item)
}
// Debug
for item in &self.content {
println!("name: \"{}\" path: {:?}", item.name, item.path);
}
Ok(())
}

View File

@ -2,6 +2,6 @@ pub mod mdbook;
pub mod bookitem;
mod bookconfig;
use self::bookconfig::BookConfig;
use self::bookitem::BookItem;
pub use self::bookitem::{BookItem, BookItems};
pub use self::bookconfig::BookConfig;
pub use self::mdbook::MDBook;

View File

@ -1,5 +1,8 @@
mod book;
mod parse;
pub mod renderer;
pub mod theme;
pub use book::mdbook::MDBook;
use book::bookitem::BookItem;
pub use book::MDBook;
pub use book::BookItem;
pub use book::BookConfig;

View File

@ -82,7 +82,7 @@ fn parse_line(l: &str) -> Option<BookItem> {
let mut name;
let mut path;
// Remove leading and trailing spaces or tabs
let mut line = l.trim_matches(|c: char| { c == ' ' || c == '\t' });
let line = l.trim_matches(|c: char| { c == ' ' || c == '\t' });
if let Some(c) = line.chars().nth(0) {
match c {

View File

@ -0,0 +1,121 @@
extern crate handlebars;
extern crate rustc_serialize;
use renderer::Renderer;
use book::{BookItems, BookItem, BookConfig};
use theme;
use std::path::{Path, PathBuf, Component};
use std::fs::{self, File, metadata};
use std::error::Error;
use std::io::{self, Read, Write};
use self::handlebars::Handlebars;
use self::rustc_serialize::json::{Json, ToJson};
use std::collections::BTreeMap;
pub struct HtmlHandlebars;
impl Renderer for HtmlHandlebars {
fn render(&self, book: BookItems, config: &BookConfig) -> Result<(), Box<Error>> {
let mut handlebars = Handlebars::new();
// Load template
let t = theme::get_index_hbs();
// Register template
try!(handlebars.register_template_string("index", t.to_owned()));
let mut data = BTreeMap::new();
let mut chapters: Vec<(String, BookItem)> = vec![];
// Hacky way to prevent move error... Not optimal
for (section, item) in book.clone() {
chapters.push((section, item.clone()));
}
data.insert("chapters".to_string(), chapters.to_json());
for (_, item) in book {
if item.path != PathBuf::new() {
let rendered = try!(handlebars.render("index", &data));
let mut file = try!(create_file(config.dest(), &item.path));
try!(file.write_all(&rendered.into_bytes()));
}
}
Ok(())
}
}
impl HtmlHandlebars {
pub fn new() -> Self {
HtmlHandlebars
}
fn _load_template(&self, path: &Path) -> Result<String, Box<Error>> {
let mut file = try!(File::open(path));
let mut s = String::new();
try!(file.read_to_string(&mut s));
Ok(s)
}
}
fn create_file(working_directory: &Path, path: &Path) -> Result<File, Box<Error>> {
println!("create_file:\n\t{:?}\n\t{:?}", working_directory, path);
// Extract filename
let mut file_name;
if let Some(name) = path.file_stem() {
file_name = String::from(name.to_str().unwrap());
}
else { return Err(Box::new(io::Error::new(io::ErrorKind::Other, "No filename"))) }
file_name.push_str(".html");
// Delete filename from path
let mut path = path.to_path_buf();
path.pop();
// Create directories if they do not exist
let mut constructed_path = PathBuf::from(working_directory);
for component in path.components() {
let mut dir;
match component {
Component::Normal(_) => { dir = PathBuf::from(component.as_os_str()); },
_ => continue,
}
constructed_path.push(&dir);
println!("constructed path= {:?}\ndir= {:?}", constructed_path, dir.as_os_str());
// Check if path exists
match metadata(&constructed_path) {
// Any way to combine the Err and first Ok branch ??
Err(_) => {
try!(fs::create_dir(&constructed_path))
},
Ok(f) => {
if !f.is_dir() {
try!(fs::create_dir(&constructed_path))
} else {
println!("Exists ??");
continue
}
},
}
}
let file = try!(File::create(
constructed_path.join(file_name)
));
Ok(file)
}

5
src/renderer/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub use self::renderer::Renderer;
pub use self::html_handlebars::HtmlHandlebars;
pub mod renderer;
pub mod html_handlebars;

8
src/renderer/renderer.rs Normal file
View File

@ -0,0 +1,8 @@
use book::{BookItem, BookItems};
use book::BookConfig;
use std::error::Error;
pub trait Renderer {
fn render(&self, book: BookItems, config: &BookConfig) -> Result<(), Box<Error>>;
}

16
src/theme/index.hbs Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html lang="{{ language }}">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{% block description %}{% endblock %}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="book.css" media="screen" title="no title" charset="utf-8">
</head>
<body>
<script src="book.js"></script>
</body>
</html>

5
src/theme/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub fn get_index_hbs() -> &'static str {
let index = include_str!("index.hbs");
index
}