Skip to content

Commit

Permalink
Auto merge of rust-lang#137274 - yotamofek:pr/rustdoc/lazy-escape, r=…
Browse files Browse the repository at this point in the history
…<try>

Allow lazy HTML escaping

Inspired by [this comment](rust-lang#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.
  • Loading branch information
bors committed Feb 19, 2025
2 parents ed49386 + c7f0154 commit bfce716
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 78 deletions.
14 changes: 8 additions & 6 deletions src/librustdoc/clean/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,16 @@ impl Cfg {
}

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

let mut msg = format!("Available {on} {}", Display(self, Format::LongPlain));
if self.should_append_only_to_description() {
msg.push_str(" only");
}
msg
fmt::from_fn(move |f| {
write!(f, "Available {on} {}", Display(self, Format::LongPlain))?;
if self.should_append_only_to_description() {
f.write_str(" only")?;
}
Ok(())
})
}

fn should_capitalize_first_letter(&self) -> bool {
Expand Down
107 changes: 55 additions & 52 deletions src/librustdoc/html/escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,58 @@ use std::fmt;

use unicode_segmentation::UnicodeSegmentation;

#[inline]
fn escape(s: &str, mut w: impl fmt::Write, escape_quotes: bool) -> fmt::Result {
// Because the internet is always right, turns out there's not that many
// characters to escape: http://stackoverflow.com/questions/7381974
let pile_o_bits = s;
let mut last = 0;
for (i, ch) in s.char_indices() {
let s = match ch {
'>' => "&gt;",
'<' => "&lt;",
'&' => "&amp;",
'\'' if escape_quotes => "&#39;",
'"' if escape_quotes => "&quot;",
_ => continue,
};
w.write_str(&pile_o_bits[last..i])?;
w.write_str(s)?;
// NOTE: we only expect single byte characters here - which is fine as long as we
// only match single byte characters
last = i + 1;
}

if last < s.len() {
w.write_str(&pile_o_bits[last..])?;
}
Ok(())
}

struct WriteEscaped<W: fmt::Write> {
writer: W,
escape_quotes: bool,
}

impl<W: fmt::Write> fmt::Write for WriteEscaped<W> {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
escape(s, &mut self.writer, self.escape_quotes)
}
}

/// Wrapper struct which will emit the HTML-escaped version of the contained
/// string when passed to a format string.
pub(crate) struct Escape<'a>(pub &'a str);
pub(crate) struct Escape<T>(pub T);

impl fmt::Display for Escape<'_> {
impl<T: fmt::Display> fmt::Display for Escape<T> {
#[inline]
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
// Because the internet is always right, turns out there's not that many
// characters to escape: http://stackoverflow.com/questions/7381974
let Escape(s) = *self;
let pile_o_bits = s;
let mut last = 0;
for (i, ch) in s.char_indices() {
let s = match ch {
'>' => "&gt;",
'<' => "&lt;",
'&' => "&amp;",
'\'' => "&#39;",
'"' => "&quot;",
_ => continue,
};
fmt.write_str(&pile_o_bits[last..i])?;
fmt.write_str(s)?;
// NOTE: we only expect single byte characters here - which is fine as long as we
// only match single byte characters
last = i + 1;
}

if last < s.len() {
fmt.write_str(&pile_o_bits[last..])?;
}
Ok(())
self.0.fmt(
&mut fmt
.options()
.create_formatter(&mut WriteEscaped { writer: fmt, escape_quotes: true }),
)
}
}

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

impl fmt::Display for EscapeBodyText<'_> {
impl<T: fmt::Display> fmt::Display for EscapeBodyText<T> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
// Because the internet is always right, turns out there's not that many
// characters to escape: http://stackoverflow.com/questions/7381974
let EscapeBodyText(s) = *self;
let pile_o_bits = s;
let mut last = 0;
for (i, ch) in s.char_indices() {
let s = match ch {
'>' => "&gt;",
'<' => "&lt;",
'&' => "&amp;",
_ => continue,
};
fmt.write_str(&pile_o_bits[last..i])?;
fmt.write_str(s)?;
// NOTE: we only expect single byte characters here - which is fine as long as we
// only match single byte characters
last = i + 1;
}

