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