mirror of
https://github.com/yiisoft/yii2.git
synced 2025-08-26 06:15:19 +08:00
Fix #16831: Fix console Table Widget does not render correctly in combination with ANSI formatting
This commit is contained in:
@ -13,6 +13,7 @@ Yii Framework 2 Change Log
|
||||
- Bug #18308: Fixed `\yii\base\Model::getErrorSummary()` reverse order (DrDeath72)
|
||||
- Bug #18313: Fix multipart form data parse with double quotes (wsaid)
|
||||
- Bug #18317: Additional PHP 8 compatibility fixes (samdark, bizley)
|
||||
- Bug #16831: Fix console Table Widget does not render correctly in combination with ANSI formatting (issidorov, cebe)
|
||||
|
||||
2.0.38 September 14, 2020
|
||||
-------------------------
|
||||
|
@ -243,36 +243,35 @@ class Table extends Widget
|
||||
|
||||
$buffer = '';
|
||||
$arrayPointer = [];
|
||||
$finalChunk = [];
|
||||
$alreadyPrintedCells = [];
|
||||
$renderedChunkTexts = [];
|
||||
for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) {
|
||||
$buffer .= $spanLeft . ' ';
|
||||
foreach ($size as $index => $cellSize) {
|
||||
$cell = isset($row[$index]) ? $row[$index] : null;
|
||||
$prefix = '';
|
||||
$chunk = '';
|
||||
if ($index !== 0) {
|
||||
$buffer .= $spanMiddle . ' ';
|
||||
}
|
||||
if (is_array($cell)) {
|
||||
if (empty($finalChunk[$index])) {
|
||||
$finalChunk[$index] = '';
|
||||
if (empty($renderedChunkTexts[$index])) {
|
||||
$renderedChunkTexts[$index] = '';
|
||||
$start = 0;
|
||||
$prefix = $this->listPrefix;
|
||||
if (!isset($arrayPointer[$index])) {
|
||||
$arrayPointer[$index] = 0;
|
||||
}
|
||||
} else {
|
||||
$start = mb_strwidth($renderedChunkTexts[$index], Yii::$app->charset);
|
||||
}
|
||||
$chunk = $cell[$arrayPointer[$index]];
|
||||
$finalChunk[$index] .= $chunk;
|
||||
if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) {
|
||||
$chunk = Console::ansiColorizedSubstr($cell[$arrayPointer[$index]], $start, $cellSize - 4);
|
||||
$renderedChunkTexts[$index] .= Console::stripAnsiFormat($chunk);
|
||||
$fullChunkText = Console::stripAnsiFormat($cell[$arrayPointer[$index]]);
|
||||
if (isset($cell[$arrayPointer[$index] + 1]) && $renderedChunkTexts[$index] === $fullChunkText) {
|
||||
$arrayPointer[$index]++;
|
||||
$finalChunk[$index] = '';
|
||||
$renderedChunkTexts[$index] = '';
|
||||
}
|
||||
} else {
|
||||
if (!isset($alreadyPrintedCells[$index])) {
|
||||
$chunk = $cell;
|
||||
}
|
||||
$alreadyPrintedCells[$index] = true;
|
||||
$chunk = Console::ansiColorizedSubstr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2);
|
||||
}
|
||||
$chunk = $prefix . $chunk;
|
||||
$repeat = $cellSize - Console::ansiStrwidth($chunk) - 1;
|
||||
@ -346,15 +345,23 @@ class Table extends Widget
|
||||
$totalWidth += $columnWidth;
|
||||
}
|
||||
|
||||
$relativeWidth = $screenWidth / $totalWidth;
|
||||
|
||||
if ($totalWidth > $screenWidth) {
|
||||
$minWidth = 3;
|
||||
$fixWidths = [];
|
||||
$relativeWidth = $screenWidth / $totalWidth;
|
||||
foreach ($this->columnWidths as $j => $width) {
|
||||
$this->columnWidths[$j] = (int) ($width * $relativeWidth);
|
||||
if ($j === count($this->columnWidths)) {
|
||||
$this->columnWidths = $totalWidth;
|
||||
$scaledWidth = (int) ($width * $relativeWidth);
|
||||
if ($scaledWidth < $minWidth) {
|
||||
$fixWidths[$j] = 3;
|
||||
}
|
||||
}
|
||||
|
||||
$totalFixWidth = array_sum($fixWidths);
|
||||
$relativeWidth = ($screenWidth - $totalFixWidth) / ($totalWidth - $totalFixWidth);
|
||||
foreach ($this->columnWidths as $j => $width) {
|
||||
if (!array_key_exists($j, $fixWidths)) {
|
||||
$this->columnWidths[$j] = (int) ($width * $relativeWidth);
|
||||
}
|
||||
$totalWidth -= $this->columnWidths[$j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -331,7 +331,7 @@ class BaseConsole
|
||||
*/
|
||||
public static function stripAnsiFormat($string)
|
||||
{
|
||||
return preg_replace('/\033\[[\d;?]*\w/', '', $string);
|
||||
return preg_replace(self::ansiCodesPattern(), '', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -355,6 +355,67 @@ class BaseConsole
|
||||
return mb_strwidth(static::stripAnsiFormat($string), Yii::$app->charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the portion with ANSI color codes of string specified by the start and length parameters.
|
||||
* If string has color codes, then will be return "TEXT_COLOR + TEXT_STRING + DEFAULT_COLOR",
|
||||
* else will be simple "TEXT_STRING".
|
||||
* @param string $string
|
||||
* @param int $start
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public static function ansiColorizedSubstr($string, $start, $length)
|
||||
{
|
||||
if ($start < 0 || $length <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$textItems = preg_split(self::ansiCodesPattern(), $string);
|
||||
|
||||
preg_match_all(self::ansiCodesPattern(), $string, $colors);
|
||||
$colors = count($colors) ? $colors[0] : [];
|
||||
array_unshift($colors, '');
|
||||
|
||||
$result = '';
|
||||
$curPos = 0;
|
||||
$inRange = false;
|
||||
|
||||
foreach ($textItems as $k => $textItem) {
|
||||
$color = $colors[$k];
|
||||
|
||||
if ($curPos <= $start && $start < $curPos + Console::ansiStrwidth($textItem)) {
|
||||
$text = mb_substr($textItem, $start - $curPos, null, Yii::$app->charset);
|
||||
$inRange = true;
|
||||
} else {
|
||||
$text = $textItem;
|
||||
}
|
||||
|
||||
if ($inRange) {
|
||||
$result .= $color . $text;
|
||||
$diff = $length - Console::ansiStrwidth($result);
|
||||
if ($diff <= 0) {
|
||||
if ($diff < 0) {
|
||||
$result = mb_substr($result, 0, $diff, Yii::$app->charset);
|
||||
}
|
||||
$defaultColor = static::renderColoredString('%n');
|
||||
if ($color && $color != $defaultColor) {
|
||||
$result .= $defaultColor;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$curPos += mb_strlen($textItem, Yii::$app->charset);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function ansiCodesPattern()
|
||||
{
|
||||
return /** @lang PhpRegExp */ '/\033\[[\d;?]*\w/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an ANSI formatted string to HTML.
|
||||
*
|
||||
|
@ -379,4 +379,150 @@ EXPECTED;
|
||||
]);
|
||||
$this->assertEqualsWithoutLE($table, $table);
|
||||
}
|
||||
|
||||
public function testLineBreakTableCell()
|
||||
{
|
||||
$table = new Table();
|
||||
|
||||
$expected = <<<"EXPECTED"
|
||||
╔══════════════════════╗
|
||||
║ test ║
|
||||
╟──────────────────────╢
|
||||
║ AAAAAAAAAAAAAAAAAAAA ║
|
||||
║ BBBBBBBBBBBBBBBBBBBB ║
|
||||
║ CCCCC ║
|
||||
╟──────────────────────╢
|
||||
║ • AAAAAAAAAAAAAAAAAA ║
|
||||
║ BBBBBBB ║
|
||||
║ • CCCCCCCCCCCCCCCCCC ║
|
||||
║ DDDDDDD ║
|
||||
╚══════════════════════╝
|
||||
|
||||
EXPECTED;
|
||||
|
||||
$this->assertEqualsWithoutLE(
|
||||
$expected,
|
||||
$table->setHeaders(['test'])
|
||||
->setRows([
|
||||
['AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBCCCCC'],
|
||||
[[
|
||||
'AAAAAAAAAAAAAAAAAABBBBBBB',
|
||||
'CCCCCCCCCCCCCCCCCCDDDDDDD',
|
||||
]],
|
||||
])
|
||||
->setScreenWidth(25)
|
||||
->run()
|
||||
);
|
||||
}
|
||||
|
||||
public function testColorizedLineBreakTableCell()
|
||||
{
|
||||
$table = new Table();
|
||||
|
||||
$expected = <<<"EXPECTED"
|
||||
╔══════════════════════╗
|
||||
║ test ║
|
||||
╟──────────────────────╢
|
||||
║ \e[33mAAAAAAAAAAAAAAAAAAAA\e[0m ║
|
||||
║ \e[33mBBBBBBBBBBBBBBBBBBBB\e[0m ║
|
||||
║ \e[33mCCCCC\e[0m ║
|
||||
╟──────────────────────╢
|
||||
║ \e[31mAAAAAAAAAAAAAAAAAAAA\e[0m ║
|
||||
║ \e[32mBBBBBBBBBBBBBBBBBBBB\e[0m ║
|
||||
║ \e[34mCCCCC\e[0m ║
|
||||
╟──────────────────────╢
|
||||
║ • \e[31mAAAAAAAAAAAAAAAAAA\e[0m ║
|
||||
║ \e[31mBBBBBBB\e[0m ║
|
||||
║ • \e[33mCCCCCCCCCCCCCCCCCC\e[0m ║
|
||||
║ \e[33mDDDDDDD\e[0m ║
|
||||
╟──────────────────────╢
|
||||
║ • \e[35mAAAAAAAAAAAAAAAAAA\e[0m ║
|
||||
║ \e[31mBBBBBBB\e[0m ║
|
||||
║ • \e[32mCCCCCCCCCCCCCCCCCC\e[0m ║
|
||||
║ \e[34mDDDDDDD\e[0m ║
|
||||
╚══════════════════════╝
|
||||
|
||||
EXPECTED;
|
||||
|
||||
$this->assertEqualsWithoutLE(
|
||||
$expected,
|
||||
$table->setHeaders(['test'])
|
||||
->setRows([
|
||||
[Console::renderColoredString('%yAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBCCCCC%n')],
|
||||
[Console::renderColoredString('%rAAAAAAAAAAAAAAAAAAAA%gBBBBBBBBBBBBBBBBBBBB%bCCCCC%n')],
|
||||
[[
|
||||
Console::renderColoredString('%rAAAAAAAAAAAAAAAAAABBBBBBB%n'),
|
||||
Console::renderColoredString('%yCCCCCCCCCCCCCCCCCCDDDDDDD%n'),
|
||||
]],
|
||||
[[
|
||||
Console::renderColoredString('%mAAAAAAAAAAAAAAAAAA%rBBBBBBB%n'),
|
||||
Console::renderColoredString('%gCCCCCCCCCCCCCCCCCC%bDDDDDDD%n'),
|
||||
]],
|
||||
])
|
||||
->setScreenWidth(25)
|
||||
->run()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $smallString
|
||||
* @dataProvider dataMinimumWidth
|
||||
*/
|
||||
public function testMinimumWidth($smallString)
|
||||
{
|
||||
$bigString = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
|
||||
|
||||
(new Table())
|
||||
->setHeaders(['t1', 't2', ''])
|
||||
->setRows([
|
||||
[$bigString, $bigString, $smallString],
|
||||
])
|
||||
->setScreenWidth(20)
|
||||
->run();
|
||||
|
||||
// Without exceptions
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function dataMinimumWidth()
|
||||
{
|
||||
return [
|
||||
['X'],
|
||||
[''],
|
||||
[['X', 'X', 'X']],
|
||||
[[]],
|
||||
[['']]
|
||||
];
|
||||
}
|
||||
|
||||
public function testTableWithAnsiFormat()
|
||||
{
|
||||
$table = new Table();
|
||||
|
||||
// test fullwidth chars
|
||||
// @see https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms
|
||||
$expected = <<<EXPECTED
|
||||
╔═══════════════╤═══════════════╤═══════════════╗
|
||||
║ test1 │ test2 │ \e[31mtest3\e[0m ║
|
||||
╟───────────────┼───────────────┼───────────────╢
|
||||
║ \e[34mtestcontent11\e[0m │ \e[33mtestcontent12\e[0m │ testcontent13 ║
|
||||
╟───────────────┼───────────────┼───────────────╢
|
||||
║ testcontent21 │ testcontent22 │ • a ║
|
||||
║ │ │ • \e[35mb\e[0m ║
|
||||
║ │ │ • \e[32mc\e[0m ║
|
||||
╚═══════════════╧═══════════════╧═══════════════╝
|
||||
|
||||
EXPECTED;
|
||||
|
||||
$this->assertEqualsWithoutLE($expected, $table->setHeaders(['test1', 'test2', Console::ansiFormat('test3', [Console::FG_RED])])
|
||||
->setRows([
|
||||
[Console::ansiFormat('testcontent11', [Console::FG_BLUE]), Console::ansiFormat('testcontent12', [Console::FG_YELLOW]), 'testcontent13'],
|
||||
['testcontent21', 'testcontent22', [
|
||||
'a',
|
||||
Console::ansiFormat('b', [Console::FG_PURPLE]),
|
||||
Console::ansiFormat('c', [Console::FG_GREEN]),
|
||||
]],
|
||||
])->setScreenWidth(200)->run()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,12 @@ use yii\helpers\BaseConsole;
|
||||
*/
|
||||
class BaseConsoleTest extends TestCase
|
||||
{
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->mockApplication();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
@ -21,9 +27,57 @@ class BaseConsoleTest extends TestCase
|
||||
$actual = BaseConsole::renderColoredString($data);
|
||||
$expected = "\033[33mfoo";
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
|
||||
$actual = BaseConsole::renderColoredString($data, false);
|
||||
$expected = "foo";
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function ansiColorizedSubstr_withoutColors()
|
||||
{
|
||||
$str = 'FooBar';
|
||||
|
||||
$actual = BaseConsole::ansiColorizedSubstr($str, 0, 3);
|
||||
$expected = BaseConsole::renderColoredString('Foo');
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
$actual = BaseConsole::ansiColorizedSubstr($str, 3, 3);
|
||||
$expected = BaseConsole::renderColoredString('Bar');
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
$actual = BaseConsole::ansiColorizedSubstr($str, 1, 4);
|
||||
$expected = BaseConsole::renderColoredString('ooBa');
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider ansiColorizedSubstr_withColors_data
|
||||
* @param $str
|
||||
* @param $start
|
||||
* @param $length
|
||||
* @param $expected
|
||||
*/
|
||||
public function ansiColorizedSubstr_withColors($str, $start, $length, $expected)
|
||||
{
|
||||
$ansiStr = BaseConsole::renderColoredString($str);
|
||||
|
||||
$ansiActual = BaseConsole::ansiColorizedSubstr($ansiStr, $start, $length);
|
||||
$ansiExpected = BaseConsole::renderColoredString($expected);
|
||||
$this->assertEquals($ansiExpected, $ansiActual);
|
||||
}
|
||||
|
||||
public function ansiColorizedSubstr_withColors_data()
|
||||
{
|
||||
return [
|
||||
['%rFoo%gBar%n', 0, 3, '%rFoo%n'],
|
||||
['%rFoo%gBar%n', 3, 3, '%gBar%n'],
|
||||
['%rFoo%gBar%n', 1, 4, '%roo%gBa%n'],
|
||||
['Foo%yBar%nYes', 1, 7, 'oo%yBar%nYe'],
|
||||
['Foo%yBar%nYes', 5, 3, '%yr%nYe'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user