From 79ef10bfc36c63a944c004f4028cb6ba2bcb6552 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 25 Oct 2025 20:12:05 +0200 Subject: [PATCH] markdown: Add support for `HTML` table column `align` attribute (#41163) This PR allows you to define `align="right"` for example to change the default alignment on **HTML** table columns. This PR also refactors where we store the alignments in order to make it so you can define it column based instead of only row based. See that the `Revenue` column is left aligned instead of the default `centered`. **Result** Screenshot 2025-10-25 at 11 01 38 **Code example** ```HTML
Region Revenue Growth
Q2 2024 Q3 2024
North America $2.8M $2.4B +85,614%
Europe $1.2M $1.9B +158,233%
Asia-Pacific $0.5M $1.4B +279,900%
``` Release Notes: - markdown preview: Add support for `HTML` table column `align` attribute --- .../markdown_preview/src/markdown_elements.rs | 2 +- .../markdown_preview/src/markdown_parser.rs | 192 +++++++++++++++--- .../markdown_preview/src/markdown_renderer.rs | 18 +- 3 files changed, 172 insertions(+), 40 deletions(-) diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index b0a36a4cf29c386204f6fd1a347a839009e1c357..993c52910e704ed4f0f05194b8bf3974350d4d0c 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -106,7 +106,6 @@ pub struct ParsedMarkdownTable { pub source_range: Range, pub header: Vec, pub body: Vec, - pub column_alignments: Vec, } #[derive(Debug, Clone, Copy, Default)] @@ -126,6 +125,7 @@ pub struct ParsedMarkdownTableColumn { pub row_span: usize, pub is_header: bool, pub children: MarkdownParagraph, + pub alignment: ParsedMarkdownTableAlignment, } #[derive(Debug)] diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 28388923a75f14c601dcafecb2008570e309561f..fd3e21272674d96f70ba4103087bfe6248c3c6c0 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -466,7 +466,10 @@ impl<'a> MarkdownParser<'a> { let mut body = vec![]; let mut row_columns = vec![]; let mut in_header = true; - let column_alignments = alignment.iter().map(Self::convert_alignment).collect(); + let column_alignments = alignment + .iter() + .map(Self::convert_alignment) + .collect::>(); loop { if self.eof() { @@ -489,6 +492,10 @@ impl<'a> MarkdownParser<'a> { row_span: 1, is_header: in_header, children: cell_contents, + alignment: column_alignments + .get(row_columns.len()) + .copied() + .unwrap_or_default(), }); } Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => { @@ -515,7 +522,6 @@ impl<'a> MarkdownParser<'a> { source_range, header, body, - column_alignments, } } @@ -988,6 +994,8 @@ impl<'a> MarkdownParser<'a> { let mut children = MarkdownParagraph::new(); self.consume_paragraph(source_range, node, &mut children); + let is_header = matches!(name.local, local_name!("th")); + Some(ParsedMarkdownTableColumn { col_span: std::cmp::max( Self::attr_value(attrs, local_name!("colspan")) @@ -1001,8 +1009,22 @@ impl<'a> MarkdownParser<'a> { .unwrap_or(1), 1, ), - is_header: matches!(name.local, local_name!("th")), + is_header, children, + alignment: Self::attr_value(attrs, local_name!("align")) + .and_then(|align| match align.as_str() { + "left" => Some(ParsedMarkdownTableAlignment::Left), + "center" => Some(ParsedMarkdownTableAlignment::Center), + "right" => Some(ParsedMarkdownTableAlignment::Right), + _ => None, + }) + .unwrap_or_else(|| { + if is_header { + ParsedMarkdownTableAlignment::Center + } else { + ParsedMarkdownTableAlignment::default() + } + }), }) } _ => None, @@ -1155,7 +1177,6 @@ impl<'a> MarkdownParser<'a> { Some(ParsedMarkdownTable { source_range, body: body_rows, - column_alignments: Vec::default(), header: header_rows, }) } else { @@ -1653,17 +1674,53 @@ mod tests { children: vec![ParsedMarkdownElement::Table(table( 0..366, vec![row(vec![ - column(1, 1, true, text("Id", 0..366)), - column(1, 1, true, text("Name ", 0..366)) + column( + 1, + 1, + true, + text("Id", 0..366), + ParsedMarkdownTableAlignment::Center + ), + column( + 1, + 1, + true, + text("Name ", 0..366), + ParsedMarkdownTableAlignment::Center + ) ])], vec![ row(vec![ - column(1, 1, false, text("1", 0..366)), - column(1, 1, false, text("Chris", 0..366)) + column( + 1, + 1, + false, + text("1", 0..366), + ParsedMarkdownTableAlignment::None + ), + column( + 1, + 1, + false, + text("Chris", 0..366), + ParsedMarkdownTableAlignment::None + ) ]), row(vec![ - column(1, 1, false, text("2", 0..366)), - column(1, 1, false, text("Dennis", 0..366)) + column( + 1, + 1, + false, + text("2", 0..366), + ParsedMarkdownTableAlignment::None + ), + column( + 1, + 1, + false, + text("Dennis", 0..366), + ParsedMarkdownTableAlignment::None + ) ]), ], ))], @@ -1697,12 +1754,36 @@ mod tests { vec![], vec![ row(vec![ - column(1, 1, false, text("1", 0..240)), - column(1, 1, false, text("Chris", 0..240)) + column( + 1, + 1, + false, + text("1", 0..240), + ParsedMarkdownTableAlignment::None + ), + column( + 1, + 1, + false, + text("Chris", 0..240), + ParsedMarkdownTableAlignment::None + ) ]), row(vec![ - column(1, 1, false, text("2", 0..240)), - column(1, 1, false, text("Dennis", 0..240)) + column( + 1, + 1, + false, + text("2", 0..240), + ParsedMarkdownTableAlignment::None + ), + column( + 1, + 1, + false, + text("Dennis", 0..240), + ParsedMarkdownTableAlignment::None + ) ]), ], ))], @@ -1730,8 +1811,20 @@ mod tests { children: vec![ParsedMarkdownElement::Table(table( 0..150, vec![row(vec![ - column(1, 1, true, text("Id", 0..150)), - column(1, 1, true, text("Name", 0..150)) + column( + 1, + 1, + true, + text("Id", 0..150), + ParsedMarkdownTableAlignment::Center + ), + column( + 1, + 1, + true, + text("Name", 0..150), + ParsedMarkdownTableAlignment::Center + ) ])], vec![], ))], @@ -1915,8 +2008,20 @@ Some other content let expected_table = table( 0..48, vec![row(vec![ - column(1, 1, true, text("Header 1", 1..11)), - column(1, 1, true, text("Header 2", 12..22)), + column( + 1, + 1, + true, + text("Header 1", 1..11), + ParsedMarkdownTableAlignment::None, + ), + column( + 1, + 1, + true, + text("Header 2", 12..22), + ParsedMarkdownTableAlignment::None, + ), ])], vec![], ); @@ -1938,17 +2043,53 @@ Some other content let expected_table = table( 0..95, vec![row(vec![ - column(1, 1, true, text("Header 1", 1..11)), - column(1, 1, true, text("Header 2", 12..22)), + column( + 1, + 1, + true, + text("Header 1", 1..11), + ParsedMarkdownTableAlignment::None, + ), + column( + 1, + 1, + true, + text("Header 2", 12..22), + ParsedMarkdownTableAlignment::None, + ), ])], vec![ row(vec![ - column(1, 1, false, text("Cell 1", 49..59)), - column(1, 1, false, text("Cell 2", 60..70)), + column( + 1, + 1, + false, + text("Cell 1", 49..59), + ParsedMarkdownTableAlignment::None, + ), + column( + 1, + 1, + false, + text("Cell 2", 60..70), + ParsedMarkdownTableAlignment::None, + ), ]), row(vec![ - column(1, 1, false, text("Cell 3", 73..83)), - column(1, 1, false, text("Cell 4", 84..94)), + column( + 1, + 1, + false, + text("Cell 3", 73..83), + ParsedMarkdownTableAlignment::None, + ), + column( + 1, + 1, + false, + text("Cell 4", 84..94), + ParsedMarkdownTableAlignment::None, + ), ]), ], ); @@ -2410,7 +2551,6 @@ fn main() { body: Vec, ) -> ParsedMarkdownTable { ParsedMarkdownTable { - column_alignments: Vec::new(), source_range, header, body, @@ -2426,12 +2566,14 @@ fn main() { row_span: usize, is_header: bool, children: MarkdownParagraph, + alignment: ParsedMarkdownTableAlignment, ) -> ParsedMarkdownTableColumn { ParsedMarkdownTableColumn { col_span, row_span, is_header, children, + alignment, } } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index c6f4354423e927fba64294f2f581faf1f4356a5d..0996e40811f3d42afe748d6ee4a53a0c53757d9e 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -497,7 +497,7 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) - for (row_idx, row) in parsed.header.iter().chain(parsed.body.iter()).enumerate() { let mut col_idx = 0; - for (cell_idx, cell) in row.columns.iter().enumerate() { + for cell in row.columns.iter() { // Skip columns occupied by row-spanning cells from previous rows while col_idx < max_column_count && grid_occupied[row_idx][col_idx] { col_idx += 1; @@ -507,19 +507,7 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) - break; } - let alignment = parsed - .column_alignments - .get(cell_idx) - .copied() - .unwrap_or_else(|| { - if cell.is_header { - ParsedMarkdownTableAlignment::Center - } else { - ParsedMarkdownTableAlignment::None - } - }); - - let container = match alignment { + let container = match cell.alignment { ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(), ParsedMarkdownTableAlignment::Center => v_flex().items_center(), ParsedMarkdownTableAlignment::Right => v_flex().items_end(), @@ -917,6 +905,7 @@ mod tests { row_span, is_header: false, children, + alignment: ParsedMarkdownTableAlignment::None, } } @@ -930,6 +919,7 @@ mod tests { row_span, is_header: false, children, + alignment: ParsedMarkdownTableAlignment::None, } }