1 //! Markdown formatting for rustdoc.
3 //! This module implements markdown formatting through the pulldown-cmark library.
6 //! #![feature(rustc_private)]
8 //! extern crate rustc_span;
10 //! use rustc_span::edition::Edition;
11 //! use rustdoc::html::markdown::{HeadingOffset, IdMap, Markdown, ErrorCodes};
13 //! let s = "My *markdown* _text_";
14 //! let mut id_map = IdMap::new();
15 //! let md = Markdown {
19 //! error_codes: ErrorCodes::Yes,
20 //! edition: Edition::Edition2015,
21 //! playground: &None,
22 //! heading_offset: HeadingOffset::H2,
23 //! custom_code_classes_in_docs: true,
25 //! let html = md.into_string();
26 //! // ... something using html
29 use rustc_data_structures
::fx
::FxHashMap
;
30 use rustc_errors
::{DiagnosticBuilder, DiagnosticMessage}
;
31 use rustc_hir
::def_id
::DefId
;
32 use rustc_middle
::ty
::TyCtxt
;
33 pub(crate) use rustc_resolve
::rustdoc
::main_body_opts
;
34 use rustc_resolve
::rustdoc
::may_be_doc_link
;
35 use rustc_span
::edition
::Edition
;
36 use rustc_span
::{Span, Symbol}
;
38 use once_cell
::sync
::Lazy
;
40 use std
::collections
::VecDeque
;
42 use std
::iter
::Peekable
;
43 use std
::ops
::{ControlFlow, Range}
;
44 use std
::str::{self, CharIndices}
;
46 use crate::clean
::RenderedLink
;
48 use crate::html
::escape
::Escape
;
49 use crate::html
::format
::Buffer
;
50 use crate::html
::highlight
;
51 use crate::html
::length_limit
::HtmlWithLimit
;
52 use crate::html
::render
::small_url_encode
;
53 use crate::html
::toc
::TocBuilder
;
56 html
, BrokenLink
, CodeBlockKind
, CowStr
, Event
, LinkType
, OffsetIter
, Options
, Parser
, Tag
,
62 const MAX_HEADER_LEVEL
: u32 = 6;
64 /// Options for rendering Markdown in summaries (e.g., in search results).
65 pub(crate) fn summary_opts() -> Options
{
66 Options
::ENABLE_TABLES
67 | Options
::ENABLE_FOOTNOTES
68 | Options
::ENABLE_STRIKETHROUGH
69 | Options
::ENABLE_TASKLISTS
70 | Options
::ENABLE_SMART_PUNCTUATION
73 #[derive(Debug, Clone, Copy)]
74 pub enum HeadingOffset
{
83 /// When `to_string` is called, this struct will emit the HTML corresponding to
84 /// the rendered version of the contained markdown string.
85 pub struct Markdown
<'a
> {
87 /// A list of link replacements.
88 pub links
: &'a
[RenderedLink
],
89 /// The current list of used header IDs.
90 pub ids
: &'a
mut IdMap
,
91 /// Whether to allow the use of explicit error codes in doctest lang strings.
92 pub error_codes
: ErrorCodes
,
93 /// Default edition to use when parsing doctests (to add a `fn main`).
95 pub playground
: &'a Option
<Playground
>,
96 /// Offset at which we render headings.
97 /// E.g. if `heading_offset: HeadingOffset::H2`, then `# something` renders an `<h2>`.
98 pub heading_offset
: HeadingOffset
,
99 /// `true` if the `custom_code_classes_in_docs` feature is enabled.
100 pub custom_code_classes_in_docs
: bool
,
102 /// A struct like `Markdown` that renders the markdown with a table of contents.
103 pub(crate) struct MarkdownWithToc
<'a
> {
104 pub(crate) content
: &'a
str,
105 pub(crate) ids
: &'a
mut IdMap
,
106 pub(crate) error_codes
: ErrorCodes
,
107 pub(crate) edition
: Edition
,
108 pub(crate) playground
: &'a Option
<Playground
>,
109 /// `true` if the `custom_code_classes_in_docs` feature is enabled.
110 pub(crate) custom_code_classes_in_docs
: bool
,
112 /// A tuple struct like `Markdown` that renders the markdown escaping HTML tags
113 /// and includes no paragraph tags.
114 pub(crate) struct MarkdownItemInfo
<'a
>(pub(crate) &'a
str, pub(crate) &'a
mut IdMap
);
115 /// A tuple struct like `Markdown` that renders only the first paragraph.
116 pub(crate) struct MarkdownSummaryLine
<'a
>(pub &'a
str, pub &'a
[RenderedLink
]);
118 #[derive(Copy, Clone, PartialEq, Debug)]
119 pub enum ErrorCodes
{
125 pub(crate) fn from(b
: bool
) -> Self {
127 true => ErrorCodes
::Yes
,
128 false => ErrorCodes
::No
,
132 pub(crate) fn as_bool(self) -> bool
{
134 ErrorCodes
::Yes
=> true,
135 ErrorCodes
::No
=> false,
140 /// Controls whether a line will be hidden or shown in HTML output.
142 /// All lines are used in documentation tests.
149 fn for_html(self) -> Option
<Cow
<'a
, str>> {
151 Line
::Shown(l
) => Some(l
),
152 Line
::Hidden(_
) => None
,
156 fn for_code(self) -> Cow
<'a
, str> {
159 Line
::Hidden(l
) => Cow
::Borrowed(l
),
164 // FIXME: There is a minor inconsistency here. For lines that start with ##, we
165 // have no easy way of removing a potential single space after the hashes, which
166 // is done in the single # case. This inconsistency seems okay, if non-ideal. In
167 // order to fix it we'd have to iterate to find the first non-# character, and
168 // then reallocate to remove it; which would make us return a String.
169 fn map_line(s
: &str) -> Line
<'_
> {
170 let trimmed
= s
.trim();
171 if trimmed
.starts_with("##") {
172 Line
::Shown(Cow
::Owned(s
.replacen("##", "#", 1)))
173 } else if let Some(stripped
) = trimmed
.strip_prefix("# ") {
175 Line
::Hidden(stripped
)
176 } else if trimmed
== "#" {
177 // We cannot handle '#text' because it could be #[attr].
180 Line
::Shown(Cow
::Borrowed(s
))
184 /// Convert chars from a title for an id.
186 /// "Hello, world!" -> "hello-world"
187 fn slugify(c
: char) -> Option
<char> {
188 if c
.is_alphanumeric() || c
== '
-'
|| c
== '_'
{
189 if c
.is_ascii() { Some(c.to_ascii_lowercase()) }
else { Some(c) }
190 } else if c
.is_whitespace() && c
.is_ascii() {
197 #[derive(Clone, Debug)]
198 pub struct Playground
{
199 pub crate_name
: Option
<Symbol
>,
203 /// Adds syntax highlighting and playground Run buttons to Rust code blocks.
204 struct CodeBlocks
<'p
, 'a
, I
: Iterator
<Item
= Event
<'a
>>> {
206 check_error_codes
: ErrorCodes
,
208 // Information about the playground if a URL has been specified, containing an
209 // optional crate name and the URL.
210 playground
: &'p Option
<Playground
>,
211 custom_code_classes_in_docs
: bool
,
214 impl<'p
, 'a
, I
: Iterator
<Item
= Event
<'a
>>> CodeBlocks
<'p
, 'a
, I
> {
217 error_codes
: ErrorCodes
,
219 playground
: &'p Option
<Playground
>,
220 custom_code_classes_in_docs
: bool
,
224 check_error_codes
: error_codes
,
227 custom_code_classes_in_docs
,
232 impl<'a
, I
: Iterator
<Item
= Event
<'a
>>> Iterator
for CodeBlocks
<'_
, 'a
, I
> {
233 type Item
= Event
<'a
>;
235 fn next(&mut self) -> Option
<Self::Item
> {
236 let event
= self.inner
.next();
237 let Some(Event
::Start(Tag
::CodeBlock(kind
))) = event
else {
241 let mut original_text
= String
::new();
242 for event
in &mut self.inner
{
244 Event
::End(Tag
::CodeBlock(..)) => break,
245 Event
::Text(ref s
) => {
246 original_text
.push_str(s
);
252 let LangString { added_classes, compile_fail, should_panic, ignore, edition, .. }
=
254 CodeBlockKind
::Fenced(ref lang
) => {
255 let parse_result
= LangString
::parse_without_check(
257 self.check_error_codes
,
259 self.custom_code_classes_in_docs
,
261 if !parse_result
.rust
{
262 let added_classes
= parse_result
.added_classes
;
263 let lang_string
= if let Some(lang
) = parse_result
.unknown
.first() {
264 format
!("language-{}", lang
)
268 let whitespace
= if added_classes
.is_empty() { "" }
else { " " }
;
269 return Some(Event
::Html(
271 "<div class=\"example-wrap\">\
272 <pre class=\"{lang_string}{whitespace}{added_classes}\">\
276 added_classes
= added_classes
.join(" "),
277 text
= Escape(&original_text
),
284 CodeBlockKind
::Indented
=> Default
::default(),
287 let lines
= original_text
.lines().filter_map(|l
| map_line(l
).for_html());
288 let text
= lines
.intersperse("\n".into()).collect
::<String
>();
290 let explicit_edition
= edition
.is_some();
291 let edition
= edition
.unwrap_or(self.edition
);
293 let playground_button
= self.playground
.as_ref().and_then(|playground
| {
294 let krate
= &playground
.crate_name
;
295 let url
= &playground
.url
;
299 let test
= original_text
301 .map(|l
| map_line(l
).for_code())
302 .intersperse("\n".into())
303 .collect
::<String
>();
304 let krate
= krate
.as_ref().map(|s
| s
.as_str());
306 doctest
::make_test(&test
, krate
, false, &Default
::default(), edition
, None
);
307 let channel
= if test
.contains("#![feature(") { "&version=nightly" }
else { "" }
;
309 let test_escaped
= small_url_encode(test
);
311 "<a class=\"test-arrow\" \
313 href=\"{url}?code={test_escaped}{channel}&edition={edition}\">Run</a>",
317 let tooltip
= if ignore
!= Ignore
::None
{
318 highlight
::Tooltip
::Ignore
319 } else if compile_fail
{
320 highlight
::Tooltip
::CompileFail
321 } else if should_panic
{
322 highlight
::Tooltip
::ShouldPanic
323 } else if explicit_edition
{
324 highlight
::Tooltip
::Edition(edition
)
326 highlight
::Tooltip
::None
329 // insert newline to clearly separate it from the
330 // previous block so we can shorten the html output
331 let mut s
= Buffer
::new();
334 highlight
::render_example_with_highlighting(
338 playground_button
.as_deref(),
341 Some(Event
::Html(s
.into_inner().into()))
345 /// Make headings links with anchor IDs and build up TOC.
346 struct LinkReplacer
<'a
, I
: Iterator
<Item
= Event
<'a
>>> {
348 links
: &'a
[RenderedLink
],
349 shortcut_link
: Option
<&'a RenderedLink
>,
352 impl<'a
, I
: Iterator
<Item
= Event
<'a
>>> LinkReplacer
<'a
, I
> {
353 fn new(iter
: I
, links
: &'a
[RenderedLink
]) -> Self {
354 LinkReplacer { inner: iter, links, shortcut_link: None }
358 impl<'a
, I
: Iterator
<Item
= Event
<'a
>>> Iterator
for LinkReplacer
<'a
, I
> {
359 type Item
= Event
<'a
>;
361 fn next(&mut self) -> Option
<Self::Item
> {
362 let mut event
= self.inner
.next();
364 // Replace intra-doc links and remove disambiguators from shortcut links (`[fn@f]`).
366 // This is a shortcut link that was resolved by the broken_link_callback: `[fn@f]`
367 // Remove any disambiguator.
368 Some(Event
::Start(Tag
::Link(
369 // [fn@f] or [fn@f][]
370 LinkType
::ShortcutUnknown
| LinkType
::CollapsedUnknown
,
374 debug
!("saw start of shortcut link to {dest} with title {title}");
375 // If this is a shortcut link, it was resolved by the broken_link_callback.
376 // So the URL will already be updated properly.
377 let link
= self.links
.iter().find(|&link
| *link
.href
== **dest
);
378 // Since this is an external iterator, we can't replace the inner text just yet.
379 // Store that we saw a link so we know to replace it later.
380 if let Some(link
) = link
{
381 trace
!("it matched");
382 assert
!(self.shortcut_link
.is_none(), "shortcut links cannot be nested");
383 self.shortcut_link
= Some(link
);
384 if title
.is_empty() && !link
.tooltip
.is_empty() {
385 *title
= CowStr
::Borrowed(link
.tooltip
.as_ref());
389 // Now that we're done with the shortcut link, don't replace any more text.
390 Some(Event
::End(Tag
::Link(
391 LinkType
::ShortcutUnknown
| LinkType
::CollapsedUnknown
,
395 debug
!("saw end of shortcut link to {dest}");
396 if self.links
.iter().any(|link
| *link
.href
== **dest
) {
397 assert
!(self.shortcut_link
.is_some(), "saw closing link without opening tag");
398 self.shortcut_link
= None
;
401 // Handle backticks in inline code blocks, but only if we're in the middle of a shortcut link.
403 Some(Event
::Code(text
)) => {
404 trace
!("saw code {text}");
405 if let Some(link
) = self.shortcut_link
{
406 // NOTE: this only replaces if the code block is the *entire* text.
407 // If only part of the link has code highlighting, the disambiguator will not be removed.
409 // This is a limitation from `collect_intra_doc_links`: it passes a full link,
410 // and does not distinguish at all between code blocks.
411 // So we could never be sure we weren't replacing too much:
412 // [fn@my_`f`unc] is treated the same as [my_func()] in that pass.
414 // NOTE: .get(1..len() - 1) is to strip the backticks
415 if let Some(link
) = self.links
.iter().find(|l
| {
417 && Some(&**text
) == l
.original_text
.get(1..l
.original_text
.len() - 1)
419 debug
!("replacing {text} with {new_text}", new_text
= link
.new_text
);
420 *text
= CowStr
::Borrowed(&link
.new_text
);
424 // Replace plain text in links, but only in the middle of a shortcut link.
426 Some(Event
::Text(text
)) => {
427 trace
!("saw text {text}");
428 if let Some(link
) = self.shortcut_link
{
429 // NOTE: same limitations as `Event::Code`
430 if let Some(link
) = self
433 .find(|l
| l
.href
== link
.href
&& **text
== *l
.original_text
)
435 debug
!("replacing {text} with {new_text}", new_text
= link
.new_text
);
436 *text
= CowStr
::Borrowed(&link
.new_text
);
440 // If this is a link, but not a shortcut link,
441 // replace the URL, since the broken_link_callback was not called.
442 Some(Event
::Start(Tag
::Link(_
, dest
, title
))) => {
443 if let Some(link
) = self.links
.iter().find(|&link
| *link
.original_text
== **dest
) {
444 *dest
= CowStr
::Borrowed(link
.href
.as_ref());
445 if title
.is_empty() && !link
.tooltip
.is_empty() {
446 *title
= CowStr
::Borrowed(link
.tooltip
.as_ref());
450 // Anything else couldn't have been a valid Rust path, so no need to replace the text.
454 // Yield the modified event
459 /// Wrap HTML tables into `<div>` to prevent having the doc blocks width being too big.
460 struct TableWrapper
<'a
, I
: Iterator
<Item
= Event
<'a
>>> {
462 stored_events
: VecDeque
<Event
<'a
>>,
465 impl<'a
, I
: Iterator
<Item
= Event
<'a
>>> TableWrapper
<'a
, I
> {
466 fn new(iter
: I
) -> Self {
467 Self { inner: iter, stored_events: VecDeque::new() }
471 impl<'a
, I
: Iterator
<Item
= Event
<'a
>>> Iterator
for TableWrapper
<'a
, I
> {
472 type Item
= Event
<'a
>;
474 fn next(&mut self) -> Option
<Self::Item
> {
475 if let Some(first
) = self.stored_events
.pop_front() {
479 let event
= self.inner
.next()?
;
482 Event
::Start(Tag
::Table(t
)) => {
483 self.stored_events
.push_back(Event
::Start(Tag
::Table(t
)));
484 Event
::Html(CowStr
::Borrowed("<div>"))
486 Event
::End(Tag
::Table(t
)) => {
487 self.stored_events
.push_back(Event
::Html(CowStr
::Borrowed("</div>")));
488 Event
::End(Tag
::Table(t
))
495 type SpannedEvent
<'a
> = (Event
<'a
>, Range
<usize>);
497 /// Make headings links with anchor IDs and build up TOC.
498 struct HeadingLinks
<'a
, 'b
, 'ids
, I
> {
500 toc
: Option
<&'b
mut TocBuilder
>,
501 buf
: VecDeque
<SpannedEvent
<'a
>>,
502 id_map
: &'ids
mut IdMap
,
503 heading_offset
: HeadingOffset
,
506 impl<'a
, 'b
, 'ids
, I
> HeadingLinks
<'a
, 'b
, 'ids
, I
> {
509 toc
: Option
<&'b
mut TocBuilder
>,
510 ids
: &'ids
mut IdMap
,
511 heading_offset
: HeadingOffset
,
513 HeadingLinks { inner: iter, toc, buf: VecDeque::new(), id_map: ids, heading_offset }
517 impl<'a
, 'b
, 'ids
, I
: Iterator
<Item
= SpannedEvent
<'a
>>> Iterator
518 for HeadingLinks
<'a
, 'b
, 'ids
, I
>
520 type Item
= SpannedEvent
<'a
>;
522 fn next(&mut self) -> Option
<Self::Item
> {
523 if let Some(e
) = self.buf
.pop_front() {
527 let event
= self.inner
.next();
528 if let Some((Event
::Start(Tag
::Heading(level
, _
, _
)), _
)) = event
{
529 let mut id
= String
::new();
530 for event
in &mut self.inner
{
532 Event
::End(Tag
::Heading(..)) => break,
533 Event
::Start(Tag
::Link(_
, _
, _
)) | Event
::End(Tag
::Link(..)) => {}
534 Event
::Text(text
) | Event
::Code(text
) => {
535 id
.extend(text
.chars().filter_map(slugify
));
536 self.buf
.push_back(event
);
538 _
=> self.buf
.push_back(event
),
541 let id
= self.id_map
.derive(id
);
543 if let Some(ref mut builder
) = self.toc
{
544 let mut html_header
= String
::new();
545 html
::push_html(&mut html_header
, self.buf
.iter().map(|(ev
, _
)| ev
.clone()));
546 let sec
= builder
.push(level
as u32, html_header
, id
.clone());
547 self.buf
.push_front((Event
::Html(format
!("{sec} ").into()), 0..0));
551 std
::cmp
::min(level
as u32 + (self.heading_offset
as u32), MAX_HEADER_LEVEL
);
552 self.buf
.push_back((Event
::Html(format
!("</a></h{level}>").into()), 0..0));
554 let start_tags
= format
!(
555 "<h{level} id=\"{id}\">\
558 return Some((Event
::Html(start_tags
.into()), 0..0));
564 /// Extracts just the first paragraph.
565 struct SummaryLine
<'a
, I
: Iterator
<Item
= Event
<'a
>>> {
572 impl<'a
, I
: Iterator
<Item
= Event
<'a
>>> SummaryLine
<'a
, I
> {
573 fn new(iter
: I
) -> Self {
574 SummaryLine { inner: iter, started: false, depth: 0, skipped_tags: 0 }
578 fn check_if_allowed_tag(t
: &Tag
<'_
>) -> bool
{
590 fn is_forbidden_tag(t
: &Tag
<'_
>) -> bool
{
598 | Tag
::FootnoteDefinition(_
)
602 impl<'a
, I
: Iterator
<Item
= Event
<'a
>>> Iterator
for SummaryLine
<'a
, I
> {
603 type Item
= Event
<'a
>;
605 fn next(&mut self) -> Option
<Self::Item
> {
606 if self.started
&& self.depth
== 0 {
612 if let Some(event
) = self.inner
.next() {
613 let mut is_start
= true;
614 let is_allowed_tag
= match event
{
615 Event
::Start(ref c
) => {
616 if is_forbidden_tag(c
) {
617 self.skipped_tags
+= 1;
621 check_if_allowed_tag(c
)
623 Event
::End(ref c
) => {
624 if is_forbidden_tag(c
) {
625 self.skipped_tags
+= 1;
630 check_if_allowed_tag(c
)
632 Event
::FootnoteReference(_
) => {
633 self.skipped_tags
+= 1;
639 self.skipped_tags
+= 1;
641 return if !is_allowed_tag
{
643 Some(Event
::Start(Tag
::Paragraph
))
645 Some(Event
::End(Tag
::Paragraph
))
655 /// Moves all footnote definitions to the end and add back links to the
657 struct Footnotes
<'a
, I
> {
659 footnotes
: FxHashMap
<String
, (Vec
<Event
<'a
>>, u16)>,
662 impl<'a
, I
> Footnotes
<'a
, I
> {
663 fn new(iter
: I
) -> Self {
664 Footnotes { inner: iter, footnotes: FxHashMap::default() }
667 fn get_entry(&mut self, key
: &str) -> &mut (Vec
<Event
<'a
>>, u16) {
668 let new_id
= self.footnotes
.len() + 1;
669 let key
= key
.to_owned();
670 self.footnotes
.entry(key
).or_insert((Vec
::new(), new_id
as u16))
674 impl<'a
, I
: Iterator
<Item
= SpannedEvent
<'a
>>> Iterator
for Footnotes
<'a
, I
> {
675 type Item
= SpannedEvent
<'a
>;
677 fn next(&mut self) -> Option
<Self::Item
> {
679 match self.inner
.next() {
680 Some((Event
::FootnoteReference(ref reference
), range
)) => {
681 let entry
= self.get_entry(reference
);
682 let reference
= format
!(
683 "<sup id=\"fnref{0}\"><a href=\"#fn{0}\">{0}</a></sup>",
686 return Some((Event
::Html(reference
.into()), range
));
688 Some((Event
::Start(Tag
::FootnoteDefinition(def
)), _
)) => {
689 let mut content
= Vec
::new();
690 for (event
, _
) in &mut self.inner
{
691 if let Event
::End(Tag
::FootnoteDefinition(..)) = event
{
696 let entry
= self.get_entry(&def
);
697 (*entry
).0 = content
;
699 Some(e
) => return Some(e
),
701 if !self.footnotes
.is_empty() {
702 let mut v
: Vec
<_
> = self.footnotes
.drain().map(|(_
, x
)| x
).collect();
703 v
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
704 let mut ret
= String
::from("<div class=\"footnotes\"><hr><ol>");
705 for (mut content
, id
) in v
{
706 write
!(ret
, "<li id=\"fn{id}\">").unwrap();
707 let mut is_paragraph
= false;
708 if let Some(&Event
::End(Tag
::Paragraph
)) = content
.last() {
712 html
::push_html(&mut ret
, content
.into_iter());
713 write
!(ret
, " <a href=\"#fnref{id}\">↩</a>").unwrap();
715 ret
.push_str("</p>");
717 ret
.push_str("</li>");
719 ret
.push_str("</ol></div>");
720 return Some((Event
::Html(ret
.into()), 0..0));
730 pub(crate) fn find_testable_code
<T
: doctest
::Tester
>(
733 error_codes
: ErrorCodes
,
734 enable_per_target_ignores
: bool
,
735 extra_info
: Option
<&ExtraInfo
<'_
>>,
736 custom_code_classes_in_docs
: bool
,
742 enable_per_target_ignores
,
745 custom_code_classes_in_docs
,
749 pub(crate) fn find_codes
<T
: doctest
::Tester
>(
752 error_codes
: ErrorCodes
,
753 enable_per_target_ignores
: bool
,
754 extra_info
: Option
<&ExtraInfo
<'_
>>,
755 include_non_rust
: bool
,
756 custom_code_classes_in_docs
: bool
,
758 let mut parser
= Parser
::new(doc
).into_offset_iter();
759 let mut prev_offset
= 0;
760 let mut nb_lines
= 0;
761 let mut register_header
= None
;
762 while let Some((event
, offset
)) = parser
.next() {
764 Event
::Start(Tag
::CodeBlock(kind
)) => {
765 let block_info
= match kind
{
766 CodeBlockKind
::Fenced(ref lang
) => {
773 enable_per_target_ignores
,
775 custom_code_classes_in_docs
,
779 CodeBlockKind
::Indented
=> Default
::default(),
781 if !include_non_rust
&& !block_info
.rust
{
785 let mut test_s
= String
::new();
787 while let Some((Event
::Text(s
), _
)) = parser
.next() {
792 .map(|l
| map_line(l
).for_code())
793 .collect
::<Vec
<Cow
<'_
, str>>>()
796 nb_lines
+= doc
[prev_offset
..offset
.start
].lines().count();
797 // If there are characters between the preceding line ending and
798 // this code block, `str::lines` will return an additional line,
799 // which we subtract here.
800 if nb_lines
!= 0 && !&doc
[prev_offset
..offset
.start
].ends_with('
\n'
) {
803 let line
= tests
.get_line() + nb_lines
+ 1;
804 tests
.add_test(text
, block_info
, line
);
805 prev_offset
= offset
.start
;
807 Event
::Start(Tag
::Heading(level
, _
, _
)) => {
808 register_header
= Some(level
as u32);
810 Event
::Text(ref s
) if register_header
.is_some() => {
811 let level
= register_header
.unwrap();
812 tests
.register_header(s
, level
);
813 register_header
= None
;
820 pub(crate) struct ExtraInfo
<'tcx
> {
826 impl<'tcx
> ExtraInfo
<'tcx
> {
827 pub(crate) fn new(tcx
: TyCtxt
<'tcx
>, def_id
: DefId
, sp
: Span
) -> ExtraInfo
<'tcx
> {
828 ExtraInfo { def_id, sp, tcx }
831 fn error_invalid_codeblock_attr(&self, msg
: impl Into
<DiagnosticMessage
>) {
832 if let Some(def_id
) = self.def_id
.as_local() {
833 self.tcx
.struct_span_lint_hir(
834 crate::lint
::INVALID_CODEBLOCK_ATTRIBUTES
,
835 self.tcx
.local_def_id_to_hir_id(def_id
),
843 fn error_invalid_codeblock_attr_with_help(
845 msg
: impl Into
<DiagnosticMessage
>,
846 f
: impl for<'a
, 'b
> FnOnce(&'b
mut DiagnosticBuilder
<'a
, ()>),
848 if let Some(def_id
) = self.def_id
.as_local() {
849 self.tcx
.struct_span_lint_hir(
850 crate::lint
::INVALID_CODEBLOCK_ATTRIBUTES
,
851 self.tcx
.local_def_id_to_hir_id(def_id
),
860 #[derive(Eq, PartialEq, Clone, Debug)]
861 pub(crate) struct LangString
{
862 pub(crate) original
: String
,
863 pub(crate) should_panic
: bool
,
864 pub(crate) no_run
: bool
,
865 pub(crate) ignore
: Ignore
,
866 pub(crate) rust
: bool
,
867 pub(crate) test_harness
: bool
,
868 pub(crate) compile_fail
: bool
,
869 pub(crate) error_codes
: Vec
<String
>,
870 pub(crate) edition
: Option
<Edition
>,
871 pub(crate) added_classes
: Vec
<String
>,
872 pub(crate) unknown
: Vec
<String
>,
875 #[derive(Eq, PartialEq, Clone, Debug)]
876 pub(crate) enum Ignore
{
882 /// This is the parser for fenced codeblocks attributes. It implements the following eBNF:
885 /// lang-string = *(token-list / delimited-attribute-list / comment)
887 /// bareword = LEADINGCHAR *(CHAR)
888 /// bareword-without-leading-char = CHAR *(CHAR)
889 /// quoted-string = QUOTE *(NONQUOTE) QUOTE
890 /// token = bareword / quoted-string
891 /// token-without-leading-char = bareword-without-leading-char / quoted-string
892 /// sep = COMMA/WS *(COMMA/WS)
893 /// attribute = (DOT token)/(token EQUAL token-without-leading-char)
894 /// attribute-list = [sep] attribute *(sep attribute) [sep]
895 /// delimited-attribute-list = OPEN-CURLY-BRACKET attribute-list CLOSE-CURLY-BRACKET
896 /// token-list = [sep] token *(sep token) [sep]
897 /// comment = OPEN_PAREN *(all characters) CLOSE_PAREN
900 /// CLOSE_PARENT = ")"
901 /// OPEN-CURLY-BRACKET = "{"
902 /// CLOSE-CURLY-BRACKET = "}"
903 /// LEADINGCHAR = ALPHA | DIGIT | "_" | "-" | ":"
904 /// ; All ASCII punctuation except comma, quote, equals, backslash, grave (backquote) and braces.
905 /// ; Comma is used to separate language tokens, so it can't be used in one.
906 /// ; Quote is used to allow otherwise-disallowed characters in language tokens.
907 /// ; Equals is used to make key=value pairs in attribute blocks.
908 /// ; Backslash and grave are special Markdown characters.
909 /// ; Braces are used to start an attribute block.
910 /// CHAR = ALPHA | DIGIT | "_" | "-" | ":" | "." | "!" | "#" | "$" | "%" | "&" | "*" | "+" | "/" |
911 /// ";" | "<" | ">" | "?" | "@" | "^" | "|" | "~"
912 /// NONQUOTE = %x09 / %x20 / %x21 / %x23-7E ; TAB / SPACE / all printable characters except `"`
917 /// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
921 pub(crate) struct TagIterator
<'a
, 'tcx
> {
922 inner
: Peekable
<CharIndices
<'a
>>,
924 is_in_attribute_block
: bool
,
925 extra
: Option
<&'a ExtraInfo
<'tcx
>>,
928 #[derive(Clone, Debug, Eq, PartialEq)]
929 pub(crate) enum LangStringToken
<'a
> {
931 ClassAttribute(&'a
str),
932 KeyValueAttribute(&'a
str, &'a
str),
935 fn is_leading_char(c
: char) -> bool
{
936 c
== '_'
|| c
== '
-'
|| c
== '
:'
|| c
.is_ascii_alphabetic() || c
.is_ascii_digit()
938 fn is_bareword_char(c
: char) -> bool
{
939 is_leading_char(c
) || ".!#$%&*+/;<>?@^|~".contains(c
)
941 fn is_separator(c
: char) -> bool
{
942 c
== ' '
|| c
== '
,'
|| c
== '
\t'
950 impl<'a
, 'tcx
> TagIterator
<'a
, 'tcx
> {
951 pub(crate) fn new(data
: &'a
str, extra
: Option
<&'a ExtraInfo
<'tcx
>>) -> Self {
952 Self { inner: data.char_indices().peekable(), data, is_in_attribute_block: false, extra }
955 fn emit_error(&self, err
: impl Into
<DiagnosticMessage
>) {
956 if let Some(extra
) = self.extra
{
957 extra
.error_invalid_codeblock_attr(err
);
961 fn skip_separators(&mut self) -> Option
<usize> {
962 while let Some((pos
, c
)) = self.inner
.peek() {
963 if !is_separator(*c
) {
971 fn parse_string(&mut self, start
: usize) -> Option
<Indices
> {
972 while let Some((pos
, c
)) = self.inner
.next() {
974 return Some(Indices { start: start + 1, end: pos });
977 self.emit_error("unclosed quote string `
\"`
");
981 fn parse_class(&mut self, start: usize) -> Option<LangStringToken<'a>> {
982 while let Some((pos, c)) = self.inner.peek().copied() {
983 if is_bareword_char(c) {
986 let class = &self.data[start + 1..pos];
987 if class.is_empty() {
988 self.emit_error(format!("unexpected `{c}` character after `
.`
"));
990 } else if self.check_after_token() {
991 return Some(LangStringToken::ClassAttribute(class));
997 let class = &self.data[start + 1..];
998 if class.is_empty() {
999 self.emit_error("missing character after `
.`
");
1001 } else if self.check_after_token() {
1002 Some(LangStringToken::ClassAttribute(class))
1008 fn parse_token(&mut self, start: usize) -> Option<Indices> {
1009 while let Some((pos, c)) = self.inner.peek() {
1010 if !is_bareword_char(*c) {
1011 return Some(Indices { start, end: *pos });
1015 self.emit_error("unexpected end
");
1019 fn parse_key_value(&mut self, c: char, start: usize) -> Option<LangStringToken<'a>> {
1021 if c == '"' { self.parse_string(start)? }
else { self.parse_token(start)? }
;
1022 if key_indices
.start
== key_indices
.end
{
1023 self.emit_error("unexpected empty string as key");
1027 if let Some((_
, c
)) = self.inner
.next() {
1029 self.emit_error(format
!("expected `=`, found `{}`", c
));
1033 self.emit_error("unexpected end");
1036 let value_indices
= match self.inner
.next() {
1037 Some((pos
, '
"')) => self.parse_string(pos)?,
1038 Some((pos, c)) if is_bareword_char(c) => self.parse_token(pos)?,
1040 self.emit_error(format!("unexpected `{c}` character after `
=`
"));
1044 self.emit_error("expected value after `
=`
");
1048 if value_indices.start == value_indices.end {
1049 self.emit_error("unexpected empty string
as value
");
1051 } else if self.check_after_token() {
1052 Some(LangStringToken::KeyValueAttribute(
1053 &self.data[key_indices.start..key_indices.end],
1054 &self.data[value_indices.start..value_indices.end],
1061 /// Returns `false` if an error was emitted.
1062 fn check_after_token(&mut self) -> bool {
1063 if let Some((_, c)) = self.inner.peek().copied() {
1064 if c == '}' || is_separator(c) || c == '(' {
1067 self.emit_error(format!("unexpected `{c}` character
"));
1071 // The error will be caught on the next iteration.
1076 fn parse_in_attribute_block(&mut self) -> Option<LangStringToken<'a>> {
1077 if let Some((pos, c)) = self.inner.next() {
1079 self.is_in_attribute_block = false;
1081 } else if c == '.' {
1082 return self.parse_class(pos);
1083 } else if c == '"'
|| is_leading_char(c
) {
1084 return self.parse_key_value(c
, pos
);
1086 self.emit_error(format
!("unexpected character `{c}`"));
1090 self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
1094 /// Returns `false` if an error was emitted.
1095 fn skip_paren_block(&mut self) -> bool
{
1096 while let Some((_
, c
)) = self.inner
.next() {
1101 self.emit_error("unclosed comment: missing `)` at the end");
1105 fn parse_outside_attribute_block(&mut self, start
: usize) -> Option
<LangStringToken
<'a
>> {
1106 while let Some((pos
, c
)) = self.inner
.next() {
1109 self.emit_error("expected ` `
, `
{` or `
,` found `
\"`
");
1112 let indices = self.parse_string(pos)?;
1113 if let Some((_, c)) = self.inner.peek().copied()
1118 self.emit_error(format!("expected ` `
, `{{` or `,` after `\"`, found `{c}`
"));
1121 return Some(LangStringToken::LangToken(&self.data[indices.start..indices.end]));
1122 } else if c == '{' {
1123 self.is_in_attribute_block = true;
1125 } else if is_separator(c) {
1127 return Some(LangStringToken::LangToken(&self.data[start..pos]));
1130 } else if c == '(' {
1131 if !self.skip_paren_block() {
1135 return Some(LangStringToken::LangToken(&self.data[start..pos]));
1138 } else if pos == start && is_leading_char(c) {
1140 } else if pos != start && is_bareword_char(c) {
1143 self.emit_error(format!("unexpected character `{c}`
"));
1147 let token = &self.data[start..];
1148 if token.is_empty() { None } else { Some(LangStringToken::LangToken(&self.data[start..])) }
1152 impl<'a, 'tcx> Iterator for TagIterator<'a, 'tcx> {
1153 type Item = LangStringToken<'a>;
1155 fn next(&mut self) -> Option<Self::Item> {
1156 let Some(start) = self.skip_separators() else {
1157 if self.is_in_attribute_block {
1158 self.emit_error("unclosed attribute
block (`{}`
): missing `
}` at the end
");
1162 if self.is_in_attribute_block {
1163 self.parse_in_attribute_block()
1165 self.parse_outside_attribute_block(start)
1170 fn tokens(string: &str) -> impl Iterator<Item = LangStringToken<'_>> {
1171 // Pandoc, which Rust once used for generating documentation,
1172 // expects lang strings to be surrounded by `{}` and for each token
1173 // to be proceeded by a `.`. Since some of these lang strings are still
1174 // loose in the wild, we strip a pair of surrounding `{}` from the lang
1175 // string and a leading `.` from each token.
1177 let string = string.trim();
1179 let first = string.chars().next();
1180 let last = string.chars().last();
1183 if first == Some('{') && last == Some('}') { &string[1..string.len() - 1] } else { string };
1186 .split(|c| c == ',' || c == ' ' || c == '\t')
1188 .map(|token| token.strip_prefix('.').unwrap_or(token))
1189 .filter(|token| !token.is_empty())
1190 .map(|token| LangStringToken::LangToken(token))
1193 impl Default for LangString {
1194 fn default() -> Self {
1196 original: String::new(),
1197 should_panic: false,
1199 ignore: Ignore::None,
1201 test_harness: false,
1202 compile_fail: false,
1203 error_codes: Vec::new(),
1205 added_classes: Vec::new(),
1206 unknown: Vec::new(),
1212 fn parse_without_check(
1214 allow_error_code_check: ErrorCodes,
1215 enable_per_target_ignores: bool,
1216 custom_code_classes_in_docs: bool,
1220 allow_error_code_check,
1221 enable_per_target_ignores,
1223 custom_code_classes_in_docs,
1229 allow_error_code_check: ErrorCodes,
1230 enable_per_target_ignores: bool,
1231 extra: Option<&ExtraInfo<'_>>,
1232 custom_code_classes_in_docs: bool,
1234 let allow_error_code_check = allow_error_code_check.as_bool();
1235 let mut seen_rust_tags = false;
1236 let mut seen_other_tags = false;
1237 let mut seen_custom_tag = false;
1238 let mut data = LangString::default();
1239 let mut ignores = vec![];
1241 data.original = string.to_owned();
1243 let mut call = |tokens: &mut dyn Iterator<Item = LangStringToken<'_>>| {
1244 for token in tokens {
1246 LangStringToken::LangToken("should_panic
") => {
1247 data.should_panic = true;
1248 seen_rust_tags = !seen_other_tags;
1250 LangStringToken::LangToken("no_run
") => {
1252 seen_rust_tags = !seen_other_tags;
1254 LangStringToken::LangToken("ignore
") => {
1255 data.ignore = Ignore::All;
1256 seen_rust_tags = !seen_other_tags;
1258 LangStringToken::LangToken(x) if x.starts_with("ignore
-") => {
1259 if enable_per_target_ignores {
1260 ignores.push(x.trim_start_matches("ignore
-").to_owned());
1261 seen_rust_tags = !seen_other_tags;
1264 LangStringToken::LangToken("rust
") => {
1266 seen_rust_tags = true;
1268 LangStringToken::LangToken("custom
") => {
1269 if custom_code_classes_in_docs {
1270 seen_custom_tag = true;
1272 seen_other_tags = true;
1275 LangStringToken::LangToken("test_harness
") => {
1276 data.test_harness = true;
1277 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1279 LangStringToken::LangToken("compile_fail
") => {
1280 data.compile_fail = true;
1281 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1284 LangStringToken::LangToken(x) if x.starts_with("edition
") => {
1285 data.edition = x[7..].parse::<Edition>().ok();
1287 LangStringToken::LangToken(x)
1288 if x.starts_with("rust
") && x[4..].parse::<Edition>().is_ok() =>
1290 if let Some(extra) = extra {
1291 extra.error_invalid_codeblock_attr_with_help(
1292 format!("unknown attribute `{x}`
"),
1295 "there is an attribute with a similar name
: `edition{}`
",
1302 LangStringToken::LangToken(x)
1303 if allow_error_code_check && x.starts_with('E') && x.len() == 5 =>
1305 if x[1..].parse::<u32>().is_ok() {
1306 data.error_codes.push(x.to_owned());
1307 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1309 seen_other_tags = true;
1312 LangStringToken::LangToken(x) if extra.is_some() => {
1313 let s = x.to_lowercase();
1314 if let Some((flag, help)) = if s == "compile
-fail
"
1315 || s == "compile_fail
"
1316 || s == "compilefail
"
1320 "the code block will either not be tested
if not marked
as a rust one
\
1321 or won't fail
if it compiles successfully
",
1323 } else if s == "should
-panic
" || s == "should_panic
" || s == "shouldpanic
" {
1326 "the code block will either not be tested
if not marked
as a rust one
\
1327 or won't fail
if it doesn't panic when running
",
1329 } else if s == "no
-run
" || s == "no_run
" || s == "norun
" {
1332 "the code block will either not be tested
if not marked
as a rust one
\
1333 or will be
run (which you might not want
)",
1335 } else if s == "test
-harness
" || s == "test_harness
" || s == "testharness
" {
1338 "the code block will either not be tested
if not marked
as a rust one
\
1339 or the code will be wrapped inside a main function
",
1344 if let Some(extra) = extra {
1345 extra.error_invalid_codeblock_attr_with_help(
1346 format!("unknown attribute `{x}`
"),
1349 "there is an attribute with a similar name
: `{flag}`
"
1356 seen_other_tags = true;
1357 data.unknown.push(x.to_owned());
1359 LangStringToken::LangToken(x) => {
1360 seen_other_tags = true;
1361 data.unknown.push(x.to_owned());
1363 LangStringToken::KeyValueAttribute(key, value) => {
1364 if custom_code_classes_in_docs {
1366 data.added_classes.push(value.to_owned());
1367 } else if let Some(extra) = extra {
1368 extra.error_invalid_codeblock_attr(format!(
1369 "unsupported attribute `{key}`
"
1373 seen_other_tags = true;
1376 LangStringToken::ClassAttribute(class) => {
1377 data.added_classes.push(class.to_owned());
1383 if custom_code_classes_in_docs {
1384 call(&mut TagIterator::new(string, extra))
1386 call(&mut tokens(string))
1389 // ignore-foo overrides ignore
1390 if !ignores.is_empty() {
1391 data.ignore = Ignore::Some(ignores);
1394 data.rust &= !seen_custom_tag && (!seen_other_tags || seen_rust_tags);
1401 pub fn into_string(self) -> String {
1410 custom_code_classes_in_docs,
1413 // This is actually common enough to special-case
1415 return String::new();
1417 let mut replacer = |broken_link: BrokenLink<'_>| {
1420 .find(|link| &*link.original_text == &*broken_link.reference)
1421 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1424 let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
1425 let p = p.into_offset_iter();
1427 let mut s = String::with_capacity(md.len() * 3 / 2);
1429 let p = HeadingLinks::new(p, None, ids, heading_offset);
1430 let p = Footnotes::new(p);
1431 let p = LinkReplacer::new(p.map(|(ev, _)| ev), links);
1432 let p = TableWrapper::new(p);
1433 let p = CodeBlocks::new(p, codes, edition, playground, custom_code_classes_in_docs);
1434 html::push_html(&mut s, p);
1440 impl MarkdownWithToc<'_> {
1441 pub(crate) fn into_string(self) -> String {
1442 let MarkdownWithToc {
1448 custom_code_classes_in_docs,
1451 let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
1453 let mut s = String::with_capacity(md.len() * 3 / 2);
1455 let mut toc = TocBuilder::new();
1458 let p = HeadingLinks::new(p, Some(&mut toc), ids, HeadingOffset::H1);
1459 let p = Footnotes::new(p);
1460 let p = TableWrapper::new(p.map(|(ev, _)| ev));
1461 let p = CodeBlocks::new(p, codes, edition, playground, custom_code_classes_in_docs);
1462 html::push_html(&mut s, p);
1465 format!("<nav id
=\"TOC
\">{toc}
</nav
>{s}
", toc = toc.into_toc().print())
1469 impl MarkdownItemInfo<'_> {
1470 pub(crate) fn into_string(self) -> String {
1471 let MarkdownItemInfo(md, ids) = self;
1473 // This is actually common enough to special-case
1475 return String::new();
1477 let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
1479 // Treat inline HTML as plain text.
1480 let p = p.map(|event| match event.0 {
1481 Event::Html(text) => (Event::Text(text), event.1),
1485 let mut s = String::with_capacity(md.len() * 3 / 2);
1487 let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
1488 let p = Footnotes::new(p);
1489 let p = TableWrapper::new(p.map(|(ev, _)| ev));
1490 let p = p.filter(|event| {
1491 !matches!(event, Event::Start(Tag::Paragraph) | Event::End(Tag::Paragraph))
1493 html::push_html(&mut s, p);
1499 impl MarkdownSummaryLine<'_> {
1500 pub(crate) fn into_string_with_has_more_content(self) -> (String, bool) {
1501 let MarkdownSummaryLine(md, links) = self;
1502 // This is actually common enough to special-case
1504 return (String::new(), false);
1507 let mut replacer = |broken_link: BrokenLink<'_>| {
1510 .find(|link| &*link.original_text == &*broken_link.reference)
1511 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1514 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer))
1516 let mut summary = SummaryLine::new(p);
1518 let mut s = String::new();
1520 let without_paragraphs = LinkReplacer::new(&mut summary, links).filter(|event| {
1521 !matches!(event, Event::Start(Tag::Paragraph) | Event::End(Tag::Paragraph))
1524 html::push_html(&mut s, without_paragraphs);
1526 let has_more_content =
1527 matches!(summary.inner.peek(), Some(Event::Start(_))) || summary.skipped_tags > 0;
1529 (s, has_more_content)
1532 pub(crate) fn into_string(self) -> String {
1533 self.into_string_with_has_more_content().0
1537 /// Renders a subset of Markdown in the first paragraph of the provided Markdown.
1539 /// - *Italics*, **bold**, and `inline code` styles **are** rendered.
1540 /// - Headings and links are stripped (though the text *is* rendered).
1541 /// - HTML, code blocks, and everything else are ignored.
1543 /// Returns a tuple of the rendered HTML string and whether the output was shortened
1544 /// due to the provided `length_limit`.
1545 fn markdown_summary_with_limit(
1547 link_names: &[RenderedLink],
1548 length_limit: usize,
1549 ) -> (String, bool) {
1551 return (String::new(), false);
1554 let mut replacer = |broken_link: BrokenLink<'_>| {
1557 .find(|link| &*link.original_text == &*broken_link.reference)
1558 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1561 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
1562 let mut p = LinkReplacer::new(p, link_names);
1564 let mut buf = HtmlWithLimit::new(length_limit);
1565 let mut stopped_early = false;
1566 p.try_for_each(|event| {
1568 Event::Text(text) => {
1570 text.split_inclusive(char::is_whitespace).try_for_each(|word| buf.push(word));
1572 stopped_early = true;
1576 Event::Code(code) => {
1577 buf.open_tag("code
");
1578 let r = buf.push(code);
1580 stopped_early = true;
1586 Event::Start(tag) => match tag {
1587 Tag::Emphasis => buf.open_tag("em
"),
1588 Tag::Strong => buf.open_tag("strong
"),
1589 Tag::CodeBlock(..) => return ControlFlow::Break(()),
1592 Event::End(tag) => match tag {
1593 Tag::Emphasis | Tag::Strong => buf.close_tag(),
1594 Tag::Paragraph | Tag::Heading(..) => return ControlFlow::Break(()),
1597 Event::HardBreak | Event::SoftBreak => buf.push(" ")?,
1600 ControlFlow::Continue(())
1603 (buf.finish(), stopped_early)
1606 /// Renders a shortened first paragraph of the given Markdown as a subset of Markdown,
1607 /// making it suitable for contexts like the search index.
1609 /// Will shorten to 59 or 60 characters, including an ellipsis (…) if it was shortened.
1611 /// See [`markdown_summary_with_limit`] for details about what is rendered and what is not.
1612 pub(crate) fn short_markdown_summary(markdown: &str, link_names: &[RenderedLink]) -> String {
1613 let (mut s, was_shortened) = markdown_summary_with_limit(markdown, link_names, 59);
1622 /// Renders the first paragraph of the provided markdown as plain text.
1623 /// Useful for alt-text.
1625 /// - Headings, links, and formatting are stripped.
1626 /// - Inline code is rendered as-is, surrounded by backticks.
1627 /// - HTML and code blocks are ignored.
1628 pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> String {
1630 return String::new();
1633 let mut s = String::with_capacity(md.len() * 3 / 2);
1635 let mut replacer = |broken_link: BrokenLink<'_>| {
1638 .find(|link| &*link.original_text == &*broken_link.reference)
1639 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1642 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
1646 Event::Text(text) => s.push_str(text),
1647 Event::Code(code) => {
1652 Event::HardBreak | Event::SoftBreak => s.push(' '),
1653 Event::Start(Tag::CodeBlock(..)) => break,
1654 Event::End(Tag::Paragraph) => break,
1655 Event::End(Tag::Heading(..)) => break,
1664 pub(crate) struct MarkdownLink {
1667 pub display_text: Option<String>,
1668 pub range: MarkdownLinkRange,
1671 #[derive(Clone, Debug)]
1672 pub(crate) enum MarkdownLinkRange {
1673 /// Normally, markdown link warnings point only at the destination.
1674 Destination(Range<usize>),
1675 /// In some cases, it's not possible to point at the destination.
1676 /// Usually, this happens because backslashes `\\` are used.
1677 /// When that happens, point at the whole link, and don't provide structured suggestions.
1678 WholeLink(Range<usize>),
1681 impl MarkdownLinkRange {
1682 /// Extracts the inner range.
1683 pub fn inner_range(&self) -> &Range<usize> {
1685 MarkdownLinkRange::Destination(range) => range,
1686 MarkdownLinkRange::WholeLink(range) => range,
1691 pub(crate) fn markdown_links<'md, R>(
1693 preprocess_link: impl Fn(MarkdownLink) -> Option<R>,
1699 // FIXME: remove this function once pulldown_cmark can provide spans for link definitions.
1700 let locate = |s: &str, fallback: Range<usize>| unsafe {
1701 let s_start = s.as_ptr();
1702 let s_end = s_start.add(s.len());
1703 let md_start = md.as_ptr();
1704 let md_end = md_start.add(md.len());
1705 if md_start <= s_start && s_end <= md_end {
1706 let start = s_start.offset_from(md_start) as usize;
1707 let end = s_end.offset_from(md_start) as usize;
1708 MarkdownLinkRange::Destination(start..end)
1710 MarkdownLinkRange::WholeLink(fallback)
1714 let span_for_link = |link: &CowStr<'_>, span: Range<usize>| {
1715 // For diagnostics, we want to underline the link's definition but `span` will point at
1716 // where the link is used. This is a problem for reference-style links, where the definition
1717 // is separate from the usage.
1720 // `Borrowed` variant means the string (the link's destination) may come directly from
1721 // the markdown text and we can locate the original link destination.
1722 // NOTE: LinkReplacer also provides `Borrowed` but possibly from other sources,
1723 // so `locate()` can fall back to use `span`.
1724 CowStr::Borrowed(s) => locate(s, span),
1726 // For anything else, we can only use the provided range.
1727 CowStr::Boxed(_) | CowStr::Inlined(_) => MarkdownLinkRange::WholeLink(span),
1731 let span_for_offset_backward = |span: Range<usize>, open: u8, close: u8| {
1732 let mut open_brace = !0;
1733 let mut close_brace = !0;
1734 for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate().rev() {
1735 let i = i + span.start;
1741 if close_brace < span.start || close_brace >= span.end {
1742 return MarkdownLinkRange::WholeLink(span);
1744 let mut nesting = 1;
1745 for (i, b) in md.as_bytes()[span.start..close_brace].iter().copied().enumerate().rev() {
1746 let i = i + span.start;
1758 assert!(open_brace != close_brace);
1759 if open_brace < span.start || open_brace >= span.end {
1760 return MarkdownLinkRange::WholeLink(span);
1762 // do not actually include braces in the span
1763 let range = (open_brace + 1)..close_brace;
1764 MarkdownLinkRange::Destination(range)
1767 let span_for_offset_forward = |span: Range<usize>, open: u8, close: u8| {
1768 let mut open_brace = !0;
1769 let mut close_brace = !0;
1770 for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate() {
1771 let i = i + span.start;
1777 if open_brace < span.start || open_brace >= span.end {
1778 return MarkdownLinkRange::WholeLink(span);
1780 let mut nesting = 0;
1781 for (i, b) in md.as_bytes()[open_brace..span.end].iter().copied().enumerate() {
1782 let i = i + open_brace;
1794 assert!(open_brace != close_brace);
1795 if open_brace < span.start || open_brace >= span.end {
1796 return MarkdownLinkRange::WholeLink(span);
1798 // do not actually include braces in the span
1799 let range = (open_brace + 1)..close_brace;
1800 MarkdownLinkRange::Destination(range)
1803 let mut broken_link_callback = |link: BrokenLink<'md>| Some((link.reference, "".into()));
1804 let mut event_iter = Parser::new_with_broken_link_callback(
1807 Some(&mut broken_link_callback),
1809 .into_offset_iter();
1810 let mut links = Vec::new();
1812 while let Some((event, span)) = event_iter.next() {
1814 Event::Start(Tag::Link(link_type, dest, _)) if may_be_doc_link(link_type) => {
1815 let range = match link_type {
1816 // Link is pulled from the link itself.
1817 LinkType::ReferenceUnknown | LinkType::ShortcutUnknown => {
1818 span_for_offset_backward(span, b'[', b']')
1820 LinkType::CollapsedUnknown => span_for_offset_forward(span, b'[', b']'),
1821 LinkType::Inline => span_for_offset_backward(span, b'(', b')'),
1822 // Link is pulled from elsewhere in the document.
1823 LinkType::Reference | LinkType::Collapsed | LinkType::Shortcut => {
1824 span_for_link(&dest, span)
1826 LinkType::Autolink | LinkType::Email => unreachable!(),
1829 let display_text = if matches!(
1832 | LinkType::ReferenceUnknown
1833 | LinkType::Reference
1834 | LinkType::Shortcut
1835 | LinkType::ShortcutUnknown
1837 collect_link_data(&mut event_iter)
1842 if let Some(link) = preprocess_link(MarkdownLink {
1844 link: dest.into_string(),
1858 /// Collects additional data of link.
1859 fn collect_link_data<'input, 'callback>(
1860 event_iter: &mut OffsetIter<'input, 'callback>,
1861 ) -> Option<String> {
1862 let mut display_text: Option<String> = None;
1863 let mut append_text = |text: CowStr<'_>| {
1864 if let Some(display_text) = &mut display_text {
1865 display_text.push_str(&text);
1867 display_text = Some(text.to_string());
1871 while let Some((event, _span)) = event_iter.next() {
1873 Event::Text(text) => {
1876 Event::Code(code) => {
1890 pub(crate) struct RustCodeBlock {
1891 /// The range in the markdown that the code block occupies. Note that this includes the fences
1892 /// for fenced code blocks.
1893 pub(crate) range: Range<usize>,
1894 /// The range in the markdown that the code within the code block occupies.
1895 pub(crate) code: Range<usize>,
1896 pub(crate) is_fenced: bool,
1897 pub(crate) lang_string: LangString,
1900 /// Returns a range of bytes for each code block in the markdown that is tagged as `rust` or
1901 /// untagged (and assumed to be rust).
1902 pub(crate) fn rust_code_blocks(
1904 extra_info: &ExtraInfo<'_>,
1905 custom_code_classes_in_docs: bool,
1906 ) -> Vec<RustCodeBlock> {
1907 let mut code_blocks = vec![];
1913 let mut p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
1915 while let Some((event, offset)) = p.next() {
1916 if let Event::Start(Tag::CodeBlock(syntax)) = event {
1917 let (lang_string, code_start, code_end, range, is_fenced) = match syntax {
1918 CodeBlockKind::Fenced(syntax) => {
1919 let syntax = syntax.as_ref();
1920 let lang_string = if syntax.is_empty() {
1928 custom_code_classes_in_docs,
1931 if !lang_string.rust {
1934 let (code_start, mut code_end) = match p.next() {
1935 Some((Event::Text(_), offset)) => (offset.start, offset.end),
1936 Some((_, sub_offset)) => {
1937 let code = Range { start: sub_offset.start, end: sub_offset.start };
1938 code_blocks.push(RustCodeBlock {
1947 let code = Range { start: offset.end, end: offset.end };
1948 code_blocks.push(RustCodeBlock {
1957 while let Some((Event::Text(_), offset)) = p.next() {
1958 code_end = offset.end;
1960 (lang_string, code_start, code_end, offset, true)
1962 CodeBlockKind::Indented => {
1963 // The ending of the offset goes too far sometime so we reduce it by one in
1965 if offset.end > offset.start && md.get(offset.end..=offset.end) == Some("\n") {
1967 LangString::default(),
1970 Range { start: offset.start, end: offset.end - 1 },
1974 (LangString::default(), offset.start, offset.end, offset, false)
1979 code_blocks.push(RustCodeBlock {
1982 code: Range { start: code_start, end: code_end },
1991 #[derive(Clone, Default, Debug)]
1993 map: FxHashMap<Cow<'static, str>, usize>,
1996 // The map is pre-initialized and cloned each time to avoid reinitializing it repeatedly.
1997 static DEFAULT_ID_MAP: Lazy<FxHashMap<Cow<'static, str>, usize>> = Lazy::new(|| init_id_map());
1999 fn init_id_map() -> FxHashMap<Cow<'static, str>, usize> {
2000 let mut map = FxHashMap::default();
2001 // This is the list of IDs used in JavaScript.
2002 map.insert("help
".into(), 1);
2003 map.insert("settings
".into(), 1);
2004 map.insert("not
-displayed
".into(), 1);
2005 map.insert("alternative
-display
".into(), 1);
2006 map.insert("search
".into(), 1);
2007 map.insert("crate-search
".into(), 1);
2008 map.insert("crate-search
-div
".into(), 1);
2009 // This is the list of IDs used in HTML generated in Rust (including the ones
2010 // used in tera template files).
2011 map.insert("themeStyle
".into(), 1);
2012 map.insert("settings
-menu
".into(), 1);
2013 map.insert("help
-button
".into(), 1);
2014 map.insert("sidebar
-button
".into(), 1);
2015 map.insert("main
-content
".into(), 1);
2016 map.insert("toggle
-all
-docs
".into(), 1);
2017 map.insert("all
-types
".into(), 1);
2018 map.insert("default-settings
".into(), 1);
2019 map.insert("sidebar
-vars
".into(), 1);
2020 map.insert("copy
-path
".into(), 1);
2021 map.insert("TOC
".into(), 1);
2022 // This is the list of IDs used by rustdoc sections (but still generated by
2024 map.insert("fields
".into(), 1);
2025 map.insert("variants
".into(), 1);
2026 map.insert("implementors
-list
".into(), 1);
2027 map.insert("synthetic
-implementors
-list
".into(), 1);
2028 map.insert("foreign
-impls
".into(), 1);
2029 map.insert("implementations
".into(), 1);
2030 map.insert("trait-implementations
".into(), 1);
2031 map.insert("synthetic
-implementations
".into(), 1);
2032 map.insert("blanket
-implementations
".into(), 1);
2033 map.insert("required
-associated
-types
".into(), 1);
2034 map.insert("provided
-associated
-types
".into(), 1);
2035 map.insert("provided
-associated
-consts
".into(), 1);
2036 map.insert("required
-associated
-consts
".into(), 1);
2037 map.insert("required
-methods
".into(), 1);
2038 map.insert("provided
-methods
".into(), 1);
2039 map.insert("object
-safety
".into(), 1);
2040 map.insert("implementors
".into(), 1);
2041 map.insert("synthetic
-implementors
".into(), 1);
2042 map.insert("implementations
-list
".into(), 1);
2043 map.insert("trait-implementations
-list
".into(), 1);
2044 map.insert("synthetic
-implementations
-list
".into(), 1);
2045 map.insert("blanket
-implementations
-list
".into(), 1);
2046 map.insert("deref
-methods
".into(), 1);
2047 map.insert("layout
".into(), 1);
2048 map.insert("aliased
-type".into(), 1);
2053 pub fn new() -> Self {
2054 IdMap { map: DEFAULT_ID_MAP.clone() }
2057 pub(crate) fn derive<S: AsRef<str> + ToString>(&mut self, candidate: S) -> String {
2058 let id = match self.map.get_mut(candidate.as_ref()) {
2059 None => candidate.to_string(),
2061 let id = format!("{}
-{}
", candidate.as_ref(), *a);
2067 self.map.insert(id.clone().into(), 1);