Passed
Push — master ( c7ca74...f0db67 )
by Alec
02:21
created

ConsoleColor::apply()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

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