Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 200 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of Psy Shell.
5
 *
6
 * (c) 2012-2022 Justin Hileman
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Psy\Formatter;
13
 
14
use Psy\Exception\RuntimeException;
15
use Symfony\Component\Console\Formatter\OutputFormatter;
16
 
17
/**
18
 * A pretty-printer for code.
19
 */
20
class CodeFormatter implements ReflectorFormatter
21
{
22
    const LINE_MARKER = '  <urgent>></urgent> ';
23
    const NO_LINE_MARKER = '    ';
24
 
25
    const HIGHLIGHT_DEFAULT = 'default';
26
    const HIGHLIGHT_KEYWORD = 'keyword';
27
 
28
    const HIGHLIGHT_PUBLIC = 'public';
29
    const HIGHLIGHT_PROTECTED = 'protected';
30
    const HIGHLIGHT_PRIVATE = 'private';
31
 
32
    const HIGHLIGHT_CONST = 'const';
33
    const HIGHLIGHT_NUMBER = 'number';
34
    const HIGHLIGHT_STRING = 'string';
35
    const HIGHLIGHT_COMMENT = 'comment';
36
    const HIGHLIGHT_INLINE_HTML = 'inline_html';
37
 
38
    private static $tokenMap = [
39
        // Not highlighted
40
        \T_OPEN_TAG           => self::HIGHLIGHT_DEFAULT,
41
        \T_OPEN_TAG_WITH_ECHO => self::HIGHLIGHT_DEFAULT,
42
        \T_CLOSE_TAG          => self::HIGHLIGHT_DEFAULT,
43
        \T_STRING             => self::HIGHLIGHT_DEFAULT,
44
        \T_VARIABLE           => self::HIGHLIGHT_DEFAULT,
45
        \T_NS_SEPARATOR       => self::HIGHLIGHT_DEFAULT,
46
 
47
        // Visibility
48
        \T_PUBLIC    => self::HIGHLIGHT_PUBLIC,
49
        \T_PROTECTED => self::HIGHLIGHT_PROTECTED,
50
        \T_PRIVATE   => self::HIGHLIGHT_PRIVATE,
51
 
52
        // Constants
53
        \T_DIR      => self::HIGHLIGHT_CONST,
54
        \T_FILE     => self::HIGHLIGHT_CONST,
55
        \T_METHOD_C => self::HIGHLIGHT_CONST,
56
        \T_NS_C     => self::HIGHLIGHT_CONST,
57
        \T_LINE     => self::HIGHLIGHT_CONST,
58
        \T_CLASS_C  => self::HIGHLIGHT_CONST,
59
        \T_FUNC_C   => self::HIGHLIGHT_CONST,
60
        \T_TRAIT_C  => self::HIGHLIGHT_CONST,
61
 
62
        // Types
63
        \T_DNUMBER                  => self::HIGHLIGHT_NUMBER,
64
        \T_LNUMBER                  => self::HIGHLIGHT_NUMBER,
65
        \T_ENCAPSED_AND_WHITESPACE  => self::HIGHLIGHT_STRING,
66
        \T_CONSTANT_ENCAPSED_STRING => self::HIGHLIGHT_STRING,
67
 
68
        // Comments
69
        \T_COMMENT     => self::HIGHLIGHT_COMMENT,
70
        \T_DOC_COMMENT => self::HIGHLIGHT_COMMENT,
71
 
72
        // @todo something better here?
73
        \T_INLINE_HTML => self::HIGHLIGHT_INLINE_HTML,
74
    ];
75
 
76
    /**
77
     * Format the code represented by $reflector for shell output.
78
     *
79
     * @param \Reflector  $reflector
80
     * @param string|null $colorMode (deprecated and ignored)
81
     *
82
     * @return string formatted code
83
     */
84
    public static function format(\Reflector $reflector, string $colorMode = null): string
85
    {
86
        if (self::isReflectable($reflector)) {
87
            if ($code = @\file_get_contents($reflector->getFileName())) {
88
                return self::formatCode($code, self::getStartLine($reflector), $reflector->getEndLine());
89
            }
90
        }
91
 
92
        throw new RuntimeException('Source code unavailable');
93
    }
94
 
95
    /**
96
     * Format code for shell output.
97
     *
98
     * Optionally, restrict by $startLine and $endLine line numbers, or pass $markLine to add a line marker.
99
     *
100
     * @param string   $code
101
     * @param int      $startLine
102
     * @param int|null $endLine
103
     * @param int|null $markLine
104
     *
105
     * @return string formatted code
106
     */
107
    public static function formatCode(string $code, int $startLine = 1, int $endLine = null, int $markLine = null): string
108
    {
109
        $spans = self::tokenizeSpans($code);
110
        $lines = self::splitLines($spans, $startLine, $endLine);
111
        $lines = self::formatLines($lines);
112
        $lines = self::numberLines($lines, $markLine);
113
 
114
        return \implode('', \iterator_to_array($lines));
115
    }
116
 
117
    /**
118
     * Get the start line for a given Reflector.
119
     *
120
     * Tries to incorporate doc comments if possible.
121
     *
122
     * This is typehinted as \Reflector but we've narrowed the input via self::isReflectable already.
123
     *
124
     * @param \ReflectionClass|\ReflectionFunctionAbstract $reflector
125
     *
126
     * @return int
127
     */
128
    private static function getStartLine(\Reflector $reflector): int
129
    {
130
        $startLine = $reflector->getStartLine();
131
 
132
        if ($docComment = $reflector->getDocComment()) {
133
            $startLine -= \preg_match_all('/(\r\n?|\n)/', $docComment) + 1;
134
        }
135
 
136
        return \max($startLine, 1);
137
    }
138
 
139
    /**
140
     * Split code into highlight spans.
141
     *
142
     * Tokenize via \token_get_all, then map these tokens to internal highlight types, combining
143
     * adjacent spans of the same highlight type.
144
     *
145
     * @todo consider switching \token_get_all() out for PHP-Parser-based formatting at some point.
146
     *
147
     * @param string $code
148
     *
149
     * @return \Generator [$spanType, $spanText] highlight spans
150
     */
151
    private static function tokenizeSpans(string $code): \Generator
152
    {
153
        $spanType = null;
154
        $buffer = '';
155
 
156
        foreach (\token_get_all($code) as $token) {
157
            $nextType = self::nextHighlightType($token, $spanType);
158
            $spanType = $spanType ?: $nextType;
159
 
160
            if ($spanType !== $nextType) {
161
                yield [$spanType, $buffer];
162
                $spanType = $nextType;
163
                $buffer = '';
164
            }
165
 
166
            $buffer .= \is_array($token) ? $token[1] : $token;
167
        }
168
 
169
        if ($spanType !== null && $buffer !== '') {
170
            yield [$spanType, $buffer];
171
        }
172
    }
173
 
174
    /**
175
     * Given a token and the current highlight span type, compute the next type.
176
     *
177
     * @param array|string $token       \token_get_all token
178
     * @param string|null  $currentType
179
     *
180
     * @return string|null
181
     */
182
    private static function nextHighlightType($token, $currentType)
183
    {
184
        if ($token === '"') {
185
            return self::HIGHLIGHT_STRING;
186
        }
187
 
188
        if (\is_array($token)) {
189
            if ($token[0] === \T_WHITESPACE) {
190
                return $currentType;
191
            }
192
 
193
            if (\array_key_exists($token[0], self::$tokenMap)) {
194
                return self::$tokenMap[$token[0]];
195
            }
196
        }
197
 
198
        return self::HIGHLIGHT_KEYWORD;
199
    }
200
 
201
    /**
202
     * Group highlight spans into an array of lines.
203
     *
204
     * Optionally, restrict by start and end line numbers.
205
     *
206
     * @param \Generator $spans     as [$spanType, $spanText] pairs
207
     * @param int        $startLine
208
     * @param int|null   $endLine
209
     *
210
     * @return \Generator lines, each an array of [$spanType, $spanText] pairs
211
     */
212
    private static function splitLines(\Generator $spans, int $startLine = 1, int $endLine = null): \Generator
213
    {
214
        $lineNum = 1;
215
        $buffer = [];
216
 
217
        foreach ($spans as list($spanType, $spanText)) {
218
            foreach (\preg_split('/(\r\n?|\n)/', $spanText) as $index => $spanLine) {
219
                if ($index > 0) {
220
                    if ($lineNum >= $startLine) {
221
                        yield $lineNum => $buffer;
222
                    }
223
 
224
                    $lineNum++;
225
                    $buffer = [];
226
 
227
                    if ($endLine !== null && $lineNum > $endLine) {
228
                        return;
229
                    }
230
                }
231
 
232
                if ($spanLine !== '') {
233
                    $buffer[] = [$spanType, $spanLine];
234
                }
235
            }
236
        }
237
 
238
        if (!empty($buffer)) {
239
            yield $lineNum => $buffer;
240
        }
241
    }
242
 
243
    /**
244
     * Format lines of highlight spans for shell output.
245
     *
246
     * @param \Generator $spanLines lines, each an array of [$spanType, $spanText] pairs
247
     *
248
     * @return \Generator Formatted lines
249
     */
250
    private static function formatLines(\Generator $spanLines): \Generator
251
    {
252
        foreach ($spanLines as $lineNum => $spanLine) {
253
            $line = '';
254
 
255
            foreach ($spanLine as list($spanType, $spanText)) {
256
                if ($spanType === self::HIGHLIGHT_DEFAULT) {
257
                    $line .= OutputFormatter::escape($spanText);
258
                } else {
259
                    $line .= \sprintf('<%s>%s</%s>', $spanType, OutputFormatter::escape($spanText), $spanType);
260
                }
261
            }
262
 
263
            yield $lineNum => $line.\PHP_EOL;
264
        }
265
    }
266
 
267
    /**
268
     * Prepend line numbers to formatted lines.
269
     *
270
     * Lines must be in an associative array with the correct keys in order to be numbered properly.
271
     *
272
     * Optionally, pass $markLine to add a line marker.
273
     *
274
     * @param \Generator $lines    Formatted lines
275
     * @param int|null   $markLine
276
     *
277
     * @return \Generator Numbered, formatted lines
278
     */
279
    private static function numberLines(\Generator $lines, int $markLine = null): \Generator
280
    {
281
        $lines = \iterator_to_array($lines);
282
 
283
        // Figure out how much space to reserve for line numbers.
284
        \end($lines);
285
        $pad = \strlen(\key($lines));
286
 
287
        // If $markLine is before or after our line range, don't bother reserving space for the marker.
288
        if ($markLine !== null) {
289
            if ($markLine > \key($lines)) {
290
                $markLine = null;
291
            }
292
 
293
            \reset($lines);
294
            if ($markLine < \key($lines)) {
295
                $markLine = null;
296
            }
297
        }
298
 
299
        foreach ($lines as $lineNum => $line) {
300
            $mark = '';
301
            if ($markLine !== null) {
302
                $mark = ($markLine === $lineNum) ? self::LINE_MARKER : self::NO_LINE_MARKER;
303
            }
304
 
305
            yield \sprintf("%s<aside>%{$pad}s</aside>: %s", $mark, $lineNum, $line);
306
        }
307
    }
308
 
309
    /**
310
     * Check whether a Reflector instance is reflectable by this formatter.
311
     *
312
     * @param \Reflector $reflector
313
     *
314
     * @return bool
315
     */
316
    private static function isReflectable(\Reflector $reflector): bool
317
    {
318
        return ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) && \is_file($reflector->getFileName());
319
    }
320
}