From a52f41d4142e882ba628832d9a2666b3e62d6e65 Mon Sep 17 00:00:00 2001 From: Daisuke Ohata Date: Tue, 15 Apr 2014 19:04:31 +0900 Subject: [PATCH] [Console]Improve formatter for double-width character --- src/Symfony/Component/Console/Application.php | 91 +++++++++++++------ .../Component/Console/Helper/Helper.php | 6 +- .../Console/Tests/ApplicationTest.php | 27 ++++++ ...plication_renderexception_doublewidth1.txt | 11 +++ ..._renderexception_doublewidth1decorated.txt | 11 +++ ...plication_renderexception_doublewidth2.txt | 12 +++ .../Tests/Helper/FormatterHelperTest.php | 15 +++ 7 files changed, 143 insertions(+), 30 deletions(-) create mode 100644 src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1.txt create mode 100644 src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt create mode 100644 src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth2.txt diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 0753e19ed9..2833bd23de 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -99,7 +99,7 @@ class Application * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * - * @return integer 0 if everything went fine, or an error code + * @return int 0 if everything went fine, or an error code * * @throws \Exception When doRun returns Exception * @@ -159,7 +159,7 @@ class Application * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * - * @return integer 0 if everything went fine, or an error code + * @return int 0 if everything went fine, or an error code */ public function doRun(InputInterface $input, OutputInterface $output) { @@ -270,7 +270,7 @@ class Application /** * Sets whether to catch exceptions or not during commands execution. * - * @param bool $boolean Whether to catch exceptions or not during commands execution + * @param bool $boolean Whether to catch exceptions or not during commands execution * * @api */ @@ -282,7 +282,7 @@ class Application /** * Sets whether to automatically exit after a command execution or not. * - * @param bool $boolean Whether to automatically exit after a command execution or not + * @param bool $boolean Whether to automatically exit after a command execution or not * * @api */ @@ -449,7 +449,7 @@ class Application * * @param string $name The command name or alias * - * @return Boolean true if the command exists, false otherwise + * @return bool true if the command exists, false otherwise * * @api */ @@ -674,8 +674,8 @@ class Application /** * Returns a text representation of the Application. * - * @param string $namespace An optional namespace name - * @param bool $raw Whether to return raw command list + * @param string $namespace An optional namespace name + * @param bool $raw Whether to return raw command list * * @return string A string representing the Application * @@ -691,8 +691,8 @@ class Application /** * Returns an XML representation of the Application. * - * @param string $namespace An optional namespace name - * @param bool $asDom Whether to return a DOM or an XML string + * @param string $namespace An optional namespace name + * @param bool $asDom Whether to return a DOM or an XML string * * @return string|\DOMDocument An XML string representing the Application * @@ -708,34 +708,22 @@ class Application /** * Renders a caught exception. * - * @param \Exception $e An exception instance + * @param \Exception $e An exception instance * @param OutputInterface $output An OutputInterface instance */ public function renderException($e, $output) { - $strlen = function ($string) { - if (!function_exists('mb_strlen')) { - return strlen($string); - } - - if (false === $encoding = mb_detect_encoding($string)) { - return strlen($string); - } - - return mb_strlen($string, $encoding); - }; - do { $title = sprintf(' [%s] ', get_class($e)); - $len = $strlen($title); + $len = $this->stringWidth($title); // HHVM only accepts 32 bits integer in str_split, even when PHP_INT_MAX is a 64 bit integer: https://github.com/facebook/hhvm/issues/1327 $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : (defined('HHVM_VERSION') ? 1 << 31 : PHP_INT_MAX); $formatter = $output->getFormatter(); $lines = array(); foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { - foreach (str_split($line, $width - 4) as $line) { + foreach ($this->splitStringByWidth($line, $width - 4) as $line) { // pre-format lines to get the right string length - $lineLength = $strlen(preg_replace('/\[[^m]*m/', '', $formatter->format($line))) + 4; + $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $formatter->format($line))) + 4; $lines[] = array($line, $lineLength); $len = max($lineLength, $len); @@ -744,7 +732,7 @@ class Application $messages = array('', ''); $messages[] = $emptyLine = $formatter->format(sprintf('%s', str_repeat(' ', $len))); - $messages[] = $formatter->format(sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $strlen($title))))); + $messages[] = $formatter->format(sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title))))); foreach ($lines as $line) { $messages[] = $formatter->format(sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1]))); } @@ -890,7 +878,7 @@ class Application * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * - * @return integer 0 if everything went fine, or an error code + * @return int 0 if everything went fine, or an error code */ protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) { @@ -1125,4 +1113,53 @@ class Application return array_keys($alternatives); } + + private function stringWidth($string) + { + if (!function_exists('mb_strwidth')) { + return strlen($string); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return strlen($string); + } + + return mb_strwidth($string, $encoding); + } + + private function splitStringByWidth($string, $width) + { + // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. + // additionally, array_slice() is not enough as some character has doubled width. + // we need a function to split string not by character count but by string width + + if (!function_exists('mb_strwidth')) { + return str_split($string, $width); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return str_split($string, $width); + } + + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = array(); + $line = ''; + foreach (preg_split('//u', $utf8String) as $char) { + // test if $char could be appended to current line + if (mb_strwidth($line.$char) <= $width) { + $line .= $char; + continue; + } + // if not, push current line to array and make new line + $lines[] = str_pad($line, $width); + $line = $char; + } + if (strlen($line)) { + $lines[] = count($lines) ? str_pad($line, $width) : $line; + } + + mb_convert_variables($encoding, 'utf8', $lines); + + return $lines; + } } diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 534b9f4319..b2a8389fa0 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -45,11 +45,11 @@ abstract class Helper implements HelperInterface * * @param string $string The string to check its length * - * @return integer The length of the string + * @return int The length of the string */ protected function strlen($string) { - if (!function_exists('mb_strlen')) { + if (!function_exists('mb_strwidth')) { return strlen($string); } @@ -57,6 +57,6 @@ abstract class Helper implements HelperInterface return strlen($string); } - return mb_strlen($string, $encoding); + return mb_strwidth($string, $encoding); } } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 10dcb30951..0965d24ad5 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -469,6 +469,33 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal'); } + public function testRenderExceptionWithDoubleWidthCharacters() + { + $application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth')); + $application->setAutoExit(false); + $application->expects($this->any()) + ->method('getTerminalWidth') + ->will($this->returnValue(120)); + $application->register('foo')->setCode(function () {throw new \Exception('エラーメッセージ');}); + $tester = new ApplicationTester($application); + + $tester->run(array('command' => 'foo'), array('decorated' => false)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getDisplay(true), '->renderException() renderes a pretty exceptions with previous exceptions'); + + $tester->run(array('command' => 'foo'), array('decorated' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getDisplay(true), '->renderException() renderes a pretty exceptions with previous exceptions'); + + $application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth')); + $application->setAutoExit(false); + $application->expects($this->any()) + ->method('getTerminalWidth') + ->will($this->returnValue(32)); + $application->register('foo')->setCode(function () {throw new \Exception('コマンドの実行中にエラーが発生しました。');}); + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'foo'), array('decorated' => false)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal'); + } + public function testRun() { $application = new Application(); diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1.txt new file mode 100644 index 0000000000..6a98660364 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1.txt @@ -0,0 +1,11 @@ + + + + [Exception] + エラーメッセージ + + + +foo + + diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt new file mode 100644 index 0000000000..c68a60f564 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt @@ -0,0 +1,11 @@ + + +  + [Exception]  + エラーメッセージ  +  + + +foo + + diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth2.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth2.txt new file mode 100644 index 0000000000..545cd7b0b4 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth2.txt @@ -0,0 +1,12 @@ + + + + [Exception] + コマンドの実行中にエラーが + 発生しました。 + + + +foo + + diff --git a/src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php index 87a45e1d44..34d70d8fc2 100644 --- a/src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php @@ -69,6 +69,21 @@ class FormatterHelperTest extends \PHPUnit_Framework_TestCase ); } + public function testFormatBlockWithDoubleWidthDiacriticLetters() + { + if (!extension_loaded('mbstring')) { + $this->markTestSkipped('This test requires mbstring to work.'); + } + $formatter = new FormatterHelper(); + $this->assertEquals( + ' '."\n" . + ' 表示するテキスト '."\n" . + ' ', + $formatter->formatBlock('表示するテキスト', 'error', true), + '::formatBlock() formats a message in a block' + ); + } + public function testFormatBlockLGEscaping() { $formatter = new FormatterHelper();