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