diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 399bbc2213368..89fb893ed7404 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -1,6 +1,8 @@ CHANGELOG ========= + * Refactor word wrapping with WordWrapperHelper + 4.2.0 ----- @@ -24,7 +26,7 @@ CHANGELOG * `OutputFormatter` throws an exception when unknown options are used * removed `QuestionHelper::setInputStream()/getInputStream()` - * removed `Application::getTerminalWidth()/getTerminalHeight()` and + * removed `Application::getTerminalWidth()/getTerminalHeight()` and `Application::setTerminalDimensions()/getTerminalDimensions()` * removed `ConsoleExceptionEvent` * removed `ConsoleEvents::EXCEPTION` @@ -50,7 +52,7 @@ CHANGELOG with value optional explicitly passed empty * added console.error event to catch exceptions thrown by other listeners * deprecated console.exception event in favor of console.error -* added ability to handle `CommandNotFoundException` through the +* added ability to handle `CommandNotFoundException` through the `console.error` event * deprecated default validation in `SymfonyQuestionHelper::ask` @@ -66,7 +68,7 @@ CHANGELOG ----- * added truncate method to FormatterHelper - * added setColumnWidth(s) method to Table + * added setColumnWidth(s) method to Table 2.8.3 ----- diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatter.php b/src/Symfony/Component/Console/Formatter/OutputFormatter.php index a333f45bd6205..f6519a1fcf544 100644 --- a/src/Symfony/Component/Console/Formatter/OutputFormatter.php +++ b/src/Symfony/Component/Console/Formatter/OutputFormatter.php @@ -12,18 +12,22 @@ namespace Symfony\Component\Console\Formatter; use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Helper\WordWrapperHelper; /** * Formatter class for console output. * * @author Konstantin Kudryashov * @author Roland Franssen + * @author Krisztián Ferenczi */ class OutputFormatter implements WrappableOutputFormatterInterface { private $decorated; private $styles = []; private $styleStack; + private $defaultWrapCutOption = WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING; /** * Escapes "<" special char in given text. @@ -127,23 +131,36 @@ public function getStyle($name) } /** - * {@inheritdoc} + * @return int */ - public function format($message) + public function getDefaultWrapCutOption(): int { - return $this->formatAndWrap((string) $message, 0); + return $this->defaultWrapCutOption; + } + + /** + * @param int $defaultWrapCutOption + * + * @return $this + * + * @see WordWrapperHelper + */ + public function setDefaultWrapCutOption(int $defaultWrapCutOption) + { + $this->defaultWrapCutOption = $defaultWrapCutOption; + + return $this; } /** * {@inheritdoc} */ - public function formatAndWrap(string $message, int $width) + public function format($message) { + $message = (string) $message; $offset = 0; $output = ''; - $tagRegex = '[a-z][a-z0-9,_=;-]*+'; - $currentLineLength = 0; - preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, PREG_OFFSET_CAPTURE); + preg_match_all(Helper::getFormatTagRegexPattern(), $message, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[0] as $i => $match) { $pos = $match[1]; $text = $match[0]; @@ -153,7 +170,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, $currentLineLength); + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset)); $offset = $pos + \strlen($text); // opening tag? @@ -166,8 +183,8 @@ public function formatAndWrap(string $message, int $width) if (!$open && !$tag) { // $this->styleStack->pop(); - } elseif (false === $style = $this->createStyleFromString($tag)) { - $output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength); + } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) { + $output .= $this->applyCurrentStyle($text); } elseif ($open) { $this->styleStack->push($style); } else { @@ -175,7 +192,7 @@ public function formatAndWrap(string $message, int $width) } } - $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength); + $output .= $this->applyCurrentStyle(substr($message, $offset)); if (false !== strpos($output, "\0")) { return strtr($output, ["\0" => '\\', '\\<' => '<']); @@ -184,6 +201,22 @@ public function formatAndWrap(string $message, int $width) return str_replace('\\<', '<', $output); } + /** + * {@inheritdoc} + */ + public function formatAndWrap(string $message, int $width) + { + return $this->format($this->wordwrap($message, $width)); + } + + /** + * {@inheritdoc} + */ + public function wordwrap(string $message, int $width, int $cutOption = null): string + { + return WordWrapperHelper::wrap($message, $width, null === $cutOption ? $this->defaultWrapCutOption : $cutOption); + } + /** * @return OutputFormatterStyleStack */ @@ -195,6 +228,8 @@ public function getStyleStack() /** * Tries to create new style instance from string. * + * @param string $string + * * @return OutputFormatterStyle|false False if string is not format string */ private function createStyleFromString(string $string) @@ -233,46 +268,17 @@ 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, int &$currentLineLength): string + private function applyCurrentStyle(string $text): string { - if ('' === $text) { - return ''; - } - - 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] ?? ''); - - if (!$currentLineLength && '' !== $current && "\n" !== substr($current, -1)) { - $text = "\n".$text; - } - - $lines = explode("\n", $text); - if ($width === $currentLineLength = \strlen(end($lines))) { - $currentLineLength = 0; - } - - if ($this->isDecorated()) { + if ($this->isDecorated() && \strlen($text) > 0) { + $lines = explode("\n", $text); foreach ($lines as $i => $line) { $lines[$i] = $this->styleStack->getCurrent()->apply($line); } + + return implode("\n", $lines); } - return implode("\n", $lines); + return $text; } } diff --git a/src/Symfony/Component/Console/Formatter/WrappableOutputFormatterInterface.php b/src/Symfony/Component/Console/Formatter/WrappableOutputFormatterInterface.php index 6694053f057ea..818cf70e6c35b 100644 --- a/src/Symfony/Component/Console/Formatter/WrappableOutputFormatterInterface.php +++ b/src/Symfony/Component/Console/Formatter/WrappableOutputFormatterInterface.php @@ -15,6 +15,7 @@ * Formatter interface for console output that supports word wrapping. * * @author Roland Franssen + * @author Krisztián Ferenczi */ interface WrappableOutputFormatterInterface extends OutputFormatterInterface { @@ -22,4 +23,15 @@ interface WrappableOutputFormatterInterface extends OutputFormatterInterface * Formats a message according to the given styles, wrapping at `$width` (0 means no wrapping). */ public function formatAndWrap(string $message, int $width); + + /** + * Separate word wrapping method. + * + * @param string $message + * @param int $width + * @param int|null $cutOption + * + * @return string + */ + public function wordwrap(string $message, int $width, int $cutOption = null): string; } diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 0ddddf6bc5023..68e3b9baa608c 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -20,6 +20,8 @@ */ abstract class Helper implements HelperInterface { + const FORMAT_TAG_REGEX = '[a-z][a-z0-9,_=;-]*+'; + protected $helperSet = null; /** @@ -123,6 +125,25 @@ public static function strlenWithoutDecoration(OutputFormatterInterface $formatt return self::strlen(self::removeDecoration($formatter, $string)); } + /** + * Sometimes we need to find the format tags. This regex "placeholders": + * - \\0 --> full open or close tag + * - \\1 --> tag "inside" + * - \\2 --> only tag name. + * + * | \\0 | \\1 | \\2 | + * | ---------- | -------- | ------- | + * | | comment | comment | + * | | /comment | comment | + * | | / | (empty) | + * + * @return string + */ + public static function getFormatTagRegexPattern(): string + { + return sprintf('{<((%1$s)|/(%1$s)?)>}ix', self::FORMAT_TAG_REGEX); + } + public static function removeDecoration(OutputFormatterInterface $formatter, $string) { $isDecorated = $formatter->isDecorated(); diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index 99aa83d4c8173..d22c5672871f6 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -82,6 +82,8 @@ class Table */ private $columnWidths = []; private $columnMaxWidths = []; + private $columnWordWrapCutOption = []; + private $defaultColumnWordWrapCutOption = WordWrapperHelper::CUT_LONG_WORDS; private static $styles; @@ -228,13 +230,72 @@ public function setColumnWidths(array $widths) * * @return $this */ - public function setColumnMaxWidth(int $columnIndex, int $width): self + public function setColumnMaxWidth(int $columnIndex, int $width, int $cutOption = null): 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; + $this->columnWordWrapCutOption[$columnIndex] = $cutOption; + + return $this; + } + + /** + * Sets the cut options of a column. + * + * @param int $columnIndex + * @param int $cutOptions + * + * @return $this + */ + public function setColumnWordWrapCutOption(int $columnIndex, int $cutOptions): self + { + $this->columnWordWrapCutOption[$columnIndex] = $cutOptions; + + return $this; + } + + /** + * @param int $columnIndex + * + * @return int + */ + public function getColumnWordWrapCutOption(int $columnIndex): int + { + if (!\array_key_exists($columnIndex, $this->columnWordWrapCutOption) + || null === $this->columnWordWrapCutOption[$columnIndex] + ) { + return $this->defaultColumnWordWrapCutOption; + } + + return $this->columnWordWrapCutOption[$columnIndex]; + } + + /** + * @param int $defaultColumnWordWrapCutOption + * + * @return $this + */ + public function setDefaultColumnWordWrapCutOption(int $defaultColumnWordWrapCutOption): self + { + $this->defaultColumnWordWrapCutOption = $defaultColumnWordWrapCutOption; + + return $this; + } + + /** + * @param array $columnWordWrapCutOptions + * + * @return $this + */ + public function setColumnsWordWrapCutOptions(array $columnWordWrapCutOptions): self + { + $this->columnWordWrapCutOption = []; + foreach ($columnWordWrapCutOptions as $columnIndex => $columnOption) { + $this->setColumnWordWrapCutOption($columnIndex, $columnOption); + } return $this; } @@ -523,7 +584,11 @@ private function buildTableRows($rows) // 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]); + $cell = $formatter->format($formatter->wordwrap( + $cell, + $this->columnMaxWidths[$column], + $this->getColumnWordWrapCutOption($column) + )); } if (!strstr($cell, "\n")) { continue; diff --git a/src/Symfony/Component/Console/Helper/WordWrapperHelper.php b/src/Symfony/Component/Console/Helper/WordWrapperHelper.php new file mode 100644 index 0000000000000..f115e67685c52 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/WordWrapperHelper.php @@ -0,0 +1,532 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * It helps you to wrap long text with pretty breaks and useful cuts. You can control the cuts with the control option: + * - CUT_DISABLE: Always break the text at word boundary. + * - CUT_LONG_WORDS: If the word is longer than one row it will be cut. + * - CUT_WORDS: Always break at set length, it will cut all words. It would be useful if you have little + * space. (Info: It "contains" the CUT_LONG_WORDS option) + * - CUT_URLS: Lots of terminal can recognize URL-s in text and make them clickable (if there isn't break + * inside the URL) The URLS can be long, default we keep it in one block even if it gets ugly + * response. You can switch this behavior off with this option. The result will be pretty, + * but the URL won't be clickable. + * - CUT_FILL_UP_MISSING: The program will fill up the rows with spaces in order to every row will be same long. + * - CUT_NO_REPLACE_EOL: The program will replace the PHP_EOL in the input string to $break. + * + * + * $message = "This is a comment message with info ..."; + * // Default: + * $output->writeln(WordWrapperHelper::wrap($message, 120); + * // Use custom settings: + * $output->writeln(WordWrapperHelper::wrap( + * $message, + * 20, + * WordWrapperHelper::CUT_ALL | WordWrapperHelper::CUT_FILL_UP_MISSING, + * PHP_EOL + * ); + * + * + * Known problems, limitations: + * - You can't call WordWrapperHelper::wrap() inside a "running wrap" because there are "cache" properties and + * it causes problems within a Singleton class. Solution: you can create a WordWrapperHelper object, and + * use the $wrapper->wordwrap() non-static method. + * - If you use escaped tags AND (the line width is too short OR you use the CUT_WORDS option): `\Message\`, + * the wrapper could wrap inside the tag: + * + * \Me + * ssage\ + * + * In this case maybe the OutputFormatter won't remove the second `\` character, because the wrapper broke the + * tag also, so it will shown like this: + * + * Me + * ssage\ + * + * @author Krisztián Ferenczi + */ +class WordWrapperHelper extends Helper +{ + // Defaults + /** @var int */ + const DEFAULT_WIDTH = 120; + /** @var string */ + const DEFAULT_BREAK = "\n"; + /** @var int */ + const DEFAULT_CUT = self::CUT_LONG_WORDS; + + // Cut options + const CUT_DISABLE = 0; + const CUT_LONG_WORDS = 1; + const CUT_WORDS = 3; // Cut long words too + const CUT_URLS = 4; + const CUT_ALL = 7; + const CUT_FILL_UP_MISSING = 8; + const CUT_NO_REPLACE_EOL = 16; + + /** + * This is a ZERO_WIDTH_SPACE UTF-8 character. It is used when we try to protect the escaped tags, eg: `\`. + * + * @see https://en.wikipedia.org/wiki/Zero-width_space + * @see https://www.fileformat.info/info/unicode/char/200b/index.htm + */ + const ESCAPE_PROTECTION_CHAR = "\u{200B}"; + + /** + * @var self + */ + protected static $instance; + + /** + * "Cache". + * + * @var int|null + */ + protected $width; + + /** + * "Cache". Here you can use the CUT_* constants. + * + * @var int|null + */ + protected $cutOption; + + /** + * "Cache". + * + * @var string + */ + protected $break; + + /** + * "Cache". We collect the new lines into this array. + * + * @var array + */ + protected $newLines; + + /** + * "Cache". The current line "words". + * + * @var array + */ + protected $newLineTokens; + + /** + * "Cache". The current line "real" length, without the formatter "tags" and the spaces! + * + * @var int + */ + protected $currentLength; + + /** + * "Singleton.", but it isn't forbidden to create new objects, if you want. + * + * @return WordWrapperHelper + */ + protected static function getInstance(): self + { + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Returns the canonical name of this helper. + * + * @return string The canonical name + */ + public function getName() + { + return 'WordWrapperHelper'; + } + + /** + * @param string $string The text + * @param int $width Character width of one line + * @param int $cutOption You can mix your needs with CUT_* constants + * @param string $break The line breaking character(s) + * + * @return string + */ + public static function wrap(string $string, int $width = self::DEFAULT_WIDTH, int $cutOption = self::DEFAULT_CUT, string $break = self::DEFAULT_BREAK): string + { + $wrapper = self::getInstance(); + + return $wrapper->wordwrap($string, $width, $cutOption, $break); + } + + /** + * @param string $string The text + * @param int $width Character width of one line + * @param int $cutOptions You can mix your needs with CUT_* constants + * @param string $break The line breaking character(s) + * + * @return string + */ + public function wordwrap(string $string, int $width = self::DEFAULT_WIDTH, int $cutOptions = self::DEFAULT_CUT, string $break = self::DEFAULT_BREAK): string + { + // If the "cache" properties are in use, the program throw an exception, because you mustn't wrap inside a wrap. + // If you need this, you can create a new wrapper object and you have to use this. + if (null !== $this->width) { + throw new \LogicException('You mustn\'t wrap inside a wrap!'); + } + if ($width <= 0) { + throw new \InvalidArgumentException('You have to set more than 0 width!'); + } + if (0 == mb_strlen($break)) { + throw new \InvalidArgumentException('You have to use existing end of the line character or string!'); + } + // Reset all cache properties. + $this->reset($width, $cutOptions, $break); + // Unifies the line endings + if (!$this->hasCutOption(self::CUT_NO_REPLACE_EOL)) { + $string = str_replace(PHP_EOL, "\n", $string); + $string = str_replace("\r\n", "\n", $string); + $string = str_replace("\n\r", "\n", $string); + $string = str_replace("\r", "\n", $string); + } + // Protect the escaped characters and tags. + $string = $this->escape($string); + // Slice string by break string + $lines = explode($break, $string); + foreach ($lines as $n => $line) { + // Token can be a word + foreach (explode(' ', $line) as $token) { + $virtualTokenLength = $this->getVirtualTokenLength($token); + $lineLength = $this->getCurrentLineLength(); + if ($lineLength + $virtualTokenLength < $width) { + $this->addTokenToLine($token, $virtualTokenLength); + } else { + $this->handleLineEnding($token, $virtualTokenLength); + } + } + $this->closeLine(); + } + + return $this->finish(); + } + + /** + * This function handles what to happen at end of the line. + * + * @param string $token + * @param int $virtualTokenLength + */ + protected function handleLineEnding(string $token, int $virtualTokenLength): void + { + switch (true) { + // Token is an URL and we don't want to cut it + case $this->tokenIsAnUrl($token) && !$this->hasCutOption(self::CUT_URLS): + $this->closeLine(); + $this->addTokenToLine($token, $virtualTokenLength); + break; + // We cut everything + case $this->hasCutOption(self::CUT_WORDS): + $this->sliceToken($token); + break; + // We want to cut the long words + case $virtualTokenLength > $this->width && $this->hasCutOption(self::CUT_LONG_WORDS): + // A little prettifying, avoid like this: + // Lorem ipsum ve + // rylongtext dol + // or sit amet + // With this: + // Lorem ipsum + // verylongtext + // dolor sit amet + if ($this->getFreeSpace() < 5 && $this->width > 10) { + $this->closeLine(); + } + $this->sliceToken($token); + break; + // Other situation... + default: + $this->closeLine(); + $this->addTokenToLine($token, $virtualTokenLength); + break; + } + } + + /** + * Close a line. + */ + protected function closeLine(): void + { + if (\count($this->newLineTokens)) { + // If the last word is empty, and there are other words, we drop it. + if (\count($this->newLineTokens) > 1 && '' == $this->newLineTokens[\count($this->newLineTokens) - 1]) { + array_pop($this->newLineTokens); + } + $line = $this->unescape(trim(implode(' ', $this->newLineTokens))); + // Fill up the ends if necessary + if ($this->hasCutOption(self::CUT_FILL_UP_MISSING)) { + $line .= str_repeat(' ', max($this->getFreeSpace(), 0)); + } + // Add current line and reset "line caches". + $this->newLines[] = $line; + $this->newLineTokens = []; + $this->currentLength = 0; + } + } + + /** + * Register a token with set length. We will implode them with ' ' - space -, that is why the $appendToLast could + * be important if we adding a part of longer word. + * + * $appendToLast = false + * Error --> Error + * + * $appendToLast = true + * Error --> Error + * + * @param string $token + * @param int $virtualTokenLength + * @param bool $appendToLast we set it true if we slice a longer word with tags eg + */ + protected function addTokenToLine(string $token, int $virtualTokenLength, bool $appendToLast = false): void + { + if ($appendToLast) { + $last = \count($this->newLineTokens) > 0 + ? array_pop($this->newLineTokens) + : ''; + $token = $last.$token; + } + $this->newLineTokens[] = $token; + $this->currentLength += $virtualTokenLength; + } + + /** + * We try to protect every escaped characters, especially the escaped formatting tags:. + * + * \ --> [ZERO_WIDTH_SPACE]\<[ZERO_WIDTH_SPACE]error> + * + * In this form the tag regular expression won't find this as a tag. + * + * !!! PAY ATTANTION !!! Don't use the mb_* functions with preg_match() position answers. preg_match() gets "bytes", + * not characters! + * + * @param string $string + * + * @return string + */ + protected function escape(string $string): string + { + $output = ''; + $offset = 0; + // The OFFSET value will be in byte!!!! Don't use mb_* functions when you use these numbers! + preg_match_all('{\\\\<}u', $string, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $protectedBlock = $match[0]; + // Don't use the mb_* function here!!! + $output .= substr($string, $offset, $pos - $offset) + .self::ESCAPE_PROTECTION_CHAR + .$protectedBlock + .self::ESCAPE_PROTECTION_CHAR; + // Don't use the mb_* function here!!! + $offset = $pos + \strlen($protectedBlock); + } + // Don't use the mb_* function here!!! + $output .= substr($string, $offset); + + return $output; + } + + protected function unescape(string $string): string + { + return preg_replace('{'.self::ESCAPE_PROTECTION_CHAR.'(\\\\<)'.self::ESCAPE_PROTECTION_CHAR.'}u', '\\1', $string); + } + + /** + * Close everything and build the formatted text. + * + * @return string + */ + protected function finish(): string + { + $this->closeLine(); + $fullEscapedText = implode($this->break, $this->newLines); + $unescaped = $this->unescape($fullEscapedText); + + // reset "caches" + $this->width = null; + $this->cutOption = null; + $this->break = null; + + return $unescaped; + } + + /** + * Reset and set properties. + * + * @param int $width + * @param int $cutOptions + * @param string $break + */ + protected function reset(int $width, int $cutOptions, string $break): void + { + $this->width = $width; + $this->cutOption = $cutOptions; + $this->break = $break; + $this->newLineTokens = []; + $this->newLines = []; + } + + /** + * How long the current line is: currentLength + number of spaces (token numbers - 1). + * + * @return int + */ + protected function getCurrentLineLength(): int + { + return $this->currentLength + \count($this->newLineTokens) - 1; + } + + protected function getFreeSpace(): int + { + return $this->width - $this->getCurrentLineLength(); + } + + /** + * Virtual token length = length without "formatter tags". Eg: + * - lorem --> 5 + * - lorem --> 5 + * - \lorem\ --> 24 // We removed the two \ escaping character! + * + * @param string $token + * + * @return int + */ + protected function getVirtualTokenLength(string $token): int + { + $virtualTokenLength = mb_strlen($token); + if (false !== strpos($token, '<') || false !== strpos($token, self::ESCAPE_PROTECTION_CHAR)) { + $untaggedToken = $this->pregReplaceTags('', $token); + $unescapedToken = $this->unescape($untaggedToken); + // Remove escaped tags + $virtualTokenLength = mb_strlen($unescapedToken) - substr_count($unescapedToken, '\\<'); + } + + return $virtualTokenLength; + } + + /** + * @param string $token + * + * @return bool + */ + protected function tokenIsAnUrl(string $token): bool + { + return false !== mb_strpos($token, 'http://') || false !== mb_strpos($token, 'https://'); + } + + /** + * Slice a long token. + * + * !!! PAY ATTENTION !!! Don't use the mb_* functions with preg_match() position answers. preg_match() gets "bytes", + * not characters! + * + * @param string $token + */ + protected function sliceToken(string $token): void + { + if ($this->getFreeSpace() <= 0) { + $this->closeLine(); + } + $offset = 0; + preg_match_all(Helper::getFormatTagRegexPattern(), $token, $matches, PREG_OFFSET_CAPTURE); + // Init: append to this... See addTokenToLine(), $appendToLast parameter + $this->addTokenToLine('', 0); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $tag = $match[0]; + + // Add the text up to the next tag. + // Don't replace it to mb_* function! + $block = \substr($token, $offset, $pos - $offset); + $offset = $pos + \strlen($tag); + + $this->breakLongToken($block); + if (0 == $this->getFreeSpace() && '/' != $tag[1]) { + $this->closeLine(); + } + $this->addTokenToLine($tag, 0, true); + } + // Don't replace it to mb_* function! + $block = \substr($token, $offset); + if ($block) { + $this->breakLongToken($block); + } + } + + protected function breakLongToken(string $token): void + { + $freeChars = $this->getFreeSpace(); + $token = $this->unescape($token); + $prefix = \mb_substr($token, 0, $freeChars); + $this->addTokenToLine($prefix, \mb_strlen($prefix), true); + $tokenLength = \mb_strlen($token); + for ($offset = $freeChars; $offset < $tokenLength; $offset += $this->width) { + $subLength = min($this->width, $tokenLength - $offset); + $subToken = \mb_substr($token, $offset, $subLength); + if ($subLength + $this->getCurrentLineLength() > $this->width) { + $this->closeLine(); + } + $this->addTokenToLine($subToken, \mb_strlen($subToken), true); + } + } + + /** + * It checks the cut option is set. See the CUT_* constants. + * + * @param int $option + * + * @return bool + */ + protected function hasCutOption(int $option): bool + { + return ($this->cutOption & $option) === $option; + } + + /** + * It replaces all tags to something different. If you want to use original tags, use the `\\0` placeholder:. + * + * Eg: + * $replacement = 'STARTTAG>\\0 and + * \\1: comment and /comment or / (!: the close tag could be ``) + * \\2: comment or '' (if the close tag is ``) + * + * @param string $replacement + * @param string $string + * + * @return string + */ + protected function pregReplaceTags(string $replacement, string $string): string + { + return preg_replace( + Helper::getFormatTagRegexPattern(), + $replacement, + $string + ); + } +} diff --git a/src/Symfony/Component/Console/Style/SymfonyStyle.php b/src/Symfony/Component/Console/Style/SymfonyStyle.php index b46162de683e8..1452e9c4d3086 100644 --- a/src/Symfony/Component/Console/Style/SymfonyStyle.php +++ b/src/Symfony/Component/Console/Style/SymfonyStyle.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\SymfonyQuestionHelper; use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\WordWrapperHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; @@ -406,7 +407,12 @@ private function createBlock(iterable $messages, string $type = null, string $st $message = OutputFormatter::escape($message); } - $lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true))); + $lines = array_merge($lines, explode(PHP_EOL, WordWrapperHelper::wrap( + $message, + $this->lineLength - $prefixLength - $indentLength, + WordWrapperHelper::CUT_LONG_WORDS, + PHP_EOL + ))); if (\count($messages) > 1 && $key < \count($messages) - 1) { $lines[] = ''; diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/lipsum.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/lipsum.txt new file mode 100644 index 0000000000000..1af6faf7b8865 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/lipsum.txt @@ -0,0 +1,5 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis ex in dolor mollis aliquet. Maecenas pharetra massa ipsum, id tincidunt velit tincidunt et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas dignissim augue non risus bibendum, id volutpat elit varius. Phasellus a aliquam elit. Integer sem ipsum, posuere vel mauris et, lobortis tempor urna. Morbi in metus lobortis, iaculis nulla id, dapibus dolor. Donec cursus, justo a finibus faucibus, enim sapien venenatis tortor, id pretium felis dolor ut nunc. Integer viverra feugiat dolor, in ornare lacus porta ac. Cras at arcu quam. Proin fringilla vestibulum magna ut feugiat. Sed at tempor tellus. Aliquam erat volutpat. Aliquam erat volutpat. Fusce efficitur arcu efficitur nibh varius, quis tempor enim imperdiet. Sed sagittis purus non urna imperdiet blandit. + +Aliquam a bibendum diam, nec semper ipsum. Donec magna felis, molestie in nulla ut, cursus luctus nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla lobortis nulla mi. Fusce a libero nec lorem aliquet molestie. Praesent sem diam, ultricies et posuere eu, blandit at lacus. Proin eleifend nisi id neque porta tincidunt. Quisque tempus purus sit amet erat posuere hendrerit. Proin vitae nisi eros. In hac habitasse platea dictumst. Ut ut metus eu diam aliquam feugiat. Pellentesque leo urna, efficitur ac velit quis, tempor molestie odio. Cras vitae cursus leo. + +Duis in libero ut odio porta dignissim. In molestie diam at ante dictum ullamcorper. Ut auctor eros elit, nec molestie erat viverra ut. Vestibulum nisl ex, cursus quis magna id, pulvinar condimentum sem. Nunc ac orci volutpat turpis luctus tempor. Phasellus non ultrices est. Proin quis risus ut risus interdum pellentesque. Vestibulum lobortis, est in accumsan porttitor, nunc neque vulputate ipsum, vel varius metus orci condimentum magna. Duis vulputate leo et mi interdum sollicitudin. Suspendisse malesuada sagittis urna ac mattis. Cras vel nisi eu enim interdum maximus in mollis augue. In faucibus vestibulum lectus at pharetra. Maecenas maximus massa ac massa ornare, sit amet blandit lacus ultricies. Suspendisse tristique pellentesque velit, a finibus libero pellentesque vel. Quisque pellentesque ornare tincidunt. diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/lipsum_with_tags.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/lipsum_with_tags.txt new file mode 100644 index 0000000000000..e3bf1f6d73880 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/lipsum_with_tags.txt @@ -0,0 +1,5 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis ex in dolor mollis aliquet. Maecenas pharetra massa ipsum, id tincidunt velit tincidunt et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas dignissim augue non risus bibendum, id volutpat elit varius. Phasellus a aliquam elit. Integer sem ipsum, posuere vel mauris et, lobortis tempor urna. Morbi in metus lobortis, iaculis nulla id, dapibus dolor. Donec cursus, justo a finibus faucibus, enim sapien venenatis tortor, id pretium felis dolor ut nunc. Integer viverra feugiat dolor, in ornare lacus porta ac. Cras at arcu quam. Proin fringilla vestibulum magna ut feugiat. Sed at tempor tellus. Aliquam erat volutpat. Aliquam erat volutpat. Fusce efficitur arcu efficitur nibh varius, quis tempor enim imperdiet. Sed sagittis purus non urna imperdiet blandit. + +Aliquam a bibendum diam, nec semper ipsum. Donec magna felis, molestie in nulla ut, cursus luctus nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla lobortis nulla mi. Fusce a libero nec lorem aliquet molestie. Praesent sem diam, ultricies et posuere eu, blandit at lacus. Proin eleifend nisi id neque porta tincidunt. Quisque tempus purus sit amet erat posuere hendrerit. Proin vitae nisi eros. In hac habitasse platea dictumst. Ut ut metus eu diam aliquam feugiat. Pellentesque leo urna, efficitur ac velit quis, tempor molestie odio. Cras vitae cursus leo. + +Duis in libero ut odio porta dignissim. In molestie diam at ante dictum ullamcorper. Ut auctor eros elit, nec molestie erat viverra ut. Vestibulum nisl ex, cursus quis magna id, pulvinar condimentum sem. Nunc ac orci volutpat turpis luctus tempor. Phasellus non ultrices est. Proin quis risus ut risus interdum pellentesque. Vestibulum lobortis, est in accumsan porttitor, nunc neque vulputate ipsum, vel varius metus orci condimentum magna. Duis vulputate leo et mi interdum sollicitudin. Suspendisse malesuada sagittis urna ac mattis. Cras vel nisi eu enim interdum maximus in mollis augue. In faucibus vestibulum lectus at pharetra. Maecenas maximus massa ac massa ornare, sit amet blandit lacus ultricies. Suspendisse tristique pellentesque velit, a finibus libero pellentesque vel. Quisque pellentesque ornare tincidunt. diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/lipsum_with_tags_and_custom_break.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/lipsum_with_tags_and_custom_break.txt new file mode 100644 index 0000000000000..5483823fb8c05 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/lipsum_with_tags_and_custom_break.txt @@ -0,0 +1,5 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis ex in dolor mollis aliquet. Maecenas pharetra massa ipsum, id tincidunt velit tincidunt et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas dignissim augue non risus bibendum, id volutpat elit varius. Phasellus a aliquam elit. Integer sem ipsum, posuere vel mauris et, lobortis tempor urna. Morbi in metus lobortis, iaculis nulla id, dapibus dolor. Donec cursus, justo a finibus faucibus, enim sapien venenatis tortor, id pretium felis dolor ut nunc. Integer viverra feugiat dolor, in ornare lacus porta ac. Cras at arcu quam. Proin fringilla vestibulum magna ut feugiat. Sed at tempor tellus. Aliquam erat volutpat. Aliquam erat volutpat. Fusce efficitur arcu efficitur nibh varius, quis tempor enim imperdiet. Sed sagittis purus non urna imperdiet blandit.__break__ +__break__ +Aliquam a bibendum diam, nec semper ipsum. Donec magna felis, molestie in nulla ut, cursus luctus nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla lobortis nulla mi. Fusce a libero nec lorem aliquet molestie. Praesent sem diam, ultricies et posuere eu, blandit at lacus. Proin eleifend nisi id neque porta tincidunt. Quisque tempus purus sit amet erat posuere hendrerit. Proin vitae nisi eros. In hac habitasse platea dictumst. Ut ut metus eu diam aliquam feugiat. Pellentesque leo urna, efficitur ac velit quis, tempor molestie odio. Cras vitae cursus leo.__break__ +__break__ +Duis in libero ut odio porta dignissim. In molestie diam at ante dictum ullamcorper. Ut auctor eros elit, nec molestie erat viverra ut. Vestibulum nisl ex, cursus quis magna id, pulvinar condimentum sem. Nunc ac orci volutpat turpis luctus tempor. Phasellus non ultrices est. Proin quis risus ut risus interdum pellentesque. Vestibulum lobortis, est in accumsan porttitor, nunc neque vulputate ipsum, vel varius metus orci condimentum magna. Duis vulputate leo et mi interdum sollicitudin. Suspendisse malesuada sagittis urna ac mattis. Cras vel nisi eu enim interdum maximus in mollis augue. In faucibus vestibulum lectus at pharetra. Maecenas maximus massa ac massa ornare, sit amet blandit lacus ultricies. Suspendisse tristique pellentesque velit, a finibus libero pellentesque vel. Quisque pellentesque ornare tincidunt.__break__ diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/utf120.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/utf120.txt new file mode 100644 index 0000000000000..1ade5947f9316 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/utf120.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis ex in dolor mollis aliquet. UTF8:öüóőúéáűíÖÜÓŐÚÉÁŰÍ diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/with_long_words.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/with_long_words.txt new file mode 100644 index 0000000000000..932b7b722b642 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/input/with_long_words.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet: https://www.google.com/search?safe=off&ei=5jSGXIGbGofCkwWcpLGIBA&q=Lorem+ipsum+dolor+sit+amet%2C+consectetur+adipiscing+elit.&oq=Lorem+ipsum+dolor+sit+amet%2C+consectetur+adipiscing+elit.&gs_l=psy-ab.3..0j0i22i30l9.24765.24765..26089...0.0..0.98.98.1......0....2j1..gws-wiz.......0i71.RLWmD-mYSAY Lorem ipsum dolor + +Test longer than 5 chars in the end: +Simple long word: thisisaverylongwordthisisaverylongwordthisisaverylongword + +Test less than 5 chars in the end: +Simple long word.........: thisisaverylongwordthisisaverylongwordthisisaverylongword + +Long word with "tags": thisisaverylongwordthisisaverylongwordthisisaverylongword diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/lipsum.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/lipsum.txt new file mode 100644 index 0000000000000..b4d91795ca115 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/lipsum.txt @@ -0,0 +1,24 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis ex in dolor mollis aliquet. Maecenas pharetra massa +ipsum, id tincidunt velit tincidunt et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac +turpis egestas. Maecenas dignissim augue non risus bibendum, id volutpat elit varius. Phasellus a aliquam elit. Integer +sem ipsum, posuere vel mauris et, lobortis tempor urna. Morbi in metus lobortis, iaculis nulla id, dapibus dolor. Donec +cursus, justo a finibus faucibus, enim sapien venenatis tortor, id pretium felis dolor ut nunc. Integer viverra feugiat +dolor, in ornare lacus porta ac. Cras at arcu quam. Proin fringilla vestibulum magna ut feugiat. Sed at tempor tellus. +Aliquam erat volutpat. Aliquam erat volutpat. Fusce efficitur arcu efficitur nibh varius, quis tempor enim imperdiet. +Sed sagittis purus non urna imperdiet blandit. + +Aliquam a bibendum diam, nec semper ipsum. Donec magna felis, molestie in nulla ut, cursus luctus nisl. Lorem ipsum +dolor sit amet, consectetur adipiscing elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla +lobortis nulla mi. Fusce a libero nec lorem aliquet molestie. Praesent sem diam, ultricies et posuere eu, blandit at +lacus. Proin eleifend nisi id neque porta tincidunt. Quisque tempus purus sit amet erat posuere hendrerit. Proin vitae +nisi eros. In hac habitasse platea dictumst. Ut ut metus eu diam aliquam feugiat. Pellentesque leo urna, efficitur ac +velit quis, tempor molestie odio. Cras vitae cursus leo. + +Duis in libero ut odio porta dignissim. In molestie diam at ante dictum ullamcorper. Ut auctor eros elit, nec molestie +erat viverra ut. Vestibulum nisl ex, cursus quis magna id, pulvinar condimentum sem. Nunc ac orci volutpat turpis luctus +tempor. Phasellus non ultrices est. Proin quis risus ut risus interdum pellentesque. Vestibulum lobortis, est in +accumsan porttitor, nunc neque vulputate ipsum, vel varius metus orci condimentum magna. Duis vulputate leo et mi +interdum sollicitudin. Suspendisse malesuada sagittis urna ac mattis. Cras vel nisi eu enim interdum maximus in mollis +augue. In faucibus vestibulum lectus at pharetra. Maecenas maximus massa ac massa ornare, sit amet blandit lacus +ultricies. Suspendisse tristique pellentesque velit, a finibus libero pellentesque vel. Quisque pellentesque ornare +tincidunt. diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/lipsum_with_tags.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/lipsum_with_tags.txt new file mode 100644 index 0000000000000..441db530bd664 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/lipsum_with_tags.txt @@ -0,0 +1,24 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis ex in dolor mollis aliquet. Maecenas pharetra massa +ipsum, id tincidunt velit tincidunt et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac +turpis egestas. Maecenas dignissim augue non risus bibendum, id volutpat elit varius. Phasellus a aliquam elit. Integer +sem ipsum, posuere vel mauris et, lobortis tempor urna. Morbi in metus lobortis, iaculis nulla id, dapibus dolor. Donec +cursus, justo a finibus faucibus, enim sapien venenatis tortor, id pretium felis dolor ut nunc. Integer viverra feugiat +dolor, in ornare lacus porta ac. Cras at arcu quam. Proin fringilla vestibulum magna ut feugiat. Sed at tempor tellus. +Aliquam erat volutpat. Aliquam erat volutpat. Fusce efficitur arcu efficitur nibh varius, quis tempor enim imperdiet. +Sed sagittis purus non urna imperdiet blandit. + +Aliquam a bibendum diam, nec semper ipsum. Donec magna felis, molestie in nulla ut, cursus luctus nisl. Lorem ipsum +dolor sit amet, consectetur adipiscing elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla +lobortis nulla mi. Fusce a libero nec lorem aliquet molestie. Praesent sem diam, ultricies et posuere eu, blandit at +lacus. Proin eleifend nisi id neque porta tincidunt. Quisque tempus purus sit amet erat posuere hendrerit. Proin vitae +nisi eros. In hac habitasse platea dictumst. Ut ut metus eu diam aliquam feugiat. Pellentesque leo urna, efficitur ac +velit quis, tempor molestie odio. Cras vitae cursus leo. + +Duis in libero ut odio porta dignissim. In molestie diam at ante dictum ullamcorper. Ut auctor eros elit, nec molestie +erat viverra ut. Vestibulum nisl ex, cursus quis magna id, pulvinar condimentum sem. Nunc ac orci volutpat turpis luctus +tempor. Phasellus non ultrices est. Proin quis risus ut risus interdum pellentesque. Vestibulum lobortis, est in +accumsan porttitor, nunc neque vulputate ipsum, vel varius metus orci condimentum magna. Duis vulputate leo et mi +interdum sollicitudin. Suspendisse malesuada sagittis urna ac mattis. Cras vel nisi eu enim interdum maximus in mollis +augue. In faucibus vestibulum lectus at pharetra. Maecenas maximus massa ac massa ornare, sit amet blandit lacus +ultricies. Suspendisse tristique pellentesque velit, a finibus libero pellentesque vel. Quisque pellentesque ornare +tincidunt. diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/lipsum_with_tags_and_custom_break.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/lipsum_with_tags_and_custom_break.txt new file mode 100644 index 0000000000000..3a88148ee732e --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/lipsum_with_tags_and_custom_break.txt @@ -0,0 +1,24 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis ex in dolor mollis aliquet. Maecenas pharetra massa__break__ +ipsum, id tincidunt velit tincidunt et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac__break__ +turpis egestas. Maecenas dignissim augue non risus bibendum, id volutpat elit varius. Phasellus a aliquam elit. Integer__break__ +sem ipsum, posuere vel mauris et, lobortis tempor urna. Morbi in metus lobortis, iaculis nulla id, dapibus dolor. Donec__break__ +cursus, justo a finibus faucibus, enim sapien venenatis tortor, id pretium felis dolor ut nunc. Integer viverra feugiat__break__ +dolor, in ornare lacus porta ac. Cras at arcu quam. Proin fringilla vestibulum magna ut feugiat. Sed at tempor tellus.__break__ +Aliquam erat volutpat. Aliquam erat volutpat. Fusce efficitur arcu efficitur nibh varius, quis tempor enim imperdiet.__break__ +Sed sagittis purus non urna imperdiet blandit.__break__ +__break__ +Aliquam a bibendum diam, nec semper ipsum. Donec magna felis, molestie in nulla ut, cursus luctus nisl. Lorem ipsum__break__ +dolor sit amet, consectetur adipiscing elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla__break__ +lobortis nulla mi. Fusce a libero nec lorem aliquet molestie. Praesent sem diam, ultricies et posuere eu, blandit at__break__ +lacus. Proin eleifend nisi id neque porta tincidunt. Quisque tempus purus sit amet erat posuere hendrerit. Proin vitae__break__ +nisi eros. In hac habitasse platea dictumst. Ut ut metus eu diam aliquam feugiat. Pellentesque leo urna, efficitur ac__break__ +velit quis, tempor molestie odio. Cras vitae cursus leo.__break__ +__break__ +Duis in libero ut odio porta dignissim. In molestie diam at ante dictum ullamcorper. Ut auctor eros elit, nec molestie__break__ +erat viverra ut. Vestibulum nisl ex, cursus quis magna id, pulvinar condimentum sem. Nunc ac orci volutpat turpis luctus__break__ +tempor. Phasellus non ultrices est. Proin quis risus ut risus interdum pellentesque. Vestibulum lobortis, est in__break__ +accumsan porttitor, nunc neque vulputate ipsum, vel varius metus orci condimentum magna. Duis vulputate leo et mi__break__ +interdum sollicitudin. Suspendisse malesuada sagittis urna ac mattis. Cras vel nisi eu enim interdum maximus in mollis__break__ +augue. In faucibus vestibulum lectus at pharetra. Maecenas maximus massa ac massa ornare, sit amet blandit lacus__break__ +ultricies. Suspendisse tristique pellentesque velit, a finibus libero pellentesque vel. Quisque pellentesque ornare__break__ +tincidunt.__break__ diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/utf120.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/utf120.txt new file mode 100644 index 0000000000000..1ade5947f9316 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/utf120.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis ex in dolor mollis aliquet. UTF8:öüóőúéáűíÖÜÓŐÚÉÁŰÍ diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/with_long_words_with_cut_all.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/with_long_words_with_cut_all.txt new file mode 100644 index 0000000000000..e8b33f2ed76fa --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/with_long_words_with_cut_all.txt @@ -0,0 +1,28 @@ +Lorem ipsum dolor sit amet: ht +tps://www.google.com/search?sa +fe=off&ei=5jSGXIGbGofCkwWcpLGI +BA&q=Lorem+ipsum+dolor+sit+ame +t%2C+consectetur+adipiscing+el +it.&oq=Lorem+ipsum+dolor+sit+a +met%2C+consectetur+adipiscing+ +elit.&gs_l=psy-ab.3..0j0i22i30 +l9.24765.24765..26089...0.0..0 +.98.98.1......0....2j1..gws-wi +z.......0i71.RLWmD-mYSAY Lorem +ipsum dolor + +Test longer than 5 chars in th +e end: +Simple long word: thisisaveryl +ongwordthisisaverylongwordthis +isaverylongword + +Test less than 5 chars in the +end: +Simple long word.........: thi +sisaverylongwordthisisaverylon +gwordthisisaverylongword + +Long word with "tags": thisisa +verylongwordthisisaverylongwor +dthisisaverylongword diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/with_long_words_with_default_cut.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/with_long_words_with_default_cut.txt new file mode 100644 index 0000000000000..e61ed58982943 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/with_long_words_with_default_cut.txt @@ -0,0 +1,19 @@ +Lorem ipsum dolor sit amet: +https://www.google.com/search?safe=off&ei=5jSGXIGbGofCkwWcpLGIBA&q=Lorem+ipsum+dolor+sit+amet%2C+consectetur+adipiscing+elit.&oq=Lorem+ipsum+dolor+sit+amet%2C+consectetur+adipiscing+elit.&gs_l=psy-ab.3..0j0i22i30l9.24765.24765..26089...0.0..0.98.98.1......0....2j1..gws-wiz.......0i71.RLWmD-mYSAY +Lorem ipsum dolor + +Test longer than 5 chars in +the end: +Simple long word: thisisaveryl +ongwordthisisaverylongwordthis +isaverylongword + +Test less than 5 chars in the +end: +Simple long word.........: +thisisaverylongwordthisisavery +longwordthisisaverylongword + +Long word with "tags": thisisa +verylongwordthisisaverylongwor +dthisisaverylongword diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/with_long_words_without_cut.txt b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/with_long_words_without_cut.txt new file mode 100644 index 0000000000000..0a7c617b4f490 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Helper/WordWrapper/output/with_long_words_without_cut.txt @@ -0,0 +1,16 @@ +Lorem ipsum dolor sit amet: +https://www.google.com/search?safe=off&ei=5jSGXIGbGofCkwWcpLGIBA&q=Lorem+ipsum+dolor+sit+amet%2C+consectetur+adipiscing+elit.&oq=Lorem+ipsum+dolor+sit+amet%2C+consectetur+adipiscing+elit.&gs_l=psy-ab.3..0j0i22i30l9.24765.24765..26089...0.0..0.98.98.1......0....2j1..gws-wiz.......0i71.RLWmD-mYSAY +Lorem ipsum dolor + +Test longer than 5 chars in +the end: +Simple long word: +thisisaverylongwordthisisaverylongwordthisisaverylongword + +Test less than 5 chars in the +end: +Simple long word.........: +thisisaverylongwordthisisaverylongwordthisisaverylongword + +Long word with "tags": +thisisaverylongwordthisisaverylongwordthisisaverylongword diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt index 0f3704b7482ea..4ecb6ae30e755 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt @@ -1,7 +1,6 @@ - // Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et  - // dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea  - // commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla  - // pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim - // id est laborum + // Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + // aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.  + // Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + // sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum diff --git a/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php index 3a7906173862c..b651cd3bc74c4 100644 --- a/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php +++ b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php @@ -277,8 +277,8 @@ public function testContentWithLineBreaks() $formatter = new OutputFormatter(true); $this->assertEquals(<<format(<<<'EOF' @@ -287,8 +287,8 @@ public function testContentWithLineBreaks() )); $this->assertEquals(<<format(<<<'EOF' some text @@ -297,9 +297,9 @@ public function testContentWithLineBreaks() )); $this->assertEquals(<<format(<<<'EOF' @@ -309,10 +309,10 @@ public function testContentWithLineBreaks() )); $this->assertEquals(<<format(<<<'EOF' @@ -327,19 +327,19 @@ public function testFormatAndWrap() { $formatter = new OutputFormatter(true); - $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)); + $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 \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\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 \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 \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', 5)); $formatter = new OutputFormatter(); - $this->assertSame("fo\nob\nar\nba\nz", $formatter->formatAndWrap('foobar baz', 2)); + $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\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)); + $this->assertSame("pre \nfoo \nbar \nbaz \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 43b90fd073c91..7cdba4757b1a3 100644 --- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\TableStyle; +use Symfony\Component\Console\Helper\WordWrapperHelper; use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\StreamOutput; @@ -1057,7 +1058,7 @@ public function testColumnMaxWidths() ->setRows([ ['Divine Comedy', 'A Tale of Two Cities', 'The Lord of the Rings', 'And Then There Were None'], ]) - ->setColumnMaxWidth(1, 5) + ->setColumnMaxWidth(1, 5, WordWrapperHelper::CUT_ALL) ->setColumnMaxWidth(2, 10) ->setColumnMaxWidth(3, 15); @@ -1065,12 +1066,12 @@ public function testColumnMaxWidths() $expected = << + */ +class WordWrapperHelperTest extends TestCase +{ + /** + * @param int $width + * @param string $break + * + * @dataProvider dpTestConstructorExceptions + */ + public function testConstructorExceptions($width, $break) + { + $wordWrapper = new WordWrapperHelper(); + $this->expectException(\InvalidArgumentException::class); + $wordWrapper->wordwrap('test', $width, WordWrapperHelper::DEFAULT_CUT, $break); + } + + public function dpTestConstructorExceptions() + { + return [ + [0, PHP_EOL], + [-1, PHP_EOL], + [0, ''], + [1, ''], + ]; + } + + /** + * @param string $input + * @param int $width + * @param string $break + * @param int $cutOptions + * @param string $output + * + * @dataProvider dpWordwrap + */ + public function testWordwrap($input, $width, $cutOptions, $break, $output) + { + $wordWrapper = new WordWrapperHelper(); + $response = $wordWrapper->wordwrap($this->getInputContents($input), $width, $cutOptions, $break); + + $this->assertEquals($this->getOutputContents($output), $response); + } + + /** + * Maybe in the future it should behave differently from wordwrap() function that is why it same the other now. + * + * @param string $input + * @param int $width + * @param string $break + * @param int $cutOptions + * @param string $output + * + * @dataProvider dpWordwrap + */ + public function testStaticWrap($input, $width, $cutOptions, $break, $output) + { + $response = WordWrapperHelper::wrap($this->getInputContents($input), $width, $cutOptions, $break); + + $this->assertEquals($this->getOutputContents($output), $response); + } + + public function dpWordwrap() + { + $baseBreak = "\n"; + $customBreak = "__break__\n"; + + return [ + // Check empty + ['', 2, WordWrapperHelper::CUT_ALL, $baseBreak, ''], + ['', 2, WordWrapperHelper::CUT_ALL | WordWrapperHelper::CUT_FILL_UP_MISSING, $baseBreak, ' '], + [$baseBreak, 2, WordWrapperHelper::CUT_ALL, $baseBreak, $baseBreak], + [$baseBreak, 2, WordWrapperHelper::CUT_ALL | WordWrapperHelper::CUT_FILL_UP_MISSING, $baseBreak, ' '.$baseBreak.' '], + // Check limit and UTF-8 + [ + 'öüóőúéáű', + 8, + WordWrapperHelper::CUT_LONG_WORDS, + $baseBreak, + 'öüóőúéáű', + ], + [ + 'öüóőúéáű', + 4, + WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING, + $baseBreak, + 'öüóő'.$baseBreak.'úéáű', + ], + [ + 'öüóőúéáű', + 6, + WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING, + $baseBreak, + 'öüóőúé'.$baseBreak.'áű ', + ], + // UTF-8 + tags + [ + 'öüóőúéáű', + 8, + WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING, + $baseBreak, + 'öüóőúéáű', + ], + [ + 'öüóőúéáű', + 8, + WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING, + $baseBreak, + 'öüóőúéáű', + ], + [ + 'foo bar baz', + 3, + WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING, + $baseBreak, + implode($baseBreak, ['foo', 'bar', 'baz']), + ], + [ + 'foo bar baz', + 2, + WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING, + $baseBreak, + implode($baseBreak, ['fo', 'o ', 'ba', 'r ', 'ba', 'z ']), + ], + // Escaped tags + [ + 'foo \bar\ baz', + 3, + WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING, + $baseBreak, + implode($baseBreak, ['foo', '\b', 'ar\\', ' ', 'baz']), + ], + [ + 'foobarbaz foo', + 3, + WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING, + $baseBreak, + implode($baseBreak, ['foo', 'bar', 'baz', 'foo']), + ], + [ + 'foobarbaz foo', + 2, + WordWrapperHelper::CUT_LONG_WORDS | WordWrapperHelper::CUT_FILL_UP_MISSING, + $baseBreak, + implode($baseBreak, ['fo', 'ob', 'ar', 'ba', 'z ', 'fo', 'o ']), + ], + // Check simple text + [ + 'lipsum.txt', + 120, + WordWrapperHelper::CUT_LONG_WORDS, + $baseBreak, + 'lipsum.txt', + ], + // Check colored text + [ + 'lipsum_with_tags.txt', + 120, + WordWrapperHelper::CUT_LONG_WORDS, + $baseBreak, + 'lipsum_with_tags.txt', + ], + // Check custom break + [ + 'lipsum_with_tags_and_custom_break.txt', + 120, + WordWrapperHelper::CUT_LONG_WORDS, + $customBreak, + 'lipsum_with_tags_and_custom_break.txt', + ], + // Check long words + [ + 'with_long_words.txt', + 30, + WordWrapperHelper::CUT_LONG_WORDS, + $baseBreak, + 'with_long_words_with_default_cut.txt', + ], + [ + 'with_long_words.txt', + 30, + WordWrapperHelper::CUT_DISABLE, + $baseBreak, + 'with_long_words_without_cut.txt', + ], + [ + 'with_long_words.txt', + 30, + WordWrapperHelper::CUT_ALL, + $baseBreak, + 'with_long_words_with_cut_all.txt', + ], + ]; + } + + protected function getInputContents($filenameOrText) + { + $file = __DIR__.'/../Fixtures/Helper/WordWrapper/input/'.$filenameOrText; + + return file_exists($file) && is_file($file) + ? file_get_contents($file) + : $filenameOrText; + } + + protected function getOutputContents($filenameOrText) + { + $file = __DIR__.'/../Fixtures/Helper/WordWrapper/output/'.$filenameOrText; + + return file_exists($file) && is_file($file) + ? file_get_contents($file) + : $filenameOrText; + } +}