watch and serve are back

This commit is contained in:
Gambhiro 2017-01-11 15:17:34 +00:00
parent c021940331
commit 3aa8f7d925
4 changed files with 189 additions and 28 deletions

View File

@ -99,6 +99,9 @@
</div> </div>
<!-- Livereload script (if served using the cli tool) -->
{{{livereload}}}
<script src="js/highlight.js"></script> <script src="js/highlight.js"></script>
<script src="js/book.js"></script> <script src="js/book.js"></script>
</body> </body>

View File

@ -29,6 +29,12 @@ use std::path::{Path, PathBuf};
use clap::{App, ArgMatches, SubCommand, AppSettings}; use clap::{App, ArgMatches, SubCommand, AppSettings};
// Uses for the Watch feature
#[cfg(feature = "watch")]
use notify::Watcher;
#[cfg(feature = "watch")]
use std::sync::mpsc::channel;
use mdbook::MDBook; use mdbook::MDBook;
use mdbook::renderer::{Renderer, HtmlHandlebars}; use mdbook::renderer::{Renderer, HtmlHandlebars};
use mdbook::utils; use mdbook::utils;
@ -164,7 +170,24 @@ fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
// Watch command implementation // Watch command implementation
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> { fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
// TODO watch let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir);
book.read_config();
trigger_on_change(&mut book, |event, book| {
if let Some(path) = event.path {
println!("File changed: {:?}\nBuilding book...\n", path);
// TODO figure out render format intent when we acutally have different renderers
let renderer = HtmlHandlebars::new();
match renderer.build(&book_dir) {
Err(e) => println!("Error while building: {:?}", e),
_ => {},
}
println!("");
}
});
println!("watch"); println!("watch");
Ok(()) Ok(())
} }
@ -172,8 +195,72 @@ fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
// Serve command implementation // Serve command implementation
#[cfg(feature = "serve")] #[cfg(feature = "serve")]
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> { fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
// TODO serve const RELOAD_COMMAND: &'static str = "reload";
println!("serve");
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir);
book.read_config();
book.parse_books();
book.link_translations();
let port = args.value_of("port").unwrap_or("3000");
let ws_port = args.value_of("ws-port").unwrap_or("3001");
let interface = args.value_of("interface").unwrap_or("localhost");
let public_address = args.value_of("address").unwrap_or(interface);
let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, ws_port);
book.livereload_script = Some(format!(r#"
<script type="text/javascript">
var socket = new WebSocket("ws://{}:{}");
socket.onmessage = function (event) {{
if (event.data === "{}") {{
socket.close();
location.reload(true); // force reload from server (not from cache)
}}
}};
window.onbeforeunload = function() {{
socket.close();
}}
</script>
"#, public_address, ws_port, RELOAD_COMMAND));
// TODO it's OK that serve only makes sense for the html output format, but formatlize that selection
let renderer = HtmlHandlebars::new();
try!(renderer.render(&book));
let staticfile = staticfile::Static::new(book.get_dest_base());
let iron = iron::Iron::new(staticfile);
let _iron = iron.http(&*address).unwrap();
let ws_server = ws::WebSocket::new(|_| {
|_| {
Ok(())
}
}).unwrap();
let broadcaster = ws_server.broadcaster();
std::thread::spawn(move || {
ws_server.listen(&*ws_address).unwrap();
});
println!("\nServing on {}", address);
trigger_on_change(&mut book, move |event, book| {
if let Some(path) = event.path {
println!("File changed: {:?}\nBuilding book...\n", path);
let renderer = HtmlHandlebars::new();
match renderer.render(&book) {
Err(e) => println!("Error while building: {:?}", e),
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
}
println!("");
}
});
Ok(()) Ok(())
} }
@ -196,3 +283,62 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
env::current_dir().unwrap() env::current_dir().unwrap()
} }
} }
// Calls the closure when a book source file is changed. This is blocking!
#[cfg(feature = "watch")]
fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
where F: Fn(notify::Event, &mut MDBook) -> ()
{
// Create a channel to receive the events.
let (tx, rx) = channel();
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
match w {
Ok(mut watcher) => {
// Add the source directory to the watcher
if let Err(e) = watcher.watch(book.get_src_base()) {
println!("Error while watching {:?}:\n {:?}", book.get_src_base(), e);
::std::process::exit(0);
};
// Add the book.toml or book.json file to the watcher if it exists,
// because it's not located in the source directory
if let Err(_) = watcher.watch(book.get_project_root().join("book.toml")) {
// do nothing if book.toml is not found
}
if let Err(_) = watcher.watch(book.get_project_root().join("book.json")) {
// do nothing if book.json is not found
}
let mut previous_time = time::get_time();
println!("\nListening for changes...\n");
loop {
match rx.recv() {
Ok(event) => {
// Skip the event if an event has already been issued in the last second
let time = time::get_time();
if time - previous_time < time::Duration::seconds(1) {
continue;
} else {
previous_time = time;
}
closure(event, book);
},
Err(e) => {
println!("An error occured: {:?}", e);
},
}
}
},
Err(e) => {
println!("Error while trying to watch the files:\n\n\t{:?}", e);
::std::process::exit(0);
},
}
}

