fix(markdown): expand presentational tables into blocks

Honor presentation and none table roles with ASCII case-insensitive matching and HTML whitespace trimming. Preserve captions, cell content, and independent conversion of nested data tables.

Validation: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (18,162 passed, 13 skipped).
This commit is contained in:
ldm0
2026-09-17 01:25:45 +08:00
parent a14ec9788c
commit 2a37e002f4
3 changed files with 60 additions and 11 deletions
+9 -7
View File
@@ -64,13 +64,15 @@ and cell line breaks and paragraph boundaries use inline `<br>` tags. Attribute
newlines are encoded or normalized so they cannot break a table row. GFM supplies
missing trailing cells in short rows; the converter does not allocate a padded grid.
Complex tables retain Turndown core's ordinary block expansion, in DOM order with
blank lines between cells. This includes non-unit spans, multiple `thead` rows,
rows wider than an explicit header, or cells containing nested tables, lists,
headings, quotes or preformatted blocks. Inner simple tables can still produce
GFM when their containing layout table is expanded. Roles, borders and CSS do
not select another conversion path, so structurally simple layout tables also
produce GFM. Complex layouts remain readable blocks instead of raw HTML tables.
Tables with `role="presentation"` or `role="none"` retain ordinary block expansion,
in DOM order with blank lines between cells, even when structurally simple. Role
matching ignores ASCII case and surrounding HTML whitespace. The same fallback
applies to complex tables: non-unit spans, multiple `thead` rows, rows wider than
an explicit header, or cells containing nested tables, lists, headings, quotes
or preformatted blocks. Inner simple tables can still produce GFM when their
containing layout table is expanded. Other roles, borders and CSS do not select
another conversion path. Complex layouts remain readable blocks instead of raw
HTML tables.
Supported output includes headings, paragraphs, emphasis, strikethrough, links,
images, lists, blockquotes, hard breaks, and fenced code. Inline HTML is used when
+6
View File
@@ -26,6 +26,12 @@ impl<Id: Copy> Table<Id> {
depth: usize,
max_depth: usize,
) -> Option<Self> {
if dom.attribute(node, "role").is_some_and(|role| {
let role = role.trim_matches(is_space);
role.eq_ignore_ascii_case("presentation") || role.eq_ignore_ascii_case("none")
}) {
return None;
}
let mut table = Self {
captions: Vec::new(),
cells: Vec::new().into_iter(),
+45 -4
View File
@@ -55,14 +55,55 @@ fn wrapping_a_table_preserves_its_content() {
}
#[test]
fn table_roles_do_not_change_conversion() {
fn presentational_tables_expand_into_blocks() {
for role in ["presentation", "none", " PRESENTATION ", "\tNoNe\r\n"] {
for header in [
"",
"<tr><th>Name</th><th>Value</th></tr>",
"<thead><tr><td>Name</td><td>Value</td></tr></thead>",
] {
let html = format!(
"<table role='{role}'><caption><strong>Title</strong></caption>{header}<tbody><tr><td><a href='/one'>one</a></td><td><code>two|three</code></td></tr></tbody><tfoot><tr><td>end</td></tr></tfoot></table>"
);
let heading = if header.is_empty() {
""
} else {
"Name\n\nValue\n\n"
};
assert_eq!(
markdown(&html),
format!("**Title**\n\n{heading}[one](/one)\n\n`two|three`\n\nend"),
"{html}"
);
}
}
}
#[test]
fn presentational_table_roles_do_not_propagate_to_nested_tables() {
for role in ["presentation", "none"] {
let inner =
"<table><tr><th>Name</th><th>Value</th></tr><tr><td>one</td><td>two</td></tr></table>";
let html = format!(
"<table role='{role}'><tr><td>before</td><td>{inner}</td><td>after</td></tr></table>"
);
assert_eq!(
markdown(&html),
"before\n\n| Name | Value |\n| --- | --- |\n| one | two |\n\nafter",
"{html}"
);
}
}
#[test]
fn other_table_roles_do_not_change_conversion() {
for role in [
"",
"presentation",
"none",
" PRESENTATION ",
"table",
"grid",
"treegrid",
"presentationish",
"none-other",
] {
let html = format!(
"<table role='{role}'><tr><th>Name</th><th>Value</th></tr><tr><td>one</td><td>two</td></tr></table>"