ANSI::move()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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