From 489b7f74114c72aeea45493f056df4268cbe1d09 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Mon, 11 Nov 2024 18:39:48 +0100 Subject: [PATCH 1/7] fix: allow empty column for table --- src/Output/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Output/Table.php b/src/Output/Table.php index 7f9924c..4960833 100644 --- a/src/Output/Table.php +++ b/src/Output/Table.php @@ -94,7 +94,7 @@ protected function normalize(array $rows): array foreach ($head as $col => &$value) { $cols = array_column($rows, $col); - $span = array_map('strlen', $cols); + $span = array_map(fn($col) => strlen($col ?? ''), $cols); $span[] = strlen($col); $value = max($span); } From 4ec104effb0dc049bd81e1b843723e67b6069c31 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Mon, 11 Nov 2024 18:46:13 +0100 Subject: [PATCH 2/7] test: provides more tests for generating tables. takes into account different scenarios --- tests/Output/TableTest.php | 218 +++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tests/Output/TableTest.php diff --git a/tests/Output/TableTest.php b/tests/Output/TableTest.php new file mode 100644 index 0000000..6eac77d --- /dev/null +++ b/tests/Output/TableTest.php @@ -0,0 +1,218 @@ + + * + * + * Licensed under MIT license. + */ + +namespace Ahc\Cli\Test\Output; + +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_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)); + } +} From 88bbcc4bd7e1603f972ef19b50422f7b3cfa22e2 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Mon, 11 Nov 2024 19:16:59 +0100 Subject: [PATCH 3/7] feat: possibility to define a paticular color for a single column of table --- src/Output/Table.php | 26 +++++++++++++++++--- tests/Output/TableTest.php | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/Output/Table.php b/src/Output/Table.php index 4960833..c067330 100644 --- a/src/Output/Table.php +++ b/src/Output/Table.php @@ -61,7 +61,16 @@ public function render(array $rows, array $styles = []): string [$start, $end] = $styles[['even', 'odd'][(int) $odd]]; foreach ($head as $col => $size) { - $parts[] = str_pad($row[$col] ?? '', $size, ' '); + $text = $row[$col] ?? ''; + + if (preg_match('/(\\x1b(?:.+)m)/U', $text, $matches)) { + $line = str_replace($matches[1], '', $text); + $line = preg_replace('/\\x1b\[0m/', '', $line); + + $size += strlen($text) - strlen($line); + } + + $parts[] = str_pad($text, $size, ' '); } $odd = !$odd; @@ -93,8 +102,19 @@ protected function normalize(array $rows): array } foreach ($head as $col => &$value) { - $cols = array_column($rows, $col); - $span = array_map(fn($col) => strlen($col ?? ''), $cols); + $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); } diff --git a/tests/Output/TableTest.php b/tests/Output/TableTest.php index 6eac77d..e847b20 100644 --- a/tests/Output/TableTest.php +++ b/tests/Output/TableTest.php @@ -11,6 +11,7 @@ namespace Ahc\Cli\Test\Output; +use Ahc\Cli\Output\Color; use Ahc\Cli\Output\Table; use Ahc\Cli\Test\CliTestCase; @@ -215,4 +216,53 @@ public function test_render_with_custom_styles(): void $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'] + ]; + + var_dump($color->ok('25')); + // exit; + $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)); + } } From 4a7118f3172d56ac70900da2a24fe402a77a3741 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 14 Nov 2024 16:40:21 +0100 Subject: [PATCH 4/7] feat: customising table cells using the `$styles` parameter --- src/Output/Table.php | 41 +++++++--- tests/Output/TableTest.php | 153 ++++++++++++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 14 deletions(-) diff --git a/src/Output/Table.php b/src/Output/Table.php index c067330..6c7d8b9 100644 --- a/src/Output/Table.php +++ b/src/Output/Table.php @@ -45,36 +45,53 @@ 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) { + $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 { + $style = $styles[['even', 'odd'][(int) $odd]]; + } + + [$start, $end] = $style; + $text = $row[$col] ?? ''; if (preg_match('/(\\x1b(?:.+)m)/U', $text, $matches)) { - $line = str_replace($matches[1], '', $text); - $line = preg_replace('/\\x1b\[0m/', '', $line); + $word = str_replace($matches[1], '', $text); + $word = preg_replace('/\\x1b\[0m/', '', $word); - $size += strlen($text) - strlen($line); + $size += strlen($text) - strlen($word); } - $parts[] = str_pad($text, $size, ' '); + $parts[] = "$start " . str_pad($text, $size, ' ') . " $end"; } $odd = !$odd; - $body[] = "|$start " . implode(" $end|$start ", $parts) . " $end|"; + $body[] = '|' . implode('|', $parts) . '|'; } $dash = '+' . implode('+', $dash) . '+' . PHP_EOL; @@ -132,9 +149,11 @@ protected function normalizeStyles(array $styles): array ]; foreach ($styles as $for => $style) { - if (isset($default[$for])) { - $default[$for] = ['<' . trim($style, '<> ') . '>', '']; + if (is_string($style) && $style !== '') { + $style = ['<' . trim($style, '<> ') . '>', '']; } + + $default[$for] = $style; } return $default; diff --git a/tests/Output/TableTest.php b/tests/Output/TableTest.php index e847b20..164f392 100644 --- a/tests/Output/TableTest.php +++ b/tests/Output/TableTest.php @@ -239,7 +239,6 @@ public function test_render_with_ansi_color_codes_in_cell_content(): void $this->assertSame($expectedOutput, trim($result)); } - public function test_render_with_ansi_color_codes_in_cell_content_using_colors_class(): void { $color = new Color(); @@ -250,8 +249,6 @@ public function test_render_with_ansi_color_codes_in_cell_content_using_colors_c ['name' => $color->info('Bob Johnson'), 'age' => '40'] ]; - var_dump($color->ok('25')); - // exit; $expectedOutput = "+-------------+-----+" . PHP_EOL . "| Name | Age |" . PHP_EOL . @@ -265,4 +262,154 @@ public function test_render_with_ansi_color_codes_in_cell_content_using_colors_c $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_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_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_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, '|')); + } } From dda0b45bafe2f7fe6b8a4fff371c2356e80d4f10 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 14 Nov 2024 17:39:14 +0100 Subject: [PATCH 5/7] tests: add tests for table rendering --- tests/Output/TableTest.php | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/Output/TableTest.php b/tests/Output/TableTest.php index 164f392..6b3bcde 100644 --- a/tests/Output/TableTest.php +++ b/tests/Output/TableTest.php @@ -48,6 +48,28 @@ public function test_render_with_single_row_and_column(): void $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 = [ @@ -412,4 +434,51 @@ public function test_render_with_large_number_of_columns(): void $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)); + } } From db1547006c94987e4d990385fcdb709952ce544d Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 14 Nov 2024 18:46:41 +0100 Subject: [PATCH 6/7] patch: add support for dynamically defining the style of a cell via a callable --- src/Output/Table.php | 31 ++++++-- tests/Output/TableTest.php | 155 +++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 6 deletions(-) diff --git a/src/Output/Table.php b/src/Output/Table.php index 6c7d8b9..5f9c441 100644 --- a/src/Output/Table.php +++ b/src/Output/Table.php @@ -72,13 +72,14 @@ public function render(array $rows, array $styles = []): string $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]]; } - [$start, $end] = $style; - - $text = $row[$col] ?? ''; + $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); @@ -150,12 +151,30 @@ protected function normalizeStyles(array $styles): array foreach ($styles as $for => $style) { if (is_string($style) && $style !== '') { - $style = ['<' . trim($style, '<> ') . '>', '']; + $default[$for] = ['<' . trim($style, '<> ') . '>', '']; + } else if (str_contains($for, ':') && is_callable($style)) { + $default[$for] = $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 index 6b3bcde..90f4b09 100644 --- a/tests/Output/TableTest.php +++ b/tests/Output/TableTest.php @@ -363,6 +363,107 @@ public function test_render_with_row_specific_styles(): void $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 = [ @@ -392,6 +493,35 @@ public function test_render_with_mixed_specific_styles(): void $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 = [ @@ -411,6 +541,31 @@ public function test_render_with_empty_styles_array(): void $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; From 9dacc23ed1ce2702664557c357491cd4cfa6bebc Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 14 Nov 2024 19:27:28 +0100 Subject: [PATCH 7/7] docs: update readme to add doc for table customization --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) 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.