Skip to content

Commit f3661dc

Browse files
committed
rustdoc: word wrap CamelCase in the item list table
This is an alternative to ee6459d. 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.
1 parent 4db3d12 commit f3661dc

9 files changed

+117
-5
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

+44
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,45 @@ 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+
let is_uppercase = || s.chars().any(|c| c.is_uppercase());
101+
let next_is_uppercase =
102+
|| pk.map_or(true, |(_, t)| t.chars().any(|c| c.is_uppercase()));
103+
let next_is_underscore = || pk.map_or(true, |(_, t)| t.contains('_'));
104+
if (i - last > 3 && is_uppercase() && !next_is_uppercase())
105+
|| (s.contains('_') && !next_is_underscore())
106+
{
107+
EscapeBodyText(&text[last..i]).fmt(fmt)?;
108+
fmt.write_str("<wbr>")?;
109+
last = i;
110+
}
111+
}
112+
if last < text.len() {
113+
EscapeBodyText(&text[last..]).fmt(fmt)?;
114+
}
115+
Ok(())
116+
}
117+
}
118+
119+
#[cfg(test)]
120+
mod tests;

src/librustdoc/html/escape/tests.rs

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
// real(istic) examples
10+
assert_eq!(&E("FirstSecond").to_string(), "First<wbr>Second");
11+
assert_eq!(&E("First_Second").to_string(), "First<wbr>_Second");
12+
assert_eq!(&E("First<T>_Second").to_string(), "First&lt;<wbr>T&gt;<wbr>_Second");
13+
assert_eq!(&E("first_second").to_string(), "first<wbr>_second");
14+
assert_eq!(&E("MY_CONSTANT").to_string(), "MY<wbr>_CONSTANT");
15+
assert_eq!(&E("___________").to_string(), "___________");
16+
// a string won't get wrapped if it's less than 8 bytes
17+
assert_eq!(&E("HashSet").to_string(), "HashSet");
18+
// an individual word won't get wrapped if it's less than 4 bytes
19+
assert_eq!(&E("VecDequeue").to_string(), "VecDequeue");
20+
assert_eq!(&E("VecDequeueSet").to_string(), "VecDequeue<wbr>Set");
21+
// how to handle acronyms
22+
assert_eq!(&E("BTreeMap").to_string(), "BTree<wbr>Map");
23+
assert_eq!(&E("HTTPSProxy").to_string(), "HTTPS<wbr>Proxy");
24+
// more corners
25+
assert_eq!(&E("ṼẽçÑñéå").to_string(), "Ṽẽç<wbr>Ññéå");
26+
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}");
27+
assert_eq!(&E("LPFNACCESSIBLEOBJECTFROMWINDOW").to_string(), "LPFNACCESSIBLEOBJECTFROMWINDOW");
28+
}
29+
// property test
30+
#[test]
31+
fn escape_body_text_with_wbr_makes_sense() {
32+
use itertools::Itertools as _;
33+
34+
use super::EscapeBodyTextWithWbr as E;
35+
const C: [u8; 3] = [b'a', b'A', b'_'];
36+
for chars in [
37+
C.into_iter(),
38+
C.into_iter(),
39+
C.into_iter(),
40+
C.into_iter(),
41+
C.into_iter(),
42+
C.into_iter(),
43+
C.into_iter(),
44+
C.into_iter(),
45+
]
46+
.into_iter()
47+
.multi_cartesian_product()
48+
{
49+
let s = String::from_utf8(chars).unwrap();
50+
assert_eq!(s.len(), 8);
51+
let esc = E(&s).to_string();
52+
assert!(!esc.contains("<wbr><wbr>"));
53+
assert!(!esc.ends_with("<wbr>"));
54+
assert!(!esc.starts_with("<wbr>"));
55+
assert_eq!(&esc.replace("<wbr>", ""), &s);
56+
}
57+
}

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/render/print_item.rs

+3-3
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_(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<li><div class="item-name"><a class="struct" href="struct.CreateSubscriptionPaymentSettingsPaymentMethodOptionsCustomerBalanceBankTransferEuBankTransfer.html" title="struct extremely_long_typename::CreateSubscriptionPaymentSettingsPaymentMethodOptionsCustomerBalanceBankTransferEuBankTransfer">Create<wbr />Subscription<wbr />Payment<wbr />Settings<wbr />Payment<wbr />Method<wbr />Options<wbr />Customer<wbr />Balance<wbr />Bank<wbr />Transfer<wbr />EuBank<wbr />Transfer</a></div></li>
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// ignore-tidy-linelength
2+
// Make sure that, if an extremely long type name is named,
3+
// the item table has it line wrapped.
4+
// There should be some reasonably-placed `<wbr>` tags in the snapshot file.
5+
6+
// @snapshot extremely_long_typename "extremely_long_typename/index.html" '//ul[@class="item-table"]/li'
7+
pub struct CreateSubscriptionPaymentSettingsPaymentMethodOptionsCustomerBalanceBankTransferEuBankTransfer;
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<ul class="item-table"><li><div class="item-name"><a class="constant" href="constant.MY_CONSTANT.html" title="constant item_desc_list_at_start::MY_CONSTANT">MY_CONSTANT</a></div><div class="desc docblock-short">Groups: <code>SamplePatternSGIS</code>, <code>SamplePatternEXT</code></div></li></ul>
1+
<ul class="item-table"><li><div class="item-name"><a class="constant" href="constant.MY_CONSTANT.html" title="constant item_desc_list_at_start::MY_CONSTANT">MY<wbr />_CONSTANT</a></div><div class="desc docblock-short">Groups: <code>SamplePatternSGIS</code>, <code>SamplePatternEXT</code></div></li></ul>

0 commit comments

Comments
 (0)