Completed
Push — master ( 61e87b...b8e375 )
by Harry
01:35
created

ANSI::filter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 2
1
<?php
2
/**
3
 * This file is part of graze/console-diff-renderer.
4
 *
5
 * Copyright (c) 2017 Nature Delivered Ltd. <https://www.graze.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license https://github.com/graze/console-diff-renderer/blob/master/LICENSE.md
11
 * @link    https://github.com/graze/console-diff-renderer
12
 */
13
14
namespace Graze\DiffRenderer\Terminal;
15
16
class ANSI implements CursorInterface
17
{
18
    const ESCAPE = "\e";
19
20
    const CODE_MOVE_POSITION       = '[%d;%dH'; // line, column
21
    const CODE_MOVE_POSITION_FORCE = '[%d;%df'; // line, column
22
    const CODE_MOVE_UP_LINES       = '[%dA'; // lines
23
    const CODE_MOVE_DOWN_LINES     = '[%dB'; // lines
24
    const CODE_MOVE_FORWARD        = '[%dC'; // columns
25
    const CODE_MOVE_BACKWARDS      = '[%dD'; // columns
26
27
    const CODE_ERASE_TO_END_OF_LINE   = '[K';
28
    const CODE_ERASE_TO_START_OF_LINE = '[1K';
29
    const CODE_ERASE_LINE             = '[2K';
30
    const CODE_ERASE_DOWN             = '[J';
31
    const CODE_ERASE_UP               = '[1J';
32
    const CODE_ERASE_SCREEN           = '[2J';
33
34
    const REGEX_ANSI       = "/(?:\r|\e(?:\\[[0-9;]*[HfABCDKJcnRsurgim]|\\[=\\[[0-9]{1,2}[hl]|[c\\(\\)78DMH]))/";
35
    const REGEX_FORMAT     = "/\e\\[((?:[0-9][0-9;]*|))m/";
36
    const REGEX_STYLE_ITEM = '/\b(?:(?:([34]8);(?:2;\d{1,3};\d{1,3};\d{1,3}|5;\d{1,3}))|(\d+)(?<!38|48))\b/';
37
    const REGEX_FIRST_KEY  = '/^(\d+)/';
38
39
    const STYLE_RESET = '0';
40
41
    /** @var array */
42
    protected $filter = [];
43
    /** @var array */
44
    private $formats;
45
46
    public function __construct()
47
    {
48
        $this->formats = [
49
            '1'   => ['21', '22', '39', '49'],
50
            '2'   => ['22', '39', '49'],
51
            '3'   => ['23'],
52
            '4'   => ['24'],
53
            '5'   => ['25'],
54
            '6'   => ['25'],
55
            '7'   => ['27'],
56
            '8'   => ['28'],
57
            '9'   => ['29'],
58
            '11'  => ['10'],
59
            '12'  => ['10'],
60
            '13'  => ['10'],
61
            '14'  => ['10'],
62
            '15'  => ['10'],
63
            '16'  => ['10'],
64
            '17'  => ['10'],
65
            '18'  => ['10'],
66
            '19'  => ['10'],
67
            '20'  => ['23'],
68
            '30'  => ['39'],
69
            '31'  => ['39'],
70
            '32'  => ['39'],
71
            '33'  => ['39'],
72
            '34'  => ['39'],
73
            '35'  => ['39'],
74
            '36'  => ['39'],
75
            '37'  => ['39'],
76
            '38'  => ['39'],
77
            '40'  => ['49'],
78
            '41'  => ['49'],
79
            '42'  => ['49'],
80
            '43'  => ['49'],
81
            '44'  => ['49'],
82
            '45'  => ['49'],
83
            '46'  => ['49'],
84
            '47'  => ['49'],
85
            '48'  => ['49'],
86
            '51'  => ['54'],
87
            '52'  => ['54'],
88
            '53'  => ['55'],
89
            '60'  => ['65'],
90
            '61'  => ['65'],
91
            '62'  => ['65'],
92
            '63'  => ['65'],
93
            '64'  => ['65'],
94
            '90'  => ['39'],
95
            '91'  => ['39'],
96
            '92'  => ['39'],
97
            '93'  => ['39'],
98
            '94'  => ['39'],
99
            '95'  => ['39'],
100
            '96'  => ['39'],
101
            '97'  => ['39'],
102
            '100' => ['49'],
103
            '101' => ['49'],
104
            '102' => ['49'],
105
            '103' => ['49'],
106
            '104' => ['49'],
107
            '105' => ['49'],
108
            '106' => ['49'],
109
            '107' => ['49'],
110
        ];
111
    }
112
113
    /**
114
     * @param int $line
115
     * @param int $column
116
     *
117
     * @return string
118
     */
119
    public function move($line, $column)
120
    {
121
        return static::ESCAPE . sprintf(static::CODE_MOVE_POSITION, $line, $column);
122
    }
123
124
    /**
125
     * @param int $lines
126
     *
127
     * @return string
128
     */
129
    public function moveUp($lines)
130
    {
131
        return static::ESCAPE . sprintf(static::CODE_MOVE_UP_LINES, $lines);
132
    }
133
134
    /**
135
     * @param int $lines
136
     *
137
     * @return string
138
     */
139
    public function moveDown($lines)
140
    {
141
        return static::ESCAPE . sprintf(static::CODE_MOVE_DOWN_LINES, $lines);
142
    }
143
144
    /**
145
     * @param int $columns
146
     *
147
     * @return string
148
     */
149
    public function moveLeft($columns)
150
    {
151
        return static::ESCAPE . sprintf(static::CODE_MOVE_BACKWARDS, $columns);
152
    }
153
154
    /**
155
     * @param int $columns
156
     *
157
     * @return string
158
     */
159
    public function moveRight($columns)
160
    {
161
        return static::ESCAPE . sprintf(static::CODE_MOVE_FORWARD, $columns);
162
    }
163
164
    /**
165
     * @return string
166
     */
167
    public function eraseToEnd()
168
    {
169
        return static::ESCAPE . static::CODE_ERASE_TO_END_OF_LINE;
170
    }
171
172
    /**
173
     * @return string
174
     */
175
    public function eraseToStart()
176
    {
177
        return static::ESCAPE . static::CODE_ERASE_TO_START_OF_LINE;
178
    }
179
180
    /**
181
     * @return string
182
     */
183
    public function eraseDown()
184
    {
185
        return static::ESCAPE . static::CODE_ERASE_DOWN;
186
    }
187
188
    /**
189
     * @return string
190
     */
191
    public function eraseUp()
192
    {
193
        return static::ESCAPE . static::CODE_ERASE_UP;
194
    }
195
196
    /**
197
     * @return string
198
     */
199
    public function eraseScreen()
200
    {
201
        return static::ESCAPE . static::CODE_ERASE_SCREEN;
202
    }
203
204
    /**
205
     * Filter takes a string with Cursor movements and filters them out
206
     *
207
     * @param string $string
208
     * @param string $replacement Optional character or string to replace specific codes with
209
     *
210
     * @return string
211
     */
212
    public function filter($string, $replacement = '')
213
    {
214
        if ($replacement !== '') {
215
            return preg_replace_callback(static::REGEX_ANSI, function ($matches) use ($replacement) {
216
                return str_repeat($replacement, mb_strlen($matches[0]));
217
            }, $string);
218
        }
219
        return preg_replace(static::REGEX_ANSI, $replacement, $string);
220
    }
221
222
    /**
223
     * Gets the styling that would be active at the end of this string
224
     *
225
     * @param string $string
226
     *
227
     * @return string
228
     */
229
    public function getCurrentFormatting($string)
230
    {
231
        $stack = [];
232
        foreach ($this->getStyleStack($string) as $style) {
233
            if (preg_match(static::REGEX_FIRST_KEY, $style, $matches)) {
234
                $key = $matches[0];
235
236
                // if this is a valid setting style, add it to the stack
237
                if (array_key_exists($key, $this->formats)) {
238
                    $stack[] = ['key' => $key, 'style' => $style];
239
                } else {
240
                    // otherwise remove all elements that this turns off from the current stack
241
                    if ($key === static::STYLE_RESET) {
242
                        $stack = [];
243
                    } else {
244
                        $stack = array_filter($stack, function ($item) use ($key) {
245
                            return !in_array($key, $this->formats[$item['key']]);
246
                        });
247
                    }
248
                }
249
            }
250
        }
251
252
        if (count($stack) === 0) {
253
            return '';
254
        }
255
256
        $items = array_map(function ($item) {
257
            return $item['style'];
258
        }, $stack);
259
        return sprintf(static::ESCAPE . '[%sm', implode(';', $items));
260
    }
261
262
    /**
263
     * Get all the styles in order that should be applied at the end
264
     *
265
     * @param string $string
266
     *
267
     * @return \Generator|void Iterator of numbers representing styles
268
     */
269
    private function getStyleStack($string)
270
    {
271
        if (preg_match_all(static::REGEX_FORMAT, $string, $matches)) {
272
            foreach ($matches[1] as $grouping) {
273
                if (preg_match_all(static::REGEX_STYLE_ITEM, $grouping, $styles)) {
274
                    foreach ($styles[0] as $style) {
275
                        yield $style;
276
                    }
277
                } else {
278
                    yield static::STYLE_RESET;
279
                }
280
            }
281
        }
282
        return;
283
    }
284
}
285