@@ -4,16 +4,17 @@ use std::sync::mpsc::channel; | |||
use std::time::{Instant, Duration}; | |||
use std::thread; | |||
use chrono::prelude::*; | |||
use iron::{Iron, Request, IronResult, Response, status}; | |||
use mount::Mount; | |||
use staticfile::Static; | |||
use notify::{Watcher, RecursiveMode, watcher}; | |||
use ws::{WebSocket}; | |||
use ws::{WebSocket, Sender}; | |||
use gutenberg::Site; | |||
use gutenberg::errors::{Result}; | |||
use ::time_elapsed; | |||
use ::report_elapsed_time; | |||
#[derive(Debug, PartialEq)] | |||
@@ -31,10 +32,38 @@ fn livereload_handler(_: &mut Request) -> IronResult<Response> { | |||
} | |||
fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &str) { | |||
match res { | |||
Ok(_) => { | |||
broadcaster.send(format!(r#" | |||
{{ | |||
"command": "reload", | |||
"path": "{}", | |||
"originalPath": "", | |||
"liveCSS": true, | |||
"liveImg": true, | |||
"protocol": ["http://livereload.com/protocols/official-7"] | |||
}}"#, reload_path) | |||
).unwrap(); | |||
}, | |||
Err(e) => { | |||
println!("Failed to build the site"); | |||
println!("Error: {}", e); | |||
for e in e.iter().skip(1) { | |||
println!("Reason: {}", e) | |||
} | |||
} | |||
} | |||
} | |||
// Most of it taken from mdbook | |||
pub fn serve(interface: &str, port: &str) -> Result<()> { | |||
println!("Building site..."); | |||
let start = Instant::now(); | |||
let mut site = Site::new(true)?; | |||
site.build()?; | |||
report_elapsed_time(start); | |||
let address = format!("{}:{}", interface, port); | |||
let ws_address = format!("{}:{}", interface, "1112"); | |||
@@ -67,7 +96,7 @@ pub fn serve(interface: &str, port: &str) -> Result<()> { | |||
watcher.watch("templates/", RecursiveMode::Recursive).unwrap(); | |||
let pwd = format!("{}", env::current_dir().unwrap().display()); | |||
println!("Listening for changes in {}/{{content, static, templates}}", pwd); | |||
println!("Press CTRL+C to stop"); | |||
println!("Press CTRL+C to stop\n"); | |||
use notify::DebouncedEvent::*; | |||
@@ -85,41 +114,23 @@ pub fn serve(interface: &str, port: &str) -> Result<()> { | |||
continue; | |||
} | |||
println!("Change detected, rebuilding site"); | |||
let what_changed = detect_change_kind(&pwd, &path); | |||
let mut reload_path = String::new(); | |||
match what_changed { | |||
(ChangeKind::Content, _) => println!("Content changed {}", path.display()), | |||
(ChangeKind::Templates, _) => println!("Template changed {}", path.display()), | |||
println!("Change detected @ {}", Local::now().format("%Y-%m-%d %H:%M:%S").to_string()); | |||
let start = Instant::now(); | |||
match detect_change_kind(&pwd, &path) { | |||
(ChangeKind::Content, _) => { | |||
println!("-> Content changed {}", path.display()); | |||
rebuild_done_handling(&broadcaster, site.rebuild(), ""); | |||
}, | |||
(ChangeKind::Templates, _) => { | |||
println!("-> Template changed {}", path.display()); | |||
rebuild_done_handling(&broadcaster, site.rebuild_after_template_change(), ""); | |||
}, | |||
(ChangeKind::StaticFiles, p) => { | |||
reload_path = p; | |||
println!("Static file changes detected {}", path.display()); | |||
println!("-> Static file changes detected {}", path.display()); | |||
rebuild_done_handling(&broadcaster, site.copy_static_directory(), &p); | |||
}, | |||
}; | |||
println!("Reloading {}", reload_path); | |||
let start = Instant::now(); | |||
match site.rebuild() { | |||
Ok(_) => { | |||
println!("Done in {:.1}s.\n", time_elapsed(start)); | |||
broadcaster.send(format!(r#" | |||
{{ | |||
"command": "reload", | |||
"path": "{}", | |||
"originalPath": "", | |||
"liveCSS": true, | |||
"liveImg": true, | |||
"protocol": ["http://livereload.com/protocols/official-7"] | |||
}}"#, reload_path)).unwrap(); | |||
}, | |||
Err(e) => { | |||
println!("Failed to build the site"); | |||
println!("Error: {}", e); | |||
for e in e.iter().skip(1) { | |||
println!("Reason: {}", e) | |||
} | |||
} | |||
} | |||
report_elapsed_time(start); | |||
} | |||
_ => {} | |||
} | |||
@@ -14,6 +14,7 @@ pub struct Config { | |||
pub title: String, | |||
/// Base URL of the site | |||
pub base_url: String, | |||
/// Whether to highlight all code blocks found in markdown files. Defaults to false | |||
pub highlight_code: Option<bool>, | |||
/// Description of the site | |||
@@ -19,9 +19,15 @@ mod cmd; | |||
// Print the time elapsed rounded to 1 decimal | |||
fn time_elapsed(instant: Instant) -> f64 { | |||
let duration_ms = Duration::from_std(instant.elapsed()).unwrap().num_milliseconds() as f64 / 1000.0; | |||
(duration_ms * 10.0).round() / 10.0 | |||
fn report_elapsed_time(instant: Instant) { | |||
let duration_ms = Duration::from_std(instant.elapsed()).unwrap().num_milliseconds() as f64; | |||
if duration_ms < 1000.0 { | |||
println!("Done in {}ms.\n", duration_ms); | |||
} else { | |||
let duration_sec = duration_ms / 1000.0; | |||
println!("Done in {:.1}s.\n", ((duration_sec * 10.0).round() / 10.0)); | |||
} | |||
} | |||
@@ -58,10 +64,11 @@ fn main() { | |||
}; | |||
}, | |||
("build", Some(_)) => { | |||
println!("Building site"); | |||
let start = Instant::now(); | |||
match cmd::build() { | |||
Ok(()) => { | |||
println!("Site built in {:.1}s.", time_elapsed(start)); | |||
report_elapsed_time(start); | |||
}, | |||
Err(e) => { | |||
println!("Failed to build the site"); | |||
@@ -1,6 +1,6 @@ | |||
use std::collections::HashMap; | |||
use std::iter::FromIterator; | |||
use std::fs::{create_dir, remove_dir_all, copy, remove_file}; | |||
use std::fs::{remove_dir_all, copy, remove_file}; | |||
use std::path::Path; | |||
use glob::glob; | |||
@@ -11,7 +11,7 @@ use walkdir::WalkDir; | |||
use errors::{Result, ResultExt}; | |||
use config::{Config, get_config}; | |||
use page::Page; | |||
use utils::create_file; | |||
use utils::{create_file, create_directory}; | |||
lazy_static! { | |||
@@ -108,10 +108,6 @@ impl Site { | |||
html | |||
} | |||
/// Reload the Tera templates | |||
pub fn reload_templates(&mut self) -> Result<()> { | |||
Ok(()) | |||
} | |||
/// Copy the content of the `static` folder into the `public` folder | |||
/// | |||
@@ -131,7 +127,7 @@ impl Site { | |||
if entry.path().is_dir() { | |||
if !target_path.exists() { | |||
create_dir(&target_path)?; | |||
create_directory(&target_path)?; | |||
} | |||
} else { | |||
if target_path.exists() { | |||
@@ -143,24 +139,34 @@ impl Site { | |||
Ok(()) | |||
} | |||
/// Deletes the `public` directory if it exists | |||
pub fn clean(&self) -> Result<()> { | |||
if Path::new("public").exists() { | |||
// Delete current `public` directory so we can start fresh | |||
remove_dir_all("public").chain_err(|| "Couldn't delete `public` directory")?; | |||
} | |||
Ok(()) | |||
} | |||
/// Re-parse and re-generate the site | |||
/// Very dumb for now, ideally it would only rebuild what changed | |||
pub fn rebuild(&mut self) -> Result<()> { | |||
self.parse_site()?; | |||
self.templates.full_reload()?; | |||
self.build() | |||
} | |||
/// Builds the site to the `public` directory after deleting it | |||
pub fn build(&self) -> Result<()> { | |||
if Path::new("public").exists() { | |||
// Delete current `public` directory so we can start fresh | |||
remove_dir_all("public").chain_err(|| "Couldn't delete `public` directory")?; | |||
} | |||
pub fn rebuild_after_template_change(&mut self) -> Result<()> { | |||
self.templates.full_reload()?; | |||
println!("Reloaded templates"); | |||
self.build_pages() | |||
} | |||
// Start from scratch | |||
create_dir("public")?; | |||
pub fn build_pages(&self) -> Result<()> { | |||
let public = Path::new("public"); | |||
if !public.exists() { | |||
create_directory(&public)?; | |||
} | |||
let mut pages = vec![]; | |||
let mut category_pages: HashMap<String, Vec<&Page>> = HashMap::new(); | |||
@@ -175,7 +181,7 @@ impl Site { | |||
current_path.push(section); | |||
if !current_path.exists() { | |||
create_dir(¤t_path)?; | |||
create_directory(¤t_path)?; | |||
} | |||
} | |||
@@ -185,7 +191,7 @@ impl Site { | |||
} | |||
// Make sure the folder exists | |||
create_dir(¤t_path)?; | |||
create_directory(¤t_path)?; | |||
// Finally, create a index.html file there with the page rendered | |||
let output = page.render_html(&self.templates, &self.config)?; | |||
create_file(current_path.join("index.html"), &self.inject_livereload(output))?; | |||
@@ -205,9 +211,6 @@ impl Site { | |||
self.render_categories_and_tags(RenderList::Categories, &category_pages)?; | |||
self.render_categories_and_tags(RenderList::Tags, &tag_pages)?; | |||
self.render_sitemap()?; | |||
self.render_rss_feed()?; | |||
// And finally the index page | |||
let mut context = Context::new(); | |||
pages.sort_by(|a, b| a.partial_cmp(b).unwrap()); | |||
@@ -216,10 +219,18 @@ impl Site { | |||
let index = self.templates.render("index.html", &context)?; | |||
create_file(public.join("index.html"), &self.inject_livereload(index))?; | |||
self.copy_static_directory()?; | |||
Ok(()) | |||
} | |||
/// Builds the site to the `public` directory after deleting it | |||
pub fn build(&self) -> Result<()> { | |||
self.clean()?; | |||
self.build_pages()?; | |||
self.render_sitemap()?; | |||
self.render_rss_feed()?; | |||
self.copy_static_directory() | |||
} | |||
/// Render the /{categories, list} pages and each individual category/tag page | |||
fn render_categories_and_tags(&self, kind: RenderList, container: &HashMap<String, Vec<&Page>>) -> Result<()> { | |||
if container.is_empty() { | |||
@@ -235,7 +246,7 @@ impl Site { | |||
let public = Path::new("public"); | |||
let mut output_path = public.to_path_buf(); | |||
output_path.push(name); | |||
create_dir(&output_path)?; | |||
create_directory(&output_path)?; | |||
// First we render the list of categories/tags page | |||
let mut sorted_container = vec![]; | |||
@@ -262,7 +273,7 @@ impl Site { | |||
context.add("config", &self.config); | |||
let single_output = self.templates.render(single_tpl_name, &context)?; | |||
create_dir(&output_path.join(&slug))?; | |||
create_directory(&output_path.join(&slug))?; | |||
create_file( | |||
output_path.join(&slug).join("index.html"), | |||
&self.inject_livereload(single_output) | |||
@@ -283,14 +294,6 @@ impl Site { | |||
Ok(()) | |||
} | |||
fn get_rss_feed_url(&self) -> String { | |||
if self.config.base_url.ends_with("/") { | |||
format!("{}{}", self.config.base_url, "feed.xml") | |||
} else { | |||
format!("{}/{}", self.config.base_url, "feed.xml") | |||
} | |||
} | |||
fn render_rss_feed(&self) -> Result<()> { | |||
let mut context = Context::new(); | |||
let mut pages = self.pages.values() | |||
@@ -306,7 +309,13 @@ impl Site { | |||
context.add("pages", &pages); | |||
context.add("last_build_date", &pages[0].meta.date); | |||
context.add("config", &self.config); | |||
context.add("feed_url", &self.get_rss_feed_url()); | |||
let rss_feed_url = if self.config.base_url.ends_with("/") { | |||
format!("{}{}", self.config.base_url, "feed.xml") | |||
} else { | |||
format!("{}/{}", self.config.base_url, "feed.xml") | |||
}; | |||
context.add("feed_url", &rss_feed_url); | |||
let sitemap = self.templates.render("rss.xml", &context)?; | |||
@@ -1,8 +1,8 @@ | |||
use std::io::prelude::*; | |||
use std::fs::{File}; | |||
use std::fs::{File, create_dir}; | |||
use std::path::Path; | |||
use errors::Result; | |||
use errors::{Result, ResultExt}; | |||
pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> { | |||
@@ -10,3 +10,14 @@ pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> { | |||
file.write_all(content.as_bytes())?; | |||
Ok(()) | |||
} | |||
/// Very similar to create_dir from the std except it checks if the folder | |||
/// exists before creating it | |||
pub fn create_directory<P: AsRef<Path>>(path: P) -> Result<()> { | |||
let path = path.as_ref(); | |||
if !path.exists() { | |||
create_dir(path) | |||
.chain_err(|| format!("Was not able to create folder {}", path.display()))?; | |||
} | |||
Ok(()) | |||
} |