Test Failed
Push — master ( 16186d...411172 )
by Alec
02:49
created

ConsoleColor::refineColorLevel()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

Changes 0
Metric Value
cc 3
eloc 10
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 14
ccs 6
cts 7
cp 0.8571
crap 3.0261
rs 9.9332
1
<?php declare(strict_types=1);
2
3
namespace AlecRabbit\ConsoleColour;
4
5
use AlecRabbit\Cli\Tools\Core\Terminal;
6
use AlecRabbit\ConsoleColour\Contracts\Styles;
7
use AlecRabbit\ConsoleColour\Core\Contracts\ConsoleColorInterface;
8
use AlecRabbit\ConsoleColour\Exception\InvalidStyleException;
9
use const AlecRabbit\COLOR256_TERMINAL;
10
use const AlecRabbit\COLOR_TERMINAL;
11
use const AlecRabbit\CSI;
12
use const AlecRabbit\NO_COLOR_TERMINAL;
13
use const AlecRabbit\TRUECOLOR_TERMINAL;
14
15
class ConsoleColor implements ConsoleColorInterface
16
{
17
    /** @var bool */
18
    protected $supported;
19
    /** @var bool */
20
    protected $forced;
21
    /** @var array */
22
    protected $themes = [];
23
    /** @var int */
24
    protected $colorLevel;
25
26
    /**
27
     * ConsoleColor constructor.
28
     * @param null|bool|resource $stream
29
     * @param null|int $colorLevel
30
     */
31
    public function __construct($stream = null, ?int $colorLevel = null)
32
    {
33 30
        $this->colorLevel = $this->refineColorLevel($stream, $colorLevel);
34
    }
35 30
36 30
    /**
37 30
     * @param mixed $stream
38
     * @param null|int $colorLevel
39 7
     * @return int
40
     */
41 7
    protected function refineColorLevel($stream, ?int $colorLevel): int
42 7
    {
43 7
        $colorSupport = Terminal::colorSupport($stream);
44 7
        if (null === $colorLevel) {
45
            $this->supported = $colorSupport >= COLOR_TERMINAL;
46
            $this->forced = false;
47
            return $colorSupport;
48
        }
49
        $this->supported = $colorSupport >= $colorLevel;
50
        $this->forced = $colorSupport < $colorLevel;
51 7
        if (NO_COLOR_TERMINAL === $colorLevel) {
52
            $this->supported = false;
53 7
        }
54
        return $colorLevel;
55
    }
56
57
    /**
58 7
     * @return int
59
     */
60
    public function getColorLevel(): int
61 17
    {
62
        return $this->colorLevel;
63 17
    }
64 12
65
    /** {@inheritdoc} */
66
    public function apply($styles, $text): string
67
    {
68 17
        if (!$this->isApplicable()) {
69 17
            return $text;
70
        }
71
72 11
        $sequences =
73 2
            $this->getSequencesFrom(
74
                $this->refineStyles($styles)
75
            );
76 10
77
        if (empty($sequences)) {
78
            return $text;
79
        }
80 23
81
        return $this->applySequences($text, $sequences);
82 23
    }
83
84
    /** {@inheritdoc} */
85
    public function isApplicable(): bool
86
    {
87
        return $this->supported || $this->forced;
88
    }
89
90 15
//    /** {@inheritdoc} */
91
//    public function isSupported(?int $colorLevel = null): bool
92 15
//    {
93
//        return $this->supported || $this->forced;
94 15
//    }
95 15
//
96 2
    /**
97 15
     * @param array $styles
98 11
     * @return array
99
     * @throws InvalidStyleException
100 4
     */
101
    protected function getSequencesFrom(array $styles): array
102
    {
103
        $sequences = [[]];
104
105 11
        foreach ($styles as $style) {
106 11
            if (isset($this->themes[$style])) {
107
                $sequences[] = $this->themeSequence($style);
108
            } elseif ($this->isValid($style)) {
109
                $sequences[][] = $this->styleSequence($style);
110
            } else {
111
                throw new InvalidStyleException($style);
112
            }
113 11
        }
114 11
115
        return
116
            array_filter(
117
                array_merge(...$sequences),
118
                /**
119
                 * @param mixed $val
120
                 * @return bool
121
                 * @psalm-suppress MissingClosureParamType
122 2
                 */
123
                static function ($val): bool {
124 2
                    return $val !== null;
125 2
                }
126 2
            );
127
    }
128 2
129
    /**
130
     * @param string $name
131
     * @return string[]|null[]
132
     */
133
    protected function themeSequence($name): array
134
    {
135 11
        $sequences = [];
136
        foreach ($this->themes[$name] as $style) {
137 11
            $sequences[] = $this->styleSequence($style);
138 10
        }
139
        return $sequences;
140
    }
141 3
142 3
    /**
143
     * @param string $style
144
     * @return null|string
145
     */
146 3
    protected function styleSequence($style): ?string
147
    {
148
        if (\array_key_exists($style, static::CODES)) {
149
            return static::CODES[$style];
150 4
        }
151
152 4
        if (!$this->are256ColorsSupported()) {
153
            return null;
154
        }
155
156
        return
157
            $this->process256ColorStyle($style);
158
    }
159 3
160
    /** {@inheritdoc} */
161 3
    public function are256ColorsSupported(): bool
162
    {
163 3
        return $this->colorLevel >= COLOR256_TERMINAL;
164 3
    }
165 3
166 3
    /**
167
     * @param string $style
168
     * @return string
169
     */
170
    protected function process256ColorStyle(string $style): string
171
    {
172
        preg_match(self::COLOR256_REGEXP, $style, $matches);
173
        return
174 25
            sprintf(
175
                '%s;5;%s',
176
                $matches[1] === self::BG ? self::BACKGROUND : self::FOREGROUND,
177 25
                $matches[2]
178
            );
179
    }
180
181
    /**
182
     * @param string $style
183
     * @return bool
184 27
     */
185
    protected function isValid($style): bool
186 27
    {
187 15
        return
188
            \array_key_exists($style, static::CODES) || (bool)preg_match(self::COLOR256_REGEXP, $style);
189 27
    }
190 25
191
    /**
192
     * @param mixed $styles
193
     * @return array
194
     */
195
    protected function refineStyles($styles): array
196 27
    {
197
        if (\is_int($styles) || \is_string($styles)) {
198 27
            $styles = [$styles];
199 2
        }
200
        $this->assertStyles($styles);
201 25
        return $styles;
202
    }
203
204
    /**
205
     * @param mixed $styles
206
     */
207
    protected function assertStyles($styles): void
208 10
    {
209
        if (!\is_array($styles)) {
210
            throw new \InvalidArgumentException('Styles must be type of int, string or array.');
211 10
        }
212 10
    }
213 10
214
    /**
215
     * @param string $text
216
     * @param array $sequences
217
     * @return string
218
     */
219
    protected function applySequences(string $text, array $sequences): string
220 10
    {
221
        return
222
            $this->escSequence(implode(';', $sequences)) .
223 10
            $text .
224
            $this->escSequence((string)self::RESET);
225
    }
226
227 1
    /**
228
     * @param string $value
229 1
     * @return string
230
     */
231
    protected function escSequence(string $value): string
232
    {
233 1
        return
234
            CSI . $value . 'm';
235 1
    }
236
237
    /** {@inheritdoc} */
238
    public function isTrueColorSupported(): bool
239 1
    {
240
        return $this->colorLevel >= TRUECOLOR_TERMINAL;
241 1
    }
242 1
243 1
    /** {@inheritdoc} */
244
    public function isForced(): bool
245 1
    {
246
        return $this->forced;
247
    }
248 14
249
    /** {@inheritdoc} */
250 14
    public function getThemes(): array
251 1
    {
252
        return $this->themes;
253
    }
254 14
255
    /** {@inheritdoc} */
256 14
    public function setThemes(array $themes): void
257 14
    {
258 4
        $this->themes = [];
259
        foreach ($themes as $name => $styles) {
260
            $this->addTheme($name, $styles);
261 10
        }
262 10
    }
263
264
    /** {@inheritdoc} */
265 1
    public function addTheme($name, $styles, bool $override = false): void
266
    {
267 1
        if (\array_key_exists($name, $this->themes) && false === $override) {
268
            throw new \RuntimeException('Theme [' . $name . '] is already set.');
269
        }
270
271 1
        $styles = $this->refineStyles($styles);
272
273 1
        foreach ($styles as $style) {
274 1
            if (!$this->isValid($style)) {
275
                throw new InvalidStyleException($style);
276
            }
277 1
        }
278
        $this->themes[$name] = $styles;
279 1
    }
280
281
    /** {@inheritdoc} */
282
    public function hasTheme($name): bool
283
    {
284
        return isset($this->themes[$name]);
285
    }
286
287
    /** {@inheritdoc} */
288
    public function removeTheme($name): void
289
    {
290
        unset($this->themes[$name]);
291
    }
292
293
    /** {@inheritdoc} */
294
    public function getPossibleStyles(): array
295
    {
296
        return array_keys(Styles::NAMES);
297
    }
298
299
    /**
300
     * @return bool
301
     */
302
    public function isSupported(): bool
303
    {
304
        return $this->supported;
305
    }
306
}
307