Merge branch 'upstream/master' into refactor-hbs-renderer

Notably, this takes into account the curly-quotes pull request (#305)
This commit is contained in:
Michael Bryan 2017-06-24 16:07:01 +08:00
commit c3dfabd5a2
28 changed files with 501 additions and 324 deletions

View File

@ -16,7 +16,7 @@ exclude = [
[dependencies] [dependencies]
clap = "2.24" clap = "2.24"
handlebars = "0.26" handlebars = "0.27"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"

View File

@ -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:** By default, the HTML book will be rendered in the `root/book` directory, but this option lets you specify another
destination fodler. 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. - **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. - **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. - **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] [output.html]
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book` destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
theme = "my-theme" theme = "my-theme"
curly-quotes = true
google-analytics = "123456" google-analytics = "123456"
additional-css = ["custom.css", "custom2.css"] additional-css = ["custom.css", "custom2.css"]
``` ```

View File

@ -9,13 +9,15 @@ extern crate mdbook;
use mdbook::MDBook; use mdbook::MDBook;
use std::path::Path; use std::path::Path;
# #[allow(unused_variables)]
fn main() { fn main() {
let mut book = MDBook::new(Path::new("my-book")) // Path to root let mut book = MDBook::new("my-book") // Path to root
.set_src(Path::new("src")) // Path from root to source directory .with_source("src") // Path from root to source directory
.set_dest(Path::new("book")) // Path from root to output directory .with_destination("book") // Path from root to output directory
.read_config(); // Parse book.toml or book.json file for configuration .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
} }
``` ```

View File

@ -13,14 +13,6 @@ extern crate time;
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
extern crate crossbeam; 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::env;
use std::error::Error; use std::error::Error;
use std::ffi::OsStr; 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("-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("-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("--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)'")) .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'"))
.subcommand(SubCommand::with_name("watch") .subcommand(SubCommand::with_name("watch")
.about("Watch the files for changes") .about("Watch the files for changes")
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'") .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("-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)'")) .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'"))
.subcommand(SubCommand::with_name("serve") .subcommand(SubCommand::with_name("serve")
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.") .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("[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("-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("-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("-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)'") .arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'")
@ -90,7 +85,7 @@ fn main() {
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
("watch", Some(sub_matches)) => watch(sub_matches), ("watch", Some(sub_matches)) => watch(sub_matches),
#[cfg(feature = "serve")] #[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), ("test", Some(sub_matches)) => test(sub_matches),
(_, _) => unreachable!(), (_, _) => unreachable!(),
}; };
@ -173,7 +168,7 @@ fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
let book = MDBook::new(&book_dir).read_config()?; let book = MDBook::new(&book_dir).read_config()?;
let mut book = match args.value_of("dest-dir") { 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, None => book,
}; };
@ -181,6 +176,10 @@ fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
book.create_missing = false; book.create_missing = false;
} }
if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
}
book.build()?; book.build()?;
if let Some(d) = book.get_destination() { if let Some(d) = book.get_destination() {
@ -200,10 +199,14 @@ fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
let book = MDBook::new(&book_dir).read_config()?; let book = MDBook::new(&book_dir).read_config()?;
let mut book = match args.value_of("dest-dir") { 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, None => book,
}; };
if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
}
if args.is_present("open") { if args.is_present("open") {
book.build()?; book.build()?;
if let Some(d) = book.get_destination() { if let Some(d) = book.get_destination() {
@ -222,35 +225,64 @@ fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
Ok(()) Ok(())
} }
// Watch command implementation
#[cfg(feature = "serve")] #[cfg(feature = "serve")]
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> { mod serve {
const RELOAD_COMMAND: &'static str = "reload"; extern crate iron;
extern crate staticfile;
extern crate ws;
let book_dir = get_book_dir(args); use std;
let book = MDBook::new(&book_dir).read_config()?; 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") { use {get_book_dir, open, trigger_on_change};
Some(dest_dir) => book.with_destination(Path::new(dest_dir)),
None => book,
};
if let None = book.get_destination() { struct ErrorRecover;
println!("The HTML renderer is not set up, impossible to serve the files.");
std::process::exit(2); impl AfterMiddleware for ErrorRecover {
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
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"); // Watch command implementation
let ws_port = args.value_of("websocket-port").unwrap_or("3001"); pub fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
let interface = args.value_of("interface").unwrap_or("localhost"); const RELOAD_COMMAND: &'static str = "reload";
let public_address = args.value_of("address").unwrap_or(interface);
let open_browser = args.is_present("open");
let address = format!("{}:{}", interface, port); let book_dir = get_book_dir(args);
let ws_address = format!("{}:{}", interface, ws_port); 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#"
<script type="text/javascript"> <script type="text/javascript">
var socket = new WebSocket("ws://{}:{}"); var socket = new WebSocket("ws://{}:{}");
socket.onmessage = function (event) {{ socket.onmessage = function (event) {{
@ -265,42 +297,43 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
}} }}
</script> </script>
"#, "#,
public_address, public_address,
ws_port, ws_port,
RELOAD_COMMAND)); RELOAD_COMMAND));
book.build()?; book.build()?;
let staticfile = staticfile::Static::new(book.get_destination().expect("destination is present, checked before")); let mut chain = Chain::new(staticfile::Static::new(book.get_destination()
let iron = iron::Iron::new(staticfile); .expect("destination is present, checked before")));
let _iron = iron.http(&*address).unwrap(); 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); let serving_url = format!("http://{}", address);
println!("\nServing on: {}", serving_url); println!("\nServing on: {}", serving_url);
if open_browser { if open_browser {
open(serving_url); 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(),
} }
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<Error>> { fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config()?; let mut book = MDBook::new(&book_dir).read_config()?;

View File

@ -39,9 +39,9 @@ impl MDBook {
/// ```no_run /// ```no_run
/// # extern crate mdbook; /// # extern crate mdbook;
/// # use mdbook::MDBook; /// # use mdbook::MDBook;
/// # use std::path::Path; /// # #[allow(unused_variables)]
/// # fn main() { /// # 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 /// They can both be changed by using [`set_src()`](#method.set_src) and
/// [`set_dest()`](#method.set_dest) /// [`set_dest()`](#method.set_dest)
pub fn new(root: &Path) -> MDBook { pub fn new<P: Into<PathBuf>>(root: P) -> MDBook {
let root = root.into();
if !root.exists() || !root.is_dir() { if !root.exists() || !root.is_dir() {
warn!("{:?} No directory with that name", root); warn!("{:?} No directory with that name", root);
} }
@ -84,9 +85,9 @@ impl MDBook {
/// # extern crate mdbook; /// # extern crate mdbook;
/// # use mdbook::MDBook; /// # use mdbook::MDBook;
/// # use mdbook::BookItem; /// # use mdbook::BookItem;
/// # use std::path::Path; /// # #[allow(unused_variables)]
/// # fn main() { /// # fn main() {
/// # let mut book = MDBook::new(Path::new("mybook")); /// # let book = MDBook::new("mybook");
/// for item in book.iter() { /// for item in book.iter() {
/// match item { /// match item {
/// &BookItem::Chapter(ref section, ref chapter) => {}, /// &BookItem::Chapter(ref section, ref chapter) => {},
@ -347,10 +348,10 @@ impl MDBook {
/// extern crate mdbook; /// extern crate mdbook;
/// use mdbook::MDBook; /// use mdbook::MDBook;
/// use mdbook::renderer::HtmlHandlebars; /// use mdbook::renderer::HtmlHandlebars;
/// # use std::path::Path;
/// ///
/// # #[allow(unused_variables)]
/// fn main() { /// fn main() {
/// let mut book = MDBook::new(Path::new("mybook")) /// let book = MDBook::new("mybook")
/// .set_renderer(Box::new(HtmlHandlebars::new())); /// .set_renderer(Box::new(HtmlHandlebars::new()));
/// ///
/// // In this example we replace the default renderer /// // In this example we replace the default renderer
@ -479,6 +480,23 @@ impl MDBook {
None 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<String> { pub fn get_google_analytics_id(&self) -> Option<String> {
if let Some(htmlconfig) = self.config.get_html_config() { if let Some(htmlconfig) = self.config.get_html_config() {
return htmlconfig.get_google_analytics_id(); return htmlconfig.get_google_analytics_id();

View File

@ -6,6 +6,7 @@ use super::tomlconfig::TomlHtmlConfig;
pub struct HtmlConfig { pub struct HtmlConfig {
destination: PathBuf, destination: PathBuf,
theme: Option<PathBuf>, theme: Option<PathBuf>,
curly_quotes: bool,
google_analytics: Option<String>, google_analytics: Option<String>,
additional_css: Vec<PathBuf>, additional_css: Vec<PathBuf>,
additional_js: Vec<PathBuf>, additional_js: Vec<PathBuf>,
@ -27,6 +28,7 @@ impl HtmlConfig {
HtmlConfig { HtmlConfig {
destination: root.into().join("book"), destination: root.into().join("book"),
theme: None, theme: None,
curly_quotes: false,
google_analytics: None, google_analytics: None,
additional_css: Vec::new(), additional_css: Vec::new(),
additional_js: 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() { if tomlconfig.google_analytics.is_some() {
self.google_analytics = tomlconfig.google_analytics; self.google_analytics = tomlconfig.google_analytics;
} }
@ -110,6 +116,14 @@ impl HtmlConfig {
self 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<String> { pub fn get_google_analytics_id(&self) -> Option<String> {
self.google_analytics.clone() self.google_analytics.clone()
} }

View File

@ -39,5 +39,3 @@ impl JsonConfig {
return Ok(config); return Ok(config);
} }
} }

View File

@ -24,6 +24,7 @@ pub struct TomlHtmlConfig {
pub destination: Option<PathBuf>, pub destination: Option<PathBuf>,
pub theme: Option<PathBuf>, pub theme: Option<PathBuf>,
pub google_analytics: Option<String>, pub google_analytics: Option<String>,
pub curly_quotes: Option<bool>,
pub additional_css: Option<Vec<PathBuf>>, pub additional_css: Option<Vec<PathBuf>>,
pub additional_js: Option<Vec<PathBuf>>, pub additional_js: Option<Vec<PathBuf>>,
} }

View File

@ -21,16 +21,16 @@
//! extern crate mdbook; //! extern crate mdbook;
//! //!
//! use mdbook::MDBook; //! use mdbook::MDBook;
//! use std::path::Path;
//! //!
//! # #[allow(unused_variables)]
//! fn main() { //! fn main() {
//! let mut book = MDBook::new(Path::new("my-book")) // Path to root //! let mut book = MDBook::new("my-book") // Path to root
//! .with_source(Path::new("src")) // Path from root to source directory //! .with_source("src") // Path from root to source directory
//! .with_destination(Path::new("book")) // Path from root to output directory //! .with_destination("book") // Path from root to output directory
//! .read_config() // Parse book.json file for configuration //! .read_config() // Parse book.toml configuration file
//! .expect("I don't handle the error for the configuration file, but you should!"); //! .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::MDBook;
//! # use mdbook::renderer::HtmlHandlebars; //! # use mdbook::renderer::HtmlHandlebars;
//! # use std::path::Path;
//! # //! #
//! # #[allow(unused_variables)]
//! # fn main() { //! # fn main() {
//! # let your_renderer = HtmlHandlebars::new(); //! # 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<BookItems>` and you get //! If you make a renderer, you get the book constructed in form of `Vec<BookItems>` and you get
@ -61,12 +61,11 @@
//! //!
//! ## utils //! ## 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 //! 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.
//! utils::fs::create_path(path: &Path)
//! ```
//! This function creates all the directories in a given path if they do not exist
//! //!
//! Make sure to take a look at it. //! Make sure to take a look at it.

View File

@ -49,7 +49,7 @@ impl HtmlHandlebars {
content = helpers::playpen::render_playpen(&content, p); 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); print_content.push_str(&content);
// Update the context with data for this file // Update the context with data for this file

View File

@ -1,8 +1,8 @@
use std::path::Path; use std::path::Path;
use std::collections::{VecDeque, BTreeMap}; use std::collections::BTreeMap;
use serde_json; use serde_json;
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable}; use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable, Context};
// Handlebars helper for navigation // 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!("[fn]: previous (handlebars helper)");
debug!("[*]: Get data from context"); debug!("[*]: Get data from context");
// get value from context data let chapters = rc.evaluate_absolute("chapters")
// rc.get_path() is current json parent path, you should always use it like this .and_then(|c| {
// param is the key of value you want to display serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
let chapters = rc.context() .map_err(|_| RenderError::new("Could not decode the JSON data"))
.navigate(rc.get_path(), &VecDeque::new(), "chapters") })?;
.to_owned();
let current = rc.context() let current = rc.evaluate_absolute("path")?
.navigate(rc.get_path(), &VecDeque::new(), "path") .as_str()
.to_string() .ok_or(RenderError::new("Type error for `path`, string expected"))?
.replace("\"", ""); .replace("\"", "");
debug!("[*]: Decode chapters from JSON");
// Decode json format
let decoded: Vec<BTreeMap<String, String>> = 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<BTreeMap<String, String>> = None; let mut previous: Option<BTreeMap<String, String>> = None;
debug!("[*]: Search for current Chapter"); debug!("[*]: Search for current Chapter");
// Search for current chapter and return previous entry // Search for current chapter and return previous entry
for item in decoded { for item in chapters {
match item.get("path") { match item.get("path") {
Some(path) if !path.is_empty() => { 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(); let mut previous_chapter = BTreeMap::new();
// Chapter title // Chapter title
match previous.get("name") { previous
Some(n) => { .get("name")
debug!("[*]: Inserting title: {}", n); .ok_or(RenderError::new("No title found for chapter in JSON data"))
previous_chapter.insert("title".to_owned(), json!(n)) .and_then(|n| {
}, previous_chapter.insert("title".to_owned(), json!(n));
None => { Ok(())
debug!("[*]: No title found for chapter"); })?;
return Err(RenderError::new("No title found for chapter in JSON data"));
},
};
// Chapter link // 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"); debug!("[*]: Render template");
// Render template // Render template
match _h.template() { _h.template()
Some(t) => { .ok_or(RenderError::new("Error with the handlebars template"))
*rc.context_mut() = updated_context; .and_then(|t| {
t.render(r, rc)?; let mut local_rc = rc.with_context(Context::wraps(&previous_chapter));
}, t.render(r, &mut local_rc)
None => return Err(RenderError::new("Error with the handlebars template")), })?;
}
} }
break; break;
} else { } else {
previous = Some(item.clone()); previous = Some(item.clone());
@ -102,7 +83,6 @@ pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(
_ => continue, _ => continue,
} }
} }
Ok(()) Ok(())
@ -115,29 +95,21 @@ pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), R
debug!("[fn]: next (handlebars helper)"); debug!("[fn]: next (handlebars helper)");
debug!("[*]: Get data from context"); debug!("[*]: Get data from context");
// get value from context data let chapters = rc.evaluate_absolute("chapters")
// rc.get_path() is current json parent path, you should always use it like this .and_then(|c| {
// param is the key of value you want to display serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
let chapters = rc.context() .map_err(|_| RenderError::new("Could not decode the JSON data"))
.navigate(rc.get_path(), &VecDeque::new(), "chapters") })?;
.to_owned(); let current = rc.evaluate_absolute("path")?
.as_str()
let current = rc.context() .ok_or(RenderError::new("Type error for `path`, string expected"))?
.navigate(rc.get_path(), &VecDeque::new(), "path")
.to_string()
.replace("\"", ""); .replace("\"", "");
debug!("[*]: Decode chapters from JSON");
// Decode json format
let decoded: Vec<BTreeMap<String, String>> = 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<BTreeMap<String, String>> = None; let mut previous: Option<BTreeMap<String, String>> = None;
debug!("[*]: Search for current Chapter"); debug!("[*]: Search for current Chapter");
// Search for current chapter and return previous entry // Search for current chapter and return previous entry
for item in decoded { for item in chapters {
match item.get("path") { 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 { if let Some(previous) = previous {
let previous_path = match previous.get("path") { let previous_path = previous
Some(p) => p, .get("path")
None => return Err(RenderError::new("No path found for chapter in JSON data")), .ok_or(RenderError::new("No path found for chapter in JSON data"))?;
};
if previous_path == &current { if previous_path == &current {
@ -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' // Create new BTreeMap to extend the context: 'title' and 'link'
let mut next_chapter = BTreeMap::new(); let mut next_chapter = BTreeMap::new();
match item.get("name") { item.get("name")
Some(n) => { .ok_or(RenderError::new("No title found for chapter in JSON data"))
debug!("[*]: Inserting title: {}", n); .and_then(|n| {
next_chapter.insert("title".to_owned(), json!(n)); next_chapter.insert("title".to_owned(), json!(n));
}, Ok(())
None => return Err(RenderError::new("No title found for chapter in JSON data")), })?;
}
Path::new(path)
let link = Path::new(path).with_extension("html"); .with_extension("html")
debug!("[*]: Inserting link: {:?}", link); .to_str()
.ok_or(RenderError::new("Link could not converted to str"))
match link.to_str() { .and_then(|l| {
Some(l) => { debug!("[*]: Inserting link: {:?}", l);
// Hack for windows who tends to use `\` as separator instead of `/` // Hack for windows who tends to use `\` as separator instead of `/`
next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/"))); next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/")));
}, Ok(())
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);
debug!("[*]: Render template"); debug!("[*]: Render template");
// Render template // Render template
match _h.template() { _h.template()
Some(t) => { .ok_or(RenderError::new("Error with the handlebars template"))
*rc.context_mut() = updated_context; .and_then(|t| {
t.render(r, rc)?; let mut local_rc = rc.with_context(Context::wraps(&next_chapter));
}, t.render(r, &mut local_rc)
None => return Err(RenderError::new("Error with the handlebars template")), })?;
}
break; break;
} }
} }

View File

@ -3,7 +3,7 @@ use std::fs::File;
use std::io::Read; use std::io::Read;
pub fn render_playpen(s: &str, path: &Path) -> String { pub fn render_playpen<P: AsRef<Path>>(s: &str, path: P) -> String {
// When replacing one thing in a string by something with a different length, // When replacing one thing in a string by something with a different length,
// the indices after that will not correspond, // the indices after that will not correspond,
// we therefore have to store the difference to correct this // we therefore have to store the difference to correct this
@ -36,7 +36,12 @@ pub fn render_playpen(s: &str, path: &Path) -> String {
continue; continue;
}; };
let replacement = String::new() + "<pre><code class=\"language-rust\">" + &file_content + "</code></pre>"; 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(&s[previous_end_index..playpen.start_index]);
replaced.push_str(&replacement); replaced.push_str(&replacement);
@ -60,7 +65,8 @@ struct Playpen {
escaped: bool, escaped: bool,
} }
fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> { fn find_playpens<P: AsRef<Path>>(s: &str, base_path: P) -> Vec<Playpen> {
let base_path = base_path.as_ref();
let mut playpens = vec![]; let mut playpens = vec![];
for (i, _) in s.match_indices("{{#playpen") { for (i, _) in s.match_indices("{{#playpen") {
debug!("[*]: find_playpen"); debug!("[*]: find_playpen");
@ -122,28 +128,28 @@ fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
#[test] #[test]
fn test_find_playpens_no_playpen() { fn test_find_playpens_no_playpen() {
let s = "Some random text without playpen..."; let s = "Some random text without playpen...";
assert!(find_playpens(s, Path::new("")) == vec![]); assert!(find_playpens(s, "") == vec![]);
} }
#[test] #[test]
fn test_find_playpens_partial_playpen() { fn test_find_playpens_partial_playpen() {
let s = "Some random text with {{#playpen..."; let s = "Some random text with {{#playpen...";
assert!(find_playpens(s, Path::new("")) == vec![]); assert!(find_playpens(s, "") == vec![]);
} }
#[test] #[test]
fn test_find_playpens_empty_playpen() { fn test_find_playpens_empty_playpen() {
let s = "Some random text with {{#playpen}} and {{#playpen }}..."; let s = "Some random text with {{#playpen}} and {{#playpen }}...";
assert!(find_playpens(s, Path::new("")) == vec![]); assert!(find_playpens(s, "") == vec![]);
} }
#[test] #[test]
fn test_find_playpens_simple_playpen() { fn test_find_playpens_simple_playpen() {
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}..."; 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 { vec![Playpen {
start_index: 22, start_index: 22,
end_index: 42, end_index: 42,
@ -164,9 +170,9 @@ fn test_find_playpens_simple_playpen() {
fn test_find_playpens_complex_playpen() { fn test_find_playpens_complex_playpen() {
let s = "Some random text with {{#playpen file.rs editable}} and {{#playpen test.rs editable }}..."; 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 { vec![Playpen {
start_index: 22, start_index: 22,
end_index: 51, end_index: 51,
@ -187,9 +193,9 @@ fn test_find_playpens_complex_playpen() {
fn test_find_playpens_escaped_playpen() { fn test_find_playpens_escaped_playpen() {
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ..."; 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 { vec![Playpen {
start_index: 39, start_index: 39,
end_index: 68, end_index: 68,

View File

@ -1,5 +1,5 @@
use std::path::Path; use std::path::Path;
use std::collections::{VecDeque, BTreeMap}; use std::collections::BTreeMap;
use serde_json; use serde_json;
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper}; use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper};
@ -15,21 +15,21 @@ impl HelperDef for RenderToc {
// get value from context data // get value from context data
// rc.get_path() is current json parent path, you should always use it like this // 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 // param is the key of value you want to display
let chapters = rc.context() let chapters = rc.evaluate_absolute("chapters")
.navigate(rc.get_path(), &VecDeque::new(), "chapters") .and_then(|c| {
.to_owned(); serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
let current = rc.context() .map_err(|_| RenderError::new("Could not decode the JSON data"))
.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("\"", ""); .replace("\"", "");
rc.writer.write_all("<ul class=\"chapter\">".as_bytes())?;
// Decode json format rc.writer.write_all("<ul class=\"chapter\">".as_bytes())?;
let decoded: Vec<BTreeMap<String, String>> = serde_json::from_str(&chapters.to_string()).unwrap();
let mut current_level = 1; let mut current_level = 1;
for item in decoded { for item in chapters {
// Spacer // Spacer
if item.get("spacer").is_some() { if item.get("spacer").is_some() {

View File

@ -3,6 +3,9 @@ body {
font-family: "Open Sans", sans-serif; font-family: "Open Sans", sans-serif;
color: #333; color: #333;
} }
body {
margin: 0;
}
code { code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace; font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
font-size: 0.875em; font-size: 0.875em;
@ -41,7 +44,7 @@ table thead td {
font-weight: 700; font-weight: 700;
} }
.sidebar { .sidebar {
position: absolute; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -102,43 +105,31 @@ table thead td {
white-space: nowrap; white-space: nowrap;
} }
.page-wrapper { .page-wrapper {
position: absolute; padding-left: 300px;
overflow-y: auto;
left: 315px;
right: 0;
top: 0;
bottom: 0;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
-webkit-overflow-scrolling: touch;
min-height: 100%; min-height: 100%;
-webkit-transition: left 0.5s; -webkit-transition: padding-left 0.5s;
-moz-transition: left 0.5s; -moz-transition: padding-left 0.5s;
-o-transition: left 0.5s; -o-transition: padding-left 0.5s;
-ms-transition: left 0.5s; -ms-transition: padding-left 0.5s;
transition: left 0.5s; transition: padding-left 0.5s;
} }
@media only screen and (max-width: 1060px) { @media only screen and (max-width: 1060px) {
.page-wrapper { .page-wrapper {
left: 15px; padding-left: 0;
padding-right: 15px;
} }
} }
.sidebar-hidden .page-wrapper { .sidebar-hidden .page-wrapper {
left: 15px; padding-left: 0;
} }
.sidebar-visible .page-wrapper { .sidebar-visible .page-wrapper {
left: 315px; padding-left: 300px;
} }
.page { .page {
position: absolute; outline: 0;
top: 0; padding: 0 15px;
right: 0;
left: 0;
bottom: 0;
padding-right: 15px;
overflow-y: auto;
} }
.content { .content {
margin-left: auto; margin-left: auto;
@ -209,7 +200,7 @@ table thead td {
font-size: 2.5em; font-size: 2.5em;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
position: absolute; position: fixed;
top: 50px /* Height of menu-bar */; top: 50px /* Height of menu-bar */;
bottom: 0; bottom: 0;
margin: 0; margin: 0;
@ -249,11 +240,27 @@ table thead td {
text-decoration: none; text-decoration: none;
} }
.previous { .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 { .next {
right: 15px; right: 15px;
} }
.sidebar-hidden .previous {
left: 15px;
}
.sidebar-visible .previous {
left: 315px;
}
.theme-popup { .theme-popup {
position: relative; position: relative;
left: 10px; left: 10px;
@ -266,6 +273,7 @@ table thead td {
padding: 2px 10px; padding: 2px 10px;
line-height: 25px; line-height: 25px;
white-space: nowrap; white-space: nowrap;
cursor: pointer;
} }
.theme-popup .theme:hover:first-child, .theme-popup .theme:hover:first-child,
.theme-popup .theme:hover:last-child { .theme-popup .theme:hover:last-child {

View File

@ -52,8 +52,9 @@ $( document ).ready(function() {
// Interesting DOM Elements // Interesting DOM Elements
var sidebar = $("#sidebar"); var sidebar = $("#sidebar");
var page_wrapper = $("#page-wrapper");
var content = $("#content"); // Help keyboard navigation by always focusing on page content
$(".page").focus();
// Toggle sidebar // Toggle sidebar
$("#sidebar-toggle").click(sidebarToggle); $("#sidebar-toggle").click(sidebarToggle);
@ -100,11 +101,20 @@ $( document ).ready(function() {
$('.theme').click(function(){ $('.theme').click(function(){
var theme = $(this).attr('id'); var theme = $(this).attr('id');
set_theme(theme); 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) { function set_theme(theme) {

View File

@ -76,7 +76,7 @@
<div id="page-wrapper" class="page-wrapper"> <div id="page-wrapper" class="page-wrapper">
<div class="page"> <div class="page" tabindex="-1">
<div id="menu-bar" class="menu-bar"> <div id="menu-bar" class="menu-bar">
<div class="left-buttons"> <div class="left-buttons">
<i id="sidebar-toggle" class="fa fa-bars"></i> <i id="sidebar-toggle" class="fa fa-bars"></i>
@ -96,13 +96,13 @@
<!-- Mobile navigation buttons --> <!-- Mobile navigation buttons -->
{{#previous}} {{#previous}}
<a href="{{link}}" class="mobile-nav-chapters previous"> <a rel="prev" href="{{link}}" class="mobile-nav-chapters previous">
<i class="fa fa-angle-left"></i> <i class="fa fa-angle-left"></i>
</a> </a>
{{/previous}} {{/previous}}
{{#next}} {{#next}}
<a href="{{link}}" class="mobile-nav-chapters next"> <a rel="next" href="{{link}}" class="mobile-nav-chapters next">
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
</a> </a>
{{/next}} {{/next}}

View File

@ -3,6 +3,10 @@ html, body {
color: #333 color: #333
} }
body {
margin: 0;
}
code { code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace; font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
font-size: 0.875em; font-size: 0.875em;

View File

@ -3,7 +3,7 @@
text-align: center text-align: center
text-decoration: none text-decoration: none
position: absolute position: fixed
top: 50px /* Height of menu-bar */ top: 50px /* Height of menu-bar */
bottom: 0 bottom: 0
margin: 0 margin: 0
@ -19,5 +19,20 @@
.mobile-nav-chapters { display: none } .mobile-nav-chapters { display: none }
.nav-chapters:hover { text-decoration: none } .nav-chapters:hover { text-decoration: none }
.previous { left: 0 } .previous {
.next { right: 15px } left: $sidebar-width + $page-padding
transition: left 0.5s
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
left: $page-padding
}
}
.next { right: $page-padding }
.sidebar-hidden .previous {
left: $page-padding
}
.sidebar-visible .previous {
left: $sidebar-width + $page-padding
}

View File

@ -1,43 +1,30 @@
@require 'variables' @require 'variables'
.page-wrapper { .page-wrapper {
position: absolute padding-left: $sidebar-width
overflow-y: auto
left: $sidebar-width + 15px
right: 0
top: 0
bottom: 0
box-sizing: border-box box-sizing: border-box
-webkit-overflow-scrolling: touch
min-height: 100% min-height: 100%
// Animation: slide away // Animation: slide away
transition: left 0.5s transition: padding-left 0.5s
@media only screen and (max-width: 1060px) { @media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
left: 15px; padding-left: 0
padding-right: 15px;
} }
} }
.sidebar-hidden .page-wrapper { .sidebar-hidden .page-wrapper {
left: 15px padding-left: 0
} }
.sidebar-visible .page-wrapper { .sidebar-visible .page-wrapper {
left: $sidebar-width + 15px padding-left: $sidebar-width
} }
.page { .page {
position: absolute outline: 0
top: 0 padding: 0 $page-padding
right: 0
left: 0
bottom: 0
padding-right: 15px
overflow-y: auto
} }
.content { .content {

View File

@ -1,7 +1,7 @@
@require 'variables' @require 'variables'
.sidebar { .sidebar {
position: absolute position: fixed
left: 0 left: 0
top: 0 top: 0
bottom: 0 bottom: 0
@ -15,7 +15,7 @@
// Animation: slide away // Animation: slide away
transition: left 0.5s transition: left 0.5s
@media only screen and (max-width: 1060px) { @media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
left: - $sidebar-width left: - $sidebar-width
} }

View File

@ -12,6 +12,7 @@
padding: 2px 10px padding: 2px 10px
line-height: 25px line-height: 25px
white-space: nowrap white-space: nowrap
cursor: pointer
&:hover:first-child, &:hover:first-child,
&:hover:last-child { &:hover:last-child {

View File

@ -1 +1,3 @@
$sidebar-width = 300px $sidebar-width = 300px
$page-padding = 15px
$max-page-width-with-hidden-sidebar = 1060px

View File

@ -1,4 +1,4 @@
use std::path::{Path, Component}; use std::path::{Path, PathBuf, Component};
use std::error::Error; use std::error::Error;
use std::io::{self, Read}; use std::io::{self, Read};
use std::fs::{self, File}; use std::fs::{self, File};
@ -30,16 +30,16 @@ pub fn file_to_string(path: &Path) -> Result<String, Box<Error>> {
/// This is mostly interesting for a relative path to point back to the /// This is mostly interesting for a relative path to point back to the
/// directory from where the path starts. /// directory from where the path starts.
/// ///
/// ```ignore /// ```rust
/// let mut path = Path::new("some/relative/path"); /// # extern crate mdbook;
/// /// #
/// println!("{}", path_to_root(&path)); /// # use std::path::Path;
/// ``` /// # use mdbook::utils::fs::path_to_root;
/// /// #
/// **Outputs** /// # fn main() {
/// /// let path = Path::new("some/relative/path");
/// ```text /// assert_eq!(path_to_root(path), "../../");
/// "../../" /// # }
/// ``` /// ```
/// ///
/// **note:** it's not very fool-proof, if you find a situation where /// **note:** it's not very fool-proof, if you find a situation where
@ -47,11 +47,11 @@ pub fn file_to_string(path: &Path) -> Result<String, Box<Error>> {
/// Consider [submitting a new issue](https://github.com/azerupi/mdBook/issues) /// Consider [submitting a new issue](https://github.com/azerupi/mdBook/issues)
/// or a [pull-request](https://github.com/azerupi/mdBook/pulls) to improve it. /// or a [pull-request](https://github.com/azerupi/mdBook/pulls) to improve it.
pub fn path_to_root(path: &Path) -> String { pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
debug!("[fn]: path_to_root"); debug!("[fn]: path_to_root");
// Remove filename and add "../" for every directory // Remove filename and add "../" for every directory
path.to_path_buf() path.into()
.parent() .parent()
.expect("") .expect("")
.components() .components()

View File

@ -1,13 +1,14 @@
pub mod fs; pub mod fs;
use pulldown_cmark::{Parser, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES}; use pulldown_cmark::{Parser, Event, Tag, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
use std::borrow::Cow;
/// ///
/// ///
/// Wrapper around the pulldown-cmark parser and renderer to render markdown /// Wrapper around the pulldown-cmark parser and renderer to render markdown
pub fn render_markdown(text: &str) -> String { pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2); let mut s = String::with_capacity(text.len() * 3 / 2);
let mut opts = Options::empty(); let mut opts = Options::empty();
@ -15,6 +16,107 @@ pub fn render_markdown(text: &str) -> String {
opts.insert(OPTION_ENABLE_FOOTNOTES); opts.insert(OPTION_ENABLE_FOOTNOTES);
let p = Parser::new_ext(text, opts); let p = Parser::new_ext(text, opts);
html::push_html(&mut s, p); let mut converter = EventQuoteConverter::new(curly_quotes);
let events = p.map(|event| converter.convert(event));
html::push_html(&mut s, events);
s s
} }
struct EventQuoteConverter {
enabled: bool,
convert_text: bool,
}
impl EventQuoteConverter {
fn new(enabled: bool) -> Self {
EventQuoteConverter { enabled: enabled, convert_text: true }
}
fn convert<'a>(&mut self, event: Event<'a>) -> Event<'a> {
if !self.enabled {
return event;
}
match event {
Event::Start(Tag::CodeBlock(_)) |
Event::Start(Tag::Code) => {
self.convert_text = false;
event
},
Event::End(Tag::CodeBlock(_)) |
Event::End(Tag::Code) => {
self.convert_text = true;
event
},
Event::Text(ref text) if self.convert_text => Event::Text(Cow::from(convert_quotes_to_curly(text))),
_ => event,
}
}
}
fn convert_quotes_to_curly(original_text: &str) -> String {
// We'll consider the start to be "whitespace".
let mut preceded_by_whitespace = true;
original_text
.chars()
.map(|original_char| {
let converted_char = match original_char {
'\'' => if preceded_by_whitespace { '' } else { '' },
'"' => if preceded_by_whitespace { '“' } else { '”' },
_ => original_char,
};
preceded_by_whitespace = original_char.is_whitespace();
converted_char
})
.collect()
}
#[cfg(test)]
mod tests {
mod render_markdown {
use super::super::render_markdown;
#[test]
fn it_can_keep_quotes_straight() {
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
}
#[test]
fn it_can_make_quotes_curly_except_when_they_are_in_code() {
let input = r#"
'one'
```
'two'
```
`'three'` 'four'"#;
let expected = r#"<p>one</p>
<pre><code>'two'
</code></pre>
<p><code>'three'</code> four</p>
"#;
assert_eq!(render_markdown(input, true), expected);
}
}
mod convert_quotes_to_curly {
use super::super::convert_quotes_to_curly;
#[test]
fn it_converts_single_quotes() {
assert_eq!(convert_quotes_to_curly("'one', 'two'"), "one, two");
}
#[test]
fn it_converts_double_quotes() {
assert_eq!(convert_quotes_to_curly(r#""one", "two""#), "“one”, “two”");
}
#[test]
fn it_treats_tab_as_whitespace() {
assert_eq!(convert_quotes_to_curly("\t'one'"), "\tone");
}
}
}

View File

@ -1,7 +1,6 @@
extern crate mdbook; extern crate mdbook;
extern crate tempdir; extern crate tempdir;
use std::path::Path;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
@ -15,8 +14,8 @@ fn do_not_overwrite_unspecified_config_values() {
let dir = TempDir::new("mdbook").expect("Could not create a temp dir"); let dir = TempDir::new("mdbook").expect("Could not create a temp dir");
let book = MDBook::new(dir.path()) let book = MDBook::new(dir.path())
.with_source(Path::new("bar")) .with_source("bar")
.with_destination(Path::new("baz")); .with_destination("baz");
assert_eq!(book.get_root(), dir.path()); assert_eq!(book.get_root(), dir.path());
assert_eq!(book.get_source(), dir.path().join("bar")); assert_eq!(book.get_source(), dir.path().join("bar"));

View File

@ -4,7 +4,7 @@ use mdbook::config::jsonconfig::JsonConfig;
use std::path::PathBuf; use std::path::PathBuf;
// Tests that the `title` key is correcly parsed in the TOML config // Tests that the `src` key is correctly parsed in the JSON config
#[test] #[test]
fn from_json_source() { fn from_json_source() {
let json = r#"{ let json = r#"{
@ -17,7 +17,7 @@ fn from_json_source() {
assert_eq!(config.get_source(), PathBuf::from("root/source")); assert_eq!(config.get_source(), PathBuf::from("root/source"));
} }
// Tests that the `title` key is correcly parsed in the TOML config // Tests that the `title` key is correctly parsed in the JSON config
#[test] #[test]
fn from_json_title() { fn from_json_title() {
let json = r#"{ let json = r#"{
@ -30,7 +30,7 @@ fn from_json_title() {
assert_eq!(config.get_title(), "Some title"); assert_eq!(config.get_title(), "Some title");
} }
// Tests that the `description` key is correcly parsed in the TOML config // Tests that the `description` key is correctly parsed in the JSON config
#[test] #[test]
fn from_json_description() { fn from_json_description() {
let json = r#"{ let json = r#"{
@ -43,7 +43,7 @@ fn from_json_description() {
assert_eq!(config.get_description(), "This is a description"); assert_eq!(config.get_description(), "This is a description");
} }
// Tests that the `author` key is correcly parsed in the TOML config // Tests that the `author` key is correctly parsed in the JSON config
#[test] #[test]
fn from_json_author() { fn from_json_author() {
let json = r#"{ let json = r#"{
@ -56,7 +56,7 @@ fn from_json_author() {
assert_eq!(config.get_authors(), &[String::from("John Doe")]); assert_eq!(config.get_authors(), &[String::from("John Doe")]);
} }
// Tests that the `output.html.destination` key is correcly parsed in the TOML config // Tests that the `dest` key is correctly parsed in the JSON config
#[test] #[test]
fn from_json_destination() { fn from_json_destination() {
let json = r#"{ let json = r#"{
@ -71,7 +71,7 @@ fn from_json_destination() {
assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook")); assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook"));
} }
// Tests that the `output.html.theme` key is correcly parsed in the TOML config // Tests that the `theme_path` key is correctly parsed in the JSON config
#[test] #[test]
fn from_json_output_html_theme() { fn from_json_output_html_theme() {
let json = r#"{ let json = r#"{

View File

@ -4,7 +4,7 @@ use mdbook::config::tomlconfig::TomlConfig;
use std::path::PathBuf; use std::path::PathBuf;
// Tests that the `title` key is correcly parsed in the TOML config // Tests that the `source` key is correctly parsed in the TOML config
#[test] #[test]
fn from_toml_source() { fn from_toml_source() {
let toml = r#"source = "source""#; let toml = r#"source = "source""#;
@ -15,7 +15,7 @@ fn from_toml_source() {
assert_eq!(config.get_source(), PathBuf::from("root/source")); assert_eq!(config.get_source(), PathBuf::from("root/source"));
} }
// Tests that the `title` key is correcly parsed in the TOML config // Tests that the `title` key is correctly parsed in the TOML config
#[test] #[test]
fn from_toml_title() { fn from_toml_title() {
let toml = r#"title = "Some title""#; let toml = r#"title = "Some title""#;
@ -26,7 +26,7 @@ fn from_toml_title() {
assert_eq!(config.get_title(), "Some title"); assert_eq!(config.get_title(), "Some title");
} }
// Tests that the `description` key is correcly parsed in the TOML config // Tests that the `description` key is correctly parsed in the TOML config
#[test] #[test]
fn from_toml_description() { fn from_toml_description() {
let toml = r#"description = "This is a description""#; let toml = r#"description = "This is a description""#;
@ -37,7 +37,7 @@ fn from_toml_description() {
assert_eq!(config.get_description(), "This is a description"); assert_eq!(config.get_description(), "This is a description");
} }
// Tests that the `author` key is correcly parsed in the TOML config // Tests that the `author` key is correctly parsed in the TOML config
#[test] #[test]
fn from_toml_author() { fn from_toml_author() {
let toml = r#"author = "John Doe""#; let toml = r#"author = "John Doe""#;
@ -48,7 +48,7 @@ fn from_toml_author() {
assert_eq!(config.get_authors(), &[String::from("John Doe")]); assert_eq!(config.get_authors(), &[String::from("John Doe")]);
} }
// Tests that the `authors` key is correcly parsed in the TOML config // Tests that the `authors` key is correctly parsed in the TOML config
#[test] #[test]
fn from_toml_authors() { fn from_toml_authors() {
let toml = r#"authors = ["John Doe", "Jane Doe"]"#; let toml = r#"authors = ["John Doe", "Jane Doe"]"#;
@ -59,7 +59,7 @@ fn from_toml_authors() {
assert_eq!(config.get_authors(), &[String::from("John Doe"), String::from("Jane Doe")]); assert_eq!(config.get_authors(), &[String::from("John Doe"), String::from("Jane Doe")]);
} }
// Tests that the `output.html.destination` key is correcly parsed in the TOML config // Tests that the `output.html.destination` key is correctly parsed in the TOML config
#[test] #[test]
fn from_toml_output_html_destination() { fn from_toml_output_html_destination() {
let toml = r#"[output.html] let toml = r#"[output.html]
@ -73,7 +73,7 @@ fn from_toml_output_html_destination() {
assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook")); assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook"));
} }
// Tests that the `output.html.theme` key is correcly parsed in the TOML config // Tests that the `output.html.theme` key is correctly parsed in the TOML config
#[test] #[test]
fn from_toml_output_html_theme() { fn from_toml_output_html_theme() {
let toml = r#"[output.html] let toml = r#"[output.html]
@ -87,7 +87,21 @@ fn from_toml_output_html_theme() {
assert_eq!(htmlconfig.get_theme().expect("the theme key was provided"), &PathBuf::from("root/theme")); assert_eq!(htmlconfig.get_theme().expect("the theme key was provided"), &PathBuf::from("root/theme"));
} }
// Tests that the `output.html.google-analytics` key is correcly parsed in the TOML config // Tests that the `output.html.curly-quotes` key is correctly parsed in the TOML config
#[test]
fn from_toml_output_html_curly_quotes() {
let toml = r#"[output.html]
curly-quotes = true"#;
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig");
assert_eq!(htmlconfig.get_curly_quotes(), true);
}
// Tests that the `output.html.google-analytics` key is correctly parsed in the TOML config
#[test] #[test]
fn from_toml_output_html_google_analytics() { fn from_toml_output_html_google_analytics() {
let toml = r#"[output.html] let toml = r#"[output.html]
@ -101,8 +115,7 @@ fn from_toml_output_html_google_analytics() {
assert_eq!(htmlconfig.get_google_analytics_id().expect("the google-analytics key was provided"), String::from("123456")); assert_eq!(htmlconfig.get_google_analytics_id().expect("the google-analytics key was provided"), String::from("123456"));
} }
// Tests that the `output.html.additional-css` key is correctly parsed in the TOML config
// Tests that the `output.html.additional-css` key is correcly parsed in the TOML config
#[test] #[test]
fn from_toml_output_html_additional_stylesheet() { fn from_toml_output_html_additional_stylesheet() {
let toml = r#"[output.html] let toml = r#"[output.html]
@ -116,7 +129,7 @@ fn from_toml_output_html_additional_stylesheet() {
assert_eq!(htmlconfig.get_additional_css(), &[PathBuf::from("root/custom.css"), PathBuf::from("root/two/custom.css")]); assert_eq!(htmlconfig.get_additional_css(), &[PathBuf::from("root/custom.css"), PathBuf::from("root/two/custom.css")]);
} }
// Tests that the `output.html.additional-js` key is correcly parsed in the TOML config // Tests that the `output.html.additional-js` key is correctly parsed in the TOML config
#[test] #[test]
fn from_toml_output_html_additional_scripts() { fn from_toml_output_html_additional_scripts() {
let toml = r#"[output.html] let toml = r#"[output.html]