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