mirror of
https://github.com/rust-lang/mdBook
synced 2024-12-12 13:52:36 +00:00
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:
parent
35be20da8b
commit
4d4f35ecba
12 changed files with 289 additions and 53 deletions
|
@ -5,3 +5,5 @@ authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
|
|||
|
||||
[dependencies]
|
||||
getopts = "*"
|
||||
handlebars = "*"
|
||||
rustc-serialize = "*"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
121
src/renderer/html_handlebars.rs
Normal file
121
src/renderer/html_handlebars.rs
Normal 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
5
src/renderer/mod.rs
Normal 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
8
src/renderer/renderer.rs
Normal 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
16
src/theme/index.hbs
Normal 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
5
src/theme/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
pub fn get_index_hbs() -> &'static str {
|
||||
let index = include_str!("index.hbs");
|
||||
index
|
||||
}
|
Loading…
Reference in a new issue