summaryrefslogtreecommitdiff
path: root/vendor/symfony/console/Formatter/OutputFormatter.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/symfony/console/Formatter/OutputFormatter.php')
-rw-r--r--vendor/symfony/console/Formatter/OutputFormatter.php268
1 files changed, 268 insertions, 0 deletions
diff --git a/vendor/symfony/console/Formatter/OutputFormatter.php b/vendor/symfony/console/Formatter/OutputFormatter.php
new file mode 100644
index 0000000..8e81e59
--- /dev/null
+++ b/vendor/symfony/console/Formatter/OutputFormatter.php
@@ -0,0 +1,268 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Formatter;
13
14use Symfony\Component\Console\Exception\InvalidArgumentException;
15
16use function Symfony\Component\String\b;
17
18/**
19 * Formatter class for console output.
20 *
21 * @author Konstantin Kudryashov <ever.zet@gmail.com>
22 * @author Roland Franssen <franssen.roland@gmail.com>
23 */
24class OutputFormatter implements WrappableOutputFormatterInterface
25{
26 private bool $decorated;
27 private array $styles = [];
28 private OutputFormatterStyleStack $styleStack;
29
30 public function __clone()
31 {
32 $this->styleStack = clone $this->styleStack;
33 foreach ($this->styles as $key => $value) {
34 $this->styles[$key] = clone $value;
35 }
36 }
37
38 /**
39 * Escapes "<" and ">" special chars in given text.
40 */
41 public static function escape(string $text): string
42 {
43 $text = preg_replace('/([^\\\\]|^)([<>])/', '$1\\\\$2', $text);
44
45 return self::escapeTrailingBackslash($text);
46 }
47
48 /**
49 * Escapes trailing "\" in given text.
50 *
51 * @internal
52 */
53 public static function escapeTrailingBackslash(string $text): string
54 {
55 if (str_ends_with($text, '\\')) {
56 $len = \strlen($text);
57 $text = rtrim($text, '\\');
58 $text = str_replace("\0", '', $text);
59 $text .= str_repeat("\0", $len - \strlen($text));
60 }
61
62 return $text;
63 }
64
65 /**
66 * Initializes console output formatter.
67 *
68 * @param OutputFormatterStyleInterface[] $styles Array of "name => FormatterStyle" instances
69 */
70 public function __construct(bool $decorated = false, array $styles = [])
71 {
72 $this->decorated = $decorated;
73
74 $this->setStyle('error', new OutputFormatterStyle('white', 'red'));
75 $this->setStyle('info', new OutputFormatterStyle('green'));
76 $this->setStyle('comment', new OutputFormatterStyle('yellow'));
77 $this->setStyle('question', new OutputFormatterStyle('black', 'cyan'));
78
79 foreach ($styles as $name => $style) {
80 $this->setStyle($name, $style);
81 }
82
83 $this->styleStack = new OutputFormatterStyleStack();
84 }
85
86 public function setDecorated(bool $decorated): void
87 {
88 $this->decorated = $decorated;
89 }
90
91 public function isDecorated(): bool
92 {
93 return $this->decorated;
94 }
95
96 public function setStyle(string $name, OutputFormatterStyleInterface $style): void
97 {
98 $this->styles[strtolower($name)] = $style;
99 }
100
101 public function hasStyle(string $name): bool
102 {
103 return isset($this->styles[strtolower($name)]);
104 }
105
106 public function getStyle(string $name): OutputFormatterStyleInterface
107 {
108 if (!$this->hasStyle($name)) {
109 throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name));
110 }
111
112 return $this->styles[strtolower($name)];
113 }
114
115 public function format(?string $message): ?string
116 {
117 return $this->formatAndWrap($message, 0);
118 }
119
120 public function formatAndWrap(?string $message, int $width): string
121 {
122 if (null === $message) {
123 return '';
124 }
125
126 $offset = 0;
127 $output = '';
128 $openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*';
129 $closeTagRegex = '[a-z][^<>]*+';
130 $currentLineLength = 0;
131 preg_match_all("#<(($openTagRegex) | /($closeTagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE);
132 foreach ($matches[0] as $i => $match) {
133 $pos = $match[1];
134 $text = $match[0];
135
136 if (0 != $pos && '\\' == $message[$pos - 1]) {
137 continue;
138 }
139
140 // add the text up to the next tag
141 $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
142 $offset = $pos + \strlen($text);
143
144 // opening tag?
145 if ($open = '/' !== $text[1]) {
146 $tag = $matches[1][$i][0];
147 } else {
148 $tag = $matches[3][$i][0] ?? '';
149 }
150
151 if (!$open && !$tag) {
152 // </>
153 $this->styleStack->pop();
154 } elseif (null === $style = $this->createStyleFromString($tag)) {
155 $output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength);
156 } elseif ($open) {
157 $this->styleStack->push($style);
158 } else {
159 $this->styleStack->pop($style);
160 }
161 }
162
163 $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
164
165 return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
166 }
167
168 public function getStyleStack(): OutputFormatterStyleStack
169 {
170 return $this->styleStack;
171 }
172
173 /**
174 * Tries to create new style instance from string.
175 */
176 private function createStyleFromString(string $string): ?OutputFormatterStyleInterface
177 {
178 if (isset($this->styles[$string])) {
179 return $this->styles[$string];
180 }
181
182 if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) {
183 return null;
184 }
185
186 $style = new OutputFormatterStyle();
187 foreach ($matches as $match) {
188 array_shift($match);
189 $match[0] = strtolower($match[0]);
190
191 if ('fg' == $match[0]) {
192 $style->setForeground(strtolower($match[1]));
193 } elseif ('bg' == $match[0]) {
194 $style->setBackground(strtolower($match[1]));
195 } elseif ('href' === $match[0]) {
196 $url = preg_replace('{\\\\([<>])}', '$1', $match[1]);
197 $style->setHref($url);
198 } elseif ('options' === $match[0]) {
199 preg_match_all('([^,;]+)', strtolower($match[1]), $options);
200 $options = array_shift($options);
201 foreach ($options as $option) {
202 $style->setOption($option);
203 }
204 } else {
205 return null;
206 }
207 }
208
209 return $style;
210 }
211
212 /**
213 * Applies current style from stack to text, if must be applied.
214 */
215 private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string
216 {
217 if ('' === $text) {
218 return '';
219 }
220
221 if (!$width) {
222 return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text;
223 }
224
225 if (!$currentLineLength && '' !== $current) {
226 $text = ltrim($text);
227 }
228
229 if ($currentLineLength) {
230 $prefix = substr($text, 0, $i = $width - $currentLineLength)."\n";
231 $text = substr($text, $i);
232 } else {
233 $prefix = '';
234 }
235
236 preg_match('~(\\n)$~', $text, $matches);
237 $text = $prefix.$this->addLineBreaks($text, $width);
238 $text = rtrim($text, "\n").($matches[1] ?? '');
239
240 if (!$currentLineLength && '' !== $current && !str_ends_with($current, "\n")) {
241 $text = "\n".$text;
242 }
243
244 $lines = explode("\n", $text);
245
246 foreach ($lines as $line) {
247 $currentLineLength += \strlen($line);
248 if ($width <= $currentLineLength) {
249 $currentLineLength = 0;
250 }
251 }
252
253 if ($this->isDecorated()) {
254 foreach ($lines as $i => $line) {
255 $lines[$i] = $this->styleStack->getCurrent()->apply($line);
256 }
257 }
258
259 return implode("\n", $lines);
260 }
261
262 private function addLineBreaks(string $text, int $width): string
263 {
264 $encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8';
265
266 return b($text)->toCodePointString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding);
267 }
268}