From b3f2e26eb718ab927f0cdaadb3b87d22129ca2dc Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Tue, 24 Jul 2018 17:57:17 +0200 Subject: [PATCH 1/2] [WebProfilerBundle] Real-time twig debug, aka xray view --- .../Resources/views/Collector/twig.html.twig | 7 + .../views/Profiler/base_js.html.twig | 138 +++++++++++++++++- .../Resources/views/Profiler/toolbar.css.twig | 39 +++++ .../Twig/Node/HtmlDebugEnterComment.php | 35 +++++ .../Twig/Node/HtmlDebugLeaveComment.php | 34 +++++ .../Twig/NodeVisitor/HtmlDebugNodeVisitor.php | 85 +++++++++++ .../Twig/WebProfilerExtension.php | 9 ++ 7 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Twig/Node/HtmlDebugEnterComment.php create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Twig/Node/HtmlDebugLeaveComment.php create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Twig/NodeVisitor/HtmlDebugNodeVisitor.php diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/twig.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/twig.html.twig index 28df9d26ea792..4c98e30475f77 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/twig.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/twig.html.twig @@ -25,6 +25,13 @@ Macro Calls {{ collector.macrocount }} +
+ +
+
+ Enable debug
+ +
{% endset %} {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index fb266c554bf9e..90e555d9ceff7 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -362,6 +362,103 @@ } {% endif %} + var twigDebugEnabled = function () { + return !!document.getElementById('__twig-debug-bar'); + }; + + var twigDebugEnable = function () { + if (twigDebugEnabled()) { + return; + } + var comments = document.createNodeIterator(document.body, NodeFilter.SHOW_COMMENT), MAX_ZINDEX = 2147483647; + while (comments.nextNode()) { + var comment = comments.referenceNode, match = comment.data.match(/^TWIG-START: (.+)$/); + if (!match) { + continue; + } + + var parts = match[1].split(' '), el = comment.nextSibling, stack = []; + while (!(!el || (el.nodeType === 8 && el.data === ('TWIG-END: ' + parts[0] + ' ' + parts[1])))) { + if (el.nodeType === 3 || (el.nodeType === 1 && el.tagName !== 'SCRIPT')) { + stack.push(el); + } + el = el.nextSibling; + } + + var bbox = {top: Number.MAX_SAFE_INTEGER, left: Number.MAX_SAFE_INTEGER, right: 0, bottom: 0}, i, c; + for (i = 0, c = stack.length; i < c; ++i) { + var target = stack[i], rect; + if (target.nodeType === 3) { + target = document.createRange(); + target.selectNodeContents(stack[i]); + } + rect = target.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) { + continue; + } + bbox.top = Math.min(bbox.top, rect.top); + bbox.left = Math.min(bbox.left, rect.left); + bbox.right = Math.max(bbox.right, rect.right); + bbox.bottom = Math.max(bbox.bottom, rect.bottom); + } + bbox.width = bbox.right - bbox.left; + bbox.height = bbox.bottom - bbox.top; + if (bbox.width <= 0 || bbox.height <= 0) { + continue; + } + + var visual = document.createElement('DIV'); + visual.dataset.twigDebug = parts.join(' '); + visual.style.top = parseInt(bbox.top) + 'px'; + visual.style.left = parseInt(bbox.left) + 'px'; + visual.style.width = parseInt(bbox.width) + 'px'; + visual.style.height = parseInt(bbox.height) + 'px'; + visual.style.zIndex = String(MAX_ZINDEX - Math.min((MAX_ZINDEX - 99999), parseInt(bbox.width * bbox.height))); + addEventListener(visual, 'mouseenter', function () { + var bar = document.getElementById('__twig-debug-bar'), rect = this.getBoundingClientRect(), data = this.dataset.twigDebug.split(' '), label; + switch (data[0] || null) { + case 'MACRO': + label = 'Macro "' + data[2] +'" in "' + data[3] + '" line ' + data[4]; + break; + case 'BLOCK': + label = 'Block "' + data[2] +'" in "' + data[3] + '" line ' + data[4]; + break; + case 'TEMPLATE': + label = 'Template "' + data[2] +'"'; + break; + default: + label = 'Unknown twig element'; + break; + + } + if (hasClass(bar, 'colorized')) { + bar.setAttribute('title', label); + bar.textContent = ''; + } else { + bar.textContent = label; + } + bar.style.top = parseInt(rect.top + rect.height) + 'px'; + bar.style.left = parseInt(rect.left) + 'px'; + bar.style.display = 'block'; + }); + addEventListener(visual, 'mouseleave', function () { + document.getElementById('__twig-debug-bar').style.display = 'none'; + }); + document.body.appendChild(visual); + } + + var bar = document.createElement('DIV'); + bar.setAttribute('id', '__twig-debug-bar'); + bar.style.zIndex = String(MAX_ZINDEX); + document.body.appendChild(bar); + }; + + var twigDebugDisable = function () { + document.querySelectorAll('[data-twig-debug], #__twig-debug-bar').forEach(function (el) { + el.parentNode.removeChild(el); + }); + }; + return { hasClass: hasClass, @@ -507,6 +604,39 @@ dumpInfo.style.minHeight = ''; }); } + + addEventListener(document.getElementById('__twig-debug-enable'), 'click', function () { + var discover = document.getElementById('__twig-debug-discover'); + discover.textContent = 'Discover'; + if (twigDebugEnabled()) { + twigDebugDisable(); + discover.style.display = 'none'; + this.textContent = 'Enable debug'; + } else { + twigDebugEnable(); + discover.style.display = 'initial'; + this.textContent = 'Disable debug'; + } + }); + + addEventListener(document.getElementById('__twig-debug-discover'), 'click', function() { + document.querySelectorAll('[data-twig-debug]').forEach(function (el) { + toggleClass(el, 'discovered'); + }); + this.textContent = document.querySelector('[data-twig-debug].discovered') ? 'Cancel discovery' : 'Discover'; + }); + + addEventListener(document.getElementById('__twig-debug-color'), 'change', function() { + var style = document.getElementById('twig-debug-style'); + if (!style) { + style = document.createElement('STYLE'); + document.body.appendChild(style); + } + style.textContent = '#__twig-debug-bar { background: ' + this.value + '; }'; + style.textContent += '[data-twig-debug]:hover { outline-color: ' + this.value + '; }'; + style.textContent += '[data-twig-debug].discovered:not(:hover):after { color: ' + this.value + '; }'; + addClass(document.getElementById('__twig-debug-bar'), 'colorized'); + }); }, function(xhr) { if (xhr.status !== 0) { @@ -675,7 +805,13 @@ toggles[i].setAttribute('data-processed', 'true'); } - } + }, + + twigDebugEnabled: twigDebugEnabled, + + twigDebugEnable: twigDebugEnable, + + twigDebugDisable: twigDebugDisable }; })(); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig index 09056e177093b..f2e0849340637 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig @@ -550,6 +550,45 @@ div.sf-toolbar .sf-toolbar-block a:hover { margin-right: 10px; } +#__twig-debug-bar { + position: absolute; + display: none; + padding: 3px; + margin-top: 3px; + margin-left: -5px; + background: red; + color: white; + font-family: monospace; + font-weight: bold; +} + +#__twig-debug-bar.colorized:after { + mix-blend-mode: difference; + content: attr(title); +} + +#__twig-debug-color { + display: block; + width: 80px; + margin: 5px 0 0 0; +} + +[data-twig-debug] { + position: absolute; +} +[data-twig-debug]:hover { + outline: 2px dashed red; + outline-offset: 3px; +} +[data-twig-debug].discovered:not(:hover):after { + content: ' \25E2'; + font-size: x-small; + color: red; + position: absolute; + right: 0; + bottom: 0; +} + /***** Media query print: Do not print the Toolbar. *****/ @media print { .sf-toolbar { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Twig/Node/HtmlDebugEnterComment.php b/src/Symfony/Bundle/WebProfilerBundle/Twig/Node/HtmlDebugEnterComment.php new file mode 100644 index 0000000000000..554d6fb896a21 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Twig/Node/HtmlDebugEnterComment.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Twig\Node; + +use Twig\Node\Node; + +/** + * @author Yonel Ceruto + */ +class HtmlDebugEnterComment extends Node implements \Twig_NodeOutputInterface +{ + public function __construct($type, $hash, $name, $template, $lineno) + { + parent::__construct(array(), array('type' => $type, 'hash' => $hash, 'name' => $name, 'template' => $template), $lineno); + } + + public function compile(\Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('echo ') + ->string(sprintf("\n", $this->getAttribute('type'), rtrim($this->getAttribute('hash').' '.$this->getAttribute('name')), $this->getAttribute('template'), $this->lineno)) + ->raw(";\n") + ; + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Twig/Node/HtmlDebugLeaveComment.php b/src/Symfony/Bundle/WebProfilerBundle/Twig/Node/HtmlDebugLeaveComment.php new file mode 100644 index 0000000000000..ca1cf582750b8 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Twig/Node/HtmlDebugLeaveComment.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Twig\Node; + +use Twig\Node\Node; + +/** + * @author Yonel Ceruto + */ +class HtmlDebugLeaveComment extends Node implements \Twig_NodeOutputInterface +{ + public function __construct($type, $hash) + { + parent::__construct(array(), array('type' => $type, 'hash' => $hash)); + } + + public function compile(\Twig_Compiler $compiler) + { + $compiler + ->write('echo ') + ->string(sprintf("\n", $this->getAttribute('type'), $this->getAttribute('hash'))) + ->raw(";\n") + ; + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Twig/NodeVisitor/HtmlDebugNodeVisitor.php b/src/Symfony/Bundle/WebProfilerBundle/Twig/NodeVisitor/HtmlDebugNodeVisitor.php new file mode 100644 index 0000000000000..3e51353845086 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Twig/NodeVisitor/HtmlDebugNodeVisitor.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Twig\NodeVisitor; + +use Symfony\Bundle\WebProfilerBundle\Twig\Node\HtmlDebugEnterComment; +use Symfony\Bundle\WebProfilerBundle\Twig\Node\HtmlDebugLeaveComment; + +/** + * @author Yonel Ceruto + */ +class HtmlDebugNodeVisitor extends \Twig_BaseNodeVisitor +{ + public const TEMPLATE = 'TEMPLATE'; + public const BLOCK = 'BLOCK'; + public const MACRO = 'MACRO'; + + public function getPriority(): int + { + return 0; + } + + protected function doEnterNode(\Twig_Node $node, \Twig_Environment $env) + { + return $node; + } + + protected function doLeaveNode(\Twig_Node $node, \Twig_Environment $env) + { + if (!$this->supports($node)) { + return $node; + } + + $nodeName = $node->hasAttribute('name') ? $node->getAttribute('name') : ''; + $templateName = $node->getTemplateName(); + $hash = md5($nodeName.$templateName); + $line = $node->getTemplateLine(); + + if ($node instanceof \Twig_Node_Module) { + $node->setNode('display_start', new \Twig_Node(array( + new HtmlDebugEnterComment(self::TEMPLATE, $hash, $nodeName, $templateName, $line), + $node->getNode('display_start'), + ))); + $node->setNode('display_end', new \Twig_Node(array( + $node->getNode('display_end'), + new HtmlDebugLeaveComment(self::TEMPLATE, $hash), + ))); + } elseif ($node instanceof \Twig_Node_Block) { + $node->setNode('body', new \Twig_Node_Body(array( + new HtmlDebugEnterComment(self::BLOCK, $hash, $nodeName, $templateName, $line), + $node->getNode('body'), + new HtmlDebugLeaveComment(self::BLOCK, $hash), + ))); + } elseif ($node instanceof \Twig_Node_Macro) { + $node->setNode('body', new \Twig_Node_Body(array( + new HtmlDebugEnterComment(self::MACRO, $hash, $nodeName, $templateName, $line), + $node->getNode('body'), + new HtmlDebugLeaveComment(self::MACRO, $hash), + ))); + } + + return $node; + } + + private function supports(\Twig_Node $node): bool + { + if (!$node instanceof \Twig_Node_Module && !$node instanceof \Twig_Node_Block && !$node instanceof \Twig_Node_Macro) { + return false; + } + + if ('.html.twig' !== substr($name = $node->getTemplateName(), -10)) { + return false; + } + + return false === strpos($name, '@WebProfiler/'); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php index c714ff0642472..1bec0a41627cd 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\WebProfilerBundle\Twig; +use Symfony\Bundle\WebProfilerBundle\Twig\NodeVisitor\HtmlDebugNodeVisitor; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Twig\Environment; @@ -69,6 +70,14 @@ public function getFunctions() ); } + /** + * {@inheritdoc} + */ + public function getNodeVisitors(): array + { + return array(new HtmlDebugNodeVisitor()); + } + public function dumpData(Environment $env, Data $data, $maxDepth = 0) { $this->dumper->setCharset($env->getCharset()); From a09ecd4052f51f836056655fcea2d54439fd6cb7 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Tue, 24 Jul 2018 21:53:09 +0200 Subject: [PATCH 2/2] handle scroll/resize --- .../Resources/views/Collector/twig.html.twig | 2 +- .../Resources/views/Profiler/base_js.html.twig | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/twig.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/twig.html.twig index 4c98e30475f77..7bf290efbda07 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/twig.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/twig.html.twig @@ -29,7 +29,7 @@ {% endset %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index 90e555d9ceff7..663d3f0a044da 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -409,8 +409,8 @@ var visual = document.createElement('DIV'); visual.dataset.twigDebug = parts.join(' '); - visual.style.top = parseInt(bbox.top) + 'px'; - visual.style.left = parseInt(bbox.left) + 'px'; + visual.style.top = parseInt(bbox.top + document.documentElement.scrollTop || document.body.scrollTop) + 'px'; + visual.style.left = parseInt(bbox.left + document.documentElement.scrollLeft || document.body.scrollLeft) + 'px'; visual.style.width = parseInt(bbox.width) + 'px'; visual.style.height = parseInt(bbox.height) + 'px'; visual.style.zIndex = String(MAX_ZINDEX - Math.min((MAX_ZINDEX - 99999), parseInt(bbox.width * bbox.height))); @@ -437,8 +437,8 @@ } else { bar.textContent = label; } - bar.style.top = parseInt(rect.top + rect.height) + 'px'; - bar.style.left = parseInt(rect.left) + 'px'; + bar.style.top = parseInt(rect.top + (document.documentElement.scrollTop || document.body.scrollTop) + rect.height) + 'px'; + bar.style.left = parseInt(rect.left + (document.documentElement.scrollLeft || document.body.scrollLeft)) + 'px'; bar.style.display = 'block'; }); addEventListener(visual, 'mouseleave', function () { @@ -605,6 +605,14 @@ }); } + addEventListener(window, 'resize', function() { + if (twigDebugEnabled()) { + twigDebugDisable(); + twigDebugEnable(); + document.getElementById('__twig-debug-discover').textContent = 'Discover'; + } + }); + addEventListener(document.getElementById('__twig-debug-enable'), 'click', function () { var discover = document.getElementById('__twig-debug-discover'); discover.textContent = 'Discover'; @@ -613,6 +621,7 @@ discover.style.display = 'none'; this.textContent = 'Enable debug'; } else { + window.scrollTo(0, 0); twigDebugEnable(); discover.style.display = 'initial'; this.textContent = 'Disable debug'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy