markdown: Add support for `HTML` block quotes (#39755)

Remco Smits created

This PR adds support for HTML block quotes, that also allows you to have
nested variant of it.

<img width="1441" height="804" alt="Screenshot 2025-10-08 at 10 25 57"
src="https://github.com/user-attachments/assets/4e1da766-fb54-4e87-8654-1ea14330bc97"
/>

Code example used in screenshot:

```html
<blockquote>
    <p>
        Words can be like X-rays, if you use them properly—they’ll go through
        anything. You read and you’re pierced.
    </p>
    <blockquote>
        <p>
            lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor.
        </p>
    </blockquote>
</blockquote>
```

Release Notes:

- Markdown: Added support for `HTML` block quotes

Change summary

crates/markdown_preview/src/markdown_parser.rs | 77 ++++++++++++++++++++
1 file changed, 77 insertions(+)

Detailed changes

crates/markdown_preview/src/markdown_parser.rs 🔗

@@ -880,6 +880,10 @@ impl<'a> MarkdownParser<'a> {
                             contents: paragraph,
                         }));
                     }
+                } else if local_name!("blockquote") == name.local {
+                    if let Some(blockquote) = self.extract_html_blockquote(node, source_range) {
+                        elements.push(ParsedMarkdownElement::BlockQuote(blockquote));
+                    }
                 } else if local_name!("table") == name.local {
                     if let Some(table) = self.extract_html_table(node, source_range) {
                         elements.push(ParsedMarkdownElement::Table(table));
@@ -1002,6 +1006,24 @@ impl<'a> MarkdownParser<'a> {
         Some(image)
     }
 
+    fn extract_html_blockquote(
+        &self,
+        node: &Rc<markup5ever_rcdom::Node>,
+        source_range: Range<usize>,
+    ) -> Option<ParsedMarkdownBlockQuote> {
+        let mut children = Vec::new();
+        self.consume_children(source_range.clone(), node, &mut children);
+
+        if children.is_empty() {
+            None
+        } else {
+            Some(ParsedMarkdownBlockQuote {
+                children,
+                source_range,
+            })
+        }
+    }
+
     fn extract_html_table(
         &self,
         node: &Rc<markup5ever_rcdom::Node>,
@@ -1410,6 +1432,61 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_html_block_quote() {
+        let parsed = parse(
+            "<blockquote>
+              <p>some description</p>
+            </blockquote>",
+        )
+        .await;
+
+        assert_eq!(
+            ParsedMarkdown {
+                children: vec![block_quote(
+                    vec![ParsedMarkdownElement::Paragraph(text(
+                        "some description",
+                        0..76
+                    ))],
+                    0..76,
+                )]
+            },
+            parsed
+        );
+    }
+
+    #[gpui::test]
+    async fn test_html_nested_block_quote() {
+        let parsed = parse(
+            "<blockquote>
+              <p>some description</p>
+              <blockquote>
+                <p>second description</p>
+              </blockquote>
+            </blockquote>",
+        )
+        .await;
+
+        assert_eq!(
+            ParsedMarkdown {
+                children: vec![block_quote(
+                    vec![
+                        ParsedMarkdownElement::Paragraph(text("some description", 0..173)),
+                        block_quote(
+                            vec![ParsedMarkdownElement::Paragraph(text(
+                                "second description",
+                                0..173
+                            ))],
+                            0..173,
+                        )
+                    ],
+                    0..173,
+                )]
+            },
+            parsed
+        );
+    }
+
     #[gpui::test]
     async fn test_html_table() {
         let parsed = parse(