Skip to content

Commit a52f41d

Browse files
denkiryokuhatsudenfabpot
authored andcommitted
[Console]Improve formatter for double-width character
1 parent c2e134f commit a52f41d

File tree

7 files changed

+143
-30
lines changed

7 files changed

+143
-30
lines changed

src/Symfony/Component/Console/Application.php

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public function setDispatcher(EventDispatcherInterface $dispatcher)
9999
* @param InputInterface $input An Input instance
100100
* @param OutputInterface $output An Output instance
101101
*
102-
* @return integer 0 if everything went fine, or an error code
102+
* @return int 0 if everything went fine, or an error code
103103
*
104104
* @throws \Exception When doRun returns Exception
105105
*
@@ -159,7 +159,7 @@ public function run(InputInterface $input = null, OutputInterface $output = null
159159
* @param InputInterface $input An Input instance
160160
* @param OutputInterface $output An Output instance
161161
*
162-
* @return integer 0 if everything went fine, or an error code
162+
* @return int 0 if everything went fine, or an error code
163163
*/
164164
public function doRun(InputInterface $input, OutputInterface $output)
165165
{
@@ -270,7 +270,7 @@ public function getHelp()
270270
/**
271271
* Sets whether to catch exceptions or not during commands execution.
272272
*
273-
* @param bool $boolean Whether to catch exceptions or not during commands execution
273+
* @param bool $boolean Whether to catch exceptions or not during commands execution
274274
*
275275
* @api
276276
*/
@@ -282,7 +282,7 @@ public function setCatchExceptions($boolean)
282282
/**
283283
* Sets whether to automatically exit after a command execution or not.
284284
*
285-
* @param bool $boolean Whether to automatically exit after a command execution or not
285+
* @param bool $boolean Whether to automatically exit after a command execution or not
286286
*
287287
* @api
288288
*/
@@ -449,7 +449,7 @@ public function get($name)
449449
*
450450
* @param string $name The command name or alias
451451
*
452-
* @return Boolean true if the command exists, false otherwise
452+
* @return bool true if the command exists, false otherwise
453453
*
454454
* @api
455455
*/
@@ -674,8 +674,8 @@ public static function getAbbreviations($names)
674674
/**
675675
* Returns a text representation of the Application.
676676
*
677-
* @param string $namespace An optional namespace name
678-
* @param bool $raw Whether to return raw command list
677+
* @param string $namespace An optional namespace name
678+
* @param bool $raw Whether to return raw command list
679679
*
680680
* @return string A string representing the Application
681681
*
@@ -691,8 +691,8 @@ public function asText($namespace = null, $raw = false)
691691
/**
692692
* Returns an XML representation of the Application.
693693
*
694-
* @param string $namespace An optional namespace name
695-
* @param bool $asDom Whether to return a DOM or an XML string
694+
* @param string $namespace An optional namespace name
695+
* @param bool $asDom Whether to return a DOM or an XML string
696696
*
697697
* @return string|\DOMDocument An XML string representing the Application
698698
*
@@ -708,34 +708,22 @@ public function asXml($namespace = null, $asDom = false)
708708
/**
709709
* Renders a caught exception.
710710
*
711-
* @param \Exception $e An exception instance
711+
* @param \Exception $e An exception instance
712712
* @param OutputInterface $output An OutputInterface instance
713713
*/
714714
public function renderException($e, $output)
715715
{
716-
$strlen = function ($string) {
717-
if (!function_exists('mb_strlen')) {
718-
return strlen($string);
719-
}
720-
721-
if (false === $encoding = mb_detect_encoding($string)) {
722-
return strlen($string);
723-
}
724-
725-
return mb_strlen($string, $encoding);
726-
};
727-
728716
do {
729717
$title = sprintf(' [%s] ', get_class($e));
730-
$len = $strlen($title);
718+
$len = $this->stringWidth($title);
731719
// 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
732720
$width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : (defined('HHVM_VERSION') ? 1 << 31 : PHP_INT_MAX);
733721
$formatter = $output->getFormatter();
734722
$lines = array();
735723
foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) {
736-
foreach (str_split($line, $width - 4) as $line) {
724+
foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
737725
// pre-format lines to get the right string length
738-
$lineLength = $strlen(preg_replace('/\[[^m]*m/', '', $formatter->format($line))) + 4;
726+
$lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $formatter->format($line))) + 4;
739727
$lines[] = array($line, $lineLength);
740728

741729
$len = max($lineLength, $len);
@@ -744,7 +732,7 @@ public function renderException($e, $output)
744732

745733
$messages = array('', '');
746734
$messages[] = $emptyLine = $formatter->format(sprintf('<error>%s</error>', str_repeat(' ', $len)));
747-
$messages[] = $formatter->format(sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - $strlen($title)))));
735+
$messages[] = $formatter->format(sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))));
748736
foreach ($lines as $line) {
749737
$messages[] = $formatter->format(sprintf('<error> %s %s</error>', $line[0], str_repeat(' ', $len - $line[1])));
750738
}
@@ -890,7 +878,7 @@ protected function configureIO(InputInterface $input, OutputInterface $output)
890878
* @param InputInterface $input An Input instance
891879
* @param OutputInterface $output An Output instance
892880
*
893-
* @return integer 0 if everything went fine, or an error code
881+
* @return int 0 if everything went fine, or an error code
894882
*/
895883
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
896884
{
@@ -1125,4 +1113,53 @@ private function findAlternatives($name, $collection, $abbrevs, $callback = null
11251113

11261114
return array_keys($alternatives);
11271115
}
1116+
1117+
private function stringWidth($string)
1118+
{
1119+
if (!function_exists('mb_strwidth')) {
1120+
return strlen($string);
1121+
}
1122+
1123+
if (false === $encoding = mb_detect_encoding($string)) {
1124+
return strlen($string);
1125+
}
1126+
1127+
return mb_strwidth($string, $encoding);
1128+
}
1129+
1130+
private function splitStringByWidth($string, $width)
1131+
{
1132+
// str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
1133+
// additionally, array_slice() is not enough as some character has doubled width.
1134+
// we need a function to split string not by character count but by string width
1135+
1136+
if (!function_exists('mb_strwidth')) {
1137+
return str_split($string, $width);
1138+
}
1139+
1140+
if (false === $encoding = mb_detect_encoding($string)) {
1141+
return str_split($string, $width);
1142+
}
1143+
1144+
$utf8String = mb_convert_encoding($string, 'utf8', $encoding);
1145+
$lines = array();
1146+
$line = '';
1147+
foreach (preg_split('//u', $utf8String) as $char) {
1148+
// test if $char could be appended to current line
1149+
if (mb_strwidth($line.$char) <= $width) {
1150+
$line .= $char;
1151+
continue;
1152+
}
1153+
// if not, push current line to array and make new line
1154+
$lines[] = str_pad($line, $width);
1155+
$line = $char;
1156+
}
1157+
if (strlen($line)) {
1158+
$lines[] = count($lines) ? str_pad($line, $width) : $line;
1159+
}
1160+
1161+
mb_convert_variables($encoding, 'utf8', $lines);
1162+
1163+
return $lines;
1164+
}
11281165
}

