From f11736e574958fcd10ec40b69db61b406645fc2e Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 15 Aug 2026 19:50:43 +0200 Subject: [PATCH] fix: HTML table import keeps row order and invents no header cells The importer read the header off the first row containing any th, wrote that row as the djot header row, and moved it to the top. Two defects followed. Rows were reordered. A table whose third row held a th came back with that row first, so the reading order of the content changed - not the markup, the content. A data cell was promoted. A lone mixed row (th + td) became a header row and both cells came back as th, so a screen reader announces a header that nobody wrote. A row is now the header row only when it is the first row and every one of its cells is a th. Every other row is written in place as a body row. djot has no per-cell header spelling: a header row is the row before the separator, and that is all a djot table can express. So a th outside a leading all-header row cannot survive, and the only choice is which loss to take. Losing the header marker loses an annotation the author wrote; promoting a data cell invents one they did not. Inventing is worse, and reordering is worse still, so both are gone. Rows inside thead still count as leading rows, and a table with no th anywhere still imports as body rows with no separator. --- docs/guide/converters.md | 2 + src/Converter/HtmlToDjot.php | 12 +++--- tests/TestCase/Converter/HtmlToDjotTest.php | 47 +++++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/docs/guide/converters.md b/docs/guide/converters.md index d48a1c3..8cc45fe 100644 --- a/docs/guide/converters.md +++ b/docs/guide/converters.md @@ -168,6 +168,8 @@ $djot = $converter->convert($html); | `
` + `` + `
` | Image with `^ caption` | | `
` + `
` + `
` | Block quote with `^ caption` | +Djot can only spell a leading header row, so a `` elsewhere imports as a data cell. + **File Operations:** ```php diff --git a/src/Converter/HtmlToDjot.php b/src/Converter/HtmlToDjot.php index 9c847be..99de9fe 100644 --- a/src/Converter/HtmlToDjot.php +++ b/src/Converter/HtmlToDjot.php @@ -1592,7 +1592,6 @@ protected function processTable(DOMElement $node): string { $rows = []; $headerRow = null; - $headerRowAttrs = ''; $columnCount = 0; $captionText = ''; $alignments = []; @@ -1606,9 +1605,9 @@ protected function processTable(DOMElement $node): string // Find all rows $trElements = $this->getDirectTableRows($node); - foreach ($trElements as $tr) { + foreach ($trElements as $rowIndex => $tr) { $cells = []; - $isHeader = false; + $allCellsAreHeaders = true; $columnIndex = 0; foreach ($tr->childNodes as $cell) { @@ -1624,8 +1623,8 @@ protected function processTable(DOMElement $node): string } else { $cells[] = $cellContent; } - if ($tag === 'th') { - $isHeader = true; + if ($tag !== 'th') { + $allCellsAreHeaders = false; } if (!isset($alignments[$columnIndex])) { $alignments[$columnIndex] = $this->extractTableCellAlignment($cell); @@ -1644,9 +1643,8 @@ protected function processTable(DOMElement $node): string $row = '| ' . implode(' | ', $cells) . ' |' . $rowAttrSuffix; - if ($isHeader && $headerRow === null) { + if ($rowIndex === 0 && $allCellsAreHeaders) { $headerRow = $row; - $headerRowAttrs = $rowAttrSuffix; } else { $rows[] = $row; } diff --git a/tests/TestCase/Converter/HtmlToDjotTest.php b/tests/TestCase/Converter/HtmlToDjotTest.php index 60d6377..19becfd 100644 --- a/tests/TestCase/Converter/HtmlToDjotTest.php +++ b/tests/TestCase/Converter/HtmlToDjotTest.php @@ -389,6 +389,53 @@ public function testTable(): void $this->assertStringContainsString('| Alice | 30 |', $result); } + public function testTableHeaderCellInThirdRowDoesNotReorderRows(): void + { + $html = '
a
b
H
'; + + $djot = $this->converter->convert($html); + + $this->assertSame("| a |\n| b |\n| H |\n", $djot); + $this->assertSame( + "\n\n\n\n\n\n\n\n\n\n
a
b
H
\n", + (new DjotConverter())->convert($djot), + ); + } + + public function testTableMixedFirstRowImportsAsDataCells(): void + { + $html = '
R1
'; + + $djot = $this->converter->convert($html); + + $this->assertSame("| R | 1 |\n", $djot); + $this->assertSame( + "\n\n\n\n\n
R1
\n", + (new DjotConverter())->convert($djot), + ); + } + + public function testTableLeadingAllHeaderRowImportsAsHeader(): void + { + $html = '
AB
12
'; + + $this->assertSame("| A | B |\n|---|---|\n| 1 | 2 |\n", $this->converter->convert($html)); + } + + public function testTableSectionsPreserveLeadingHeaderAndBodyRows(): void + { + $html = '
A
1
'; + + $this->assertSame("| A |\n|---|\n| 1 |\n", $this->converter->convert($html)); + } + + public function testTableWithoutHeaderCellsHasNoSeparator(): void + { + $html = '
a
b
'; + + $this->assertSame("| a |\n| b |\n", $this->converter->convert($html)); + } + public function testNestedTableDoesNotLeakInnerRowsIntoOuterTable(): void { $html = '
outer
inner
';