From 4005ae45d749281c43b405c936399bdf5badb03a Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 28 Nov 2024 14:49:05 +0100 Subject: [PATCH 1/4] fix: the presence of unicode characters in the table distorts it --- src/Output/Table.php | 27 +++++++++++++++++++-------- tests/Output/TableTest.php | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/Output/Table.php b/src/Output/Table.php index 19400a2..eb306a0 100644 --- a/src/Output/Table.php +++ b/src/Output/Table.php @@ -23,11 +23,11 @@ use function implode; use function is_array; use function max; +use function mb_strwidth; +use function mb_substr; use function reset; use function sprintf; -use function str_pad; use function str_repeat; -use function strlen; use function trim; use const PHP_EOL; @@ -51,7 +51,7 @@ public function render(array $rows, array $styles = []): string $pos = 0; foreach ($head as $col => $size) { $dash[] = str_repeat('-', $size + 2); - $title[] = str_pad($this->toWords($col), $size, ' '); + $title[] = $this->strPad($this->toWords($col), $size, ' '); $positions[$col] = ++$pos; } @@ -62,7 +62,6 @@ public function render(array $rows, array $styles = []): string $parts = []; $line++; - [$start, $end] = $styles[['even', 'odd'][(int) $odd]]; foreach ($head as $col => $size) { $colNumber = $positions[$col]; @@ -85,10 +84,10 @@ public function render(array $rows, array $styles = []): string $word = str_replace($matches[1], '', $text); $word = preg_replace('/\\x1b\[0m/', '', $word); - $size += strlen($text) - strlen($word); + $size += mb_strwidth($text) - mb_strwidth($word); } - $parts[] = "$start " . str_pad($text, $size, ' ') . " $end"; + $parts[] = "$start " . $this->strPad($text, $size, ' ') . " $end"; } $odd = !$odd; @@ -132,8 +131,8 @@ protected function normalize(array $rows): array return $col; }, $cols); - $span = array_map('strlen', $cols); - $span[] = strlen($col); + $span = array_map('mb_strwidth', $cols); + $span[] = mb_strwidth($col); $value = max($span); } @@ -177,4 +176,16 @@ protected function parseStyle(array|callable $style, $val, array $row, array $ta return ['', '']; } + + /** + * Pad a multibyte string to a certain length with another multibyte string + */ + protected function strPad(string $string, int $length, string $pad_string = ' '): string + { + if (1 > $paddingRequired = $length - mb_strwidth($string)) { + return $string; + } + + return $string . mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired); + } } diff --git a/tests/Output/TableTest.php b/tests/Output/TableTest.php index 87fc6a2..dbff2ce 100644 --- a/tests/Output/TableTest.php +++ b/tests/Output/TableTest.php @@ -639,4 +639,26 @@ public function test_render_with_html_like_tags_in_cell_content(): void $this->assertSame($expectedOutput, trim($result)); } + + public function test_render_with_unicode_characters_in_cell_content(): void + { + $rows = [ + ['name' => 'François', 'greeting' => 'Bonjour'], + ['name' => 'Jürgen', 'greeting' => 'Guten Tag'], + ['name' => '北京', 'greeting' => '你好'] + ]; + + $expectedOutput = + "+----------+-----------+" . PHP_EOL . + "| Name | Greeting |" . PHP_EOL . + "+----------+-----------+" . PHP_EOL . + "| François | Bonjour |" . PHP_EOL . + "| Jürgen | Guten Tag |" . PHP_EOL . + "| 北京 | 你好 |" . PHP_EOL . + "+----------+-----------+"; + + $result = $this->table->render($rows); + + $this->assertSame($expectedOutput, trim($result)); + } } From 628ede7484b93a823b01863c818c63457a1b61f5 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 28 Nov 2024 16:13:26 +0100 Subject: [PATCH 2/4] fix: allow unicode characters in `justify` method --- src/Output/Writer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Output/Writer.php b/src/Output/Writer.php index ca1cff9..a187357 100644 --- a/src/Output/Writer.php +++ b/src/Output/Writer.php @@ -339,7 +339,7 @@ public function justify(string $first, ?string $second = null, array $options = $second = (string) $second; $terminalWidth = $this->terminal->width() ?? 80; - $dashWidth = $terminalWidth - (strlen($first) + strlen($second)); + $dashWidth = $terminalWidth - (mb_strwidth($first) + mb_strwidth($second)); // remove left and right margins because we're going to add 1 space on each side (after/before the text). // if we don't have a second element, we just remove the left margin $dashWidth -= $second === '' ? 1 : 2; From 6ccd82f11ede0705baf7e8a9eb1b7193aa79cda0 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Fri, 29 Nov 2024 10:45:31 +0100 Subject: [PATCH 3/4] patch: provide fallback for mb_funcs when mbstring ext isn't loaded --- src/Helper/InflectsString.php | 28 ++++++++++++++++++++++++++++ src/Output/Table.php | 12 +++++------- src/Output/Writer.php | 6 ++++-- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/Helper/InflectsString.php b/src/Helper/InflectsString.php index 299375a..f458620 100644 --- a/src/Helper/InflectsString.php +++ b/src/Helper/InflectsString.php @@ -12,7 +12,11 @@ namespace Ahc\Cli\Helper; use function lcfirst; +use function mb_strwidth; +use function mb_substr; use function str_replace; +use function strlen; +use function substr; use function trim; use function ucwords; @@ -47,4 +51,28 @@ public function toWords(string $string): string return ucwords($words); } + + /** + * Return width of string + */ + public function strwidth(string $string): int + { + if (function_exists('mb_strwidth')) { + return mb_strwidth($string); + } + + return strlen($string); + } + + /** + * Get part of string + */ + public function substr(string $string, int $start, ?int $length = null): string + { + if (function_exists('mb_substr')) { + return mb_substr($string, $start, $length); + } + + return substr($string, $start, $length); + } } diff --git a/src/Output/Table.php b/src/Output/Table.php index eb306a0..ed4a872 100644 --- a/src/Output/Table.php +++ b/src/Output/Table.php @@ -23,8 +23,6 @@ use function implode; use function is_array; use function max; -use function mb_strwidth; -use function mb_substr; use function reset; use function sprintf; use function str_repeat; @@ -84,7 +82,7 @@ public function render(array $rows, array $styles = []): string $word = str_replace($matches[1], '', $text); $word = preg_replace('/\\x1b\[0m/', '', $word); - $size += mb_strwidth($text) - mb_strwidth($word); + $size += $this->strwidth($text) - $this->strwidth($word); } $parts[] = "$start " . $this->strPad($text, $size, ' ') . " $end"; @@ -131,8 +129,8 @@ protected function normalize(array $rows): array return $col; }, $cols); - $span = array_map('mb_strwidth', $cols); - $span[] = mb_strwidth($col); + $span = array_map([$this, 'strwidth'], $cols); + $span[] = $this->strwidth($col); $value = max($span); } @@ -182,10 +180,10 @@ protected function parseStyle(array|callable $style, $val, array $row, array $ta */ protected function strPad(string $string, int $length, string $pad_string = ' '): string { - if (1 > $paddingRequired = $length - mb_strwidth($string)) { + if (1 > $paddingRequired = $length - $this->strwidth($string)) { return $string; } - return $string . mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired); + return $string . $this->substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired); } } diff --git a/src/Output/Writer.php b/src/Output/Writer.php index a187357..7d955c9 100644 --- a/src/Output/Writer.php +++ b/src/Output/Writer.php @@ -11,6 +11,7 @@ namespace Ahc\Cli\Output; +use Ahc\Cli\Helper\InflectsString; use Ahc\Cli\Helper\Terminal; use function fopen; @@ -19,7 +20,6 @@ use function method_exists; use function str_repeat; use function stripos; -use function strlen; use function strpos; use function ucfirst; @@ -190,6 +190,8 @@ */ class Writer { + use InflectsString; + /** @var resource Output file handle */ protected $stream; @@ -339,7 +341,7 @@ public function justify(string $first, ?string $second = null, array $options = $second = (string) $second; $terminalWidth = $this->terminal->width() ?? 80; - $dashWidth = $terminalWidth - (mb_strwidth($first) + mb_strwidth($second)); + $dashWidth = $terminalWidth - ($this->strwidth($first) + $this->strwidth($second)); // remove left and right margins because we're going to add 1 space on each side (after/before the text). // if we don't have a second element, we just remove the left margin $dashWidth -= $second === '' ? 1 : 2; From fe273ace6676742d517b24d6b8ddea2a6579c581 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Fri, 29 Nov 2024 10:46:34 +0100 Subject: [PATCH 4/4] skip test that need mbstring extension --- tests/Output/TableTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Output/TableTest.php b/tests/Output/TableTest.php index dbff2ce..51bc634 100644 --- a/tests/Output/TableTest.php +++ b/tests/Output/TableTest.php @@ -642,6 +642,10 @@ public function test_render_with_html_like_tags_in_cell_content(): void public function test_render_with_unicode_characters_in_cell_content(): void { + if (! extension_loaded('mbstring')) { + $this->markTestSkipped('The mbstring extension is not installed. This test will faill without it'); + } + $rows = [ ['name' => 'François', 'greeting' => 'Bonjour'], ['name' => 'Jürgen', 'greeting' => 'Guten Tag'],