Skip to content

Commit 69f611a

Browse files
committed
Auto merge of #137274 - yotamofek:pr/rustdoc/lazy-escape, r=<try>
Allow lazy HTML escaping Inspired by [this comment](#136828 (comment)) by `@aDotInTheVoid` . Makes `Escape` and `EscapeBodyText` accept any `impl fmt::Display`, instead of a `&str`, which allows us to avoid a few interim `String` allocations. This opens up room for more lazifying, but I'll keep those for a separate PR. Unfortunately, I think there might be a hit to performance because of the double vtable-indirection caused by wrapping a `fmt::Formatter` in another one, but I think that I should be able to gain that perf back by doing more lazy printing (either the small things improvements I made in this PR, or in later ones) Probably better to review each commit individually.
2 parents ed49386 + 1dd2f33 commit 69f611a

File tree

7 files changed

+84
-79
lines changed

7 files changed

+84
-79
lines changed

src/librustdoc/clean/cfg.rs

+8-6
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,16 @@ impl Cfg {
189189
}
190190

191191
/// Renders the configuration for long display, as a long plain text description.
192-
pub(crate) fn render_long_plain(&self) -> String {
192+
pub(crate) fn render_long_plain(&self) -> impl fmt::Display + '_ {
193193
let on = if self.should_use_with_in_description() { "with" } else { "on" };
194194

195-
let mut msg = format!("Available {on} {}", Display(self, Format::LongPlain));
196-
if self.should_append_only_to_description() {
197-
msg.push_str(" only");
198-
}
199-
msg
195+
fmt::from_fn(move |f| {
196+
write!(f, "Available {on} {}", Display(self, Format::LongPlain))?;
197+
if self.should_append_only_to_description() {
198+
f.write_str(" only")?;
199+
}
200+
Ok(())
201+
})
200202
}
201203

202204
fn should_capitalize_first_letter(&self) -> bool {

src/librustdoc/html/escape.rs

+55-52
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,58 @@ use std::fmt;
77

88
use unicode_segmentation::UnicodeSegmentation;
99

10+
#[inline]
11+
fn escape(s: &str, mut w: impl fmt::Write, escape_quotes: bool) -> fmt::Result {
12+
// Because the internet is always right, turns out there's not that many
13+
// characters to escape: http://stackoverflow.com/questions/7381974
14+
let pile_o_bits = s;
15+
let mut last = 0;
16+
for (i, ch) in s.char_indices() {
17+
let s = match ch {
18+
'>' => "&gt;",
19+
'<' => "&lt;",
20+
'&' => "&amp;",
21+
'\'' if escape_quotes => "&#39;",
22+
'"' if escape_quotes => "&quot;",
23+
_ => continue,
24+
};
25+
w.write_str(&pile_o_bits[last..i])?;
26+
w.write_str(s)?;
27+
// NOTE: we only expect single byte characters here - which is fine as long as we
28+
// only match single byte characters
29+
last = i + 1;
30+
}
31+
32+
if last < s.len() {
33+
w.write_str(&pile_o_bits[last..])?;
34+
}
35+
Ok(())
36+
}
37+
38+
struct WriteEscaped<W: fmt::Write> {
39+
writer: W,
40+
escape_quotes: bool,
41+
}
42+
43+
impl<W: fmt::Write> fmt::Write for WriteEscaped<W> {
44+
#[inline]
45+
fn write_str(&mut self, s: &str) -> fmt::Result {
46+
escape(s, &mut self.writer, self.escape_quotes)
47+
}
48+
}
49+
1050
/// Wrapper struct which will emit the HTML-escaped version of the contained
1151
/// string when passed to a format string.
12-
pub(crate) struct Escape<'a>(pub &'a str);
52+
pub(crate) struct Escape<T>(pub T);
1353

14-
impl fmt::Display for Escape<'_> {
54+
impl<T: fmt::Display> fmt::Display for Escape<T> {
55+
#[inline]
1556
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
16-
// Because the internet is always right, turns out there's not that many
17-
// characters to escape: http://stackoverflow.com/questions/7381974
18-
let Escape(s) = *self;
19-
let pile_o_bits = s;
20-
let mut last = 0;
21-
for (i, ch) in s.char_indices() {
22-
let s = match ch {
23-
'>' => "&gt;",
24-
'<' => "&lt;",
25-
'&' => "&amp;",
26-
'\'' => "&#39;",
27-
'"' => "&quot;",
28-
_ => continue,
29-
};
30-
fmt.write_str(&pile_o_bits[last..i])?;
31-
fmt.write_str(s)?;
32-
// NOTE: we only expect single byte characters here - which is fine as long as we
33-
// only match single byte characters
34-
last = i + 1;
35-
}
36-
37-
if last < s.len() {
38-
fmt.write_str(&pile_o_bits[last..])?;
39-
}
40-
Ok(())
57+
self.0.fmt(
58+
&mut fmt
59+
.options()
60+
.create_formatter(&mut WriteEscaped { writer: fmt, escape_quotes: true }),
61+
)
4162
}
4263
}
4364

@@ -47,33 +68,15 @@ impl fmt::Display for Escape<'_> {
4768
/// This is only safe to use for text nodes. If you need your output to be
4869
/// safely contained in an attribute, use [`Escape`]. If you don't know the
4970
/// difference, use [`Escape`].
50-
pub(crate) struct EscapeBodyText<'a>(pub &'a str);
71+
pub(crate) struct EscapeBodyText<T>(pub T);
5172

