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