about summary refs log tree commit diff
path: root/src/rustbook
diff options
context:
space:
mode:
authorAlex Crichton <alex@alexcrichton.com>2015-01-08 09:21:57 -0800
committerAlex Crichton <alex@alexcrichton.com>2015-01-08 09:21:57 -0800
commitcdc75bc34c3a281bf0c49c3a8a31bbe5f8d229d6 (patch)
treeadede39989a0cf04520bfc90a0b54cefdbfd5e18 /src/rustbook
parent2f99a41fe1a27a48e96bc2616ec9faa6de924386 (diff)
parent16a6ebd1f60871464c731306aa9007aab30f0dbf (diff)
downloadrust-cdc75bc34c3a281bf0c49c3a8a31bbe5f8d229d6.tar.gz
rust-cdc75bc34c3a281bf0c49c3a8a31bbe5f8d229d6.zip
rollup merge of #19897: steveklabnik/trpl
An updated version of https://github.com/rust-lang/rust/pull/19461

This version vendors aturon/rust-book@731f7bf and builds it when building the docs. This is almost great, except my `make`-foo is poor, so I have my own personal paths in `mk/docs.mk`. How should I best get around that?

/cc @brson
Diffstat (limited to 'src/rustbook')
-rw-r--r--src/rustbook/book.rs166
-rwxr-xr-xsrc/rustbook/build.rs191
-rw-r--r--src/rustbook/css.rs72
-rw-r--r--src/rustbook/error.rs76
-rw-r--r--src/rustbook/help.rs46
-rwxr-xr-xsrc/rustbook/main.rs74
-rw-r--r--src/rustbook/serve.rs36
-rw-r--r--src/rustbook/subcommand.rs44
-rw-r--r--src/rustbook/term.rs31
-rw-r--r--src/rustbook/test.rs75
10 files changed, 811 insertions, 0 deletions
diff --git a/src/rustbook/book.rs b/src/rustbook/book.rs
new file mode 100644
index 00000000000..45a864e3378
--- /dev/null
+++ b/src/rustbook/book.rs
@@ -0,0 +1,166 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Basic data structures for representing a book.
+
+use std::io::BufferedReader;
+use std::iter;
+use std::iter::AdditiveIterator;
+use regex::Regex;
+
+pub struct BookItem {
+    pub title: String,
+    pub path: Path,
+    pub path_to_root: Path,
+    pub children: Vec<BookItem>,
+}
+
+pub struct Book {
+    pub chapters: Vec<BookItem>,
+}
+
+/// A depth-first iterator over a book.
+pub struct BookItems<'a> {
+    cur_items: &'a [BookItem],
+    cur_idx: uint,
+    stack: Vec<(&'a [BookItem], uint)>,
+}
+
+impl<'a> Iterator for BookItems<'a> {
+    type Item = (String, &'a BookItem);
+
+    fn next(&mut self) -> Option<(String, &'a BookItem)> {
+        loop {
+            if self.cur_idx >= self.cur_items.len() {
+                match self.stack.pop() {
+                    None => return None,
+                    Some((parent_items, parent_idx)) => {
+                        self.cur_items = parent_items;
+                        self.cur_idx = parent_idx + 1;
+                    }
+                }
+            } else {
+                let cur = self.cur_items.get(self.cur_idx).unwrap();
+
+                let mut section = "".to_string();
+                for &(_, idx) in self.stack.iter() {
+                    section.push_str(&(idx + 1).to_string()[]);
+                    section.push('.');
+                }
+                section.push_str(&(self.cur_idx + 1).to_string()[]);
+                section.push('.');
+
+                self.stack.push((self.cur_items, self.cur_idx));
+                self.cur_items = &cur.children[];
+                self.cur_idx = 0;
+                return Some((section, cur))
+            }
+        }
+    }
+}
+
+impl Book {
+    pub fn iter(&self) -> BookItems {
+        BookItems {
+            cur_items: &self.chapters[],
+            cur_idx: 0,
+            stack: Vec::new(),
+        }
+    }
+}
+
+/// Construct a book by parsing a summary (markdown table of contents).
+pub fn parse_summary<R: Reader>(input: R, src: &Path) -> Result<Book, Vec<String>> {
+    fn collapse(stack: &mut Vec<BookItem>,
+                top_items: &mut Vec<BookItem>,
+                to_level: uint) {
+        loop {
+            if stack.len() < to_level { return }
+            if stack.len() == 1 {
+                top_items.push(stack.pop().unwrap());
+                return;
+            }
+
+            let tip = stack.pop().unwrap();
+            let last = stack.len() - 1;
+            stack[last].children.push(tip);
+        }
+    }
+
+    let regex = r"(?P<indent>[\t ]*)\*[:space:]*\[(?P<title>.*)\]\((?P<path>.*)\)";
+    let item_re = Regex::new(regex).unwrap();
+    let mut top_items = vec!();
+    let mut stack = vec!();
+    let mut errors = vec!();
+
+    // always include the introduction
+    top_items.push(BookItem {
+        title: "Introduction".to_string(),
+        path: Path::new("README.md"),
+        path_to_root: Path::new("."),
+        children: vec!(),
+    });
+
+    for line_result in BufferedReader::new(input).lines() {
+        let line = match line_result {
+            Ok(line) => line,
+            Err(err) => {
+                errors.push(err.desc.to_string()); // FIXME: include detail
+                return Err(errors);
+            }
+        };
+
+        item_re.captures(&line[]).map(|cap| {
+            let given_path = cap.name("path");
+            let title = cap.name("title").unwrap().to_string();
+
+            let path_from_root = match src.join(given_path.unwrap()).path_relative_from(src) {
+                Some(p) => p,
+                None => {
+                    errors.push(format!("Paths in SUMMARY.md must be relative, \
+                                         but path '{}' for section '{}' is not.",
+                                         given_path.unwrap(), title));
+                    Path::new("")
+                }
+            };
+            let path_to_root = Path::new(iter::repeat("../")
+                                             .take(path_from_root.components().count() - 1)
+                                             .collect::<String>());
+            let item = BookItem {
+                title: title,
+                path: path_from_root,
+                path_to_root: path_to_root,
+                children: vec!(),
+            };
+            let level = cap.name("indent").unwrap().chars().map(|c| {
+                match c {
+                    ' ' => 1u,
+                    '\t' => 4,
+                    _ => unreachable!()
+                }
+            }).sum() / 4 + 1;
+
+            if level > stack.len() + 1 {
+                // FIXME: better error message
+                errors.push(format!("Section '{}' is indented too many levels.", item.title));
+            } else if level <= stack.len() {
+                collapse(&mut stack, &mut top_items, level);
+            }
+            stack.push(item)
+        });
+    }
+
+    if errors.is_empty() {
+        collapse(&mut stack, &mut top_items, 1);
+        Ok(Book { chapters: top_items })
+    } else {
+        Err(errors)
+    }
+}
diff --git a/src/rustbook/build.rs b/src/rustbook/build.rs
new file mode 100755
index 00000000000..db79e0b45e0
--- /dev/null
+++ b/src/rustbook/build.rs
@@ -0,0 +1,191 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Implementation of the `build` subcommand, used to compile a book.
+
+use std::os;
+use std::io;
+use std::io::{fs, File, BufferedWriter, TempDir, IoResult};
+
+use subcommand::Subcommand;
+use term::Term;
+use error::{Error, CliResult, CommandResult};
+use book;
+use book::{Book, BookItem};
+use css;
+
+use regex::Regex;
+
+use rustdoc;
+
+struct Build;
+
+pub fn parse_cmd(name: &str) -> Option<Box<Subcommand>> {
+    if name == "build" {
+        Some(box Build as Box<Subcommand>)
+    } else {
+        None
+    }
+}
+
+fn write_toc(book: &Book, path_to_root: &Path, out: &mut Writer) -> IoResult<()> {
+    fn walk_items(items: &[BookItem],
+                  section: &str,
+                  path_to_root: &Path,
+                  out: &mut Writer) -> IoResult<()> {
+        for (i, item) in items.iter().enumerate() {
+            try!(walk_item(item, &format!("{}{}.", section, i + 1)[], path_to_root, out));
+        }
+        Ok(())
+    }
+    fn walk_item(item: &BookItem,
+                 section: &str,
+                 path_to_root: &Path,
+                 out: &mut Writer) -> IoResult<()> {
+        try!(writeln!(out, "<li><a href='{}'><b>{}</b> {}</a>",
+                 path_to_root.join(item.path.with_extension("html")).display(),
+                 section,
+                 item.title));
+        if !item.children.is_empty() {
+            try!(writeln!(out, "<ul class='section'>"));
+            let _ = walk_items(&item.children[], section, path_to_root, out);
+            try!(writeln!(out, "</ul>"));
+        }
+        try!(writeln!(out, "</li>"));
+
+        Ok(())
+    }
+
+    try!(writeln!(out, "<div id='toc'>"));
+    try!(writeln!(out, "<ul class='chapter'>"));
+    try!(walk_items(&book.chapters[], "", path_to_root, out));
+    try!(writeln!(out, "</ul>"));
+    try!(writeln!(out, "</div>"));
+
+    Ok(())
+}
+
+fn render(book: &Book, tgt: &Path) -> CliResult<()> {
+    let tmp = TempDir::new("rust-book")
+                      .ok()
+                      // FIXME: lift to Result instead
+                      .expect("could not create temporary directory");
+
+    for (section, item) in book.iter() {
+        println!("{} {}", section, item.title);
+
+        let out_path = tgt.join(item.path.dirname());
+
+        let regex = r"\[(?P<title>[^]]*)\]\((?P<url_stem>[^)]*)\.(?P<ext>md|markdown)\)";
+        let md_urls = Regex::new(regex).unwrap();
+
+        let src;
+        if os::args().len() < 3 {
+            src = os::getcwd().unwrap().clone();
+        } else {
+            src = Path::new(os::args()[2].clone());
+        }
+        // preprocess the markdown, rerouting markdown references to html references
+        let markdown_data = try!(File::open(&src.join(&item.path)).read_to_string());
+        let preprocessed_path = tmp.path().join(item.path.filename().unwrap());
+        {
+            let urls = md_urls.replace_all(&markdown_data[], "[$title]($url_stem.html)");
+            try!(File::create(&preprocessed_path)
+                      .write_str(&urls[]));
+        }
+
+        // write the prelude to a temporary HTML file for rustdoc inclusion
+        let prelude = tmp.path().join("prelude.html");
+        {
+            let mut toc = BufferedWriter::new(try!(File::create(&prelude)));
+            let _ = write_toc(book, &item.path_to_root, &mut toc);
+            try!(writeln!(&mut toc, "<div id='page-wrapper'>"));
+            try!(writeln!(&mut toc, "<div id='page'>"));
+        }
+
+        // write the postlude to a temporary HTML file for rustdoc inclusion
+        let postlude = tmp.path().join("postlude.html");
+        {
+            let mut toc = BufferedWriter::new(try!(File::create(&postlude)));
+            try!(writeln!(&mut toc, "</div></div>"));
+        }
+
+        try!(fs::mkdir_recursive(&out_path, io::USER_DIR));
+
+        let rustdoc_args: &[String] = &[
+            "".to_string(),
+            preprocessed_path.display().to_string(),
+            format!("-o{}", out_path.display()),
+            format!("--html-before-content={}", prelude.display()),
+            format!("--html-after-content={}", postlude.display()),
+            format!("--markdown-css={}", item.path_to_root.join("rust-book.css").display()),
+            "--markdown-no-toc".to_string(),
+        ];
+        let output_result = rustdoc::main_args(rustdoc_args);
+        if output_result != 0 {
+
+            let message = format!("Could not execute `rustdoc`: {}", output_result);
+            return Err(box message as Box<Error>);
+        }
+    }
+
+    // create index.html from the root README
+    try!(fs::copy(&tgt.join("README.html"), &tgt.join("index.html")));
+    Ok(())
+}
+
+impl Subcommand for Build {
+    fn parse_args(&mut self, _: &[String]) -> CliResult<()> {
+        Ok(())
+    }
+    fn usage(&self) {}
+    fn execute(&mut self, term: &mut Term) -> CommandResult<()> {
+        let cwd = os::getcwd().unwrap();
+        let src;
+        let tgt;
+
+        if os::args().len() < 3 {
+            src = cwd.clone();
+        } else {
+            src = Path::new(os::args()[2].clone());
+        }
+
+        if os::args().len() < 4 {
+            tgt = cwd.join("_book");
+        } else {
+            tgt = Path::new(os::args()[3].clone());
+        }
+
+        let _ = fs::mkdir(&tgt, io::USER_DIR); // FIXME: handle errors
+
+        // FIXME: handle errors
+        let _ = File::create(&tgt.join("rust-book.css")).write_str(css::STYLE);
+
+        let summary = File::open(&src.join("SUMMARY.md"));
+        match book::parse_summary(summary, &src) {
+            Ok(book) => {
+                // execute rustdoc on the whole book
+                let _ = render(&book, &tgt).map_err(|err| {
+                    term.err(&format!("error: {}", err.description())[]);
+                    err.detail().map(|detail| {
+                        term.err(&format!("detail: {}", detail)[]);
+                    })
+                });
+            }
+            Err(errors) => {
+                for err in errors.into_iter() {
+                    term.err(&err[]);
+                }
+            }
+        }
+
+        Ok(()) // lol
+    }
+}
diff --git a/src/rustbook/css.rs b/src/rustbook/css.rs
new file mode 100644
index 00000000000..8eb66e71d3a
--- /dev/null
+++ b/src/rustbook/css.rs
@@ -0,0 +1,72 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+// The rust-book CSS in string form.
+
+pub static STYLE: &'static str = r#"
+@import url("//static.rust-lang.org/doc/master/rust.css");
+
+body {
+    max-width:none;
+}
+
+#toc {
+    position: absolute;
+    left: 0px;
+    top: 0px;
+    bottom: 0px;
+    width: 250px;
+    overflow-y: auto;
+    border-right: 1px solid rgba(0, 0, 0, 0.07);
+    padding: 10px 10px;
+    font-size: 16px;
+    background: none repeat scroll 0% 0% #FFF;
+    box-sizing: border-box;
+}
+
+#page-wrapper {
+    position: absolute;
+    overflow-y: auto;
+    left: 260px;
+    right: 0px;
+    top: 0px;
+    bottom: 0px;
+    box-sizing: border-box;
+    background: none repeat scroll 0% 0% #FFF;
+}
+
+#page {
+    margin-left: auto;
+    margin-right:auto;
+    width: 750px;
+}
+
+.chapter {
+    list-style: none outside none;
+    padding-left: 0px;
+    line-height: 30px;
+}
+
+.section {
+    list-style: none outside none;
+    padding-left: 20px;
+    line-height: 30px;
+}
+
+.section li {
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+}
+
+.chapter li a {
+    color: #000000;
+}
+"#;
diff --git a/src/rustbook/error.rs b/src/rustbook/error.rs
new file mode 100644
index 00000000000..1d3baef8c1c
--- /dev/null
+++ b/src/rustbook/error.rs
@@ -0,0 +1,76 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Error handling utilities. WIP.
+
+use std::fmt;
+use std::fmt::{Show, Formatter};
+
+use std::io::IoError;
+
+pub type CliError = Box<Error + 'static>;
+pub type CliResult<T> = Result<T, CliError>;
+
+pub type CommandError = Box<Error + 'static>;
+pub type CommandResult<T> = Result<T, CommandError>;
+
+pub trait Error {
+    fn description(&self) -> &str;
+
+    fn detail(&self) -> Option<&str> { None }
+    fn cause(&self) -> Option<&Error> { None }
+}
+
+pub trait FromError<E> {
+    fn from_err(err: E) -> Self;
+}
+
+impl Show for Box<Error + 'static> {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        write!(f, "{}", self.description())
+    }
+}
+
+impl<E: Error + 'static> FromError<E> for Box<Error + 'static> {
+    fn from_err(err: E) -> Box<Error + 'static> {
+        box err as Box<Error>
+    }
+}
+
+impl<'a> Error for &'a str {
+    fn description<'b>(&'b self) -> &'b str {
+        *self
+    }
+}
+
+impl Error for String {
+    fn description<'a>(&'a self) -> &'a str {
+        &self[]
+    }
+}
+
+impl FromError<()> for () {
+    fn from_err(_: ()) -> () { () }
+}
+
+impl FromError<IoError> for IoError {
+    fn from_err(error: IoError) -> IoError { error }
+}
+
+impl Error for IoError {
+    fn description(&self) -> &str {
+        self.desc
+    }
+    fn detail(&self) -> Option<&str> {
+        self.detail.as_ref().map(|s| &s[])
+    }
+}
+
+//fn iter_map_err<T, U, E, I: Iterator<Result<T,E>>>(iter: I,
diff --git a/src/rustbook/help.rs b/src/rustbook/help.rs
new file mode 100644
index 00000000000..7fd8214f731
--- /dev/null
+++ b/src/rustbook/help.rs
@@ -0,0 +1,46 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Implementation of the `help` subcommand. Currently just prints basic usage info.
+
+use subcommand::Subcommand;
+use error::CliResult;
+use error::CommandResult;
+use term::Term;
+
+struct Help;
+
+pub fn parse_cmd(name: &str) -> Option<Box<Subcommand>> {
+    match name {
+        "help" | "--help" | "-h" | "-?" => Some(box Help as Box<Subcommand>),
+        _ => None
+    }
+}
+
+impl Subcommand for Help {
+    fn parse_args(&mut self, _: &[String]) -> CliResult<()> {
+        Ok(())
+    }
+    fn usage(&self) {}
+    fn execute(&mut self, _: &mut Term) -> CommandResult<()> {
+        usage();
+        Ok(())
+    }
+}
+
+pub fn usage() {
+    println!("Usage: rust-book <command> [<args>]");
+    println!("");
+    println!("The <command> must be one of:");
+    println!("  help    Print this message.");
+    println!("  build   Build the book in subdirectory _book");
+    println!("  serve   --NOT YET IMPLEMENTED--");
+    println!("  test    --NOT YET IMPLEMENTED--");
+}
diff --git a/src/rustbook/main.rs b/src/rustbook/main.rs
new file mode 100755
index 00000000000..acb4edb7a45
--- /dev/null
+++ b/src/rustbook/main.rs
@@ -0,0 +1,74 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+#![feature(slicing_syntax, box_syntax)]
+
+extern crate regex;
+
+extern crate rustdoc;
+
+use std::os;
+use subcommand::Subcommand;
+use term::Term;
+
+macro_rules! try (
+    ($expr:expr) => ({
+        use error;
+        match $expr {
+            Ok(val) => val,
+            Err(err) => return Err(error::FromError::from_err(err))
+        }
+    })
+);
+
+mod term;
+mod error;
+mod book;
+
+mod subcommand;
+mod help;
+mod build;
+mod serve;
+mod test;
+
+mod css;
+
+#[cfg(not(test))] // thanks #12327
+fn main() {
+    let mut term = Term::new();
+    let cmd = os::args();
+
+    if cmd.len() < 1 {
+        help::usage()
+    } else {
+        match subcommand::parse_name(&cmd[1][]) {
+            Some(mut subcmd) => {
+                match subcmd.parse_args(cmd.tail()) {
+                    Ok(_) => {
+                        match subcmd.execute(&mut term) {
+                            Ok(_) => (),
+                            Err(_) => os::set_exit_status(-1),
+                        }
+                    }
+                    Err(err) => {
+                        println!("{}", err.description());
+                        println!("");
+                        subcmd.usage();
+                    }
+                }
+            }
+            None => {
+                println!("Unrecognized command '{}'.", cmd[1]);
+                println!("");
+                help::usage();
+            }
+        }
+    }
+}
diff --git a/src/rustbook/serve.rs b/src/rustbook/serve.rs
new file mode 100644
index 00000000000..808527dcef9
--- /dev/null
+++ b/src/rustbook/serve.rs
@@ -0,0 +1,36 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Implementation of the `serve` subcommand. Just a stub for now.
+
+use subcommand::Subcommand;
+use error::CliResult;
+use error::CommandResult;
+use term::Term;
+
+struct Serve;
+
+pub fn parse_cmd(name: &str) -> Option<Box<Subcommand>> {
+    if name == "serve" {
+        Some(box Serve as Box<Subcommand>)
+    } else {
+        None
+    }
+}
+
+impl Subcommand for Serve {
+    fn parse_args(&mut self, _: &[String]) -> CliResult<()> {
+        Ok(())
+    }
+    fn usage(&self) {}
+    fn execute(&mut self, _: &mut Term) -> CommandResult<()> {
+        Ok(())
+    }
+}
diff --git a/src/rustbook/subcommand.rs b/src/rustbook/subcommand.rs
new file mode 100644
index 00000000000..473739c919d
--- /dev/null
+++ b/src/rustbook/subcommand.rs
@@ -0,0 +1,44 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Common API for all rust-book subcommands.
+
+use error::CliResult;
+use error::CommandResult;
+use term::Term;
+
+use help;
+use build;
+use serve;
+use test;
+
+pub trait Subcommand {
+    /// Mutate the subcommand by parsing its arguments.
+    ///
+    /// Returns `Err` on a parsing error.
+    fn parse_args(&mut self, args: &[String]) -> CliResult<()>;
+    /// Print the CLI usage information.
+    fn usage(&self);
+    /// Actually execute the subcommand.
+    fn execute(&mut self, term: &mut Term) -> CommandResult<()>;
+}
+
+/// Create a Subcommand object based on its name.
+pub fn parse_name(name: &str) -> Option<Box<Subcommand>> {
+    for parser in [
+        help::parse_cmd as fn(&str) -> Option<Box<Subcommand>>,
+        build::parse_cmd as fn(&str) -> Option<Box<Subcommand>>,
+        serve::parse_cmd as fn(&str) -> Option<Box<Subcommand>>,
+        test::parse_cmd as fn(&str) -> Option<Box<Subcommand>>].iter() {
+        let parsed = (*parser)(name);
+        if parsed.is_some() { return parsed }
+    }
+    None
+}
diff --git a/src/rustbook/term.rs b/src/rustbook/term.rs
new file mode 100644
index 00000000000..18306d6ec20
--- /dev/null
+++ b/src/rustbook/term.rs
@@ -0,0 +1,31 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! An abstraction of the terminal. Eventually, provide color and
+//! verbosity support. For now, just a wrapper around stdout/stderr.
+
+use std::io::stdio;
+
+pub struct Term {
+    err: Box<Writer + 'static>
+}
+
+impl Term {
+    pub fn new() -> Term {
+        Term {
+            err: box stdio::stderr() as Box<Writer>,
+        }
+    }
+
+    pub fn err(&mut self, msg: &str) {
+        // swallow any errors
+        let _ = self.err.write_line(msg);
+    }
+}
diff --git a/src/rustbook/test.rs b/src/rustbook/test.rs
new file mode 100644
index 00000000000..f2bf92585f7
--- /dev/null
+++ b/src/rustbook/test.rs
@@ -0,0 +1,75 @@
+// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Implementation of the `test` subcommand. Just a stub for now.
+
+use subcommand::Subcommand;
+use error::CliResult;
+use error::CommandResult;
+use error::Error;
+use term::Term;
+use book;
+use std::io::{Command, File};
+use std::os;
+
+struct Test;
+
+pub fn parse_cmd(name: &str) -> Option<Box<Subcommand>> {
+    if name == "test" {
+        Some(box Test as Box<Subcommand>)
+    } else {
+        None
+    }
+}
+
+impl Subcommand for Test {
+    fn parse_args(&mut self, _: &[String]) -> CliResult<()> {
+        Ok(())
+    }
+    fn usage(&self) {}
+    fn execute(&mut self, term: &mut Term) -> CommandResult<()> {
+        let cwd = os::getcwd().unwrap();
+        let src = cwd.clone();
+
+        let summary = File::open(&src.join("SUMMARY.md"));
+        match book::parse_summary(summary, &src) {
+            Ok(book) => {
+                for (_, item) in book.iter() {
+                    let output_result = Command::new("rustdoc")
+                        .arg(&item.path)
+                        .arg("--test")
+                        .output();
+                    match output_result {
+                        Ok(output) => {
+                            if !output.status.success() {
+                                term.err(&format!("{}\n{}",
+                                         String::from_utf8_lossy(&output.output[]),
+                                         String::from_utf8_lossy(&output.error[]))[]);
+                                return Err(box "Some tests failed." as Box<Error>);
+                            }
+
+                        }
+                        Err(e) => {
+                            let message = format!("Could not execute `rustdoc`: {}", e);
+                            return Err(box message as Box<Error>);
+                        }
+                    }
+                }
+            }
+            Err(errors) => {
+                for err in errors.into_iter() {
+                    term.err(&err[]);
+                }
+                return Err(box "There was an error." as Box<Error>);
+            }
+        }
+        Ok(()) // lol
+    }
+}