diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 5ce62224d35e5..9450262b0af1f 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -223,6 +223,71 @@ impl<'p, 'a, I: Iterator>> CodeBlocks<'p, 'a, I> { } } +/// This iterator strips all the leading and trailing empty lines in the code block. For example: +/// +/// ``` +/// +/// let x = 12; +/// +/// // hello +/// +/// ``` +/// +/// It'll only keep: +/// +/// ``` +/// let x = 12; +/// +/// // hello +/// ``` +struct CodeblockTextIter<'a, T: Iterator>> { + iter: T, + found_first_non_empty: bool, + pending_content: VecDeque>, +} + +impl<'a, T: Iterator>> CodeblockTextIter<'a, T> { + fn new(iter: T) -> Self { + Self { iter, found_first_non_empty: false, pending_content: VecDeque::new() } + } +} + +impl<'a, T: Iterator>> Iterator for CodeblockTextIter<'a, T> { + type Item = Cow<'a, str>; + + fn next(&mut self) -> Option { + // If there are still stored content because there were empty lines before a non-empty one, + // we need to provide all of them too. + if let Some(next) = self.pending_content.pop_front() { + return Some(next); + } + while let Some(next) = self.iter.next() { + // As long as we don't encounter the first non-empty lines, we skip all of them. + if !self.found_first_non_empty { + if !next.trim().is_empty() { + self.found_first_non_empty = true; + return Some(next); + } + } else if !next.trim().is_empty() { + // We need to check the buffer since it could have been filled in the meantime + // if empty lines were encountered. + if let Some(front) = self.pending_content.pop_front() { + self.pending_content.push_back(next); + return Some(front); + } + return Some(next); + } else { + // We encountered an empty line but it's maybe not the last line so we need to + // store it just in case. + self.pending_content.push_back(next); + } + } + // We clear the content in case `next` is called afterwards. + self.pending_content.clear(); + None + } +} + impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { type Item = Event<'a>; @@ -246,8 +311,8 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { _ => {} } } - let lines = origtext.lines().filter_map(|l| map_line(l).for_html()); - let text = lines.intersperse("\n".into()).collect::(); + let lines = CodeblockTextIter::new(origtext.lines().filter_map(|l| map_line(l).for_html())); + let text = lines.intersperse(Cow::Borrowed("\n")).collect::(); let parse_result = match kind { CodeBlockKind::Fenced(ref lang) => { diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index e4f72a057892f..e74bfafacd5d8 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -1,5 +1,6 @@ use super::{find_testable_code, plain_text_summary, short_markdown_summary}; use super::{ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, Markdown, MarkdownItemInfo}; +use rustc_span::create_default_session_globals_then; use rustc_span::edition::{Edition, DEFAULT_EDITION}; #[test] @@ -309,3 +310,65 @@ fn test_find_testable_code_line() { t("```rust\n```\n```rust\n```", &[1, 3]); t("```rust\n```\n ```rust\n```", &[1, 3]); } + +#[test] +fn test_handling_of_starting_and_trailing_empty_lines() { + fn t(input: &str, expect: &str) { + let mut map = IdMap::new(); + let output = Markdown { + content: input, + links: &[], + ids: &mut map, + error_codes: ErrorCodes::Yes, + edition: DEFAULT_EDITION, + playground: &None, + heading_offset: HeadingOffset::H2, + } + .into_string(); + assert_eq!(output, expect, "original: {}", input); + } + + create_default_session_globals_then(|| { + t( + "\ +``` +// hello + + +f(); + +// bye +```", + r#" +
// hello
+
+
+f();
+
+// bye
+"#, + ); + t( + "\ +``` + + +// hello + +f(); + +// bye + + + +```", + r#" +
// hello
+
+f();
+
+// bye
+"#, + ); + }); +}