watch and serve are back
This commit is contained in:
parent
c021940331
commit
3aa8f7d925
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue