1use std::borrow::Cow;
30use std::collections::VecDeque;
31use std::fmt::{self, Write};
32use std::iter::Peekable;
33use std::ops::{ControlFlow, Range};
34use std::path::PathBuf;
35use std::str::{self, CharIndices};
36use std::sync::atomic::AtomicUsize;
37use std::sync::{Arc, Weak};
38
39use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
40use rustc_errors::{Diag, DiagMessage};
41use rustc_hir::def_id::LocalDefId;
42use rustc_middle::ty::TyCtxt;
43pub(crate) use rustc_resolve::rustdoc::main_body_opts;
44use rustc_resolve::rustdoc::pulldown_cmark::{
45 self, BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd, html,
46};
47use rustc_resolve::rustdoc::{DocFragment, may_be_doc_link, source_span_for_markdown_range};
48use rustc_span::edition::Edition;
49use rustc_span::{Span, Symbol};
50use tracing::{debug, trace};
51
52use crate::clean::RenderedLink;
53use crate::doctest;
54use crate::doctest::GlobalTestOptions;
55use crate::html::escape::{Escape, EscapeBodyText};
56use crate::html::highlight;
57use crate::html::length_limit::HtmlWithLimit;
58use crate::html::render::small_url_encode;
59use crate::html::toc::{Toc, TocBuilder};
60
61mod footnotes;
62#[cfg(test)]
63mod tests;
64
65const MAX_HEADER_LEVEL: u32 = 6;
66
67pub(crate) fn summary_opts() -> Options {
69 Options::ENABLE_TABLES
70 | Options::ENABLE_FOOTNOTES
71 | Options::ENABLE_STRIKETHROUGH
72 | Options::ENABLE_TASKLISTS
73 | Options::ENABLE_SMART_PUNCTUATION
74}
75
76#[derive(Debug, Clone, Copy)]
77pub enum HeadingOffset {
78 H1 = 0,
79 H2,
80 H3,
81 H4,
82 H5,
83 H6,
84}
85
86pub struct Markdown<'a> {
89 pub content: &'a str,
90 pub links: &'a [RenderedLink],
92 pub ids: &'a mut IdMap,
94 pub error_codes: ErrorCodes,
96 pub edition: Edition,
98 pub playground: &'a Option<Playground>,
99 pub heading_offset: HeadingOffset,
102}
103pub(crate) struct MarkdownWithToc<'a> {
105 pub(crate) content: &'a str,
106 pub(crate) links: &'a [RenderedLink],
107 pub(crate) ids: &'a mut IdMap,
108 pub(crate) error_codes: ErrorCodes,
109 pub(crate) edition: Edition,
110 pub(crate) playground: &'a Option<Playground>,
111}
112
113pub(crate) struct MarkdownItemInfo<'a> {
116 pub(crate) content: &'a str,
117 pub(crate) links: &'a [RenderedLink],
118 pub(crate) ids: &'a mut IdMap,
119}
120
121pub(crate) struct MarkdownSummaryLine<'a>(pub &'a str, pub &'a [RenderedLink]);
123
124#[derive(Copy, Clone, PartialEq, Debug)]
125pub enum ErrorCodes {
126 Yes,
127 No,
128}
129
130impl ErrorCodes {
131 pub(crate) fn from(b: bool) -> Self {
132 match b {
133 true => ErrorCodes::Yes,
134 false => ErrorCodes::No,
135 }
136 }
137
138 pub(crate) fn as_bool(self) -> bool {
139 match self {
140 ErrorCodes::Yes => true,
141 ErrorCodes::No => false,
142 }
143 }
144}
145
146pub(crate) enum Line<'a> {
150 Hidden(&'a str),
151 Shown(Cow<'a, str>),
152}
153
154impl<'a> Line<'a> {
155 fn for_html(self) -> Option<Cow<'a, str>> {
156 match self {
157 Line::Shown(l) => Some(l),
158 Line::Hidden(_) => None,
159 }
160 }
161
162 pub(crate) fn for_code(self) -> Cow<'a, str> {
163 match self {
164 Line::Shown(l) => l,
165 Line::Hidden(l) => Cow::Borrowed(l),
166 }
167 }
168}
169
170pub(crate) fn map_line(s: &str) -> Line<'_> {
178 let trimmed = s.trim();
179 if trimmed.starts_with("##") {
180 Line::Shown(Cow::Owned(s.replacen("##", "#", 1)))
181 } else if let Some(stripped) = trimmed.strip_prefix("# ") {
182 Line::Hidden(stripped)
184 } else if trimmed == "#" {
185 Line::Hidden("")
187 } else {
188 Line::Shown(Cow::Borrowed(s))
189 }
190}
191
192fn slugify(c: char) -> Option<char> {
196 if c.is_alphanumeric() || c == '-' || c == '_' {
197 if c.is_ascii() { Some(c.to_ascii_lowercase()) } else { Some(c) }
198 } else if c.is_whitespace() && c.is_ascii() {
199 Some('-')
200 } else {
201 None
202 }
203}
204
205#[derive(Debug)]
206pub struct Playground {
207 pub crate_name: Option<Symbol>,
208 pub url: String,
209}
210
211struct CodeBlocks<'p, 'a, I: Iterator<Item = Event<'a>>> {
213 inner: I,
214 check_error_codes: ErrorCodes,
215 edition: Edition,
216 playground: &'p Option<Playground>,
219}
220
221impl<'p, 'a, I: Iterator<Item = Event<'a>>> CodeBlocks<'p, 'a, I> {
222 fn new(
223 iter: I,
224 error_codes: ErrorCodes,
225 edition: Edition,
226 playground: &'p Option<Playground>,
227 ) -> Self {
228 CodeBlocks { inner: iter, check_error_codes: error_codes, edition, playground }
229 }
230}
231
232impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
233 type Item = Event<'a>;
234
235 fn next(&mut self) -> Option<Self::Item> {
236 let event = self.inner.next();
237 let Some(Event::Start(Tag::CodeBlock(kind))) = event else {
238 return event;
239 };
240
241 let mut original_text = String::new();
242 for event in &mut self.inner {
243 match event {
244 Event::End(TagEnd::CodeBlock) => break,
245 Event::Text(ref s) => {
246 original_text.push_str(s);
247 }
248 _ => {}
249 }
250 }
251
252 let LangString { added_classes, compile_fail, should_panic, ignore, edition, .. } =
253 match kind {
254 CodeBlockKind::Fenced(ref lang) => {
255 let parse_result =
256 LangString::parse_without_check(lang, self.check_error_codes);
257 if !parse_result.rust {
258 let added_classes = parse_result.added_classes;
259 let lang_string = if let Some(lang) = parse_result.unknown.first() {
260 format!("language-{lang}")
261 } else {
262 String::new()
263 };
264 let whitespace = if added_classes.is_empty() { "" } else { " " };
265 return Some(Event::Html(
266 format!(
267 "<div class=\"example-wrap\">\
268 <pre class=\"{lang_string}{whitespace}{added_classes}\">\
269 <code>{text}</code>\
270 </pre>\
271 </div>",
272 added_classes = added_classes.join(" "),
273 text = Escape(original_text.trim_suffix('\n')),
274 )
275 .into(),
276 ));
277 }
278 parse_result
279 }
280 CodeBlockKind::Indented => Default::default(),
281 };
282
283 let lines = original_text.lines().filter_map(|l| map_line(l).for_html());
284 let text = lines.intersperse("\n".into()).collect::<String>();
285
286 let explicit_edition = edition.is_some();
287 let edition = edition.unwrap_or(self.edition);
288
289 let playground_button = self.playground.as_ref().and_then(|playground| {
290 let krate = &playground.crate_name;
291 let url = &playground.url;
292 if url.is_empty() {
293 return None;
294 }
295 let test = original_text
296 .lines()
297 .map(|l| map_line(l).for_code())
298 .intersperse("\n".into())
299 .collect::<String>();
300 let krate = krate.as_ref().map(|s| s.as_str());
301
302 let opts = GlobalTestOptions {
305 crate_name: krate.map(String::from).unwrap_or_default(),
306 no_crate_inject: false,
307 insert_indent_space: true,
308 args_file: PathBuf::new(),
309 };
310 let mut builder = doctest::BuildDocTestBuilder::new(&test).edition(edition);
311 if let Some(krate) = krate {
312 builder = builder.crate_name(krate);
313 }
314 let doctest = builder.build(None);
315 let (wrapped, _) = doctest.generate_unique_doctest(&test, false, &opts, krate);
316 let test = wrapped.to_string();
317 let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" };
318
319 let test_escaped = small_url_encode(test);
320 Some(format!(
321 "<a class=\"test-arrow\" \
322 target=\"_blank\" \
323 title=\"Run code\" \
324 href=\"{url}?code={test_escaped}{channel}&edition={edition}\"></a>",
325 ))
326 });
327
328 let tooltip = {
329 use highlight::Tooltip::*;
330
331 if ignore == Ignore::All {
332 Some(IgnoreAll)
333 } else if let Ignore::Some(platforms) = ignore {
334 Some(IgnoreSome(platforms))
335 } else if compile_fail {
336 Some(CompileFail)
337 } else if should_panic {
338 Some(ShouldPanic)
339 } else if explicit_edition {
340 Some(Edition(edition))
341 } else {
342 None
343 }
344 };
345
346 let s = format!(
349 "\n{}",
350 highlight::render_example_with_highlighting(
351 &text,
352 tooltip.as_ref(),
353 playground_button.as_deref(),
354 &added_classes,
355 edition,
356 )
357 );
358 Some(Event::Html(s.into()))
359 }
360}
361
362struct LinkReplacerInner<'a> {
364 links: &'a [RenderedLink],
365 shortcut_link: Option<&'a RenderedLink>,
366}
367
368struct LinkReplacer<'a, I: Iterator<Item = Event<'a>>> {
369 iter: I,
370 inner: LinkReplacerInner<'a>,
371}
372
373impl<'a, I: Iterator<Item = Event<'a>>> LinkReplacer<'a, I> {
374 fn new(iter: I, links: &'a [RenderedLink]) -> Self {
375 LinkReplacer { iter, inner: { LinkReplacerInner { links, shortcut_link: None } } }
376 }
377}
378
379struct SpannedLinkReplacer<'a, I: Iterator<Item = SpannedEvent<'a>>> {
382 iter: I,
383 inner: LinkReplacerInner<'a>,
384}
385
386impl<'a, I: Iterator<Item = SpannedEvent<'a>>> SpannedLinkReplacer<'a, I> {
387 fn new(iter: I, links: &'a [RenderedLink]) -> Self {
388 SpannedLinkReplacer { iter, inner: { LinkReplacerInner { links, shortcut_link: None } } }
389 }
390}
391
392impl<'a> LinkReplacerInner<'a> {
393 fn handle_event(&mut self, event: &mut Event<'a>) {
394 match event {
396 Event::Start(Tag::Link {
399 link_type: LinkType::ShortcutUnknown | LinkType::CollapsedUnknown,
401 dest_url,
402 title,
403 ..
404 }) => {
405 debug!("saw start of shortcut link to {dest_url} with title {title}");
406 let link = self.links.iter().find(|&link| *link.href == **dest_url);
409 if let Some(link) = link {
412 trace!("it matched");
413 assert!(self.shortcut_link.is_none(), "shortcut links cannot be nested");
414 self.shortcut_link = Some(link);
415 if title.is_empty() && !link.tooltip.is_empty() {
416 *title = CowStr::Borrowed(link.tooltip.as_ref());
417 }
418 }
419 }
420 Event::End(TagEnd::Link) if self.shortcut_link.is_some() => {
422 debug!("saw end of shortcut link");
423 self.shortcut_link = None;
424 }
425 Event::Code(text) => {
428 trace!("saw code {text}");
429 if let Some(link) = self.shortcut_link {
430 if let Some(link) = self.links.iter().find(|l| {
440 l.href == link.href
441 && Some(&**text) == l.original_text.get(1..l.original_text.len() - 1)
442 }) {
443 debug!("replacing {text} with {new_text}", new_text = link.new_text);
444 *text = CowStr::Borrowed(&link.new_text);
445 }
446 }
447 }
448 Event::Text(text) => {
451 trace!("saw text {text}");
452 if let Some(link) = self.shortcut_link {
453 if let Some(link) = self
455 .links
456 .iter()
457 .find(|l| l.href == link.href && **text == *l.original_text)
458 {
459 debug!("replacing {text} with {new_text}", new_text = link.new_text);
460 *text = CowStr::Borrowed(&link.new_text);
461 }
462 }
463 }
464 Event::Start(Tag::Link { dest_url, title, .. }) => {
467 if let Some(link) =
468 self.links.iter().find(|&link| *link.original_text == **dest_url)
469 {
470 *dest_url = CowStr::Borrowed(link.href.as_ref());
471 if title.is_empty() && !link.tooltip.is_empty() {
472 *title = CowStr::Borrowed(link.tooltip.as_ref());
473 }
474 }
475 }
476 _ => {}
478 }
479 }
480}
481
482impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
483 type Item = Event<'a>;
484
485 fn next(&mut self) -> Option<Self::Item> {
486 let mut event = self.iter.next();
487 if let Some(ref mut event) = event {
488 self.inner.handle_event(event);
489 }
490 event
492 }
493}
494
495impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for SpannedLinkReplacer<'a, I> {
496 type Item = SpannedEvent<'a>;
497
498 fn next(&mut self) -> Option<Self::Item> {
499 let (mut event, range) = self.iter.next()?;
500 self.inner.handle_event(&mut event);
501 Some((event, range))
503 }
504}
505
506struct TableWrapper<'a, I: Iterator<Item = Event<'a>>> {
508 inner: I,
509 stored_events: VecDeque<Event<'a>>,
510}
511
512impl<'a, I: Iterator<Item = Event<'a>>> TableWrapper<'a, I> {
513 fn new(iter: I) -> Self {
514 Self { inner: iter, stored_events: VecDeque::new() }
515 }
516}
517
518impl<'a, I: Iterator<Item = Event<'a>>> Iterator for TableWrapper<'a, I> {
519 type Item = Event<'a>;
520
521 fn next(&mut self) -> Option<Self::Item> {
522 if let Some(first) = self.stored_events.pop_front() {
523 return Some(first);
524 }
525
526 let event = self.inner.next()?;
527
528 Some(match event {
529 Event::Start(Tag::Table(t)) => {
530 self.stored_events.push_back(Event::Start(Tag::Table(t)));
531 Event::Html(CowStr::Borrowed(r#"<div class="table">"#))
532 }
533 Event::End(TagEnd::Table) => {
534 self.stored_events.push_back(Event::Html(CowStr::Borrowed("</div>")));
535 Event::End(TagEnd::Table)
536 }
537 e => e,
538 })
539 }
540}
541
542type SpannedEvent<'a> = (Event<'a>, Range<usize>);
543
544struct HeadingLinks<'a, 'b, 'ids, I> {
546 inner: I,
547 toc: Option<&'b mut TocBuilder>,
548 buf: VecDeque<SpannedEvent<'a>>,
549 id_map: &'ids mut IdMap,
550 heading_offset: HeadingOffset,
551}
552
553impl<'b, 'ids, I> HeadingLinks<'_, 'b, 'ids, I> {
554 fn new(
555 iter: I,
556 toc: Option<&'b mut TocBuilder>,
557 ids: &'ids mut IdMap,
558 heading_offset: HeadingOffset,
559 ) -> Self {
560 HeadingLinks { inner: iter, toc, buf: VecDeque::new(), id_map: ids, heading_offset }
561 }
562}
563
564impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for HeadingLinks<'a, '_, '_, I> {
565 type Item = SpannedEvent<'a>;
566
567 fn next(&mut self) -> Option<Self::Item> {
568 if let Some(e) = self.buf.pop_front() {
569 return Some(e);
570 }
571
572 let event = self.inner.next();
573 if let Some((Event::Start(Tag::Heading { level, .. }), _)) = event {
574 let mut id = String::new();
575 for event in &mut self.inner {
576 match &event.0 {
577 Event::End(TagEnd::Heading(_)) => break,
578 Event::Text(text) | Event::Code(text) => {
579 id.extend(text.chars().filter_map(slugify));
580 self.buf.push_back(event);
581 }
582 _ => self.buf.push_back(event),
583 }
584 }
585 let id = self.id_map.derive(id);
586 let percent_encoded_id = small_url_encode(id.clone());
587
588 if let Some(ref mut builder) = self.toc {
589 let mut text_header = String::new();
590 plain_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut text_header);
591 let mut html_header = String::new();
592 html_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut html_header);
593 let sec = builder.push(level as u32, text_header, html_header, id.clone());
594 self.buf.push_front((Event::Html(format!("{sec} ").into()), 0..0));
595 }
596
597 let level =
598 std::cmp::min(level as u32 + (self.heading_offset as u32), MAX_HEADER_LEVEL);
599 self.buf.push_back((Event::Html(format!("</h{level}>").into()), 0..0));
600
601 let start_tags = format!(
602 "<h{level} id=\"{id}\"><a class=\"doc-anchor\" href=\"#{percent_encoded_id}\">§</a>"
603 );
604 return Some((Event::Html(start_tags.into()), 0..0));
605 }
606 event
607 }
608}
609
610struct SummaryLine<'a, I: Iterator<Item = Event<'a>>> {
612 inner: I,
613 started: bool,
614 depth: u32,
615 skipped_tags: u32,
616}
617
618impl<'a, I: Iterator<Item = Event<'a>>> SummaryLine<'a, I> {
619 fn new(iter: I) -> Self {
620 SummaryLine { inner: iter, started: false, depth: 0, skipped_tags: 0 }
621 }
622}
623
624fn check_if_allowed_tag(t: &TagEnd) -> bool {
625 matches!(
626 t,
627 TagEnd::Paragraph
628 | TagEnd::Emphasis
629 | TagEnd::Strong
630 | TagEnd::Strikethrough
631 | TagEnd::Link
632 | TagEnd::BlockQuote
633 )
634}
635
636fn is_forbidden_tag(t: &TagEnd) -> bool {
637 matches!(
638 t,
639 TagEnd::CodeBlock
640 | TagEnd::Table
641 | TagEnd::TableHead
642 | TagEnd::TableRow
643 | TagEnd::TableCell
644 | TagEnd::FootnoteDefinition
645 )
646}
647
648impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SummaryLine<'a, I> {
649 type Item = Event<'a>;
650
651 fn next(&mut self) -> Option<Self::Item> {
652 if self.started && self.depth == 0 {
653 return None;
654 }
655 if !self.started {
656 self.started = true;
657 }
658 if let Some(event) = self.inner.next() {
659 let mut is_start = true;
660 let is_allowed_tag = match event {
661 Event::Start(ref c) => {
662 if is_forbidden_tag(&c.to_end()) {
663 self.skipped_tags += 1;
664 return None;
665 }
666 self.depth += 1;
667 check_if_allowed_tag(&c.to_end())
668 }
669 Event::End(ref c) => {
670 if is_forbidden_tag(c) {
671 self.skipped_tags += 1;
672 return None;
673 }
674 self.depth -= 1;
675 is_start = false;
676 check_if_allowed_tag(c)
677 }
678 Event::FootnoteReference(_) => {
679 self.skipped_tags += 1;
680 false
681 }
682 _ => true,
683 };
684 if !is_allowed_tag {
685 self.skipped_tags += 1;
686 }
687 return if !is_allowed_tag {
688 if is_start {
689 Some(Event::Start(Tag::Paragraph))
690 } else {
691 Some(Event::End(TagEnd::Paragraph))
692 }
693 } else {
694 Some(event)
695 };
696 }
697 None
698 }
699}
700
701pub(crate) enum CodeBlockPosition {
705 Relative(MdRelLine),
706 Span(Span),
707}
708
709pub(crate) struct MdRelLine {
716 offset: usize,
717}
718
719impl MdRelLine {
720 pub(crate) const fn new(offset: usize) -> Self {
722 Self { offset }
723 }
724
725 pub(crate) const fn offset(self) -> usize {
727 self.offset
728 }
729}
730
731#[derive(Clone, Debug)]
732pub(crate) struct CodeLineMapping {
733 pub(crate) generated: Range<usize>,
734 pub(crate) original: Span,
735}
736
737pub(crate) fn find_testable_code<T: doctest::DocTestVisitor>(
738 doc: &str,
739 tests: &mut T,
740 error_codes: ErrorCodes,
741 extra_info: Option<&ExtraInfo<'_, '_>>,
742) {
743 find_codes(doc, tests, error_codes, extra_info, false)
744}
745
746pub(crate) fn find_codes<T: doctest::DocTestVisitor>(
747 doc: &str,
748 tests: &mut T,
749 error_codes: ErrorCodes,
750 extra_info: Option<&ExtraInfo<'_, '_>>,
751 include_non_rust: bool,
752) {
753 let mut parser = Parser::new_ext(doc, main_body_opts()).into_offset_iter();
754 let mut prev_offset = 0;
755 let mut nb_lines = 0;
756 let mut register_header = None;
757 while let Some((event, offset)) = parser.next() {
758 match event {
759 Event::Start(Tag::CodeBlock(kind)) => {
760 let block_info = match kind {
761 CodeBlockKind::Fenced(ref lang) => {
762 if lang.is_empty() {
763 Default::default()
764 } else {
765 LangString::parse(lang, error_codes, extra_info)
766 }
767 }
768 CodeBlockKind::Indented => Default::default(),
769 };
770 if !include_non_rust && !block_info.rust {
771 continue;
772 }
773
774 let mut test_s = String::new();
775 let mut text_events = Vec::new();
776
777 while let Some((Event::Text(s), offset)) = parser.next() {
778 let start = test_s.len();
779 test_s.push_str(&s);
780 text_events.push((start..test_s.len(), offset));
781 }
782 let (text, code_mappings) = map_code_block(doc, &test_s, &text_events, extra_info);
783
784 nb_lines += doc[prev_offset..offset.start].lines().count();
785 if nb_lines != 0 && !&doc[prev_offset..offset.start].ends_with('\n') {
789 nb_lines -= 1;
790 }
791
792 let position = if let Some(extra_info) = extra_info
793 && let Some(fragments) = &extra_info.fragments
794 {
795 let code_block_str = &doc[offset.clone()];
796 let nl_offset = code_block_str.find('\n').unwrap_or(3);
798 let lang_range = offset.start..(offset.start + nl_offset);
799 if let Some((sp, _)) =
800 source_span_for_markdown_range(extra_info.tcx, doc, &lang_range, fragments)
801 {
802 CodeBlockPosition::Span(sp)
803 } else {
804 CodeBlockPosition::Relative(MdRelLine::new(nb_lines))
805 }
806 } else {
807 CodeBlockPosition::Relative(MdRelLine::new(nb_lines))
808 };
809
810 tests.visit_test(text, block_info, position, code_mappings);
811 prev_offset = offset.start;
812 }
813 Event::Start(Tag::Heading { level, .. }) => {
814 register_header = Some(level as u32);
815 }
816 Event::Text(ref s) if register_header.is_some() => {
817 let level = register_header.unwrap();
818 tests.visit_header(s, level);
819 register_header = None;
820 }
821 _ => {}
822 }
823 }
824}
825
826fn map_code_block(
827 doc: &str,
828 code: &str,
829 text_events: &[(Range<usize>, Range<usize>)],
830 extra_info: Option<&ExtraInfo<'_, '_>>,
831) -> (String, Vec<CodeLineMapping>) {
832 let mut text = String::new();
833 let mut code_mappings = Vec::new();
834 let mut code_line_start = 0;
835
836 for (line_index, line) in code.lines().enumerate() {
837 if line_index != 0 {
838 text.push('\n');
839 }
840
841 let generated_start = text.len();
842 let mapped_line = map_line(line).for_code();
843 text.push_str(&mapped_line);
844 let generated = generated_start..text.len();
845
846 if mapped_line.as_ref() == line
847 && let Some(extra_info) = extra_info
848 && let Some(fragments) = extra_info.fragments
849 {
850 let code_line = code_line_start..code_line_start + line.len();
851 if let Some(md_range) = markdown_range_for_code_range(text_events, code_line)
852 && let Some((original, _)) =
853 source_span_for_markdown_range(extra_info.tcx, doc, &md_range, fragments)
854 {
855 code_mappings.push(CodeLineMapping { generated, original });
856 }
857 }
858
859 code_line_start += line.len() + 1;
860 }
861
862 (text, code_mappings)
863}
864
865fn markdown_range_for_code_range(
866 text_events: &[(Range<usize>, Range<usize>)],
867 code_range: Range<usize>,
868) -> Option<Range<usize>> {
869 text_events.iter().find_map(|(event_code_range, event_md_range)| {
870 if event_code_range.start <= code_range.start && code_range.end <= event_code_range.end {
871 let start = event_md_range.start + code_range.start - event_code_range.start;
872 let end = event_md_range.start + code_range.end - event_code_range.start;
873 Some(start..end)
874 } else {
875 None
876 }
877 })
878}
879
880pub(crate) struct ExtraInfo<'doc, 'tcx> {
881 def_id: LocalDefId,
882 sp: Span,
883 tcx: TyCtxt<'tcx>,
884 fragments: Option<&'doc [DocFragment]>,
885}
886
887impl<'doc, 'tcx> ExtraInfo<'doc, 'tcx> {
888 pub(crate) fn new(
889 tcx: TyCtxt<'tcx>,
890 def_id: LocalDefId,
891 sp: Span,
892 fragments: Option<&'doc [DocFragment]>,
893 ) -> ExtraInfo<'doc, 'tcx> {
894 ExtraInfo { def_id, sp, tcx, fragments }
895 }
896
897 fn error_invalid_codeblock_attr(&self, msg: impl Into<DiagMessage>) {
898 self.error_invalid_codeblock_attr_with_help(msg, |_| {});
899 }
900
901 fn error_invalid_codeblock_attr_with_help(
902 &self,
903 msg: impl Into<DiagMessage>,
904 f: impl for<'a, 'b> FnOnce(&'b mut Diag<'a>),
905 ) {
906 self.tcx.emit_node_span_lint(
907 crate::lint::INVALID_CODEBLOCK_ATTRIBUTES,
908 self.tcx.local_def_id_to_hir_id(self.def_id),
909 self.sp,
910 rustc_errors::DiagDecorator(|lint| {
911 lint.primary_message(msg);
912 f(lint);
913 }),
914 );
915 }
916}
917
918#[derive(Eq, PartialEq, Clone, Debug)]
919pub(crate) struct LangString {
920 pub(crate) original: String,
921 pub(crate) should_panic: bool,
922 pub(crate) no_run: bool,
923 pub(crate) ignore: Ignore,
924 pub(crate) rust: bool,
925 pub(crate) test_harness: bool,
926 pub(crate) compile_fail: bool,
927 pub(crate) standalone_crate: bool,
928 pub(crate) error_codes: Vec<String>,
929 pub(crate) edition: Option<Edition>,
930 pub(crate) added_classes: Vec<String>,
931 pub(crate) unknown: Vec<String>,
932}
933
934#[derive(Eq, PartialEq, Clone, Debug)]
935pub(crate) enum Ignore {
936 All,
937 None,
938 Some(Vec<String>),
939}
940
941pub(crate) struct TagIterator<'a, 'tcx> {
982 inner: Peekable<CharIndices<'a>>,
983 data: &'a str,
984 is_in_attribute_block: bool,
985 extra: Option<&'a ExtraInfo<'a, 'tcx>>,
986 is_error: bool,
987}
988
989#[derive(Clone, Debug, Eq, PartialEq)]
990pub(crate) enum LangStringToken<'a> {
991 LangToken(&'a str),
992 ClassAttribute(&'a str),
993 KeyValueAttribute(&'a str, &'a str),
994}
995
996fn is_leading_char(c: char) -> bool {
997 c == '_' || c == '-' || c == ':' || c.is_ascii_alphabetic() || c.is_ascii_digit()
998}
999fn is_bareword_char(c: char) -> bool {
1000 is_leading_char(c) || ".!#$%&*+/;<>?@^|~".contains(c)
1001}
1002fn is_separator(c: char) -> bool {
1003 c == ' ' || c == ',' || c == '\t'
1004}
1005
1006struct Indices {
1007 start: usize,
1008 end: usize,
1009}
1010
1011impl<'a, 'tcx> TagIterator<'a, 'tcx> {
1012 pub(crate) fn new(data: &'a str, extra: Option<&'a ExtraInfo<'a, 'tcx>>) -> Self {
1013 Self {
1014 inner: data.char_indices().peekable(),
1015 data,
1016 is_in_attribute_block: false,
1017 extra,
1018 is_error: false,
1019 }
1020 }
1021
1022 fn emit_error(&mut self, err: impl Into<DiagMessage>) {
1023 if let Some(extra) = self.extra {
1024 extra.error_invalid_codeblock_attr(err);
1025 }
1026 self.is_error = true;
1027 }
1028
1029 fn skip_separators(&mut self) -> Option<usize> {
1030 while let Some((pos, c)) = self.inner.peek() {
1031 if !is_separator(*c) {
1032 return Some(*pos);
1033 }
1034 self.inner.next();
1035 }
1036 None
1037 }
1038
1039 fn parse_string(&mut self, start: usize) -> Option<Indices> {
1040 for (pos, c) in self.inner.by_ref() {
1041 if c == '"' {
1042 return Some(Indices { start: start + 1, end: pos });
1043 }
1044 }
1045 self.emit_error("unclosed quote string `\"`");
1046 None
1047 }
1048
1049 fn parse_class(&mut self, start: usize) -> Option<LangStringToken<'a>> {
1050 while let Some((pos, c)) = self.inner.peek().copied() {
1051 if is_bareword_char(c) {
1052 self.inner.next();
1053 } else {
1054 let class = &self.data[start + 1..pos];
1055 if class.is_empty() {
1056 self.emit_error(format!("unexpected `{c}` character after `.`"));
1057 return None;
1058 } else if self.check_after_token() {
1059 return Some(LangStringToken::ClassAttribute(class));
1060 } else {
1061 return None;
1062 }
1063 }
1064 }
1065 let class = &self.data[start + 1..];
1066 if class.is_empty() {
1067 self.emit_error("missing character after `.`");
1068 None
1069 } else if self.check_after_token() {
1070 Some(LangStringToken::ClassAttribute(class))
1071 } else {
1072 None
1073 }
1074 }
1075
1076 fn parse_token(&mut self, start: usize) -> Option<Indices> {
1077 while let Some((pos, c)) = self.inner.peek() {
1078 if !is_bareword_char(*c) {
1079 return Some(Indices { start, end: *pos });
1080 }
1081 self.inner.next();
1082 }
1083 self.emit_error("unexpected end");
1084 None
1085 }
1086
1087 fn parse_key_value(&mut self, c: char, start: usize) -> Option<LangStringToken<'a>> {
1088 let key_indices =
1089 if c == '"' { self.parse_string(start)? } else { self.parse_token(start)? };
1090 if key_indices.start == key_indices.end {
1091 self.emit_error("unexpected empty string as key");
1092 return None;
1093 }
1094
1095 if let Some((_, c)) = self.inner.next() {
1096 if c != '=' {
1097 self.emit_error(format!("expected `=`, found `{c}`"));
1098 return None;
1099 }
1100 } else {
1101 self.emit_error("unexpected end");
1102 return None;
1103 }
1104 let value_indices = match self.inner.next() {
1105 Some((pos, '"')) => self.parse_string(pos)?,
1106 Some((pos, c)) if is_bareword_char(c) => self.parse_token(pos)?,
1107 Some((_, c)) => {
1108 self.emit_error(format!("unexpected `{c}` character after `=`"));
1109 return None;
1110 }
1111 None => {
1112 self.emit_error("expected value after `=`");
1113 return None;
1114 }
1115 };
1116 if value_indices.start == value_indices.end {
1117 self.emit_error("unexpected empty string as value");
1118 None
1119 } else if self.check_after_token() {
1120 Some(LangStringToken::KeyValueAttribute(
1121 &self.data[key_indices.start..key_indices.end],
1122 &self.data[value_indices.start..value_indices.end],
1123 ))
1124 } else {
1125 None
1126 }
1127 }
1128
1129 fn check_after_token(&mut self) -> bool {
1131 if let Some((_, c)) = self.inner.peek().copied() {
1132 if c == '}' || is_separator(c) || c == '(' {
1133 true
1134 } else {
1135 self.emit_error(format!("unexpected `{c}` character"));
1136 false
1137 }
1138 } else {
1139 true
1141 }
1142 }
1143
1144 fn parse_in_attribute_block(&mut self) -> Option<LangStringToken<'a>> {
1145 if let Some((pos, c)) = self.inner.next() {
1146 if c == '}' {
1147 self.is_in_attribute_block = false;
1148 return self.next();
1149 } else if c == '.' {
1150 return self.parse_class(pos);
1151 } else if c == '"' || is_leading_char(c) {
1152 return self.parse_key_value(c, pos);
1153 } else {
1154 self.emit_error(format!("unexpected character `{c}`"));
1155 return None;
1156 }
1157 }
1158 self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
1159 None
1160 }
1161
1162 fn skip_paren_block(&mut self) -> bool {
1164 for (_, c) in self.inner.by_ref() {
1165 if c == ')' {
1166 return true;
1167 }
1168 }
1169 self.emit_error("unclosed comment: missing `)` at the end");
1170 false
1171 }
1172
1173 fn parse_outside_attribute_block(&mut self, start: usize) -> Option<LangStringToken<'a>> {
1174 while let Some((pos, c)) = self.inner.next() {
1175 if c == '"' {
1176 if pos != start {
1177 self.emit_error("expected ` `, `{` or `,` found `\"`");
1178 return None;
1179 }
1180 let indices = self.parse_string(pos)?;
1181 if let Some((_, c)) = self.inner.peek().copied()
1182 && c != '{'
1183 && !is_separator(c)
1184 && c != '('
1185 {
1186 self.emit_error(format!("expected ` `, `{{` or `,` after `\"`, found `{c}`"));
1187 return None;
1188 }
1189 return Some(LangStringToken::LangToken(&self.data[indices.start..indices.end]));
1190 } else if c == '{' {
1191 self.is_in_attribute_block = true;
1192 return self.next();
1193 } else if is_separator(c) {
1194 if pos != start {
1195 return Some(LangStringToken::LangToken(&self.data[start..pos]));
1196 }
1197 return self.next();
1198 } else if c == '(' {
1199 if !self.skip_paren_block() {
1200 return None;
1201 }
1202 if pos != start {
1203 return Some(LangStringToken::LangToken(&self.data[start..pos]));
1204 }
1205 return self.next();
1206 } else if (pos == start && is_leading_char(c)) || (pos != start && is_bareword_char(c))
1207 {
1208 continue;
1209 } else {
1210 self.emit_error(format!("unexpected character `{c}`"));
1211 return None;
1212 }
1213 }
1214 let token = &self.data[start..];
1215 if token.is_empty() { None } else { Some(LangStringToken::LangToken(&self.data[start..])) }
1216 }
1217}
1218
1219impl<'a> Iterator for TagIterator<'a, '_> {
1220 type Item = LangStringToken<'a>;
1221
1222 fn next(&mut self) -> Option<Self::Item> {
1223 if self.is_error {
1224 return None;
1225 }
1226 let Some(start) = self.skip_separators() else {
1227 if self.is_in_attribute_block {
1228 self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
1229 }
1230 return None;
1231 };
1232 if self.is_in_attribute_block {
1233 self.parse_in_attribute_block()
1234 } else {
1235 self.parse_outside_attribute_block(start)
1236 }
1237 }
1238}
1239
1240impl Default for LangString {
1241 fn default() -> Self {
1242 Self {
1243 original: String::new(),
1244 should_panic: false,
1245 no_run: false,
1246 ignore: Ignore::None,
1247 rust: true,
1248 test_harness: false,
1249 compile_fail: false,
1250 standalone_crate: false,
1251 error_codes: Vec::new(),
1252 edition: None,
1253 added_classes: Vec::new(),
1254 unknown: Vec::new(),
1255 }
1256 }
1257}
1258
1259impl LangString {
1260 fn parse_without_check(string: &str, allow_error_code_check: ErrorCodes) -> Self {
1261 Self::parse(string, allow_error_code_check, None)
1262 }
1263
1264 fn parse(
1265 string: &str,
1266 allow_error_code_check: ErrorCodes,
1267 extra: Option<&ExtraInfo<'_, '_>>,
1268 ) -> Self {
1269 let allow_error_code_check = allow_error_code_check.as_bool();
1270 let mut seen_rust_tags = false;
1271 let mut seen_other_tags = false;
1272 let mut seen_custom_tag = false;
1273 let mut data = LangString::default();
1274 let mut ignores = vec![];
1275
1276 data.original = string.to_owned();
1277
1278 let mut call = |tokens: &mut dyn Iterator<Item = LangStringToken<'_>>| {
1279 for token in tokens {
1280 match token {
1281 LangStringToken::LangToken("should_panic") => {
1282 data.should_panic = true;
1283 seen_rust_tags = !seen_other_tags;
1284 }
1285 LangStringToken::LangToken("no_run") => {
1286 data.no_run = true;
1287 seen_rust_tags = !seen_other_tags;
1288 }
1289 LangStringToken::LangToken("ignore") => {
1290 data.ignore = Ignore::All;
1291 seen_rust_tags = !seen_other_tags;
1292 }
1293 LangStringToken::LangToken(x)
1294 if let Some(ignore) = x.strip_prefix("ignore-") =>
1295 {
1296 ignores.push(ignore.to_owned());
1297 seen_rust_tags = !seen_other_tags;
1298 }
1299 LangStringToken::LangToken("rust") => {
1300 data.rust = true;
1301 seen_rust_tags = true;
1302 }
1303 LangStringToken::LangToken("custom") => {
1304 seen_custom_tag = true;
1305 }
1306 LangStringToken::LangToken("test_harness") => {
1307 data.test_harness = true;
1308 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1309 }
1310 LangStringToken::LangToken("compile_fail") => {
1311 data.compile_fail = true;
1312 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1313 data.no_run = true;
1314 }
1315 LangStringToken::LangToken("standalone_crate") => {
1316 data.standalone_crate = true;
1317 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1318 }
1319 LangStringToken::LangToken(x)
1320 if let Some(edition) = x.strip_prefix("edition") =>
1321 {
1322 data.edition = edition.parse::<Edition>().ok();
1323 }
1324 LangStringToken::LangToken(x)
1325 if let Some(edition) = x.strip_prefix("rust")
1326 && edition.parse::<Edition>().is_ok()
1327 && let Some(extra) = extra =>
1328 {
1329 extra.error_invalid_codeblock_attr_with_help(
1330 format!("unknown attribute `{x}`"),
1331 |lint| {
1332 lint.help(format!(
1333 "there is an attribute with a similar name: `edition{edition}`"
1334 ));
1335 },
1336 );
1337 }
1338 LangStringToken::LangToken(x)
1339 if allow_error_code_check
1340 && let Some(error_code) = x.strip_prefix('E')
1341 && error_code.len() == 4 =>
1342 {
1343 if error_code.parse::<u32>().is_ok() {
1344 data.error_codes.push(x.to_owned());
1345 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1346 } else {
1347 seen_other_tags = true;
1348 }
1349 }
1350 LangStringToken::LangToken(x) if let Some(extra) = extra => {
1351 if let Some(help) = match x.to_lowercase().as_str() {
1352 "compile-fail" | "compile_fail" | "compilefail" => Some(
1353 "use `compile_fail` to invert the results of this test, so that it \
1354 passes if it cannot be compiled and fails if it can",
1355 ),
1356 "should-panic" | "should_panic" | "shouldpanic" => Some(
1357 "use `should_panic` to invert the results of this test, so that if \
1358 passes if it panics and fails if it does not",
1359 ),
1360 "no-run" | "no_run" | "norun" => Some(
1361 "use `no_run` to compile, but not run, the code sample during \
1362 testing",
1363 ),
1364 "test-harness" | "test_harness" | "testharness" => Some(
1365 "use `test_harness` to run functions marked `#[test]` instead of a \
1366 potentially-implicit `main` function",
1367 ),
1368 "standalone" | "standalone_crate" | "standalone-crate"
1369 if extra.sp.at_least_rust_2024() =>
1370 {
1371 Some(
1372 "use `standalone_crate` to compile this code block \
1373 separately",
1374 )
1375 }
1376 _ => None,
1377 } {
1378 extra.error_invalid_codeblock_attr_with_help(
1379 format!("unknown attribute `{x}`"),
1380 |lint| {
1381 lint.help(help).help(
1382 "this code block may be skipped during testing, \
1383 because unknown attributes are treated as markers for \
1384 code samples written in other programming languages, \
1385 unless it is also explicitly marked as `rust`",
1386 );
1387 },
1388 );
1389 }
1390 seen_other_tags = true;
1391 data.unknown.push(x.to_owned());
1392 }
1393 LangStringToken::LangToken(x) => {
1394 seen_other_tags = true;
1395 data.unknown.push(x.to_owned());
1396 }
1397 LangStringToken::KeyValueAttribute("class", value) => {
1398 data.added_classes.push(value.to_owned());
1399 }
1400 LangStringToken::KeyValueAttribute(key, ..) if let Some(extra) = extra => {
1401 extra
1402 .error_invalid_codeblock_attr(format!("unsupported attribute `{key}`"));
1403 }
1404 LangStringToken::ClassAttribute(class) => {
1405 data.added_classes.push(class.to_owned());
1406 }
1407 _ => {}
1408 }
1409 }
1410 };
1411
1412 let mut tag_iter = TagIterator::new(string, extra);
1413 call(&mut tag_iter);
1414
1415 if !ignores.is_empty() {
1417 data.ignore = Ignore::Some(ignores);
1418 }
1419
1420 data.rust &= !seen_custom_tag && (!seen_other_tags || seen_rust_tags) && !tag_iter.is_error;
1421
1422 data
1423 }
1424}
1425
1426impl<'a> Markdown<'a> {
1427 pub fn write_into(self, f: impl fmt::Write) -> fmt::Result {
1428 if self.content.is_empty() {
1430 return Ok(());
1431 }
1432
1433 html::write_html_fmt(f, self.into_iter())
1434 }
1435
1436 fn into_iter(self) -> CodeBlocks<'a, 'a, impl Iterator<Item = Event<'a>>> {
1437 let Markdown {
1438 content: md,
1439 links,
1440 ids,
1441 error_codes: codes,
1442 edition,
1443 playground,
1444 heading_offset,
1445 } = self;
1446
1447 let replacer = move |broken_link: BrokenLink<'_>| {
1448 links
1449 .iter()
1450 .find(|link| *link.original_text == *broken_link.reference)
1451 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1452 };
1453
1454 let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(replacer));
1455 let p = p.into_offset_iter();
1456
1457 ids.handle_footnotes(|ids, existing_footnotes| {
1458 let p = HeadingLinks::new(p, None, ids, heading_offset);
1459 let p = SpannedLinkReplacer::new(p, links);
1460 let p = footnotes::Footnotes::new(p, existing_footnotes);
1461 let p = TableWrapper::new(p.map(|(ev, _)| ev));
1462 CodeBlocks::new(p, codes, edition, playground)
1463 })
1464 }
1465
1466 pub(crate) fn split_summary_and_content(self) -> (Option<String>, Option<String>) {
1472 if self.content.is_empty() {
1473 return (None, None);
1474 }
1475 let mut p = self.into_iter();
1476
1477 let mut event_level = 0;
1478 let mut summary_events = Vec::new();
1479 let mut get_next_tag = false;
1480
1481 let mut end_of_summary = false;
1482 while let Some(event) = p.next() {
1483 match event {
1484 Event::Start(_) => event_level += 1,
1485 Event::End(kind) => {
1486 event_level -= 1;
1487 if event_level == 0 {
1488 end_of_summary = true;
1490 get_next_tag = kind == TagEnd::Table;
1492 }
1493 }
1494 _ => {}
1495 }
1496 summary_events.push(event);
1497 if end_of_summary {
1498 if get_next_tag && let Some(event) = p.next() {
1499 summary_events.push(event);
1500 }
1501 break;
1502 }
1503 }
1504 let mut summary = String::new();
1505 html::push_html(&mut summary, summary_events.into_iter());
1506 if summary.is_empty() {
1507 return (None, None);
1508 }
1509 let mut content = String::new();
1510 html::push_html(&mut content, p);
1511
1512 if content.is_empty() { (Some(summary), None) } else { (Some(summary), Some(content)) }
1513 }
1514}
1515
1516impl MarkdownWithToc<'_> {
1517 pub(crate) fn into_parts(self) -> (Toc, String) {
1518 let MarkdownWithToc { content: md, links, ids, error_codes: codes, edition, playground } =
1519 self;
1520
1521 if md.is_empty() {
1523 return (Toc { entries: Vec::new() }, String::new());
1524 }
1525 let mut replacer = |broken_link: BrokenLink<'_>| {
1526 links
1527 .iter()
1528 .find(|link| *link.original_text == *broken_link.reference)
1529 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1530 };
1531
1532 let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
1533 let p = p.into_offset_iter();
1534
1535 let mut s = String::with_capacity(md.len() * 3 / 2);
1536
1537 let mut toc = TocBuilder::new();
1538
1539 ids.handle_footnotes(|ids, existing_footnotes| {
1540 let p = HeadingLinks::new(p, Some(&mut toc), ids, HeadingOffset::H1);
1541 let p = footnotes::Footnotes::new(p, existing_footnotes);
1542 let p = TableWrapper::new(p.map(|(ev, _)| ev));
1543 let p = CodeBlocks::new(p, codes, edition, playground);
1544 html::push_html(&mut s, p);
1545 });
1546
1547 (toc.into_toc(), s)
1548 }
1549
1550 pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
1551 let (toc, s) = self.into_parts();
1552 write!(f, "<nav id=\"rustdoc\">{toc}</nav>{s}", toc = toc.print())
1553 }
1554}
1555
1556impl<'a> MarkdownItemInfo<'a> {
1557 pub(crate) fn new(content: &'a str, links: &'a [RenderedLink], ids: &'a mut IdMap) -> Self {
1558 Self { content, links, ids }
1559 }
1560
1561 pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
1562 let MarkdownItemInfo { content: md, links, ids } = self;
1563
1564 if md.is_empty() {
1566 return Ok(());
1567 }
1568
1569 let replacer = move |broken_link: BrokenLink<'_>| {
1570 links
1571 .iter()
1572 .find(|link| *link.original_text == *broken_link.reference)
1573 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1574 };
1575
1576 let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(replacer));
1577 let p = p.into_offset_iter();
1578
1579 let p = p.map(|event| match event.0 {
1581 Event::Html(text) | Event::InlineHtml(text) => (Event::Text(text), event.1),
1582 _ => event,
1583 });
1584
1585 ids.handle_footnotes(|ids, existing_footnotes| {
1586 let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
1587 let p = SpannedLinkReplacer::new(p, links);
1588 let p = footnotes::Footnotes::new(p, existing_footnotes);
1589 let p = TableWrapper::new(p.map(|(ev, _)| ev));
1590 html::write_html_fmt(&mut f, p)?;
1592
1593 Ok(())
1594 })
1595 }
1596}
1597
1598impl MarkdownSummaryLine<'_> {
1599 pub(crate) fn into_string_with_has_more_content(self) -> (String, bool) {
1600 let MarkdownSummaryLine(md, links) = self;
1601 if md.is_empty() {
1603 return (String::new(), false);
1604 }
1605
1606 let mut replacer = |broken_link: BrokenLink<'_>| {
1607 links
1608 .iter()
1609 .find(|link| *link.original_text == *broken_link.reference)
1610 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1611 };
1612
1613 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer))
1614 .peekable();
1615 let mut summary = SummaryLine::new(p);
1616
1617 let mut s = String::new();
1618
1619 let without_paragraphs = LinkReplacer::new(&mut summary, links).filter(|event| {
1620 !matches!(event, Event::Start(Tag::Paragraph) | Event::End(TagEnd::Paragraph))
1621 });
1622
1623 html::push_html(&mut s, without_paragraphs);
1624
1625 let has_more_content = matches!(summary.inner.peek(), Some(Event::Start(_) | Event::Rule))
1626 || summary.skipped_tags > 0;
1627
1628 (s, has_more_content)
1629 }
1630
1631 pub(crate) fn into_string(self) -> String {
1632 self.into_string_with_has_more_content().0
1633 }
1634}
1635
1636fn markdown_summary_with_limit(
1645 md: &str,
1646 link_names: &[RenderedLink],
1647 length_limit: usize,
1648) -> (String, bool) {
1649 if md.is_empty() {
1650 return (String::new(), false);
1651 }
1652
1653 let mut replacer = |broken_link: BrokenLink<'_>| {
1654 link_names
1655 .iter()
1656 .find(|link| *link.original_text == *broken_link.reference)
1657 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1658 };
1659
1660 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
1661 let mut p = LinkReplacer::new(p, link_names);
1662
1663 let mut buf = HtmlWithLimit::new(length_limit);
1664 let mut stopped_early = false;
1665 let _ = p.try_for_each(|event| {
1666 match &event {
1667 Event::Text(text) => {
1668 let r =
1669 text.split_inclusive(char::is_whitespace).try_for_each(|word| buf.push(word));
1670 if r.is_break() {
1671 stopped_early = true;
1672 }
1673 return r;
1674 }
1675 Event::Code(code) => {
1676 buf.open_tag("code");
1677 let r = buf.push(code);
1678 if r.is_break() {
1679 stopped_early = true;
1680 } else {
1681 buf.close_tag();
1682 }
1683 return r;
1684 }
1685 Event::Start(tag) => match tag {
1686 Tag::Emphasis => buf.open_tag("em"),
1687 Tag::Strong => buf.open_tag("strong"),
1688 Tag::CodeBlock(..) => return ControlFlow::Break(()),
1689 _ => {}
1690 },
1691 Event::End(tag) => match tag {
1692 TagEnd::Emphasis | TagEnd::Strong => buf.close_tag(),
1693 TagEnd::Paragraph | TagEnd::Heading(_) => return ControlFlow::Break(()),
1694 _ => {}
1695 },
1696 Event::HardBreak | Event::SoftBreak => buf.push(" ")?,
1697 _ => {}
1698 };
1699 ControlFlow::Continue(())
1700 });
1701
1702 (buf.finish(), stopped_early)
1703}
1704
1705pub(crate) fn short_markdown_summary(markdown: &str, link_names: &[RenderedLink]) -> String {
1712 let (mut s, was_shortened) = markdown_summary_with_limit(markdown, link_names, 59);
1713
1714 if was_shortened {
1715 s.push('…');
1716 }
1717
1718 s
1719}
1720
1721pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> String {
1728 if md.is_empty() {
1729 return String::new();
1730 }
1731
1732 let mut s = String::with_capacity(md.len() * 3 / 2);
1733
1734 let mut replacer = |broken_link: BrokenLink<'_>| {
1735 link_names
1736 .iter()
1737 .find(|link| *link.original_text == *broken_link.reference)
1738 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1739 };
1740
1741 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
1742
1743 plain_text_from_events(p, &mut s);
1744
1745 s
1746}
1747
1748pub(crate) fn plain_text_from_events<'a>(
1749 events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
1750 s: &mut String,
1751) {
1752 for event in events {
1753 match &event {
1754 Event::Text(text) => s.push_str(text),
1755 Event::Code(code) => {
1756 s.push('`');
1757 s.push_str(code);
1758 s.push('`');
1759 }
1760 Event::HardBreak | Event::SoftBreak => s.push(' '),
1761 Event::Start(Tag::CodeBlock(..)) => break,
1762 Event::End(TagEnd::Paragraph) => break,
1763 Event::End(TagEnd::Heading(..)) => break,
1764 _ => (),
1765 }
1766 }
1767}
1768
1769pub(crate) fn html_text_from_events<'a>(
1770 events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
1771 s: &mut String,
1772) {
1773 for event in events {
1774 match &event {
1775 Event::Text(text) => {
1776 write!(s, "{}", EscapeBodyText(text)).expect("string alloc infallible")
1777 }
1778 Event::Code(code) => {
1779 s.push_str("<code>");
1780 write!(s, "{}", EscapeBodyText(code)).expect("string alloc infallible");
1781 s.push_str("</code>");
1782 }
1783 Event::HardBreak | Event::SoftBreak => s.push(' '),
1784 Event::Start(Tag::CodeBlock(..)) => break,
1785 Event::End(TagEnd::Paragraph) => break,
1786 Event::End(TagEnd::Heading(..)) => break,
1787 _ => (),
1788 }
1789 }
1790}
1791
1792#[derive(Debug)]
1793pub(crate) struct MarkdownLink {
1794 pub kind: LinkType,
1795 pub link: String,
1796 pub range: MarkdownLinkRange,
1797}
1798
1799#[derive(Clone, Debug)]
1800pub(crate) enum MarkdownLinkRange {
1801 Destination(Range<usize>),
1803 WholeLink(Range<usize>),
1807}
1808
1809impl MarkdownLinkRange {
1810 pub fn inner_range(&self) -> &Range<usize> {
1812 match self {
1813 MarkdownLinkRange::Destination(range) => range,
1814 MarkdownLinkRange::WholeLink(range) => range,
1815 }
1816 }
1817}
1818
1819pub(crate) fn markdown_links<'md, R>(
1820 md: &'md str,
1821 preprocess_link: impl Fn(MarkdownLink) -> Option<R>,
1822) -> Vec<R> {
1823 use itertools::Itertools;
1824 if md.is_empty() {
1825 return vec![];
1826 }
1827
1828 let locate = |s: &str, fallback: Range<usize>| unsafe {
1830 let s_start = s.as_ptr();
1831 let s_end = s_start.add(s.len());
1832 let md_start = md.as_ptr();
1833 let md_end = md_start.add(md.len());
1834 if md_start <= s_start && s_end <= md_end {
1835 let start = s_start.offset_from(md_start) as usize;
1836 let end = s_end.offset_from(md_start) as usize;
1837 MarkdownLinkRange::Destination(start..end)
1838 } else {
1839 MarkdownLinkRange::WholeLink(fallback)
1840 }
1841 };
1842
1843 let span_for_link = |link: &CowStr<'_>, span: Range<usize>| {
1844 match link {
1849 CowStr::Borrowed(s) => locate(s, span),
1854
1855 CowStr::Boxed(_) | CowStr::Inlined(_) => MarkdownLinkRange::WholeLink(span),
1857 }
1858 };
1859
1860 let span_for_refdef = |link: &CowStr<'_>, span: Range<usize>| {
1861 let mut square_brace_count = 0;
1864 let mut iter = md.as_bytes()[span.start..span.end].iter().copied().enumerate();
1865 for (_i, c) in &mut iter {
1866 match c {
1867 b':' if square_brace_count == 0 => break,
1868 b'[' => square_brace_count += 1,
1869 b']' => square_brace_count -= 1,
1870 _ => {}
1871 }
1872 }
1873 while let Some((i, c)) = iter.next() {
1874 if c == b'<' {
1875 while let Some((j, c)) = iter.next() {
1876 match c {
1877 b'\\' => {
1878 let _ = iter.next();
1879 }
1880 b'>' => {
1881 return MarkdownLinkRange::Destination(
1882 i + 1 + span.start..j + span.start,
1883 );
1884 }
1885 _ => {}
1886 }
1887 }
1888 } else if !c.is_ascii_whitespace() {
1889 for (j, c) in iter.by_ref() {
1890 if c.is_ascii_whitespace() {
1891 return MarkdownLinkRange::Destination(i + span.start..j + span.start);
1892 }
1893 }
1894 return MarkdownLinkRange::Destination(i + span.start..span.end);
1895 }
1896 }
1897 span_for_link(link, span)
1898 };
1899
1900 let span_for_offset_backward = |span: Range<usize>, open: u8, close: u8| {
1901 let mut open_brace = !0;
1902 let mut close_brace = !0;
1903 for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate().rev() {
1904 let i = i + span.start;
1905 if b == close {
1906 close_brace = i;
1907 break;
1908 }
1909 }
1910 if close_brace < span.start || close_brace >= span.end {
1911 return MarkdownLinkRange::WholeLink(span);
1912 }
1913 let mut nesting = 1;
1914 for (i, b) in md.as_bytes()[span.start..close_brace].iter().copied().enumerate().rev() {
1915 let i = i + span.start;
1916 if b == close {
1917 nesting += 1;
1918 }
1919 if b == open {
1920 nesting -= 1;
1921 }
1922 if nesting == 0 {
1923 open_brace = i;
1924 break;
1925 }
1926 }
1927 assert!(open_brace != close_brace);
1928 if open_brace < span.start || open_brace >= span.end {
1929 return MarkdownLinkRange::WholeLink(span);
1930 }
1931 let range = (open_brace + 1)..close_brace;
1933 MarkdownLinkRange::Destination(range)
1934 };
1935
1936 let span_for_offset_forward = |span: Range<usize>, open: u8, close: u8| {
1937 let mut open_brace = !0;
1938 let mut close_brace = !0;
1939 for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate() {
1940 let i = i + span.start;
1941 if b == open {
1942 open_brace = i;
1943 break;
1944 }
1945 }
1946 if open_brace < span.start || open_brace >= span.end {
1947 return MarkdownLinkRange::WholeLink(span);
1948 }
1949 let mut nesting = 0;
1950 for (i, b) in md.as_bytes()[open_brace..span.end].iter().copied().enumerate() {
1951 let i = i + open_brace;
1952 if b == close {
1953 nesting -= 1;
1954 }
1955 if b == open {
1956 nesting += 1;
1957 }
1958 if nesting == 0 {
1959 close_brace = i;
1960 break;
1961 }
1962 }
1963 assert!(open_brace != close_brace);
1964 if open_brace < span.start || open_brace >= span.end {
1965 return MarkdownLinkRange::WholeLink(span);
1966 }
1967 let range = (open_brace + 1)..close_brace;
1969 MarkdownLinkRange::Destination(range)
1970 };
1971
1972 let mut broken_link_callback = |link: BrokenLink<'md>| Some((link.reference, "".into()));
1973 let event_iter = Parser::new_with_broken_link_callback(
1974 md,
1975 main_body_opts(),
1976 Some(&mut broken_link_callback),
1977 )
1978 .into_offset_iter();
1979 let mut links = Vec::new();
1980
1981 let mut refdefs = FxIndexMap::default();
1982 for (label, refdef) in event_iter.reference_definitions().iter().sorted_by_key(|x| x.0) {
1983 refdefs.insert(label.to_string(), (false, refdef.dest.to_string(), refdef.span.clone()));
1984 }
1985
1986 for (event, span) in event_iter {
1987 match event {
1988 Event::Start(Tag::Link { link_type, dest_url, id, .. })
1989 if may_be_doc_link(link_type) =>
1990 {
1991 let range = match link_type {
1992 LinkType::ReferenceUnknown | LinkType::ShortcutUnknown => {
1994 span_for_offset_backward(span, b'[', b']')
1995 }
1996 LinkType::CollapsedUnknown => span_for_offset_forward(span, b'[', b']'),
1997 LinkType::Inline => span_for_offset_backward(span, b'(', b')'),
1998 LinkType::Reference | LinkType::Collapsed | LinkType::Shortcut => {
2000 if let Some((is_used, dest_url, span)) = refdefs.get_mut(&id[..]) {
2001 *is_used = true;
2002 span_for_refdef(&CowStr::from(&dest_url[..]), span.clone())
2003 } else {
2004 span_for_link(&dest_url, span)
2005 }
2006 }
2007 LinkType::Autolink | LinkType::Email => unreachable!(),
2008 };
2009
2010 if let Some(link) = preprocess_link(MarkdownLink {
2011 kind: link_type,
2012 link: dest_url.into_string(),
2013 range,
2014 }) {
2015 links.push(link);
2016 }
2017 }
2018 _ => {}
2019 }
2020 }
2021
2022 for (_label, (is_used, dest_url, span)) in refdefs.into_iter() {
2023 if !is_used
2024 && let Some(link) = preprocess_link(MarkdownLink {
2025 kind: LinkType::Reference,
2026 range: span_for_refdef(&CowStr::from(&dest_url[..]), span),
2027 link: dest_url,
2028 })
2029 {
2030 links.push(link);
2031 }
2032 }
2033
2034 links
2035}
2036
2037#[derive(Debug)]
2038pub(crate) struct RustCodeBlock {
2039 pub(crate) range: Range<usize>,
2042 pub(crate) code: Range<usize>,
2044 pub(crate) is_fenced: bool,
2045 pub(crate) lang_string: LangString,
2046}
2047
2048pub(crate) fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_, '_>) -> Vec<RustCodeBlock> {
2051 let mut code_blocks = vec![];
2052
2053 if md.is_empty() {
2054 return code_blocks;
2055 }
2056
2057 let mut p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
2058
2059 while let Some((event, offset)) = p.next() {
2060 if let Event::Start(Tag::CodeBlock(syntax)) = event {
2061 let (lang_string, code_start, code_end, range, is_fenced) = match syntax {
2062 CodeBlockKind::Fenced(syntax) => {
2063 let syntax = syntax.as_ref();
2064 let lang_string = if syntax.is_empty() {
2065 Default::default()
2066 } else {
2067 LangString::parse(syntax, ErrorCodes::Yes, Some(extra_info))
2068 };
2069 if !lang_string.rust {
2070 continue;
2071 }
2072 let (code_start, mut code_end) = match p.next() {
2073 Some((Event::Text(_), offset)) => (offset.start, offset.end),
2074 Some((_, sub_offset)) => {
2075 let code = Range { start: sub_offset.start, end: sub_offset.start };
2076 code_blocks.push(RustCodeBlock {
2077 is_fenced: true,
2078 range: offset,
2079 code,
2080 lang_string,
2081 });
2082 continue;
2083 }
2084 None => {
2085 let code = Range { start: offset.end, end: offset.end };
2086 code_blocks.push(RustCodeBlock {
2087 is_fenced: true,
2088 range: offset,
2089 code,
2090 lang_string,
2091 });
2092 continue;
2093 }
2094 };
2095 while let Some((Event::Text(_), offset)) = p.next() {
2096 code_end = offset.end;
2097 }
2098 (lang_string, code_start, code_end, offset, true)
2099 }
2100 CodeBlockKind::Indented => {
2101 if offset.end > offset.start && md.get(offset.end..=offset.end) == Some("\n") {
2104 (
2105 LangString::default(),
2106 offset.start,
2107 offset.end,
2108 Range { start: offset.start, end: offset.end - 1 },
2109 false,
2110 )
2111 } else {
2112 (LangString::default(), offset.start, offset.end, offset, false)
2113 }
2114 }
2115 };
2116
2117 code_blocks.push(RustCodeBlock {
2118 is_fenced,
2119 range,
2120 code: Range { start: code_start, end: code_end },
2121 lang_string,
2122 });
2123 }
2124 }
2125
2126 code_blocks
2127}
2128
2129#[derive(Clone, Default, Debug)]
2130pub struct IdMap {
2131 map: FxHashMap<String, usize>,
2132 existing_footnotes: Arc<AtomicUsize>,
2133}
2134
2135fn is_default_id(id: &str) -> bool {
2136 matches!(
2137 id,
2138 "help"
2140 | "settings"
2141 | "not-displayed"
2142 | "alternative-display"
2143 | "search"
2144 | "crate-search"
2145 | "crate-search-div"
2146 | "themeStyle"
2149 | "settings-menu"
2150 | "help-button"
2151 | "sidebar-button"
2152 | "main-content"
2153 | "toggle-all-docs"
2154 | "all-types"
2155 | "default-settings"
2156 | "sidebar-vars"
2157 | "copy-path"
2158 | "rustdoc-toc"
2159 | "rustdoc-modnav"
2160 | "fields"
2163 | "variants"
2164 | "implementors-list"
2165 | "synthetic-implementors-list"
2166 | "foreign-impls"
2167 | "implementations"
2168 | "trait-implementations"
2169 | "synthetic-implementations"
2170 | "blanket-implementations"
2171 | "required-associated-types"
2172 | "provided-associated-types"
2173 | "provided-associated-consts"
2174 | "required-associated-consts"
2175 | "required-methods"
2176 | "provided-methods"
2177 | "dyn-compatibility"
2178 | "implementors"
2179 | "synthetic-implementors"
2180 | "implementations-list"
2181 | "trait-implementations-list"
2182 | "synthetic-implementations-list"
2183 | "blanket-implementations-list"
2184 | "deref-methods"
2185 | "layout"
2186 | "aliased-type",
2187 )
2188}
2189
2190impl IdMap {
2191 pub fn new() -> Self {
2192 IdMap { map: FxHashMap::default(), existing_footnotes: Arc::new(AtomicUsize::new(0)) }
2193 }
2194
2195 pub(crate) fn derive<S: AsRef<str> + ToString>(&mut self, candidate: S) -> String {
2196 let id = match self.map.get_mut(candidate.as_ref()) {
2197 None => {
2198 let candidate = candidate.to_string();
2199 if is_default_id(&candidate) {
2200 let id = format!("{}-{}", candidate, 1);
2201 self.map.insert(candidate, 2);
2202 id
2203 } else {
2204 candidate
2205 }
2206 }
2207 Some(a) => {
2208 let id = format!("{}-{}", candidate.as_ref(), *a);
2209 *a += 1;
2210 id
2211 }
2212 };
2213
2214 self.map.insert(id.clone(), 1);
2215 id
2216 }
2217
2218 pub(crate) fn handle_footnotes<'a, T, F: FnOnce(&'a mut Self, Weak<AtomicUsize>) -> T>(
2221 &'a mut self,
2222 closure: F,
2223 ) -> T {
2224 let existing_footnotes = Arc::downgrade(&self.existing_footnotes);
2225
2226 closure(self, existing_footnotes)
2227 }
2228
2229 pub(crate) fn clear(&mut self) {
2230 self.map.clear();
2231 self.existing_footnotes = Arc::new(AtomicUsize::new(0));
2232 }
2233}