]>
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 | //! | |
48663c56 XL |
8 | //! extern crate syntax; |
9 | //! | |
10 | //! use syntax::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 XL |
15 | //! let md = Markdown(s, &[], &mut id_map, ErrorCodes::Yes, Edition::Edition2015, &None); |
16 | //! let html = md.to_string(); | |
1a4d82fc JJ |
17 | //! // ... something using html |
18 | //! ``` | |
19 | ||
1a4d82fc JJ |
20 | #![allow(non_camel_case_types)] |
21 | ||
b7449926 | 22 | use rustc_data_structures::fx::FxHashMap; |
85aaf69f | 23 | use std::cell::RefCell; |
b7449926 | 24 | use std::collections::VecDeque; |
9346a6ac | 25 | use std::default::Default; |
416331ca | 26 | use std::fmt::Write; |
94b46f34 XL |
27 | use std::borrow::Cow; |
28 | use std::ops::Range; | |
1a4d82fc | 29 | use std::str; |
0bf4aa26 | 30 | use syntax::edition::Edition; |
1a4d82fc | 31 | |
9fa01778 XL |
32 | use crate::html::toc::TocBuilder; |
33 | use crate::html::highlight; | |
34 | use crate::test; | |
1a4d82fc | 35 | |
48663c56 XL |
36 | use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag}; |
37 | ||
416331ca XL |
38 | #[cfg(test)] |
39 | mod tests; | |
40 | ||
48663c56 XL |
41 | fn opts() -> Options { |
42 | Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES | |
43 | } | |
cc61c64b | 44 | |
416331ca XL |
45 | /// When `to_string` is called, this struct will emit the HTML corresponding to |
46 | /// the rendered version of the contained markdown string. | |
b7449926 | 47 | pub struct Markdown<'a>( |
dc9dc135 XL |
48 | pub &'a str, |
49 | /// A list of link replacements. | |
50 | pub &'a [(String, String)], | |
51 | /// The current list of used header IDs. | |
416331ca | 52 | pub &'a mut IdMap, |
dc9dc135 XL |
53 | /// Whether to allow the use of explicit error codes in doctest lang strings. |
54 | pub ErrorCodes, | |
55 | /// Default edition to use when parsing doctests (to add a `fn main`). | |
56 | pub Edition, | |
416331ca | 57 | pub &'a Option<Playground>, |
dc9dc135 XL |
58 | ); |
59 | /// A tuple struct like `Markdown` that renders the markdown with a table of contents. | |
60 | pub struct MarkdownWithToc<'a>( | |
61 | pub &'a str, | |
416331ca | 62 | pub &'a mut IdMap, |
dc9dc135 XL |
63 | pub ErrorCodes, |
64 | pub Edition, | |
416331ca | 65 | pub &'a Option<Playground>, |
dc9dc135 XL |
66 | ); |
67 | /// A tuple struct like `Markdown` that renders the markdown escaping HTML tags. | |
416331ca XL |
68 | pub struct MarkdownHtml<'a>( |
69 | pub &'a str, | |
70 | pub &'a mut IdMap, | |
71 | pub ErrorCodes, | |
72 | pub Edition, | |
73 | pub &'a Option<Playground>, | |
74 | ); | |
dc9dc135 | 75 | /// A tuple struct like `Markdown` that renders only the first paragraph. |
2c00a5a8 | 76 | pub struct MarkdownSummaryLine<'a>(pub &'a str, pub &'a [(String, String)]); |
cc61c64b | 77 | |
b7449926 XL |
78 | #[derive(Copy, Clone, PartialEq, Debug)] |
79 | pub enum ErrorCodes { | |
80 | Yes, | |
81 | No, | |
82 | } | |
83 | ||
84 | impl ErrorCodes { | |
85 | pub fn from(b: bool) -> Self { | |
86 | match b { | |
87 | true => ErrorCodes::Yes, | |
88 | false => ErrorCodes::No, | |
89 | } | |
90 | } | |
91 | ||
92 | pub fn as_bool(self) -> bool { | |
93 | match self { | |
94 | ErrorCodes::Yes => true, | |
95 | ErrorCodes::No => false, | |
96 | } | |
97 | } | |
98 | } | |
99 | ||
7cac9316 XL |
100 | /// Controls whether a line will be hidden or shown in HTML output. |
101 | /// | |
102 | /// All lines are used in documentation tests. | |
103 | enum Line<'a> { | |
104 | Hidden(&'a str), | |
8faf50e0 | 105 | Shown(Cow<'a, str>), |
7cac9316 XL |
106 | } |
107 | ||
108 | impl<'a> Line<'a> { | |
8faf50e0 | 109 | fn for_html(self) -> Option<Cow<'a, str>> { |
7cac9316 XL |
110 | match self { |
111 | Line::Shown(l) => Some(l), | |
112 | Line::Hidden(_) => None, | |
113 | } | |
114 | } | |
115 | ||
8faf50e0 | 116 | fn for_code(self) -> Cow<'a, str> { |
7cac9316 | 117 | match self { |
8faf50e0 XL |
118 | Line::Shown(l) => l, |
119 | Line::Hidden(l) => Cow::Borrowed(l), | |
7cac9316 XL |
120 | } |
121 | } | |
122 | } | |
123 | ||
124 | // FIXME: There is a minor inconsistency here. For lines that start with ##, we | |
125 | // have no easy way of removing a potential single space after the hashes, which | |
126 | // is done in the single # case. This inconsistency seems okay, if non-ideal. In | |
127 | // order to fix it we'd have to iterate to find the first non-# character, and | |
128 | // then reallocate to remove it; which would make us return a String. | |
9fa01778 | 129 | fn map_line(s: &str) -> Line<'_> { |
cc61c64b | 130 | let trimmed = s.trim(); |
7cac9316 | 131 | if trimmed.starts_with("##") { |
8faf50e0 | 132 | Line::Shown(Cow::Owned(s.replacen("##", "#", 1))) |
cc61c64b | 133 | } else if trimmed.starts_with("# ") { |
7cac9316 XL |
134 | // # text |
135 | Line::Hidden(&trimmed[2..]) | |
136 | } else if trimmed == "#" { | |
137 | // We cannot handle '#text' because it could be #[attr]. | |
138 | Line::Hidden("") | |
cc61c64b | 139 | } else { |
8faf50e0 | 140 | Line::Shown(Cow::Borrowed(s)) |
cc61c64b XL |
141 | } |
142 | } | |
143 | ||
cc61c64b XL |
144 | /// Convert chars from a title for an id. |
145 | /// | |
146 | /// "Hello, world!" -> "hello-world" | |
147 | fn slugify(c: char) -> Option<char> { | |
148 | if c.is_alphanumeric() || c == '-' || c == '_' { | |
149 | if c.is_ascii() { | |
150 | Some(c.to_ascii_lowercase()) | |
151 | } else { | |
152 | Some(c) | |
153 | } | |
154 | } else if c.is_whitespace() && c.is_ascii() { | |
155 | Some('-') | |
156 | } else { | |
157 | None | |
158 | } | |
159 | } | |
160 | ||
416331ca XL |
161 | #[derive(Clone, Debug)] |
162 | pub struct Playground { | |
163 | pub crate_name: Option<String>, | |
164 | pub url: String, | |
165 | } | |
cc61c64b | 166 | |
9fa01778 | 167 | /// Adds syntax highlighting and playground Run buttons to Rust code blocks. |
416331ca | 168 | struct CodeBlocks<'p, 'a, I: Iterator<Item = Event<'a>>> { |
cc61c64b | 169 | inner: I, |
b7449926 | 170 | check_error_codes: ErrorCodes, |
48663c56 | 171 | edition: Edition, |
416331ca XL |
172 | // Information about the playground if a URL has been specified, containing an |
173 | // optional crate name and the URL. | |
174 | playground: &'p Option<Playground>, | |
cc61c64b XL |
175 | } |
176 | ||
416331ca XL |
177 | impl<'p, 'a, I: Iterator<Item = Event<'a>>> CodeBlocks<'p, 'a, I> { |
178 | fn new( | |
179 | iter: I, | |
180 | error_codes: ErrorCodes, | |
181 | edition: Edition, | |
182 | playground: &'p Option<Playground>, | |
183 | ) -> Self { | |
cc61c64b XL |
184 | CodeBlocks { |
185 | inner: iter, | |
b7449926 | 186 | check_error_codes: error_codes, |
48663c56 | 187 | edition, |
416331ca | 188 | playground, |
cc61c64b XL |
189 | } |
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 XL |
198 | let compile_fail; |
199 | let ignore; | |
0bf4aa26 | 200 | let edition; |
cc61c64b | 201 | if let Some(Event::Start(Tag::CodeBlock(lang))) = event { |
e1599b0c | 202 | let parse_result = LangString::parse(&lang, self.check_error_codes, false); |
ea8adc8c | 203 | if !parse_result.rust { |
cc61c64b XL |
204 | return Some(Event::Start(Tag::CodeBlock(lang))); |
205 | } | |
ea8adc8c XL |
206 | compile_fail = parse_result.compile_fail; |
207 | ignore = parse_result.ignore; | |
0bf4aa26 | 208 | edition = parse_result.edition; |
cc61c64b XL |
209 | } else { |
210 | return event; | |
211 | } | |
212 | ||
48663c56 XL |
213 | let explicit_edition = edition.is_some(); |
214 | let edition = edition.unwrap_or(self.edition); | |
215 | ||
cc61c64b XL |
216 | let mut origtext = String::new(); |
217 | for event in &mut self.inner { | |
218 | match event { | |
219 | Event::End(Tag::CodeBlock(..)) => break, | |
220 | Event::Text(ref s) => { | |
221 | origtext.push_str(s); | |
222 | } | |
223 | _ => {} | |
224 | } | |
225 | } | |
7cac9316 | 226 | let lines = origtext.lines().filter_map(|l| map_line(l).for_html()); |
9fa01778 | 227 | let text = lines.collect::<Vec<Cow<'_, str>>>().join("\n"); |
416331ca XL |
228 | // insert newline to clearly separate it from the |
229 | // previous block so we can shorten the html output | |
230 | let mut s = String::from("\n"); | |
231 | let playground_button = self.playground.as_ref().and_then(|playground| { | |
232 | let krate = &playground.crate_name; | |
233 | let url = &playground.url; | |
234 | if url.is_empty() { | |
235 | return None; | |
236 | } | |
237 | let test = origtext.lines() | |
238 | .map(|l| map_line(l).for_code()) | |
239 | .collect::<Vec<Cow<'_, str>>>().join("\n"); | |
240 | let krate = krate.as_ref().map(|s| &**s); | |
241 | let (test, _) = test::make_test(&test, krate, false, | |
242 | &Default::default(), edition); | |
243 | let channel = if test.contains("#![feature(") { | |
244 | "&version=nightly" | |
ea8adc8c | 245 | } else { |
416331ca | 246 | "" |
ea8adc8c | 247 | }; |
0bf4aa26 | 248 | |
416331ca XL |
249 | let edition_string = format!("&edition={}", edition); |
250 | ||
251 | // These characters don't need to be escaped in a URI. | |
252 | // FIXME: use a library function for percent encoding. | |
253 | fn dont_escape(c: u8) -> bool { | |
254 | (b'a' <= c && c <= b'z') || | |
255 | (b'A' <= c && c <= b'Z') || | |
256 | (b'0' <= c && c <= b'9') || | |
257 | c == b'-' || c == b'_' || c == b'.' || | |
258 | c == b'~' || c == b'!' || c == b'\'' || | |
259 | c == b'(' || c == b')' || c == b'*' | |
0bf4aa26 | 260 | } |
416331ca XL |
261 | let mut test_escaped = String::new(); |
262 | for b in test.bytes() { | |
263 | if dont_escape(b) { | |
264 | test_escaped.push(char::from(b)); | |
265 | } else { | |
266 | write!(test_escaped, "%{:02X}", b).unwrap(); | |
267 | } | |
268 | } | |
269 | Some(format!( | |
270 | r#"<a class="test-arrow" target="_blank" href="{}?code={}{}{}">Run</a>"#, | |
271 | url, test_escaped, channel, edition_string | |
272 | )) | |
273 | }); | |
274 | ||
e1599b0c | 275 | let tooltip = if ignore != Ignore::None { |
416331ca XL |
276 | Some(("This example is not tested".to_owned(), "ignore")) |
277 | } else if compile_fail { | |
278 | Some(("This example deliberately fails to compile".to_owned(), "compile_fail")) | |
279 | } else if explicit_edition { | |
280 | Some((format!("This code runs with edition {}", edition), "edition")) | |
281 | } else { | |
282 | None | |
283 | }; | |
284 | ||
285 | if let Some((s1, s2)) = tooltip { | |
286 | s.push_str(&highlight::render_with_highlighting( | |
287 | &text, | |
288 | Some(&format!("rust-example-rendered{}", | |
e1599b0c | 289 | if ignore != Ignore::None { " ignore" } |
416331ca XL |
290 | else if compile_fail { " compile_fail" } |
291 | else if explicit_edition { " edition " } | |
292 | else { "" })), | |
293 | playground_button.as_ref().map(String::as_str), | |
294 | Some((s1.as_str(), s2)))); | |
295 | Some(Event::Html(s.into())) | |
296 | } else { | |
297 | s.push_str(&highlight::render_with_highlighting( | |
298 | &text, | |
299 | Some(&format!("rust-example-rendered{}", | |
e1599b0c | 300 | if ignore != Ignore::None { " ignore" } |
416331ca XL |
301 | else if compile_fail { " compile_fail" } |
302 | else if explicit_edition { " edition " } | |
303 | else { "" })), | |
304 | playground_button.as_ref().map(String::as_str), | |
305 | None)); | |
306 | Some(Event::Html(s.into())) | |
307 | } | |
cc61c64b XL |
308 | } |
309 | } | |
310 | ||
9fa01778 | 311 | /// Make headings links with anchor IDs and build up TOC. |
2c00a5a8 XL |
312 | struct LinkReplacer<'a, 'b, I: Iterator<Item = Event<'a>>> { |
313 | inner: I, | |
0531ce1d | 314 | links: &'b [(String, String)], |
2c00a5a8 XL |
315 | } |
316 | ||
317 | impl<'a, 'b, I: Iterator<Item = Event<'a>>> LinkReplacer<'a, 'b, I> { | |
318 | fn new(iter: I, links: &'b [(String, String)]) -> Self { | |
319 | LinkReplacer { | |
320 | inner: iter, | |
0531ce1d | 321 | links, |
2c00a5a8 XL |
322 | } |
323 | } | |
324 | } | |
325 | ||
326 | impl<'a, 'b, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, 'b, I> { | |
327 | type Item = Event<'a>; | |
328 | ||
329 | fn next(&mut self) -> Option<Self::Item> { | |
330 | let event = self.inner.next(); | |
48663c56 XL |
331 | if let Some(Event::Start(Tag::Link(kind, dest, text))) = event { |
332 | if let Some(&(_, ref replace)) = self.links.iter().find(|link| link.0 == *dest) { | |
333 | Some(Event::Start(Tag::Link(kind, replace.to_owned().into(), text))) | |
2c00a5a8 | 334 | } else { |
48663c56 | 335 | Some(Event::Start(Tag::Link(kind, dest, text))) |
2c00a5a8 XL |
336 | } |
337 | } else { | |
338 | event | |
339 | } | |
340 | } | |
341 | } | |
342 | ||
9fa01778 | 343 | /// Make headings links with anchor IDs and build up TOC. |
b7449926 | 344 | struct HeadingLinks<'a, 'b, 'ids, I: Iterator<Item = Event<'a>>> { |
cc61c64b XL |
345 | inner: I, |
346 | toc: Option<&'b mut TocBuilder>, | |
347 | buf: VecDeque<Event<'a>>, | |
b7449926 | 348 | id_map: &'ids mut IdMap, |
cc61c64b XL |
349 | } |
350 | ||
b7449926 XL |
351 | impl<'a, 'b, 'ids, I: Iterator<Item = Event<'a>>> HeadingLinks<'a, 'b, 'ids, I> { |
352 | fn new(iter: I, toc: Option<&'b mut TocBuilder>, ids: &'ids mut IdMap) -> Self { | |
cc61c64b XL |
353 | HeadingLinks { |
354 | inner: iter, | |
3b2f2976 | 355 | toc, |
cc61c64b | 356 | buf: VecDeque::new(), |
b7449926 | 357 | id_map: ids, |
cc61c64b XL |
358 | } |
359 | } | |
360 | } | |
361 | ||
b7449926 | 362 | impl<'a, 'b, 'ids, I: Iterator<Item = Event<'a>>> Iterator for HeadingLinks<'a, 'b, 'ids, I> { |
cc61c64b XL |
363 | type Item = Event<'a>; |
364 | ||
365 | fn next(&mut self) -> Option<Self::Item> { | |
366 | if let Some(e) = self.buf.pop_front() { | |
367 | return Some(e); | |
368 | } | |
369 | ||
370 | let event = self.inner.next(); | |
371 | if let Some(Event::Start(Tag::Header(level))) = event { | |
372 | let mut id = String::new(); | |
373 | for event in &mut self.inner { | |
48663c56 | 374 | match &event { |
cc61c64b | 375 | Event::End(Tag::Header(..)) => break, |
48663c56 XL |
376 | Event::Text(text) | Event::Code(text) => { |
377 | id.extend(text.chars().filter_map(slugify)); | |
378 | } | |
cc61c64b XL |
379 | _ => {}, |
380 | } | |
381 | self.buf.push_back(event); | |
382 | } | |
b7449926 | 383 | let id = self.id_map.derive(id); |
cc61c64b XL |
384 | |
385 | if let Some(ref mut builder) = self.toc { | |
386 | let mut html_header = String::new(); | |
387 | html::push_html(&mut html_header, self.buf.iter().cloned()); | |
388 | let sec = builder.push(level as u32, html_header, id.clone()); | |
389 | self.buf.push_front(Event::InlineHtml(format!("{} ", sec).into())); | |
390 | } | |
391 | ||
392 | self.buf.push_back(Event::InlineHtml(format!("</a></h{}>", level).into())); | |
393 | ||
394 | let start_tags = format!("<h{level} id=\"{id}\" class=\"section-header\">\ | |
395 | <a href=\"#{id}\">", | |
396 | id = id, | |
397 | level = level); | |
398 | return Some(Event::InlineHtml(start_tags.into())); | |
399 | } | |
400 | event | |
401 | } | |
402 | } | |
403 | ||
404 | /// Extracts just the first paragraph. | |
405 | struct SummaryLine<'a, I: Iterator<Item = Event<'a>>> { | |
406 | inner: I, | |
407 | started: bool, | |
408 | depth: u32, | |
409 | } | |
410 | ||
411 | impl<'a, I: Iterator<Item = Event<'a>>> SummaryLine<'a, I> { | |
412 | fn new(iter: I) -> Self { | |
413 | SummaryLine { | |
414 | inner: iter, | |
415 | started: false, | |
416 | depth: 0, | |
417 | } | |
418 | } | |
419 | } | |
420 | ||
9fa01778 | 421 | fn check_if_allowed_tag(t: &Tag<'_>) -> bool { |
8faf50e0 XL |
422 | match *t { |
423 | Tag::Paragraph | |
8faf50e0 XL |
424 | | Tag::Item |
425 | | Tag::Emphasis | |
426 | | Tag::Strong | |
48663c56 | 427 | | Tag::Link(..) |
8faf50e0 XL |
428 | | Tag::BlockQuote => true, |
429 | _ => false, | |
430 | } | |
431 | } | |
432 | ||
cc61c64b XL |
433 | impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SummaryLine<'a, I> { |
434 | type Item = Event<'a>; | |
435 | ||
436 | fn next(&mut self) -> Option<Self::Item> { | |
437 | if self.started && self.depth == 0 { | |
438 | return None; | |
439 | } | |
440 | if !self.started { | |
441 | self.started = true; | |
442 | } | |
a1dfa0c6 XL |
443 | while let Some(event) = self.inner.next() { |
444 | let mut is_start = true; | |
445 | let is_allowed_tag = match event { | |
446 | Event::Start(Tag::CodeBlock(_)) | Event::End(Tag::CodeBlock(_)) => { | |
447 | return None; | |
448 | } | |
449 | Event::Start(ref c) => { | |
450 | self.depth += 1; | |
451 | check_if_allowed_tag(c) | |
452 | } | |
453 | Event::End(ref c) => { | |
454 | self.depth -= 1; | |
455 | is_start = false; | |
456 | check_if_allowed_tag(c) | |
457 | } | |
458 | _ => { | |
459 | true | |
460 | } | |
461 | }; | |
462 | return if is_allowed_tag == false { | |
463 | if is_start { | |
464 | Some(Event::Start(Tag::Paragraph)) | |
465 | } else { | |
466 | Some(Event::End(Tag::Paragraph)) | |
467 | } | |
8faf50e0 | 468 | } else { |
a1dfa0c6 XL |
469 | Some(event) |
470 | }; | |
cc61c64b | 471 | } |
a1dfa0c6 | 472 | None |
cc61c64b XL |
473 | } |
474 | } | |
475 | ||
476 | /// Moves all footnote definitions to the end and add back links to the | |
477 | /// references. | |
478 | struct Footnotes<'a, I: Iterator<Item = Event<'a>>> { | |
479 | inner: I, | |
b7449926 | 480 | footnotes: FxHashMap<String, (Vec<Event<'a>>, u16)>, |
cc61c64b XL |
481 | } |
482 | ||
483 | impl<'a, I: Iterator<Item = Event<'a>>> Footnotes<'a, I> { | |
484 | fn new(iter: I) -> Self { | |
485 | Footnotes { | |
486 | inner: iter, | |
b7449926 | 487 | footnotes: FxHashMap::default(), |
cc61c64b XL |
488 | } |
489 | } | |
490 | fn get_entry(&mut self, key: &str) -> &mut (Vec<Event<'a>>, u16) { | |
491 | let new_id = self.footnotes.keys().count() + 1; | |
492 | let key = key.to_owned(); | |
493 | self.footnotes.entry(key).or_insert((Vec::new(), new_id as u16)) | |
494 | } | |
495 | } | |
496 | ||
497 | impl<'a, I: Iterator<Item = Event<'a>>> Iterator for Footnotes<'a, I> { | |
498 | type Item = Event<'a>; | |
499 | ||
500 | fn next(&mut self) -> Option<Self::Item> { | |
501 | loop { | |
502 | match self.inner.next() { | |
503 | Some(Event::FootnoteReference(ref reference)) => { | |
504 | let entry = self.get_entry(&reference); | |
abe05a73 | 505 | let reference = format!("<sup id=\"fnref{0}\"><a href=\"#fn{0}\">{0}\ |
cc61c64b XL |
506 | </a></sup>", |
507 | (*entry).1); | |
508 | return Some(Event::Html(reference.into())); | |
509 | } | |
510 | Some(Event::Start(Tag::FootnoteDefinition(def))) => { | |
511 | let mut content = Vec::new(); | |
512 | for event in &mut self.inner { | |
513 | if let Event::End(Tag::FootnoteDefinition(..)) = event { | |
514 | break; | |
515 | } | |
516 | content.push(event); | |
517 | } | |
518 | let entry = self.get_entry(&def); | |
519 | (*entry).0 = content; | |
520 | } | |
521 | Some(e) => return Some(e), | |
522 | None => { | |
523 | if !self.footnotes.is_empty() { | |
524 | let mut v: Vec<_> = self.footnotes.drain().map(|(_, x)| x).collect(); | |
525 | v.sort_by(|a, b| a.1.cmp(&b.1)); | |
526 | let mut ret = String::from("<div class=\"footnotes\"><hr><ol>"); | |
527 | for (mut content, id) in v { | |
abe05a73 | 528 | write!(ret, "<li id=\"fn{}\">", id).unwrap(); |
cc61c64b XL |
529 | let mut is_paragraph = false; |
530 | if let Some(&Event::End(Tag::Paragraph)) = content.last() { | |
531 | content.pop(); | |
532 | is_paragraph = true; | |
533 | } | |
534 | html::push_html(&mut ret, content.into_iter()); | |
535 | write!(ret, | |
abe05a73 | 536 | " <a href=\"#fnref{}\" rev=\"footnote\">↩</a>", |
cc61c64b XL |
537 | id).unwrap(); |
538 | if is_paragraph { | |
539 | ret.push_str("</p>"); | |
540 | } | |
541 | ret.push_str("</li>"); | |
542 | } | |
543 | ret.push_str("</ol></div>"); | |
544 | return Some(Event::Html(ret.into())); | |
545 | } else { | |
546 | return None; | |
547 | } | |
548 | } | |
549 | } | |
550 | } | |
551 | } | |
552 | } | |
1a4d82fc | 553 | |
e1599b0c XL |
554 | pub fn find_testable_code<T: test::Tester>(doc: &str, tests: &mut T, error_codes: ErrorCodes, |
555 | enable_per_target_ignores: bool) { | |
cc61c64b XL |
556 | let mut parser = Parser::new(doc); |
557 | let mut prev_offset = 0; | |
558 | let mut nb_lines = 0; | |
559 | let mut register_header = None; | |
48663c56 | 560 | while let Some(event) = parser.next() { |
cc61c64b XL |
561 | match event { |
562 | Event::Start(Tag::CodeBlock(s)) => { | |
48663c56 XL |
563 | let offset = parser.get_offset(); |
564 | ||
cc61c64b XL |
565 | let block_info = if s.is_empty() { |
566 | LangString::all_false() | |
567 | } else { | |
e1599b0c | 568 | LangString::parse(&*s, error_codes, enable_per_target_ignores) |
cc61c64b XL |
569 | }; |
570 | if !block_info.rust { | |
48663c56 | 571 | continue; |
cc61c64b XL |
572 | } |
573 | let mut test_s = String::new(); | |
48663c56 XL |
574 | |
575 | while let Some(Event::Text(s)) = parser.next() { | |
576 | test_s.push_str(&s); | |
2c00a5a8 | 577 | } |
48663c56 XL |
578 | |
579 | let text = test_s | |
580 | .lines() | |
581 | .map(|l| map_line(l).for_code()) | |
582 | .collect::<Vec<Cow<'_, str>>>() | |
583 | .join("\n"); | |
584 | nb_lines += doc[prev_offset..offset].lines().count(); | |
585 | let line = tests.get_line() + nb_lines; | |
586 | tests.add_test(text, block_info, line); | |
587 | prev_offset = offset; | |
cc61c64b XL |
588 | } |
589 | Event::Start(Tag::Header(level)) => { | |
590 | register_header = Some(level as u32); | |
591 | } | |
592 | Event::Text(ref s) if register_header.is_some() => { | |
593 | let level = register_header.unwrap(); | |
594 | if s.is_empty() { | |
595 | tests.register_header("", level); | |
596 | } else { | |
597 | tests.register_header(s, level); | |
598 | } | |
599 | register_header = None; | |
600 | } | |
601 | _ => {} | |
602 | } | |
603 | } | |
604 | } | |
605 | ||
85aaf69f | 606 | #[derive(Eq, PartialEq, Clone, Debug)] |
b7449926 | 607 | pub struct LangString { |
8bb4bdeb | 608 | original: String, |
b7449926 XL |
609 | pub should_panic: bool, |
610 | pub no_run: bool, | |
e1599b0c | 611 | pub ignore: Ignore, |
b7449926 XL |
612 | pub rust: bool, |
613 | pub test_harness: bool, | |
614 | pub compile_fail: bool, | |
615 | pub error_codes: Vec<String>, | |
616 | pub allow_fail: bool, | |
0bf4aa26 | 617 | pub edition: Option<Edition> |
1a4d82fc JJ |
618 | } |
619 | ||
e1599b0c XL |
620 | #[derive(Eq, PartialEq, Clone, Debug)] |
621 | pub enum Ignore { | |
622 | All, | |
623 | None, | |
624 | Some(Vec<String>), | |
625 | } | |
626 | ||
1a4d82fc JJ |
627 | impl LangString { |
628 | fn all_false() -> LangString { | |
629 | LangString { | |
8bb4bdeb | 630 | original: String::new(), |
c34b1796 | 631 | should_panic: false, |
1a4d82fc | 632 | no_run: false, |
e1599b0c | 633 | ignore: Ignore::None, |
1a4d82fc JJ |
634 | rust: true, // NB This used to be `notrust = false` |
635 | test_harness: false, | |
7453a54e | 636 | compile_fail: false, |
3157f602 | 637 | error_codes: Vec::new(), |
041b39d2 | 638 | allow_fail: false, |
0bf4aa26 | 639 | edition: None, |
1a4d82fc JJ |
640 | } |
641 | } | |
642 | ||
e1599b0c XL |
643 | fn parse( |
644 | string: &str, | |
645 | allow_error_code_check: ErrorCodes, | |
646 | enable_per_target_ignores: bool | |
647 | ) -> LangString { | |
b7449926 | 648 | let allow_error_code_check = allow_error_code_check.as_bool(); |
1a4d82fc JJ |
649 | let mut seen_rust_tags = false; |
650 | let mut seen_other_tags = false; | |
651 | let mut data = LangString::all_false(); | |
e1599b0c | 652 | let mut ignores = vec![]; |
1a4d82fc | 653 | |
8bb4bdeb | 654 | data.original = string.to_owned(); |
85aaf69f | 655 | let tokens = string.split(|c: char| |
1a4d82fc JJ |
656 | !(c == '_' || c == '-' || c.is_alphanumeric()) |
657 | ); | |
658 | ||
659 | for token in tokens { | |
cc61c64b | 660 | match token.trim() { |
1a4d82fc | 661 | "" => {}, |
cc61c64b XL |
662 | "should_panic" => { |
663 | data.should_panic = true; | |
664 | seen_rust_tags = seen_other_tags == false; | |
665 | } | |
666 | "no_run" => { data.no_run = true; seen_rust_tags = !seen_other_tags; } | |
e1599b0c XL |
667 | "ignore" => { data.ignore = Ignore::All; seen_rust_tags = !seen_other_tags; } |
668 | x if x.starts_with("ignore-") => if enable_per_target_ignores { | |
669 | ignores.push(x.trim_start_matches("ignore-").to_owned()); | |
670 | seen_rust_tags = !seen_other_tags; | |
671 | } | |
041b39d2 | 672 | "allow_fail" => { data.allow_fail = true; seen_rust_tags = !seen_other_tags; } |
cc61c64b XL |
673 | "rust" => { data.rust = true; seen_rust_tags = true; } |
674 | "test_harness" => { | |
675 | data.test_harness = true; | |
676 | seen_rust_tags = !seen_other_tags || seen_rust_tags; | |
677 | } | |
ea8adc8c | 678 | "compile_fail" => { |
7453a54e | 679 | data.compile_fail = true; |
cc61c64b | 680 | seen_rust_tags = !seen_other_tags || seen_rust_tags; |
7453a54e | 681 | data.no_run = true; |
3157f602 | 682 | } |
60c5eb7d | 683 | x if x.starts_with("edition") => { |
0bf4aa26 XL |
684 | data.edition = x[7..].parse::<Edition>().ok(); |
685 | } | |
3157f602 | 686 | x if allow_error_code_check && x.starts_with("E") && x.len() == 5 => { |
b7449926 | 687 | if x[1..].parse::<u32>().is_ok() { |
3157f602 | 688 | data.error_codes.push(x.to_owned()); |
cc61c64b | 689 | seen_rust_tags = !seen_other_tags || seen_rust_tags; |
3157f602 XL |
690 | } else { |
691 | seen_other_tags = true; | |
692 | } | |
693 | } | |
1a4d82fc JJ |
694 | _ => { seen_other_tags = true } |
695 | } | |
696 | } | |
e1599b0c XL |
697 | // ignore-foo overrides ignore |
698 | if !ignores.is_empty() { | |
699 | data.ignore = Ignore::Some(ignores); | |
700 | } | |
1a4d82fc JJ |
701 | |
702 | data.rust &= !seen_other_tags || seen_rust_tags; | |
703 | ||
704 | data | |
705 | } | |
706 | } | |
707 | ||
416331ca XL |
708 | impl Markdown<'_> { |
709 | pub fn to_string(self) -> String { | |
710 | let Markdown(md, links, mut ids, codes, edition, playground) = self; | |
cc61c64b | 711 | |
1a4d82fc | 712 | // This is actually common enough to special-case |
416331ca | 713 | if md.is_empty() { return String::new(); } |
0531ce1d XL |
714 | let replacer = |_: &str, s: &str| { |
715 | if let Some(&(_, ref replace)) = links.into_iter().find(|link| &*link.0 == s) { | |
716 | Some((replace.clone(), s.to_owned())) | |
717 | } else { | |
718 | None | |
719 | } | |
720 | }; | |
cc61c64b | 721 | |
48663c56 | 722 | let p = Parser::new_with_broken_link_callback(md, opts(), Some(&replacer)); |
cc61c64b | 723 | |
0531ce1d | 724 | let mut s = String::with_capacity(md.len() * 3 / 2); |
cc61c64b | 725 | |
b7449926 XL |
726 | let p = HeadingLinks::new(p, None, &mut ids); |
727 | let p = LinkReplacer::new(p, links); | |
416331ca | 728 | let p = CodeBlocks::new(p, codes, edition, playground); |
b7449926 XL |
729 | let p = Footnotes::new(p); |
730 | html::push_html(&mut s, p); | |
0531ce1d | 731 | |
416331ca | 732 | s |
1a4d82fc JJ |
733 | } |
734 | } | |
735 | ||
416331ca XL |
736 | impl MarkdownWithToc<'_> { |
737 | pub fn to_string(self) -> String { | |
738 | let MarkdownWithToc(md, mut ids, codes, edition, playground) = self; | |
cc61c64b | 739 | |
48663c56 | 740 | let p = Parser::new_ext(md, opts()); |
cc61c64b | 741 | |
0531ce1d | 742 | let mut s = String::with_capacity(md.len() * 3 / 2); |
cc61c64b | 743 | |
0531ce1d | 744 | let mut toc = TocBuilder::new(); |
cc61c64b | 745 | |
b7449926 XL |
746 | { |
747 | let p = HeadingLinks::new(p, Some(&mut toc), &mut ids); | |
416331ca | 748 | let p = CodeBlocks::new(p, codes, edition, playground); |
b7449926 XL |
749 | let p = Footnotes::new(p); |
750 | html::push_html(&mut s, p); | |
751 | } | |
cc61c64b | 752 | |
e74abb32 | 753 | format!("<nav id=\"TOC\">{}</nav>{}", toc.into_toc().print(), s) |
32a655c1 SL |
754 | } |
755 | } | |
756 | ||
416331ca XL |
757 | impl MarkdownHtml<'_> { |
758 | pub fn to_string(self) -> String { | |
759 | let MarkdownHtml(md, mut ids, codes, edition, playground) = self; | |
cc61c64b | 760 | |
32a655c1 | 761 | // This is actually common enough to special-case |
416331ca | 762 | if md.is_empty() { return String::new(); } |
48663c56 | 763 | let p = Parser::new_ext(md, opts()); |
cc61c64b | 764 | |
0531ce1d XL |
765 | // Treat inline HTML as plain text. |
766 | let p = p.map(|event| match event { | |
767 | Event::Html(text) | Event::InlineHtml(text) => Event::Text(text), | |
768 | _ => event | |
769 | }); | |
cc61c64b | 770 | |
0531ce1d | 771 | let mut s = String::with_capacity(md.len() * 3 / 2); |
cc61c64b | 772 | |
b7449926 | 773 | let p = HeadingLinks::new(p, None, &mut ids); |
416331ca | 774 | let p = CodeBlocks::new(p, codes, edition, playground); |
b7449926 XL |
775 | let p = Footnotes::new(p); |
776 | html::push_html(&mut s, p); | |
cc61c64b | 777 | |
416331ca | 778 | s |
85aaf69f SL |
779 | } |
780 | } | |
781 | ||
416331ca XL |
782 | impl MarkdownSummaryLine<'_> { |
783 | pub fn to_string(self) -> String { | |
784 | let MarkdownSummaryLine(md, links) = self; | |
cc61c64b | 785 | // This is actually common enough to special-case |
416331ca | 786 | if md.is_empty() { return String::new(); } |
cc61c64b | 787 | |
0531ce1d XL |
788 | let replacer = |_: &str, s: &str| { |
789 | if let Some(&(_, ref replace)) = links.into_iter().find(|link| &*link.0 == s) { | |
790 | Some((replace.clone(), s.to_owned())) | |
791 | } else { | |
792 | None | |
793 | } | |
794 | }; | |
795 | ||
8faf50e0 | 796 | let p = Parser::new_with_broken_link_callback(md, Options::empty(), Some(&replacer)); |
cc61c64b XL |
797 | |
798 | let mut s = String::new(); | |
799 | ||
2c00a5a8 | 800 | html::push_html(&mut s, LinkReplacer::new(SummaryLine::new(p), links)); |
cc61c64b | 801 | |
416331ca | 802 | s |
85aaf69f | 803 | } |
cc61c64b | 804 | } |
85aaf69f | 805 | |
cc61c64b XL |
806 | pub fn plain_summary_line(md: &str) -> String { |
807 | struct ParserWrapper<'a> { | |
808 | inner: Parser<'a>, | |
809 | is_in: isize, | |
810 | is_first: bool, | |
85aaf69f SL |
811 | } |
812 | ||
cc61c64b XL |
813 | impl<'a> Iterator for ParserWrapper<'a> { |
814 | type Item = String; | |
85aaf69f | 815 | |
cc61c64b XL |
816 | fn next(&mut self) -> Option<String> { |
817 | let next_event = self.inner.next(); | |
818 | if next_event.is_none() { | |
819 | return None | |
820 | } | |
821 | let next_event = next_event.unwrap(); | |
822 | let (ret, is_in) = match next_event { | |
823 | Event::Start(Tag::Paragraph) => (None, 1), | |
cc61c64b | 824 | Event::Start(Tag::Header(_)) => (None, 1), |
48663c56 | 825 | Event::Code(code) => (Some(format!("`{}`", code)), 0), |
cc61c64b XL |
826 | Event::Text(ref s) if self.is_in > 0 => (Some(s.as_ref().to_owned()), 0), |
827 | Event::End(Tag::Paragraph) | Event::End(Tag::Header(_)) => (None, -1), | |
828 | _ => (None, 0), | |
829 | }; | |
830 | if is_in > 0 || (is_in < 0 && self.is_in > 0) { | |
831 | self.is_in += is_in; | |
832 | } | |
833 | if ret.is_some() { | |
834 | self.is_first = false; | |
835 | ret | |
836 | } else { | |
837 | Some(String::new()) | |
838 | } | |
839 | } | |
840 | } | |
841 | let mut s = String::with_capacity(md.len() * 3 / 2); | |
842 | let mut p = ParserWrapper { | |
843 | inner: Parser::new(md), | |
844 | is_in: 0, | |
845 | is_first: true, | |
846 | }; | |
847 | while let Some(t) = p.next() { | |
848 | if !t.is_empty() { | |
849 | s.push_str(&t); | |
850 | } | |
1a4d82fc | 851 | } |
e1599b0c | 852 | s |
1a4d82fc JJ |
853 | } |
854 | ||
94b46f34 | 855 | pub fn markdown_links(md: &str) -> Vec<(String, Option<Range<usize>>)> { |
2c00a5a8 XL |
856 | if md.is_empty() { |
857 | return vec![]; | |
858 | } | |
859 | ||
0531ce1d XL |
860 | let mut links = vec![]; |
861 | let shortcut_links = RefCell::new(vec![]); | |
2c00a5a8 | 862 | |
0531ce1d | 863 | { |
94b46f34 XL |
864 | let locate = |s: &str| unsafe { |
865 | let s_start = s.as_ptr(); | |
866 | let s_end = s_start.add(s.len()); | |
867 | let md_start = md.as_ptr(); | |
868 | let md_end = md_start.add(md.len()); | |
869 | if md_start <= s_start && s_end <= md_end { | |
870 | let start = s_start.offset_from(md_start) as usize; | |
871 | let end = s_end.offset_from(md_start) as usize; | |
872 | Some(start..end) | |
873 | } else { | |
874 | None | |
875 | } | |
876 | }; | |
877 | ||
0531ce1d | 878 | let push = |_: &str, s: &str| { |
94b46f34 | 879 | shortcut_links.borrow_mut().push((s.to_owned(), locate(s))); |
0531ce1d XL |
880 | None |
881 | }; | |
48663c56 | 882 | let p = Parser::new_with_broken_link_callback(md, opts(), Some(&push)); |
2c00a5a8 | 883 | |
b7449926 XL |
884 | // There's no need to thread an IdMap through to here because |
885 | // the IDs generated aren't going to be emitted anywhere. | |
886 | let mut ids = IdMap::new(); | |
887 | let iter = Footnotes::new(HeadingLinks::new(p, None, &mut ids)); | |
2c00a5a8 | 888 | |
0531ce1d | 889 | for ev in iter { |
48663c56 | 890 | if let Event::Start(Tag::Link(_, dest, _)) = ev { |
0531ce1d | 891 | debug!("found link: {}", dest); |
94b46f34 | 892 | links.push(match dest { |
48663c56 XL |
893 | CowStr::Borrowed(s) => (s.to_owned(), locate(s)), |
894 | s @ CowStr::Boxed(..) | s @ CowStr::Inlined(..) => (s.into_string(), None), | |
94b46f34 | 895 | }); |
2c00a5a8 XL |
896 | } |
897 | } | |
0531ce1d | 898 | } |
2c00a5a8 | 899 | |
0531ce1d XL |
900 | let mut shortcut_links = shortcut_links.into_inner(); |
901 | links.extend(shortcut_links.drain(..)); | |
2c00a5a8 | 902 | |
0531ce1d | 903 | links |
2c00a5a8 XL |
904 | } |
905 | ||
9fa01778 XL |
906 | #[derive(Debug)] |
907 | crate struct RustCodeBlock { | |
908 | /// The range in the markdown that the code block occupies. Note that this includes the fences | |
909 | /// for fenced code blocks. | |
910 | pub range: Range<usize>, | |
911 | /// The range in the markdown that the code within the code block occupies. | |
912 | pub code: Range<usize>, | |
913 | pub is_fenced: bool, | |
914 | pub syntax: Option<String>, | |
915 | } | |
916 | ||
917 | /// Returns a range of bytes for each code block in the markdown that is tagged as `rust` or | |
918 | /// untagged (and assumed to be rust). | |
919 | crate fn rust_code_blocks(md: &str) -> Vec<RustCodeBlock> { | |
920 | let mut code_blocks = vec![]; | |
921 | ||
922 | if md.is_empty() { | |
923 | return code_blocks; | |
924 | } | |
925 | ||
48663c56 | 926 | let mut p = Parser::new_ext(md, opts()); |
9fa01778 XL |
927 | |
928 | let mut code_block_start = 0; | |
929 | let mut code_start = 0; | |
930 | let mut is_fenced = false; | |
931 | let mut previous_offset = 0; | |
932 | let mut in_rust_code_block = false; | |
933 | while let Some(event) = p.next() { | |
934 | let offset = p.get_offset(); | |
935 | ||
936 | match event { | |
937 | Event::Start(Tag::CodeBlock(syntax)) => { | |
938 | let lang_string = if syntax.is_empty() { | |
939 | LangString::all_false() | |
940 | } else { | |
e1599b0c | 941 | LangString::parse(&*syntax, ErrorCodes::Yes, false) |
9fa01778 XL |
942 | }; |
943 | ||
944 | if lang_string.rust { | |
945 | in_rust_code_block = true; | |
946 | ||
947 | code_start = offset; | |
948 | code_block_start = match md[previous_offset..offset].find("```") { | |
949 | Some(fence_idx) => { | |
950 | is_fenced = true; | |
951 | previous_offset + fence_idx | |
952 | } | |
e1599b0c XL |
953 | None => { |
954 | is_fenced = false; | |
955 | offset | |
956 | } | |
9fa01778 XL |
957 | }; |
958 | } | |
959 | } | |
960 | Event::End(Tag::CodeBlock(syntax)) if in_rust_code_block => { | |
961 | in_rust_code_block = false; | |
962 | ||
963 | let code_block_end = if is_fenced { | |
964 | let fence_str = &md[previous_offset..offset] | |
965 | .chars() | |
966 | .rev() | |
967 | .collect::<String>(); | |
968 | fence_str | |
969 | .find("```") | |
970 | .map(|fence_idx| offset - fence_idx) | |
971 | .unwrap_or_else(|| offset) | |
972 | } else if md | |
973 | .as_bytes() | |
974 | .get(offset) | |
975 | .map(|b| *b == b'\n') | |
976 | .unwrap_or_default() | |
977 | { | |
978 | offset - 1 | |
979 | } else { | |
980 | offset | |
981 | }; | |
982 | ||
983 | let code_end = if is_fenced { | |
984 | previous_offset | |
985 | } else { | |
986 | code_block_end | |
987 | }; | |
988 | ||
989 | code_blocks.push(RustCodeBlock { | |
990 | is_fenced, | |
991 | range: Range { | |
992 | start: code_block_start, | |
993 | end: code_block_end, | |
994 | }, | |
995 | code: Range { | |
996 | start: code_start, | |
997 | end: code_end, | |
998 | }, | |
999 | syntax: if !syntax.is_empty() { | |
48663c56 | 1000 | Some(syntax.into_string()) |
9fa01778 XL |
1001 | } else { |
1002 | None | |
1003 | }, | |
1004 | }); | |
1005 | } | |
1006 | _ => (), | |
1007 | } | |
1008 | ||
1009 | previous_offset = offset; | |
1010 | } | |
1011 | ||
1012 | code_blocks | |
1013 | } | |
1014 | ||
a1dfa0c6 | 1015 | #[derive(Clone, Default, Debug)] |
b7449926 XL |
1016 | pub struct IdMap { |
1017 | map: FxHashMap<String, usize>, | |
1018 | } | |
1019 | ||
1020 | impl IdMap { | |
1021 | pub fn new() -> Self { | |
1022 | IdMap::default() | |
1023 | } | |
1024 | ||
1025 | pub fn populate<I: IntoIterator<Item=String>>(&mut self, ids: I) { | |
1026 | for id in ids { | |
1027 | let _ = self.derive(id); | |
1028 | } | |
1029 | } | |
1030 | ||
1031 | pub fn reset(&mut self) { | |
1032 | self.map = FxHashMap::default(); | |
1033 | } | |
1034 | ||
1035 | pub fn derive(&mut self, candidate: String) -> String { | |
1036 | let id = match self.map.get_mut(&candidate) { | |
1037 | None => candidate, | |
1038 | Some(a) => { | |
1039 | let id = format!("{}-{}", candidate, *a); | |
1040 | *a += 1; | |
1041 | id | |
1042 | } | |
1043 | }; | |
1044 | ||
1045 | self.map.insert(id.clone(), 1); | |
1046 | id | |
1047 | } | |
1048 | } |