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