52-
impl fmt::Display for EscapeBodyText<'_> {
73+
impl<T: fmt::Display> fmt::Display for EscapeBodyText<T> {
5374
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
54-
// Because the internet is always right, turns out there's not that many
55-
// characters to escape: http://stackoverflow.com/questions/7381974
56-
let EscapeBodyText(s) = *self;
57-
let pile_o_bits = s;
58-
let mut last = 0;
59-
for (i, ch) in s.char_indices() {
60-
let s = match ch {
61-
'>' => "&gt;",
62-
'<' => "&lt;",
63-
'&' => "&amp;",
64-
_ => continue,
65-
};
66-
fmt.write_str(&pile_o_bits[last..i])?;
67-
fmt.write_str(s)?;
68-
// NOTE: we only expect single byte characters here - which is fine as long as we
69-
// only match single byte characters
70-
last = i + 1;
71-
}
72-
73-
if last < s.len() {
74-
fmt.write_str(&pile_o_bits[last..])?;
75-
}
76-
Ok(())
75+
self.0.fmt(
76+
&mut fmt
77+
.options()
78+
.create_formatter(&mut WriteEscaped { writer: fmt, escape_quotes: false }),
79+
)
7780
}
7881
}
7982

src/librustdoc/html/escape/tests.rs

+10-15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
use std::iter;
2+
3+
#[test]
4+
fn escape() {
5+
use super::Escape as E;
6+
assert_eq!(format!("<Hello> {}", E("<World>")), "<Hello> &lt;World&gt;");
7+
}
8+
19
// basic examples
210
#[test]
311
fn escape_body_text_with_wbr() {
@@ -47,21 +55,8 @@ fn escape_body_text_with_wbr_makes_sense() {
4755
use itertools::Itertools as _;
4856

4957
use super::EscapeBodyTextWithWbr as E;
50-
const C: [u8; 3] = [b'a', b'A', b'_'];
51-
for chars in [
52-
C.into_iter(),
53-
C.into_iter(),
54-
C.into_iter(),
55-
C.into_iter(),
56-
C.into_iter(),
57-
C.into_iter(),
58-
C.into_iter(),
59-
C.into_iter(),
60-
]
61-
.into_iter()
62-
.multi_cartesian_product()
63-
{
64-
let s = String::from_utf8(chars).unwrap();
58+
for chars in iter::repeat("aA_").take(8).map(str::chars).multi_cartesian_product() {
59+
let s = chars.into_iter().collect::<String>();
6560
assert_eq!(s.len(), 8);
6661
let esc = E(&s).to_string();
6762
assert!(!esc.contains("<wbr><wbr>"));

src/librustdoc/html/format.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ pub(crate) fn anchor<'a: 'cx, 'cx>(
853853
f,
854854
r#"<a class="{short_ty}" href="{url}" title="{short_ty} {path}">{text}</a>"#,
855855
path = join_with_double_colon(&fqp),
856-
text = EscapeBodyText(text.as_str()),
856+
text = EscapeBodyText(text),
857857
)
858858
} else {
859859
f.write_str(text.as_str())

src/librustdoc/html/render/mod.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ fn short_item_info(
720720
}
721721
DeprecatedSince::Future => String::from("Deprecating in a future version"),
722722
DeprecatedSince::NonStandard(since) => {
723-
format!("Deprecated since {}", Escape(since.as_str()))
723+
format!("Deprecated since {}", Escape(since))
724724
}
725725
DeprecatedSince::Unspecified | DeprecatedSince::Err => String::from("Deprecated"),
726726
};
@@ -1518,8 +1518,8 @@ pub(crate) fn notable_traits_button<'a, 'tcx>(
15181518
fmt::from_fn(|f| {
15191519
write!(
15201520
f,
1521-
" <a href=\"#\" class=\"tooltip\" data-notable-ty=\"{ty}\">ⓘ</a>",
1522-
ty = Escape(&format!("{:#}", ty.print(cx))),
1521+
" <a href=\"#\" class=\"tooltip\" data-notable-ty=\"{ty:#}\">ⓘ</a>",
1522+
ty = Escape(ty.print(cx)),
15231523
)
15241524
})
15251525
})

src/librustdoc/html/render/print_item.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -534,12 +534,16 @@ fn extra_info_tags<'a, 'tcx: 'a>(
534534
import_def_id: Option<DefId>,
535535
) -> impl Display + 'a + Captures<'tcx> {
536536
fmt::from_fn(move |f| {
537-
fn tag_html<'a>(class: &'a str, title: &'a str, contents: &'a str) -> impl Display + 'a {
537+
fn tag_html<'a>(
538+
class: impl fmt::Display + 'a,
539+
title: impl fmt::Display + 'a,
540+
contents: impl fmt::Display + 'a,
541+
) -> impl Display + 'a {
538542
fmt::from_fn(move |f| {
539543
write!(
540544
f,
541545
r#"<wbr><span class="stab {class}" title="{title}">{contents}</span>"#,
542-
title = Escape(title),
546+
title = Escape(&title),
543547
)
544548
})
545549
}

src/librustdoc/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#![feature(debug_closure_helpers)]
99
#![feature(file_buffered)]
1010
#![feature(format_args_nl)]
11+
#![feature(formatting_options)]
1112
#![feature(if_let_guard)]
1213
#![feature(impl_trait_in_assoc_type)]
1314
#![feature(iter_intersperse)]

0 commit comments

Comments
 (0)