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