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]
clap = "2.24"
handlebars = "0.26"
handlebars = "0.27"
serde = "1.0"
serde_derive = "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 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"]
```

View File

@ -9,11 +9,13 @@ 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
}

View File

@ -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<Error>> {
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<Error>> {
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<Error>> {
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,10 +225,35 @@ fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
Ok(())
}
// Watch command implementation
#[cfg(feature = "serve")]
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
mod serve {
extern crate iron;
extern crate staticfile;
extern crate ws;
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;
use {get_book_dir, open, trigger_on_change};
struct ErrorRecover;
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),
}
}
}
// Watch command implementation
pub fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
const RELOAD_COMMAND: &'static str = "reload";
let book_dir = get_book_dir(args);
@ -241,6 +269,10 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
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");
@ -271,9 +303,10 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
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();
@ -298,9 +331,9 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
});
Ok(())
}
}
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config()?;

View File

@ -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<P: Into<PathBuf>>(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<String> {
if let Some(htmlconfig) = self.config.get_html_config() {
return htmlconfig.get_google_analytics_id();

View File

@ -6,6 +6,7 @@ use super::tomlconfig::TomlHtmlConfig;
pub struct HtmlConfig {
destination: PathBuf,
theme: Option<PathBuf>,
curly_quotes: bool,
google_analytics: Option<String>,
additional_css: Vec<PathBuf>,
additional_js: Vec<PathBuf>,
@ -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<String> {
self.google_analytics.clone()
}

View File

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

View File

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

View File

@ -21,14 +21,14 @@
//! 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
//! }
@ -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<BookItems>` 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.

View File

@ -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

View File

@ -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::<Vec<BTreeMap<String, String>>>(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<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;
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::<Vec<BTreeMap<String, String>>>(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<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;
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 == &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'
let mut next_chapter = BTreeMap::new();
match item.get("name") {
Some(n) => {
debug!("[*]: Inserting title: {}", n);
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));
},
None => return Err(RenderError::new("No title found for chapter in JSON data")),
}
Ok(())
})?;
let link = Path::new(path).with_extension("html");
debug!("[*]: Inserting link: {:?}", link);
match link.to_str() {
Some(l) => {
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("\\", "/")));
},
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);
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;
}
}

View File

