[Console]Improve formatter for double-width character

This commit is contained in:
Daisuke Ohata 2014-04-15 19:04:31 +09:00 committed by Fabien Potencier
parent c2e134fbb9
commit a52f41d414
7 changed files with 143 additions and 30 deletions

View File

@ -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('<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])));
}
@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -0,0 +1,11 @@
[Exception]
エラーメッセージ
foo

View File

@ -0,0 +1,11 @@
 
 [Exception] 
 エラーメッセージ 
 
foo

View File

@ -0,0 +1,12 @@
[Exception]
コマンドの実行中にエラーが
発生しました。
foo

View File

@ -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(
'<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();