Skip to main content

rustdoc/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
8use std::borrow::Cow;
9use std::collections::VecDeque;
10use std::fmt::{self, Display, Write};
11use std::iter;
12
13use itertools::Either;
14use rustc_data_structures::fx::FxIndexMap;
15use rustc_lexer::{Cursor, FrontmatterAllowed, LiteralKind, TokenKind};
16use rustc_span::BytePos;
17use rustc_span::edition::Edition;
18use rustc_span::symbol::Symbol;
19
20use super::format;
21use crate::clean::PrimitiveType;
22use crate::display::Joined as _;
23use crate::html::escape::EscapeBodyText;
24use crate::html::format::HrefInfo;
25use crate::html::macro_expansion::ExpandedCode;
26use crate::html::render::Context;
27use crate::html::span_map::{DUMMY_SP, LinkFromSrc, Span};
28
29/// This type is needed in case we want to render links on items to allow to go to their definition.
30pub(crate) struct HrefContext<'a, 'tcx> {
31    pub(crate) context: &'a Context<'tcx>,
32    /// This span contains the current file we're going through.
33    pub(crate) file_span: Span,
34    /// This field is used to know "how far" from the top of the directory we are to link to either
35    /// documentation pages or other source pages.
36    pub(crate) root_path: &'a str,
37    /// This field is used to calculate precise local URLs.
38    pub(crate) current_href: String,
39}
40
41/// Decorations are represented as a map from CSS class to vector of character ranges.
42/// Each range will be wrapped in a span with that class.
43#[derive(Default)]
44pub(crate) struct DecorationInfo(pub(crate) FxIndexMap<&'static str, Vec<(u32, u32)>>);
45
46#[derive(Eq, PartialEq, Clone)]
47pub(crate) enum Tooltip {
48    IgnoreAll,
49    IgnoreSome(Vec<String>),
50    CompileFail,
51    ShouldPanic,
52    Edition(Edition),
53}
54
55/// Highlights `src` as an inline example, returning the HTML output.
56pub(crate) fn render_example_with_highlighting(
57    src: &str,
58    tooltip: Option<&Tooltip>,
59    playground_button: Option<&str>,
60    extra_classes: &[String],
61    edition: Edition,
62) -> impl Display {
63    fmt::from_fn(move |f| {
64        write_header("rust-example-rendered", tooltip, extra_classes).fmt(f)?;
65        let edition = match tooltip {
66            Some(Tooltip::Edition(edition)) => *edition,
67            _ => edition,
68        };
69        write_code(f, src, None, None, edition, None);
70        write_footer(playground_button).fmt(f)
71    })
72}
73
74fn write_header(class: &str, tooltip: Option<&Tooltip>, extra_classes: &[String]) -> impl Display {
75    fmt::from_fn(move |f| {
76        write!(
77            f,
78            "<div class=\"example-wrap{}\">",
79            tooltip
80                .map(|tooltip| match tooltip {
81                    Tooltip::IgnoreAll | Tooltip::IgnoreSome(_) => " ignore",
82                    Tooltip::CompileFail => " compile_fail",
83                    Tooltip::ShouldPanic => " should_panic",
84                    Tooltip::Edition(_) => " edition",
85                })
86                .unwrap_or_default()
87        )?;
88
89        if let Some(tooltip) = tooltip {
90            let tooltip = fmt::from_fn(|f| match tooltip {
91                Tooltip::IgnoreAll => f.write_str("This example is not tested"),
92                Tooltip::IgnoreSome(platforms) => {
93                    f.write_str("This example is not tested on ")?;
94                    match &platforms[..] {
95                        [] => unreachable!(),
96                        [platform] => f.write_str(platform)?,
97                        [first, second] => write!(f, "{first} or {second}")?,
98                        [platforms @ .., last] => {
99                            for platform in platforms {
100                                write!(f, "{platform}, ")?;
101                            }
102                            write!(f, "or {last}")?;
103                        }
104                    }
105                    Ok(())
106                }
107                Tooltip::CompileFail => f.write_str("This example deliberately fails to compile"),
108                Tooltip::ShouldPanic => f.write_str("This example panics"),
109                Tooltip::Edition(edition) => write!(f, "This example runs with edition {edition}"),
110            });
111
112            write!(f, "<a href=\"#\" class=\"tooltip\" title=\"{tooltip}\">ⓘ</a>")?;
113        }
114
115        let classes = fmt::from_fn(|f| {
116            iter::once("rust")
117                .chain(Some(class).filter(|class| !class.is_empty()))
118                .chain(extra_classes.iter().map(String::as_str))
119                .joined(" ", f)
120        });
121
122        write!(f, "<pre class=\"{classes}\"><code>")
123    })
124}
125
126/// Check if two `Class` can be merged together. In the following rules, "unclassified" means `None`
127/// basically (since it's `Option<Class>`). The following rules apply:
128///
129/// * If two `Class` have the same variant, then they can be merged.
130/// * If the other `Class` is unclassified and only contains white characters (backline,
131///   whitespace, etc), it can be merged.
132/// * `Class::Ident` is considered the same as unclassified (because it doesn't have an associated
133///   CSS class).
134fn can_merge(class1: Option<Class>, class2: Option<Class>, text: &str) -> bool {
135    match (class1, class2) {
136        (Some(c1), Some(c2)) => c1.is_equal_to(c2),
137        (Some(Class::Ident(_)), None) | (None, Some(Class::Ident(_))) => true,
138        (Some(Class::Macro(_)), _) => false,
139        (Some(_), None) | (None, Some(_)) => text.trim().is_empty(),
140        (None, None) => true,
141    }
142}
143
144#[derive(Debug)]
145struct ClassInfo {
146    class: Class,
147    /// If `Some`, then it means the tag was opened and needs to be closed.
148    closing_tag: Option<&'static str>,
149    /// Set to `true` by `exit_elem` to signal that all the elements of this class have been pushed.
150    ///
151    /// The class will be closed and removed from the stack when the next non-mergeable item is
152    /// pushed. When it is removed, the closing tag will be written if (and only if)
153    /// `self.closing_tag` is `Some`.
154    pending_exit: bool,
155}
156
157impl ClassInfo {
158    fn new(class: Class, closing_tag: Option<&'static str>) -> Self {
159        Self { class, closing_tag, pending_exit: closing_tag.is_some() }
160    }
161
162    fn close_tag<W: Write>(&self, out: &mut W) {
163        if let Some(closing_tag) = self.closing_tag {
164            out.write_str(closing_tag).unwrap();
165        }
166    }
167
168    fn is_open(&self) -> bool {
169        self.closing_tag.is_some()
170    }
171}
172
173/// This represents the stack of HTML elements. For example a macro expansion
174/// will contain other elements which might themselves contain other elements
175/// (like macros).
176///
177/// This allows to easily handle HTML tags instead of having a more complicated
178/// state machine to keep track of which tags are open.
179#[derive(Debug)]
180struct ClassStack {
181    open_classes: Vec<ClassInfo>,
182}
183
184impl ClassStack {
185    fn new() -> Self {
186        Self { open_classes: Vec::new() }
187    }
188
189    fn enter_elem<W: Write>(
190        &mut self,
191        out: &mut W,
192        href_context: &Option<HrefContext<'_, '_>>,
193        new_class: Class,
194        closing_tag: Option<&'static str>,
195    ) {
196        if let Some(current_class) = self.open_classes.last_mut() {
197            if can_merge(Some(current_class.class), Some(new_class), "") {
198                current_class.pending_exit = false;
199                return;
200            } else if current_class.pending_exit {
201                current_class.close_tag(out);
202                self.open_classes.pop();
203            }
204        }
205        let mut class_info = ClassInfo::new(new_class, closing_tag);
206        if closing_tag.is_none() {
207            if matches!(new_class, Class::Decoration(_) | Class::Original) {
208                // Even if a whitespace characters follows, we need to open the class right away
209                // as these characters are part of the element.
210                // FIXME: Should we instead add a new boolean field to `ClassInfo` to force a
211                // non-open tag to be added if another one comes before it's open?
212                write!(out, "<span class=\"{}\">", new_class.as_html()).unwrap();
213                class_info.closing_tag = Some("</span>");
214            } else if new_class.get_span().is_some()
215                && let Some(closing_tag) =
216                    string_without_closing_tag(out, "", Some(class_info.class), href_context, false)
217                && !closing_tag.is_empty()
218            {
219                class_info.closing_tag = Some(closing_tag);
220            }
221        }
222
223        self.open_classes.push(class_info);
224    }
225
226    /// This sets the `pending_exit` field to `true`. Meaning that if we try to push another stack
227    /// which is not compatible with this one, it will exit the current one before adding the new
228    /// one.
229    fn exit_elem(&mut self) {
230        let current_class =
231            self.open_classes.last_mut().expect("`exit_elem` called on empty class stack");
232        if !current_class.pending_exit {
233            current_class.pending_exit = true;
234            return;
235        }
236        // If the current class was already closed, it means we are actually closing its parent.
237        self.open_classes.pop();
238        let current_class =
239            self.open_classes.last_mut().expect("`exit_elem` called on empty class stack parent");
240        current_class.pending_exit = true;
241    }
242
243    fn last_class(&self) -> Option<Class> {
244        self.open_classes.last().map(|c| c.class)
245    }
246
247    fn last_class_is_open(&self) -> bool {
248        if let Some(last) = self.open_classes.last() {
249            last.is_open()
250        } else {
251            // If there is no class, then it's already open.
252            true
253        }
254    }
255
256    fn close_last_if_needed<W: Write>(&mut self, out: &mut W) {
257        if let Some(last) = self.open_classes.pop_if(|class| class.pending_exit && class.is_open())
258        {
259            last.close_tag(out);
260        }
261    }
262
263    fn push<W: Write>(
264        &mut self,
265        out: &mut W,
266        href_context: &Option<HrefContext<'_, '_>>,
267        class: Option<Class>,
268        text: Cow<'_, str>,
269        needs_escape: bool,
270    ) {
271        // If the new token cannot be merged with the currently open `Class`, we close the `Class`
272        // if possible.
273        if !can_merge(self.last_class(), class, &text) {
274            self.close_last_if_needed(out)
275        }
276
277        let current_class = self.last_class();
278
279        // If we have a `Class` that hasn't been "open" yet (ie, we received only an `EnterSpan`
280        // event), we need to open the `Class` before going any further so the new token will be
281        // written inside it.
282        if class.is_none() && !self.last_class_is_open() {
283            if let Some(current_class_info) = self.open_classes.last_mut() {
284                let class_s = current_class_info.class.as_html();
285                if !class_s.is_empty() {
286                    write!(out, "<span class=\"{class_s}\">").unwrap();
287                }
288                current_class_info.closing_tag = Some("</span>");
289            }
290        }
291
292        let current_class_is_open = self.open_classes.last().is_some_and(|c| c.is_open());
293        let can_merge = can_merge(class, current_class, &text);
294        let should_open_tag = !current_class_is_open || !can_merge;
295
296        let text =
297            if needs_escape { Either::Left(&EscapeBodyText(&text)) } else { Either::Right(text) };
298
299        let closing_tag =
300            string_without_closing_tag(out, &text, class, href_context, should_open_tag);
301        if class.is_some() && should_open_tag && closing_tag.is_none() {
302            panic!(
303                "called `string_without_closing_tag` with a class but no closing tag was returned"
304            );
305        } else if let Some(closing_tag) = closing_tag
306            && !closing_tag.is_empty()
307        {
308            // If this is a link, we need to close it right away and not open a new `Class`,
309            // otherwise extra content would go into the `<a>` HTML tag.
310            if closing_tag == "</a>" {
311                out.write_str(closing_tag).unwrap();
312            // If the current `Class` is not compatible with this one, we create a new `Class`.
313            } else if let Some(class) = class
314                && !can_merge
315            {
316                self.enter_elem(out, href_context, class, Some("</span>"));
317            // Otherwise, we consider the actual `Class` to have been open.
318            } else if let Some(current_class_info) = self.open_classes.last_mut() {
319                current_class_info.closing_tag = Some("</span>");
320            }
321        }
322    }
323
324    /// This method closes all open tags and returns the list of `Class` which were not already
325    /// closed (ie `pending_exit` set to `true`).
326    ///
327    /// It is used when starting a macro expansion: we need to close all HTML tags and then to
328    /// reopen them inside the newly created expansion HTML tag. Same goes when we close the
329    /// expansion.
330    fn empty_stack<W: Write>(&mut self, out: &mut W) -> Vec<Class> {
331        let mut classes = Vec::with_capacity(self.open_classes.len());
332
333        // We close all open tags and only keep the ones that were not already waiting to be closed.
334        while let Some(class_info) = self.open_classes.pop() {
335            class_info.close_tag(out);
336            if !class_info.pending_exit {
337                classes.push(class_info.class);
338            }
339        }
340        classes
341    }
342}
343
344/// This type is used as a conveniency to prevent having to pass all its fields as arguments into
345/// the various functions (which became its methods).
346struct TokenHandler<'a, 'tcx, F: Write> {
347    out: &'a mut F,
348    class_stack: ClassStack,
349    /// We need to keep the `Class` for each element because it could contain a `Span` which is
350    /// used to generate links.
351    href_context: Option<HrefContext<'a, 'tcx>>,
352    line_number_kind: LineNumberKind,
353    line: u32,
354    max_lines: u32,
355}
356
357impl<F: Write> std::fmt::Debug for TokenHandler<'_, '_, F> {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        f.debug_struct("TokenHandler").field("class_stack", &self.class_stack).finish()
360    }
361}
362
363impl<'a, F: Write> TokenHandler<'a, '_, F> {
364    fn handle_backline(&mut self) -> Option<impl Display + use<F>> {
365        self.line += 1;
366        if self.line < self.max_lines {
367            return Some(self.line_number_kind.render(self.line));
368        }
369        None
370    }
371
372    fn push_token_without_backline_check(
373        &mut self,
374        class: Option<Class>,
375        text: Cow<'a, str>,
376        needs_escape: bool,
377    ) {
378        self.class_stack.push(self.out, &self.href_context, class, text, needs_escape);
379    }
380
381    fn push_token(&mut self, class: Option<Class>, text: Cow<'a, str>) {
382        if text == "\n"
383            && let Some(backline) = self.handle_backline()
384        {
385            write!(self.out, "{text}{backline}").unwrap();
386        } else {
387            self.push_token_without_backline_check(class, text, true);
388        }
389    }
390
391    fn start_expansion(&mut self) {
392        // We close all open tags.
393        let classes = self.class_stack.empty_stack(self.out);
394
395        // We start the expansion tag.
396        self.class_stack.enter_elem(self.out, &self.href_context, Class::Expansion, None);
397        self.push_token_without_backline_check(
398            Some(Class::Expansion),
399            Cow::Owned(format!(
400                "<input id=expand-{} \
401                     tabindex=0 \
402                     type=checkbox \
403                     aria-label=\"Collapse/expand macro\" \
404                     title=\"Collapse/expand macro\">",
405                self.line,
406            )),
407            false,
408        );
409
410        // We re-open all tags that didn't have `pending_exit` set to `true`.
411        for class in classes.into_iter().rev() {
412            self.class_stack.enter_elem(self.out, &self.href_context, class, None);
413        }
414    }
415
416    fn add_expanded_code(&mut self, expanded_code: &ExpandedCode) {
417        self.push_token_without_backline_check(
418            None,
419            Cow::Owned(format!("<span class=expanded>{}</span>", expanded_code.code)),
420            false,
421        );
422        self.class_stack.enter_elem(self.out, &self.href_context, Class::Original, None);
423    }
424
425    fn close_expansion(&mut self) {
426        // We close all open tags.
427        let classes = self.class_stack.empty_stack(self.out);
428
429        // We re-open all tags without expansion-related ones.
430        for class in classes.into_iter().rev() {
431            if !matches!(class, Class::Expansion | Class::Original) {
432                self.class_stack.enter_elem(self.out, &self.href_context, class, None);
433            }
434        }
435    }
436
437    /// Used when we're done with the current expansion "original code" (ie code before expansion).
438    /// We close all tags inside `Class::Original` and only keep the ones that were not closed yet.
439    fn close_original_tag(&mut self) {
440        let mut classes_to_reopen = Vec::new();
441        while let Some(mut class_info) = self.class_stack.open_classes.pop() {
442            if class_info.class == Class::Original {
443                while let Some(class_info) = classes_to_reopen.pop() {
444                    self.class_stack.open_classes.push(class_info);
445                }
446                class_info.close_tag(self.out);
447                return;
448            }
449            class_info.close_tag(self.out);
450            if !class_info.pending_exit {
451                class_info.closing_tag = None;
452                classes_to_reopen.push(class_info);
453            }
454        }
455        panic!("Didn't find `Class::Original` to close");
456    }
457}
458
459impl<F: Write> Drop for TokenHandler<'_, '_, F> {
460    /// When leaving, we need to flush all pending data to not have missing content.
461    fn drop(&mut self) {
462        self.class_stack.empty_stack(self.out);
463    }
464}
465
466/// Represents the type of line number to be generated as HTML.
467#[derive(Clone, Copy)]
468enum LineNumberKind {
469    /// Used for scraped code examples.
470    Scraped,
471    /// Used for source code pages.
472    Normal,
473    /// Code examples in documentation don't have line number generated by rustdoc.
474    Empty,
475}
476
477impl LineNumberKind {
478    fn render(self, line: u32) -> impl Display {
479        fmt::from_fn(move |f| {
480            match self {
481                // https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#data-nosnippet-attr
482                // Do not show "1 2 3 4 5 ..." in web search results.
483                Self::Scraped => write!(f, "<span data-nosnippet>{line}</span>"),
484                Self::Normal => write!(f, "<a href=#{line} id={line} data-nosnippet>{line}</a>"),
485                Self::Empty => Ok(()),
486            }
487        })
488    }
489}
490
491fn get_next_expansion(
492    expanded_codes: &[ExpandedCode],
493    line: u32,
494    span: Span,
495) -> Option<&ExpandedCode> {
496    expanded_codes.iter().find(|code| code.start_line == line && code.span.lo() > span.lo())
497}
498
499fn get_expansion<'a, W: Write>(
500    token_handler: &mut TokenHandler<'_, '_, W>,
501    expanded_codes: &'a [ExpandedCode],
502    span: Span,
503) -> Option<&'a ExpandedCode> {
504    let expanded_code = get_next_expansion(expanded_codes, token_handler.line, span)?;
505    token_handler.start_expansion();
506    Some(expanded_code)
507}
508
509fn end_expansion<'a, W: Write>(
510    token_handler: &mut TokenHandler<'_, '_, W>,
511    expanded_codes: &'a [ExpandedCode],
512    span: Span,
513) -> Option<&'a ExpandedCode> {
514    // We close `Class::Original` and everything open inside it.
515    token_handler.close_original_tag();
516    // Then we check if we have another macro expansion on the same line.
517    let expansion = get_next_expansion(expanded_codes, token_handler.line, span);
518    if expansion.is_none() {
519        token_handler.close_expansion();
520    }
521    expansion
522}
523
524#[derive(Clone, Copy)]
525pub(super) struct LineInfo {
526    pub(super) start_line: u32,
527    max_lines: u32,
528    pub(super) is_scraped_example: bool,
529}
530
531impl LineInfo {
532    pub(super) fn new(max_lines: u32) -> Self {
533        Self { start_line: 1, max_lines: max_lines + 1, is_scraped_example: false }
534    }
535
536    pub(super) fn new_scraped(max_lines: u32, start_line: u32) -> Self {
537        Self {
538            start_line: start_line + 1,
539            max_lines: max_lines + start_line + 1,
540            is_scraped_example: true,
541        }
542    }
543}
544
545/// Convert the given `src` source code into HTML by adding classes for highlighting.
546///
547/// This code is used to render code blocks (in the documentation) as well as the source code pages.
548///
549/// Some explanations on the last arguments:
550///
551/// In case we are rendering a code block and not a source code file, `href_context` will be `None`.
552/// To put it more simply: if `href_context` is `None`, the code won't try to generate links to an
553/// item definition.
554///
555/// More explanations about spans and how we use them here are provided in the
556pub(super) fn write_code(
557    out: &mut impl Write,
558    src: &str,
559    href_context: Option<HrefContext<'_, '_>>,
560    decoration_info: Option<&DecorationInfo>,
561    edition: Edition,
562    line_info: Option<LineInfo>,
563) {
564    // This replace allows to fix how the code source with DOS backline characters is displayed.
565    let src =
566        // The first "\r\n" should be fairly close to the beginning of the string relatively
567        // to its overall length, and most strings handled by rustdoc likely don't have
568        // DOS backlines anyway.
569        // Checking for the single ASCII character '\r' is much more efficient than checking for
570        // the whole string "\r\n".
571        if src.contains('\r') { src.replace("\r\n", "\n").into() } else { Cow::Borrowed(src) };
572    let mut token_handler = TokenHandler {
573        out,
574        href_context,
575        line_number_kind: match line_info {
576            Some(line_info) => {
577                if line_info.is_scraped_example {
578                    LineNumberKind::Scraped
579                } else {
580                    LineNumberKind::Normal
581                }
582            }
583            None => LineNumberKind::Empty,
584        },
585        line: 0,
586        max_lines: u32::MAX,
587        class_stack: ClassStack::new(),
588    };
589
590    if let Some(line_info) = line_info {
591        token_handler.line = line_info.start_line - 1;
592        token_handler.max_lines = line_info.max_lines;
593        if let Some(backline) = token_handler.handle_backline() {
594            token_handler.push_token_without_backline_check(
595                None,
596                Cow::Owned(backline.to_string()),
597                false,
598            );
599        }
600    }
601
602    let (expanded_codes, file_span) = match token_handler.href_context.as_ref().and_then(|c| {
603        let expanded_codes = c.context.shared.expanded_codes.get(&c.file_span.lo())?;
604        Some((expanded_codes, c.file_span))
605    }) {
606        Some((expanded_codes, file_span)) => (expanded_codes.as_slice(), file_span),
607        None => (&[] as &[ExpandedCode], DUMMY_SP),
608    };
609    let mut current_expansion = get_expansion(&mut token_handler, expanded_codes, file_span);
610
611    classify(
612        &src,
613        token_handler.href_context.as_ref().map_or(DUMMY_SP, |c| c.file_span),
614        decoration_info,
615        edition,
616        &mut |span, highlight| match highlight {
617            Highlight::Token { text, class } => {
618                token_handler.push_token(class, Cow::Borrowed(text));
619
620                if text == "\n" {
621                    if current_expansion.is_none() {
622                        current_expansion = get_expansion(&mut token_handler, expanded_codes, span);
623                    }
624                    if let Some(ref current_expansion) = current_expansion
625                        && current_expansion.span.lo() == span.hi()
626                    {
627                        token_handler.add_expanded_code(current_expansion);
628                    }
629                } else {
630                    let mut need_end = false;
631                    if let Some(ref current_expansion) = current_expansion {
632                        if current_expansion.span.lo() == span.hi() {
633                            token_handler.add_expanded_code(current_expansion);
634                        } else if current_expansion.end_line == token_handler.line
635                            && span.hi() >= current_expansion.span.hi()
636                        {
637                            need_end = true;
638                        }
639                    }
640                    if need_end {
641                        current_expansion = end_expansion(&mut token_handler, expanded_codes, span);
642                    }
643                }
644            }
645            Highlight::EnterSpan { class } => {
646                token_handler.class_stack.enter_elem(
647                    token_handler.out,
648                    &token_handler.href_context,
649                    class,
650                    None,
651                );
652            }
653            Highlight::ExitSpan => {
654                token_handler.class_stack.exit_elem();
655            }
656        },
657    );
658}
659
660fn write_footer(playground_button: Option<&str>) -> impl Display {
661    fmt::from_fn(move |f| write!(f, "</code></pre>{}</div>", playground_button.unwrap_or_default()))
662}
663
664/// How a span of text is classified. Mostly corresponds to token kinds.
665#[derive(Clone, Copy, Debug, Eq, PartialEq)]
666enum Class {
667    Comment,
668    DocComment,
669    Attribute,
670    KeyWord,
671    /// Keywords that do pointer/reference stuff.
672    RefKeyWord,
673    Self_(Span),
674    Macro(Span),
675    MacroNonTerminal,
676    String,
677    Number,
678    Bool,
679    /// `Ident` isn't rendered in the HTML but we still need it for the `Span` it contains.
680    Ident(Span),
681    Lifetime,
682    PreludeTy(Span),
683    PreludeVal(Span),
684    QuestionMark,
685    Decoration(&'static str),
686    /// Macro expansion.
687    Expansion,
688    /// "original" code without macro expansion.
689    Original,
690}
691
692impl Class {
693    /// It is only looking at the variant, not the variant content.
694    ///
695    /// It is used mostly to group multiple similar HTML elements into one `<span>` instead of
696    /// multiple ones.
697    fn is_equal_to(self, other: Self) -> bool {
698        match (self, other) {
699            (Self::Self_(_), Self::Self_(_))
700            | (Self::Macro(_), Self::Macro(_))
701            | (Self::Ident(_), Self::Ident(_)) => true,
702            (Self::Decoration(c1), Self::Decoration(c2)) => c1 == c2,
703            (x, y) => x == y,
704        }
705    }
706
707    /// Returns the css class expected by rustdoc for each `Class`.
708    fn as_html(self) -> &'static str {
709        match self {
710            Class::Comment => "comment",
711            Class::DocComment => "doccomment",
712            Class::Attribute => "attr",
713            Class::KeyWord => "kw",
714            Class::RefKeyWord => "kw-2",
715            Class::Self_(_) => "self",
716            Class::Macro(_) => "macro",
717            Class::MacroNonTerminal => "macro-nonterminal",
718            Class::String => "string",
719            Class::Number => "number",
720            Class::Bool => "bool-val",
721            Class::Ident(_) => "",
722            Class::Lifetime => "lifetime",
723            Class::PreludeTy(_) => "prelude-ty",
724            Class::PreludeVal(_) => "prelude-val",
725            Class::QuestionMark => "question-mark",
726            Class::Decoration(kind) => kind,
727            Class::Expansion => "expansion",
728            Class::Original => "original",
729        }
730    }
731
732    /// In case this is an item which can be converted into a link to a definition, it'll contain
733    /// a "span" (a tuple representing `(lo, hi)` equivalent of `Span`).
734    fn get_span(self) -> Option<Span> {
735        match self {
736            Self::Ident(sp)
737            | Self::Self_(sp)
738            | Self::Macro(sp)
739            | Self::PreludeTy(sp)
740            | Self::PreludeVal(sp) => Some(sp),
741            Self::Comment
742            | Self::DocComment
743            | Self::Attribute
744            | Self::KeyWord
745            | Self::RefKeyWord
746            | Self::MacroNonTerminal
747            | Self::String
748            | Self::Number
749            | Self::Bool
750            | Self::Lifetime
751            | Self::QuestionMark
752            | Self::Decoration(_)
753            | Self::Original
754            | Self::Expansion => None,
755        }
756    }
757}
758
759impl fmt::Display for Class {
760    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
761        let html = self.as_html();
762        if html.is_empty() {
763            return Ok(());
764        }
765        write!(f, " class=\"{html}\"")
766    }
767}
768
769#[derive(Debug)]
770enum Highlight<'a> {
771    Token { text: &'a str, class: Option<Class> },
772    EnterSpan { class: Class },
773    ExitSpan,
774}
775
776struct TokenIter<'a> {
777    src: &'a str,
778    cursor: Cursor<'a>,
779}
780
781impl<'a> TokenIter<'a> {
782    fn new(src: &'a str) -> Self {
783        Self { src, cursor: Cursor::new(src, FrontmatterAllowed::Yes) }
784    }
785}
786
787impl<'a> Iterator for TokenIter<'a> {
788    type Item = (TokenKind, &'a str);
789    fn next(&mut self) -> Option<(TokenKind, &'a str)> {
790        let token = self.cursor.advance_token();
791        if token.kind == TokenKind::Eof {
792            return None;
793        }
794        let (text, rest) = self.src.split_at(token.len as usize);
795        self.src = rest;
796        Some((token.kind, text))
797    }
798}
799
800/// Used to know if a keyword followed by a `!` should never be treated as a macro.
801const NON_MACRO_KEYWORDS: &[&str] = &["if", "while", "match", "break", "return", "impl"];
802
803/// This iterator comes from the same idea than "Peekable" except that it allows to "peek" more than
804/// just the next item by using `peek_next`. The `peek` method always returns the next item after
805/// the current one whereas `peek_next` will return the next item after the last one peeked.
806///
807/// You can use both `peek` and `peek_next` at the same time without problem.
808struct PeekIter<'a> {
809    stored: VecDeque<(TokenKind, &'a str)>,
810    /// This position is reinitialized when using `next`. It is used in `peek_next`.
811    peek_pos: usize,
812    iter: TokenIter<'a>,
813}
814
815impl<'a> PeekIter<'a> {
816    fn new(iter: TokenIter<'a>) -> Self {
817        Self { stored: VecDeque::new(), peek_pos: 0, iter }
818    }
819    /// Returns the next item after the current one. It doesn't interfere with `peek_next` output.
820    fn peek(&mut self) -> Option<(TokenKind, &'a str)> {
821        if self.stored.is_empty()
822            && let Some(next) = self.iter.next()
823        {
824            self.stored.push_back(next);
825        }
826        self.stored.front().copied()
827    }
828    /// Returns the next item after the last one peeked. It doesn't interfere with `peek` output.
829    fn peek_next(&mut self) -> Option<(TokenKind, &'a str)> {
830        self.peek_pos += 1;
831        if self.peek_pos - 1 < self.stored.len() {
832            self.stored.get(self.peek_pos - 1)
833        } else if let Some(next) = self.iter.next() {
834            self.stored.push_back(next);
835            self.stored.back()
836        } else {
837            None
838        }
839        .copied()
840    }
841
842    fn peek_next_if<F: Fn((TokenKind, &'a str)) -> bool>(
843        &mut self,
844        f: F,
845    ) -> Option<(TokenKind, &'a str)> {
846        let next = self.peek_next()?;
847        if f(next) {
848            Some(next)
849        } else {
850            // We go one step back.
851            self.peek_pos -= 1;
852            None
853        }
854    }
855
856    fn stop_peeking(&mut self) {
857        self.peek_pos = 0;
858    }
859}
860
861impl<'a> Iterator for PeekIter<'a> {
862    type Item = (TokenKind, &'a str);
863    fn next(&mut self) -> Option<Self::Item> {
864        if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() }
865    }
866}
867
868/// Custom spans inserted into the source. Eg --scrape-examples uses this to highlight function calls
869struct Decorations {
870    starts: Vec<(u32, &'static str)>,
871    ends: Vec<u32>,
872}
873
874impl Decorations {
875    fn new(info: &DecorationInfo) -> Self {
876        // Extract tuples (start, end, kind) into separate sequences of (start, kind) and (end).
877        let (mut starts, mut ends): (Vec<_>, Vec<_>) = info
878            .0
879            .iter()
880            .flat_map(|(&kind, ranges)| ranges.iter().map(move |&(lo, hi)| ((lo, kind), hi)))
881            .unzip();
882
883        // Sort the sequences in document order.
884        starts.sort_by_key(|(lo, _)| *lo);
885        ends.sort();
886
887        Decorations { starts, ends }
888    }
889}
890
891/// Convenient wrapper to create a [`Span`] from a position in the file.
892fn new_span(lo: u32, text: &str, file_span: Span) -> Span {
893    let hi = lo + text.len() as u32;
894    let file_lo = file_span.lo();
895    file_span.with_lo(file_lo + BytePos(lo)).with_hi(file_lo + BytePos(hi))
896}
897
898fn classify<'src>(
899    src: &'src str,
900    file_span: Span,
901    decoration_info: Option<&DecorationInfo>,
902    edition: Edition,
903    sink: &mut dyn FnMut(Span, Highlight<'src>),
904) {
905    let offset = rustc_lexer::strip_shebang(src);
906
907    if let Some(offset) = offset {
908        sink(DUMMY_SP, Highlight::Token { text: &src[..offset], class: Some(Class::Comment) });
909    }
910
911    let mut classifier =
912        Classifier::new(src, offset.unwrap_or_default(), file_span, decoration_info, edition);
913
914    loop {
915        if let Some(decs) = classifier.decorations.as_mut() {
916            let byte_pos = classifier.byte_pos;
917            let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count();
918            for (_, kind) in decs.starts.drain(0..n_starts) {
919                sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Decoration(kind) });
920            }
921
922            let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count();
923            for _ in decs.ends.drain(0..n_ends) {
924                sink(DUMMY_SP, Highlight::ExitSpan);
925            }
926        }
927
928        if let Some((TokenKind::Colon | TokenKind::Ident, _)) = classifier.tokens.peek()
929            && let Some(nb_items) = classifier.get_full_ident_path()
930        {
931            let start = classifier.byte_pos as usize;
932            let len: usize = iter::from_fn(|| classifier.next())
933                .take(nb_items)
934                .map(|(_, text, _)| text.len())
935                .sum();
936            let text = &classifier.src[start..start + len];
937            classifier.advance(TokenKind::Ident, text, sink, start as u32);
938        } else if let Some((token, text, before)) = classifier.next() {
939            classifier.advance(token, text, sink, before);
940        } else {
941            break;
942        }
943    }
944}
945
946/// Processes program tokens, classifying strings of text by highlighting
947/// category (`Class`).
948struct Classifier<'src> {
949    tokens: PeekIter<'src>,
950    in_attribute: bool,
951    in_macro: bool,
952    in_macro_nonterminal: bool,
953    byte_pos: u32,
954    file_span: Span,
955    src: &'src str,
956    decorations: Option<Decorations>,
957    edition: Edition,
958}
959
960impl<'src> Classifier<'src> {
961    /// Takes as argument the source code to HTML-ify and the source code file span
962    /// which will be used later on by the `span_correspondence_map`.
963    fn new(
964        src: &'src str,
965        byte_pos: usize,
966        file_span: Span,
967        decoration_info: Option<&DecorationInfo>,
968        edition: Edition,
969    ) -> Self {
970        Classifier {
971            tokens: PeekIter::new(TokenIter::new(&src[byte_pos..])),
972            in_attribute: false,
973            in_macro: false,
974            in_macro_nonterminal: false,
975            byte_pos: byte_pos as u32,
976            file_span,
977            src,
978            decorations: decoration_info.map(Decorations::new),
979            edition,
980        }
981    }
982
983    /// Concatenate colons and idents as one when possible.
984    fn get_full_ident_path(&mut self) -> Option<usize> {
985        let mut has_ident = false;
986        let mut nb_items = 0;
987
988        let ret = loop {
989            let mut nb = 0;
990            while self.tokens.peek_next_if(|(token, _)| token == TokenKind::Colon).is_some() {
991                nb += 1;
992                nb_items += 1;
993            }
994            // Ident path can start with "::" but if we already have content in the ident path,
995            // the "::" is mandatory.
996            if has_ident && nb == 0 {
997                break Some(nb_items);
998            } else if nb != 0 && nb != 2 {
999                if has_ident {
1000                    // Following `;` will be handled on its own.
1001                    break Some(nb_items - 1);
1002                } else {
1003                    break None;
1004                }
1005            }
1006
1007            if let Some((TokenKind::Ident, text)) =
1008                self.tokens.peek_next_if(|(token, _)| token == TokenKind::Ident)
1009                && let symbol = Symbol::intern(text)
1010                && (symbol.is_path_segment_keyword() || !self.is_keyword(symbol))
1011            {
1012                has_ident = true;
1013                nb_items += 1;
1014            } else if nb > 0 && has_ident {
1015                // Drop all the colons we just peeked (e.g. `Option::<T>` → keep `Option`).
1016                break Some(nb_items - nb);
1017            } else if has_ident {
1018                break Some(nb_items);
1019            } else {
1020                break None;
1021            }
1022        };
1023        self.tokens.stop_peeking();
1024        ret
1025    }
1026
1027    /// Wraps the tokens iteration to ensure that the `byte_pos` is always correct.
1028    ///
1029    /// It returns the token's kind, the token as a string and its byte position in the source
1030    /// string.
1031    fn next(&mut self) -> Option<(TokenKind, &'src str, u32)> {
1032        if let Some((kind, text)) = self.tokens.next() {
1033            let before = self.byte_pos;
1034            self.byte_pos += text.len() as u32;
1035            Some((kind, text, before))
1036        } else {
1037            None
1038        }
1039    }
1040
1041    fn new_macro_span(
1042        &mut self,
1043        text: &'src str,
1044        sink: &mut dyn FnMut(Span, Highlight<'src>),
1045        before: u32,
1046        file_span: Span,
1047    ) {
1048        self.in_macro = true;
1049        let span = new_span(before, text, file_span);
1050        sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Macro(span) });
1051        sink(span, Highlight::Token { text, class: None });
1052    }
1053
1054    /// Single step of highlighting. This will classify `token`, but maybe also a couple of
1055    /// following ones as well.
1056    ///
1057    /// `before` is the position of the given token in the `source` string and is used as "lo" byte
1058    /// in case we want to try to generate a link for this token using the
1059    /// `span_correspondence_map`.
1060    fn advance(
1061        &mut self,
1062        token: TokenKind,
1063        text: &'src str,
1064        sink: &mut dyn FnMut(Span, Highlight<'src>),
1065        before: u32,
1066    ) {
1067        let lookahead = self.peek();
1068        let file_span = self.file_span;
1069        let no_highlight = |sink: &mut dyn FnMut(_, _)| {
1070            sink(new_span(before, text, file_span), Highlight::Token { text, class: None })
1071        };
1072        let whitespace = |sink: &mut dyn FnMut(_, _)| {
1073            let mut start = 0u32;
1074            for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
1075                sink(
1076                    new_span(before + start, part, file_span),
1077                    Highlight::Token { text: part, class: None },
1078                );
1079                start += part.len() as u32;
1080            }
1081        };
1082        let class = match token {
1083            TokenKind::Whitespace => return whitespace(sink),
1084            TokenKind::LineComment { doc_style } | TokenKind::BlockComment { doc_style, .. } => {
1085                if doc_style.is_some() {
1086                    Class::DocComment
1087                } else {
1088                    Class::Comment
1089                }
1090            }
1091            TokenKind::Frontmatter { .. } => Class::Comment,
1092            // Consider this as part of a macro invocation if there was a
1093            // leading identifier.
1094            TokenKind::Bang if self.in_macro => {
1095                self.in_macro = false;
1096                sink(new_span(before, text, file_span), Highlight::Token { text, class: None });
1097                sink(DUMMY_SP, Highlight::ExitSpan);
1098                return;
1099            }
1100
1101            // Assume that '&' or '*' is the reference or dereference operator
1102            // or a reference or pointer type. Unless, of course, it looks like
1103            // a logical and or a multiplication operator: `&&` or `* `.
1104            TokenKind::Star => match self.tokens.peek() {
1105                Some((TokenKind::Whitespace, _)) => return whitespace(sink),
1106                Some((TokenKind::Ident, "mut")) => {
1107                    self.next();
1108                    sink(
1109                        DUMMY_SP,
1110                        Highlight::Token { text: "*mut", class: Some(Class::RefKeyWord) },
1111                    );
1112                    return;
1113                }
1114                Some((TokenKind::Ident, "const")) => {
1115                    self.next();
1116                    sink(
1117                        DUMMY_SP,
1118                        Highlight::Token { text: "*const", class: Some(Class::RefKeyWord) },
1119                    );
1120                    return;
1121                }
1122                _ => Class::RefKeyWord,
1123            },
1124            TokenKind::And => match self.tokens.peek() {
1125                Some((TokenKind::And, _)) => {
1126                    self.next();
1127                    sink(DUMMY_SP, Highlight::Token { text: "&&", class: None });
1128                    return;
1129                }
1130                Some((TokenKind::Eq, _)) => {
1131                    self.next();
1132                    sink(DUMMY_SP, Highlight::Token { text: "&=", class: None });
1133                    return;
1134                }
1135                Some((TokenKind::Whitespace, _)) => return whitespace(sink),
1136                Some((TokenKind::Ident, "mut")) => {
1137                    self.next();
1138                    sink(
1139                        DUMMY_SP,
1140                        Highlight::Token { text: "&mut", class: Some(Class::RefKeyWord) },
1141                    );
1142                    return;
1143                }
1144                _ => Class::RefKeyWord,
1145            },
1146
1147            // These can either be operators, or arrows.
1148            TokenKind::Eq => match lookahead {
1149                Some(TokenKind::Eq) => {
1150                    self.next();
1151                    sink(DUMMY_SP, Highlight::Token { text: "==", class: None });
1152                    return;
1153                }
1154                Some(TokenKind::Gt) => {
1155                    self.next();
1156                    sink(DUMMY_SP, Highlight::Token { text: "=>", class: None });
1157                    return;
1158                }
1159                _ => return no_highlight(sink),
1160            },
1161            TokenKind::Minus if lookahead == Some(TokenKind::Gt) => {
1162                self.next();
1163                sink(DUMMY_SP, Highlight::Token { text: "->", class: None });
1164                return;
1165            }
1166
1167            // Other operators.
1168            TokenKind::Minus
1169            | TokenKind::Plus
1170            | TokenKind::Or
1171            | TokenKind::Slash
1172            | TokenKind::Caret
1173            | TokenKind::Percent
1174            | TokenKind::Bang
1175            | TokenKind::Lt
1176            | TokenKind::Gt => return no_highlight(sink),
1177
1178            // Miscellaneous, no highlighting.
1179            TokenKind::Dot
1180            | TokenKind::Semi
1181            | TokenKind::Comma
1182            | TokenKind::OpenParen
1183            | TokenKind::CloseParen
1184            | TokenKind::OpenBrace
1185            | TokenKind::CloseBrace
1186            | TokenKind::OpenBracket
1187            | TokenKind::At
1188            | TokenKind::Tilde
1189            | TokenKind::Colon
1190            | TokenKind::Unknown => return no_highlight(sink),
1191
1192            TokenKind::Question => Class::QuestionMark,
1193
1194            TokenKind::Dollar => match lookahead {
1195                Some(TokenKind::Ident) => {
1196                    self.in_macro_nonterminal = true;
1197                    Class::MacroNonTerminal
1198                }
1199                _ => return no_highlight(sink),
1200            },
1201
1202            // This might be the start of an attribute. We're going to want to
1203            // continue highlighting it as an attribute until the ending ']' is
1204            // seen, so skip out early. Down below we terminate the attribute
1205            // span when we see the ']'.
1206            TokenKind::Pound => {
1207                match lookahead {
1208                    // Case 1: #![inner_attribute]
1209                    Some(TokenKind::Bang) => {
1210                        self.next();
1211                        if let Some(TokenKind::OpenBracket) = self.peek() {
1212                            self.in_attribute = true;
1213                            sink(
1214                                new_span(before, text, file_span),
1215                                Highlight::EnterSpan { class: Class::Attribute },
1216                            );
1217                        }
1218                        sink(DUMMY_SP, Highlight::Token { text: "#", class: None });
1219                        sink(DUMMY_SP, Highlight::Token { text: "!", class: None });
1220                        return;
1221                    }
1222                    // Case 2: #[outer_attribute]
1223                    Some(TokenKind::OpenBracket) => {
1224                        self.in_attribute = true;
1225                        sink(
1226                            new_span(before, text, file_span),
1227                            Highlight::EnterSpan { class: Class::Attribute },
1228                        );
1229                    }
1230                    _ => (),
1231                }
1232                return no_highlight(sink);
1233            }
1234            TokenKind::CloseBracket => {
1235                if self.in_attribute {
1236                    self.in_attribute = false;
1237                    sink(
1238                        new_span(before, text, file_span),
1239                        Highlight::Token { text: "]", class: None },
1240                    );
1241                    sink(DUMMY_SP, Highlight::ExitSpan);
1242                    return;
1243                }
1244                return no_highlight(sink);
1245            }
1246            TokenKind::Literal { kind, .. } => match kind {
1247                // Text literals.
1248                LiteralKind::Byte { .. }
1249                | LiteralKind::Char { .. }
1250                | LiteralKind::Str { .. }
1251                | LiteralKind::ByteStr { .. }
1252                | LiteralKind::RawStr { .. }
1253                | LiteralKind::RawByteStr { .. }
1254                | LiteralKind::CStr { .. }
1255                | LiteralKind::RawCStr { .. } => Class::String,
1256                // Number literals.
1257                LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number,
1258            },
1259            TokenKind::GuardedStrPrefix => return no_highlight(sink),
1260            TokenKind::RawIdent if self.check_if_macro_call("") => {
1261                self.new_macro_span(text, sink, before, file_span);
1262                return;
1263            }
1264            // Macro non-terminals (meta vars) take precedence.
1265            TokenKind::Ident if self.in_macro_nonterminal => {
1266                self.in_macro_nonterminal = false;
1267                Class::MacroNonTerminal
1268            }
1269            TokenKind::Ident => {
1270                let span = || new_span(before, text, file_span);
1271
1272                match text {
1273                    "ref" | "mut" => Class::RefKeyWord,
1274                    "false" | "true" => Class::Bool,
1275                    "self" | "Self" => Class::Self_(span()),
1276                    "Option" | "Result" => Class::PreludeTy(span()),
1277                    "Some" | "None" | "Ok" | "Err" => Class::PreludeVal(span()),
1278                    _ if self.is_weak_keyword(text) || self.is_keyword(Symbol::intern(text)) => {
1279                        // So if it's not a keyword which can be followed by a value (like `if` or
1280                        // `return`) and the next non-whitespace token is a `!`, then we consider
1281                        // it's a macro.
1282                        if !NON_MACRO_KEYWORDS.contains(&text) && self.check_if_macro_call(text) {
1283                            self.new_macro_span(text, sink, before, file_span);
1284                            return;
1285                        }
1286                        Class::KeyWord
1287                    }
1288                    // If it's not a keyword and the next non whitespace token is a `!`, then
1289                    // we consider it's a macro.
1290                    _ if self.check_if_macro_call(text) => {
1291                        self.new_macro_span(text, sink, before, file_span);
1292                        return;
1293                    }
1294                    _ => Class::Ident(span()),
1295                }
1296            }
1297            TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => {
1298                Class::Ident(new_span(before, text, file_span))
1299            }
1300            TokenKind::Lifetime { .. }
1301            | TokenKind::RawLifetime
1302            | TokenKind::UnknownPrefixLifetime => Class::Lifetime,
1303            TokenKind::Eof => panic!("Eof in advance"),
1304        };
1305        // Anything that didn't return above is the simple case where we the
1306        // class just spans a single token, so we can use the `string` method.
1307        let mut start = 0u32;
1308        for part in text.split('\n').intersperse("\n").filter(|s| !s.is_empty()) {
1309            sink(
1310                new_span(before + start, part, file_span),
1311                Highlight::Token { text: part, class: Some(class) },
1312            );
1313            start += part.len() as u32;
1314        }
1315    }
1316
1317    fn is_weak_keyword(&mut self, text: &str) -> bool {
1318        // NOTE: `yeet` (`do yeet $expr`), `catch` (`do catch $block`), `default` (specialization),
1319        // `contract_{ensures,requires}`, `builtin` (builtin_syntax) & `reuse` (fn_delegation) are
1320        // too difficult or annoying to properly detect under this simple scheme.
1321
1322        let matches = match text {
1323            "auto" => |text| text == "trait", // `auto trait Trait {}` (`auto_traits`)
1324            "pin" => |text| text == "const" || text == "mut", // `&pin mut Type` (`pin_ergonomics`)
1325            "raw" => |text| text == "const" || text == "mut", // `&raw const local`
1326            "safe" => |text| text == "fn" || text == "extern", // `unsafe extern { safe fn f(); }`
1327            "union" => |_| true,              // `union Untagged { field: () }`
1328            _ => return false,
1329        };
1330        matches!(self.peek_non_trivia(), Some((TokenKind::Ident, text)) if matches(text))
1331    }
1332
1333    fn is_keyword(&self, symbol: Symbol) -> bool {
1334        symbol.is_reserved(|| self.edition)
1335    }
1336
1337    fn peek(&mut self) -> Option<TokenKind> {
1338        self.tokens.peek().map(|(kind, _)| kind)
1339    }
1340
1341    fn peek_non_trivia(&mut self) -> Option<(TokenKind, &str)> {
1342        while let Some(token @ (kind, _)) = self.tokens.peek_next() {
1343            if let TokenKind::Whitespace
1344            | TokenKind::LineComment { doc_style: None }
1345            | TokenKind::BlockComment { doc_style: None, .. } = kind
1346            {
1347                continue;
1348            }
1349            self.tokens.stop_peeking();
1350            return Some(token);
1351        }
1352        self.tokens.stop_peeking();
1353        None
1354    }
1355
1356    fn check_if_macro_call(&mut self, ident: &str) -> bool {
1357        let mut has_bang = false;
1358        let is_macro_rule_ident = ident == "macro_rules";
1359
1360        while let Some((kind, _)) = self.tokens.peek_next() {
1361            if let TokenKind::Whitespace
1362            | TokenKind::LineComment { doc_style: None }
1363            | TokenKind::BlockComment { doc_style: None, .. } = kind
1364            {
1365                continue;
1366            }
1367            if !has_bang {
1368                if kind != TokenKind::Bang {
1369                    break;
1370                }
1371                has_bang = true;
1372                continue;
1373            }
1374            self.tokens.stop_peeking();
1375            if is_macro_rule_ident {
1376                return matches!(kind, TokenKind::Ident | TokenKind::RawIdent);
1377            }
1378            return matches!(
1379                kind,
1380                TokenKind::OpenParen | TokenKind::OpenBracket | TokenKind::OpenBrace
1381            );
1382        }
1383        self.tokens.stop_peeking();
1384        false
1385    }
1386}
1387
1388fn generate_link_to_def(
1389    out: &mut impl Write,
1390    text_s: &str,
1391    klass: Class,
1392    href_context: &Option<HrefContext<'_, '_>>,
1393    def_span: Span,
1394    open_tag: bool,
1395) -> bool {
1396    if let Some(href_context) = href_context
1397        && let Some(href) =
1398            href_context.context.shared.span_correspondence_map.get(&def_span).and_then(|href| {
1399                let context = href_context.context;
1400                // FIXME: later on, it'd be nice to provide two links (if possible) for all items:
1401                // one to the documentation page and one to the source definition.
1402                // FIXME: currently, external items only generate a link to their documentation,
1403                // a link to their definition can be generated using this:
1404                // https://github.com/rust-lang/rust/blob/60f1a2fc4b535ead9c85ce085fdce49b1b097531/src/librustdoc/html/render/context.rs#L315-L338
1405                match href {
1406                    LinkFromSrc::Local(span) => {
1407                        context.href_from_span_relative(*span, &href_context.current_href)
1408                    }
1409                    LinkFromSrc::External(def_id) => {
1410                        format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1411                            .ok()
1412                            .map(|HrefInfo { url, .. }| url)
1413                    }
1414                    LinkFromSrc::Primitive(prim) => format::href_with_root_path(
1415                        PrimitiveType::primitive_locations(context.tcx())[prim],
1416                        context,
1417                        Some(href_context.root_path),
1418                    )
1419                    .ok()
1420                    .map(|HrefInfo { url, .. }| url),
1421                    LinkFromSrc::Doc(def_id) => {
1422                        format::href_with_root_path(*def_id, context, Some(href_context.root_path))
1423                            .ok()
1424                            .map(|HrefInfo { url, .. }| url)
1425                    }
1426                }
1427            })
1428    {
1429        if !open_tag {
1430            // We're already inside an element which has the same klass, no need to give it
1431            // again.
1432            write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1433        } else {
1434            let klass_s = klass.as_html();
1435            if klass_s.is_empty() {
1436                write!(out, "<a href=\"{href}\">{text_s}").unwrap();
1437            } else {
1438                write!(out, "<a class=\"{klass_s}\" href=\"{href}\">{text_s}").unwrap();
1439            }
1440        }
1441        return true;
1442    }
1443    false
1444}
1445
1446/// This function writes `text` into `out` with some modifications depending on `klass`:
1447///
1448/// * If `klass` is `None`, `text` is written into `out` with no modification.
1449/// * If `klass` is `Some` but `klass.get_span()` is `None`, it writes the text wrapped in a
1450///   `<span>` with the provided `klass`.
1451/// * If `klass` is `Some` and has a [`rustc_span::Span`], it then tries to generate a link (`<a>`
1452///   element) by retrieving the link information from the `span_correspondence_map` that was filled
1453///   in `span_map.rs::collect_spans_and_sources`. If it cannot retrieve the information, then it's
1454///   the same as the second point (`klass` is `Some` but doesn't have a [`rustc_span::Span`]).
1455fn string_without_closing_tag<T: Display>(
1456    out: &mut impl Write,
1457    text: T,
1458    klass: Option<Class>,
1459    href_context: &Option<HrefContext<'_, '_>>,
1460    open_tag: bool,
1461) -> Option<&'static str> {
1462    let Some(klass) = klass else {
1463        write!(out, "{text}").unwrap();
1464        return None;
1465    };
1466    let Some(def_span) = klass.get_span() else {
1467        if !open_tag {
1468            write!(out, "{text}").unwrap();
1469            return None;
1470        }
1471        write!(out, "<span class=\"{klass}\">{text}", klass = klass.as_html()).unwrap();
1472        return Some("</span>");
1473    };
1474
1475    let mut added_links = false;
1476    let mut text_s = text.to_string();
1477    if text_s.contains("::") {
1478        let mut span = def_span.with_hi(def_span.lo());
1479        text_s = text_s.split("::").intersperse("::").fold(String::new(), |mut path, t| {
1480            span = span.with_hi(span.hi() + BytePos(t.len() as _));
1481            match t {
1482                "::" => write!(&mut path, "::"),
1483                "self" | "Self" => write!(
1484                    &mut path,
1485                    "<span class=\"{klass}\">{t}</span>",
1486                    klass = Class::Self_(DUMMY_SP).as_html(),
1487                ),
1488                "crate" | "super" => {
1489                    write!(
1490                        &mut path,
1491                        "<span class=\"{klass}\">{t}</span>",
1492                        klass = Class::KeyWord.as_html(),
1493                    )
1494                }
1495                t => {
1496                    if !t.is_empty()
1497                        && generate_link_to_def(&mut path, t, klass, href_context, span, open_tag)
1498                    {
1499                        added_links = true;
1500                        write!(&mut path, "</a>")
1501                    } else {
1502                        write!(&mut path, "{t}")
1503                    }
1504                }
1505            }
1506            .expect("Failed to build source HTML path");
1507            span = span.with_lo(span.lo() + BytePos(t.len() as _));
1508            path
1509        });
1510    }
1511
1512    if !added_links && generate_link_to_def(out, &text_s, klass, href_context, def_span, open_tag) {
1513        return Some("</a>");
1514    }
1515    if !open_tag {
1516        out.write_str(&text_s).unwrap();
1517        return None;
1518    }
1519    let klass_s = klass.as_html();
1520    if klass_s.is_empty() {
1521        out.write_str(&text_s).unwrap();
1522        Some("")
1523    } else {
1524        write!(out, "<span class=\"{klass_s}\">{text_s}").unwrap();
1525        Some("</span>")
1526    }
1527}
1528
1529#[cfg(test)]
1530mod tests;