diff --git a/README.md b/README.md index 6f50431..4976efc 100644 --- a/README.md +++ b/README.md @@ -609,12 +609,70 @@ $writer->table([ 'head' => 'boldGreen', // For the table heading 'odd' => 'bold', // For the odd rows (1st row is odd, then 3, 5 etc) 'even' => 'comment', // For the even rows (2nd row is even, then 4, 6 etc) + '1:1' => 'red', // For cell in row 1 col 1 (1 based count, 'apple' in this example) + '2:*' => '', // For all cells in row 2 (1 based count) + '*:2' => '', // For all cells in col 2 (1 based count) + 'b-c' => '', // For all columns named 'b-c' (same as '*:2' in this example) + '*:*' => 'blue', // For all cells in table (Set all cells to blue) ]); +``` + +You can define the style of a cell dynamically using a callback. You could then apply one style or another depending on a value. + +```php +$rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ['name' => 'Bob Johnson', 'age' => '40'], +]; -// 'head', 'odd', 'even' are all the styles for now -// In future we may support styling a column by its name! +$styles = [ + '*:2' => function ($val, $row) { + return $row['age'] >= 30 ? 'boldRed' : ''; + }, +]; + +$writer->table($rows, $styles); ``` +The example above only processes the cells in the second column of the table. Yf you want to process any cell, you can use the `*:*` key. You could then customise each cell in the table + +```php +$rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ['name' => 'Alice Bob', 'age' => '10'], + ['name' => 'Big Johnson', 'age' => '40'], + ['name' => 'Jane X', 'age' => '50'], + ['name' => 'John Smith', 'age' => '20'], + ['name' => 'Bob John', 'age' => '28'], +]; + +$styles = [ + '*:*' => function ($val, $row) { + if ($val === 'Jane X') { + return 'yellow'; + } + if ($val == 10 || $val == 20) { + return 'boldPurple'; + } + if (str_contains($val, 'Bob')) { + return 'blue'; + } + return $row['age'] >= 30 ? 'boldRed' : ''; + }, +]; + +$writer->table($rows, $styles); +``` + +> **Note: Priority in increasing order:** +> - `odd` or `even` +> - `2:*` (row) +> - `*:2` or `b-c <-> column name` (col) +> - `*:*` any cell in table +> - `1:1` (cell) = **highest priority** + #### Justify content (Display setting) If you want to display certain configurations (from your .env file for example) a bit like Laravel does (via the `php artisan about` command) you can use the `justify` method. diff --git a/src/Output/Table.php b/src/Output/Table.php index 7f9924c..5f9c441 100644 --- a/src/Output/Table.php +++ b/src/Output/Table.php @@ -45,27 +45,54 @@ public function render(array $rows, array $styles = []): string [$head, $rows] = $table; $styles = $this->normalizeStyles($styles); - $title = $body = $dash = []; + $title = $body = $dash = $positions = []; [$start, $end] = $styles['head']; + $pos = 0; foreach ($head as $col => $size) { - $dash[] = str_repeat('-', $size + 2); - $title[] = str_pad($this->toWords($col), $size, ' '); + $dash[] = str_repeat('-', $size + 2); + $title[] = str_pad($this->toWords($col), $size, ' '); + $positions[$col] = ++$pos; } $title = "|$start " . implode(" $end|$start ", $title) . " $end|" . PHP_EOL; $odd = true; - foreach ($rows as $row) { + foreach ($rows as $line => $row) { $parts = []; + $line++; [$start, $end] = $styles[['even', 'odd'][(int) $odd]]; foreach ($head as $col => $size) { - $parts[] = str_pad($row[$col] ?? '', $size, ' '); + $colNumber = $positions[$col]; + + if (isset($styles[$line . ':' . $colNumber])) { // cell, 1:1 + $style = $styles[$line . ':' . $colNumber]; + } else if (isset($styles[$col]) || isset($styles['*:' . $colNumber])) { // col, *:2 or b + $style = $styles['*:' . $colNumber] ?? $styles[$col]; + } else if (isset($styles[$line . ':*'])) { // row, 2:* + $style = $styles[$line . ':*']; + } else if (isset($styles['*:*'])) { // any cell, *:* + $style = $styles['*:*']; + } else { + $style = $styles[['even', 'odd'][(int) $odd]]; + } + + $text = $row[$col] ?? ''; + [$start, $end] = $this->parseStyle($style, $text, $row, $rows); + + if (preg_match('/(\\x1b(?:.+)m)/U', $text, $matches)) { + $word = str_replace($matches[1], '', $text); + $word = preg_replace('/\\x1b\[0m/', '', $word); + + $size += strlen($text) - strlen($word); + } + + $parts[] = "$start " . str_pad($text, $size, ' ') . " $end"; } $odd = !$odd; - $body[] = "|$start " . implode(" $end|$start ", $parts) . " $end|"; + $body[] = '|' . implode('|', $parts) . '|'; } $dash = '+' . implode('+', $dash) . '+' . PHP_EOL; @@ -93,7 +120,18 @@ protected function normalize(array $rows): array } foreach ($head as $col => &$value) { - $cols = array_column($rows, $col); + $cols = array_column($rows, $col); + $cols = array_map(function($col) { + $col ??= ''; + + if (preg_match('/(\\x1b(?:.+)m)/U', $col, $matches)) { + $col = str_replace($matches[1], '', $col); + $col = preg_replace('/\\x1b\[0m/', '', $col); + } + + return $col; + }, $cols); + $span = array_map('strlen', $cols); $span[] = strlen($col); $value = max($span); @@ -112,11 +150,31 @@ protected function normalizeStyles(array $styles): array ]; foreach ($styles as $for => $style) { - if (isset($default[$for])) { + if (is_string($style) && $style !== '') { $default[$for] = ['<' . trim($style, '<> ') . '>', '']; + } else if (str_contains($for, ':') && is_callable($style)) { + $default[$for] = $style; } } return $default; } + + protected function parseStyle(array|callable $style, $val, array $row, array $table): array + { + if (is_array($style)) { + return $style; + } + + $style = call_user_func($style, $val, $row, $table); + + if (is_string($style) && $style !== '') { + return ['<' . trim($style, '<> ') . '>', '']; + } + if (is_array($style) && count($style) === 2) { + return $style; + } + + return ['', '']; + } } diff --git a/tests/Output/TableTest.php b/tests/Output/TableTest.php new file mode 100644 index 0000000..90f4b09 --- /dev/null +++ b/tests/Output/TableTest.php @@ -0,0 +1,639 @@ + + * + * + * Licensed under MIT license. + */ + +namespace Ahc\Cli\Test\Output; + +use Ahc\Cli\Output\Color; +use Ahc\Cli\Output\Table; +use Ahc\Cli\Test\CliTestCase; + +class TableTest extends CliTestCase +{ + protected Table $table; + + public function setUp(): void + { + parent::setUp(); + + $this->table = new Table(); + } + + public function test_render_returns_empty_string_for_empty_rows(): void + { + $result = $this->table->render([]); + + $this->assertSame('', $result); + } + + public function test_render_with_single_row_and_column(): void + { + $rows = [['header' => 'values']]; + $expectedOutput = + "+--------+" . PHP_EOL . + "| Header |" . PHP_EOL . + "+--------+" . PHP_EOL . + "| values |" . PHP_EOL . + "+--------+"; + + $result = $this->table->render($rows); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_single_column(): void + { + $rows = [ + ['name' => 'John Doe'], + ['name' => 'Jane Smith'], + ['name' => 'Bob Johnson'] + ]; + + $expectedOutput = + "+-------------+" . PHP_EOL . + "| Name |" . PHP_EOL . + "+-------------+" . PHP_EOL . + "| John Doe |" . PHP_EOL . + "| Jane Smith |" . PHP_EOL . + "| Bob Johnson |" . PHP_EOL . + "+-------------+"; + + $result = $this->table->render($rows); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_multiple_rows_and_columns(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30', 'city' => 'New York'], + ['name' => 'Jane Smith', 'age' => '25', 'city' => 'Los Angeles'], + ['name' => 'Bob Johnson', 'age' => '40', 'city' => 'Chicago'] + ]; + + $expectedOutput = + "+-------------+-----+-------------+" . PHP_EOL . + "| Name | Age | City |" . PHP_EOL . + "+-------------+-----+-------------+" . PHP_EOL . + "| John Doe | 30 | New York |" . PHP_EOL . + "| Jane Smith | 25 | Los Angeles |" . PHP_EOL . + "| Bob Johnson | 40 | Chicago |" . PHP_EOL . + "+-------------+-----+-------------+" ; + + $result = $this->table->render($rows); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_different_styles_for_odd_and_even_rows(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ['name' => 'Bob Johnson', 'age' => '40'] + ]; + + $styles = [ + 'odd' => 'bold', + 'even' => 'comment' + ]; + + $expectedOutput = + "+-------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+-------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "| Bob Johnson | 40 |" . PHP_EOL . + "+-------------+-----+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_padded_column_content(): void + { + $rows = [ + ['name' => 'John', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ['name' => 'Bob', 'age' => '40'] + ]; + + $expectedOutput = + "+------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+------------+-----+" . PHP_EOL . + "| John | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "| Bob | 40 |" . PHP_EOL . + "+------------+-----+"; + + $result = $this->table->render($rows); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_generates_correct_separators_between_header_and_body(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'] + ]; + + $expectedOutput = + "+------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "+------------+-----+"; + + $result = $this->table->render($rows); + + $this->assertStringContainsString("+------------+-----+" . PHP_EOL, $result); + $this->assertStringContainsString("| Name | Age |" . PHP_EOL, $result); + $this->assertStringContainsString("+------------+-----+" . PHP_EOL, $result); + $this->assertEquals(3, substr_count($result, "+------------+-----+" . PHP_EOL)); + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_handles_missing_values_in_rows_gracefully(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30', 'city' => 'New York'], + ['name' => 'Jane Smith', 'age' => '25'], + ['name' => 'Bob Johnson', 'city' => 'Chicago'] + ]; + + $expectedOutput = + "+-------------+-----+----------+" . PHP_EOL . + "| Name | Age | City |" . PHP_EOL . + "+-------------+-----+----------+" . PHP_EOL . + "| John Doe | 30 | New York |" . PHP_EOL . + "| Jane Smith | 25 | |" . PHP_EOL . + "| Bob Johnson | | Chicago |" . PHP_EOL . + "+-------------+-----+----------+"; + + $result = $this->table->render($rows); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_converts_column_names_to_words(): void + { + $rows = [ + ['first_name' => 'John', 'last_name' => 'Doe', 'age_in_years' => '30'], + ['first_name' => 'Jane', 'last_name' => 'Smith', 'age_in_years' => '25'] + ]; + + $expectedOutput = + "+------------+-----------+--------------+" . PHP_EOL . + "| First Name | Last Name | Age In Years |" . PHP_EOL . + "+------------+-----------+--------------+" . PHP_EOL . + "| John | Doe | 30 |" . PHP_EOL . + "| Jane | Smith | 25 |" . PHP_EOL . + "+------------+-----------+--------------+"; + + $result = $this->table->render($rows); + + $this->assertStringContainsString('| First Name |', $result); + $this->assertStringContainsString('| Last Name |', $result); + $this->assertStringContainsString('| Age In Years |', $result); + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_custom_styles(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ]; + + $styles = [ + 'head' => 'boldGreen', // For the table heading + 'odd' => 'bold', // For the odd rows (1st row is odd, then 3, 5 etc) + 'even' => 'comment', // For the even rows (2nd row is even, then 4, 6 etc) + ]; + + $expectedOutput = + "+------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "+------------+-----+"; + + $result = $this->table->render($rows, $styles); + + $this->assertStringContainsString("", $result); + $this->assertStringContainsString("", $result); + $this->assertStringContainsString("", $result); + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_ansi_color_codes_in_cell_content(): void + { + $rows = [ + ['name' => "\033[31mJohn Doe\033[0m", 'age' => '30'], + ['name' => 'Jane Smith', 'age' => "\033[32m25\033[0m"], + ['name' => "\033[34mBob Johnson\033[0m", 'age' => '40'] + ]; + + $expectedOutput = + "+-------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+-------------+-----+" . PHP_EOL . + "| \033[31mJohn Doe\033[0m | 30 |" . PHP_EOL . + "| Jane Smith | \033[32m25\033[0m |" . PHP_EOL . + "| \033[34mBob Johnson\033[0m | 40 |" . PHP_EOL . + "+-------------+-----+"; + + $result = $this->table->render($rows); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_ansi_color_codes_in_cell_content_using_colors_class(): void + { + $color = new Color(); + + $rows = [ + ['name' => $color->error('John Doe'), 'age' => '30'], + ['name' => 'Jane Smith', 'age' => $color->ok('25')], + ['name' => $color->info('Bob Johnson'), 'age' => '40'] + ]; + + $expectedOutput = + "+-------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+-------------+-----+" . PHP_EOL . + "| \033[0;31mJohn Doe\033[0m | 30 |" . PHP_EOL . + "| Jane Smith | \033[0;32m25\033[0m |" . PHP_EOL . + "| \033[0;34mBob Johnson\033[0m | 40 |" . PHP_EOL . + "+-------------+-----+"; + + $result = $this->table->render($rows); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_cell_specific_styles(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ]; + + $styles = [ + 'head' => 'boldGreen', + '1:1' => 'boldRed', // Cell-specific style for first row, first column + '2:2' => 'boldBlue', // Cell-specific style for second row, second column + ]; + + $expectedOutput = + "+------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "+------------+-----+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_column_specific_styles(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ]; + + $styles = [ + 'head' => 'boldGreen', + '*:2' => 'boldBlue', // Column-specific style for the second column + ]; + + $expectedOutput = + "+------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "+------------+-----+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_row_specific_styles(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ['name' => 'Bob Johnson', 'age' => '40'], + ]; + + $styles = [ + 'head' => 'boldGreen', + '2:*' => 'boldRed', // Row-specific style for the second row + ]; + + $expectedOutput = + "+-------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+-------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "| Bob Johnson | 40 |" . PHP_EOL . + "+-------------+-----+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_callable_styles(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ]; + + $styles = [ + 'head' => 'boldGreen', + '1:1' => function ($val, $row, $table) { + return $val === 'John Doe' ? 'boldRed' : ''; + }, + '2:2' => function ($val, $row, $table) { + return $val === '25' ? 'boldBlue' : ''; + }, + ]; + + $expectedOutput = + "+------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "+------------+-----+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_callable_styles_using_row(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ['name' => 'Bob Johnson', 'age' => '40'], + ]; + + $styles = [ + 'head' => 'boldGreen', + '*:2' => function ($val, $row) { + if ($val == 25) { + return 'boldYellow'; + } + + return $row['age'] >= 30 ? 'boldRed' : ''; + }, + ]; + + $expectedOutput = + "+-------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+-------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "| Bob Johnson | 40 |" . PHP_EOL . + "+-------------+-----+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_callable_styles_on_any_cell(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ['name' => 'Alice Bob', 'age' => '10'], + ['name' => 'Bob Johnson', 'age' => '40'], + ['name' => 'Jane X', 'age' => '50'], + ]; + + $styles = [ + 'head' => 'boldGreen', + '*:*' => function ($val, $row) { + if ($val === 'Jane X') { + return 'yellow'; + } + if ($val == 10) { + return 'purple'; + } + return $row['age'] >= 30 ? 'boldRed' : ''; + }, + ]; + + $expectedOutput = + "+-------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+-------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "| Alice Bob | 10 |" . PHP_EOL . + "| Bob Johnson | 40 |" . PHP_EOL . + "| Jane X | 50 |" . PHP_EOL . + "+-------------+-----+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } + public function test_render_with_mixed_specific_styles(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30', 'city' => 'New York'], + ['name' => 'Jane Smith', 'age' => '25', 'city' => 'Los Angeles'], + ['name' => 'Bob Johnson', 'age' => '40', 'city' => 'Chicago'], + ]; + + $styles = [ + 'head' => 'boldGreen', + '1:2' => 'boldRed', // Cell-specific style for first row, second column + '*:3' => 'boldBlue', // Column-specific style for the third column + '3:*' => 'italic', // Row-specific style for the third row + ]; + + $expectedOutput = + "+-------------+-----+-------------+" . PHP_EOL . + "| Name | Age | City |" . PHP_EOL . + "+-------------+-----+-------------+" . PHP_EOL . + "| John Doe | 30 | New York |" . PHP_EOL . + "| Jane Smith | 25 | Los Angeles |" . PHP_EOL . + "| Bob Johnson | 40 | Chicago |" . PHP_EOL . + "+-------------+-----+-------------+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_styles_using_column_name(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30', 'city' => 'New York'], + ['name' => 'Jane Smith', 'age' => '25', 'city' => 'Los Angeles'], + ['name' => 'Bob Johnson', 'age' => '40', 'city' => 'Chicago'], + ]; + + $styles = [ + 'head' => 'boldGreen', + '1:2' => 'boldRed', // Cell-specific style for first row, second column + 'city' => 'boldBlue', + 'name' => 'italic', + ]; + + $expectedOutput = + "+-------------+-----+-------------+" . PHP_EOL . + "| Name | Age | City |" . PHP_EOL . + "+-------------+-----+-------------+" . PHP_EOL . + "| John Doe | 30 | New York |" . PHP_EOL . + "| Jane Smith | 25 | Los Angeles |" . PHP_EOL . + "| Bob Johnson | 40 | Chicago |" . PHP_EOL . + "+-------------+-----+-------------+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_with_empty_styles_array(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ]; + + $expectedOutput = + "+------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "+------------+-----+"; + + $result = $this->table->render($rows, []); + + $this->assertSame($expectedOutput, trim($result)); + } + + public function test_render_handles_invalid_style_keys_gracefully(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ]; + + $invalidStyles = [ + 'invalidKey' => 'boldRed', // Invalid style key + 'head' => 'boldGreen', + ]; + + $expectedOutput = + "+------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "+------------+-----+"; + + $result = $this->table->render($rows, $invalidStyles); + + $this->assertSame($expectedOutput, trim($result)); + } + public function test_render_with_large_number_of_columns(): void + { + $columns = 100; + $rows = [ + array_combine( + array_map(fn($i) => "col$i", range(1, $columns)), + array_map(fn($i) => "value$i", range(1, $columns)) + ) + ]; + + $result = $this->table->render($rows); + + $this->assertStringContainsString('| Col1 | Col2 | Col3 |', $result); + $this->assertStringContainsString('| Col98 | Col99 | Col100 |', $result); + $this->assertStringContainsString('| value1 | value2 | value3 |', $result); + $this->assertStringContainsString('| value98 | value99 | value100 |', $result); + + $expectedLineCount = 5; // Header, separator lines, and data row + $this->assertEquals($expectedLineCount, substr_count($result, PHP_EOL)); + + $expectedColumnCount = $columns + $columns + 2; // start + columns + separators + end + $this->assertEquals($expectedColumnCount, substr_count($result, '|')); + } + + public function test_render_handles_large_number_of_rows(): void + { + $rows = []; + for ($i = 0; $i < 1000; $i++) { + $rows[] = [ + 'id' => $i, + 'name' => "Name $i", + 'email' => "email$i@example.com" + ]; + } + + $result = $this->table->render($rows); + + $this->assertStringContainsString('| Id | Name | Email |', $result); + $this->assertStringContainsString('| 0 | Name 0 | email0@example.com |', $result); + $this->assertStringContainsString('| 999 | Name 999 | email999@example.com |', $result); + $this->assertEquals(1004, substr_count($result, PHP_EOL)); // 1000 data rows + 4 border rows + } + + public function test_render_with_html_like_tags_in_cell_content(): void + { + $rows = [ + ['name' => 'John Doe', 'age' => '30'], + ['name' => 'Jane Smith', 'age' => '25'], + ['name' => 'Bob Johnson', 'age' => '40'] + ]; + + $styles = [ + 'head' => 'boldGreen', + 'odd' => 'bold', + 'even' => 'comment', + ]; + + $expectedOutput = + "+--------------------------+-----+" . PHP_EOL . + "| Name | Age |" . PHP_EOL . + "+--------------------------+-----+" . PHP_EOL . + "| John Doe | 30 |" . PHP_EOL . + "| Jane Smith | 25 |" . PHP_EOL . + "| Bob Johnson | 40 |" . PHP_EOL . + "+--------------------------+-----+"; + + $result = $this->table->render($rows, $styles); + + $this->assertSame($expectedOutput, trim($result)); + } +}