From c0ad4e234dbcdb80a714e6d8b0328e1e13308f9a Mon Sep 17 00:00:00 2001
From: jfmcdowell <206422+jfmcdowell@users.noreply.github.com>
Date: Mon, 13 Apr 2026 22:31:33 -0400
Subject: [PATCH 1/2] fix(markdown_parser): trim trailing newline in tight
lists inside blockquotes
MdQuotePrefix blocks leaked into list item content, causing
last_is_paragraph to evaluate false and skip trailing newline
trimming on the first
of tight lists nested in blockquotes.
---
crates/biome_markdown_parser/src/to_html.rs | 74 ++++++++++++++++++++-
1 file changed, 72 insertions(+), 2 deletions(-)
diff --git a/crates/biome_markdown_parser/src/to_html.rs b/crates/biome_markdown_parser/src/to_html.rs
index 2e8d049343de..172654c97a0e 100644
--- a/crates/biome_markdown_parser/src/to_html.rs
+++ b/crates/biome_markdown_parser/src/to_html.rs
@@ -645,12 +645,12 @@ impl<'a> HtmlRenderer<'a> {
let first_is_paragraph = blocks
.iter()
- .find(|b| !is_newline_block(b))
+ .find(|b| !is_transparent_block(b))
.is_some_and(is_paragraph_block);
let last_is_paragraph = blocks
.iter()
.rev()
- .find(|b| !is_newline_block(b))
+ .find(|b| !is_transparent_block(b))
.is_some_and(is_paragraph_block);
let leading_newline = !is_tight || !first_is_paragraph;
@@ -1773,6 +1773,19 @@ fn is_newline_block(block: &AnyMdBlock) -> bool {
)
}
+/// Check if a block is structural-only and produces no rendered content.
+/// This includes newline blocks, continuation indents, and quote prefix markers
+/// that can appear as children of list item content when lists are nested
+/// inside blockquotes.
+fn is_transparent_block(block: &AnyMdBlock) -> bool {
+ matches!(
+ block,
+ AnyMdBlock::AnyMdLeafBlock(
+ AnyMdLeafBlock::MdNewline(_) | AnyMdLeafBlock::MdContinuationIndent(_),
+ ) | AnyMdBlock::MdQuotePrefix(_)
+ )
+}
+
/// Check if blocks are effectively empty (empty or only newlines).
fn is_empty_content(blocks: &[AnyMdBlock]) -> bool {
blocks.is_empty() || blocks.iter().all(is_newline_block)
@@ -2116,4 +2129,61 @@ mod tests {
"\n\n- ordered
\n
\n"
);
}
+
+ #[test]
+ fn test_tight_bullet_list_in_blockquote() {
+ let parsed = parse_markdown("> - a\n> - b\n");
+ let html = document_to_html(
+ &parsed.tree(),
+ parsed.list_tightness(),
+ parsed.list_item_indents(),
+ parsed.quote_indents(),
+ );
+ assert_eq!(
+ html,
+ "\n\n
\n"
+ );
+ }
+
+ #[test]
+ fn test_tight_ordered_list_in_blockquote() {
+ let parsed = parse_markdown("> 1. a\n> 2. b\n");
+ let html = document_to_html(
+ &parsed.tree(),
+ parsed.list_tightness(),
+ parsed.list_item_indents(),
+ parsed.quote_indents(),
+ );
+ assert_eq!(
+ html,
+ "\n\n- a
\n- b
\n
\n
\n"
+ );
+ }
+
+ #[test]
+ fn test_tight_three_item_bullet_list_in_blockquote() {
+ let parsed = parse_markdown("> - a\n> - b\n> - c\n");
+ let html = document_to_html(
+ &parsed.tree(),
+ parsed.list_tightness(),
+ parsed.list_item_indents(),
+ parsed.quote_indents(),
+ );
+ assert_eq!(
+ html,
+ "\n\n
\n"
+ );
+ }
+
+ #[test]
+ fn test_tight_list_not_in_blockquote() {
+ let parsed = parse_markdown("- a\n- b\n");
+ let html = document_to_html(
+ &parsed.tree(),
+ parsed.list_tightness(),
+ parsed.list_item_indents(),
+ parsed.quote_indents(),
+ );
+ assert_eq!(html, "\n");
+ }
}
From 4acaa14056b7850c23200a70647ec4b17f7fa53a Mon Sep 17 00:00:00 2001
From: jfmcdowell <206422+jfmcdowell@users.noreply.github.com>
Date: Tue, 14 Apr 2026 10:13:01 -0400
Subject: [PATCH 2/2] refactor(markdown): reuse transparent block check for
empty content
---
crates/biome_markdown_parser/src/to_html.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/biome_markdown_parser/src/to_html.rs b/crates/biome_markdown_parser/src/to_html.rs
index 172654c97a0e..2c39544428c9 100644
--- a/crates/biome_markdown_parser/src/to_html.rs
+++ b/crates/biome_markdown_parser/src/to_html.rs
@@ -1788,7 +1788,7 @@ fn is_transparent_block(block: &AnyMdBlock) -> bool {
/// Check if blocks are effectively empty (empty or only newlines).
fn is_empty_content(blocks: &[AnyMdBlock]) -> bool {
- blocks.is_empty() || blocks.iter().all(is_newline_block)
+ blocks.is_empty() || blocks.iter().all(is_transparent_block)
}
fn list_item_required_indent(entry: &ListItemIndent) -> usize {