rustdoc/html/
markdown.rs

1//! Markdown formatting for rustdoc.
2//!
3//! This module implements markdown formatting through the pulldown-cmark library.
4//!
5//! ```
6//! #![feature(rustc_private)]
7//!
8//! extern crate rustc_span;
9//!
10//! use rustc_span::edition::Edition;
11//! use rustdoc::html::markdown::{HeadingOffset, IdMap, Markdown, ErrorCodes};
12//!
13//! let s = "My *markdown* _text_";
14//! let mut id_map = IdMap::new();
15//! let md = Markdown {
16//!     content: s,
17//!     links: &[],
18//!     ids: &mut id_map,
19//!     error_codes: ErrorCodes::Yes,
20//!     edition: Edition::Edition2015,
21//!     playground: &None,
22//!     heading_offset: HeadingOffset::H2,
23//! };
24//! let mut html = String::new();
25//! md.write_into(&mut html).unwrap();
26//! // ... something using html
27//! ```
28
29use std::borrow::Cow;
30use std::collections::VecDeque;
31use std::fmt::{self, Write};
32use std::iter::Peekable;
33use std::ops::{ControlFlow, Range};
34use std::path::PathBuf;
35use std::str::{self, CharIndices};
36use std::sync::atomic::AtomicUsize;
37use std::sync::{Arc, Weak};
38
39use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
40use rustc_errors::{Diag, DiagMessage};
41use rustc_hir::def_id::LocalDefId;
42use rustc_middle::ty::TyCtxt;
43pub(crate) use rustc_resolve::rustdoc::main_body_opts;
44use rustc_resolve::rustdoc::may_be_doc_link;
45use rustc_resolve::rustdoc::pulldown_cmark::{
46    self, BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd, html,
47};
48use rustc_span::edition::Edition;
49use rustc_span::{Span, Symbol};
50use tracing::{debug, trace};
51
52use crate::clean::RenderedLink;
53use crate::doctest;
54use crate::doctest::GlobalTestOptions;
55use crate::html::escape::{Escape, EscapeBodyText};
56use crate::html::highlight;
57use crate::html::length_limit::HtmlWithLimit;
58use crate::html::render::small_url_encode;
59use crate::html::toc::{Toc, TocBuilder};
60
61mod footnotes;
62#[cfg(test)]
63mod tests;
64
65const MAX_HEADER_LEVEL: u32 = 6;
66
67/// Options for rendering Markdown in summaries (e.g., in search results).
68pub(crate) fn summary_opts() -> Options {
69    Options::ENABLE_TABLES
70        | Options::ENABLE_FOOTNOTES
71        | Options::ENABLE_STRIKETHROUGH
72        | Options::ENABLE_TASKLISTS
73        | Options::ENABLE_SMART_PUNCTUATION
74}
75
76#[derive(Debug, Clone, Copy)]
77pub enum HeadingOffset {
78    H1 = 0,
79    H2,
80    H3,
81    H4,
82    H5,
83    H6,
84}
85
86/// When `to_string` is called, this struct will emit the HTML corresponding to
87/// the rendered version of the contained markdown string.
88pub struct Markdown<'a> {
89    pub content: &'a str,
90    /// A list of link replacements.
91    pub links: &'a [RenderedLink],
92    /// The current list of used header IDs.
93    pub ids: &'a mut IdMap,
94    /// Whether to allow the use of explicit error codes in doctest lang strings.
95    pub error_codes: ErrorCodes,
96    /// Default edition to use when parsing doctests (to add a `fn main`).
97    pub edition: Edition,
98    pub playground: &'a Option<Playground>,
99    /// Offset at which we render headings.
100    /// E.g. if `heading_offset: HeadingOffset::H2`, then `# something` renders an `<h2>`.
101    pub heading_offset: HeadingOffset,
102}
103/// A struct like `Markdown` that renders the markdown with a table of contents.
104pub(crate) struct MarkdownWithToc<'a> {
105    pub(crate) content: &'a str,
106    pub(crate) links: &'a [RenderedLink],
107    pub(crate) ids: &'a mut IdMap,
108    pub(crate) error_codes: ErrorCodes,
109    pub(crate) edition: Edition,
110    pub(crate) playground: &'a Option<Playground>,
111}
112/// A tuple struct like `Markdown` that renders the markdown escaping HTML tags
113/// and includes no paragraph tags.
114pub(crate) struct MarkdownItemInfo<'a>(pub(crate) &'a str, pub(crate) &'a mut IdMap);
115/// A tuple struct like `Markdown` that renders only the first paragraph.
116pub(crate) struct MarkdownSummaryLine<'a>(pub &'a str, pub &'a [RenderedLink]);
117
118#[derive(Copy, Clone, PartialEq, Debug)]
119pub enum ErrorCodes {
120    Yes,
121    No,
122}
123
124impl ErrorCodes {
125    pub(crate) fn from(b: bool) -> Self {
126        match b {
127            true => ErrorCodes::Yes,
128            false => ErrorCodes::No,
129        }
130    }
131
132    pub(crate) fn as_bool(self) -> bool {
133        match self {
134            ErrorCodes::Yes => true,
135            ErrorCodes::No => false,
136        }
137    }
138}
139
140/// Controls whether a line will be hidden or shown in HTML output.
141///
142/// All lines are used in documentation tests.
143pub(crate) enum Line<'a> {
144    Hidden(&'a str),
145    Shown(Cow<'a, str>),
146}
147
148impl<'a> Line<'a> {
149    fn for_html(self) -> Option<Cow<'a, str>> {
150        match self {
151            Line::Shown(l) => Some(l),
152            Line::Hidden(_) => None,
153        }
154    }
155
156    pub(crate) fn for_code(self) -> Cow<'a, str> {
157        match self {
158            Line::Shown(l) => l,
159            Line::Hidden(l) => Cow::Borrowed(l),
160        }
161    }
162}
163
164/// This function is used to handle the "hidden lines" (ie starting with `#`) in
165/// doctests. It also transforms `##` back into `#`.
166// FIXME: There is a minor inconsistency here. For lines that start with ##, we
167// have no easy way of removing a potential single space after the hashes, which
168// is done in the single # case. This inconsistency seems okay, if non-ideal. In
169// order to fix it we'd have to iterate to find the first non-# character, and
170// then reallocate to remove it; which would make us return a String.
171pub(crate) fn map_line(s: &str) -> Line<'_> {
172    let trimmed = s.trim();
173    if trimmed.starts_with("##") {
174        Line::Shown(Cow::Owned(s.replacen("##", "#", 1)))
175    } else if let Some(stripped) = trimmed.strip_prefix("# ") {
176        // # text
177        Line::Hidden(stripped)
178    } else if trimmed == "#" {
179        // We cannot handle '#text' because it could be #[attr].
180        Line::Hidden("")
181    } else {
182        Line::Shown(Cow::Borrowed(s))
183    }
184}
185
186/// Convert chars from a title for an id.
187///
188/// "Hello, world!" -> "hello-world"
189fn slugify(c: char) -> Option<char> {
190    if c.is_alphanumeric() || c == '-' || c == '_' {
191        if c.is_ascii() { Some(c.to_ascii_lowercase()) } else { Some(c) }
192    } else if c.is_whitespace() && c.is_ascii() {
193        Some('-')
194    } else {
195        None
196    }
197}
198
199#[derive(Debug)]
200pub struct Playground {
201    pub crate_name: Option<Symbol>,
202    pub url: String,
203}
204
205/// Adds syntax highlighting and playground Run buttons to Rust code blocks.
206struct CodeBlocks<'p, 'a, I: Iterator<Item = Event<'a>>> {
207    inner: I,
208    check_error_codes: ErrorCodes,
209    edition: Edition,
210    // Information about the playground if a URL has been specified, containing an
211    // optional crate name and the URL.
212    playground: &'p Option<Playground>,
213}
214
215impl<'p, 'a, I: Iterator<Item = Event<'a>>> CodeBlocks<'p, 'a, I> {
216    fn new(
217        iter: I,
218        error_codes: ErrorCodes,
219        edition: Edition,
220        playground: &'p Option<Playground>,
221    ) -> Self {
222        CodeBlocks { inner: iter, check_error_codes: error_codes, edition, playground }
223    }
224}
225
226impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
227    type Item = Event<'a>;
228
229    fn next(&mut self) -> Option<Self::Item> {
230        let event = self.inner.next();
231        let Some(Event::Start(Tag::CodeBlock(kind))) = event else {
232            return event;
233        };
234
235        let mut original_text = String::new();
236        for event in &mut self.inner {
237            match event {
238                Event::End(TagEnd::CodeBlock) => break,
239                Event::Text(ref s) => {
240                    original_text.push_str(s);
241                }
242                _ => {}
243            }
244        }
245
246        let LangString { added_classes, compile_fail, should_panic, ignore, edition, .. } =
247            match kind {
248                CodeBlockKind::Fenced(ref lang) => {
249                    let parse_result =
250                        LangString::parse_without_check(lang, self.check_error_codes);
251                    if !parse_result.rust {
252                        let added_classes = parse_result.added_classes;
253                        let lang_string = if let Some(lang) = parse_result.unknown.first() {
254                            format!("language-{lang}")
255                        } else {
256                            String::new()
257                        };
258                        let whitespace = if added_classes.is_empty() { "" } else { " " };
259                        return Some(Event::Html(
260                            format!(
261                                "<div class=\"example-wrap\">\
262                                 <pre class=\"{lang_string}{whitespace}{added_classes}\">\
263                                     <code>{text}</code>\
264                                 </pre>\
265                             </div>",
266                                added_classes = added_classes.join(" "),
267                                text = Escape(original_text.trim_suffix('\n')),
268                            )
269                            .into(),
270                        ));
271                    }
272                    parse_result
273                }
274                CodeBlockKind::Indented => Default::default(),
275            };
276
277        let lines = original_text.lines().filter_map(|l| map_line(l).for_html());
278        let text = lines.intersperse("\n".into()).collect::<String>();
279
280        let explicit_edition = edition.is_some();
281        let edition = edition.unwrap_or(self.edition);
282
283        let playground_button = self.playground.as_ref().and_then(|playground| {
284            let krate = &playground.crate_name;
285            let url = &playground.url;
286            if url.is_empty() {
287                return None;
288            }
289            let test = original_text
290                .lines()
291                .map(|l| map_line(l).for_code())
292                .intersperse("\n".into())
293                .collect::<String>();
294            let krate = krate.as_ref().map(|s| s.as_str());
295
296            // FIXME: separate out the code to make a code block into runnable code
297            //        from the complicated doctest logic
298            let opts = GlobalTestOptions {
299                crate_name: krate.map(String::from).unwrap_or_default(),
300                no_crate_inject: false,
301                insert_indent_space: true,
302                args_file: PathBuf::new(),
303            };
304            let mut builder = doctest::BuildDocTestBuilder::new(&test).edition(edition);
305            if let Some(krate) = krate {
306                builder = builder.crate_name(krate);
307            }
308            let doctest = builder.build(None);
309            let (wrapped, _) = doctest.generate_unique_doctest(&test, false, &opts, krate);
310            let test = wrapped.to_string();
311            let channel = if test.contains("#![feature(") { "&amp;version=nightly" } else { "" };
312
313            let test_escaped = small_url_encode(test);
314            Some(format!(
315                "<a class=\"test-arrow\" \
316                    target=\"_blank\" \
317                    title=\"Run code\" \
318                    href=\"{url}?code={test_escaped}{channel}&amp;edition={edition}\"></a>",
319            ))
320        });
321
322        let tooltip = {
323            use highlight::Tooltip::*;
324
325            if ignore == Ignore::All {
326                Some(IgnoreAll)
327            } else if let Ignore::Some(platforms) = ignore {
328                Some(IgnoreSome(platforms))
329            } else if compile_fail {
330                Some(CompileFail)
331            } else if should_panic {
332                Some(ShouldPanic)
333            } else if explicit_edition {
334                Some(Edition(edition))
335            } else {
336                None
337            }
338        };
339
340        // insert newline to clearly separate it from the
341        // previous block so we can shorten the html output
342        let s = format!(
343            "\n{}",
344            highlight::render_example_with_highlighting(
345                &text,
346                tooltip.as_ref(),
347                playground_button.as_deref(),
348                &added_classes,
349            )
350        );
351        Some(Event::Html(s.into()))
352    }
353}
354
355/// Make headings links with anchor IDs and build up TOC.
356struct LinkReplacerInner<'a> {
357    links: &'a [RenderedLink],
358    shortcut_link: Option<&'a RenderedLink>,
359}
360
361struct LinkReplacer<'a, I: Iterator<Item = Event<'a>>> {
362    iter: I,
363    inner: LinkReplacerInner<'a>,
364}
365
366impl<'a, I: Iterator<Item = Event<'a>>> LinkReplacer<'a, I> {
367    fn new(iter: I, links: &'a [RenderedLink]) -> Self {
368        LinkReplacer { iter, inner: { LinkReplacerInner { links, shortcut_link: None } } }
369    }
370}
371
372// FIXME: Once we have specialized trait impl (for `Iterator` impl on `LinkReplacer`),
373// we can remove this type and move back `LinkReplacerInner` fields into `LinkReplacer`.
374struct SpannedLinkReplacer<'a, I: Iterator<Item = SpannedEvent<'a>>> {
375    iter: I,
376    inner: LinkReplacerInner<'a>,
377}
378
379impl<'a, I: Iterator<Item = SpannedEvent<'a>>> SpannedLinkReplacer<'a, I> {
380    fn new(iter: I, links: &'a [RenderedLink]) -> Self {
381        SpannedLinkReplacer { iter, inner: { LinkReplacerInner { links, shortcut_link: None } } }
382    }
383}
384
385impl<'a> LinkReplacerInner<'a> {
386    fn handle_event(&mut self, event: &mut Event<'a>) {
387        // Replace intra-doc links and remove disambiguators from shortcut links (`[fn@f]`).
388        match event {
389            // This is a shortcut link that was resolved by the broken_link_callback: `[fn@f]`
390            // Remove any disambiguator.
391            Event::Start(Tag::Link {
392                // [fn@f] or [fn@f][]
393                link_type: LinkType::ShortcutUnknown | LinkType::CollapsedUnknown,
394                dest_url,
395                title,
396                ..
397            }) => {
398                debug!("saw start of shortcut link to {dest_url} with title {title}");
399                // If this is a shortcut link, it was resolved by the broken_link_callback.
400                // So the URL will already be updated properly.
401                let link = self.links.iter().find(|&link| *link.href == **dest_url);
402                // Since this is an external iterator, we can't replace the inner text just yet.
403                // Store that we saw a link so we know to replace it later.
404                if let Some(link) = link {
405                    trace!("it matched");
406                    assert!(self.shortcut_link.is_none(), "shortcut links cannot be nested");
407                    self.shortcut_link = Some(link);
408                    if title.is_empty() && !link.tooltip.is_empty() {
409                        *title = CowStr::Borrowed(link.tooltip.as_ref());
410                    }
411                }
412            }
413            // Now that we're done with the shortcut link, don't replace any more text.
414            Event::End(TagEnd::Link) if self.shortcut_link.is_some() => {
415                debug!("saw end of shortcut link");
416                self.shortcut_link = None;
417            }
418            // Handle backticks in inline code blocks, but only if we're in the middle of a shortcut link.
419            // [`fn@f`]
420            Event::Code(text) => {
421                trace!("saw code {text}");
422                if let Some(link) = self.shortcut_link {
423                    // NOTE: this only replaces if the code block is the *entire* text.
424                    // If only part of the link has code highlighting, the disambiguator will not be removed.
425                    // e.g. [fn@`f`]
426                    // This is a limitation from `collect_intra_doc_links`: it passes a full link,
427                    // and does not distinguish at all between code blocks.
428                    // So we could never be sure we weren't replacing too much:
429                    // [fn@my_`f`unc] is treated the same as [my_func()] in that pass.
430                    //
431                    // NOTE: .get(1..len() - 1) is to strip the backticks
432                    if let Some(link) = self.links.iter().find(|l| {
433                        l.href == link.href
434                            && Some(&**text) == l.original_text.get(1..l.original_text.len() - 1)
435                    }) {
436                        debug!("replacing {text} with {new_text}", new_text = link.new_text);
437                        *text = CowStr::Borrowed(&link.new_text);
438                    }
439                }
440            }
441            // Replace plain text in links, but only in the middle of a shortcut link.
442            // [fn@f]
443            Event::Text(text) => {
444                trace!("saw text {text}");
445                if let Some(link) = self.shortcut_link {
446                    // NOTE: same limitations as `Event::Code`
447                    if let Some(link) = self
448                        .links
449                        .iter()
450                        .find(|l| l.href == link.href && **text == *l.original_text)
451                    {
452                        debug!("replacing {text} with {new_text}", new_text = link.new_text);
453                        *text = CowStr::Borrowed(&link.new_text);
454                    }
455                }
456            }
457            // If this is a link, but not a shortcut link,
458            // replace the URL, since the broken_link_callback was not called.
459            Event::Start(Tag::Link { dest_url, title, .. }) => {
460                if let Some(link) =
461                    self.links.iter().find(|&link| *link.original_text == **dest_url)
462                {
463                    *dest_url = CowStr::Borrowed(link.href.as_ref());
464                    if title.is_empty() && !link.tooltip.is_empty() {
465                        *title = CowStr::Borrowed(link.tooltip.as_ref());
466                    }
467                }
468            }
469            // Anything else couldn't have been a valid Rust path, so no need to replace the text.
470            _ => {}
471        }
472    }
473}
474
475impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
476    type Item = Event<'a>;
477
478    fn next(&mut self) -> Option<Self::Item> {
479        let mut event = self.iter.next();
480        if let Some(ref mut event) = event {
481            self.inner.handle_event(event);
482        }
483        // Yield the modified event
484        event
485    }
486}
487
488impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for SpannedLinkReplacer<'a, I> {
489    type Item = SpannedEvent<'a>;
490
491    fn next(&mut self) -> Option<Self::Item> {
492        let (mut event, range) = self.iter.next()?;
493        self.inner.handle_event(&mut event);
494        // Yield the modified event
495        Some((event, range))
496    }
497}
498
499/// Wrap HTML tables into `<div>` to prevent having the doc blocks width being too big.
500struct TableWrapper<'a, I: Iterator<Item = Event<'a>>> {
501    inner: I,
502    stored_events: VecDeque<Event<'a>>,
503}
504
505impl<'a, I: Iterator<Item = Event<'a>>> TableWrapper<'a, I> {
506    fn new(iter: I) -> Self {
507        Self { inner: iter, stored_events: VecDeque::new() }
508    }
509}
510
511impl<'a, I: Iterator<Item = Event<'a>>> Iterator for TableWrapper<'a, I> {
512    type Item = Event<'a>;
513
514    fn next(&mut self) -> Option<Self::Item> {
515        if let Some(first) = self.stored_events.pop_front() {
516            return Some(first);
517        }
518
519        let event = self.inner.next()?;
520
521        Some(match event {
522            Event::Start(Tag::Table(t)) => {
523                self.stored_events.push_back(Event::Start(Tag::Table(t)));
524                Event::Html(CowStr::Borrowed("<div>"))
525            }
526            Event::End(TagEnd::Table) => {
527                self.stored_events.push_back(Event::Html(CowStr::Borrowed("</div>")));
528                Event::End(TagEnd::Table)
529            }
530            e => e,
531        })
532    }
533}
534
535type SpannedEvent<'a> = (Event<'a>, Range<usize>);
536
537/// Make headings links with anchor IDs and build up TOC.
538struct HeadingLinks<'a, 'b, 'ids, I> {
539    inner: I,
540    toc: Option<&'b mut TocBuilder>,
541    buf: VecDeque<SpannedEvent<'a>>,
542    id_map: &'ids mut IdMap,
543    heading_offset: HeadingOffset,
544}
545
546impl<'b, 'ids, I> HeadingLinks<'_, 'b, 'ids, I> {
547    fn new(
548        iter: I,
549        toc: Option<&'b mut TocBuilder>,
550        ids: &'ids mut IdMap,
551        heading_offset: HeadingOffset,
552    ) -> Self {
553        HeadingLinks { inner: iter, toc, buf: VecDeque::new(), id_map: ids, heading_offset }
554    }
555}
556
557impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for HeadingLinks<'a, '_, '_, I> {
558    type Item = SpannedEvent<'a>;
559
560    fn next(&mut self) -> Option<Self::Item> {
561        if let Some(e) = self.buf.pop_front() {
562            return Some(e);
563        }
564
565        let event = self.inner.next();
566        if let Some((Event::Start(Tag::Heading { level, .. }), _)) = event {
567            let mut id = String::new();
568            for event in &mut self.inner {
569                match &event.0 {
570                    Event::End(TagEnd::Heading(_)) => break,
571                    Event::Text(text) | Event::Code(text) => {
572                        id.extend(text.chars().filter_map(slugify));
573                        self.buf.push_back(event);
574                    }
575                    _ => self.buf.push_back(event),
576                }
577            }
578            let id = self.id_map.derive(id);
579
580            if let Some(ref mut builder) = self.toc {
581                let mut text_header = String::new();
582                plain_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut text_header);
583                let mut html_header = String::new();
584                html_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut html_header);
585                let sec = builder.push(level as u32, text_header, html_header, id.clone());
586                self.buf.push_front((Event::Html(format!("{sec} ").into()), 0..0));
587            }
588
589            let level =
590                std::cmp::min(level as u32 + (self.heading_offset as u32), MAX_HEADER_LEVEL);
591            self.buf.push_back((Event::Html(format!("</h{level}>").into()), 0..0));
592
593            let start_tags =
594                format!("<h{level} id=\"{id}\"><a class=\"doc-anchor\" href=\"#{id}\">§</a>");
595            return Some((Event::Html(start_tags.into()), 0..0));
596        }
597        event
598    }
599}
600
601/// Extracts just the first paragraph.
602struct SummaryLine<'a, I: Iterator<Item = Event<'a>>> {
603    inner: I,
604    started: bool,
605    depth: u32,
606    skipped_tags: u32,
607}
608
609impl<'a, I: Iterator<Item = Event<'a>>> SummaryLine<'a, I> {
610    fn new(iter: I) -> Self {
611        SummaryLine { inner: iter, started: false, depth: 0, skipped_tags: 0 }
612    }
613}
614
615fn check_if_allowed_tag(t: &TagEnd) -> bool {
616    matches!(
617        t,
618        TagEnd::Paragraph
619            | TagEnd::Emphasis
620            | TagEnd::Strong
621            | TagEnd::Strikethrough
622            | TagEnd::Link
623            | TagEnd::BlockQuote
624    )
625}
626
627fn is_forbidden_tag(t: &TagEnd) -> bool {
628    matches!(
629        t,
630        TagEnd::CodeBlock
631            | TagEnd::Table
632            | TagEnd::TableHead
633            | TagEnd::TableRow
634            | TagEnd::TableCell
635            | TagEnd::FootnoteDefinition
636    )
637}
638
639impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SummaryLine<'a, I> {
640    type Item = Event<'a>;
641
642    fn next(&mut self) -> Option<Self::Item> {
643        if self.started && self.depth == 0 {
644            return None;
645        }
646        if !self.started {
647            self.started = true;
648        }
649        if let Some(event) = self.inner.next() {
650            let mut is_start = true;
651            let is_allowed_tag = match event {
652                Event::Start(ref c) => {
653                    if is_forbidden_tag(&c.to_end()) {
654                        self.skipped_tags += 1;
655                        return None;
656                    }
657                    self.depth += 1;
658                    check_if_allowed_tag(&c.to_end())
659                }
660                Event::End(ref c) => {
661                    if is_forbidden_tag(c) {
662                        self.skipped_tags += 1;
663                        return None;
664                    }
665                    self.depth -= 1;
666                    is_start = false;
667                    check_if_allowed_tag(c)
668                }
669                Event::FootnoteReference(_) => {
670                    self.skipped_tags += 1;
671                    false
672                }
673                _ => true,
674            };
675            if !is_allowed_tag {
676                self.skipped_tags += 1;
677            }
678            return if !is_allowed_tag {
679                if is_start {
680                    Some(Event::Start(Tag::Paragraph))
681                } else {
682                    Some(Event::End(TagEnd::Paragraph))
683                }
684            } else {
685                Some(event)
686            };
687        }
688        None
689    }
690}
691
692/// A newtype that represents a relative line number in Markdown.
693///
694/// In other words, this represents an offset from the first line of Markdown
695/// in a doc comment or other source. If the first Markdown line appears on line 32,
696/// and the `MdRelLine` is 3, then the absolute line for this one is 35. I.e., it's
697/// a zero-based offset.
698pub(crate) struct MdRelLine {
699    offset: usize,
700}
701
702impl MdRelLine {
703    /// See struct docs.
704    pub(crate) const fn new(offset: usize) -> Self {
705        Self { offset }
706    }
707
708    /// See struct docs.
709    pub(crate) const fn offset(self) -> usize {
710        self.offset
711    }
712}
713
714pub(crate) fn find_testable_code<T: doctest::DocTestVisitor>(
715    doc: &str,
716    tests: &mut T,
717    error_codes: ErrorCodes,
718    extra_info: Option<&ExtraInfo<'_>>,
719) {
720    find_codes(doc, tests, error_codes, extra_info, false)
721}
722
723pub(crate) fn find_codes<T: doctest::DocTestVisitor>(
724    doc: &str,
725    tests: &mut T,
726    error_codes: ErrorCodes,
727    extra_info: Option<&ExtraInfo<'_>>,
728    include_non_rust: bool,
729) {
730    let mut parser = Parser::new_ext(doc, main_body_opts()).into_offset_iter();
731    let mut prev_offset = 0;
732    let mut nb_lines = 0;
733    let mut register_header = None;
734    while let Some((event, offset)) = parser.next() {
735        match event {
736            Event::Start(Tag::CodeBlock(kind)) => {
737                let block_info = match kind {
738                    CodeBlockKind::Fenced(ref lang) => {
739                        if lang.is_empty() {
740                            Default::default()
741                        } else {
742                            LangString::parse(lang, error_codes, extra_info)
743                        }
744                    }
745                    CodeBlockKind::Indented => Default::default(),
746                };
747                if !include_non_rust && !block_info.rust {
748                    continue;
749                }
750
751                let mut test_s = String::new();
752
753                while let Some((Event::Text(s), _)) = parser.next() {
754                    test_s.push_str(&s);
755                }
756                let text = test_s
757                    .lines()
758                    .map(|l| map_line(l).for_code())
759                    .collect::<Vec<Cow<'_, str>>>()
760                    .join("\n");
761
762                nb_lines += doc[prev_offset..offset.start].lines().count();
763                // If there are characters between the preceding line ending and
764                // this code block, `str::lines` will return an additional line,
765                // which we subtract here.
766                if nb_lines != 0 && !&doc[prev_offset..offset.start].ends_with('\n') {
767                    nb_lines -= 1;
768                }
769                let line = MdRelLine::new(nb_lines);
770                tests.visit_test(text, block_info, line);
771                prev_offset = offset.start;
772            }
773            Event::Start(Tag::Heading { level, .. }) => {
774                register_header = Some(level as u32);
775            }
776            Event::Text(ref s) if register_header.is_some() => {
777                let level = register_header.unwrap();
778                tests.visit_header(s, level);
779                register_header = None;
780            }
781            _ => {}
782        }
783    }
784}
785
786pub(crate) struct ExtraInfo<'tcx> {
787    def_id: LocalDefId,
788    sp: Span,
789    tcx: TyCtxt<'tcx>,
790}
791
792impl<'tcx> ExtraInfo<'tcx> {
793    pub(crate) fn new(tcx: TyCtxt<'tcx>, def_id: LocalDefId, sp: Span) -> ExtraInfo<'tcx> {
794        ExtraInfo { def_id, sp, tcx }
795    }
796
797    fn error_invalid_codeblock_attr(&self, msg: impl Into<DiagMessage>) {
798        self.tcx.node_span_lint(
799            crate::lint::INVALID_CODEBLOCK_ATTRIBUTES,
800            self.tcx.local_def_id_to_hir_id(self.def_id),
801            self.sp,
802            |lint| {
803                lint.primary_message(msg);
804            },
805        );
806    }
807
808    fn error_invalid_codeblock_attr_with_help(
809        &self,
810        msg: impl Into<DiagMessage>,
811        f: impl for<'a, 'b> FnOnce(&'b mut Diag<'a, ()>),
812    ) {
813        self.tcx.node_span_lint(
814            crate::lint::INVALID_CODEBLOCK_ATTRIBUTES,
815            self.tcx.local_def_id_to_hir_id(self.def_id),
816            self.sp,
817            |lint| {
818                lint.primary_message(msg);
819                f(lint);
820            },
821        );
822    }
823}
824
825#[derive(Eq, PartialEq, Clone, Debug)]
826pub(crate) struct LangString {
827    pub(crate) original: String,
828    pub(crate) should_panic: bool,
829    pub(crate) no_run: bool,
830    pub(crate) ignore: Ignore,
831    pub(crate) rust: bool,
832    pub(crate) test_harness: bool,
833    pub(crate) compile_fail: bool,
834    pub(crate) standalone_crate: bool,
835    pub(crate) error_codes: Vec<String>,
836    pub(crate) edition: Option<Edition>,
837    pub(crate) added_classes: Vec<String>,
838    pub(crate) unknown: Vec<String>,
839}
840
841#[derive(Eq, PartialEq, Clone, Debug)]
842pub(crate) enum Ignore {
843    All,
844    None,
845    Some(Vec<String>),
846}
847
848/// This is the parser for fenced codeblocks attributes. It implements the following eBNF:
849///
850/// ```eBNF
851/// lang-string = *(token-list / delimited-attribute-list / comment)
852///
853/// bareword = LEADINGCHAR *(CHAR)
854/// bareword-without-leading-char = CHAR *(CHAR)
855/// quoted-string = QUOTE *(NONQUOTE) QUOTE
856/// token = bareword / quoted-string
857/// token-without-leading-char = bareword-without-leading-char / quoted-string
858/// sep = COMMA/WS *(COMMA/WS)
859/// attribute = (DOT token)/(token EQUAL token-without-leading-char)
860/// attribute-list = [sep] attribute *(sep attribute) [sep]
861/// delimited-attribute-list = OPEN-CURLY-BRACKET attribute-list CLOSE-CURLY-BRACKET
862/// token-list = [sep] token *(sep token) [sep]
863/// comment = OPEN_PAREN *(all characters) CLOSE_PAREN
864///
865/// OPEN_PAREN = "("
866/// CLOSE_PARENT = ")"
867/// OPEN-CURLY-BRACKET = "{"
868/// CLOSE-CURLY-BRACKET = "}"
869/// LEADINGCHAR = ALPHA | DIGIT | "_" | "-" | ":"
870/// ; All ASCII punctuation except comma, quote, equals, backslash, grave (backquote) and braces.
871/// ; Comma is used to separate language tokens, so it can't be used in one.
872/// ; Quote is used to allow otherwise-disallowed characters in language tokens.
873/// ; Equals is used to make key=value pairs in attribute blocks.
874/// ; Backslash and grave are special Markdown characters.
875/// ; Braces are used to start an attribute block.
876/// CHAR = ALPHA | DIGIT | "_" | "-" | ":" | "." | "!" | "#" | "$" | "%" | "&" | "*" | "+" | "/" |
877///        ";" | "<" | ">" | "?" | "@" | "^" | "|" | "~"
878/// NONQUOTE = %x09 / %x20 / %x21 / %x23-7E ; TAB / SPACE / all printable characters except `"`
879/// COMMA = ","
880/// DOT = "."
881/// EQUAL = "="
882///
883/// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
884/// DIGIT = %x30-39
885/// WS = %x09 / " "
886/// ```
887pub(crate) struct TagIterator<'a, 'tcx> {
888    inner: Peekable<CharIndices<'a>>,
889    data: &'a str,
890    is_in_attribute_block: bool,
891    extra: Option<&'a ExtraInfo<'tcx>>,
892    is_error: bool,
893}
894
895#[derive(Clone, Debug, Eq, PartialEq)]
896pub(crate) enum LangStringToken<'a> {
897    LangToken(&'a str),
898    ClassAttribute(&'a str),
899    KeyValueAttribute(&'a str, &'a str),
900}
901
902fn is_leading_char(c: char) -> bool {
903    c == '_' || c == '-' || c == ':' || c.is_ascii_alphabetic() || c.is_ascii_digit()
904}
905fn is_bareword_char(c: char) -> bool {
906    is_leading_char(c) || ".!#$%&*+/;<>?@^|~".contains(c)
907}
908fn is_separator(c: char) -> bool {
909    c == ' ' || c == ',' || c == '\t'
910}
911
912struct Indices {
913    start: usize,
914    end: usize,
915}
916
917impl<'a, 'tcx> TagIterator<'a, 'tcx> {
918    pub(crate) fn new(data: &'a str, extra: Option<&'a ExtraInfo<'tcx>>) -> Self {
919        Self {
920            inner: data.char_indices().peekable(),
921            data,
922            is_in_attribute_block: false,
923            extra,
924            is_error: false,
925        }
926    }
927
928    fn emit_error(&mut self, err: impl Into<DiagMessage>) {
929        if let Some(extra) = self.extra {
930            extra.error_invalid_codeblock_attr(err);
931        }
932        self.is_error = true;
933    }
934
935    fn skip_separators(&mut self) -> Option<usize> {
936        while let Some((pos, c)) = self.inner.peek() {
937            if !is_separator(*c) {
938                return Some(*pos);
939            }
940            self.inner.next();
941        }
942        None
943    }
944
945    fn parse_string(&mut self, start: usize) -> Option<Indices> {
946        for (pos, c) in self.inner.by_ref() {
947            if c == '"' {
948                return Some(Indices { start: start + 1, end: pos });
949            }
950        }
951        self.emit_error("unclosed quote string `\"`");
952        None
953    }
954
955    fn parse_class(&mut self, start: usize) -> Option<LangStringToken<'a>> {
956        while let Some((pos, c)) = self.inner.peek().copied() {
957            if is_bareword_char(c) {
958                self.inner.next();
959            } else {
960                let class = &self.data[start + 1..pos];
961                if class.is_empty() {
962                    self.emit_error(format!("unexpected `{c}` character after `.`"));
963                    return None;
964                } else if self.check_after_token() {
965                    return Some(LangStringToken::ClassAttribute(class));
966                } else {
967                    return None;
968                }
969            }
970        }
971        let class = &self.data[start + 1..];
972        if class.is_empty() {
973            self.emit_error("missing character after `.`");
974            None
975        } else if self.check_after_token() {
976            Some(LangStringToken::ClassAttribute(class))
977        } else {
978            None
979        }
980    }
981
982    fn parse_token(&mut self, start: usize) -> Option<Indices> {
983        while let Some((pos, c)) = self.inner.peek() {
984            if !is_bareword_char(*c) {
985                return Some(Indices { start, end: *pos });
986            }
987            self.inner.next();
988        }
989        self.emit_error("unexpected end");
990        None
991    }
992
993    fn parse_key_value(&mut self, c: char, start: usize) -> Option<LangStringToken<'a>> {
994        let key_indices =
995            if c == '"' { self.parse_string(start)? } else { self.parse_token(start)? };
996        if key_indices.start == key_indices.end {
997            self.emit_error("unexpected empty string as key");
998            return None;
999        }
1000
1001        if let Some((_, c)) = self.inner.next() {
1002            if c != '=' {
1003                self.emit_error(format!("expected `=`, found `{c}`"));
1004                return None;
1005            }
1006        } else {
1007            self.emit_error("unexpected end");
1008            return None;
1009        }
1010        let value_indices = match self.inner.next() {
1011            Some((pos, '"')) => self.parse_string(pos)?,
1012            Some((pos, c)) if is_bareword_char(c) => self.parse_token(pos)?,
1013            Some((_, c)) => {
1014                self.emit_error(format!("unexpected `{c}` character after `=`"));
1015                return None;
1016            }
1017            None => {
1018                self.emit_error("expected value after `=`");
1019                return None;
1020            }
1021        };
1022        if value_indices.start == value_indices.end {
1023            self.emit_error("unexpected empty string as value");
1024            None
1025        } else if self.check_after_token() {
1026            Some(LangStringToken::KeyValueAttribute(
1027                &self.data[key_indices.start..key_indices.end],
1028                &self.data[value_indices.start..value_indices.end],
1029            ))
1030        } else {
1031            None
1032        }
1033    }
1034
1035    /// Returns `false` if an error was emitted.
1036    fn check_after_token(&mut self) -> bool {
1037        if let Some((_, c)) = self.inner.peek().copied() {
1038            if c == '}' || is_separator(c) || c == '(' {
1039                true
1040            } else {
1041                self.emit_error(format!("unexpected `{c}` character"));
1042                false
1043            }
1044        } else {
1045            // The error will be caught on the next iteration.
1046            true
1047        }
1048    }
1049
1050    fn parse_in_attribute_block(&mut self) -> Option<LangStringToken<'a>> {
1051        if let Some((pos, c)) = self.inner.next() {
1052            if c == '}' {
1053                self.is_in_attribute_block = false;
1054                return self.next();
1055            } else if c == '.' {
1056                return self.parse_class(pos);
1057            } else if c == '"' || is_leading_char(c) {
1058                return self.parse_key_value(c, pos);
1059            } else {
1060                self.emit_error(format!("unexpected character `{c}`"));
1061                return None;
1062            }
1063        }
1064        self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
1065        None
1066    }
1067
1068    /// Returns `false` if an error was emitted.
1069    fn skip_paren_block(&mut self) -> bool {
1070        for (_, c) in self.inner.by_ref() {
1071            if c == ')' {
1072                return true;
1073            }
1074        }
1075        self.emit_error("unclosed comment: missing `)` at the end");
1076        false
1077    }
1078
1079    fn parse_outside_attribute_block(&mut self, start: usize) -> Option<LangStringToken<'a>> {
1080        while let Some((pos, c)) = self.inner.next() {
1081            if c == '"' {
1082                if pos != start {
1083                    self.emit_error("expected ` `, `{` or `,` found `\"`");
1084                    return None;
1085                }
1086                let indices = self.parse_string(pos)?;
1087                if let Some((_, c)) = self.inner.peek().copied()
1088                    && c != '{'
1089                    && !is_separator(c)
1090                    && c != '('
1091                {
1092                    self.emit_error(format!("expected ` `, `{{` or `,` after `\"`, found `{c}`"));
1093                    return None;
1094                }
1095                return Some(LangStringToken::LangToken(&self.data[indices.start..indices.end]));
1096            } else if c == '{' {
1097                self.is_in_attribute_block = true;
1098                return self.next();
1099            } else if is_separator(c) {
1100                if pos != start {
1101                    return Some(LangStringToken::LangToken(&self.data[start..pos]));
1102                }
1103                return self.next();
1104            } else if c == '(' {
1105                if !self.skip_paren_block() {
1106                    return None;
1107                }
1108                if pos != start {
1109                    return Some(LangStringToken::LangToken(&self.data[start..pos]));
1110                }
1111                return self.next();
1112            } else if (pos == start && is_leading_char(c)) || (pos != start && is_bareword_char(c))
1113            {
1114                continue;
1115            } else {
1116                self.emit_error(format!("unexpected character `{c}`"));
1117                return None;
1118            }
1119        }
1120        let token = &self.data[start..];
1121        if token.is_empty() { None } else { Some(LangStringToken::LangToken(&self.data[start..])) }
1122    }
1123}
1124
1125impl<'a> Iterator for TagIterator<'a, '_> {
1126    type Item = LangStringToken<'a>;
1127
1128    fn next(&mut self) -> Option<Self::Item> {
1129        if self.is_error {
1130            return None;
1131        }
1132        let Some(start) = self.skip_separators() else {
1133            if self.is_in_attribute_block {
1134                self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
1135            }
1136            return None;
1137        };
1138        if self.is_in_attribute_block {
1139            self.parse_in_attribute_block()
1140        } else {
1141            self.parse_outside_attribute_block(start)
1142        }
1143    }
1144}
1145
1146impl Default for LangString {
1147    fn default() -> Self {
1148        Self {
1149            original: String::new(),
1150            should_panic: false,
1151            no_run: false,
1152            ignore: Ignore::None,
1153            rust: true,
1154            test_harness: false,
1155            compile_fail: false,
1156            standalone_crate: false,
1157            error_codes: Vec::new(),
1158            edition: None,
1159            added_classes: Vec::new(),
1160            unknown: Vec::new(),
1161        }
1162    }
1163}
1164
1165impl LangString {
1166    fn parse_without_check(string: &str, allow_error_code_check: ErrorCodes) -> Self {
1167        Self::parse(string, allow_error_code_check, None)
1168    }
1169
1170    fn parse(
1171        string: &str,
1172        allow_error_code_check: ErrorCodes,
1173        extra: Option<&ExtraInfo<'_>>,
1174    ) -> Self {
1175        let allow_error_code_check = allow_error_code_check.as_bool();
1176        let mut seen_rust_tags = false;
1177        let mut seen_other_tags = false;
1178        let mut seen_custom_tag = false;
1179        let mut data = LangString::default();
1180        let mut ignores = vec![];
1181
1182        data.original = string.to_owned();
1183
1184        let mut call = |tokens: &mut dyn Iterator<Item = LangStringToken<'_>>| {
1185            for token in tokens {
1186                match token {
1187                    LangStringToken::LangToken("should_panic") => {
1188                        data.should_panic = true;
1189                        seen_rust_tags = !seen_other_tags;
1190                    }
1191                    LangStringToken::LangToken("no_run") => {
1192                        data.no_run = true;
1193                        seen_rust_tags = !seen_other_tags;
1194                    }
1195                    LangStringToken::LangToken("ignore") => {
1196                        data.ignore = Ignore::All;
1197                        seen_rust_tags = !seen_other_tags;
1198                    }
1199                    LangStringToken::LangToken(x)
1200                        if let Some(ignore) = x.strip_prefix("ignore-") =>
1201                    {
1202                        ignores.push(ignore.to_owned());
1203                        seen_rust_tags = !seen_other_tags;
1204                    }
1205                    LangStringToken::LangToken("rust") => {
1206                        data.rust = true;
1207                        seen_rust_tags = true;
1208                    }
1209                    LangStringToken::LangToken("custom") => {
1210                        seen_custom_tag = true;
1211                    }
1212                    LangStringToken::LangToken("test_harness") => {
1213                        data.test_harness = true;
1214                        seen_rust_tags = !seen_other_tags || seen_rust_tags;
1215                    }
1216                    LangStringToken::LangToken("compile_fail") => {
1217                        data.compile_fail = true;
1218                        seen_rust_tags = !seen_other_tags || seen_rust_tags;
1219                        data.no_run = true;
1220                    }
1221                    LangStringToken::LangToken("standalone_crate") => {
1222                        data.standalone_crate = true;
1223                        seen_rust_tags = !seen_other_tags || seen_rust_tags;
1224                    }
1225                    LangStringToken::LangToken(x)
1226                        if let Some(edition) = x.strip_prefix("edition") =>
1227                    {
1228                        data.edition = edition.parse::<Edition>().ok();
1229                    }
1230                    LangStringToken::LangToken(x)
1231                        if let Some(edition) = x.strip_prefix("rust")
1232                            && edition.parse::<Edition>().is_ok()
1233                            && let Some(extra) = extra =>
1234                    {
1235                        extra.error_invalid_codeblock_attr_with_help(
1236                            format!("unknown attribute `{x}`"),
1237                            |lint| {
1238                                lint.help(format!(
1239                                    "there is an attribute with a similar name: `edition{edition}`"
1240                                ));
1241                            },
1242                        );
1243                    }
1244                    LangStringToken::LangToken(x)
1245                        if allow_error_code_check
1246                            && let Some(error_code) = x.strip_prefix('E')
1247                            && error_code.len() == 4 =>
1248                    {
1249                        if error_code.parse::<u32>().is_ok() {
1250                            data.error_codes.push(x.to_owned());
1251                            seen_rust_tags = !seen_other_tags || seen_rust_tags;
1252                        } else {
1253                            seen_other_tags = true;
1254                        }
1255                    }
1256                    LangStringToken::LangToken(x) if let Some(extra) = extra => {
1257                        if let Some(help) = match x.to_lowercase().as_str() {
1258                            "compile-fail" | "compile_fail" | "compilefail" => Some(
1259                                "use `compile_fail` to invert the results of this test, so that it \
1260                                passes if it cannot be compiled and fails if it can",
1261                            ),
1262                            "should-panic" | "should_panic" | "shouldpanic" => Some(
1263                                "use `should_panic` to invert the results of this test, so that if \
1264                                passes if it panics and fails if it does not",
1265                            ),
1266                            "no-run" | "no_run" | "norun" => Some(
1267                                "use `no_run` to compile, but not run, the code sample during \
1268                                testing",
1269                            ),
1270                            "test-harness" | "test_harness" | "testharness" => Some(
1271                                "use `test_harness` to run functions marked `#[test]` instead of a \
1272                                potentially-implicit `main` function",
1273                            ),
1274                            "standalone" | "standalone_crate" | "standalone-crate"
1275                                if extra.sp.at_least_rust_2024() =>
1276                            {
1277                                Some(
1278                                    "use `standalone_crate` to compile this code block \
1279                                        separately",
1280                                )
1281                            }
1282                            _ => None,
1283                        } {
1284                            extra.error_invalid_codeblock_attr_with_help(
1285                                format!("unknown attribute `{x}`"),
1286                                |lint| {
1287                                    lint.help(help).help(
1288                                        "this code block may be skipped during testing, \
1289                                            because unknown attributes are treated as markers for \
1290                                            code samples written in other programming languages, \
1291                                            unless it is also explicitly marked as `rust`",
1292                                    );
1293                                },
1294                            );
1295                        }
1296                        seen_other_tags = true;
1297                        data.unknown.push(x.to_owned());
1298                    }
1299                    LangStringToken::LangToken(x) => {
1300                        seen_other_tags = true;
1301                        data.unknown.push(x.to_owned());
1302                    }
1303                    LangStringToken::KeyValueAttribute("class", value) => {
1304                        data.added_classes.push(value.to_owned());
1305                    }
1306                    LangStringToken::KeyValueAttribute(key, ..) if let Some(extra) = extra => {
1307                        extra
1308                            .error_invalid_codeblock_attr(format!("unsupported attribute `{key}`"));
1309                    }
1310                    LangStringToken::ClassAttribute(class) => {
1311                        data.added_classes.push(class.to_owned());
1312                    }
1313                    _ => {}
1314                }
1315            }
1316        };
1317
1318        let mut tag_iter = TagIterator::new(string, extra);
1319        call(&mut tag_iter);
1320
1321        // ignore-foo overrides ignore
1322        if !ignores.is_empty() {
1323            data.ignore = Ignore::Some(ignores);
1324        }
1325
1326        data.rust &= !seen_custom_tag && (!seen_other_tags || seen_rust_tags) && !tag_iter.is_error;
1327
1328        data
1329    }
1330}
1331
1332impl<'a> Markdown<'a> {
1333    pub fn write_into(self, f: impl fmt::Write) -> fmt::Result {
1334        // This is actually common enough to special-case
1335        if self.content.is_empty() {
1336            return Ok(());
1337        }
1338
1339        html::write_html_fmt(f, self.into_iter())
1340    }
1341
1342    fn into_iter(self) -> CodeBlocks<'a, 'a, impl Iterator<Item = Event<'a>>> {
1343        let Markdown {
1344            content: md,
1345            links,
1346            ids,
1347            error_codes: codes,
1348            edition,
1349            playground,
1350            heading_offset,
1351        } = self;
1352
1353        let replacer = move |broken_link: BrokenLink<'_>| {
1354            links
1355                .iter()
1356                .find(|link| *link.original_text == *broken_link.reference)
1357                .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1358        };
1359
1360        let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(replacer));
1361        let p = p.into_offset_iter();
1362
1363        ids.handle_footnotes(|ids, existing_footnotes| {
1364            let p = HeadingLinks::new(p, None, ids, heading_offset);
1365            let p = SpannedLinkReplacer::new(p, links);
1366            let p = footnotes::Footnotes::new(p, existing_footnotes);
1367            let p = TableWrapper::new(p.map(|(ev, _)| ev));
1368            CodeBlocks::new(p, codes, edition, playground)
1369        })
1370    }
1371
1372    /// Convert markdown to (summary, remaining) HTML.
1373    ///
1374    /// - The summary is the first top-level Markdown element (usually a paragraph, but potentially
1375    ///   any block).
1376    /// - The remaining docs contain everything after the summary.
1377    pub(crate) fn split_summary_and_content(self) -> (Option<String>, Option<String>) {
1378        if self.content.is_empty() {
1379            return (None, None);
1380        }
1381        let mut p = self.into_iter();
1382
1383        let mut event_level = 0;
1384        let mut summary_events = Vec::new();
1385        let mut get_next_tag = false;
1386
1387        let mut end_of_summary = false;
1388        while let Some(event) = p.next() {
1389            match event {
1390                Event::Start(_) => event_level += 1,
1391                Event::End(kind) => {
1392                    event_level -= 1;
1393                    if event_level == 0 {
1394                        // We're back at the "top" so it means we're done with the summary.
1395                        end_of_summary = true;
1396                        // We surround tables with `<div>` HTML tags so this is a special case.
1397                        get_next_tag = kind == TagEnd::Table;
1398                    }
1399                }
1400                _ => {}
1401            }
1402            summary_events.push(event);
1403            if end_of_summary {
1404                if get_next_tag && let Some(event) = p.next() {
1405                    summary_events.push(event);
1406                }
1407                break;
1408            }
1409        }
1410        let mut summary = String::new();
1411        html::push_html(&mut summary, summary_events.into_iter());
1412        if summary.is_empty() {
1413            return (None, None);
1414        }
1415        let mut content = String::new();
1416        html::push_html(&mut content, p);
1417
1418        if content.is_empty() { (Some(summary), None) } else { (Some(summary), Some(content)) }
1419    }
1420}
1421
1422impl MarkdownWithToc<'_> {
1423    pub(crate) fn into_parts(self) -> (Toc, String) {
1424        let MarkdownWithToc { content: md, links, ids, error_codes: codes, edition, playground } =
1425            self;
1426
1427        // This is actually common enough to special-case
1428        if md.is_empty() {
1429            return (Toc { entries: Vec::new() }, String::new());
1430        }
1431        let mut replacer = |broken_link: BrokenLink<'_>| {
1432            links
1433                .iter()
1434                .find(|link| *link.original_text == *broken_link.reference)
1435                .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1436        };
1437
1438        let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
1439        let p = p.into_offset_iter();
1440
1441        let mut s = String::with_capacity(md.len() * 3 / 2);
1442
1443        let mut toc = TocBuilder::new();
1444
1445        ids.handle_footnotes(|ids, existing_footnotes| {
1446            let p = HeadingLinks::new(p, Some(&mut toc), ids, HeadingOffset::H1);
1447            let p = footnotes::Footnotes::new(p, existing_footnotes);
1448            let p = TableWrapper::new(p.map(|(ev, _)| ev));
1449            let p = CodeBlocks::new(p, codes, edition, playground);
1450            html::push_html(&mut s, p);
1451        });
1452
1453        (toc.into_toc(), s)
1454    }
1455
1456    pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
1457        let (toc, s) = self.into_parts();
1458        write!(f, "<nav id=\"rustdoc\">{toc}</nav>{s}", toc = toc.print())
1459    }
1460}
1461
1462impl MarkdownItemInfo<'_> {
1463    pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
1464        let MarkdownItemInfo(md, ids) = self;
1465
1466        // This is actually common enough to special-case
1467        if md.is_empty() {
1468            return Ok(());
1469        }
1470        let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
1471
1472        // Treat inline HTML as plain text.
1473        let p = p.map(|event| match event.0 {
1474            Event::Html(text) | Event::InlineHtml(text) => (Event::Text(text), event.1),
1475            _ => event,
1476        });
1477
1478        ids.handle_footnotes(|ids, existing_footnotes| {
1479            let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
1480            let p = footnotes::Footnotes::new(p, existing_footnotes);
1481            let p = TableWrapper::new(p.map(|(ev, _)| ev));
1482            let p = p.filter(|event| {
1483                !matches!(event, Event::Start(Tag::Paragraph) | Event::End(TagEnd::Paragraph))
1484            });
1485            html::write_html_fmt(&mut f, p)
1486        })
1487    }
1488}
1489
1490impl MarkdownSummaryLine<'_> {
1491    pub(crate) fn into_string_with_has_more_content(self) -> (String, bool) {
1492        let MarkdownSummaryLine(md, links) = self;
1493        // This is actually common enough to special-case
1494        if md.is_empty() {
1495            return (String::new(), false);
1496        }
1497
1498        let mut replacer = |broken_link: BrokenLink<'_>| {
1499            links
1500                .iter()
1501                .find(|link| *link.original_text == *broken_link.reference)
1502                .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1503        };
1504
1505        let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer))
1506            .peekable();
1507        let mut summary = SummaryLine::new(p);
1508
1509        let mut s = String::new();
1510
1511        let without_paragraphs = LinkReplacer::new(&mut summary, links).filter(|event| {
1512            !matches!(event, Event::Start(Tag::Paragraph) | Event::End(TagEnd::Paragraph))
1513        });
1514
1515        html::push_html(&mut s, without_paragraphs);
1516
1517        let has_more_content =
1518            matches!(summary.inner.peek(), Some(Event::Start(_))) || summary.skipped_tags > 0;
1519
1520        (s, has_more_content)
1521    }
1522
1523    pub(crate) fn into_string(self) -> String {
1524        self.into_string_with_has_more_content().0
1525    }
1526}
1527
1528/// Renders a subset of Markdown in the first paragraph of the provided Markdown.
1529///
1530/// - *Italics*, **bold**, and `inline code` styles **are** rendered.
1531/// - Headings and links are stripped (though the text *is* rendered).
1532/// - HTML, code blocks, and everything else are ignored.
1533///
1534/// Returns a tuple of the rendered HTML string and whether the output was shortened
1535/// due to the provided `length_limit`.
1536fn markdown_summary_with_limit(
1537    md: &str,
1538    link_names: &[RenderedLink],
1539    length_limit: usize,
1540) -> (String, bool) {
1541    if md.is_empty() {
1542        return (String::new(), false);
1543    }
1544
1545    let mut replacer = |broken_link: BrokenLink<'_>| {
1546        link_names
1547            .iter()
1548            .find(|link| *link.original_text == *broken_link.reference)
1549            .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1550    };
1551
1552    let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
1553    let mut p = LinkReplacer::new(p, link_names);
1554
1555    let mut buf = HtmlWithLimit::new(length_limit);
1556    let mut stopped_early = false;
1557    let _ = p.try_for_each(|event| {
1558        match &event {
1559            Event::Text(text) => {
1560                let r =
1561                    text.split_inclusive(char::is_whitespace).try_for_each(|word| buf.push(word));
1562                if r.is_break() {
1563                    stopped_early = true;
1564                }
1565                return r;
1566            }
1567            Event::Code(code) => {
1568                buf.open_tag("code");
1569                let r = buf.push(code);
1570                if r.is_break() {
1571                    stopped_early = true;
1572                } else {
1573                    buf.close_tag();
1574                }
1575                return r;
1576            }
1577            Event::Start(tag) => match tag {
1578                Tag::Emphasis => buf.open_tag("em"),
1579                Tag::Strong => buf.open_tag("strong"),
1580                Tag::CodeBlock(..) => return ControlFlow::Break(()),
1581                _ => {}
1582            },
1583            Event::End(tag) => match tag {
1584                TagEnd::Emphasis | TagEnd::Strong => buf.close_tag(),
1585                TagEnd::Paragraph | TagEnd::Heading(_) => return ControlFlow::Break(()),
1586                _ => {}
1587            },
1588            Event::HardBreak | Event::SoftBreak => buf.push(" ")?,
1589            _ => {}
1590        };
1591        ControlFlow::Continue(())
1592    });
1593
1594    (buf.finish(), stopped_early)
1595}
1596
1597/// Renders a shortened first paragraph of the given Markdown as a subset of Markdown,
1598/// making it suitable for contexts like the search index.
1599///
1600/// Will shorten to 59 or 60 characters, including an ellipsis (…) if it was shortened.
1601///
1602/// See [`markdown_summary_with_limit`] for details about what is rendered and what is not.
1603pub(crate) fn short_markdown_summary(markdown: &str, link_names: &[RenderedLink]) -> String {
1604    let (mut s, was_shortened) = markdown_summary_with_limit(markdown, link_names, 59);
1605
1606    if was_shortened {
1607        s.push('…');
1608    }
1609
1610    s
1611}
1612
1613/// Renders the first paragraph of the provided markdown as plain text.
1614/// Useful for alt-text.
1615///
1616/// - Headings, links, and formatting are stripped.
1617/// - Inline code is rendered as-is, surrounded by backticks.
1618/// - HTML and code blocks are ignored.
1619pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> String {
1620    if md.is_empty() {
1621        return String::new();
1622    }
1623
1624    let mut s = String::with_capacity(md.len() * 3 / 2);
1625
1626    let mut replacer = |broken_link: BrokenLink<'_>| {
1627        link_names
1628            .iter()
1629            .find(|link| *link.original_text == *broken_link.reference)
1630            .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1631    };
1632
1633    let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
1634
1635    plain_text_from_events(p, &mut s);
1636
1637    s
1638}
1639
1640pub(crate) fn plain_text_from_events<'a>(
1641    events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
1642    s: &mut String,
1643) {
1644    for event in events {
1645        match &event {
1646            Event::Text(text) => s.push_str(text),
1647            Event::Code(code) => {
1648                s.push('`');
1649                s.push_str(code);
1650                s.push('`');
1651            }
1652            Event::HardBreak | Event::SoftBreak => s.push(' '),
1653            Event::Start(Tag::CodeBlock(..)) => break,
1654            Event::End(TagEnd::Paragraph) => break,
1655            Event::End(TagEnd::Heading(..)) => break,
1656            _ => (),
1657        }
1658    }
1659}
1660
1661pub(crate) fn html_text_from_events<'a>(
1662    events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
1663    s: &mut String,
1664) {
1665    for event in events {
1666        match &event {
1667            Event::Text(text) => {
1668                write!(s, "{}", EscapeBodyText(text)).expect("string alloc infallible")
1669            }
1670            Event::Code(code) => {
1671                s.push_str("<code>");
1672                write!(s, "{}", EscapeBodyText(code)).expect("string alloc infallible");
1673                s.push_str("</code>");
1674            }
1675            Event::HardBreak | Event::SoftBreak => s.push(' '),
1676            Event::Start(Tag::CodeBlock(..)) => break,
1677            Event::End(TagEnd::Paragraph) => break,
1678            Event::End(TagEnd::Heading(..)) => break,
1679            _ => (),
1680        }
1681    }
1682}
1683
1684#[derive(Debug)]
1685pub(crate) struct MarkdownLink {
1686    pub kind: LinkType,
1687    pub link: String,
1688    pub range: MarkdownLinkRange,
1689}
1690
1691#[derive(Clone, Debug)]
1692pub(crate) enum MarkdownLinkRange {
1693    /// Normally, markdown link warnings point only at the destination.
1694    Destination(Range<usize>),
1695    /// In some cases, it's not possible to point at the destination.
1696    /// Usually, this happens because backslashes `\\` are used.
1697    /// When that happens, point at the whole link, and don't provide structured suggestions.
1698    WholeLink(Range<usize>),
1699}
1700
1701impl MarkdownLinkRange {
1702    /// Extracts the inner range.
1703    pub fn inner_range(&self) -> &Range<usize> {
1704        match self {
1705            MarkdownLinkRange::Destination(range) => range,
1706            MarkdownLinkRange::WholeLink(range) => range,
1707        }
1708    }
1709}
1710
1711pub(crate) fn markdown_links<'md, R>(
1712    md: &'md str,
1713    preprocess_link: impl Fn(MarkdownLink) -> Option<R>,
1714) -> Vec<R> {
1715    use itertools::Itertools;
1716    if md.is_empty() {
1717        return vec![];
1718    }
1719
1720    // FIXME: remove this function once pulldown_cmark can provide spans for link definitions.
1721    let locate = |s: &str, fallback: Range<usize>| unsafe {
1722        let s_start = s.as_ptr();
1723        let s_end = s_start.add(s.len());
1724        let md_start = md.as_ptr();
1725        let md_end = md_start.add(md.len());
1726        if md_start <= s_start && s_end <= md_end {
1727            let start = s_start.offset_from(md_start) as usize;
1728            let end = s_end.offset_from(md_start) as usize;
1729            MarkdownLinkRange::Destination(start..end)
1730        } else {
1731            MarkdownLinkRange::WholeLink(fallback)
1732        }
1733    };
1734
1735    let span_for_link = |link: &CowStr<'_>, span: Range<usize>| {
1736        // For diagnostics, we want to underline the link's definition but `span` will point at
1737        // where the link is used. This is a problem for reference-style links, where the definition
1738        // is separate from the usage.
1739
1740        match link {
1741            // `Borrowed` variant means the string (the link's destination) may come directly from
1742            // the markdown text and we can locate the original link destination.
1743            // NOTE: LinkReplacer also provides `Borrowed` but possibly from other sources,
1744            // so `locate()` can fall back to use `span`.
1745            CowStr::Borrowed(s) => locate(s, span),
1746
1747            // For anything else, we can only use the provided range.
1748            CowStr::Boxed(_) | CowStr::Inlined(_) => MarkdownLinkRange::WholeLink(span),
1749        }
1750    };
1751
1752    let span_for_refdef = |link: &CowStr<'_>, span: Range<usize>| {
1753        // We want to underline the link's definition, but `span` will point at the entire refdef.
1754        // Skip the label, then try to find the entire URL.
1755        let mut square_brace_count = 0;
1756        let mut iter = md.as_bytes()[span.start..span.end].iter().copied().enumerate();
1757        for (_i, c) in &mut iter {
1758            match c {
1759                b':' if square_brace_count == 0 => break,
1760                b'[' => square_brace_count += 1,
1761                b']' => square_brace_count -= 1,
1762                _ => {}
1763            }
1764        }
1765        while let Some((i, c)) = iter.next() {
1766            if c == b'<' {
1767                while let Some((j, c)) = iter.next() {
1768                    match c {
1769                        b'\\' => {
1770                            let _ = iter.next();
1771                        }
1772                        b'>' => {
1773                            return MarkdownLinkRange::Destination(
1774                                i + 1 + span.start..j + span.start,
1775                            );
1776                        }
1777                        _ => {}
1778                    }
1779                }
1780            } else if !c.is_ascii_whitespace() {
1781                for (j, c) in iter.by_ref() {
1782                    if c.is_ascii_whitespace() {
1783                        return MarkdownLinkRange::Destination(i + span.start..j + span.start);
1784                    }
1785                }
1786                return MarkdownLinkRange::Destination(i + span.start..span.end);
1787            }
1788        }
1789        span_for_link(link, span)
1790    };
1791
1792    let span_for_offset_backward = |span: Range<usize>, open: u8, close: u8| {
1793        let mut open_brace = !0;
1794        let mut close_brace = !0;
1795        for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate().rev() {
1796            let i = i + span.start;
1797            if b == close {
1798                close_brace = i;
1799                break;
1800            }
1801        }
1802        if close_brace < span.start || close_brace >= span.end {
1803            return MarkdownLinkRange::WholeLink(span);
1804        }
1805        let mut nesting = 1;
1806        for (i, b) in md.as_bytes()[span.start..close_brace].iter().copied().enumerate().rev() {
1807            let i = i + span.start;
1808            if b == close {
1809                nesting += 1;
1810            }
1811            if b == open {
1812                nesting -= 1;
1813            }
1814            if nesting == 0 {
1815                open_brace = i;
1816                break;
1817            }
1818        }
1819        assert!(open_brace != close_brace);
1820        if open_brace < span.start || open_brace >= span.end {
1821            return MarkdownLinkRange::WholeLink(span);
1822        }
1823        // do not actually include braces in the span
1824        let range = (open_brace + 1)..close_brace;
1825        MarkdownLinkRange::Destination(range)
1826    };
1827
1828    let span_for_offset_forward = |span: Range<usize>, open: u8, close: u8| {
1829        let mut open_brace = !0;
1830        let mut close_brace = !0;
1831        for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate() {
1832            let i = i + span.start;
1833            if b == open {
1834                open_brace = i;
1835                break;
1836            }
1837        }
1838        if open_brace < span.start || open_brace >= span.end {
1839            return MarkdownLinkRange::WholeLink(span);
1840        }
1841        let mut nesting = 0;
1842        for (i, b) in md.as_bytes()[open_brace..span.end].iter().copied().enumerate() {
1843            let i = i + open_brace;
1844            if b == close {
1845                nesting -= 1;
1846            }
1847            if b == open {
1848                nesting += 1;
1849            }
1850            if nesting == 0 {
1851                close_brace = i;
1852                break;
1853            }
1854        }
1855        assert!(open_brace != close_brace);
1856        if open_brace < span.start || open_brace >= span.end {
1857            return MarkdownLinkRange::WholeLink(span);
1858        }
1859        // do not actually include braces in the span
1860        let range = (open_brace + 1)..close_brace;
1861        MarkdownLinkRange::Destination(range)
1862    };
1863
1864    let mut broken_link_callback = |link: BrokenLink<'md>| Some((link.reference, "".into()));
1865    let event_iter = Parser::new_with_broken_link_callback(
1866        md,
1867        main_body_opts(),
1868        Some(&mut broken_link_callback),
1869    )
1870    .into_offset_iter();
1871    let mut links = Vec::new();
1872
1873    let mut refdefs = FxIndexMap::default();
1874    for (label, refdef) in event_iter.reference_definitions().iter().sorted_by_key(|x| x.0) {
1875        refdefs.insert(label.to_string(), (false, refdef.dest.to_string(), refdef.span.clone()));
1876    }
1877
1878    for (event, span) in event_iter {
1879        match event {
1880            Event::Start(Tag::Link { link_type, dest_url, id, .. })
1881                if may_be_doc_link(link_type) =>
1882            {
1883                let range = match link_type {
1884                    // Link is pulled from the link itself.
1885                    LinkType::ReferenceUnknown | LinkType::ShortcutUnknown => {
1886                        span_for_offset_backward(span, b'[', b']')
1887                    }
1888                    LinkType::CollapsedUnknown => span_for_offset_forward(span, b'[', b']'),
1889                    LinkType::Inline => span_for_offset_backward(span, b'(', b')'),
1890                    // Link is pulled from elsewhere in the document.
1891                    LinkType::Reference | LinkType::Collapsed | LinkType::Shortcut => {
1892                        if let Some((is_used, dest_url, span)) = refdefs.get_mut(&id[..]) {
1893                            *is_used = true;
1894                            span_for_refdef(&CowStr::from(&dest_url[..]), span.clone())
1895                        } else {
1896                            span_for_link(&dest_url, span)
1897                        }
1898                    }
1899                    LinkType::Autolink | LinkType::Email => unreachable!(),
1900                };
1901
1902                if let Some(link) = preprocess_link(MarkdownLink {
1903                    kind: link_type,
1904                    link: dest_url.into_string(),
1905                    range,
1906                }) {
1907                    links.push(link);
1908                }
1909            }
1910            _ => {}
1911        }
1912    }
1913
1914    for (_label, (is_used, dest_url, span)) in refdefs.into_iter() {
1915        if !is_used
1916            && let Some(link) = preprocess_link(MarkdownLink {
1917                kind: LinkType::Reference,
1918                range: span_for_refdef(&CowStr::from(&dest_url[..]), span),
1919                link: dest_url,
1920            })
1921        {
1922            links.push(link);
1923        }
1924    }
1925
1926    links
1927}
1928
1929#[derive(Debug)]
1930pub(crate) struct RustCodeBlock {
1931    /// The range in the markdown that the code block occupies. Note that this includes the fences
1932    /// for fenced code blocks.
1933    pub(crate) range: Range<usize>,
1934    /// The range in the markdown that the code within the code block occupies.
1935    pub(crate) code: Range<usize>,
1936    pub(crate) is_fenced: bool,
1937    pub(crate) lang_string: LangString,
1938}
1939
1940/// Returns a range of bytes for each code block in the markdown that is tagged as `rust` or
1941/// untagged (and assumed to be rust).
1942pub(crate) fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_>) -> Vec<RustCodeBlock> {
1943    let mut code_blocks = vec![];
1944
1945    if md.is_empty() {
1946        return code_blocks;
1947    }
1948
1949    let mut p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
1950
1951    while let Some((event, offset)) = p.next() {
1952        if let Event::Start(Tag::CodeBlock(syntax)) = event {
1953            let (lang_string, code_start, code_end, range, is_fenced) = match syntax {
1954                CodeBlockKind::Fenced(syntax) => {
1955                    let syntax = syntax.as_ref();
1956                    let lang_string = if syntax.is_empty() {
1957                        Default::default()
1958                    } else {
1959                        LangString::parse(syntax, ErrorCodes::Yes, Some(extra_info))
1960                    };
1961                    if !lang_string.rust {
1962                        continue;
1963                    }
1964                    let (code_start, mut code_end) = match p.next() {
1965                        Some((Event::Text(_), offset)) => (offset.start, offset.end),
1966                        Some((_, sub_offset)) => {
1967                            let code = Range { start: sub_offset.start, end: sub_offset.start };
1968                            code_blocks.push(RustCodeBlock {
1969                                is_fenced: true,
1970                                range: offset,
1971                                code,
1972                                lang_string,
1973                            });
1974                            continue;
1975                        }
1976                        None => {
1977                            let code = Range { start: offset.end, end: offset.end };
1978                            code_blocks.push(RustCodeBlock {
1979                                is_fenced: true,
1980                                range: offset,
1981                                code,
1982                                lang_string,
1983                            });
1984                            continue;
1985                        }
1986                    };
1987                    while let Some((Event::Text(_), offset)) = p.next() {
1988                        code_end = offset.end;
1989                    }
1990                    (lang_string, code_start, code_end, offset, true)
1991                }
1992                CodeBlockKind::Indented => {
1993                    // The ending of the offset goes too far sometime so we reduce it by one in
1994                    // these cases.
1995                    if offset.end > offset.start && md.get(offset.end..=offset.end) == Some("\n") {
1996                        (
1997                            LangString::default(),
1998                            offset.start,
1999                            offset.end,
2000                            Range { start: offset.start, end: offset.end - 1 },
2001                            false,
2002                        )
2003                    } else {
2004                        (LangString::default(), offset.start, offset.end, offset, false)
2005                    }
2006                }
2007            };
2008
2009            code_blocks.push(RustCodeBlock {
2010                is_fenced,
2011                range,
2012                code: Range { start: code_start, end: code_end },
2013                lang_string,
2014            });
2015        }
2016    }
2017
2018    code_blocks
2019}
2020
2021#[derive(Clone, Default, Debug)]
2022pub struct IdMap {
2023    map: FxHashMap<String, usize>,
2024    existing_footnotes: Arc<AtomicUsize>,
2025}
2026
2027fn is_default_id(id: &str) -> bool {
2028    matches!(
2029        id,
2030        // This is the list of IDs used in JavaScript.
2031        "help"
2032        | "settings"
2033        | "not-displayed"
2034        | "alternative-display"
2035        | "search"
2036        | "crate-search"
2037        | "crate-search-div"
2038        // This is the list of IDs used in HTML generated in Rust (including the ones
2039        // used in tera template files).
2040        | "themeStyle"
2041        | "settings-menu"
2042        | "help-button"
2043        | "sidebar-button"
2044        | "main-content"
2045        | "toggle-all-docs"
2046        | "all-types"
2047        | "default-settings"
2048        | "sidebar-vars"
2049        | "copy-path"
2050        | "rustdoc-toc"
2051        | "rustdoc-modnav"
2052        // This is the list of IDs used by rustdoc sections (but still generated by
2053        // rustdoc).
2054        | "fields"
2055        | "variants"
2056        | "implementors-list"
2057        | "synthetic-implementors-list"
2058        | "foreign-impls"
2059        | "implementations"
2060        | "trait-implementations"
2061        | "synthetic-implementations"
2062        | "blanket-implementations"
2063        | "required-associated-types"
2064        | "provided-associated-types"
2065        | "provided-associated-consts"
2066        | "required-associated-consts"
2067        | "required-methods"
2068        | "provided-methods"
2069        | "dyn-compatibility"
2070        | "implementors"
2071        | "synthetic-implementors"
2072        | "implementations-list"
2073        | "trait-implementations-list"
2074        | "synthetic-implementations-list"
2075        | "blanket-implementations-list"
2076        | "deref-methods"
2077        | "layout"
2078        | "aliased-type"
2079    )
2080}
2081
2082impl IdMap {
2083    pub fn new() -> Self {
2084        IdMap { map: FxHashMap::default(), existing_footnotes: Arc::new(AtomicUsize::new(0)) }
2085    }
2086
2087    pub(crate) fn derive<S: AsRef<str> + ToString>(&mut self, candidate: S) -> String {
2088        let id = match self.map.get_mut(candidate.as_ref()) {
2089            None => {
2090                let candidate = candidate.to_string();
2091                if is_default_id(&candidate) {
2092                    let id = format!("{}-{}", candidate, 1);
2093                    self.map.insert(candidate, 2);
2094                    id
2095                } else {
2096                    candidate
2097                }
2098            }
2099            Some(a) => {
2100                let id = format!("{}-{}", candidate.as_ref(), *a);
2101                *a += 1;
2102                id
2103            }
2104        };
2105
2106        self.map.insert(id.clone(), 1);
2107        id
2108    }
2109
2110    /// Method to handle `existing_footnotes` increment automatically (to prevent forgetting
2111    /// about it).
2112    pub(crate) fn handle_footnotes<'a, T, F: FnOnce(&'a mut Self, Weak<AtomicUsize>) -> T>(
2113        &'a mut self,
2114        closure: F,
2115    ) -> T {
2116        let existing_footnotes = Arc::downgrade(&self.existing_footnotes);
2117
2118        closure(self, existing_footnotes)
2119    }
2120
2121    pub(crate) fn clear(&mut self) {
2122        self.map.clear();
2123        self.existing_footnotes = Arc::new(AtomicUsize::new(0));
2124    }
2125}