src/Symfony/Component/Console/Helper/Helper.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,18 @@ public function getHelperSet()
4545
*
4646
* @param string $string The string to check its length
4747
*
48-
* @return integer The length of the string
48+
* @return int The length of the string
4949
*/
5050
protected function strlen($string)
5151
{
52-
if (!function_exists('mb_strlen')) {
52+
if (!function_exists('mb_strwidth')) {
5353
return strlen($string);
5454
}
5555

5656
if (false === $encoding = mb_detect_encoding($string)) {
5757
return strlen($string);
5858
}
5959

60-
return mb_strlen($string, $encoding);
60+
return mb_strwidth($string, $encoding);
6161
}
6262
}

src/Symfony/Component/Console/Tests/ApplicationTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,33 @@ public function testRenderException()
469469
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal');
470470
}
471471

472+
public function testRenderExceptionWithDoubleWidthCharacters()
473+
{
474+
$application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth'));
475+
$application->setAutoExit(false);
476+
$application->expects($this->any())
477+
->method('getTerminalWidth')
478+
->will($this->returnValue(120));
479+
$application->register('foo')->setCode(function () {throw new \Exception('エラーメッセージ');});
480+
$tester = new ApplicationTester($application);
481+
482+
$tester->run(array('command' => 'foo'), array('decorated' => false));
483+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getDisplay(true), '->renderException() renderes a pretty exceptions with previous exceptions');
484+
485+
$tester->run(array('command' => 'foo'), array('decorated' => true));
486+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getDisplay(true), '->renderException() renderes a pretty exceptions with previous exceptions');
487+
488+
$application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth'));
489+
$application->setAutoExit(false);
490+
$application->expects($this->any())
491+
->method('getTerminalWidth')
492+
->will($this->returnValue(32));
493+
$application->register('foo')->setCode(function () {throw new \Exception('コマンドの実行中にエラーが発生しました。');});
494+
$tester = new ApplicationTester($application);
495+
$tester->run(array('command' => 'foo'), array('decorated' => false));
496+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal');
497+
}
498+
472499
public function testRun()
473500
{
474501
$application = new Application();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
3+
4+
[Exception]
5+
エラーメッセージ
6+
7+
8+
9+
foo
10+
11+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
3+
 
4+
 [Exception] 
5+
 エラーメッセージ 
6+
 
7+
8+
9+
foo
10+
11+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
3+
4+
[Exception]
5+
コマンドの実行中にエラーが
6+
発生しました。
7+
8+
9+
10+
foo
11+
12+

src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@ public function testFormatBlockWithDiacriticLetters()
6969
);
7070
}
7171

72+
public function testFormatBlockWithDoubleWidthDiacriticLetters()
73+
{
74+
if (!extension_loaded('mbstring')) {
75+
$this->markTestSkipped('This test requires mbstring to work.');
76+
}
77+
$formatter = new FormatterHelper();
78+
$this->assertEquals(
79+
'<error> </error>'."\n" .
80+
'<error> 表示するテキスト </error>'."\n" .
81+
'<error> </error>',
82+
$formatter->formatBlock('表示するテキスト', 'error', true),
83+
'::formatBlock() formats a message in a block'
84+
);
85+
}
86+
7287
public function testFormatBlockLGEscaping()
7388
{
7489
$formatter = new FormatterHelper();

0 commit comments

Comments
 (0)