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