Skip to content

Commit b3a2363

Browse files
authored
Unrolled build for rust-lang#126247
Rollup merge of rust-lang#126247 - notriddle:notriddle/word-wrap-item-table, r=GuillaumeGomez rustdoc: word wrap CamelCase in the item list table and sidebar This is an alternative to rust-lang#126209. That is, it fixes the issue that affects the very long type names in https://docs.rs/async-stripe/0.31.0/stripe/index.html#structs. This is, necessarily, a pile of nasty heuristics. We need to balance a few issues: - Sometimes, there's no real word break. For example, `BTreeMap` should be `BTree<wbr>Map`, not `B<wbr>Tree<wbr>Map`. - Sometimes, there's a legit word break, but the name is tiny and the HTML overhead isn't worth it. For example, if we're typesetting `TyCtx`, writing `Ty<wbr>Ctx` would have an HTML overhead of 50%. Line breaking inside it makes no sense. # Screenshots | Before | After | | ------ | ----- | | ![image](https://github.com/rust-lang/rust/assets/1593513/d51201fd-46c0-4f48-aee6-a477eadba288) | ![image](https://github.com/rust-lang/rust/assets/1593513/d8e77582-adcf-4966-bbfd-19dfdad7336a)
2 parents 612a33f + ac303df commit b3a2363

26 files changed

+208
-46
lines changed

Cargo.lock

+1
Original file line numberDiff line numberDiff line change
@@ -4826,6 +4826,7 @@ dependencies = [
48264826
"tracing",
48274827
"tracing-subscriber",
48284828
"tracing-tree",
4829+
"unicode-segmentation",
48294830
]
48304831

48314832
[[package]]

src/librustdoc/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ tempfile = "3"
2323
tracing = "0.1"
2424
tracing-tree = "0.3.0"
2525
threadpool = "1.8.1"
26+
unicode-segmentation = "1.9"
2627

2728
[dependencies.tracing-subscriber]
2829
version = "0.3.3"

src/librustdoc/html/escape.rs

+55
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
66
use std::fmt;
77

8+
use unicode_segmentation::UnicodeSegmentation;
9+
810
/// Wrapper struct which will emit the HTML-escaped version of the contained
911
/// string when passed to a format string.
1012
pub(crate) struct Escape<'a>(pub &'a str);
@@ -74,3 +76,56 @@ impl<'a> fmt::Display for EscapeBodyText<'a> {
7476
Ok(())
7577
}
7678
}
79+
80+
/// Wrapper struct which will emit the HTML-escaped version of the contained
81+
/// string when passed to a format string. This function also word-breaks
82+
/// CamelCase and snake_case word names.
83+
///
84+
/// This is only safe to use for text nodes. If you need your output to be
85+
/// safely contained in an attribute, use [`Escape`]. If you don't know the
86+
/// difference, use [`Escape`].
87+
pub(crate) struct EscapeBodyTextWithWbr<'a>(pub &'a str);
88+
89+
impl<'a> fmt::Display for EscapeBodyTextWithWbr<'a> {
90+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
91+
let EscapeBodyTextWithWbr(text) = *self;
92+
if text.len() < 8 {
93+
return EscapeBodyText(text).fmt(fmt);
94+
}
95+
let mut last = 0;
96+
let mut it = text.grapheme_indices(true).peekable();
97+
let _ = it.next(); // don't insert wbr before first char
98+
while let Some((i, s)) = it.next() {
99+
let pk = it.peek();
100+
if s.chars().all(|c| c.is_whitespace()) {
101+
// don't need "First <wbr>Second"; the space is enough
102+
EscapeBodyText(&text[last..i]).fmt(fmt)?;
103+
last = i;
104+
continue;
105+
}
106+
let is_uppercase = || s.chars().any(|c| c.is_uppercase());
107+
let next_is_uppercase =
108+
|| pk.map_or(true, |(_, t)| t.chars().any(|c| c.is_uppercase()));
109+
let next_is_underscore = || pk.map_or(true, |(_, t)| t.contains('_'));
110+
let next_is_colon = || pk.map_or(true, |(_, t)| t.contains(':'));
111+
if i - last > 3 && is_uppercase() && !next_is_uppercase() {
112+
EscapeBodyText(&text[last..i]).fmt(fmt)?;
113+
fmt.write_str("<wbr>")?;
114+
last = i;
115+
} else if (s.contains(':') && !next_is_colon())
116+
|| (s.contains('_') && !next_is_underscore())
117+
{
118+
EscapeBodyText(&text[last..i + 1]).fmt(fmt)?;
119+
fmt.write_str("<wbr>")?;
120+
last = i + 1;
121+
}
122+
}
123+
if last < text.len() {
124+
EscapeBodyText(&text[last..]).fmt(fmt)?;
125+
}
126+
Ok(())
127+
}
128+
}
129+
130+
#[cfg(test)]
131+
mod tests;

