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:
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user