watch and serve are back
This commit is contained in:
parent
c021940331
commit
3aa8f7d925
|
@ -99,6 +99,9 @@
|
|||
|
||||
</div>
|
||||
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
{{{livereload}}}
|
||||
|
||||
<script src="js/highlight.js"></script>
|
||||
<script src="js/book.js"></script>
|
||||
</body>
|
||||
|
|
|
@ -29,6 +29,12 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
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::renderer::{Renderer, HtmlHandlebars};
|
||||
use mdbook::utils;
|
||||
|
@ -164,7 +170,24 @@ fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
|||
// Watch command implementation
|
||||
#[cfg(feature = "watch")]
|
||||
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");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -172,8 +195,72 @@ fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
|
|||
// Serve command implementation
|
||||
#[cfg(feature = "serve")]
|
||||
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
// TODO serve
|
||||
println!("serve");
|
||||
const RELOAD_COMMAND: &'static str = "reload";
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -196,3 +283,62 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
|||
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);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,9 @@ pub struct MDBook {
|
|||
/// Html Handlebars: `project_root` + `assets/_html-template`.
|
||||
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
|
||||
/// `book`.
|
||||
dest_base: PathBuf,
|
||||
|
@ -46,9 +49,6 @@ pub struct MDBook {
|
|||
/// default or CLI argument.
|
||||
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
|
||||
/// key. The keys can be two-letter codes of the translation such as 'en' or
|
||||
/// 'fr', but this is not enforced.
|
||||
|
@ -71,7 +71,6 @@ pub struct MDBook {
|
|||
/// block:
|
||||
///
|
||||
/// ```toml
|
||||
/// livereload = true
|
||||
/// title = "Alice in Wonderland"
|
||||
/// author = "Lewis Carroll"
|
||||
/// ```
|
||||
|
@ -79,21 +78,19 @@ pub struct MDBook {
|
|||
/// For multiple languages, declare them in blocks:
|
||||
///
|
||||
/// ```toml
|
||||
/// livereload = true
|
||||
///
|
||||
/// [translations.en]
|
||||
/// [[translations.en]]
|
||||
/// title = "Alice in Wonderland"
|
||||
/// author = "Lewis Carroll"
|
||||
/// language = { name = "English", code = "en" }
|
||||
/// is_main_book = true
|
||||
///
|
||||
/// [translations.fr]
|
||||
/// [[translations.fr]]
|
||||
/// title = "Alice au pays des merveilles"
|
||||
/// author = "Lewis Carroll"
|
||||
/// translator = "Henri Bué"
|
||||
/// language = { name = "Français", code = "fr" }
|
||||
///
|
||||
/// [translations.hu]
|
||||
/// [[translations.hu]]
|
||||
/// title = "Alice Csodaországban"
|
||||
/// author = "Lewis Carroll"
|
||||
/// translator = "Kosztolányi Dezső"
|
||||
|
@ -104,8 +101,9 @@ pub struct MDBook {
|
|||
/// Space indentation in SUMMARY.md, defaults to 4 spaces.
|
||||
pub indent_spaces: i32,
|
||||
|
||||
/// Whether to include the livereload snippet in the output html.
|
||||
pub livereload: bool,
|
||||
/// The `<script>` tag to insert in the render template. It is used with the
|
||||
/// 'serve' command, which is responsible for setting it.
|
||||
pub livereload_script: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for MDBook {
|
||||
|
@ -113,11 +111,12 @@ impl Default for MDBook {
|
|||
let mut proj: MDBook = MDBook {
|
||||
project_root: PathBuf::from("".to_string()),
|
||||
template_dir: PathBuf::from("".to_string()),
|
||||
src_base: PathBuf::from("src".to_string()),
|
||||
dest_base: PathBuf::from("book".to_string()),
|
||||
render_intent: RenderIntent::HtmlHandlebars,
|
||||
translations: HashMap::new(),
|
||||
indent_spaces: 4,
|
||||
livereload: false,
|
||||
livereload_script: None,
|
||||
};
|
||||
proj.set_project_root(&env::current_dir().unwrap());
|
||||
// sets default template_dir
|
||||
|
@ -293,13 +292,6 @@ impl MDBook {
|
|||
}
|
||||
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 isn't, take the rest of the config as one book.
|
||||
|
||||
|
@ -459,6 +451,19 @@ impl MDBook {
|
|||
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 {
|
||||
self.project_root.join(&self.dest_base)
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ impl Renderer for HtmlHandlebars {
|
|||
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.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
|
||||
try!(self.process_items(&book.toc, &book, &handlebars));
|
||||
try!(self.process_items(&book.toc, &book, &book_project.livereload_script, &handlebars));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -226,6 +226,7 @@ impl HtmlHandlebars {
|
|||
fn process_items(&self,
|
||||
items: &Vec<TocItem>,
|
||||
book: &Book,
|
||||
livereload_script: &Option<String>,
|
||||
handlebars: &Handlebars)
|
||||
-> Result<(), Box<Error>> {
|
||||
|
||||
|
@ -241,11 +242,11 @@ impl HtmlHandlebars {
|
|||
// Option but currently only used for rendering a chapter as
|
||||
// index.html.
|
||||
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 {
|
||||
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,
|
||||
chapter: &Chapter,
|
||||
book: &Book,
|
||||
livereload_script: &Option<String>,
|
||||
handlebars: &Handlebars)
|
||||
-> Result<(), Box<Error>> {
|
||||
|
||||
|
@ -269,7 +271,7 @@ impl HtmlHandlebars {
|
|||
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
|
||||
debug!("[*]: Render template");
|
||||
|
@ -296,7 +298,8 @@ impl HtmlHandlebars {
|
|||
|
||||
fn make_data(book: &Book,
|
||||
chapter: &Chapter,
|
||||
content: &str)
|
||||
content: &str,
|
||||
livereload_script: &Option<String>)
|
||||
-> Result<serde_json::Map<String, serde_json::Value>, Box<Error>> {
|
||||
|
||||
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("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
|
||||
|
||||
let mut path = if let Some(ref dest_path) = chapter.dest_path {
|
||||
|
|
Loading…
Reference in New Issue