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