10000 [WebProfilerBundle][WIP] Real-time twig debug, aka xray view by ro0NL · Pull Request #28056 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
[WebProfilerBundle] Real-time twig debug, aka xray view
  • Loading branch information
ro0NL committed Jul 24, 2018
commit b3f2e26eb718ab927f0cdaadb3b87d22129ca2dc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
<b>Macro Calls</b>
<span class="sf-toolbar-status">{{ collector.macrocount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<input type="color" id="__twig-debug-color" value="#ff0000">
</div>
<div class="sf-toolbar-info-piece">
<span><a href="javascript:;" id="__twig-debug-enable">Enable debug</a></span><br>
<span><a href="javascript:;" id="__twig-debug-discover" style="display: none;">Discover</a></span>
Copy link
Member

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.

</div>
{% endset %}

{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,103 @@
}
{% endif %}

var twigDebugEnabled = function () {
return !!document.getElementById('__twig-debug-bar');
};

var twigDebugEnable = function () {
Copy link
Member

Choose a reason for hiding this comment

The 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:

  • ensure the code is compatible with old IE versions
  • feature-detect the support for features being used, and forbid enabling the Twig debug mode if not supported.

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,

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -675,7 +805,13 @@

toggles[i].setAttribute('data-processed', 'true');
}
}
},

twigDebugEnabled: twigDebugEnabled,

twigDebugEnable: twigDebugEnable,

twigDebugDisable: twigDebugDisable
};
})();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(:h 17AE over):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 {
Expand Down
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
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, should Twig\Node\NodeOutputInterface be used instead.

{
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/');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
0