@@ -10,6 +10,13 @@ extern crate notify;
10
10
#[ cfg( feature = "watch" ) ]
11
11
extern crate time;
12
12
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;
13
20
14
21
use std:: env;
15
22
use std:: error:: Error ;
@@ -50,6 +57,11 @@ fn main() {
50
57
. subcommand ( SubCommand :: with_name ( "watch" )
51
58
. about ( "Watch the files for changes" )
52
59
. 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)'" ) )
53
65
. subcommand ( SubCommand :: with_name ( "test" )
54
66
. about ( "Test that code samples compile" ) )
55
67
. get_matches ( ) ;
@@ -60,6 +72,8 @@ fn main() {
60
72
( "build" , Some ( sub_matches) ) => build ( sub_matches) ,
61
73
#[ cfg( feature = "watch" ) ]
62
74
( "watch" , Some ( sub_matches) ) => watch ( sub_matches) ,
75
+ #[ cfg( feature = "serve" ) ]
76
+ ( "serve" , Some ( sub_matches) ) => serve ( sub_matches) ,
63
77
( "test" , Some ( sub_matches) ) => test ( sub_matches) ,
64
78
( _, _) => unreachable ! ( ) ,
65
79
} ;
@@ -148,77 +162,85 @@ fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
148
162
#[ cfg( feature = "watch" ) ]
149
163
fn watch ( args : & ArgMatches ) -> Result < ( ) , Box < Error > > {
150
164
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 ( ) ;
152
166
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: {:?}\n Building book...\n " , path) ;
170
+ match book. build ( ) {
171
+ Err ( e) => println ! ( "Error while building: {:?}" , e) ,
172
+ _ => { } ,
173
+ }
174
+ println ! ( "" ) ;
175
+ }
176
+ } ) ;
155
177
156
- let w: Result < notify:: RecommendedWatcher , notify:: Error > = notify:: Watcher :: new ( tx) ;
178
+ Ok ( ( ) )
179
+ }
157
180
158
- match w {
159
- Ok ( mut watcher) => {
160
181
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" ;
166
186
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 ( ) ) ;
172
210
173
- let mut previous_time = time :: get_time ( ) ;
211
+ try! ( book . build ( ) ) ;
174
212
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: {:?}\n Building 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 ( ) ;
209
216
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: {:?}\n Building 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
+ } ) ;
216
239
217
240
Ok ( ( ) )
218
241
}
219
242
220
243
221
-
222
244
fn test ( args : & ArgMatches ) -> Result < ( ) , Box < Error > > {
223
245
let book_dir = get_book_dir ( args) ;
224
246
let mut book = MDBook :: new ( & book_dir) . read_config ( ) ;
@@ -229,7 +251,6 @@ fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
229
251
}
230
252
231
253
232
-
233
254
fn get_book_dir ( args : & ArgMatches ) -> PathBuf {
234
255
if let Some ( dir) = args. value_of ( "dir" ) {
235
256
// Check if path is relative from current dir, or absolute...
@@ -243,3 +264,57 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
243
264
env:: current_dir ( ) . unwrap ( )
244
265
}
245
266
}
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 ! ( "\n Listening 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
+ }
0 commit comments