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