markdown: Fix table header alignment and apply alignment to cell content (#56179)

Smit Barmase created

Follow up: https://github.com/zed-industries/zed/pull/53465

For Markdown tables, headers are now always centered (ignoring column
alignment), matching standard Markdown rendering behavior. For HTML
tables, headers default to center but respect explicit `align`
attributes.

This also propagates alignment to paragraphs and headings inside table
cells, not just the cell container itself.

Release Notes:

- N/A

Change summary

crates/markdown/src/html/html_parser.rs | 45 +++++++++++++++
crates/markdown/src/markdown.rs         | 78 +++++++++++++++++++++++---
2 files changed, 114 insertions(+), 9 deletions(-)

Detailed changes

crates/markdown/src/html/html_parser.rs 🔗

@@ -867,6 +867,51 @@ mod tests {
         assert_eq!(table.body[1].columns.len(), 2);
     }
 
+    #[test]
+    fn parses_html_table_th_defaults_to_center() {
+        let html = "<table><thead><tr><th>H1</th><th>H2</th></tr></thead><tbody><tr><td>a</td><td>b</td></tr></tbody></table>";
+        let parsed = parse_html_block(html, 0..html.len()).unwrap();
+
+        let ParsedHtmlElement::Table(table) = &parsed.children[0] else {
+            panic!("expected table");
+        };
+
+        assert_eq!(table.header.len(), 1);
+        for column in &table.header[0].columns {
+            assert!(column.is_header);
+            assert_eq!(column.alignment, Alignment::Center);
+        }
+
+        for column in &table.body[0].columns {
+            assert!(!column.is_header);
+            assert_eq!(column.alignment, Alignment::None);
+        }
+    }
+
+    #[test]
+    fn parses_html_table_explicit_align_attribute_preserved() {
+        let html = "<table>\
+            <thead><tr>\
+                <th align=\"right\">H1</th>\
+                <th align=\"left\">H2</th>\
+            </tr></thead>\
+            <tbody><tr>\
+                <td align=\"center\">a</td>\
+                <td align=\"right\">b</td>\
+            </tr></tbody>\
+        </table>";
+        let parsed = parse_html_block(html, 0..html.len()).unwrap();
+
+        let ParsedHtmlElement::Table(table) = &parsed.children[0] else {
+            panic!("expected table");
+        };
+
+        assert_eq!(table.header[0].columns[0].alignment, Alignment::Right);
+        assert_eq!(table.header[0].columns[1].alignment, Alignment::Left);
+        assert_eq!(table.body[0].columns[0].alignment, Alignment::Center);
+        assert_eq!(table.body[0].columns[1].alignment, Alignment::Right);
+    }
+
     #[test]
     fn parses_html_list_as_explicit_list_node() {
         let parsed = parse_html_block(

crates/markdown/src/markdown.rs 🔗

@@ -1730,15 +1730,28 @@ impl Element for MarkdownElement {
                             }
                         }
                         MarkdownTag::Paragraph => {
-                            self.push_markdown_paragraph(&mut builder, range, markdown_end, None);
+                            let text_align_override = builder
+                                .table
+                                .current_cell_alignment()
+                                .and_then(alignment_to_text_align);
+                            self.push_markdown_paragraph(
+                                &mut builder,
+                                range,
+                                markdown_end,
+                                text_align_override,
+                            );
                         }
                         MarkdownTag::Heading { level, .. } => {
+                            let text_align_override = builder
+                                .table
+                                .current_cell_alignment()
+                                .and_then(alignment_to_text_align);
                             self.push_markdown_heading(
                                 &mut builder,
                                 *level,
                                 range,
                                 markdown_end,
-                                None,
+                                text_align_override,
                             );
                         }
                         MarkdownTag::BlockQuote(kind) => {
@@ -2000,13 +2013,10 @@ impl Element for MarkdownElement {
                             let is_header = builder.table.in_head;
                             let row_index = builder.table.row_index;
                             let col_index = builder.table.col_index;
-                            let alignment = builder.table.alignments.get(col_index).copied();
-                            let text_align = match alignment {
-                                Some(Alignment::Left) => TextAlign::Left,
-                                Some(Alignment::Center) => TextAlign::Center,
-                                Some(Alignment::Right) => TextAlign::Right,
-                                _ => self.style.base_text_style.text_align,
-                            };
+                            let alignment = builder.table.current_cell_alignment();
+                            let text_align = alignment
+                                .and_then(alignment_to_text_align)
+                                .unwrap_or(self.style.base_text_style.text_align);
 
                             let mut cell_div = div()
                                 .flex()
@@ -2445,6 +2455,25 @@ impl TableState {
     fn end_cell(&mut self) {
         self.col_index += 1;
     }
+
+    fn current_cell_alignment(&self) -> Option<Alignment> {
+        if self.alignments.is_empty() {
+            return None;
+        }
+        if self.in_head {
+            return Some(Alignment::Center);
+        }
+        self.alignments.get(self.col_index).copied()
+    }
+}
+
+fn alignment_to_text_align(alignment: Alignment) -> Option<TextAlign> {
+    match alignment {
+        Alignment::Left => Some(TextAlign::Left),
+        Alignment::Center => Some(TextAlign::Center),
+        Alignment::Right => Some(TextAlign::Right),
+        Alignment::None => None,
+    }
 }
 
 struct MarkdownElementBuilder {
@@ -3474,6 +3503,37 @@ mod tests {
         assert_eq!(second_word, "b");
     }
 
+    #[test]
+    fn test_table_state_current_cell_alignment_centers_headers() {
+        let mut table = TableState::default();
+        table.start(vec![Alignment::Left, Alignment::Right, Alignment::None]);
+
+        table.start_head();
+        for _ in 0..3 {
+            assert_eq!(table.current_cell_alignment(), Some(Alignment::Center));
+            table.end_cell();
+        }
+
+        table.end_head();
+        table.start_row();
+        assert_eq!(table.current_cell_alignment(), Some(Alignment::Left));
+        table.end_cell();
+        assert_eq!(table.current_cell_alignment(), Some(Alignment::Right));
+        table.end_cell();
+        assert_eq!(table.current_cell_alignment(), Some(Alignment::None));
+        table.end_cell();
+        table.end_row();
+
+        table.end();
+        assert_eq!(table.current_cell_alignment(), None);
+    }
+
+    #[test]
+    fn test_table_state_current_cell_alignment_outside_table() {
+        let table = TableState::default();
+        assert_eq!(table.current_cell_alignment(), None);
+    }
+
     #[test]
     fn test_table_checkbox_detection() {
         let md = "| Done |\n|------|\n| [x] |\n| [ ] |";