src/librustdoc/html/escape/tests.rs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// basic examples
2+
#[test]
3+
fn escape_body_text_with_wbr() {
4+
use super::EscapeBodyTextWithWbr as E;
5+
// extreme corner cases
6+
assert_eq!(&E("").to_string(), "");
7+
assert_eq!(&E("a").to_string(), "a");
8+
assert_eq!(&E("A").to_string(), "A");
9+
assert_eq!(&E("_").to_string(), "_");
10+
assert_eq!(&E(":").to_string(), ":");
11+
assert_eq!(&E(" ").to_string(), " ");
12+
assert_eq!(&E("___________").to_string(), "___________");
13+
assert_eq!(&E(":::::::::::").to_string(), ":::::::::::");
14+
assert_eq!(&E(" ").to_string(), " ");
15+
// real(istic) examples
16+
assert_eq!(&E("FirstSecond").to_string(), "First<wbr>Second");
17+
assert_eq!(&E("First_Second").to_string(), "First_<wbr>Second");
18+
assert_eq!(&E("First Second").to_string(), "First Second");
19+
assert_eq!(&E("First HSecond").to_string(), "First HSecond");
20+
assert_eq!(&E("First HTTPSecond").to_string(), "First HTTP<wbr>Second");
21+
assert_eq!(&E("First SecondThird").to_string(), "First Second<wbr>Third");
22+
assert_eq!(&E("First<T>_Second").to_string(), "First&lt;<wbr>T&gt;_<wbr>Second");
23+
assert_eq!(&E("first_second").to_string(), "first_<wbr>second");
24+
assert_eq!(&E("first:second").to_string(), "first:<wbr>second");
25+
assert_eq!(&E("first::second").to_string(), "first::<wbr>second");
26+
assert_eq!(&E("MY_CONSTANT").to_string(), "MY_<wbr>CONSTANT");
27+
// a string won't get wrapped if it's less than 8 bytes
28+
assert_eq!(&E("HashSet").to_string(), "HashSet");
29+
// an individual word won't get wrapped if it's less than 4 bytes
30+
assert_eq!(&E("VecDequeue").to_string(), "VecDequeue");
31+
assert_eq!(&E("VecDequeueSet").to_string(), "VecDequeue<wbr>Set");
32+
// how to handle acronyms
33+
assert_eq!(&E("BTreeMap").to_string(), "BTree<wbr>Map");
34+
assert_eq!(&E("HTTPSProxy").to_string(), "HTTPS<wbr>Proxy");
35+
// more corners
36+
assert_eq!(&E("ṼẽçÑñéå").to_string(), "Ṽẽç<wbr>Ññéå");
37+
assert_eq!(&E("V\u{0300}e\u{0300}c\u{0300}D\u{0300}e\u{0300}q\u{0300}u\u{0300}e\u{0300}u\u{0300}e\u{0300}").to_string(), "V\u{0300}e\u{0300}c\u{0300}<wbr>D\u{0300}e\u{0300}q\u{0300}u\u{0300}e\u{0300}u\u{0300}e\u{0300}");
38+
assert_eq!(&E("LPFNACCESSIBLEOBJECTFROMWINDOW").to_string(), "LPFNACCESSIBLEOBJECTFROMWINDOW");
39+
}
40+
// property test
41+
#[test]
42+
fn escape_body_text_with_wbr_makes_sense() {
43+
use itertools::Itertools as _;
44+
45+
use super::EscapeBodyTextWithWbr as E;
46+
const C: [u8; 3] = [b'a', b'A', b'_'];
47+
for chars in [
48+
C.into_iter(),
49+
C.into_iter(),
50+
C.into_iter(),
51+
C.into_iter(),
52+
C.into_iter(),
53+
C.into_iter(),
54+
C.into_iter(),
55+
C.into_iter(),
56+
]
57+
.into_iter()
58+
.multi_cartesian_product()
59+
{
60+
let s = String::from_utf8(chars).unwrap();
61+
assert_eq!(s.len(), 8);
62+
let esc = E(&s).to_string();
63+
assert!(!esc.contains("<wbr><wbr>"));
64+
assert!(!esc.ends_with("<wbr>"));
65+
assert!(!esc.starts_with("<wbr>"));
66+
assert_eq!(&esc.replace("<wbr>", ""), &s);
67+
}
68+
}

