From 169357c7376f7788ab3d2b4b47d6e4f999386407 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 11 May 2026 23:09:28 +0200 Subject: [PATCH 01/19] cs --- src/Tracy/Bar/dist/dumps.agent.phtml | 1 + src/Tracy/Bar/dist/info.panel.phtml | 6 +++--- src/Tracy/Bar/dist/warnings.agent.phtml | 1 + src/Tracy/Bar/dist/warnings.panel.phtml | 2 +- src/Tracy/Bar/panels/info.panel.latte | 6 +++--- src/Tracy/Bar/panels/warnings.panel.latte | 2 +- src/Tracy/BlueScreen/assets/agent.latte | 2 +- src/Tracy/BlueScreen/assets/page.latte | 4 ++-- src/Tracy/BlueScreen/assets/section-header.latte | 2 +- src/Tracy/BlueScreen/dist/agent.phtml | 2 +- src/Tracy/BlueScreen/dist/page.phtml | 4 ++-- src/Tracy/BlueScreen/dist/section-header.phtml | 2 +- tests/Tracy/Bar.renderAgent().phpt | 4 +--- tests/Tracy/BlueScreen.renderAgent().phpt | 4 +--- tests/Tracy/Helpers.isAgent().phpt | 4 +--- 15 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/Tracy/Bar/dist/dumps.agent.phtml b/src/Tracy/Bar/dist/dumps.agent.phtml index ea5e8ea59..23d1512a9 100644 --- a/src/Tracy/Bar/dist/dumps.agent.phtml +++ b/src/Tracy/Bar/dist/dumps.agent.phtml @@ -17,3 +17,4 @@ foreach ($data as $item) /* pos 5:1 */ { echo "\n"; } + diff --git a/src/Tracy/Bar/dist/info.panel.phtml b/src/Tracy/Bar/dist/info.panel.phtml index e026ff40c..e87dfa97c 100644 --- a/src/Tracy/Bar/dist/info.panel.phtml +++ b/src/Tracy/Bar/dist/info.panel.phtml @@ -56,7 +56,7 @@ if ($packages || $devPackages) /* pos 35:3 */ { echo '

Composer Packages ('; echo Tracy\Helpers::escapeHtml(count($packages)) /* pos 37:24 */; - echo Tracy\Helpers::escapeHtml($devPackages ? ' + ' . count($devPackages) . ' dev' : '') /* pos 37:42 */; + echo Tracy\Helpers::escapeHtml($devPackages ? ' + ' . count($devPackages) . ' dev' : null) /* pos 37:42 */; echo ')

@@ -72,7 +72,7 @@ if ($packages || $devPackages) /* pos 35:3 */ { echo ' '; echo Tracy\Helpers::escapeHtml($package->version) /* pos 44:11 */; - echo Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '') /* pos 44:30 */; + echo Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : null) /* pos 44:30 */; echo ' '; @@ -95,7 +95,7 @@ if ($packages || $devPackages) /* pos 35:3 */ { echo ' '; echo Tracy\Helpers::escapeHtml($package->version) /* pos 54:12 */; - echo Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '') /* pos 54:31 */; + echo Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : null) /* pos 54:31 */; echo ' '; diff --git a/src/Tracy/Bar/dist/warnings.agent.phtml b/src/Tracy/Bar/dist/warnings.agent.phtml index ba0292e8b..2eda20ace 100644 --- a/src/Tracy/Bar/dist/warnings.agent.phtml +++ b/src/Tracy/Bar/dist/warnings.agent.phtml @@ -12,3 +12,4 @@ foreach ($data as $item => $count) /* pos 5:1 */ { echo "\n"; } + diff --git a/src/Tracy/Bar/dist/warnings.panel.phtml b/src/Tracy/Bar/dist/warnings.panel.phtml index 78b6c0ed5..fd722292a 100644 --- a/src/Tracy/Bar/dist/warnings.panel.phtml +++ b/src/Tracy/Bar/dist/warnings.panel.phtml @@ -10,7 +10,7 @@ foreach ($data as $item => $count) /* pos 6:3 */ { [$file, $line, $message] = explode('|', $item, 3) /* pos 7:4 */; echo ' '; - echo Tracy\Helpers::escapeHtml($count ? $count . '×' : '') /* pos 9:29 */; + echo Tracy\Helpers::escapeHtml($count ? $count . '×' : null) /* pos 9:29 */; echo '
';
 	echo Tracy\Helpers::escapeHtml($message) /* pos 10:14 */;
diff --git a/src/Tracy/Bar/panels/info.panel.latte b/src/Tracy/Bar/panels/info.panel.latte
index d2335706e..747857ae1 100644
--- a/src/Tracy/Bar/panels/info.panel.latte
+++ b/src/Tracy/Bar/panels/info.panel.latte
@@ -34,14 +34,14 @@
 
 		{if $packages || $devPackages}
 			

- Composer Packages ({count($packages)}{$devPackages ? ' + ' . count($devPackages) . ' dev' : ''}) + Composer Packages ({count($packages)}{$devPackages ? ' + ' . count($devPackages) . ' dev'})

- +
{$package->name}{$package->version}{strpos($package->version, dev) !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : ''}{$package->version}{strpos($package->version, dev) !== false && $package->hash ? ' #' . substr($package->hash, 0, 4)}
@@ -51,7 +51,7 @@ - +
{$package->name}{$package->version}{strpos($package->version, dev) !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : ''}{$package->version}{strpos($package->version, dev) !== false && $package->hash ? ' #' . substr($package->hash, 0, 4)}
{/if} diff --git a/src/Tracy/Bar/panels/warnings.panel.latte b/src/Tracy/Bar/panels/warnings.panel.latte index e639c71c0..0d50b83e3 100644 --- a/src/Tracy/Bar/panels/warnings.panel.latte +++ b/src/Tracy/Bar/panels/warnings.panel.latte @@ -6,7 +6,7 @@ {foreach $data as $item => $count} {do [$file, $line, $message] = explode('|', $item, 3)} - {$count ? $count . '×' : ''} + {$count ? $count . '×'}
{$message} in {Tracy\Helpers::editorLink($file, (int) $line)}
{/foreach} diff --git a/src/Tracy/BlueScreen/assets/agent.latte b/src/Tracy/BlueScreen/assets/agent.latte index 3e2a32bac..214b93919 100644 --- a/src/Tracy/BlueScreen/assets/agent.latte +++ b/src/Tracy/BlueScreen/assets/agent.latte @@ -45,7 +45,7 @@ This is an error page generated by Tracy (https://tracy.nette.org). {foreach Helpers::getExceptionChain($exception) as $i => $ex} {do $title = $blueScreen->getExceptionTitle($ex)} - {do $code = $ex->getCode() ? ' #' . $ex->getCode() : ''} + {do $code = $ex->getCode() ? ' #' . $ex->getCode()} {if $i === 0} # {$title}: {$ex->getMessage()}{$code} {else} diff --git a/src/Tracy/BlueScreen/assets/page.latte b/src/Tracy/BlueScreen/assets/page.latte index 0458a3597..7f80014b2 100644 --- a/src/Tracy/BlueScreen/assets/page.latte +++ b/src/Tracy/BlueScreen/assets/page.latte @@ -6,7 +6,7 @@ {varType string $js} {varType string $source} {do $title = $blueScreen->getExceptionTitle($exception)} -{do $code = $exception->getCode() ? ' #' . $exception->getCode() : ''} +{do $code = $exception->getCode() ? ' #' . $exception->getCode()} {do $chain = Helpers::getExceptionChain($exception)} {* *} @@ -23,7 +23,7 @@ {if count($chain) > 1} {/if} diff --git a/src/Tracy/BlueScreen/assets/section-header.latte b/src/Tracy/BlueScreen/assets/section-header.latte index b46182da4..98938cc96 100644 --- a/src/Tracy/BlueScreen/assets/section-header.latte +++ b/src/Tracy/BlueScreen/assets/section-header.latte @@ -5,7 +5,7 @@ {varType Tracy\BlueScreen $blueScreen} {do $title = $blueScreen->getExceptionTitle($ex)} -{do $code = $ex->getCode() ? ' #' . $ex->getCode() : ''} +{do $code = $ex->getCode() ? ' #' . $ex->getCode()}

{$title}{$code}

diff --git a/src/Tracy/BlueScreen/dist/agent.phtml b/src/Tracy/BlueScreen/dist/agent.phtml index fdaf58fb1..d0ab02451 100644 --- a/src/Tracy/BlueScreen/dist/agent.phtml +++ b/src/Tracy/BlueScreen/dist/agent.phtml @@ -63,7 +63,7 @@ Mapped source: '; echo "\n"; foreach (Helpers::getExceptionChain($exception) as $i => $ex) /* pos 46:1 */ { $title = $blueScreen->getExceptionTitle($ex) /* pos 47:2 */; - $code = $ex->getCode() ? ' #' . $ex->getCode() : '' /* pos 48:2 */; + $code = $ex->getCode() ? ' #' . $ex->getCode() : null /* pos 48:2 */; if ($i === 0) /* pos 49:2 */ { echo '# '; echo Tracy\Helpers::escapeMd($title) /* pos 50:5 */; diff --git a/src/Tracy/BlueScreen/dist/page.phtml b/src/Tracy/BlueScreen/dist/page.phtml index 587232dab..b6e2bb73b 100644 --- a/src/Tracy/BlueScreen/dist/page.phtml +++ b/src/Tracy/BlueScreen/dist/page.phtml @@ -8,7 +8,7 @@ use Tracy\Helpers; /** @var string $js */ /** @var string $source */ $title = $blueScreen->getExceptionTitle($exception) /* pos 8:1 */; -$code = $exception->getCode() ? ' #' . $exception->getCode() : '' /* pos 9:1 */; +$code = $exception->getCode() ? ' #' . $exception->getCode() : null /* pos 9:1 */; $chain = Helpers::getExceptionChain($exception) /* pos 10:1 */; echo '

@@ -39,7 +39,7 @@ if (count($chain) > 1) /* pos 24:2 */ { echo Tracy\Helpers::escapeHtml(get_debug_type($ex)) /* pos 26:12 */; echo ': '; echo Tracy\Helpers::escapeHtml($ex->getMessage()) /* pos 26:35 */; - echo Tracy\Helpers::escapeHtml($ex->getCode() ? ' #' . $ex->getCode() : '') /* pos 26:54 */; + echo Tracy\Helpers::escapeHtml($ex->getCode() ? ' #' . $ex->getCode() : null) /* pos 26:54 */; echo ' '; diff --git a/src/Tracy/BlueScreen/dist/section-header.phtml b/src/Tracy/BlueScreen/dist/section-header.phtml index f6d912bc0..4cdd26a20 100644 --- a/src/Tracy/BlueScreen/dist/section-header.phtml +++ b/src/Tracy/BlueScreen/dist/section-header.phtml @@ -7,7 +7,7 @@ use Tracy\Helpers; /** @var Tracy\BlueScreen $blueScreen */ echo "\n"; $title = $blueScreen->getExceptionTitle($ex) /* pos 7:1 */; -$code = $ex->getCode() ? ' #' . $ex->getCode() : '' /* pos 8:1 */; +$code = $ex->getCode() ? ' #' . $ex->getCode() : null /* pos 8:1 */; echo '
'; diff --git a/tests/Tracy/Bar.renderAgent().phpt b/tests/Tracy/Bar.renderAgent().phpt index bad6f6e84..9a434206a 100644 --- a/tests/Tracy/Bar.renderAgent().phpt +++ b/tests/Tracy/Bar.renderAgent().phpt @@ -1,11 +1,9 @@ - Date: Tue, 19 May 2026 15:03:18 +0200 Subject: [PATCH 02/19] tools: added latte-convert template compiler --- .gitattributes | 3 + composer.json | 3 +- tools/latte-convert/.gitignore | 3 + tools/latte-convert/compile.php | 57 ++++ tools/latte-convert/composer.json | 17 + tools/latte-convert/readme.md | 42 +++ tools/latte-convert/src/Engine.php | 68 ++++ tools/latte-convert/src/Escaper.php | 34 ++ tools/latte-convert/src/Extension.php | 156 +++++++++ tools/latte-convert/src/Nodes.php | 298 ++++++++++++++++++ tools/latte-convert/tests/bootstrap.php | 9 + tools/latte-convert/tests/compiler.phpt | 208 ++++++++++++ tools/latte-convert/tests/fixtures.phpt | 23 ++ .../tests/fixtures/kitchen-sink.latte | 72 +++++ .../tests/fixtures/kitchen-sink.phtml | 140 ++++++++ 15 files changed, 1132 insertions(+), 1 deletion(-) create mode 100644 tools/latte-convert/.gitignore create mode 100644 tools/latte-convert/compile.php create mode 100644 tools/latte-convert/composer.json create mode 100644 tools/latte-convert/readme.md create mode 100644 tools/latte-convert/src/Engine.php create mode 100644 tools/latte-convert/src/Escaper.php create mode 100644 tools/latte-convert/src/Extension.php create mode 100644 tools/latte-convert/src/Nodes.php create mode 100644 tools/latte-convert/tests/bootstrap.php create mode 100644 tools/latte-convert/tests/compiler.phpt create mode 100644 tools/latte-convert/tests/fixtures.phpt create mode 100644 tools/latte-convert/tests/fixtures/kitchen-sink.latte create mode 100644 tools/latte-convert/tests/fixtures/kitchen-sink.phtml diff --git a/.gitattributes b/.gitattributes index 433a2de9a..70074d01a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,9 @@ ncs.* export-ignore phpstan*.neon export-ignore src/**/*.latte export-ignore tests/ export-ignore +tools/latte-convert/ export-ignore *.php* diff=php *.sh text eol=lf +tools/latte-convert/tests/fixtures/*.latte text eol=lf +tools/latte-convert/tests/fixtures/*.phtml text eol=lf diff --git a/composer.json b/composer.json index 1c213cbfb..a4f2e7530 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "minimum-stability": "dev", "scripts": { "phpstan": "phpstan analyse", - "tester": "tester tests" + "tester": "tester tests", + "compile-templates": "@php tools/latte-convert/compile.php src/Tracy" }, "extra": { "branch-alias": { diff --git a/tools/latte-convert/.gitignore b/tools/latte-convert/.gitignore new file mode 100644 index 000000000..ce5bb4e8f --- /dev/null +++ b/tools/latte-convert/.gitignore @@ -0,0 +1,3 @@ +/vendor +/composer.lock +/tests/output diff --git a/tools/latte-convert/compile.php b/tools/latte-convert/compile.php new file mode 100644 index 000000000..7551fde96 --- /dev/null +++ b/tools/latte-convert/compile.php @@ -0,0 +1,57 @@ + single file or each *.latte in dir, output to sibling dist/ + * php compile.php single file with explicit output + * + * Files whose basename contains "agent" are compiled in text (markdown) mode. + */ + +require __DIR__ . '/vendor/autoload.php'; + +/** @var list $args */ +$args = $_SERVER['argv'] ?? []; + +if (count($args) < 2) { + fwrite(STDERR, "Usage: php compile.php []\n"); + exit(1); +} + +$engine = new Tracy\Tools\Compiler\Engine; +$path = $args[1]; +if (is_file($path)) { + compileFile($engine, $path, $args[2] ?? deriveOutput($path)); +} elseif (is_dir($path)) { + $iter = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); + foreach (new RegexIterator($iter, '~\.latte$~') as $file) { + compileFile($engine, $file->getPathname(), deriveOutput($file->getPathname())); + } +} else { + fwrite(STDERR, "Not a file or directory: $path\n"); + exit(1); +} + + +function deriveOutput(string $latteFile): string +{ + $distDir = dirname($latteFile, 2) . '/dist'; + if (!is_dir($distDir)) { + mkdir($distDir, recursive: true); + } + return $distDir . '/' . basename($latteFile, '.latte') . '.phtml'; +} + + +function compileFile(Tracy\Tools\Compiler\Engine $engine, string $input, string $output): void +{ + $textMode = str_contains(basename($input), 'agent'); + $engine->setContentType($textMode ? Latte\ContentType::Text : Latte\ContentType::Html); + if (file_put_contents($output, $engine->compile($input)) === false) { + fwrite(STDERR, "Error writing to '$output'\n"); + exit(1); + } + echo "$input -> $output\n"; +} diff --git a/tools/latte-convert/composer.json b/tools/latte-convert/composer.json new file mode 100644 index 000000000..09f11efad --- /dev/null +++ b/tools/latte-convert/composer.json @@ -0,0 +1,17 @@ +{ + "name": "tracy/latte-convert", + "description": "Compiles Tracy's .latte templates into standalone .phtml files with no Latte runtime dependency.", + "license": "BSD-3-Clause", + "type": "library", + "require": { + "php": ">=8.2", + "latte/latte": "^3.1.4" + }, + "require-dev": { + "nette/tester": "^2.6" + }, + "autoload": { + "classmap": ["src"] + }, + "minimum-stability": "dev" +} diff --git a/tools/latte-convert/readme.md b/tools/latte-convert/readme.md new file mode 100644 index 000000000..c0c5f28f4 --- /dev/null +++ b/tools/latte-convert/readme.md @@ -0,0 +1,42 @@ +# Tracy Latte Convert + +Build-time tool that compiles Tracy's `.latte` panel templates into standalone +`.phtml` files with no Latte runtime dependency. The generated `.phtml` files +are committed to the repo and shipped to end users. + +For Tracy maintainers only — end users of `tracy/tracy` never need this. + +## Requirements + +- PHP 8.2+ +- `latte/latte` ^3.1.4 with the following patches (not yet upstreamed): + - `Compiler\Escaper` non-final + - `Compiler\PrintContext::__construct()` accepts optional `?Escaper` + - `Compiler\TagParser::parseType()` accepts `Token::Php_Sr` + - `Compiler\TemplateParser::applyDedent()` uses regex `/^(\t+| +)/` + +## Usage + +```bash +# one-time setup +cd tools/latte-convert && composer install + +# compile all Tracy templates +composer compile-templates + +# compile a single .latte file (output auto-derived to sibling dist/) +php tools/latte-convert/compile.php src/Tracy/Bar/assets/bar.latte + +# compile a single file with explicit output +php tools/latte-convert/compile.php in.latte out.phtml + +# compile every .latte in a directory tree (recursive) +php tools/latte-convert/compile.php src/Tracy + +# tests +cd tools/latte-convert && vendor/bin/tester tests +``` + +**Output convention:** each `/.latte` compiles to `/../dist/.phtml` +(sibling `dist/` of the input directory). Templates whose basename contains `agent` +are compiled in text (markdown) mode. diff --git a/tools/latte-convert/src/Engine.php b/tools/latte-convert/src/Engine.php new file mode 100644 index 000000000..7731a2c8f --- /dev/null +++ b/tools/latte-convert/src/Engine.php @@ -0,0 +1,68 @@ +setFeature(Latte\Feature::Dedent, true); + } + + + protected function addDefaultExtensions(): void + { + $this->addExtension(new Extension); + } + + + /** + * Generates standalone PHP code from AST (no class wrapper, no Latte runtime). + */ + public function generate(TemplateNode $node, string $name): string + { + $escaper = new Escaper($node->contentType); + + $features = []; + foreach (Latte\Feature::cases() as $feature) { + if ($this->hasFeature($feature)) { + $features[$feature->name] = true; + } + } + + $context = new PrintContext($node->contentType, $features, $escaper); + + // Compile head (varType, use declarations) + $head = $node->head->print($context); + + // Compile main content + $main = $node->main->print($context); + + // Assemble standalone PHP + $code = "getContentType()) { + Latte\ContentType::Html => match ($this->getState()) { + self::HtmlText, self::HtmlAttribute, self::HtmlTag, self::HtmlBogusTag, self::HtmlComment + => 'Tracy\Helpers::escapeHtml(' . $str . ')', + self::HtmlRawText + => 'Tracy\Helpers::jsonEncode(' . $str . ', true)', + default => 'Tracy\Helpers::escapeHtml(' . $str . ')', + }, + Latte\ContentType::Text => 'Tracy\Helpers::escapeMd(' . $str . ')', + default => $str, + }; + } + + + public function escapeMandatory(string $str, ?Latte\Compiler\Position $position = null): string + { + return $str; + } +} diff --git a/tools/latte-convert/src/Extension.php b/tools/latte-convert/src/Extension.php new file mode 100644 index 000000000..6dbe20216 --- /dev/null +++ b/tools/latte-convert/src/Extension.php @@ -0,0 +1,156 @@ +formatMessage\(~'; + + + public function getTags(): array + { + return [ + // Standard Latte nodes (standalone-compatible, no $this references) + 'if' => Nodes\IfNode::create(...), + 'ifset' => Nodes\IfNode::create(...), + 'foreach' => Nodes\ForeachNode::create(...), + 'for' => Nodes\ForNode::create(...), + 'while' => Nodes\WhileNode::create(...), + 'switch' => Nodes\SwitchNode::create(...), + 'do' => Nodes\DoNode::create(...), + 'var' => Nodes\VarNode::create(...), + 'default' => Nodes\VarNode::create(...), + 'exitIf' => JumpNode::create(...), + 'continueIf' => JumpNode::create(...), + 'breakIf' => JumpNode::create(...), + '=' => PrintNode::create(...), + 'l' => fn(Latte\Compiler\Tag $tag) => new Latte\Compiler\Nodes\TextNode('{', $tag->position), + 'r' => fn(Latte\Compiler\Tag $tag) => new Latte\Compiler\Nodes\TextNode('}', $tag->position), + + // Custom Tracy nodes (generate standalone PHP) + 'varType' => VarTypeNode::create(...), + 'use' => UseNode::create(...), + 'define' => DefineNode::create(...), + 'include' => IncludeNode::create(...), + 'try' => TryNode::create(...), + ]; + } + + + public function getPasses(): array + { + return [ + 'tracyAttributes' => $this->attributePass(...), + 'tracyPrintNode' => $this->printNodePass(...), + ]; + } + + + /** + * Replaces ExpressionAttributeNode with Tracy-compatible inline code. + * Runs at compile-time, classifies attributes and generates standalone PHP. + */ + private function attributePass(TemplateNode $node): void + { + $noEscapePattern = self::NoEscapePattern; + + (new NodeTraverser)->traverse($node, function (Node $node) use ($noEscapePattern): ?Node { + if (!$node instanceof ExpressionAttributeNode) { + return null; + } + + $attr = $node; + $type = HtmlHelpers::classifyAttributeType($attr->name); + $isArray = $attr->value instanceof ArrayNode; + $hasJsonFilter = (bool) $attr->modifier->removeFilter('json'); + + // list attributes with non-array values fall back to regular attribute handling + if ($type === 'list' && !$isArray) { + $type = ''; + } + + // Check if expression contains known HTML-generating functions + $valueCode = $attr->value->print(new PrintContext); + $shouldEscape = !$hasJsonFilter && !preg_match($noEscapePattern, $valueCode); + + return new AuxiliaryNode( + function (PrintContext $context) use ($attr, $type, $shouldEscape, $hasJsonFilter): string { + $value = $attr->value->print($context); + if ($hasJsonFilter) { + $value = 'Tracy\Helpers::jsonEncode(' . $value . ')'; + } + $namePart = $attr->indentation . $attr->name; + $pos = $attr->value->position; + $line = $pos ? "/* pos $pos->line" . ($pos->column ? ":$pos->column" : '') . ' */' : ''; + + if (!$shouldEscape) { + $q = $hasJsonFilter ? "'" : '"'; + return 'echo ($ʟ_tmp = (' . $value . ')) === null ? \'\' : ' + . var_export($namePart . '=' . $q, true) . ' . $ʟ_tmp . ' . var_export($q, true) . " $line;\n"; + } + + return match ($type) { + 'bool' => "echo ($value) ? " . var_export($namePart, true) . " : '' $line;\n", + 'list' => 'echo ($ʟ_tmp = array_filter(' . $value . ')) ? ' + . var_export($namePart . '="', true) . " . Tracy\\Helpers::escapeHtml(implode(' ', \$ʟ_tmp)) . '\"' : '' $line;\n", + default => 'echo ($ʟ_tmp = (' . $value . ')) === null ? \'\' : ' + . var_export($namePart . '="', true) . " . Tracy\\Helpers::escapeHtml(\$ʟ_tmp) . '\"' $line;\n", + }; + }, + [$attr->value], + ); + }); + } + + + /** + * Handles |json filter and auto-disables escaping for known HTML-generating functions. + */ + private function printNodePass(TemplateNode $node): void + { + (new NodeTraverser)->traverse($node, function (Node $node): ?Node { + if (!$node instanceof PrintNode) { + return null; + } + + // |json filter → wrap in Tracy\Helpers::jsonEncode() + if ($node->modifier->removeFilter('json')) { + $node->modifier->escape = false; + $origExpr = $node->expression; + $node->expression = new Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode( + fn(PrintContext $context) => 'Tracy\Helpers::jsonEncode(' . $origExpr->print($context) . ')', + [$origExpr], + ); + return $node; + } + + // Disable escaping for functions that return pre-escaped HTML + if ($node->modifier->escape) { + $code = $node->expression->print(new PrintContext); + if (preg_match(self::NoEscapePattern, $code)) { + $node->modifier->escape = false; + } + } + + return null; + }); + } +} diff --git a/tools/latte-convert/src/Nodes.php b/tools/latte-convert/src/Nodes.php new file mode 100644 index 000000000..20c507cf4 --- /dev/null +++ b/tools/latte-convert/src/Nodes.php @@ -0,0 +1,298 @@ +expectArguments(); + $node = new static; + $typeNode = $tag->parser->parseType(); + $node->typeStr = $typeNode ? $typeNode->type : ''; + $token = $tag->parser->stream->consume(Token::Php_Variable); + $node->variable = $token->text; + return $node; + } + + + public function print(PrintContext $context): string + { + return "/** @var $this->typeStr $this->variable */\n"; + } + + + public function &getIterator(): \Generator + { + false && yield; + } +} + + +/** + * {use Tracy\Helpers} + * Generates PHP use statement. + */ +class UseNode extends StatementNode +{ + public string $namespace; + + + public static function create(Tag $tag): static + { + $tag->expectArguments(); + $node = new static; + // Consume all remaining tokens as the namespace path + $parts = []; + while (!$tag->parser->stream->is(Token::End)) { + $parts[] = $tag->parser->stream->consume()->text; + } + $node->namespace = implode('', $parts); + return $node; + } + + + public function print(PrintContext $context): string + { + return 'use ' . $this->namespace . ";\n"; + } + + + public function &getIterator(): \Generator + { + false && yield; + } +} + + +/** + * {define name $param1, $param2} ... {/define} + * Generates $_blocks closure (standard Latte DefineNode generates class methods). + */ +class DefineNode extends StatementNode +{ + public string $name; + public string $params; + public AreaNode $content; + + + /** @return \Generator, array{AreaNode, ?Tag}, static> */ + public static function create(Tag $tag, TemplateParser $parser): \Generator + { + $tag->expectArguments(); + $node = $tag->node = new static; + + // Parse block name + $tag->parser->stream->tryConsume('#'); + $nameExpr = $tag->parser->parseUnquotedStringOrExpression(); + if ($nameExpr instanceof Scalar\StringNode) { + $node->name = $nameExpr->value; + } else { + throw new CompileException('Block name must be a static string.', $tag->position); + } + + // Parse parameters as raw string + $tag->parser->stream->tryConsume(','); + $params = []; + while (!$tag->parser->stream->is(Token::End)) { + $params[] = $tag->parser->stream->consume()->text; + } + $node->params = implode('', $params); + + [$node->content, $endTag] = yield; + if ($endTag) { + $endTag->parser->stream->tryConsume($node->name); + } + + return $node; + } + + + public function print(PrintContext $context): string + { + $content = $this->content->print($context); + return '$_blocks[' . var_export($this->name, true) . '] = function (' . $this->params . ") use (&\$_blocks) {\n" + . $content + . "};\n"; + } + + + public function &getIterator(): \Generator + { + yield $this->content; + } +} + + +/** + * {include name $args} or {include 'file.phtml'} + * Block include: $_blocks['name']($args) + * File include: require __DIR__ . '/file.phtml' + */ +class IncludeNode extends StatementNode +{ + public bool $isFile = false; + public string $name; + public string $args; + + + public static function create(Tag $tag, TemplateParser $parser): static + { + $tag->outputMode = $tag::OutputRemoveIndentation; + $tag->expectArguments(); + + $node = new static; + + // Try to detect block vs file keyword + $tag->parser->tryConsumeTokenBeforeUnquotedString('block', 'file'); + $tag->parser->stream->tryConsume('#'); + + $nameExpr = $tag->parser->parseUnquotedStringOrExpression(); + if ($nameExpr instanceof Scalar\StringNode) { + $node->name = $nameExpr->value; + $node->isFile = str_contains($node->name, '.'); + } else { + throw new CompileException('Include name must be a static string.', $tag->position); + } + + // Parse remaining arguments as raw string + $tag->parser->stream->tryConsume(','); + $args = []; + while (!$tag->parser->stream->is(Token::End)) { + $args[] = $tag->parser->stream->consume()->text; + } + $node->args = implode('', $args); + + return $node; + } + + + public function print(PrintContext $context): string + { + if ($this->isFile) { + return 'require __DIR__ . ' . var_export('/' . $this->name, true) . ";\n"; + } + + return '$_blocks[' . var_export($this->name, true) . "]($this->args);\n"; + } + + + public function &getIterator(): \Generator + { + false && yield; + } +} + + +/** + * {try} ... {rollback} ... {/try} + * Generates simple try/catch (standard Latte TryNode uses output buffering). + */ +class TryNode extends StatementNode +{ + public AreaNode $try; + public ?AreaNode $catch = null; + + + /** @return \Generator, array{AreaNode, ?Tag}, static> */ + public static function create(Tag $tag): \Generator + { + $node = $tag->node = new static; + [$node->try, $nextTag] = yield ['rollback', 'else']; + if ($nextTag?->name === 'rollback' || $nextTag?->name === 'else') { + [$node->catch] = yield; + } + + return $node; + } + + + public function print(PrintContext $context): string + { + $code = "try {\n" . $this->try->print($context) . "}\n"; + if ($this->catch) { + $code .= "catch (\\Throwable) {\n" . $this->catch->print($context) . "}\n"; + } else { + $code .= "catch (\\Throwable) {}\n"; + } + + return $code; + } + + + public function &getIterator(): \Generator + { + yield $this->try; + if ($this->catch) { + yield $this->catch; + } + } +} + + +/** + * {exitIf $cond}, {continueIf $cond}, {breakIf $cond} + * Simplified JumpNode without parent-tag context checks. + */ +class JumpNode extends StatementNode +{ + public string $type; + public ExpressionNode $condition; + + + public static function create(Tag $tag): static + { + $tag->expectArguments(); + $tag->outputMode = $tag::OutputRemoveIndentation; + $node = new static; + $node->type = $tag->name; + $node->condition = $tag->parser->parseExpression(); + return $node; + } + + + public function print(PrintContext $context): string + { + return $context->format( + "if (%node) %line %raw\n", + $this->condition, + $this->position, + match ($this->type) { + 'breakIf' => 'break;', + 'continueIf' => 'continue;', + 'exitIf' => 'return;', + default => 'return;', + }, + ); + } + + + public function &getIterator(): \Generator + { + yield $this->condition; + } +} diff --git a/tools/latte-convert/tests/bootstrap.php b/tools/latte-convert/tests/bootstrap.php new file mode 100644 index 000000000..e45ea119d --- /dev/null +++ b/tools/latte-convert/tests/bootstrap.php @@ -0,0 +1,9 @@ +setLoader(new Latte\Loaders\StringLoader([$name => $template])); + $engine->setContentType(Latte\ContentType::Html); + return $engine->compile($name); +} + + +function compileText(string $template): string +{ + global $engine; + static $n = 0; + $name = 'text' . $n++; + $engine->setLoader(new Latte\Loaders\StringLoader([$name => $template])); + $engine->setContentType(Latte\ContentType::Text); + return $engine->compile($name); +} + + +// Escaping + +test('HTML text is escaped', function () { + $r = compile('{$foo}'); + Assert::contains('Tracy\Helpers::escapeHtml($foo)', $r); +}); + +test('|noescape disables escaping', function () { + $r = compile('{$foo|noescape}'); + Assert::notContains('escapeHtml', $r); + Assert::contains('$foo', $r); +}); + +test('script context uses jsonEncode', function () { + $r = compile(''); + Assert::contains('Tracy\Helpers::jsonEncode($data, true)', $r); +}); + +test('text mode disables escaping', function () { + $r = compileText('{$foo}'); + Assert::notContains('escapeHtml', $r); + Assert::notContains('jsonEncode', $r); +}); + +test('regular expression is escaped', function () { + $r = compile('{$name}'); + Assert::contains('Tracy\Helpers::escapeHtml($name)', $r); +}); + + +// noEscape for known functions + +test('editorLink not escaped', function () { + $r = compile('{Helpers::editorLink($f, $l)}'); + Assert::notContains('escapeHtml', $r); +}); + +test('highlightFile not escaped', function () { + $r = compile('{BlueScreen::highlightFile($f, $l)}'); + Assert::notContains('escapeHtml', $r); +}); + +test('$dump() not escaped', function () { + $r = compile('{$dump($v)}'); + Assert::notContains('escapeHtml', $r); +}); + +test('Dumper::toHtml not escaped', function () { + $r = compile('{Dumper::toHtml($v)}'); + Assert::notContains('escapeHtml', $r); +}); + +test('formatMessage not escaped', function () { + $r = compile('{$blueScreen->formatMessage($ex)}'); + Assert::notContains('escapeHtml', $r); +}); + + +// |json filter + +test('|json in text context', function () { + $r = compile('{=[1,2,3]|json}'); + Assert::contains('Tracy\Helpers::jsonEncode([1, 2, 3])', $r); + Assert::notContains('escapeHtml', $r); +}); + +test('|json in attribute uses single quotes', function () { + $r = compile(''); + Assert::contains('jsonEncode', $r); + Assert::contains("foo=\\'", $r); +}); + +test('|json on data attribute', function () { + $r = compile('
');
+	Assert::contains('Tracy\Helpers::jsonEncode($snapshot)', $r);
+	Assert::contains("data-snapshot=\\'", $r);
+	Assert::notContains('escapeHtml', $r);
+});
+
+
+// Attribute expressions
+
+test('regular attr={$var}', function () {
+	$r = compile('
'); + Assert::contains('escapeHtml', $r); + Assert::contains('attr="', $r); +}); + +test('boolean attr (hidden)', function () { + $r = compile(''); + Assert::contains("' hidden'", $r); +}); + +test('class={[...]} list attribute', function () { + $r = compile('
'); + Assert::contains('array_filter', $r); + Assert::contains('implode', $r); +}); + +test('noEscape in attribute (formatSnapshotAttribute)', function () { + $r = compile(''); + Assert::notContains('escapeHtml', $r); +}); + + +// Tags + +test('{varType}', function () { + $r = compile('{varType string $name}'); + Assert::contains('/** @var string $name */', $r); +}); + +test('{use}', function () { + $r = compile('{use Tracy\Helpers}'); + Assert::contains('use Tracy\Helpers;', $r); +}); + +test('{var}', function () { + $r = compile('{var $x = 42}'); + Assert::contains('$x = 42', $r); +}); + +test('{do}', function () { + $r = compile('{do $x = 1}'); + Assert::contains('$x = 1', $r); +}); + +test('{if}/{else}/{/if}', function () { + $r = compile('{if $cond}A{else}B{/if}'); + Assert::contains('if ($cond)', $r); + Assert::contains('else', $r); +}); + +test('{foreach}', function () { + $r = compile('{foreach $items as $item}{$item}{/foreach}'); + Assert::contains('foreach ($items as $item)', $r); +}); + +test('{exitIf}', function () { + $r = compile('{exitIf $done}'); + Assert::contains('if ($done)', $r); + Assert::contains('return;', $r); +}); + +test('{continueIf}', function () { + $r = compile('{foreach $a as $b}{continueIf $b}{/foreach}'); + Assert::contains('continue;', $r); +}); + +test('{define}/{include} block', function () { + $r = compile('{define foo}hello{/define}{include foo}'); + Assert::contains("\$_blocks['foo'] = function", $r); + Assert::contains("\$_blocks['foo']()", $r); +}); + +test('{include file}', function () { + $r = compile("{include 'section.phtml'}"); + Assert::contains("require __DIR__ . '/section.phtml'", $r); +}); + +test('{try}/{rollback}', function () { + $r = compile('{try}{do $x = dangerous()}{rollback}{do $x = fallback()}{/try}'); + Assert::contains('try {', $r); + Assert::contains('catch (\Throwable)', $r); +}); + + +// Output format + +test('starts with setContentType($textMode ? Latte\ContentType::Text : Latte\ContentType::Html); + Assert::same(file_get_contents($phtmlFile), $engine->compile($latteFile)); + }); +} diff --git a/tools/latte-convert/tests/fixtures/kitchen-sink.latte b/tools/latte-convert/tests/fixtures/kitchen-sink.latte new file mode 100644 index 000000000..95d39cd8e --- /dev/null +++ b/tools/latte-convert/tests/fixtures/kitchen-sink.latte @@ -0,0 +1,72 @@ +{varType string $file} +{varType int $line} +{varType int $expanded} +{use Tracy\Helpers} + + +

Queries: {$count}{$totalTime ? foo(' time ms ', " time ms ", '', "") : Anjb-d}, {$name . (string) $foo} {=\JSON_UNESCAPED_SLASHES}

+ +
+ {define foo} + hello + {exitIf true} + {/define} + + {define bar $arg, $arg} + hello + {/define} + + {include foo} + {include for $var1, var} + {include 'section-exception.phtml'} + + {foreach $events as [$connection, $query, $trace, $time, $rows, $error, $command, $explain]} + + + {if $error} + ERROR + {elseif $time !== null}{sprintf('%0.3f', $time * 1000)} + {/if} + + {if $explain}
explain{/if} + {if $trace}
trace{/if} + + + + {Nette\Database\Helpers::dumpSql($query, $connection)|noescape} + + {if $trace} + {substr_replace(Helpers::editorLink($trace[0][file], $trace[0][line]), ' class="nette-DbConnectionPanel-source"', 2, 0)|noescape} + + {foreach $trace as $row} + + + + + {/foreach} +
{isset($row[file]) ? Tracy\Helpers::editorLink($row[file], $row[line])}{$row[class] ?? ''}{$row[type] ?? ''}{$row[function]}()
+ {/if} + + + {$rows} + + {/foreach} + + {if count($events) < $count}

...and more

{/if} +
+ +
+ + +link +
+ +{try} + {do $x = dangerous()} +{rollback} + {do $x = fallback()} +{/try} diff --git a/tools/latte-convert/tests/fixtures/kitchen-sink.phtml b/tools/latte-convert/tests/fixtures/kitchen-sink.phtml new file mode 100644 index 000000000..e0f2760c2 --- /dev/null +++ b/tools/latte-convert/tests/fixtures/kitchen-sink.phtml @@ -0,0 +1,140 @@ + + #tracy-debug td.nette-DbConnectionPanel-sql { background: white !important; overflow-x: auto; max-width: 0; } + #tracy-debug .nette-DbConnectionPanel-source { color: #BBB !important } + #tracy-debug .nette-DbConnectionPanel-explain td { white-space: pre } + + +

Queries: '; +echo Tracy\Helpers::escapeHtml($count) /* pos 11:14 */; +echo Tracy\Helpers::escapeHtml($totalTime ? foo(' time ms ', ' time ms ', '', '') : 'Anjb-d') /* pos 11:22 */; +echo ', '; +echo Tracy\Helpers::escapeHtml($name . (string) $foo) /* pos 11:85 */; +echo ' '; +echo Tracy\Helpers::escapeHtml(\JSON_UNESCAPED_SLASHES) /* pos 11:109 */; +echo '

+ +
+'; +$_blocks['foo'] = function () use (&$_blocks) { + echo ' hello +'; + if (true) /* pos 16:3 */ return; +}; +echo "\n"; +$_blocks['bar'] = function ($arg,$arg) use (&$_blocks) { + echo ' hello +'; +}; +echo "\n"; +$_blocks['foo'](); +$_blocks['for']($var1,var); +require __DIR__ . '/section-exception.phtml'; +echo "\n"; +foreach ($events as [$connection, $query, $trace, $time, $rows, $error, $command, $explain]) /* pos 27:2 */ { + echo ' + +'; + if ($error) /* pos 30:5 */ { + echo ' ERROR + '; + } elseif ($time !== null) /* pos 32:6 */ { + echo Tracy\Helpers::escapeHtml(sprintf('%0.3f', $time * 1000)) /* pos 32:29 */; + echo "\n"; + } + + echo "\n"; + if ($explain) /* pos 35:5 */ { + echo '
explain'; + } + echo "\n"; + if ($trace) /* pos 36:5 */ { + echo '
trace'; + } + echo ' + + + + '; + echo Nette\Database\Helpers::dumpSql($query, $connection) /* pos 40:5 */; + echo ' + +'; + if ($trace) /* pos 42:5 */ { + echo ' '; + echo substr_replace(Helpers::editorLink($trace[0]['file'], $trace[0]['line']), ' class="nette-DbConnectionPanel-source"', 2, 0) /* pos 43:6 */; + echo ' + +'; + foreach ($trace as $row) /* pos 45:7 */ { + echo ' + + + +'; + + } + + echo '
'; + echo isset($row['file']) ? Tracy\Helpers::editorLink($row['file'], $row['line']) : null /* pos 47:13 */; + echo ''; + echo Tracy\Helpers::escapeHtml($row['class'] ?? '') /* pos 48:13 */; + echo Tracy\Helpers::escapeHtml($row['type'] ?? '') /* pos 48:32 */; + echo Tracy\Helpers::escapeHtml($row['function']) /* pos 48:50 */; + echo '()
+'; + } + echo ' + + '; + echo Tracy\Helpers::escapeHtml($rows) /* pos 55:8 */; + echo ' + +'; + +} + +echo "\n"; +if (count($events) < $count) /* pos 59:2 */ { + echo '

...and more

'; +} +echo ' +
+ +
+ + +link +
+ +'; +try { + $x = dangerous() /* pos 69:2 */; +} catch (\Throwable) { + $x = fallback() /* pos 71:2 */; +} From 173f1f8e8a50e4f8fdd776515f56f6925bc2bb13 Mon Sep 17 00:00:00 2001 From: Chris Yates Date: Sun, 12 Apr 2026 22:06:43 +0100 Subject: [PATCH 03/19] readme: added Yii3 Integration (#599) * opened 3.0-dev * requires PHP 8.1 * uses PascalCase constants * removed bridge for Latte * Logger: added typehints * Logger: md5 replaced with xxHash * error.log changed to warning.log * bar: errors panel => warnings * readme: added link to Joomla integration (#583) Link to Joomla! integration. * used attribute Deprecated * Update readme.md Add link to Yii3 integration --------- Co-authored-by: David Grudl Co-authored-by: n3t --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 933419c07..2298b2d43 100644 --- a/readme.md +++ b/readme.md @@ -457,5 +457,6 @@ This is a list of unofficial integrations to other frameworks and CMS: - Symfony framework: [kutny/tracy-bundle](https://github.com/kutny/tracy-bundle), [VasekPurchart/Tracy-Blue-Screen-Bundle](https://github.com/VasekPurchart/Tracy-Blue-Screen-Bundle) - [Wordpress](https://github.com/ktstudio/WP-Tracy) - [Joomla! CMS](https://n3t.bitbucket.io/extension/n3t-debug/) +- [Yii3 framework](https://github.com/beastbytes/yii-tracy) ... feel free to be famous, create an integration for your favourite platform! From 28ffb0c507be4ef51b77b4056a5bb22a7308a09d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 20 Feb 2026 04:05:00 +0100 Subject: [PATCH 04/19] added CLAUDE.md --- .gitattributes | 1 + CLAUDE.md | 573 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 574 insertions(+) create mode 100644 CLAUDE.md diff --git a/.gitattributes b/.gitattributes index 70074d01a..3ea365552 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ .gitattributes export-ignore .github/ export-ignore .gitignore export-ignore +CLAUDE.md export-ignore ncs.* export-ignore phpstan*.neon export-ignore src/**/*.latte export-ignore diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..8defaa420 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,573 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Tracy is a debugging and error visualization library for PHP (8.2-8.5). It provides beautiful error pages (BlueScreen), an interactive debug toolbar (Bar), advanced variable dumping, and production-ready error logging. + +**Key features:** +- BlueScreen: Beautiful error/exception visualization with stack traces +- Tracy Bar: Floating debug toolbar with extensible panel system +- Dumper: Advanced variable dumping with multiple output formats +- Logger: Error logging with email notifications +- Production/development mode auto-detection +- AI agent support: automated browsers (navigator.webdriver) get markdown output via console.error/console.log + +## Essential Commands + +### Testing +```bash +# Run all tests - ALWAYS use php-cgi (html tests only run with php-cgi) +vendor/bin/tester tests -p php-cgi -s + +# Run single test file +vendor/bin/tester tests/Tracy/Debugger.timer().phpt -p php-cgi -s + +# Run tests in specific directory +vendor/bin/tester tests/Dumper/ -s +``` + +### Code Quality +```bash +# Run PHPStan static analysis (level 7) +composer run phpstan + +# Lint JavaScript assets +npm run lint +npm run lint:fix +``` + +## Core Architecture + +### Main Components (Facade Pattern) + +**Tracy\Debugger** (src/Tracy/Debugger/Debugger.php) - Central facade +- `enable()` - Initialize Tracy +- `dump()` / `bdump()` - Variable dumping +- `log()` - Error logging +- `timer()` - Performance profiling +- Global functions available: `dump()`, `dumpe()`, `bdump()` + +**Strategy Pattern:** +- `DevelopmentStrategy` - Shows full debug info, Tracy Bar +- `ProductionStrategy` - Logs errors, shows user-friendly messages +- `DeferredContent` - Manages deferred rendering for AJAX/redirect requests +- Auto-detection: localhost = development, otherwise production +- Agent detection: `Helpers::isAgent()` checks `$_COOKIE['tracy-webdriver']` — set by `bar.js` when `navigator.webdriver` is `true` (Chrome MCP, Playwright, Puppeteer, Selenium) + +### Core Components + +**BlueScreen** (src/Tracy/BlueScreen/) +- Error/exception page rendering with stack traces +- Multiple output formats: HTML, CLI, markdown (for AI agents via console) +- `renderAgent()` returns markdown string, output via `console.error()` in page ` + +``` + +### Session Storage + +**Use native PHP session:** +```php +session_start(); +Debugger::setSessionStorage(new Tracy\NativeSession); +Debugger::enable(); +``` + +**Complex session initialization:** +```php +Debugger::setSessionStorage(new Tracy\NativeSession); +Debugger::enable(); + +// Custom session initialization +session_start(); + +Debugger::dispatch(); // Inform Tracy session is ready +``` + +### nginx Configuration + +If Tracy doesn't work on nginx, fix the `try_files` directive: + +```nginx +# Wrong +try_files $uri $uri/ /index.php; + +# Correct +try_files $uri $uri/ /index.php$is_args$args; +``` + +### IDE Integration + +Tracy can open files directly in your editor when clicking file names in error pages. + +**Editor integration scripts:** +- Windows: `tools/open-in-editor/windows/` +- Linux: `tools/open-in-editor/linux/` +- macOS: Use built-in URL schemes + +**Built-in editor URLs (macOS):** +```php +// PhpStorm +Tracy\Debugger::$editor = 'phpstorm://open?file=%file&line=%line'; + +// VS Code +Tracy\Debugger::$editor = 'vscode://file/%file:%line'; + +// TextMate +Tracy\Debugger::$editor = 'txmt://open/?url=file://%file&line=%line'; +``` + +**Editor path mapping for remote/Docker:** +```php +Debugger::$editorMapping = [ + '/var/www/html' => 'W:\\Projects\\myapp', // Docker to Windows + '/app' => '/Users/dev/projects/myapp', // Container to macOS +]; +``` From 9dc4e3af192052865ba6a068721d253d1a112c90 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 26 Mar 2026 14:33:18 +0100 Subject: [PATCH 05/19] opened 2.13-dev --- composer.json | 2 +- src/Tracy/Debugger/Debugger.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a4f2e7530..440f5f3e0 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.12-dev" + "dev-master": "2.13-dev" } }, "config": { diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php index 2b581311c..c6d20cd49 100644 --- a/src/Tracy/Debugger/Debugger.php +++ b/src/Tracy/Debugger/Debugger.php @@ -17,7 +17,7 @@ */ class Debugger { - public const Version = '2.12.0'; + public const Version = '2.13-dev'; /** server modes for Debugger::enable() */ public const From 8c1553f55579c19ac096a3ecded53150cb9026df Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 20 Feb 2026 08:53:24 +0100 Subject: [PATCH 06/19] added Shadow DOM for CSS isolation of Bar --- src/Tracy/Bar/assets/bar.css | 5 +++ src/Tracy/Bar/assets/bar.js | 21 ++++++++++--- src/Tracy/Dumper/assets/dumper.js | 51 ++++++++++++++++++++++++------- src/Tracy/assets/helpers.js | 2 +- src/Tracy/assets/reset.css | 6 ++-- src/Tracy/assets/table-sort.js | 5 +-- src/Tracy/assets/tabs.js | 6 ++-- src/Tracy/assets/toggle.js | 6 ++-- 8 files changed, 77 insertions(+), 25 deletions(-) diff --git a/src/Tracy/Bar/assets/bar.css b/src/Tracy/Bar/assets/bar.css index 5f71d82b8..64f724460 100644 --- a/src/Tracy/Bar/assets/bar.css +++ b/src/Tracy/Bar/assets/bar.css @@ -2,6 +2,11 @@ * This file is part of the Tracy (https://tracy.nette.org) */ +/* shadow DOM host */ +:host { + display: contents; +} + /* common styles */ #tracy-debug { --tracy-space: 10px; diff --git a/src/Tracy/Bar/assets/bar.js b/src/Tracy/Bar/assets/bar.js index f90637e81..37e71b077 100644 --- a/src/Tracy/Bar/assets/bar.js +++ b/src/Tracy/Bar/assets/bar.js @@ -26,7 +26,7 @@ function getOption(key) { class Panel { constructor(id) { this.id = id; - this.elem = document.getElementById(this.id); + this.elem = Debug.layer.querySelector('#' + CSS.escape(this.id)); this.elem.Tracy = this.elem.Tracy || {}; } @@ -253,7 +253,7 @@ Panel.zIndexCounter = 1; class Bar { init() { this.id = 'tracy-debug-bar'; - this.elem = document.getElementById(this.id); + this.elem = Debug.layer.querySelector('#' + this.id); draggable(this.elem, { handles: this.elem.querySelectorAll('li:first-child'), @@ -357,7 +357,7 @@ class Bar { close() { - document.getElementById('tracy-debug').style.display = 'none'; + Debug.layer.style.display = 'none'; } @@ -396,10 +396,23 @@ class Debug { static init(content) { Debug.bar = new Bar; Debug.panels = {}; + + // Shadow DOM for CSS isolation + let host = document.createElement('tracy-bar'); + let shadow = host.attachShadow({ mode: 'open' }); + Debug.shadow = shadow; + + // Clone CSS from document.head into shadow root + document.querySelectorAll('style.tracy-debug').forEach((s) => { + shadow.appendChild(s.cloneNode(true)); + }); + 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'; Debug.bar.init(); diff --git a/src/Tracy/Dumper/assets/dumper.js b/src/Tracy/Dumper/assets/dumper.js index 81417ad71..43d080c94 100644 --- a/src/Tracy/Dumper/assets/dumper.js +++ b/src/Tracy/Dumper/assets/dumper.js @@ -46,9 +46,9 @@ class Dumper { Dumper.inited = true; document.documentElement.addEventListener('click', (e) => { - let el; + let el, target = e.composedPath()[0] || e.target; // enables & ctrl or cmd key - if ((e.ctrlKey || e.metaKey) && (el = e.target.closest('[data-tracy-href]'))) { + if ((e.ctrlKey || e.metaKey) && (el = target.closest('[data-tracy-href]'))) { location.href = el.getAttribute('data-tracy-href'); return false; } @@ -56,9 +56,9 @@ class Dumper { }); document.documentElement.addEventListener('tracy-beforetoggle', (e) => { - let el; + let el, target = e.composedPath()[0] || e.target; // initializes lazy inside
-			if ((el = e.target.closest('[data-tracy-snapshot]'))) {
+			if ((el = target.closest('[data-tracy-snapshot]'))) {
 				let snapshot = JSON.parse(el.getAttribute('data-tracy-snapshot'));
 				el.removeAttribute('data-tracy-snapshot');
 				el.querySelectorAll('[data-tracy-dump]').forEach((toggler) => {
@@ -72,7 +72,8 @@ class Dumper {
 		});
 
 		document.documentElement.addEventListener('tracy-toggle', (e) => {
-			if (!e.target.matches('.tracy-dump *')) {
+			let target = e.composedPath()[0] || e.target;
+			if (!target.matches('.tracy-dump *')) {
 				return;
 			}
 
@@ -98,21 +99,23 @@ class Dumper {
 		});
 
 		document.documentElement.addEventListener('animationend', (e) => {
+			let target = e.composedPath()[0] || e.target;
 			if (e.animationName === 'tracy-dump-flash') {
-				e.target.classList.toggle('tracy-dump-flash', false);
+				target.classList.toggle('tracy-dump-flash', false);
 			}
 		});
 
 		document.addEventListener('mouseover', (e) => {
-			if (!e.target.matches('.tracy-dump *')) {
+			let target = e.composedPath()[0] || e.target;
+			if (!target.matches('.tracy-dump *')) {
 				return;
 			}
 
 			let el;
 
-			if (e.target.matches('.tracy-dump-hash') && (el = e.target.closest('tracy-div'))) {
+			if (target.matches('.tracy-dump-hash') && (el = target.closest('[id="tracy-debug"]') || target.closest('tracy-div'))) {
 				el.querySelectorAll('.tracy-dump-hash').forEach((el) => {
-					if (el.textContent === e.target.textContent) {
+					if (el.textContent === target.textContent) {
 						el.classList.add('tracy-dump-highlight');
 					}
 				});
@@ -125,8 +128,9 @@ class Dumper {
 		});
 
 		document.addEventListener('mouseout', (e) => {
-			if (e.target.matches('.tracy-dump-hash')) {
-				document.querySelectorAll('.tracy-dump-hash.tracy-dump-highlight').forEach((el) => {
+			let target = e.composedPath()[0] || e.target;
+			if (target.matches('.tracy-dump-hash')) {
+				target.getRootNode().querySelectorAll('.tracy-dump-hash.tracy-dump-highlight').forEach((el) => {
 					el.classList.remove('tracy-dump-highlight');
 				});
 			}
@@ -381,6 +385,31 @@ function UnknownEntityException() {}
 let Tracy = window.Tracy = window.Tracy || {};
 Tracy.Dumper = Tracy.Dumper || Dumper;
 
+if (!customElements.get('tracy-dump')) {
+	customElements.define('tracy-dump', class extends HTMLElement {
+		connectedCallback() {
+			if (this.shadowRoot) {
+				return;
+			}
+			let shadow = this.attachShadow({ mode: 'open' });
+
+			// clone CSS from document into shadow root
+			document.querySelectorAll('style.tracy-debug, style.tracy-dump-style').forEach((s) => {
+				shadow.appendChild(s.cloneNode(true));
+			});
+
+			// move only non-style/script children into shadow root
+			Array.from(this.childNodes).forEach((child) => {
+				if (child.nodeType !== 1 || (child.tagName !== 'STYLE' && child.tagName !== 'SCRIPT')) {
+					shadow.appendChild(child);
+				}
+			});
+
+			Tracy.Dumper.init(shadow);
+		}
+	});
+}
+
 function init() {
 	Tracy.Dumper.init();
 }
diff --git a/src/Tracy/assets/helpers.js b/src/Tracy/assets/helpers.js
index 83c71b8e7..c578878f7 100644
--- a/src/Tracy/assets/helpers.js
+++ b/src/Tracy/assets/helpers.js
@@ -5,7 +5,7 @@ function init() {
 		document.addEventListener(
 			'click',
 			(e) => {
-				e.target.closest('a[href^="editor:"]')?.setAttribute('target', '_blank');
+				(e.composedPath()[0] || e.target).closest('a[href^="editor:"]')?.setAttribute('target', '_blank');
 			},
 			true,
 		);
diff --git a/src/Tracy/assets/reset.css b/src/Tracy/assets/reset.css
index b0a6a6fc8..7c821ab91 100644
--- a/src/Tracy/assets/reset.css
+++ b/src/Tracy/assets/reset.css
@@ -2,12 +2,14 @@
  * This file is part of the Tracy (https://tracy.nette.org)
  */
 
-:root {
+:root,
+:host {
 	--tracy-space: 16px;
 }
 
 @media (max-width: 600px) {
-	:root {
+	:root,
+	:host {
 		--tracy-space: 8px;
 	}
 }
diff --git a/src/Tracy/assets/table-sort.js b/src/Tracy/assets/table-sort.js
index 39bae80f6..0f7e3e0e9 100644
--- a/src/Tracy/assets/table-sort.js
+++ b/src/Tracy/assets/table-sort.js
@@ -6,10 +6,11 @@
 class TableSort {
 	static init() {
 		document.documentElement.addEventListener('click', (e) => {
+			let target = e.composedPath()[0] || e.target;
 			if ((window.getSelection().type !== 'Range')
-				&& e.target.matches('.tracy-sortable > :first-child > tr:first-child *')
+				&& target.matches('.tracy-sortable > :first-child > tr:first-child *')
 			) {
-				TableSort.sort(e.target.closest('td,th'));
+				TableSort.sort(target.closest('td,th'));
 			}
 		});
 
diff --git a/src/Tracy/assets/tabs.js b/src/Tracy/assets/tabs.js
index 8ec2fe7b2..7b109d071 100644
--- a/src/Tracy/assets/tabs.js
+++ b/src/Tracy/assets/tabs.js
@@ -6,11 +6,11 @@
 class Tabs {
 	static init() {
 		document.documentElement.addEventListener('click', (e) => {
-			let label, context;
+			let label, context, target = e.composedPath()[0] || e.target;
 			if (
 				!e.shiftKey && !e.ctrlKey && !e.metaKey
-				&& (label = e.target.closest('.tracy-tab-label'))
-				&& (context = e.target.closest('.tracy-tabs'))
+				&& (label = target.closest('.tracy-tab-label'))
+				&& (context = target.closest('.tracy-tabs'))
 			) {
 				Tabs.toggle(context, label);
 				e.preventDefault();
diff --git a/src/Tracy/assets/toggle.js b/src/Tracy/assets/toggle.js
index aabefb5f1..7d927affa 100644
--- a/src/Tracy/assets/toggle.js
+++ b/src/Tracy/assets/toggle.js
@@ -16,7 +16,7 @@ class Toggle {
 			let el;
 			if (
 				!e.shiftKey && !e.ctrlKey && !e.metaKey
-				&& (el = e.target.closest('.tracy-toggle'))
+				&& (el = (e.composedPath()[0] || e.target).closest('.tracy-toggle'))
 				&& Math.pow(start[0] - e.clientX, 2) + Math.pow(start[1] - e.clientY, 2) < MOVE_THRESHOLD
 			) {
 				Toggle.toggle(el, undefined, e);
@@ -40,13 +40,14 @@ class Toggle {
 
 		el.dispatchEvent(new CustomEvent('tracy-beforetoggle', {
 			bubbles: true,
+			composed: true,
 			detail: { collapsed: !expand, originalEvent: e },
 		}));
 
 		if (!ref || ref === '#') {
 			ref = '+';
 		} else if (ref.substr(0, 1) === '#') {
-			dest = document;
+			dest = el.getRootNode();
 		}
 		ref = ref.match(/(\^\s*([^+\s]*)\s*)?(\+\s*(\S*)\s*)?(.*)/);
 		dest = ref[1] ? dest.parentNode : dest;
@@ -59,6 +60,7 @@ class Toggle {
 
 		el.dispatchEvent(new CustomEvent('tracy-toggle', {
 			bubbles: true,
+			composed: true,
 			detail: { relatedTarget: dest, collapsed: !expand, originalEvent: e },
 		}));
 	}

From 936a01ada99cc1be5a2c67a4aedf7c82103311e2 Mon Sep 17 00:00:00 2001
From: David Grudl 
Date: Fri, 20 Feb 2026 08:53:24 +0100
Subject: [PATCH 07/19] added Shadow DOM for CSS isolation of Dumper

---
 src/Tracy/Debugger/Debugger.php      | 4 ++--
 src/Tracy/Dumper/Dumper.php          | 2 +-
 tests/Dumper/Dumper.dump().html.phpt | 2 +-
 tests/Dumper/dump().html.phpt        | 8 ++++----
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php
index c6d20cd49..222a1f911 100644
--- a/src/Tracy/Debugger/Debugger.php
+++ b/src/Tracy/Debugger/Debugger.php
@@ -495,7 +495,7 @@ public static function dump(mixed $var, bool $return = false): mixed
 
 		} elseif (!self::$productionMode) {
 			$html = Helpers::isHtmlMode();
-			echo $html ? '' : '';
+			echo $html ? '' : '';
 			Dumper::dump($var, [
 				Dumper::DEPTH => self::$maxDepth,
 				Dumper::TRUNCATE => self::$maxLength,
@@ -504,7 +504,7 @@ public static function dump(mixed $var, bool $return = false): mixed
 				Dumper::THEME => self::$dumpTheme,
 				Dumper::KEYS_TO_HIDE => self::$keysToHide,
 			]);
-			echo $html ? '' : '';
+			echo $html ? '' : '';
 
 			if ($html && Helpers::isAgent()) {
 				Helpers::consoleLog(Dumper::toText($var, [
diff --git a/src/Tracy/Dumper/Dumper.php b/src/Tracy/Dumper/Dumper.php
index 5bee48b40..19e2a2945 100644
--- a/src/Tracy/Dumper/Dumper.php
+++ b/src/Tracy/Dumper/Dumper.php
@@ -179,7 +179,7 @@ public static function renderAssets(): void
 			. file_get_contents(__DIR__ . '/../assets/toggle.css')
 			. file_get_contents(__DIR__ . '/assets/dumper-light.css')
 			. file_get_contents(__DIR__ . '/assets/dumper-dark.css');
-		echo "", str_replace('\n";
+		echo "\n";
 
 		if (!Debugger::isEnabled() || !Debugger::$showBar) {
 			$s = '(function(){' . file_get_contents(__DIR__ . '/../assets/toggle.js') . '})();'
diff --git a/tests/Dumper/Dumper.dump().html.phpt b/tests/Dumper/Dumper.dump().html.phpt
index 7e6293144..571ae312a 100644
--- a/tests/Dumper/Dumper.dump().html.phpt
+++ b/tests/Dumper/Dumper.dump().html.phpt
@@ -21,7 +21,7 @@ test('html mode', function () {
 	Assert::same(123, Dumper::dump(123));
 	Assert::match(
 		<<<'XX'
-			
+			
 			
 			
Dumper::dump(123)) 📍
+			%a%
 			
 			
dump(123) 📍123
- +
XX, ob_get_clean(), ); @@ -39,10 +39,10 @@ test('dark theme', function () { dump(123); Assert::match( <<<'XX' -
dump(123) 📍123
- + XX, ob_get_clean(), ); From c6f34c7988d446ca2b80b78cf08e19c82b4067bc Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 20 Feb 2026 09:06:15 +0100 Subject: [PATCH 08/19] added Shadow DOM for CSS isolation of BlueScreen --- src/Tracy/BlueScreen/assets/bluescreen.css | 5 +++ src/Tracy/BlueScreen/assets/bluescreen.js | 46 +++++++++++++++------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/Tracy/BlueScreen/assets/bluescreen.css b/src/Tracy/BlueScreen/assets/bluescreen.css index dd80a54a2..6b2d7d70e 100644 --- a/src/Tracy/BlueScreen/assets/bluescreen.css +++ b/src/Tracy/BlueScreen/assets/bluescreen.css @@ -2,6 +2,11 @@ * This file is part of the Tracy (https://tracy.nette.org) */ +/* shadow DOM host */ +:host { + display: contents; +} + html.tracy-bs-visible, html.tracy-bs-visible body { display: block; diff --git a/src/Tracy/BlueScreen/assets/bluescreen.js b/src/Tracy/BlueScreen/assets/bluescreen.js index c8983b207..c487f2478 100644 --- a/src/Tracy/BlueScreen/assets/bluescreen.js +++ b/src/Tracy/BlueScreen/assets/bluescreen.js @@ -8,28 +8,42 @@ class BlueScreen { let blueScreen = document.getElementById('tracy-bs'); + // Shadow DOM for CSS isolation + let host = document.createElement('tracy-bs'); + let shadow = host.attachShadow({ mode: 'open' }); + BlueScreen.shadow = shadow; + + // Clone CSS into shadow root + 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.platform.indexOf('Mac') > -1) { blueScreen.classList.add('tracy-mac'); } blueScreen.addEventListener('tracy-toggle', (e) => { - if (e.target.matches('#tracy-bs-toggle')) { // blue screen toggle + let target = e.composedPath()[0] || e.target; + 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); + Tracy.Dumper.init(shadow); + (new ResizeObserver(() => stickyFooter(shadow))).observe(blueScreen); if (document.documentElement.classList.contains('tracy-bs-visible')) { blueScreen.scrollIntoView(); @@ -41,33 +55,37 @@ class BlueScreen { // enables toggling via ESC document.addEventListener('keyup', (e) => { if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { // ESC - 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 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('#tracy-bs 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); } From 1c69f41fc0eecd9beaca8949422c6edf94594884 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 20 Feb 2026 09:54:13 +0100 Subject: [PATCH 09/19] removed CSS reset rules superseded by Shadow DOM --- src/Tracy/Bar/assets/bar.css | 1 + src/Tracy/assets/reset.css | 367 +---------------------------------- 2 files changed, 8 insertions(+), 360 deletions(-) diff --git a/src/Tracy/Bar/assets/bar.css b/src/Tracy/Bar/assets/bar.css index 64f724460..70c9310b9 100644 --- a/src/Tracy/Bar/assets/bar.css +++ b/src/Tracy/Bar/assets/bar.css @@ -37,6 +37,7 @@ body#tracy-debug { /* in popup window */ #tracy-debug h2, #tracy-debug h3 { + font-size: inherit; font-weight: bold; } diff --git a/src/Tracy/assets/reset.css b/src/Tracy/assets/reset.css index 7c821ab91..83816dc3b 100644 --- a/src/Tracy/assets/reset.css +++ b/src/Tracy/assets/reset.css @@ -14,375 +14,22 @@ } } -tracy-div:not(a b), -tracy-div:not(a b) * { - font: inherit; - line-height: inherit; - color: inherit; - background: transparent; +*, +*::before, +*::after { + box-sizing: border-box; margin: 0; padding: 0; - border: none; - text-align: inherit; - list-style: inherit; - opacity: 1; - border-radius: 0; - box-shadow: none; - text-shadow: none; - box-sizing: border-box; - text-decoration: none; - text-transform: inherit; - white-space: inherit; - float: none; - clear: none; - max-width: initial; - min-width: initial; - max-height: initial; - min-height: initial; -} - -tracy-div:not(a b) *:hover { - color: inherit; - background: transparent; -} - -tracy-div:not(a b) *:not(svg):not(img):not(table) { - width: initial; - height: initial; -} - -tracy-div:not(a b):before, -tracy-div:not(a b):after, -tracy-div:not(a b) *:before, -tracy-div:not(a b) *:after { - all: unset; -} - -tracy-div:not(a b) b, -tracy-div:not(a b) strong { - font-weight: bold; -} - -tracy-div:not(a b) small { - font-size: smaller; -} - -tracy-div:not(a b) i, -tracy-div:not(a b) em { - font-style: italic; -} - -tracy-div:not(a b) big { - font-size: larger; -} - -tracy-div:not(a b) small, -tracy-div:not(a b) sub, -tracy-div:not(a b) sup { - font-size: smaller; -} - -tracy-div:not(a b) ins { - text-decoration: underline; -} - -tracy-div:not(a b) del { - text-decoration: line-through; } -tracy-div:not(a b) table { +table { border-collapse: collapse; } -tracy-div:not(a b) pre { - font-family: monospace; - white-space: pre; -} - -tracy-div:not(a b) code, -tracy-div:not(a b) kbd, -tracy-div:not(a b) samp { - font-family: monospace; -} - -tracy-div:not(a b) input { - background-color: white; +input { padding: 1px; - border: 1px solid; } -tracy-div:not(a b) textarea { - background-color: white; - border: 1px solid; +textarea { padding: 2px; - white-space: pre-wrap; -} - -tracy-div:not(a b) select { - border: 1px solid; - white-space: pre; -} - -tracy-div:not(a b) article, -tracy-div:not(a b) aside, -tracy-div:not(a b) details, -tracy-div:not(a b) div, -tracy-div:not(a b) figcaption, -tracy-div:not(a b) footer, -tracy-div:not(a b) form, -tracy-div:not(a b) header, -tracy-div:not(a b) hgroup, -tracy-div:not(a b) main, -tracy-div:not(a b) nav, -tracy-div:not(a b) section, -tracy-div:not(a b) summary, -tracy-div:not(a b) pre, -tracy-div:not(a b) p, -tracy-div:not(a b) dl, -tracy-div:not(a b) dd, -tracy-div:not(a b) dt, -tracy-div:not(a b) blockquote, -tracy-div:not(a b) figure, -tracy-div:not(a b) address, -tracy-div:not(a b) h1, -tracy-div:not(a b) h2, -tracy-div:not(a b) h3, -tracy-div:not(a b) h4, -tracy-div:not(a b) h5, -tracy-div:not(a b) h6, -tracy-div:not(a b) ul, -tracy-div:not(a b) ol, -tracy-div:not(a b) li, -tracy-div:not(a b) hr { - display: block; -} - -tracy-div:not(a b) a, -tracy-div:not(a b) b, -tracy-div:not(a b) big, -tracy-div:not(a b) code, -tracy-div:not(a b) em, -tracy-div:not(a b) i, -tracy-div:not(a b) small, -tracy-div:not(a b) span, -tracy-div:not(a b) strong { - display: inline; -} - -tracy-div:not(a b) table { - display: table; -} - -tracy-div:not(a b) tr { - display: table-row; -} - -tracy-div:not(a b) col { - display: table-column; -} - -tracy-div:not(a b) colgroup { - display: table-column-group; -} - -tracy-div:not(a b) tbody { - display: table-row-group; -} - -tracy-div:not(a b) thead { - display: table-header-group; -} - -tracy-div:not(a b) tfoot { - display: table-footer-group; -} - -tracy-div:not(a b) td { - display: table-cell; -} - -tracy-div:not(a b) th { - display: table-cell; -} - - - -/* TableSort */ -tracy-div:not(a b) .tracy-sortable > :first-child > tr:first-child > * { - position: relative; -} - -tracy-div:not(a b) .tracy-sortable > :first-child > tr:first-child > *:hover:before { - position: absolute; - right: .3em; - content: "\21C5"; - opacity: .4; - font-weight: normal; -} - - -/* dump */ -tracy-div:not(a b) .tracy-dump div { - padding-left: 3ex; -} - -tracy-div:not(a b) .tracy-dump div div { - border-left: 1px solid rgba(0, 0, 0, .1); - margin-left: .5ex; -} - -tracy-div:not(a b) .tracy-dump div div:hover { - border-left-color: rgba(0, 0, 0, .25); -} - -tracy-div:not(a b) .tracy-dump { - background: #FDF5CE; - padding: .4em .7em; - border: 1px dotted silver; - overflow: auto; -} - -tracy-div:not(a b) table .tracy-dump.tracy-dump { /* overwrite .tracy-dump.tracy-light etc. */ - padding: 0; - margin: 0; - border: none; -} - -tracy-div:not(a b) .tracy-dump-location { - color: gray; - font-size: 80%; - text-decoration: none; - background: none; - opacity: .5; - float: right; - cursor: pointer; -} - -tracy-div:not(a b) .tracy-dump-location:hover, -tracy-div:not(a b) .tracy-dump-location:focus { - color: gray; - background: none; - opacity: 1; -} - -tracy-div:not(a b) .tracy-dump-array, -tracy-div:not(a b) .tracy-dump-object { - color: #C22; -} - -tracy-div:not(a b) .tracy-dump-string { - color: #35D; - white-space: break-spaces; -} - -tracy-div:not(a b) div.tracy-dump-string { - position: relative; - padding-left: 3.5ex; -} - -tracy-div:not(a b) .tracy-dump-lq { - margin-left: calc(-1ex - 1px); -} - -tracy-div:not(a b) div.tracy-dump-string:before { - content: ''; - position: absolute; - left: calc(3ex - 1px); - top: 1.5em; - bottom: 0; - border-left: 1px solid rgba(0, 0, 0, .1); -} - -tracy-div:not(a b) .tracy-dump-virtual span, -tracy-div:not(a b) .tracy-dump-dynamic span, -tracy-div:not(a b) .tracy-dump-string span { - color: rgba(0, 0, 0, 0.5); -} - -tracy-div:not(a b) .tracy-dump-virtual i, -tracy-div:not(a b) .tracy-dump-dynamic i, -tracy-div:not(a b) .tracy-dump-string i { - font-size: 80%; - font-style: normal; - color: rgba(0, 0, 0, 0.5); - user-select: none; -} - -tracy-div:not(a b) .tracy-dump-number { - color: #090; -} - -tracy-div:not(a b) .tracy-dump-null, -tracy-div:not(a b) .tracy-dump-bool { - color: #850; -} - -tracy-div:not(a b) .tracy-dump-virtual { - font-style: italic; -} - -tracy-div:not(a b) .tracy-dump-public::after { - content: ' pub'; -} - -tracy-div:not(a b) .tracy-dump-protected::after { - content: ' pro'; -} - -tracy-div:not(a b) .tracy-dump-private::after { - content: ' pri'; -} - -tracy-div:not(a b) .tracy-dump-public::after, -tracy-div:not(a b) .tracy-dump-protected::after, -tracy-div:not(a b) .tracy-dump-private::after, -tracy-div:not(a b) .tracy-dump-hash { - font-size: 85%; - color: rgba(0, 0, 0, 0.5); -} - -tracy-div:not(a b) .tracy-dump-indent { - display: none; -} - -tracy-div:not(a b) .tracy-dump-highlight { - background: #C22; - color: white; - border-radius: 2px; - padding: 0 2px; - margin: 0 -2px; -} - -tracy-div:not(a b) span[data-tracy-href] { - border-bottom: 1px dotted rgba(0, 0, 0, .2); -} - - -/* toggle */ -tracy-div:not(a b) .tracy-toggle:after { - content: ''; - display: inline-block; - vertical-align: middle; - line-height: 0; - border-top: .6ex solid; - border-right: .6ex solid transparent; - border-left: .6ex solid transparent; - transform: scale(1, 1.5); - margin: 0 .2ex 0 .7ex; - transition: .1s transform; - opacity: .5; -} - -tracy-div:not(a b) .tracy-toggle.tracy-collapsed:after { - transform: rotate(-90deg) scale(1, 1.5) translate(.1ex, 0); -} - - -/* tabs */ -tracy-div:not(a b) .tracy-tab-label { - user-select: none; -} - -tracy-div:not(a b) .tracy-tab-panel:not(.tracy-active) { - display: none; } From bca26ad54bb934285ee3a4bd1a2e3d48f8cfd265 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 10 Apr 2026 02:00:46 +0200 Subject: [PATCH 10/19] removed inner containers and ID-based CSS scoping from Bar and BlueScreen --- src/Tracy/Bar/assets/bar.css | 109 +++++++------ src/Tracy/Bar/assets/bar.js | 45 +++--- src/Tracy/BlueScreen/assets/bluescreen.css | 152 +++++++++--------- src/Tracy/BlueScreen/assets/bluescreen.js | 13 +- src/Tracy/BlueScreen/assets/content.latte | 6 +- src/Tracy/BlueScreen/dist/content.phtml | 6 +- src/Tracy/Debugger/DeferredContent.php | 22 ++- src/Tracy/Dumper/assets/dumper.js | 2 +- .../expected/Debugger.error-in-eval.expect | 6 +- .../Debugger.exception.fiber.html.expect | 4 +- .../Debugger.exception.generator.html.expect | 4 +- .../expected/Debugger.exception.html.expect | 6 +- ...ebugger.exception.in-generator.html.expect | 4 +- .../expected/Debugger.strict.html.expect | 6 +- 14 files changed, 202 insertions(+), 183 deletions(-) diff --git a/src/Tracy/Bar/assets/bar.css b/src/Tracy/Bar/assets/bar.css index 70c9310b9..d01640e27 100644 --- a/src/Tracy/Bar/assets/bar.css +++ b/src/Tracy/Bar/assets/bar.css @@ -4,44 +4,49 @@ /* shadow DOM host */ :host { - display: contents; -} - -/* common styles */ -#tracy-debug { --tracy-space: 10px; - display: none; direction: ltr; + display: none; } -body#tracy-debug { /* in popup window */ +body.tracy-debug { /* in popup window */ display: block; } -#tracy-debug:not(body) { - position: absolute; - left: 0; - top: 0; -} +/* popup window overrides (higher specificity to beat bluescreen bare selectors) */ +body.tracy-debug a { color: #125EAE; text-decoration: none; padding: 0; margin: 0; } +body.tracy-debug a:hover, +body.tracy-debug a:focus { background-color: #125EAE; color: white; } +body.tracy-debug h1 { font: normal normal 23px/1.4 Tahoma, sans-serif; line-height: 1; color: #575753; margin: 0; } +body.tracy-debug h2, +body.tracy-debug h3 { font-size: inherit; font-weight: bold; } +body.tracy-debug table { background: #FDF5CE; width: 100%; } +body.tracy-debug td, +body.tracy-debug th { border: 1px solid #E6DFBF; padding: 2px 5px; vertical-align: top; text-align: left; } +body.tracy-debug th { background: #F4F3F1; color: #655E5E; font-size: 90%; font-weight: bold; } +body.tracy-debug pre, +body.tracy-debug code { font: 9pt/1.5 Consolas, monospace; } +body.tracy-debug svg { display: inline; } -#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, @@ -53,44 +58,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; } @@ -188,7 +193,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; @@ -196,11 +201,11 @@ body#tracy-debug { /* in popup window */ text-align: left; } -body#tracy-debug .tracy-panel { /* in popup window */ +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; @@ -208,29 +213,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); @@ -241,24 +246,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; @@ -267,18 +272,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; @@ -286,25 +291,25 @@ 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 37e71b077..98ee65579 100644 --- a/src/Tracy/Bar/assets/bar.js +++ b/src/Tracy/Bar/assets/bar.js @@ -26,7 +26,7 @@ function getOption(key) { class Panel { constructor(id) { this.id = id; - this.elem = Debug.layer.querySelector('#' + CSS.escape(this.id)); + this.elem = Debug.shadow.querySelector('#' + CSS.escape(this.id)); this.elem.Tracy = this.elem.Tracy || {}; } @@ -37,7 +37,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, { @@ -154,7 +154,7 @@ class Panel { } let doc = win.document; - doc.write(''); + doc.write(''); let script = doc.createElement('script'); script.src = baseUrl + '_tracy_bar=js&XDEBUG_SESSION_STOP=1'; @@ -163,10 +163,10 @@ class Panel { 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; @@ -253,7 +253,7 @@ Panel.zIndexCounter = 1; class Bar { init() { this.id = 'tracy-debug-bar'; - this.elem = Debug.layer.querySelector('#' + this.id); + this.elem = Debug.shadow.querySelector('#' + this.id); draggable(this.elem, { handles: this.elem.querySelectorAll('li:first-child'), @@ -357,7 +357,7 @@ class Bar { close() { - Debug.layer.style.display = 'none'; + Debug.host.style.display = 'none'; } @@ -399,25 +399,28 @@ class Debug { // Shadow DOM for CSS isolation let host = document.createElement('tracy-bar'); + Debug.host = host; let shadow = host.attachShadow({ mode: 'open' }); Debug.shadow = shadow; - // Clone CSS from document.head into shadow root - document.querySelectorAll('style.tracy-debug').forEach((s) => { + // Clone CSS from document.head into shadow root (exclude BlueScreen CSS) + document.querySelectorAll('style.tracy-debug:not(.tracy-bs-css)').forEach((s) => { shadow.appendChild(s.cloneNode(true)); }); - Debug.layer = document.createElement('tracy-div'); - Debug.layer.setAttribute('id', 'tracy-debug'); - Debug.layer.innerHTML = content; - shadow.appendChild(Debug.layer); + // Insert content directly into shadow root + let temp = document.createElement('div'); + temp.innerHTML = content; + while (temp.firstChild) { + shadow.appendChild(temp.firstChild); + } (document.body || document.documentElement).appendChild(host); - evalScripts(Debug.layer); - Debug.layer.style.display = 'block'; + evalScripts(shadow); + 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(); }); @@ -455,12 +458,16 @@ class Debug { }); } - Debug.layer.insertAdjacentHTML('beforeend', content.panels); - evalScripts(Debug.layer); + let temp = document.createElement('div'); + temp.innerHTML = content.panels; + while (temp.firstChild) { + Debug.shadow.appendChild(temp.firstChild); + } + evalScripts(Debug.shadow); 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(); diff --git a/src/Tracy/BlueScreen/assets/bluescreen.css b/src/Tracy/BlueScreen/assets/bluescreen.css index 6b2d7d70e..a6e8d8d9b 100644 --- a/src/Tracy/BlueScreen/assets/bluescreen.css +++ b/src/Tracy/BlueScreen/assets/bluescreen.css @@ -14,7 +14,7 @@ html.tracy-bs-visible body { position: static; } -#tracy-bs { +.tracy-bs { font: 9pt/1.5 Verdana, sans-serif; background: white; color: #333; @@ -26,23 +26,23 @@ html.tracy-bs-visible body { text-align: left; } -#tracy-bs a { +a { text-decoration: none; color: #328ADC; padding: 0 4px; margin: 0 -4px; } -#tracy-bs a + a { +a + a { margin-left: 0; } -#tracy-bs a:hover, -#tracy-bs a:focus { +a:hover, +a:focus { color: #085AA3; } -#tracy-bs-toggle { +.tracy-bs-toggle { position: absolute; right: .5em; top: .5em; @@ -52,7 +52,7 @@ html.tracy-bs-visible body { padding: 3px; } -#tracy-bs-toggle.tracy-collapsed { +.tracy-bs-toggle.tracy-collapsed { position: fixed; } @@ -66,7 +66,7 @@ html.tracy-bs-visible body { display: none; } -#tracy-bs :where(:is( +:where(:is( h1, h2, h3, h4, h5, h6, p, ol, ul, dl, @@ -77,34 +77,34 @@ html.tracy-bs-visible body { margin-top: var(--tracy-space); } -#tracy-bs h1 { +h1 { font-size: 15pt; font-weight: normal; text-shadow: 1px 1px 2px rgba(0, 0, 0, .3); } -#tracy-bs h1 span { +h1 span { white-space: pre-wrap; } -#tracy-bs h2 { +h2 { font-size: 14pt; font-weight: normal; } -#tracy-bs h3 { +h3 { font-size: 10pt; font-weight: bold; } -#tracy-bs pre, -#tracy-bs code, -#tracy-bs table { +pre, +code, +table { font: 9pt/1.5 Consolas, monospace !important; } -#tracy-bs pre, -#tracy-bs table { +pre, +table { background: #FDF5CE; padding: .4em .7em; border: 2px solid #ffffffa6; @@ -112,46 +112,46 @@ html.tracy-bs-visible body { overflow: auto; } -#tracy-bs table pre { +table pre { padding: 0; margin: 0; border: none; box-shadow: none; } -#tracy-bs table { +table { border-collapse: collapse; width: 100%; } -#tracy-bs td, -#tracy-bs th { +td, +th { vertical-align: top; text-align: left; padding: 2px 6px; border: 1px solid #e6dfbf; } -#tracy-bs th { +th { font-weight: bold; } -#tracy-bs tr > :first-child { +tr > :first-child { width: 20%; } -#tracy-bs tr:nth-child(2n), -#tracy-bs tr:nth-child(2n) pre { +tr:nth-child(2n), +tr:nth-child(2n) pre { background-color: #F7F0CB; } -#tracy-bs .tracy-footer--sticky { +.tracy-footer--sticky { position: fixed; width: 100%; bottom: 0; } -#tracy-bs footer ul { +footer ul { font-size: 7pt; padding: var(--tracy-space); margin: var(--tracy-space) 0 0; @@ -161,11 +161,11 @@ html.tracy-bs-visible body { list-style: none; } -#tracy-bs .tracy-footer-logo { +.tracy-footer-logo { position: relative; } -#tracy-bs .tracy-footer-logo a { +.tracy-footer-logo a { position: absolute; bottom: 0; right: 0; @@ -177,18 +177,18 @@ html.tracy-bs-visible body { margin: 0; } -#tracy-bs .tracy-footer-logo a:hover, -#tracy-bs .tracy-footer-logo a:focus { +.tracy-footer-logo a:hover, +.tracy-footer-logo a:focus { opacity: 1; transition: opacity 0.1s; } -#tracy-bs .tracy-section { +.tracy-section { padding: var(--tracy-space); } -#tracy-bs .tracy-section-panel { +.tracy-section-panel { background: #5040200E; padding: var(--tracy-space); border-radius: 8px; @@ -196,103 +196,103 @@ html.tracy-bs-visible body { overflow: hidden; } -#tracy-bs .outer, /* deprecated */ -#tracy-bs .tracy-pane { +.outer, /* deprecated */ +.tracy-pane { overflow: auto; } -#tracy-bs.tracy-mac .tracy-pane { +.tracy-bs.tracy-mac .tracy-pane { padding-bottom: 12px; } /* header */ -#tracy-bs .tracy-section--error { +.tracy-section--error { background: #CD1818; color: white; } -#tracy-bs .tracy-section--error p, -#tracy-bs .tracy-section--error h1 { +.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 { +.tracy-section--error::selection, +.tracy-section--error ::selection { color: black !important; background: #FDF5CE !important; } -#tracy-bs .tracy-section--error h1 a { +.tracy-section--error h1 a { color: #ffefa1 !important; } -#tracy-bs .tracy-section--error span span { +.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 { +.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 { +.tracy-section--error:hover a.tracy-action { opacity: .6; } -#tracy-bs .tracy-section--error a.tracy-action:hover { +.tracy-section--error a.tracy-action:hover { opacity: 1; } -#tracy-bs .tracy-section--error i { +.tracy-section--error i { color: #ffefa1; font-style: normal; } -#tracy-bs .tracy-section--error:has(.tracy-caused) { +.tracy-section--error:has(.tracy-caused) { border-radius: 0 0 0 8px; overflow: hidden; } -#tracy-bs .tracy-caused { +.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 { +.tracy-caused a { color: white; } /* source code */ -#tracy-bs pre.tracy-code > div { +pre.tracy-code > div { min-width: fit-content; white-space: pre; } -#tracy-bs .tracy-code-comment { +.tracy-code-comment { color: rgba(0, 0, 0, 0.5); font-style: italic; } -#tracy-bs .tracy-code-keyword { +.tracy-code-keyword { color: #D24; font-weight: bold; } -#tracy-bs .tracy-code-var { +.tracy-code-var { font-weight: bold; } -#tracy-bs .tracy-line-highlight { +.tracy-line-highlight { background: #CD1818; color: white; font-weight: bold; @@ -302,71 +302,71 @@ html.tracy-bs-visible body { margin: 0 -1ch -1lh; } -#tracy-bs .tracy-column-highlight { +.tracy-column-highlight { display: inline-block; backdrop-filter: grayscale(1); margin: 0 -1px; padding: 0 1px; } -#tracy-bs .tracy-line { +.tracy-line { color: #9F9C7F; font-weight: normal; font-style: normal; } -#tracy-bs a.tracy-editor { +a.tracy-editor { color: inherit; border-bottom: 1px dotted rgba(0, 0, 0, .3); border-radius: 3px; } -#tracy-bs a.tracy-editor:hover { +a.tracy-editor:hover { background: #0001; } -#tracy-bs span[data-tracy-href] { +span[data-tracy-href] { border-bottom: 1px dotted rgba(0, 0, 0, .3); } -#tracy-bs .tracy-dump-whitespace { +.tracy-dump-whitespace { color: #0003; } -#tracy-bs .tracy-callstack { +.tracy-callstack { display: grid; overflow: auto; grid-template-columns: max-content 1fr; row-gap: calc(.5 * var(--tracy-space)); } -#tracy-bs .tracy-callstack-file { +.tracy-callstack-file { text-align: right; padding-right: var(--tracy-space); white-space: nowrap; } -#tracy-bs .tracy-callstack-callee { +.tracy-callstack-callee { white-space: nowrap; } -#tracy-bs .tracy-callstack-additional { +.tracy-callstack-additional { grid-column-start: 1; grid-column-end: 3; } -#tracy-bs .tracy-callstack-args tr:first-child > * { +.tracy-callstack-args tr:first-child > * { position: relative; } -#tracy-bs .tracy-callstack-args tr:first-child td:before { +.tracy-callstack-args tr:first-child td:before { position: absolute; right: .3em; content: 'may not be true'; opacity: .4; } -#tracy-bs .tracy-panel-fadein { +.tracy-panel-fadein { animation: tracy-panel-fadein .12s ease; } @@ -376,26 +376,26 @@ html.tracy-bs-visible body { } } -#tracy-bs .tracy-section--causedby { +.tracy-section--causedby { flex-direction: column; padding: 0; } -#tracy-bs .tracy-section--causedby:not(.tracy-collapsed) { +.tracy-section--causedby:not(.tracy-collapsed) { display: flex; } -#tracy-bs .tracy-section--causedby .tracy-section--error { +.tracy-section--causedby .tracy-section--error { background: #cd1818a6; } -#tracy-bs .tracy-section--error + .tracy-section--stack { +.tracy-section--error + .tracy-section--stack { margin-top: calc(1.5 * var(--tracy-space)); } /* tabs */ -#tracy-bs .tracy-tab-bar { +.tracy-tab-bar { display: flex; list-style: none; padding-left: 0; @@ -405,7 +405,7 @@ html.tracy-bs-visible body { column-gap: var(--tracy-space); } -#tracy-bs .tracy-tab-bar a { +.tracy-tab-bar a { display: block; padding: calc(.5 * var(--tracy-space)) var(--tracy-space); margin: 0; @@ -416,11 +416,11 @@ html.tracy-bs-visible body { transition: all 0.1s; } -#tracy-bs .tracy-tab-bar > .tracy-active a { +.tracy-tab-bar > .tracy-active a { background: white; } -#tracy-bs .tracy-tab-panel { +.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 c487f2478..9af0ea746 100644 --- a/src/Tracy/BlueScreen/assets/bluescreen.js +++ b/src/Tracy/BlueScreen/assets/bluescreen.js @@ -6,15 +6,16 @@ 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; - // Clone CSS into shadow root - document.querySelectorAll('style.tracy-debug').forEach((s) => { + // Clone CSS into shadow root (exclude Bar CSS) + document.querySelectorAll('style.tracy-debug:not(.tracy-bar-css)').forEach((s) => { shadow.appendChild(s.cloneNode(true)); }); @@ -28,7 +29,7 @@ class BlueScreen { blueScreen.addEventListener('tracy-toggle', (e) => { let target = e.composedPath()[0] || e.target; - if (target.matches('#tracy-bs-toggle')) { // blue screen toggle + if (target.matches('.tracy-bs-toggle')) { // blue screen toggle document.documentElement.classList.toggle('tracy-bs-visible', !e.detail.collapsed); } else if (!target.matches('.tracy-dump *') && e.detail.originalEvent) { // panel toggle @@ -55,7 +56,7 @@ class BlueScreen { // enables toggling via ESC document.addEventListener('keyup', (e) => { if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { // ESC - let toggle = BlueScreen.shadow && BlueScreen.shadow.querySelector('#tracy-bs-toggle'); + let toggle = BlueScreen.shadow && BlueScreen.shadow.querySelector('.tracy-bs-toggle'); if (toggle) { Tracy.Toggle.toggle(toggle); } @@ -82,7 +83,7 @@ class BlueScreen { } function stickyFooter(root) { - let footer = root && root.querySelector('#tracy-bs footer'); + let footer = root && root.querySelector('footer'); if (!footer) { return; } diff --git a/src/Tracy/BlueScreen/assets/content.latte b/src/Tracy/BlueScreen/assets/content.latte index 6b37a21b7..b617acb96 100644 --- a/src/Tracy/BlueScreen/assets/content.latte +++ b/src/Tracy/BlueScreen/assets/content.latte @@ -19,8 +19,8 @@ {varType Fiber[] $fibers} {* *} - -  +
+ 
{do $ex = $exception} @@ -71,4 +71,4 @@
- +
diff --git a/src/Tracy/BlueScreen/dist/content.phtml b/src/Tracy/BlueScreen/dist/content.phtml index 7d04ce72f..75f2fca22 100644 --- a/src/Tracy/BlueScreen/dist/content.phtml +++ b/src/Tracy/BlueScreen/dist/content.phtml @@ -19,8 +19,8 @@ use Tracy\Helpers; /** @var mixed[][] $obStatus */ /** @var Generator[] $generators */ /** @var Fiber[] $fibers */ -echo ' -  +echo '
+ 
'; @@ -109,5 +109,5 @@ echo ' - +
'; diff --git a/src/Tracy/Debugger/DeferredContent.php b/src/Tracy/Debugger/DeferredContent.php index 2dfe9f688..9eef1d4dc 100644 --- a/src/Tracy/Debugger/DeferredContent.php +++ b/src/Tracy/Debugger/DeferredContent.php @@ -119,16 +119,16 @@ public function sendAssets(): bool private function buildJsCss(): string { - $css = array_map(file_get_contents(...), array_merge([ + $sharedCss = array_map(file_get_contents(...), array_merge([ __DIR__ . '/../assets/reset.css', - __DIR__ . '/../Bar/assets/bar.css', __DIR__ . '/../assets/toggle.css', __DIR__ . '/../assets/table-sort.css', __DIR__ . '/../assets/tabs.css', __DIR__ . '/../Dumper/assets/dumper-light.css', __DIR__ . '/../Dumper/assets/dumper-dark.css', - __DIR__ . '/../BlueScreen/assets/bluescreen.css', ], Debugger::$customCssFiles)); + $barCss = file_get_contents(__DIR__ . '/../Bar/assets/bar.css') ?: throw new \RuntimeException('Cannot read bar.css'); + $bsCss = file_get_contents(__DIR__ . '/../BlueScreen/assets/bluescreen.css') ?: throw new \RuntimeException('Cannot read bluescreen.css'); $js1 = array_map(fn($file) => '(function() {' . file_get_contents($file) . '})();', [ __DIR__ . '/../Bar/assets/bar.js', @@ -143,11 +143,17 @@ private function buildJsCss(): string $str = "'use strict'; (function(){ - var el = document.createElement('style'); - el.setAttribute('nonce', document.currentScript.getAttribute('nonce') || document.currentScript.nonce); - el.className='tracy-debug'; - el.textContent=" . Helpers::jsonEncode(Helpers::minifyCss(implode('', $css))) . "; - document.head.appendChild(el);}) + var n = document.currentScript.getAttribute('nonce') || document.currentScript.nonce; + function s(css, cls) { + var el = document.createElement('style'); + el.setAttribute('nonce', n); + el.className = cls; + el.textContent = css; + document.head.appendChild(el); + } + s(" . json_encode(Helpers::minifyCss(implode('', $sharedCss))) . ",'tracy-debug'); + s(" . json_encode(Helpers::minifyCss($barCss)) . ",'tracy-debug tracy-bar-css'); + s(" . json_encode(Helpers::minifyCss($bsCss)) . ",'tracy-debug tracy-bs-css');}) ();\n" . implode('', $js1) . implode('', $js2); return $str; diff --git a/src/Tracy/Dumper/assets/dumper.js b/src/Tracy/Dumper/assets/dumper.js index 43d080c94..ab4f7d0d1 100644 --- a/src/Tracy/Dumper/assets/dumper.js +++ b/src/Tracy/Dumper/assets/dumper.js @@ -113,7 +113,7 @@ class Dumper { let el; - if (target.matches('.tracy-dump-hash') && (el = target.closest('[id="tracy-debug"]') || target.closest('tracy-div'))) { + if (target.matches('.tracy-dump-hash') && (el = target.getRootNode())) { el.querySelectorAll('.tracy-dump-hash').forEach((el) => { if (el.textContent === target.textContent) { el.classList.add('tracy-dump-highlight'); diff --git a/tests/Tracy/expected/Debugger.error-in-eval.expect b/tests/Tracy/expected/Debugger.error-in-eval.expect index 11fd0b6a4..38dded697 100644 --- a/tests/Tracy/expected/Debugger.error-in-eval.expect +++ b/tests/Tracy/expected/Debugger.error-in-eval.expect @@ -16,8 +16,8 @@ - -  +
+ 
@@ -251,7 +251,7 @@
- +
diff --git a/tests/Tracy/expected/Debugger.exception.fiber.html.expect b/tests/Tracy/expected/Debugger.exception.fiber.html.expect index e32104186..fa94f698e 100644 --- a/tests/Tracy/expected/Debugger.exception.fiber.html.expect +++ b/tests/Tracy/expected/Debugger.exception.fiber.html.expect @@ -16,8 +16,8 @@ - -  +
+ 
diff --git a/tests/Tracy/expected/Debugger.exception.generator.html.expect b/tests/Tracy/expected/Debugger.exception.generator.html.expect index bba16447c..09c2aea00 100644 --- a/tests/Tracy/expected/Debugger.exception.generator.html.expect +++ b/tests/Tracy/expected/Debugger.exception.generator.html.expect @@ -16,8 +16,8 @@ - -  +
+ 
diff --git a/tests/Tracy/expected/Debugger.exception.html.expect b/tests/Tracy/expected/Debugger.exception.html.expect index 493667edd..aee55b3e1 100644 --- a/tests/Tracy/expected/Debugger.exception.html.expect +++ b/tests/Tracy/expected/Debugger.exception.html.expect @@ -16,8 +16,8 @@ - -  +
+ 
@@ -321,7 +321,7 @@
- +
diff --git a/tests/Tracy/expected/Debugger.exception.in-generator.html.expect b/tests/Tracy/expected/Debugger.exception.in-generator.html.expect index ddaebf9e7..67065fabf 100644 --- a/tests/Tracy/expected/Debugger.exception.in-generator.html.expect +++ b/tests/Tracy/expected/Debugger.exception.in-generator.html.expect @@ -16,8 +16,8 @@ - -  +
+ 
diff --git a/tests/Tracy/expected/Debugger.strict.html.expect b/tests/Tracy/expected/Debugger.strict.html.expect index 6198e27df..1eba3474c 100644 --- a/tests/Tracy/expected/Debugger.strict.html.expect +++ b/tests/Tracy/expected/Debugger.strict.html.expect @@ -16,8 +16,8 @@ - -  +
+ 
@@ -260,7 +260,7 @@
- +