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