src/librustdoc/html/format.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use crate::clean::utils::find_nearest_parent_module;
3232
use crate::clean::{self, ExternalCrate, PrimitiveType};
3333
use crate::formats::cache::Cache;
3434
use crate::formats::item_type::ItemType;
35-
use crate::html::escape::Escape;
35+
use crate::html::escape::{Escape, EscapeBodyText};
3636
use crate::html::render::Context;
3737
use crate::passes::collect_intra_doc_links::UrlFragment;
3838

@@ -988,6 +988,7 @@ pub(crate) fn anchor<'a, 'cx: 'a>(
988988
f,
989989
r#"<a class="{short_ty}" href="{url}" title="{short_ty} {path}">{text}</a>"#,
990990
path = join_with_double_colon(&fqp),
991+
text = EscapeBodyText(text.as_str()),
991992
)
992993
} else {
993994
f.write_str(text.as_str())

src/librustdoc/html/layout.rs

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ struct PageLayout<'a> {
6969
display_krate_version_extra: &'a str,
7070
}
7171

72+
pub(crate) use crate::html::render::sidebar::filters;
73+
7274
pub(crate) fn render<T: Print, S: Print>(
7375
layout: &Layout,
7476
page: &Page<'_>,

src/librustdoc/html/render/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ mod tests;
3030

3131
mod context;
3232
mod print_item;
33-
mod sidebar;
33+
pub(crate) mod sidebar;
3434
mod span_map;
3535
mod type_layout;
3636
mod write_shared;

src/librustdoc/html/render/print_item.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use crate::clean;
2929
use crate::config::ModuleSorting;
3030
use crate::formats::item_type::ItemType;
3131
use crate::formats::Impl;
32-
use crate::html::escape::Escape;
32+
use crate::html::escape::{Escape, EscapeBodyTextWithWbr};
3333
use crate::html::format::{
3434
display_fn, join_with_double_colon, print_abi_with_space, print_constness_with_space,
3535
print_where_clause, visibility_print_with_space, Buffer, Ending, PrintWithSpace,
@@ -423,7 +423,7 @@ fn item_module(w: &mut Buffer, cx: &mut Context<'_>, item: &clean::Item, items:
423423
"<div class=\"item-name\"><code>{}extern crate {} as {};",
424424
visibility_print_with_space(myitem, cx),
425425
anchor(myitem.item_id.expect_def_id(), src, cx),
426-
myitem.name.unwrap(),
426+
EscapeBodyTextWithWbr(myitem.name.unwrap().as_str()),
427427
),
428428
None => write!(
429429
w,
@@ -520,7 +520,7 @@ fn item_module(w: &mut Buffer, cx: &mut Context<'_>, item: &clean::Item, items:
520520
{stab_tags}\
521521
</div>\
522522
{docs_before}{docs}{docs_after}",
523-
name = myitem.name.unwrap(),
523+
name = EscapeBodyTextWithWbr(myitem.name.unwrap().as_str()),
524524
visibility_and_hidden = visibility_and_hidden,
525525
stab_tags = extra_info_tags(myitem, item, tcx),
526526
class = myitem.type_(),
@@ -558,7 +558,7 @@ fn extra_info_tags<'a, 'tcx: 'a>(
558558
display_fn(move |f| {
559559
write!(
560560
f,
561-
r#"<span class="stab {class}" title="{title}">{contents}</span>"#,
561+
r#"<wbr><span class="stab {class}" title="{title}">{contents}</span>"#,
562562
title = Escape(title),
563563
)
564564
})

src/librustdoc/html/render/sidebar.rs

+16
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,22 @@ impl<'a> Link<'a> {
7777
}
7878
}
7979

80+
pub(crate) mod filters {
81+
use std::fmt::Display;
82+
83+
use rinja::filters::Safe;
84+
85+
use crate::html::escape::EscapeBodyTextWithWbr;
86+
use crate::html::render::display_fn;
87+
pub(crate) fn wrapped<T>(v: T) -> rinja::Result<Safe<impl Display>>
88+
where
89+
T: Display,
90+
{
91+
let string = v.to_string();
92+
Ok(Safe(display_fn(move |f| EscapeBodyTextWithWbr(&string).fmt(f))))
93+
}
94+
}
95+
8096
pub(super) fn print_sidebar(cx: &Context<'_>, it: &clean::Item, buffer: &mut Buffer) {
8197
let blocks: Vec<LinkBlock<'_>> = match *it.kind {
8298
clean::StructItem(ref s) => sidebar_struct(cx, it, s),

src/librustdoc/html/static/css/rustdoc.css

+4-1
Original file line numberDiff line numberDiff line change
@@ -586,12 +586,15 @@ ul.block, .block li {
586586
}
587587

588588
.sidebar h2 {
589+
text-wrap: balance;
589590
overflow-wrap: anywhere;
590591
padding: 0;
591592
margin: 0.7rem 0;
592593
}
593594

594595
.sidebar h3 {
596+
text-wrap: balance;
597+
overflow-wrap: anywhere;
595598
font-size: 1.125rem; /* 18px */
596599
padding: 0;
597600
margin: 0;
@@ -2222,7 +2225,7 @@ in src-script.js and main.js
22222225
width: 33%;
22232226
}
22242227
.item-table > li > div {
2225-
word-break: break-all;
2228+
overflow-wrap: anywhere;
22262229
}
22272230
}
22282231

src/librustdoc/html/templates/page.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
</a> {# #}
9999
{% endif %}
100100
<h2> {# #}
101-
<a href="{{page.root_path|safe}}{{display_krate_with_trailing_slash|safe}}index.html">{{display_krate}}</a> {# #}
101+
<a href="{{page.root_path|safe}}{{display_krate_with_trailing_slash|safe}}index.html">{{display_krate|wrapped|safe}}</a> {# #}
102102
{% if !display_krate_version_number.is_empty() %}
103103
<span class="version">{{+ display_krate_version_number}}</span>
104104
{% endif %}

src/librustdoc/html/templates/sidebar.html

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{% if !title.is_empty() %}
22
<h2 class="location"> {# #}
3-
<a href="#">{{title_prefix}}{{title}}</a> {# #}
3+
<a href="#">{{title_prefix}}{{title|wrapped|safe}}</a> {# #}
44
</h2>
55
{% endif %}
66
<div class="sidebar-elems">
@@ -15,7 +15,9 @@ <h2 class="location"> {# #}
1515
{% for block in blocks %}
1616
{% if block.should_render() %}
1717
{% if !block.heading.name.is_empty() %}
18-
<h3><a href="#{{block.heading.href|safe}}">{{block.heading.name}}</a></h3>
18+
<h3><a href="#{{block.heading.href|safe}}"> {# #}
19+
{{block.heading.name|wrapped|safe}} {# #}
20+
</a></h3> {# #}
1921
{% endif %}
2022
{% if !block.links.is_empty() %}
2123
<ul class="block{% if !block.class.is_empty() +%} {{+block.class}}{% endif %}">
@@ -29,6 +31,6 @@ <h3><a href="#{{block.heading.href|safe}}">{{block.heading.name}}</a></h3>
2931
</section>
3032
{% endif %}
3133
{% if !path.is_empty() %}
32-
<h2><a href="{% if is_mod %}../{% endif %}index.html">In {{+ path}}</a></h2>
34+
<h2><a href="{% if is_mod %}../{% endif %}index.html">In {{+ path|wrapped|safe}}</a></h2>
3335
{% endif %}
3436
</div>

src/tools/compiletest/src/runtest.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2731,6 +2731,7 @@ impl<'test> TestCx<'test> {
27312731

27322732
#[rustfmt::skip]
27332733
let tidy_args = [
2734+
"--new-blocklevel-tags", "rustdoc-search",
27342735
"--indent", "yes",
27352736
"--indent-spaces", "2",
27362737
"--wrap", "0",

tests/rustdoc-gui/duplicate-macro-reexport.goml

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/macro.a.html"
44
wait-for: ".sidebar-elems .macro"
55
// Check there is only one macro named "a" listed in the sidebar.
66
assert-count: (
7-
"//*[@class='sidebar-elems']//*[@class='block macro']//li/a[text()='a']",
7+
"//*[@class='sidebar-elems']//*[@class='block macro']//li/a[normalize-space()='a']",
88
1,
99
)
1010
// Check there is only one macro named "b" listed in the sidebar.
1111
assert-count: (
12-
"//*[@class='sidebar-elems']//*[@class='block macro']//li/a[text()='b']",
12+
"//*[@class='sidebar-elems']//*[@class='block macro']//li/a[normalize-space()='b']",
1313
1,
1414
)

tests/rustdoc-gui/font-weight.goml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// This test checks that the font weight is correctly applied.
22
go-to: "file://" + |DOC_PATH| + "/lib2/struct.Foo.html"
3-
assert-css: ("//*[@class='rust item-decl']//a[text()='Alias']", {"font-weight": "400"})
3+
assert-css: ("//*[@class='rust item-decl']//a[normalize-space()='Alias']", {"font-weight": "400"})
44
assert-css: (
5-
"//*[@class='structfield section-header']//a[text()='Alias']",
5+
"//*[@class='structfield section-header']//a[normalize-space()='Alias']",
66
{"font-weight": "400"},
77
)
88
assert-css: ("#method\.a_method > .code-header", {"font-weight": "600"})

tests/rustdoc-gui/item-info.goml

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ assert-position: (".item-info .stab", {"x": 245})
1212
// test for <https://github.com/rust-lang/rust/issues/118615>.
1313
set-window-size: (850, 800)
1414
store-position: (
15-
"//*[@class='stab portability']//code[text()='Win32_System']",
15+
"//*[@class='stab portability']//code[normalize-space()='Win32_System']",
1616
{"x": first_line_x, "y": first_line_y},
1717
)
1818
store-position: (
19-
"//*[@class='stab portability']//code[text()='Win32_System_Diagnostics']",
19+
"//*[@class='stab portability']//code[normalize-space()='Win32_System_Diagnostics']",
2020
{"x": second_line_x, "y": second_line_y},
2121
)
2222
assert: |first_line_x| != |second_line_x| && |first_line_x| == 516 && |second_line_x| == 272

0 commit comments

Comments
 (0)