Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[Console]Improve formatter for double-width character
  • Loading branch information
denkiryokuhatsuden authored and fabpot committed May 12, 2014
1 parent c2e134f commit a52f41d
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 30 deletions.
91 changes: 64 additions & 27 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -99,7 +99,7 @@ public function setDispatcher(EventDispatcherInterface $dispatcher)
* @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
*
Expand Down Expand Up @@ -159,7 +159,7 @@ public function run(InputInterface $input = null, OutputInterface $output = null
* @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)
{
Expand Down Expand Up @@ -270,7 +270,7 @@ public function getHelp()
/**
* 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
*/
Expand All @@ -282,7 +282,7 @@ public function setCatchExceptions($boolean)
/**
* 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
*/
Expand Down Expand Up @@ -449,7 +449,7 @@ public function get($name)
*
* @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
*/
Expand Down Expand Up @@ -674,8 +674,8 @@ public static function getAbbreviations($names)
/**
* 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
*
Expand All @@ -691,8 +691,8 @@ public function asText($namespace = null, $raw = false)
/**
* 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
*
Expand All @@ -708,34 +708,22 @@ public function asXml($namespace = null, $asDom = false)
/**
* 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);
Expand All @@ -744,7 +732,7 @@ public function renderException($e, $output)

$messages = array('', '');
$messages[] = $emptyLine = $formatter->format(sprintf('<error>%s</error>', str_repeat(' ', $len)));
$messages[] = $formatter->format(sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - $strlen($title)))));
$messages[] = $formatter->format(sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))));
foreach ($lines as $line) {
$messages[] = $formatter->format(sprintf('<error> %s %s</error>', $line[0], str_repeat(' ', $len - $line[1])));
}
Expand Down Expand Up @@ -890,7 +878,7 @@ protected function configureIO(InputInterface $input, OutputInterface $output)
* @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)
{
Expand Down Expand Up @@ -1125,4 +1113,53 @@ private function findAlternatives($name, $collection, $abbrevs, $callback = null

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;
}
}
6 changes: 3 additions & 3 deletions src/Symfony/Component/Console/Helper/Helper.php
Expand Up @@ -45,18 +45,18 @@ public function getHelperSet()
*
* @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);
}

if (false === $encoding = mb_detect_encoding($string)) {
return strlen($string);
}

return mb_strlen($string, $encoding);
return mb_strwidth($string, $encoding);
}
}
27 changes: 27 additions & 0 deletions src/Symfony/Component/Console/Tests/ApplicationTest.php
Expand Up @@ -469,6 +469,33 @@ public function testRenderException()
$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();
Expand Down
@@ -0,0 +1,11 @@



[Exception]
エラーメッセージ



foo


@@ -0,0 +1,11 @@


 
 [Exception] 
 エラーメッセージ 
 


foo


@@ -0,0 +1,12 @@



[Exception]
コマンドの実行中にエラーが
発生しました。



foo


15 changes: 15 additions & 0 deletions src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php
Expand Up @@ -69,6 +69,21 @@ public function testFormatBlockWithDiacriticLetters()
);
}

public function testFormatBlockWithDoubleWidthDiacriticLetters()
{
if (!extension_loaded('mbstring')) {
$this->markTestSkipped('This test requires mbstring to work.');
}
$formatter = new FormatterHelper();
$this->assertEquals(
'<error> </error>'."\n" .
'<error> 表示するテキスト </error>'."\n" .
'<error> </error>',
$formatter->formatBlock('表示するテキスト', 'error', true),
'::formatBlock() formats a message in a block'
);
}

public function testFormatBlockLGEscaping()
{
$formatter = new FormatterHelper();
Expand Down

0 comments on commit a52f41d

Please sign in to comment.