Auto merge of #142910 - yotamofek:pr/rustdoc/markdown-lazy-to-string, r=GuillaumeGomez

Lazy-ify some markdown rendering

Seems to have a positive effect in my local perf runs 😍

r? `@GuillaumeGomez` if you're interested, otherwise feel free to reassign
(would also love a perf run)
This commit is contained in:
bors 2025-07-03 10:33:29 +00:00
commit 9e64506923
5 changed files with 83 additions and 86 deletions

View file

@ -35,10 +35,9 @@ impl ExternalHtml {
) -> Option<ExternalHtml> {
let codes = ErrorCodes::from(nightly_build);
let ih = load_external_files(in_header, dcx)?;
let bc = load_external_files(before_content, dcx)?;
let m_bc = load_external_files(md_before_content, dcx)?;
let bc = format!(
"{bc}{}",
let bc = {
let mut bc = load_external_files(before_content, dcx)?;
let m_bc = load_external_files(md_before_content, dcx)?;
Markdown {
content: &m_bc,
links: &[],
@ -48,12 +47,13 @@ impl ExternalHtml {
playground,
heading_offset: HeadingOffset::H2,
}
.into_string()
);
let ac = load_external_files(after_content, dcx)?;
let m_ac = load_external_files(md_after_content, dcx)?;
let ac = format!(
"{ac}{}",
.write_into(&mut bc)
.unwrap();
bc
};
let ac = {
let mut ac = load_external_files(after_content, dcx)?;
let m_ac = load_external_files(md_after_content, dcx)?;
Markdown {
content: &m_ac,
links: &[],
@ -63,8 +63,10 @@ impl ExternalHtml {
playground,
heading_offset: HeadingOffset::H2,
}
.into_string()
);
.write_into(&mut ac)
.unwrap();
ac
};
Some(ExternalHtml { in_header: ih, before_content: bc, after_content: ac })
}
}

View file

@ -21,13 +21,14 @@
//! playground: &None,
//! heading_offset: HeadingOffset::H2,
//! };
//! let html = md.into_string();
//! let mut html = String::new();
//! md.write_into(&mut html).unwrap();
//! // ... something using html
//! ```
use std::borrow::Cow;
use std::collections::VecDeque;
use std::fmt::Write;
use std::fmt::{self, Write};
use std::iter::Peekable;
use std::ops::{ControlFlow, Range};
use std::path::PathBuf;
@ -1328,16 +1329,13 @@ impl LangString {
}
impl<'a> Markdown<'a> {
pub fn into_string(self) -> String {
pub fn write_into(self, f: impl fmt::Write) -> fmt::Result {
// This is actually common enough to special-case
if self.content.is_empty() {
return String::new();
return Ok(());
}
let mut s = String::with_capacity(self.content.len() * 3 / 2);
html::push_html(&mut s, self.into_iter());
s
html::write_html_fmt(f, self.into_iter())
}
fn into_iter(self) -> CodeBlocks<'a, 'a, impl Iterator<Item = Event<'a>>> {
@ -1453,19 +1451,20 @@ impl MarkdownWithToc<'_> {
(toc.into_toc(), s)
}
pub(crate) fn into_string(self) -> String {
pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
let (toc, s) = self.into_parts();
format!("<nav id=\"rustdoc\">{toc}</nav>{s}", toc = toc.print())
write!(f, "<nav id=\"rustdoc\">{toc}</nav>{s}", toc = toc.print())
}
}
impl MarkdownItemInfo<'_> {
pub(crate) fn into_string(self) -> String {
pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
let MarkdownItemInfo(md, ids) = self;
// This is actually common enough to special-case
if md.is_empty() {
return String::new();
return Ok(());
}
let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
@ -1475,8 +1474,6 @@ impl MarkdownItemInfo<'_> {
_ => event,
});
let mut s = String::with_capacity(md.len() * 3 / 2);
ids.handle_footnotes(|ids, existing_footnotes| {
let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
let p = footnotes::Footnotes::new(p, existing_footnotes);
@ -1484,10 +1481,8 @@ impl MarkdownItemInfo<'_> {
let p = p.filter(|event| {
!matches!(event, Event::Start(Tag::Paragraph) | Event::End(TagEnd::Paragraph))
});
html::push_html(&mut s, p);
});
s
html::write_html_fmt(&mut f, p)
})
}
}

View file

