Matthias Andreas Benkard | 7b2a3a1 | 2021-08-16 10:57:25 +0200 | [diff] [blame] | 1 | <?php |
| 2 | |
| 3 | /* |
| 4 | * This file is part of the Symfony package. |
| 5 | * |
| 6 | * (c) Fabien Potencier <fabien@symfony.com> |
| 7 | * |
| 8 | * For the full copyright and license information, please view the LICENSE |
| 9 | * file that was distributed with this source code. |
| 10 | */ |
| 11 | |
| 12 | namespace Symfony\Component\VarDumper\Dumper; |
| 13 | |
| 14 | use Symfony\Component\VarDumper\Cloner\Cursor; |
| 15 | use Symfony\Component\VarDumper\Cloner\Data; |
| 16 | |
| 17 | /** |
| 18 | * HtmlDumper dumps variables as HTML. |
| 19 | * |
| 20 | * @author Nicolas Grekas <p@tchwork.com> |
| 21 | */ |
| 22 | class HtmlDumper extends CliDumper |
| 23 | { |
| 24 | public static $defaultOutput = 'php://output'; |
| 25 | |
| 26 | protected static $themes = [ |
| 27 | 'dark' => [ |
| 28 | 'default' => 'background-color:#18171B; color:#FF8400; line-height:1.2em; font:12px Menlo, Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: break-all', |
| 29 | 'num' => 'font-weight:bold; color:#1299DA', |
| 30 | 'const' => 'font-weight:bold', |
| 31 | 'str' => 'font-weight:bold; color:#56DB3A', |
| 32 | 'note' => 'color:#1299DA', |
| 33 | 'ref' => 'color:#A0A0A0', |
| 34 | 'public' => 'color:#FFFFFF', |
| 35 | 'protected' => 'color:#FFFFFF', |
| 36 | 'private' => 'color:#FFFFFF', |
| 37 | 'meta' => 'color:#B729D9', |
| 38 | 'key' => 'color:#56DB3A', |
| 39 | 'index' => 'color:#1299DA', |
| 40 | 'ellipsis' => 'color:#FF8400', |
| 41 | 'ns' => 'user-select:none;', |
| 42 | ], |
| 43 | 'light' => [ |
| 44 | 'default' => 'background:none; color:#CC7832; line-height:1.2em; font:12px Menlo, Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: break-all', |
| 45 | 'num' => 'font-weight:bold; color:#1299DA', |
| 46 | 'const' => 'font-weight:bold', |
| 47 | 'str' => 'font-weight:bold; color:#629755;', |
| 48 | 'note' => 'color:#6897BB', |
| 49 | 'ref' => 'color:#6E6E6E', |
| 50 | 'public' => 'color:#262626', |
| 51 | 'protected' => 'color:#262626', |
| 52 | 'private' => 'color:#262626', |
| 53 | 'meta' => 'color:#B729D9', |
| 54 | 'key' => 'color:#789339', |
| 55 | 'index' => 'color:#1299DA', |
| 56 | 'ellipsis' => 'color:#CC7832', |
| 57 | 'ns' => 'user-select:none;', |
| 58 | ], |
| 59 | ]; |
| 60 | |
| 61 | protected $dumpHeader; |
| 62 | protected $dumpPrefix = '<pre class=sf-dump id=%s data-indent-pad="%s">'; |
| 63 | protected $dumpSuffix = '</pre><script>Sfdump(%s)</script>'; |
| 64 | protected $dumpId = 'sf-dump'; |
| 65 | protected $colors = true; |
| 66 | protected $headerIsDumped = false; |
| 67 | protected $lastDepth = -1; |
| 68 | protected $styles; |
| 69 | |
Matthias Andreas Benkard | 1ba5381 | 2022-12-27 17:32:58 +0100 | [diff] [blame^] | 70 | private array $displayOptions = [ |
Matthias Andreas Benkard | 7b2a3a1 | 2021-08-16 10:57:25 +0200 | [diff] [blame] | 71 | 'maxDepth' => 1, |
| 72 | 'maxStringLength' => 160, |
| 73 | 'fileLinkFormat' => null, |
| 74 | ]; |
Matthias Andreas Benkard | 1ba5381 | 2022-12-27 17:32:58 +0100 | [diff] [blame^] | 75 | private array $extraDisplayOptions = []; |
Matthias Andreas Benkard | 7b2a3a1 | 2021-08-16 10:57:25 +0200 | [diff] [blame] | 76 | |
| 77 | /** |
| 78 | * {@inheritdoc} |
| 79 | */ |
| 80 | public function __construct($output = null, string $charset = null, int $flags = 0) |
| 81 | { |
| 82 | AbstractDumper::__construct($output, $charset, $flags); |
| 83 | $this->dumpId = 'sf-dump-'.mt_rand(); |
| 84 | $this->displayOptions['fileLinkFormat'] = ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); |
| 85 | $this->styles = static::$themes['dark'] ?? self::$themes['dark']; |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * {@inheritdoc} |
| 90 | */ |
| 91 | public function setStyles(array $styles) |
| 92 | { |
| 93 | $this->headerIsDumped = false; |
| 94 | $this->styles = $styles + $this->styles; |
| 95 | } |
| 96 | |
| 97 | public function setTheme(string $themeName) |
| 98 | { |
| 99 | if (!isset(static::$themes[$themeName])) { |
| 100 | throw new \InvalidArgumentException(sprintf('Theme "%s" does not exist in class "%s".', $themeName, static::class)); |
| 101 | } |
| 102 | |
| 103 | $this->setStyles(static::$themes[$themeName]); |
| 104 | } |
| 105 | |
| 106 | /** |
| 107 | * Configures display options. |
| 108 | * |
| 109 | * @param array $displayOptions A map of display options to customize the behavior |
| 110 | */ |
| 111 | public function setDisplayOptions(array $displayOptions) |
| 112 | { |
| 113 | $this->headerIsDumped = false; |
| 114 | $this->displayOptions = $displayOptions + $this->displayOptions; |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Sets an HTML header that will be dumped once in the output stream. |
| 119 | */ |
| 120 | public function setDumpHeader(?string $header) |
| 121 | { |
| 122 | $this->dumpHeader = $header; |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Sets an HTML prefix and suffix that will encapse every single dump. |
| 127 | */ |
| 128 | public function setDumpBoundaries(string $prefix, string $suffix) |
| 129 | { |
| 130 | $this->dumpPrefix = $prefix; |
| 131 | $this->dumpSuffix = $suffix; |
| 132 | } |
| 133 | |
| 134 | /** |
| 135 | * {@inheritdoc} |
| 136 | */ |
Matthias Andreas Benkard | 1ba5381 | 2022-12-27 17:32:58 +0100 | [diff] [blame^] | 137 | public function dump(Data $data, $output = null, array $extraDisplayOptions = []): ?string |
Matthias Andreas Benkard | 7b2a3a1 | 2021-08-16 10:57:25 +0200 | [diff] [blame] | 138 | { |
| 139 | $this->extraDisplayOptions = $extraDisplayOptions; |
| 140 | $result = parent::dump($data, $output); |
| 141 | $this->dumpId = 'sf-dump-'.mt_rand(); |
| 142 | |
| 143 | return $result; |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * Dumps the HTML header. |
| 148 | */ |
| 149 | protected function getDumpHeader() |
| 150 | { |
| 151 | $this->headerIsDumped = $this->outputStream ?? $this->lineDumper; |
| 152 | |
| 153 | if (null !== $this->dumpHeader) { |
| 154 | return $this->dumpHeader; |
| 155 | } |
| 156 | |
| 157 | $line = str_replace('{$options}', json_encode($this->displayOptions, \JSON_FORCE_OBJECT), <<<'EOHTML' |
| 158 | <script> |
| 159 | Sfdump = window.Sfdump || (function (doc) { |
| 160 | |
| 161 | var refStyle = doc.createElement('style'), |
| 162 | rxEsc = /([.*+?^${}()|\[\]\/\\])/g, |
| 163 | idRx = /\bsf-dump-\d+-ref[012]\w+\b/, |
| 164 | keyHint = 0 <= navigator.platform.toUpperCase().indexOf('MAC') ? 'Cmd' : 'Ctrl', |
| 165 | addEventListener = function (e, n, cb) { |
| 166 | e.addEventListener(n, cb, false); |
| 167 | }; |
| 168 | |
| 169 | refStyle.innerHTML = 'pre.sf-dump .sf-dump-compact, .sf-dump-str-collapse .sf-dump-str-collapse, .sf-dump-str-expand .sf-dump-str-expand { display: none; }'; |
| 170 | (doc.documentElement.firstElementChild || doc.documentElement.children[0]).appendChild(refStyle); |
| 171 | refStyle = doc.createElement('style'); |
| 172 | (doc.documentElement.firstElementChild || doc.documentElement.children[0]).appendChild(refStyle); |
| 173 | |
| 174 | if (!doc.addEventListener) { |
| 175 | addEventListener = function (element, eventName, callback) { |
| 176 | element.attachEvent('on' + eventName, function (e) { |
| 177 | e.preventDefault = function () {e.returnValue = false;}; |
| 178 | e.target = e.srcElement; |
| 179 | callback(e); |
| 180 | }); |
| 181 | }; |
| 182 | } |
| 183 | |
| 184 | function toggle(a, recursive) { |
| 185 | var s = a.nextSibling || {}, oldClass = s.className, arrow, newClass; |
| 186 | |
| 187 | if (/\bsf-dump-compact\b/.test(oldClass)) { |
| 188 | arrow = 'â–¼'; |
| 189 | newClass = 'sf-dump-expanded'; |
| 190 | } else if (/\bsf-dump-expanded\b/.test(oldClass)) { |
| 191 | arrow = 'â–¶'; |
| 192 | newClass = 'sf-dump-compact'; |
| 193 | } else { |
| 194 | return false; |
| 195 | } |
| 196 | |
| 197 | if (doc.createEvent && s.dispatchEvent) { |
| 198 | var event = doc.createEvent('Event'); |
| 199 | event.initEvent('sf-dump-expanded' === newClass ? 'sfbeforedumpexpand' : 'sfbeforedumpcollapse', true, false); |
| 200 | |
| 201 | s.dispatchEvent(event); |
| 202 | } |
| 203 | |
| 204 | a.lastChild.innerHTML = arrow; |
| 205 | s.className = s.className.replace(/\bsf-dump-(compact|expanded)\b/, newClass); |
| 206 | |
| 207 | if (recursive) { |
| 208 | try { |
| 209 | a = s.querySelectorAll('.'+oldClass); |
| 210 | for (s = 0; s < a.length; ++s) { |
| 211 | if (-1 == a[s].className.indexOf(newClass)) { |
| 212 | a[s].className = newClass; |
| 213 | a[s].previousSibling.lastChild.innerHTML = arrow; |
| 214 | } |
| 215 | } |
| 216 | } catch (e) { |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | return true; |
| 221 | }; |
| 222 | |
| 223 | function collapse(a, recursive) { |
| 224 | var s = a.nextSibling || {}, oldClass = s.className; |
| 225 | |
| 226 | if (/\bsf-dump-expanded\b/.test(oldClass)) { |
| 227 | toggle(a, recursive); |
| 228 | |
| 229 | return true; |
| 230 | } |
| 231 | |
| 232 | return false; |
| 233 | }; |
| 234 | |
| 235 | function expand(a, recursive) { |
| 236 | var s = a.nextSibling || {}, oldClass = s.className; |
| 237 | |
| 238 | if (/\bsf-dump-compact\b/.test(oldClass)) { |
| 239 | toggle(a, recursive); |
| 240 | |
| 241 | return true; |
| 242 | } |
| 243 | |
| 244 | return false; |
| 245 | }; |
| 246 | |
| 247 | function collapseAll(root) { |
| 248 | var a = root.querySelector('a.sf-dump-toggle'); |
| 249 | if (a) { |
| 250 | collapse(a, true); |
| 251 | expand(a); |
| 252 | |
| 253 | return true; |
| 254 | } |
| 255 | |
| 256 | return false; |
| 257 | } |
| 258 | |
| 259 | function reveal(node) { |
| 260 | var previous, parents = []; |
| 261 | |
| 262 | while ((node = node.parentNode || {}) && (previous = node.previousSibling) && 'A' === previous.tagName) { |
| 263 | parents.push(previous); |
| 264 | } |
| 265 | |
| 266 | if (0 !== parents.length) { |
| 267 | parents.forEach(function (parent) { |
| 268 | expand(parent); |
| 269 | }); |
| 270 | |
| 271 | return true; |
| 272 | } |
| 273 | |
| 274 | return false; |
| 275 | } |
| 276 | |
| 277 | function highlight(root, activeNode, nodes) { |
| 278 | resetHighlightedNodes(root); |
| 279 | |
| 280 | Array.from(nodes||[]).forEach(function (node) { |
| 281 | if (!/\bsf-dump-highlight\b/.test(node.className)) { |
| 282 | node.className = node.className + ' sf-dump-highlight'; |
| 283 | } |
| 284 | }); |
| 285 | |
| 286 | if (!/\bsf-dump-highlight-active\b/.test(activeNode.className)) { |
| 287 | activeNode.className = activeNode.className + ' sf-dump-highlight-active'; |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | function resetHighlightedNodes(root) { |
| 292 | Array.from(root.querySelectorAll('.sf-dump-str, .sf-dump-key, .sf-dump-public, .sf-dump-protected, .sf-dump-private')).forEach(function (strNode) { |
| 293 | strNode.className = strNode.className.replace(/\bsf-dump-highlight\b/, ''); |
| 294 | strNode.className = strNode.className.replace(/\bsf-dump-highlight-active\b/, ''); |
| 295 | }); |
| 296 | } |
| 297 | |
| 298 | return function (root, x) { |
| 299 | root = doc.getElementById(root); |
| 300 | |
| 301 | var indentRx = new RegExp('^('+(root.getAttribute('data-indent-pad') || ' ').replace(rxEsc, '\\$1')+')+', 'm'), |
| 302 | options = {$options}, |
| 303 | elt = root.getElementsByTagName('A'), |
| 304 | len = elt.length, |
| 305 | i = 0, s, h, |
| 306 | t = []; |
| 307 | |
| 308 | while (i < len) t.push(elt[i++]); |
| 309 | |
| 310 | for (i in x) { |
| 311 | options[i] = x[i]; |
| 312 | } |
| 313 | |
| 314 | function a(e, f) { |
| 315 | addEventListener(root, e, function (e, n) { |
| 316 | if ('A' == e.target.tagName) { |
| 317 | f(e.target, e); |
| 318 | } else if ('A' == e.target.parentNode.tagName) { |
| 319 | f(e.target.parentNode, e); |
| 320 | } else { |
| 321 | n = /\bsf-dump-ellipsis\b/.test(e.target.className) ? e.target.parentNode : e.target; |
| 322 | |
| 323 | if ((n = n.nextElementSibling) && 'A' == n.tagName) { |
| 324 | if (!/\bsf-dump-toggle\b/.test(n.className)) { |
| 325 | n = n.nextElementSibling || n; |
| 326 | } |
| 327 | |
| 328 | f(n, e, true); |
| 329 | } |
| 330 | } |
| 331 | }); |
| 332 | }; |
| 333 | function isCtrlKey(e) { |
| 334 | return e.ctrlKey || e.metaKey; |
| 335 | } |
| 336 | function xpathString(str) { |
| 337 | var parts = str.match(/[^'"]+|['"]/g).map(function (part) { |
| 338 | if ("'" == part) { |
| 339 | return '"\'"'; |
| 340 | } |
| 341 | if ('"' == part) { |
| 342 | return "'\"'"; |
| 343 | } |
| 344 | |
| 345 | return "'" + part + "'"; |
| 346 | }); |
| 347 | |
| 348 | return "concat(" + parts.join(",") + ", '')"; |
| 349 | } |
| 350 | function xpathHasClass(className) { |
| 351 | return "contains(concat(' ', normalize-space(@class), ' '), ' " + className +" ')"; |
| 352 | } |
| 353 | addEventListener(root, 'mouseover', function (e) { |
| 354 | if ('' != refStyle.innerHTML) { |
| 355 | refStyle.innerHTML = ''; |
| 356 | } |
| 357 | }); |
| 358 | a('mouseover', function (a, e, c) { |
| 359 | if (c) { |
| 360 | e.target.style.cursor = "pointer"; |
| 361 | } else if (a = idRx.exec(a.className)) { |
| 362 | try { |
| 363 | refStyle.innerHTML = 'pre.sf-dump .'+a[0]+'{background-color: #B729D9; color: #FFF !important; border-radius: 2px}'; |
| 364 | } catch (e) { |
| 365 | } |
| 366 | } |
| 367 | }); |
| 368 | a('click', function (a, e, c) { |
| 369 | if (/\bsf-dump-toggle\b/.test(a.className)) { |
| 370 | e.preventDefault(); |
| 371 | if (!toggle(a, isCtrlKey(e))) { |
| 372 | var r = doc.getElementById(a.getAttribute('href').substr(1)), |
| 373 | s = r.previousSibling, |
| 374 | f = r.parentNode, |
| 375 | t = a.parentNode; |
| 376 | t.replaceChild(r, a); |
| 377 | f.replaceChild(a, s); |
| 378 | t.insertBefore(s, r); |
| 379 | f = f.firstChild.nodeValue.match(indentRx); |
| 380 | t = t.firstChild.nodeValue.match(indentRx); |
| 381 | if (f && t && f[0] !== t[0]) { |
| 382 | r.innerHTML = r.innerHTML.replace(new RegExp('^'+f[0].replace(rxEsc, '\\$1'), 'mg'), t[0]); |
| 383 | } |
| 384 | if (/\bsf-dump-compact\b/.test(r.className)) { |
| 385 | toggle(s, isCtrlKey(e)); |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | if (c) { |
| 390 | } else if (doc.getSelection) { |
| 391 | try { |
| 392 | doc.getSelection().removeAllRanges(); |
| 393 | } catch (e) { |
| 394 | doc.getSelection().empty(); |
| 395 | } |
| 396 | } else { |
| 397 | doc.selection.empty(); |
| 398 | } |
| 399 | } else if (/\bsf-dump-str-toggle\b/.test(a.className)) { |
| 400 | e.preventDefault(); |
| 401 | e = a.parentNode.parentNode; |
| 402 | e.className = e.className.replace(/\bsf-dump-str-(expand|collapse)\b/, a.parentNode.className); |
| 403 | } |
| 404 | }); |
| 405 | |
| 406 | elt = root.getElementsByTagName('SAMP'); |
| 407 | len = elt.length; |
| 408 | i = 0; |
| 409 | |
| 410 | while (i < len) t.push(elt[i++]); |
| 411 | len = t.length; |
| 412 | |
| 413 | for (i = 0; i < len; ++i) { |
| 414 | elt = t[i]; |
| 415 | if ('SAMP' == elt.tagName) { |
| 416 | a = elt.previousSibling || {}; |
| 417 | if ('A' != a.tagName) { |
| 418 | a = doc.createElement('A'); |
| 419 | a.className = 'sf-dump-ref'; |
| 420 | elt.parentNode.insertBefore(a, elt); |
| 421 | } else { |
| 422 | a.innerHTML += ' '; |
| 423 | } |
| 424 | a.title = (a.title ? a.title+'\n[' : '[')+keyHint+'+click] Expand all children'; |
| 425 | a.innerHTML += elt.className == 'sf-dump-compact' ? '<span>â–¶</span>' : '<span>â–¼</span>'; |
| 426 | a.className += ' sf-dump-toggle'; |
| 427 | |
| 428 | x = 1; |
| 429 | if ('sf-dump' != elt.parentNode.className) { |
| 430 | x += elt.parentNode.getAttribute('data-depth')/1; |
| 431 | } |
| 432 | } else if (/\bsf-dump-ref\b/.test(elt.className) && (a = elt.getAttribute('href'))) { |
| 433 | a = a.substr(1); |
| 434 | elt.className += ' '+a; |
| 435 | |
| 436 | if (/[\[{]$/.test(elt.previousSibling.nodeValue)) { |
| 437 | a = a != elt.nextSibling.id && doc.getElementById(a); |
| 438 | try { |
| 439 | s = a.nextSibling; |
| 440 | elt.appendChild(a); |
| 441 | s.parentNode.insertBefore(a, s); |
| 442 | if (/^[@#]/.test(elt.innerHTML)) { |
| 443 | elt.innerHTML += ' <span>â–¶</span>'; |
| 444 | } else { |
| 445 | elt.innerHTML = '<span>â–¶</span>'; |
| 446 | elt.className = 'sf-dump-ref'; |
| 447 | } |
| 448 | elt.className += ' sf-dump-toggle'; |
| 449 | } catch (e) { |
| 450 | if ('&' == elt.innerHTML.charAt(0)) { |
| 451 | elt.innerHTML = '…'; |
| 452 | elt.className = 'sf-dump-ref'; |
| 453 | } |
| 454 | } |
| 455 | } |
| 456 | } |
| 457 | } |
| 458 | |
| 459 | if (doc.evaluate && Array.from && root.children.length > 1) { |
| 460 | root.setAttribute('tabindex', 0); |
| 461 | |
| 462 | SearchState = function () { |
| 463 | this.nodes = []; |
| 464 | this.idx = 0; |
| 465 | }; |
| 466 | SearchState.prototype = { |
| 467 | next: function () { |
| 468 | if (this.isEmpty()) { |
| 469 | return this.current(); |
| 470 | } |
| 471 | this.idx = this.idx < (this.nodes.length - 1) ? this.idx + 1 : 0; |
| 472 | |
| 473 | return this.current(); |
| 474 | }, |
| 475 | previous: function () { |
| 476 | if (this.isEmpty()) { |
| 477 | return this.current(); |
| 478 | } |
| 479 | this.idx = this.idx > 0 ? this.idx - 1 : (this.nodes.length - 1); |
| 480 | |
| 481 | return this.current(); |
| 482 | }, |
| 483 | isEmpty: function () { |
| 484 | return 0 === this.count(); |
| 485 | }, |
| 486 | current: function () { |
| 487 | if (this.isEmpty()) { |
| 488 | return null; |
| 489 | } |
| 490 | return this.nodes[this.idx]; |
| 491 | }, |
| 492 | reset: function () { |
| 493 | this.nodes = []; |
| 494 | this.idx = 0; |
| 495 | }, |
| 496 | count: function () { |
| 497 | return this.nodes.length; |
| 498 | }, |
| 499 | }; |
| 500 | |
| 501 | function showCurrent(state) |
| 502 | { |
| 503 | var currentNode = state.current(), currentRect, searchRect; |
| 504 | if (currentNode) { |
| 505 | reveal(currentNode); |
| 506 | highlight(root, currentNode, state.nodes); |
| 507 | if ('scrollIntoView' in currentNode) { |
| 508 | currentNode.scrollIntoView(true); |
| 509 | currentRect = currentNode.getBoundingClientRect(); |
| 510 | searchRect = search.getBoundingClientRect(); |
| 511 | if (currentRect.top < (searchRect.top + searchRect.height)) { |
| 512 | window.scrollBy(0, -(searchRect.top + searchRect.height + 5)); |
| 513 | } |
| 514 | } |
| 515 | } |
| 516 | counter.textContent = (state.isEmpty() ? 0 : state.idx + 1) + ' of ' + state.count(); |
| 517 | } |
| 518 | |
| 519 | var search = doc.createElement('div'); |
| 520 | search.className = 'sf-dump-search-wrapper sf-dump-search-hidden'; |
| 521 | search.innerHTML = ' |
| 522 | <input type="text" class="sf-dump-search-input"> |
| 523 | <span class="sf-dump-search-count">0 of 0<\/span> |
| 524 | <button type="button" class="sf-dump-search-input-previous" tabindex="-1"> |
| 525 | <svg viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1683 1331l-166 165q-19 19-45 19t-45-19L896 965l-531 531q-19 19-45 19t-45-19l-166-165q-19-19-19-45.5t19-45.5l742-741q19-19 45-19t45 19l742 741q19 19 19 45.5t-19 45.5z"\/><\/svg> |
| 526 | <\/button> |
| 527 | <button type="button" class="sf-dump-search-input-next" tabindex="-1"> |
| 528 | <svg viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1683 808l-742 741q-19 19-45 19t-45-19L109 808q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"\/><\/svg> |
| 529 | <\/button> |
| 530 | '; |
| 531 | root.insertBefore(search, root.firstChild); |
| 532 | |
| 533 | var state = new SearchState(); |
| 534 | var searchInput = search.querySelector('.sf-dump-search-input'); |
| 535 | var counter = search.querySelector('.sf-dump-search-count'); |
| 536 | var searchInputTimer = 0; |
| 537 | var previousSearchQuery = ''; |
| 538 | |
| 539 | addEventListener(searchInput, 'keyup', function (e) { |
| 540 | var searchQuery = e.target.value; |
| 541 | /* Don't perform anything if the pressed key didn't change the query */ |
| 542 | if (searchQuery === previousSearchQuery) { |
| 543 | return; |
| 544 | } |
| 545 | previousSearchQuery = searchQuery; |
| 546 | clearTimeout(searchInputTimer); |
| 547 | searchInputTimer = setTimeout(function () { |
| 548 | state.reset(); |
| 549 | collapseAll(root); |
| 550 | resetHighlightedNodes(root); |
| 551 | if ('' === searchQuery) { |
| 552 | counter.textContent = '0 of 0'; |
| 553 | |
| 554 | return; |
| 555 | } |
| 556 | |
| 557 | var classMatches = [ |
| 558 | "sf-dump-str", |
| 559 | "sf-dump-key", |
| 560 | "sf-dump-public", |
| 561 | "sf-dump-protected", |
| 562 | "sf-dump-private", |
| 563 | ].map(xpathHasClass).join(' or '); |
| 564 | |
| 565 | var xpathResult = doc.evaluate('.//span[' + classMatches + '][contains(translate(child::text(), ' + xpathString(searchQuery.toUpperCase()) + ', ' + xpathString(searchQuery.toLowerCase()) + '), ' + xpathString(searchQuery.toLowerCase()) + ')]', root, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); |
| 566 | |
| 567 | while (node = xpathResult.iterateNext()) state.nodes.push(node); |
| 568 | |
| 569 | showCurrent(state); |
| 570 | }, 400); |
| 571 | }); |
| 572 | |
| 573 | Array.from(search.querySelectorAll('.sf-dump-search-input-next, .sf-dump-search-input-previous')).forEach(function (btn) { |
| 574 | addEventListener(btn, 'click', function (e) { |
| 575 | e.preventDefault(); |
| 576 | -1 !== e.target.className.indexOf('next') ? state.next() : state.previous(); |
| 577 | searchInput.focus(); |
| 578 | collapseAll(root); |
| 579 | showCurrent(state); |
| 580 | }) |
| 581 | }); |
| 582 | |
| 583 | addEventListener(root, 'keydown', function (e) { |
| 584 | var isSearchActive = !/\bsf-dump-search-hidden\b/.test(search.className); |
| 585 | if ((114 === e.keyCode && !isSearchActive) || (isCtrlKey(e) && 70 === e.keyCode)) { |
| 586 | /* F3 or CMD/CTRL + F */ |
| 587 | if (70 === e.keyCode && document.activeElement === searchInput) { |
| 588 | /* |
| 589 | * If CMD/CTRL + F is hit while having focus on search input, |
| 590 | * the user probably meant to trigger browser search instead. |
| 591 | * Let the browser execute its behavior: |
| 592 | */ |
| 593 | return; |
| 594 | } |
| 595 | |
| 596 | e.preventDefault(); |
| 597 | search.className = search.className.replace(/\bsf-dump-search-hidden\b/, ''); |
| 598 | searchInput.focus(); |
| 599 | } else if (isSearchActive) { |
| 600 | if (27 === e.keyCode) { |
| 601 | /* ESC key */ |
| 602 | search.className += ' sf-dump-search-hidden'; |
| 603 | e.preventDefault(); |
| 604 | resetHighlightedNodes(root); |
| 605 | searchInput.value = ''; |
| 606 | } else if ( |
| 607 | (isCtrlKey(e) && 71 === e.keyCode) /* CMD/CTRL + G */ |
| 608 | || 13 === e.keyCode /* Enter */ |
| 609 | || 114 === e.keyCode /* F3 */ |
| 610 | ) { |
| 611 | e.preventDefault(); |
| 612 | e.shiftKey ? state.previous() : state.next(); |
| 613 | collapseAll(root); |
| 614 | showCurrent(state); |
| 615 | } |
| 616 | } |
| 617 | }); |
| 618 | } |
| 619 | |
| 620 | if (0 >= options.maxStringLength) { |
| 621 | return; |
| 622 | } |
| 623 | try { |
| 624 | elt = root.querySelectorAll('.sf-dump-str'); |
| 625 | len = elt.length; |
| 626 | i = 0; |
| 627 | t = []; |
| 628 | |
| 629 | while (i < len) t.push(elt[i++]); |
| 630 | len = t.length; |
| 631 | |
| 632 | for (i = 0; i < len; ++i) { |
| 633 | elt = t[i]; |
| 634 | s = elt.innerText || elt.textContent; |
| 635 | x = s.length - options.maxStringLength; |
| 636 | if (0 < x) { |
| 637 | h = elt.innerHTML; |
| 638 | elt[elt.innerText ? 'innerText' : 'textContent'] = s.substring(0, options.maxStringLength); |
| 639 | elt.className += ' sf-dump-str-collapse'; |
| 640 | elt.innerHTML = '<span class=sf-dump-str-collapse>'+h+'<a class="sf-dump-ref sf-dump-str-toggle" title="Collapse"> â—€</a></span>'+ |
| 641 | '<span class=sf-dump-str-expand>'+elt.innerHTML+'<a class="sf-dump-ref sf-dump-str-toggle" title="'+x+' remaining characters"> â–¶</a></span>'; |
| 642 | } |
| 643 | } |
| 644 | } catch (e) { |
| 645 | } |
| 646 | }; |
| 647 | |
| 648 | })(document); |
| 649 | </script><style> |
| 650 | pre.sf-dump { |
| 651 | display: block; |
| 652 | white-space: pre; |
| 653 | padding: 5px; |
| 654 | overflow: initial !important; |
| 655 | } |
| 656 | pre.sf-dump:after { |
| 657 | content: ""; |
| 658 | visibility: hidden; |
| 659 | display: block; |
| 660 | height: 0; |
| 661 | clear: both; |
| 662 | } |
| 663 | pre.sf-dump span { |
| 664 | display: inline; |
| 665 | } |
| 666 | pre.sf-dump a { |
| 667 | text-decoration: none; |
| 668 | cursor: pointer; |
| 669 | border: 0; |
| 670 | outline: none; |
| 671 | color: inherit; |
| 672 | } |
| 673 | pre.sf-dump img { |
| 674 | max-width: 50em; |
| 675 | max-height: 50em; |
| 676 | margin: .5em 0 0 0; |
| 677 | padding: 0; |
| 678 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAHUlEQVQY02O8zAABilCaiQEN0EeA8QuUcX9g3QEAAjcC5piyhyEAAAAASUVORK5CYII=) #D3D3D3; |
| 679 | } |
| 680 | pre.sf-dump .sf-dump-ellipsis { |
| 681 | display: inline-block; |
| 682 | overflow: visible; |
| 683 | text-overflow: ellipsis; |
| 684 | max-width: 5em; |
| 685 | white-space: nowrap; |
| 686 | overflow: hidden; |
| 687 | vertical-align: top; |
| 688 | } |
| 689 | pre.sf-dump .sf-dump-ellipsis+.sf-dump-ellipsis { |
| 690 | max-width: none; |
| 691 | } |
| 692 | pre.sf-dump code { |
| 693 | display:inline; |
| 694 | padding:0; |
| 695 | background:none; |
| 696 | } |
| 697 | .sf-dump-public.sf-dump-highlight, |
| 698 | .sf-dump-protected.sf-dump-highlight, |
| 699 | .sf-dump-private.sf-dump-highlight, |
| 700 | .sf-dump-str.sf-dump-highlight, |
| 701 | .sf-dump-key.sf-dump-highlight { |
| 702 | background: rgba(111, 172, 204, 0.3); |
| 703 | border: 1px solid #7DA0B1; |
| 704 | border-radius: 3px; |
| 705 | } |
| 706 | .sf-dump-public.sf-dump-highlight-active, |
| 707 | .sf-dump-protected.sf-dump-highlight-active, |
| 708 | .sf-dump-private.sf-dump-highlight-active, |
| 709 | .sf-dump-str.sf-dump-highlight-active, |
| 710 | .sf-dump-key.sf-dump-highlight-active { |
| 711 | background: rgba(253, 175, 0, 0.4); |
| 712 | border: 1px solid #ffa500; |
| 713 | border-radius: 3px; |
| 714 | } |
| 715 | pre.sf-dump .sf-dump-search-hidden { |
| 716 | display: none !important; |
| 717 | } |
| 718 | pre.sf-dump .sf-dump-search-wrapper { |
| 719 | font-size: 0; |
| 720 | white-space: nowrap; |
| 721 | margin-bottom: 5px; |
| 722 | display: flex; |
| 723 | position: -webkit-sticky; |
| 724 | position: sticky; |
| 725 | top: 5px; |
| 726 | } |
| 727 | pre.sf-dump .sf-dump-search-wrapper > * { |
| 728 | vertical-align: top; |
| 729 | box-sizing: border-box; |
| 730 | height: 21px; |
| 731 | font-weight: normal; |
| 732 | border-radius: 0; |
| 733 | background: #FFF; |
| 734 | color: #757575; |
| 735 | border: 1px solid #BBB; |
| 736 | } |
| 737 | pre.sf-dump .sf-dump-search-wrapper > input.sf-dump-search-input { |
| 738 | padding: 3px; |
| 739 | height: 21px; |
| 740 | font-size: 12px; |
| 741 | border-right: none; |
| 742 | border-top-left-radius: 3px; |
| 743 | border-bottom-left-radius: 3px; |
| 744 | color: #000; |
| 745 | min-width: 15px; |
| 746 | width: 100%; |
| 747 | } |
| 748 | pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next, |
| 749 | pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-previous { |
| 750 | background: #F2F2F2; |
| 751 | outline: none; |
| 752 | border-left: none; |
| 753 | font-size: 0; |
| 754 | line-height: 0; |
| 755 | } |
| 756 | pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next { |
| 757 | border-top-right-radius: 3px; |
| 758 | border-bottom-right-radius: 3px; |
| 759 | } |
| 760 | pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next > svg, |
| 761 | pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-previous > svg { |
| 762 | pointer-events: none; |
| 763 | width: 12px; |
| 764 | height: 12px; |
| 765 | } |
| 766 | pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-count { |
| 767 | display: inline-block; |
| 768 | padding: 0 5px; |
| 769 | margin: 0; |
| 770 | border-left: none; |
| 771 | line-height: 21px; |
| 772 | font-size: 12px; |
| 773 | } |
| 774 | EOHTML |
| 775 | ); |
| 776 | |
| 777 | foreach ($this->styles as $class => $style) { |
| 778 | $line .= 'pre.sf-dump'.('default' === $class ? ', pre.sf-dump' : '').' .sf-dump-'.$class.'{'.$style.'}'; |
| 779 | } |
| 780 | $line .= 'pre.sf-dump .sf-dump-ellipsis-note{'.$this->styles['note'].'}'; |
| 781 | |
| 782 | return $this->dumpHeader = preg_replace('/\s+/', ' ', $line).'</style>'.$this->dumpHeader; |
| 783 | } |
| 784 | |
| 785 | /** |
| 786 | * {@inheritdoc} |
| 787 | */ |
| 788 | public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut) |
| 789 | { |
| 790 | if ('' === $str && isset($cursor->attr['img-data'], $cursor->attr['content-type'])) { |
| 791 | $this->dumpKey($cursor); |
| 792 | $this->line .= $this->style('default', $cursor->attr['img-size'] ?? '', []); |
| 793 | $this->line .= $cursor->depth >= $this->displayOptions['maxDepth'] ? ' <samp class=sf-dump-compact>' : ' <samp class=sf-dump-expanded>'; |
| 794 | $this->endValue($cursor); |
| 795 | $this->line .= $this->indentPad; |
| 796 | $this->line .= sprintf('<img src="data:%s;base64,%s" /></samp>', $cursor->attr['content-type'], base64_encode($cursor->attr['img-data'])); |
| 797 | $this->endValue($cursor); |
| 798 | } else { |
| 799 | parent::dumpString($cursor, $str, $bin, $cut); |
| 800 | } |
| 801 | } |
| 802 | |
| 803 | /** |
| 804 | * {@inheritdoc} |
| 805 | */ |
Matthias Andreas Benkard | 1ba5381 | 2022-12-27 17:32:58 +0100 | [diff] [blame^] | 806 | public function enterHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild) |
Matthias Andreas Benkard | 7b2a3a1 | 2021-08-16 10:57:25 +0200 | [diff] [blame] | 807 | { |
| 808 | if (Cursor::HASH_OBJECT === $type) { |
| 809 | $cursor->attr['depth'] = $cursor->depth; |
| 810 | } |
| 811 | parent::enterHash($cursor, $type, $class, false); |
| 812 | |
| 813 | if ($cursor->skipChildren || $cursor->depth >= $this->displayOptions['maxDepth']) { |
| 814 | $cursor->skipChildren = false; |
| 815 | $eol = ' class=sf-dump-compact>'; |
| 816 | } else { |
| 817 | $this->expandNextHash = false; |
| 818 | $eol = ' class=sf-dump-expanded>'; |
| 819 | } |
| 820 | |
| 821 | if ($hasChild) { |
| 822 | $this->line .= '<samp data-depth='.($cursor->depth + 1); |
| 823 | if ($cursor->refIndex) { |
| 824 | $r = Cursor::HASH_OBJECT !== $type ? 1 - (Cursor::HASH_RESOURCE !== $type) : 2; |
| 825 | $r .= $r && 0 < $cursor->softRefHandle ? $cursor->softRefHandle : $cursor->refIndex; |
| 826 | |
| 827 | $this->line .= sprintf(' id=%s-ref%s', $this->dumpId, $r); |
| 828 | } |
| 829 | $this->line .= $eol; |
| 830 | $this->dumpLine($cursor->depth); |
| 831 | } |
| 832 | } |
| 833 | |
| 834 | /** |
| 835 | * {@inheritdoc} |
| 836 | */ |
Matthias Andreas Benkard | 1ba5381 | 2022-12-27 17:32:58 +0100 | [diff] [blame^] | 837 | public function leaveHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild, int $cut) |
Matthias Andreas Benkard | 7b2a3a1 | 2021-08-16 10:57:25 +0200 | [diff] [blame] | 838 | { |
| 839 | $this->dumpEllipsis($cursor, $hasChild, $cut); |
| 840 | if ($hasChild) { |
| 841 | $this->line .= '</samp>'; |
| 842 | } |
| 843 | parent::leaveHash($cursor, $type, $class, $hasChild, 0); |
| 844 | } |
| 845 | |
| 846 | /** |
| 847 | * {@inheritdoc} |
| 848 | */ |
Matthias Andreas Benkard | 1ba5381 | 2022-12-27 17:32:58 +0100 | [diff] [blame^] | 849 | protected function style(string $style, string $value, array $attr = []): string |
Matthias Andreas Benkard | 7b2a3a1 | 2021-08-16 10:57:25 +0200 | [diff] [blame] | 850 | { |
| 851 | if ('' === $value) { |
| 852 | return ''; |
| 853 | } |
| 854 | |
| 855 | $v = esc($value); |
| 856 | |
| 857 | if ('ref' === $style) { |
| 858 | if (empty($attr['count'])) { |
| 859 | return sprintf('<a class=sf-dump-ref>%s</a>', $v); |
| 860 | } |
| 861 | $r = ('#' !== $v[0] ? 1 - ('@' !== $v[0]) : 2).substr($value, 1); |
| 862 | |
| 863 | return sprintf('<a class=sf-dump-ref href=#%s-ref%s title="%d occurrences">%s</a>', $this->dumpId, $r, 1 + $attr['count'], $v); |
| 864 | } |
| 865 | |
| 866 | if ('const' === $style && isset($attr['value'])) { |
| 867 | $style .= sprintf(' title="%s"', esc(is_scalar($attr['value']) ? $attr['value'] : json_encode($attr['value']))); |
| 868 | } elseif ('public' === $style) { |
| 869 | $style .= sprintf(' title="%s"', empty($attr['dynamic']) ? 'Public property' : 'Runtime added dynamic property'); |
| 870 | } elseif ('str' === $style && 1 < $attr['length']) { |
| 871 | $style .= sprintf(' title="%d%s characters"', $attr['length'], $attr['binary'] ? ' binary or non-UTF-8' : ''); |
| 872 | } elseif ('note' === $style && 0 < ($attr['depth'] ?? 0) && false !== $c = strrpos($value, '\\')) { |
| 873 | $style .= ' title=""'; |
| 874 | $attr += [ |
| 875 | 'ellipsis' => \strlen($value) - $c, |
| 876 | 'ellipsis-type' => 'note', |
| 877 | 'ellipsis-tail' => 1, |
| 878 | ]; |
| 879 | } elseif ('protected' === $style) { |
| 880 | $style .= ' title="Protected property"'; |
| 881 | } elseif ('meta' === $style && isset($attr['title'])) { |
| 882 | $style .= sprintf(' title="%s"', esc($this->utf8Encode($attr['title']))); |
| 883 | } elseif ('private' === $style) { |
| 884 | $style .= sprintf(' title="Private property defined in class: `%s`"', esc($this->utf8Encode($attr['class']))); |
| 885 | } |
| 886 | $map = static::$controlCharsMap; |
| 887 | |
| 888 | if (isset($attr['ellipsis'])) { |
| 889 | $class = 'sf-dump-ellipsis'; |
| 890 | if (isset($attr['ellipsis-type'])) { |
| 891 | $class = sprintf('"%s sf-dump-ellipsis-%s"', $class, $attr['ellipsis-type']); |
| 892 | } |
| 893 | $label = esc(substr($value, -$attr['ellipsis'])); |
| 894 | $style = str_replace(' title="', " title=\"$v\n", $style); |
| 895 | $v = sprintf('<span class=%s>%s</span>', $class, substr($v, 0, -\strlen($label))); |
| 896 | |
| 897 | if (!empty($attr['ellipsis-tail'])) { |
| 898 | $tail = \strlen(esc(substr($value, -$attr['ellipsis'], $attr['ellipsis-tail']))); |
| 899 | $v .= sprintf('<span class=%s>%s</span>%s', $class, substr($label, 0, $tail), substr($label, $tail)); |
| 900 | } else { |
| 901 | $v .= $label; |
| 902 | } |
| 903 | } |
| 904 | |
| 905 | $v = "<span class=sf-dump-{$style}>".preg_replace_callback(static::$controlCharsRx, function ($c) use ($map) { |
| 906 | $s = $b = '<span class="sf-dump-default'; |
| 907 | $c = $c[$i = 0]; |
| 908 | if ($ns = "\r" === $c[$i] || "\n" === $c[$i]) { |
| 909 | $s .= ' sf-dump-ns'; |
| 910 | } |
| 911 | $s .= '">'; |
| 912 | do { |
| 913 | if (("\r" === $c[$i] || "\n" === $c[$i]) !== $ns) { |
| 914 | $s .= '</span>'.$b; |
| 915 | if ($ns = !$ns) { |
| 916 | $s .= ' sf-dump-ns'; |
| 917 | } |
| 918 | $s .= '">'; |
| 919 | } |
| 920 | |
| 921 | $s .= $map[$c[$i]] ?? sprintf('\x%02X', \ord($c[$i])); |
| 922 | } while (isset($c[++$i])); |
| 923 | |
| 924 | return $s.'</span>'; |
| 925 | }, $v).'</span>'; |
| 926 | |
| 927 | if (isset($attr['file']) && $href = $this->getSourceLink($attr['file'], $attr['line'] ?? 0)) { |
| 928 | $attr['href'] = $href; |
| 929 | } |
| 930 | if (isset($attr['href'])) { |
| 931 | $target = isset($attr['file']) ? '' : ' target="_blank"'; |
| 932 | $v = sprintf('<a href="%s"%s rel="noopener noreferrer">%s</a>', esc($this->utf8Encode($attr['href'])), $target, $v); |
| 933 | } |
| 934 | if (isset($attr['lang'])) { |
| 935 | $v = sprintf('<code class="%s">%s</code>', esc($attr['lang']), $v); |
| 936 | } |
| 937 | |
| 938 | return $v; |
| 939 | } |
| 940 | |
| 941 | /** |
| 942 | * {@inheritdoc} |
| 943 | */ |
| 944 | protected function dumpLine(int $depth, bool $endOfValue = false) |
| 945 | { |
| 946 | if (-1 === $this->lastDepth) { |
| 947 | $this->line = sprintf($this->dumpPrefix, $this->dumpId, $this->indentPad).$this->line; |
| 948 | } |
| 949 | if ($this->headerIsDumped !== ($this->outputStream ?? $this->lineDumper)) { |
| 950 | $this->line = $this->getDumpHeader().$this->line; |
| 951 | } |
| 952 | |
| 953 | if (-1 === $depth) { |
| 954 | $args = ['"'.$this->dumpId.'"']; |
| 955 | if ($this->extraDisplayOptions) { |
| 956 | $args[] = json_encode($this->extraDisplayOptions, \JSON_FORCE_OBJECT); |
| 957 | } |
| 958 | // Replace is for BC |
| 959 | $this->line .= sprintf(str_replace('"%s"', '%s', $this->dumpSuffix), implode(', ', $args)); |
| 960 | } |
| 961 | $this->lastDepth = $depth; |
| 962 | |
| 963 | $this->line = mb_convert_encoding($this->line, 'HTML-ENTITIES', 'UTF-8'); |
| 964 | |
| 965 | if (-1 === $depth) { |
| 966 | AbstractDumper::dumpLine(0); |
| 967 | } |
| 968 | AbstractDumper::dumpLine($depth); |
| 969 | } |
| 970 | |
| 971 | private function getSourceLink(string $file, int $line) |
| 972 | { |
| 973 | $options = $this->extraDisplayOptions + $this->displayOptions; |
| 974 | |
| 975 | if ($fmt = $options['fileLinkFormat']) { |
| 976 | return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line); |
| 977 | } |
| 978 | |
| 979 | return false; |
| 980 | } |
| 981 | } |
| 982 | |
| 983 | function esc(string $str) |
| 984 | { |
| 985 | return htmlspecialchars($str, \ENT_QUOTES, 'UTF-8'); |
| 986 | } |