diff --git a/src/Parser/Block/TableParser.php b/src/Parser/Block/TableParser.php index e7b3769..f3a56f7 100644 --- a/src/Parser/Block/TableParser.php +++ b/src/Parser/Block/TableParser.php @@ -34,6 +34,10 @@ public function isTableRow(string $line): bool return false; } + // Trailing whitespace after the closing pipe is insignificant (parity + // with carve-js / carve-rs); strip it before the structural checks. + $line = rtrim($line, " \t"); + // Strip row attributes if present (|...|{.class}) $lineWithoutRowAttrs = $this->stripRowAttributes($line); @@ -88,7 +92,29 @@ public function extractRowAttributes(string $line): array */ public function isSeparatorRow(string $line): bool { - return preg_match('/^\|[\s:|-]+\|$/', $line) === 1 && str_contains($line, '-'); + // Trailing whitespace after the closing pipe is insignificant. + $line = rtrim($line, " \t"); + + $len = strlen($line); + if ($len < 2 || $line[0] !== '|' || $line[$len - 1] !== '|') { + return false; + } + + // Every cell must be a delimiter cell: optional whitespace, an optional + // leading ':', one or more '-', an optional trailing ':', optional + // whitespace. An EMPTY cell (`|---||`) or any other content disqualifies + // the row -- it is then an ordinary data row (matches carve-js/carve-rs). + $cells = $this->parseTableCells($line); + if ($cells === []) { + return false; + } + foreach ($cells as $cell) { + if (preg_match('/^\s*:?-+:?\s*$/', $cell) !== 1) { + return false; + } + } + + return true; } /** @@ -152,6 +178,9 @@ public function parseTableCells(string $line): array // Strip row attributes first $line = $this->stripRowAttributes($line); + // Trailing whitespace after the closing pipe is insignificant. + $line = rtrim($line, " \t"); + // Remove leading and trailing | $line = substr($line, 1, -1); @@ -357,6 +386,9 @@ public function parseTableCellsRaw(string $line): array // Strip row attributes first $line = $this->stripRowAttributes($line); + // Trailing whitespace after the closing pipe is insignificant. + $line = rtrim($line, " \t"); + // Must start with | to be a potential table row if (!str_starts_with($line, '|')) { return []; diff --git a/src/Parser/BlockParser.php b/src/Parser/BlockParser.php index 05538b5..95773c7 100644 --- a/src/Parser/BlockParser.php +++ b/src/Parser/BlockParser.php @@ -2750,7 +2750,9 @@ protected function tryParseTable(Node $parent, array $lines, int $start): ?int // Strip row attributes for validation (|...|{.class} → |...|) $lineWithoutRowAttrs = $this->tableParser->stripRowAttributes($currentLine); - if (!preg_match('/^\|.*\|$/', $lineWithoutRowAttrs)) { + // Trailing whitespace after the closing pipe is insignificant + // (parity with carve-js / carve-rs). + if (!preg_match('/^\|.*\|[ \t]*$/', $lineWithoutRowAttrs)) { break; } diff --git a/tests/TestCase/TableDelimiterRowTest.php b/tests/TestCase/TableDelimiterRowTest.php new file mode 100644 index 0000000..3e446b5 --- /dev/null +++ b/tests/TestCase/TableDelimiterRowTest.php @@ -0,0 +1,56 @@ +converter = new DjotConverter(); + } + + public function testSeparatorRowWithTrailingWhitespaceStillPromotesHeader(): void + { + // Trailing whitespace after the closing pipe is insignificant; the + // separator must still promote the first row to a header. + $result = $this->converter->convert("| H | G |\n|---| \n| a | b |"); + + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringNotContainsString('

', $result); + } + + public function testDataRowWithTrailingWhitespaceStillParsesAsTable(): void + { + $result = $this->converter->convert('| a | '); + + $this->assertStringContainsString('

', $result); + $this->assertStringNotContainsString('

', $result); + } + + public function testSeparatorRowWithEmptyCellIsNotASeparator(): void + { + // `|---||` has an empty second cell, so it is NOT a delimiter row: the + // first row must not be promoted to a header. + $result = $this->converter->convert("| H | G |\n|---||\n| a | b |"); + + $this->assertStringNotContainsString('

', $result); + } +}
HGa', $result); + $this->assertStringContainsString('H