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