diff --git a/Cargo.toml b/Cargo.toml index dfee0600..8c588564 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mdbook" -version = "0.0.3" +version = "0.0.4" authors = ["Mathieu David "] description = "create books from markdown files (like Gitbook)" documentation = "http://azerupi.github.io/mdBook/index.html" @@ -15,11 +15,11 @@ exclude = [ ] [dependencies] -clap = "*" -handlebars = "*" -rustc-serialize = "*" -pulldown-cmark = "*" -crossbeam = "^0.1.5" +clap = "~1.5.3" +handlebars = "~0.12.0" +rustc-serialize = "~0.3.16" +pulldown-cmark = "~0.0.3" + # Watch feature [dependencies.notify] @@ -30,9 +30,13 @@ optional = true time = "^0.1.33" optional = true +[dependencies.crossbeam] +time = "^0.2.0" +optional = true + # Tests [dev-dependencies] -tempdir = "*" +tempdir = "~0.3.4" [features] @@ -40,7 +44,7 @@ default = ["output", "watch"] debug = [] output = [] regenerate-css = [] -watch = ["notify", "time"] +watch = ["notify", "time", "crossbeam"] [[bin]] doc = false diff --git a/README.md b/README.md index 67963d95..ed5880e7 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,10 @@ To have an idea of what a rendered book looks like,take a look at the [**Documen ## Installation ``` -git clone --depth=1 https://github.com/azerupi/mdBook.git -cd mdBook -cargo build --release +cargo install mdbook ``` -The executable `mdbook` will be in the `./target/release` folder, this should be added to the path. - -If you want to regenerate the css (stylesheet), make sure that you installed `stylus` and `nib` from `npm` because it is used to compile the stylesheets +If you want to regenerate the css (stylesheet), clone the git repo locally and make sure that you installed `stylus` and `nib` from `npm` because it is used to compile the stylesheets Install [node.js](https://nodejs.org/en/) diff --git a/src/bin/mdbook.rs b/src/bin/mdbook.rs index 71cbf1d9..50b9a2e2 100644 --- a/src/bin/mdbook.rs +++ b/src/bin/mdbook.rs @@ -1,3 +1,4 @@ +#[macro_use] extern crate mdbook; #[macro_use] extern crate clap; @@ -49,6 +50,8 @@ fn main() { .subcommand(SubCommand::with_name("watch") .about("Watch the files for changes") .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'")) + .subcommand(SubCommand::with_name("test") + .about("Test that code samples compile")) .get_matches(); // Check which subcomamnd the user ran... @@ -57,6 +60,7 @@ fn main() { ("build", Some(sub_matches)) => build(sub_matches), #[cfg(feature = "watch")] ("watch", Some(sub_matches)) => watch(sub_matches), + ("test", Some(sub_matches)) => test(sub_matches), (_, _) => unreachable!() }; @@ -142,8 +146,17 @@ fn watch(args: &ArgMatches) -> Result<(), Box> { match w { Ok(mut watcher) => { - watcher.watch(book.get_src()).unwrap(); - watcher.watch(book_dir.join("book.json")).unwrap(); + // Add the source directory to the watcher + if let Err(e) = watcher.watch(book.get_src()) { + println!("Error while watching {:?}:\n {:?}", book.get_src(), e); + ::std::process::exit(0); + }; + + // Add the book.json file to the watcher if it exists, because it's not + // located in the source directory + if let Err(_) = watcher.watch(book_dir.join("book.json")) { + // do nothing if book.json is not found + } let previous_time = time::get_time().sec; @@ -184,11 +197,22 @@ fn watch(args: &ArgMatches) -> Result<(), Box> { } } + Ok(()) + } + + + +fn test(args: &ArgMatches) -> Result<(), Box> { + let book_dir = get_book_dir(args); + let mut book = MDBook::new(&book_dir).read_config(); + + try!(book.test()); + Ok(()) } -// Helper function that returns the right path if either a relative or absolute path is passed + fn get_book_dir(args: &ArgMatches) -> PathBuf { if let Some(dir) = args.value_of("dir") { // Check if path is relative from current dir, or absolute... diff --git a/src/book/mdbook.rs b/src/book/mdbook.rs index e2ee2581..02430414 100644 --- a/src/book/mdbook.rs +++ b/src/book/mdbook.rs @@ -1,7 +1,10 @@ use std::path::{Path, PathBuf}; use std::fs::{self, File}; -use std::io::Write; use std::error::Error; +use std::io; +use std::io::Write; +use std::io::ErrorKind; +use std::process::Command; use {BookConfig, BookItem, theme, parse, utils}; use book::BookItems; @@ -257,6 +260,39 @@ impl MDBook { self } + pub fn test(&mut self) -> Result<(), Box> { + // read in the chapters + try!(self.parse_summary()); + for item in self.iter() { + + match *item { + BookItem::Chapter(_, ref ch) => { + if ch.path != PathBuf::new() { + + let path = self.get_src().join(&ch.path); + + println!("[*]: Testing file: {:?}", path); + + let output_result = Command::new("rustdoc") + .arg(&path) + .arg("--test") + .output(); + let output = try!(output_result); + + if !output.status.success() { + return Err(Box::new(io::Error::new(ErrorKind::Other, format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr)))) as Box); + } + } + } + _ => {} + } + } + Ok(()) + } + pub fn set_dest(mut self, dest: &Path) -> Self { // Handle absolute and relative paths diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 6055b27f..f45079cb 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -114,7 +114,7 @@ impl Renderer for HtmlHandlebars { // This could cause a problem when someone displays code containing // on the front page, however this case should be very very rare... - content = content.lines().filter(|line| !line.contains("> = match json::decode(&chapters.to_string()) { Ok(data) => data, - Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data"}), + Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data".to_owned()}), }; let mut previous: Option> = None; @@ -55,7 +55,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext }, None => { debug!("[*]: No title found for chapter"); - return Err(RenderError{ desc: "No title found for chapter in JSON data" }) + return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() }) } }; @@ -68,10 +68,10 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext match path.to_str() { Some(p) => { previous_chapter.insert("link".to_owned(), p.to_json()); }, - None => return Err(RenderError{ desc: "Link could not be converted to str" }) + None => return Err(RenderError{ desc: "Link could not be converted to str".to_owned() }) } }, - None => return Err(RenderError{ desc: "No path found for chapter in JSON data" }) + None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() }) } debug!("[*]: Inject in context"); @@ -84,7 +84,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext Some(t) => { try!(t.render(&updated_context, r, rc)); }, - None => return Err(RenderError{ desc: "Error with the handlebars template" }) + None => return Err(RenderError{ desc: "Error with the handlebars template".to_owned() }) } } @@ -124,7 +124,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> // Decode json format let decoded: Vec> = match json::decode(&chapters.to_string()) { Ok(data) => data, - Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data"}), + Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data".to_owned() }), }; let mut previous: Option> = None; @@ -140,7 +140,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> let previous_path = match previous.get("path") { Some(p) => p, - None => return Err(RenderError{ desc: "No path found for chapter in JSON data"}) + None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() }) }; if previous_path == ¤t { @@ -155,7 +155,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> debug!("[*]: Inserting title: {}", n); next_chapter.insert("title".to_owned(), n.to_json()); } - None => return Err(RenderError{ desc: "No title found for chapter in JSON data"}) + None => return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() }) } @@ -164,7 +164,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> match link.to_str() { Some(l) => { next_chapter.insert("link".to_owned(), l.to_json()); }, - None => return Err(RenderError{ desc: "Link could not converted to str"}) + None => return Err(RenderError{ desc: "Link could not converted to str".to_owned() }) } debug!("[*]: Inject in context"); @@ -178,7 +178,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Some(t) => { try!(t.render(&updated_context, r, rc)); }, - None => return Err(RenderError{ desc: "Error with the handlebars template" }) + None => return Err(RenderError{ desc: "Error with the handlebars template".to_owned() }) } break diff --git a/src/theme/book.css b/src/theme/book.css index 102b1762..108e39e0 100644 --- a/src/theme/book.css +++ b/src/theme/book.css @@ -9,6 +9,9 @@ body { .right { float: right; } +.hidden { + display: none; +} h2, h3 { margin-top: 2.5em; @@ -222,7 +225,7 @@ h3 { left: 0; } .next { - right: 0; + right: 15px; } .theme-popup { position: fixed; @@ -270,9 +273,30 @@ h3 { } } .light { +/* Inline code */ color: #333; background-color: #fff; } +.light :not(pre) > .hljs { + display: inline-block; + vertical-align: middle; + padding: 0.1em 0.3em; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.light pre { + position: relative; +} +.light pre > i { + position: absolute; + right: 5px; + top: 5px; + color: #364149; + cursor: pointer; +} +.light pre > i :hover { + color: #008cff; +} .light .sidebar { background-color: #fafafa; color: #364149; @@ -307,7 +331,7 @@ h3 { .light .mobile-nav-chapters { background-color: #fafafa; } -.light .content a { +.light .content a:link { color: #4183c4; } .light .theme-popup { @@ -318,9 +342,30 @@ h3 { background-color: #e6e6e6; } .coal { +/* Inline code */ color: #98a3ad; background-color: #141617; } +.coal :not(pre) > .hljs { + display: inline-block; + vertical-align: middle; + padding: 0.1em 0.3em; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.coal pre { + position: relative; +} +.coal pre > i { + position: absolute; + right: 5px; + top: 5px; + color: #a1adb8; + cursor: pointer; +} +.coal pre > i :hover { + color: #3473ad; +} .coal .sidebar { background-color: #292c2f; color: #a1adb8; @@ -355,7 +400,7 @@ h3 { .coal .mobile-nav-chapters { background-color: #292c2f; } -.coal .content a { +.coal .content a:link { color: #2b79a2; } .coal .theme-popup { @@ -366,9 +411,30 @@ h3 { background-color: #1f2124; } .navy { +/* Inline code */ color: #bcbdd0; background-color: #161923; } +.navy :not(pre) > .hljs { + display: inline-block; + vertical-align: middle; + padding: 0.1em 0.3em; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.navy pre { + position: relative; +} +.navy pre > i { + position: absolute; + right: 5px; + top: 5px; + color: #c8c9db; + cursor: pointer; +} +.navy pre > i :hover { + color: #2b79a2; +} .navy .sidebar { background-color: #282d3f; color: #c8c9db; @@ -403,7 +469,7 @@ h3 { .navy .mobile-nav-chapters { background-color: #282d3f; } -.navy .content a { +.navy .content a:link { color: #2b79a2; } .navy .theme-popup { @@ -414,9 +480,30 @@ h3 { background-color: #282e40; } .rust { +/* Inline code */ color: #262625; background-color: #e1e1db; } +.rust :not(pre) > .hljs { + display: inline-block; + vertical-align: middle; + padding: 0.1em 0.3em; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.rust pre { + position: relative; +} +.rust pre > i { + position: absolute; + right: 5px; + top: 5px; + color: #c8c9db; + cursor: pointer; +} +.rust pre > i :hover { + color: #e69f67; +} .rust .sidebar { background-color: #3b2e2a; color: #c8c9db; @@ -451,7 +538,7 @@ h3 { .rust .mobile-nav-chapters { background-color: #3b2e2a; } -.rust .content a { +.rust .content a:link { color: #2b79a2; } .rust .theme-popup { diff --git a/src/theme/book.js b/src/theme/book.js index 0754fee7..58d8eed9 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -8,7 +8,7 @@ $( document ).ready(function() { // Set theme var theme = localStorage.getItem('theme'); - if (theme == null) { theme = 'light'; } + if (theme === null) { theme = 'light'; } set_theme(theme); @@ -28,6 +28,15 @@ $( document ).ready(function() { var html = $("html"); var sidebar = $("#sidebar"); var page_wrapper = $("#page-wrapper"); + var content = $("#content"); + + + // Add anchors for all content headers + content.find("h1, h2, h3, h4, h5").wrap(function(){ + var wrapper = $(""); + wrapper.attr("name", $(this).text()); + return wrapper; + }); // Toggle sidebar @@ -50,6 +59,13 @@ $( document ).ready(function() { }); + // Scroll sidebar to current active section + var activeSection = sidebar.find(".active"); + if(activeSection.length) { + sidebar.scrollTop(activeSection.offset().top); + } + + // Print button $("#print-button").click(function(){ var printWindow = window.open("print.html"); @@ -77,7 +93,7 @@ $( document ).ready(function() { $('.theme').click(function(){ var theme = $(this).attr('id'); - set_theme(theme) + set_theme(theme); }); } @@ -96,4 +112,54 @@ $( document ).ready(function() { $('body').removeClass().addClass(theme); } + + + // Hide Rust code lines prepended with a specific character + var hiding_character = "#"; + + $("code.language-rust").each(function(i, block){ + + // hide lines + var lines = $(this).html().split("\n"); + var first_non_hidden_line = false; + var lines_hidden = false; + + for(var n = 0; n < lines.length; n++){ + if($.trim(lines[n])[0] == hiding_character){ + if(first_non_hidden_line){ + lines[n] = "" + "\n" + lines[n].substr(1) + ""; + } + else { + lines[n] = "" + lines[n].substr(1) + "\n" + ""; + } + lines_hidden = true; + } + else if(first_non_hidden_line) { + lines[n] = "\n" + lines[n]; + } + else { + first_non_hidden_line = true; + } + } + $(this).html(lines.join("")); + + // If no lines were hidden, return + if(!lines_hidden) { return; } + + // add expand button + $(this).parent().prepend(""); + + $(this).parent().find("i").click(function(e){ + if( $(this).hasClass("fa-expand") ) { + $(this).removeClass("fa-expand").addClass("fa-compress"); + $(this).parent().find("span.hidden").removeClass("hidden").addClass("unhidden"); + } + else { + $(this).removeClass("fa-compress").addClass("fa-expand"); + $(this).parent().find("span.unhidden").removeClass("unhidden").addClass("hidden"); + } + }); + }); + + }); diff --git a/src/theme/highlight.css b/src/theme/highlight.css index 616dba0d..61681a2b 100644 --- a/src/theme/highlight.css +++ b/src/theme/highlight.css @@ -10,14 +10,6 @@ -webkit-text-size-adjust: none; } -/* Inline code */ -:not(pre) > .hljs { - display: inline-block; - vertical-align: middle; - padding: 0.1em 0.3em; - border-radius: 3px; -} - /* Atelier-Dune Comment */ .hljs-comment { diff --git a/src/theme/stylus/general.styl b/src/theme/stylus/general.styl index 13f616f6..e798e8e0 100644 --- a/src/theme/stylus/general.styl +++ b/src/theme/stylus/general.styl @@ -11,4 +11,8 @@ html, body { float: right } +.hidden { + display: none; +} + h2, h3 { margin-top: 2.5em } diff --git a/src/theme/stylus/nav-icons.styl b/src/theme/stylus/nav-icons.styl index 2ffb93f7..211db682 100644 --- a/src/theme/stylus/nav-icons.styl +++ b/src/theme/stylus/nav-icons.styl @@ -20,4 +20,4 @@ .mobile-nav-chapters { display: none } .nav-chapters:hover { text-decoration: none } .previous { left: 0 } -.next { right: 0 } +.next { right: 15px } diff --git a/src/theme/stylus/themes/base.styl b/src/theme/stylus/themes/base.styl index fdbcc9dc..0a87b587 100644 --- a/src/theme/stylus/themes/base.styl +++ b/src/theme/stylus/themes/base.styl @@ -1,4 +1,28 @@ .{unquote($theme-name)} { + /* Inline code */ + :not(pre) > .hljs { + display: inline-block; + vertical-align: middle; + padding: 0.1em 0.3em; + border-radius: 3px; + } + + pre { + position: relative; + } + pre > i { + position: absolute; + right: 5px; + top: 5px; + + color: $sidebar-fg; + cursor: pointer; + + :hover { + color: $sidebar-active; + } + } + color: $fg background-color: $bg @@ -43,7 +67,7 @@ background-color: $sidebar-bg } - .content a { + .content a:link { color: $links }