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