diff --git a/src/Rules/TableRule.php b/src/Rules/TableRule.php index c5e16f9..09b7930 100644 --- a/src/Rules/TableRule.php +++ b/src/Rules/TableRule.php @@ -50,30 +50,27 @@ public function parse(Parser $parser): ?Token $line = $parser->consumeUntil(Parser::NEW_LINE); $parser->consumeWhile(Parser::NEW_LINE); - // Filter out separator rows - $isSeparator = trim($line, ':| -') === '' && str_contains($line, '-'); + $token = $parser->lastToken; - if ($isSeparator) { - // TODO: determine alignment based on the separator - return null; - } + // Only the row right after the header is a separator, later rows that look like one are data rows + $isSeparator = + $token instanceof TableToken + && $token->alignments === [] + && trim($line, ':| -') === '' + && str_contains($line, '-'); - // Create cells - if (str_starts_with($line, '|')) { - $line = substr($line, 1); - } + if ($isSeparator) { + $token->alignments = array_map( + $this->alignment(...), + $this->cells($line), + ); - if (str_ends_with($line, '|')) { - $line = substr($line, 0, -1); + return null; } - $cells = $line - |> (fn ($x) => explode('|', $x)) - |> (fn ($x) => array_map(trim(...), $x)) - |> array_values(...); + $cells = $this->cells($line); // Determine if is header row - $token = $parser->lastToken; $isHeader = ! $token instanceof TableToken; $row = new TableRow($cells, $isHeader); @@ -87,4 +84,39 @@ public function parse(Parser $parser): ?Token return null; } + + /** + * @return list + */ + private function cells(string $line): array + { + if (str_starts_with($line, '|')) { + $line = substr($line, 1); + } + + if (str_ends_with($line, '|')) { + $line = substr($line, 0, -1); + } + + return $line + |> (fn ($x) => explode('|', $x)) + |> (fn ($x) => array_map(trim(...), $x)) + |> array_values(...); + } + + /** + * `:---` aligns left, `:---:` center and `---:` right + */ + private function alignment(string $separator): ?string + { + return match ([ + str_starts_with($separator, ':'), + str_ends_with($separator, ':'), + ]) { + [true, true] => 'center', + [true, false] => 'left', + [false, true] => 'right', + default => null, + }; + } } diff --git a/src/Tokens/TableRow.php b/src/Tokens/TableRow.php index 77ef2c2..d9388af 100644 --- a/src/Tokens/TableRow.php +++ b/src/Tokens/TableRow.php @@ -5,7 +5,7 @@ final readonly class TableRow { public function __construct( - /** @var string[] */ + /** @var list */ public array $cells, public bool $isHeader, ) {} diff --git a/src/Tokens/TableToken.php b/src/Tokens/TableToken.php index ea69f2d..16cd639 100644 --- a/src/Tokens/TableToken.php +++ b/src/Tokens/TableToken.php @@ -19,6 +19,8 @@ final class TableToken implements Token public function __construct( /** @var \Tempest\Markdown\Tokens\TableRow[] */ public array $rows = [], + /** @var list Alignment of each column: left, center, right or null */ + public array $alignments = [], ) {} public function parse(Parser $parser): string @@ -52,8 +54,13 @@ public function parse(Parser $parser): string foreach ($headerRows as $row) { $table .= ''; - foreach ($row->cells as $cell) { - $table .= '' . $parser->parse($cell)->html . ''; + foreach ($row->cells as $column => $cell) { + $table .= + 'align($column) + . '>' + . $parser->parse($cell)->html + . ''; } $table .= ''; @@ -68,8 +75,13 @@ public function parse(Parser $parser): string foreach ($dataRows as $row) { $table .= ''; - foreach ($row->cells as $cell) { - $table .= '' . $parser->parse($cell)->html . ''; + foreach ($row->cells as $column => $cell) { + $table .= + 'align($column) + . '>' + . $parser->parse($cell)->html + . ''; } $table .= ''; @@ -82,4 +94,11 @@ public function parse(Parser $parser): string return $table; } + + private function align(int $column): string + { + $alignment = $this->alignments[$column] ?? null; + + return $alignment === null ? '' : ' align="' . $alignment . '"'; + } } diff --git a/tests/Rules/TableRuleTest.php b/tests/Rules/TableRuleTest.php index a33ba5e..773de99 100644 --- a/tests/Rules/TableRuleTest.php +++ b/tests/Rules/TableRuleTest.php @@ -61,7 +61,63 @@ public function test_lex_separator_with_alignment(): void ); $this->assertSame( - '
ABC
123
', + '
ABC
123
', + $html, + ); + } + + #[Test] + public function test_columns_without_alignment(): void + { + $html = + (string) new Parser(highlighter: null, rules: [new TableRule()])->parse( + "| A | B |\n| --- | ---: |\n| 1 | 2 |", + ); + + $this->assertSame( + '
AB
12
', + $html, + ); + } + + #[Test] + public function test_alignment_without_spaces_or_trailing_pipe(): void + { + $html = + (string) new Parser(highlighter: null, rules: [new TableRule()])->parse( + "| A | B |\n|:-:|-:\n| 1 | 2 |", + ); + + $this->assertSame( + '
AB
12
', + $html, + ); + } + + #[Test] + public function test_only_the_row_after_the_header_is_a_separator(): void + { + $html = + (string) new Parser(highlighter: null, rules: [new TableRule()])->parse( + "| A |\n| :--- |\n| 1 |\n| --- |\n| 2 |", + ); + + $this->assertSame( + '
A
1
---
2
', + $html, + ); + } + + #[Test] + public function test_alignment_applies_to_every_data_row(): void + { + $html = + (string) new Parser(highlighter: null, rules: [new TableRule()])->parse( + "| A |\n| :---: |\n| 1 |\n| 2 |", + ); + + $this->assertSame( + '
A
1
2
', $html, ); } @@ -75,7 +131,7 @@ public function test_table_with_empty_cells(): void ); $this->assertSame( - '
ABC
3
', + '
ABC
3
', $html, ); } diff --git a/tests/Tokens/TableTokenTest.php b/tests/Tokens/TableTokenTest.php index 0b31b25..eeece33 100644 --- a/tests/Tokens/TableTokenTest.php +++ b/tests/Tokens/TableTokenTest.php @@ -64,6 +64,23 @@ public function test_parse_with_image(): void ); } + #[Test] + public function test_parse_with_alignments(): void + { + $token = new TableToken( + rows: [ + new TableRow(['A', 'B', 'C'], isHeader: true), + new TableRow(['1', '2', '3'], isHeader: false), + ], + alignments: ['left', null, 'right'], + ); + + $this->assertEquals( + '
ABC
123
', + $token->parse(new Parser()), + ); + } + #[Test] public function test_parse_multiple_rows(): void {