Passed
Push — master ( 26c234...c8a6d1 )
by Marwan
09:32
created

tabulate()   B

Complexity

Conditions 8

Size

Total Lines 73

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
cc 8
c 0
b 0
f 0
dl 0
loc 73
ccs 0
cts 0
cp 0
crap 72
rs 7.3446

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
        function highlight($file, $line = null) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
190
            return (new HTML(false))
191
                ->open('div', ['class' => 'code-highlight'])
192
                    ->open('ul', ['class' => 'code'])
193
                        ->execute(function (HTML $html) use ($file, $line) {
194
                            $file   = (string)$file;
195
                            $line   = (int)$line;
196
                            $lines  = file_exists($file) ? file($file) : [];
197
                            $count  = count($lines);
198
                            $offset = $line === 0 ? $count : 5;
199
200
                            for ($i = $line - $offset; $i < $line + $offset; $i++) {
201
                                if (!($i > 0 && $i < $count)) {
202
                                    continue;
203
                                }
204
205
                                $highlightedCode = highlight_string('<?php ' . $lines[$i], true);
206
                                $highlightedCode = preg_replace(
207
                                    ['/\n/', '/<br ?\/?>/', '/&lt;\?php&nbsp;/'],
208
                                    ['', '', ''],
209
                                    $highlightedCode
210
                                );
211
212
                                $causer = $i === $line - 1;
213
                                $number = strval($i + 1);
214
215
                                if ($causer) {
216
                                    $number = str_pad('>', strlen($number), '=', STR_PAD_LEFT);
217
                                }
218
219
                                $html
220
                                    ->open('li')
221
                                        ->condition($causer === true)
222
                                        ->span($number, ['class' => 'code-line exception-line'])
223
                                        ->condition($causer === false)
224
                                        ->span($number, ['class' => 'code-line'])
225
                                        ->node($highlightedCode)
226
                                    ->close();
227
                            }
228
                        })
229
                    ->close()
230
                ->close()
231
            ->return();
232
        };
233
234
        function tabulate($trace) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
235
            return (new HTML(false))
236
                ->p('<i>Fields with * can reveal more info. * Hoverable. ** Clickable.</i>')
237
                ->open('div', ['class' => 'scrollable'])
238
                    ->open('table', ['class' => 'table'])
239
                        ->open('thead', ['class' => 'table-head'])
240
                            ->open('tr', ['class' => 'table-row'])
241
                                ->th('No.&nbsp;**', ['class' => 'table-cell compact'])
242
                                ->th('File&nbsp;*', ['class' => 'table-cell'])
243
                                ->th('Line', ['class' => 'table-cell compact'])
244
                                ->th('Class', ['class' => 'table-cell'])
245
                                ->th('Function', ['class' => 'table-cell'])
246
                                ->th('Arguments&nbsp;*', ['class' => 'table-cell'])
247
                            ->close()
248
                        ->close()
249
                        ->open('tbody', ['class' => 'table-body'])
250
                            ->execute(function (HTML $html) use ($trace) {
251
                                foreach ($trace as $i => $trace) {
252
                                    $count = (int)$i + 1;
253
254
                                    $html
255
                                        ->open('tr', ['class' => 'table-row ' . ($count % 2 == 0 ? 'even' : 'odd')])
256
                                            ->td(isset($trace['file']) ? '' : strval($count), ['class' => 'table-cell number'])
257
                                            ->td(
258
                                                isset($trace['file'])
259
                                                    ? sprintf('<a href="vscode://file/%s:%d" title="Open in VS Code">%s</a>', $trace['file'], $trace['line'], basename($trace['file']))
260
                                                    : 'N/A',
261
                                                ['class' => 'table-cell file pop-up', 'title' => $trace['file'] ?? 'N/A']
262
                                            )
263
                                            ->td(strval($trace['line'] ?? 'N/A'), ['class' => 'table-cell line'])
264
                                            ->td(strval($trace['class'] ?? 'N/A'), ['class' => 'table-cell class monospace'])
265
                                            ->td(strval($trace['function'] ?? 'N/A'), ['class' => 'table-cell function monospace'])
266
                                            ->open('td', ['class' => 'table-cell arguments monospace'])
267
                                                ->execute(function (HTML $html) use ($trace) {
268
                                                    if (!isset($trace['args'])) {
269
                                                        $html->node('NULL');
270
271
                                                        return;
272
                                                    }
273
274
                                                    foreach ($trace['args'] as $argument) {
275
                                                        $html->span(gettype($argument), [
276
                                                            'class' => 'argument pop-up',
277
                                                            'title' => htmlspecialchars(
278
                                                                Misc::callObjectMethod(Dumper::class, 'exportExpression', $argument),
0 ignored issues
show
Bug introduced by
MAKS\Velox\Helper\Dumper::class of type string is incompatible with the type object expected by parameter $object of MAKS\Velox\Helper\Misc::callObjectMethod(). ( Ignorable by Annotation )

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

278
                                                                Misc::callObjectMethod(/** @scrutinizer ignore-type */ Dumper::class, 'exportExpression', $argument),
Loading history...
279
                                                                ENT_QUOTES,
280
                                                                'UTF-8'
281
                                                            ),
282
                                                        ]);
283
                                                    }
284
                                                })
285
                                            ->close()
286
                                        ->close()
287
                                        ->execute(function (HTML $html) use ($trace, $count) {
288
                                            isset($trace['file']) && $html
289
                                                ->open('tr', ['class' => 'table-row additional', 'id' => 'trace-' . $count])
290
                                                    ->open('td', ['class' => 'table-cell', 'colspan' => 6])
291
                                                        ->open('details', ['class' => 'accordion'])
292
                                                            ->summary(strval($count), ['class' => 'accordion-summary'])
293
                                                            ->div(
294
                                                                highlight($trace['file'] ?? null, $trace['line'] ?? null),
295
                                                                ['class' => 'accordion-details']
296
                                                            )
297
                                                        ->close()
298
                                                    ->close()
299
                                                ->close();
300
                                        });
301
                                }
302
                            })
