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