]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/highlight.rs
Rollup merge of #100392 - nnethercote:simplify-visitors, r=cjgillot
[rust.git] / src / librustdoc / html / highlight.rs
1 //! Basic syntax highlighting functionality.
2 //!
3 //! This module uses librustc_ast'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::clean::PrimitiveType;
9 use crate::html::escape::Escape;
10 use crate::html::render::Context;
11
12 use std::collections::VecDeque;
13 use std::fmt::{Display, Write};
14
15 use rustc_data_structures::fx::FxHashMap;
16 use rustc_lexer::{LiteralKind, TokenKind};
17 use rustc_span::edition::Edition;
18 use rustc_span::symbol::Symbol;
19 use rustc_span::{BytePos, Span, DUMMY_SP};
20
21 use super::format::{self, Buffer};
22 use super::render::LinkFromSrc;
23
24 /// This type is needed in case we want to render links on items to allow to go to their definition.
25 pub(crate) struct HrefContext<'a, 'b, 'c> {
26     pub(crate) context: &'a Context<'b>,
27     /// This span contains the current file we're going through.
28     pub(crate) file_span: Span,
29     /// This field is used to know "how far" from the top of the directory we are to link to either
30     /// documentation pages or other source pages.
31     pub(crate) root_path: &'c str,
32 }
33
34 /// Decorations are represented as a map from CSS class to vector of character ranges.
35 /// Each range will be wrapped in a span with that class.
36 #[derive(Default)]
37 pub(crate) struct DecorationInfo(pub(crate) FxHashMap<&'static str, Vec<(u32, u32)>>);
38
39 #[derive(Eq, PartialEq, Clone, Copy)]
40 pub(crate) enum Tooltip {
41     Ignore,
42     CompileFail,
43     ShouldPanic,
44     Edition(Edition),
45     None,
46 }
47
48 /// Highlights `src` as an inline example, returning the HTML output.
49 pub(crate) fn render_example_with_highlighting(
50     src: &str,
51     out: &mut Buffer,
52     tooltip: Tooltip,
53     playground_button: Option<&str>,
54 ) {
55     let class = match tooltip {
56         Tooltip::Ignore => " ignore",
57         Tooltip::CompileFail => " compile_fail",
58         Tooltip::ShouldPanic => " should_panic",
59         Tooltip::Edition(_) => " edition",
60         Tooltip::None => "",
61     };
62
63     if tooltip != Tooltip::None {
64         write!(
65             out,
66             "<div class='information'><div class='tooltip{}'{}>ⓘ</div></div>",
67             class,
68             if let Tooltip::Edition(edition_info) = tooltip {
69                 format!(" data-edition=\"{}\"", edition_info)
70             } else {
71                 String::new()
72             },
73         );
74     }
75
76     write_header(out, &format!("rust-example-rendered{}", class), None);
77     write_code(out, src, None, None);
78     write_footer(out, playground_button);
79 }
80
81 /// Highlights `src` as a macro, returning the HTML output.
82 pub(crate) fn render_macro_with_highlighting(src: &str, out: &mut Buffer) {
83     write_header(out, "macro", None);
84     write_code(out, src, None, None);
85     write_footer(out, None);
86 }
87
88 /// Highlights `src` as a source code page, returning the HTML output.
89 pub(crate) fn render_source_with_highlighting(
90     src: &str,
91     out: &mut Buffer,
92     line_numbers: Buffer,
93     href_context: HrefContext<'_, '_, '_>,
94     decoration_info: DecorationInfo,
95 ) {
96     write_header(out, "", Some(line_numbers));
97     write_code(out, src, Some(href_context), Some(decoration_info));
98     write_footer(out, None);
99 }
100
101 fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>) {
102     write!(out, "<div class=\"example-wrap\">");
103     if let Some(extra) = extra_content {
104         out.push_buffer(extra);
105     }
106     if class.is_empty() {
107         write!(out, "<pre class=\"rust\">");
108     } else {
109         write!(out, "<pre class=\"rust {}\">", class);
110     }
111     write!(out, "<code>");
112 }
113
114 /// Convert the given `src` source code into HTML by adding classes for highlighting.
115 ///
116 /// This code is used to render code blocks (in the documentation) as well as the source code pages.
117 ///
118 /// Some explanations on the last arguments:
119 ///
120 /// In case we are rendering a code block and not a source code file, `href_context` will be `None`.
121 /// To put it more simply: if `href_context` is `None`, the code won't try to generate links to an
122 /// item definition.
123 ///
124 /// More explanations about spans and how we use them here are provided in the
125 fn write_code(
126     out: &mut Buffer,
127     src: &str,
128     href_context: Option<HrefContext<'_, '_, '_>>,
129     decoration_info: Option<DecorationInfo>,
130 ) {
131     // This replace allows to fix how the code source with DOS backline characters is displayed.
132     let src = src.replace("\r\n", "\n");
133     let mut closing_tags: Vec<&'static str> = Vec::new();
134     Classifier::new(
135         &src,
136         href_context.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP),
137         decoration_info,
138     )
139     .highlight(&mut |highlight| {
140         match highlight {
141             Highlight::Token { text, class } => string(out, Escape(text), class, &href_context),
142             Highlight::EnterSpan { class } => {
143                 closing_tags.push(enter_span(out, class, &href_context))
144             }
145             Highlight::ExitSpan => {
146                 exit_span(out, closing_tags.pop().expect("ExitSpan without EnterSpan"))
147             }
148         };
149     });
150 }
151
152 fn write_footer(out: &mut Buffer, playground_button: Option<&str>) {
153     writeln!(out, "</code></pre>{}</div>", playground_button.unwrap_or_default());
154 }
155
156 /// How a span of text is classified. Mostly corresponds to token kinds.
157 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
158 enum Class {
159     Comment,
160     DocComment,
161     Attribute,
162     KeyWord,
163     // Keywords that do pointer/reference stuff.
164     RefKeyWord,
165     Self_(Span),
166     Op,
167     Macro(Span),
168     MacroNonTerminal,
169     String,
170     Number,
171     Bool,
172     Ident(Span),
173     Lifetime,
174     PreludeTy,
175     PreludeVal,
176     QuestionMark,
177     Decoration(&'static str),
178 }
179
180 impl Class {
181     /// Returns the css class expected by rustdoc for each `Class`.
182     fn as_html(self) -> &'static str {
183         match self {
184             Class::Comment => "comment",
185             Class::DocComment => "doccomment",
186             Class::Attribute => "attribute",
187             Class::KeyWord => "kw",
188             Class::RefKeyWord => "kw-2",
189             Class::Self_(_) => "self",
190             Class::Op => "op",
191             Class::Macro(_) => "macro",
192             Class::MacroNonTerminal => "macro-nonterminal",
193             Class::String => "string",
194             Class::Number => "number",
195             Class::Bool => "bool-val",
196             Class::Ident(_) => "ident",
197             Class::Lifetime => "lifetime",
198             Class::PreludeTy => "prelude-ty",
199             Class::PreludeVal => "prelude-val",
200             Class::QuestionMark => "question-mark",
201             Class::Decoration(kind) => kind,
202         }
203     }
204
205     /// In case this is an item which can be converted into a link to a definition, it'll contain
206     /// a "span" (a tuple representing `(lo, hi)` equivalent of `Span`).
207     fn get_span(self) -> Option<Span> {
208         match self {
209             Self::Ident(sp) | Self::Self_(sp) | Self::Macro(sp) => Some(sp),
210             Self::Comment
211             | Self::DocComment
212             | Self::Attribute
213             | Self::KeyWord
214             | Self::RefKeyWord
215             | Self::Op
216             | Self::MacroNonTerminal
217             | Self::String
218             | Self::Number
219             | Self::Bool
220             | Self::Lifetime
221             | Self::PreludeTy
222             | Self::PreludeVal
223             | Self::QuestionMark
224             | Self::Decoration(_) => None,
225         }
226     }
227 }
228
229 enum Highlight<'a> {
230     Token { text: &'a str, class: Option<Class> },
231     EnterSpan { class: Class },
232     ExitSpan,
233 }
234
235 struct TokenIter<'a> {
236     src: &'a str,
237 }
238
239 impl<'a> Iterator for TokenIter<'a> {
240     type Item = (TokenKind, &'a str);
241     fn next(&mut self) -> Option<(TokenKind, &'a str)> {
242         if self.src.is_empty() {
243             return None;
244         }
245         let token = rustc_lexer::first_token(self.src);
246         let (text, rest) = self.src.split_at(token.len as usize);
247         self.src = rest;
248         Some((token.kind, text))
249     }
250 }
251
252 /// Classifies into identifier class; returns `None` if this is a non-keyword identifier.
253 fn get_real_ident_class(text: &str, allow_path_keywords: bool) -> Option<Class> {
254     let ignore: &[&str] =
255         if allow_path_keywords { &["self", "Self", "super", "crate"] } else { &["self", "Self"] };
256     if ignore.iter().any(|k| *k == text) {
257         return None;
258     }
259     Some(match text {
260         "ref" | "mut" => Class::RefKeyWord,
261         "false" | "true" => Class::Bool,
262         _ if Symbol::intern(text).is_reserved(|| Edition::Edition2021) => Class::KeyWord,
263         _ => return None,
264     })
265 }
266
267 /// This iterator comes from the same idea than "Peekable" except that it allows to "peek" more than
268 /// just the next item by using `peek_next`. The `peek` method always returns the next item after
269 /// the current one whereas `peek_next` will return the next item after the last one peeked.
270 ///
271 /// You can use both `peek` and `peek_next` at the same time without problem.
272 struct PeekIter<'a> {
273     stored: VecDeque<(TokenKind, &'a str)>,
274     /// This position is reinitialized when using `next`. It is used in `peek_next`.
275     peek_pos: usize,
276     iter: TokenIter<'a>,
277 }
278
279 impl<'a> PeekIter<'a> {
280     fn new(iter: TokenIter<'a>) -> Self {
281         Self { stored: VecDeque::new(), peek_pos: 0, iter }
282     }
283     /// Returns the next item after the current one. It doesn't interfer with `peek_next` output.
284     fn peek(&mut self) -> Option<&(TokenKind, &'a str)> {
285         if self.stored.is_empty() {
286             if let Some(next) = self.iter.next() {
287                 self.stored.push_back(next);
288             }
289         }
290         self.stored.front()
291     }
292     /// Returns the next item after the last one peeked. It doesn't interfer with `peek` output.
293     fn peek_next(&mut self) -> Option<&(TokenKind, &'a str)> {
294         self.peek_pos += 1;
295         if self.peek_pos - 1 < self.stored.len() {
296             self.stored.get(self.peek_pos - 1)
297         } else if let Some(next) = self.iter.next() {
298             self.stored.push_back(next);
299             self.stored.back()
300         } else {
301             None
302         }
303     }
304 }
305
306 impl<'a> Iterator for PeekIter<'a> {
307     type Item = (TokenKind, &'a str);
308     fn next(&mut self) -> Option<Self::Item> {
309         self.peek_pos = 0;
310         if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() }
311     }
312 }
313
314 /// Custom spans inserted into the source. Eg --scrape-examples uses this to highlight function calls
315 struct Decorations {
316     starts: Vec<(u32, &'static str)>,
317     ends: Vec<u32>,
318 }
319
320 impl Decorations {
321     fn new(info: DecorationInfo) -> Self {
322         // Extract tuples (start, end, kind) into separate sequences of (start, kind) and (end).
323         let (mut starts, mut ends): (Vec<_>, Vec<_>) = info
324             .0
325             .into_iter()
326             .flat_map(|(kind, ranges)| ranges.into_iter().map(move |(lo, hi)| ((lo, kind), hi)))
327             .unzip();
328
329         // Sort the sequences in document order.
330         starts.sort_by_key(|(lo, _)| *lo);
331         ends.sort();
332
333         Decorations { starts, ends }
334     }
335 }
336
337 /// Processes program tokens, classifying strings of text by highlighting
338 /// category (`Class`).
339 struct Classifier<'a> {
340     tokens: PeekIter<'a>,
341     in_attribute: bool,
342     in_macro: bool,
343     in_macro_nonterminal: bool,
344     byte_pos: u32,
345     file_span: Span,
346     src: &'a str,
347     decorations: Option<Decorations>,
348 }
349
350 impl<'a> Classifier<'a> {
351     /// Takes as argument the source code to HTML-ify, the rust edition to use and the source code
352     /// file span which will be used later on by the `span_correspondance_map`.
353     fn new(src: &str, file_span: Span, decoration_info: Option<DecorationInfo>) -> Classifier<'_> {
354         let tokens = PeekIter::new(TokenIter { src });
355         let decorations = decoration_info.map(Decorations::new);
356         Classifier {
357             tokens,
358             in_attribute: false,
359             in_macro: false,
360             in_macro_nonterminal: false,
361             byte_pos: 0,
362             file_span,
363             src,
364             decorations,
365         }
366     }
367
368     /// Convenient wrapper to create a [`Span`] from a position in the file.
369     fn new_span(&self, lo: u32, text: &str) -> Span {
370         let hi = lo + text.len() as u32;
371         let file_lo = self.file_span.lo();
372         self.file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi))
373     }
374
375     /// Concatenate colons and idents as one when possible.
376     fn get_full_ident_path(&mut self) -> Vec<(TokenKind, usize, usize)> {
377         let start = self.byte_pos as usize;
378         let mut pos = start;
379         let mut has_ident = false;
380
381         loop {
382             let mut nb = 0;
383             while let Some((TokenKind::Colon, _)) = self.tokens.peek() {
384                 self.tokens.next();
385                 nb += 1;
386             }
387             // Ident path can start with "::" but if we already have content in the ident path,
388             // the "::" is mandatory.
389             if has_ident && nb == 0 {
390                 return vec![(TokenKind::Ident, start, pos)];
391             } else if nb != 0 && nb != 2 {
392                 if has_ident {
393                     return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
394                 } else {
395                     return vec![(TokenKind::Colon, start, pos + nb)];
396                 }
397             }
398
399             if let Some((None, text)) = self.tokens.peek().map(|(token, text)| {
400                 if *token == TokenKind::Ident {
401                     let class = get_real_ident_class(text, true);
402                     (class, text)
403                 } else {
404                     // Doesn't matter which Class we put in here...
405                     (Some(Class::Comment), text)
406                 }
407             }) {
408                 // We only "add" the colon if there is an ident behind.
409                 pos += text.len() + nb;
410                 has_ident = true;
411                 self.tokens.next();
412             } else if nb > 0 && has_ident {
413                 return vec![(TokenKind::Ident, start, pos), (TokenKind::Colon, pos, pos + nb)];
414             } else if nb > 0 {
415                 return vec![(TokenKind::Colon, start, start + nb)];
416             } else if has_ident {
417                 return vec![(TokenKind::Ident, start, pos)];
418             } else {
419                 return Vec::new();
420             }
421         }
422     }
423
424     /// Wraps the tokens iteration to ensure that the `byte_pos` is always correct.
425     ///
426     /// It returns the token's kind, the token as a string and its byte position in the source
427     /// string.
428     fn next(&mut self) -> Option<(TokenKind, &'a str, u32)> {
429         if let Some((kind, text)) = self.tokens.next() {
430             let before = self.byte_pos;
431             self.byte_pos += text.len() as u32;
432             Some((kind, text, before))
433         } else {
434             None
435         }
436     }
437
438     /// Exhausts the `Classifier` writing the output into `sink`.
439     ///
440     /// The general structure for this method is to iterate over each token,
441     /// possibly giving it an HTML span with a class specifying what flavor of
442     /// token is used.
443     fn highlight(mut self, sink: &mut dyn FnMut(Highlight<'a>)) {
444         loop {
445             if let Some(decs) = self.decorations.as_mut() {
446                 let byte_pos = self.byte_pos;
447                 let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count();
448                 for (_, kind) in decs.starts.drain(0..n_starts) {
449                     sink(Highlight::EnterSpan { class: Class::Decoration(kind) });
450                 }
451
452                 let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count();
453                 for _ in decs.ends.drain(0..n_ends) {
454                     sink(Highlight::ExitSpan);
455                 }
456             }
457
458             if self
459                 .tokens
460                 .peek()
461                 .map(|t| matches!(t.0, TokenKind::Colon | TokenKind::Ident))
462                 .unwrap_or(false)
463             {
464                 let tokens = self.get_full_ident_path();
465                 for (token, start, end) in &tokens {
466                     let text = &self.src[*start..*end];
467                     self.advance(*token, text, sink, *start as u32);
468                     self.byte_pos += text.len() as u32;
469                 }
470                 if !tokens.is_empty() {
471                     continue;
472                 }
473             }
474             if let Some((token, text, before)) = self.next() {
475                 self.advance(token, text, sink, before);
476             } else {
477                 break;
478             }
479         }
480     }
481
482     /// Single step of highlighting. This will classify `token`, but maybe also a couple of
483     /// following ones as well.
484     ///
485     /// `before` is the position of the given token in the `source` string and is used as "lo" byte
486     /// in case we want to try to generate a link for this token using the
487     /// `span_correspondance_map`.
488     fn advance(
489         &mut self,
490         token: TokenKind,
491         text: &'a str,
492         sink: &mut dyn FnMut(Highlight<'a>),
493         before: u32,
494     ) {
495         let lookahead = self.peek();
496         let no_highlight = |sink: &mut dyn FnMut(_)| sink(Highlight::Token { text, class: None });
497         let class = match token {
498             TokenKind::Whitespace => return no_highlight(sink),
499             TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => {
500                 if doc_style.is_some() {
501                     Class::DocComment
502                 } else {
503                     Class::Comment
504                 }
505             }
506             // Consider this as part of a macro invocation if there was a
507             // leading identifier.
508             TokenKind::Bang if self.in_macro => {
509                 self.in_macro = false;
510                 sink(Highlight::Token { text, class: None });
511                 sink(Highlight::ExitSpan);
512                 return;
513             }
514
515             // Assume that '&' or '*' is the reference or dereference operator
516             // or a reference or pointer type. Unless, of course, it looks like
517             // a logical and or a multiplication operator: `&&` or `* `.
518             TokenKind::Star => match self.tokens.peek() {
519                 Some((TokenKind::Whitespace, _)) => Class::Op,
520                 Some((TokenKind::Ident, "mut")) => {
521                     self.next();
522                     sink(Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) });
523                     return;
524                 }
525                 Some((TokenKind::Ident, "const")) => {
526                     self.next();
527                     sink(Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) });
528                     return;
529                 }
530                 _ => Class::RefKeyWord,
531             },
532             TokenKind::And => match self.tokens.peek() {
533                 Some((TokenKind::And, _)) => {
534                     self.next();
535                     sink(Highlight::Token { text: "&&", class: Some(Class::Op) });
536                     return;
537                 }
538                 Some((TokenKind::Eq, _)) => {
539                     self.next();
540                     sink(Highlight::Token { text: "&=", class: Some(Class::Op) });
541                     return;
542                 }
543                 Some((TokenKind::Whitespace, _)) => Class::Op,
544                 Some((TokenKind::Ident, "mut")) => {
545                     self.next();
546                     sink(Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) });
547                     return;
548                 }
549                 _ => Class::RefKeyWord,
550             },
551
552             // These can either be operators, or arrows.
553             TokenKind::Eq => match lookahead {
554                 Some(TokenKind::Eq) => {
555                     self.next();
556                     sink(Highlight::Token { text: "==", class: Some(Class::Op) });
557                     return;
558                 }
559                 Some(TokenKind::Gt) => {
560                     self.next();
561                     sink(Highlight::Token { text: "=>", class: None });
562                     return;
563                 }
564                 _ => Class::Op,
565             },
566             TokenKind::Minus if lookahead == Some(TokenKind::Gt) => {
567                 self.next();
568                 sink(Highlight::Token { text: "->", class: None });
569                 return;
570             }
571
572             // Other operators.
573             TokenKind::Minus
574             | TokenKind::Plus
575             | TokenKind::Or
576             | TokenKind::Slash
577             | TokenKind::Caret
578             | TokenKind::Percent
579             | TokenKind::Bang
580             | TokenKind::Lt
581             | TokenKind::Gt => Class::Op,
582
583             // Miscellaneous, no highlighting.
584             TokenKind::Dot
585             | TokenKind::Semi
586             | TokenKind::Comma
587             | TokenKind::OpenParen
588             | TokenKind::CloseParen
589             | TokenKind::OpenBrace
590             | TokenKind::CloseBrace
591             | TokenKind::OpenBracket
592             | TokenKind::At
593             | TokenKind::Tilde
594             | TokenKind::Colon
595             | TokenKind::Unknown => return no_highlight(sink),
596
597             TokenKind::Question => Class::QuestionMark,
598
599             TokenKind::Dollar => match lookahead {
600                 Some(TokenKind::Ident) => {
601                     self.in_macro_nonterminal = true;
602                     Class::MacroNonTerminal
603                 }
604                 _ => return no_highlight(sink),
605             },
606
607             // This might be the start of an attribute. We're going to want to
608             // continue highlighting it as an attribute until the ending ']' is
609             // seen, so skip out early. Down below we terminate the attribute
610             // span when we see the ']'.
611             TokenKind::Pound => {
612                 match lookahead {
613                     // Case 1: #![inner_attribute]
614                     Some(TokenKind::Bang) => {
615                         self.next();
616                         if let Some(TokenKind::OpenBracket) = self.peek() {
617                             self.in_attribute = true;
618                             sink(Highlight::EnterSpan { class: Class::Attribute });
619                         }
620                         sink(Highlight::Token { text: "#", class: None });
621                         sink(Highlight::Token { text: "!", class: None });
622                         return;
623                     }
624                     // Case 2: #[outer_attribute]
625                     Some(TokenKind::OpenBracket) => {
626                         self.in_attribute = true;
627                         sink(Highlight::EnterSpan { class: Class::Attribute });
628                     }
629                     _ => (),
630                 }
631                 return no_highlight(sink);
632             }
633             TokenKind::CloseBracket => {
634                 if self.in_attribute {
635                     self.in_attribute = false;
636                     sink(Highlight::Token { text: "]", class: None });
637                     sink(Highlight::ExitSpan);
638                     return;
639                 }
640                 return no_highlight(sink);
641             }
642             TokenKind::Literal { kind, .. } => match kind {
643                 // Text literals.
644                 LiteralKind::Byte { .. }
645                 | LiteralKind::Char { .. }
646                 | LiteralKind::Str { .. }
647                 | LiteralKind::ByteStr { .. }
648                 | LiteralKind::RawStr { .. }
649                 | LiteralKind::RawByteStr { .. } => Class::String,
650                 // Number literals.
651                 LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number,
652             },
653             TokenKind::Ident | TokenKind::RawIdent if lookahead == Some(TokenKind::Bang) => {
654                 self.in_macro = true;
655                 sink(Highlight::EnterSpan { class: Class::Macro(self.new_span(before, text)) });
656                 sink(Highlight::Token { text, class: None });
657                 return;
658             }
659             TokenKind::Ident => match get_real_ident_class(text, false) {
660                 None => match text {
661                     "Option" | "Result" => Class::PreludeTy,
662                     "Some" | "None" | "Ok" | "Err" => Class::PreludeVal,
663                     // "union" is a weak keyword and is only considered as a keyword when declaring
664                     // a union type.
665                     "union" if self.check_if_is_union_keyword() => Class::KeyWord,
666                     _ if self.in_macro_nonterminal => {
667                         self.in_macro_nonterminal = false;
668                         Class::MacroNonTerminal
669                     }
670                     "self" | "Self" => Class::Self_(self.new_span(before, text)),
671                     _ => Class::Ident(self.new_span(before, text)),
672                 },
673                 Some(c) => c,
674             },
675             TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => {
676                 Class::Ident(self.new_span(before, text))
677             }
678             TokenKind::Lifetime { .. } => Class::Lifetime,
679         };
680         // Anything that didn't return above is the simple case where we the
681         // class just spans a single token, so we can use the `string` method.
682         sink(Highlight::Token { text, class: Some(class) });
683     }
684
685     fn peek(&mut self) -> Option<TokenKind> {
686         self.tokens.peek().map(|(token_kind, _text)| *token_kind)
687     }
688
689     fn check_if_is_union_keyword(&mut self) -> bool {
690         while let Some(kind) = self.tokens.peek_next().map(|(token_kind, _text)| token_kind) {
691             if *kind == TokenKind::Whitespace {
692                 continue;
693             }
694             return *kind == TokenKind::Ident;
695         }
696         false
697     }
698 }
699
700 /// Called when we start processing a span of text that should be highlighted.
701 /// The `Class` argument specifies how it should be highlighted.
702 fn enter_span(
703     out: &mut Buffer,
704     klass: Class,
705     href_context: &Option<HrefContext<'_, '_, '_>>,
706 ) -> &'static str {
707     string_without_closing_tag(out, "", Some(klass), href_context).expect(
708         "internal error: enter_span was called with Some(klass) but did not return a \
709             closing HTML tag",
710     )
711 }
712
713 /// Called at the end of a span of highlighted text.
714 fn exit_span(out: &mut Buffer, closing_tag: &str) {
715     out.write_str(closing_tag);
716 }
717
718 /// Called for a span of text. If the text should be highlighted differently
719 /// from the surrounding text, then the `Class` argument will be a value other
720 /// than `None`.
721 ///
722 /// The following sequences of callbacks are equivalent:
723 /// ```plain
724 ///     enter_span(Foo), string("text", None), exit_span()
725 ///     string("text", Foo)
726 /// ```
727 ///
728 /// The latter can be thought of as a shorthand for the former, which is more
729 /// flexible.
730 ///
731 /// Note that if `context` is not `None` and that the given `klass` contains a `Span`, the function
732 /// will then try to find this `span` in the `span_correspondance_map`. If found, it'll then
733 /// generate a link for this element (which corresponds to where its definition is located).
734 fn string<T: Display>(
735     out: &mut Buffer,
736     text: T,
737     klass: Option<Class>,
738     href_context: &Option<HrefContext<'_, '_, '_>>,
739 ) {
740     if let Some(closing_tag) = string_without_closing_tag(out, text, klass, href_context) {
741         out.write_str(closing_tag);
742     }
743 }
744
745 /// This function writes `text` into `out` with some modifications depending on `klass`:
746 ///
747 /// * If `klass` is `None`, `text` is written into `out` with no modification.
748 /// * If `klass` is `Some` but `klass.get_span()` is `None`, it writes the text wrapped in a
749 ///   `<span>` with the provided `klass`.
750 /// * If `klass` is `Some` and has a [`rustc_span::Span`], it then tries to generate a link (`<a>`
751 ///   element) by retrieving the link information from the `span_correspondance_map` that was filled
752 ///   in `span_map.rs::collect_spans_and_sources`. If it cannot retrieve the information, then it's
753 ///   the same as the second point (`klass` is `Some` but doesn't have a [`rustc_span::Span`]).
754 fn string_without_closing_tag<T: Display>(
755     out: &mut Buffer,
756     text: T,
757     klass: Option<Class>,
758     href_context: &Option<HrefContext<'_, '_, '_>>,
759 ) -> Option<&'static str> {
760     let Some(klass) = klass
761     else {
762         write!(out, "{}", text);
763         return None;
764     };
765     let Some(def_span) = klass.get_span()
766     else {
767         write!(out, "<span class=\"{}\">{}", klass.as_html(), text);
768         return Some("</span>");
769     };
770
771     let mut text_s = text.to_string();
772     if text_s.contains("::") {
773         text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| {
774             match t {
775                 "self" | "Self" => write!(
776                     &mut path,
777                     "<span class=\"{}\">{}</span>",
778                     Class::Self_(DUMMY_SP).as_html(),
779                     t
780                 ),
781                 "crate" | "super" => {
782                     write!(&mut path, "<span class=\"{}\">{}</span>", Class::KeyWord.as_html(), t)
783                 }
784                 t => write!(&mut path, "{}", t),
785             }
786             .expect("Failed to build source HTML path");
787             path
788         });
789     }
790     if let Some(href_context) = href_context {
791         if let Some(href) =
792             href_context.context.shared.span_correspondance_map.get(&def_span).and_then(|href| {
793                 let context = href_context.context;
794                 // FIXME: later on, it'd be nice to provide two links (if possible) for all items:
795                 // one to the documentation page and one to the source definition.
796                 // FIXME: currently, external items only generate a link to their documentation,
797                 // a link to their definition can be generated using this:
798                 // https://github.com/rust-lang/rust/blob/60f1a2fc4b535ead9c85ce085fdce49b1b097531/src/librustdoc/html/render/context.rs#L315-L338
799                 match href {
800                     LinkFromSrc::Local(span) => context
801                         .href_from_span(*span, true)
802                         .map(|s| format!("{}{}", href_context.root_path, s)),
803                     LinkFromSrc::External(def_id) => {
804                         format::href_with_root_path(*def_id, context, Some(href_context.root_path))
805                             .ok()
806                             .map(|(url, _, _)| url)
807                     }
808                     LinkFromSrc::Primitive(prim) => format::href_with_root_path(
809                         PrimitiveType::primitive_locations(context.tcx())[prim],
810                         context,
811                         Some(href_context.root_path),
812                     )
813                     .ok()
814                     .map(|(url, _, _)| url),
815                 }
816             })
817         {
818             write!(out, "<a class=\"{}\" href=\"{}\">{}", klass.as_html(), href, text_s);
819             return Some("</a>");
820         }
821     }
822     write!(out, "<span class=\"{}\">{}", klass.as_html(), text_s);
823     Some("</span>")
824 }
825
826 #[cfg(test)]
827 mod tests;