Skip to main content

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