Auto merge of #156801 - yotamofek:pr/rustdoc/lazy-escape, r=<try>

Allow lazy HTML escaping
This commit is contained in:
bors
2026-05-21 13:37:59 +00:00
7 changed files with 85 additions and 36 deletions
+4 -6
View File
@@ -111,7 +111,7 @@ pub(crate) fn render_short_html(&self) -> String {
msg
}
fn render_long_inner(&self, format: Format) -> String {
fn render_long_inner(&self, format: Format) -> impl fmt::Display {
let on = if self.omit_preposition() {
" "
} else if self.should_use_with_in_description() {
@@ -132,14 +132,12 @@ fn render_long_inner(&self, format: Format) -> String {
}
/// Renders the configuration for long display, as a long HTML description.
pub(crate) fn render_long_html(&self) -> String {
let mut msg = self.render_long_inner(Format::LongHtml);
msg.push('.');
msg
pub(crate) fn render_long_html(&self) -> impl fmt::Display {
fmt::from_fn(|f| write!(f, "{}.", self.render_long_inner(Format::LongHtml)))
}
/// 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 {
self.render_long_inner(Format::LongPlain)
}
+55 -7
View File
@@ -5,16 +5,60 @@
use std::fmt;
use pulldown_cmark_escape::FmtWriter;
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 {
pulldown_cmark_escape::escape_html(FmtWriter(fmt), self.0)
self.0.fmt(
&mut fmt
.options()
.create_formatter(&mut WriteEscaped { writer: fmt, escape_quotes: true }),
)
}
}
@@ -24,11 +68,15 @@ fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
/// 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 {
pulldown_cmark_escape::escape_html_body_text(FmtWriter(fmt), self.0)
self.0.fmt(
&mut fmt
.options()
.create_formatter(&mut WriteEscaped { writer: fmt, escape_quotes: false }),
)
}
}
+10 -15
View File
@@ -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() {
@@ -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>"));
+1 -1
View File
@@ -875,7 +875,7 @@ pub(crate) fn print_anchor(did: DefId, text: Symbol, cx: &Context<'_>) -> impl D
r#"<a class="{kind}" href="{url}{anchor}" title="{kind} {path}">{text}</a>"#,
anchor = fragment(did, cx.tcx()),
path = join_path_syms(rust_path),
text = EscapeBodyText(text.as_str()),
text = EscapeBodyText(text),
)
} else {
f.write_str(text.as_str())
+8 -5
View File
@@ -845,7 +845,10 @@ fn document_item_info(
ItemInfo { items }
}
fn portability(item: &clean::Item, parent: Option<&clean::Item>) -> Option<String> {
fn portability<'a>(
item: &'a clean::Item,
parent: Option<&'a clean::Item>,
) -> Option<impl fmt::Display + 'a> {
let cfg = match (&item.cfg, parent.and_then(|p| p.cfg.as_ref())) {
(Some(cfg), Some(parent_cfg)) => cfg.simplify_with(parent_cfg),
(cfg, _) => cfg.as_deref().cloned(),
@@ -858,7 +861,7 @@ fn portability(item: &clean::Item, parent: Option<&clean::Item>) -> Option<Strin
parent_cfg = parent.and_then(|p| p.cfg.as_ref()),
);
Some(cfg?.render_long_html())
cfg.map(|cfg| fmt::from_fn(move |f| cfg.render_long_html().fmt(f)))
}
#[derive(Template)]
@@ -901,7 +904,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"),
};
@@ -935,7 +938,7 @@ fn short_item_info(
}
if let Some(message) = portability(item, parent) {
extra_info.push(ShortItemInfo::Portability { message });
extra_info.push(ShortItemInfo::Portability { message: message.to_string() });
}
extra_info
@@ -1682,7 +1685,7 @@ fn notable_traits_button(ty: &clean::Type, cx: &Context<'_>) -> Option<impl fmt:
write!(
f,
" <a href=\"#\" class=\"tooltip\" data-notable-ty=\"{ty}\">ⓘ</a>",
ty = Escape(&format!("{:#}", print_type(ty, cx))),
ty = Escape(format_args!("{:#}", print_type(ty, cx))),
)
})
})
+6 -2
View File
@@ -486,12 +486,16 @@ fn print_extra_info_tags(
import_def_id: Option<DefId>,
) -> impl Display {
fmt::from_fn(move |f| {
fn tag_html(class: &str, title: &str, contents: &str) -> impl Display {
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),
)
})
}
+1
View File
@@ -7,6 +7,7 @@
#![feature(ascii_char_variants)]
#![feature(deref_patterns)]
#![feature(file_buffered)]
#![feature(format_args_nl)]
#![feature(formatting_options)]
#![feature(iter_intersperse)]
#![feature(iter_order_by)]