Merge branch 'upstream/master' into refactor-hbs-renderer
Notably, this takes into account the curly-quotes pull request (#305)
This commit is contained in:
commit
c3dfabd5a2
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
```
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -39,5 +39,3 @@ impl JsonConfig {
|
|||
return Ok(config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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>>,
|
||||
}
|
||||
|
|
25
src/lib.rs
25
src/lib.rs
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 == ¤t {
|
||||
|
||||
|
@ -157,41 +128,33 @@ pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), R
|
|||
// Create new BTreeMap to extend the context: 'title' and 'link'
|
||||
let mut next_chapter = BTreeMap::new();
|
||||
|
||||
match item.get("name") {
|
||||
Some(n) => {
|
||||
debug!("[*]: Inserting title: {}", n);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
padding: 2px 10px
|
||||
line-height: 25px
|
||||
white-space: nowrap
|
||||
cursor: pointer
|
||||
|
||||
&:hover:first-child,
|
||||
&:hover:last-child {
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
$sidebar-width = 300px
|
||||
$page-padding = 15px
|
||||
$max-page-width-with-hidden-sidebar = 1060px
|
||||
|
|
|
@ -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()
|
||||
|
|
108
src/utils/mod.rs
108
src/utils/mod.rs
|
@ -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'"), "\t‘one’");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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#"{
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue