Skip to content

Commit 69b8ef8

Browse files
committed
rustdoc: run on plain Markdown files.
This theoretically gives rustdoc the ability to render our guides, tutorial and manual (not in practice, since the files themselves need to be adjusted slightly to use Sundown-compatible functionality). Fixes #11392.
1 parent e959c87 commit 69b8ef8

File tree

5 files changed

+292
-28
lines changed

5 files changed

+292
-28
lines changed

src/doc/rustdoc.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,28 @@ rustdoc will implicitly add `extern crate <crate>;` where `<crate>` is the name
181181
the crate being tested to the top of each code example. This means that rustdoc
182182
must be able to find a compiled version of the library crate being tested. Extra
183183
search paths may be added via the `-L` flag to `rustdoc`.
184+
185+
# Standalone Markdown files
186+
187+
As well as Rust crates, rustdoc supports rendering pure Markdown files
188+
into HTML and testing the code snippets from them. A Markdown file is
189+
detected by a `.md` or `.markdown` extension.
190+
191+
There are 4 options to modify the output that Rustdoc creates.
192+
- `--markdown-css PATH`: adds a `<link rel="stylesheet">` tag pointing to `PATH`.
193+
- `--markdown-in-header FILE`: includes the contents of `FILE` at the
194+
end of the `<head>...</head>` section.
195+
- `--markdown-before-content FILE`: includes the contents of `FILE`
196+
directly after `<body>`, before the rendered content (including the
197+
title).
198+
- `--markdown-after-content FILE`: includes the contents of `FILE`
199+
directly before `</body>`, after all the rendered content.
200+
201+
All of these can be specified multiple times, and they are output in
202+
the order in which they are specified. The first line of the file must
203+
be the title, prefixed with `%` (e.g. this page has `% Rust
204+
Documentation` on the first line).
205+
206+
Like with a Rust crate, the `--test` argument will run the code
207+
examples to check they compile, and obeys any `--test-args` flags. The
208+
tests are named after the last `#` heading.

src/librustdoc/html/markdown.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828

2929
use std::cast;
3030
use std::fmt;
31-
use std::intrinsics;
3231
use std::io;
3332
use std::libc;
3433
use std::local_data;
@@ -258,14 +257,27 @@ pub fn find_testable_code(doc: &str, tests: &mut ::test::Collector) {
258257
};
259258
if ignore { return }
260259
vec::raw::buf_as_slice((*text).data, (*text).size as uint, |text| {
261-
let tests: &mut ::test::Collector = intrinsics::transmute(opaque);
260+
let tests = &mut *(opaque as *mut ::test::Collector);
262261
let text = str::from_utf8(text).unwrap();
263262
let mut lines = text.lines().map(|l| stripped_filtered_line(l).unwrap_or(l));
264263
let text = lines.to_owned_vec().connect("\n");
265264
tests.add_test(text, should_fail, no_run);
266265
})
267266
}
268267
}
268+
extern fn header(_ob: *buf, text: *buf, level: libc::c_int, opaque: *libc::c_void) {
269+
unsafe {
270+
let tests = &mut *(opaque as *mut ::test::Collector);
271+
if text.is_null() {
272+
tests.register_header("", level as u32);
273+
} else {
274+
vec::raw::buf_as_slice((*text).data, (*text).size as uint, |text| {
275+
let text = str::from_utf8(text).unwrap();
276+
tests.register_header(text, level as u32);
277+
})
278+
}
279+
}
280+
}
269281

270282
unsafe {
271283
let ob = bufnew(OUTPUT_UNIT);
@@ -276,7 +288,7 @@ pub fn find_testable_code(doc: &str, tests: &mut ::test::Collector) {
276288
blockcode: Some(block),
277289
blockquote: None,
278290
blockhtml: None,
279-
header: None,
291+
header: Some(header),
280292
other: mem::init()
281293
};
282294

src/librustdoc/lib.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
#[crate_type = "dylib"];
1515
#[crate_type = "rlib"];
1616

17-
#[feature(globs, struct_variant, managed_boxes)];
17+
#[feature(globs, struct_variant, managed_boxes, macro_rules)];
1818

1919
extern crate syntax;
2020
extern crate rustc;
@@ -26,6 +26,7 @@ extern crate collections;
2626
extern crate testing = "test";
2727
extern crate time;
2828

29+
use std::cell::RefCell;
2930
use std::local_data;
3031
use std::io;
3132
use std::io::{File, MemWriter};
@@ -44,6 +45,7 @@ pub mod html {
4445
pub mod markdown;
4546
pub mod render;
4647
}
48+
pub mod markdown;
4749
pub mod passes;
4850
pub mod plugins;
4951
pub mod visit_ast;
@@ -105,6 +107,19 @@ pub fn opts() -> ~[getopts::OptGroup] {
105107
optflag("", "test", "run code examples as tests"),
106108
optmulti("", "test-args", "arguments to pass to the test runner",
107109
"ARGS"),
110+
optmulti("", "markdown-css", "CSS files to include via <link> in a rendered Markdown file",
111+
"FILES"),
112+
optmulti("", "markdown-in-header",
113+
"files to include inline in the <head> section of a rendered Markdown file",
114+
"FILES"),
115+
optmulti("", "markdown-before-content",
116+
"files to include inline between <body> and the content of a rendered \
117+
Markdown file",
118+
"FILES"),
119+
optmulti("", "markdown-after-content",
120+
"files to include inline between the content and </body> of a rendered \
121+
Markdown file",
122+
"FILES"),
108123
]
109124
}
110125

@@ -137,8 +152,24 @@ pub fn main_args(args: &[~str]) -> int {
137152
}
138153
let input = matches.free[0].as_slice();
139154

140-
if matches.opt_present("test") {
141-
return test::run(input, &matches);
155+
let libs = matches.opt_strs("L").map(|s| Path::new(s.as_slice()));
156+
let libs = @RefCell::new(libs.move_iter().collect());
157+
158+
let test_args = matches.opt_strs("test-args");
159+
let test_args = test_args.iter().flat_map(|s| s.words()).map(|s| s.to_owned()).to_owned_vec();
160+
161+
let should_test = matches.opt_present("test");
162+
let markdown_input = input.ends_with(".md") || input.ends_with(".markdown");
163+
164+
let output = matches.opt_str("o").map(|s| Path::new(s));
165+
166+
match (should_test, markdown_input) {
167+
(true, true) => return markdown::test(input, libs, test_args),
168+
(true, false) => return test::run(input, libs, test_args),
169+
170+
(false, true) => return markdown::render(input, output.unwrap_or(Path::new("doc")),
171+
&matches),
172+
(false, false) => {}
142173
}
143174

144175
if matches.opt_strs("passes") == ~[~"list"] {
@@ -163,7 +194,6 @@ pub fn main_args(args: &[~str]) -> int {
163194

164195
info!("going to format");
165196
let started = time::precise_time_ns();
166-
let output = matches.opt_str("o").map(|s| Path::new(s));
167197
match matches.opt_str("w") {
168198
Some(~"html") | None => {
169199
match html::render::run(krate, output.unwrap_or(Path::new("doc"))) {

src/librustdoc/markdown.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
use std::{str, io};
12+
use std::cell::RefCell;
13+
use std::vec_ng::Vec;
14+
15+
use collections::HashSet;
16+
17+
use getopts;
18+
use testing;
19+
20+
use html::escape::Escape;
21+
use html::markdown::{Markdown, find_testable_code, reset_headers};
22+
use test::Collector;
23+
24+
fn load_string(input: &Path) -> io::IoResult<Option<~str>> {
25+
let mut f = try!(io::File::open(input));
26+
let d = try!(f.read_to_end());
27+
Ok(str::from_utf8_owned(d))
28+
}
29+
macro_rules! load_or_return {
30+
($input: expr, $cant_read: expr, $not_utf8: expr) => {
31+
{
32+
let input = Path::new($input);
33+
match load_string(&input) {
34+
Err(e) => {
35+
let _ = writeln!(&mut io::stderr(),
36+
"error reading `{}`: {}", input.display(), e);
37+
return $cant_read;
38+
}
39+
Ok(None) => {
40+
let _ = writeln!(&mut io::stderr(),
41+
"error reading `{}`: not UTF-8", input.display());
42+
return $not_utf8;
43+
}
44+
Ok(Some(s)) => s
45+
}
46+
}
47+
}
48+
}
49+
50+
/// Separate any lines at the start of the file that begin with `%`.
51+
fn extract_leading_metadata<'a>(s: &'a str) -> (Vec<&'a str>, &'a str) {
52+
let mut metadata = Vec::new();
53+
for line in s.lines() {
54+
if line.starts_with("%") {
55+
// remove %<whitespace>
56+
metadata.push(line.slice_from(1).trim_left())
57+
} else {
58+
let line_start_byte = s.subslice_offset(line);
59+
return (metadata, s.slice_from(line_start_byte));
60+
}
61+
}
62+
// if we're here, then all lines were metadata % lines.
63+
(metadata, "")
64+
}
65+
66+
fn load_external_files(names: &[~str]) -> Option<~str> {
67+
let mut out = ~"";
68+
for name in names.iter() {
69+
out.push_str(load_or_return!(name.as_slice(), None, None));
70+
out.push_char('\n');
71+
}
72+
Some(out)
73+
}
74+
75+
/// Render `input` (e.g. "foo.md") into an HTML file in `output`
76+
/// (e.g. output = "bar" => "bar/foo.html").
77+
pub fn render(input: &str, mut output: Path, matches: &getopts::Matches) -> int {
78+
let input_p = Path::new(input);
79+
output.push(input_p.filestem().unwrap());
80+
output.set_extension("html");
81+
82+
let mut css = ~"";
83+
for name in matches.opt_strs("markdown-css").iter() {
84+
let s = format!("<link rel=\"stylesheet\" type=\"text/css\" href=\"{}\">\n", name);
85+
css.push_str(s)
86+
}
87+
88+
let input_str = load_or_return!(input, 1, 2);
89+
90+
let (in_header, before_content, after_content) =
91+
match (load_external_files(matches.opt_strs("markdown-in-header")),
92+
load_external_files(matches.opt_strs("markdown-before-content")),
93+
load_external_files(matches.opt_strs("markdown-after-content"))) {
94+
(Some(a), Some(b), Some(c)) => (a,b,c),
95+
_ => return 3
96+
};
97+
98+
let mut out = match io::File::create(&output) {
99+
Err(e) => {
100+
let _ = writeln!(&mut io::stderr(),
101+
"error opening `{}` for writing: {}",
102+
output.display(), e);
103+
return 4;
104+
}
105+
Ok(f) => f
106+
};
107+
108+
let (metadata, text) = extract_leading_metadata(input_str);
109+
if metadata.len() == 0 {
110+
let _ = writeln!(&mut io::stderr(),
111+
"invalid markdown file: expecting initial line with `% ...TITLE...`");
112+
return 5;
113+
}
114+
let title = metadata.get(0).as_slice();
115+
116+
reset_headers();
117+
118+
let err = write!(
119+
&mut out,
120+
r#"<!doctype html>
121+
<html lang="en">
122+
<head>
123+
<meta charset="utf-8">
124+
<meta name="generator" content="rustdoc">
125+
<title>{title}</title>
126+
127+
{css}
128+
{in_header}
129+
</head>
130+
<body>
131+
<!--[if lte IE 8]>
132+
<div class="warning">
133+
This old browser is unsupported and will most likely display funky
134+
things.
135+
</div>
136+
<![endif]-->
137+
138+
{before_content}
139+
<h1 class="title">{title}</h1>
140+
{text}
141+
{after_content}
142+
</body>
143+
</html>"#,
144+
title = Escape(title),
145+
css = css,
146+
in_header = in_header,
147+
before_content = before_content,
148+
text = Markdown(text),
149+
after_content = after_content);
150+
151+
match err {
152+
Err(e) => {
153+
let _ = writeln!(&mut io::stderr(),
154+
"error writing to `{}`: {}",
155+
output.display(), e);
156+
6
157+
}
158+
Ok(_) => 0
159+
}
160+
}
161+
162+
/// Run any tests/code examples in the markdown file `input`.
163+
pub fn test(input: &str, libs: @RefCell<HashSet<Path>>, mut test_args: ~[~str]) -> int {
164+
let input_str = load_or_return!(input, 1, 2);
165+
166+
let mut collector = Collector::new(input.to_owned(), libs, true);
167+
find_testable_code(input_str, &mut collector);
168+
test_args.unshift(~"rustdoctest");
169+
testing::test_main(test_args, collector.tests);
170+
0
171+
}

0 commit comments

Comments
 (0)