markdown: Add support for HTML heading elements (#38590)

This PR adds support for HTML heading (h1, h2, h3, h4, h5, h6) elements.

**Before**
<img width="1440" height="556" alt="Screenshot 2025-09-21 at 11 05 18"
src="https://github.com/user-attachments/assets/6e7241a5-be1c-4018-ba04-f29058f97941"
/>

**After**
<img width="1436" height="598" alt="Screenshot 2025-09-21 at 10 58 12"
src="https://github.com/user-attachments/assets/3f74b5f7-6c35-41db-989b-fcaaede264b5"
/>

cc @SomeoneToIgnore

Release Notes:

- Markdown: Added support for HTML `heading` elements
This commit is contained in:
Remco Smits
2025-09-30 12:39:22 +02:00
committed by GitHub
parent 95190a2034
commit d8cafdf937
@@ -826,6 +826,33 @@ impl<'a> MarkdownParser<'a> {
if let Some(image) = self.extract_image(source_range, attrs) {
elements.push(ParsedMarkdownElement::Image(image));
}
} else if matches!(
name.local,
local_name!("h1")
| local_name!("h2")
| local_name!("h3")
| local_name!("h4")
| local_name!("h5")
| local_name!("h6")
) {
let mut paragraph = MarkdownParagraph::new();
self.consume_paragraph(source_range.clone(), node, &mut paragraph);
if !paragraph.is_empty() {
elements.push(ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
source_range,
level: match name.local {
local_name!("h1") => HeadingLevel::H1,
local_name!("h2") => HeadingLevel::H2,
local_name!("h3") => HeadingLevel::H3,
local_name!("h4") => HeadingLevel::H4,
local_name!("h5") => HeadingLevel::H5,
local_name!("h6") => HeadingLevel::H6,
_ => unreachable!(),
},
contents: paragraph,
}));
}
} else {
self.consume_children(source_range, node, elements);
}
@@ -834,6 +861,40 @@ impl<'a> MarkdownParser<'a> {
}
}
fn parse_paragraph(
&self,
source_range: Range<usize>,
node: &Rc<markup5ever_rcdom::Node>,
paragraph: &mut MarkdownParagraph,
) {
match &node.data {
markup5ever_rcdom::NodeData::Text { contents } => {
paragraph.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range,
regions: Vec::default(),
contents: contents.borrow().to_string(),
region_ranges: Vec::default(),
highlights: Vec::default(),
}));
}
markup5ever_rcdom::NodeData::Element { .. } => {
self.consume_paragraph(source_range, node, paragraph);
}
_ => {}
}
}
fn consume_paragraph(
&self,
source_range: Range<usize>,
node: &Rc<markup5ever_rcdom::Node>,
paragraph: &mut MarkdownParagraph,
) {
for node in node.children.borrow().iter() {
self.parse_paragraph(source_range.clone(), node, paragraph);
}
}
fn consume_children(
&self,
source_range: Range<usize>,
@@ -1269,6 +1330,85 @@ mod tests {
);
}
#[gpui::test]
async fn test_html_heading_tags() {
let parsed = parse("<h1>Heading</h1><h2>Heading</h2><h3>Heading</h3><h4>Heading</h4><h5>Heading</h5><h6>Heading</h6>").await;
assert_eq!(
ParsedMarkdown {
children: vec![
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H1,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H2,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H3,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H4,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H5,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H6,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
],
},
parsed
);
}
#[gpui::test]
async fn test_html_image_tag() {
let parsed = parse("<img src=\"http://example.com/foo.png\" />").await;