diff options
Diffstat (limited to '')
-rw-r--r-- | src/librustdoc/html/markdown.rs | 209 |
1 files changed, 71 insertions, 138 deletions
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 4ff67fe15..fe446ae3c 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -27,14 +27,14 @@ use rustc_data_structures::fx::FxHashMap; use rustc_hir::def_id::DefId; -use rustc_hir::HirId; use rustc_middle::ty::TyCtxt; +pub(crate) use rustc_resolve::rustdoc::main_body_opts; +use rustc_resolve::rustdoc::may_be_doc_link; use rustc_span::edition::Edition; use rustc_span::{Span, Symbol}; use once_cell::sync::Lazy; use std::borrow::Cow; -use std::cell::RefCell; use std::collections::VecDeque; use std::default::Default; use std::fmt::Write; @@ -47,6 +47,7 @@ use crate::html::escape::Escape; use crate::html::format::Buffer; use crate::html::highlight; use crate::html::length_limit::HtmlWithLimit; +use crate::html::render::small_url_encode; use crate::html::toc::TocBuilder; use pulldown_cmark::{ @@ -58,15 +59,6 @@ mod tests; const MAX_HEADER_LEVEL: u32 = 6; -/// Options for rendering Markdown in the main body of documentation. -pub(crate) fn main_body_opts() -> Options { - Options::ENABLE_TABLES - | Options::ENABLE_FOOTNOTES - | Options::ENABLE_STRIKETHROUGH - | Options::ENABLE_TASKLISTS - | Options::ENABLE_SMART_PUNCTUATION -} - /// Options for rendering Markdown in summaries (e.g., in search results). pub(crate) fn summary_opts() -> Options { Options::ENABLE_TABLES @@ -103,14 +95,14 @@ pub struct Markdown<'a> { /// E.g. if `heading_offset: HeadingOffset::H2`, then `# something` renders an `<h2>`. pub heading_offset: HeadingOffset, } -/// A tuple struct like `Markdown` that renders the markdown with a table of contents. -pub(crate) struct MarkdownWithToc<'a>( - pub(crate) &'a str, - pub(crate) &'a mut IdMap, - pub(crate) ErrorCodes, - pub(crate) Edition, - pub(crate) &'a Option<Playground>, -); +/// A struct like `Markdown` that renders the markdown with a table of contents. +pub(crate) struct MarkdownWithToc<'a> { + pub(crate) content: &'a str, + pub(crate) ids: &'a mut IdMap, + pub(crate) error_codes: ErrorCodes, + pub(crate) edition: Edition, + pub(crate) playground: &'a Option<Playground>, +} /// A tuple struct like `Markdown` that renders the markdown escaping HTML tags /// and includes no paragraph tags. pub(crate) struct MarkdownItemInfo<'a>(pub(crate) &'a str, pub(crate) &'a mut IdMap); @@ -295,30 +287,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> { doctest::make_test(&test, krate, false, &Default::default(), edition, None); let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" }; - // These characters don't need to be escaped in a URI. - // FIXME: use a library function for percent encoding. - fn dont_escape(c: u8) -> bool { - (b'a' <= c && c <= b'z') - || (b'A' <= c && c <= b'Z') - || (b'0' <= c && c <= b'9') - || c == b'-' - || c == b'_' - || c == b'.' - || c == b'~' - || c == b'!' - || c == b'\'' - || c == b'(' - || c == b')' - || c == b'*' - } - let mut test_escaped = String::new(); - for b in test.bytes() { - if dont_escape(b) { - test_escaped.push(char::from(b)); - } else { - write!(test_escaped, "%{:02X}", b).unwrap(); - } - } + let test_escaped = small_url_encode(test); Some(format!( r#"<a class="test-arrow" target="_blank" href="{}?code={}{}&edition={}">Run</a>"#, url, test_escaped, channel, edition, @@ -391,6 +360,9 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> { trace!("it matched"); assert!(self.shortcut_link.is_none(), "shortcut links cannot be nested"); self.shortcut_link = Some(link); + if title.is_empty() && !link.tooltip.is_empty() { + *title = CowStr::Borrowed(link.tooltip.as_ref()); + } } } // Now that we're done with the shortcut link, don't replace any more text. @@ -441,9 +413,12 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> { } // If this is a link, but not a shortcut link, // replace the URL, since the broken_link_callback was not called. - Some(Event::Start(Tag::Link(_, dest, _))) => { + Some(Event::Start(Tag::Link(_, dest, title))) => { if let Some(link) = self.links.iter().find(|&link| *link.original_text == **dest) { *dest = CowStr::Borrowed(link.href.as_ref()); + if title.is_empty() && !link.tooltip.is_empty() { + *title = CowStr::Borrowed(link.tooltip.as_ref()); + } } } // Anything else couldn't have been a valid Rust path, so no need to replace the text. @@ -577,10 +552,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> SummaryLine<'a, I> { } fn check_if_allowed_tag(t: &Tag<'_>) -> bool { - matches!( - t, - Tag::Paragraph | Tag::Item | Tag::Emphasis | Tag::Strong | Tag::Link(..) | Tag::BlockQuote - ) + matches!(t, Tag::Paragraph | Tag::Emphasis | Tag::Strong | Tag::Link(..) | Tag::BlockQuote) } fn is_forbidden_tag(t: &Tag<'_>) -> bool { @@ -771,11 +743,7 @@ pub(crate) fn find_testable_code<T: doctest::Tester>( } Event::Text(ref s) if register_header.is_some() => { let level = register_header.unwrap(); - if s.is_empty() { - tests.register_header("", level); - } else { - tests.register_header(s, level); - } + tests.register_header(s, level); register_header = None; } _ => {} @@ -784,45 +752,26 @@ pub(crate) fn find_testable_code<T: doctest::Tester>( } pub(crate) struct ExtraInfo<'tcx> { - id: ExtraInfoId, + def_id: DefId, sp: Span, tcx: TyCtxt<'tcx>, } -enum ExtraInfoId { - Hir(HirId), - Def(DefId), -} - impl<'tcx> ExtraInfo<'tcx> { - pub(crate) fn new(tcx: TyCtxt<'tcx>, hir_id: HirId, sp: Span) -> ExtraInfo<'tcx> { - ExtraInfo { id: ExtraInfoId::Hir(hir_id), sp, tcx } - } - - pub(crate) fn new_did(tcx: TyCtxt<'tcx>, did: DefId, sp: Span) -> ExtraInfo<'tcx> { - ExtraInfo { id: ExtraInfoId::Def(did), sp, tcx } + pub(crate) fn new(tcx: TyCtxt<'tcx>, def_id: DefId, sp: Span) -> ExtraInfo<'tcx> { + ExtraInfo { def_id, sp, tcx } } fn error_invalid_codeblock_attr(&self, msg: &str, help: &str) { - let hir_id = match self.id { - ExtraInfoId::Hir(hir_id) => hir_id, - ExtraInfoId::Def(item_did) => { - match item_did.as_local() { - Some(item_did) => self.tcx.hir().local_def_id_to_hir_id(item_did), - None => { - // If non-local, no need to check anything. - return; - } - } - } - }; - self.tcx.struct_span_lint_hir( - crate::lint::INVALID_CODEBLOCK_ATTRIBUTES, - hir_id, - self.sp, - msg, - |lint| lint.help(help), - ); + if let Some(def_id) = self.def_id.as_local() { + self.tcx.struct_span_lint_hir( + crate::lint::INVALID_CODEBLOCK_ATTRIBUTES, + self.tcx.hir().local_def_id_to_hir_id(def_id), + self.sp, + msg, + |lint| lint.help(help), + ); + } } } @@ -1029,8 +978,8 @@ impl Markdown<'_> { let mut replacer = |broken_link: BrokenLink<'_>| { links .iter() - .find(|link| link.original_text.as_str() == &*broken_link.reference) - .map(|link| (link.href.as_str().into(), link.new_text.as_str().into())) + .find(|link| &*link.original_text == &*broken_link.reference) + .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into())) }; let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer)); @@ -1051,7 +1000,7 @@ impl Markdown<'_> { impl MarkdownWithToc<'_> { pub(crate) fn into_string(self) -> String { - let MarkdownWithToc(md, ids, codes, edition, playground) = self; + let MarkdownWithToc { content: md, ids, error_codes: codes, edition, playground } = self; let p = Parser::new_ext(md, main_body_opts()).into_offset_iter(); @@ -1112,8 +1061,8 @@ impl MarkdownSummaryLine<'_> { let mut replacer = |broken_link: BrokenLink<'_>| { links .iter() - .find(|link| link.original_text.as_str() == &*broken_link.reference) - .map(|link| (link.href.as_str().into(), link.new_text.as_str().into())) + .find(|link| &*link.original_text == &*broken_link.reference) + .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into())) }; let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer)) @@ -1159,8 +1108,8 @@ fn markdown_summary_with_limit( let mut replacer = |broken_link: BrokenLink<'_>| { link_names .iter() - .find(|link| link.original_text.as_str() == &*broken_link.reference) - .map(|link| (link.href.as_str().into(), link.new_text.as_str().into())) + .find(|link| &*link.original_text == &*broken_link.reference) + .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into())) }; let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer)); @@ -1191,18 +1140,18 @@ fn markdown_summary_with_limit( Event::Start(tag) => match tag { Tag::Emphasis => buf.open_tag("em"), Tag::Strong => buf.open_tag("strong"), - Tag::CodeBlock(..) => return ControlFlow::BREAK, + Tag::CodeBlock(..) => return ControlFlow::Break(()), _ => {} }, Event::End(tag) => match tag { Tag::Emphasis | Tag::Strong => buf.close_tag(), - Tag::Paragraph | Tag::Heading(..) => return ControlFlow::BREAK, + Tag::Paragraph | Tag::Heading(..) => return ControlFlow::Break(()), _ => {} }, Event::HardBreak | Event::SoftBreak => buf.push(" ")?, _ => {} }; - ControlFlow::CONTINUE + ControlFlow::Continue(()) }); (buf.finish(), stopped_early) @@ -1230,14 +1179,23 @@ pub(crate) fn short_markdown_summary(markdown: &str, link_names: &[RenderedLink] /// - Headings, links, and formatting are stripped. /// - Inline code is rendered as-is, surrounded by backticks. /// - HTML and code blocks are ignored. -pub(crate) fn plain_text_summary(md: &str) -> String { +pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> String { if md.is_empty() { return String::new(); } let mut s = String::with_capacity(md.len() * 3 / 2); - for event in Parser::new_ext(md, summary_opts()) { + let mut replacer = |broken_link: BrokenLink<'_>| { + link_names + .iter() + .find(|link| &*link.original_text == &*broken_link.reference) + .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into())) + }; + + let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer)); + + for event in p { match &event { Event::Text(text) => s.push_str(text), Event::Code(code) => { @@ -1265,14 +1223,12 @@ pub(crate) struct MarkdownLink { pub(crate) fn markdown_links<R>( md: &str, - filter_map: impl Fn(MarkdownLink) -> Option<R>, + preprocess_link: impl Fn(MarkdownLink) -> Option<R>, ) -> Vec<R> { if md.is_empty() { return vec![]; } - let links = RefCell::new(vec![]); - // FIXME: remove this function once pulldown_cmark can provide spans for link definitions. let locate = |s: &str, fallback: Range<usize>| unsafe { let s_start = s.as_ptr(); @@ -1304,46 +1260,23 @@ pub(crate) fn markdown_links<R>( } }; - let mut push = |link: BrokenLink<'_>| { - let span = span_for_link(&link.reference, link.span); - filter_map(MarkdownLink { - kind: LinkType::ShortcutUnknown, - link: link.reference.to_string(), - range: span, - }) - .map(|link| links.borrow_mut().push(link)); - None - }; - let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut push)) - .into_offset_iter(); - - // There's no need to thread an IdMap through to here because - // the IDs generated aren't going to be emitted anywhere. - let mut ids = IdMap::new(); - let iter = Footnotes::new(HeadingLinks::new(p, None, &mut ids, HeadingOffset::H1)); - - for ev in iter { - if let Event::Start(Tag::Link( - // `<>` links cannot be intra-doc links so we skip them. - kind @ (LinkType::Inline - | LinkType::Reference - | LinkType::ReferenceUnknown - | LinkType::Collapsed - | LinkType::CollapsedUnknown - | LinkType::Shortcut - | LinkType::ShortcutUnknown), - dest, - _, - )) = ev.0 - { - debug!("found link: {dest}"); - let span = span_for_link(&dest, ev.1); - filter_map(MarkdownLink { kind, link: dest.into_string(), range: span }) - .map(|link| links.borrow_mut().push(link)); + Parser::new_with_broken_link_callback( + md, + main_body_opts(), + Some(&mut |link: BrokenLink<'_>| Some((link.reference, "".into()))), + ) + .into_offset_iter() + .filter_map(|(event, span)| match event { + Event::Start(Tag::Link(link_type, dest, _)) if may_be_doc_link(link_type) => { + preprocess_link(MarkdownLink { + kind: link_type, + range: span_for_link(&dest, span), + link: dest.into_string(), + }) } - } - - links.into_inner() + _ => None, + }) + .collect() } #[derive(Debug)] |