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