Skip to content

Commit 839dc26

Browse files
author
Robin Chalas
committed
feature symfony#28373 [Console] Support max column width in Table (ro0NL)
This PR was squashed before being merged into the 4.2-dev branch (closes symfony#28373). Discussion ---------- [Console] Support max column width in Table | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no <!-- see https://symfony.com/bc --> | Deprecations? | no | Tests pass? | yes <!-- please add some, will be required by reviewers --> | Fixed tickets | symfony#22156, symfony#27832 | License | MIT | Doc PR | symfony/symfony-docs#10300 Continuation of symfony#22225 to better preserve spaces (which preserves background colors), using `wordwrap` it caused some issues. Also the wrapping was plain wrong by not taking the current line length into account. While at it, it comes with `Table` integration :) Given ```php $table = new Table($output); $table->setColumnMaxWidth(0, 2); $table->setRow(0, ['pre <error>foo bar baz</error> post']); $table->render(); $table = new Table($output); $table->setColumnMaxWidth(0, 3); $table->setRow(0, ['pre <error>foo bar baz</error> post']); $table->render(); $table = new Table($output); $table->setColumnMaxWidth(0, 4); $table->setRow(0, ['pre <error>foo bar baz</error> post']); $table->render(); ``` ![image](https://user-images.githubusercontent.com/1047696/45101516-f19b5880-b12b-11e8-825f-6a1d84f68f47.png) Commits ------- 175f68f [Console] Support max column width in Table
2 parents eb71aaf + 175f68f commit 839dc26

File tree

4 files changed

+99
-15
lines changed

4 files changed

+99
-15
lines changed

src/Symfony/Component/Console/Formatter/OutputFormatter.php

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public function formatAndWrap(string $message, int $width)
142142
$offset = 0;
143143
$output = '';
144144
$tagRegex = '[a-z][a-z0-9,_=;-]*+';
145+
$currentLineLength = 0;
145146
preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, PREG_OFFSET_CAPTURE);
146147
foreach ($matches[0] as $i => $match) {
147148
$pos = $match[1];
@@ -152,7 +153,7 @@ public function formatAndWrap(string $message, int $width)
152153
}
153154

154155
// add the text up to the next tag
155-
$output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width);
156+
$output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
156157
$offset = $pos + \strlen($text);
157158

158159
// opening tag?
@@ -166,15 +167,15 @@ public function formatAndWrap(string $message, int $width)
166167
// </>
167168
$this->styleStack->pop();
168169
} elseif (false === $style = $this->createStyleFromString(strtolower($tag))) {
169-
$output .= $this->applyCurrentStyle($text, $output, $width);
170+
$output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength);
170171
} elseif ($open) {
171172
$this->styleStack->push($style);
172173
} else {
173174
$this->styleStack->pop($style);
174175
}
175176
}
176177

177-
$output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width);
178+
$output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
178179

179180
if (false !== strpos($output, "\0")) {
180181
return strtr($output, array("\0" => '\\', '\\<' => '<'));
@@ -231,24 +232,46 @@ private function createStyleFromString(string $string)
231232
/**
232233
* Applies current style from stack to text, if must be applied.
233234
*/
234-
private function applyCurrentStyle(string $text, string $current, int $width): string
235+
private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string
235236
{
236237
if ('' === $text) {
237238
return '';
238239
}
239240

240-
if ($width) {
241-
if ('' !== $current) {
242-
$text = ltrim($text);
243-
}
241+
if (!$width) {
242+
return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text;
243+
}
244+
245+
if (!$currentLineLength && '' !== $current) {
246+
$text = ltrim($text);
247+
}
248+
249+
if ($currentLineLength) {
250+
$prefix = substr($text, 0, $i = $width - $currentLineLength)."\n";
251+
$text = substr($text, $i);
252+
} else {
253+
$prefix = '';
254+
}
255+
256+
preg_match('~(\\n)$~', $text, $matches);
257+
$text = $prefix.preg_replace('~([^\\n]{'.$width.'})\\ *~', "\$1\n", $text);
258+
$text = rtrim($text, "\n").($matches[1] ?? '');
244259

245-
$text = wordwrap($text, $width, "\n", true);
260+
if (!$currentLineLength && '' !== $current && "\n" !== substr($current, -1)) {
261+
$text = "\n".$text;
262+
}
263+
264+
$lines = explode("\n", $text);
265+
if ($width === $currentLineLength = \strlen(end($lines))) {
266+
$currentLineLength = 0;
267+
}
246268

247-
if ('' !== $current && "\n" !== substr($current, -1)) {
248-
$text = "\n".$text;
269+
if ($this->isDecorated()) {
270+
foreach ($lines as $i => $line) {
271+
$lines[$i] = $this->styleStack->getCurrent()->apply($line);
249272
}
250273
}
251274

252-
return $this->isDecorated() && \strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text;
275+
return implode("\n", $lines);
253276
}
254277
}

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Console\Exception\InvalidArgumentException;
1515
use Symfony\Component\Console\Exception\RuntimeException;
16+
use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface;
1617
use Symfony\Component\Console\Output\ConsoleSectionOutput;
1718
use Symfony\Component\Console\Output\OutputInterface;
1819

@@ -80,6 +81,7 @@ class Table
8081
* @var array
8182
*/
8283
private $columnWidths = array();
84+
private $columnMaxWidths = array();
8385

8486
private static $styles;
8587

@@ -222,6 +224,25 @@ public function setColumnWidths(array $widths)
222224
return $this;
223225
}
224226

227+
/**
228+
* Sets the maximum width of a column.
229+
*
230+
* Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while
231+
* formatted strings are preserved.
232+
*
233+
* @return $this
234+
*/
235+
public function setColumnMaxWidth(int $columnIndex, int $width): self
236+
{
237+
if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
238+
throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, \get_class($this->output->getFormatter())));
239+
}
240+
241+
$this->columnMaxWidths[$columnIndex] = $width;
242+
243+
return $this;
244+
}
245+
225246
public function setHeaders(array $headers)
226247
{
227248
$headers = array_values($headers);
@@ -434,7 +455,6 @@ private function renderColumnSeparator($type = self::BORDER_OUTSIDE)
434455
* Example:
435456
*
436457
* | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
437-
*
438458
*/
439459
private function renderRow(array $row, string $cellFormat)
440460
{
@@ -498,12 +518,17 @@ private function calculateNumberOfColumns($rows)
498518

499519
private function buildTableRows($rows)
500520
{
521+
/** @var WrappableOutputFormatterInterface $formatter */
522+
$formatter = $this->output->getFormatter();
501523
$unmergedRows = array();
502524
for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) {
503525
$rows = $this->fillNextRows($rows, $rowKey);
504526

505527
// Remove any new line breaks and replace it with a new line
506528
foreach ($rows[$rowKey] as $column => $cell) {
529+
if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($formatter, $cell) > $this->columnMaxWidths[$column]) {
530+
$cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column]);
531+
}
507532
if (!strstr($cell, "\n")) {
508533
continue;
509534
}
@@ -711,8 +736,9 @@ private function getCellWidth(array $row, int $column): int
711736
}
712737

