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::may_be_doc_link;
45use rustc_resolve::rustdoc::pulldown_cmark::{
46 self, BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd, html,
47};
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 )
356 );
357 Some(Event::Html(s.into()))
358 }
359}
360
361struct LinkReplacerInner<'a> {
363 links: &'a [RenderedLink],
364 shortcut_link: Option<&'a RenderedLink>,
365}
366
367struct LinkReplacer<'a, I: Iterator<Item = Event<'a>>> {
368 iter: I,
369 inner: LinkReplacerInner<'a>,
370}
371
372impl<'a, I: Iterator<Item = Event<'a>>> LinkReplacer<'a, I> {
373 fn new(iter: I, links: &'a [RenderedLink]) -> Self {
374 LinkReplacer { iter, inner: { LinkReplacerInner { links, shortcut_link: None } } }
375 }
376}
377
378struct SpannedLinkReplacer<'a, I: Iterator<Item = SpannedEvent<'a>>> {
381 iter: I,
382 inner: LinkReplacerInner<'a>,
383}
384
385impl<'a, I: Iterator<Item = SpannedEvent<'a>>> SpannedLinkReplacer<'a, I> {
386 fn new(iter: I, links: &'a [RenderedLink]) -> Self {
387 SpannedLinkReplacer { iter, inner: { LinkReplacerInner { links, shortcut_link: None } } }
388 }
389}
390
391impl<'a> LinkReplacerInner<'a> {
392 fn handle_event(&mut self, event: &mut Event<'a>) {
393 match event {
395 Event::Start(Tag::Link {
398 link_type: LinkType::ShortcutUnknown | LinkType::CollapsedUnknown,
400 dest_url,
401 title,
402 ..
403 }) => {
404 debug!("saw start of shortcut link to {dest_url} with title {title}");
405 let link = self.links.iter().find(|&link| *link.href == **dest_url);
408 if let Some(link) = link {
411 trace!("it matched");
412 assert!(self.shortcut_link.is_none(), "shortcut links cannot be nested");
413 self.shortcut_link = Some(link);
414 if title.is_empty() && !link.tooltip.is_empty() {
415 *title = CowStr::Borrowed(link.tooltip.as_ref());
416 }
417 }
418 }
419 Event::End(TagEnd::Link) if self.shortcut_link.is_some() => {
421 debug!("saw end of shortcut link");
422 self.shortcut_link = None;
423 }
424 Event::Code(text) => {
427 trace!("saw code {text}");
428 if let Some(link) = self.shortcut_link {
429 if let Some(link) = self.links.iter().find(|l| {
439 l.href == link.href
440 && Some(&**text) == l.original_text.get(1..l.original_text.len() - 1)
441 }) {
442 debug!("replacing {text} with {new_text}", new_text = link.new_text);
443 *text = CowStr::Borrowed(&link.new_text);
444 }
445 }
446 }
447 Event::Text(text) => {
450 trace!("saw text {text}");
451 if let Some(link) = self.shortcut_link {
452 if let Some(link) = self
454 .links
455 .iter()
456 .find(|l| l.href == link.href && **text == *l.original_text)
457 {
458 debug!("replacing {text} with {new_text}", new_text = link.new_text);
459 *text = CowStr::Borrowed(&link.new_text);
460 }
461 }
462 }
463 Event::Start(Tag::Link { dest_url, title, .. }) => {
466 if let Some(link) =
467 self.links.iter().find(|&link| *link.original_text == **dest_url)
468 {
469 *dest_url = CowStr::Borrowed(link.href.as_ref());
470 if title.is_empty() && !link.tooltip.is_empty() {
471 *title = CowStr::Borrowed(link.tooltip.as_ref());
472 }
473 }
474 }
475 _ => {}
477 }
478 }
479}
480
481impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
482 type Item = Event<'a>;
483
484 fn next(&mut self) -> Option<Self::Item> {
485 let mut event = self.iter.next();
486 if let Some(ref mut event) = event {
487 self.inner.handle_event(event);
488 }
489 event
491 }
492}
493
494impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for SpannedLinkReplacer<'a, I> {
495 type Item = SpannedEvent<'a>;
496
497 fn next(&mut self) -> Option<Self::Item> {
498 let (mut event, range) = self.iter.next()?;
499 self.inner.handle_event(&mut event);
500 Some((event, range))
502 }
503}
504
505struct TableWrapper<'a, I: Iterator<Item = Event<'a>>> {
507 inner: I,
508 stored_events: VecDeque<Event<'a>>,
509}
510
511impl<'a, I: Iterator<Item = Event<'a>>> TableWrapper<'a, I> {
512 fn new(iter: I) -> Self {
513 Self { inner: iter, stored_events: VecDeque::new() }
514 }
515}
516
517impl<'a, I: Iterator<Item = Event<'a>>> Iterator for TableWrapper<'a, I> {
518 type Item = Event<'a>;
519
520 fn next(&mut self) -> Option<Self::Item> {
521 if let Some(first) = self.stored_events.pop_front() {
522 return Some(first);
523 }
524
525 let event = self.inner.next()?;
526
527 Some(match event {
528 Event::Start(Tag::Table(t)) => {
529 self.stored_events.push_back(Event::Start(Tag::Table(t)));
530 Event::Html(CowStr::Borrowed("<div>"))
531 }
532 Event::End(TagEnd::Table) => {
533 self.stored_events.push_back(Event::Html(CowStr::Borrowed("</div>")));
534 Event::End(TagEnd::Table)
535 }
536 e => e,
537 })
538 }
539}
540
541type SpannedEvent<'a> = (Event<'a>, Range<usize>);
542
543struct HeadingLinks<'a, 'b, 'ids, I> {
545 inner: I,
546 toc: Option<&'b mut TocBuilder>,
547 buf: VecDeque<SpannedEvent<'a>>,
548 id_map: &'ids mut IdMap,
549 heading_offset: HeadingOffset,
550}
551
552impl<'b, 'ids, I> HeadingLinks<'_, 'b, 'ids, I> {
553 fn new(
554 iter: I,
555 toc: Option<&'b mut TocBuilder>,
556 ids: &'ids mut IdMap,
557 heading_offset: HeadingOffset,
558 ) -> Self {
559 HeadingLinks { inner: iter, toc, buf: VecDeque::new(), id_map: ids, heading_offset }
560 }
561}
562
563impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for HeadingLinks<'a, '_, '_, I> {
564 type Item = SpannedEvent<'a>;
565
566 fn next(&mut self) -> Option<Self::Item> {
567 if let Some(e) = self.buf.pop_front() {
568 return Some(e);
569 }
570
571 let event = self.inner.next();
572 if let Some((Event::Start(Tag::Heading { level, .. }), _)) = event {
573 let mut id = String::new();
574 for event in &mut self.inner {
575 match &event.0 {
576 Event::End(TagEnd::Heading(_)) => break,
577 Event::Text(text) | Event::Code(text) => {
578 id.extend(text.chars().filter_map(slugify));
579 self.buf.push_back(event);
580 }
581 _ => self.buf.push_back(event),
582 }
583 }
584 let id = self.id_map.derive(id);
585 let percent_encoded_id = small_url_encode(id.clone());
586
587 if let Some(ref mut builder) = self.toc {
588 let mut text_header = String::new();
589 plain_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut text_header);
590 let mut html_header = String::new();
591 html_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut html_header);
592 let sec = builder.push(level as u32, text_header, html_header, id.clone());
593 self.buf.push_front((Event::Html(format!("{sec} ").into()), 0..0));
594 }
595
596 let level =
597 std::cmp::min(level as u32 + (self.heading_offset as u32), MAX_HEADER_LEVEL);
598 self.buf.push_back((Event::Html(format!("</h{level}>").into()), 0..0));
599
600 let start_tags = format!(
601 "<h{level} id=\"{id}\"><a class=\"doc-anchor\" href=\"#{percent_encoded_id}\">§</a>"
602 );
603 return Some((Event::Html(start_tags.into()), 0..0));
604 }
605 event
606 }
607}
608
609struct SummaryLine<'a, I: Iterator<Item = Event<'a>>> {
611 inner: I,
612 started: bool,
613 depth: u32,
614 skipped_tags: u32,
615}
616
617impl<'a, I: Iterator<Item = Event<'a>>> SummaryLine<'a, I> {
618 fn new(iter: I) -> Self {
619 SummaryLine { inner: iter, started: false, depth: 0, skipped_tags: 0 }
620 }
621}
622
623fn check_if_allowed_tag(t: &TagEnd) -> bool {
624 matches!(
625 t,
626 TagEnd::Paragraph
627 | TagEnd::Emphasis
628 | TagEnd::Strong
629 | TagEnd::Strikethrough
630 | TagEnd::Link
631 | TagEnd::BlockQuote
632 )
633}
634
635fn is_forbidden_tag(t: &TagEnd) -> bool {
636 matches!(
637 t,
638 TagEnd::CodeBlock
639 | TagEnd::Table
640 | TagEnd::TableHead
641 | TagEnd::TableRow
642 | TagEnd::TableCell
643 | TagEnd::FootnoteDefinition
644 )
645}
646
647impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SummaryLine<'a, I> {
648 type Item = Event<'a>;
649
650 fn next(&mut self) -> Option<Self::Item> {
651 if self.started && self.depth == 0 {
652 return None;
653 }
654 if !self.started {
655 self.started = true;
656 }
657 if let Some(event) = self.inner.next() {
658 let mut is_start = true;
659 let is_allowed_tag = match event {
660 Event::Start(ref c) => {
661 if is_forbidden_tag(&c.to_end()) {
662 self.skipped_tags += 1;
663 return None;
664 }
665 self.depth += 1;
666 check_if_allowed_tag(&c.to_end())
667 }
668 Event::End(ref c) => {
669 if is_forbidden_tag(c) {
670 self.skipped_tags += 1;
671 return None;
672 }
673 self.depth -= 1;
674 is_start = false;
675 check_if_allowed_tag(c)
676 }
677 Event::FootnoteReference(_) => {
678 self.skipped_tags += 1;
679 false
680 }
681 _ => true,
682 };
683 if !is_allowed_tag {
684 self.skipped_tags += 1;
685 }
686 return if !is_allowed_tag {
687 if is_start {
688 Some(Event::Start(Tag::Paragraph))
689 } else {
690 Some(Event::End(TagEnd::Paragraph))
691 }
692 } else {
693 Some(event)
694 };
695 }
696 None
697 }
698}
699
700pub(crate) struct MdRelLine {
707 offset: usize,
708}
709
710impl MdRelLine {
711 pub(crate) const fn new(offset: usize) -> Self {
713 Self { offset }
714 }
715
716 pub(crate) const fn offset(self) -> usize {
718 self.offset
719 }
720}
721
722pub(crate) fn find_testable_code<T: doctest::DocTestVisitor>(
723 doc: &str,
724 tests: &mut T,
725 error_codes: ErrorCodes,
726 extra_info: Option<&ExtraInfo<'_>>,
727) {
728 find_codes(doc, tests, error_codes, extra_info, false)
729}
730
731pub(crate) fn find_codes<T: doctest::DocTestVisitor>(
732 doc: &str,
733 tests: &mut T,
734 error_codes: ErrorCodes,
735 extra_info: Option<&ExtraInfo<'_>>,
736 include_non_rust: bool,
737) {
738 let mut parser = Parser::new_ext(doc, main_body_opts()).into_offset_iter();
739 let mut prev_offset = 0;
740 let mut nb_lines = 0;
741 let mut register_header = None;
742 while let Some((event, offset)) = parser.next() {
743 match event {
744 Event::Start(Tag::CodeBlock(kind)) => {
745 let block_info = match kind {
746 CodeBlockKind::Fenced(ref lang) => {
747 if lang.is_empty() {
748 Default::default()
749 } else {
750 LangString::parse(lang, error_codes, extra_info)
751 }
752 }
753 CodeBlockKind::Indented => Default::default(),
754 };
755 if !include_non_rust && !block_info.rust {
756 continue;
757 }
758
759 let mut test_s = String::new();
760
761 while let Some((Event::Text(s), _)) = parser.next() {
762 test_s.push_str(&s);
763 }
764 let text = test_s
765 .lines()
766 .map(|l| map_line(l).for_code())
767 .collect::<Vec<Cow<'_, str>>>()
768 .join("\n");
769
770 nb_lines += doc[prev_offset..offset.start].lines().count();
771 if nb_lines != 0 && !&doc[prev_offset..offset.start].ends_with('\n') {
775 nb_lines -= 1;
776 }
777 let line = MdRelLine::new(nb_lines);
778 tests.visit_test(text, block_info, line);
779 prev_offset = offset.start;
780 }
781 Event::Start(Tag::Heading { level, .. }) => {
782 register_header = Some(level as u32);
783 }
784 Event::Text(ref s) if register_header.is_some() => {
785 let level = register_header.unwrap();
786 tests.visit_header(s, level);
787 register_header = None;
788 }
789 _ => {}
790 }
791 }
792}
793
794pub(crate) struct ExtraInfo<'tcx> {
795 def_id: LocalDefId,
796 sp: Span,
797 tcx: TyCtxt<'tcx>,
798}
799
800impl<'tcx> ExtraInfo<'tcx> {
801 pub(crate) fn new(tcx: TyCtxt<'tcx>, def_id: LocalDefId, sp: Span) -> ExtraInfo<'tcx> {
802 ExtraInfo { def_id, sp, tcx }
803 }
804
805 fn error_invalid_codeblock_attr(&self, msg: impl Into<DiagMessage>) {
806 self.error_invalid_codeblock_attr_with_help(msg, |_| {});
807 }
808
809 fn error_invalid_codeblock_attr_with_help(
810 &self,
811 msg: impl Into<DiagMessage>,
812 f: impl for<'a, 'b> FnOnce(&'b mut Diag<'a, ()>),
813 ) {
814 self.tcx.emit_node_span_lint(
815 crate::lint::INVALID_CODEBLOCK_ATTRIBUTES,
816 self.tcx.local_def_id_to_hir_id(self.def_id),
817 self.sp,
818 rustc_errors::DiagDecorator(|lint| {
819 lint.primary_message(msg);
820 f(lint);
821 }),
822 );
823 }
824}
825
826#[derive(Eq, PartialEq, Clone, Debug)]
827pub(crate) struct LangString {
828 pub(crate) original: String,
829 pub(crate) should_panic: bool,
830 pub(crate) no_run: bool,
831 pub(crate) ignore: Ignore,
832 pub(crate) rust: bool,
833 pub(crate) test_harness: bool,
834 pub(crate) compile_fail: bool,
835 pub(crate) standalone_crate: bool,
836 pub(crate) error_codes: Vec<String>,
837 pub(crate) edition: Option<Edition>,
838 pub(crate) added_classes: Vec<String>,
839 pub(crate) unknown: Vec<String>,
840}
841
842#[derive(Eq, PartialEq, Clone, Debug)]
843pub(crate) enum Ignore {
844 All,
845 None,
846 Some(Vec<String>),
847}
848
849pub(crate) struct TagIterator<'a, 'tcx> {
889 inner: Peekable<CharIndices<'a>>,
890 data: &'a str,
891 is_in_attribute_block: bool,
892 extra: Option<&'a ExtraInfo<'tcx>>,
893 is_error: bool,
894}
895
896#[derive(Clone, Debug, Eq, PartialEq)]
897pub(crate) enum LangStringToken<'a> {
898 LangToken(&'a str),
899 ClassAttribute(&'a str),
900 KeyValueAttribute(&'a str, &'a str),
901}
902
903fn is_leading_char(c: char) -> bool {
904 c == '_' || c == '-' || c == ':' || c.is_ascii_alphabetic() || c.is_ascii_digit()
905}
906fn is_bareword_char(c: char) -> bool {
907 is_leading_char(c) || ".!#$%&*+/;<>?@^|~".contains(c)
908}
909fn is_separator(c: char) -> bool {
910 c == ' ' || c == ',' || c == '\t'
911}
912
913struct Indices {
914 start: usize,
915 end: usize,
916}
917
918impl<'a, 'tcx> TagIterator<'a, 'tcx> {
919 pub(crate) fn new(data: &'a str, extra: Option<&'a ExtraInfo<'tcx>>) -> Self {
920 Self {
921 inner: data.char_indices().peekable(),
922 data,
923 is_in_attribute_block: false,
924 extra,
925 is_error: false,
926 }
927 }
928
929 fn emit_error(&mut self, err: impl Into<DiagMessage>) {
930 if let Some(extra) = self.extra {
931 extra.error_invalid_codeblock_attr(err);
932 }
933 self.is_error = true;
934 }
935
936 fn skip_separators(&mut self) -> Option<usize> {
937 while let Some((pos, c)) = self.inner.peek() {
938 if !is_separator(*c) {
939 return Some(*pos);
940 }
941 self.inner.next();
942 }
943 None
944 }
945
946 fn parse_string(&mut self, start: usize) -> Option<Indices> {
947 for (pos, c) in self.inner.by_ref() {
948 if c == '"' {
949 return Some(Indices { start: start + 1, end: pos });
950 }
951 }
952 self.emit_error("unclosed quote string `\"`");
953 None
954 }
955
956 fn parse_class(&mut self, start: usize) -> Option<LangStringToken<'a>> {
957 while let Some((pos, c)) = self.inner.peek().copied() {
958 if is_bareword_char(c) {
959 self.inner.next();
960 } else {
961 let class = &self.data[start + 1..pos];
962 if class.is_empty() {
963 self.emit_error(format!("unexpected `{c}` character after `.`"));
964 return None;
965 } else if self.check_after_token() {
966 return Some(LangStringToken::ClassAttribute(class));
967 } else {
968 return None;
969 }
970 }
971 }
972 let class = &self.data[start + 1..];
973 if class.is_empty() {
974 self.emit_error("missing character after `.`");
975 None
976 } else if self.check_after_token() {
977 Some(LangStringToken::ClassAttribute(class))
978 } else {
979 None
980 }
981 }
982
983 fn parse_token(&mut self, start: usize) -> Option<Indices> {
984 while let Some((pos, c)) = self.inner.peek() {
985 if !is_bareword_char(*c) {
986 return Some(Indices { start, end: *pos });
987 }
988 self.inner.next();
989 }
990 self.emit_error("unexpected end");
991 None
992 }
993
994 fn parse_key_value(&mut self, c: char, start: usize) -> Option<LangStringToken<'a>> {
995 let key_indices =
996 if c == '"' { self.parse_string(start)? } else { self.parse_token(start)? };
997 if key_indices.start == key_indices.end {
998 self.emit_error("unexpected empty string as key");
999 return None;
1000 }
1001
1002 if let Some((_, c)) = self.inner.next() {
1003 if c != '=' {
1004 self.emit_error(format!("expected `=`, found `{c}`"));
1005 return None;
1006 }
1007 } else {
1008 self.emit_error("unexpected end");
1009 return None;
1010 }
1011 let value_indices = match self.inner.next() {
1012 Some((pos, '"')) => self.parse_string(pos)?,
1013 Some((pos, c)) if is_bareword_char(c) => self.parse_token(pos)?,
1014 Some((_, c)) => {
1015 self.emit_error(format!("unexpected `{c}` character after `=`"));
1016 return None;
1017 }
1018 None => {
1019 self.emit_error("expected value after `=`");
1020 return None;
1021 }
1022 };
1023 if value_indices.start == value_indices.end {
1024 self.emit_error("unexpected empty string as value");
1025 None
1026 } else if self.check_after_token() {
1027 Some(LangStringToken::KeyValueAttribute(
1028 &self.data[key_indices.start..key_indices.end],
1029 &self.data[value_indices.start..value_indices.end],
1030 ))
1031 } else {
1032 None
1033 }
1034 }
1035
1036 fn check_after_token(&mut self) -> bool {
1038 if let Some((_, c)) = self.inner.peek().copied() {
1039 if c == '}' || is_separator(c) || c == '(' {
1040 true
1041 } else {
1042 self.emit_error(format!("unexpected `{c}` character"));
1043 false
1044 }
1045 } else {
1046 true
1048 }
1049 }
1050
1051 fn parse_in_attribute_block(&mut self) -> Option<LangStringToken<'a>> {
1052 if let Some((pos, c)) = self.inner.next() {
1053 if c == '}' {
1054 self.is_in_attribute_block = false;
1055 return self.next();
1056 } else if c == '.' {
1057 return self.parse_class(pos);
1058 } else if c == '"' || is_leading_char(c) {
1059 return self.parse_key_value(c, pos);
1060 } else {
1061 self.emit_error(format!("unexpected character `{c}`"));
1062 return None;
1063 }
1064 }
1065 self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
1066 None
1067 }
1068
1069 fn skip_paren_block(&mut self) -> bool {
1071 for (_, c) in self.inner.by_ref() {
1072 if c == ')' {
1073 return true;
1074 }
1075 }
1076 self.emit_error("unclosed comment: missing `)` at the end");
1077 false
1078 }
1079
1080 fn parse_outside_attribute_block(&mut self, start: usize) -> Option<LangStringToken<'a>> {
1081 while let Some((pos, c)) = self.inner.next() {
1082 if c == '"' {
1083 if pos != start {
1084 self.emit_error("expected ` `, `{` or `,` found `\"`");
1085 return None;
1086 }
1087 let indices = self.parse_string(pos)?;
1088 if let Some((_, c)) = self.inner.peek().copied()
1089 && c != '{'
1090 && !is_separator(c)
1091 && c != '('
1092 {
1093 self.emit_error(format!("expected ` `, `{{` or `,` after `\"`, found `{c}`"));
1094 return None;
1095 }
1096 return Some(LangStringToken::LangToken(&self.data[indices.start..indices.end]));
1097 } else if c == '{' {
1098 self.is_in_attribute_block = true;
1099 return self.next();
1100 } else if is_separator(c) {
1101 if pos != start {
1102 return Some(LangStringToken::LangToken(&self.data[start..pos]));
1103 }
1104 return self.next();
1105 } else if c == '(' {
1106 if !self.skip_paren_block() {
1107 return None;
1108 }
1109 if pos != start {
1110 return Some(LangStringToken::LangToken(&self.data[start..pos]));
1111 }
1112 return self.next();
1113 } else if (pos == start && is_leading_char(c)) || (pos != start && is_bareword_char(c))
1114 {
1115 continue;
1116 } else {
1117 self.emit_error(format!("unexpected character `{c}`"));
1118 return None;
1119 }
1120 }
1121 let token = &self.data[start..];
1122 if token.is_empty() { None } else { Some(LangStringToken::LangToken(&self.data[start..])) }
1123 }
1124}
1125
1126impl<'a> Iterator for TagIterator<'a, '_> {
1127 type Item = LangStringToken<'a>;
1128
1129 fn next(&mut self) -> Option<Self::Item> {
1130 if self.is_error {
1131 return None;
1132 }
1133 let Some(start) = self.skip_separators() else {
1134 if self.is_in_attribute_block {
1135 self.emit_error("unclosed attribute block (`{}`): missing `}` at the end");
1136 }
1137 return None;
1138 };
1139 if self.is_in_attribute_block {
1140 self.parse_in_attribute_block()
1141 } else {
1142 self.parse_outside_attribute_block(start)
1143 }
1144 }
1145}
1146
1147impl Default for LangString {
1148 fn default() -> Self {
1149 Self {
1150 original: String::new(),
1151 should_panic: false,
1152 no_run: false,
1153 ignore: Ignore::None,
1154 rust: true,
1155 test_harness: false,
1156 compile_fail: false,
1157 standalone_crate: false,
1158 error_codes: Vec::new(),
1159 edition: None,
1160 added_classes: Vec::new(),
1161 unknown: Vec::new(),
1162 }
1163 }
1164}
1165
1166impl LangString {
1167 fn parse_without_check(string: &str, allow_error_code_check: ErrorCodes) -> Self {
1168 Self::parse(string, allow_error_code_check, None)
1169 }
1170
1171 fn parse(
1172 string: &str,
1173 allow_error_code_check: ErrorCodes,
1174 extra: Option<&ExtraInfo<'_>>,
1175 ) -> Self {
1176 let allow_error_code_check = allow_error_code_check.as_bool();
1177 let mut seen_rust_tags = false;
1178 let mut seen_other_tags = false;
1179 let mut seen_custom_tag = false;
1180 let mut data = LangString::default();
1181 let mut ignores = vec![];
1182
1183 data.original = string.to_owned();
1184
1185 let mut call = |tokens: &mut dyn Iterator<Item = LangStringToken<'_>>| {
1186 for token in tokens {
1187 match token {
1188 LangStringToken::LangToken("should_panic") => {
1189 data.should_panic = true;
1190 seen_rust_tags = !seen_other_tags;
1191 }
1192 LangStringToken::LangToken("no_run") => {
1193 data.no_run = true;
1194 seen_rust_tags = !seen_other_tags;
1195 }
1196 LangStringToken::LangToken("ignore") => {
1197 data.ignore = Ignore::All;
1198 seen_rust_tags = !seen_other_tags;
1199 }
1200 LangStringToken::LangToken(x)
1201 if let Some(ignore) = x.strip_prefix("ignore-") =>
1202 {
1203 ignores.push(ignore.to_owned());
1204 seen_rust_tags = !seen_other_tags;
1205 }
1206 LangStringToken::LangToken("rust") => {
1207 data.rust = true;
1208 seen_rust_tags = true;
1209 }
1210 LangStringToken::LangToken("custom") => {
1211 seen_custom_tag = true;
1212 }
1213 LangStringToken::LangToken("test_harness") => {
1214 data.test_harness = true;
1215 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1216 }
1217 LangStringToken::LangToken("compile_fail") => {
1218 data.compile_fail = true;
1219 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1220 data.no_run = true;
1221 }
1222 LangStringToken::LangToken("standalone_crate") => {
1223 data.standalone_crate = true;
1224 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1225 }
1226 LangStringToken::LangToken(x)
1227 if let Some(edition) = x.strip_prefix("edition") =>
1228 {
1229 data.edition = edition.parse::<Edition>().ok();
1230 }
1231 LangStringToken::LangToken(x)
1232 if let Some(edition) = x.strip_prefix("rust")
1233 && edition.parse::<Edition>().is_ok()
1234 && let Some(extra) = extra =>
1235 {
1236 extra.error_invalid_codeblock_attr_with_help(
1237 format!("unknown attribute `{x}`"),
1238 |lint| {
1239 lint.help(format!(
1240 "there is an attribute with a similar name: `edition{edition}`"
1241 ));
1242 },
1243 );
1244 }
1245 LangStringToken::LangToken(x)
1246 if allow_error_code_check
1247 && let Some(error_code) = x.strip_prefix('E')
1248 && error_code.len() == 4 =>
1249 {
1250 if error_code.parse::<u32>().is_ok() {
1251 data.error_codes.push(x.to_owned());
1252 seen_rust_tags = !seen_other_tags || seen_rust_tags;
1253 } else {
1254 seen_other_tags = true;
1255 }
1256 }
1257 LangStringToken::LangToken(x) if let Some(extra) = extra => {
1258 if let Some(help) = match x.to_lowercase().as_str() {
1259 "compile-fail" | "compile_fail" | "compilefail" => Some(
1260 "use `compile_fail` to invert the results of this test, so that it \
1261 passes if it cannot be compiled and fails if it can",
1262 ),
1263 "should-panic" | "should_panic" | "shouldpanic" => Some(
1264 "use `should_panic` to invert the results of this test, so that if \
1265 passes if it panics and fails if it does not",
1266 ),
1267 "no-run" | "no_run" | "norun" => Some(
1268 "use `no_run` to compile, but not run, the code sample during \
1269 testing",
1270 ),
1271 "test-harness" | "test_harness" | "testharness" => Some(
1272 "use `test_harness` to run functions marked `#[test]` instead of a \
1273 potentially-implicit `main` function",
1274 ),
1275 "standalone" | "standalone_crate" | "standalone-crate"
1276 if extra.sp.at_least_rust_2024() =>
1277 {
1278 Some(
1279 "use `standalone_crate` to compile this code block \
1280 separately",
1281 )
1282 }
1283 _ => None,
1284 } {
1285 extra.error_invalid_codeblock_attr_with_help(
1286 format!("unknown attribute `{x}`"),
1287 |lint| {
1288 lint.help(help).help(
1289 "this code block may be skipped during testing, \
1290 because unknown attributes are treated as markers for \
1291 code samples written in other programming languages, \
1292 unless it is also explicitly marked as `rust`",
1293 );
1294 },
1295 );
1296 }
1297 seen_other_tags = true;
1298 data.unknown.push(x.to_owned());
1299 }
1300 LangStringToken::LangToken(x) => {
1301 seen_other_tags = true;
1302 data.unknown.push(x.to_owned());
1303 }
1304 LangStringToken::KeyValueAttribute("class", value) => {
1305 data.added_classes.push(value.to_owned());
1306 }
1307 LangStringToken::KeyValueAttribute(key, ..) if let Some(extra) = extra => {
1308 extra
1309 .error_invalid_codeblock_attr(format!("unsupported attribute `{key}`"));
1310 }
1311 LangStringToken::ClassAttribute(class) => {
1312 data.added_classes.push(class.to_owned());
1313 }
1314 _ => {}
1315 }
1316 }
1317 };
1318
1319 let mut tag_iter = TagIterator::new(string, extra);
1320 call(&mut tag_iter);
1321
1322 if !ignores.is_empty() {
1324 data.ignore = Ignore::Some(ignores);
1325 }
1326
1327 data.rust &= !seen_custom_tag && (!seen_other_tags || seen_rust_tags) && !tag_iter.is_error;
1328
1329 data
1330 }
1331}
1332
1333impl<'a> Markdown<'a> {
1334 pub fn write_into(self, f: impl fmt::Write) -> fmt::Result {
1335 if self.content.is_empty() {
1337 return Ok(());
1338 }
1339
1340 html::write_html_fmt(f, self.into_iter())
1341 }
1342
1343 fn into_iter(self) -> CodeBlocks<'a, 'a, impl Iterator<Item = Event<'a>>> {
1344 let Markdown {
1345 content: md,
1346 links,
1347 ids,
1348 error_codes: codes,
1349 edition,
1350 playground,
1351 heading_offset,
1352 } = self;
1353
1354 let replacer = move |broken_link: BrokenLink<'_>| {
1355 links
1356 .iter()
1357 .find(|link| *link.original_text == *broken_link.reference)
1358 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1359 };
1360
1361 let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(replacer));
1362 let p = p.into_offset_iter();
1363
1364 ids.handle_footnotes(|ids, existing_footnotes| {
1365 let p = HeadingLinks::new(p, None, ids, heading_offset);
1366 let p = SpannedLinkReplacer::new(p, links);
1367 let p = footnotes::Footnotes::new(p, existing_footnotes);
1368 let p = TableWrapper::new(p.map(|(ev, _)| ev));
1369 CodeBlocks::new(p, codes, edition, playground)
1370 })
1371 }
1372
1373 pub(crate) fn split_summary_and_content(self) -> (Option<String>, Option<String>) {
1379 if self.content.is_empty() {
1380 return (None, None);
1381 }
1382 let mut p = self.into_iter();
1383
1384 let mut event_level = 0;
1385 let mut summary_events = Vec::new();
1386 let mut get_next_tag = false;
1387
1388 let mut end_of_summary = false;
1389 while let Some(event) = p.next() {
1390 match event {
1391 Event::Start(_) => event_level += 1,
1392 Event::End(kind) => {
1393 event_level -= 1;
1394 if event_level == 0 {
1395 end_of_summary = true;
1397 get_next_tag = kind == TagEnd::Table;
1399 }
1400 }
1401 _ => {}
1402 }
1403 summary_events.push(event);
1404 if end_of_summary {
1405 if get_next_tag && let Some(event) = p.next() {
1406 summary_events.push(event);
1407 }
1408 break;
1409 }
1410 }
1411 let mut summary = String::new();
1412 html::push_html(&mut summary, summary_events.into_iter());
1413 if summary.is_empty() {
1414 return (None, None);
1415 }
1416 let mut content = String::new();
1417 html::push_html(&mut content, p);
1418
1419 if content.is_empty() { (Some(summary), None) } else { (Some(summary), Some(content)) }
1420 }
1421}
1422
1423impl MarkdownWithToc<'_> {
1424 pub(crate) fn into_parts(self) -> (Toc, String) {
1425 let MarkdownWithToc { content: md, links, ids, error_codes: codes, edition, playground } =
1426 self;
1427
1428 if md.is_empty() {
1430 return (Toc { entries: Vec::new() }, String::new());
1431 }
1432 let mut replacer = |broken_link: BrokenLink<'_>| {
1433 links
1434 .iter()
1435 .find(|link| *link.original_text == *broken_link.reference)
1436 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1437 };
1438
1439 let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
1440 let p = p.into_offset_iter();
1441
1442 let mut s = String::with_capacity(md.len() * 3 / 2);
1443
1444 let mut toc = TocBuilder::new();
1445
1446 ids.handle_footnotes(|ids, existing_footnotes| {
1447 let p = HeadingLinks::new(p, Some(&mut toc), ids, HeadingOffset::H1);
1448 let p = footnotes::Footnotes::new(p, existing_footnotes);
1449 let p = TableWrapper::new(p.map(|(ev, _)| ev));
1450 let p = CodeBlocks::new(p, codes, edition, playground);
1451 html::push_html(&mut s, p);
1452 });
1453
1454 (toc.into_toc(), s)
1455 }
1456
1457 pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
1458 let (toc, s) = self.into_parts();
1459 write!(f, "<nav id=\"rustdoc\">{toc}</nav>{s}", toc = toc.print())
1460 }
1461}
1462
1463impl<'a> MarkdownItemInfo<'a> {
1464 pub(crate) fn new(content: &'a str, links: &'a [RenderedLink], ids: &'a mut IdMap) -> Self {
1465 Self { content, links, ids }
1466 }
1467
1468 pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
1469 let MarkdownItemInfo { content: md, links, ids } = self;
1470
1471 if md.is_empty() {
1473 return Ok(());
1474 }
1475
1476 let replacer = move |broken_link: BrokenLink<'_>| {
1477 links
1478 .iter()
1479 .find(|link| *link.original_text == *broken_link.reference)
1480 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1481 };
1482
1483 let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(replacer));
1484 let p = p.into_offset_iter();
1485
1486 let p = p.map(|event| match event.0 {
1488 Event::Html(text) | Event::InlineHtml(text) => (Event::Text(text), event.1),
1489 _ => event,
1490 });
1491
1492 ids.handle_footnotes(|ids, existing_footnotes| {
1493 let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
1494 let p = SpannedLinkReplacer::new(p, links);
1495 let p = footnotes::Footnotes::new(p, existing_footnotes);
1496 let p = TableWrapper::new(p.map(|(ev, _)| ev));
1497 html::write_html_fmt(&mut f, p)?;
1499
1500 Ok(())
1501 })
1502 }
1503}
1504
1505impl MarkdownSummaryLine<'_> {
1506 pub(crate) fn into_string_with_has_more_content(self) -> (String, bool) {
1507 let MarkdownSummaryLine(md, links) = self;
1508 if md.is_empty() {
1510 return (String::new(), false);
1511 }
1512
1513 let mut replacer = |broken_link: BrokenLink<'_>| {
1514 links
1515 .iter()
1516 .find(|link| *link.original_text == *broken_link.reference)
1517 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1518 };
1519
1520 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer))
1521 .peekable();
1522 let mut summary = SummaryLine::new(p);
1523
1524 let mut s = String::new();
1525
1526 let without_paragraphs = LinkReplacer::new(&mut summary, links).filter(|event| {
1527 !matches!(event, Event::Start(Tag::Paragraph) | Event::End(TagEnd::Paragraph))
1528 });
1529
1530 html::push_html(&mut s, without_paragraphs);
1531
1532 let has_more_content =
1533 matches!(summary.inner.peek(), Some(Event::Start(_))) || summary.skipped_tags > 0;
1534
1535 (s, has_more_content)
1536 }
1537
1538 pub(crate) fn into_string(self) -> String {
1539 self.into_string_with_has_more_content().0
1540 }
1541}
1542
1543fn markdown_summary_with_limit(
1552 md: &str,
1553 link_names: &[RenderedLink],
1554 length_limit: usize,
1555) -> (String, bool) {
1556 if md.is_empty() {
1557 return (String::new(), false);
1558 }
1559
1560 let mut replacer = |broken_link: BrokenLink<'_>| {
1561 link_names
1562 .iter()
1563 .find(|link| *link.original_text == *broken_link.reference)
1564 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1565 };
1566
1567 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
1568 let mut p = LinkReplacer::new(p, link_names);
1569
1570 let mut buf = HtmlWithLimit::new(length_limit);
1571 let mut stopped_early = false;
1572 let _ = p.try_for_each(|event| {
1573 match &event {
1574 Event::Text(text) => {
1575 let r =
1576 text.split_inclusive(char::is_whitespace).try_for_each(|word| buf.push(word));
1577 if r.is_break() {
1578 stopped_early = true;
1579 }
1580 return r;
1581 }
1582 Event::Code(code) => {
1583 buf.open_tag("code");
1584 let r = buf.push(code);
1585 if r.is_break() {
1586 stopped_early = true;
1587 } else {
1588 buf.close_tag();
1589 }
1590 return r;
1591 }
1592 Event::Start(tag) => match tag {
1593 Tag::Emphasis => buf.open_tag("em"),
1594 Tag::Strong => buf.open_tag("strong"),
1595 Tag::CodeBlock(..) => return ControlFlow::Break(()),
1596 _ => {}
1597 },
1598 Event::End(tag) => match tag {
1599 TagEnd::Emphasis | TagEnd::Strong => buf.close_tag(),
1600 TagEnd::Paragraph | TagEnd::Heading(_) => return ControlFlow::Break(()),
1601 _ => {}
1602 },
1603 Event::HardBreak | Event::SoftBreak => buf.push(" ")?,
1604 _ => {}
1605 };
1606 ControlFlow::Continue(())
1607 });
1608
1609 (buf.finish(), stopped_early)
1610}
1611
1612pub(crate) fn short_markdown_summary(markdown: &str, link_names: &[RenderedLink]) -> String {
1619 let (mut s, was_shortened) = markdown_summary_with_limit(markdown, link_names, 59);
1620
1621 if was_shortened {
1622 s.push('…');
1623 }
1624
1625 s
1626}
1627
1628pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> String {
1635 if md.is_empty() {
1636 return String::new();
1637 }
1638
1639 let mut s = String::with_capacity(md.len() * 3 / 2);
1640
1641 let mut replacer = |broken_link: BrokenLink<'_>| {
1642 link_names
1643 .iter()
1644 .find(|link| *link.original_text == *broken_link.reference)
1645 .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1646 };
1647
1648 let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
1649
1650 plain_text_from_events(p, &mut s);
1651
1652 s
1653}
1654
1655pub(crate) fn plain_text_from_events<'a>(
1656 events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
1657 s: &mut String,
1658) {
1659 for event in events {
1660 match &event {
1661 Event::Text(text) => s.push_str(text),
1662 Event::Code(code) => {
1663 s.push('`');
1664 s.push_str(code);
1665 s.push('`');
1666 }
1667 Event::HardBreak | Event::SoftBreak => s.push(' '),
1668 Event::Start(Tag::CodeBlock(..)) => break,
1669 Event::End(TagEnd::Paragraph) => break,
1670 Event::End(TagEnd::Heading(..)) => break,
1671 _ => (),
1672 }
1673 }
1674}
1675
1676pub(crate) fn html_text_from_events<'a>(
1677 events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
1678 s: &mut String,
1679) {
1680 for event in events {
1681 match &event {
1682 Event::Text(text) => {
1683 write!(s, "{}", EscapeBodyText(text)).expect("string alloc infallible")
1684 }
1685 Event::Code(code) => {
1686 s.push_str("<code>");
1687 write!(s, "{}", EscapeBodyText(code)).expect("string alloc infallible");
1688 s.push_str("</code>");
1689 }
1690 Event::HardBreak | Event::SoftBreak => s.push(' '),
1691 Event::Start(Tag::CodeBlock(..)) => break,
1692 Event::End(TagEnd::Paragraph) => break,
1693 Event::End(TagEnd::Heading(..)) => break,
1694 _ => (),
1695 }
1696 }
1697}
1698
1699#[derive(Debug)]
1700pub(crate) struct MarkdownLink {
1701 pub kind: LinkType,
1702 pub link: String,
1703 pub range: MarkdownLinkRange,
1704}
1705
1706#[derive(Clone, Debug)]
1707pub(crate) enum MarkdownLinkRange {
1708 Destination(Range<usize>),
1710 WholeLink(Range<usize>),
1714}
1715
1716impl MarkdownLinkRange {
1717 pub fn inner_range(&self) -> &Range<usize> {
1719 match self {
1720 MarkdownLinkRange::Destination(range) => range,
1721 MarkdownLinkRange::WholeLink(range) => range,
1722 }
1723 }
1724}
1725
1726pub(crate) fn markdown_links<'md, R>(
1727 md: &'md str,
1728 preprocess_link: impl Fn(MarkdownLink) -> Option<R>,
1729) -> Vec<R> {
1730 use itertools::Itertools;
1731 if md.is_empty() {
1732 return vec![];
1733 }
1734
1735 let locate = |s: &str, fallback: Range<usize>| unsafe {
1737 let s_start = s.as_ptr();
1738 let s_end = s_start.add(s.len());
1739 let md_start = md.as_ptr();
1740 let md_end = md_start.add(md.len());
1741 if md_start <= s_start && s_end <= md_end {
1742 let start = s_start.offset_from(md_start) as usize;
1743 let end = s_end.offset_from(md_start) as usize;
1744 MarkdownLinkRange::Destination(start..end)
1745 } else {
1746 MarkdownLinkRange::WholeLink(fallback)
1747 }
1748 };
1749
1750 let span_for_link = |link: &CowStr<'_>, span: Range<usize>| {
1751 match link {
1756 CowStr::Borrowed(s) => locate(s, span),
1761
1762 CowStr::Boxed(_) | CowStr::Inlined(_) => MarkdownLinkRange::WholeLink(span),
1764 }
1765 };
1766
1767 let span_for_refdef = |link: &CowStr<'_>, span: Range<usize>| {
1768 let mut square_brace_count = 0;
1771 let mut iter = md.as_bytes()[span.start..span.end].iter().copied().enumerate();
1772 for (_i, c) in &mut iter {
1773 match c {
1774 b':' if square_brace_count == 0 => break,
1775 b'[' => square_brace_count += 1,
1776 b']' => square_brace_count -= 1,
1777 _ => {}
1778 }
1779 }
1780 while let Some((i, c)) = iter.next() {
1781 if c == b'<' {
1782 while let Some((j, c)) = iter.next() {
1783 match c {
1784 b'\\' => {
1785 let _ = iter.next();
1786 }
1787 b'>' => {
1788 return MarkdownLinkRange::Destination(
1789 i + 1 + span.start..j + span.start,
1790 );
1791 }
1792 _ => {}
1793 }
1794 }
1795 } else if !c.is_ascii_whitespace() {
1796 for (j, c) in iter.by_ref() {
1797 if c.is_ascii_whitespace() {
1798 return MarkdownLinkRange::Destination(i + span.start..j + span.start);
1799 }
1800 }
1801 return MarkdownLinkRange::Destination(i + span.start..span.end);
1802 }
1803 }
1804 span_for_link(link, span)
1805 };
1806
1807 let span_for_offset_backward = |span: Range<usize>, open: u8, close: u8| {
1808 let mut open_brace = !0;
1809 let mut close_brace = !0;
1810 for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate().rev() {
1811 let i = i + span.start;
1812 if b == close {
1813 close_brace = i;
1814 break;
1815 }
1816 }
1817 if close_brace < span.start || close_brace >= span.end {
1818 return MarkdownLinkRange::WholeLink(span);
1819 }
1820 let mut nesting = 1;
1821 for (i, b) in md.as_bytes()[span.start..close_brace].iter().copied().enumerate().rev() {
1822 let i = i + span.start;
1823 if b == close {
1824 nesting += 1;
1825 }
1826 if b == open {
1827 nesting -= 1;
1828 }
1829 if nesting == 0 {
1830 open_brace = i;
1831 break;
1832 }
1833 }
1834 assert!(open_brace != close_brace);
1835 if open_brace < span.start || open_brace >= span.end {
1836 return MarkdownLinkRange::WholeLink(span);
1837 }
1838 let range = (open_brace + 1)..close_brace;
1840 MarkdownLinkRange::Destination(range)
1841 };
1842
1843 let span_for_offset_forward = |span: Range<usize>, open: u8, close: u8| {
1844 let mut open_brace = !0;
1845 let mut close_brace = !0;
1846 for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate() {
1847 let i = i + span.start;
1848 if b == open {
1849 open_brace = i;
1850 break;
1851 }
1852 }
1853 if open_brace < span.start || open_brace >= span.end {
1854 return MarkdownLinkRange::WholeLink(span);
1855 }
1856 let mut nesting = 0;
1857 for (i, b) in md.as_bytes()[open_brace..span.end].iter().copied().enumerate() {
1858 let i = i + open_brace;
1859 if b == close {
1860 nesting -= 1;
1861 }
1862 if b == open {
1863 nesting += 1;
1864 }
1865 if nesting == 0 {
1866 close_brace = i;
1867 break;
1868 }
1869 }
1870 assert!(open_brace != close_brace);
1871 if open_brace < span.start || open_brace >= span.end {
1872 return MarkdownLinkRange::WholeLink(span);
1873 }
1874 let range = (open_brace + 1)..close_brace;
1876 MarkdownLinkRange::Destination(range)
1877 };
1878
1879 let mut broken_link_callback = |link: BrokenLink<'md>| Some((link.reference, "".into()));
1880 let event_iter = Parser::new_with_broken_link_callback(
1881 md,
1882 main_body_opts(),
1883 Some(&mut broken_link_callback),
1884 )
1885 .into_offset_iter();
1886 let mut links = Vec::new();
1887
1888 let mut refdefs = FxIndexMap::default();
1889 for (label, refdef) in event_iter.reference_definitions().iter().sorted_by_key(|x| x.0) {
1890 refdefs.insert(label.to_string(), (false, refdef.dest.to_string(), refdef.span.clone()));
1891 }
1892
1893 for (event, span) in event_iter {
1894 match event {
1895 Event::Start(Tag::Link { link_type, dest_url, id, .. })
1896 if may_be_doc_link(link_type) =>
1897 {
1898 let range = match link_type {
1899 LinkType::ReferenceUnknown | LinkType::ShortcutUnknown => {
1901 span_for_offset_backward(span, b'[', b']')
1902 }
1903 LinkType::CollapsedUnknown => span_for_offset_forward(span, b'[', b']'),
1904 LinkType::Inline => span_for_offset_backward(span, b'(', b')'),
1905 LinkType::Reference | LinkType::Collapsed | LinkType::Shortcut => {
1907 if let Some((is_used, dest_url, span)) = refdefs.get_mut(&id[..]) {
1908 *is_used = true;
1909 span_for_refdef(&CowStr::from(&dest_url[..]), span.clone())
1910 } else {
1911 span_for_link(&dest_url, span)
1912 }
1913 }
1914 LinkType::Autolink | LinkType::Email => unreachable!(),
1915 };
1916
1917 if let Some(link) = preprocess_link(MarkdownLink {
1918 kind: link_type,
1919 link: dest_url.into_string(),
1920 range,
1921 }) {
1922 links.push(link);
1923 }
1924 }
1925 _ => {}
1926 }
1927 }
1928
1929 for (_label, (is_used, dest_url, span)) in refdefs.into_iter() {
1930 if !is_used
1931 && let Some(link) = preprocess_link(MarkdownLink {
1932 kind: LinkType::Reference,
1933 range: span_for_refdef(&CowStr::from(&dest_url[..]), span),
1934 link: dest_url,
1935 })
1936 {
1937 links.push(link);
1938 }
1939 }
1940
1941 links
1942}
1943
1944#[derive(Debug)]
1945pub(crate) struct RustCodeBlock {
1946 pub(crate) range: Range<usize>,
1949 pub(crate) code: Range<usize>,
1951 pub(crate) is_fenced: bool,
1952 pub(crate) lang_string: LangString,
1953}
1954
1955pub(crate) fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_>) -> Vec<RustCodeBlock> {
1958 let mut code_blocks = vec![];
1959
1960 if md.is_empty() {
1961 return code_blocks;
1962 }
1963
1964 let mut p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
1965
1966 while let Some((event, offset)) = p.next() {
1967 if let Event::Start(Tag::CodeBlock(syntax)) = event {
1968 let (lang_string, code_start, code_end, range, is_fenced) = match syntax {
1969 CodeBlockKind::Fenced(syntax) => {
1970 let syntax = syntax.as_ref();
1971 let lang_string = if syntax.is_empty() {
1972 Default::default()
1973 } else {
1974 LangString::parse(syntax, ErrorCodes::Yes, Some(extra_info))
1975 };
1976 if !lang_string.rust {
1977 continue;
1978 }
1979 let (code_start, mut code_end) = match p.next() {
1980 Some((Event::Text(_), offset)) => (offset.start, offset.end),
1981 Some((_, sub_offset)) => {
1982 let code = Range { start: sub_offset.start, end: sub_offset.start };
1983 code_blocks.push(RustCodeBlock {
1984 is_fenced: true,
1985 range: offset,
1986 code,
1987 lang_string,
1988 });
1989 continue;
1990 }
1991 None => {
1992 let code = Range { start: offset.end, end: offset.end };
1993 code_blocks.push(RustCodeBlock {
1994 is_fenced: true,
1995 range: offset,
1996 code,
1997 lang_string,
1998 });
1999 continue;
2000 }
2001 };
2002 while let Some((Event::Text(_), offset)) = p.next() {
2003 code_end = offset.end;
2004 }
2005 (lang_string, code_start, code_end, offset, true)
2006 }
2007 CodeBlockKind::Indented => {
2008 if offset.end > offset.start && md.get(offset.end..=offset.end) == Some("\n") {
2011 (
2012 LangString::default(),
2013 offset.start,
2014 offset.end,
2015 Range { start: offset.start, end: offset.end - 1 },
2016 false,
2017 )
2018 } else {
2019 (LangString::default(), offset.start, offset.end, offset, false)
2020 }
2021 }
2022 };
2023
2024 code_blocks.push(RustCodeBlock {
2025 is_fenced,
2026 range,
2027 code: Range { start: code_start, end: code_end },
2028 lang_string,
2029 });
2030 }
2031 }
2032
2033 code_blocks
2034}
2035
2036#[derive(Clone, Default, Debug)]
2037pub struct IdMap {
2038 map: FxHashMap<String, usize>,
2039 existing_footnotes: Arc<AtomicUsize>,
2040}
2041
2042fn is_default_id(id: &str) -> bool {
2043 matches!(
2044 id,
2045 "help"
2047 | "settings"
2048 | "not-displayed"
2049 | "alternative-display"
2050 | "search"
2051 | "crate-search"
2052 | "crate-search-div"
2053 | "themeStyle"
2056 | "settings-menu"
2057 | "help-button"
2058 | "sidebar-button"
2059 | "main-content"
2060 | "toggle-all-docs"
2061 | "all-types"
2062 | "default-settings"
2063 | "sidebar-vars"
2064 | "copy-path"
2065 | "rustdoc-toc"
2066 | "rustdoc-modnav"
2067 | "fields"
2070 | "variants"
2071 | "implementors-list"
2072 | "synthetic-implementors-list"
2073 | "foreign-impls"
2074 | "implementations"
2075 | "trait-implementations"
2076 | "synthetic-implementations"
2077 | "blanket-implementations"
2078 | "required-associated-types"
2079 | "provided-associated-types"
2080 | "provided-associated-consts"
2081 | "required-associated-consts"
2082 | "required-methods"
2083 | "provided-methods"
2084 | "dyn-compatibility"
2085 | "implementors"
2086 | "synthetic-implementors"
2087 | "implementations-list"
2088 | "trait-implementations-list"
2089 | "synthetic-implementations-list"
2090 | "blanket-implementations-list"
2091 | "deref-methods"
2092 | "layout"
2093 | "aliased-type"
2094 )
2095}
2096
2097impl IdMap {
2098 pub fn new() -> Self {
2099 IdMap { map: FxHashMap::default(), existing_footnotes: Arc::new(AtomicUsize::new(0)) }
2100 }
2101
2102 pub(crate) fn derive<S: AsRef<str> + ToString>(&mut self, candidate: S) -> String {
2103 let id = match self.map.get_mut(candidate.as_ref()) {
2104 None => {
2105 let candidate = candidate.to_string();
2106 if is_default_id(&candidate) {
2107 let id = format!("{}-{}", candidate, 1);
2108 self.map.insert(candidate, 2);
2109 id
2110 } else {
2111 candidate
2112 }
2113 }
2114 Some(a) => {
2115 let id = format!("{}-{}", candidate.as_ref(), *a);
2116 *a += 1;
2117 id
2118 }
2119 };
2120
2121 self.map.insert(id.clone(), 1);
2122 id
2123 }
2124
2125 pub(crate) fn handle_footnotes<'a, T, F: FnOnce(&'a mut Self, Weak<AtomicUsize>) -> T>(
2128 &'a mut self,
2129 closure: F,
2130 ) -> T {
2131 let existing_footnotes = Arc::downgrade(&self.existing_footnotes);
2132
2133 closure(self, existing_footnotes)
2134 }
2135
2136 pub(crate) fn clear(&mut self) {
2137 self.map.clear();
2138 self.existing_footnotes = Arc::new(AtomicUsize::new(0));
2139 }
2140}