diff --git a/Cargo.toml b/Cargo.toml index 098566da..ced82e51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ exclude = [ [dependencies] clap = "2.24" -handlebars = "0.26" +handlebars = "0.27" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md index 58beb2e7..39aabb67 100644 --- a/book-example/src/format/config.md +++ b/book-example/src/format/config.md @@ -66,6 +66,7 @@ The following configuration options are available: - **destination:** By default, the HTML book will be rendered in the `root/book` directory, but this option lets you specify another destination fodler. - **theme:** mdBook comes with a default theme and all the resource files needed for it. But if this option is set, mdBook will selectively overwrite the theme files with the ones found in the specified folder. +- **curly-quotes:** Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans. Defaults to `false`. - **google-analytics:** If you use Google Analytics, this option lets you enable it by simply specifying your ID in the configuration file. - **additional-css:** If you need to slightly change the appearance of your book without overwriting the whole style, you can specify a set of stylesheets that will be loaded after the default ones where you can surgically change the style. @@ -78,6 +79,7 @@ description = "The example book covers examples." [output.html] destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book` theme = "my-theme" +curly-quotes = true google-analytics = "123456" additional-css = ["custom.css", "custom2.css"] ``` diff --git a/book-example/src/lib/lib.md b/book-example/src/lib/lib.md index eaf7a888..269e8c31 100644 --- a/book-example/src/lib/lib.md +++ b/book-example/src/lib/lib.md @@ -9,13 +9,15 @@ extern crate mdbook; use mdbook::MDBook; use std::path::Path; +# #[allow(unused_variables)] fn main() { - let mut book = MDBook::new(Path::new("my-book")) // Path to root - .set_src(Path::new("src")) // Path from root to source directory - .set_dest(Path::new("book")) // Path from root to output directory - .read_config(); // Parse book.toml or book.json file for configuration + let mut book = MDBook::new("my-book") // Path to root + .with_source("src") // Path from root to source directory + .with_destination("book") // Path from root to output directory + .read_config() // Parse book.toml or book.json configuration file + .expect("I don't handle configuration file error, but you should!"); - book.build().unwrap(); // Render the book + book.build().unwrap(); // Render the book } ``` diff --git a/src/bin/mdbook.rs b/src/bin/mdbook.rs index 416aa081..71184e6f 100644 --- a/src/bin/mdbook.rs +++ b/src/bin/mdbook.rs @@ -13,14 +13,6 @@ extern crate time; #[cfg(feature = "watch")] extern crate crossbeam; -// Dependencies for the Serve feature -#[cfg(feature = "serve")] -extern crate iron; -#[cfg(feature = "serve")] -extern crate staticfile; -#[cfg(feature = "serve")] -extern crate ws; - use std::env; use std::error::Error; use std::ffi::OsStr; @@ -64,16 +56,19 @@ fn main() { .arg_from_usage("-o, --open 'Open the compiled book in a web browser'") .arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'") .arg_from_usage("--no-create 'Will not create non-existent files linked from SUMMARY.md'") + .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")) .subcommand(SubCommand::with_name("watch") .about("Watch the files for changes") .arg_from_usage("-o, --open 'Open the compiled book in a web browser'") .arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'") + .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")) .subcommand(SubCommand::with_name("serve") .about("Serve the book at http://localhost:3000. Rebuild and reload on change.") .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'") .arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'") + .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") .arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'") .arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'") .arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'") @@ -90,7 +85,7 @@ fn main() { #[cfg(feature = "watch")] ("watch", Some(sub_matches)) => watch(sub_matches), #[cfg(feature = "serve")] - ("serve", Some(sub_matches)) => serve(sub_matches), + ("serve", Some(sub_matches)) => serve::serve(sub_matches), ("test", Some(sub_matches)) => test(sub_matches), (_, _) => unreachable!(), }; @@ -173,7 +168,7 @@ fn build(args: &ArgMatches) -> Result<(), Box> { let book = MDBook::new(&book_dir).read_config()?; let mut book = match args.value_of("dest-dir") { - Some(dest_dir) => book.with_destination(Path::new(dest_dir)), + Some(dest_dir) => book.with_destination(dest_dir), None => book, }; @@ -181,6 +176,10 @@ fn build(args: &ArgMatches) -> Result<(), Box> { book.create_missing = false; } + if args.is_present("curly-quotes") { + book = book.with_curly_quotes(true); + } + book.build()?; if let Some(d) = book.get_destination() { @@ -200,10 +199,14 @@ fn watch(args: &ArgMatches) -> Result<(), Box> { let book = MDBook::new(&book_dir).read_config()?; let mut book = match args.value_of("dest-dir") { - Some(dest_dir) => book.with_destination(Path::new(dest_dir)), + Some(dest_dir) => book.with_destination(dest_dir), None => book, }; + if args.is_present("curly-quotes") { + book = book.with_curly_quotes(true); + } + if args.is_present("open") { book.build()?; if let Some(d) = book.get_destination() { @@ -222,35 +225,64 @@ fn watch(args: &ArgMatches) -> Result<(), Box> { Ok(()) } - -// Watch command implementation #[cfg(feature = "serve")] -fn serve(args: &ArgMatches) -> Result<(), Box> { - const RELOAD_COMMAND: &'static str = "reload"; +mod serve { + extern crate iron; + extern crate staticfile; + extern crate ws; - let book_dir = get_book_dir(args); - let book = MDBook::new(&book_dir).read_config()?; + use std; + use std::path::Path; + use std::error::Error; + use self::iron::{Iron, AfterMiddleware, IronResult, IronError, Request, Response, status, Set, Chain}; + use clap::ArgMatches; + use mdbook::MDBook; - let mut book = match args.value_of("dest-dir") { - Some(dest_dir) => book.with_destination(Path::new(dest_dir)), - None => book, - }; + use {get_book_dir, open, trigger_on_change}; - if let None = book.get_destination() { - println!("The HTML renderer is not set up, impossible to serve the files."); - std::process::exit(2); + struct ErrorRecover; + + impl AfterMiddleware for ErrorRecover { + fn catch(&self, _: &mut Request, err: IronError) -> IronResult { + match err.response.status { + // each error will result in 404 response + Some(_) => Ok(err.response.set(status::NotFound)), + _ => Err(err), + } + } } - let port = args.value_of("port").unwrap_or("3000"); - let ws_port = args.value_of("websocket-port").unwrap_or("3001"); - let interface = args.value_of("interface").unwrap_or("localhost"); - let public_address = args.value_of("address").unwrap_or(interface); - let open_browser = args.is_present("open"); + // Watch command implementation + pub fn serve(args: &ArgMatches) -> Result<(), Box> { + const RELOAD_COMMAND: &'static str = "reload"; - let address = format!("{}:{}", interface, port); - let ws_address = format!("{}:{}", interface, ws_port); + let book_dir = get_book_dir(args); + let book = MDBook::new(&book_dir).read_config()?; - book.set_livereload(format!(r#" + let mut book = match args.value_of("dest-dir") { + Some(dest_dir) => book.with_destination(Path::new(dest_dir)), + None => book, + }; + + if let None = book.get_destination() { + println!("The HTML renderer is not set up, impossible to serve the files."); + std::process::exit(2); + } + + if args.is_present("curly-quotes") { + book = book.with_curly_quotes(true); + } + + let port = args.value_of("port").unwrap_or("3000"); + let ws_port = args.value_of("websocket-port").unwrap_or("3001"); + let interface = args.value_of("interface").unwrap_or("localhost"); + let public_address = args.value_of("address").unwrap_or(interface); + let open_browser = args.is_present("open"); + + let address = format!("{}:{}", interface, port); + let ws_address = format!("{}:{}", interface, ws_port); + + book.set_livereload(format!(r#" "#, - public_address, - ws_port, - RELOAD_COMMAND)); + public_address, + ws_port, + RELOAD_COMMAND)); - book.build()?; + book.build()?; - let staticfile = staticfile::Static::new(book.get_destination().expect("destination is present, checked before")); - let iron = iron::Iron::new(staticfile); - let _iron = iron.http(&*address).unwrap(); + let mut chain = Chain::new(staticfile::Static::new(book.get_destination() + .expect("destination is present, checked before"))); + chain.link_after(ErrorRecover); + let _iron = Iron::new(chain).http(&*address).unwrap(); - let ws_server = ws::WebSocket::new(|_| |_| Ok(())).unwrap(); + let ws_server = ws::WebSocket::new(|_| |_| Ok(())).unwrap(); - let broadcaster = ws_server.broadcaster(); + let broadcaster = ws_server.broadcaster(); - std::thread::spawn(move || { ws_server.listen(&*ws_address).unwrap(); }); + std::thread::spawn(move || { ws_server.listen(&*ws_address).unwrap(); }); - let serving_url = format!("http://{}", address); - println!("\nServing on: {}", serving_url); + let serving_url = format!("http://{}", address); + println!("\nServing on: {}", serving_url); - if open_browser { - open(serving_url); - } - - trigger_on_change(&mut book, move |path, book| { - println!("File changed: {:?}\nBuilding book...\n", path); - match book.build() { - Err(e) => println!("Error while building: {:?}", e), - _ => broadcaster.send(RELOAD_COMMAND).unwrap(), + if open_browser { + open(serving_url); } - println!(""); - }); - Ok(()) + trigger_on_change(&mut book, move |path, book| { + println!("File changed: {:?}\nBuilding book...\n", path); + match book.build() { + Err(e) => println!("Error while building: {:?}", e), + _ => broadcaster.send(RELOAD_COMMAND).unwrap(), + } + println!(""); + }); + + Ok(()) + } } - fn test(args: &ArgMatches) -> Result<(), Box> { let book_dir = get_book_dir(args); let mut book = MDBook::new(&book_dir).read_config()?; diff --git a/src/book/mod.rs b/src/book/mod.rs index 88d0e804..efef23d1 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -39,9 +39,9 @@ impl MDBook { /// ```no_run /// # extern crate mdbook; /// # use mdbook::MDBook; - /// # use std::path::Path; + /// # #[allow(unused_variables)] /// # fn main() { - /// let book = MDBook::new(Path::new("root_dir")); + /// let book = MDBook::new("root_dir"); /// # } /// ``` /// @@ -59,8 +59,9 @@ impl MDBook { /// They can both be changed by using [`set_src()`](#method.set_src) and /// [`set_dest()`](#method.set_dest) - pub fn new(root: &Path) -> MDBook { + pub fn new>(root: P) -> MDBook { + let root = root.into(); if !root.exists() || !root.is_dir() { warn!("{:?} No directory with that name", root); } @@ -84,9 +85,9 @@ impl MDBook { /// # extern crate mdbook; /// # use mdbook::MDBook; /// # use mdbook::BookItem; - /// # use std::path::Path; + /// # #[allow(unused_variables)] /// # fn main() { - /// # let mut book = MDBook::new(Path::new("mybook")); + /// # let book = MDBook::new("mybook"); /// for item in book.iter() { /// match item { /// &BookItem::Chapter(ref section, ref chapter) => {}, @@ -347,10 +348,10 @@ impl MDBook { /// extern crate mdbook; /// use mdbook::MDBook; /// use mdbook::renderer::HtmlHandlebars; - /// # use std::path::Path; /// + /// # #[allow(unused_variables)] /// fn main() { - /// let mut book = MDBook::new(Path::new("mybook")) + /// let book = MDBook::new("mybook") /// .set_renderer(Box::new(HtmlHandlebars::new())); /// /// // In this example we replace the default renderer @@ -479,6 +480,23 @@ impl MDBook { None } + pub fn with_curly_quotes(mut self, curly_quotes: bool) -> Self { + if let Some(htmlconfig) = self.config.get_mut_html_config() { + htmlconfig.set_curly_quotes(curly_quotes); + } else { + error!("There is no HTML renderer set..."); + } + self + } + + pub fn get_curly_quotes(&self) -> bool { + if let Some(htmlconfig) = self.config.get_html_config() { + return htmlconfig.get_curly_quotes(); + } + + false + } + pub fn get_google_analytics_id(&self) -> Option { if let Some(htmlconfig) = self.config.get_html_config() { return htmlconfig.get_google_analytics_id(); diff --git a/src/config/bookconfig.rs b/src/config/bookconfig.rs index 94efe9c5..e78dcabc 100644 --- a/src/config/bookconfig.rs +++ b/src/config/bookconfig.rs @@ -113,7 +113,7 @@ impl BookConfig { htmlconfig.fill_from_tomlconfig(root, tomlhtmlconfig); } } - + self } @@ -159,7 +159,7 @@ impl BookConfig { htmlconfig.set_theme(&root, &d); } } - + self } @@ -171,16 +171,16 @@ impl BookConfig { pub fn get_root(&self) -> &Path { &self.root } - + pub fn set_source>(&mut self, source: T) -> &mut Self { let mut source = source.into(); - + // If the source path is relative, start with the root path if source.is_relative() { source = self.root.join(source); } - self.source = source; + self.source = source; self } diff --git a/src/config/htmlconfig.rs b/src/config/htmlconfig.rs index 8ea1f08f..1c42a34a 100644 --- a/src/config/htmlconfig.rs +++ b/src/config/htmlconfig.rs @@ -6,6 +6,7 @@ use super::tomlconfig::TomlHtmlConfig; pub struct HtmlConfig { destination: PathBuf, theme: Option, + curly_quotes: bool, google_analytics: Option, additional_css: Vec, additional_js: Vec, @@ -27,6 +28,7 @@ impl HtmlConfig { HtmlConfig { destination: root.into().join("book"), theme: None, + curly_quotes: false, google_analytics: None, additional_css: Vec::new(), additional_js: Vec::new(), @@ -52,6 +54,10 @@ impl HtmlConfig { } } + if let Some(curly_quotes) = tomlconfig.curly_quotes { + self.curly_quotes = curly_quotes; + } + if tomlconfig.google_analytics.is_some() { self.google_analytics = tomlconfig.google_analytics; } @@ -110,6 +116,14 @@ impl HtmlConfig { self } + pub fn get_curly_quotes(&self) -> bool { + self.curly_quotes + } + + pub fn set_curly_quotes(&mut self, curly_quotes: bool) { + self.curly_quotes = curly_quotes; + } + pub fn get_google_analytics_id(&self) -> Option { self.google_analytics.clone() } diff --git a/src/config/jsonconfig.rs b/src/config/jsonconfig.rs index 9d1e2f3f..f41f8763 100644 --- a/src/config/jsonconfig.rs +++ b/src/config/jsonconfig.rs @@ -24,9 +24,9 @@ pub struct JsonConfig { /// # use std::path::PathBuf; /// let json = r#"{ /// "title": "Some title", -/// "dest": "htmlbook" +/// "dest": "htmlbook" /// }"#; -/// +/// /// let config = JsonConfig::from_json(&json).expect("Should parse correctly"); /// assert_eq!(config.title, Some(String::from("Some title"))); /// assert_eq!(config.dest, Some(PathBuf::from("htmlbook"))); @@ -35,9 +35,7 @@ impl JsonConfig { pub fn from_json(input: &str) -> Result { let config: JsonConfig = serde_json::from_str(input) .map_err(|e| format!("Could not parse JSON: {}", e))?; - + return Ok(config); } } - - diff --git a/src/config/tomlconfig.rs b/src/config/tomlconfig.rs index ca7388bd..90cfa4c6 100644 --- a/src/config/tomlconfig.rs +++ b/src/config/tomlconfig.rs @@ -24,6 +24,7 @@ pub struct TomlHtmlConfig { pub destination: Option, pub theme: Option, pub google_analytics: Option, + pub curly_quotes: Option, pub additional_css: Option>, pub additional_js: Option>, } diff --git a/src/lib.rs b/src/lib.rs index 1b31b88d..e91f18a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,16 +21,16 @@ //! extern crate mdbook; //! //! use mdbook::MDBook; -//! use std::path::Path; //! +//! # #[allow(unused_variables)] //! fn main() { -//! let mut book = MDBook::new(Path::new("my-book")) // Path to root -//! .with_source(Path::new("src")) // Path from root to source directory -//! .with_destination(Path::new("book")) // Path from root to output directory -//! .read_config() // Parse book.json file for configuration -//! .expect("I don't handle the error for the configuration file, but you should!"); +//! let mut book = MDBook::new("my-book") // Path to root +//! .with_source("src") // Path from root to source directory +//! .with_destination("book") // Path from root to output directory +//! .read_config() // Parse book.toml configuration file +//! .expect("I don't handle configuration file errors, but you should!"); //! -//! book.build().unwrap(); // Render the book +//! book.build().unwrap(); // Render the book //! } //! ``` //! @@ -46,12 +46,12 @@ //! # //! # use mdbook::MDBook; //! # use mdbook::renderer::HtmlHandlebars; -//! # use std::path::Path; //! # +//! # #[allow(unused_variables)] //! # fn main() { //! # let your_renderer = HtmlHandlebars::new(); //! # -//! let book = MDBook::new(Path::new("my-book")).set_renderer(Box::new(your_renderer)); +//! let book = MDBook::new("my-book").set_renderer(Box::new(your_renderer)); //! # } //! ``` //! If you make a renderer, you get the book constructed in form of `Vec` and you get @@ -61,12 +61,11 @@ //! //! ## utils //! -//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the following function +//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the +//! following function [`utils::fs::create_file(path: +//! &Path)`](utils/fs/fn.create_file.html) //! -//! ```ignore -//! utils::fs::create_path(path: &Path) -//! ``` -//! This function creates all the directories in a given path if they do not exist +//! This function creates a file and returns it. But before creating the file it checks every directory in the path to see if it exists, and if it does not it will be created. //! //! Make sure to take a look at it. diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index d64a3fab..fd69ee58 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -49,7 +49,7 @@ impl HtmlHandlebars { content = helpers::playpen::render_playpen(&content, p); } - content = utils::render_markdown(&content); + content = utils::render_markdown(&content, ctx.book.get_curly_quotes()); print_content.push_str(&content); // Update the context with data for this file diff --git a/src/renderer/html_handlebars/helpers/navigation.rs b/src/renderer/html_handlebars/helpers/navigation.rs index ca37efdb..a49db8b5 100644 --- a/src/renderer/html_handlebars/helpers/navigation.rs +++ b/src/renderer/html_handlebars/helpers/navigation.rs @@ -1,8 +1,8 @@ use std::path::Path; -use std::collections::{VecDeque, BTreeMap}; +use std::collections::BTreeMap; use serde_json; -use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable}; +use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable, Context}; // Handlebars helper for navigation @@ -11,31 +11,22 @@ pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<( debug!("[fn]: previous (handlebars helper)"); debug!("[*]: Get data from context"); - // get value from context data - // rc.get_path() is current json parent path, you should always use it like this - // param is the key of value you want to display - let chapters = rc.context() - .navigate(rc.get_path(), &VecDeque::new(), "chapters") - .to_owned(); + let chapters = rc.evaluate_absolute("chapters") + .and_then(|c| { + serde_json::value::from_value::>>(c.clone()) + .map_err(|_| RenderError::new("Could not decode the JSON data")) + })?; - let current = rc.context() - .navigate(rc.get_path(), &VecDeque::new(), "path") - .to_string() + let current = rc.evaluate_absolute("path")? + .as_str() + .ok_or(RenderError::new("Type error for `path`, string expected"))? .replace("\"", ""); - - debug!("[*]: Decode chapters from JSON"); - // Decode json format - let decoded: Vec> = match serde_json::from_str(&chapters.to_string()) { - Ok(data) => data, - Err(_) => return Err(RenderError::new("Could not decode the JSON data")), - }; let mut previous: Option> = None; - debug!("[*]: Search for current Chapter"); // Search for current chapter and return previous entry - for item in decoded { + for item in chapters { match item.get("path") { Some(path) if !path.is_empty() => { @@ -49,51 +40,41 @@ pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<( let mut previous_chapter = BTreeMap::new(); // Chapter title - match previous.get("name") { - Some(n) => { - debug!("[*]: Inserting title: {}", n); - previous_chapter.insert("title".to_owned(), json!(n)) - }, - None => { - debug!("[*]: No title found for chapter"); - return Err(RenderError::new("No title found for chapter in JSON data")); - }, - }; + previous + .get("name") + .ok_or(RenderError::new("No title found for chapter in JSON data")) + .and_then(|n| { + previous_chapter.insert("title".to_owned(), json!(n)); + Ok(()) + })?; + // Chapter link + previous + .get("path") + .ok_or(RenderError::new("No path found for chapter in JSON data")) + .and_then(|p| { + Path::new(p) + .with_extension("html") + .to_str() + .ok_or(RenderError::new("Link could not be converted to str")) + .and_then(|p| { + previous_chapter + .insert("link".to_owned(), json!(p.replace("\\", "/"))); + Ok(()) + }) + })?; - match previous.get("path") { - Some(p) => { - // Hack for windows who tends to use `\` as separator instead of `/` - let path = Path::new(p).with_extension("html"); - debug!("[*]: Inserting link: {:?}", path); - - match path.to_str() { - Some(p) => { - previous_chapter.insert("link".to_owned(), json!(p.replace("\\", "/"))); - }, - None => return Err(RenderError::new("Link could not be converted to str")), - } - }, - None => return Err(RenderError::new("No path found for chapter in JSON data")), - } - - debug!("[*]: Inject in context"); - // Inject in current context - let updated_context = rc.context().extend(&previous_chapter); debug!("[*]: Render template"); // Render template - match _h.template() { - Some(t) => { - *rc.context_mut() = updated_context; - t.render(r, rc)?; - }, - None => return Err(RenderError::new("Error with the handlebars template")), - } - + _h.template() + .ok_or(RenderError::new("Error with the handlebars template")) + .and_then(|t| { + let mut local_rc = rc.with_context(Context::wraps(&previous_chapter)); + t.render(r, &mut local_rc) + })?; } - break; } else { previous = Some(item.clone()); @@ -102,7 +83,6 @@ pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<( _ => continue, } - } Ok(()) @@ -115,29 +95,21 @@ pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), R debug!("[fn]: next (handlebars helper)"); debug!("[*]: Get data from context"); - // get value from context data - // rc.get_path() is current json parent path, you should always use it like this - // param is the key of value you want to display - let chapters = rc.context() - .navigate(rc.get_path(), &VecDeque::new(), "chapters") - .to_owned(); - - let current = rc.context() - .navigate(rc.get_path(), &VecDeque::new(), "path") - .to_string() + let chapters = rc.evaluate_absolute("chapters") + .and_then(|c| { + serde_json::value::from_value::>>(c.clone()) + .map_err(|_| RenderError::new("Could not decode the JSON data")) + })?; + let current = rc.evaluate_absolute("path")? + .as_str() + .ok_or(RenderError::new("Type error for `path`, string expected"))? .replace("\"", ""); - debug!("[*]: Decode chapters from JSON"); - // Decode json format - let decoded: Vec> = match serde_json::from_str(&chapters.to_string()) { - Ok(data) => data, - Err(_) => return Err(RenderError::new("Could not decode the JSON data")), - }; let mut previous: Option> = None; debug!("[*]: Search for current Chapter"); // Search for current chapter and return previous entry - for item in decoded { + for item in chapters { match item.get("path") { @@ -145,10 +117,9 @@ pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), R if let Some(previous) = previous { - let previous_path = match previous.get("path") { - Some(p) => p, - None => return Err(RenderError::new("No path found for chapter in JSON data")), - }; + let previous_path = previous + .get("path") + .ok_or(RenderError::new("No path found for chapter in JSON data"))?; if previous_path == ¤t { @@ -157,41 +128,33 @@ pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), R // Create new BTreeMap to extend the context: 'title' and 'link' let mut next_chapter = BTreeMap::new(); - match item.get("name") { - Some(n) => { - debug!("[*]: Inserting title: {}", n); - next_chapter.insert("title".to_owned(), json!(n)); - }, - None => return Err(RenderError::new("No title found for chapter in JSON data")), - } + item.get("name") + .ok_or(RenderError::new("No title found for chapter in JSON data")) + .and_then(|n| { + next_chapter.insert("title".to_owned(), json!(n)); + Ok(()) + })?; - - let link = Path::new(path).with_extension("html"); - debug!("[*]: Inserting link: {:?}", link); - - match link.to_str() { - Some(l) => { - // Hack for windows who tends to use `\` as separator instead of `/` - next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/"))); - }, - None => return Err(RenderError::new("Link could not converted to str")), - } - - debug!("[*]: Inject in context"); - // Inject in current context - let updated_context = rc.context().extend(&next_chapter); + Path::new(path) + .with_extension("html") + .to_str() + .ok_or(RenderError::new("Link could not converted to str")) + .and_then(|l| { + debug!("[*]: Inserting link: {:?}", l); + // Hack for windows who tends to use `\` as separator instead of `/` + next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/"))); + Ok(()) + })?; debug!("[*]: Render template"); // Render template - match _h.template() { - Some(t) => { - *rc.context_mut() = updated_context; - t.render(r, rc)?; - }, - None => return Err(RenderError::new("Error with the handlebars template")), - } - + _h.template() + .ok_or(RenderError::new("Error with the handlebars template")) + .and_then(|t| { + let mut local_rc = rc.with_context(Context::wraps(&next_chapter)); + t.render(r, &mut local_rc) + })?; break; } } diff --git a/src/renderer/html_handlebars/helpers/playpen.rs b/src/renderer/html_handlebars/helpers/playpen.rs index 5e49811f..d1fc28e7 100644 --- a/src/renderer/html_handlebars/helpers/playpen.rs +++ b/src/renderer/html_handlebars/helpers/playpen.rs @@ -3,7 +3,7 @@ use std::fs::File; use std::io::Read; -pub fn render_playpen(s: &str, path: &Path) -> String { +pub fn render_playpen>(s: &str, path: P) -> String { // When replacing one thing in a string by something with a different length, // the indices after that will not correspond, // we therefore have to store the difference to correct this @@ -36,7 +36,12 @@ pub fn render_playpen(s: &str, path: &Path) -> String { continue; }; - let replacement = String::new() + "
" + &file_content + "
"; + let mut editable = ""; + if playpen.editable { + editable = ",editable"; + } + + let replacement = String::new() + "``` rust" + editable + "\n" + &file_content + "\n```\n"; replaced.push_str(&s[previous_end_index..playpen.start_index]); replaced.push_str(&replacement); @@ -60,7 +65,8 @@ struct Playpen { escaped: bool, } -fn find_playpens(s: &str, base_path: &Path) -> Vec { +fn find_playpens>(s: &str, base_path: P) -> Vec { + let base_path = base_path.as_ref(); let mut playpens = vec![]; for (i, _) in s.match_indices("{{#playpen") { debug!("[*]: find_playpen"); @@ -122,28 +128,28 @@ fn find_playpens(s: &str, base_path: &Path) -> Vec { #[test] fn test_find_playpens_no_playpen() { let s = "Some random text without playpen..."; - assert!(find_playpens(s, Path::new("")) == vec![]); + assert!(find_playpens(s, "") == vec![]); } #[test] fn test_find_playpens_partial_playpen() { let s = "Some random text with {{#playpen..."; - assert!(find_playpens(s, Path::new("")) == vec![]); + assert!(find_playpens(s, "") == vec![]); } #[test] fn test_find_playpens_empty_playpen() { let s = "Some random text with {{#playpen}} and {{#playpen }}..."; - assert!(find_playpens(s, Path::new("")) == vec![]); + assert!(find_playpens(s, "") == vec![]); } #[test] fn test_find_playpens_simple_playpen() { let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}..."; - println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new(""))); + println!("\nOUTPUT: {:?}\n", find_playpens(s, "")); - assert!(find_playpens(s, Path::new("")) == + assert!(find_playpens(s, "") == vec![Playpen { start_index: 22, end_index: 42, @@ -164,9 +170,9 @@ fn test_find_playpens_simple_playpen() { fn test_find_playpens_complex_playpen() { let s = "Some random text with {{#playpen file.rs editable}} and {{#playpen test.rs editable }}..."; - println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("dir"))); + println!("\nOUTPUT: {:?}\n", find_playpens(s, "dir")); - assert!(find_playpens(s, Path::new("dir")) == + assert!(find_playpens(s, "dir") == vec![Playpen { start_index: 22, end_index: 51, @@ -187,9 +193,9 @@ fn test_find_playpens_complex_playpen() { fn test_find_playpens_escaped_playpen() { let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ..."; - println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new(""))); + println!("\nOUTPUT: {:?}\n", find_playpens(s, "")); - assert!(find_playpens(s, Path::new("")) == + assert!(find_playpens(s, "") == vec![Playpen { start_index: 39, end_index: 68, diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs index bfc9513c..a007a8d3 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/src/renderer/html_handlebars/helpers/toc.rs @@ -1,5 +1,5 @@ use std::path::Path; -use std::collections::{VecDeque, BTreeMap}; +use std::collections::BTreeMap; use serde_json; use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper}; @@ -15,21 +15,21 @@ impl HelperDef for RenderToc { // get value from context data // rc.get_path() is current json parent path, you should always use it like this // param is the key of value you want to display - let chapters = rc.context() - .navigate(rc.get_path(), &VecDeque::new(), "chapters") - .to_owned(); - let current = rc.context() - .navigate(rc.get_path(), &VecDeque::new(), "path") - .to_string() + let chapters = rc.evaluate_absolute("chapters") + .and_then(|c| { + serde_json::value::from_value::>>(c.clone()) + .map_err(|_| RenderError::new("Could not decode the JSON data")) + })?; + let current = rc.evaluate_absolute("path")? + .as_str() + .ok_or(RenderError::new("Type error for `path`, string expected"))? .replace("\"", ""); - rc.writer.write_all("
    ".as_bytes())?; - // Decode json format - let decoded: Vec> = serde_json::from_str(&chapters.to_string()).unwrap(); + rc.writer.write_all("
      ".as_bytes())?; let mut current_level = 1; - for item in decoded { + for item in chapters { // Spacer if item.get("spacer").is_some() { diff --git a/src/theme/book.css b/src/theme/book.css index 6d3db97f..362dd8bc 100644 --- a/src/theme/book.css +++ b/src/theme/book.css @@ -3,6 +3,9 @@ body { font-family: "Open Sans", sans-serif; color: #333; } +body { + margin: 0; +} code { font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace; font-size: 0.875em; @@ -41,7 +44,7 @@ table thead td { font-weight: 700; } .sidebar { - position: absolute; + position: fixed; left: 0; top: 0; bottom: 0; @@ -102,43 +105,31 @@ table thead td { white-space: nowrap; } .page-wrapper { - position: absolute; - overflow-y: auto; - left: 315px; - right: 0; - top: 0; - bottom: 0; + padding-left: 300px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; - -webkit-overflow-scrolling: touch; min-height: 100%; - -webkit-transition: left 0.5s; - -moz-transition: left 0.5s; - -o-transition: left 0.5s; - -ms-transition: left 0.5s; - transition: left 0.5s; + -webkit-transition: padding-left 0.5s; + -moz-transition: padding-left 0.5s; + -o-transition: padding-left 0.5s; + -ms-transition: padding-left 0.5s; + transition: padding-left 0.5s; } @media only screen and (max-width: 1060px) { .page-wrapper { - left: 15px; - padding-right: 15px; + padding-left: 0; } } .sidebar-hidden .page-wrapper { - left: 15px; + padding-left: 0; } .sidebar-visible .page-wrapper { - left: 315px; + padding-left: 300px; } .page { - position: absolute; - top: 0; - right: 0; - left: 0; - bottom: 0; - padding-right: 15px; - overflow-y: auto; + outline: 0; + padding: 0 15px; } .content { margin-left: auto; @@ -209,7 +200,7 @@ table thead td { font-size: 2.5em; text-align: center; text-decoration: none; - position: absolute; + position: fixed; top: 50px /* Height of menu-bar */; bottom: 0; margin: 0; @@ -249,11 +240,27 @@ table thead td { text-decoration: none; } .previous { - left: 0; + left: 315px; + -webkit-transition: left 0.5s; + -moz-transition: left 0.5s; + -o-transition: left 0.5s; + -ms-transition: left 0.5s; + transition: left 0.5s; +} +@media only screen and (max-width: 1060px) { + .previous { + left: 15px; + } } .next { right: 15px; } +.sidebar-hidden .previous { + left: 15px; +} +.sidebar-visible .previous { + left: 315px; +} .theme-popup { position: relative; left: 10px; @@ -266,6 +273,7 @@ table thead td { padding: 2px 10px; line-height: 25px; white-space: nowrap; + cursor: pointer; } .theme-popup .theme:hover:first-child, .theme-popup .theme:hover:last-child { diff --git a/src/theme/book.js b/src/theme/book.js index 8b1cdc15..50564d32 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -52,8 +52,9 @@ $( document ).ready(function() { // Interesting DOM Elements var sidebar = $("#sidebar"); - var page_wrapper = $("#page-wrapper"); - var content = $("#content"); + + // Help keyboard navigation by always focusing on page content + $(".page").focus(); // Toggle sidebar $("#sidebar-toggle").click(sidebarToggle); @@ -100,11 +101,20 @@ $( document ).ready(function() { $('.theme').click(function(){ var theme = $(this).attr('id'); - set_theme(theme); }); } + }); + // Hide theme selector popup when clicking outside of it + $(document).click(function(event){ + var popup = $('.theme-popup'); + if(popup.length) { + var target = $(event.target); + if(!target.closest('.theme').length && !target.closest('#theme-toggle').length) { + popup.remove(); + } + } }); function set_theme(theme) { diff --git a/src/theme/index.hbs b/src/theme/index.hbs index bf316f5f..467d2fe6 100644 --- a/src/theme/index.hbs +++ b/src/theme/index.hbs @@ -76,7 +76,7 @@
      -
      +