]> git.proxmox.com Git - rustc.git/blame - src/librustdoc/html/markdown.rs
New upstream version 1.41.1+dfsg1
[rustc.git] / src / librustdoc / html / markdown.rs
CommitLineData
9fa01778 1//! Markdown formatting for rustdoc.
1a4d82fc 2//!
416331ca 3//! This module implements markdown formatting through the pulldown-cmark library.
1a4d82fc 4//!
041b39d2 5//! ```
ea8adc8c
XL
6//! #![feature(rustc_private)]
7//!
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 22use rustc_data_structures::fx::FxHashMap;
85aaf69f 23use std::cell::RefCell;
b7449926 24use std::collections::VecDeque;
9346a6ac 25use std::default::Default;
416331ca 26use std::fmt::Write;
94b46f34
XL
27use std::borrow::Cow;
28use std::ops::Range;
1a4d82fc 29use std::str;
0bf4aa26 30use syntax::edition::Edition;
1a4d82fc 31
9fa01778
XL
32use crate::html::toc::TocBuilder;
33use crate::html::highlight;
34use crate::test;
1a4d82fc 35
48663c56
XL
36use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag};
37
416331ca
XL
38#[cfg(test)]
39mod tests;
40
48663c56
XL
41fn 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 47pub 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.
60pub 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
68pub 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 76pub struct MarkdownSummaryLine<'a>(pub &'a str, pub &'a [(String, String)]);
cc61c64b 77
b7449926
XL
78#[derive(Copy, Clone, PartialEq, Debug)]
79pub enum ErrorCodes {
80 Yes,
81 No,
82}
83
84impl 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.
103enum Line<'a> {
104 Hidden(&'a str),
8faf50e0 105 Shown(Cow<'a, str>),
7cac9316
XL
106}
107
108impl<'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 129fn 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"
147fn 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)]
162pub 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 168struct 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
177impl<'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 193impl<'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 "&amp;version=nightly"
ea8adc8c 245 } else {
416331ca 246 ""
ea8adc8c 247 };
0bf4aa26 248
416331ca
XL
249 let edition_string = format!("&amp;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
312struct LinkReplacer<'a, 'b, I: Iterator<Item = Event<'a>>> {
313 inner: I,
0531ce1d 314 links: &'b [(String, String)],
2c00a5a8
XL
315}
316
317impl<'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
326impl<'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 344struct 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
351impl<'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 362impl<'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.
405struct SummaryLine<'a, I: Iterator<Item = Event<'a>>> {
406 inner: I,
407 started: bool,
408 depth: u32,
409}
410
411impl<'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 421fn 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
433impl<'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.
478struct Footnotes<'a, I: Iterator<Item = Event<'a>>> {
479 inner: I,
b7449926 480 footnotes: FxHashMap<String, (Vec<Event<'a>>, u16)>,
cc61c64b
XL
481}
482
483impl<'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
497impl<'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 "&nbsp;<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
554pub 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 607pub 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)]
621pub enum Ignore {
622 All,
623 None,
624 Some(Vec<String>),
625}
626
1a4d82fc
JJ
627impl 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
708impl 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
736impl 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
757impl 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
782impl 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
806pub 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 855pub 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)]
907crate 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).
919crate 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
1016pub struct IdMap {
1017 map: FxHashMap<String, usize>,
1018}
1019
1020impl 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}