Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace Termwind\Html;
6
 
7
use Termwind\Components\Element;
8
use Termwind\Termwind;
9
use Termwind\ValueObjects\Node;
10
 
11
/**
12
 * @internal
13
 */
14
final class CodeRenderer
15
{
16
    public const TOKEN_DEFAULT = 'token_default';
17
 
18
    public const TOKEN_COMMENT = 'token_comment';
19
 
20
    public const TOKEN_STRING = 'token_string';
21
 
22
    public const TOKEN_HTML = 'token_html';
23
 
24
    public const TOKEN_KEYWORD = 'token_keyword';
25
 
26
    public const ACTUAL_LINE_MARK = 'actual_line_mark';
27
 
28
    public const LINE_NUMBER = 'line_number';
29
 
30
    private const ARROW_SYMBOL_UTF8 = '➜';
31
 
32
    private const DELIMITER_UTF8 = 'â–• '; // 'â–¶';
33
 
34
    private const LINE_NUMBER_DIVIDER = 'line_divider';
35
 
36
    private const MARKED_LINE_NUMBER = 'marked_line';
37
 
38
    private const WIDTH = 3;
39
 
40
    /**
41
     * Holds the theme.
42
     *
43
     * @var array<string, string>
44
     */
45
    private const THEME = [
46
        self::TOKEN_STRING => 'text-gray',
47
        self::TOKEN_COMMENT => 'text-gray italic',
48
        self::TOKEN_KEYWORD => 'text-magenta strong',
49
        self::TOKEN_DEFAULT => 'strong',
50
        self::TOKEN_HTML => 'text-blue strong',
51
 
52
        self::ACTUAL_LINE_MARK => 'text-red strong',
53
        self::LINE_NUMBER => 'text-gray',
54
        self::MARKED_LINE_NUMBER => 'italic strong',
55
        self::LINE_NUMBER_DIVIDER => 'text-gray',
56
    ];
57
 
58
    private string $delimiter = self::DELIMITER_UTF8;
59
 
60
    private string $arrow = self::ARROW_SYMBOL_UTF8;
61
 
62
    private const NO_MARK = '    ';
63
 
64
    /**
65
     * Highlights HTML content from a given node and converts to the content element.
66
     */
67
    public function toElement(Node $node): Element
68
    {
69
        $line = max((int) $node->getAttribute('line'), 0);
70
        $startLine = max((int) $node->getAttribute('start-line'), 1);
71
 
72
        $html = $node->getHtml();
73
        $lines = explode("\n", $html);
74
        $extraSpaces = $this->findExtraSpaces($lines);
75
 
76
        if ($extraSpaces !== '') {
77
            $lines = array_map(static function (string $line) use ($extraSpaces): string {
78
                return str_starts_with($line, $extraSpaces) ? substr($line, strlen($extraSpaces)) : $line;
79
            }, $lines);
80
            $html = implode("\n", $lines);
81
        }
82
 
83
        $tokenLines = $this->getHighlightedLines(trim($html, "\n"), $startLine);
84
        $lines = $this->colorLines($tokenLines);
85
        $lines = $this->lineNumbers($lines, $line);
86
 
87
        return Termwind::div(trim($lines, "\n"));
88
    }
89
 
90
    /**
91
     * Finds extra spaces which should be removed from HTML.
92
     *
93
     * @param  array<int, string>  $lines
94
     */
95
    private function findExtraSpaces(array $lines): string
96
    {
97
        foreach ($lines as $line) {
98
            if ($line === '') {
99
                continue;
100
            }
101
 
102
            if (preg_replace('/\s+/', '', $line) === '') {
103
                return $line;
104
            }
105
        }
106
 
107
        return '';
108
    }
109
 
110
    /**
111
     * Returns content split into lines with numbers.
112
     *
113
     * @return array<int, array<int, array{0: string, 1: non-empty-string}>>
114
     */
115
    private function getHighlightedLines(string $source, int $startLine): array
116
    {
117
        $source = str_replace(["\r\n", "\r"], "\n", $source);
118
        $tokens = $this->tokenize($source);
119
 
120
        return $this->splitToLines($tokens, $startLine - 1);
121
    }
122
 
123
    /**
124
     * Splits content into tokens.
125
     *
126
     * @return array<int, array{0: string, 1: string}>
127
     */
128
    private function tokenize(string $source): array
129
    {
130
        $tokens = token_get_all($source);
131
 
132
        $output = [];
133
        $currentType = null;
134
        $newType = self::TOKEN_KEYWORD;
135
        $buffer = '';
136
 
137
        foreach ($tokens as $token) {
138
            if (is_array($token)) {
139
                if ($token[0] !== T_WHITESPACE) {
140
                    $newType = match ($token[0]) {
141
                        T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_CLOSE_TAG, T_STRING, T_VARIABLE,
142
                        T_DIR, T_FILE, T_METHOD_C, T_DNUMBER, T_LNUMBER, T_NS_C,
143
                        T_LINE, T_CLASS_C, T_FUNC_C, T_TRAIT_C => self::TOKEN_DEFAULT,
144
                        T_COMMENT, T_DOC_COMMENT => self::TOKEN_COMMENT,
145
                        T_ENCAPSED_AND_WHITESPACE, T_CONSTANT_ENCAPSED_STRING => self::TOKEN_STRING,
146
                        T_INLINE_HTML => self::TOKEN_HTML,
147
                        default => self::TOKEN_KEYWORD
148
                    };
149
                }
150
            } else {
151
                $newType = $token === '"' ? self::TOKEN_STRING : self::TOKEN_KEYWORD;
152
            }
153
 
154
            if ($currentType === null) {
155
                $currentType = $newType;
156
            }
157
 
158
            if ($currentType !== $newType) {
159
                $output[] = [$currentType, $buffer];
160
                $buffer = '';
161
                $currentType = $newType;
162
            }
163
 
164
            $buffer .= is_array($token) ? $token[1] : $token;
165
        }
166
 
167
        $output[] = [$newType, $buffer];
168
 
169
        return $output;
170
    }
171
 
172
    /**
173
     * Splits tokens into lines.
174
     *
175
     * @param  array<int, array{0: string, 1: string}>  $tokens
176
     * @param  int  $startLine
177
     * @return array<int, array<int, array{0: string, 1: non-empty-string}>>
178
     */
179
    private function splitToLines(array $tokens, int $startLine): array
180
    {
181
        $lines = [];
182
 
183
        $line = [];
184
        foreach ($tokens as $token) {
185
            foreach (explode("\n", $token[1]) as $count => $tokenLine) {
186
                if ($count > 0) {
187
                    $lines[$startLine++] = $line;
188
                    $line = [];
189
                }
190
 
191
                if ($tokenLine === '') {
192
                    continue;
193
                }
194
 
195
                $line[] = [$token[0], $tokenLine];
196
            }
197
        }
198
 
199
        $lines[$startLine++] = $line;
200
 
201
        return $lines;
202
    }
203
 
204
    /**
205
     * Applies colors to tokens according to a color schema.
206
     *
207
     * @param  array<int, array<int, array{0: string, 1: non-empty-string}>>  $tokenLines
208
     * @return array<int, string>
209
     */
210
    private function colorLines(array $tokenLines): array
211
    {
212
        $lines = [];
213
 
214
        foreach ($tokenLines as $lineCount => $tokenLine) {
215
            $line = '';
216
            foreach ($tokenLine as $token) {
217
                [$tokenType, $tokenValue] = $token;
218
                $line .= $this->styleToken($tokenType, $tokenValue);
219
            }
220
 
221
            $lines[$lineCount] = $line;
222
        }
223
 
224
        return $lines;
225
    }
226
 
227
    /**
228
     * Prepends line numbers into lines.
229
     *
230
     * @param  array<int, string>  $lines
231
     * @param  int  $markLine
232
     * @return string
233
     */
234
    private function lineNumbers(array $lines, int $markLine): string
235
    {
236
        $lastLine = (int) array_key_last($lines);
237
        $lineLength = strlen((string) ($lastLine + 1));
238
        $lineLength = $lineLength < self::WIDTH ? self::WIDTH : $lineLength;
239
 
240
        $snippet = '';
241
        $mark = '  '.$this->arrow.' ';
242
        foreach ($lines as $i => $line) {
243
            $coloredLineNumber = $this->coloredLineNumber(self::LINE_NUMBER, $i, $lineLength);
244
 
245
            if (0 !== $markLine) {
246
                $snippet .= ($markLine === $i + 1
247
                    ? $this->styleToken(self::ACTUAL_LINE_MARK, $mark)
248
                    : self::NO_MARK
249
                );
250
 
251
                $coloredLineNumber = ($markLine === $i + 1 ?
252
                    $this->coloredLineNumber(self::MARKED_LINE_NUMBER, $i, $lineLength) :
253
                    $coloredLineNumber
254
                );
255
            }
256
 
257
            $snippet .= $coloredLineNumber;
258
            $snippet .= $this->styleToken(self::LINE_NUMBER_DIVIDER, $this->delimiter);
259
            $snippet .= $line.PHP_EOL;
260
        }
261
 
262
        return $snippet;
263
    }
264
 
265
    /**
266
     * Formats line number and applies color according to a color schema.
267
     */
268
    private function coloredLineNumber(string $token, int $lineNumber, int $length): string
269
    {
270
        return $this->styleToken(
271
            $token, str_pad((string) ($lineNumber + 1), $length, ' ', STR_PAD_LEFT)
272
        );
273
    }
274
 
275
    /**
276
     * Formats string and applies color according to a color schema.
277
     */
278
    private function styleToken(string $token, string $string): string
279
    {
280
        return (string) Termwind::span($string, self::THEME[$token]);
281
    }
282
}