@ -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<P: AsRef<Path>>(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() + "<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(&replacement);
@ -60,7 +65,8 @@ struct Playpen {
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![];
for (i, _) in s.match_indices("{{#playpen") {
debug!("[*]: find_playpen");
@ -122,28 +128,28 @@ fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
#[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,

View File

@ -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::<Vec<BTreeMap<String, String>>>(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("<ul class=\"chapter\">".as_bytes())?;
// Decode json format
let decoded: Vec<BTreeMap<String, String>> = serde_json::from_str(&chapters.to_string()).unwrap();
rc.writer.write_all("<ul class=\"chapter\">".as_bytes())?;
let mut current_level = 1;
for item in decoded {
for item in chapters {
// Spacer
if item.get("spacer").is_some() {

View File

@ -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 {

View File

@ -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) {

View File

@ -76,7 +76,7 @@
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div class="page" tabindex="-1">
<div id="menu-bar" class="menu-bar">
<div class="left-buttons">
<i id="sidebar-toggle" class="fa fa-bars"></i>
@ -96,13 +96,13 @@
<!-- Mobile navigation buttons -->
{{#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>
</a>
{{/previous}}
{{#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>
</a>
{{/next}}

View File

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

View File

@ -3,7 +3,7 @@
text-align: center
text-decoration: none
position: absolute
position: fixed
top: 50px /* Height of menu-bar */
bottom: 0
margin: 0
@ -19,5 +19,20 @@
.mobile-nav-chapters { display: none }
.nav-chapters:hover { text-decoration: none }
.previous { left: 0 }
.next { right: 15px }
.previous {
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'
.page-wrapper {
position: absolute
overflow-y: auto
left: $sidebar-width + 15px
right: 0
top: 0
bottom: 0
padding-left: $sidebar-width
box-sizing: border-box
-webkit-overflow-scrolling: touch
min-height: 100%
// Animation: slide away
transition: left 0.5s
transition: padding-left 0.5s
@media only screen and (max-width: 1060px) {
left: 15px;
padding-right: 15px;
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
padding-left: 0
}
}
.sidebar-hidden .page-wrapper {
left: 15px
padding-left: 0
}
.sidebar-visible .page-wrapper {
left: $sidebar-width + 15px
padding-left: $sidebar-width
}
.page {
position: absolute
top: 0
right: 0
left: 0
bottom: 0
padding-right: 15px
overflow-y: auto
outline: 0
padding: 0 $page-padding
}
.content {

View File

@ -1,7 +1,7 @@
@require 'variables'
.sidebar {
position: absolute
position: fixed
left: 0
top: 0
bottom: 0
@ -15,7 +15,7 @@
// Animation: slide away
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
}

View File

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

View File

@ -1 +1,3 @@
$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::io::{self, Read};
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
/// directory from where the path starts.
///
/// ```ignore
/// let mut path = Path::new("some/relative/path");
///
/// println!("{}", path_to_root(&path));
/// ```
///
/// **Outputs**
///
/// ```text
/// "../../"
/// ```rust
/// # extern crate mdbook;
/// #
/// # use std::path::Path;
/// # use mdbook::utils::fs::path_to_root;
/// #
/// # fn main() {
/// let path = Path::new("some/relative/path");
/// assert_eq!(path_to_root(path), "../../");
/// # }
/// ```
///
/// **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)
/// 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");
// Remove filename and add "../" for every directory
path.to_path_buf()
path.into()
.parent()
.expect("")
.components()

View File

@ -1,13 +1,14 @@
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
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 opts = Options::empty();
@ -15,6 +16,107 @@ pub fn render_markdown(text: &str) -> String {
opts.insert(OPTION_ENABLE_FOOTNOTES);
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
}
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 tempdir;
use std::path::Path;
use std::fs::File;
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 book = MDBook::new(dir.path())
.with_source(Path::new("bar"))
.with_destination(Path::new("baz"));
.with_source("bar")
.with_destination("baz");
assert_eq!(book.get_root(), dir.path());
assert_eq!(book.get_source(), dir.path().join("bar"));

View File

@ -4,7 +4,7 @@ use mdbook::config::jsonconfig::JsonConfig;
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]
fn from_json_source() {
let json = r#"{
@ -17,7 +17,7 @@ fn from_json_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]
fn from_json_title() {
let json = r#"{
@ -30,7 +30,7 @@ fn from_json_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]
fn from_json_description() {
let json = r#"{
@ -43,7 +43,7 @@ fn from_json_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]
fn from_json_author() {
let json = r#"{
@ -56,7 +56,7 @@ fn from_json_author() {
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]
fn from_json_destination() {
let json = r#"{
@ -71,7 +71,7 @@ fn from_json_destination() {
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]
fn from_json_output_html_theme() {
let json = r#"{

View File

@ -4,7 +4,7 @@ use mdbook::config::tomlconfig::TomlConfig;
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]
fn from_toml_source() {
let toml = r#"source = "source""#;
@ -15,7 +15,7 @@ fn from_toml_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]
fn from_toml_title() {
let toml = r#"title = "Some title""#;
@ -26,7 +26,7 @@ fn from_toml_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]
fn from_toml_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");
}
// 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]
fn from_toml_author() {
let toml = r#"author = "John Doe""#;
@ -48,7 +48,7 @@ fn from_toml_author() {
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]
fn from_toml_authors() {
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")]);
}
// 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]
fn from_toml_output_html_destination() {
let toml = r#"[output.html]
@ -73,7 +73,7 @@ fn from_toml_output_html_destination() {
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]
fn from_toml_output_html_theme() {
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"));
}
// 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]
fn from_toml_output_html_google_analytics() {
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"));
}
// Tests that the `output.html.additional-css` key is correcly parsed in the TOML config
// Tests that the `output.html.additional-css` key is correctly parsed in the TOML config
#[test]
fn from_toml_output_html_additional_stylesheet() {
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")]);
}
// 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]
fn from_toml_output_html_additional_scripts() {
let toml = r#"[output.html]