if last < s.len() {
fmt.write_str(&pile_o_bits[last..])?;
}
Ok(())
self.0.fmt(
&mut fmt
.options()
.create_formatter(&mut WriteEscaped { writer: fmt, escape_quotes: false }),
)
}
}

Expand Down
25 changes: 10 additions & 15 deletions src/librustdoc/html/escape/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
use std::iter;

#[test]
fn escape() {
use super::Escape as E;
assert_eq!(format!("<Hello> {}", E("<World>")), "<Hello> &lt;World&gt;");
}

// basic examples
#[test]
fn escape_body_text_with_wbr() {
Expand Down Expand Up @@ -47,21 +55,8 @@ fn escape_body_text_with_wbr_makes_sense() {
use itertools::Itertools as _;

use super::EscapeBodyTextWithWbr as E;
const C: [u8; 3] = [b'a', b'A', b'_'];
for chars in [
C.into_iter(),
C.into_iter(),
C.into_iter(),
C.into_iter(),
C.into_iter(),
C.into_iter(),
C.into_iter(),
C.into_iter(),
]
.into_iter()
.multi_cartesian_product()
{
let s = String::from_utf8(chars).unwrap();
for chars in iter::repeat("aA_").take(8).map(str::chars).multi_cartesian_product() {
let s = chars.into_iter().collect::<String>();
assert_eq!(s.len(), 8);
let esc = E(&s).to_string();
assert!(!esc.contains("<wbr><wbr>"));
Expand Down
2 changes: 1 addition & 1 deletion src/librustdoc/html/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ pub(crate) fn anchor<'a: 'cx, 'cx>(
f,
r#"<a class="{short_ty}" href="{url}" title="{short_ty} {path}">{text}</a>"#,
path = join_with_double_colon(&fqp),
text = EscapeBodyText(text.as_str()),
text = EscapeBodyText(text),
)
} else {
f.write_str(text.as_str())
Expand Down
4 changes: 2 additions & 2 deletions src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ fn short_item_info(
}
DeprecatedSince::Future => String::from("Deprecating in a future version"),
DeprecatedSince::NonStandard(since) => {
format!("Deprecated since {}", Escape(since.as_str()))
format!("Deprecated since {}", Escape(since))
}
DeprecatedSince::Unspecified | DeprecatedSince::Err => String::from("Deprecated"),
};
Expand Down Expand Up @@ -1519,7 +1519,7 @@ pub(crate) fn notable_traits_button<'a, 'tcx>(
write!(
f,
" <a href=\"#\" class=\"tooltip\" data-notable-ty=\"{ty}\">ⓘ</a>",
ty = Escape(&format!("{:#}", ty.print(cx))),
ty = Escape(ty.print(cx)),
)
})
})
Expand Down
8 changes: 6 additions & 2 deletions src/librustdoc/html/render/print_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,12 +534,16 @@ fn extra_info_tags<'a, 'tcx: 'a>(
import_def_id: Option<DefId>,
) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| {
fn tag_html<'a>(class: &'a str, title: &'a str, contents: &'a str) -> impl Display + 'a {
fn tag_html<'a>(
class: impl fmt::Display + 'a,
title: impl fmt::Display + 'a,
contents: impl fmt::Display + 'a,
) -> impl Display + 'a {
fmt::from_fn(move |f| {
write!(
f,
r#"<wbr><span class="stab {class}" title="{title}">{contents}</span>"#,
title = Escape(title),
title = Escape(&title),
)
})
}
Expand Down
1 change: 1 addition & 0 deletions src/librustdoc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#![feature(debug_closure_helpers)]
#![feature(file_buffered)]
#![feature(format_args_nl)]
#![feature(formatting_options)]
#![feature(if_let_guard)]
#![feature(impl_trait_in_assoc_type)]
#![feature(iter_intersperse)]
Expand Down

0 comments on commit bfce716

Please sign in to comment.