-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[WebProfilerBundle][WIP] Real-time twig debug, aka xray view #28056
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -362,6 +362,103 @@ | |
} | ||
{% endif %} | ||
|
||
var twigDebugEnabled = function () { | ||
return !!document.getElementById('__twig-debug-bar'); | ||
}; | ||
|
||
var twigDebugEnable = function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the logic in this method is not compatible with old browsers, which is not good for something running in in the toolbar (as the toolbar runs in the userland page, which might need to support them). so there is 2 solutions here:
|
||
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 | ||
}; | ||
})(); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* 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 <yonelceruto@gmail.com> | ||
*/ | ||
class HtmlDebugEnterComment extends Node implements \Twig_NodeOutputInterface | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Symfony uses the namespaced version of all Twig classes and interfaces, to make sure we are ready for the future version of Twig dropping support for the non-namespaced version. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, should |
||
{ | ||
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("<!--TWIG-START: %s %s %s %d-->\n", $this->getAttribute('type'), rtrim($this->getAttribute('hash').' '.$this->getAttribute('name')), $this->getAttribute('template'), $this->lineno)) | ||
->raw(";\n") | ||
; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* 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 <yonelceruto@gmail.com> | ||
*/ | ||
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("<!--TWIG-END: %s %s-->\n", $this->getAttribute('type'), $this->getAttribute('hash'))) | ||
->raw(";\n") | ||
; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* 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 <yonelceruto@gmail.com> | ||
*/ | ||
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/'); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inline styles should be avoided for CSP compat. Use a class with styles instead.