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