Passed
Push — master ( c8a6d1...766bf6 )
by Marwan
01:48
created

Dumper::dumpException()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 94
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 14
Bugs 0 Features 0
Metric Value
cc 4
eloc 68
c 14
b 0
f 0
nc 2
nop 1
dl 0
loc 94
ccs 0
cts 0
cp 0
crap 20
rs 8.6981

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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{--light:#fff;--dark:#000;--accent-color:%accentColor%;--contrast-color:%contrastColor%;--font-normal:-apple-system,'Fira Sans',Ubuntu,Helvetica,Arial,sans-serif;--font-mono:'Fira Code','Ubuntu Mono',Courier,monospace;--font-base-size:16px;--container-width:85vw;--container-max-width:1364px}@media (max-width:992px){:root{--font-base-size:14px;--container-width:100%;--container-max-width:100vw}}*,::after,::before{box-sizing:border-box;scrollbar-width:thin;scrollbar-color:var(--accent-color) rgba(0,0,0,.15)}::-webkit-scrollbar{width:8px;height:8px;opacity:1;-webkit-appearance:none}::-webkit-scrollbar-thumb{background:var(--accent-color);border-radius:4px}::-webkit-scrollbar-track,::selection{background:rgba(0,0,0,.15)}body{background:var(--light);color:var(--dark);font-family:var(--font-normal);font-size:var(--font-base-size);line-height:1.5;margin:0}h1,h2,h3,h4,h5,h6{margin:0}h1{color:var(--accent-color);font-size:2rem}h2{color:var(--accent-color);font-size:1.75rem}h3{color:var(--light)}p{font-size:1rem;margin:1rem 0}a{color:var(--accent-color)}a:hover{text-decoration:underline}ul{padding:1.5rem 1rem;margin:1rem 0}li{white-space:pre;list-style-type:none}pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.monospace,code{font-family:var(--font-mono);word-wrap:break-word;word-break:break-all}.container{width:var(--container-width);max-width:var(--container-max-width);min-height:100vh;background:var(--light);padding:7vh calc((var(--container-max-width) * .03)) 10vh;margin:0 auto;overflow:hidden}.capture-section,.info-section,.trace-section{margin-bottom:3rem}.message{background:var(--accent-color);color:var(--light);padding:2rem 1rem 1rem 1rem}.scrollable{overflow-x:scroll}.code{display:block;width:max-content;min-width:100%;background:var(--contrast-color);font-family:var(--font-mono);font-size:.875rem;margin:0;overflow-y:scroll;-ms-overflow-style:none;scrollbar-width:none;cursor:initial}.code::-webkit-scrollbar{display:none}.code *{background:0 0}.code-line{display:inline-block;width:calc(3ch + (2 * .75ch));background:rgba(255,255,255,.25);color:var(--light);text-align:right;padding:.25rem .75ch;margin:0 1.5ch 0 0;user-select:none}.code-line.exception-line{color:var(--accent-color);font-weight:700}.code-line.exception-line+code>span>span:not(:first-child){padding-bottom:3px;border-bottom:2px solid var(--accent-color)}.button{display:inline-block;vertical-align:baseline;background:var(--accent-color);color:var(--light);font-size:1rem;text-decoration:none;padding:.5rem 1rem;margin:0 0 1rem 0;border:none;border-radius:2.5rem;cursor:pointer}.button:hover{background:var(--contrast-color);text-decoration:inherit}.button:last-child{margin-bottom:0}.table{width:100%;border-collapse:collapse;border-spacing:0}.table .table-cell{padding:.75rem}.table .table-head .table-cell{background:var(--contrast-color);color:var(--light);text-align:left;padding-top:.75rem;padding-bottom:.75rem}.table-cell.compact{width:1%}.table-row{background:var(--light);border-top:1px solid rgba(0,0,0,.15)}.table .table-row:hover{background:rgba(0,0,0,.065)!important}.table .table-row.additional .table-cell{padding:0}.table .table-row.odd,.table .table-row.odd+.additional{background:var(--light)}.table .table-row.even,.table .table-row.even+.additional{background:rgba(0,0,0,.035)}.table .table-row.even+.additional,.table .table-row.odd+.additional{border-top:none}.pop-up{cursor:help}.line,.number{text-align:center}.class,.function{font-size:.875rem;font-weight:700}.arguments{white-space:nowrap}.argument{display:inline-block;background:rgba(0,0,0,.125);color:var(--accent-color);font-size:.875rem;font-style:italic;padding:.125rem .5rem;margin:0 .25rem 0 0;border-radius:2.5rem}.argument:hover{background:var(--accent-color);color:var(--contrast-color)}.accordion{cursor:pointer;position:relative}.accordion-summary{width:1.5rem;height:1.5rem;background:var(--accent-color);color:var(--light);line-height:1.5rem;text-align:center;list-style:none;border-radius:50%;position:absolute;top:-2.2925rem;left:1.425rem;user-select:none;cursor:pointer}.accordion-summary:hover{background:var(--contrast-color)}.accordion-details{padding:0}",
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
        $caller = self::getValidCallerTrace();
121 2
        $blocks = self::getDumpingBlocks();
122
123 2
        $dump = '';
124
125 2
        foreach ($variable as $var) {
126 2
            $trace = sprintf($blocks['traceBlock'], $caller);
0 ignored issues
show
Bug introduced by
It seems like $blocks['traceBlock'] can also be of type MAKS\Velox\Frontend\HTML; however, parameter $format of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

126
            $trace = sprintf(/** @scrutinizer ignore-type */ $blocks['traceBlock'], $caller);
Loading history...
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' => $id = 'dump-' . uniqid()])
143
                ->style("#{$id} * { 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
        $reflection  = new \ReflectionClass($exception);
170
        $file        = $exception->getFile();
171
        $line        = $exception->getLine();
172
        $message     = $exception->getMessage();
173
        $trace       = $exception->getTrace();
174
        $traceString = $exception->getTraceAsString();
175
        $name        = $reflection->getName();
176
        $shortName   = $reflection->getShortName();
177
        $fileName    = basename($file);
178
179
        $style = Misc::interpolate(
180
            static::$styles['exceptionPage'],
181
            [
182
                'accentColor'   => static::$accentColor,
183
                'contrastColor' => static::$contrastColor
184
            ],
185
            '%%'
186
        );
187
        $favicon = '<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="512" height="512"><circle cx="256" cy="256" r="256" fill="#F00" /></svg>';
188
189
        (new HTML(false))
190
            ->node('<!DOCTYPE html>')
191
            ->open('html', ['lang' => 'en'])
192
                ->open('head')
193
                    ->title('Oops! Something went wrong')
194
                    ->link(null, ['rel' => 'icon', 'href' => 'data:image/svg+xml;base64,' . base64_encode($favicon)])
195
                    ->style($style, ['type' => 'text/css'])
196
                ->close()
197
198
                ->open('body')
199
                    ->open('div', ['class' => 'container'])
200
                        ->open('section', ['class' => 'info-section'])
201
                            ->h1('Uncaught "' . Misc::transform($shortName, 'title') . '"')
202
                            ->p(
203
                                "<code><b>{$shortName}</b></code> was thrown on line <code><b>{$line}</b></code> of file " .
204
                                "<code><b>{$fileName}</b></code> which prevented further execution of the code."
205
                            )
206
                            ->open('div', ['class' => 'message'])
207
                                ->h3($name)
208
                                // we need to decode and encode because some messages come escaped
209
                                ->p(htmlspecialchars(htmlspecialchars_decode((string)$message), ENT_QUOTES, 'UTF-8'))
210
                            ->close()
211
                        ->close()
212
213
                        ->open('section', ['class' => 'capture-section'])
214
                            ->h2('Thrown in:')
215
                            ->execute(function (HTML $html) use ($file, $line) {
216
                                if (!file_exists($file)) {
217
                                    return;
218
                                }
219
220
                                $html
221
                                    ->open('p')
222
                                        ->node("File: <code><b>{$file}</b></code>")
223
                                        ->entity('nbsp')
224
                                        ->entity('nbsp')
225
                                        ->a('Open in <b>VS Code</b>', [
226
                                            'href'  => sprintf('vscode://file/%s:%d', $file, $line),
227
                                            'class' => 'button',
228
                                        ])
229
                                    ->close();
230
231
                                $html->div(Dumper::highlightedFile($file, $line), ['class' => 'scrollable']);
232
                            })
233
                        ->close()
234
235
                        ->open('section', ['class' => 'trace-section'])
236
                            ->h2('Stack trace:')
237
                            ->execute(function (HTML $html) use ($trace, $traceString) {
238
                                if (!count($trace)) {
239
                                    $html->pre($traceString);
240
241
                                    return;
242
                                }
243
244
                                $html->node(Dumper::tabulatedStacktrace($trace));
245
                            })
246
                        ->close()
247
                    ->close()
248
                ->close()
249
            ->close()
250
        ->echo();
251
252
        App::terminate();
253
    }
254
255
    /**
256
     * Highlights the passed file with the possibility to focus a specific line.
257
     *
258
     * @param string $file The file to highlight.
259
     * @param int $line The line to focus.
260
     *
261
     * @return string The hightailed file as HTML.
262
     *
263
     * @since 1.5.5
264
     *
265
     * @codeCoverageIgnore
266
     */
267
    private static function highlightedFile(string $file, ?int $line = null): string
268
    {
269
        return (new HTML(false))
270
            ->open('div', ['class' => 'code-highlight'])
271
                ->open('ul', ['class' => 'code'])
272
                    ->execute(function (HTML $html) use ($file, $line) {
273
                        $file   = (string)$file;
274
                        $line   = (int)$line;
275
                        $lines  = file_exists($file) ? file($file) : [];
276
                        $count  = count($lines);
277
                        $offset = !$line ? $count : 5;
278
279
                        for ($i = $line - $offset; $i < $line + $offset; $i++) {
280
                            if (!($i > 0 && $i < $count)) {
281
                                continue;
282
                            }
283
284
                            $highlightedCode = highlight_string('<?php ' . $lines[$i], true);
285
                            $highlightedCode = preg_replace(
286
                                ['/\n/', '/<br ?\/?>/', '/&lt;\?php&nbsp;/'],
287
                                ['', '', ''],
288
                                $highlightedCode
289
                            );
290
291
                            $causer = $i === $line - 1;
292
                            $number = strval($i + 1);
293
294
                            if ($causer) {
295
                                $number = str_pad('>', strlen($number), '=', STR_PAD_LEFT);
296
                            }
297
298
                            $html
299
                                ->open('li')
300
                                    ->condition($causer === true)
301
                                    ->span($number, ['class' => 'code-line exception-line'])
302
                                    ->condition($causer === false)
303
                                    ->span($number, ['class' => 'code-line'])
304
                                    ->node($highlightedCode)
305
                                ->close();
306
                        }
307
                    })
308
                ->close()
309
            ->close()
310
        ->return();
311
    }
312
313
    /**
314
     * Tabulates the passed stacktrace in an HTML table.
315
     *
316
     * @param array $trace Exception stacktrace array.
317
     *
318
     * @return string The tabulated trace as HTML.
319
     *
320
     * @since 1.5.5
321
     *
322
     * @codeCoverageIgnore
323
     */
324
    private static function tabulatedStacktrace(array $trace): string
325
    {
326
        return (new HTML(false))
327
            ->p('<i>Fields with * can reveal more info. * Hoverable. ** Clickable.</i>')
328
            ->open('div', ['class' => 'scrollable'])
329
                ->open('table', ['class' => 'table'])
330
                    ->open('thead', ['class' => 'table-head'])
331
                        ->open('tr', ['class' => 'table-row'])
332
                            ->th('No.&nbsp;**', ['class' => 'table-cell compact'])
333
                            ->th('File&nbsp;*', ['class' => 'table-cell'])
334
                            ->th('Line', ['class' => 'table-cell compact'])
335
                            ->th('Class', ['class' => 'table-cell'])
336
                            ->th('Function', ['class' => 'table-cell'])
337
                            ->th('Arguments&nbsp;*', ['class' => 'table-cell'])
338
                        ->close()
339
                    ->close()
340
                    ->open('tbody', ['class' => 'table-body'])
341
                        ->execute(function (HTML $html) use ($trace) {
342
                            foreach ($trace as $i => $trace) {
343
                                $count = (int)$i + 1;
344
345
                                $html
346
                                    ->open('tr', ['class' => 'table-row ' . ($count % 2 == 0 ? 'even' : 'odd')])
347
                                        ->td(isset($trace['file']) ? '' : strval($count), ['class' => 'table-cell number'])
348
                                        ->td(
349
                                            isset($trace['file'])
350
                                                ? sprintf('<a href="vscode://file/%s:%d" title="Open in VS Code">%s</a>', $trace['file'], $trace['line'], basename($trace['file']))
351
                                                : 'N/A',
352
                                            ['class' => 'table-cell file pop-up', 'title' => $trace['file'] ?? 'N/A']
353
                                        )
354
                                        ->td(strval($trace['line'] ?? 'N/A'), ['class' => 'table-cell line'])
355
                                        ->td(strval($trace['class'] ?? 'N/A'), ['class' => 'table-cell class monospace'])
356
                                        ->td(strval($trace['function'] ?? 'N/A'), ['class' => 'table-cell function monospace'])
357
                                        ->open('td', ['class' => 'table-cell arguments monospace'])
358
                                            ->execute(function (HTML $html) use ($trace) {
359
                                                if (!isset($trace['args'])) {
360
                                                    $html->node('NULL');
361
362
                                                    return;
363
                                                }
364
365
                                                foreach ($trace['args'] as $argument) {
366
                                                    $html->span(gettype($argument), [
367
                                                        'class' => 'argument pop-up',
368
                                                        'title' => htmlspecialchars(
369
                                                            Misc::callObjectMethod(Dumper::class, 'exportExpression', $argument),
370
                                                            ENT_QUOTES,
371
                                                            'UTF-8'
372
                                                        ),
373
                                                    ]);
374
                                                }
375
                                            })
376
                                        ->close()
377
                                    ->close()
378
                                    ->execute(function (HTML $html) use ($trace, $count) {
379
                                        isset($trace['file']) && $html
380
                                            ->open('tr', ['class' => 'table-row additional', 'id' => 'trace-' . $count])
381
                                                ->open('td', ['class' => 'table-cell', 'colspan' => 6])
382
                                                    ->open('details', ['class' => 'accordion'])
383
                                                        ->summary(strval($count), ['class' => 'accordion-summary'])
384
                                                        ->div(
385
                                                            Dumper::highlightedFile($trace['file'] ?? null, $trace['line'] ?? null),
0 ignored issues
show
Bug introduced by
It seems like $trace['file'] ?? null can also be of type null; however, parameter $file of MAKS\Velox\Helper\Dumper::highlightedFile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

385
                                                            Dumper::highlightedFile(/** @scrutinizer ignore-type */ $trace['file'] ?? null, $trace['line'] ?? null),
Loading history...
386
                                                            ['class' => 'accordion-details']
387
                                                        )
388
                                                    ->close()
389
                                                ->close()
390
                                            ->close();
391
                                    });
392
                            }
393
                        })
394
                    ->close()
395
                ->close()
396
            ->close()
397
        ->return();
398
    }
399
400
    /**
401
     * Dumps an expression using `var_export()` or `print_r()`.
402
     *
403
     * @param mixed $expression
404
     *
405
     * @return string
406
     */
407 2
    private static function exportExpression($expression): string
408
    {
409 2
        $export = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $export is dead and can be removed.
Loading history...
410
411
        try {
412 2
            $export = var_export($expression, true);
413 1
        } catch (\Throwable $e) {
414 1
            $class = self::class;
415 1
            $line1 = "// {$class} failed to dump the variable. Reason: {$e->getMessage()}. " . PHP_EOL;
416 1
            $line2 = "// here is a dump of the variable using print_r()" . PHP_EOL . PHP_EOL . PHP_EOL;
417
418 1
            return $line1 . $line2 . print_r($expression, true);
0 ignored issues
show
Bug introduced by
Are you sure print_r($expression, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

418
            return $line1 . $line2 . /** @scrutinizer ignore-type */ print_r($expression, true);
Loading history...
419
        }
420
421
        // convert array construct to square brackets
422
        $acToSbPatterns = [
423 1
            '/(\()array\(/'                         => '$1[',
424
            '/\)(\))/'                              => ']$1',
425
            '/array \(/'                            => '[',
426
            '/\(object\) array\(/'                  => '(object)[',
427
            '/^([ ]*)\)(,?)$/m'                     => '$1]$2',
428
            '/\[\n\]/'                              => '[]',
429
            '/\[[ ]?\n[ ]+\]/'                      => '[]',
430
            '/=>[ ]?\n[ ]+(\[|\()/'                 => '=> $1',
431
            '/=>[ ]?\n[ ]+([a-zA-Z0-9_\x7f-\xff])/' => '=> $1',
432
            '/(\n)([ ]*)\]\)/'                      => '$1$2 ])',
433
            '/([ ]*)(\'[^\']+\') => ([\[\'])/'      => '$1$2 => $3',
434
        ];
435
436 1
        return preg_replace(
437 1
            array_keys($acToSbPatterns),
438 1
            array_values($acToSbPatterns),
439
            $export
440
        );
441
    }
442
443
    /**
444
     * Dumps an expression using `var_export()` or `print_r()` with syntax highlighting.
445
     *
446
     * @param mixed $expression
447
     * @param string|null $phpReplacement `<?php` replacement.
448
     *
449
     * @return string
450
     */
451 2
    private static function exportExpressionWithSyntaxHighlighting($expression, ?string $phpReplacement = ''): string
452
    {
453 2
        self::setSyntaxHighlighting();
454
455 2
        $export = self::exportExpression($expression);
456
457 2
        $code = highlight_string('<?php ' . $export, true);
458 2
        $html = preg_replace(
459 2
            '/&lt;\?php&nbsp;/',
460 2
            $phpReplacement ?? '',
461
            $code,
462 2
            1
463
        );
464
465 2
        if (!self::isCli()) {
466
            // @codeCoverageIgnoreStart
467
            return $html;
468
            // @codeCoverageIgnoreEnd
469
        }
470
471 2
        $mixed = preg_replace_callback(
472 2
            '/@CLR\((#\w+)\)/',
473 2
            fn ($matches) => self::getAnsiCodeFromHexColor($matches[1]),
474 2
            preg_replace(
475 2
                ['/<\w+\s+style="color:\s*(#[a-z0-9]+)">(.*?)<\/\w+>/im', '/<br ?\/?>/', '/&nbsp;/'],
476 2
                ["\e[@CLR($1)m$2\e[0m", "\n", " "],
477
                $html
478
            )
479
        );
480
481 2
        $ansi = trim(html_entity_decode(strip_tags($mixed)));
482
483 2
        return $ansi;
484
    }
485
486
    /**
487
     * Returns an array containing HTML/ANSI wrapping blocks.
488
     * Available blocks are: `traceBlock`, `dumpBlock`, and `timeBlock`.
489
     * All this blocks will contain a placeholder for a `*printf()` function to inject content.
490
     *
491
     * @return void
492
     */
493 2
    private static function getDumpingBlocks(): array
494
    {
495 2
        $isCli = self::isCli();
496
497
        $colors = [
498 2
            'accentColor'   => static::$accentColor,
499 2
            'contrastColor' => static::$contrastColor,
500
        ];
501
502
        return [
503 2
            'traceBlock' => $isCli ? "\n// \e[33;1mTRACE:\e[0m \e[34;46m[%s]\e[0m \n\n" : HTML::div('%s', [
504 2
                'style' => Misc::interpolate(static::$styles['traceBlock'], $colors, '%%')
505
            ]),
506 2
            'dumpBlock' => $isCli ? '%s' : HTML::div('%s', [
507 2
                'style' => Misc::interpolate(static::$styles['dumpBlock'], $colors, '%%')
508
            ]),
509 2
            'timeBlock' => $isCli ? "\n\n// \e[36mSTART_TIME\e[0m + \e[35m%.2f\e[0mms \n\n\n" : HTML::div('START_TIME + %.2fms', [
510 2
                'style' => Misc::interpolate(static::$styles['timeBlock'], $colors, '%%')
511
            ]),
512
        ];
513
    }
514
515
    /**
516
     * Returns the last caller trace before `dd()` or `dump()` if the format of `file:line`.
517
     *
518
     * @return string
519
     */
520 2
    private static function getValidCallerTrace(): string
521
    {
522 2
        $trace = 'Trace: N/A';
523
524 2
        array_filter(array_reverse(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)), function ($backtrace) use (&$trace) {
525 2
            static $hasFound = false;
526 2
            if (!$hasFound && in_array($backtrace['function'], ['dump', 'dd'])) {
527 2
                $trace = $backtrace['file'] . ':' . $backtrace['line'];
528 2
                $hasFound = true;
529
530 2
                return true;
531
            }
532
533 2
            return false;
534 2
        });
535
536 2
        return $trace;
537
    }
538
539
    /**
540
     * Converts a hex color to the closest standard ANSI color code.
541
     * Standard ANSI colors include: black, red, green, yellow, blue, magenta, cyan and white.
542
     *
543
     * @return int
544
     */
545 2
    private static function getAnsiCodeFromHexColor(string $color): int
546
    {
547
        $colors = [
548 2
            'black'   => ['ansi' => 30, 'rgb' => [0, 0, 0]],
549
            'red'     => ['ansi' => 31, 'rgb' => [255, 0, 0]],
550
            'green'   => ['ansi' => 32, 'rgb' => [0, 128, 0]],
551
            'yellow'  => ['ansi' => 33, 'rgb' => [255, 255, 0]],
552
            'blue'    => ['ansi' => 34, 'rgb' => [0, 0, 255]],
553
            'magenta' => ['ansi' => 35, 'rgb' => [255, 0, 255]],
554
            'cyan'    => ['ansi' => 36, 'rgb' => [0, 255, 255]],
555
            'white'   => ['ansi' => 37, 'rgb' => [255, 255, 255]],
556
            'default' => ['ansi' => 39, 'rgb' => [128, 128, 128]],
557
        ];
558
559 2
        $hexClr = ltrim($color, '#');
560 2
        $hexNum = strval(strlen($hexClr));
561
        $hexPos = [
562 2
            '3' => [0, 0, 1, 1, 2, 2],
563
            '6' => [0, 1, 2, 3, 4, 5],
564
        ];
565
566
        [$r, $g, $b] = [
567 2
            $hexClr[$hexPos[$hexNum][0]] . $hexClr[$hexPos[$hexNum][1]],
568 2
            $hexClr[$hexPos[$hexNum][2]] . $hexClr[$hexPos[$hexNum][3]],
569 2
            $hexClr[$hexPos[$hexNum][4]] . $hexClr[$hexPos[$hexNum][5]],
570
        ];
571
572 2
        $color = [hexdec($r), hexdec($g), hexdec($b)];
573
574 2
        $distances = [];
575 2
        foreach ($colors as $name => $values) {
576 2
            $distances[$name] = sqrt(
577 2
                pow($values['rgb'][0] - $color[0], 2) +
578 2
                pow($values['rgb'][1] - $color[1], 2) +
579 2
                pow($values['rgb'][2] - $color[2], 2)
580
            );
581
        }
582
583 2
        $colorName = '';
584 2
        $minDistance = pow(2, 30);
585 2
        foreach ($distances as $key => $value) {
586 2
            if ($value < $minDistance) {
587 2
                $minDistance = $value;
588 2
                $colorName   = $key;
589
            }
590
        }
591
592 2
        return $colors[$colorName]['ansi'];
593
    }
594
595
    /**
596
     * Sets PHP syntax highlighting colors according to current class state.
597
     *
598
     * @return void
599
     *
600
     * @codeCoverageIgnore
601
     */
602
    private static function setSyntaxHighlighting(): void
603
    {
604
        if (self::isCli()) {
605
            // use default entries for better contrast.
606
            return;
607
        }
608
609
        $tokens = self::$syntaxHighlightTokens;
610
611
        foreach ($tokens as $token) {
612
            $color = self::$syntaxHighlightColors[$token] ?? ini_get("highlight.{$token}");
613
            $style = self::$syntaxHighlightStyles[$token] ?? chr(8);
614
615
            $highlighting = sprintf('%s;%s', $color, $style);
616
617
            ini_set("highlight.{$token}", $highlighting);
618
        }
619
    }
620
621
    /**
622
     * Checks whether the script is currently running in CLI mode or not.
623
     *
624
     * @return bool
625
     */
626 2
    private static function isCli(): bool
627
    {
628 2
        return PHP_SAPI === 'cli';
629
    }
630
}
631