1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @author Marwan Al-Soltany <[email protected]> |
5
|
|
|
* @copyright Marwan Al-Soltany 2021 |
6
|
|
|
* For the full copyright and license information, please view |
7
|
|
|
* the LICENSE file that was distributed with this source code. |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
declare(strict_types=1); |
11
|
|
|
|
12
|
|
|
namespace MAKS\Velox\Helper; |
13
|
|
|
|
14
|
|
|
use MAKS\Velox\App; |
15
|
|
|
use MAKS\Velox\Frontend\HTML; |
16
|
|
|
use MAKS\Velox\Helper\Misc; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* A class that dumps variables and exception in a nice formatting. |
20
|
|
|
* |
21
|
|
|
* @package Velox\Helper |
22
|
|
|
* @since 1.0.0 |
23
|
|
|
*/ |
24
|
|
|
class Dumper |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* Accent color of exceptions page and dump block. |
28
|
|
|
* |
29
|
|
|
* @var string |
30
|
|
|
*/ |
31
|
|
|
public static string $accentColor = '#ff3a60'; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Contrast color of exceptions page and dump block. |
35
|
|
|
* |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
public static string $contrastColor = '#030035'; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Dumper CSS styles. |
42
|
|
|
* The array contains styles for: |
43
|
|
|
* - `exceptionPage` |
44
|
|
|
* - `traceBlock` |
45
|
|
|
* - `dumpBlock` |
46
|
|
|
* - `timeBlock` |
47
|
|
|
* |
48
|
|
|
* Currently set dumper colors can be inject in CSS using the `%accentColor%` and `%contrastColor%` placeholders. |
49
|
|
|
* |
50
|
|
|
* @var array |
51
|
|
|
* |
52
|
|
|
* @since 1.5.2 |
53
|
|
|
*/ |
54
|
|
|
public static array $styles = [ |
55
|
|
|
'exceptionPage' => ":root{--accent-color:%accentColor%;--contrast-color:%contrastColor%}*,::after,::before{box-sizing:border-box}body{background:#fff;font-family:-apple-system,'Fira Sans',Ubuntu,Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5;margin:0}h1,h2,h3,h4,h5,h6{margin:0}h1,h2{color:var(--accent-color)}h1{font-size:32px}h2{font-size:28px}h3{color:#fff}.container{width:85vw;max-width:1200px;min-height:100vh;background:#fff;padding:7vh 3vw 10vh 3vw;margin:0 auto;overflow:hidden}.message{background:var(--accent-color);color:#fff;padding:2em 1em;margin:0 0 3em 0;}.code{overflow-y:scroll;font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:14px;margin:0 0 3em 0;-ms-overflow-style: none;scrollbar-width: none}.code::-webkit-scrollbar{display:none}pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}ul{padding:2em 1em;margin:1em 0;background:var(--contrast-color)}ul li{white-space:pre;list-style-type:none;font-family:monospace}ul li span.line{display:inline-block;color:#fff;text-align:right;padding:4px 8px;user-select:none}ul li.exception-line span.line{color:var(--accent-color);font-weight:bold}ul li.exception-line span.line+code>span>span:not(:first-child){padding-bottom:3px;border-bottom:2px solid var(--accent-color)}.external-link{color:var(--accent-color)}.table-container{overflow-x:scroll}table{width:100%;border-collapse:collapse;border-spacing:0}table th{background:var(--contrast-color);color:#fff;text-align:left;padding-top:12px;padding-bottom:12px}table td,table th{border-bottom:1px solid rgba(0,0,0,0.15);padding:6px}table tr:nth-child(even){background-color:rgba(0,0,0,0.05)}table td.number{text-align:left}table td.line{text-align:left}table td.class,table td.function{font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:14px;font-weight:700}table td.arguments{white-space:nowrap}table td.arguments span{display:inline-block;background:rgba(0,0,0,.15);color:var(--accent-color);font-style:italic;padding:2px 4px;margin:0 4px 0 0;border-radius:4px}", |
56
|
|
|
'traceBlock' => "background:#fff;color:%accentColor%;font-family:-apple-system,'Fira Sans',Ubuntu,Helvetica,Arial,sans-serif;font-size:12px;padding:4px 8px;margin-bottom:18px;", |
57
|
|
|
'dumpBlock' => "display:table;background:%contrastColor%;color:#fff;font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:18px;padding:18px;margin-bottom:8px;", |
58
|
|
|
'timeBlock' => "display:table;background:%accentColor%;color:#fff;font-family:'Fira Code','Ubuntu Mono',Courier,monospace;font-size:12px;font-weight:bold;padding:12px;margin-bottom:8px;", |
59
|
|
|
]; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Colors of syntax tokens. |
63
|
|
|
* |
64
|
|
|
* @var array |
65
|
|
|
*/ |
66
|
|
|
public static array $syntaxHighlightColors = [ |
67
|
|
|
'comment' => '#aeaeae', |
68
|
|
|
'keyword' => '#00bfff', |
69
|
|
|
'string' => '#e4ba80', |
70
|
|
|
'default' => '#e8703a', |
71
|
|
|
'html' => '#ab8703', |
72
|
|
|
]; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Additional CSS styling of syntax tokens. |
76
|
|
|
* |
77
|
|
|
* @var array |
78
|
|
|
*/ |
79
|
|
|
public static array $syntaxHighlightStyles = [ |
80
|
|
|
'comment' => 'font-weight: lighter;', |
81
|
|
|
'keyword' => 'font-weight: bold;', |
82
|
|
|
'string' => '', |
83
|
|
|
'default' => '', |
84
|
|
|
'html' => '', |
85
|
|
|
]; |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* PHP highlighting syntax tokens. |
89
|
|
|
* |
90
|
|
|
* @var string[] |
91
|
|
|
*/ |
92
|
|
|
private static array $syntaxHighlightTokens = ['comment', 'keyword', 'string', 'default', 'html']; |
93
|
|
|
|
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Dumps a variable and dies. |
97
|
|
|
* |
98
|
|
|
* @param mixed ...$variable |
99
|
|
|
* |
100
|
|
|
* @return void The result will simply get echoed. |
101
|
|
|
* |
102
|
|
|
* @codeCoverageIgnore |
103
|
|
|
*/ |
104
|
|
|
public static function dd(...$variable): void |
105
|
|
|
{ |
106
|
|
|
self::dump(...$variable); |
107
|
|
|
|
108
|
|
|
App::terminate(); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Dumps a variable in a nice HTML block with syntax highlighting. |
113
|
|
|
* |
114
|
|
|
* @param mixed ...$variable |
115
|
|
|
* |
116
|
|
|
* @return void The result will simply get echoed. |
117
|
|
|
*/ |
118
|
2 |
|
public static function dump(...$variable): void |
119
|
|
|
{ |
120
|
2 |
|
$trace = self::getValidCallerTrace(); |
121
|
2 |
|
$blocks = self::getDumpingBlocks(); |
122
|
|
|
|
123
|
2 |
|
$dump = ''; |
124
|
|
|
|
125
|
2 |
|
foreach ($variable as $var) { |
126
|
2 |
|
$trace = sprintf($blocks['traceBlock'], $trace); |
|
|
|
|
127
|
2 |
|
$highlightedDump = self::exportExpressionWithSyntaxHighlighting($var, $trace); |
128
|
2 |
|
$dump .= sprintf($blocks['dumpBlock'], $highlightedDump); |
129
|
|
|
} |
130
|
|
|
|
131
|
2 |
|
$time = (microtime(true) - START_TIME) * 1000; |
132
|
2 |
|
$dump .= sprintf($blocks['timeBlock'], $time); |
133
|
|
|
|
134
|
2 |
|
if (self::isCli()) { |
135
|
2 |
|
echo $dump; |
136
|
|
|
|
137
|
2 |
|
return; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
// @codeCoverageIgnoreStart |
141
|
|
|
(new HTML(false)) |
142
|
|
|
->open('div', ['id' => 'dump']) |
143
|
|
|
->style('.dump * {background:transparent;padding:0;}') |
144
|
|
|
->div($dump) |
145
|
|
|
->close() |
146
|
|
|
->echo(); |
147
|
|
|
// @codeCoverageIgnoreEnd |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Dumps an exception in a nice HTML page or as string and exits the script. |
152
|
|
|
* |
153
|
|
|
* @param \Throwable $exception |
154
|
|
|
* |
155
|
|
|
* @return void The result will be echoed as HTML page or a string representation of the exception if the interface is CLI. |
156
|
|
|
* |
157
|
|
|
* @codeCoverageIgnore |
158
|
|
|
*/ |
159
|
|
|
public static function dumpException(\Throwable $exception): void |
160
|
|
|
{ |
161
|
|
|
if (self::isCli()) { |
162
|
|
|
echo $exception; |
163
|
|
|
|
164
|
|
|
App::terminate(); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
self::setSyntaxHighlighting(); |
168
|
|
|
|
169
|
|
|
$file = $exception->getFile(); |
170
|
|
|
$line = $exception->getLine(); |
171
|
|
|
$message = $exception->getMessage(); |
172
|
|
|
$trace = $exception->getTrace(); |
173
|
|
|
$traceString = $exception->getTraceAsString(); |
174
|
|
|
$name = get_class($exception); |
175
|
|
|
$filename = basename($file); |
176
|
|
|
$lines = file_exists($file) ? file($file) : null; |
177
|
|
|
|
178
|
|
|
$style = Misc::interpolate( |
179
|
|
|
static::$styles['exceptionPage'], |
180
|
|
|
[ |
181
|
|
|
'accentColor' => static::$accentColor, |
182
|
|
|
'contrastColor' => static::$contrastColor |
183
|
|
|
], |
184
|
|
|
'%%' |
185
|
|
|
); |
186
|
|
|
|
187
|
|
|
(new HTML(false)) |
188
|
|
|
->node('<!DOCTYPE html>') |
189
|
|
|
->open('html', ['lang' => 'en']) |
190
|
|
|
->open('head') |
191
|
|
|
->title('Oops, something went wrong') |
192
|
|
|
->style($style) |
193
|
|
|
->close() |
194
|
|
|
->open('body') |
195
|
|
|
->open('div', ['class' => 'container']) |
196
|
|
|
->h1("Uncaught {$name}") |
197
|
|
|
->p("<code><b>{$name}</b></code> was thrown on line <code><b>{$line}</b></code> of file <code><b>{$filename}</b></code> which prevented further execution of the code.") |
198
|
|
|
->open('div', ['class' => 'message']) |
199
|
|
|
->h3($name) |
200
|
|
|
->p((string)htmlspecialchars($message, ENT_QUOTES, 'UTF-8')) |
201
|
|
|
->close() |
202
|
|
|
->h2('Thrown in:') |
203
|
|
|
->execute(function (HTML $html) use ($file, $line, $lines) { |
204
|
|
|
if (!isset($lines)) { |
205
|
|
|
return; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$html |
209
|
|
|
->open('p') |
210
|
|
|
->node("File: <code><b>{$file}</b></code>") |
211
|
|
|
->entity('nbsp') |
212
|
|
|
->entity('nbsp') |
213
|
|
|
->entity('nbsp') |
214
|
|
|
->a('Open in <b>VS Code</b>', [ |
215
|
|
|
'href' => sprintf('vscode://file/%s:%d', $file, $line), |
216
|
|
|
'class' => 'external-link', |
217
|
|
|
]) |
218
|
|
|
->close(); |
219
|
|
|
|
220
|
|
|
$html->open('ul', ['class' => 'code']); |
221
|
|
|
for ($i = $line - 3; $i < $line + 4; $i++) { |
222
|
|
|
if ($i > 0 && $i < count($lines)) { |
223
|
|
|
$highlightedCode = highlight_string('<?php ' . $lines[$i], true); |
224
|
|
|
$highlightedCode = preg_replace( |
225
|
|
|
['/\n/', '/<br ?\/?>/', '/<\?php /'], |
226
|
|
|
['', '', ''], |
227
|
|
|
$highlightedCode |
228
|
|
|
); |
229
|
|
|
if ($i == $line - 1) { |
230
|
|
|
$arrow = str_pad('>', strlen("{$i}"), '=', STR_PAD_LEFT); |
231
|
|
|
$html |
232
|
|
|
->open('li', ['class' => 'exception-line']) |
233
|
|
|
->span($arrow, ['class' => 'line']) |
234
|
|
|
->node($highlightedCode) |
235
|
|
|
->close(); |
236
|
|
|
} else { |
237
|
|
|
$number = strval($i + 1); |
238
|
|
|
$html |
239
|
|
|
->open('li') |
240
|
|
|
->span($number, ['class' => 'line']) |
241
|
|
|
->node($highlightedCode) |
242
|
|
|
->close(); |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
$html->close(); |
247
|
|
|
}) |
248
|
|
|
|
249
|
|
|
->h2('Stack trace:') |
250
|
|
|
->execute(function (HTML $html) use ($trace, $traceString) { |
251
|
|
|
if (!count($trace)) { |
252
|
|
|
$html->pre($traceString); |
253
|
|
|
|
254
|
|
|
return; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$html->open('p') |
258
|
|
|
->i('Hover on fields with * to reveal more info.') |
259
|
|
|
->close(); |
260
|
|
|
|
261
|
|
|
$html->open('div', ['class' => 'table-container']) |
262
|
|
|
->open('table') |
263
|
|
|
->open('thead') |
264
|
|
|
->open('tr') |
265
|
|
|
->th('No.') |
266
|
|
|
->th('File *') |
267
|
|
|
->th('Line') |
268
|
|
|
->th('Class') |
269
|
|
|
->th('Function') |
270
|
|
|
->th('Arguments *') |
271
|
|
|
->close() |
272
|
|
|
->close() |
273
|
|
|
->open('tbody') |
274
|
|
|
->execute(function (HTML $html) use ($trace) { |
275
|
|
|
foreach ($trace as $i => $trace) { |
276
|
|
|
$count = (int)$i + 1; |
277
|
|
|
$html |
278
|
|
|
->open('tr', ['class' => $count % 2 == 0 ? 'even' : 'odd']) |
279
|
|
|
->td(strval($count), ['class' => 'number']) |
280
|
|
|
->td(isset($trace['file']) ? basename($trace['file']) : '', ['class' => 'file', 'title' => $trace['file'] ?? false]) |
281
|
|
|
->td(strval($trace['line'] ?? ''), ['class' => 'line']) |
282
|
|
|
->td(strval($trace['class'] ?? ''), ['class' => 'class']) |
283
|
|
|
->td(strval($trace['function'] ?? ''), ['class' => 'function']) |
284
|
|
|
->open('td', ['class' => 'arguments']) |
285
|
|
|
->execute(function (HTML $html) use ($trace) { |
286
|
|
|
if (!isset($trace['args'])) { |
287
|
|
|
$html->node('NULL'); |
288
|
|
|
|
289
|
|
|
return; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
foreach ($trace['args'] as $argument) { |
293
|
|
|
$html->span(gettype($argument), [ |
294
|
|
|
'title' => htmlspecialchars( |
295
|
|
|
Dumper::exportExpression($argument), |
296
|
|
|
ENT_QUOTES, |
297
|
|
|
'UTF-8' |
298
|
|
|
) |
299
|
|
|
]); |
300
|
|
|
} |
301
|
|
|
}) |
302
|
|
|
->close() |
303
|
|
|
->close(); |
304
|
|
|
} |
305
|
|
|
}) |
306
|
|
|
->close() |
307
|
|
|
->close() |
308
|
|
|
->close(); |
309
|
|
|
}) |
310
|
|
|
->close() |
311
|
|
|
->close() |
312
|
|
|
->close() |
313
|
|
|
->echo(); |
314
|
|
|
|
315
|
|
|
App::terminate(); |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* Dumps an expression using `var_export()` or `print_r()`. |
320
|
|
|
* |
321
|
|
|
* @param mixed $expression |
322
|
|
|
* |
323
|
|
|
* @return string |
324
|
|
|
*/ |
325
|
2 |
|
private static function exportExpression($expression): string |
326
|
|
|
{ |
327
|
2 |
|
$export = null; |
|
|
|
|
328
|
|
|
|
329
|
|
|
try { |
330
|
2 |
|
$export = var_export($expression, true); |
331
|
1 |
|
} catch (\Throwable $e) { |
332
|
1 |
|
$class = self::class; |
333
|
1 |
|
$line1 = "// {$class} failed to dump the variable. Reason: {$e->getMessage()}. " . PHP_EOL; |
334
|
1 |
|
$line2 = "// here is a dump of the variable using print_r()" . PHP_EOL . PHP_EOL . PHP_EOL; |
335
|
|
|
|
336
|
1 |
|
return $line1 . $line2 . print_r($expression, true); |
|
|
|
|
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
// convert array construct to square brackets |
340
|
|
|
$acToSbPatterns = [ |
341
|
1 |
|
'/(\()array\(/' => '$1[', |
342
|
|
|
'/\)(\))/' => ']$1', |
343
|
|
|
'/array \(/' => '[', |
344
|
|
|
'/\(object\) array\(/' => '(object)[', |
345
|
|
|
'/^([ ]*)\)(,?)$/m' => '$1]$2', |
346
|
|
|
'/\[\n\]/' => '[]', |
347
|
|
|
'/\[[ ]?\n[ ]+\]/' => '[]', |
348
|
|
|
'/=>[ ]?\n[ ]+(\[|\()/' => '=> $1', |
349
|
|
|
'/=>[ ]?\n[ ]+([a-zA-Z0-9_\x7f-\xff])/' => '=> $1', |
350
|
|
|
'/(\n)([ ]*)\]\)/' => '$1$2 ])', |
351
|
|
|
'/([ ]*)(\'[^\']+\') => ([\[\'])/' => '$1$2 => $3', |
352
|
|
|
]; |
353
|
|
|
|
354
|
1 |
|
return preg_replace( |
355
|
1 |
|
array_keys($acToSbPatterns), |
356
|
1 |
|
array_values($acToSbPatterns), |
357
|
|
|
$export |
358
|
|
|
); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* Dumps an expression using `var_export()` or `print_r()` with syntax highlighting. |
363
|
|
|
* |
364
|
|
|
* @param mixed $expression |
365
|
|
|
* @param string|null $phpReplacement `<?php` replacement. |
366
|
|
|
* |
367
|
|
|
* @return string |
368
|
|
|
*/ |
369
|
2 |
|
private static function exportExpressionWithSyntaxHighlighting($expression, ?string $phpReplacement = ''): string |
370
|
|
|
{ |
371
|
2 |
|
self::setSyntaxHighlighting(); |
372
|
|
|
|
373
|
2 |
|
$export = self::exportExpression($expression); |
374
|
|
|
|
375
|
2 |
|
$code = highlight_string('<?php ' . $export, true); |
376
|
2 |
|
$html = preg_replace( |
377
|
2 |
|
'/<\?php /', |
378
|
2 |
|
$phpReplacement ?? '', |
379
|
|
|
$code |
380
|
|
|
); |
381
|
|
|
|
382
|
2 |
|
if (!self::isCli()) { |
383
|
|
|
// @codeCoverageIgnoreStart |
384
|
|
|
return $html; |
385
|
|
|
// @codeCoverageIgnoreEnd |
386
|
|
|
} |
387
|
|
|
|
388
|
2 |
|
$mixed = preg_replace_callback( |
389
|
2 |
|
'/@CLR\((#\w+)\)/', |
390
|
2 |
|
fn ($matches) => self::getAnsiCodeFromHexColor($matches[1]), |
391
|
2 |
|
preg_replace( |
392
|
2 |
|
['/<\w+\s+style="color:\s*(#[a-z0-9]+)">(.*?)<\/\w+>/im', '/<br ?\/?>/', '/ /'], |
393
|
2 |
|
["\e[@CLR($1)m$2\e[0m", "\n", " "], |
394
|
|
|
$html |
395
|
|
|
) |
396
|
|
|
); |
397
|
|
|
|
398
|
2 |
|
$ansi = trim(html_entity_decode(strip_tags($mixed))); |
399
|
|
|
|
400
|
2 |
|
return $ansi; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Returns an array containing HTML/ANSI wrapping blocks. |
405
|
|
|
* Available blocks are: `traceBlock`, `dumpBlock`, and `timeBlock`. |
406
|
|
|
* All this blocks will contain a placeholder for a `*printf()` function to inject content. |
407
|
|
|
* |
408
|
|
|
* @return void |
409
|
|
|
*/ |
410
|
2 |
|
private static function getDumpingBlocks(): array |
411
|
|
|
{ |
412
|
2 |
|
$isCli = self::isCli(); |
413
|
|
|
|
414
|
|
|
$colors = [ |
415
|
2 |
|
'accentColor' => static::$accentColor, |
416
|
2 |
|
'contrastColor' => static::$contrastColor, |
417
|
|
|
]; |
418
|
|
|
|
419
|
|
|
return [ |
420
|
2 |
|
'traceBlock' => $isCli ? "\n// \e[33;1mTRACE:\e[0m \e[34;46m[%s]\e[0m \n\n" : HTML::div('%s', [ |
421
|
2 |
|
'style' => Misc::interpolate(static::$styles['traceBlock'], $colors, '%%') |
422
|
|
|
]), |
423
|
2 |
|
'dumpBlock' => $isCli ? '%s' : HTML::div('%s', [ |
424
|
2 |
|
'style' => Misc::interpolate(static::$styles['dumpBlock'], $colors, '%%') |
425
|
|
|
]), |
426
|
2 |
|
'timeBlock' => $isCli ? "\n\n// \e[36mSTART_TIME\e[0m + \e[35m%.2f\e[0mms \n\n\n" : HTML::div('START_TIME + %.2fms', [ |
427
|
2 |
|
'style' => Misc::interpolate(static::$styles['timeBlock'], $colors, '%%') |
428
|
|
|
]), |
429
|
|
|
]; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* Returns the last caller trace before `dd()` or `dump()` if the format of `file:line`. |
434
|
|
|
* |
435
|
|
|
* @return string |
436
|
|
|
*/ |
437
|
2 |
|
private static function getValidCallerTrace(): string |
438
|
|
|
{ |
439
|
2 |
|
$trace = 'Trace: N/A'; |
440
|
|
|
|
441
|
2 |
|
array_filter(array_reverse(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)), function ($backtrace) use (&$trace) { |
442
|
2 |
|
static $hasFound = false; |
443
|
2 |
|
if (!$hasFound && in_array($backtrace['function'], ['dump', 'dd'])) { |
444
|
2 |
|
$trace = $backtrace['file'] . ':' . $backtrace['line']; |
445
|
2 |
|
$hasFound = true; |
446
|
|
|
|
447
|
2 |
|
return true; |
448
|
|
|
} |
449
|
|
|
|
450
|
2 |
|
return false; |
451
|
2 |
|
}); |
452
|
|
|
|
453
|
2 |
|
return $trace; |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
/** |
457
|
|
|
* Converts a hex color to the closest standard ANSI color code. |
458
|
|
|
* Standard ANSI colors include: black, red, green, yellow, blue, magenta, cyan and white. |
459
|
|
|
* |
460
|
|
|
* @return int |
461
|
|
|
*/ |
462
|
2 |
|
private static function getAnsiCodeFromHexColor(string $color): int |
463
|
|
|
{ |
464
|
|
|
$colors = [ |
465
|
2 |
|
'black' => ['ansi' => 30, 'rgb' => [0, 0, 0]], |
466
|
|
|
'red' => ['ansi' => 31, 'rgb' => [255, 0, 0]], |
467
|
|
|
'green' => ['ansi' => 32, 'rgb' => [0, 128, 0]], |
468
|
|
|
'yellow' => ['ansi' => 33, 'rgb' => [255, 255, 0]], |
469
|
|
|
'blue' => ['ansi' => 34, 'rgb' => [0, 0, 255]], |
470
|
|
|
'magenta' => ['ansi' => 35, 'rgb' => [255, 0, 255]], |
471
|
|
|
'cyan' => ['ansi' => 36, 'rgb' => [0, 255, 255]], |
472
|
|
|
'white' => ['ansi' => 37, 'rgb' => [255, 255, 255]], |
473
|
|
|
'default' => ['ansi' => 39, 'rgb' => [128, 128, 128]], |
474
|
|
|
]; |
475
|
|
|
|
476
|
2 |
|
$hexClr = ltrim($color, '#'); |
477
|
2 |
|
$hexNum = strval(strlen($hexClr)); |
478
|
|
|
$hexPos = [ |
479
|
2 |
|
'3' => [0, 0, 1, 1, 2, 2], |
480
|
|
|
'6' => [0, 1, 2, 3, 4, 5], |
481
|
|
|
]; |
482
|
|
|
|
483
|
|
|
[$r, $g, $b] = [ |
484
|
2 |
|
$hexClr[$hexPos[$hexNum][0]] . $hexClr[$hexPos[$hexNum][1]], |
485
|
2 |
|
$hexClr[$hexPos[$hexNum][2]] . $hexClr[$hexPos[$hexNum][3]], |
486
|
2 |
|
$hexClr[$hexPos[$hexNum][4]] . $hexClr[$hexPos[$hexNum][5]], |
487
|
|
|
]; |
488
|
|
|
|
489
|
2 |
|
$color = [hexdec($r), hexdec($g), hexdec($b)]; |
490
|
|
|
|
491
|
2 |
|
$distances = []; |
492
|
2 |
|
foreach ($colors as $name => $values) { |
493
|
2 |
|
$distances[$name] = sqrt( |
494
|
2 |
|
pow($values['rgb'][0] - $color[0], 2) + |
495
|
2 |
|
pow($values['rgb'][1] - $color[1], 2) + |
496
|
2 |
|
pow($values['rgb'][2] - $color[2], 2) |
497
|
|
|
); |
498
|
|
|
} |
499
|
|
|
|
500
|
2 |
|
$colorName = ''; |
501
|
2 |
|
$minDistance = pow(2, 30); |
502
|
2 |
|
foreach ($distances as $key => $value) { |
503
|
2 |
|
if ($value < $minDistance) { |
504
|
2 |
|
$minDistance = $value; |
505
|
2 |
|
$colorName = $key; |
506
|
|
|
} |
507
|
|
|
} |
508
|
|
|
|
509
|
2 |
|
return $colors[$colorName]['ansi']; |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
/** |
513
|
|
|
* Sets PHP syntax highlighting colors according to current class state. |
514
|
|
|
* |
515
|
|
|
* @return void |
516
|
|
|
* |
517
|
|
|
* @codeCoverageIgnore |
518
|
|
|
*/ |
519
|
|
|
private static function setSyntaxHighlighting(): void |
520
|
|
|
{ |
521
|
|
|
if (self::isCli()) { |
522
|
|
|
// use default entries for better contrast. |
523
|
|
|
return; |
524
|
|
|
} |
525
|
|
|
|
526
|
|
|
$tokens = self::$syntaxHighlightTokens; |
527
|
|
|
|
528
|
|
|
foreach ($tokens as $token) { |
529
|
|
|
$color = self::$syntaxHighlightColors[$token] ?? ini_get("highlight.{$token}"); |
530
|
|
|
$style = self::$syntaxHighlightStyles[$token] ?? chr(8); |
531
|
|
|
|
532
|
|
|
$highlighting = sprintf('%s;%s', $color, $style); |
533
|
|
|
|
534
|
|
|
ini_set("highlight.{$token}", $highlighting); |
535
|
|
|
} |
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
/** |
539
|
|
|
* Checks whether the script is currently running in CLI mode or not. |
540
|
|
|
* |
541
|
|
|
* @return bool |
542
|
|
|
*/ |
543
|
2 |
|
private static function isCli(): bool |
544
|
|
|
{ |
545
|
2 |
|
return PHP_SAPI === 'cli'; |
546
|
|
|
} |
547
|
|
|
} |
548
|
|
|
|