View File

@ -38,6 +38,9 @@ pub struct MDBook {
/// Html Handlebars: `project_root` + `assets/_html-template`. /// Html Handlebars: `project_root` + `assets/_html-template`.
template_dir: PathBuf, template_dir: PathBuf,
/// Input base for all books, relative to `project_root`. Defaults to `src`.
src_base: PathBuf,// FIXME use this
/// Output base for all books, relative to `project_root`. Defaults to /// Output base for all books, relative to `project_root`. Defaults to
/// `book`. /// `book`.
dest_base: PathBuf, dest_base: PathBuf,
@ -46,9 +49,6 @@ pub struct MDBook {
/// default or CLI argument. /// default or CLI argument.
render_intent: RenderIntent, render_intent: RenderIntent,
// TODO Identify and cross-link translations either by file name, or an id
// string.
/// The book, or books in case of translations, accessible with a String /// The book, or books in case of translations, accessible with a String
/// key. The keys can be two-letter codes of the translation such as 'en' or /// key. The keys can be two-letter codes of the translation such as 'en' or
/// 'fr', but this is not enforced. /// 'fr', but this is not enforced.
@ -71,7 +71,6 @@ pub struct MDBook {
/// block: /// block:
/// ///
/// ```toml /// ```toml
/// livereload = true
/// title = "Alice in Wonderland" /// title = "Alice in Wonderland"
/// author = "Lewis Carroll" /// author = "Lewis Carroll"
/// ``` /// ```
@ -79,21 +78,19 @@ pub struct MDBook {
/// For multiple languages, declare them in blocks: /// For multiple languages, declare them in blocks:
/// ///
/// ```toml /// ```toml
/// livereload = true /// [[translations.en]]
///
/// [translations.en]
/// title = "Alice in Wonderland" /// title = "Alice in Wonderland"
/// author = "Lewis Carroll" /// author = "Lewis Carroll"
/// language = { name = "English", code = "en" } /// language = { name = "English", code = "en" }
/// is_main_book = true /// is_main_book = true
/// ///
/// [translations.fr] /// [[translations.fr]]
/// title = "Alice au pays des merveilles" /// title = "Alice au pays des merveilles"
/// author = "Lewis Carroll" /// author = "Lewis Carroll"
/// translator = "Henri Bué" /// translator = "Henri Bué"
/// language = { name = "Français", code = "fr" } /// language = { name = "Français", code = "fr" }
/// ///
/// [translations.hu] /// [[translations.hu]]
/// title = "Alice Csodaországban" /// title = "Alice Csodaországban"
/// author = "Lewis Carroll" /// author = "Lewis Carroll"
/// translator = "Kosztolányi Dezső" /// translator = "Kosztolányi Dezső"
@ -104,8 +101,9 @@ pub struct MDBook {
/// Space indentation in SUMMARY.md, defaults to 4 spaces. /// Space indentation in SUMMARY.md, defaults to 4 spaces.
pub indent_spaces: i32, pub indent_spaces: i32,
/// Whether to include the livereload snippet in the output html. /// The `<script>` tag to insert in the render template. It is used with the
pub livereload: bool, /// 'serve' command, which is responsible for setting it.
pub livereload_script: Option<String>,
} }
impl Default for MDBook { impl Default for MDBook {
@ -113,11 +111,12 @@ impl Default for MDBook {
let mut proj: MDBook = MDBook { let mut proj: MDBook = MDBook {
project_root: PathBuf::from("".to_string()), project_root: PathBuf::from("".to_string()),
template_dir: PathBuf::from("".to_string()), template_dir: PathBuf::from("".to_string()),
src_base: PathBuf::from("src".to_string()),
dest_base: PathBuf::from("book".to_string()), dest_base: PathBuf::from("book".to_string()),
render_intent: RenderIntent::HtmlHandlebars, render_intent: RenderIntent::HtmlHandlebars,
translations: HashMap::new(), translations: HashMap::new(),
indent_spaces: 4, indent_spaces: 4,
livereload: false, livereload_script: None,
}; };
proj.set_project_root(&env::current_dir().unwrap()); proj.set_project_root(&env::current_dir().unwrap());
// sets default template_dir // sets default template_dir
@ -293,13 +292,6 @@ impl MDBook {
} }
config.remove("indent_spaces"); config.remove("indent_spaces");
if let Some(a) = config.get("livereload") {
if let Some(b) = a.as_bool() {
self.livereload = b;
}
}
config.remove("livereload");
// If there is a 'translations' table, configugre each book from that. // If there is a 'translations' table, configugre each book from that.
// If there isn't, take the rest of the config as one book. // If there isn't, take the rest of the config as one book.
@ -459,6 +451,19 @@ impl MDBook {
self self
} }
pub fn get_src_base(&self) -> PathBuf {
self.project_root.join(&self.src_base)
}
pub fn set_src_base(&mut self, path: &PathBuf) -> &mut MDBook {
if path.as_os_str() == OsStr::new(".") {
self.src_base = PathBuf::from("".to_string());
} else {
self.src_base = path.to_owned();
}
self
}
pub fn get_dest_base(&self) -> PathBuf { pub fn get_dest_base(&self) -> PathBuf {
self.project_root.join(&self.dest_base) self.project_root.join(&self.dest_base)
} }

View File

@ -189,7 +189,7 @@ impl Renderer for HtmlHandlebars {
content = helpers::playpen::render_playpen(&content, p); content = helpers::playpen::render_playpen(&content, p);
} }
let mut data = try!(make_data(&book, &chapter, &content)); let mut data = try!(make_data(&book, &chapter, &content, &book_project.livereload_script));
data.remove("path_to_root"); data.remove("path_to_root");
data.insert("path_to_root".to_owned(), "".to_json()); data.insert("path_to_root".to_owned(), "".to_json());
@ -214,7 +214,7 @@ impl Renderer for HtmlHandlebars {
} }
// Render a file for every entry in the book // Render a file for every entry in the book
try!(self.process_items(&book.toc, &book, &handlebars)); try!(self.process_items(&book.toc, &book, &book_project.livereload_script, &handlebars));
} }
Ok(()) Ok(())
@ -226,6 +226,7 @@ impl HtmlHandlebars {
fn process_items(&self, fn process_items(&self,
items: &Vec<TocItem>, items: &Vec<TocItem>,
book: &Book, book: &Book,
livereload_script: &Option<String>,
handlebars: &Handlebars) handlebars: &Handlebars)
-> Result<(), Box<Error>> { -> Result<(), Box<Error>> {
@ -241,11 +242,11 @@ impl HtmlHandlebars {
// Option but currently only used for rendering a chapter as // Option but currently only used for rendering a chapter as
// index.html. // index.html.
if i.chapter.path.as_os_str().len() > 0 { if i.chapter.path.as_os_str().len() > 0 {
try!(self.process_chapter(&i.chapter, book, handlebars)); try!(self.process_chapter(&i.chapter, book, livereload_script, handlebars));
} }
if let Some(ref subs) = i.sub_items { if let Some(ref subs) = i.sub_items {
try!(self.process_items(&subs, book, handlebars)); try!(self.process_items(&subs, book, livereload_script, handlebars));
} }
}, },
@ -259,6 +260,7 @@ impl HtmlHandlebars {
fn process_chapter(&self, fn process_chapter(&self,
chapter: &Chapter, chapter: &Chapter,
book: &Book, book: &Book,
livereload_script: &Option<String>,
handlebars: &Handlebars) handlebars: &Handlebars)
-> Result<(), Box<Error>> { -> Result<(), Box<Error>> {
@ -269,7 +271,7 @@ impl HtmlHandlebars {
content = helpers::playpen::render_playpen(&content, p); content = helpers::playpen::render_playpen(&content, p);
} }
let data = try!(make_data(book, chapter, &content)); let data = try!(make_data(book, chapter, &content, livereload_script));
// Rendere the handlebars template with the data // Rendere the handlebars template with the data
debug!("[*]: Render template"); debug!("[*]: Render template");
@ -296,7 +298,8 @@ impl HtmlHandlebars {
fn make_data(book: &Book, fn make_data(book: &Book,
chapter: &Chapter, chapter: &Chapter,
content: &str) content: &str,
livereload_script: &Option<String>)
-> Result<serde_json::Map<String, serde_json::Value>, Box<Error>> { -> Result<serde_json::Map<String, serde_json::Value>, Box<Error>> {
debug!("[fn]: make_data"); debug!("[fn]: make_data");
@ -309,6 +312,10 @@ fn make_data(book: &Book,
data.insert("title".to_owned(), book.config.title.to_json()); data.insert("title".to_owned(), book.config.title.to_json());
data.insert("description".to_owned(), book.config.description.to_json()); data.insert("description".to_owned(), book.config.description.to_json());
if let Some(ref x) = *livereload_script {
data.insert("livereload".to_owned(), x.to_json());
}
// Chapter data // Chapter data
let mut path = if let Some(ref dest_path) = chapter.dest_path { let mut path = if let Some(ref dest_path) = chapter.dest_path {