// Adapted from Asciidoctor's tables test suite, found in
// https://github.com/asciidoctor/asciidoctor/blob/main/test/tables_test.rb.
//
// IMPORTANT: In porting this, I've disregarded compatibility mode (stated
// limitation of `asciidoc-parser` crate) and alternate (non-HTML) back ends.
mod psv {
use crate::tests::prelude::{inline_file_handler::InlineFileHandler, *};
#[test]
fn converts_simple_psv_table() {
let doc = Parser::default().parse("|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======");
assert_css(&doc, "table", 1);
assert_css(&doc, "table.tableblock.frame-all.grid-all.stretch", 1);
assert_css(&doc, "table > colgroup > col[width=\"33.3333%\"]", 2);
// Ruby uses `col:last-of-type`; the indexed XPath form is equivalent.
assert_xpath(&doc, "(/table/colgroup/col)[3][@width=\"33.3334%\"]", 1);
assert_css(&doc, "table tr", 3);
assert_css(&doc, "table > tbody > tr", 3);
assert_css(&doc, "table td", 9);
assert_css(
&doc,
"table > tbody > tr > td.tableblock.halign-left.valign-top > p.tableblock",
9,
);
let cells = [["A", "B", "C"], ["a", "b", "c"], ["1", "2", "3"]];
for (rowi, row) in cells.iter().enumerate() {
assert_xpath(
&doc,
&format!("(/table/tbody/tr)[{}]/td", rowi + 1),
row.len(),
);
assert_xpath(
&doc,
&format!("(/table/tbody/tr)[{}]/td/p", rowi + 1),
row.len(),
);
for (celli, cell) in row.iter().enumerate() {
assert_xpath(
&doc,
&format!("(//tr)[{}]/td[{}]/p[text()='{cell}']", rowi + 1, celli + 1),
1,
);
}
}
}
#[test]
fn should_add_direction_css_class_if_float_attribute_is_set_on_table() {
let doc = Parser::default()
.parse("[float=left]\n|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======");
assert_css(&doc, "table.left", 1);
}
#[test]
fn should_set_stripes_class_if_stripes_option_is_set() {
let doc = Parser::default()
.parse("[stripes=odd]\n|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======");
assert_css(&doc, "table.stripes-odd", 1);
}
#[test]
fn outputs_a_caption_on_simple_psv_table() {
let doc = Parser::default()
.parse(".Simple psv table\n|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======");
assert_xpath(
&doc,
"/table/caption[@class=\"title\"][text()=\"Table 1. Simple psv table\"]",
1,
);
assert_xpath(&doc, "/table/caption/following-sibling::colgroup", 1);
}
#[test]
fn only_increments_table_counter_for_tables_that_have_a_title() {
let doc = Parser::default().parse(
".First numbered table\n|=======\n|1 |2 |3\n|=======\n\n|=======\n|4 |5 |6\n|=======\n\n.Second numbered table\n|=======\n|7 |8 |9\n|=======",
);
assert_xpath(&doc, "/table", 3);
assert_xpath(&doc, "(/table)[1]/caption", 1);
assert_xpath(
&doc,
"(/table)[1]/caption[text()=\"Table 1. First numbered table\"]",
1,
);
assert_xpath(&doc, "(/table)[2]/caption", 0);
assert_xpath(&doc, "(/table)[3]/caption", 1);
assert_xpath(
&doc,
"(/table)[3]/caption[text()=\"Table 2. Second numbered table\"]",
1,
);
}
#[test]
fn uses_explicit_caption_in_front_of_title_in_place_of_default_caption_and_number() {
let doc = Parser::default().parse(
"[caption=\"All the Data. \"]\n.Simple psv table\n|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======",
);
assert_xpath(
&doc,
"/table/caption[@class=\"title\"][text()=\"All the Data. Simple psv table\"]",
1,
);
assert_xpath(&doc, "/table/caption/following-sibling::colgroup", 1);
}
#[test]
fn disables_caption_when_caption_attribute_on_table_is_empty() {
let doc = Parser::default().parse(
"[caption=]\n.Simple psv table\n|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======",
);
assert_xpath(
&doc,
"/table/caption[@class=\"title\"][text()=\"Simple psv table\"]",
1,
);
assert_xpath(&doc, "/table/caption/following-sibling::colgroup", 1);
}
#[test]
fn disables_caption_when_caption_attribute_on_table_is_empty_string() {
let doc = Parser::default().parse(
"[caption=\"\"]\n.Simple psv table\n|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======",
);
assert_xpath(
&doc,
"/table/caption[@class=\"title\"][text()=\"Simple psv table\"]",
1,
);
assert_xpath(&doc, "/table/caption/following-sibling::colgroup", 1);
}
#[test]
fn disables_caption_on_table_when_table_caption_document_attribute_is_unset() {
let doc = Parser::default().parse(
":!table-caption:\n\n.Simple psv table\n|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======",
);
assert_xpath(
&doc,
"/table/caption[@class=\"title\"][text()=\"Simple psv table\"]",
1,
);
assert_xpath(&doc, "/table/caption/following-sibling::colgroup", 1);
}
#[test]
fn ignores_escaped_separators() {
let doc = Parser::default().parse("|===\n|A \\| here| a \\| there\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > tbody > tr", 1);
assert_css(&doc, "table > tbody > tr > td", 2);
assert_xpath(&doc, "/table/tbody/tr/td[1]/p[text()=\"A | here\"]", 1);
assert_xpath(&doc, "/table/tbody/tr/td[2]/p[text()=\"a | there\"]", 1);
}
#[test]
fn preserves_escaped_delimiters_at_the_end_of_the_line() {
let doc = Parser::default()
.parse("[%header,cols=\"1,1\"]\n|===\n|A |B\\|\n|A1 |B1\\|\n|A2 |B2\\|\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead > tr", 1);
assert_xpath(&doc, "(/table/thead/tr)[1]/th", 2);
assert_xpath(&doc, "/table/thead/tr[1]/th[2][text()=\"B|\"]", 1);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 2);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p[text()=\"B1|\"]", 1);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 2);
assert_xpath(&doc, "/table/tbody/tr[2]/td[2]/p[text()=\"B2|\"]", 1);
}
#[test]
fn should_treat_trailing_pipe_as_an_empty_cell() {
let doc = Parser::default().parse("|===\n|A1 |\n|B1 |B2\n|C1 |C2\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > tbody > tr", 3);
assert_xpath(&doc, "/table/tbody/tr[1]/td", 2);
assert_xpath(&doc, "/table/tbody/tr[1]/td[1]/p[text()=\"A1\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p", 0);
assert_xpath(&doc, "/table/tbody/tr[2]/td[1]/p[text()=\"B1\"]", 1);
}
#[test]
fn should_auto_recover_with_warning_if_missing_leading_separator_on_first_cell() {
let doc = Parser::default().parse("|===\nA | here| a | there\n| x\n| y\n| z\n| end\n|===");
// The content before the first separator (`A`) is recovered as the first
// cell, so the first row has four cells and the table four columns.
assert_css(&doc, "table", 1);
assert_css(&doc, "table > tbody > tr", 2);
assert_css(&doc, "table > tbody > tr > td", 8);
assert_xpath(&doc, "/table/tbody/tr[1]/td[1]/p[text()=\"A\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p[text()=\"here\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[3]/p[text()=\"a\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[4]/p[text()=\"there\"]", 1);
// The recovery logs an error pointing at the offending line (line 2).
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::TableMissingLeadingSeparator
);
assert_eq!(warnings[0].source.line(), 2);
}
#[test]
fn performs_normal_substitutions_on_cell_content() {
let doc = Parser::default()
.parse(":show_title: Cool new show\n|===\n|{show_title} |Coming soon...\n|===");
assert_xpath(&doc, "//tbody/tr/td[1]/p[text()=\"Cool new show\"]", 1);
assert_xpath(
&doc,
"//tbody/tr/td[2]/p[text()='Coming soon\u{2026}\u{200b}']",
1,
);
}
#[test]
fn should_only_substitute_specialchars_for_literal_table_cells() {
let doc = Parser::default().parse("|===\nl|one\n*two*\nthree\n<four>\n|===");
// Ruby compares the serialized `<pre>one\n*two*\nthree\n<four></pre>`;
// the test DOM decodes entities in `text()`, so this asserts the decoded
// content (formatting markup left literal, specialchars escaped then
// decoded back).
assert_css(&doc, "table pre", 1);
assert_xpath(&doc, "/table//pre[text()=\"one\n*two*\nthree\n<four>\"]", 1);
}
#[test]
fn should_preserve_leading_spaces_but_not_leading_newlines_or_trailing_spaces_in_literal_table_cells()
{
let doc =
Parser::default().parse("[cols=2*]\n|===\nl|\n one\n two\nthree\n\n | normal\n|===");
assert_css(&doc, "table pre", 1);
assert_xpath(&doc, "/table//pre[text()=\" one\n two\nthree\"]", 1);
}
#[test]
fn should_ignore_v_table_cell_style() {
let doc =
Parser::default().parse("[cols=2*]\n|===\nv|\n one\n two\nthree\n\n | normal\n|===");
// The unrecognized `v` style is ignored, so the cell renders as a normal
// paragraph (`p.tableblock`) with leading newlines and trailing spaces
// stripped but interior indentation preserved.
assert_xpath(
&doc,
"(/table/tbody/tr/td)[1]/p[@class=\"tableblock\"][text()=\"one\n two\nthree\"]",
1,
);
}
#[test]
fn table_and_column_width_not_assigned_when_autowidth_option_is_specified() {
let doc = Parser::default()
.parse("[options=\"autowidth\"]\n|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======");
assert_css(&doc, "table", 1);
assert_css(&doc, "table.fit-content", 1);
assert_css(&doc, "table[style*=\"width\"]", 0);
assert_css(&doc, "table colgroup col", 3);
assert_css(&doc, "table colgroup col[style*=\"width\"]", 0);
}
#[test]
fn does_not_assign_column_width_for_autowidth_columns_in_html_output() {
let doc = Parser::default()
.parse("[cols=\"15%,3*~\"]\n|=======\n|A |B |C |D\n|a |b |c |d\n|1 |2 |3 |4\n|=======");
// The fixed first column keeps its computed percentage in `colpcwidth`
// and carries the HTML `width` attribute; it is not autowidth.
assert_xpath(&doc, "(/table/colgroup/col)[1][@colpcwidth=\"15\"]", 1);
assert_xpath(&doc, "(/table/colgroup/col)[1][@width=\"15%\"]", 1);
assert_xpath(&doc, "(/table/colgroup/col)[1][@autowidth-option]", 0);
// Each autowidth column still has a computed `colpcwidth` (the remaining
// space shared three ways, truncated to four places with the balance
// donated to the last column) and the `autowidth-option` marker, but no
// HTML `width` attribute.
for i in 2..=3 {
assert_xpath(
&doc,
&format!("(/table/colgroup/col)[{i}][@colpcwidth=\"28.3333\"]"),
1,
);
}
assert_xpath(&doc, "(/table/colgroup/col)[4][@colpcwidth=\"28.3334\"]", 1);
for i in 2..=4 {
assert_xpath(
&doc,
&format!("(/table/colgroup/col)[{i}][@autowidth-option]"),
1,
);
assert_xpath(&doc, &format!("(/table/colgroup/col)[{i}][@width]"), 0);
}
// The HTML-output expectations from the Ruby test: every column renders a
// `<col>`, but only the single fixed column carries a `width` attribute.
assert_css(&doc, "table", 1);
assert_css(&doc, "table colgroup col", 4);
assert_css(&doc, "table colgroup col[width]", 1);
assert_css(&doc, "table colgroup col[width=\"15%\"]", 1);
assert_css(&doc, "table colgroup col[autowidth-option]", 3);
}
#[test]
fn can_assign_autowidth_to_all_columns_even_when_table_has_a_width() {
let doc = Parser::default().parse(
"[cols=\"4*~\",width=50%]\n|=======\n|A |B |C |D\n|a |b |c |d\n|1 |2 |3 |4\n|=======",
);
// Every column is autowidth, so each takes an equal 25% share in
// `colpcwidth` and carries the `autowidth-option` marker; none carries an
// HTML `width` attribute even though the table itself has a width.
for i in 1..=4 {
assert_xpath(
&doc,
&format!("(/table/colgroup/col)[{i}][@colpcwidth=\"25\"]"),
1,
);
assert_xpath(
&doc,
&format!("(/table/colgroup/col)[{i}][@autowidth-option]"),
1,
);
assert_xpath(&doc, &format!("(/table/colgroup/col)[{i}][@width]"), 0);
}
// The explicit table width is still rendered, and no column carries a
// width-bearing `style` (the Ruby HTML-output expectations).
assert_css(&doc, "table", 1);
assert_css(&doc, "table[width=\"50%\"]", 1);
assert_css(&doc, "table colgroup col", 4);
assert_css(&doc, "table colgroup col[style]", 0);
assert_css(&doc, "table colgroup col[autowidth-option]", 4);
}
// Backend-specific test omitted: DocBook ("equally distributes remaining
// column width to autowidth columns in DocBook output").
// Backend-specific test omitted: DocBook ("should compute column widths
// based on pagewidth when width is set on table in DocBook output").
#[test]
fn explicit_table_width_is_used_even_when_autowidth_option_is_specified() {
let doc = Parser::default()
.parse("[%autowidth,width=75%]\n|=======\n|A |B |C\n|a |b |c\n|1 |2 |3\n|=======");
assert_css(&doc, "table", 1);
assert_css(&doc, "table[width]", 1);
assert_css(&doc, "table colgroup col", 3);
assert_css(&doc, "table colgroup col[style*=\"width\"]", 0);
}
#[test]
fn first_row_sets_number_of_columns_when_not_specified() {
let doc =
Parser::default().parse("|===\n|first |second |third |fourth\n|1 |2 |3\n|4\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 4);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 4);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 4);
}
#[test]
fn colspec_attribute_using_asterisk_syntax_sets_number_of_columns() {
let doc = Parser::default().parse("[cols=\"3*\"]\n|===\n|A |B |C |a |b |c |1 |2 |3\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > tbody > tr", 3);
}
#[test]
fn table_with_explicit_column_count_can_have_multiple_rows_on_a_single_line() {
let doc = Parser::default().parse("[cols=\"3*\"]\n|===\n|one |two\n|1 |2 |a |b\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 3);
assert_css(&doc, "table > tbody > tr", 2);
}
#[test]
fn table_with_explicit_deprecated_colspec_syntax_can_have_multiple_rows_on_a_single_line() {
let doc = Parser::default().parse("[cols=\"3\"]\n|===\n|one |two\n|1 |2 |a |b\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 3);
assert_css(&doc, "table > tbody > tr", 2);
}
#[test]
fn columns_are_added_for_empty_records_in_colspec_attribute() {
let doc = Parser::default().parse("[cols=\"<,\"]\n|===\n|one |two\n|1 |2 |a |b\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > tbody > tr", 3);
}
#[test]
fn cols_may_be_separated_by_semi_colon_instead_of_comma() {
let doc = Parser::default().parse("[cols=\"1s;3m\"]\n|===\n| strong\n| mono\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "col[width=\"25%\"]", 1);
assert_css(&doc, "col[width=\"75%\"]", 1);
assert_xpath(&doc, "(//td)[1]//strong", 1);
assert_xpath(&doc, "(//td)[2]//code", 1);
}
#[test]
fn cols_attribute_may_include_spaces() {
let doc = Parser::default().parse("[cols=\" 1, 1 \"]\n|===\n|one |two |1 |2 |a |b\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "col[width=\"50%\"]", 2);
assert_css(&doc, "table > tbody > tr", 3);
}
#[test]
fn blank_cols_attribute_should_be_ignored() {
let doc = Parser::default().parse("[cols=\" \"]\n|===\n|one |two\n|1 |2 |a |b\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "col[width=\"50%\"]", 2);
assert_css(&doc, "table > tbody > tr", 3);
}
#[test]
fn empty_cols_attribute_should_be_ignored() {
let doc = Parser::default().parse("[cols=\"\"]\n|===\n|one |two\n|1 |2 |a |b\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "col[width=\"50%\"]", 2);
assert_css(&doc, "table > tbody > tr", 3);
}
#[test]
fn table_with_header_and_footer() {
let doc = Parser::default().parse(
"[options=\"header,footer\"]\n|===\n|Item |Quantity\n|Item 1 |1\n|Item 2 |2\n|Item 3 |3\n|Total |6\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 1);
assert_css(&doc, "table > thead > tr", 1);
assert_css(&doc, "table > thead > tr > th", 2);
assert_css(&doc, "table > tfoot", 1);
assert_css(&doc, "table > tfoot > tr", 1);
assert_css(&doc, "table > tfoot > tr > td", 2);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 3);
// Ruby additionally asserts the section order is thead, tbody, tfoot;
// the renderer emits them in that order.
assert_xpath(&doc, "/table/thead/following-sibling::tbody", 1);
assert_xpath(&doc, "/table/tbody/following-sibling::tfoot", 1);
}
// Backend-specific test omitted: DocBook ("table with header and footer
// docbook").
// Backend-specific test omitted: DocBook ("should set horizontal and
// vertical alignment when converting to DocBook").
#[test]
fn should_preserve_frame_value_ends_when_converting_to_html() {
let doc = Parser::default().parse("[frame=ends]\n|===\n|A |B |C\n|===");
assert_css(&doc, "table.frame-ends", 1);
}
#[test]
fn should_normalize_frame_value_topbot_as_ends_when_converting_to_html() {
let doc = Parser::default().parse("[frame=topbot]\n|===\n|A |B |C\n|===");
assert_css(&doc, "table.frame-ends", 1);
}
// Backend-specific test omitted: DocBook ("should preserve frame value
// topbot when converting to DocBook").
// Backend-specific test omitted: DocBook ("should convert frame value ends
// to topbot when converting to DocBook").
// Backend-specific test omitted: DocBook ("table with landscape orientation
// in DocBook").
#[test]
fn table_with_implicit_header_row() {
let doc = Parser::default()
.parse("|===\n|Column 1 |Column 2\n\n|Data A1\n|Data B1\n\n|Data A2\n|Data B2\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 1);
assert_css(&doc, "table > thead > tr", 1);
assert_css(&doc, "table > thead > tr > th", 2);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 2);
}
#[test]
fn table_with_implicit_header_row_only() {
let doc = Parser::default().parse("|===\n|Column 1 |Column 2\n\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 1);
assert_css(&doc, "table > thead > tr", 1);
assert_css(&doc, "table > thead > tr > th", 2);
assert_css(&doc, "table > tbody", 0);
}
#[test]
fn table_with_implicit_header_row_when_other_options_set() {
let doc = Parser::default()
.parse("[%autowidth]\n|===\n|Column 1 |Column 2\n\n|Data A1\n|Data B1\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table[style*=\"width\"]", 0);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 1);
assert_css(&doc, "table > thead > tr", 1);
assert_css(&doc, "table > thead > tr > th", 2);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 1);
}
#[test]
fn no_implicit_header_row_if_second_line_not_blank() {
let doc = Parser::default()
.parse("|===\n|Column 1 |Column 2\n|Data A1\n|Data B1\n\n|Data A2\n|Data B2\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 0);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 3);
}
#[test]
fn no_implicit_header_row_if_cell_in_first_line_spans_multiple_lines() {
let doc =
Parser::default().parse("[cols=2*]\n|===\n|A1\n\n\nA1 continued|B1\n\n|A2\n|B2\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 0);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(//td)[1]/p", 2);
}
#[test]
fn should_format_first_cell_as_literal_if_there_is_no_implicit_header_row_and_column_has_l_style()
{
let doc = Parser::default().parse("[cols=\"1l,1\"]\n|===\n|literal\n|normal\n|===");
assert_css(&doc, "tbody pre", 1);
assert_css(&doc, "tbody p.tableblock", 1);
}
#[test]
fn should_format_first_cell_as_asciidoc_if_there_is_no_implicit_header_row_and_column_has_a_style()
{
let doc = Parser::default().parse("[cols=\"1a,1\"]\n|===\n| * list\n| normal\n|===");
assert_css(&doc, "tbody .ulist", 1);
assert_css(&doc, "tbody p.tableblock", 1);
}
#[test]
fn should_interpret_leading_indent_if_first_cell_is_asciidoc_and_there_is_no_implicit_header_row()
{
let doc = Parser::default().parse("[cols=\"1a,1\"]\n|===\n|\n literal\n| normal\n|===");
assert_css(&doc, "tbody pre", 1);
assert_css(&doc, "tbody p.tableblock", 1);
}
#[test]
fn should_format_first_cell_as_asciidoc_if_there_is_no_implicit_header_row_and_cell_has_a_style()
{
let doc = Parser::default().parse("|===\na| * list\n| normal\n|===");
assert_css(&doc, "tbody .ulist", 1);
assert_css(&doc, "tbody p.tableblock", 1);
}
#[test]
fn no_implicit_header_row_if_asciidoc_cell_in_first_line_spans_multiple_lines() {
let doc = Parser::default().parse(
"[cols=2*]\n|===\na|contains AsciiDoc content\n\n* a\n* b\n* c\na|contains no AsciiDoc content\n\njust text\n|A2\n|B2\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 0);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(//td)[1]//ul", 1);
}
#[test]
fn no_implicit_header_row_if_first_line_blank() {
let doc = Parser::default().parse(
"|===\n\n|Column 1 |Column 2\n\n|Data A1\n|Data B1\n\n|Data A2\n|Data B2\n\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 0);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 3);
}
#[test]
fn no_implicit_header_row_if_noheader_option_is_specified() {
let doc = Parser::default().parse(
"[%noheader]\n|===\n|Column 1 |Column 2\n\n|Data A1\n|Data B1\n\n|Data A2\n|Data B2\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 0);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 3);
}
#[test]
fn styles_not_applied_to_header_cells() {
let doc = Parser::default().parse(
"[cols=\"1h,1s,1e\",options=\"header,footer\"]\n|===\n|Name |Occupation| Website\n|Octocat |Social coding| https://github.com\n|Name |Occupation| Website\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > thead > tr > th", 3);
assert_css(&doc, "table > thead > tr > th > *", 0);
assert_css(&doc, "table > tfoot > tr > th", 1);
assert_css(&doc, "table > tfoot > tr > td", 2);
assert_css(&doc, "table > tfoot > tr > td > p > strong", 1);
assert_css(&doc, "table > tfoot > tr > td > p > em", 1);
assert_css(&doc, "table > tbody > tr > th", 1);
assert_css(&doc, "table > tbody > tr > td", 2);
assert_css(&doc, "table > tbody > tr > td > p.header", 0);
assert_css(&doc, "table > tbody > tr > td > p > strong", 1);
assert_css(&doc, "table > tbody > tr > td > p > em > a", 1);
}
#[test]
fn should_apply_text_formatting_to_cells_in_implicit_header_row_when_column_has_a_style() {
let doc = Parser::default()
.parse("[cols=\"2*a\"]\n|===\n| _foo_ | *bar*\n\n| * list item\n| paragraph\n|===");
assert_xpath(&doc, "(//thead/tr/th)[1]/em[text()=\"foo\"]", 1);
assert_xpath(&doc, "(//thead/tr/th)[2]/strong[text()=\"bar\"]", 1);
assert_css(&doc, "tbody .ulist", 1);
assert_css(&doc, "tbody .paragraph", 1);
}
#[test]
fn should_apply_style_and_text_formatting_to_cells_in_first_row_if_no_implicit_header() {
let doc = Parser::default()
.parse("[cols=\"s,e\"]\n|===\n| _strong_ | *emphasis*\n| strong\n| emphasis\n|===");
assert_xpath(
&doc,
"((//tbody/tr)[1]/td)[1]//strong/em[text()=\"strong\"]",
1,
);
assert_xpath(
&doc,
"((//tbody/tr)[1]/td)[2]//em/strong[text()=\"emphasis\"]",
1,
);
assert_xpath(
&doc,
"((//tbody/tr)[2]/td)[1]//strong[text()=\"strong\"]",
1,
);
assert_xpath(&doc, "((//tbody/tr)[2]/td)[2]//em[text()=\"emphasis\"]", 1);
}
#[test]
fn vertical_table_headers_use_th_element_instead_of_header_class() {
let doc = Parser::default().parse(
"[cols=\"1h,1s,1e\"]\n|===\n\n|Name |Occupation| Website\n\n|Octocat |Social coding| https://github.com\n\n|Name |Occupation| Website\n\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > tbody > tr > th", 3);
assert_css(&doc, "table > tbody > tr > td", 6);
assert_css(&doc, "table > tbody > tr .header", 0);
assert_css(&doc, "table > tbody > tr > td > p > strong", 3);
assert_css(&doc, "table > tbody > tr > td > p > em", 3);
assert_css(&doc, "table > tbody > tr > td > p > em > a", 1);
}
#[test]
fn supports_horizontal_and_vertical_source_data_with_blank_lines_and_table_header() {
let doc = Parser::default().parse(
".Horizontal and vertical source data\n[width=\"80%\",cols=\"3,^2,^2,10\",options=\"header\"]\n|===\n|Date |Duration |Avg HR |Notes\n\n|22-Aug-08 |10:24 | 157 |\nWorked out MSHR (max sustainable heart rate) by going hard\nfor this interval.\n\n|22-Aug-08 |23:03 | 152 |\nBack-to-back with previous interval.\n\n|24-Aug-08 |40:00 | 145 |\nModerately hard interspersed with 3x 3min intervals (2 min\nhard + 1 min really hard taking the HR up to 160).\n\nI am getting in shape!\n\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table[width=\"80%\"]", 1);
assert_xpath(
&doc,
"/table/caption[@class=\"title\"][text()=\"Table 1. Horizontal and vertical source data\"]",
1,
);
assert_css(&doc, "table > colgroup > col", 4);
// Ruby uses `col:nth-child(N)`; the indexed XPath form is equivalent.
assert_xpath(&doc, "(/table/colgroup/col)[1][@width=\"17.647%\"]", 1);
assert_xpath(&doc, "(/table/colgroup/col)[2][@width=\"11.7647%\"]", 1);
assert_xpath(&doc, "(/table/colgroup/col)[3][@width=\"11.7647%\"]", 1);
assert_xpath(&doc, "(/table/colgroup/col)[4][@width=\"58.8236%\"]", 1);
assert_css(&doc, "table > thead", 1);
assert_css(&doc, "table > thead > tr", 1);
assert_css(&doc, "table > thead > tr > th", 4);
assert_css(&doc, "table > tbody > tr", 3);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 4);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 4);
assert_xpath(&doc, "(/table/tbody/tr)[3]/td", 4);
assert_xpath(
&doc,
"/table/tbody/tr[1]/td[4]/p[text()='Worked out MSHR (max sustainable heart rate) by going hard\nfor this interval.']",
1,
);
assert_xpath(&doc, "(/table/tbody/tr)[3]/td[4]/p", 2);
assert_xpath(
&doc,
"/table/tbody/tr[3]/td[4]/p[2][text()=\"I am getting in shape!\"]",
1,
);
}
#[test]
fn percentages_as_column_widths() {
let doc =
Parser::default().parse("[cols=\"<.^10%,<90%\"]\n|===\n|column A |column B\n|===");
assert_xpath(&doc, "/table/colgroup/col", 2);
assert_xpath(&doc, "(/table/colgroup/col)[1][@width=\"10%\"]", 1);
assert_xpath(&doc, "(/table/colgroup/col)[2][@width=\"90%\"]", 1);
}
#[test]
fn spans_alignments_and_styles() {
let doc = Parser::default().parse(
"[cols=\"e,m,^,>s\",width=\"25%\"]\n|===\n|1 >s|2 |3 |4\n^|5 2.2+^.^|6 .3+<.>m|7\n^|8\nd|9 2+>|10\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col[width=\"25%\"]", 4);
assert_css(&doc, "table > tbody > tr", 4);
assert_css(&doc, "table > tbody > tr > td", 10);
// Ruby uses `tr:nth-child(N)` / `td:nth-child(N)`; the indexed XPath form
// is equivalent and supported by the test DOM.
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 4);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[3]/td", 1);
assert_xpath(&doc, "(/table/tbody/tr)[4]/td", 2);
assert_xpath(
&doc,
"((/table/tbody/tr)[1]/td)[1][@class=\"halign-left valign-top\"]/p/em",
1,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[1]/td)[2][@class=\"halign-right valign-top\"]/p/strong",
1,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[1]/td)[3][@class=\"halign-center valign-top\"]/p",
1,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[1]/td)[3][@class=\"halign-center valign-top\"]/p/*",
0,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[1]/td)[4][@class=\"halign-right valign-top\"]/p/strong",
1,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[2]/td)[1][@class=\"halign-center valign-top\"]/p/em",
1,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[2]/td)[2][@class=\"halign-center valign-middle\"][@colspan=\"2\"][@rowspan=\"2\"]/p/code",
1,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[2]/td)[3][@class=\"halign-left valign-bottom\"][@rowspan=\"3\"]/p/code",
1,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[3]/td)[1][@class=\"halign-center valign-top\"]/p/em",
1,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[4]/td)[1][@class=\"halign-left valign-top\"]/p",
1,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[4]/td)[1][@class=\"halign-left valign-top\"]/p/em",
0,
);
assert_xpath(
&doc,
"((/table/tbody/tr)[4]/td)[2][@class=\"halign-right valign-top\"][@colspan=\"2\"]/p/code",
1,
);
}
#[test]
fn sets_up_columns_correctly_if_first_row_has_cell_that_spans_columns() {
let doc =
Parser::default().parse("|===\n2+^|AAA |CCC\n|AAA |BBB |CCC\n|AAA |BBB |CCC\n|===");
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 2);
assert_xpath(&doc, "((/table/tbody/tr)[1]/td)[1][@colspan=\"2\"]", 1);
// Ruby uses `td:nth-child(1)[colspan]` / `td:nth-child(2):not([colspan])`;
// since row 1 has exactly one cell carrying a colspan, asserting the row
// has a single colspanned cell is equivalent.
assert_xpath(&doc, "(/table/tbody/tr)[1]/td[@colspan]", 1);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td[@colspan]", 0);
assert_xpath(&doc, "(/table/tbody/tr)[3]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[3]/td[@colspan]", 0);
}
#[test]
fn supports_repeating_cells() {
let doc = Parser::default().parse("|===\n3*|A\n|1 3*|2\n|b |c\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 3);
assert_css(&doc, "table > tbody > tr", 3);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[3]/td", 3);
assert_xpath(&doc, "/table/tbody/tr[1]/td[1]/p[text()=\"A\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p[text()=\"A\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[3]/p[text()=\"A\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[2]/td[1]/p[text()=\"1\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[2]/td[2]/p[text()=\"2\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[2]/td[3]/p[text()=\"2\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[3]/td[1]/p[text()=\"2\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[3]/td[2]/p[text()=\"b\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[3]/td[3]/p[text()=\"c\"]", 1);
}
// Backend-specific test omitted: DocBook ("calculates colnames correctly
// when using implicit column count and single cell with colspan").
// Backend-specific test omitted: DocBook ("calculates colnames correctly
// when using implicit column count and cells with mixed colspans").
// Backend-specific test omitted: DocBook ("assigns unique column names for
// table with implicit column count and colspans in first row").
#[test]
fn should_drop_row_but_preserve_remaining_rows_after_cell_with_colspan_exceeds_number_of_columns()
{
let doc = Parser::default().parse("[cols=2*]\n|===\n3+|A\n|B\na|C\n\nmore C\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table tr", 1);
assert_xpath(&doc, "/table/tbody/tr/td[1]/p[text()=\"B\"]", 1);
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::TableCellExceedsColumnCount
);
assert_eq!(warnings[0].source.line(), 3);
}
#[test]
fn should_drop_last_row_if_last_cell_in_table_has_colspan_that_exceeds_specified_number_of_columns()
{
let doc = Parser::default().parse("[cols=2*]\n|===\n|a 2+|b\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table *", 0);
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::TableCellExceedsColumnCount
);
assert_eq!(warnings[0].source.line(), 3);
}
#[test]
fn should_drop_last_row_if_last_cell_in_table_has_colspan_that_exceeds_implicit_number_of_columns()
{
let doc = Parser::default().parse("|===\n|a |b\n|c 2+|d\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table tr", 1);
assert_xpath(&doc, "/table/tbody/tr/td[1]/p[text()=\"a\"]", 1);
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::TableCellExceedsColumnCount
);
assert_eq!(warnings[0].source.line(), 3);
}
#[test]
fn should_take_colspan_into_account_when_taking_cells_for_row() {
let doc = Parser::default()
.parse("[cols=7]\n|===\n2+|a 2+|b 2+|c 2+|d\n|e |f |g |h |i |j |k\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table tr", 1);
assert_css(&doc, "table tr td", 7);
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::TableCellExceedsColumnCount
);
// The warning is logged against the overrunning first row (line 3).
assert_eq!(warnings[0].source.line(), 3);
}
#[test]
fn should_drop_incomplete_row_at_end_of_table_and_log_an_error() {
let doc = Parser::default().parse("[cols=2*]\n|===\n|a |b\n|c |d\n|e\n|===");
// The incomplete `|e` row is dropped, leaving two rows, and an error is
// logged against its line (line 5).
assert_css(&doc, "table", 1);
assert_css(&doc, "table tr", 2);
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::TableDroppingIncompleteRowAtEndOfTable
);
assert_eq!(warnings[0].source.line(), 5);
}
#[test]
fn should_apply_cell_style_for_column_to_repeated_content() {
let doc = Parser::default().parse(
"[cols=\",^l\"]\n|===\n|Paragraphs |Literal\n\n2*|The discussion about what is good,\nwhat is beautiful, what is noble,\nwhat is pure, and what is true\ncould always go on.\n\nWhy is that important?\nWhy would I like to do that?\n\nBecause that's the only conversation worth having.\n\nAnd whether it goes on or not after I die, I don't know.\nBut, I do know that it is the conversation I want to have while I am still alive.\n\nWhich means that to me the offer of certainty,\nthe offer of complete security,\nthe offer of an impermeable faith that can't give way\nis an offer of something not worth having.\n\nI want to live my life taking the risk all the time\nthat I don't know anything like enough yet...\nthat I haven't understood enough...\nthat I can't know enough...\nthat I am always hungrily operating on the margins\nof a potentially great harvest of future knowledge and wisdom.\n\nI wouldn't have it any other way.\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 1);
assert_css(&doc, "table > thead > tr", 1);
assert_css(&doc, "table > thead > tr > th", 2);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 1);
assert_css(&doc, "table > tbody > tr > td", 2);
// The duplicated cell (`2*|`) is cloned into both columns, and each clone
// takes its own column's style: the first (default) column renders the
// content as seven paragraphs, the second (`^l`) column as a single
// centered literal block. (Ruby uses `td:nth-child(N)`; the equivalent
// positional `td[N]` is expressed in XPath here.)
assert_xpath(
&doc,
"/table/tbody/tr/td[1][@class=\"halign-left valign-top\"]/p[@class=\"tableblock\"]",
7,
);
assert_xpath(
&doc,
"/table/tbody/tr/td[2][@class=\"halign-center valign-top\"]/div[@class=\"literal\"]/pre",
1,
);
// The literal block preserves every line of the cell, including the blank
// lines between paragraphs (26 lines total).
let vdom = doc.to_virtual_dom();
let pre = query_xpath(&vdom, "/table/tbody/tr/td[2]//pre");
assert_eq!(pre.len(), 1);
assert_eq!(
pre[0].text.as_deref().unwrap_or_default().lines().count(),
26
);
}
#[test]
fn should_not_split_paragraph_at_line_containing_only_blank_that_is_directly_adjacent_to_non_blank_lines()
{
let doc = Parser::default().parse(
"|===\n|paragraph\n{blank}\nstill one paragraph\n{blank}\nstill one paragraph\n|===",
);
// Each `{blank}` line renders as an empty line but is not blank in the
// source, so the cell stays a single paragraph.
assert_css(&doc, "p.tableblock", 1);
}
#[test]
fn should_strip_trailing_newlines_when_splitting_paragraphs() {
let doc = Parser::default()
.parse("|===\n|first wrapped\nparagraph\n\nsecond paragraph\n\nthird paragraph\n|===");
assert_xpath(
&doc,
"(//p[@class=\"tableblock\"])[1][text()=\"first wrapped\nparagraph\"]",
1,
);
assert_xpath(
&doc,
"(//p[@class=\"tableblock\"])[2][text()=\"second paragraph\"]",
1,
);
assert_xpath(
&doc,
"(//p[@class=\"tableblock\"])[3][text()=\"third paragraph\"]",
1,
);
}
#[test]
fn basic_asciidoc_cell() {
let doc = Parser::default().parse("|===\na|--\nNOTE: content\n\ncontent\n--\n|===");
assert_css(&doc, "table.tableblock", 1);
assert_css(&doc, "table.tableblock td.tableblock", 1);
assert_css(&doc, "table.tableblock td.tableblock .openblock", 1);
assert_css(
&doc,
"table.tableblock td.tableblock .openblock .admonitionblock",
1,
);
assert_css(
&doc,
"table.tableblock td.tableblock .openblock .paragraph",
1,
);
}
#[test]
fn asciidoc_table_cell_should_be_wrapped_in_div_with_class_content() {
let doc = Parser::default().parse("|===\na|AsciiDoc table cell\n|===");
assert_css(&doc, "table.tableblock td.tableblock > div.content", 1);
assert_css(
&doc,
"table.tableblock td.tableblock > div.content > div.paragraph",
1,
);
}
#[test]
fn doctype_can_be_set_in_asciidoc_table_cell() {
let doc = Parser::default().parse("|===\na|\n:doctype: inline\n\ncontent\n|===");
// An `inline` doctype renders the cell's lone paragraph as bare inline
// content, with no `.paragraph` wrapper.
assert_css(&doc, "table.tableblock", 1);
assert_css(&doc, "table.tableblock .paragraph", 0);
}
#[test]
fn should_reset_doctype_to_default_in_asciidoc_table_cell() {
// The parent is a `book`, but a cell resets to the default `article`
// doctype, so `{doctype}` is `article` and only the
// `backend-html5-doctype-article` derived attribute is defined (an
// undefined reference is left literal).
let doc = Parser::default().parse(
"= Book Title\n:doctype: book\n\n== Chapter 1\n\n|===\na|\n= AsciiDoc Table Cell\n\ndoctype={doctype}\n{backend-html5-doctype-article}\n{backend-html5-doctype-book}\n|===",
);
assert_rendered_contains(&doc, "doctype=article");
refute_rendered_contains(&doc, "{backend-html5-doctype-article}");
assert_rendered_contains(&doc, "{backend-html5-doctype-book}");
}
#[test]
fn asciidoc_table_cell_cannot_override_the_derived_doctype_flag() {
// The active `backend-html5-doctype-*` flag is a read-only intrinsic
// (empty value), so a cell-body assignment to it is silently ignored:
// `{backend-html5-doctype-article}` still resolves to the empty intrinsic
// value rather than the cell's attempted override.
//
// The parent is a `book`, so the cell's active flag
// (`backend-html5-doctype-article`, after the cell resets to the default
// doctype) differs from the parent's — the case a lock computed from the
// parent's doctype would miss.
let doc = Parser::default().parse(
"= Book Title\n:doctype: book\n\n== Chapter 1\n\n|===\na|\n= AsciiDoc Table Cell\n:backend-html5-doctype-article: custom\n\nflag=[{backend-html5-doctype-article}]\n|===",
);
assert_rendered_contains(&doc, "flag=[]");
refute_rendered_contains(&doc, "custom");
}
#[test]
fn asciidoc_table_cell_cannot_stash_a_derived_doctype_flag_while_it_is_inactive() {
// Assigning `:backend-html5-doctype-article:` while the cell's doctype is
// `book` (so that flag is not the active synthesized intrinsic) must not
// stash a per-parser override that later shadows the intrinsic once the
// cell switches its doctype back to `article`. The whole
// `backend-html5-doctype-*` namespace is read-only, so the assignment is
// ignored and `{backend-html5-doctype-article}` still resolves to the
// empty intrinsic value.
let doc = Parser::default().parse(
"= Doc Title\n\n== Chapter 1\n\n|===\na|\n= AsciiDoc Table Cell\n:doctype: book\n:backend-html5-doctype-article: custom\n:doctype: article\n\nflag=[{backend-html5-doctype-article}]\n|===",
);
assert_rendered_contains(&doc, "flag=[]");
refute_rendered_contains(&doc, "custom");
}
#[test]
fn should_update_doctype_related_attributes_in_asciidoc_table_cell_when_doctype_is_set() {
// Setting `:doctype: book` in the cell updates `{doctype}` and the
// derived `backend-html5-doctype-*` attribute accordingly.
let doc = Parser::default().parse(
"= Document Title\n:doctype: article\n\n== Chapter 1\n\n|===\na|\n= AsciiDoc Table Cell\n:doctype: book\n\ndoctype={doctype}\n{backend-html5-doctype-book}\n{backend-html5-doctype-article}\n|===",
);
assert_rendered_contains(&doc, "doctype=book");
refute_rendered_contains(&doc, "{backend-html5-doctype-book}");
assert_rendered_contains(&doc, "{backend-html5-doctype-article}");
}
#[test]
fn should_not_allow_asciidoc_table_cell_to_set_a_document_attribute_that_was_hard_set_by_the_api()
{
// `icons` is hard set to `font` by the API, so the cell's `:icons:`
// (which would otherwise change it) is ignored: the cell's NOTE
// admonition still renders with a font-based icon.
let doc = Parser::default()
.with_intrinsic_attribute("icons", "font", ModificationContext::ApiOnly)
.parse(
"|===\na|\n:icons:\n\nNOTE: This admonition does not have a font-based icon.\n|===",
);
assert_css(&doc, "td.icon .title", 0);
assert_css(&doc, "td.icon i.icon-note", 1);
}
#[test]
fn should_not_allow_asciidoc_table_cell_to_set_a_document_attribute_that_was_hard_unset_by_the_api()
{
// `icons` is hard unset by the API, so the cell's `:icons: font` is
// ignored: the cell's NOTE admonition renders with a text label.
let doc = Parser::default()
.with_intrinsic_attribute_bool("icons", false, ModificationContext::ApiOnly)
.parse(
"|===\na|\n:icons: font\n\nNOTE: This admonition does not have a font-based icon.\n|===",
);
assert_css(&doc, "td.icon .title", 1);
assert_css(&doc, "td.icon i.icon-note", 0);
assert_xpath(
&doc,
"//td[@class=\"icon\"]/*[@class=\"title\"][text()=\"Note\"]",
1,
);
}
#[test]
fn should_keep_attribute_unset_in_asciidoc_table_cell_if_unset_in_parent_document() {
// `sectids` and `table-caption` are unset in the parent and stay unset
// in the cell: the headings get no id, and both the outer table and the
// nested table render their title as a bare `<caption>`.
let doc = Parser::default().parse(
":!sectids:\n:!table-caption:\n\n== Outer Heading\n\n.Outer Table\n|===\na|\n\n== Inner Heading\n\n.Inner Table\n!===\n! table cell\n!===\n|===",
);
assert_xpath(&doc, "//h2[@id]", 0);
assert_xpath(&doc, "//caption[text()=\"Outer Table\"]", 1);
assert_xpath(&doc, "//caption[text()=\"Inner Table\"]", 1);
}
#[test]
fn should_allow_attribute_unset_in_parent_document_to_be_set_in_asciidoc_table_cell() {
// `sectids` is unset in the parent header (not locked), so the cell may
// set it: the heading after `:sectids:` in the cell gets an id.
let doc = Parser::default().parse(
":!sectids:\n\n== No ID\n\n|===\na|\n\n== No ID\n\n:sectids:\n\n== Has ID\n|===",
);
assert_css(&doc, "h2", 3);
assert_xpath(&doc, "(//h2)[1][@id]", 0);
assert_xpath(&doc, "(//h2)[2][@id]", 0);
assert_xpath(&doc, "(//h2)[3][@id=\"_has_id\"]", 1);
}
#[test]
fn should_not_allow_locked_attribute_unset_in_parent_document_to_be_set_in_asciidoc_table_cell()
{
// `sectids` is hard unset by the API, so the cell's `:sectids:` is
// ignored and no heading gets an id.
let doc = Parser::default()
.with_intrinsic_attribute_bool("sectids", false, ModificationContext::ApiOnly)
.parse("== No ID\n\n|===\na|\n\n== No ID\n\n:sectids:\n\n== Has ID\n|===");
assert_css(&doc, "h2", 3);
assert_xpath(&doc, "//h2[@id]", 0);
}
#[test]
fn showtitle_can_be_enabled_in_asciidoc_table_cell_if_unset_in_parent_document() {
// The parent hides its title; the cell shows its own. (`showtitle` and
// `notitle` are complements, so both spellings are exercised.)
for (parent, cell) in [(":!showtitle:", ":showtitle:"), (":notitle:", ":!notitle:")] {
let doc = Parser::default().parse(&format!(
"= Document Title\n{parent}\n\n|===\na|\n= Nested Document Title\n{cell}\n\ncontent\n|==="
));
assert_css(&doc, "h1", 1);
assert_css(&doc, ".tableblock h1", 1);
}
}
#[test]
fn showtitle_can_be_enabled_in_asciidoc_table_cell_if_unset_by_api() {
for (name, value, cell) in [
("showtitle", false, ":showtitle:"),
("notitle", true, ":!notitle:"),
] {
let doc = Parser::default()
.with_intrinsic_attribute_bool(name, value, ModificationContext::ApiOnly)
.parse(&format!(
"= Document Title\n\n|===\na|\n= Nested Document Title\n{cell}\n\ncontent\n|==="
));
assert_css(&doc, "h1", 1);
assert_css(&doc, ".tableblock h1", 1);
}
}
#[test]
fn showtitle_can_be_disabled_in_asciidoc_table_cell_if_set_in_parent_document() {
// The parent shows its title; the cell hides its own.
for (parent, cell) in [(":showtitle:", ":!showtitle:"), (":!notitle:", ":notitle:")] {
let doc = Parser::default().parse(&format!(
"= Document Title\n{parent}\n\n|===\na|\n= Nested Document Title\n{cell}\n\ncontent\n|==="
));
assert_css(&doc, "h1", 1);
assert_css(&doc, ".tableblock h1", 0);
}
}
#[test]
fn showtitle_can_be_disabled_in_asciidoc_table_cell_if_set_by_api() {
for (name, value, cell) in [
("showtitle", true, ":!showtitle:"),
("notitle", false, ":notitle:"),
] {
let doc = Parser::default()
.with_intrinsic_attribute_bool(name, value, ModificationContext::ApiOnly)
.parse(&format!(
"= Document Title\n\n|===\na|\n= Nested Document Title\n{cell}\n\ncontent\n|==="
));
assert_css(&doc, "h1", 1);
assert_css(&doc, ".tableblock h1", 0);
}
}
#[test]
fn asciidoc_content() {
let doc = Parser::default().parse(
"[cols=\"1e,1,5a\"]\n|===\n|Name |Backends |Description\n\n|badges |xhtml11, html5 |\nLink badges.\n\n[NOTE]\n====\nThe path names are relative.\n====\n|docinfo |All backends |\nThese attributes control docinfo:\n\ndocinfo:: Include x\ndocinfo1:: Include y\n|===",
);
assert_css(&doc, "table.tableblock > tbody > tr", 2);
assert_xpath(
&doc,
"((/table/tbody/tr)[1]/td)[3]//*[@class=\"admonitionblock\"]",
1,
);
assert_xpath(&doc, "((/table/tbody/tr)[2]/td)[3]//*[@class=\"dlist\"]", 1);
}
#[test]
fn should_preserve_leading_indentation_in_contents_of_asciidoc_table_cell_if_contents_starts_with_newline()
{
let doc = Parser::default().parse("|===\na|\n $ command\na| paragraph\n|===");
// Source-map line numbers: the table starts on line 1; cell 1's
// separator is on line 2 and its inner document begins on line 3 (the
// indented `$ command`); cell 2 is on line 4.
let table = match doc.nested_blocks().next() {
Some(crate::blocks::Block::Table(table)) => table,
other => panic!("expected a table block, got {other:?}"),
};
assert_eq!(table.span().line(), 1);
let cell1 = &table.body_rows()[0].cells()[0];
assert_eq!(cell1.span().line(), 2);
let crate::blocks::TableCellContent::AsciiDoc(inner) = cell1.content() else {
panic!("expected an AsciiDoc cell");
};
assert_eq!(inner.blocks()[0].span().line(), 3);
let cell2 = &table.body_rows()[1].cells()[0];
assert_eq!(cell2.span().line(), 4);
assert_css(&doc, "td", 2);
assert_xpath(&doc, "(//td)[1]//*[@class=\"literalblock\"]", 1);
assert_xpath(&doc, "(//td)[2]//*[@class=\"paragraph\"]", 1);
assert_xpath(&doc, "(//pre)[1][text()=\"$ command\"]", 1);
assert_xpath(&doc, "(//p)[1][text()=\"paragraph\"]", 1);
}
#[test]
fn preprocessor_directive_on_first_line_of_an_asciidoc_table_cell_should_be_processed() {
let handler =
InlineFileHandler::from_pairs([("fixtures/include-file.adoc", "included content")]);
let mut parser = Parser::default()
.with_safe_mode(SafeMode::Server)
.with_include_file_handler(handler);
let doc = parser.parse("|===\na|include::fixtures/include-file.adoc[]\n|===");
// The `include::` on the cell's first line is expanded, so the cell holds
// the included content rather than the literal directive.
assert_rendered_contains(&doc, "included content");
}
#[test]
fn error_about_unresolved_preprocessor_directive_on_first_line_of_an_asciidoc_table_cell_should_have_correct_cursor()
{
// The table (with a cell whose first line is an unresolvable `include::`)
// lives in `outer.adoc`, which the primary document reaches via its own
// `include::`. The error about the unresolved cell directive must be
// attributed to `outer.adoc` at the line the directive appears on there
// (line 5), not to the primary document or the synthesized cell content.
let handler = InlineFileHandler::from_pairs([(
"outer.adoc",
"|===\n|A |B\n\n|text\na|include::does-not-exist.adoc[]\n|===",
)]);
let mut parser = Parser::default()
.with_safe_mode(SafeMode::Server)
.with_include_file_handler(handler);
let doc = parser.parse("first\n\ninclude::outer.adoc[]\n\nlast");
// The unresolved directive is replaced (in the cell) with a message that
// names the file the directive came from.
assert_rendered_contains(&doc, "Unresolved directive in outer.adoc");
// A single warning is reported, and its cursor maps — through the
// document source map — back to `outer.adoc` line 5.
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::IncludeFileNotFound("does-not-exist.adoc".to_string())
);
assert_eq!(
doc.source_map()
.original_file_and_line(warnings[0].source.line()),
Some(crate::parser::SourceLine(Some("outer.adoc".to_string()), 5))
);
}
#[test]
fn cross_reference_link_in_an_asciidoc_table_cell_should_resolve_to_reference_in_main_document()
{
let doc =
Parser::default().parse("== Some\n\n|===\na|See <<_more>>\n|===\n\n== More\n\ncontent");
// The cross-reference inside the AsciiDoc cell resolves against the main
// document's catalog, even though its target section is defined *after*
// the table.
assert_xpath(&doc, "//a[@href=\"#_more\"]", 1);
assert_xpath(&doc, "//a[@href=\"#_more\"][text()=\"More\"]", 1);
}
#[test]
fn should_discover_anchor_at_start_of_cell_and_register_it_as_a_reference() {
let doc = Parser::default().parse(
"The highest peak in the Front Range is <<grays-peak>>, which tops <<mount-evans>> by just a few feet.\n\n[cols=\"1s,1\"]\n|===\n|[[mount-evans,Mount Evans]]Mount Evans\n|14,271 feet\n\nh|[[grays-peak,Grays Peak]]\nGrays Peak\n|14,278 feet\n|===",
);
// The anchors at the start of the cells are cataloged as document
// references.
let catalog = doc.catalog();
assert!(catalog.contains_id("mount-evans"));
assert!(catalog.contains_id("grays-peak"));
// The cross-references in the introductory paragraph resolve against
// those cataloged anchors, using each anchor's reftext.
assert_xpath(
&doc,
"(//p)[1]/a[@href=\"#grays-peak\"][text()=\"Grays Peak\"]",
1,
);
assert_xpath(
&doc,
"(//p)[1]/a[@href=\"#mount-evans\"][text()=\"Mount Evans\"]",
1,
);
// The anchors render as targets within their respective cells.
assert_xpath(&doc, "(//table/tbody/tr)[1]//td//a[@id=\"mount-evans\"]", 1);
assert_xpath(&doc, "(//table/tbody/tr)[2]//th//a[@id=\"grays-peak\"]", 1);
}
#[test]
fn should_catalog_anchor_at_start_of_cell_in_implicit_header_row_when_column_has_a_style() {
let doc = Parser::default()
.parse("[cols=1a]\n|===\n|[[foo,Foo]]* not AsciiDoc\n\n| AsciiDoc\n|===");
assert!(doc.catalog().contains_id("foo"));
}
#[test]
fn should_catalog_anchor_at_start_of_cell_in_explicit_header_row_when_column_has_a_style() {
let doc = Parser::default()
.parse("[%header,cols=1a]\n|===\n|[[foo,Foo]]* not AsciiDoc\n| AsciiDoc\n|===");
assert!(doc.catalog().contains_id("foo"));
}
#[test]
fn should_catalog_anchor_at_start_of_cell_in_first_row() {
let doc = Parser::default().parse("|===\n|[[foo,Foo]]foo\n| bar\n|===");
assert!(doc.catalog().contains_id("foo"));
}
#[test]
fn footnotes_should_not_be_shared_between_an_asciidoc_table_cell_and_the_main_document() {
let doc = Parser::default()
.parse("|===\na|AsciiDoc footnote:[A lightweight markup language.]\n|===");
// The footnote defined inside the cell is numbered and rendered within
// the cell's own (nested) document.
assert_css(&doc, "sup.footnote", 1);
assert_css(&doc, "a#_footnoteref_1", 1);
// It is *not* shared with the enclosing document: the main document's
// footnote registry stays empty, so the footnote would not appear in the
// main document's footnote list.
assert!(
doc.catalog().footnotes().is_empty(),
"cell footnote leaked into the main document's registry"
);
}
// Backend-specific test omitted: DocBook ("callout numbers should be
// globally unique, including AsciiDoc table cells"). Out of scope: it
// asserts only against DocBook output (`backend: 'docbook'`, `//co` /
// `//callout` elements), which the crate does not target.
// Out of scope (omitted): compatibility mode is a stated limitation of the
// crate, so the three "compat mode ... in AsciiDoc table cell" tests are not
// ported.
#[test]
fn nested_table() {
let doc = Parser::default().parse(
"[cols=\"1,2a\"]\n|===\n|Normal cell\n|Cell with nested table\n[cols=\"2,1\"]\n!===\n!Nested table cell 1 !Nested table cell 2\n!===\n|===",
);
assert_css(&doc, "table", 2);
assert_css(&doc, "table table", 1);
// Ruby uses `td:nth-child(2) table`; the nested table sits in the second
// cell of the (single) outer row.
assert_xpath(&doc, "/table/tbody/tr/td[2]//table", 1);
assert_xpath(&doc, "/table/tbody/tr/td[2]//table/tbody/tr/td", 2);
}
#[test]
fn can_set_format_of_nested_table_to_psv() {
let doc = Parser::default().parse(
"[cols=\"2*\"]\n|===\n|normal cell\na|\n[format=psv]\n!===\n!nested cell\n!===\n|===",
);
assert_css(&doc, "table", 2);
assert_css(&doc, "table table", 1);
assert_xpath(&doc, "/table/tbody/tr/td[2]//table", 1);
assert_xpath(&doc, "/table/tbody/tr/td[2]//table/tbody/tr/td", 1);
}
#[test]
fn asciidoc_table_cell_should_inherit_to_dir_option_from_parent_document() {
// Adapted from Asciidoctor's "AsciiDoc table cell should inherit to_dir
// option from parent document". Asciidoctor passes a `to_dir` *option*
// when building the document and, after locating the nested cell
// document, asserts it is `nested?` and that its `options[:to_dir]`
// matches the parent's.
//
// This crate has no options bag: a document's configuration is carried by
// its attributes, and Asciidoctor itself surfaces the `to_dir` option as
// the `outdir` document attribute. So the faithful analog is to configure
// the parent with an `outdir` attribute (as if from the API, the way an
// option is supplied) and confirm the nested AsciiDoc-cell document both
// reports itself as nested and has inherited that value.
let doc = Parser::default()
.with_intrinsic_attribute("outdir", "/path/to/output", ModificationContext::ApiOnly)
.parse("|===\na|\nAsciiDoc table cell\n|===");
// Locate the nested document: the single table's single cell, which is an
// AsciiDoc cell (`a|`) and therefore a nested, standalone document. This
// is the crate's equivalent of Ruby's
// `doc.blocks[0].find_by context: :document, traverse_documents: true`.
let table = match doc.nested_blocks().next() {
Some(crate::blocks::Block::Table(table)) => table,
other => panic!("expected a table block, got {other:?}"),
};
let crate::blocks::TableCellContent::AsciiDoc(nested_doc) =
table.body_rows()[0].cells()[0].content()
else {
panic!("expected an AsciiDoc cell");
};
// `assert nested_doc.nested?`
assert!(nested_doc.is_nested());
// `assert_equal doc.options[:to_dir], nested_doc.options[:to_dir]` — the
// inherited option (here the `outdir` attribute) is visible on the nested
// document and equals the parent's.
assert!(nested_doc.is_attribute_set("outdir"));
assert_eq!(
nested_doc.attribute_value("outdir"),
doc.attribute_value("outdir")
);
}
#[test]
fn asciidoc_table_cell_should_not_inherit_toc_setting_from_parent_document() {
let doc = Parser::default().parse(
"= Document Title\n:toc:\n\n== Section\n\n|===\na|\n== Section in Nested Document\n\ncontent\n|===",
);
// The outer document renders one TOC; the nested cell does not inherit
// the `toc` setting, so no TOC appears inside the table.
assert_css(&doc, ".toc", 1);
assert_css(&doc, "table .toc", 0);
}
#[test]
fn should_be_able_to_enable_toc_in_an_asciidoc_table_cell() {
let doc = Parser::default().parse(
"= Document Title\n\n== Section A\n\n|===\na|\n= Subdocument Title\n:toc:\n\n== Subdocument Section A\n\ncontent\n|===",
);
// The outer document has no TOC; the cell enables its own, so the single
// TOC is the one inside the table.
assert_css(&doc, ".toc", 1);
assert_css(&doc, "table .toc", 1);
}
#[test]
fn should_be_able_to_enable_toc_in_an_asciidoc_table_cell_even_if_hard_unset_by_api() {
// Hard-unsetting `toc` through the API (Ruby's `toc => nil`) suppresses
// the outer document's TOC but does not prevent the cell — a standalone
// nested document, and `toc` is one of the attributes a cell may set —
// from enabling its own.
let doc = Parser::default()
.with_intrinsic_attribute_bool("toc", false, ModificationContext::ApiOnly)
.parse(
"= Document Title\n\n== Section A\n\n|===\na|\n= Subdocument Title\n:toc:\n\n== Subdocument Section A\n\ncontent\n|===",
);
assert_css(&doc, ".toc", 1);
assert_css(&doc, "table .toc", 1);
}
#[test]
fn should_be_able_to_enable_toc_in_both_outer_document_and_in_an_asciidoc_table_cell() {
let doc = Parser::default().parse(
"= Document Title\n:toc:\n\n== Section A\n\n|===\na|\n= Subdocument Title\n:toc: macro\n\n[#table-cell-toc]\ntoc::[]\n\n== Subdocument Section A\n\ncontent\n|===",
);
// Two TOCs: the outer document's automatic TOC (`#toc`) and the cell's
// `toc::[]` macro TOC (carrying the `#table-cell-toc` id).
assert_css(&doc, ".toc", 2);
assert_css(&doc, "#toc", 1);
assert_css(&doc, "table .toc", 1);
assert_css(&doc, "table #table-cell-toc", 1);
}
#[test]
fn document_in_an_asciidoc_table_cell_should_not_see_doctitle_of_parent() {
let doc = Parser::default()
.parse("= Document Title\n\n[cols=\"1a\"]\n|===\n|AsciiDoc content\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > tbody > tr > td", 1);
// The cell is its own (nested) document and does not inherit the parent's
// doctitle, so its lone paragraph is not promoted into a preamble: the
// cell renders a bare `.paragraph`, never a `#preamble`.
assert_css(&doc, "table > tbody > tr > td #preamble", 0);
assert_css(&doc, "table > tbody > tr > td .paragraph", 1);
}
// Not implemented (by design): the Asciidoctor `cell_background_color` test
// is intentionally omitted. Per-cell background colors depend on the
// `{set:cellbgcolor:...}` / `{set:cellbgcolor!}` inline attribute-entry
// reference. That syntax is discouraged and likely to be removed from the
// AsciiDoc language, so this crate has decided not to implement it. See the
// "No planned support for some AsciiDoc features" section of the README.
#[test]
fn should_warn_if_table_block_is_not_terminated() {
let doc = Parser::default().parse("outside\n\n|===\n|\ninside\n\nstill inside\n\neof");
assert_xpath(&doc, "/table", 1);
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].warning, WarningType::UnterminatedDelimitedBlock);
assert_eq!(warnings[0].source.line(), 3);
}
#[test]
#[ignore]
// TODO (https://github.com/asciidoc-rs/asciidoc-parser/issues/542): an
// unterminated example block inside an AsciiDoc table cell that is itself
// attached to a list item — Asciidoctor reports the warning at the inner
// block's line (9). Enable once the cursor/line of nested-cell warnings is
// tracked (same nested-cell source-map work as #542).
fn should_show_correct_line_number_in_warning_about_unterminated_block_inside_asciidoc_table_cell()
{
let doc = Parser::default().parse(
"outside\n\n* list item\n+\n|===\n|cell\na|inside\n\n====\nunterminated example block\n|===\n\neof",
);
assert_xpath(&doc, "//ul//table", 1);
let warnings: Vec<_> = doc.warnings().collect();
assert!(
warnings
.iter()
.any(|w| w.warning == WarningType::UnterminatedDelimitedBlock
&& w.source.line() == 9)
);
}
#[test]
fn custom_separator_for_an_asciidoc_table_cell() {
let doc = Parser::default().parse(
"[cols=2,separator=!]\n|===\n!Pipe output to vim\na!\n----\nasciidoctor -o - -s test.adoc | view -\n----\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > tbody > tr", 1);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 2);
assert_xpath(&doc, "((/table/tbody/tr)[1]/td)[1]//p", 1);
assert_xpath(
&doc,
"((/table/tbody/tr)[1]/td)[2]//*[@class=\"listingblock\"]",
1,
);
}
// Backend-specific tests omitted: DocBook ("table with breakable option
// docbook 5", "table with unbreakable option docbook 5").
#[test]
fn no_implicit_header_row_if_cell_in_first_line_is_quoted_and_spans_multiple_lines() {
let doc =
Parser::default().parse("[cols=2*l]\n,===\n\"A1\n\nA1 continued\",B1\nA2,B2\n,===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > thead", 0);
assert_css(&doc, "table > tbody", 1);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(//td)[1]//pre[text()=\"A1\n\nA1 continued\"]", 1);
}
}
mod dsv {
use crate::tests::prelude::*;
#[test]
fn converts_simple_dsv_table() {
let doc = Parser::default().parse(
"[width=\"75%\",format=\"dsv\"]\n|===\nroot:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\nmysql:x:27:27:MySQL\\:Server:/var/lib/mysql:/bin/bash\ngdm:x:42:42::/var/lib/gdm:/sbin/nologin\nsshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin\nnobody:x:99:99:Nobody:/:/sbin/nologin\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col[width=\"14.2857%\"]", 6);
// Ruby uses `col:last-of-type`; the indexed XPath form is equivalent.
assert_xpath(&doc, "(/table/colgroup/col)[7][@width=\"14.2858%\"]", 1);
assert_css(&doc, "table > tbody > tr", 6);
assert_xpath(&doc, "//tr[4]/td[5]/p/text()", 0);
assert_xpath(&doc, "//tr[3]/td[5]/p[text()=\"MySQL:Server\"]", 1);
}
#[test]
fn dsv_format_shorthand() {
let doc = Parser::default().parse(":===\na:b:c\n1:2:3\n:===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 3);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 3);
}
#[test]
fn single_cell_in_dsv_table_should_only_produce_single_row() {
let doc = Parser::default().parse(":===\nsingle cell\n:===");
assert_css(&doc, "table td", 1);
}
#[test]
fn should_treat_trailing_colon_as_an_empty_cell() {
let doc = Parser::default().parse(":===\nA1:\nB1:B2\nC1:C2\n:===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > tbody > tr", 3);
assert_xpath(&doc, "/table/tbody/tr[1]/td", 2);
assert_xpath(&doc, "/table/tbody/tr[1]/td[1]/p[text()=\"A1\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p", 0);
assert_xpath(&doc, "/table/tbody/tr[2]/td[1]/p[text()=\"B1\"]", 1);
}
}
mod csv {
use crate::tests::prelude::{inline_file_handler::InlineFileHandler, *};
#[test]
fn should_treat_trailing_comma_as_an_empty_cell() {
let doc = Parser::default().parse(",===\nA1,\nB1,B2\nC1,C2\n,===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > tbody > tr", 3);
assert_xpath(&doc, "/table/tbody/tr[1]/td", 2);
assert_xpath(&doc, "/table/tbody/tr[1]/td[1]/p[text()=\"A1\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p", 0);
assert_xpath(&doc, "/table/tbody/tr[2]/td[1]/p[text()=\"B1\"]", 1);
}
#[test]
fn should_log_error_but_not_crash_if_cell_data_has_unclosed_quote() {
let doc = Parser::default().parse(",===\na,b\nc,\"\n,===");
// The unclosed quote recovers to an empty cell without crashing, and an
// error is logged for line 3 (the `c,"` line).
assert_css(&doc, "table", 1);
assert_css(&doc, "table td", 4);
assert_xpath(&doc, "(//td)[4]/p", 0);
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::TableCsvDataHasUnclosedQuote
);
assert_eq!(warnings[0].source.line(), 3);
}
#[test]
fn should_preserve_newlines_in_quoted_csv_values() {
let doc = Parser::default().parse(
"[cols=\"1,1,1l\"]\n,===\n\"A\nB\nC\",\"one\n\ntwo\n\nthree\",\"do\n\nre\n\nme\"\n,===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 3);
assert_css(&doc, "table > tbody > tr", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td", 3);
assert_xpath(&doc, "/table/tbody/tr[1]/td[1]/p[text()=\"A\nB\nC\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p", 3);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p[1][text()=\"one\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p[2][text()=\"two\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p[3][text()=\"three\"]", 1);
assert_xpath(
&doc,
"/table/tbody/tr[1]/td[3]//pre[text()=\"do\n\nre\n\nme\"]",
1,
);
}
#[test]
fn should_not_drop_trailing_empty_cell_in_tsv_data_when_loaded_from_an_include_file() {
// The `1\t2\t` record ends with a tab, so its third field is empty. The
// include is the delivery mechanism; the behavior under test is that the
// trailing empty cell survives.
let handler = InlineFileHandler::from_pairs([(
"fixtures/data.tsv",
"First\tSecond\tThird\na\tb\tc\n1\t2\t\nx\ty\tz\n",
)]);
let mut parser = Parser::default()
.with_safe_mode(SafeMode::Server)
.with_include_file_handler(handler);
let doc = parser.parse("[%header,format=tsv]\n|===\ninclude::fixtures/data.tsv[]\n|===");
assert_css(&doc, "table > tbody > tr", 3);
assert_css(&doc, "table > tbody > tr:nth-child(1) > td", 3);
assert_css(&doc, "table > tbody > tr:nth-child(2) > td", 3);
assert_css(&doc, "table > tbody > tr:nth-child(3) > td", 3);
assert_css(
&doc,
"table > tbody > tr:nth-child(2) > td:nth-child(3):empty",
1,
);
}
#[test]
fn mixed_unquoted_records_and_quoted_records_with_escaped_quotes_commas_and_wrapped_lines() {
let doc = Parser::default().parse(
"[format=\"csv\",options=\"header\"]\n|===\nYear,Make,Model,Description,Price\n1997,Ford,E350,\"ac, abs, moon\",3000.00\n1999,Chevy,\"Venture \"\"Extended Edition\"\"\",\"\",4900.00\n1999,Chevy,\"Venture \"\"Extended Edition, Very Large\"\"\",,5000.00\n1996,Jeep,Grand Cherokee,\"MUST SELL!\nair, moon roof, loaded\",4799.00\n2000,Toyota,Tundra,\"\"\"This one's gonna to blow you're socks off,\"\" per the sticker\",10000.00\n2000,Toyota,Tundra,\"Check it, \"\"this one's gonna to blow you're socks off\"\", per the sticker\",10000.00\n|===",
);
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col[width=\"20%\"]", 5);
assert_css(&doc, "table > thead > tr", 1);
assert_css(&doc, "table > tbody > tr", 6);
assert_xpath(
&doc,
"((//tbody/tr)[1]/td)[4]/p[text()=\"ac, abs, moon\"]",
1,
);
assert_xpath(
&doc,
"((//tbody/tr)[2]/td)[3]/p[text()='Venture \"Extended Edition\"']",
1,
);
assert_xpath(
&doc,
"((//tbody/tr)[4]/td)[4]/p[text()=\"MUST SELL!\nair, moon roof, loaded\"]",
1,
);
assert_xpath(
&doc,
"((//tbody/tr)[5]/td)[4]/p[text()='\"This one\u{2019}s gonna to blow you\u{2019}re socks off,\" per the sticker']",
1,
);
assert_xpath(
&doc,
"((//tbody/tr)[6]/td)[4]/p[text()='Check it, \"this one\u{2019}s gonna to blow you\u{2019}re socks off\", per the sticker']",
1,
);
}
#[test]
fn should_allow_quotes_around_a_csv_value_to_be_on_their_own_lines() {
let doc = Parser::default().parse("[cols=2*]\n,===\n\"\nA\n\",\"\nB\n\"\n,===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 2);
assert_css(&doc, "table > tbody > tr", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td", 2);
assert_xpath(&doc, "/table/tbody/tr[1]/td[1]/p[text()=\"A\"]", 1);
assert_xpath(&doc, "/table/tbody/tr[1]/td[2]/p[text()=\"B\"]", 1);
}
#[test]
fn csv_format_shorthand() {
let doc = Parser::default().parse(",===\na,b,c\n1,2,3\n,===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 3);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 3);
}
#[test]
fn tsv_as_format() {
let doc = Parser::default().parse("[format=tsv]\n,===\na\tb\tc\n1\t2\t3\n,===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 3);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 3);
}
#[test]
fn custom_csv_separator() {
let doc = Parser::default().parse("[format=csv,separator=;]\n|===\na;b;c\n1;2;3\n|===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 3);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 3);
}
#[test]
fn tab_as_separator() {
let doc = Parser::default().parse("[separator=\\t]\n,===\na\tb\tc\n1\t2\t3\n,===");
assert_css(&doc, "table", 1);
assert_css(&doc, "table > colgroup > col", 3);
assert_css(&doc, "table > tbody > tr", 2);
assert_xpath(&doc, "(/table/tbody/tr)[1]/td", 3);
assert_xpath(&doc, "(/table/tbody/tr)[2]/td", 3);
}
#[test]
fn single_cell_in_csv_table_should_only_produce_single_row() {
let doc = Parser::default().parse(",===\nsingle cell\n,===");
assert_css(&doc, "table td", 1);
}
#[test]
fn cell_formatted_with_asciidoc_style() {
let doc = Parser::default().parse(
"[cols=\"1,1,1a\",separator=;]\n,===\nelement;description;example\n\nthematic break,a visible break; also known as a horizontal rule;---\n,===",
);
assert_css(&doc, "table tbody hr", 1);
}
#[test]
fn should_strip_whitespace_around_contents_of_asciidoc_cell() {
let doc = Parser::default().parse(
"[cols=\"1,1,1a\",separator=;]\n,===\nelement;description;example\n\nparagraph;contiguous lines of words and phrases;\"\n one sentence, one line\n \"\n,===",
);
assert_xpath(
&doc,
"/table/tbody//*[@class=\"paragraph\"]/p[text()=\"one sentence, one line\"]",
1,
);
}
}