]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/highlight.rs
Rollup merge of #54967 - holmgr:master, r=estebank
[rust.git] / src / librustdoc / html / highlight.rs
1 // Copyright 2014-2016 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 //! Basic syntax highlighting functionality.
12 //!
13 //! This module uses libsyntax's lexer to provide token-based highlighting for
14 //! the HTML documentation generated by rustdoc.
15 //!
16 //! Use the `render_with_highlighting` to highlight some rust code.
17
18 use html::escape::Escape;
19
20 use std::fmt::Display;
21 use std::io;
22 use std::io::prelude::*;
23
24 use syntax::source_map::{SourceMap, FilePathMapping};
25 use syntax::parse::lexer::{self, TokenAndSpan};
26 use syntax::parse::token;
27 use syntax::parse;
28 use syntax_pos::{Span, FileName};
29
30 /// Highlights `src`, returning the HTML output.
31 pub fn render_with_highlighting(src: &str, class: Option<&str>,
32                                 extension: Option<&str>,
33                                 tooltip: Option<(&str, &str)>) -> String {
34     debug!("highlighting: ================\n{}\n==============", src);
35     let sess = parse::ParseSess::new(FilePathMapping::empty());
36     let fm = sess.source_map().new_source_file(FileName::Custom("stdin".to_string()),
37                                                src.to_string());
38
39     let mut out = Vec::new();
40     if let Some((tooltip, class)) = tooltip {
41         write!(out, "<div class='information'><div class='tooltip {}'>ⓘ<span \
42                      class='tooltiptext'>{}</span></div></div>",
43                class, tooltip).unwrap();
44     }
45     write_header(class, &mut out).unwrap();
46
47     let lexer = match lexer::StringReader::new_without_err(&sess, fm, None, "Output from rustc:") {
48         Ok(l) => l,
49         Err(_) => {
50             let first_line = src.lines().next().unwrap_or_else(|| "");
51             let mut err = sess.span_diagnostic
52                               .struct_warn(&format!("Invalid doc comment starting with: `{}`\n\
53                                                      (Ignoring this codeblock)",
54                                                     first_line));
55             err.emit();
56             return String::new();
57         }
58     };
59     let mut classifier = Classifier::new(lexer, sess.source_map());
60     if classifier.write_source(&mut out).is_err() {
61         classifier.lexer.emit_fatal_errors();
62         return format!("<pre>{}</pre>", src);
63     }
64
65     if let Some(extension) = extension {
66         write!(out, "{}", extension).unwrap();
67     }
68     write_footer(&mut out).unwrap();
69     String::from_utf8_lossy(&out[..]).into_owned()
70 }
71
72 /// Processes a program (nested in the internal `lexer`), classifying strings of
73 /// text by highlighting category (`Class`). Calls out to a `Writer` to write
74 /// each span of text in sequence.
75 struct Classifier<'a> {
76     lexer: lexer::StringReader<'a>,
77     source_map: &'a SourceMap,
78
79     // State of the classifier.
80     in_attribute: bool,
81     in_macro: bool,
82     in_macro_nonterminal: bool,
83 }
84
85 /// How a span of text is classified. Mostly corresponds to token kinds.
86 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
87 enum Class {
88     None,
89     Comment,
90     DocComment,
91     Attribute,
92     KeyWord,
93     // Keywords that do pointer/reference stuff.
94     RefKeyWord,
95     Self_,
96     Op,
97     Macro,
98     MacroNonTerminal,
99     String,
100     Number,
101     Bool,
102     Ident,
103     Lifetime,
104     PreludeTy,
105     PreludeVal,
106     QuestionMark,
107 }
108
109 /// Trait that controls writing the output of syntax highlighting. Users should
110 /// implement this trait to customize writing output.
111 ///
112 /// The classifier will call into the `Writer` implementation as it finds spans
113 /// of text to highlight. Exactly how that text should be highlighted is up to
114 /// the implementation.
115 trait Writer {
116     /// Called when we start processing a span of text that should be highlighted.
117     /// The `Class` argument specifies how it should be highlighted.
118     fn enter_span(&mut self, _: Class) -> io::Result<()>;
119
120     /// Called at the end of a span of highlighted text.
121     fn exit_span(&mut self) -> io::Result<()>;
122
123     /// Called for a span of text.  If the text should be highlighted differently from the
124     /// surrounding text, then the `Class` argument will be a value other than `None`.
125     ///
126     /// The following sequences of callbacks are equivalent:
127     /// ```plain
128     ///     enter_span(Foo), string("text", None), exit_span()
129     ///     string("text", Foo)
130     /// ```
131     /// The latter can be thought of as a shorthand for the former, which is
132     /// more flexible.
133     fn string<T: Display>(&mut self,
134                           text: T,
135                           klass: Class)
136                           -> io::Result<()>;
137 }
138
139 // Implement `Writer` for anthing that can be written to, this just implements
140 // the default rustdoc behaviour.
141 impl<U: Write> Writer for U {
142     fn string<T: Display>(&mut self,
143                           text: T,
144                           klass: Class)
145                           -> io::Result<()> {
146         match klass {
147             Class::None => write!(self, "{}", text),
148             klass => write!(self, "<span class=\"{}\">{}</span>", klass.rustdoc_class(), text),
149         }
150     }
151
152     fn enter_span(&mut self, klass: Class) -> io::Result<()> {
153         write!(self, "<span class=\"{}\">", klass.rustdoc_class())
154     }
155
156     fn exit_span(&mut self) -> io::Result<()> {
157         write!(self, "</span>")
158     }
159 }
160
161 impl<'a> Classifier<'a> {
162     fn new(lexer: lexer::StringReader<'a>, source_map: &'a SourceMap) -> Classifier<'a> {
163         Classifier {
164             lexer,
165             source_map,
166             in_attribute: false,
167             in_macro: false,
168             in_macro_nonterminal: false,
169         }
170     }
171
172     /// Gets the next token out of the lexer, emitting fatal errors if lexing fails.
173     fn try_next_token(&mut self) -> io::Result<TokenAndSpan> {
174         match self.lexer.try_next_token() {
175             Ok(tas) => Ok(tas),
176             Err(_) => {
177                 let mut err = self.lexer.sess.span_diagnostic
178                                   .struct_warn("Backing out of syntax highlighting");
179                 err.note("You probably did not intend to render this as a rust code-block");
180                 err.emit();
181                 Err(io::Error::new(io::ErrorKind::Other, ""))
182             }
183         }
184     }
185
186     /// Exhausts the `lexer` writing the output into `out`.
187     ///
188     /// The general structure for this method is to iterate over each token,
189     /// possibly giving it an HTML span with a class specifying what flavor of token
190     /// is used. All source code emission is done as slices from the source map,
191     /// not from the tokens themselves, in order to stay true to the original
192     /// source.
193     fn write_source<W: Writer>(&mut self,
194                                    out: &mut W)
195                                    -> io::Result<()> {
196         loop {
197             let next = self.try_next_token()?;
198             if next.tok == token::Eof {
199                 break;
200             }
201
202             self.write_token(out, next)?;
203         }
204
205         Ok(())
206     }
207
208     // Handles an individual token from the lexer.
209     fn write_token<W: Writer>(&mut self,
210                               out: &mut W,
211                               tas: TokenAndSpan)
212                               -> io::Result<()> {
213         let klass = match tas.tok {
214             token::Shebang(s) => {
215                 out.string(Escape(&s.as_str()), Class::None)?;
216                 return Ok(());
217             },
218
219             token::Whitespace => Class::None,
220             token::Comment => Class::Comment,
221             token::DocComment(..) => Class::DocComment,
222
223             // If this '&' or '*' token is followed by a non-whitespace token, assume that it's the
224             // reference or dereference operator or a reference or pointer type, instead of the
225             // bit-and or multiplication operator.
226             token::BinOp(token::And) | token::BinOp(token::Star)
227                 if self.lexer.peek().tok != token::Whitespace => Class::RefKeyWord,
228
229             // Consider this as part of a macro invocation if there was a
230             // leading identifier.
231             token::Not if self.in_macro => {
232                 self.in_macro = false;
233                 Class::Macro
234             }
235
236             // Operators.
237             token::Eq | token::Lt | token::Le | token::EqEq | token::Ne | token::Ge | token::Gt |
238                 token::AndAnd | token::OrOr | token::Not | token::BinOp(..) | token::RArrow |
239                 token::BinOpEq(..) | token::FatArrow => Class::Op,
240
241             // Miscellaneous, no highlighting.
242             token::Dot | token::DotDot | token::DotDotDot | token::DotDotEq | token::Comma |
243                 token::Semi | token::Colon | token::ModSep | token::LArrow | token::OpenDelim(_) |
244                 token::CloseDelim(token::Brace) | token::CloseDelim(token::Paren) |
245                 token::CloseDelim(token::NoDelim) => Class::None,
246
247             token::Question => Class::QuestionMark,
248
249             token::Dollar => {
250                 if self.lexer.peek().tok.is_ident() {
251                     self.in_macro_nonterminal = true;
252                     Class::MacroNonTerminal
253                 } else {
254                     Class::None
255                 }
256             }
257
258             // This might be the start of an attribute. We're going to want to
259             // continue highlighting it as an attribute until the ending ']' is
260             // seen, so skip out early. Down below we terminate the attribute
261             // span when we see the ']'.
262             token::Pound => {
263                 // We can't be sure that our # begins an attribute (it could
264                 // just be appearing in a macro) until we read either `#![` or
265                 // `#[` from the input stream.
266                 //
267                 // We don't want to start highlighting as an attribute until
268                 // we're confident there is going to be a ] coming up, as
269                 // otherwise # tokens in macros highlight the rest of the input
270                 // as an attribute.
271
272                 // Case 1: #![inner_attribute]
273                 if self.lexer.peek().tok == token::Not {
274                     self.try_next_token()?; // NOTE: consumes `!` token!
275                     if self.lexer.peek().tok == token::OpenDelim(token::Bracket) {
276                         self.in_attribute = true;
277                         out.enter_span(Class::Attribute)?;
278                     }
279                     out.string("#", Class::None)?;
280                     out.string("!", Class::None)?;
281                     return Ok(());
282                 }
283
284                 // Case 2: #[outer_attribute]
285                 if self.lexer.peek().tok == token::OpenDelim(token::Bracket) {
286                     self.in_attribute = true;
287                     out.enter_span(Class::Attribute)?;
288                 }
289                 out.string("#", Class::None)?;
290                 return Ok(());
291             }
292             token::CloseDelim(token::Bracket) => {
293                 if self.in_attribute {
294                     self.in_attribute = false;
295                     out.string("]", Class::None)?;
296                     out.exit_span()?;
297                     return Ok(());
298                 } else {
299                     Class::None
300                 }
301             }
302
303             token::Literal(lit, _suf) => {
304                 match lit {
305                     // Text literals.
306                     token::Byte(..) | token::Char(..) |
307                         token::ByteStr(..) | token::ByteStrRaw(..) |
308                         token::Str_(..) | token::StrRaw(..) => Class::String,
309
310                     // Number literals.
311                     token::Integer(..) | token::Float(..) => Class::Number,
312                 }
313             }
314
315             // Keywords are also included in the identifier set.
316             token::Ident(ident, is_raw) => {
317                 match &*ident.as_str() {
318                     "ref" | "mut" if !is_raw => Class::RefKeyWord,
319
320                     "self" | "Self" => Class::Self_,
321                     "false" | "true" if !is_raw => Class::Bool,
322
323                     "Option" | "Result" => Class::PreludeTy,
324                     "Some" | "None" | "Ok" | "Err" => Class::PreludeVal,
325
326                     "$crate" => Class::KeyWord,
327                     _ if tas.tok.is_reserved_ident() => Class::KeyWord,
328
329                     _ => {
330                         if self.in_macro_nonterminal {
331                             self.in_macro_nonterminal = false;
332                             Class::MacroNonTerminal
333                         } else if self.lexer.peek().tok == token::Not {
334                             self.in_macro = true;
335                             Class::Macro
336                         } else {
337                             Class::Ident
338                         }
339                     }
340                 }
341             }
342
343             token::Lifetime(..) => Class::Lifetime,
344
345             token::Eof | token::Interpolated(..) |
346             token::Tilde | token::At | token::DotEq | token::SingleQuote => Class::None,
347         };
348
349         // Anything that didn't return above is the simple case where we the
350         // class just spans a single token, so we can use the `string` method.
351         out.string(Escape(&self.snip(tas.sp)), klass)
352     }
353
354     // Helper function to get a snippet from the source_map.
355     fn snip(&self, sp: Span) -> String {
356         self.source_map.span_to_snippet(sp).unwrap()
357     }
358 }
359
360 impl Class {
361     /// Returns the css class expected by rustdoc for each `Class`.
362     fn rustdoc_class(self) -> &'static str {
363         match self {
364             Class::None => "",
365             Class::Comment => "comment",
366             Class::DocComment => "doccomment",
367             Class::Attribute => "attribute",
368             Class::KeyWord => "kw",
369             Class::RefKeyWord => "kw-2",
370             Class::Self_ => "self",
371             Class::Op => "op",
372             Class::Macro => "macro",
373             Class::MacroNonTerminal => "macro-nonterminal",
374             Class::String => "string",
375             Class::Number => "number",
376             Class::Bool => "bool-val",
377             Class::Ident => "ident",
378             Class::Lifetime => "lifetime",
379             Class::PreludeTy => "prelude-ty",
380             Class::PreludeVal => "prelude-val",
381             Class::QuestionMark => "question-mark"
382         }
383     }
384 }
385
386 fn write_header(class: Option<&str>, out: &mut dyn Write) -> io::Result<()> {
387     write!(out, "<pre class=\"rust {}\">\n", class.unwrap_or(""))
388 }
389
390 fn write_footer(out: &mut dyn Write) -> io::Result<()> {
391     write!(out, "</pre>\n")
392 }