Passed
Push — master ( 068bff...27cbbc )
by Kirill
03:24
created

Highlighter   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 138
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 20
lcom 1
cbo 6
dl 0
loc 138
ccs 0
cts 65
cp 0
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A add() 0 4 1
A highlight() 0 10 3
A renderLanguage() 0 23 5
A splitToken() 0 18 4
A isRenderable() 0 6 2
A renderErrorToken() 0 6 2
A renderBasicToken() 0 6 2
1
<?php
2
/**
3
 * This file is part of Railt package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace Railt\Console;
11
12
use Railt\Console\Language\Generic;
13
use Railt\Console\Language\LanguageInterface;
14
use Railt\Io\Readable;
15
use Railt\Lexer\Result\Token;
16
use Railt\Lexer\TokenInterface;
17
18
/**
19
 * Class Highlighter
20
 */
21
class Highlighter
22
{
23
    /**
24
     * @var int
25
     */
26
    private const CODE_SECTION_SIZE = 5;
27
28
    /**
29
     * @var array|LanguageInterface[]
30
     */
31
    private $languages = [];
32
33
    /**
34
     * Highlighter constructor.
35
     * @throws \InvalidArgumentException
36
     * @throws \Railt\Lexer\Exception\BadLexemeException
37
     */
38
    public function __construct()
39
    {
40
        $this->add(new Generic());
41
    }
42
43
    /**
44
     * @param LanguageInterface $language
45
     */
46
    public function add(LanguageInterface $language): void
47
    {
48
        \array_unshift($this->languages, $language);
49
    }
50
51
    /**
52
     * @param Readable $file
53
     * @param int $line
54
     * @return string
55
     */
56
    public function highlight(Readable $file, int $line): string
57
    {
58
        foreach ($this->languages as $language) {
59
            if ($language->match($file)) {
60
                return $this->renderLanguage($language, $file, $line);
61
            }
62
        }
63
64
        return $file->getContents();
65
    }
66
67
    /**
68
     * @param LanguageInterface $language
69
     * @param Readable $file
70
     * @param int $errorLine
71
     * @return string
72
     */
73
    private function renderLanguage(LanguageInterface $language, Readable $file, int $errorLine): string
74
    {
75
        $result        = '';
76
        $lastLine      = 0;
77
78
        foreach ($language->lex($file) as $token) {
79
            foreach ($this->splitToken($token) as $child) {
80
                $current = $file->getPosition($child->getOffset())->getLine();
81
82
                if ($this->isRenderable($errorLine, $current)) {
83
                    $atStart = $lastLine !== $current;
84
85
                    $result .= $errorLine === $current
86
                        ? $this->renderErrorToken($child, $current, $atStart)
87
                        : $this->renderBasicToken($language, $child, $current, $atStart);
88
                }
89
90
                $lastLine = $current;
91
            }
92
        }
93
94
        return $result;
95
    }
96
97
    /**
98
     * @param TokenInterface $token
99
     * @return iterable|TokenInterface[]
100
     */
101
    private function splitToken(TokenInterface $token): iterable
102
    {
103
        $offset = $token->getOffset();
104
        $chunks = \explode("\n", $token->getValue());
105
        $size   = \count($chunks);
106
107
        foreach ($chunks as $i => $chunk) {
108
            $result = new Token($token->getName(), $chunk, $offset);
109
110
            yield $result;
111
112
            if ($size > 1 && $size > $i + 1) {
113
                yield new Token('T_WHITESPACE', "\n", $offset++);
114
            }
115
116
            $offset += $result->getBytes();
117
        }
118
    }
119
120
    /**
121
     * @param int $errorLine
122
     * @param int $current
123
     * @return bool
124
     */
125
    private function isRenderable(int $errorLine, int $current): bool
126
    {
127
        return
128
            $current >= $errorLine - self::CODE_SECTION_SIZE &&
129
            $current <= $errorLine + self::CODE_SECTION_SIZE;
130
    }
131
132
    /**
133
     * @param TokenInterface $token
134
     * @param int $line
135
     * @param bool $start
136
     * @return string
137
     */
138
    private function renderErrorToken(TokenInterface $token, int $line, bool $start): string
139
    {
140
        $prefix = $start ? \sprintf('<fg=red;bg=white;options=bold,blink>%4d |></>', $line) : '';
141
142
        return $prefix . '<fg=white;bg=red;options=bold>' . $token->getValue() . '</>';
143
    }
144
145
    /**
146
     * @param LanguageInterface $language
147
     * @param TokenInterface $token
148
     * @param int $line
149
     * @param bool $start
150
     * @return string
151
     */
152
    private function renderBasicToken(LanguageInterface $language, TokenInterface $token, int $line, bool $start): string
153
    {
154
        $prefix = $start ? \sprintf('<fg=blue>%4d | </>', $line) : '';
155
156
        return $prefix . $language->highlight($token->getName(), $token->getValue());
157
    }
158
}
159