Merge branch 'master' into watch-command

This commit is contained in:
Mathieu David 2015-12-29 13:40:13 +01:00
commit b40688c880
12 changed files with 279 additions and 46 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mdbook" name = "mdbook"
version = "0.0.3" version = "0.0.4"
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"] authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
description = "create books from markdown files (like Gitbook)" description = "create books from markdown files (like Gitbook)"
documentation = "http://azerupi.github.io/mdBook/index.html" documentation = "http://azerupi.github.io/mdBook/index.html"
@ -15,11 +15,11 @@ exclude = [
] ]
[dependencies] [dependencies]
clap = "*" clap = "~1.5.3"
handlebars = "*" handlebars = "~0.12.0"
rustc-serialize = "*" rustc-serialize = "~0.3.16"
pulldown-cmark = "*" pulldown-cmark = "~0.0.3"
crossbeam = "^0.1.5"
# Watch feature # Watch feature
[dependencies.notify] [dependencies.notify]
@ -30,9 +30,13 @@ optional = true
time = "^0.1.33" time = "^0.1.33"
optional = true optional = true
[dependencies.crossbeam]
time = "^0.2.0"
optional = true
# Tests # Tests
[dev-dependencies] [dev-dependencies]
tempdir = "*" tempdir = "~0.3.4"
[features] [features]
@ -40,7 +44,7 @@ default = ["output", "watch"]
debug = [] debug = []
output = [] output = []
regenerate-css = [] regenerate-css = []
watch = ["notify", "time"] watch = ["notify", "time", "crossbeam"]
[[bin]] [[bin]]
doc = false doc = false

View File

