mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
markdown: Add support for colspan and rowspan for HTML tables (#39898)
Closes https://github.com/zed-industries/zed/issues/39837 This PR adds support for `colspan` feature that is only supported for HTML tables. I also fixed an edge case where the right side border was not applied because it didn't match the total column count. **Before** <img width="725" height="179" alt="499166907-385cc787-fc89-4e6d-bf06-c72c3c0bd775" src="https://github.com/user-attachments/assets/69586053-9893-4c92-aa89-7830d2bc7a6d" /> **After** <img width="1165" height="180" alt="Screenshot 2025-10-21 at 22 51 55" src="https://github.com/user-attachments/assets/f40686e7-d95b-45a6-be42-e226e2f77483" /> ```html <table> <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> ``` **TODO**: - [x] Add tests for rending logic - [x] Test all the tables again cc @bennetbo Release Notes: - Markdown: Added support for `colspan` and `rowspan` for HTML tables --------- Co-authored-by: Zed AI <ai@zed.nl> Co-authored-by: Anthony Eid <hello@anthonyeid.me>
This commit is contained in:
parent
96a0db24d9
commit
d558005058
3 changed files with 385 additions and 159 deletions
|
|
@ -104,25 +104,34 @@ pub enum HeadingLevel {
|
|||
#[derive(Debug)]
|
||||
pub struct ParsedMarkdownTable {
|
||||
pub source_range: Range<usize>,
|
||||
pub header: ParsedMarkdownTableRow,
|
||||
pub header: Vec<ParsedMarkdownTableRow>,
|
||||
pub body: Vec<ParsedMarkdownTableRow>,
|
||||
pub column_alignments: Vec<ParsedMarkdownTableAlignment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum ParsedMarkdownTableAlignment {
|
||||
/// Default text alignment.
|
||||
#[default]
|
||||
None,
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ParsedMarkdownTableColumn {
|
||||
pub col_span: usize,
|
||||
pub row_span: usize,
|
||||
pub is_header: bool,
|
||||
pub children: MarkdownParagraph,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ParsedMarkdownTableRow {
|
||||
pub children: Vec<MarkdownParagraph>,
|
||||
pub columns: Vec<ParsedMarkdownTableColumn>,
|
||||
}
|
||||
|
||||
impl Default for ParsedMarkdownTableRow {
|
||||
|
|
@ -134,12 +143,12 @@ impl Default for ParsedMarkdownTableRow {
|
|||
impl ParsedMarkdownTableRow {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
children: Vec::new(),
|
||||
columns: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_children(children: Vec<MarkdownParagraph>) -> Self {
|
||||
Self { children }
|
||||
pub fn with_columns(columns: Vec<ParsedMarkdownTableColumn>) -> Self {
|
||||
Self { columns }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -462,9 +462,9 @@ impl<'a> MarkdownParser<'a> {
|
|||
fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable {
|
||||
let (_event, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
let mut header = ParsedMarkdownTableRow::new();
|
||||
let mut header = vec![];
|
||||
let mut body = vec![];
|
||||
let mut current_row = vec![];
|
||||
let mut row_columns = vec![];
|
||||
let mut in_header = true;
|
||||
let column_alignments = alignment.iter().map(Self::convert_alignment).collect();
|
||||
|
||||
|
|
@ -484,17 +484,21 @@ impl<'a> MarkdownParser<'a> {
|
|||
Event::Start(Tag::TableCell) => {
|
||||
self.cursor += 1;
|
||||
let cell_contents = self.parse_text(false, Some(source_range));
|
||||
current_row.push(cell_contents);
|
||||
row_columns.push(ParsedMarkdownTableColumn {
|
||||
col_span: 1,
|
||||
row_span: 1,
|
||||
is_header: in_header,
|
||||
children: cell_contents,
|
||||
});
|
||||
}
|
||||
Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
|
||||
self.cursor += 1;
|
||||
let new_row = std::mem::take(&mut current_row);
|
||||
let columns = std::mem::take(&mut row_columns);
|
||||
if in_header {
|
||||
header.children = new_row;
|
||||
header.push(ParsedMarkdownTableRow { columns: columns });
|
||||
in_header = false;
|
||||
} else {
|
||||
let row = ParsedMarkdownTableRow::with_children(new_row);
|
||||
body.push(row);
|
||||
body.push(ParsedMarkdownTableRow::with_columns(columns));
|
||||
}
|
||||
}
|
||||
Event::End(TagEnd::Table) => {
|
||||
|
|
@ -941,6 +945,70 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_table_row(
|
||||
&self,
|
||||
source_range: Range<usize>,
|
||||
node: &Rc<markup5ever_rcdom::Node>,
|
||||
) -> Option<ParsedMarkdownTableRow> {
|
||||
let mut columns = Vec::new();
|
||||
|
||||
match &node.data {
|
||||
markup5ever_rcdom::NodeData::Element { name, .. } => {
|
||||
if local_name!("tr") != name.local {
|
||||
return None;
|
||||
}
|
||||
|
||||
for node in node.children.borrow().iter() {
|
||||
if let Some(column) = self.parse_table_column(source_range.clone(), node) {
|
||||
columns.push(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if columns.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ParsedMarkdownTableRow { columns })
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_table_column(
|
||||
&self,
|
||||
source_range: Range<usize>,
|
||||
node: &Rc<markup5ever_rcdom::Node>,
|
||||
) -> Option<ParsedMarkdownTableColumn> {
|
||||
match &node.data {
|
||||
markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
|
||||
if !matches!(name.local, local_name!("th") | local_name!("td")) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut children = MarkdownParagraph::new();
|
||||
self.consume_paragraph(source_range, node, &mut children);
|
||||
|
||||
Some(ParsedMarkdownTableColumn {
|
||||
col_span: std::cmp::max(
|
||||
Self::attr_value(attrs, local_name!("colspan"))
|
||||
.and_then(|span| span.parse().ok())
|
||||
.unwrap_or(1),
|
||||
1,
|
||||
),
|
||||
row_span: std::cmp::max(
|
||||
Self::attr_value(attrs, local_name!("rowspan"))
|
||||
.and_then(|span| span.parse().ok())
|
||||
.unwrap_or(1),
|
||||
1,
|
||||
),
|
||||
is_header: matches!(name.local, local_name!("th")),
|
||||
children,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_children(
|
||||
&self,
|
||||
source_range: Range<usize>,
|
||||
|
|
@ -1056,7 +1124,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
node: &Rc<markup5ever_rcdom::Node>,
|
||||
source_range: Range<usize>,
|
||||
) -> Option<ParsedMarkdownTable> {
|
||||
let mut header_columns = Vec::new();
|
||||
let mut header_rows = Vec::new();
|
||||
let mut body_rows = Vec::new();
|
||||
|
||||
// node should be a thead or tbody element
|
||||
|
|
@ -1066,21 +1134,16 @@ impl<'a> MarkdownParser<'a> {
|
|||
if local_name!("thead") == name.local {
|
||||
// node should be a tr element
|
||||
for node in node.children.borrow().iter() {
|
||||
let mut paragraph = MarkdownParagraph::new();
|
||||
self.consume_paragraph(source_range.clone(), node, &mut paragraph);
|
||||
|
||||
for paragraph in paragraph.into_iter() {
|
||||
header_columns.push(vec![paragraph]);
|
||||
if let Some(row) = self.parse_table_row(source_range.clone(), node) {
|
||||
header_rows.push(row);
|
||||
}
|
||||
}
|
||||
} else if local_name!("tbody") == name.local {
|
||||
// node should be a tr element
|
||||
for node in node.children.borrow().iter() {
|
||||
let mut row = MarkdownParagraph::new();
|
||||
self.consume_paragraph(source_range.clone(), node, &mut row);
|
||||
body_rows.push(ParsedMarkdownTableRow::with_children(
|
||||
row.into_iter().map(|column| vec![column]).collect(),
|
||||
));
|
||||
if let Some(row) = self.parse_table_row(source_range.clone(), node) {
|
||||
body_rows.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1088,12 +1151,12 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
if !header_columns.is_empty() || !body_rows.is_empty() {
|
||||
if !header_rows.is_empty() || !body_rows.is_empty() {
|
||||
Some(ParsedMarkdownTable {
|
||||
source_range,
|
||||
body: body_rows,
|
||||
column_alignments: Vec::default(),
|
||||
header: ParsedMarkdownTableRow::with_children(header_columns),
|
||||
header: header_rows,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
@ -1589,10 +1652,19 @@ mod tests {
|
|||
ParsedMarkdown {
|
||||
children: vec![ParsedMarkdownElement::Table(table(
|
||||
0..366,
|
||||
row(vec![text("Id", 0..366), text("Name ", 0..366)]),
|
||||
vec![row(vec![
|
||||
column(1, 1, true, text("Id", 0..366)),
|
||||
column(1, 1, true, text("Name ", 0..366))
|
||||
])],
|
||||
vec![
|
||||
row(vec![text("1", 0..366), text("Chris", 0..366)]),
|
||||
row(vec![text("2", 0..366), text("Dennis", 0..366)]),
|
||||
row(vec![
|
||||
column(1, 1, false, text("1", 0..366)),
|
||||
column(1, 1, false, text("Chris", 0..366))
|
||||
]),
|
||||
row(vec![
|
||||
column(1, 1, false, text("2", 0..366)),
|
||||
column(1, 1, false, text("Dennis", 0..366))
|
||||
]),
|
||||
],
|
||||
))],
|
||||
},
|
||||
|
|
@ -1622,10 +1694,16 @@ mod tests {
|
|||
ParsedMarkdown {
|
||||
children: vec![ParsedMarkdownElement::Table(table(
|
||||
0..240,
|
||||
row(vec![]),
|
||||
vec![],
|
||||
vec![
|
||||
row(vec![text("1", 0..240), text("Chris", 0..240)]),
|
||||
row(vec![text("2", 0..240), text("Dennis", 0..240)]),
|
||||
row(vec![
|
||||
column(1, 1, false, text("1", 0..240)),
|
||||
column(1, 1, false, text("Chris", 0..240))
|
||||
]),
|
||||
row(vec![
|
||||
column(1, 1, false, text("2", 0..240)),
|
||||
column(1, 1, false, text("Dennis", 0..240))
|
||||
]),
|
||||
],
|
||||
))],
|
||||
},
|
||||
|
|
@ -1651,7 +1729,10 @@ mod tests {
|
|||
ParsedMarkdown {
|
||||
children: vec![ParsedMarkdownElement::Table(table(
|
||||
0..150,
|
||||
row(vec![text("Id", 0..150), text("Name", 0..150)]),
|
||||
vec![row(vec![
|
||||
column(1, 1, true, text("Id", 0..150)),
|
||||
column(1, 1, true, text("Name", 0..150))
|
||||
])],
|
||||
vec![],
|
||||
))],
|
||||
},
|
||||
|
|
@ -1833,7 +1914,10 @@ Some other content
|
|||
|
||||
let expected_table = table(
|
||||
0..48,
|
||||
row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
|
||||
vec![row(vec![
|
||||
column(1, 1, true, text("Header 1", 1..11)),
|
||||
column(1, 1, true, text("Header 2", 12..22)),
|
||||
])],
|
||||
vec![],
|
||||
);
|
||||
|
||||
|
|
@ -1853,10 +1937,19 @@ Some other content
|
|||
|
||||
let expected_table = table(
|
||||
0..95,
|
||||
row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
|
||||
vec![row(vec![
|
||||
column(1, 1, true, text("Header 1", 1..11)),
|
||||
column(1, 1, true, text("Header 2", 12..22)),
|
||||
])],
|
||||
vec![
|
||||
row(vec![text("Cell 1", 49..59), text("Cell 2", 60..70)]),
|
||||
row(vec![text("Cell 3", 73..83), text("Cell 4", 84..94)]),
|
||||
row(vec![
|
||||
column(1, 1, false, text("Cell 1", 49..59)),
|
||||
column(1, 1, false, text("Cell 2", 60..70)),
|
||||
]),
|
||||
row(vec![
|
||||
column(1, 1, false, text("Cell 3", 73..83)),
|
||||
column(1, 1, false, text("Cell 4", 84..94)),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -2313,7 +2406,7 @@ fn main() {
|
|||
|
||||
fn table(
|
||||
source_range: Range<usize>,
|
||||
header: ParsedMarkdownTableRow,
|
||||
header: Vec<ParsedMarkdownTableRow>,
|
||||
body: Vec<ParsedMarkdownTableRow>,
|
||||
) -> ParsedMarkdownTable {
|
||||
ParsedMarkdownTable {
|
||||
|
|
@ -2324,8 +2417,22 @@ fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
fn row(children: Vec<MarkdownParagraph>) -> ParsedMarkdownTableRow {
|
||||
ParsedMarkdownTableRow { children }
|
||||
fn row(columns: Vec<ParsedMarkdownTableColumn>) -> ParsedMarkdownTableRow {
|
||||
ParsedMarkdownTableRow { columns }
|
||||
}
|
||||
|
||||
fn column(
|
||||
col_span: usize,
|
||||
row_span: usize,
|
||||
is_header: bool,
|
||||
children: MarkdownParagraph,
|
||||
) -> ParsedMarkdownTableColumn {
|
||||
ParsedMarkdownTableColumn {
|
||||
col_span,
|
||||
row_span,
|
||||
is_header,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ParsedMarkdownTable {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ use fs::normalize_path;
|
|||
use gpui::{
|
||||
AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, DefiniteLength, Div,
|
||||
Element, ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement,
|
||||
Keystroke, Length, Modifiers, ParentElement, Render, Resource, SharedString, Styled,
|
||||
StyledText, TextStyle, WeakEntity, Window, div, img, rems,
|
||||
Keystroke, Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText,
|
||||
TextStyle, WeakEntity, Window, div, img, rems,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
|
|
@ -19,8 +19,10 @@ use std::{
|
|||
};
|
||||
use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
|
||||
use ui::{
|
||||
Clickable, FluentBuilder, LinkPreview, StatefulInteractiveElement, StyledExt, StyledImage,
|
||||
ToggleState, Tooltip, VisibleOnHover, prelude::*, tooltip_container,
|
||||
ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, IconSize,
|
||||
InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, Pixels, Rems,
|
||||
StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, Tooltip, VisibleOnHover,
|
||||
h_flex, tooltip_container, v_flex,
|
||||
};
|
||||
use workspace::{OpenOptions, OpenVisible, Workspace};
|
||||
|
||||
|
|
@ -467,132 +469,100 @@ impl gpui::RenderOnce for MarkdownCheckbox {
|
|||
}
|
||||
}
|
||||
|
||||
fn paragraph_len(paragraphs: &MarkdownParagraph) -> usize {
|
||||
paragraphs
|
||||
.iter()
|
||||
.map(|paragraph| match paragraph {
|
||||
MarkdownParagraphChunk::Text(text) => text.contents.len(),
|
||||
// TODO: Scale column width based on image size
|
||||
MarkdownParagraphChunk::Image(_) => 1,
|
||||
})
|
||||
.sum()
|
||||
fn calculate_table_columns_count(rows: &Vec<ParsedMarkdownTableRow>) -> usize {
|
||||
let mut actual_column_count = 0;
|
||||
for row in rows {
|
||||
actual_column_count = actual_column_count.max(
|
||||
row.columns
|
||||
.iter()
|
||||
.map(|column| column.col_span)
|
||||
.sum::<usize>(),
|
||||
);
|
||||
}
|
||||
actual_column_count
|
||||
}
|
||||
|
||||
fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
|
||||
let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()];
|
||||
let actual_header_column_count = calculate_table_columns_count(&parsed.header);
|
||||
let actual_body_column_count = calculate_table_columns_count(&parsed.body);
|
||||
let max_column_count = std::cmp::max(actual_header_column_count, actual_body_column_count);
|
||||
|
||||
for (index, cell) in parsed.header.children.iter().enumerate() {
|
||||
let length = paragraph_len(cell);
|
||||
max_lengths[index] = length;
|
||||
}
|
||||
let total_rows = parsed.header.len() + parsed.body.len();
|
||||
|
||||
for row in &parsed.body {
|
||||
for (index, cell) in row.children.iter().enumerate() {
|
||||
let length = paragraph_len(cell);
|
||||
// Track which grid cells are occupied by spanning cells
|
||||
let mut grid_occupied = vec![vec![false; max_column_count]; total_rows];
|
||||
|
||||
if index >= max_lengths.len() {
|
||||
max_lengths.resize(index + 1, length);
|
||||
let mut cells = Vec::with_capacity(total_rows * max_column_count);
|
||||
|
||||
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() {
|
||||
// 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;
|
||||
}
|
||||
|
||||
if length > max_lengths[index] {
|
||||
max_lengths[index] = length;
|
||||
if col_idx >= max_column_count {
|
||||
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 {
|
||||
ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(),
|
||||
ParsedMarkdownTableAlignment::Center => v_flex().items_center(),
|
||||
ParsedMarkdownTableAlignment::Right => v_flex().items_end(),
|
||||
};
|
||||
|
||||
let cell_element = container
|
||||
.col_span(cell.col_span.min(max_column_count - col_idx) as u16)
|
||||
.row_span(cell.row_span.min(total_rows - row_idx) as u16)
|
||||
.children(render_markdown_text(&cell.children, cx))
|
||||
.px_2()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.size_full()
|
||||
.border_color(cx.border_color)
|
||||
.when(cell.is_header, |this| {
|
||||
this.bg(cx.title_bar_background_color)
|
||||
})
|
||||
.when(cell.row_span > 1, |this| this.justify_center())
|
||||
.when(row_idx % 2 == 1, |this| this.bg(cx.panel_background_color));
|
||||
|
||||
cells.push(cell_element);
|
||||
|
||||
// Mark grid positions as occupied for row-spanning cells
|
||||
for r in 0..cell.row_span {
|
||||
for c in 0..cell.col_span {
|
||||
if row_idx + r < total_rows && col_idx + c < max_column_count {
|
||||
grid_occupied[row_idx + r][col_idx + c] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
col_idx += cell.col_span;
|
||||
}
|
||||
}
|
||||
|
||||
let total_max_length: usize = max_lengths.iter().sum();
|
||||
let max_column_widths: Vec<f32> = max_lengths
|
||||
.iter()
|
||||
.map(|&length| length as f32 / total_max_length as f32)
|
||||
.collect();
|
||||
|
||||
let header = render_markdown_table_row(
|
||||
&parsed.header,
|
||||
&parsed.column_alignments,
|
||||
&max_column_widths,
|
||||
true,
|
||||
0,
|
||||
cx,
|
||||
);
|
||||
|
||||
let body: Vec<AnyElement> = parsed
|
||||
.body
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, row)| {
|
||||
render_markdown_table_row(
|
||||
row,
|
||||
&parsed.column_alignments,
|
||||
&max_column_widths,
|
||||
false,
|
||||
index,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
div().child(header).children(body).into_any()
|
||||
}
|
||||
|
||||
fn render_markdown_table_row(
|
||||
parsed: &ParsedMarkdownTableRow,
|
||||
alignments: &Vec<ParsedMarkdownTableAlignment>,
|
||||
max_column_widths: &Vec<f32>,
|
||||
is_header: bool,
|
||||
row_index: usize,
|
||||
cx: &mut RenderContext,
|
||||
) -> AnyElement {
|
||||
let mut items = Vec::with_capacity(parsed.children.len());
|
||||
let count = parsed.children.len();
|
||||
|
||||
for (index, cell) in parsed.children.iter().enumerate() {
|
||||
let alignment = alignments
|
||||
.get(index)
|
||||
.copied()
|
||||
.unwrap_or(ParsedMarkdownTableAlignment::None);
|
||||
|
||||
let contents = render_markdown_text(cell, cx);
|
||||
|
||||
let container = match alignment {
|
||||
ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(),
|
||||
ParsedMarkdownTableAlignment::Center => v_flex().items_center(),
|
||||
ParsedMarkdownTableAlignment::Right => v_flex().items_end(),
|
||||
};
|
||||
|
||||
let max_width = max_column_widths.get(index).unwrap_or(&0.0);
|
||||
let mut cell = container
|
||||
.w(Length::Definite(relative(*max_width)))
|
||||
.h_full()
|
||||
.children(contents)
|
||||
.px_2()
|
||||
.py_1()
|
||||
.border_color(cx.border_color)
|
||||
.border_l_1();
|
||||
|
||||
if count == index + 1 {
|
||||
cell = cell.border_r_1();
|
||||
}
|
||||
|
||||
if is_header {
|
||||
cell = cell.bg(cx.title_bar_background_color).opacity(0.6)
|
||||
}
|
||||
|
||||
items.push(cell);
|
||||
}
|
||||
|
||||
let mut row = h_flex().border_color(cx.border_color);
|
||||
|
||||
if is_header {
|
||||
row = row.border_y_1();
|
||||
} else {
|
||||
row = row.border_b_1();
|
||||
}
|
||||
|
||||
if row_index % 2 == 1 {
|
||||
row = row.bg(cx.panel_background_color)
|
||||
}
|
||||
|
||||
row.children(items).into_any_element()
|
||||
cx.with_common_p(div())
|
||||
.grid()
|
||||
.size_full()
|
||||
.grid_cols(max_column_count as u16)
|
||||
.border_1()
|
||||
.border_color(cx.border_color)
|
||||
.children(cells)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_markdown_block_quote(
|
||||
|
|
@ -903,3 +873,143 @@ impl Render for InteractiveMarkdownElementTooltip {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::markdown_elements::ParsedMarkdownTableColumn;
|
||||
use crate::markdown_elements::ParsedMarkdownText;
|
||||
|
||||
fn text(text: &str) -> MarkdownParagraphChunk {
|
||||
MarkdownParagraphChunk::Text(ParsedMarkdownText {
|
||||
source_range: 0..text.len(),
|
||||
contents: SharedString::new(text),
|
||||
highlights: Default::default(),
|
||||
region_ranges: Default::default(),
|
||||
regions: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn column(
|
||||
col_span: usize,
|
||||
row_span: usize,
|
||||
children: Vec<MarkdownParagraphChunk>,
|
||||
) -> ParsedMarkdownTableColumn {
|
||||
ParsedMarkdownTableColumn {
|
||||
col_span,
|
||||
row_span,
|
||||
is_header: false,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
fn column_with_row_span(
|
||||
col_span: usize,
|
||||
row_span: usize,
|
||||
children: Vec<MarkdownParagraphChunk>,
|
||||
) -> ParsedMarkdownTableColumn {
|
||||
ParsedMarkdownTableColumn {
|
||||
col_span,
|
||||
row_span,
|
||||
is_header: false,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_table_columns_count() {
|
||||
assert_eq!(0, calculate_table_columns_count(&vec![]));
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![
|
||||
column(1, 1, vec![text("column1")])
|
||||
])])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
2,
|
||||
calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![
|
||||
column(1, 1, vec![text("column1")]),
|
||||
column(1, 1, vec![text("column2")]),
|
||||
])])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
2,
|
||||
calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![
|
||||
column(2, 1, vec![text("column1")])
|
||||
])])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
3,
|
||||
calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![
|
||||
column(1, 1, vec![text("column1")]),
|
||||
column(2, 1, vec![text("column2")]),
|
||||
])])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
2,
|
||||
calculate_table_columns_count(&vec![
|
||||
ParsedMarkdownTableRow::with_columns(vec![
|
||||
column(1, 1, vec![text("column1")]),
|
||||
column(1, 1, vec![text("column2")]),
|
||||
]),
|
||||
ParsedMarkdownTableRow::with_columns(vec![column(1, 1, vec![text("column1")]),])
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
3,
|
||||
calculate_table_columns_count(&vec![
|
||||
ParsedMarkdownTableRow::with_columns(vec![
|
||||
column(1, 1, vec![text("column1")]),
|
||||
column(1, 1, vec![text("column2")]),
|
||||
]),
|
||||
ParsedMarkdownTableRow::with_columns(vec![column(3, 3, vec![text("column1")]),])
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_row_span_support() {
|
||||
assert_eq!(
|
||||
3,
|
||||
calculate_table_columns_count(&vec![
|
||||
ParsedMarkdownTableRow::with_columns(vec![
|
||||
column_with_row_span(1, 2, vec![text("spans 2 rows")]),
|
||||
column(1, 1, vec![text("column2")]),
|
||||
column(1, 1, vec![text("column3")]),
|
||||
]),
|
||||
ParsedMarkdownTableRow::with_columns(vec![
|
||||
// First column is covered by row span from above
|
||||
column(1, 1, vec![text("column2 row2")]),
|
||||
column(1, 1, vec![text("column3 row2")]),
|
||||
])
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
4,
|
||||
calculate_table_columns_count(&vec![
|
||||
ParsedMarkdownTableRow::with_columns(vec![
|
||||
column_with_row_span(1, 3, vec![text("spans 3 rows")]),
|
||||
column_with_row_span(2, 1, vec![text("spans 2 cols")]),
|
||||
column(1, 1, vec![text("column4")]),
|
||||
]),
|
||||
ParsedMarkdownTableRow::with_columns(vec![
|
||||
// First column covered by row span
|
||||
column(1, 1, vec![text("column2")]),
|
||||
column(1, 1, vec![text("column3")]),
|
||||
column(1, 1, vec![text("column4")]),
|
||||
]),
|
||||
ParsedMarkdownTableRow::with_columns(vec![
|
||||
// First column still covered by row span
|
||||
column(3, 1, vec![text("spans 3 cols")]),
|
||||
])
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue