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