@ -13,14 +13,10 @@ To have an idea of what a rendered book looks like,take a look at the [**Documen
## Installation ## Installation
``` ```
git clone --depth=1 https://github.com/azerupi/mdBook.git cargo install mdbook
cd mdBook
cargo build --release
``` ```
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), clone the git repo locally and 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), 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/) Install [node.js](https://nodejs.org/en/)

View File

@ -1,3 +1,4 @@
#[macro_use]
extern crate mdbook; extern crate mdbook;
#[macro_use] #[macro_use]
extern crate clap; extern crate clap;
@ -49,6 +50,8 @@ fn main() {
.subcommand(SubCommand::with_name("watch") .subcommand(SubCommand::with_name("watch")
.about("Watch the files for changes") .about("Watch the files for changes")
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'")) .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(); .get_matches();
// Check which subcomamnd the user ran... // Check which subcomamnd the user ran...
@ -57,6 +60,7 @@ fn main() {
("build", Some(sub_matches)) => build(sub_matches), ("build", Some(sub_matches)) => build(sub_matches),
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
("watch", Some(sub_matches)) => watch(sub_matches), ("watch", Some(sub_matches)) => watch(sub_matches),
("test", Some(sub_matches)) => test(sub_matches),
(_, _) => unreachable!() (_, _) => unreachable!()
}; };
@ -142,8 +146,17 @@ fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
match w { match w {
Ok(mut watcher) => { Ok(mut watcher) => {
watcher.watch(book.get_src()).unwrap(); // Add the source directory to the watcher
watcher.watch(book_dir.join("book.json")).unwrap(); 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; let previous_time = time::get_time().sec;
@ -188,7 +201,18 @@ fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
} }
// Helper function that returns the right path if either a relative or absolute path is passed
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config();
try!(book.test());
Ok(())
}
fn get_book_dir(args: &ArgMatches) -> PathBuf { fn get_book_dir(args: &ArgMatches) -> PathBuf {
if let Some(dir) = args.value_of("dir") { if let Some(dir) = args.value_of("dir") {
// Check if path is relative from current dir, or absolute... // Check if path is relative from current dir, or absolute...

View File

@ -1,7 +1,10 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write;
use std::error::Error; 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 {BookConfig, BookItem, theme, parse, utils};
use book::BookItems; use book::BookItems;
@ -257,6 +260,39 @@ impl MDBook {
self self
} }
pub fn test(&mut self) -> Result<(), Box<Error>> {
// 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<Error>);
}
}
}
_ => {}
}
}
Ok(())
}
pub fn set_dest(mut self, dest: &Path) -> Self { pub fn set_dest(mut self, dest: &Path) -> Self {
// Handle absolute and relative paths // Handle absolute and relative paths

View File

@ -114,7 +114,7 @@ impl Renderer for HtmlHandlebars {
// This could cause a problem when someone displays code containing <base href=...> // This could cause a problem when someone displays code containing <base href=...>
// on the front page, however this case should be very very rare... // on the front page, however this case should be very very rare...
content = content.lines().filter(|line| !line.contains("<base href=")).collect(); content = content.lines().filter(|line| !line.contains("<base href=")).collect::<Vec<&str>>().join("\n");
try!(index_file.write_all(content.as_bytes())); try!(index_file.write_all(content.as_bytes()));

View File

@ -27,7 +27,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
// Decode json format // Decode json format
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) { let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
Ok(data) => data, 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<BTreeMap<String, String>> = None; let mut previous: Option<BTreeMap<String, String>> = None;
@ -55,7 +55,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
}, },
None => { None => {
debug!("[*]: No title found for chapter"); 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() { match path.to_str() {
Some(p) => { previous_chapter.insert("link".to_owned(), p.to_json()); }, 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"); debug!("[*]: Inject in context");
@ -84,7 +84,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
Some(t) => { Some(t) => {
try!(t.render(&updated_context, r, rc)); 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 // Decode json format
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) { let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
Ok(data) => data, 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<BTreeMap<String, String>> = None; let mut previous: Option<BTreeMap<String, String>> = None;
@ -140,7 +140,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
let previous_path = match previous.get("path") { let previous_path = match previous.get("path") {
Some(p) => p, 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 == &current { if previous_path == &current {
@ -155,7 +155,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
debug!("[*]: Inserting title: {}", n); debug!("[*]: Inserting title: {}", n);
next_chapter.insert("title".to_owned(), n.to_json()); 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() { match link.to_str() {
Some(l) => { next_chapter.insert("link".to_owned(), l.to_json()); }, 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"); debug!("[*]: Inject in context");
@ -178,7 +178,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
Some(t) => { Some(t) => {
try!(t.render(&updated_context, r, rc)); 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 break

View File

@ -9,6 +9,9 @@ body {
.right { .right {
float: right; float: right;
} }
.hidden {
display: none;
}
h2, h2,
h3 { h3 {
margin-top: 2.5em; margin-top: 2.5em;
@ -222,7 +225,7 @@ h3 {
left: 0; left: 0;
} }
.next { .next {
right: 0; right: 15px;
} }
.theme-popup { .theme-popup {
position: fixed; position: fixed;
@ -270,9 +273,30 @@ h3 {
} }
} }
.light { .light {
/* Inline code */
color: #333; color: #333;
background-color: #fff; 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 { .light .sidebar {
background-color: #fafafa; background-color: #fafafa;
color: #364149; color: #364149;
@ -307,7 +331,7 @@ h3 {
.light .mobile-nav-chapters { .light .mobile-nav-chapters {
background-color: #fafafa; background-color: #fafafa;
} }
.light .content a { .light .content a:link {
color: #4183c4; color: #4183c4;
} }
.light .theme-popup { .light .theme-popup {
@ -318,9 +342,30 @@ h3 {
background-color: #e6e6e6; background-color: #e6e6e6;
} }
.coal { .coal {
/* Inline code */
color: #98a3ad; color: #98a3ad;
background-color: #141617; 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 { .coal .sidebar {
background-color: #292c2f; background-color: #292c2f;
color: #a1adb8; color: #a1adb8;
@ -355,7 +400,7 @@ h3 {
.coal .mobile-nav-chapters { .coal .mobile-nav-chapters {
background-color: #292c2f; background-color: #292c2f;
} }
.coal .content a { .coal .content a:link {
color: #2b79a2; color: #2b79a2;
} }
.coal .theme-popup { .coal .theme-popup {
@ -366,9 +411,30 @@ h3 {
background-color: #1f2124; background-color: #1f2124;
} }
.navy { .navy {
/* Inline code */
color: #bcbdd0; color: #bcbdd0;
background-color: #161923; 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 { .navy .sidebar {
background-color: #282d3f; background-color: #282d3f;
color: #c8c9db; color: #c8c9db;
@ -403,7 +469,7 @@ h3 {
.navy .mobile-nav-chapters { .navy .mobile-nav-chapters {
background-color: #282d3f; background-color: #282d3f;
} }
.navy .content a { .navy .content a:link {
color: #2b79a2; color: #2b79a2;
} }
.navy .theme-popup { .navy .theme-popup {
@ -414,9 +480,30 @@ h3 {
background-color: #282e40; background-color: #282e40;
} }
.rust { .rust {
/* Inline code */
color: #262625; color: #262625;
background-color: #e1e1db; 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 { .rust .sidebar {
background-color: #3b2e2a; background-color: #3b2e2a;
color: #c8c9db; color: #c8c9db;
@ -451,7 +538,7 @@ h3 {
.rust .mobile-nav-chapters { .rust .mobile-nav-chapters {
background-color: #3b2e2a; background-color: #3b2e2a;
} }
.rust .content a { .rust .content a:link {
color: #2b79a2; color: #2b79a2;
} }
.rust .theme-popup { .rust .theme-popup {

View File

@ -8,7 +8,7 @@ $( document ).ready(function() {
// Set theme // Set theme
var theme = localStorage.getItem('theme'); var theme = localStorage.getItem('theme');
if (theme == null) { theme = 'light'; } if (theme === null) { theme = 'light'; }
set_theme(theme); set_theme(theme);
@ -28,6 +28,15 @@ $( document ).ready(function() {
var html = $("html"); var html = $("html");
var sidebar = $("#sidebar"); var sidebar = $("#sidebar");
var page_wrapper = $("#page-wrapper"); 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 = $("<a>");
wrapper.attr("name", $(this).text());
return wrapper;
});
// Toggle sidebar // 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
$("#print-button").click(function(){ $("#print-button").click(function(){
var printWindow = window.open("print.html"); var printWindow = window.open("print.html");
@ -77,7 +93,7 @@ $( document ).ready(function() {
$('.theme').click(function(){ $('.theme').click(function(){
var theme = $(this).attr('id'); var theme = $(this).attr('id');
set_theme(theme) set_theme(theme);
}); });
} }
@ -96,4 +112,54 @@ $( document ).ready(function() {
$('body').removeClass().addClass(theme); $('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] = "<span class=\"hidden\">" + "\n" + lines[n].substr(1) + "</span>";
}
else {
lines[n] = "<span class=\"hidden\">" + lines[n].substr(1) + "\n" + "</span>";
}
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("<i class=\"fa fa-expand\"></i>");
$(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");
}
});
});
}); });

View File

@ -10,14 +10,6 @@
-webkit-text-size-adjust: none; -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 */ /* Atelier-Dune Comment */
.hljs-comment { .hljs-comment {

View File

@ -11,4 +11,8 @@ html, body {
float: right float: right
} }
.hidden {
display: none;
}
h2, h3 { margin-top: 2.5em } h2, h3 { margin-top: 2.5em }

View File

@ -20,4 +20,4 @@
.mobile-nav-chapters { display: none } .mobile-nav-chapters { display: none }
.nav-chapters:hover { text-decoration: none } .nav-chapters:hover { text-decoration: none }
.previous { left: 0 } .previous { left: 0 }
.next { right: 0 } .next { right: 15px }

View File

@ -1,4 +1,28 @@
.{unquote($theme-name)} { .{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 color: $fg
background-color: $bg background-color: $bg
@ -43,7 +67,7 @@
background-color: $sidebar-bg background-color: $sidebar-bg
} }
.content a { .content a:link {
color: $links color: $links
} }