diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatter.php b/src/Symfony/Component/Console/Formatter/OutputFormatter.php index 255f3031d957e..fd3bbd12d30be 100644 --- a/src/Symfony/Component/Console/Formatter/OutputFormatter.php +++ b/src/Symfony/Component/Console/Formatter/OutputFormatter.php @@ -142,6 +142,7 @@ public function formatAndWrap(string $message, int $width) $offset = 0; $output = ''; $tagRegex = '[a-z][a-z0-9,_=;-]*+'; + $currentLineLength = 0; preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[0] as $i => $match) { $pos = $match[1]; @@ -152,7 +153,7 @@ public function formatAndWrap(string $message, int $width) } // add the text up to the next tag - $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width); + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength); $offset = $pos + \strlen($text); // opening tag? @@ -166,7 +167,7 @@ public function formatAndWrap(string $message, int $width) // $this->styleStack->pop(); } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) { - $output .= $this->applyCurrentStyle($text, $output, $width); + $output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength); } elseif ($open) { $this->styleStack->push($style); } else { @@ -174,7 +175,7 @@ public function formatAndWrap(string $message, int $width) } } - $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width); + $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength); if (false !== strpos($output, "\0")) { return strtr($output, array("\0" => '\\', '\\<' => '<')); @@ -231,24 +232,46 @@ private function createStyleFromString(string $string) /** * Applies current style from stack to text, if must be applied. */ - private function applyCurrentStyle(string $text, string $current, int $width): string + private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string { if ('' === $text) { return ''; } - if ($width) { - if ('' !== $current) { - $text = ltrim($text); - } + if (!$width) { + return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text; + } + + if (!$currentLineLength && '' !== $current) { + $text = ltrim($text); + } + + if ($currentLineLength) { + $prefix = substr($text, 0, $i = $width - $currentLineLength)."\n"; + $text = substr($text, $i); + } else { + $prefix = ''; + } + + preg_match('~(\\n)$~', $text, $matches); + $text = $prefix.preg_replace('~([^\\n]{'.$width.'})\\ *~', "\$1\n", $text); + $text = rtrim($text, "\n").($matches[1] ?? ''); - $text = wordwrap($text, $width, "\n", true); + if (!$currentLineLength && '' !== $current && "\n" !== substr($current, -1)) { + $text = "\n".$text; + } + + $lines = explode("\n", $text); + if ($width === $currentLineLength = \strlen(end($lines))) { + $currentLineLength = 0; + } - if ('' !== $current && "\n" !== substr($current, -1)) { - $text = "\n".$text; + if ($this->isDecorated()) { + foreach ($lines as $i => $line) { + $lines[$i] = $this->styleStack->getCurrent()->apply($line); } } - return $this->isDecorated() && \strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text; + return implode("\n", $lines); } } diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index a895953dfb121..efa53203d9afd 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface; use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\OutputInterface; @@ -80,6 +81,7 @@ class Table * @var array */ private $columnWidths = array(); + private $columnMaxWidths = array(); private static $styles; @@ -222,6 +224,25 @@ public function setColumnWidths(array $widths) return $this; } + /** + * Sets the maximum width of a column. + * + * Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while + * formatted strings are preserved. + * + * @return $this + */ + public function setColumnMaxWidth(int $columnIndex, int $width): self + { + if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) { + 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()))); + } + + $this->columnMaxWidths[$columnIndex] = $width; + + return $this; + } + public function setHeaders(array $headers) { $headers = array_values($headers); @@ -434,7 +455,6 @@ private function renderColumnSeparator($type = self::BORDER_OUTSIDE) * Example: * * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | - * */ private function renderRow(array $row, string $cellFormat) { @@ -498,12 +518,17 @@ private function calculateNumberOfColumns($rows) private function buildTableRows($rows) { + /** @var WrappableOutputFormatterInterface $formatter */ + $formatter = $this->output->getFormatter(); $unmergedRows = array(); for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) { $rows = $this->fillNextRows($rows, $rowKey); // Remove any new line breaks and replace it with a new line foreach ($rows[$rowKey] as $column => $cell) { + if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($formatter, $cell) > $this->columnMaxWidths[$column]) { + $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column]); + } if (!strstr($cell, "\n")) { continue; } @@ -711,8 +736,9 @@ private function getCellWidth(array $row, int $column): int } $columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0; + $cellWidth = max($cellWidth, $columnWidth); - return max($cellWidth, $columnWidth); + return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth; } /** diff --git a/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php index b9a8559766696..b51668cfa7e20 100644 --- a/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php +++ b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php @@ -327,11 +327,19 @@ public function testFormatAndWrap() { $formatter = new OutputFormatter(true); - $this->assertSame("pre\n\033[37;41mfoo\nbar\nbaz\033[39;49m\npos\nt", $formatter->formatAndWrap('pre foo bar baz post', 3)); + $this->assertSame("fo\no\e[37;41mb\e[39;49m\n\e[37;41mar\e[39;49m\nba\nz", $formatter->formatAndWrap('foobar baz', 2)); + $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 foo bar baz post', 2)); + $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 foo bar baz post', 3)); + $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 foo bar baz post', 4)); + $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 foo bar baz post', 5)); $formatter = new OutputFormatter(); + $this->assertSame("fo\nob\nar\nba\nz", $formatter->formatAndWrap('foobar baz', 2)); + $this->assertSame("pr\ne \nfo\no \nba\nr \nba\nz \npo\nst", $formatter->formatAndWrap('pre foo bar baz post', 2)); $this->assertSame("pre\nfoo\nbar\nbaz\npos\nt", $formatter->formatAndWrap('pre foo bar baz post', 3)); + $this->assertSame("pre \nfoo \nbar \nbaz \npost", $formatter->formatAndWrap('pre foo bar baz post', 4)); + $this->assertSame("pre f\noo ba\nr baz\npost", $formatter->formatAndWrap('pre foo bar baz post', 5)); } } diff --git a/src/Symfony/Component/Console/Tests/Helper/TableTest.php b/src/Symfony/Component/Console/Tests/Helper/TableTest.php index a7c77942cc690..bf37a0ab4eb63 100644 --- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php @@ -1050,6 +1050,33 @@ public function renderSetTitle() ); } + public function testColumnMaxWidths() + { + $table = new Table($output = $this->getOutputStream()); + $table + ->setRows(array( + array('Divine Comedy', 'A Tale of Two Cities', 'The Lord of the Rings', 'And Then There Were None'), + )) + ->setColumnMaxWidth(1, 5) + ->setColumnMaxWidth(2, 10) + ->setColumnMaxWidth(3, 15); + + $table->render(); + + $expected = + <<assertEquals($expected, $this->getOutputContent($output)); + } + protected function getOutputStream($decorated = false) { return new StreamOutput($this->stream, StreamOutput::VERBOSITY_NORMAL, $decorated);