Skip to content

Commit e0822ec

Browse files
committed
rustdoc: do not use plain summary for trait impls
Fixes #38386. Fixes #48332. Fixes #49430. Fixes #62741. Fixes #73474.
1 parent db534b3 commit e0822ec

File tree

7 files changed

+142
-70
lines changed

7 files changed

+142
-70
lines changed

src/librustdoc/formats/cache.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::formats::item_type::ItemType;
1616
use crate::formats::Impl;
1717
use crate::html::render::cache::{extern_location, get_index_search_type, ExternalLocation};
1818
use crate::html::render::IndexItem;
19-
use crate::html::render::{plain_summary_line, shorten};
19+
use crate::html::render::{plain_text_summary, shorten};
2020

2121
thread_local!(crate static CACHE_KEY: RefCell<Arc<Cache>> = Default::default());
2222

@@ -313,7 +313,7 @@ impl DocFolder for Cache {
313313
ty: item.type_(),
314314
name: s.to_string(),
315315
path: path.join("::"),
316-
desc: shorten(plain_summary_line(item.doc_value())),
316+
desc: shorten(plain_text_summary(item.doc_value())),
317317
parent,
318318
parent_idx: None,
319319
search_type: get_index_search_type(&item),

src/librustdoc/html/markdown.rs

+22-33
Original file line numberDiff line numberDiff line change
@@ -955,44 +955,33 @@ impl MarkdownSummaryLine<'_> {
955955
}
956956
}
957957