@ -297,7 +297,8 @@ fn test_lang_string_tokenizer() {
fn test_header() {
fn t(input: &str, expect: &str) {
let mut map = IdMap::new();
let output = Markdown {
let mut output = String::new();
Markdown {
content: input,
links: &[],
ids: &mut map,
@ -306,7 +307,8 @@ fn test_header() {
playground: &None,
heading_offset: HeadingOffset::H2,
}
.into_string();
.write_into(&mut output)
.unwrap();
assert_eq!(output, expect, "original: {}", input);
}
@ -348,7 +350,8 @@ fn test_header() {
fn test_header_ids_multiple_blocks() {
let mut map = IdMap::new();
fn t(map: &mut IdMap, input: &str, expect: &str) {
let output = Markdown {
let mut output = String::new();
Markdown {
content: input,
links: &[],
ids: map,
@ -357,7 +360,8 @@ fn test_header_ids_multiple_blocks() {
playground: &None,
heading_offset: HeadingOffset::H2,
}
.into_string();
.write_into(&mut output)
.unwrap();
assert_eq!(output, expect, "original: {}", input);
}
@ -466,7 +470,8 @@ fn test_plain_text_summary() {
fn test_markdown_html_escape() {
fn t(input: &str, expect: &str) {
let mut idmap = IdMap::new();
let output = MarkdownItemInfo(input, &mut idmap).into_string();
let mut output = String::new();
MarkdownItemInfo(input, &mut idmap).write_into(&mut output).unwrap();
assert_eq!(output, expect, "original: {}", input);
}
@ -496,7 +501,8 @@ fn test_find_testable_code_line() {
fn test_ascii_with_prepending_hashtag() {
fn t(input: &str, expect: &str) {
let mut map = IdMap::new();
let output = Markdown {
let mut output = String::new();
Markdown {
content: input,
links: &[],
ids: &mut map,
@ -505,7 +511,8 @@ fn test_ascii_with_prepending_hashtag() {
playground: &None,
heading_offset: HeadingOffset::H2,
}
.into_string();
.write_into(&mut output)
.unwrap();
assert_eq!(output, expect, "original: {}", input);
}

View file

@ -508,22 +508,21 @@ fn scrape_examples_help(shared: &SharedContext<'_>) -> String {
the Rustdoc book]({DOC_RUST_LANG_ORG_VERSION}/rustdoc/scraped-examples.html)."
));
let mut ids = IdMap::default();
format!(
"<div class=\"main-heading\">\
<h1>About scraped examples</h1>\
</div>\
<div>{}</div>",
Markdown {
fmt::from_fn(|f| Markdown {
content: &content,
links: &[],
ids: &mut ids,
ids: &mut IdMap::default(),
error_codes: shared.codes,
edition: shared.edition(),
playground: &shared.playground,
heading_offset: HeadingOffset::H1,
}
.into_string()
.write_into(f))
)
}
@ -555,20 +554,18 @@ fn render_markdown(
heading_offset: HeadingOffset,
) -> impl fmt::Display {
fmt::from_fn(move |f| {
write!(
f,
"<div class=\"docblock\">{}</div>",
Markdown {
content: md_text,
links: &links,
ids: &mut cx.id_map.borrow_mut(),
error_codes: cx.shared.codes,
edition: cx.shared.edition(),
playground: &cx.shared.playground,
heading_offset,
}
.into_string()
)
f.write_str("<div class=\"docblock\">")?;
Markdown {
content: md_text,
links: &links,
ids: &mut cx.id_map.borrow_mut(),
error_codes: cx.shared.codes,
edition: cx.shared.edition(),
playground: &cx.shared.playground,
heading_offset,
}
.write_into(&mut *f)?;
f.write_str("</div>")
})
}
@ -752,7 +749,7 @@ fn short_item_info(
let mut id_map = cx.id_map.borrow_mut();
let html = MarkdownItemInfo(note, &mut id_map);
message.push_str(": ");
message.push_str(&html.into_string());
html.write_into(&mut message).unwrap();
}
extra_info.push(ShortItemInfo::Deprecation { message });
}

View file

@ -8,7 +8,7 @@
//!
//! [docs]: https://doc.rust-lang.org/stable/rustdoc/#using-standalone-markdown-files
use std::fmt::Write as _;
use std::fmt::{self, Write as _};
use std::fs::{File, create_dir_all, read_to_string};
use std::io::prelude::*;
use std::path::Path;
@ -77,32 +77,33 @@ pub(crate) fn render_and_write<P: AsRef<Path>>(
}
let title = metadata[0];
let mut ids = IdMap::new();
let error_codes = ErrorCodes::from(options.unstable_features.is_nightly_build());
let text = if !options.markdown_no_toc {
MarkdownWithToc {
content: text,
links: &[],
ids: &mut ids,
error_codes,
edition,
playground: &playground,
let text = fmt::from_fn(|f| {
if !options.markdown_no_toc {
MarkdownWithToc {
content: text,
links: &[],
ids: &mut IdMap::new(),
error_codes,
edition,
playground: &playground,
}
.write_into(f)
} else {
Markdown {
content: text,
links: &[],
ids: &mut IdMap::new(),
error_codes,
edition,
playground: &playground,
heading_offset: HeadingOffset::H1,
}
.write_into(f)
}
.into_string()
} else {
Markdown {
content: text,
links: &[],
ids: &mut ids,
error_codes,
edition,
playground: &playground,
heading_offset: HeadingOffset::H1,
}
.into_string()
};
});
let err = write!(
let res = write!(
&mut out,
r#"<!DOCTYPE html>
<html lang="en">
@ -130,15 +131,10 @@ pub(crate) fn render_and_write<P: AsRef<Path>>(
</body>
</html>"#,
title = Escape(title),
css = css,
in_header = options.external_html.in_header,
before_content = options.external_html.before_content,
text = text,
after_content = options.external_html.after_content,
);
match err {
Err(e) => Err(format!("cannot write to `{output}`: {e}", output = output.display())),
Ok(_) => Ok(()),
}
res.map_err(|e| format!("cannot write to `{output}`: {e}", output = output.display()))
}