303
                        ->close()
304
                    ->close()
305
                ->close()
306
            ->return();
307
        }
308
309
        (new HTML(false))
310
            ->node('<!DOCTYPE html>')
311
            ->open('html', ['lang' => 'en'])
312
                ->open('head')
313
                    ->title('Oops! Something went wrong')
314
                    ->link(null, ['rel' => 'icon', 'href' => 'data:image/svg+xml;base64,' . base64_encode($favicon)])
315
                    ->style($style, ['type' => 'text/css'])
316
                ->close()
317
318
                ->open('body')
319
                    ->open('div', ['class' => 'container'])
320
                        ->open('section', ['class' => 'info-section'])
321
                            ->h1('Uncaught "' . Misc::transform($shortName, 'title') . '"')
322
                            ->p(
323
                                "<code><b>{$shortName}</b></code> was thrown on line <code><b>{$line}</b></code> of file " .
324
                                "<code><b>{$fileName}</b></code> which prevented further execution of the code."
325
                            )
326
                            ->open('div', ['class' => 'message'])
327
                                ->h3($name)
328
                                // we need to decode and encode because some messages come escaped
329
                                ->p(htmlspecialchars(htmlspecialchars_decode((string)$message), ENT_QUOTES, 'UTF-8'))
330
                            ->close()
331
                        ->close()
332
333
                        ->open('section', ['class' => 'capture-section'])
334
                            ->h2('Thrown in:')
335
                            ->execute(function (HTML $html) use ($file, $line) {
336
                                if (!file_exists($file)) {
337
                                    return;
338
                                }
339
340
                                $html
341
                                    ->open('p')
342
                                        ->node("File: <code><b>{$file}</b></code>")
343
                                        ->entity('nbsp')
344
                                        ->entity('nbsp')
345
                                        ->a('Open in <b>VS Code</b>', [
346
                                            'href'  => sprintf('vscode://file/%s:%d', $file, $line),
347
                                            'class' => 'button',
348
                                        ])
349
                                    ->close();
350
351
                                $html->div(highlight($file, $line), ['class' => 'scrollable']);
352
                            })
353
                        ->close()
354
355
                        ->open('section', ['class' => 'trace-section'])
356
                            ->h2('Stack trace:')
357
                            ->execute(function (HTML $html) use ($trace, $traceString) {
358
                                if (!count($trace)) {
359
                                    $html->pre($traceString);
360
361
                                    return;
362
                                }
363
364
                                $html->node(tabulate($trace));
365
                            })
366
                        ->close()
367
                    ->close()
368
                ->close()
369
            ->close()
370
        ->echo();
371
372
        App::terminate();
373
    }
374
375
    /**
376
     * Dumps an expression using `var_export()` or `print_r()`.
377
     *
378
     * @param mixed $expression
379
     *
380
     * @return string
381
     */
382 2
    private static function exportExpression($expression): string
383
    {
384 2
        $export = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $export is dead and can be removed.
Loading history...
385
386
        try {
387 2
            $export = var_export($expression, true);
388 1
        } catch (\Throwable $e) {
389 1
            $class = self::class;
390 1
            $line1 = "// {$class} failed to dump the variable. Reason: {$e->getMessage()}. " . PHP_EOL;
391 1
            $line2 = "// here is a dump of the variable using print_r()" . PHP_EOL . PHP_EOL . PHP_EOL;
392
393 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

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