958-
pub fn plain_summary_line(md: &str) -> String {
959-
struct ParserWrapper<'a> {
960-
inner: Parser<'a>,
961-
is_in: isize,
962-
is_first: bool,
958+
/// Renders the first paragraph of the provided markdown as plain text.
959+
///
960+
/// - Headings, links, and formatting are stripped.
961+
/// - Inline code is rendered as-is, surrounded by backticks.
962+
/// - HTML and code blocks are ignored.
963+
pub fn plain_text_summary(md: &str) -> String {
964+
if md.is_empty() {
965+
return String::new();
963966
}
964967

965-
impl<'a> Iterator for ParserWrapper<'a> {
966-
type Item = String;
967-
968-
fn next(&mut self) -> Option<String> {
969-
let next_event = self.inner.next()?;
970-
let (ret, is_in) = match next_event {
971-
Event::Start(Tag::Paragraph) => (None, 1),
972-
Event::Start(Tag::Heading(_)) => (None, 1),
973-
Event::Code(code) => (Some(format!("`{}`", code)), 0),
974-
Event::Text(ref s) if self.is_in > 0 => (Some(s.as_ref().to_owned()), 0),
975-
Event::End(Tag::Paragraph | Tag::Heading(_)) => (None, -1),
976-
_ => (None, 0),
977-
};
978-
if is_in > 0 || (is_in < 0 && self.is_in > 0) {
979-
self.is_in += is_in;
980-
}
981-
if ret.is_some() {
982-
self.is_first = false;
983-
ret
984-
} else {
985-
Some(String::new())
968+
let mut s = String::with_capacity(md.len() * 3 / 2);
969+
970+
for event in Parser::new_ext(md, Options::ENABLE_STRIKETHROUGH) {
971+
match &event {
972+
Event::Text(text) => s.push_str(text),
973+
Event::Code(code) => {
974+
s.push('`');
975+
s.push_str(code);
976+
s.push('`');
986977
}
978+
Event::HardBreak | Event::SoftBreak => s.push(' '),
979+
Event::Start(Tag::CodeBlock(..)) => break,
980+
Event::End(Tag::Paragraph) => break,
981+
_ => (),
987982
}
988983
}
989-
let mut s = String::with_capacity(md.len() * 3 / 2);
990-
let p = ParserWrapper {
991-
inner: Parser::new_ext(md, Options::ENABLE_STRIKETHROUGH),
992-
is_in: 0,
993-
is_first: true,
994-
};
995-
p.filter(|t| !t.is_empty()).for_each(|i| s.push_str(&i));
984+
996985
s
997986
}
998987

src/librustdoc/html/markdown/tests.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::plain_summary_line;
1+
use super::plain_text_summary;
22
use super::{ErrorCodes, IdMap, Ignore, LangString, Markdown, MarkdownHtml};
33
use rustc_span::edition::{Edition, DEFAULT_EDITION};
44
use std::cell::RefCell;
@@ -210,18 +210,25 @@ fn test_header_ids_multiple_blocks() {
210210
}
211211

212212
#[test]
213-
fn test_plain_summary_line() {
213+
fn test_plain_text_summary() {
214214
fn t(input: &str, expect: &str) {
215-
let output = plain_summary_line(input);
215+
let output = plain_text_summary(input);
216216
assert_eq!(output, expect, "original: {}", input);
217217
}
218218

219219
t("hello [Rust](https://www.rust-lang.org) :)", "hello Rust :)");
220+
t("**bold**", "bold");
221+
t("Multi-line\nsummary", "Multi-line summary");
222+
t("Hard-break \nsummary", "Hard-break summary");
223+
t("hello [Rust] :)\n\n[Rust]: https://www.rust-lang.org", "hello Rust :)");
220224
t("hello [Rust](https://www.rust-lang.org \"Rust\") :)", "hello Rust :)");
221225
t("code `let x = i32;` ...", "code `let x = i32;` ...");
222226
t("type `Type<'static>` ...", "type `Type<'static>` ...");
223227
t("# top header", "top header");
224228
t("## header", "header");
229+
t("first paragraph\n\nsecond paragraph", "first paragraph");
230+
t("```\nfn main() {}\n```", "");
231+
t("<div>hello</div>", "");
225232
}
226233

227234
#[test]

src/librustdoc/html/render/cache.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::clean::types::GetDefId;
99
use crate::clean::{self, AttributesExt};
1010
use crate::formats::cache::Cache;
1111
use crate::formats::item_type::ItemType;
12-
use crate::html::render::{plain_summary_line, shorten};
12+
use crate::html::render::{plain_text_summary, shorten};
1313
use crate::html::render::{Generic, IndexItem, IndexItemFunctionType, RenderType, TypeWithKind};
1414

1515
/// Indicates where an external crate can be found.
@@ -78,7 +78,7 @@ pub fn build_index(krate: &clean::Crate, cache: &mut Cache) -> String {
7878
ty: item.type_(),
7979
name: item.name.clone().unwrap(),
8080
path: fqp[..fqp.len() - 1].join("::"),
81-
desc: shorten(plain_summary_line(item.doc_value())),
81+
desc: shorten(plain_text_summary(item.doc_value())),
8282
parent: Some(did),
8383
parent_idx: None,
8484
search_type: get_index_search_type(&item),
@@ -127,7 +127,7 @@ pub fn build_index(krate: &clean::Crate, cache: &mut Cache) -> String {
127127
let crate_doc = krate
128128
.module
129129
.as_ref()
130-
.map(|module| shorten(plain_summary_line(module.doc_value())))
130+
.map(|module| shorten(plain_text_summary(module.doc_value())))
131131
.unwrap_or(String::new());
132132

133133
#[derive(Serialize)]

src/librustdoc/html/render/mod.rs

+33-29
Original file line numberDiff line numberDiff line change
@@ -1506,6 +1506,7 @@ impl Context {
15061506
}
15071507
}
15081508

1509+
/// Construct a map of items shown in the sidebar to a plain-text summary of their docs.
15091510
fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap<String, Vec<NameDoc>> {
15101511
// BTreeMap instead of HashMap to get a sorted output
15111512
let mut map: BTreeMap<_, Vec<_>> = BTreeMap::new();
@@ -1522,7 +1523,7 @@ impl Context {
15221523
let short = short.to_string();
15231524
map.entry(short)
15241525
.or_default()
1525-
.push((myname, Some(plain_summary_line(item.doc_value()))));
1526+
.push((myname, Some(plain_text_summary(item.doc_value()))));
15261527
}
15271528

15281529
if self.shared.sort_modules_alphabetically {
@@ -1728,22 +1729,15 @@ fn full_path(cx: &Context, item: &clean::Item) -> String {
17281729
s
17291730
}
17301731

1732+
/// Renders the first paragraph of the given markdown as plain text, making it suitable for
1733+
/// contexts like alt-text or the search index.
1734+
///
1735+
/// If no markdown is supplied, the empty string is returned.
1736+
///
1737+
/// See [`markdown::plain_text_summary`] for further details.
17311738
#[inline]
1732-
crate fn plain_summary_line(s: Option<&str>) -> String {
1733-
let s = s.unwrap_or("");
1734-
// This essentially gets the first paragraph of text in one line.
1735-
let mut line = s
1736-
.lines()
1737-
.skip_while(|line| line.chars().all(|c| c.is_whitespace()))
1738-
.take_while(|line| line.chars().any(|c| !c.is_whitespace()))
1739-
.fold(String::new(), |mut acc, line| {
1740-
acc.push_str(line);
1741-
acc.push(' ');
1742-
acc
1743-
});
1744-
// remove final whitespace
1745-
line.pop();
1746-
markdown::plain_summary_line(&line[..])
1739+
crate fn plain_text_summary(s: Option<&str>) -> String {
1740+
s.map(markdown::plain_text_summary).unwrap_or_default()
17471741
}
17481742

17491743
crate fn shorten(s: String) -> String {
@@ -1800,25 +1794,35 @@ fn render_markdown(
18001794
)
18011795
}
18021796

1797+
/// Writes a documentation block containing only the first paragraph of the documentation. If the
1798+
/// docs are longer, a "Read more" link is appended to the end.
18031799
fn document_short(
18041800
w: &mut Buffer,
1805-
cx: &Context,
18061801
item: &clean::Item,
18071802
link: AssocItemLink<'_>,
18081803
prefix: &str,
18091804
is_hidden: bool,
18101805
) {
18111806
if let Some(s) = item.doc_value() {
1812-
let markdown = if s.contains('\n') {
1813-
format!(
1814-
"{} [Read more]({})",
1815-
&plain_summary_line(Some(s)),
1816-
naive_assoc_href(item, link)
1817-
)
1818-
} else {
1819-
plain_summary_line(Some(s))
1820-
};
1821-
render_markdown(w, cx, &markdown, item.links(), prefix, is_hidden);
1807+
let mut summary_html = MarkdownSummaryLine(s, &item.links()).into_string();
1808+
1809+
if s.contains('\n') {
1810+
let link = format!(r#" <a href="{}">Read more</a>"#, naive_assoc_href(item, link));
1811+
1812+
if let Some(idx) = summary_html.rfind("</p>") {
1813+
summary_html.insert_str(idx, &link);
1814+
} else {
1815+
summary_html.push_str(&link);
1816+
}
1817+
}
1818+
1819+
write!(
1820+
w,
1821+
"<div class='docblock{}'>{}{}</div>",
1822+
if is_hidden { " hidden" } else { "" },
1823+
prefix,
1824+
summary_html,
1825+
);
18221826
} else if !prefix.is_empty() {
18231827
write!(
18241828
w,
@@ -3689,7 +3693,7 @@ fn render_impl(
36893693
} else if show_def_docs {
36903694
// In case the item isn't documented,
36913695
// provide short documentation from the trait.
3692-
document_short(w, cx, it, link, "", is_hidden);
3696+
document_short(w, it, link, "", is_hidden);
36933697
}
36943698
}
36953699
} else {
@@ -3701,7 +3705,7 @@ fn render_impl(
37013705
} else {
37023706
document_stability(w, cx, item, is_hidden);
37033707
if show_def_docs {
3704-
document_short(w, cx, item, link, "", is_hidden);
3708+
document_short(w, item, link, "", is_hidden);
37053709
}
37063710
}
37073711
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#![crate_type = "lib"]
2+
#![crate_name = "summaries"]
3+
4+
//! This summary has a [link] and `code`.
5+
//!
6+
//! This is the second paragraph.
7+
//!
8+
//! [link]: https://example.com
9+
10+
// @has search-index.js 'This summary has a link and `code`.'
11+
// @!has - 'second paragraph'
12+
13+
/// This `code` should be in backticks.
14+
///
15+
/// This text should not be rendered.
16+
pub struct Sidebar;
17+
18+
// @has summaries/sidebar-items.js 'This `code` should be in backticks.'
19+
// @!has - 'text should not be rendered'
20+
21+
/// ```text
22+
/// this block should not be rendered
23+
/// ```
24+
pub struct Sidebar2;
25+
26+
// @!has summaries/sidebar-items.js 'block should not be rendered'

src/test/rustdoc/trait-impl.rs

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
pub trait Trait {
2+
/// Some long docs here.
3+
///
4+
/// These docs are long enough that a link will be added to the end.
5+
fn a();
6+
7+
/// These docs contain a [reference link].
8+
///
9+
/// [reference link]: https://example.com
10+
fn b();
11+
12+
/// ```
13+
/// This code block should not be in the output, but a Read more link should be generated
14+
/// ```
15+
fn c();
16+
17+
/// Escaped formatting a\*b\*c\* works
18+
fn d();
19+
}
20+
21+
pub struct Struct;
22+
23+
impl Trait for Struct {
24+
// @has trait_impl/struct.Struct.html '//*[@id="method.a"]/../div/p' 'Some long docs'
25+
// @!has - '//*[@id="method.a"]/../div/p' 'link will be added'
26+
// @has - '//*[@id="method.a"]/../div/p/a' 'Read more'
27+
// @has - '//*[@id="method.a"]/../div/p/a/@href' 'trait.Trait.html'
28+
fn a() {}
29+
30+
// @has trait_impl/struct.Struct.html '//*[@id="method.b"]/../div/p' 'These docs contain'
31+
// @has - '//*[@id="method.b"]/../div/p/a' 'reference link'
32+
// @has - '//*[@id="method.b"]/../div/p/a/@href' 'https://example.com'
33+
// @has - '//*[@id="method.b"]/../div/p/a' 'Read more'
34+
// @has - '//*[@id="method.b"]/../div/p/a/@href' 'trait.Trait.html'
35+
fn b() {}
36+
37+
// @!has trait_impl/struct.Struct.html '//*[@id="method.c"]/../div/p' 'code block'
38+
// @has - '//*[@id="method.c"]/../div/p/a' 'Read more'
39+
// @has - '//*[@id="method.c"]/../div/p/a/@href' 'trait.Trait.html'
40+
fn c() {}
41+
42+
// @has trait_impl/struct.Struct.html '//*[@id="method.d"]/../div/p' \
43+
// 'Escaped formatting a*b*c* works'
44+
// @!has trait_impl/struct.Struct.html '//*[@id="method.d"]/../div/p/em'
45+
fn d() {}
46+
}

0 commit comments

Comments
 (0)