use crate::{
HasSpan, Parser, Span,
blocks::{
Block, ColumnStyle, ContentModel, DataFormat, HorizontalAlignment, IsBlock, TableBlock,
TableCellContent, VerticalAlignment,
},
content::SubstitutionGroup,
parser::ModificationContext,
};
fn parse_table(source: &str) -> TableBlock<'_> {
let mut parser = Parser::default();
let mi = Block::parse(Span::new(source), &mut parser)
.unwrap_if_no_warnings()
.unwrap();
match mi.item {
Block::Table(table) => table,
other => panic!("expected a table block, got {other:?}"),
}
}
fn row_text(row: &crate::blocks::TableRow<'_>) -> Vec<String> {
row.cells()
.iter()
.map(|cell| match cell.content() {
TableCellContent::Simple(content) => content.rendered().to_string(),
TableCellContent::AsciiDoc(_) => panic!("expected simple cell content"),
})
.collect()
}
#[test]
fn two_columns_three_rows() {
let table = parse_table(
"[cols=\"1,1\"]\n|===\n|Cell in column 1, row 1\n|Cell in column 2, row 1\n\n|Cell in column 1, row 2\n|Cell in column 2, row 2\n\n|Cell in column 1, row 3\n|Cell in column 2, row 3\n|===",
);
assert_eq!(table.content_model(), ContentModel::Table);
assert_eq!(table.raw_context().as_ref(), "table");
assert_eq!(table.columns().len(), 2);
assert!(table.header_row().is_none());
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(
rows,
vec![
vec![
"Cell in column 1, row 1".to_string(),
"Cell in column 2, row 1".to_string()
],
vec![
"Cell in column 1, row 2".to_string(),
"Cell in column 2, row 2".to_string()
],
vec![
"Cell in column 1, row 3".to_string(),
"Cell in column 2, row 3".to_string()
],
]
);
}
#[test]
fn multiple_cells_on_one_line() {
let table = parse_table(
"[cols=\"1,1\"]\n|===\n|Cell in column 1, row 1\n|Cell in column 2, row 1\n\n|Cell in column 1, row 2 |Cell in column 2, row 2\n|Cell in column 1, row 3 |Cell in column 2, row 3\n|===",
);
assert_eq!(table.body_rows().len(), 3);
assert_eq!(
row_text(&table.body_rows()[2]),
vec![
"Cell in column 1, row 3".to_string(),
"Cell in column 2, row 3".to_string()
]
);
}
#[test]
fn implicit_header_row() {
let table = parse_table(
"[cols=\"1,1\"]\n|===\n|Cell in column 1, header row |Cell in column 2, header row\n\n|Cell in column 1, row 2\n|Cell in column 2, row 2\n\n|Cell in column 1, row 3\n|Cell in column 2, row 3\n|===",
);
let header = table.header_row().unwrap();
assert_eq!(
row_text(header),
vec![
"Cell in column 1, header row".to_string(),
"Cell in column 2, header row".to_string()
]
);
assert_eq!(table.body_rows().len(), 2);
}
#[test]
fn implicit_columns_from_first_row() {
let table = parse_table(
"|===\n\n|Cell in column 1, row 1 |Cell in column 2, row 1 |Cell in column 3, row 1\n\n|Cell in column 1, row 2\n|Cell in column 2, row 2\n|Cell in column 3, row 2\n|===",
);
assert_eq!(table.columns().len(), 3);
assert!(table.header_row().is_none());
assert_eq!(table.body_rows().len(), 2);
assert_eq!(
row_text(&table.body_rows()[0]),
vec![
"Cell in column 1, row 1".to_string(),
"Cell in column 2, row 1".to_string(),
"Cell in column 3, row 1".to_string()
]
);
}
#[test]
fn column_multiplier() {
let table = parse_table("[cols=\"5,3*\"]\n|===\n|a |b |c |d\n|===");
let widths: Vec<usize> = table.columns().iter().map(|c| c.width()).collect();
assert_eq!(widths, vec![5, 1, 1, 1]);
}
#[test]
fn table_width_attribute() {
assert_eq!(parse_table("[width=75%]\n|===\n|a\n|===").width(), Some(75));
assert_eq!(parse_table("[width=75]\n|===\n|a\n|===").width(), Some(75));
assert_eq!(parse_table("|===\n|a\n|===").width(), None);
assert_eq!(parse_table("[width=0]\n|===\n|a\n|===").width(), None);
assert_eq!(parse_table("[width=101]\n|===\n|a\n|===").width(), None);
assert_eq!(parse_table("[width=half]\n|===\n|a\n|===").width(), None);
}
#[test]
fn autowidth_option_makes_columns_autowidth() {
for source in [
"[%autowidth]\n|===\n|a |b\n|===",
"[options=\"autowidth\"]\n|===\n|a |b\n|===",
] {
let table = parse_table(source);
assert!(table.is_autowidth());
assert!(table.columns().iter().all(|c| c.is_autowidth()));
}
let table = parse_table("[%autowidth,cols=\"2,1\"]\n|===\n|a |b\n|===");
assert!(table.columns().iter().all(|c| c.is_autowidth()));
let widths: Vec<usize> = table.columns().iter().map(|c| c.width()).collect();
assert_eq!(widths, vec![2, 1]);
let table = parse_table("|===\n|a |b\n|===");
assert!(!table.is_autowidth());
assert!(table.columns().iter().all(|c| !c.is_autowidth()));
}
#[test]
fn autowidth_column_via_tilde() {
let table = parse_table("[cols=\"25h,~,~\"]\n|===\n|a |b |c\n|===");
let columns: Vec<(usize, bool)> = table
.columns()
.iter()
.map(|c| (c.width(), c.is_autowidth()))
.collect();
assert_eq!(columns, vec![(25, false), (1, true), (1, true)]);
assert!(!table.is_autowidth());
}
#[test]
fn cell_content_is_substituted() {
let table = parse_table("|===\n|*bold* and _italic_\n|===");
assert_eq!(
row_text(&table.body_rows()[0]),
vec!["<strong>bold</strong> and <em>italic</em>".to_string()]
);
}
#[test]
fn leading_and_trailing_whitespace_stripped() {
let table = parse_table("[cols=\"1,1\"]\n|===\n|a | b spaced\n|===");
assert_eq!(
row_text(&table.body_rows()[0]),
vec!["a".to_string(), "b spaced".to_string()]
);
}
#[test]
fn block_is_recognized_via_debug() {
let mut parser = Parser::default();
let mi = Block::parse(Span::new("|===\n|a |b\n|==="), &mut parser)
.unwrap_if_no_warnings()
.unwrap();
let debug_output = format!("{:?}", mi.item);
assert!(debug_output.starts_with("Block::Table"));
}
#[test]
fn unterminated_table_warns() {
let mut parser = Parser::default();
let maw = Block::parse(Span::new("|===\n|a |b"), &mut parser);
assert!(maw.warnings.iter().any(|w| matches!(
w.warning,
crate::warnings::WarningType::UnterminatedDelimitedBlock
)));
}
#[test]
fn block_level_accessors() {
let mut parser = Parser::default();
let block = Block::parse(Span::new("|===\n|a |b\n|==="), &mut parser)
.unwrap_if_no_warnings()
.unwrap()
.item;
assert_eq!(block.content_model(), ContentModel::Table);
assert_eq!(block.raw_context().as_ref(), "table");
assert_eq!(block.resolved_context().as_ref(), "table");
assert!(block.rendered_content().is_none());
assert_eq!(block.nested_blocks().count(), 0);
assert!(block.declared_style().is_none());
assert!(block.id().is_none());
assert!(block.roles().is_empty());
assert!(block.options().is_empty());
assert!(block.title_source().is_none());
assert!(block.title().is_none());
assert!(block.anchor().is_none());
assert!(block.anchor_reftext().is_none());
assert!(block.attrlist().is_none());
assert_eq!(block.substitution_group(), SubstitutionGroup::Normal);
assert_eq!(block.span().data(), "|===\n|a |b\n|===");
}
#[test]
fn escaped_cell_separator() {
let table = parse_table("|===\n|a \\| b\n|===");
assert_eq!(table.body_rows().len(), 1);
assert_eq!(row_text(&table.body_rows()[0]), vec!["a | b".to_string()]);
}
#[test]
fn separator_after_backslash_is_escaped() {
let table = parse_table("|===\n|a\\\\|b\n|===");
assert_eq!(table.body_rows().len(), 1);
assert_eq!(row_text(&table.body_rows()[0]), vec!["a\\|b".to_string()]);
}
#[test]
fn cols_with_empty_specifier() {
let table = parse_table("[cols=\"1,,1\"]\n|===\n|a |b |c\n|===");
assert_eq!(table.columns().len(), 3);
}
#[test]
fn no_cols_and_first_line_without_a_cell() {
let table = parse_table("|===\nnot a cell\n|===");
assert!(table.columns().is_empty());
assert!(table.header_row().is_none());
assert!(table.body_rows().is_empty());
}
#[test]
fn header_option_without_cells() {
let table = parse_table("[%header,cols=\"1,1\"]\n|===\nnot a cell\n|===");
assert_eq!(table.columns().len(), 2);
assert!(table.header_row().is_none());
assert!(table.body_rows().is_empty());
}
#[test]
fn titled_table_is_captioned() {
let table = parse_table(".A table with a title\n|===\n|a |b\n|===");
assert_eq!(table.title(), Some("A table with a title"));
assert_eq!(table.caption(), Some("Table 1. "));
assert_eq!(table.number(), Some(1));
}
#[test]
fn untitled_table_has_no_caption() {
let table = parse_table("|===\n|a |b\n|===");
assert!(table.title().is_none());
assert!(table.caption().is_none());
assert!(table.number().is_none());
}
#[test]
fn caption_and_number_are_reported_through_the_isblock_trait() {
fn via_trait<'src>(block: &impl IsBlock<'src>) -> (Option<String>, Option<usize>) {
(block.caption().map(str::to_string), block.number())
}
let table = parse_table(".A table with a title\n|===\n|a |b\n|===");
assert_eq!(via_trait(&table), (Some("Table 1. ".to_string()), Some(1)));
let untitled = parse_table("|===\n|a |b\n|===");
assert_eq!(via_trait(&untitled), (None, None));
}
#[test]
fn captioned_tables_are_numbered_in_document_order() {
let doc = Parser::default()
.parse(".First\n|===\n|a\n|===\n\n|===\n|b\n|===\n\n.Second\n|===\n|c\n|===");
let captions: Vec<(Option<&str>, Option<usize>)> = doc
.nested_blocks()
.filter_map(|block| match block {
Block::Table(table) => Some((table.caption(), table.number())),
_ => None,
})
.collect();
assert_eq!(
captions,
vec![
(Some("Table 1. "), Some(1)),
(None, None),
(Some("Table 2. "), Some(2)),
]
);
}
#[test]
fn table_caption_can_be_relabeled() {
let doc = Parser::default().parse(":table-caption: Spreadsheet\n\n.Numbers\n|===\n|a\n|===");
let caption = doc.nested_blocks().find_map(|block| match block {
Block::Table(table) => table.caption().map(|c| c.to_string()),
_ => None,
});
assert_eq!(caption.as_deref(), Some("Spreadsheet 1. "));
}
#[test]
fn unsetting_table_caption_suppresses_the_label() {
let doc = Parser::default().parse(":!table-caption:\n\n.Numbers\n|===\n|a\n|===");
let table = doc
.nested_blocks()
.find_map(|block| match block {
Block::Table(table) => Some(table),
_ => None,
})
.unwrap();
assert_eq!(table.title(), Some("Numbers"));
assert!(table.caption().is_none());
}
#[test]
fn empty_table_caption_suppresses_the_label() {
let mut parser = Parser::default().with_intrinsic_attribute(
"table-caption",
"",
ModificationContext::Anywhere,
);
let doc = parser.parse(".First\n|===\n|a\n|===\n\n.Second\n|===\n|b\n|===");
let observed: Vec<(Option<&str>, Option<&str>)> = doc
.nested_blocks()
.filter_map(|block| match block {
Block::Table(table) => Some((table.title(), table.caption())),
_ => None,
})
.collect();
assert_eq!(
observed,
vec![(Some("First"), None), (Some("Second"), None)]
);
}
#[test]
fn caption_attribute_sets_the_label_verbatim() {
let table =
parse_table("[caption=\"Table A. \"]\n.A table with a custom label\n|===\n|a\n|===");
assert_eq!(table.title(), Some("A table with a custom label"));
assert_eq!(table.caption(), Some("Table A. "));
}
#[test]
fn caption_attribute_does_not_consume_a_table_number() {
let doc = Parser::default().parse(
".First\n|===\n|a\n|===\n\n[caption=\"Table A. \"]\n.Custom\n|===\n|b\n|===\n\n.Third\n|===\n|c\n|===",
);
let captions: Vec<(Option<&str>, Option<usize>)> = doc
.nested_blocks()
.filter_map(|block| match block {
Block::Table(table) => Some((table.caption(), table.number())),
_ => None,
})
.collect();
assert_eq!(
captions,
vec![
(Some("Table 1. "), Some(1)),
(Some("Table A. "), None),
(Some("Table 2. "), Some(2)),
]
);
}
#[test]
fn caption_attribute_applies_even_when_table_caption_is_unset() {
let doc = Parser::default()
.parse(":!table-caption:\n\n[caption=\"Forced. \"]\n.Numbers\n|===\n|a\n|===");
let caption = doc.nested_blocks().find_map(|block| match block {
Block::Table(table) => table.caption().map(|c| c.to_string()),
_ => None,
});
assert_eq!(caption.as_deref(), Some("Forced. "));
}
#[test]
fn caption_attribute_is_ignored_without_a_title() {
let table = parse_table("[caption=\"Table A. \"]\n|===\n|a\n|===");
assert!(table.title().is_none());
assert!(table.caption().is_none());
}
#[test]
fn empty_caption_attribute_suppresses_the_label() {
let table = parse_table("[caption=]\n.A table with a title but no label\n|===\n|a\n|===");
assert_eq!(table.title(), Some("A table with a title but no label"));
assert!(table.caption().is_none());
}
#[test]
fn empty_caption_attribute_does_not_consume_a_table_number() {
let doc = Parser::default().parse(
".First\n|===\n|a\n|===\n\n[caption=]\n.Unlabeled\n|===\n|b\n|===\n\n.Third\n|===\n|c\n|===",
);
let captions: Vec<(Option<&str>, Option<usize>)> = doc
.nested_blocks()
.filter_map(|block| match block {
Block::Table(table) => Some((table.caption(), table.number())),
_ => None,
})
.collect();
assert_eq!(
captions,
vec![
(Some("Table 1. "), Some(1)),
(None, None),
(Some("Table 2. "), Some(2)),
]
);
}
#[test]
fn malformed_vertical_operator_falls_back_to_defaults() {
let table = parse_table("[cols=\".x,1\"]\n|===\n|a |b\n|===");
let columns = table.columns();
assert_eq!(columns.len(), 2);
assert_eq!(columns[0].width(), 1);
assert_eq!(columns[0].h_align(), HorizontalAlignment::Left);
assert_eq!(columns[0].v_align(), VerticalAlignment::Top);
assert_eq!(columns[0].style(), ColumnStyle::Default);
}
#[test]
fn literal_column_processes_content_verbatim() {
let table = parse_table("[cols=\"l\"]\n|===\n|lit *z* and <x>\n|===");
assert_eq!(table.columns()[0].style(), ColumnStyle::Literal);
match table.body_rows()[0].cells()[0].content() {
TableCellContent::Simple(content) => {
assert_eq!(content.rendered(), "lit *z* and <x>");
}
TableCellContent::AsciiDoc(_) => panic!("expected simple cell content"),
}
}
#[test]
fn malformed_style_operator_falls_back_to_default() {
let table = parse_table("[cols=\"1em\"]\n|===\n|a\n|===");
assert_eq!(table.columns()[0].style(), ColumnStyle::Default);
}
#[test]
fn asciidoc_cell_resolves_references_in_nested_blocks() {
let doc = Parser::default()
.parse("[#target]\nTarget paragraph.\n\n[cols=\"a\"]\n|===\n|See xref:target[].\n|===");
let table = doc
.nested_blocks()
.find_map(|block| match block {
Block::Table(table) => Some(table),
_ => None,
})
.unwrap();
let blocks = match table.body_rows()[0].cells()[0].content() {
TableCellContent::AsciiDoc(cell) => cell.blocks(),
TableCellContent::Simple(_) => panic!("expected AsciiDoc cell content"),
};
let rendered = blocks[0].rendered_content().unwrap();
assert!(
rendered.contains("href=\"#target\""),
"xref was not resolved: {rendered}"
);
}
#[test]
fn asciidoc_cell_attributes_are_scoped_to_the_cell() {
let mut parser = Parser::default();
let doc = parser.parse(
":parent-attr: inherited\n\n[cols=\"a\"]\n|===\n|\n:cell-attr: leaked\ncell sees: {parent-attr} {cell-attr}\n|===",
);
let table = doc
.nested_blocks()
.find_map(|block| match block {
Block::Table(table) => Some(table),
_ => None,
})
.unwrap();
let blocks = match table.body_rows()[0].cells()[0].content() {
TableCellContent::AsciiDoc(cell) => cell.blocks(),
TableCellContent::Simple(_) => panic!("expected AsciiDoc cell content"),
};
let rendered: String = blocks
.iter()
.filter_map(|block| block.rendered_content())
.collect::<Vec<_>>()
.join("\n");
assert!(
rendered.contains("inherited"),
"cell did not inherit the parent attribute: {rendered}"
);
assert!(
rendered.contains("leaked"),
"cell did not see its own attribute: {rendered}"
);
assert!(!parser.has_attribute("cell-attr"));
assert!(parser.has_attribute("parent-attr"));
}
fn asciidoc_cell_text(table: &TableBlock<'_>, row: usize, col: usize) -> String {
match table.body_rows()[row].cells()[col].content() {
TableCellContent::AsciiDoc(cell) => cell
.blocks()
.iter()
.filter_map(|block| block.rendered_content())
.collect::<Vec<_>>()
.join("\n"),
TableCellContent::Simple(_) => panic!("expected AsciiDoc cell content"),
}
}
#[test]
fn asciidoc_cell_cannot_modify_a_parent_attribute() {
let mut parser = Parser::default();
let doc = parser.parse(
":locked: parent\n\n[cols=\"a\"]\n|===\n|\n:locked: child\n\nvalue is {locked}\n|===",
);
let table = doc
.nested_blocks()
.find_map(|block| match block {
Block::Table(table) => Some(table),
_ => None,
})
.unwrap();
assert_eq!(asciidoc_cell_text(table, 0, 0), "value is parent");
assert_eq!(
parser.attribute_value("locked"),
crate::document::InterpretedValue::Value("parent".to_string())
);
}
#[test]
fn asciidoc_cell_may_set_an_attribute_unset_in_the_parent() {
let mut parser = Parser::default();
let doc = parser.parse(
":locked: value\n:locked!:\n\n[cols=\"a\"]\n|===\n|\n:locked: child\n\nvalue is {locked}\n|===",
);
let table = doc
.nested_blocks()
.find_map(|block| match block {
Block::Table(table) => Some(table),
_ => None,
})
.unwrap();
assert_eq!(asciidoc_cell_text(table, 0, 0), "value is child");
}
#[test]
fn asciidoc_cell_may_modify_an_exempt_attribute() {
let mut parser = Parser::default();
let doc = parser.parse(
":compat-mode: parent\n:locked: parent\n\n[cols=\"a\"]\n|===\n|\n:compat-mode: child\n:locked: child\n\nc={compat-mode} l={locked}\n|===",
);
let table = doc
.nested_blocks()
.find_map(|block| match block {
Block::Table(table) => Some(table),
_ => None,
})
.unwrap();
assert_eq!(asciidoc_cell_text(table, 0, 0), "c=child l=parent");
}
#[test]
fn cell_specifier_style_operator_locates_separator() {
let table = parse_table("|===\n|a s|b\n|===");
assert_eq!(table.columns().len(), 2);
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(rows, vec![vec!["a".to_string(), "b".to_string()]]);
let cells = table.body_rows()[0].cells();
assert_eq!(cells[0].style(), ColumnStyle::Default);
assert_eq!(cells[1].style(), ColumnStyle::Strong);
}
#[test]
fn unrecognized_cell_style_operator_inherits_column_style() {
let table = parse_table("[cols=\"m,m\"]\n|===\n|head1 |head2\n\nz|zee s|ess\n|===");
let cells = table.body_rows()[0].cells();
assert_eq!(cells[0].style(), ColumnStyle::Monospace);
assert_eq!(cells[1].style(), ColumnStyle::Strong);
}
#[test]
fn cell_specifier_span_operator_without_factor_locates_separator() {
let table = parse_table("|===\n|a +|b\n|===");
assert_eq!(table.columns().len(), 2);
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(rows, vec![vec!["a".to_string(), "b".to_string()]]);
}
#[test]
fn non_specifier_token_is_a_plain_cell_separator() {
let table = parse_table("|===\n|a foo|b\n|===");
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(rows, vec![vec!["a foo".to_string(), "b".to_string()]]);
}
#[test]
fn dot_not_followed_by_vertical_operator_is_a_plain_cell_separator() {
let table = parse_table("|===\n|a .x|b\n|===");
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(rows, vec![vec!["a .x".to_string(), "b".to_string()]]);
}
#[test]
fn cell_span_exceeding_columns_drops_overrunning_row() {
let mut parser = Parser::default();
let maw = Block::parse(
Span::new("[cols=\"2*\"]\n|===\n|x 3+|y\n|z |w\n|==="),
&mut parser,
);
assert!(maw.warnings.iter().any(|w| matches!(
w.warning,
crate::warnings::WarningType::TableCellExceedsColumnCount
)));
let table = match maw.item.unwrap().item {
Block::Table(table) => table,
other => panic!("expected a table block, got {other:?}"),
};
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(rows, vec![vec!["z".to_string(), "w".to_string()]]);
}
#[test]
fn row_fully_covered_by_rowspans_drops_following_cell() {
let mut parser = Parser::default();
let maw = Block::parse(
Span::new("[cols=\"2*\"]\n|===\n.2+|a .2+|b\n|c1 |c2\n|d1 |d2\n|==="),
&mut parser,
);
assert_eq!(
maw.warnings
.iter()
.filter(|w| matches!(
w.warning,
crate::warnings::WarningType::TableCellExceedsColumnCount
))
.count(),
1
);
assert_eq!(
maw.warnings
.iter()
.filter(|w| matches!(
w.warning,
crate::warnings::WarningType::TableDroppingIncompleteRowAtEndOfTable
))
.count(),
1
);
let table = match maw.item.unwrap().item {
Block::Table(table) => table,
other => panic!("expected a table block, got {other:?}"),
};
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(
rows,
vec![
vec!["a".to_string(), "b".to_string()],
vec!["c2".to_string(), "d1".to_string()],
]
);
}
#[test]
fn dropped_overrunning_cell_keeps_its_rowspan_carryover() {
let mut parser = Parser::default();
let maw = Block::parse(
Span::new("[cols=\"2*\"]\n|===\n|a 2.2+|b\n|c\n|d\n|e\n|==="),
&mut parser,
);
assert_eq!(
maw.warnings
.iter()
.filter(|w| matches!(
w.warning,
crate::warnings::WarningType::TableCellExceedsColumnCount
))
.count(),
2
);
let table = match maw.item.unwrap().item {
Block::Table(table) => table,
other => panic!("expected a table block, got {other:?}"),
};
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(rows, vec![vec!["d".to_string(), "e".to_string()]]);
}
#[test]
fn huge_row_span_factor_does_not_overallocate() {
let mut parser = Parser::default();
let maw = Block::parse(Span::new("|===\n.1000000000+|a\n|b\n|==="), &mut parser);
assert!(maw.warnings.iter().any(|w| matches!(
w.warning,
crate::warnings::WarningType::TableCellExceedsColumnCount
)));
let table = match maw.item.unwrap().item {
Block::Table(table) => table,
other => panic!("expected a table block, got {other:?}"),
};
assert_eq!(table.body_rows().len(), 1);
let cell = &table.body_rows()[0].cells()[0];
assert_eq!(cell.colspan(), 1);
assert_eq!(cell.rowspan(), 1_000_000_000);
assert_eq!(row_text(&table.body_rows()[0]), vec!["a".to_string()]);
}
#[test]
fn duplication_clones_content_and_properties() {
let table = parse_table("[cols=\"3*\"]\n|===\n3*e|x\n|===");
let cells = table.body_rows()[0].cells();
assert_eq!(cells.len(), 3);
for cell in cells {
assert_eq!(cell.colspan(), 1);
assert_eq!(cell.rowspan(), 1);
assert_eq!(cell.style(), ColumnStyle::Emphasis);
}
assert_eq!(
row_text(&table.body_rows()[0]),
vec!["x".to_string(), "x".to_string(), "x".to_string()]
);
}
#[test]
fn duplication_factor_ignores_row_part() {
let table = parse_table("[cols=\"4*\"]\n|===\n2.3*|a |b |c\n|===");
let cells = table.body_rows()[0].cells();
assert_eq!(cells.len(), 4);
assert_eq!(cells[0].rowspan(), 1);
assert_eq!(
row_text(&table.body_rows()[0]),
vec![
"a".to_string(),
"a".to_string(),
"b".to_string(),
"c".to_string()
]
);
}
#[test]
fn duplication_factor_of_zero_drops_the_cell() {
let table = parse_table("[cols=\"3*\"]\n|===\n0*|x |a |b |c\n|===");
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(
rows,
vec![vec!["a".to_string(), "b".to_string(), "c".to_string()]]
);
}
#[test]
fn bare_duplication_operator_clones_once() {
let table = parse_table("|===\n|a *|b\n|===");
assert_eq!(table.columns().len(), 2);
let rows: Vec<_> = table.body_rows().iter().map(row_text).collect();
assert_eq!(rows, vec![vec!["a".to_string(), "b".to_string()]]);
}
#[test]
fn huge_duplication_factor_does_not_overallocate() {
let table = parse_table("|===\n1000000000*|x\n|===");
let total: usize = table.body_rows().iter().map(|r| r.cells().len()).sum();
assert_eq!(total, 1_000);
assert_eq!(row_text(&table.body_rows()[0])[0], "x".to_string());
}
#[test]
fn data_format_attribute_recognizes_each_value() {
assert_eq!(
parse_table("|===\n|a |b\n|===").data_format(),
DataFormat::Psv
);
assert_eq!(
parse_table("[format=psv]\n|===\n|a |b\n|===").data_format(),
DataFormat::Psv
);
assert_eq!(
parse_table("[format=csv]\n|===\na,b\n|===").data_format(),
DataFormat::Csv
);
assert_eq!(
parse_table("[format=tsv]\n|===\na\tb\n|===").data_format(),
DataFormat::Tsv
);
assert_eq!(
parse_table("[format=dsv]\n|===\na:b\n|===").data_format(),
DataFormat::Dsv
);
assert_eq!(
parse_table("[format=bogus]\n|===\n|a |b\n|===").data_format(),
DataFormat::Psv
);
}
#[test]
fn csv_unclosed_quote_is_literal() {
let table = parse_table("[format=csv]\n|===\nx,\"unterminated\n|===");
assert_eq!(table.columns().len(), 2);
assert_eq!(row_text(&table.body_rows()[0]), ["x", "\"unterminated"]);
let table = parse_table("[format=csv]\n|===\n\",a\n|===");
assert_eq!(table.columns().len(), 1);
assert_eq!(row_text(&table.body_rows()[0]), ["\",a"]);
}
#[test]
fn csv_lone_quote_is_unclosed_and_empty_with_warning() {
let mut parser = Parser::default();
let maw = Block::parse(Span::new("[format=csv]\n|===\n\"\n|==="), &mut parser);
assert!(maw.warnings.iter().any(|w| matches!(
w.warning,
crate::warnings::WarningType::TableCsvDataHasUnclosedQuote
)));
let table = match maw.item.unwrap().item {
Block::Table(table) => table,
other => panic!("expected a table block, got {other:?}"),
};
assert_eq!(table.columns().len(), 1);
assert_eq!(row_text(&table.body_rows()[0]), [""]);
}
#[test]
fn csv_text_after_closing_quote_is_literal() {
let table = parse_table("[format=csv]\n|===\n\"abc\"x,d\n|===");
assert_eq!(table.columns().len(), 1);
assert_eq!(row_text(&table.body_rows()[0]), ["\"abc\"x,d"]);
}
#[test]
fn csv_escaped_quotes_collapse() {
let table = parse_table("[format=csv]\n|===\n\"a\"\"b\",a\"\"b\n|===");
assert_eq!(table.columns().len(), 2);
assert_eq!(row_text(&table.body_rows()[0]), ["a\"b", "a\"b"]);
let table = parse_table("[format=csv]\n|===\n\"a\"\",b\n|===");
assert_eq!(table.columns().len(), 1);
assert_eq!(row_text(&table.body_rows()[0]), ["\"a\",b"]);
}
#[test]
fn csv_trailing_separator_keeps_empty_cell() {
let table = parse_table("[format=csv]\n|===\na,b,\nc,d,e\n|===");
assert_eq!(table.columns().len(), 3);
assert_eq!(row_text(&table.body_rows()[0]), ["a", "b", ""]);
assert_eq!(row_text(&table.body_rows()[1]), ["c", "d", "e"]);
}