use markdown2pdf::markdown::*;
use super::common::parse;
fn first_table(tokens: &[Token]) -> (&Vec<TableCell<Token>>, &Vec<markdown2pdf::markdown::TableAlignment>, &Vec<Vec<TableCell<Token>>>) {
let Some(Token::Table { headers, aligns, rows }) =
tokens.iter().find(|t| matches!(t, Token::Table { .. }))
else {
panic!("expected Table, got {:?}", tokens);
};
(headers, aligns, rows)
}
#[test]
fn basic_table() {
let tokens = parse("| a | b |\n| --- | --- |\n| 1 | 2 |\n");
let (headers, _, rows) = first_table(&tokens);
assert_eq!(headers.len(), 2);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].len(), 2);
}
#[test]
fn table_requires_outer_pipes() {
let tokens = parse("a | b\n--- | ---\n1 | 2\n");
assert!(!tokens.iter().any(|t| matches!(t, Token::Table { .. })));
}
#[test]
fn single_column_table() {
let tokens = parse("| a |\n| --- |\n| x |\n");
let (headers, _, rows) = first_table(&tokens);
assert_eq!(headers.len(), 1);
assert_eq!(rows[0].len(), 1);
assert_eq!(Token::collect_all_text(&rows[0][0].content), "x");
}
#[test]
fn alignment_left() {
let tokens = parse("| a |\n| :--- |\n| x |\n");
let (_, aligns, _) = first_table(&tokens);
assert!(matches!(aligns[0], markdown2pdf::markdown::TableAlignment::Left));
}
#[test]
fn alignment_right() {
let tokens = parse("| a |\n| ---: |\n| x |\n");
let (_, aligns, _) = first_table(&tokens);
assert!(matches!(aligns[0], markdown2pdf::markdown::TableAlignment::Right));
}
#[test]
fn alignment_center() {
let tokens = parse("| a |\n| :---: |\n| x |\n");
let (_, aligns, _) = first_table(&tokens);
assert!(matches!(aligns[0], markdown2pdf::markdown::TableAlignment::Center));
}
#[test]
fn alignment_default() {
let tokens = parse("| a |\n| --- |\n| x |\n");
let (_, aligns, _) = first_table(&tokens);
let _ = aligns[0]; assert_eq!(aligns.len(), 1);
}
#[test]
fn mixed_alignments_per_column() {
let tokens = parse("| a | b | c |\n| :--- | :---: | ---: |\n| 1 | 2 | 3 |\n");
let (_, aligns, _) = first_table(&tokens);
assert_eq!(aligns.len(), 3);
}
#[test]
fn row_with_fewer_cells_pads() {
let tokens = parse("| a | b | c |\n| --- | --- | --- |\n| 1 | 2 |\n");
let (headers, _, rows) = first_table(&tokens);
assert_eq!(headers.len(), 3);
assert!(!rows.is_empty());
}
#[test]
fn row_with_more_cells_keeps_all() {
let tokens = parse("| a | b |\n| --- | --- |\n| 1 | 2 | 3 |\n");
let (headers, _, rows) = first_table(&tokens);
assert_eq!(headers.len(), 2);
assert_eq!(rows[0].len(), 3);
}
#[test]
fn escaped_pipe_currently_still_splits_known_gap() {
let tokens = parse(r"| a | b |
| --- | --- |
| x \| y | z |
");
let (_, _, rows) = first_table(&tokens);
assert_eq!(rows[0].len(), 3, "got {:?}", rows[0]);
}
#[test]
fn cell_with_emphasis() {
let tokens = parse("| a |\n| --- |\n| *x* |\n");
let (_, _, rows) = first_table(&tokens);
assert!(rows[0][0].content.iter().any(|t| matches!(t, Token::Emphasis { .. })));
}
#[test]
fn cell_with_inline_code() {
let tokens = parse("| a |\n| --- |\n| `x` |\n");
let (_, _, rows) = first_table(&tokens);
assert!(rows[0][0].content.iter().any(|t| matches!(t, Token::Code { block: false, .. })));
}
#[test]
fn cell_with_link() {
let tokens = parse("| a |\n| --- |\n| [t](u) |\n");
let (_, _, rows) = first_table(&tokens);
assert!(rows[0][0].content.iter().any(|t| matches!(t, Token::Link { .. })));
}
#[test]
fn cell_with_strikethrough() {
let tokens = parse("| a |\n| --- |\n| ~~x~~ |\n");
let (_, _, rows) = first_table(&tokens);
assert!(rows[0][0].content.iter().any(|t| matches!(t, Token::Strikethrough(_))));
}
#[test]
fn missing_alignment_row_is_not_a_table() {
let tokens = parse("| a | b |\n| 1 | 2 |\n");
assert!(!tokens.iter().any(|t| matches!(t, Token::Table { .. })));
}
#[test]
fn two_back_to_back_tables() {
let tokens = parse(
"| a |\n| --- |\n| 1 |\n\n| b |\n| --- |\n| 2 |\n"
);
let tables: Vec<_> = tokens.iter().filter(|t| matches!(t, Token::Table { .. })).collect();
assert_eq!(tables.len(), 2, "expected two tables, got {:?}", tokens);
}
#[test]
fn empty_cell() {
let tokens = parse("| a | b |\n| --- | --- |\n| | x |\n");
let (_, _, rows) = first_table(&tokens);
assert_eq!(Token::collect_all_text(&rows[0][0].content), "");
}
#[test]
fn plain_gfm_cells_have_unit_spans() {
let tokens = parse("| a | b | c |\n| --- | --- | --- |\n| 1 | | 3 |\n");
let (headers, _, rows) = first_table(&tokens);
for cell in headers.iter().chain(rows.iter().flatten()) {
assert_eq!(cell.colspan, 1);
assert_eq!(cell.rowspan, 1);
assert!(!cell.covered);
}
}
#[test]
fn marker_cell_extends_colspan() {
let tokens = parse("| Group | > | Regular |\n| --- | --- | --- |\n| A | B | C |\n");
let (headers, _, _) = first_table(&tokens);
assert_eq!(headers.len(), 3);
assert_eq!(Token::collect_all_text(&headers[0].content), "Group");
assert_eq!(headers[0].colspan, 2);
assert!(headers[1].covered);
assert_eq!(Token::collect_all_text(&headers[2].content), "Regular");
}
#[test]
fn caret_cell_extends_rowspan_from_cell_above() {
let tokens = parse("| Key | Value |\n| --- | --- |\n| A | one |\n| ^ | two |\n");
let (_, _, rows) = first_table(&tokens);
assert_eq!(Token::collect_all_text(&rows[0][0].content), "A");
assert_eq!(rows[0][0].rowspan, 2);
assert!(rows[1][0].covered);
assert_eq!(Token::collect_all_text(&rows[1][1].content), "two");
}
#[test]
fn rowspan_chains_across_three_rows() {
let tokens =
parse("| Key | Value |\n| --- | --- |\n| A | 1 |\n| ^ | 2 |\n| ^ | 3 |\n");
let (_, _, rows) = first_table(&tokens);
assert_eq!(Token::collect_all_text(&rows[0][0].content), "A");
assert_eq!(rows[0][0].rowspan, 3);
assert!(rows[1][0].covered);
assert!(rows[2][0].covered);
assert_eq!(Token::collect_all_text(&rows[2][1].content), "3");
}
#[test]
fn rowspan_binds_to_nearest_cell_above_not_topmost() {
let tokens =
parse("| K | V |\n| --- | --- |\n| A | x |\n| B | y |\n| ^ | z |\n");
let (_, _, rows) = first_table(&tokens);
assert_eq!(rows[0][0].rowspan, 1, "A should not span");
assert_eq!(Token::collect_all_text(&rows[1][0].content), "B");
assert_eq!(rows[1][0].rowspan, 2, "B continues into the ^ row");
assert!(rows[2][0].covered);
}
#[test]
fn colspan_and_rowspan_combine_without_misbinding() {
let tokens = parse(
"| Span | > | Tail |\n| --- | --- | --- |\n| Merged | > | a |\n| ^ | > | b |\n",
);
let (headers, _, rows) = first_table(&tokens);
assert_eq!(headers[0].colspan, 2);
assert!(headers[1].covered);
assert_eq!(Token::collect_all_text(&rows[0][0].content), "Merged");
assert_eq!(rows[0][0].colspan, 2);
assert_eq!(rows[0][0].rowspan, 2, "Merged extends into the ^ row");
assert!(rows[0][1].covered);
assert_eq!(Token::collect_all_text(&rows[0][2].content), "a");
assert!(rows[1][0].covered);
assert!(rows[1][1].covered);
assert_eq!(Token::collect_all_text(&rows[1][2].content), "b");
}
#[test]
fn escaped_gt_is_literal_not_a_colspan_marker() {
let tokens =
parse("| a | \\> | c |\n| --- | --- | --- |\n| 1 | 2 | 3 |\n");
let (headers, _, _) = first_table(&tokens);
assert_eq!(headers.len(), 3);
assert_eq!(headers[0].colspan, 1, "escaped marker must not extend");
assert!(!headers[1].covered);
assert_eq!(Token::collect_all_text(&headers[1].content), ">");
}
#[test]
fn escaped_caret_is_literal_not_a_rowspan_marker() {
let tokens =
parse("| K | V |\n| --- | --- |\n| A | one |\n| \\^ | two |\n");
let (_, _, rows) = first_table(&tokens);
assert_eq!(rows[0][0].rowspan, 1, "escaped marker must not extend");
assert!(!rows[1][0].covered);
assert_eq!(Token::collect_all_text(&rows[1][0].content), "^");
}
#[test]
fn leading_marker_with_no_origin_stays_literal() {
let tokens = parse("| > | a | b |\n| --- | --- | --- |\n| ^ | x | y |\n");
let (headers, _, rows) = first_table(&tokens);
assert!(!headers[0].covered);
assert_eq!(Token::collect_all_text(&headers[0].content), ">");
assert_eq!(headers[0].colspan, 1);
assert!(!rows[0][0].covered);
assert_eq!(Token::collect_all_text(&rows[0][0].content), "^");
}
#[test]
fn colspan_in_a_data_row() {
let tokens = parse("| a | b | c |\n| --- | --- | --- |\n| wide | > | end |\n");
let (_, _, rows) = first_table(&tokens);
assert_eq!(Token::collect_all_text(&rows[0][0].content), "wide");
assert_eq!(rows[0][0].colspan, 2);
assert!(rows[0][1].covered);
assert_eq!(Token::collect_all_text(&rows[0][2].content), "end");
}