Skip to content

Commit 6aa6546

Browse files
committed
Merge pull request #128 from Bobo1239/serve-squashed
Implement Serve feature
2 parents c071406 + c805129 commit 6aa6546

File tree

7 files changed

+173
-62
lines changed

7 files changed

+173
-62
lines changed

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,24 @@ notify = { version = "2.5.5", optional = true }
2525
time = { version = "0.1.34", optional = true }
2626
crossbeam = { version = "0.2.8", optional = true }
2727

28+
# Serve feature
29+
iron = { version = "0.3", optional = true }
30+
staticfile = { version = "0.2", optional = true }
31+
ws = { version = "0.4.6", optional = true}
32+
2833

2934
# Tests
3035
[dev-dependencies]
3136
tempdir = "0.3.4"
3237

3338

3439
[features]
35-
default = ["output", "watch"]
40+
default = ["output", "watch", "serve"]
3641
debug = []
3742
output = []
3843
regenerate-css = []
3944
watch = ["notify", "time", "crossbeam"]
45+
serve = ["iron", "staticfile", "ws"]
4046

4147
[[bin]]
4248
doc = false

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# mdBook
1+
# mdBook
22

33
<table>
44
<tr>
@@ -100,6 +100,10 @@ Here are the main commands you will want to run, for a more exhaustive explanati
100100
101101
When you run this command, mdbook will watch your markdown files to rebuild the book on every change. This avoids having to come back to the terminal to type `mdbook build` over and over again.
102102
103+
- `mdbook serve`
104+
105+
Does the same thing as `mdbook watch` but additionally serves the book at `http://localhost:3000` (port is changeable) and reloads the browser when a change occures.
106+
103107
### As a library
104108
105109
Aside from the command line interface, this crate can also be used as a library. This means that you could integrate it in an existing project, like a web-app for example. Since the command line interface is just a wrapper around the library functionality, when you use this crate as a library you have full access to all the functionality of the command line interface with and easy to use API and more!

src/bin/mdbook.rs

Lines changed: 134 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ extern crate notify;
1010
#[cfg(feature = "watch")]
1111
extern crate time;
1212

13+
// Dependencies for the Serve feature
14+
#[cfg(feature = "serve")]
15+
extern crate iron;
16+
#[cfg(feature = "serve")]
17+
extern crate staticfile;
18+
#[cfg(feature = "serve")]
19+
extern crate ws;
1320

1421
use std::env;
1522
use std::error::Error;
@@ -50,6 +57,11 @@ fn main() {
5057
.subcommand(SubCommand::with_name("watch")
5158
.about("Watch the files for changes")
5259
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
60+
.subcommand(SubCommand::with_name("serve")
61+
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.")
62+
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'")
63+
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
64+
.arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'"))
5365
.subcommand(SubCommand::with_name("test")
5466
.about("Test that code samples compile"))
5567
.get_matches();
@@ -60,6 +72,8 @@ fn main() {
6072
("build", Some(sub_matches)) => build(sub_matches),
6173
#[cfg(feature = "watch")]
6274
("watch", Some(sub_matches)) => watch(sub_matches),
75+
#[cfg(feature = "serve")]
76+
("serve", Some(sub_matches)) => serve(sub_matches),
6377
("test", Some(sub_matches)) => test(sub_matches),
6478
(_, _) => unreachable!(),
6579
};
@@ -148,77 +162,85 @@ fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
148162
#[cfg(feature = "watch")]
149163
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
150164
let book_dir = get_book_dir(args);
151-
let book = MDBook::new(&book_dir).read_config();
165+
let mut book = MDBook::new(&book_dir).read_config();
152166

153-
// Create a channel to receive the events.
154-
let (tx, rx) = channel();
167+
trigger_on_change(&mut book, |event, book| {
168+
if let Some(path) = event.path {
169+
println!("File changed: {:?}\nBuilding book...\n", path);
170+
match book.build() {
171+
Err(e) => println!("Error while building: {:?}", e),
172+
_ => {},
173+
}
174+
println!("");
175+
}
176+
});
155177

156-
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
178+
Ok(())
179+
}
157180

158-
match w {
159-
Ok(mut watcher) => {
160181

161-
// Add the source directory to the watcher
162-
if let Err(e) = watcher.watch(book.get_src()) {
163-
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
164-
::std::process::exit(0);
165-
};
182+
// Watch command implementation
183+
#[cfg(feature = "serve")]
184+
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
185+
const RELOAD_COMMAND: &'static str = "reload";
166186

167-
// Add the book.json file to the watcher if it exists, because it's not
168-
// located in the source directory
169-
if let Err(_) = watcher.watch(book_dir.join("book.json")) {
170-
// do nothing if book.json is not found
171-
}
187+
let book_dir = get_book_dir(args);
188+
let mut book = MDBook::new(&book_dir).read_config();
189+
let port = args.value_of("port").unwrap_or("3000");
190+
let ws_port = args.value_of("ws-port").unwrap_or("3001");
191+
192+
let address = format!("localhost:{}", port);
193+
let ws_address = format!("localhost:{}", ws_port);
194+
195+
book.set_livereload(format!(r#"
196+
<script type="text/javascript">
197+
var socket = new WebSocket("ws://localhost:{}");
198+
socket.onmessage = function (event) {{
199+
if (event.data === "{}") {{
200+
socket.close();
201+
location.reload(true); // force reload from server (not from cache)
202+
}}
203+
}};
204+
205+
window.onbeforeunload = function() {{
206+
socket.close();
207+
}}
208+
</script>
209+
"#, ws_port, RELOAD_COMMAND).to_owned());
172210

173-
let mut previous_time = time::get_time();
211+
try!(book.build());
174212

175-
crossbeam::scope(|scope| {
176-
loop {
177-
match rx.recv() {
178-
Ok(event) => {
179-
180-
// Skip the event if an event has already been issued in the last second
181-
let time = time::get_time();
182-
if time - previous_time < time::Duration::seconds(1) {
183-
continue;
184-
} else {
185-
previous_time = time;
186-
}
187-
188-
if let Some(path) = event.path {
189-
// Trigger the build process in a new thread (to keep receiving events)
190-
scope.spawn(move || {
191-
println!("File changed: {:?}\nBuilding book...\n", path);
192-
match build(args) {
193-
Err(e) => println!("Error while building: {:?}", e),
194-
_ => {},
195-
}
196-
println!("");
197-
});
198-
199-
} else {
200-
continue;
201-
}
202-
},
203-
Err(e) => {
204-
println!("An error occured: {:?}", e);
205-
},
206-
}
207-
}
208-
});
213+
let staticfile = staticfile::Static::new(book.get_dest());
214+
let iron = iron::Iron::new(staticfile);
215+
let _iron = iron.http(&*address).unwrap();
209216

210-
},
211-
Err(e) => {
212-
println!("Error while trying to watch the files:\n\n\t{:?}", e);
213-
::std::process::exit(0);
214-
},
215-
}
217+
let ws_server = ws::WebSocket::new(|_| {
218+
|_| {
219+
Ok(())
220+
}
221+
}).unwrap();
222+
223+
let broadcaster = ws_server.broadcaster();
224+
225+
std::thread::spawn(move || {
226+
ws_server.listen(&*ws_address).unwrap();
227+
});
228+
229+
trigger_on_change(&mut book, move |event, book| {
230+
if let Some(path) = event.path {
231+
println!("File changed: {:?}\nBuilding book...\n", path);
232+
match book.build() {
233+
Err(e) => println!("Error while building: {:?}", e),
234+
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
235+
}
236+
println!("");
237+
}
238+
});
216239

217240
Ok(())
218241
}
219242

220243

221-
222244
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
223245
let book_dir = get_book_dir(args);
224246
let mut book = MDBook::new(&book_dir).read_config();
@@ -229,7 +251,6 @@ fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
229251
}
230252

231253

232-
233254
fn get_book_dir(args: &ArgMatches) -> PathBuf {
234255
if let Some(dir) = args.value_of("dir") {
235256
// Check if path is relative from current dir, or absolute...
@@ -243,3 +264,57 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
243264
env::current_dir().unwrap()
244265
}
245266
}
267+
268+
269+
// Calls the closure when a book source file is changed. This is blocking!
270+
fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
271+
where F: Fn(notify::Event, &mut MDBook) -> ()
272+
{
273+
// Create a channel to receive the events.
274+
let (tx, rx) = channel();
275+
276+
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
277+
278+
match w {
279+
Ok(mut watcher) => {
280+
// Add the source directory to the watcher
281+
if let Err(e) = watcher.watch(book.get_src()) {
282+
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
283+
::std::process::exit(0);
284+
};
285+
286+
// Add the book.json file to the watcher if it exists, because it's not
287+
// located in the source directory
288+
if let Err(_) = watcher.watch(book.get_root().join("book.json")) {
289+
// do nothing if book.json is not found
290+
}
291+
292+
let mut previous_time = time::get_time();
293+
294+
println!("\nListening for changes...\n");
295+
296+
loop {
297+
match rx.recv() {
298+
Ok(event) => {
299+
// Skip the event if an event has already been issued in the last second
300+
let time = time::get_time();
301+
if time - previous_time < time::Duration::seconds(1) {
302+
continue;
303+
} else {
304+
previous_time = time;
305+
}
306+
307+
closure(event, book);
308+
},
309+
Err(e) => {
310+
println!("An error occured: {:?}", e);
311+
},
312+
}
313+
}
314+
},
315+
Err(e) => {
316+
println!("Error while trying to watch the files:\n\n\t{:?}", e);
317+
::std::process::exit(0);
318+
},
319+
}
320+
}

src/book/mdbook.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pub struct MDBook {
1515
config: BookConfig,
1616
pub content: Vec<BookItem>,
1717
renderer: Box<Renderer>,
18+
#[cfg(feature = "serve")]
19+
livereload: Option<String>,
1820
}
1921

2022
impl MDBook {
@@ -38,6 +40,7 @@ impl MDBook {
3840
.set_dest(&root.join("book"))
3941
.to_owned(),
4042
renderer: Box::new(HtmlHandlebars::new()),
43+
livereload: None,
4144
}
4245
}
4346

@@ -398,6 +401,23 @@ impl MDBook {
398401
&self.config.description
399402
}
400403

404+
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
405+
self.livereload = Some(livereload);
406+
self
407+
}
408+
409+
pub fn unset_livereload(&mut self) -> &Self {
410+
self.livereload = None;
411+
self
412+
}
413+
414+
pub fn get_livereload(&self) -> Option<&String> {
415+
match self.livereload {
416+
Some(ref livereload) => Some(&livereload),
417+
None => None,
418+
}
419+
}
420+
401421
// Construct book
402422
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
403423
// When append becomes stable, use self.content.append() ...

src/renderer/html_handlebars/hbs_renderer.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String, Json>, Box<Error>> {
287287
data.insert("title".to_owned(), book.get_title().to_json());
288288
data.insert("description".to_owned(), book.get_description().to_json());
289289
data.insert("favicon".to_owned(), "favicon.png".to_json());
290+
if let Some(livereload) = book.get_livereload() {
291+
data.insert("livereload".to_owned(), livereload.to_json());
292+
}
290293

291294
let mut chapters = vec![];
292295

src/theme/index.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@
107107
}
108108
</script>
109109

110+
<!-- Livereload script (if served using the cli tool) -->
111+
{{{livereload}}}
112+
110113
<script src="highlight.js"></script>
111114
<script src="book.js"></script>
112115
</body>

src/utils/fs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
150150
debug!("[*] creating path for file: {:?}",
151151
&to.join(entry.path().file_name().expect("a file should have a file name...")));
152152

153-
output!("[*] copying file: {:?}\n to {:?}",
153+
output!("[*] Copying file: {:?}\n to {:?}",
154154
entry.path(),
155155
&to.join(entry.path().file_name().expect("a file should have a file name...")));
156156
try!(fs::copy(entry.path(),

0 commit comments

Comments
 (0)