diff options
| author | bors <bors@rust-lang.org> | 2016-03-11 04:38:04 -0800 |
|---|---|---|
| committer | bors <bors@rust-lang.org> | 2016-03-11 04:38:04 -0800 |
| commit | aeb85a953322df7773095186e9762f3fe73375e2 (patch) | |
| tree | 81ea15e110932e2a990351a9bea602599c23cb6c /src/tools | |
| parent | 40c85cd8aec5de06140252610ad4bdc352324e2c (diff) | |
| parent | 3e6fed3a7a3f783bf967f6c73455743848f31167 (diff) | |
Auto merge of #32133 - alexcrichton:linkchecker, r=brson
Add a link validator to rustbuild This commit was originally targeted at just adding a link checking script to the rustbuild system. This ended up snowballing a bit to extend rustbuild to be amenable to various tools we have as part of the build system in general. There's a new `src/tools` directory which has a number of scripts/programs that are purely intended to be used as part of the build system and CI of this repository. This is currently inhabited by rustbook, the error index generator, and a new linkchecker script added as part of this PR. I suspect that more tools like compiletest, tidy scripts, snapshot scripts, etc will migrate their way into this directory over time. The commit which adds the error index generator shows the steps necessary to add new tools to the build system, namely: 1. New steps are defined for building the tool and running the tool 2. The dependencies are configured 3. The steps are implemented In terms of the link checker, these commits do a few things: * A new `src/tools/linkchecker` script is added. This will read an entire documentation tree looking for broken relative links (HTTP links aren't followed yet). * A large number of broken links throughout the documentation were fixed. Many of these were just broken when viewed from core as opposed to std, but were easily fixed. * A few rustdoc bugs here and there were fixed
Diffstat (limited to 'src/tools')
| -rw-r--r-- | src/tools/error_index_generator/Cargo.lock | 4 | ||||
| -rw-r--r-- | src/tools/error_index_generator/Cargo.toml | 8 | ||||
| -rw-r--r-- | src/tools/error_index_generator/main.rs | 208 | ||||
| -rw-r--r-- | src/tools/linkchecker/Cargo.lock | 64 | ||||
| -rw-r--r-- | src/tools/linkchecker/Cargo.toml | 11 | ||||
| -rw-r--r-- | src/tools/linkchecker/main.rs | 161 | ||||
| -rw-r--r-- | src/tools/rustbook/Cargo.lock | 4 | ||||
| -rw-r--r-- | src/tools/rustbook/Cargo.toml | 8 | ||||
| -rw-r--r-- | src/tools/rustbook/book.rs | 171 | ||||
| -rw-r--r-- | src/tools/rustbook/build.rs | 225 | ||||
| -rw-r--r-- | src/tools/rustbook/error.rs | 36 | ||||
| -rw-r--r-- | src/tools/rustbook/help.rs | 46 | ||||
| -rw-r--r-- | src/tools/rustbook/main.rs | 72 | ||||
| -rw-r--r-- | src/tools/rustbook/serve.rs | 36 | ||||
| -rw-r--r-- | src/tools/rustbook/static/rustbook.css | 157 | ||||
| -rw-r--r-- | src/tools/rustbook/static/rustbook.js | 78 | ||||
| -rw-r--r-- | src/tools/rustbook/subcommand.rs | 44 | ||||
| -rw-r--r-- | src/tools/rustbook/term.rs | 34 | ||||
| -rw-r--r-- | src/tools/rustbook/test.rs | 75 |
19 files changed, 1442 insertions, 0 deletions
diff --git a/src/tools/error_index_generator/Cargo.lock b/src/tools/error_index_generator/Cargo.lock new file mode 100644 index 00000000000..b7d2cfcaaa1 --- /dev/null +++ b/src/tools/error_index_generator/Cargo.lock @@ -0,0 +1,4 @@ +[root] +name = "error_index_generator" +version = "0.0.0" + diff --git a/src/tools/error_index_generator/Cargo.toml b/src/tools/error_index_generator/Cargo.toml new file mode 100644 index 00000000000..5c5ca273e9c --- /dev/null +++ b/src/tools/error_index_generator/Cargo.toml @@ -0,0 +1,8 @@ +[package] +authors = ["The Rust Project Developers"] +name = "error_index_generator" +version = "0.0.0" + +[[bin]] +name = "error_index_generator" +path = "main.rs" diff --git a/src/tools/error_index_generator/main.rs b/src/tools/error_index_generator/main.rs new file mode 100644 index 00000000000..4343aef00a9 --- /dev/null +++ b/src/tools/error_index_generator/main.rs @@ -0,0 +1,208 @@ +// Copyright 2015 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(rustc_private, rustdoc)] + +extern crate syntax; +extern crate rustdoc; +extern crate serialize as rustc_serialize; + +use std::collections::BTreeMap; +use std::env; +use std::error::Error; +use std::fs::{read_dir, File}; +use std::io::{Read, Write}; +use std::path::Path; +use std::path::PathBuf; + +use syntax::diagnostics::metadata::{get_metadata_dir, ErrorMetadataMap, ErrorMetadata}; + +use rustdoc::html::markdown::Markdown; +use rustc_serialize::json; + +enum OutputFormat { + HTML(HTMLFormatter), + Markdown(MarkdownFormatter), + Unknown(String), +} + +impl OutputFormat { + fn from(format: &str) -> OutputFormat { + match &*format.to_lowercase() { + "html" => OutputFormat::HTML(HTMLFormatter), + "markdown" => OutputFormat::Markdown(MarkdownFormatter), + s => OutputFormat::Unknown(s.to_owned()), + } + } +} + +trait Formatter { + fn header(&self, output: &mut Write) -> Result<(), Box<Error>>; + fn title(&self, output: &mut Write) -> Result<(), Box<Error>>; + fn error_code_block(&self, output: &mut Write, info: &ErrorMetadata, + err_code: &str) -> Result<(), Box<Error>>; + fn footer(&self, output: &mut Write) -> Result<(), Box<Error>>; +} + +struct HTMLFormatter; +struct MarkdownFormatter; + +impl Formatter for HTMLFormatter { + fn header(&self, output: &mut Write) -> Result<(), Box<Error>> { + try!(write!(output, r##"<!DOCTYPE html> +<html> +<head> +<title>Rust Compiler Error Index</title> +<meta charset="utf-8"> +<!-- Include rust.css after main.css so its rules take priority. --> +<link rel="stylesheet" type="text/css" href="main.css"/> +<link rel="stylesheet" type="text/css" href="rust.css"/> +<style> +.error-undescribed {{ + display: none; +}} +</style> +</head> +<body> +"##)); + Ok(()) + } + + fn title(&self, output: &mut Write) -> Result<(), Box<Error>> { + try!(write!(output, "<h1>Rust Compiler Error Index</h1>\n")); + Ok(()) + } + + fn error_code_block(&self, output: &mut Write, info: &ErrorMetadata, + err_code: &str) -> Result<(), Box<Error>> { + // Enclose each error in a div so they can be shown/hidden en masse. + let desc_desc = match info.description { + Some(_) => "error-described", + None => "error-undescribed", + }; + let use_desc = match info.use_site { + Some(_) => "error-used", + None => "error-unused", + }; + try!(write!(output, "<div class=\"{} {}\">", desc_desc, use_desc)); + + // Error title (with self-link). + try!(write!(output, + "<h2 id=\"{0}\" class=\"section-header\"><a href=\"#{0}\">{0}</a></h2>\n", + err_code)); + + // Description rendered as markdown. + match info.description { + Some(ref desc) => try!(write!(output, "{}", Markdown(desc))), + None => try!(write!(output, "<p>No description.</p>\n")), + } + + try!(write!(output, "</div>\n")); + Ok(()) + } + + fn footer(&self, output: &mut Write) -> Result<(), Box<Error>> { + try!(write!(output, "</body>\n</html>")); + Ok(()) + } +} + +impl Formatter for MarkdownFormatter { + #[allow(unused_variables)] + fn header(&self, output: &mut Write) -> Result<(), Box<Error>> { + Ok(()) + } + + fn title(&self, output: &mut Write) -> Result<(), Box<Error>> { + try!(write!(output, "# Rust Compiler Error Index\n")); + Ok(()) + } + + fn error_code_block(&self, output: &mut Write, info: &ErrorMetadata, + err_code: &str) -> Result<(), Box<Error>> { + Ok(match info.description { + Some(ref desc) => try!(write!(output, "## {}\n{}\n", err_code, desc)), + None => (), + }) + } + + #[allow(unused_variables)] + fn footer(&self, output: &mut Write) -> Result<(), Box<Error>> { + Ok(()) + } +} + +/// Load all the metadata files from `metadata_dir` into an in-memory map. +fn load_all_errors(metadata_dir: &Path) -> Result<ErrorMetadataMap, Box<Error>> { + let mut all_errors = BTreeMap::new(); + + for entry in try!(read_dir(metadata_dir)) { + let path = try!(entry).path(); + + let mut metadata_str = String::new(); + try!(File::open(&path).and_then(|mut f| f.read_to_string(&mut metadata_str))); + + let some_errors: ErrorMetadataMap = try!(json::decode(&metadata_str)); + + for (err_code, info) in some_errors { + all_errors.insert(err_code, info); + } + } + + Ok(all_errors) +} + +/// Output an HTML page for the errors in `err_map` to `output_path`. +fn render_error_page<T: Formatter>(err_map: &ErrorMetadataMap, output_path: &Path, + formatter: T) -> Result<(), Box<Error>> { + let mut output_file = try!(File::create(output_path)); + + try!(formatter.header(&mut output_file)); + try!(formatter.title(&mut output_file)); + + for (err_code, info) in err_map { + try!(formatter.error_code_block(&mut output_file, info, err_code)); + } + + formatter.footer(&mut output_file) +} + +fn main_with_result(format: OutputFormat, dst: &Path) -> Result<(), Box<Error>> { + let build_arch = try!(env::var("CFG_BUILD")); + let metadata_dir = get_metadata_dir(&build_arch); + let err_map = try!(load_all_errors(&metadata_dir)); + match format { + OutputFormat::Unknown(s) => panic!("Unknown output format: {}", s), + OutputFormat::HTML(h) => try!(render_error_page(&err_map, dst, h)), + OutputFormat::Markdown(m) => try!(render_error_page(&err_map, dst, m)), + } + Ok(()) +} + +fn parse_args() -> (OutputFormat, PathBuf) { + let mut args = env::args().skip(1); + let format = args.next().map(|a| OutputFormat::from(&a)) + .unwrap_or(OutputFormat::from("html")); + let dst = args.next().map(PathBuf::from).unwrap_or_else(|| { + match format { + OutputFormat::HTML(..) => PathBuf::from("doc/error-index.html"), + OutputFormat::Markdown(..) => PathBuf::from("doc/error-index.md"), + OutputFormat::Unknown(..) => PathBuf::from("<nul>"), + } + }); + (format, dst) +} + +fn main() { + let (format, dst) = parse_args(); + if let Err(e) = main_with_result(format, &dst) { + panic!("{}", e.description()); + } +} diff --git a/src/tools/linkchecker/Cargo.lock b/src/tools/linkchecker/Cargo.lock new file mode 100644 index 00000000000..8e94137d213 --- /dev/null +++ b/src/tools/linkchecker/Cargo.lock @@ -0,0 +1,64 @@ +[root] +name = "linkchecker" +version = "0.1.0" +dependencies = [ + "url 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "matches" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rand" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-serialize" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "url" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "uuid" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + diff --git a/src/tools/linkchecker/Cargo.toml b/src/tools/linkchecker/Cargo.toml new file mode 100644 index 00000000000..29fc78a65e9 --- /dev/null +++ b/src/tools/linkchecker/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "linkchecker" +version = "0.1.0" +authors = ["Alex Crichton <alex@alexcrichton.com>"] + +[dependencies] +url = "0.5" + +[[bin]] +name = "linkchecker" +path = "main.rs" diff --git a/src/tools/linkchecker/main.rs b/src/tools/linkchecker/main.rs new file mode 100644 index 00000000000..e5e88081bc4 --- /dev/null +++ b/src/tools/linkchecker/main.rs @@ -0,0 +1,161 @@ +// Copyright 2016 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. + +//! Script to check the validity of `href` links in our HTML documentation. +//! +//! In the past we've been quite error prone to writing in broken links as most +//! of them are manually rather than automatically added. As files move over +//! time or apis change old links become stale or broken. The purpose of this +//! script is to check all relative links in our documentation to make sure they +//! actually point to a valid place. +//! +//! Currently this doesn't actually do any HTML parsing or anything fancy like +//! that, it just has a simple "regex" to search for `href` tags. These values +//! are then translated to file URLs if possible and then the destination is +//! asserted to exist. +//! +//! A few whitelisted exceptions are allowed as there's known bugs in rustdoc, +//! but this should catch the majority of "broken link" cases. + +extern crate url; + +use std::env; +use std::fs::File; +use std::io::prelude::*; +use std::path::Path; + +use url::{Url, UrlParser}; + +macro_rules! t { + ($e:expr) => (match $e { + Ok(e) => e, + Err(e) => panic!("{} failed with {}", stringify!($e), e), + }) +} + +fn main() { + let docs = env::args().nth(1).unwrap(); + let docs = env::current_dir().unwrap().join(docs); + let mut url = Url::from_file_path(&docs).unwrap(); + let mut errors = false; + walk(&docs, &docs, &mut url, &mut errors); + if errors { + panic!("found some broken links"); + } +} + +fn walk(root: &Path, dir: &Path, url: &mut Url, errors: &mut bool) { + for entry in t!(dir.read_dir()).map(|e| t!(e)) { + let path = entry.path(); + let kind = t!(entry.file_type()); + url.path_mut().unwrap().push(entry.file_name().into_string().unwrap()); + if kind.is_dir() { + walk(root, &path, url, errors); + } else { + check(root, &path, url, errors); + } + url.path_mut().unwrap().pop(); + } +} + +fn check(root: &Path, file: &Path, base: &Url, errors: &mut bool) { + // ignore js files as they are not prone to errors as the rest of the + // documentation is and they otherwise bring up false positives. + if file.extension().and_then(|s| s.to_str()) == Some("js") { + return + } + + let pretty_file = file.strip_prefix(root).unwrap_or(file); + + // Unfortunately we're not 100% full of valid links today to we need a few + // whitelists to get this past `make check` today. + if let Some(path) = pretty_file.to_str() { + // FIXME(#32129) + if path == "std/string/struct.String.html" { + return + } + // FIXME(#32130) + if path.contains("btree_set/struct.BTreeSet.html") || + path == "collections/struct.BTreeSet.html" { + return + } + // FIXME(#31948) + if path.contains("ParseFloatError") { + return + } + + // currently + if path == "std/sys/ext/index.html" { + return + } + + // weird reexports, but this module is on its way out, so chalk it up to + // "rustdoc weirdness" and move on from there + if path.contains("scoped_tls") { + return + } + } + + let mut parser = UrlParser::new(); + parser.base_url(base); + let mut contents = String::new(); + if t!(File::open(file)).read_to_string(&mut contents).is_err() { + return + } + + for (i, mut line) in contents.lines().enumerate() { + // Search for anything that's the regex 'href[ ]*=[ ]*".*?"' + while let Some(j) = line.find(" href") { + let rest = &line[j + 5..]; + line = rest; + let pos_equals = match rest.find("=") { + Some(i) => i, + None => continue, + }; + if rest[..pos_equals].trim_left_matches(" ") != "" { + continue + } + let rest = &rest[pos_equals + 1..]; + let pos_quote = match rest.find("\"").or_else(|| rest.find("'")) { + Some(i) => i, + None => continue, + }; + if rest[..pos_quote].trim_left_matches(" ") != "" { + continue + } + let rest = &rest[pos_quote + 1..]; + let url = match rest.find("\"").or_else(|| rest.find("'")) { + Some(i) => &rest[..i], + None => continue, + }; + + // Once we've plucked out the URL, parse it using our base url and + // then try to extract a file path. If either if these fail then we + // just keep going. + let parsed_url = match parser.parse(url) { + Ok(url) => url, + Err(..) => continue, + }; + let path = match parsed_url.to_file_path() { + Ok(path) => path, + Err(..) => continue, + }; + + // Alright, if we've found a file name then this file had better + // exist! If it doesn't then we register and print an error. + if !path.exists() { + *errors = true; + print!("{}:{}: broken link - ", pretty_file.display(), i + 1); + let pretty_path = path.strip_prefix(root).unwrap_or(&path); + println!("{}", pretty_path.display()); + } + } + } +} diff --git a/src/tools/rustbook/Cargo.lock b/src/tools/rustbook/Cargo.lock new file mode 100644 index 00000000000..e541ce4b2b8 --- /dev/null +++ b/src/tools/rustbook/Cargo.lock @@ -0,0 +1,4 @@ +[root] +name = "rustbook" +version = "0.0.0" + diff --git a/src/tools/rustbook/Cargo.toml b/src/tools/rustbook/Cargo.toml new file mode 100644 index 00000000000..956392ca540 --- /dev/null +++ b/src/tools/rustbook/Cargo.toml @@ -0,0 +1,8 @@ +[package] +authors = ["The Rust Project Developers"] +name = "rustbook" +version = "0.0.0" + +[[bin]] +name = "rustbook" +path = "main.rs" diff --git a/src/tools/rustbook/book.rs b/src/tools/rustbook/book.rs new file mode 100644 index 00000000000..36a37dba1fa --- /dev/null +++ b/src/tools/rustbook/book.rs @@ -0,0 +1,171 @@ +// 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::prelude::*; +use std::io::BufReader; +use std::iter; +use std::path::{Path, PathBuf}; + +pub struct BookItem { + pub title: String, + pub path: PathBuf, + pub path_to_root: PathBuf, + 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: usize, + stack: Vec<(&'a [BookItem], usize)>, +} + +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 { + 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(input: &mut Read, src: &Path) -> Result<Book, Vec<String>> { + fn collapse(stack: &mut Vec<BookItem>, + top_items: &mut Vec<BookItem>, + to_level: usize) { + 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 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: PathBuf::from("README.md"), + path_to_root: PathBuf::from(""), + children: vec!(), + }); + + for line_result in BufReader::new(input).lines() { + let line = match line_result { + Ok(line) => line, + Err(err) => { + errors.push(err.to_string()); + return Err(errors); + } + }; + + let star_idx = match line.find("*") { Some(i) => i, None => continue }; + + let start_bracket = star_idx + line[star_idx..].find("[").unwrap(); + let end_bracket = start_bracket + line[start_bracket..].find("](").unwrap(); + let start_paren = end_bracket + 1; + let end_paren = start_paren + line[start_paren..].find(")").unwrap(); + + let given_path = &line[start_paren + 1 .. end_paren]; + let title = line[start_bracket + 1..end_bracket].to_string(); + let indent = &line[..star_idx]; + + let path_from_root = match src.join(given_path).strip_prefix(src) { + Ok(p) => p.to_path_buf(), + Err(..) => { + errors.push(format!("paths in SUMMARY.md must be relative, \ + but path '{}' for section '{}' is not.", + given_path, title)); + PathBuf::new() + } + }; + let path_to_root = PathBuf::from(&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 = indent.chars().map(|c| -> usize { + match c { + ' ' => 1, + '\t' => 4, + _ => unreachable!() + } + }).sum::<usize>() / 4 + 1; + + if level > stack.len() + 1 { + errors.push(format!("section '{}' is indented too deeply; \ + found {}, expected {} or less", + item.title, level, stack.len() + 1)); + } 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/tools/rustbook/build.rs b/src/tools/rustbook/build.rs new file mode 100644 index 00000000000..70ed98519f9 --- /dev/null +++ b/src/tools/rustbook/build.rs @@ -0,0 +1,225 @@ +// Copyright 2014-2015 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::env; +use std::fs::{self, File}; +use std::io::prelude::*; +use std::io::{self, BufWriter}; +use std::path::{Path, PathBuf}; +use rustc_back::tempdir::TempDir; + +use subcommand::Subcommand; +use term::Term; +use error::{err, CliResult, CommandResult}; +use book; +use book::{Book, BookItem}; + +use rustdoc; + +struct Build; + +pub fn parse_cmd(name: &str) -> Option<Box<Subcommand>> { + if name == "build" { + Some(Box::new(Build)) + } else { + None + } +} + +fn write_toc(book: &Book, current_page: &BookItem, out: &mut Write) -> io::Result<()> { + fn walk_items(items: &[BookItem], + section: &str, + current_page: &BookItem, + out: &mut Write) -> io::Result<()> { + for (i, item) in items.iter().enumerate() { + try!(walk_item(item, &format!("{}{}.", section, i + 1)[..], current_page, out)); + } + Ok(()) + } + fn walk_item(item: &BookItem, + section: &str, + current_page: &BookItem, + out: &mut Write) -> io::Result<()> { + let class_string = if item.path == current_page.path { + "class='active'" + } else { + "" + }; + + try!(writeln!(out, "<li><a {} href='{}'><b>{}</b> {}</a>", + class_string, + current_page.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, current_page, out); + try!(writeln!(out, "</ul>")); + } + try!(writeln!(out, "</li>")); + + Ok(()) + } + + try!(writeln!(out, "<div id='toc' class='mobile-hidden'>")); + try!(writeln!(out, "<ul class='chapter'>")); + try!(walk_items(&book.chapters[..], "", ¤t_page, out)); + try!(writeln!(out, "</ul>")); + try!(writeln!(out, "</div>")); + + Ok(()) +} + +fn render(book: &Book, tgt: &Path) -> CliResult<()> { + let tmp = try!(TempDir::new("rustbook")); + + for (_section, item) in book.iter() { + let out_path = match item.path.parent() { + Some(p) => tgt.join(p), + None => tgt.to_path_buf(), + }; + + let src; + if env::args().len() < 3 { + src = env::current_dir().unwrap().clone(); + } else { + src = PathBuf::from(&env::args().nth(2).unwrap()); + } + // preprocess the markdown, rerouting markdown references to html + // references + let mut markdown_data = String::new(); + try!(File::open(&src.join(&item.path)).and_then(|mut f| { + f.read_to_string(&mut markdown_data) + })); + let preprocessed_path = tmp.path().join(item.path.file_name().unwrap()); + { + let urls = markdown_data.replace(".md)", ".html)"); + try!(File::create(&preprocessed_path).and_then(|mut f| { + f.write_all(urls.as_bytes()) + })); + } + + // write the prelude to a temporary HTML file for rustdoc inclusion + let prelude = tmp.path().join("prelude.html"); + { + let mut buffer = BufWriter::new(try!(File::create(&prelude))); + try!(writeln!(&mut buffer, r#" + <div id="nav"> + <button id="toggle-nav"> + <span class="sr-only">Toggle navigation</span> + <span class="bar"></span> + <span class="bar"></span> + <span class="bar"></span> + </button> + </div>"#)); + let _ = write_toc(book, &item, &mut buffer); + try!(writeln!(&mut buffer, "<div id='page-wrapper'>")); + try!(writeln!(&mut buffer, "<div id='page'>")); + } + + // write the postlude to a temporary HTML file for rustdoc inclusion + let postlude = tmp.path().join("postlude.html"); + { + let mut buffer = BufWriter::new(try!(File::create(&postlude))); + try!(writeln!(&mut buffer, "<script src='rustbook.js'></script>")); + try!(writeln!(&mut buffer, "<script src='playpen.js'></script>")); + try!(writeln!(&mut buffer, "</div></div>")); + } + + try!(fs::create_dir_all(&out_path)); + + 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-playground-url=https://play.rust-lang.org"), + format!("--markdown-css={}", item.path_to_root.join("rustbook.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` with {:?}: {}", + rustdoc_args, output_result); + return Err(err(&message)); + } + } + + // create index.html from the root README + try!(fs::copy(&tgt.join("README.html"), &tgt.join("index.html"))); + + // Copy js for playpen + let mut playpen = try!(File::create(tgt.join("playpen.js"))); + let js = include_bytes!("../../librustdoc/html/static/playpen.js"); + try!(playpen.write_all(js)); + 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 = env::current_dir().unwrap(); + let src; + let tgt; + + if env::args().len() < 3 { + src = cwd.clone(); + } else { + src = PathBuf::from(&env::args().nth(2).unwrap()); + } + + if env::args().len() < 4 { + tgt = cwd.join("_book"); + } else { + tgt = PathBuf::from(&env::args().nth(3).unwrap()); + } + + // `_book` directory may already exist from previous runs. Check and + // delete it if it exists. + for entry in try!(fs::read_dir(&cwd)) { + let path = try!(entry).path(); + if path == tgt { try!(fs::remove_dir_all(&tgt)) } + } + try!(fs::create_dir(&tgt)); + + // Copy static files + let css = include_bytes!("static/rustbook.css"); + let js = include_bytes!("static/rustbook.js"); + + let mut css_file = try!(File::create(tgt.join("rustbook.css"))); + try!(css_file.write_all(css)); + + let mut js_file = try!(File::create(tgt.join("rustbook.js"))); + try!(js_file.write_all(js)); + + + let mut summary = try!(File::open(&src.join("SUMMARY.md"))); + match book::parse_summary(&mut summary, &src) { + Ok(book) => { + // execute rustdoc on the whole book + render(&book, &tgt) + } + Err(errors) => { + let n = errors.len(); + for err in errors { + term.err(&format!("error: {}", err)[..]); + } + + Err(err(&format!("{} errors occurred", n))) + } + } + } +} diff --git a/src/tools/rustbook/error.rs b/src/tools/rustbook/error.rs new file mode 100644 index 00000000000..e896dee2791 --- /dev/null +++ b/src/tools/rustbook/error.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. + +//! Error handling utilities. WIP. + +use std::error::Error; +use std::fmt; + +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 fn err(s: &str) -> CliError { + #[derive(Debug)] + struct E(String); + + impl Error for E { + fn description(&self) -> &str { &self.0 } + } + impl fmt::Display for E { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } + } + + Box::new(E(s.to_string())) +} diff --git a/src/tools/rustbook/help.rs b/src/tools/rustbook/help.rs new file mode 100644 index 00000000000..c90c2b93609 --- /dev/null +++ b/src/tools/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::new(Help)), + _ => 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: rustbook <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/tools/rustbook/main.rs b/src/tools/rustbook/main.rs new file mode 100644 index 00000000000..bd4fc899293 --- /dev/null +++ b/src/tools/rustbook/main.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. + +#![deny(warnings)] + +#![feature(iter_arith)] +#![feature(rustc_private)] +#![feature(rustdoc)] + +extern crate rustdoc; +extern crate rustc_back; + +use std::env; +use std::error::Error; +use std::process; +use std::sync::atomic::{AtomicIsize, ATOMIC_ISIZE_INIT, Ordering}; +use subcommand::Subcommand; +use term::Term; + +mod term; +mod error; +mod book; + +mod subcommand; +mod help; +mod build; +mod serve; +mod test; + +static EXIT_STATUS: AtomicIsize = ATOMIC_ISIZE_INIT; + +pub fn main() { + let mut term = Term::new(); + let cmd: Vec<_> = env::args().collect(); + + if cmd.len() <= 1 { + help::usage() + } else { + match subcommand::parse_name(&cmd[1][..]) { + Some(mut subcmd) => { + match subcmd.parse_args(&cmd[..cmd.len()-1]) { + Ok(_) => { + match subcmd.execute(&mut term) { + Ok(_) => (), + Err(err) => { + term.err(&format!("error: {}", err)); + } + } + } + Err(err) => { + println!("{}", err.description()); + println!(""); + subcmd.usage(); + } + } + } + None => { + println!("Unrecognized command '{}'.", cmd[1]); + println!(""); + help::usage(); + } + } + } + process::exit(EXIT_STATUS.load(Ordering::SeqCst) as i32); +} diff --git a/src/tools/rustbook/serve.rs b/src/tools/rustbook/serve.rs new file mode 100644 index 00000000000..2fa7b7eed7b --- /dev/null +++ b/src/tools/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::new(Serve)) + } 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/tools/rustbook/static/rustbook.css b/src/tools/rustbook/static/rustbook.css new file mode 100644 index 00000000000..ba0151fa2ed --- /dev/null +++ b/src/tools/rustbook/static/rustbook.css @@ -0,0 +1,157 @@ +/** + * Copyright 2013-2015 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. + */ + +@import url('../rust.css'); + +body { + max-width: none; + font: 16px/1.6 'Source Serif Pro', Georgia, Times, 'Times New Roman', serif; + color: #333; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Open Sans', 'Fira Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: bold; + color: #333; +} + +@media only screen { + #toc { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 300px; + overflow-y: auto; + border-right: 1px solid #e8e8e8; + padding: 0 15px; + font-size: 14px; + background-color: #fafafa; + -webkit-overflow-scrolling: touch; + } + + #page-wrapper { + position: absolute; + top: 0; + left: 300px; + right: 0; + padding: 0 15px; + -webkit-overflow-scrolling: touch; + } +} + +@media only print { + #toc, #nav { + display: none; + } +} + +@media only screen and (max-width: 1023px) { + #toc { + width: 100%; + top: 40px; + } + + #page-wrapper { + top: 40px; + left: 0; + } + + .mobile-hidden { + display: none; + } +} + +#page { + margin: 0 auto; + max-width: 750px; + padding-bottom: 50px; +} + +.chapter { + list-style: none; + padding-left: 0; + line-height: 30px; +} + +.section { + list-style: none; + padding-left: 20px; + line-height: 40px; +} + +.section li { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.chapter li a { + color: #333; + padding: 5px 0; +} + +.chapter li a.active, +.chapter li a:hover { + color: #008cff; + text-decoration: none; +} + +#toggle-nav { + cursor: pointer; + margin-top: 5px; + width: 30px; + height: 30px; + background-color: #fff; + border: 1px solid #666; + border-radius: 3px; + padding: 3px 3px 0 3px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.bar { + display: block; + background-color: #000; + border-radius: 2px; + width: 100%; + height: 2px; + margin: 2px 0 3px; + padding: 0; +} + +pre { + padding: 11px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f7f7f7; + border: 0; + border-radius: 3px; +} + +.left { + float: left; +} + +.right { + float: right; +} diff --git a/src/tools/rustbook/static/rustbook.js b/src/tools/rustbook/static/rustbook.js new file mode 100644 index 00000000000..d8ab15260ed --- /dev/null +++ b/src/tools/rustbook/static/rustbook.js @@ -0,0 +1,78 @@ +// Copyright 2014-2015 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. + +/*jslint browser: true, es5: true */ +/*globals $: true, rootPath: true */ + +document.addEventListener('DOMContentLoaded', function() { + 'use strict'; + + document.getElementById('toggle-nav').onclick = function(e) { + var toc = document.getElementById('toc'); + var pagewrapper = document.getElementById('page-wrapper'); + toggleClass(toc, 'mobile-hidden'); + toggleClass(pagewrapper, 'mobile-hidden'); + }; + + function toggleClass(el, className) { + // from http://youmightnotneedjquery.com/ + if (el.classList) { + el.classList.toggle(className); + } else { + var classes = el.className.split(' '); + var existingIndex = classes.indexOf(className); + + if (existingIndex >= 0) { + classes.splice(existingIndex, 1); + } else { + classes.push(className); + } + + el.className = classes.join(' '); + } + } + + // The below code is used to add prev and next navigation links to the + // bottom of each of the sections. + // It works by extracting the current page based on the url and iterates + // over the menu links until it finds the menu item for the current page. We + // then create a copy of the preceding and following menu links and add the + // correct css class and insert them into the bottom of the page. + var toc = document.getElementById('toc').getElementsByTagName('a'); + var href = document.location.pathname.split('/').pop(); + + if (href === 'index.html' || href === '') { + href = 'README.html'; + } + + for (var i = 0; i < toc.length; i++) { + if (toc[i].attributes.href.value.split('/').pop() === href) { + var nav = document.createElement('p'); + + if (i > 0) { + var prevNode = toc[i-1].cloneNode(true); + prevNode.className = 'left'; + prevNode.setAttribute('rel', 'prev'); + nav.appendChild(prevNode); + } + + if (i < toc.length - 1) { + var nextNode = toc[i+1].cloneNode(true); + nextNode.className = 'right'; + nextNode.setAttribute('rel', 'next'); + nav.appendChild(nextNode); + } + + document.getElementById('page').appendChild(nav); + + break; + } + } +}); diff --git a/src/tools/rustbook/subcommand.rs b/src/tools/rustbook/subcommand.rs new file mode 100644 index 00000000000..a66c2b4f302 --- /dev/null +++ b/src/tools/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 rustbook 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>> { + let cmds: [fn(&str) -> Option<Box<Subcommand>>; 4] = [help::parse_cmd, + build::parse_cmd, + serve::parse_cmd, + test::parse_cmd]; + for parser in &cmds { + let parsed = (*parser)(name); + if parsed.is_some() { return parsed } + } + None +} diff --git a/src/tools/rustbook/term.rs b/src/tools/rustbook/term.rs new file mode 100644 index 00000000000..cdd25e67c8f --- /dev/null +++ b/src/tools/rustbook/term.rs @@ -0,0 +1,34 @@ +// 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; +use std::io::prelude::*; +use std::sync::atomic::Ordering; + +pub struct Term { + err: Box<Write + 'static> +} + +impl Term { + pub fn new() -> Term { + Term { + err: Box::new(io::stderr()) + } + } + + pub fn err(&mut self, msg: &str) { + // swallow any errors + let _ = writeln!(&mut self.err, "{}", msg); + ::EXIT_STATUS.store(101, Ordering::SeqCst); + } +} diff --git a/src/tools/rustbook/test.rs b/src/tools/rustbook/test.rs new file mode 100644 index 00000000000..72df0768e7b --- /dev/null +++ b/src/tools/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::{err, CliResult, CommandResult}; +use term::Term; +use book; + +use std::fs::File; +use std::env; +use std::process::Command; + +struct Test; + +pub fn parse_cmd(name: &str) -> Option<Box<Subcommand>> { + if name == "test" { + Some(Box::new(Test)) + } 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 = env::current_dir().unwrap(); + let src = cwd.clone(); + + let mut summary = try!(File::open(&src.join("SUMMARY.md"))); + match book::parse_summary(&mut 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.stdout), + String::from_utf8_lossy(&output.stderr))); + return Err(err("some tests failed")); + } + + } + Err(e) => { + let message = format!("could not execute `rustdoc`: {}", e); + return Err(err(&message)) + } + } + } + } + Err(errors) => { + for err in errors { + term.err(&err[..]); + } + return Err(err("there was an error")) + } + } + Ok(()) // lol + } +} |
