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