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