713738
$columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0;
739+
$cellWidth = max($cellWidth, $columnWidth);
714740

715-
return max($cellWidth, $columnWidth);
741+
return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
716742
}
717743

718744
/**

src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,11 +327,19 @@ public function testFormatAndWrap()
327327
{
328328
$formatter = new OutputFormatter(true);
329329

330-
$this->assertSame("pre\n\033[37;41mfoo\nbar\nbaz\033[39;49m\npos\nt", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 3));
330+
$this->assertSame("fo\no\e[37;41mb\e[39;49m\n\e[37;41mar\e[39;49m\nba\nz", $formatter->formatAndWrap('foo<error>bar</error> baz', 2));
331+
$this->assertSame("pr\ne \e[37;41m\e[39;49m\n\e[37;41mfo\e[39;49m\n\e[37;41mo \e[39;49m\n\e[37;41mba\e[39;49m\n\e[37;41mr \e[39;49m\n\e[37;41mba\e[39;49m\n\e[37;41mz\e[39;49m \npo\nst", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 2));
332+
$this->assertSame("pre\e[37;41m\e[39;49m\n\e[37;41mfoo\e[39;49m\n\e[37;41mbar\e[39;49m\n\e[37;41mbaz\e[39;49m\npos\nt", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 3));
333+
$this->assertSame("pre \e[37;41m\e[39;49m\n\e[37;41mfoo \e[39;49m\n\e[37;41mbar \e[39;49m\n\e[37;41mbaz\e[39;49m \npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 4));
334+
$this->assertSame("pre \e[37;41mf\e[39;49m\n\e[37;41moo ba\e[39;49m\n\e[37;41mr baz\e[39;49m\npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 5));
331335

332336
$formatter = new OutputFormatter();
333337

338+
$this->assertSame("fo\nob\nar\nba\nz", $formatter->formatAndWrap('foo<error>bar</error> baz', 2));
339+
$this->assertSame("pr\ne \nfo\no \nba\nr \nba\nz \npo\nst", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 2));
334340
$this->assertSame("pre\nfoo\nbar\nbaz\npos\nt", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 3));
341+
$this->assertSame("pre \nfoo \nbar \nbaz \npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 4));
342+
$this->assertSame("pre f\noo ba\nr baz\npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 5));
335343
}
336344
}
337345

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,33 @@ public function renderSetTitle()
10501050
);
10511051
}
10521052

1053+
public function testColumnMaxWidths()
1054+
{
1055+
$table = new Table($output = $this->getOutputStream());
1056+
$table
1057+
->setRows(array(
1058+
array('Divine Comedy', 'A Tale of Two Cities', 'The Lord of the Rings', 'And Then There Were None'),
1059+
))
1060+
->setColumnMaxWidth(1, 5)
1061+
->setColumnMaxWidth(2, 10)
1062+
->setColumnMaxWidth(3, 15);
1063+
1064+
$table->render();
1065+
1066+
$expected =
1067+
<<<TABLE
1068+
+---------------+-------+------------+-----------------+
1069+
| Divine Comedy | A Tal | The Lord o | And Then There |
1070+
| | e of | f the Ring | Were None |
1071+
| | Two C | s | |
1072+
| | ities | | |
1073+
+---------------+-------+------------+-----------------+
1074+
1075+
TABLE;
1076+
1077+
$this->assertEquals($expected, $this->getOutputContent($output));
1078+
}
1079+
10531080
protected function getOutputStream($decorated = false)
10541081
{
10551082
return new StreamOutput($this->stream, StreamOutput::VERBOSITY_NORMAL, $decorated);

0 commit comments

Comments
 (0)