2015-07-07 03:12:24 +08:00
extern crate mdbook ;
2015-08-01 12:59:05 +08:00
#[ macro_use ]
extern crate clap ;
2016-08-14 21:55:10 +08:00
extern crate log ;
extern crate env_logger ;
2017-01-02 01:42:47 +08:00
extern crate open ;
2015-11-09 21:31:00 +08:00
// Dependencies for the Watch feature
2015-09-27 20:38:37 +08:00
#[ cfg(feature = " watch " ) ]
extern crate notify ;
2015-11-09 21:31:00 +08:00
#[ cfg(feature = " watch " ) ]
extern crate time ;
2016-05-09 03:51:34 +08:00
#[ cfg(feature = " watch " ) ]
extern crate crossbeam ;
2015-11-09 21:31:00 +08:00
2016-04-02 10:46:05 +08:00
// Dependencies for the Serve feature
#[ cfg(feature = " serve " ) ]
extern crate iron ;
#[ cfg(feature = " serve " ) ]
extern crate staticfile ;
#[ cfg(feature = " serve " ) ]
2016-04-03 02:04:51 +08:00
extern crate ws ;
2015-08-01 12:59:05 +08:00
2015-07-07 03:12:24 +08:00
use std ::env ;
2015-08-01 12:59:05 +08:00
use std ::error ::Error ;
2017-01-02 01:42:47 +08:00
use std ::ffi ::OsStr ;
2015-08-01 12:59:05 +08:00
use std ::io ::{ self , Write } ;
use std ::path ::{ Path , PathBuf } ;
2016-03-20 00:45:58 +08:00
use clap ::{ App , ArgMatches , SubCommand , AppSettings } ;
2015-07-07 03:12:24 +08:00
2015-11-09 21:31:00 +08:00
// Uses for the Watch feature
2015-09-27 20:38:37 +08:00
#[ cfg(feature = " watch " ) ]
use notify ::Watcher ;
#[ cfg(feature = " watch " ) ]
2017-01-02 07:59:22 +08:00
use std ::time ::Duration ;
#[ cfg(feature = " watch " ) ]
2015-09-27 20:38:37 +08:00
use std ::sync ::mpsc ::channel ;
2015-11-09 21:31:00 +08:00
2015-07-07 08:56:19 +08:00
use mdbook ::MDBook ;
2015-07-07 03:12:24 +08:00
const NAME : & 'static str = " mdbook " ;
fn main ( ) {
2016-08-14 21:55:10 +08:00
env_logger ::init ( ) . unwrap ( ) ;
2015-08-01 12:59:05 +08:00
// Create a list of valid arguments and sub-commands
let matches = App ::new ( NAME )
. about ( " Create a book in form of a static website from markdown files " )
. author ( " Mathieu David <mathieudavid@mathieudavid.org> " )
// Get the version from our Cargo.toml using clap's crate_version!() macro
. version ( & * format! ( " v {} " , crate_version! ( ) ) )
2016-03-20 00:45:58 +08:00
. setting ( AppSettings ::SubcommandRequired )
2016-06-12 07:08:48 +08:00
. after_help ( " For more information about a specific command, try `mdbook <command> --help` \n Source code for mdbook available at: https://github.com/azerupi/mdBook " )
2015-08-01 12:59:05 +08:00
. subcommand ( SubCommand ::with_name ( " init " )
. about ( " Create boilerplate structure and files in the directory " )
2015-08-11 22:13:41 +08:00
// the {n} denotes a newline which will properly aligned in all help messages
2017-01-12 20:23:39 +08:00
. arg_from_usage ( " [dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)' " )
2015-08-11 22:42:19 +08:00
. arg_from_usage ( " --theme 'Copies the default theme into your source folder' " )
. arg_from_usage ( " --force 'skip confirmation prompts' " ) )
2015-08-01 12:59:05 +08:00
. subcommand ( SubCommand ::with_name ( " build " )
. about ( " Build the book from the markdown files " )
2017-01-02 01:42:47 +08:00
. arg_from_usage ( " -o, --open 'Open the compiled book in a web browser' " )
2017-01-12 20:26:22 +08:00
. arg_from_usage ( " -d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)' " )
2017-04-18 09:53:27 +08:00
. arg_from_usage ( " --no-create 'Will not create non-existent files linked from SUMMARY.md' " )
2017-01-12 20:23:39 +08:00
. arg_from_usage ( " [dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)' " ) )
2015-08-01 12:59:05 +08:00
. subcommand ( SubCommand ::with_name ( " watch " )
2015-09-27 20:38:37 +08:00
. about ( " Watch the files for changes " )
2017-01-02 01:42:47 +08:00
. arg_from_usage ( " -o, --open 'Open the compiled book in a web browser' " )
2017-01-12 20:26:22 +08:00
. arg_from_usage ( " -d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)' " )
2017-01-12 20:23:39 +08:00
. arg_from_usage ( " [dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)' " ) )
2016-04-02 10:46:05 +08:00
. subcommand ( SubCommand ::with_name ( " serve " )
. about ( " Serve the book at http://localhost:3000. Rebuild and reload on change. " )
2017-01-12 20:23:39 +08:00
. arg_from_usage ( " [dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)' " )
2017-01-12 20:26:22 +08:00
. arg_from_usage ( " -d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)' " )
2016-04-02 10:46:05 +08:00
. arg_from_usage ( " -p, --port=[port] 'Use another port{n}(Defaults to 3000)' " )
2017-05-26 19:18:32 +08:00
. arg_from_usage ( " -w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)' " )
2016-08-06 05:40:00 +08:00
. arg_from_usage ( " -i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)' " )
2017-01-02 01:42:47 +08:00
. arg_from_usage ( " -a, --address=[address] 'Address that the browser can reach the websocket server from{n}(Defaults to the interface address)' " )
. arg_from_usage ( " -o, --open 'Open the book server in a web browser' " ) )
2015-12-16 02:55:23 +08:00
. subcommand ( SubCommand ::with_name ( " test " )
. about ( " Test that code samples compile " ) )
2015-08-01 12:59:05 +08:00
. get_matches ( ) ;
// Check which subcomamnd the user ran...
let res = match matches . subcommand ( ) {
2016-03-18 05:31:28 +08:00
( " init " , Some ( sub_matches ) ) = > init ( sub_matches ) ,
2015-08-01 12:59:05 +08:00
( " build " , Some ( sub_matches ) ) = > build ( sub_matches ) ,
2015-09-27 20:38:37 +08:00
#[ cfg(feature = " watch " ) ]
( " watch " , Some ( sub_matches ) ) = > watch ( sub_matches ) ,
2016-04-02 10:46:05 +08:00
#[ cfg(feature = " serve " ) ]
( " serve " , Some ( sub_matches ) ) = > serve ( sub_matches ) ,
2015-12-16 02:55:23 +08:00
( " test " , Some ( sub_matches ) ) = > test ( sub_matches ) ,
2016-03-18 05:31:28 +08:00
( _ , _ ) = > unreachable! ( ) ,
2015-07-07 03:12:24 +08:00
} ;
2015-08-01 12:59:05 +08:00
if let Err ( e ) = res {
2015-08-11 22:13:41 +08:00
writeln! ( & mut io ::stderr ( ) , " An error occured: \n {} " , e ) . ok ( ) ;
2016-08-07 02:54:07 +08:00
::std ::process ::exit ( 101 ) ;
2015-07-07 03:12:24 +08:00
}
}
2015-11-09 21:31:00 +08:00
// Simple function that user comfirmation
2015-08-11 22:42:19 +08:00
fn confirm ( ) -> bool {
io ::stdout ( ) . flush ( ) . unwrap ( ) ;
let mut s = String ::new ( ) ;
io ::stdin ( ) . read_line ( & mut s ) . ok ( ) ;
match & * s . trim ( ) {
" Y " | " y " | " yes " | " Yes " = > true ,
2016-03-18 05:31:28 +08:00
_ = > false ,
2015-08-11 22:42:19 +08:00
}
}
2015-11-09 21:31:00 +08:00
// Init command implementation
2015-08-01 12:59:05 +08:00
fn init ( args : & ArgMatches ) -> Result < ( ) , Box < Error > > {
2015-08-11 22:13:41 +08:00
2015-08-01 12:59:05 +08:00
let book_dir = get_book_dir ( args ) ;
2015-08-30 00:51:23 +08:00
let mut book = MDBook ::new ( & book_dir ) ;
2015-07-07 03:12:24 +08:00
2015-08-11 22:13:41 +08:00
// Call the function that does the initialization
2017-05-19 19:04:37 +08:00
book . init ( ) ? ;
2015-08-11 22:13:41 +08:00
// If flag `--theme` is present, copy theme to src
if args . is_present ( " theme " ) {
2016-02-28 23:28:11 +08:00
// Skip this if `--force` is present
2015-08-11 22:42:19 +08:00
if ! args . is_present ( " force " ) {
// Print warning
2017-05-19 05:52:38 +08:00
print! ( " \n Copying the default theme to {:?} " , book . get_source ( ) ) ;
2015-08-11 22:42:19 +08:00
println! ( " could potentially overwrite files already present in that directory. " ) ;
print! ( " \n Are you sure you want to continue? (y/n) " ) ;
// Read answer from user and exit if it's not 'yes'
if ! confirm ( ) {
2015-08-13 16:46:56 +08:00
println! ( " \n Skipping... \n " ) ;
println! ( " All done, no errors... " ) ;
2015-08-11 22:42:19 +08:00
::std ::process ::exit ( 0 ) ;
}
}
2015-08-11 22:13:41 +08:00
// Call the function that copies the theme
2017-05-19 19:04:37 +08:00
book . copy_theme ( ) ? ;
2015-08-11 22:42:19 +08:00
println! ( " \n Theme copied. " ) ;
2015-08-11 22:13:41 +08:00
}
2016-03-07 16:52:19 +08:00
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
2017-05-19 05:52:38 +08:00
let is_dest_inside_root = book . get_destination ( )
. map ( | p | p . starts_with ( book . get_root ( ) ) )
. unwrap_or ( false ) ;
2016-02-28 23:28:11 +08:00
if ! args . is_present ( " force " ) & & is_dest_inside_root {
2016-03-03 02:38:39 +08:00
println! ( " \n Do you want a .gitignore to be created? (y/n) " ) ;
2016-02-28 23:28:11 +08:00
if confirm ( ) {
book . create_gitignore ( ) ;
println! ( " \n .gitignore created. " ) ;
2016-03-18 05:31:28 +08:00
}
2016-02-28 23:28:11 +08:00
}
2015-08-13 16:46:56 +08:00
println! ( " \n All done, no errors... " ) ;
2015-08-11 22:13:41 +08:00
Ok ( ( ) )
2015-07-07 03:12:24 +08:00
}
2015-11-09 21:31:00 +08:00
// Build command implementation
2015-08-01 12:59:05 +08:00
fn build ( args : & ArgMatches ) -> Result < ( ) , Box < Error > > {
let book_dir = get_book_dir ( args ) ;
2017-05-19 05:52:38 +08:00
let book = MDBook ::new ( & book_dir ) . read_config ( ) ? ;
2017-01-12 20:26:22 +08:00
let mut book = match args . value_of ( " dest-dir " ) {
2017-05-19 05:52:38 +08:00
Some ( dest_dir ) = > book . with_destination ( Path ::new ( dest_dir ) ) ,
2017-05-19 19:04:37 +08:00
None = > book ,
2017-01-12 20:26:22 +08:00
} ;
2015-07-07 08:56:19 +08:00
2017-04-18 09:53:27 +08:00
if args . is_present ( " no-create " ) {
book . create_missing = false ;
}
2017-05-19 19:04:37 +08:00
book . build ( ) ? ;
2015-08-11 22:13:41 +08:00
2017-05-19 05:52:38 +08:00
if let Some ( d ) = book . get_destination ( ) {
if args . is_present ( " open " ) {
open ( d . join ( " index.html " ) ) ;
}
2017-01-02 01:42:47 +08:00
}
2015-08-11 22:13:41 +08:00
Ok ( ( ) )
2015-07-07 03:12:24 +08:00
}
2015-11-09 21:31:00 +08:00
// Watch command implementation
2015-09-27 20:38:37 +08:00
#[ cfg(feature = " watch " ) ]
fn watch ( args : & ArgMatches ) -> Result < ( ) , Box < Error > > {
let book_dir = get_book_dir ( args ) ;
2017-05-19 05:52:38 +08:00
let book = MDBook ::new ( & book_dir ) . read_config ( ) ? ;
2017-01-12 20:26:22 +08:00
let mut book = match args . value_of ( " dest-dir " ) {
2017-05-19 05:52:38 +08:00
Some ( dest_dir ) = > book . with_destination ( Path ::new ( dest_dir ) ) ,
2017-05-19 19:04:37 +08:00
None = > book ,
2017-01-12 20:26:22 +08:00
} ;
2015-09-27 20:38:37 +08:00
2017-01-02 01:42:47 +08:00
if args . is_present ( " open " ) {
2017-05-19 19:04:37 +08:00
book . build ( ) ? ;
2017-05-19 05:52:38 +08:00
if let Some ( d ) = book . get_destination ( ) {
open ( d . join ( " index.html " ) ) ;
}
2017-01-02 01:42:47 +08:00
}
2017-01-02 07:59:22 +08:00
trigger_on_change ( & mut book , | path , book | {
println! ( " File changed: {:?} \n Building book... \n " , path ) ;
2017-02-16 11:01:26 +08:00
if let Err ( e ) = book . build ( ) {
println! ( " Error while building: {:?} " , e ) ;
2016-04-02 10:46:05 +08:00
}
2017-01-02 07:59:22 +08:00
println! ( " " ) ;
2016-04-02 10:46:05 +08:00
} ) ;
2016-03-18 05:31:28 +08:00
2016-04-02 10:46:05 +08:00
Ok ( ( ) )
}
2016-03-18 05:31:28 +08:00
2016-04-02 10:46:05 +08:00
// Watch command implementation
#[ cfg(feature = " serve " ) ]
fn serve ( args : & ArgMatches ) -> Result < ( ) , Box < Error > > {
2016-04-03 02:04:51 +08:00
const RELOAD_COMMAND : & 'static str = " reload " ;
2016-04-02 10:46:05 +08:00
let book_dir = get_book_dir ( args ) ;
2017-05-19 05:52:38 +08:00
let book = MDBook ::new ( & book_dir ) . read_config ( ) ? ;
2017-01-12 20:26:22 +08:00
let mut book = match args . value_of ( " dest-dir " ) {
2017-05-19 05:52:38 +08:00
Some ( dest_dir ) = > book . with_destination ( Path ::new ( dest_dir ) ) ,
2017-05-19 19:04:37 +08:00
None = > book ,
2017-01-12 20:26:22 +08:00
} ;
2017-05-19 05:52:38 +08:00
if let None = book . get_destination ( ) {
println! ( " The HTML renderer is not set up, impossible to serve the files. " ) ;
std ::process ::exit ( 2 ) ;
}
2016-04-02 10:46:05 +08:00
let port = args . value_of ( " port " ) . unwrap_or ( " 3000 " ) ;
2017-05-26 19:18:32 +08:00
let ws_port = args . value_of ( " websocket-port " ) . unwrap_or ( " 3001 " ) ;
2016-08-06 05:40:00 +08:00
let interface = args . value_of ( " interface " ) . unwrap_or ( " localhost " ) ;
let public_address = args . value_of ( " address " ) . unwrap_or ( interface ) ;
2017-01-02 01:42:47 +08:00
let open_browser = args . is_present ( " open " ) ;
2016-04-02 10:46:05 +08:00
2016-08-06 05:40:00 +08:00
let address = format! ( " {} : {} " , interface , port ) ;
let ws_address = format! ( " {} : {} " , interface , ws_port ) ;
2016-04-03 02:04:51 +08:00
2016-04-02 10:46:05 +08:00
book . set_livereload ( format! ( r #"
< script type = " text/javascript " >
2016-08-06 05:40:00 +08:00
var socket = new WebSocket ( " ws://{}:{} " ) ;
2016-04-02 10:46:05 +08:00
socket . onmessage = function ( event ) { {
2016-04-03 02:04:51 +08:00
if ( event . data = = = " {} " ) { {
2016-04-02 10:46:05 +08:00
socket . close ( ) ;
location . reload ( true ) ; // force reload from server (not from cache)
} }
} } ;
window . onbeforeunload = function ( ) { {
socket . close ( ) ;
} }
< / script >
2017-05-19 19:04:37 +08:00
" #,
public_address ,
ws_port ,
RELOAD_COMMAND ) ) ;
2016-03-18 05:31:28 +08:00
2017-05-19 19:04:37 +08:00
book . build ( ) ? ;
2016-03-18 05:31:28 +08:00
2017-05-19 05:52:38 +08:00
let staticfile = staticfile ::Static ::new ( book . get_destination ( ) . expect ( " destination is present, checked before " ) ) ;
2016-04-02 10:46:05 +08:00
let iron = iron ::Iron ::new ( staticfile ) ;
2016-04-03 02:04:51 +08:00
let _iron = iron . http ( & * address ) . unwrap ( ) ;
2016-03-18 05:31:28 +08:00
2017-05-19 19:04:37 +08:00
let ws_server = ws ::WebSocket ::new ( | _ | | _ | Ok ( ( ) ) ) . unwrap ( ) ;
2016-04-03 02:04:51 +08:00
let broadcaster = ws_server . broadcaster ( ) ;
2017-05-19 19:04:37 +08:00
std ::thread ::spawn ( move | | { ws_server . listen ( & * ws_address ) . unwrap ( ) ; } ) ;
2016-03-18 05:31:28 +08:00
2017-05-27 17:34:46 +08:00
let serving_url = format! ( " http:// {} " , address ) ;
println! ( " \n Serving on: {} " , serving_url ) ;
2016-04-28 04:29:48 +08:00
2017-01-02 01:42:47 +08:00
if open_browser {
2017-05-27 17:34:46 +08:00
open ( serving_url ) ;
2017-01-02 01:42:47 +08:00
}
2017-01-02 07:59:22 +08:00
trigger_on_change ( & mut book , move | path , book | {
println! ( " File changed: {:?} \n Building book... \n " , path ) ;
match book . build ( ) {
Err ( e ) = > println! ( " Error while building: {:?} " , e ) ,
_ = > broadcaster . send ( RELOAD_COMMAND ) . unwrap ( ) ,
2016-04-02 10:46:05 +08:00
}
2017-01-02 07:59:22 +08:00
println! ( " " ) ;
2016-04-02 10:46:05 +08:00
} ) ;
2016-03-18 05:31:28 +08:00
Ok ( ( ) )
}
2015-12-29 20:40:13 +08:00
2015-12-16 02:55:23 +08:00
fn test ( args : & ArgMatches ) -> Result < ( ) , Box < Error > > {
let book_dir = get_book_dir ( args ) ;
2017-05-19 05:52:38 +08:00
let mut book = MDBook ::new ( & book_dir ) . read_config ( ) ? ;
2015-12-16 02:55:23 +08:00
2017-05-19 19:04:37 +08:00
book . test ( ) ? ;
2015-12-16 02:55:23 +08:00
2015-09-27 20:38:37 +08:00
Ok ( ( ) )
}
2015-08-01 12:59:05 +08:00
fn get_book_dir ( args : & ArgMatches ) -> PathBuf {
if let Some ( dir ) = args . value_of ( " dir " ) {
// Check if path is relative from current dir, or absolute...
let p = Path ::new ( dir ) ;
if p . is_relative ( ) {
2016-03-18 05:31:28 +08:00
env ::current_dir ( ) . unwrap ( ) . join ( dir )
2015-08-01 12:59:05 +08:00
} else {
2016-03-18 05:31:28 +08:00
p . to_path_buf ( )
2015-08-01 12:59:05 +08:00
}
2015-07-18 06:04:20 +08:00
} else {
2015-08-01 12:59:05 +08:00
env ::current_dir ( ) . unwrap ( )
2015-07-07 03:12:24 +08:00
}
}
2016-04-02 10:46:05 +08:00
2017-01-02 01:42:47 +08:00
fn open < P : AsRef < OsStr > > ( path : P ) {
if let Err ( e ) = open ::that ( path ) {
println! ( " Error opening web browser: {} " , e ) ;
}
}
2016-04-02 10:46:05 +08:00
// Calls the closure when a book source file is changed. This is blocking!
2016-05-09 03:51:34 +08:00
#[ cfg(feature = " watch " ) ]
2016-04-02 10:46:05 +08:00
fn trigger_on_change < F > ( book : & mut MDBook , closure : F ) -> ( )
2017-01-02 07:59:22 +08:00
where F : Fn ( & Path , & mut MDBook ) -> ( )
2016-04-02 10:46:05 +08:00
{
2017-01-02 07:59:22 +08:00
use notify ::RecursiveMode ::* ;
use notify ::DebouncedEvent ::* ;
2016-04-02 10:46:05 +08:00
// Create a channel to receive the events.
let ( tx , rx ) = channel ( ) ;
2017-01-02 07:59:22 +08:00
let mut watcher = match notify ::watcher ( tx , Duration ::from_secs ( 1 ) ) {
Ok ( w ) = > w ,
2016-04-02 10:46:05 +08:00
Err ( e ) = > {
println! ( " Error while trying to watch the files: \n \n \t {:?} " , e ) ;
::std ::process ::exit ( 0 ) ;
2017-05-19 19:04:37 +08:00
} ,
2017-01-02 07:59:22 +08:00
} ;
// Add the source directory to the watcher
2017-05-19 05:52:38 +08:00
if let Err ( e ) = watcher . watch ( book . get_source ( ) , Recursive ) {
println! ( " Error while watching {:?} : \n {:?} " , book . get_source ( ) , e ) ;
2017-01-02 07:59:22 +08:00
::std ::process ::exit ( 0 ) ;
} ;
2017-06-01 19:11:39 +08:00
// Add the theme directory to the watcher
watcher . watch ( book . get_theme_path ( ) , Recursive ) . unwrap_or_default ( ) ;
2017-01-02 07:59:22 +08:00
// Add the book.{json,toml} file to the watcher if it exists, because it's not
// located in the source directory
2017-05-19 19:04:37 +08:00
if watcher
. watch ( book . get_root ( ) . join ( " book.json " ) , NonRecursive )
. is_err ( ) {
2017-01-02 07:59:22 +08:00
// do nothing if book.json is not found
}
2017-05-19 19:04:37 +08:00
if watcher
. watch ( book . get_root ( ) . join ( " book.toml " ) , NonRecursive )
. is_err ( ) {
2017-01-02 08:00:21 +08:00
// do nothing if book.toml is not found
}
2017-01-02 07:59:22 +08:00
println! ( " \n Listening for changes... \n " ) ;
loop {
match rx . recv ( ) {
2017-05-19 19:04:37 +08:00
Ok ( event ) = > {
match event {
NoticeWrite ( path ) |
NoticeRemove ( path ) |
Create ( path ) |
Write ( path ) |
Remove ( path ) |
Rename ( _ , path ) = > {
closure ( & path , book ) ;
} ,
_ = > { } ,
2017-01-02 07:59:22 +08:00
}
} ,
Err ( e ) = > {
println! ( " An error occured: {:?} " , e ) ;
} ,
}
2016-04-02 10:46:05 +08:00
}
}