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