Passed
Push — master ( 5fa21f...f08728 )
by Caen
03:34 queued 15s
created

DumpsDocumentState::createDumpNodeMapEntry()   B

Complexity

Conditions 8
Paths 2

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 8
eloc 21
c 4
b 0
f 0
nc 2
nop 1
dl 0
loc 34
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Hyde\Testing\Support\HtmlTesting;
6
7
use Hyde\Hyde;
8
use JetBrains\PhpStorm\NoReturn;
9
use Illuminate\Support\Collection;
10
11
use function e;
12
use function dd;
13
use function trim;
14
use function sprintf;
15
use function ucfirst;
16
use function implode;
17
use function explode;
18
use function is_array;
19
use function in_array;
20
use function array_map;
21
use function microtime;
22
use function is_numeric;
23
use function array_keys;
24
use function str_repeat;
25
use function array_filter;
26
use function base64_encode;
27
use function number_format;
28
use function memory_get_usage;
29
use function file_put_contents;
30
31
/** @internal Single use trait for {@see \Hyde\Testing\Support\HtmlTesting\TestableHtmlDocument} */
32
trait DumpsDocumentState
33
{
34
    public function getStructure(): string
35
    {
36
        // Create a structure map of the document, containing only the tag names and their children
37
38
        $structure = '';
39
40
        $this->nodes->each(function (TestableHtmlElement $node) use (&$structure): void {
41
            $structure .= $this->createStructureMapEntry($node, 0);
42
        });
43
44
        return trim($structure);
45
    }
46
47
    public function getTextRepresentation(): string
48
    {
49
        $text = '';
50
51
        $this->nodes->each(function (TestableHtmlElement $node) use (&$text): void {
52
            $text .= $this->createTextMapEntry($node);
53
        });
54
55
        $text = explode("\n", $text);
56
        $text = array_map('trim', $text);
57
        $text = array_filter($text, 'trim');
58
        $text = array_filter($text, fn (string $line): bool => in_array($line, ['(Inline style content)', '(Inline script content)']) === false);
59
60
        return trim(implode("\n", $text));
61
    }
62
63
    public function dump(bool $writeHtml = true): string
64
    {
65
        $timeStart = microtime(true);
66
        memory_get_usage(true);
67
68
        $html = $this->createAstInspectionDump();
69
70
        $timeEnd = number_format((microtime(true) - $timeStart) * 1000, 2);
71
        $memoryUsage = number_format(memory_get_usage(true) / 1024 / 1024, 2);
72
73
        $html = str_replace('{{ $footer }}', sprintf("\n<footer><p><small>Generated in %s ms, using %s MB of memory.</small></p></footer>", $timeEnd, $memoryUsage), $html);
74
75
        if ($writeHtml) {
76
            file_put_contents(Hyde::path('document-dump.html'), $html);
77
        }
78
79
        return $html;
80
    }
81
82
    #[NoReturn]
83
    public function dd(bool $writeHtml = true): void
84
    {
85
        $this->dump($writeHtml);
86
87
        dd($this->nodes);
88
    }
89
90
    protected function createStructureMapEntry(TestableHtmlElement $node, int $level): string
91
    {
92
        return sprintf("\n%s%s%s", str_repeat('    ', $level), $node->tag, $node->nodes->map(function (TestableHtmlElement $node) use ($level): string {
93
            return $this->createStructureMapEntry($node, $level + 1);
94
        })->implode(''));
95
    }
96
97
    protected function createTextMapEntry(TestableHtmlElement $node, bool $addNewline = true): string
98
    {
99
        $childEntries = $node->nodes->map(function (TestableHtmlElement $node): string {
100
            $isInline = in_array($node->tag, ['a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'br', 'button', 'cite', 'code', 'dfn', 'em', 'i', 'img', 'input', 'kbd', 'label', 'map', 'object', 'q', 'samp', 'script', 'select', 'small', 'span', 'strong', 'sub', 'sup', 'textarea', 'time', 'tt', 'var']);
101
102
            return $this->createTextMapEntry($node, ! $isInline);
103
        })->implode('');
104
105
        return sprintf('%s%s%s', $addNewline ? "\n" : ' ', $node->text, $childEntries);
106
    }
107
108
    protected function createAstInspectionDump(): string
109
    {
110
        $html = '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document Dump</title><style>body { font-family: sans-serif; } .node { margin-left: 1em; }</style></head><body><h1>Document Dump</h1>';
111
112
        $html .= '<h2>Abstract Syntax Tree Node Inspection</h2>';
113
        $openAllButton = '<script>function openAll() {document.querySelectorAll(\'details\').forEach((el) => el.open = true);}</script><a href="javascript:openAll();" onclick="this.remove();">Open all</a>';
114
        $html .= sprintf("\n<details open><summary><strong>Document</strong> <small>$openAllButton</small></summary>\n<ul>%s</ul></details>\n", $this->nodes->map(function (TestableHtmlElement $node): string {
115
            return $this->createDumpNodeMapEntry($node);
116
        })->implode(''));
117
118
        $html .= sprintf('<section style="display: flex; flex-direction: row; flex-wrap: wrap; gap: 1em;"><div>
119
            <h2>Document Preview</h2><iframe src="data:text/html;base64,%s" width="960px" height="600px"></iframe></div><div>
120
            <h2>Raw HTML</h2><textarea cols="120" rows="30" readonly style="width: 960px; height: 600px; white-space: pre; font-family: monospace;">%s</textarea>
121
        </div></section>', base64_encode($this->html), e($this->html));
122
123
        $html .= sprintf('<h3>Node Structure</h3><div style="max-width: 1440px; overflow-x: auto; border: 1px solid #333; padding: 0.5rem 1rem;"><pre><code>%s</code></pre></div>', $this->getStructure());
124
        $html .= sprintf('<h3>Text Representation</h3><div style="max-width: 1440px; overflow-x: auto; border: 1px solid #333; padding: 0.5rem 1rem;"><pre><code>%s</code></pre></div>', $this->getTextRepresentation());
125
126
        return $html.'<hr>'.'{{ $footer }}'.'</body></html>';
127
    }
128
129
    protected function createDumpNodeMapEntry(TestableHtmlElement $node): string
130
    {
131
        $data = $node->toArray();
132
133
        $list = sprintf("\n    <ul class=\"node\">\n%s  </ul>\n", implode('', array_map(function (string|iterable $value, string $key): string {
134
            if ($value instanceof Collection) {
135
                if ($value->isEmpty()) {
136
                    return sprintf("      <li><strong>%s</strong>: <span>None</span></li>\n", ucfirst($key));
137
                }
138
139
                return sprintf("      <li><strong>%s</strong>: <ul>%s</ul></li>\n", ucfirst($key), $value->map(function (TestableHtmlElement $node): string {
140
                    return $this->createDumpNodeMapEntry($node);
141
                })->implode(''));
142
            }
143
144
            if (is_array($value)) {
145
                if (! is_numeric(array_key_first($value))) {
146
                    $value = array_map(function (string $value, string $key): string {
147
                        return sprintf('%s: %s', $key, str_contains($value, ' ') ? sprintf('"%s"', $value) : $value);
148
                    }, $value, array_keys($value));
149
                }
150
                $value = implode(', ', $value);
151
            }
152
153
            return sprintf("      <li><strong>%s</strong>: <span>%s</span></li>\n", ucfirst($key), $value);
154
        }, $data, array_keys($data))));
155
156
        if ($node->text) {
157
            $title = sprintf('<%s>%s</%s>', $node->tag, e($node->text), $node->tag);
158
        } else {
159
            $title = sprintf('<%s>', $node->tag);
160
        }
161
162
        return sprintf("  <li><%s><summary><strong>%s</strong></summary>%s  </details></li>\n", $node->tag === 'html' ? 'details open' : 'details', e($title), $list);
163
    }
164
}
165