]> git.proxmox.com Git - rustc.git/blame - src/librustdoc/html/markdown.rs
New upstream version 1.68.2+dfsg1
[rustc.git] / src / librustdoc / html / markdown.rs
CommitLineData
9fa01778 1//! Markdown formatting for rustdoc.
1a4d82fc 2//!
416331ca 3//! This module implements markdown formatting through the pulldown-cmark library.
1a4d82fc 4//!
041b39d2 5//! ```
ea8adc8c
XL
6//! #![feature(rustc_private)]
7//!
dfeec247 8//! extern crate rustc_span;
48663c56 9//!
dfeec247 10//! use rustc_span::edition::Edition;
c295e0f8 11//! use rustdoc::html::markdown::{HeadingOffset, IdMap, Markdown, ErrorCodes};
1a4d82fc
JJ
12//!
13//! let s = "My *markdown* _text_";
b7449926 14//! let mut id_map = IdMap::new();
c295e0f8
XL
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//! };
3dfed10e 24//! let html = md.into_string();
1a4d82fc
JJ
25//! // ... something using html
26//! ```
27
b7449926 28use rustc_data_structures::fx::FxHashMap;
f9f354fc
XL
29use rustc_hir::def_id::DefId;
30use rustc_hir::HirId;
31use rustc_middle::ty::TyCtxt;
dfeec247 32use rustc_span::edition::Edition;
6522a427 33use rustc_span::{Span, Symbol};
94222f64 34
04454e1e 35use once_cell::sync::Lazy;
dfeec247 36use std::borrow::Cow;
5869c6ff 37use std::cell::RefCell;
b7449926 38use std::collections::VecDeque;
9346a6ac 39use std::default::Default;
416331ca 40use std::fmt::Write;
94222f64 41use std::ops::{ControlFlow, Range};
1a4d82fc
JJ
42use std::str;
43
1b1a35ee
XL
44use crate::clean::RenderedLink;
45use crate::doctest;
136023e0 46use crate::html::escape::Escape;
94222f64 47use crate::html::format::Buffer;
9fa01778 48use crate::html::highlight;
94222f64 49use crate::html::length_limit::HtmlWithLimit;
dfeec247 50use crate::html::toc::TocBuilder;
1a4d82fc 51
5869c6ff
XL
52use pulldown_cmark::{
53 html, BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag,
54};
55
416331ca
XL
56#[cfg(test)]
57mod tests;
58
c295e0f8
XL
59const MAX_HEADER_LEVEL: u32 = 6;
60
fc512014 61/// Options for rendering Markdown in the main body of documentation.
c295e0f8 62pub(crate) fn main_body_opts() -> Options {
6a06907d
XL
63 Options::ENABLE_TABLES
64 | Options::ENABLE_FOOTNOTES
65 | Options::ENABLE_STRIKETHROUGH
66 | Options::ENABLE_TASKLISTS
67 | Options::ENABLE_SMART_PUNCTUATION
48663c56 68}
cc61c64b 69
c295e0f8 70/// Options for rendering Markdown in summaries (e.g., in search results).
fc512014 71pub(crate) fn summary_opts() -> Options {
c295e0f8
XL
72 Options::ENABLE_TABLES
73 | Options::ENABLE_FOOTNOTES
74 | Options::ENABLE_STRIKETHROUGH
75 | Options::ENABLE_TASKLISTS
76 | Options::ENABLE_SMART_PUNCTUATION
77}
78
79#[derive(Debug, Clone, Copy)]
80pub enum HeadingOffset {
81 H1 = 0,
82 H2,
83 H3,
84 H4,
85 H5,
86 H6,
fc512014
XL
87}
88
416331ca
XL
89/// When `to_string` is called, this struct will emit the HTML corresponding to
90/// the rendered version of the contained markdown string.
c295e0f8
XL
91pub struct Markdown<'a> {
92 pub content: &'a str,
dc9dc135 93 /// A list of link replacements.
c295e0f8 94 pub links: &'a [RenderedLink],
dc9dc135 95 /// The current list of used header IDs.
c295e0f8 96 pub ids: &'a mut IdMap,
dc9dc135 97 /// Whether to allow the use of explicit error codes in doctest lang strings.
c295e0f8 98 pub error_codes: ErrorCodes,
dc9dc135 99 /// Default edition to use when parsing doctests (to add a `fn main`).
c295e0f8
XL
100 pub edition: Edition,
101 pub playground: &'a Option<Playground>,
102 /// Offset at which we render headings.
103 /// E.g. if `heading_offset: HeadingOffset::H2`, then `# something` renders an `<h2>`.
104 pub heading_offset: HeadingOffset,
105}
dc9dc135 106/// A tuple struct like `Markdown` that renders the markdown with a table of contents.
923072b8
FG
107pub(crate) struct MarkdownWithToc<'a>(
108 pub(crate) &'a str,
109 pub(crate) &'a mut IdMap,
110 pub(crate) ErrorCodes,
111 pub(crate) Edition,
112 pub(crate) &'a Option<Playground>,
dc9dc135 113);
2b03887a
FG
114/// A tuple struct like `Markdown` that renders the markdown escaping HTML tags
115/// and includes no paragraph tags.
116pub(crate) struct MarkdownItemInfo<'a>(pub(crate) &'a str, pub(crate) &'a mut IdMap);
dc9dc135 117/// A tuple struct like `Markdown` that renders only the first paragraph.
923072b8 118pub(crate) struct MarkdownSummaryLine<'a>(pub &'a str, pub &'a [RenderedLink]);
cc61c64b 119
b7449926
XL
120#[derive(Copy, Clone, PartialEq, Debug)]
121pub enum ErrorCodes {
122 Yes,
123 No,
124}
125
126impl ErrorCodes {
923072b8 127 pub(crate) fn from(b: bool) -> Self {
b7449926
XL
128 match b {
129 true => ErrorCodes::Yes,
130 false => ErrorCodes::No,
131 }
132 }
133
923072b8 134 pub(crate) fn as_bool(self) -> bool {
b7449926
XL
135 match self {
136 ErrorCodes::Yes => true,
137 ErrorCodes::No => false,
138 }
139 }
140}
141
7cac9316
XL
142/// Controls whether a line will be hidden or shown in HTML output.
143///
144/// All lines are used in documentation tests.
145enum Line<'a> {
146 Hidden(&'a str),
8faf50e0 147 Shown(Cow<'a, str>),
7cac9316
XL
148}
149
150impl<'a> Line<'a> {
8faf50e0 151 fn for_html(self) -> Option<Cow<'a, str>> {
7cac9316
XL
152 match self {
153 Line::Shown(l) => Some(l),
154 Line::Hidden(_) => None,
155 }
156 }
157
8faf50e0 158 fn for_code(self) -> Cow<'a, str> {
7cac9316 159 match self {
8faf50e0
XL
160 Line::Shown(l) => l,
161 Line::Hidden(l) => Cow::Borrowed(l),
7cac9316
XL
162 }
163 }
164}
165
166// FIXME: There is a minor inconsistency here. For lines that start with ##, we
167// have no easy way of removing a potential single space after the hashes, which
168// is done in the single # case. This inconsistency seems okay, if non-ideal. In
169// order to fix it we'd have to iterate to find the first non-# character, and
170// then reallocate to remove it; which would make us return a String.
9fa01778 171fn map_line(s: &str) -> Line<'_> {
cc61c64b 172 let trimmed = s.trim();
7cac9316 173 if trimmed.starts_with("##") {
8faf50e0 174 Line::Shown(Cow::Owned(s.replacen("##", "#", 1)))
fc512014 175 } else if let Some(stripped) = trimmed.strip_prefix("# ") {
7cac9316 176 // # text
3c0e092e 177 Line::Hidden(stripped)
7cac9316
XL
178 } else if trimmed == "#" {
179 // We cannot handle '#text' because it could be #[attr].
180 Line::Hidden("")
cc61c64b 181 } else {
8faf50e0 182 Line::Shown(Cow::Borrowed(s))
cc61c64b
XL
183 }
184}
185
cc61c64b
XL
186/// Convert chars from a title for an id.
187///
188/// "Hello, world!" -> "hello-world"
189fn slugify(c: char) -> Option<char> {
190 if c.is_alphanumeric() || c == '-' || c == '_' {
dfeec247 191 if c.is_ascii() { Some(c.to_ascii_lowercase()) } else { Some(c) }
cc61c64b
XL
192 } else if c.is_whitespace() && c.is_ascii() {
193 Some('-')
194 } else {
195 None
196 }
197}
198
416331ca
XL
199#[derive(Clone, Debug)]
200pub struct Playground {
6522a427 201 pub crate_name: Option<Symbol>,
416331ca
XL
202 pub url: String,
203}
cc61c64b 204
9fa01778 205/// Adds syntax highlighting and playground Run buttons to Rust code blocks.
416331ca 206struct CodeBlocks<'p, 'a, I: Iterator<Item = Event<'a>>> {
cc61c64b 207 inner: I,
b7449926 208 check_error_codes: ErrorCodes,
48663c56 209 edition: Edition,
416331ca
XL
210 // Information about the playground if a URL has been specified, containing an
211 // optional crate name and the URL.
212 playground: &'p Option<Playground>,
cc61c64b
XL
213}
214
416331ca
XL
215impl<'p, 'a, I: Iterator<Item = Event<'a>>> CodeBlocks<'p, 'a, I> {
216 fn new(
217 iter: I,
218 error_codes: ErrorCodes,
219 edition: Edition,
220 playground: &'p Option<Playground>,
221 ) -> Self {
dfeec247 222 CodeBlocks { inner: iter, check_error_codes: error_codes, edition, playground }
cc61c64b
XL
223 }
224}
225
416331ca 226impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
cc61c64b
XL
227 type Item = Event<'a>;
228
229 fn next(&mut self) -> Option<Self::Item> {
230 let event = self.inner.next();
ea8adc8c 231 let compile_fail;
f035d41b 232 let should_panic;
ea8adc8c 233 let ignore;
0bf4aa26 234 let edition;
5099ac24 235 let Some(Event::Start(Tag::CodeBlock(kind))) = event else {
cc61c64b 236 return event;
136023e0 237 };
48663c56 238
6522a427 239 let mut original_text = String::new();
cc61c64b
XL
240 for event in &mut self.inner {
241 match event {
242 Event::End(Tag::CodeBlock(..)) => break,
243 Event::Text(ref s) => {
6522a427 244 original_text.push_str(s);
cc61c64b
XL
245 }
246 _ => {}
247 }
248 }
5869c6ff 249
136023e0
XL
250 let parse_result = match kind {
251 CodeBlockKind::Fenced(ref lang) => {
252 let parse_result =
3c0e092e 253 LangString::parse_without_check(lang, self.check_error_codes, false);
136023e0
XL
254 if !parse_result.rust {
255 return Some(Event::Html(
256 format!(
257 "<div class=\"example-wrap\">\
94222f64 258 <pre class=\"language-{}\"><code>{}</code></pre>\
136023e0 259 </div>",
94222f64 260 lang,
6522a427 261 Escape(&original_text),
136023e0
XL
262 )
263 .into(),
264 ));
265 }
266 parse_result
267 }
268 CodeBlockKind::Indented => Default::default(),
269 };
270
6522a427 271 let lines = original_text.lines().filter_map(|l| map_line(l).for_html());
2b03887a
FG
272 let text = lines.intersperse("\n".into()).collect::<String>();
273
136023e0
XL
274 compile_fail = parse_result.compile_fail;
275 should_panic = parse_result.should_panic;
276 ignore = parse_result.ignore;
277 edition = parse_result.edition;
278
279 let explicit_edition = edition.is_some();
280 let edition = edition.unwrap_or(self.edition);
281
416331ca
XL
282 let playground_button = self.playground.as_ref().and_then(|playground| {
283 let krate = &playground.crate_name;
284 let url = &playground.url;
285 if url.is_empty() {
286 return None;
287 }
6522a427 288 let test = original_text
dfeec247 289 .lines()
416331ca 290 .map(|l| map_line(l).for_code())
04454e1e
FG
291 .intersperse("\n".into())
292 .collect::<String>();
6522a427 293 let krate = krate.as_ref().map(|s| s.as_str());
fc512014
XL
294 let (test, _, _) =
295 doctest::make_test(&test, krate, false, &Default::default(), edition, None);
dfeec247 296 let channel = if test.contains("#![feature(") { "&amp;version=nightly" } else { "" };
0bf4aa26 297
416331ca
XL
298 // These characters don't need to be escaped in a URI.
299 // FIXME: use a library function for percent encoding.
300 fn dont_escape(c: u8) -> bool {
dfeec247
XL
301 (b'a' <= c && c <= b'z')
302 || (b'A' <= c && c <= b'Z')
303 || (b'0' <= c && c <= b'9')
304 || c == b'-'
305 || c == b'_'
306 || c == b'.'
307 || c == b'~'
308 || c == b'!'
309 || c == b'\''
310 || c == b'('
311 || c == b')'
312 || c == b'*'
0bf4aa26 313 }
416331ca
XL
314 let mut test_escaped = String::new();
315 for b in test.bytes() {
316 if dont_escape(b) {
317 test_escaped.push(char::from(b));
318 } else {
319 write!(test_escaped, "%{:02X}", b).unwrap();
320 }
321 }
322 Some(format!(
04454e1e
FG
323 r#"<a class="test-arrow" target="_blank" href="{}?code={}{}&amp;edition={}">Run</a>"#,
324 url, test_escaped, channel, edition,
416331ca
XL
325 ))
326 });
327
e1599b0c 328 let tooltip = if ignore != Ignore::None {
f2b60f7d 329 highlight::Tooltip::Ignore
416331ca 330 } else if compile_fail {
f2b60f7d 331 highlight::Tooltip::CompileFail
f035d41b 332 } else if should_panic {
f2b60f7d 333 highlight::Tooltip::ShouldPanic
416331ca 334 } else if explicit_edition {
f2b60f7d 335 highlight::Tooltip::Edition(edition)
416331ca 336 } else {
f2b60f7d 337 highlight::Tooltip::None
416331ca
XL
338 };
339
5869c6ff
XL
340 // insert newline to clearly separate it from the
341 // previous block so we can shorten the html output
342 let mut s = Buffer::new();
343 s.push_str("\n");
f2b60f7d
FG
344
345 highlight::render_example_with_highlighting(
5869c6ff
XL
346 &text,
347 &mut s,
fc512014 348 tooltip,
f2b60f7d 349 playground_button.as_deref(),
5869c6ff
XL
350 );
351 Some(Event::Html(s.into_inner().into()))
cc61c64b
XL
352 }
353}
354
9fa01778 355/// Make headings links with anchor IDs and build up TOC.
1b1a35ee 356struct LinkReplacer<'a, I: Iterator<Item = Event<'a>>> {
2c00a5a8 357 inner: I,
1b1a35ee
XL
358 links: &'a [RenderedLink],
359 shortcut_link: Option<&'a RenderedLink>,
2c00a5a8
XL
360}
361
1b1a35ee
XL
362impl<'a, I: Iterator<Item = Event<'a>>> LinkReplacer<'a, I> {
363 fn new(iter: I, links: &'a [RenderedLink]) -> Self {
364 LinkReplacer { inner: iter, links, shortcut_link: None }
2c00a5a8
XL
365 }
366}
367
1b1a35ee 368impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
2c00a5a8
XL
369 type Item = Event<'a>;
370
371 fn next(&mut self) -> Option<Self::Item> {
1b1a35ee
XL
372 let mut event = self.inner.next();
373
374 // Replace intra-doc links and remove disambiguators from shortcut links (`[fn@f]`).
375 match &mut event {
376 // This is a shortcut link that was resolved by the broken_link_callback: `[fn@f]`
377 // Remove any disambiguator.
378 Some(Event::Start(Tag::Link(
379 // [fn@f] or [fn@f][]
380 LinkType::ShortcutUnknown | LinkType::CollapsedUnknown,
381 dest,
382 title,
383 ))) => {
384 debug!("saw start of shortcut link to {} with title {}", dest, title);
385 // If this is a shortcut link, it was resolved by the broken_link_callback.
386 // So the URL will already be updated properly.
387 let link = self.links.iter().find(|&link| *link.href == **dest);
388 // Since this is an external iterator, we can't replace the inner text just yet.
389 // Store that we saw a link so we know to replace it later.
390 if let Some(link) = link {
391 trace!("it matched");
392 assert!(self.shortcut_link.is_none(), "shortcut links cannot be nested");
393 self.shortcut_link = Some(link);
394 }
2c00a5a8 395 }
1b1a35ee
XL
396 // Now that we're done with the shortcut link, don't replace any more text.
397 Some(Event::End(Tag::Link(
398 LinkType::ShortcutUnknown | LinkType::CollapsedUnknown,
399 dest,
400 _,
401 ))) => {
402 debug!("saw end of shortcut link to {}", dest);
fc512014 403 if self.links.iter().any(|link| *link.href == **dest) {
1b1a35ee
XL
404 assert!(self.shortcut_link.is_some(), "saw closing link without opening tag");
405 self.shortcut_link = None;
406 }
407 }
408 // Handle backticks in inline code blocks, but only if we're in the middle of a shortcut link.
409 // [`fn@f`]
410 Some(Event::Code(text)) => {
411 trace!("saw code {}", text);
412 if let Some(link) = self.shortcut_link {
413 trace!("original text was {}", link.original_text);
414 // NOTE: this only replaces if the code block is the *entire* text.
415 // If only part of the link has code highlighting, the disambiguator will not be removed.
416 // e.g. [fn@`f`]
417 // This is a limitation from `collect_intra_doc_links`: it passes a full link,
418 // and does not distinguish at all between code blocks.
419 // So we could never be sure we weren't replacing too much:
420 // [fn@my_`f`unc] is treated the same as [my_func()] in that pass.
421 //
422 // NOTE: &[1..len() - 1] is to strip the backticks
423 if **text == link.original_text[1..link.original_text.len() - 1] {
424 debug!("replacing {} with {}", text, link.new_text);
425 *text = CowStr::Borrowed(&link.new_text);
426 }
427 }
428 }
429 // Replace plain text in links, but only in the middle of a shortcut link.
430 // [fn@f]
431 Some(Event::Text(text)) => {
432 trace!("saw text {}", text);
433 if let Some(link) = self.shortcut_link {
434 trace!("original text was {}", link.original_text);
435 // NOTE: same limitations as `Event::Code`
436 if **text == *link.original_text {
437 debug!("replacing {} with {}", text, link.new_text);
438 *text = CowStr::Borrowed(&link.new_text);
439 }
440 }
441 }
442 // If this is a link, but not a shortcut link,
443 // replace the URL, since the broken_link_callback was not called.
444 Some(Event::Start(Tag::Link(_, dest, _))) => {
445 if let Some(link) = self.links.iter().find(|&link| *link.original_text == **dest) {
446 *dest = CowStr::Borrowed(link.href.as_ref());
447 }
448 }
449 // Anything else couldn't have been a valid Rust path, so no need to replace the text.
450 _ => {}
2c00a5a8 451 }
1b1a35ee
XL
452
453 // Yield the modified event
454 event
2c00a5a8
XL
455 }
456}
457
c295e0f8
XL
458/// Wrap HTML tables into `<div>` to prevent having the doc blocks width being too big.
459struct TableWrapper<'a, I: Iterator<Item = Event<'a>>> {
460 inner: I,
461 stored_events: VecDeque<Event<'a>>,
462}
463
464impl<'a, I: Iterator<Item = Event<'a>>> TableWrapper<'a, I> {
465 fn new(iter: I) -> Self {
466 Self { inner: iter, stored_events: VecDeque::new() }
467 }
468}
469
470impl<'a, I: Iterator<Item = Event<'a>>> Iterator for TableWrapper<'a, I> {
471 type Item = Event<'a>;
472
473 fn next(&mut self) -> Option<Self::Item> {
474 if let Some(first) = self.stored_events.pop_front() {
475 return Some(first);
476 }
477
478 let event = self.inner.next()?;
479
480 Some(match event {
481 Event::Start(Tag::Table(t)) => {
482 self.stored_events.push_back(Event::Start(Tag::Table(t)));
483 Event::Html(CowStr::Borrowed("<div>"))
484 }
485 Event::End(Tag::Table(t)) => {
486 self.stored_events.push_back(Event::Html(CowStr::Borrowed("</div>")));
487 Event::End(Tag::Table(t))
488 }
489 e => e,
490 })
491 }
492}
493
5869c6ff
XL
494type SpannedEvent<'a> = (Event<'a>, Range<usize>);
495
9fa01778 496/// Make headings links with anchor IDs and build up TOC.
fc512014 497struct HeadingLinks<'a, 'b, 'ids, I> {
cc61c64b
XL
498 inner: I,
499 toc: Option<&'b mut TocBuilder>,
5869c6ff 500 buf: VecDeque<SpannedEvent<'a>>,
b7449926 501 id_map: &'ids mut IdMap,
c295e0f8 502 heading_offset: HeadingOffset,
cc61c64b
XL
503}
504
fc512014 505impl<'a, 'b, 'ids, I> HeadingLinks<'a, 'b, 'ids, I> {
c295e0f8
XL
506 fn new(
507 iter: I,
508 toc: Option<&'b mut TocBuilder>,
509 ids: &'ids mut IdMap,
510 heading_offset: HeadingOffset,
511 ) -> Self {
512 HeadingLinks { inner: iter, toc, buf: VecDeque::new(), id_map: ids, heading_offset }
cc61c64b
XL
513 }
514}
515
5869c6ff 516impl<'a, 'b, 'ids, I: Iterator<Item = SpannedEvent<'a>>> Iterator
fc512014
XL
517 for HeadingLinks<'a, 'b, 'ids, I>
518{
5869c6ff 519 type Item = SpannedEvent<'a>;
cc61c64b
XL
520
521 fn next(&mut self) -> Option<Self::Item> {
522 if let Some(e) = self.buf.pop_front() {
523 return Some(e);
524 }
525
526 let event = self.inner.next();
a2a8927a 527 if let Some((Event::Start(Tag::Heading(level, _, _)), _)) = event {
cc61c64b
XL
528 let mut id = String::new();
529 for event in &mut self.inner {
fc512014 530 match &event.0 {
74b04a01 531 Event::End(Tag::Heading(..)) => break,
fc512014 532 Event::Start(Tag::Link(_, _, _)) | Event::End(Tag::Link(..)) => {}
48663c56
XL
533 Event::Text(text) | Event::Code(text) => {
534 id.extend(text.chars().filter_map(slugify));
fc512014 535 self.buf.push_back(event);
48663c56 536 }
fc512014 537 _ => self.buf.push_back(event),
cc61c64b 538 }
cc61c64b 539 }
b7449926 540 let id = self.id_map.derive(id);
cc61c64b
XL
541
542 if let Some(ref mut builder) = self.toc {
543 let mut html_header = String::new();
fc512014 544 html::push_html(&mut html_header, self.buf.iter().map(|(ev, _)| ev.clone()));
cc61c64b 545 let sec = builder.push(level as u32, html_header, id.clone());
fc512014 546 self.buf.push_front((Event::Html(format!("{} ", sec).into()), 0..0));
cc61c64b
XL
547 }
548
a2a8927a
XL
549 let level =
550 std::cmp::min(level as u32 + (self.heading_offset as u32), MAX_HEADER_LEVEL);
fc512014 551 self.buf.push_back((Event::Html(format!("</a></h{}>", level).into()), 0..0));
cc61c64b 552
dfeec247 553 let start_tags = format!(
5099ac24 554 "<h{level} id=\"{id}\">\
dfeec247
XL
555 <a href=\"#{id}\">",
556 id = id,
557 level = level
558 );
fc512014 559 return Some((Event::Html(start_tags.into()), 0..0));
cc61c64b
XL
560 }
561 event
562 }
563}
564
565/// Extracts just the first paragraph.
566struct SummaryLine<'a, I: Iterator<Item = Event<'a>>> {
567 inner: I,
568 started: bool,
569 depth: u32,
6522a427 570 skipped_tags: u32,
cc61c64b
XL
571}
572
573impl<'a, I: Iterator<Item = Event<'a>>> SummaryLine<'a, I> {
574 fn new(iter: I) -> Self {
6522a427 575 SummaryLine { inner: iter, started: false, depth: 0, skipped_tags: 0 }
cc61c64b
XL
576 }
577}
578
9fa01778 579fn check_if_allowed_tag(t: &Tag<'_>) -> bool {
5869c6ff
XL
580 matches!(
581 t,
582 Tag::Paragraph | Tag::Item | Tag::Emphasis | Tag::Strong | Tag::Link(..) | Tag::BlockQuote
583 )
8faf50e0
XL
584}
585
136023e0
XL
586fn is_forbidden_tag(t: &Tag<'_>) -> bool {
587 matches!(t, Tag::CodeBlock(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell)
588}
589
cc61c64b
XL
590impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SummaryLine<'a, I> {
591 type Item = Event<'a>;
592
593 fn next(&mut self) -> Option<Self::Item> {
594 if self.started && self.depth == 0 {
595 return None;
596 }
597 if !self.started {
598 self.started = true;
599 }
ba9703b0 600 if let Some(event) = self.inner.next() {
a1dfa0c6
XL
601 let mut is_start = true;
602 let is_allowed_tag = match event {
a1dfa0c6 603 Event::Start(ref c) => {
136023e0 604 if is_forbidden_tag(c) {
6522a427 605 self.skipped_tags += 1;
136023e0
XL
606 return None;
607 }
a1dfa0c6
XL
608 self.depth += 1;
609 check_if_allowed_tag(c)
610 }
611 Event::End(ref c) => {
136023e0 612 if is_forbidden_tag(c) {
6522a427 613 self.skipped_tags += 1;
136023e0
XL
614 return None;
615 }
a1dfa0c6
XL
616 self.depth -= 1;
617 is_start = false;
618 check_if_allowed_tag(c)
619 }
dfeec247 620 _ => true,
a1dfa0c6 621 };
6522a427
EL
622 if !is_allowed_tag {
623 self.skipped_tags += 1;
624 }
74b04a01 625 return if !is_allowed_tag {
a1dfa0c6
XL
626 if is_start {
627 Some(Event::Start(Tag::Paragraph))
628 } else {
629 Some(Event::End(Tag::Paragraph))
630 }
8faf50e0 631 } else {
a1dfa0c6
XL
632 Some(event)
633 };
cc61c64b 634 }
a1dfa0c6 635 None
cc61c64b
XL
636 }
637}
638
639/// Moves all footnote definitions to the end and add back links to the
640/// references.
fc512014 641struct Footnotes<'a, I> {
cc61c64b 642 inner: I,
b7449926 643 footnotes: FxHashMap<String, (Vec<Event<'a>>, u16)>,
cc61c64b
XL
644}
645
fc512014 646impl<'a, I> Footnotes<'a, I> {
cc61c64b 647 fn new(iter: I) -> Self {
dfeec247 648 Footnotes { inner: iter, footnotes: FxHashMap::default() }
cc61c64b 649 }
fc512014 650
cc61c64b 651 fn get_entry(&mut self, key: &str) -> &mut (Vec<Event<'a>>, u16) {
3c0e092e 652 let new_id = self.footnotes.len() + 1;
cc61c64b
XL
653 let key = key.to_owned();
654 self.footnotes.entry(key).or_insert((Vec::new(), new_id as u16))
655 }
656}
657
5869c6ff
XL
658impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
659 type Item = SpannedEvent<'a>;
cc61c64b
XL
660
661 fn next(&mut self) -> Option<Self::Item> {
662 loop {
663 match self.inner.next() {
fc512014 664 Some((Event::FootnoteReference(ref reference), range)) => {
3c0e092e 665 let entry = self.get_entry(reference);
dfeec247 666 let reference = format!(
1b1a35ee 667 "<sup id=\"fnref{0}\"><a href=\"#fn{0}\">{0}</a></sup>",
dfeec247
XL
668 (*entry).1
669 );
fc512014 670 return Some((Event::Html(reference.into()), range));
cc61c64b 671 }
fc512014 672 Some((Event::Start(Tag::FootnoteDefinition(def)), _)) => {
cc61c64b 673 let mut content = Vec::new();
fc512014 674 for (event, _) in &mut self.inner {
cc61c64b
XL
675 if let Event::End(Tag::FootnoteDefinition(..)) = event {
676 break;
677 }
678 content.push(event);
679 }
680 let entry = self.get_entry(&def);
681 (*entry).0 = content;
682 }
683 Some(e) => return Some(e),
684 None => {
685 if !self.footnotes.is_empty() {
686 let mut v: Vec<_> = self.footnotes.drain().map(|(_, x)| x).collect();
687 v.sort_by(|a, b| a.1.cmp(&b.1));
688 let mut ret = String::from("<div class=\"footnotes\"><hr><ol>");
689 for (mut content, id) in v {
abe05a73 690 write!(ret, "<li id=\"fn{}\">", id).unwrap();
cc61c64b
XL
691 let mut is_paragraph = false;
692 if let Some(&Event::End(Tag::Paragraph)) = content.last() {
693 content.pop();
694 is_paragraph = true;
695 }
696 html::push_html(&mut ret, content.into_iter());
136023e0 697 write!(ret, "&nbsp;<a href=\"#fnref{}\">↩</a>", id).unwrap();
cc61c64b
XL
698 if is_paragraph {
699 ret.push_str("</p>");
700 }
701 ret.push_str("</li>");
702 }
703 ret.push_str("</ol></div>");
fc512014 704 return Some((Event::Html(ret.into()), 0..0));
cc61c64b
XL
705 } else {
706 return None;
707 }
708 }
709 }
710 }
711 }
712}
1a4d82fc 713
923072b8 714pub(crate) fn find_testable_code<T: doctest::Tester>(
dfeec247
XL
715 doc: &str,
716 tests: &mut T,
717 error_codes: ErrorCodes,
718 enable_per_target_ignores: bool,
5869c6ff 719 extra_info: Option<&ExtraInfo<'_>>,
dfeec247 720) {
74b04a01 721 let mut parser = Parser::new(doc).into_offset_iter();
cc61c64b
XL
722 let mut prev_offset = 0;
723 let mut nb_lines = 0;
724 let mut register_header = None;
74b04a01 725 while let Some((event, offset)) = parser.next() {
cc61c64b 726 match event {
74b04a01
XL
727 Event::Start(Tag::CodeBlock(kind)) => {
728 let block_info = match kind {
729 CodeBlockKind::Fenced(ref lang) => {
730 if lang.is_empty() {
fc512014 731 Default::default()
74b04a01 732 } else {
f9f354fc
XL
733 LangString::parse(
734 lang,
735 error_codes,
736 enable_per_target_ignores,
737 extra_info,
738 )
74b04a01
XL
739 }
740 }
fc512014 741 CodeBlockKind::Indented => Default::default(),
cc61c64b
XL
742 };
743 if !block_info.rust {
48663c56 744 continue;
cc61c64b 745 }
74b04a01 746
cc61c64b 747 let mut test_s = String::new();
48663c56 748
74b04a01 749 while let Some((Event::Text(s), _)) = parser.next() {
48663c56 750 test_s.push_str(&s);
2c00a5a8 751 }
48663c56
XL
752 let text = test_s
753 .lines()
754 .map(|l| map_line(l).for_code())
755 .collect::<Vec<Cow<'_, str>>>()
756 .join("\n");
74b04a01
XL
757
758 nb_lines += doc[prev_offset..offset.start].lines().count();
c295e0f8
XL
759 // If there are characters between the preceding line ending and
760 // this code block, `str::lines` will return an additional line,
761 // which we subtract here.
762 if nb_lines != 0 && !&doc[prev_offset..offset.start].ends_with('\n') {
763 nb_lines -= 1;
764 }
74b04a01 765 let line = tests.get_line() + nb_lines + 1;
48663c56 766 tests.add_test(text, block_info, line);
74b04a01 767 prev_offset = offset.start;
cc61c64b 768 }
a2a8927a 769 Event::Start(Tag::Heading(level, _, _)) => {
cc61c64b
XL
770 register_header = Some(level as u32);
771 }
772 Event::Text(ref s) if register_header.is_some() => {
773 let level = register_header.unwrap();
774 if s.is_empty() {
775 tests.register_header("", level);
776 } else {
777 tests.register_header(s, level);
778 }
779 register_header = None;
780 }
781 _ => {}
782 }
783 }
784}
785
923072b8 786pub(crate) struct ExtraInfo<'tcx> {
cdc7bbd5 787 id: ExtraInfoId,
f9f354fc 788 sp: Span,
5869c6ff 789 tcx: TyCtxt<'tcx>,
f9f354fc
XL
790}
791
cdc7bbd5
XL
792enum ExtraInfoId {
793 Hir(HirId),
794 Def(DefId),
795}
796
5869c6ff 797impl<'tcx> ExtraInfo<'tcx> {
923072b8 798 pub(crate) fn new(tcx: TyCtxt<'tcx>, hir_id: HirId, sp: Span) -> ExtraInfo<'tcx> {
cdc7bbd5 799 ExtraInfo { id: ExtraInfoId::Hir(hir_id), sp, tcx }
f9f354fc
XL
800 }
801
923072b8 802 pub(crate) fn new_did(tcx: TyCtxt<'tcx>, did: DefId, sp: Span) -> ExtraInfo<'tcx> {
cdc7bbd5 803 ExtraInfo { id: ExtraInfoId::Def(did), sp, tcx }
f9f354fc
XL
804 }
805
806 fn error_invalid_codeblock_attr(&self, msg: &str, help: &str) {
cdc7bbd5
XL
807 let hir_id = match self.id {
808 ExtraInfoId::Hir(hir_id) => hir_id,
809 ExtraInfoId::Def(item_did) => {
f9f354fc 810 match item_did.as_local() {
3dfed10e 811 Some(item_did) => self.tcx.hir().local_def_id_to_hir_id(item_did),
f9f354fc
XL
812 None => {
813 // If non-local, no need to check anything.
814 return;
815 }
816 }
817 }
f9f354fc
XL
818 };
819 self.tcx.struct_span_lint_hir(
6a06907d 820 crate::lint::INVALID_CODEBLOCK_ATTRIBUTES,
f9f354fc
XL
821 hir_id,
822 self.sp,
2b03887a
FG
823 msg,
824 |lint| lint.help(help),
f9f354fc
XL
825 );
826 }
827}
828
85aaf69f 829#[derive(Eq, PartialEq, Clone, Debug)]
923072b8 830pub(crate) struct LangString {
8bb4bdeb 831 original: String,
923072b8
FG
832 pub(crate) should_panic: bool,
833 pub(crate) no_run: bool,
834 pub(crate) ignore: Ignore,
835 pub(crate) rust: bool,
836 pub(crate) test_harness: bool,
837 pub(crate) compile_fail: bool,
838 pub(crate) error_codes: Vec<String>,
839 pub(crate) edition: Option<Edition>,
1a4d82fc
JJ
840}
841
e1599b0c 842#[derive(Eq, PartialEq, Clone, Debug)]
923072b8 843pub(crate) enum Ignore {
e1599b0c
XL
844 All,
845 None,
846 Some(Vec<String>),
847}
848
fc512014
XL
849impl Default for LangString {
850 fn default() -> Self {
851 Self {
8bb4bdeb 852 original: String::new(),
c34b1796 853 should_panic: false,
1a4d82fc 854 no_run: false,
e1599b0c 855 ignore: Ignore::None,
fc512014 856 rust: true,
1a4d82fc 857 test_harness: false,
7453a54e 858 compile_fail: false,
3157f602 859 error_codes: Vec::new(),
0bf4aa26 860 edition: None,
1a4d82fc
JJ
861 }
862 }
fc512014 863}
1a4d82fc 864
fc512014 865impl LangString {
f9f354fc
XL
866 fn parse_without_check(
867 string: &str,
868 allow_error_code_check: ErrorCodes,
869 enable_per_target_ignores: bool,
870 ) -> LangString {
871 Self::parse(string, allow_error_code_check, enable_per_target_ignores, None)
872 }
873
6a06907d
XL
874 fn tokens(string: &str) -> impl Iterator<Item = &str> {
875 // Pandoc, which Rust once used for generating documentation,
876 // expects lang strings to be surrounded by `{}` and for each token
877 // to be proceeded by a `.`. Since some of these lang strings are still
878 // loose in the wild, we strip a pair of surrounding `{}` from the lang
879 // string and a leading `.` from each token.
880
881 let string = string.trim();
882
883 let first = string.chars().next();
884 let last = string.chars().last();
885
886 let string = if first == Some('{') && last == Some('}') {
887 &string[1..string.len() - 1]
888 } else {
889 string
890 };
891
892 string
893 .split(|c| c == ',' || c == ' ' || c == '\t')
894 .map(str::trim)
3c0e092e 895 .map(|token| token.strip_prefix('.').unwrap_or(token))
6a06907d
XL
896 .filter(|token| !token.is_empty())
897 }
898
e1599b0c
XL
899 fn parse(
900 string: &str,
901 allow_error_code_check: ErrorCodes,
dfeec247 902 enable_per_target_ignores: bool,
5869c6ff 903 extra: Option<&ExtraInfo<'_>>,
e1599b0c 904 ) -> LangString {
b7449926 905 let allow_error_code_check = allow_error_code_check.as_bool();
1a4d82fc
JJ
906 let mut seen_rust_tags = false;
907 let mut seen_other_tags = false;
fc512014 908 let mut data = LangString::default();
e1599b0c 909 let mut ignores = vec![];
1a4d82fc 910
8bb4bdeb 911 data.original = string.to_owned();
6a06907d 912
5099ac24 913 for token in Self::tokens(string) {
6a06907d 914 match token {
cc61c64b
XL
915 "should_panic" => {
916 data.should_panic = true;
74b04a01 917 seen_rust_tags = !seen_other_tags;
cc61c64b 918 }
dfeec247
XL
919 "no_run" => {
920 data.no_run = true;
921 seen_rust_tags = !seen_other_tags;
922 }
923 "ignore" => {
924 data.ignore = Ignore::All;
925 seen_rust_tags = !seen_other_tags;
926 }
927 x if x.starts_with("ignore-") => {
928 if enable_per_target_ignores {
929 ignores.push(x.trim_start_matches("ignore-").to_owned());
930 seen_rust_tags = !seen_other_tags;
931 }
932 }
dfeec247
XL
933 "rust" => {
934 data.rust = true;
935 seen_rust_tags = true;
936 }
cc61c64b
XL
937 "test_harness" => {
938 data.test_harness = true;
939 seen_rust_tags = !seen_other_tags || seen_rust_tags;
940 }
ea8adc8c 941 "compile_fail" => {
7453a54e 942 data.compile_fail = true;
cc61c64b 943 seen_rust_tags = !seen_other_tags || seen_rust_tags;
7453a54e 944 data.no_run = true;
3157f602 945 }
60c5eb7d 946 x if x.starts_with("edition") => {
0bf4aa26
XL
947 data.edition = x[7..].parse::<Edition>().ok();
948 }
74b04a01 949 x if allow_error_code_check && x.starts_with('E') && x.len() == 5 => {
b7449926 950 if x[1..].parse::<u32>().is_ok() {
3157f602 951 data.error_codes.push(x.to_owned());
cc61c64b 952 seen_rust_tags = !seen_other_tags || seen_rust_tags;
3157f602
XL
953 } else {
954 seen_other_tags = true;
955 }
956 }
f9f354fc
XL
957 x if extra.is_some() => {
958 let s = x.to_lowercase();
3c0e092e
XL
959 if let Some((flag, help)) = if s == "compile-fail"
960 || s == "compile_fail"
961 || s == "compilefail"
962 {
f9f354fc
XL
963 Some((
964 "compile_fail",
965 "the code block will either not be tested if not marked as a rust one \
966 or won't fail if it compiles successfully",
967 ))
968 } else if s == "should-panic" || s == "should_panic" || s == "shouldpanic" {
969 Some((
970 "should_panic",
971 "the code block will either not be tested if not marked as a rust one \
972 or won't fail if it doesn't panic when running",
973 ))
974 } else if s == "no-run" || s == "no_run" || s == "norun" {
975 Some((
976 "no_run",
977 "the code block will either not be tested if not marked as a rust one \
978 or will be run (which you might not want)",
979 ))
f9f354fc
XL
980 } else if s == "test-harness" || s == "test_harness" || s == "testharness" {
981 Some((
982 "test_harness",
983 "the code block will either not be tested if not marked as a rust one \
984 or the code will be wrapped inside a main function",
985 ))
986 } else {
987 None
988 } {
3c0e092e
XL
989 if let Some(extra) = extra {
990 extra.error_invalid_codeblock_attr(
991 &format!("unknown attribute `{}`. Did you mean `{}`?", x, flag),
992 help,
993 );
f9f354fc 994 }
f9f354fc
XL
995 }
996 seen_other_tags = true;
997 }
dfeec247 998 _ => seen_other_tags = true,
1a4d82fc
JJ
999 }
1000 }
6a06907d 1001
e1599b0c
XL
1002 // ignore-foo overrides ignore
1003 if !ignores.is_empty() {
1004 data.ignore = Ignore::Some(ignores);
1005 }
1a4d82fc
JJ
1006
1007 data.rust &= !seen_other_tags || seen_rust_tags;
1008
1009 data
1010 }
1011}
1012
416331ca 1013impl Markdown<'_> {
3dfed10e 1014 pub fn into_string(self) -> String {
c295e0f8
XL
1015 let Markdown {
1016 content: md,
1017 links,
923072b8 1018 ids,
c295e0f8
XL
1019 error_codes: codes,
1020 edition,
1021 playground,
1022 heading_offset,
1023 } = self;
cc61c64b 1024
1a4d82fc 1025 // This is actually common enough to special-case
dfeec247
XL
1026 if md.is_empty() {
1027 return String::new();
1028 }
1b1a35ee 1029 let mut replacer = |broken_link: BrokenLink<'_>| {
3c0e092e
XL
1030 links
1031 .iter()
a2a8927a 1032 .find(|link| link.original_text.as_str() == &*broken_link.reference)
3c0e092e 1033 .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
0531ce1d 1034 };
cc61c64b 1035
c295e0f8 1036 let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
fc512014 1037 let p = p.into_offset_iter();
cc61c64b 1038
0531ce1d 1039 let mut s = String::with_capacity(md.len() * 3 / 2);
cc61c64b 1040
923072b8 1041 let p = HeadingLinks::new(p, None, ids, heading_offset);
b7449926 1042 let p = Footnotes::new(p);
fc512014 1043 let p = LinkReplacer::new(p.map(|(ev, _)| ev), links);
c295e0f8 1044 let p = TableWrapper::new(p);
fc512014 1045 let p = CodeBlocks::new(p, codes, edition, playground);
b7449926 1046 html::push_html(&mut s, p);
0531ce1d 1047
416331ca 1048 s
1a4d82fc
JJ
1049 }
1050}
1051
416331ca 1052impl MarkdownWithToc<'_> {
923072b8
FG
1053 pub(crate) fn into_string(self) -> String {
1054 let MarkdownWithToc(md, ids, codes, edition, playground) = self;
cc61c64b 1055
c295e0f8 1056 let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
cc61c64b 1057
0531ce1d 1058 let mut s = String::with_capacity(md.len() * 3 / 2);
cc61c64b 1059
0531ce1d 1060 let mut toc = TocBuilder::new();
cc61c64b 1061
b7449926 1062 {
923072b8 1063 let p = HeadingLinks::new(p, Some(&mut toc), ids, HeadingOffset::H1);
b7449926 1064 let p = Footnotes::new(p);
c295e0f8
XL
1065 let p = TableWrapper::new(p.map(|(ev, _)| ev));
1066 let p = CodeBlocks::new(p, codes, edition, playground);
b7449926
XL
1067 html::push_html(&mut s, p);
1068 }
cc61c64b 1069
e74abb32 1070 format!("<nav id=\"TOC\">{}</nav>{}", toc.into_toc().print(), s)
32a655c1
SL
1071 }
1072}
1073
2b03887a 1074impl MarkdownItemInfo<'_> {
923072b8 1075 pub(crate) fn into_string(self) -> String {
2b03887a 1076 let MarkdownItemInfo(md, ids) = self;
cc61c64b 1077
32a655c1 1078 // This is actually common enough to special-case
dfeec247
XL
1079 if md.is_empty() {
1080 return String::new();
1081 }
c295e0f8 1082 let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
cc61c64b 1083
0531ce1d 1084 // Treat inline HTML as plain text.
fc512014
XL
1085 let p = p.map(|event| match event.0 {
1086 Event::Html(text) => (Event::Text(text), event.1),
dfeec247 1087 _ => event,
0531ce1d 1088 });
cc61c64b 1089
0531ce1d 1090 let mut s = String::with_capacity(md.len() * 3 / 2);
cc61c64b 1091
923072b8 1092 let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
b7449926 1093 let p = Footnotes::new(p);
c295e0f8 1094 let p = TableWrapper::new(p.map(|(ev, _)| ev));
2b03887a
FG
1095 let p = p.filter(|event| {
1096 !matches!(event, Event::Start(Tag::Paragraph) | Event::End(Tag::Paragraph))
1097 });
b7449926 1098 html::push_html(&mut s, p);
cc61c64b 1099
416331ca 1100 s
85aaf69f
SL
1101 }
1102}
1103
416331ca 1104impl MarkdownSummaryLine<'_> {
6522a427 1105 pub(crate) fn into_string_with_has_more_content(self) -> (String, bool) {
416331ca 1106 let MarkdownSummaryLine(md, links) = self;
cc61c64b 1107 // This is actually common enough to special-case
dfeec247 1108 if md.is_empty() {
6522a427 1109 return (String::new(), false);
dfeec247 1110 }
cc61c64b 1111
1b1a35ee 1112 let mut replacer = |broken_link: BrokenLink<'_>| {
3c0e092e
XL
1113 links
1114 .iter()
a2a8927a 1115 .find(|link| link.original_text.as_str() == &*broken_link.reference)
3c0e092e 1116 .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
0531ce1d
XL
1117 };
1118
6522a427
EL
1119 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer))
1120 .peekable();
1121 let mut summary = SummaryLine::new(p);
cc61c64b
XL
1122
1123 let mut s = String::new();
1124
6522a427 1125 let without_paragraphs = LinkReplacer::new(&mut summary, links).filter(|event| {
f2b60f7d
FG
1126 !matches!(event, Event::Start(Tag::Paragraph) | Event::End(Tag::Paragraph))
1127 });
1128
1129 html::push_html(&mut s, without_paragraphs);
cc61c64b 1130
6522a427
EL
1131 let has_more_content =
1132 matches!(summary.inner.peek(), Some(Event::Start(_))) || summary.skipped_tags > 0;
1133
1134 (s, has_more_content)
1135 }
1136
1137 pub(crate) fn into_string(self) -> String {
1138 self.into_string_with_has_more_content().0
85aaf69f 1139 }
cc61c64b 1140}
85aaf69f 1141
fc512014
XL
1142/// Renders a subset of Markdown in the first paragraph of the provided Markdown.
1143///
1144/// - *Italics*, **bold**, and `inline code` styles **are** rendered.
1145/// - Headings and links are stripped (though the text *is* rendered).
1146/// - HTML, code blocks, and everything else are ignored.
1147///
1148/// Returns a tuple of the rendered HTML string and whether the output was shortened
1149/// due to the provided `length_limit`.
136023e0
XL
1150fn markdown_summary_with_limit(
1151 md: &str,
1152 link_names: &[RenderedLink],
1153 length_limit: usize,
1154) -> (String, bool) {
fc512014
XL
1155 if md.is_empty() {
1156 return (String::new(), false);
1157 }
1158
136023e0 1159 let mut replacer = |broken_link: BrokenLink<'_>| {
3c0e092e
XL
1160 link_names
1161 .iter()
a2a8927a 1162 .find(|link| link.original_text.as_str() == &*broken_link.reference)
3c0e092e 1163 .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
136023e0
XL
1164 };
1165
c295e0f8 1166 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
94222f64 1167 let mut p = LinkReplacer::new(p, link_names);
136023e0 1168
94222f64
XL
1169 let mut buf = HtmlWithLimit::new(length_limit);
1170 let mut stopped_early = false;
1171 p.try_for_each(|event| {
fc512014
XL
1172 match &event {
1173 Event::Text(text) => {
94222f64
XL
1174 let r =
1175 text.split_inclusive(char::is_whitespace).try_for_each(|word| buf.push(word));
1176 if r.is_break() {
1177 stopped_early = true;
fc512014 1178 }
94222f64 1179 return r;
fc512014
XL
1180 }
1181 Event::Code(code) => {
94222f64
XL
1182 buf.open_tag("code");
1183 let r = buf.push(code);
1184 if r.is_break() {
fc512014 1185 stopped_early = true;
94222f64
XL
1186 } else {
1187 buf.close_tag();
fc512014 1188 }
94222f64 1189 return r;
fc512014
XL
1190 }
1191 Event::Start(tag) => match tag {
94222f64
XL
1192 Tag::Emphasis => buf.open_tag("em"),
1193 Tag::Strong => buf.open_tag("strong"),
1194 Tag::CodeBlock(..) => return ControlFlow::BREAK,
fc512014
XL
1195 _ => {}
1196 },
1197 Event::End(tag) => match tag {
94222f64
XL
1198 Tag::Emphasis | Tag::Strong => buf.close_tag(),
1199 Tag::Paragraph | Tag::Heading(..) => return ControlFlow::BREAK,
fc512014
XL
1200 _ => {}
1201 },
94222f64 1202 Event::HardBreak | Event::SoftBreak => buf.push(" ")?,
fc512014 1203 _ => {}
94222f64
XL
1204 };
1205 ControlFlow::CONTINUE
1206 });
fc512014 1207
94222f64 1208 (buf.finish(), stopped_early)
fc512014
XL
1209}
1210
1211/// Renders a shortened first paragraph of the given Markdown as a subset of Markdown,
1212/// making it suitable for contexts like the search index.
1213///
1214/// Will shorten to 59 or 60 characters, including an ellipsis (…) if it was shortened.
1215///
1216/// See [`markdown_summary_with_limit`] for details about what is rendered and what is not.
923072b8 1217pub(crate) fn short_markdown_summary(markdown: &str, link_names: &[RenderedLink]) -> String {
136023e0 1218 let (mut s, was_shortened) = markdown_summary_with_limit(markdown, link_names, 59);
fc512014
XL
1219
1220 if was_shortened {
1221 s.push('…');
1222 }
1223
1224 s
1225}
1226
1b1a35ee 1227/// Renders the first paragraph of the provided markdown as plain text.
fc512014 1228/// Useful for alt-text.
1b1a35ee
XL
1229///
1230/// - Headings, links, and formatting are stripped.
1231/// - Inline code is rendered as-is, surrounded by backticks.
1232/// - HTML and code blocks are ignored.
923072b8 1233pub(crate) fn plain_text_summary(md: &str) -> String {
1b1a35ee
XL
1234 if md.is_empty() {
1235 return String::new();
85aaf69f
SL
1236 }
1237
1b1a35ee
XL
1238 let mut s = String::with_capacity(md.len() * 3 / 2);
1239
fc512014 1240 for event in Parser::new_ext(md, summary_opts()) {
1b1a35ee
XL
1241 match &event {
1242 Event::Text(text) => s.push_str(text),
1243 Event::Code(code) => {
1244 s.push('`');
1245 s.push_str(code);
1246 s.push('`');
cc61c64b 1247 }
1b1a35ee
XL
1248 Event::HardBreak | Event::SoftBreak => s.push(' '),
1249 Event::Start(Tag::CodeBlock(..)) => break,
1250 Event::End(Tag::Paragraph) => break,
6a06907d 1251 Event::End(Tag::Heading(..)) => break,
1b1a35ee 1252 _ => (),
cc61c64b
XL
1253 }
1254 }
1b1a35ee 1255
e1599b0c 1256 s
1a4d82fc
JJ
1257}
1258
cdc7bbd5 1259#[derive(Debug)]
923072b8 1260pub(crate) struct MarkdownLink {
5869c6ff
XL
1261 pub kind: LinkType,
1262 pub link: String,
1263 pub range: Range<usize>,
1264}
1265
923072b8
FG
1266pub(crate) fn markdown_links<R>(
1267 md: &str,
1268 filter_map: impl Fn(MarkdownLink) -> Option<R>,
1269) -> Vec<R> {
2c00a5a8
XL
1270 if md.is_empty() {
1271 return vec![];
1272 }
1273
5869c6ff
XL
1274 let links = RefCell::new(vec![]);
1275
1276 // FIXME: remove this function once pulldown_cmark can provide spans for link definitions.
1277 let locate = |s: &str, fallback: Range<usize>| unsafe {
1278 let s_start = s.as_ptr();
1279 let s_end = s_start.add(s.len());
1280 let md_start = md.as_ptr();
1281 let md_end = md_start.add(md.len());
1282 if md_start <= s_start && s_end <= md_end {
1283 let start = s_start.offset_from(md_start) as usize;
1284 let end = s_end.offset_from(md_start) as usize;
1285 start..end
1286 } else {
1287 fallback
fc512014
XL
1288 }
1289 };
5869c6ff
XL
1290
1291 let span_for_link = |link: &CowStr<'_>, span: Range<usize>| {
1292 // For diagnostics, we want to underline the link's definition but `span` will point at
1293 // where the link is used. This is a problem for reference-style links, where the definition
1294 // is separate from the usage.
1295 match link {
1296 // `Borrowed` variant means the string (the link's destination) may come directly from
1297 // the markdown text and we can locate the original link destination.
1298 // NOTE: LinkReplacer also provides `Borrowed` but possibly from other sources,
1299 // so `locate()` can fall back to use `span`.
1300 CowStr::Borrowed(s) => locate(s, span),
1301
1302 // For anything else, we can only use the provided range.
1303 CowStr::Boxed(_) | CowStr::Inlined(_) => span,
1304 }
1305 };
1306
fc512014 1307 let mut push = |link: BrokenLink<'_>| {
a2a8927a 1308 let span = span_for_link(&link.reference, link.span);
04454e1e 1309 filter_map(MarkdownLink {
5869c6ff 1310 kind: LinkType::ShortcutUnknown,
a2a8927a 1311 link: link.reference.to_string(),
5869c6ff 1312 range: span,
04454e1e
FG
1313 })
1314 .map(|link| links.borrow_mut().push(link));
fc512014
XL
1315 None
1316 };
c295e0f8
XL
1317 let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut push))
1318 .into_offset_iter();
fc512014
XL
1319
1320 // There's no need to thread an IdMap through to here because
1321 // the IDs generated aren't going to be emitted anywhere.
1322 let mut ids = IdMap::new();
c295e0f8 1323 let iter = Footnotes::new(HeadingLinks::new(p, None, &mut ids, HeadingOffset::H1));
fc512014
XL
1324
1325 for ev in iter {
04454e1e
FG
1326 if let Event::Start(Tag::Link(
1327 // `<>` links cannot be intra-doc links so we skip them.
1328 kind @ (LinkType::Inline
1329 | LinkType::Reference
1330 | LinkType::ReferenceUnknown
1331 | LinkType::Collapsed
1332 | LinkType::CollapsedUnknown
1333 | LinkType::Shortcut
1334 | LinkType::ShortcutUnknown),
1335 dest,
1336 _,
1337 )) = ev.0
1338 {
5e7ed085 1339 debug!("found link: {dest}");
fc512014 1340 let span = span_for_link(&dest, ev.1);
04454e1e
FG
1341 filter_map(MarkdownLink { kind, link: dest.into_string(), range: span })
1342 .map(|link| links.borrow_mut().push(link));
2c00a5a8 1343 }
0531ce1d 1344 }
2c00a5a8 1345
5869c6ff 1346 links.into_inner()
2c00a5a8
XL
1347}
1348
9fa01778 1349#[derive(Debug)]
923072b8 1350pub(crate) struct RustCodeBlock {
9fa01778
XL
1351 /// The range in the markdown that the code block occupies. Note that this includes the fences
1352 /// for fenced code blocks.
923072b8 1353 pub(crate) range: Range<usize>,
9fa01778 1354 /// The range in the markdown that the code within the code block occupies.
923072b8
FG
1355 pub(crate) code: Range<usize>,
1356 pub(crate) is_fenced: bool,
1357 pub(crate) lang_string: LangString,
9fa01778
XL
1358}
1359
1360/// Returns a range of bytes for each code block in the markdown that is tagged as `rust` or
1361/// untagged (and assumed to be rust).
923072b8 1362pub(crate) fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_>) -> Vec<RustCodeBlock> {
9fa01778
XL
1363 let mut code_blocks = vec![];
1364
1365 if md.is_empty() {
1366 return code_blocks;
1367 }
1368
c295e0f8 1369 let mut p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
9fa01778 1370
74b04a01 1371 while let Some((event, offset)) = p.next() {
ba9703b0 1372 if let Event::Start(Tag::CodeBlock(syntax)) = event {
94222f64 1373 let (lang_string, code_start, code_end, range, is_fenced) = match syntax {
ba9703b0
XL
1374 CodeBlockKind::Fenced(syntax) => {
1375 let syntax = syntax.as_ref();
1376 let lang_string = if syntax.is_empty() {
fc512014 1377 Default::default()
ba9703b0 1378 } else {
f9f354fc 1379 LangString::parse(&*syntax, ErrorCodes::Yes, false, Some(extra_info))
ba9703b0
XL
1380 };
1381 if !lang_string.rust {
1382 continue;
1383 }
ba9703b0
XL
1384 let (code_start, mut code_end) = match p.next() {
1385 Some((Event::Text(_), offset)) => (offset.start, offset.end),
1386 Some((_, sub_offset)) => {
1387 let code = Range { start: sub_offset.start, end: sub_offset.start };
1388 code_blocks.push(RustCodeBlock {
1389 is_fenced: true,
1390 range: offset,
1391 code,
94222f64 1392 lang_string,
ba9703b0 1393 });
74b04a01 1394 continue;
9fa01778 1395 }
ba9703b0
XL
1396 None => {
1397 let code = Range { start: offset.end, end: offset.end };
1398 code_blocks.push(RustCodeBlock {
1399 is_fenced: true,
1400 range: offset,
1401 code,
94222f64 1402 lang_string,
ba9703b0
XL
1403 });
1404 continue;
e1599b0c 1405 }
ba9703b0
XL
1406 };
1407 while let Some((Event::Text(_), offset)) = p.next() {
1408 code_end = offset.end;
74b04a01 1409 }
94222f64 1410 (lang_string, code_start, code_end, offset, true)
ba9703b0
XL
1411 }
1412 CodeBlockKind::Indented => {
1413 // The ending of the offset goes too far sometime so we reduce it by one in
1414 // these cases.
3c0e092e 1415 if offset.end > offset.start && md.get(offset.end..=offset.end) == Some("\n") {
ba9703b0 1416 (
94222f64 1417 LangString::default(),
ba9703b0
XL
1418 offset.start,
1419 offset.end,
1420 Range { start: offset.start, end: offset.end - 1 },
1421 false,
1422 )
1423 } else {
94222f64 1424 (LangString::default(), offset.start, offset.end, offset, false)
74b04a01 1425 }
ba9703b0
XL
1426 }
1427 };
9fa01778 1428
ba9703b0
XL
1429 code_blocks.push(RustCodeBlock {
1430 is_fenced,
1431 range,
1432 code: Range { start: code_start, end: code_end },
94222f64 1433 lang_string,
ba9703b0 1434 });
9fa01778 1435 }
9fa01778
XL
1436 }
1437
1438 code_blocks
1439}
1440
a1dfa0c6 1441#[derive(Clone, Default, Debug)]
b7449926 1442pub struct IdMap {
04454e1e 1443 map: FxHashMap<Cow<'static, str>, usize>,
b7449926
XL
1444}
1445
04454e1e
FG
1446// The map is pre-initialized and cloned each time to avoid reinitializing it repeatedly.
1447static DEFAULT_ID_MAP: Lazy<FxHashMap<Cow<'static, str>, usize>> = Lazy::new(|| init_id_map());
1448
1449fn init_id_map() -> FxHashMap<Cow<'static, str>, usize> {
f9f354fc 1450 let mut map = FxHashMap::default();
136023e0 1451 // This is the list of IDs used in Javascript.
2b03887a 1452 map.insert("help".into(), 1);
04454e1e
FG
1453 map.insert("settings".into(), 1);
1454 map.insert("not-displayed".into(), 1);
1455 map.insert("alternative-display".into(), 1);
1456 map.insert("search".into(), 1);
f2b60f7d
FG
1457 map.insert("crate-search".into(), 1);
1458 map.insert("crate-search-div".into(), 1);
136023e0
XL
1459 // This is the list of IDs used in HTML generated in Rust (including the ones
1460 // used in tera template files).
04454e1e
FG
1461 map.insert("mainThemeStyle".into(), 1);
1462 map.insert("themeStyle".into(), 1);
04454e1e
FG
1463 map.insert("settings-menu".into(), 1);
1464 map.insert("help-button".into(), 1);
1465 map.insert("main-content".into(), 1);
04454e1e
FG
1466 map.insert("toggle-all-docs".into(), 1);
1467 map.insert("all-types".into(), 1);
1468 map.insert("default-settings".into(), 1);
1469 map.insert("rustdoc-vars".into(), 1);
1470 map.insert("sidebar-vars".into(), 1);
1471 map.insert("copy-path".into(), 1);
1472 map.insert("TOC".into(), 1);
136023e0
XL
1473 // This is the list of IDs used by rustdoc sections (but still generated by
1474 // rustdoc).
04454e1e
FG
1475 map.insert("fields".into(), 1);
1476 map.insert("variants".into(), 1);
1477 map.insert("implementors-list".into(), 1);
1478 map.insert("synthetic-implementors-list".into(), 1);
1479 map.insert("foreign-impls".into(), 1);
1480 map.insert("implementations".into(), 1);
1481 map.insert("trait-implementations".into(), 1);
1482 map.insert("synthetic-implementations".into(), 1);
1483 map.insert("blanket-implementations".into(), 1);
1484 map.insert("required-associated-types".into(), 1);
1485 map.insert("provided-associated-types".into(), 1);
1486 map.insert("provided-associated-consts".into(), 1);
1487 map.insert("required-associated-consts".into(), 1);
1488 map.insert("required-methods".into(), 1);
1489 map.insert("provided-methods".into(), 1);
1490 map.insert("implementors".into(), 1);
1491 map.insert("synthetic-implementors".into(), 1);
1492 map.insert("implementations-list".into(), 1);
1493 map.insert("trait-implementations-list".into(), 1);
1494 map.insert("synthetic-implementations-list".into(), 1);
1495 map.insert("blanket-implementations-list".into(), 1);
1496 map.insert("deref-methods".into(), 1);
064997fb 1497 map.insert("layout".into(), 1);
f9f354fc
XL
1498 map
1499}
1500
b7449926
XL
1501impl IdMap {
1502 pub fn new() -> Self {
04454e1e 1503 IdMap { map: DEFAULT_ID_MAP.clone() }
b7449926
XL
1504 }
1505
923072b8 1506 pub(crate) fn derive<S: AsRef<str> + ToString>(&mut self, candidate: S) -> String {
5869c6ff
XL
1507 let id = match self.map.get_mut(candidate.as_ref()) {
1508 None => candidate.to_string(),
b7449926 1509 Some(a) => {
5869c6ff 1510 let id = format!("{}-{}", candidate.as_ref(), *a);
b7449926
XL
1511 *a += 1;
1512 id
1513 }
1514 };
1515
04454e1e 1516 self.map.insert(id.clone().into(), 1);
b7449926
XL
1517 id
1518 }
1519}