HtmlDumper.php 33 KB

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