diff --git a/composer.json b/composer.json index a4f2e7530..a772617ef 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.12-dev" + "dev-master": "3.0-dev" } }, "config": { diff --git a/src/Bridges/Nette/MailSender.php b/src/Bridges/Nette/MailSender.php index 996438c15..8f7b01887 100644 --- a/src/Bridges/Nette/MailSender.php +++ b/src/Bridges/Nette/MailSender.php @@ -26,7 +26,7 @@ public function __construct( } - public function send(mixed $message, string $email): void + public function send(mixed $message, string $email, ?string $exceptionFile = null): void { $host = preg_replace('#[^\w.-]+#', '', $this->host ?? $_SERVER['SERVER_NAME'] ?? php_uname('n')); @@ -41,7 +41,11 @@ public function send(mixed $message, string $email): void } $mail->setSubject('PHP: An error occurred on the server ' . $host); - $mail->setBody(Tracy\Logger::formatMessage($message) . "\n\nsource: " . Tracy\Helpers::getSource()); + $mail->setBody( + Tracy\Logger::formatMessage($message) + . "\n\nsource: " . Tracy\Helpers::getSource() + . ($exceptionFile ? "\nreport: " . basename($exceptionFile) : ''), + ); $this->mailer->send($mail); } diff --git a/src/Bridges/Nette/TracyExtension.php b/src/Bridges/Nette/TracyExtension.php index b9ee02594..e6106ce92 100644 --- a/src/Bridges/Nette/TracyExtension.php +++ b/src/Bridges/Nette/TracyExtension.php @@ -156,14 +156,14 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void } $initialize->addBody($builder->formatPhp('if ($logger instanceof Tracy\Logger) $logger->mailer = ?;', [ - [new Statement(Tracy\Bridges\Nette\MailSender::class, $params), 'send'], + [new Statement(Tracy\Bridges\Nette\MailSender::class, $params), 'send'], // TODO: nette/di must be able to create closures ])); } if ($this->debugMode) { foreach ($config->bar as $item) { if (is_string($item) && str_starts_with($item, '@')) { - $item = new Statement(['@' . $builder::THIS_CONTAINER, 'getService'], [substr($item, 1)]); + $item = new Statement(['@' . $builder::ThisContainer, 'getService'], [substr($item, 1)]); } elseif (is_string($item)) { $item = new Statement($item); } diff --git a/src/Tracy/Bar/AgentPanel.php b/src/Tracy/Bar/AgentPanel.php new file mode 100644 index 000000000..ae61ee9fc --- /dev/null +++ b/src/Tracy/Bar/AgentPanel.php @@ -0,0 +1,20 @@ +panels as $panel) { - if (method_exists($panel, 'getAgentInfo') && ($text = $panel->getAgentInfo()) !== null) { - $parts[] = $text; + foreach ($this->panels as $id => $panel) { + try { + if ( + ($panel instanceof AgentPanel || method_exists($panel, 'getAgentInfo')) + && ($text = $panel->getAgentInfo()) !== null + ) { + $parts[] = $text; + } + } catch (\Throwable $e) { // a broken panel must not kill the whole bar render + $parts[] = "Error in panel $id: {$e->getMessage()}"; } } diff --git a/src/Tracy/Bar/DefaultBarPanel.php b/src/Tracy/Bar/DefaultBarPanel.php index e46b990e0..258c456ad 100644 --- a/src/Tracy/Bar/DefaultBarPanel.php +++ b/src/Tracy/Bar/DefaultBarPanel.php @@ -13,7 +13,7 @@ * @internal */ #[\AllowDynamicProperties] -class DefaultBarPanel implements IBarPanel +class DefaultBarPanel implements IBarPanel, AgentPanel { public mixed $data = null; diff --git a/src/Tracy/Bar/IBarPanel.php b/src/Tracy/Bar/IBarPanel.php index 1e522e809..7d465941c 100644 --- a/src/Tracy/Bar/IBarPanel.php +++ b/src/Tracy/Bar/IBarPanel.php @@ -10,7 +10,7 @@ /** * Tracy Bar panel providing a tab label and optional panel content. - * @method ?string getAgentInfo() Returns markdown summary for AI agents. + * @method ?string getAgentInfo() Returns markdown summary for AI agents, see also the AgentPanel interface. */ interface IBarPanel { diff --git a/src/Tracy/Bar/assets/bar.css b/src/Tracy/Bar/assets/bar.css index 5f71d82b8..cee1ff3ff 100644 --- a/src/Tracy/Bar/assets/bar.css +++ b/src/Tracy/Bar/assets/bar.css @@ -2,40 +2,54 @@ * This file is part of the Tracy (https://tracy.nette.org) */ -/* common styles */ -#tracy-debug { +@layer tracy-components { + +/* shadow DOM host */ +:host { --tracy-space: 10px; - display: none; direction: ltr; + display: none; } -body#tracy-debug { /* in popup window */ - display: block; +/* host is shown as a manual popover in the top layer, undo UA popover geometry */ +:host([popover]) { + position: static; + inset: auto; + width: auto; + height: auto; + margin: 0; + border: none; + padding: 0; + background: none; + overflow: visible; + color: inherit; } -#tracy-debug:not(body) { - position: absolute; - left: 0; - top: 0; +body#tracy-debug { /* in popup window, the sheet is adopted by the document */ + --tracy-space: 10px; + direction: ltr; + display: block; } -#tracy-debug a { +/* common styles */ +a { color: #125EAE; text-decoration: none; } -#tracy-debug a:hover, -#tracy-debug a:focus { +a:hover, +a:focus { background-color: #125EAE; color: white; } -#tracy-debug h2, -#tracy-debug h3 { +h2, +h3 { + font-size: inherit; font-weight: bold; } -#tracy-debug :where(:is( +:where(:is( h1, h2, h3, h4, h5, h6, p, ol, ul, dl, @@ -47,44 +61,44 @@ body#tracy-debug { /* in popup window */ margin-top: var(--tracy-space); } -#tracy-debug table { +table { background: #FDF5CE; width: 100%; } -#tracy-debug tr:nth-child(2n) td { +tr:nth-child(2n) td { background: rgba(0, 0, 0, 0.02); } -#tracy-debug td, -#tracy-debug th { +td, +th { border: 1px solid #E6DFBF; padding: 2px 5px; vertical-align: top; text-align: left; } -#tracy-debug th { +th { background: #F4F3F1; color: #655E5E; font-size: 90%; font-weight: bold; } -#tracy-debug pre, -#tracy-debug code { +pre, +code { font: 9pt/1.5 Consolas, monospace; } -#tracy-debug table .tracy-right { +table .tracy-right { text-align: right; } -#tracy-debug svg { +svg { display: inline; } -#tracy-debug .tracy-dump { +.tracy-dump { margin: 0; padding: 2px 5px; } @@ -182,7 +196,7 @@ body#tracy-debug { /* in popup window */ /* panels */ -#tracy-debug .tracy-panel { +.tracy-panel { display: none; font: normal normal 12px/1.5 sans-serif; background: white; @@ -194,7 +208,7 @@ body#tracy-debug .tracy-panel { /* in popup window */ display: block; } -#tracy-debug h1 { +h1 { font: normal normal 23px/1.4 Tahoma, sans-serif; line-height: 1; color: #575753; @@ -202,29 +216,29 @@ body#tracy-debug .tracy-panel { /* in popup window */ word-wrap: break-word; } -#tracy-debug .tracy-inner { +.tracy-inner { overflow: auto; flex: 1; } -#tracy-debug .tracy-panel .tracy-icons { +.tracy-panel .tracy-icons { display: none; } -#tracy-debug .tracy-panel-ajax h1::after, -#tracy-debug .tracy-panel-redirect h1::after { +.tracy-panel-ajax h1::after, +.tracy-panel-redirect h1::after { content: 'ajax'; float: right; font-size: 65%; margin: 0 .3em; } -#tracy-debug .tracy-panel-redirect h1::after { +.tracy-panel-redirect h1::after { content: 'redirect'; } -#tracy-debug .tracy-mode-peek, -#tracy-debug .tracy-mode-float { +.tracy-mode-peek, +.tracy-mode-float { position: fixed; flex-direction: column; padding: var(--tracy-space); @@ -235,24 +249,24 @@ body#tracy-debug .tracy-panel { /* in popup window */ border: 1px solid rgba(0, 0, 0, 0.1); } -#tracy-debug .tracy-mode-peek, -#tracy-debug .tracy-mode-float:not(.tracy-panel-resized) { +.tracy-mode-peek, +.tracy-mode-float:not(.tracy-panel-resized) { max-width: 700px; max-height: 500px; } @media (max-height: 555px) { - #tracy-debug .tracy-mode-peek, - #tracy-debug .tracy-mode-float:not(.tracy-panel-resized) { + .tracy-mode-peek, + .tracy-mode-float:not(.tracy-panel-resized) { max-height: 100vh; } } -#tracy-debug .tracy-mode-peek h1 { +.tracy-mode-peek h1 { cursor: move; } -#tracy-debug .tracy-mode-float { +.tracy-mode-float { display: flex; opacity: .95; transition: opacity 0.2s; @@ -261,18 +275,18 @@ body#tracy-debug .tracy-panel { /* in popup window */ resize: both; } -#tracy-debug .tracy-focused { +.tracy-focused { display: flex; opacity: 1; transition: opacity 0.1s; } -#tracy-debug .tracy-mode-float h1 { +.tracy-mode-float h1 { cursor: move; padding-right: 25px; } -#tracy-debug .tracy-mode-float .tracy-icons { +.tracy-mode-float .tracy-icons { display: block; position: absolute; top: 0; @@ -280,25 +294,27 @@ body#tracy-debug .tracy-panel { /* in popup window */ font-size: 18px; } -#tracy-debug .tracy-mode-window { +.tracy-mode-window { padding: var(--tracy-space); } -#tracy-debug .tracy-icons a { +.tracy-icons a { color: #575753; } -#tracy-debug .tracy-icons a:hover { +.tracy-icons a:hover { color: white; } -#tracy-debug .tracy-inner-container { +.tracy-inner-container { min-width: fit-content; } @media print { - #tracy-debug * { + * { display: none; } } + +} diff --git a/src/Tracy/Bar/assets/bar.js b/src/Tracy/Bar/assets/bar.js index a95eb3387..f7f1dc546 100644 --- a/src/Tracy/Bar/assets/bar.js +++ b/src/Tracy/Bar/assets/bar.js @@ -32,7 +32,7 @@ function restoreJSON(key) { class Panel { constructor(id) { this.id = id; - this.elem = document.getElementById(this.id); + this.elem = Debug.shadow.querySelector('#' + CSS.escape(this.id)); this.elem.Tracy = this.elem.Tracy || {}; } @@ -43,7 +43,7 @@ class Panel { this.init = function () {}; elem.innerHTML = elem.tracyContent = elem.dataset.tracyContent; delete elem.dataset.tracyContent; - Tracy.Dumper.init(Debug.layer); + Tracy.Dumper.init(Debug.shadow); evalScripts(elem); draggable(elem, { @@ -166,14 +166,17 @@ class Panel { let script = doc.createElement('script'); script.src = baseUrl + '_tracy_bar=js&XDEBUG_SESSION_STOP=1'; script.async = true; - script.addEventListener('load', () => win.Tracy.Dumper.init()); + script.addEventListener('load', () => { + win.Tracy.adoptStyleSheets(doc, ['shared', 'bar']); + win.Tracy.Dumper.init(); + }); doc.head.appendChild(script); let meta = this.elem.parentElement.lastElementChild; - doc.body.innerHTML = '' + doc.body.innerHTML = '
' + '
' + this.elem.tracyContent + '
' + meta.outerHTML - + ''; + + '
'; evalScripts(doc.body); if (this.elem.querySelector('h1')) { doc.title = this.elem.querySelector('h1').textContent; @@ -260,7 +263,7 @@ Panel.zIndexCounter = 1; class Bar { init() { this.id = 'tracy-debug-bar'; - this.elem = document.getElementById(this.id); + this.elem = Debug.shadow.querySelector('#' + this.id); draggable(this.elem, { handles: this.elem.querySelectorAll('li:first-child'), @@ -364,7 +367,7 @@ class Bar { close() { - document.getElementById('tracy-debug').style.display = 'none'; + Debug.host.style.display = 'none'; } @@ -403,15 +406,30 @@ class Debug { static init(content) { Debug.bar = new Bar; Debug.panels = {}; + + // Shadow DOM for CSS isolation + let host = document.createElement('tracy-bar'); + Debug.host = host; + let shadow = host.attachShadow({ mode: 'open' }); + Debug.shadow = shadow; + Tracy.adoptStyleSheets(shadow, ['shared', 'bar']); + + // #tracy-debug wrapper is kept for backward compatibility of third-party panel CSS Debug.layer = document.createElement('tracy-div'); Debug.layer.setAttribute('id', 'tracy-debug'); Debug.layer.innerHTML = content; - (document.body || document.documentElement).appendChild(Debug.layer); + shadow.appendChild(Debug.layer); + + (document.body || document.documentElement).appendChild(host); evalScripts(Debug.layer); - Debug.layer.style.display = 'block'; + if (host.showPopover) { // top layer wins over any host-page z-index or modal + host.popover = 'manual'; + host.showPopover(); + } + host.style.display = 'block'; Debug.bar.init(); - Debug.layer.querySelectorAll('.tracy-panel').forEach((panel) => { + Debug.shadow.querySelectorAll('.tracy-panel').forEach((panel) => { Debug.panels[panel.id] = new Panel(panel.id); Debug.panels[panel.id].restorePosition(); }); @@ -454,7 +472,7 @@ class Debug { Debug.bar.elem.insertAdjacentHTML('beforeend', content.bar); let ajaxBar = Debug.bar.elem.querySelector('.tracy-row:last-child'); - Debug.layer.querySelectorAll('.tracy-panel').forEach((panel) => { + Debug.shadow.querySelectorAll('.tracy-panel').forEach((panel) => { if (!Debug.panels[panel.id]) { Debug.panels[panel.id] = new Panel(panel.id); Debug.panels[panel.id].restorePosition(); @@ -558,83 +576,68 @@ function evalScripts(elem) { } -let dragging; - -function draggable(elem, options) { - let dE = document.documentElement, started, deltaX, deltaY, clientX, clientY; - options = options || {}; +function draggable(elem, options = {}) { + let started, deltaX, deltaY, clientX, clientY, rafId; let redraw = () => { - if (dragging) { - setPosition(elem, { left: clientX + deltaX, top: clientY + deltaY }); - requestAnimationFrame(redraw); - } + setPosition(elem, { left: clientX + deltaX, top: clientY + deltaY }); + rafId = requestAnimationFrame(redraw); }; - let onMove = (e) => { - if (e.buttons === 0) { - return onEnd(e); - } - if (!started) { - if (options.draggedClass) { - elem.classList.add(options.draggedClass); + options.handles.forEach((handle) => { + handle.style.touchAction = 'none'; // prevents scrolling while dragging on touch devices + + handle.addEventListener('pointerdown', (e) => { + if (!e.isPrimary || e.button !== 0) { + return; } + e.preventDefault(); + e.stopPropagation(); + handle.setPointerCapture(e.pointerId); // guarantees pointerup delivery even outside the window + + let pos = getPosition(elem); + clientX = e.clientX; + clientY = e.clientY; + deltaX = pos.left - clientX; + deltaY = pos.top - clientY; + started = false; if (options.start) { options.start(e, elem); } - started = true; - } - - clientX = e.touches ? e.touches[0].clientX : e.clientX; - clientY = e.touches ? e.touches[0].clientY : e.clientY; - return false; - }; + }); - let onEnd = (e) => { - if (started) { - if (options.draggedClass) { - elem.classList.remove(options.draggedClass); + handle.addEventListener('pointermove', (e) => { + if (!handle.hasPointerCapture(e.pointerId)) { + return; } - if (options.stop) { - options.stop(e, elem); + if (!started) { + started = true; + if (options.draggedClass) { + elem.classList.add(options.draggedClass); + } + rafId = requestAnimationFrame(redraw); } - } - dragging = null; - dE.removeEventListener('mousemove', onMove); - dE.removeEventListener('mouseup', onEnd); - dE.removeEventListener('touchmove', onMove); - dE.removeEventListener('touchend', onEnd); - return false; - }; - - let onStart = (e) => { - e.preventDefault(); - e.stopPropagation(); - - if (dragging) { // missed mouseup out of window? - return onEnd(e); - } + clientX = e.clientX; + clientY = e.clientY; + }); - let pos = getPosition(elem); - clientX = e.touches ? e.touches[0].clientX : e.clientX; - clientY = e.touches ? e.touches[0].clientY : e.clientY; - deltaX = pos.left - clientX; - deltaY = pos.top - clientY; - dragging = true; - started = false; - dE.addEventListener('mousemove', onMove); - dE.addEventListener('mouseup', onEnd); - dE.addEventListener('touchmove', onMove); - dE.addEventListener('touchend', onEnd); - requestAnimationFrame(redraw); - if (options.start) { - options.start(e, elem); - } - }; + let onEnd = (e) => { + if (!handle.hasPointerCapture(e.pointerId)) { + return; + } + cancelAnimationFrame(rafId); + if (started) { + if (options.draggedClass) { + elem.classList.remove(options.draggedClass); + } + if (options.stop) { + options.stop(e, elem); + } + } + }; - options.handles.forEach((handle) => { - handle.addEventListener('mousedown', onStart); - handle.addEventListener('touchstart', onStart); + handle.addEventListener('pointerup', onEnd); + handle.addEventListener('pointercancel', onEnd); handle.addEventListener('click', (e) => { if (started) { diff --git a/src/Tracy/Bar/dist/info.agent.phtml b/src/Tracy/Bar/dist/info.agent.phtml new file mode 100644 index 000000000..4535efc28 --- /dev/null +++ b/src/Tracy/Bar/dist/info.agent.phtml @@ -0,0 +1,19 @@ +time ??= microtime(true) - Tracy\Debugger::$time /* pos 2:1 */; +require __DIR__ . '/../panels/info.panel.php'; +/** @var array $info */ +if (!$info) /* pos 5:1 */ return; +echo '## System Info + +'; +foreach ($info as $key => $val) /* pos 8:1 */ { + echo '- '; + echo Tracy\Helpers::escapeMd($key) /* pos 9:4 */; + echo ': '; + echo Tracy\Helpers::escapeMd($val) /* pos 9:13 */; + echo "\n"; + +} + diff --git a/src/Tracy/Bar/panels/info.agent.latte b/src/Tracy/Bar/panels/info.agent.latte new file mode 100644 index 000000000..4587a811d --- /dev/null +++ b/src/Tracy/Bar/panels/info.agent.latte @@ -0,0 +1,10 @@ +{varType Tracy\DefaultBarPanel $this} +{do $this->time ??= microtime(true) - Tracy\Debugger::$time} +{include '../panels/info.panel.php'} +{varType array $info} +{exitIf !$info} +## System Info + +{foreach $info as $key => $val} + - {=$key}: {=$val} +{/foreach} diff --git a/src/Tracy/Bar/panels/info.panel.php b/src/Tracy/Bar/panels/info.panel.php index 325a9cad1..4878af78e 100644 --- a/src/Tracy/Bar/panels/info.panel.php +++ b/src/Tracy/Bar/panels/info.panel.php @@ -25,8 +25,10 @@ return $ip; }; -$opcache = function_exists('opcache_get_status') ? @opcache_get_status() : null; // @ can be restricted -$cachedFiles = isset($opcache['scripts']) ? array_intersect(array_keys($opcache['scripts']), get_included_files()) : []; +$opcache = function_exists('opcache_get_status') ? @opcache_get_status(false) : null; // @ can be restricted +$cachedFiles = $opcache && function_exists('opcache_is_script_cached') + ? array_filter(get_included_files(), fn(string $file): bool => @opcache_is_script_cached($file)) + : []; $jit = $opcache['jit'] ?? null; $info = [ diff --git a/src/Tracy/BlueScreen/BlueScreen.php b/src/Tracy/BlueScreen/BlueScreen.php index a670f58f0..11dff1176 100644 --- a/src/Tracy/BlueScreen/BlueScreen.php +++ b/src/Tracy/BlueScreen/BlueScreen.php @@ -42,7 +42,7 @@ class BlueScreen public bool $showEnvironment = true; - /** @var array<\Closure(?\Throwable): ?array{tab: string, panel: string}> */ + /** @var array<\Closure(?\Throwable): ?array{tab: string, panel: string, text?: string, bottom?: bool, collapsed?: bool}> */ private array $panels = []; /** @var array<\Closure(\Throwable): ?array{link: string, label: string}> */ @@ -57,17 +57,22 @@ class BlueScreen /** @var \WeakMap<\Fiber|\Generator, true> */ private \WeakMap $fibers; + /** @var \WeakMap<\Throwable, array{array, array}> */ + private \WeakMap $renderContext; + public function __construct() { $this->fileGenerators[] = self::generateNewPhpFileContents(...); $this->fibers = new \WeakMap; + $this->renderContext = new \WeakMap; } /** - * Add custom panel. - * @param callable(?\Throwable): ?array{tab: string, panel: string} $panel + * Add custom panel. The callback is invoked with the exception (possibly repeatedly for each + * exception in the chain) and once with null for panels rendered below the call stack. + * @param callable(?\Throwable): ?array{tab: string, panel: string, text?: string, bottom?: bool, collapsed?: bool} $panel */ public function addPanel(callable $panel): static { @@ -181,7 +186,8 @@ private function renderTemplate( ?array $logLocation = null, ): void { - [$generators, $fibers] = $this->findGeneratorsAndFibers($exception); + // expensive object-graph traversal is computed once and shared by all render targets (HTML page, AJAX, log file, agent markdown) + [$generators, $fibers] = $this->renderContext[$exception] ??= $this->findGeneratorsAndFibers($exception); $headersSent = headers_sent($headersFile, $headersLine); $obStatus = Debugger::$obStatus; $showEnvironment = $this->showEnvironment && (!str_contains($exception->getMessage(), 'Allowed memory size')); @@ -203,32 +209,41 @@ private function renderTemplate( $dump = $this->getDumper(); $agentDump = $this->getAgentDumper(); + if (str_ends_with($template, 'page.phtml')) { // the only template embedding assets + [$css, $js] = $this->prepareAssets(); + } + + $nonce = $toScreen ? Helpers::getNonce() : null; + $actions = $toScreen ? $this->renderActions($exception) : []; + $blueScreen = $this; + + require $template; + $this->snapshot = []; + } + + + /** @return array{string, string} minified CSS and JS for the standalone error page */ + private function prepareAssets(): array + { $css = array_map(file_get_contents(...), array_merge([ __DIR__ . '/../assets/reset.css', __DIR__ . '/assets/bluescreen.css', __DIR__ . '/../assets/toggle.css', __DIR__ . '/../assets/table-sort.css', __DIR__ . '/../assets/tabs.css', - __DIR__ . '/../Dumper/assets/dumper-light.css', + __DIR__ . '/../Dumper/assets/dumper.css', ], Debugger::$customCssFiles)); - $css = Helpers::minifyCss(implode('', $css)); $js = array_map(fn($file) => '(function(){' . file_get_contents($file) . '})();', [ + __DIR__ . '/../assets/helpers.js', __DIR__ . '/../assets/toggle.js', __DIR__ . '/../assets/table-sort.js', __DIR__ . '/../assets/tabs.js', - __DIR__ . '/../assets/helpers.js', __DIR__ . '/../Dumper/assets/dumper.js', __DIR__ . '/assets/bluescreen.js', ]); - $js = Helpers::minifyJs(implode('', $js)); - - $nonce = $toScreen ? Helpers::getNonce() : null; - $actions = $toScreen ? $this->renderActions($exception) : []; - $blueScreen = $this; - require $template; - $this->snapshot = []; + return [Helpers::minifyCss(implode('', $css)), Helpers::minifyJs(implode('', $js))]; } @@ -266,6 +281,28 @@ private function renderPanels(?\Throwable $ex): array } + /** + * Collects plain-text (markdown) sections provided by custom panels via the 'text' key. + * @return list<\stdClass> + */ + private function renderPanelsAgent(?\Throwable $ex): array + { + $res = []; + foreach ($this->panels as $callback) { + try { + $panel = $callback($ex); + if (!empty($panel['tab']) && !empty($panel['text'])) { + $res[] = (object) $panel; + } + } catch (\Throwable) { + // the error is already reported in the HTML blue screen + } + } + + return $res; + } + + /** * @return list */ @@ -370,7 +407,7 @@ public static function highlightFile( ? CodeHighlighter::highlightPhp($source, $line, $column) : '
' . CodeHighlighter::highlightLine(htmlspecialchars($source, ENT_IGNORE, 'UTF-8'), $line, $column) . '
'; - if ($editor = Helpers::editorUri($file, $line)) { + if ($editor = Helpers::editorUri($file, line: $line, column: $column)) { $source = substr_replace($source, ' title="Ctrl-Click to open in editor" data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0); } diff --git a/src/Tracy/BlueScreen/assets/agent.latte b/src/Tracy/BlueScreen/assets/agent.latte index 214b93919..e3f8831f8 100644 --- a/src/Tracy/BlueScreen/assets/agent.latte +++ b/src/Tracy/BlueScreen/assets/agent.latte @@ -112,6 +112,14 @@ This is an error page generated by Tracy (https://tracy.nette.org). {/if} {/foreach} +{do $panelsText = array_merge($blueScreen->renderPanelsAgent($exception), $blueScreen->renderPanelsAgent(null))} +{foreach $panelsText as $panel} + + ## {$panel->tab} + + {=$panel->text|noescape} +{/foreach} + {if $lastError} ## Last Muted Error diff --git a/src/Tracy/BlueScreen/assets/bluescreen.css b/src/Tracy/BlueScreen/assets/bluescreen.css index dd80a54a2..c3eabfdc5 100644 --- a/src/Tracy/BlueScreen/assets/bluescreen.css +++ b/src/Tracy/BlueScreen/assets/bluescreen.css @@ -2,6 +2,13 @@ * This file is part of the Tracy (https://tracy.nette.org) */ +@layer tracy-components { + +/* shadow DOM host */ +:host { + display: contents; +} + html.tracy-bs-visible, html.tracy-bs-visible body { display: block; @@ -9,7 +16,7 @@ html.tracy-bs-visible body { position: static; } -#tracy-bs { +.tracy-bs { font: 9pt/1.5 Verdana, sans-serif; background: white; color: #333; @@ -19,404 +26,406 @@ html.tracy-bs-visible body { top: 0; width: 100%; text-align: left; -} -#tracy-bs a { - text-decoration: none; - color: #328ADC; - padding: 0 4px; - margin: 0 -4px; -} + a { + text-decoration: none; + color: #328ADC; + padding: 0 4px; + margin: 0 -4px; + } -#tracy-bs a + a { - margin-left: 0; -} + a + a { + margin-left: 0; + } -#tracy-bs a:hover, -#tracy-bs a:focus { - color: #085AA3; -} + a:hover, + a:focus { + color: #085AA3; + } -#tracy-bs-toggle { - position: absolute; - right: .5em; - top: .5em; - text-decoration: none; - background: #CD1818; - color: white !important; - padding: 3px; -} + .tracy-bs-toggle { + position: absolute; + right: .5em; + top: .5em; + text-decoration: none; + background: #CD1818; + color: white !important; + padding: 3px; + } -#tracy-bs-toggle.tracy-collapsed { - position: fixed; -} + .tracy-bs-toggle.tracy-collapsed { + position: fixed; + } -.tracy-bs-main { - display: flex; - flex-direction: column; - padding-bottom: 80vh; -} + .tracy-bs-main { + display: flex; + flex-direction: column; + padding-bottom: 80vh; + } -.tracy-bs-main.tracy-collapsed { - display: none; -} + .tracy-bs-main.tracy-collapsed { + display: none; + } -#tracy-bs :where(:is( - h1, h2, h3, h4, h5, h6, - p, - ol, ul, dl, - pre, table, hr, - .tracy-section-panel, - .tracy-pane -):not(:first-child)) { - margin-top: var(--tracy-space); -} + :where(:is( + h1, h2, h3, h4, h5, h6, + p, + ol, ul, dl, + pre, table, hr, + .tracy-section-panel, + .tracy-pane + ):not(:first-child)) { + margin-top: var(--tracy-space); + } -#tracy-bs h1 { - font-size: 15pt; - font-weight: normal; - text-shadow: 1px 1px 2px rgba(0, 0, 0, .3); -} + h1 { + font-size: 15pt; + font-weight: normal; + text-shadow: 1px 1px 2px rgba(0, 0, 0, .3); + } -#tracy-bs h1 span { - white-space: pre-wrap; -} + h1 span { + white-space: pre-wrap; + } -#tracy-bs h2 { - font-size: 14pt; - font-weight: normal; -} + h2 { + font-size: 14pt; + font-weight: normal; + } -#tracy-bs h3 { - font-size: 10pt; - font-weight: bold; -} + h3 { + font-size: 10pt; + font-weight: bold; + } -#tracy-bs pre, -#tracy-bs code, -#tracy-bs table { - font: 9pt/1.5 Consolas, monospace !important; -} + pre, + code, + table { + font: 9pt/1.5 Consolas, monospace !important; + } -#tracy-bs pre, -#tracy-bs table { - background: #FDF5CE; - padding: .4em .7em; - border: 2px solid #ffffffa6; - box-shadow: 1px 2px 6px #00000005; - overflow: auto; -} + pre, + table { + background: #FDF5CE; + padding: .4em .7em; + border: 2px solid #ffffffa6; + box-shadow: 1px 2px 6px #00000005; + overflow: auto; + } -#tracy-bs table pre { - padding: 0; - margin: 0; - border: none; - box-shadow: none; -} + table pre { + padding: 0; + margin: 0; + border: none; + box-shadow: none; + } -#tracy-bs table { - border-collapse: collapse; - width: 100%; -} + table { + border-collapse: collapse; + width: 100%; + } -#tracy-bs td, -#tracy-bs th { - vertical-align: top; - text-align: left; - padding: 2px 6px; - border: 1px solid #e6dfbf; -} + td, + th { + vertical-align: top; + text-align: left; + padding: 2px 6px; + border: 1px solid #e6dfbf; + } -#tracy-bs th { - font-weight: bold; -} + th { + font-weight: bold; + } -#tracy-bs tr > :first-child { - width: 20%; -} + tr > :first-child { + width: 20%; + } -#tracy-bs tr:nth-child(2n), -#tracy-bs tr:nth-child(2n) pre { - background-color: #F7F0CB; -} + tr:nth-child(2n), + tr:nth-child(2n) pre { + background-color: #F7F0CB; + } -#tracy-bs .tracy-footer--sticky { - position: fixed; - width: 100%; - bottom: 0; -} + .tracy-footer--sticky { + position: fixed; + width: 100%; + bottom: 0; + } -#tracy-bs footer ul { - font-size: 7pt; - padding: var(--tracy-space); - margin: var(--tracy-space) 0 0; - color: #777; - background: #F6F5F3; - border-top: 1px solid #DDD; - list-style: none; -} + footer ul { + font-size: 7pt; + padding: var(--tracy-space); + margin: var(--tracy-space) 0 0; + color: #777; + background: #F6F5F3; + border-top: 1px solid #DDD; + list-style: none; + } -#tracy-bs .tracy-footer-logo { - position: relative; -} + .tracy-footer-logo { + position: relative; + } -#tracy-bs .tracy-footer-logo a { - position: absolute; - bottom: 0; - right: 0; - width: 100px; - height: 50px; - background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAUBAMAAAD/1DctAAAAMFBMVEWupZzj39rEvbTy8O3X0sz9/PvGwLu8tavQysHq6OS0rKP5+Pbd2dT29fPMxbzPx8DKErMJAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGUlEQVQoFX3TQWgTQRQA0MWLIJJDYehBTykhG5ERTx56K1u8eEhCYtomE7x5L4iLh0ViF7egewuFFqSIYE6hIHsIYQ6CQSg9CDKn4QsNCRlB59C74J/ZNHW1+An5+bOPyf6/s46oz2P+A0yIeZZ2ieEHi6TOnLKTxvWq+b52mxlVO3xnM1s7xLX1504XQH65OnW2dBqn7cCkYsFsfYsWpyY/2salmFTpEyzeR8zosYqMdiPDXdyU52K1wgEa/SjGpdEwUAxqvRfckQCDOyFearsEHe2grvkh/cFAHKvdtI3lcVceKQIOFpv+FOZaNPQBwJZLPp+hfrvT5JZXaUFsR8zqQc9qSgAharkfS5M/5F6nGJJAtXq/eLr3ucZpHccSxOOIPaQhtHohpCH2Xu6rLmQ0djnr4/+J3C6v+AW8/XWYxwYNdlhWj/P5fPSTQwVr0T9lGxdaBCqErNZaqYnEwbkjEB3NasGF3lPdrHa1nnxNOMgj0+neePUPjd2v/qVvUv29ifvc19huQ48qwXShy/9o8o3OSk0cs37mOFd0Ydgvsf/oZEnPVtggfd66lORn9mDyyzXU13SRtH2L6aR5T/snGAcZPfAXz5J1YlJWBEuxdMYqQecpBrlM49xAbmqyHA+xlA1FxBtqT2xmJoNXZlIt74ZBLeJ9ZGDqByNI7p543idzJ23vXEv7IgnsxiS+eNtwNbFdLq7+Bi4wQ0I4SVb9AAAAAElFTkSuQmCC') no-repeat; - opacity: .6; - padding: 0; - margin: 0; -} + .tracy-footer-logo a { + position: absolute; + bottom: 0; + right: 0; + width: 100px; + height: 50px; + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAUBAMAAAD/1DctAAAAMFBMVEWupZzj39rEvbTy8O3X0sz9/PvGwLu8tavQysHq6OS0rKP5+Pbd2dT29fPMxbzPx8DKErMJAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGUlEQVQoFX3TQWgTQRQA0MWLIJJDYehBTykhG5ERTx56K1u8eEhCYtomE7x5L4iLh0ViF7egewuFFqSIYE6hIHsIYQ6CQSg9CDKn4QsNCRlB59C74J/ZNHW1+An5+bOPyf6/s46oz2P+A0yIeZZ2ieEHi6TOnLKTxvWq+b52mxlVO3xnM1s7xLX1504XQH65OnW2dBqn7cCkYsFsfYsWpyY/2salmFTpEyzeR8zosYqMdiPDXdyU52K1wgEa/SjGpdEwUAxqvRfckQCDOyFearsEHe2grvkh/cFAHKvdtI3lcVceKQIOFpv+FOZaNPQBwJZLPp+hfrvT5JZXaUFsR8zqQc9qSgAharkfS5M/5F6nGJJAtXq/eLr3ucZpHccSxOOIPaQhtHohpCH2Xu6rLmQ0djnr4/+J3C6v+AW8/XWYxwYNdlhWj/P5fPSTQwVr0T9lGxdaBCqErNZaqYnEwbkjEB3NasGF3lPdrHa1nnxNOMgj0+neePUPjd2v/qVvUv29ifvc19huQ48qwXShy/9o8o3OSk0cs37mOFd0Ydgvsf/oZEnPVtggfd66lORn9mDyyzXU13SRtH2L6aR5T/snGAcZPfAXz5J1YlJWBEuxdMYqQecpBrlM49xAbmqyHA+xlA1FxBtqT2xmJoNXZlIt74ZBLeJ9ZGDqByNI7p543idzJ23vXEv7IgnsxiS+eNtwNbFdLq7+Bi4wQ0I4SVb9AAAAAElFTkSuQmCC') no-repeat; + opacity: .6; + padding: 0; + margin: 0; + } -#tracy-bs .tracy-footer-logo a:hover, -#tracy-bs .tracy-footer-logo a:focus { - opacity: 1; - transition: opacity 0.1s; -} + .tracy-footer-logo a:hover, + .tracy-footer-logo a:focus { + opacity: 1; + transition: opacity 0.1s; + } -#tracy-bs .tracy-section { - padding: var(--tracy-space); -} + .tracy-section { + padding: var(--tracy-space); + } -#tracy-bs .tracy-section-panel { - background: #5040200E; - padding: var(--tracy-space); - border-radius: 8px; - box-shadow: inset 1px 1px 0px 0 #00000005; - overflow: hidden; -} + .tracy-section-panel { + background: #5040200E; + padding: var(--tracy-space); + border-radius: 8px; + box-shadow: inset 1px 1px 0px 0 #00000005; + overflow: hidden; + } -#tracy-bs .outer, /* deprecated */ -#tracy-bs .tracy-pane { - overflow: auto; -} + .outer, /* deprecated */ + .tracy-pane { + overflow: auto; + } -#tracy-bs.tracy-mac .tracy-pane { - padding-bottom: 12px; -} + &.tracy-mac .tracy-pane { + padding-bottom: 12px; + } -/* header */ -#tracy-bs .tracy-section--error { - background: #CD1818; - color: white; -} + /* header */ + .tracy-section--error { + background: #CD1818; + color: white; + } -#tracy-bs .tracy-section--error p, -#tracy-bs .tracy-section--error h1 { - font-size: 13pt; - color: white; -} + .tracy-section--error p, + .tracy-section--error h1 { + font-size: 13pt; + color: white; + } -#tracy-bs .tracy-section--error::selection, -#tracy-bs .tracy-section--error ::selection { - color: black !important; - background: #FDF5CE !important; -} + .tracy-section--error::selection, + .tracy-section--error ::selection { + color: black !important; + background: #FDF5CE !important; + } -#tracy-bs .tracy-section--error h1 a { - color: #ffefa1 !important; -} + .tracy-section--error h1 a { + color: #ffefa1 !important; + } -#tracy-bs .tracy-section--error span span { - font-size: 80%; - color: rgba(255, 255, 255, 0.5); - text-shadow: none; -} + .tracy-section--error span span { + font-size: 80%; + color: rgba(255, 255, 255, 0.5); + text-shadow: none; + } -#tracy-bs .tracy-section--error a.tracy-action { - color: white !important; - opacity: 0; - font-size: .7em; - border-bottom: none !important; -} + .tracy-section--error a.tracy-action { + color: white !important; + opacity: 0; + font-size: .7em; + border-bottom: none !important; + } -#tracy-bs .tracy-section--error:hover a.tracy-action { - opacity: .6; -} + .tracy-section--error:hover a.tracy-action { + opacity: .6; + } -#tracy-bs .tracy-section--error a.tracy-action:hover { - opacity: 1; -} + .tracy-section--error a.tracy-action:hover { + opacity: 1; + } -#tracy-bs .tracy-section--error i { - color: #ffefa1; - font-style: normal; -} + .tracy-section--error i { + color: #ffefa1; + font-style: normal; + } -#tracy-bs .tracy-section--error:has(.tracy-caused) { - border-radius: 0 0 0 8px; - overflow: hidden; -} + .tracy-section--error:has(.tracy-caused) { + border-radius: 0 0 0 8px; + overflow: hidden; + } -#tracy-bs .tracy-caused { - margin: var(--tracy-space) calc(-1 * var(--tracy-space)) calc(-1 * var(--tracy-space)); - padding: .3em var(--tracy-space); - background: #df8075; - white-space: nowrap; -} + .tracy-caused { + margin: var(--tracy-space) calc(-1 * var(--tracy-space)) calc(-1 * var(--tracy-space)); + padding: .3em var(--tracy-space); + background: #df8075; + white-space: nowrap; + } -#tracy-bs .tracy-caused a { - color: white; -} + .tracy-caused a { + color: white; + } -/* source code */ -#tracy-bs pre.tracy-code > div { - min-width: fit-content; - white-space: pre; -} + /* source code */ + pre.tracy-code > div { + min-width: fit-content; + white-space: pre; + } -#tracy-bs .tracy-code-comment { - color: rgba(0, 0, 0, 0.5); - font-style: italic; -} + .tracy-code-comment { + color: rgba(0, 0, 0, 0.5); + font-style: italic; + } -#tracy-bs .tracy-code-keyword { - color: #D24; - font-weight: bold; -} + .tracy-code-keyword { + color: #D24; + font-weight: bold; + } -#tracy-bs .tracy-code-var { - font-weight: bold; -} + .tracy-code-var { + font-weight: bold; + } -#tracy-bs .tracy-line-highlight { - background: #CD1818; - color: white; - font-weight: bold; - font-style: normal; - display: block; - padding: 0 1ch; - margin: 0 -1ch -1lh; -} + .tracy-line-highlight { + background: #CD1818; + color: white; + font-weight: bold; + font-style: normal; + display: block; + padding: 0 1ch; + margin: 0 -1ch -1lh; + } -#tracy-bs .tracy-column-highlight { - display: inline-block; - backdrop-filter: grayscale(1); - margin: 0 -1px; - padding: 0 1px; -} + .tracy-column-highlight { + display: inline-block; + backdrop-filter: grayscale(1); + margin: 0 -1px; + padding: 0 1px; + } -#tracy-bs .tracy-line { - color: #9F9C7F; - font-weight: normal; - font-style: normal; -} + .tracy-line { + color: #9F9C7F; + font-weight: normal; + font-style: normal; + } -#tracy-bs a.tracy-editor { - color: inherit; - border-bottom: 1px dotted rgba(0, 0, 0, .3); - border-radius: 3px; -} + a.tracy-editor { + color: inherit; + border-bottom: 1px dotted rgba(0, 0, 0, .3); + border-radius: 3px; + } -#tracy-bs a.tracy-editor:hover { - background: #0001; -} + a.tracy-editor:hover { + background: #0001; + } -#tracy-bs span[data-tracy-href] { - border-bottom: 1px dotted rgba(0, 0, 0, .3); -} + span[data-tracy-href] { + border-bottom: 1px dotted rgba(0, 0, 0, .3); + } -#tracy-bs .tracy-dump-whitespace { - color: #0003; -} + .tracy-dump-whitespace { + color: #0003; + } -#tracy-bs .tracy-callstack { - display: grid; - overflow: auto; - grid-template-columns: max-content 1fr; - row-gap: calc(.5 * var(--tracy-space)); -} + .tracy-callstack { + display: grid; + overflow: auto; + grid-template-columns: max-content 1fr; + row-gap: calc(.5 * var(--tracy-space)); + } -#tracy-bs .tracy-callstack-file { - text-align: right; - padding-right: var(--tracy-space); - white-space: nowrap; -} + .tracy-callstack-file { + text-align: right; + padding-right: var(--tracy-space); + white-space: nowrap; + } -#tracy-bs .tracy-callstack-callee { - white-space: nowrap; -} + .tracy-callstack-callee { + white-space: nowrap; + } -#tracy-bs .tracy-callstack-additional { - grid-column-start: 1; - grid-column-end: 3; -} + .tracy-callstack-additional { + grid-column-start: 1; + grid-column-end: 3; + } -#tracy-bs .tracy-callstack-args tr:first-child > * { - position: relative; -} + .tracy-callstack-args tr:first-child > * { + position: relative; + } -#tracy-bs .tracy-callstack-args tr:first-child td:before { - position: absolute; - right: .3em; - content: 'may not be true'; - opacity: .4; -} + .tracy-callstack-args tr:first-child td:before { + position: absolute; + right: .3em; + content: 'may not be true'; + opacity: .4; + } -#tracy-bs .tracy-panel-fadein { - animation: tracy-panel-fadein .12s ease; -} + .tracy-panel-fadein { + animation: tracy-panel-fadein .12s ease; + } -@keyframes tracy-panel-fadein { - 0% { - opacity: 0; + @keyframes tracy-panel-fadein { + 0% { + opacity: 0; + } } -} -#tracy-bs .tracy-section--causedby { - flex-direction: column; - padding: 0; -} + .tracy-section--causedby { + flex-direction: column; + padding: 0; + } -#tracy-bs .tracy-section--causedby:not(.tracy-collapsed) { - display: flex; -} + .tracy-section--causedby:not(.tracy-collapsed) { + display: flex; + } -#tracy-bs .tracy-section--causedby .tracy-section--error { - background: #cd1818a6; -} + .tracy-section--causedby .tracy-section--error { + background: #cd1818a6; + } -#tracy-bs .tracy-section--error + .tracy-section--stack { - margin-top: calc(1.5 * var(--tracy-space)); -} + .tracy-section--error + .tracy-section--stack { + margin-top: calc(1.5 * var(--tracy-space)); + } -/* tabs */ -#tracy-bs .tracy-tab-bar { - display: flex; - list-style: none; - padding-left: 0; - margin: 0; - width: 100%; - font-size: 110%; - column-gap: var(--tracy-space); -} + /* tabs */ + .tracy-tab-bar { + display: flex; + list-style: none; + padding-left: 0; + margin: 0; + width: 100%; + font-size: 110%; + column-gap: var(--tracy-space); + } -#tracy-bs .tracy-tab-bar a { - display: block; - padding: calc(.5 * var(--tracy-space)) var(--tracy-space); - margin: 0; - height: 100%; - box-sizing: border-box; - border-radius: 5px 5px 0 0; - text-decoration: none; - transition: all 0.1s; -} + .tracy-tab-bar a { + display: block; + padding: calc(.5 * var(--tracy-space)) var(--tracy-space); + margin: 0; + height: 100%; + box-sizing: border-box; + border-radius: 5px 5px 0 0; + text-decoration: none; + transition: all 0.1s; + } -#tracy-bs .tracy-tab-bar > .tracy-active a { - background: white; + .tracy-tab-bar > .tracy-active a { + background: white; + } + + .tracy-tab-panel { + border-top: 2px solid white; + padding-top: var(--tracy-space); + overflow: auto; + } } -#tracy-bs .tracy-tab-panel { - border-top: 2px solid white; - padding-top: var(--tracy-space); - overflow: auto; } diff --git a/src/Tracy/BlueScreen/assets/bluescreen.js b/src/Tracy/BlueScreen/assets/bluescreen.js index 9e53d88bc..7249b0dd3 100644 --- a/src/Tracy/BlueScreen/assets/bluescreen.js +++ b/src/Tracy/BlueScreen/assets/bluescreen.js @@ -6,7 +6,27 @@ class BlueScreen { static init(ajax) { BlueScreen.globalInit(); - let blueScreen = document.getElementById('tracy-bs'); + let blueScreen = document.querySelector('.tracy-bs'); + + // Shadow DOM for CSS isolation + let host = document.createElement('tracy-bs'); + let shadow = host.attachShadow({ mode: 'open' }); + BlueScreen.shadow = shadow; + BlueScreen.host = host; + + if (ajax) { // injected into a host page + Tracy.adoptStyleSheets(shadow, ['shared', 'bluescreen']); + // all bluescreen rules are scoped to .tracy-bs which lives in the shadow root, + // document-level adoption only activates the html.tracy-bs-visible rules + Tracy.adoptStyleSheets(document, ['bluescreen']); + } else { // standalone error page, styles are in document.head so the page works without JavaScript + document.querySelectorAll('style.tracy-debug').forEach((s) => { + shadow.appendChild(s.cloneNode(true)); + }); + } + + shadow.appendChild(blueScreen); + document.body.appendChild(host); document.documentElement.classList.add('tracy-bs-visible'); if (navigator.userAgent.includes('Mac')) { @@ -14,22 +34,25 @@ class BlueScreen { } blueScreen.addEventListener('tracy-toggle', (e) => { - if (e.target.matches('#tracy-bs-toggle')) { // blue screen toggle + let target = Tracy.retarget(e); + if (target.matches('.tracy-bs-toggle')) { // blue screen toggle document.documentElement.classList.toggle('tracy-bs-visible', !e.detail.collapsed); - } else if (!e.target.matches('.tracy-dump *') && e.detail.originalEvent) { // panel toggle + } else if (!target.matches('.tracy-dump *') && e.detail.originalEvent) { // panel toggle e.detail.relatedTarget.classList.toggle('tracy-panel-fadein', !e.detail.collapsed); } }); if (!ajax) { - document.body.appendChild(blueScreen); - let id = location.href + document.querySelector('.tracy-section--error').textContent; + let id = location.href + shadow.querySelector('.tracy-section--error').textContent; Tracy.Toggle.persist(blueScreen, sessionStorage.getItem('tracy-toggles-bskey') === id); sessionStorage.setItem('tracy-toggles-bskey', id); } - (new ResizeObserver(stickyFooter)).observe(blueScreen); + BlueScreen.initCopyButton(shadow); + + Tracy.Dumper.init(shadow); + (new ResizeObserver(() => stickyFooter(shadow))).observe(blueScreen); if (document.documentElement.classList.contains('tracy-bs-visible')) { blueScreen.scrollIntoView(); @@ -41,33 +64,57 @@ class BlueScreen { // enables toggling via ESC document.addEventListener('keyup', (e) => { if (e.key === 'Escape' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { - Tracy.Toggle.toggle(document.getElementById('tracy-bs-toggle')); + let toggle = BlueScreen.shadow && BlueScreen.shadow.querySelector('.tracy-bs-toggle'); + if (toggle) { + Tracy.Toggle.toggle(toggle); + } } }); Tracy.TableSort.init(); Tracy.Tabs.init(); - window.addEventListener('scroll', stickyFooter); + window.addEventListener('scroll', () => stickyFooter(BlueScreen.shadow)); BlueScreen.globalInit = function () {}; } + static initCopyButton(shadow) { + let source = shadow.querySelector('.tracy-markdown-source'); + let button = shadow.querySelector('a[data-tracy-copy]'); + if (!source || !button || !navigator.clipboard) { // clipboard requires a secure context + return; + } + + button.hidden = false; + button.addEventListener('click', (e) => { + e.preventDefault(); + navigator.clipboard.writeText(JSON.parse(source.textContent)) + .then(() => { + let label = button.textContent; + button.textContent = 'copied ✓'; + setTimeout(() => button.textContent = label, 1500); + }); + }); + } + + static loadAjax(content) { - let ajaxBs = document.getElementById('tracy-bs'); - if (ajaxBs) { - ajaxBs.remove(); + let host = document.querySelector('tracy-bs'); + if (host) { + host.remove(); } document.body.insertAdjacentHTML('beforeend', content); - ajaxBs = document.getElementById('tracy-bs'); - Tracy.Dumper.init(ajaxBs); BlueScreen.init(true); } } -function stickyFooter() { - let footer = document.querySelector('#tracy-bs footer'); +function stickyFooter(root) { + let footer = root && root.querySelector('footer'); + if (!footer) { + return; + } footer.classList.toggle('tracy-footer--sticky', false); // to measure footer.offsetTop footer.classList.toggle('tracy-footer--sticky', footer.offsetHeight + footer.offsetTop - window.innerHeight - document.documentElement.scrollTop < 0); } diff --git a/src/Tracy/BlueScreen/assets/content.latte b/src/Tracy/BlueScreen/assets/content.latte index 6b37a21b7..5b89e4674 100644 --- a/src/Tracy/BlueScreen/assets/content.latte +++ b/src/Tracy/BlueScreen/assets/content.latte @@ -12,6 +12,7 @@ {varType bool $showEnvironment} {varType Tracy\BlueScreen $blueScreen} {varType bool $headersSent} +{varType bool $toScreen} {varType ?string $headersFile} {varType ?int $headersLine} {varType mixed[][] $obStatus} @@ -19,8 +20,8 @@ {varType Fiber[] $fibers} {* *} - -  +
+ 
{do $ex = $exception} @@ -71,4 +72,8 @@
- + {if $toScreen} + {* markdown source for the Copy button; must be after the snapshot meta, renderAgent() resets the snapshot state *} + + {/if} +
diff --git a/src/Tracy/BlueScreen/assets/page.latte b/src/Tracy/BlueScreen/assets/page.latte index 7f80014b2..ebd3cabf0 100644 --- a/src/Tracy/BlueScreen/assets/page.latte +++ b/src/Tracy/BlueScreen/assets/page.latte @@ -1,6 +1,7 @@ {use Tracy\Helpers} {varType Tracy\BlueScreen $blueScreen} {varType Throwable $exception} +{varType bool $toScreen} {varType ?string $nonce} {varType string $css} {varType string $js} @@ -40,7 +41,7 @@ {str_replace(['

@@ -20,26 +21,26 @@ echo ' '; -echo Tracy\Helpers::escapeHtml($title) /* pos 22:9 */; +echo Tracy\Helpers::escapeHtml($title) /* pos 23:9 */; echo ': '; -echo Tracy\Helpers::escapeHtml($exception->getMessage()) /* pos 22:19 */; -echo Tracy\Helpers::escapeHtml($code) /* pos 22:45 */; +echo Tracy\Helpers::escapeHtml($exception->getMessage()) /* pos 23:19 */; +echo Tracy\Helpers::escapeHtml($code) /* pos 23:45 */; echo ' '; -if (count($chain) > 1) /* pos 24:2 */ { +if (count($chain) > 1) /* pos 25:2 */ { echo ' @@ -63,21 +64,21 @@ echo ' require __DIR__ . '/content.phtml'; echo ' \'use strict\'; '; -echo str_replace(['