bug #10714 [Console]Improve formatter for double-width character (denkiryokuhatsuden)
This PR was squashed before being merged into the 2.3 branch (closes #10714).
Discussion
----------
[Console]Improve formatter for double-width character
| Q | A
| ------------- | ---
| Bug fix? | yes
| New feature? | no
| BC breaks? | yes for one that expecting current broken output
| Deprecations? | no
| Fixed tickets |
| Tests pass? | yes
| License | MIT
| Doc PR |
EDIT: fixed the table above not to remove irrelevant line
As mb_strlen just returns "how many chars in the string",
formatting with double-width character is bit broken.
The test I add is skipped when mbstring extension is not loaded.
I'm afraid if some of you cannot properly display japanese string.
(表示するテキスト just means "Some text to display")
Commits
-------
a52f41d
[Console]Improve formatter for double-width character
This commit is contained in:
commit
a29a60debd
|
@ -99,7 +99,7 @@ class Application
|
|||
* @param InputInterface $input An Input instance
|
||||
* @param OutputInterface $output An Output instance
|
||||
*
|
||||
* @return int 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 int 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 bool 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,26 +708,16 @@ 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);
|
||||
|
||||
$width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX;
|
||||
// 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
|
||||
if (defined('HHVM_VERSION') && $width > 1 << 31) {
|
||||
|
@ -736,9 +726,9 @@ class Application
|
|||
$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);
|
||||
|
@ -747,7 +737,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])));
|
||||
}
|
||||
|
@ -893,7 +883,7 @@ class Application
|
|||
* @param InputInterface $input An Input instance
|
||||
* @param OutputInterface $output An Output instance
|
||||
*
|
||||
* @return int 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)
|
||||
{
|
||||
|
@ -1128,4 +1118,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ abstract class Helper implements HelperInterface
|
|||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
|
||||
|
||||
[Exception]
|
||||
エラーメッセージ
|
||||
|
||||
|
||||
|
||||
foo
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
|
||||
[37;41m [0m
|
||||
[37;41m [Exception] [0m
|
||||
[37;41m エラーメッセージ [0m
|
||||
[37;41m [0m
|
||||
|
||||
|
||||
[32mfoo[0m
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
|
||||
|
||||
[Exception]
|
||||
コマンドの実行中にエラーが
|
||||
発生しました。
|
||||
|
||||
|
||||
|
||||
foo
|
||||
|
||||
|
|
@ -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();
|
||||
|
|
Reference in New Issue