markdown: Add support for HTML table captions (#41192)

Thanks to @Angelk90 for pointing out that, we were missing this feature.
So this PR implement the caption feature for HTML tables for the
markdown preview.

**Code example**
```html
<table>
    <caption>Revenue by Region</caption>
    <tr>
        <th rowspan="2">Region</th>
        <th colspan="2">Revenue</th>
        <th rowspan="2">Growth</th>
    </tr>
    <tr>
        <th>Q2 2024</th>
        <th>Q3 2024</th>
    </tr>
    <tr>
        <td>North America</td>
        <td>$2.8M</td>
        <td>$2.4B</td>
        <td>+85,614%</td>
    </tr>
    <tr>
        <td>Europe</td>
        <td>$1.2M</td>
        <td>$1.9B</td>
        <td>+158,233%</td>
    </tr>
    <tr>
        <td>Asia-Pacific</td>
        <td>$0.5M</td>
        <td>$1.4B</td>
        <td>+279,900%</td>
    </tr>
</table>
```

**Result**:
<img width="1201" height="774" alt="Screenshot 2025-10-25 at 21 18 01"
src="https://github.com/user-attachments/assets/c2a8c1c2-f861-40df-b5c9-549932818f6e"
/>

Release Notes:

- Markdown preview: Added support for `HTML` table captions
This commit is contained in:
Remco Smits
2025-11-07 09:47:12 +01:00
committed by GitHub
parent d4ec55b183
commit 760747f127
3 changed files with 99 additions and 7 deletions
@@ -108,6 +108,7 @@ pub struct ParsedMarkdownTable {
pub source_range: Range<usize>,
pub header: Vec<ParsedMarkdownTableRow>,
pub body: Vec<ParsedMarkdownTableRow>,
pub caption: Option<MarkdownParagraph>,
}
#[derive(Debug, Clone, Copy, Default)]
+87 -1
View File
@@ -533,6 +533,7 @@ impl<'a> MarkdownParser<'a> {
source_range,
header,
body,
caption: None,
}
}
@@ -1232,11 +1233,17 @@ impl<'a> MarkdownParser<'a> {
) -> Option<ParsedMarkdownTable> {
let mut header_rows = Vec::new();
let mut body_rows = Vec::new();
let mut caption = None;
// node should be a thead or tbody element
// node should be a thead, tbody or caption element
for node in node.children.borrow().iter() {
match &node.data {
markup5ever_rcdom::NodeData::Element { name, .. } => {
if local_name!("caption") == name.local {
let mut paragraph = MarkdownParagraph::new();
self.parse_paragraph(source_range.clone(), node, &mut paragraph);
caption = Some(paragraph);
}
if local_name!("thead") == name.local {
// node should be a tr element
for node in node.children.borrow().iter() {
@@ -1262,6 +1269,7 @@ impl<'a> MarkdownParser<'a> {
source_range,
body: body_rows,
header: header_rows,
caption,
})
} else {
None
@@ -1919,6 +1927,7 @@ mod tests {
ParsedMarkdown {
children: vec![ParsedMarkdownElement::Table(table(
0..366,
None,
vec![row(vec![
column(
1,
@@ -1975,6 +1984,77 @@ mod tests {
);
}
#[gpui::test]
async fn test_html_table_with_caption() {
let parsed = parse(
"<table>
<caption>My Table</caption>
<tbody>
<tr>
<td>1</td>
<td>Chris</td>
</tr>
<tr>
<td>2</td>
<td>Dennis</td>
</tr>
</tbody>
</table>",
)
.await;
assert_eq!(
ParsedMarkdown {
children: vec![ParsedMarkdownElement::Table(table(
0..280,
Some(vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..280,
contents: "My Table".into(),
highlights: Default::default(),
region_ranges: Default::default(),
regions: Default::default()
})]),
vec![],
vec![
row(vec![
column(
1,
1,
false,
text("1", 0..280),
ParsedMarkdownTableAlignment::None
),
column(
1,
1,
false,
text("Chris", 0..280),
ParsedMarkdownTableAlignment::None
)
]),
row(vec![
column(
1,
1,
false,
text("2", 0..280),
ParsedMarkdownTableAlignment::None
),
column(
1,
1,
false,
text("Dennis", 0..280),
ParsedMarkdownTableAlignment::None
)
]),
],
))],
},
parsed
);
}
#[gpui::test]
async fn test_html_table_without_headings() {
let parsed = parse(
@@ -1997,6 +2077,7 @@ mod tests {
ParsedMarkdown {
children: vec![ParsedMarkdownElement::Table(table(
0..240,
None,
vec![],
vec![
row(vec![
@@ -2056,6 +2137,7 @@ mod tests {
ParsedMarkdown {
children: vec![ParsedMarkdownElement::Table(table(
0..150,
None,
vec![row(vec![
column(
1,
@@ -2253,6 +2335,7 @@ Some other content
let expected_table = table(
0..48,
None,
vec![row(vec![
column(
1,
@@ -2288,6 +2371,7 @@ Some other content
let expected_table = table(
0..95,
None,
vec![row(vec![
column(
1,
@@ -2809,6 +2893,7 @@ fn main() {
fn table(
source_range: Range<usize>,
caption: Option<MarkdownParagraph>,
header: Vec<ParsedMarkdownTableRow>,
body: Vec<ParsedMarkdownTableRow>,
) -> ParsedMarkdownTable {
@@ -2816,6 +2901,7 @@ fn main() {
source_range,
header,
body,
caption,
}
}
@@ -561,12 +561,17 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -
}
cx.with_common_p(div())
.grid()
.size_full()
.grid_cols(max_column_count as u16)
.border_1()
.border_color(cx.border_color)
.children(cells)
.when_some(parsed.caption.as_ref(), |this, caption| {
this.children(render_markdown_text(caption, cx))
})
.child(
div()
.grid()
.grid_cols(max_column_count as u16)
.border_1()
.border_color(cx.border_color)
.children(cells),
)
.into_any()
}