Skip to content

Commit 6baef5b

Browse files
committed
[Twig] Added syntax highlighters for PHP and Twig on an exception page
1 parent ca92cd0 commit 6baef5b

File tree

10 files changed

+444
-15
lines changed

10 files changed

+444
-15
lines changed

src/Symfony/Bridge/Twig/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* added TwigFlattener which adds twig files into the FlattenException
8+
* added PHP and Twig syntax highlighters
89

910
2.5.0
1011
-----
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Debug;
13+
14+
/**
15+
* The base class for syntax highlighters
16+
*
17+
* @author Martin Hasoň <martin.hason@gmail.com>
18+
*/
19+
abstract class Highlighter
20+
{
21+
/**
22+
* Highlights a code
23+
*
24+
* @param string $code The code
25+
* @param int $line The selected line number
26+
* @param int $count The number of lines above and below the selected line
27+
*
28+
* @return string The highlighted code
29+
*/
30+
abstract public function highlight($code, $line = -1, $count = -1);
31+
32+
/**
33+
* Returns true if this class is able to highlight the given file name.
34+
*
35+
* @param string $name The file name
36+
*
37+
* @return bool true if this class supports the given file name, false otherwise
38+
*/
39+
abstract public function supports($file);
40+
41+
protected function createLines($lines, $line, $count)
42+
{
43+
$code = '';
44+
$lastOpenSpan = '';
45+
46+
if ($count < 0) {
47+
$count = count($lines);
48+
}
49+
50+
$from = max(max($line, 1) - $count, 1);
51+
if ($from > 1 && 0 !== strpos($line[$from - 1], '<span')) {
52+
for ($i = $from - 2; $i >= 0; $i--) {
53+
if (preg_match('#^.*(</?span[^>]*>)#', $lines[$i], $match) && '/' != $match[1][1]) {
54+
$lastOpenSpan = $match[1];
55+
break;
56+
}
57+
}
58+
}
59+
60+
for ($i = $from, $max = min(max($line, 1) + $count, count($lines)); $i <= $max; $i++) {
61+
$code .= '<li'.($i == $line ? ' class="selected"' : '').'><code>'.($lastOpenSpan.$lines[$i - 1]);
62+
if (preg_match('#^.*(</?span[^>]*>)#', $lines[$i - 1], $match)) {
63+
$lastOpenSpan = '/' != $match[1][1] ? $match[1] : '';
64+
}
65+
66+
if ($lastOpenSpan) {
67+
$code .= '</span>';
68+
}
69+
70+
$code .= "</code></li>\n";
71+
}
72+
73+
return '<ol class="code" start="'.max($line - $count, 1).'">'.$code.'</ol>';
74+
}
75+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Debug;
13+
14+
/**
15+
* Simple PHP syntax highlighter
16+
*
17+
* @author Martin Hasoň <martin.hason@gmail.com>
18+
*/
19+
class PHPHighlighter extends Highlighter
20+
{
21+
private $styles;
22+
private $regex;
23+
24+
public function __construct()
25+
{
26+
$style = ' style="color: %s"';
27+
$this->styles = array(
28+
' class="comment"' => sprintf($style, ini_get('highlight.comment')),
29+
' class="name"' => sprintf($style, ini_get('highlight.default')),
30+
' class="tag"' => sprintf($style, ini_get('highlight.html')),
31+
' class="operator"' => sprintf($style, ini_get('highlight.keyword')),
32+
' class="string"' => sprintf($style, ini_get('highlight.string')),
33+
);
34+
35+
$this->regex = '
36+
/(?:
37+
(?P<variable>\$[a-zA-Z0-9]+)|
38+
(?P<keyword>
39+
\b(?:__halt_compiler|abstract|and|array|as|break|callable|case|catch|class|clone|const|continue|
40+
declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|extends|
41+
final|finally|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|
42+
list|namespace|new|or|print|private|protected|public|require|require_once|return|static|switch|throw|trait|try|
43+
unset|use|var|while|xor|yield)(?![^<"\']*?["\'])\b
44+
)
45+
)/ix'
46+
;
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
public function highlight($code, $line = -1, $count = -1)
53+
{
54+
// highlight_file could throw warnings
55+
// see https://bugs.php.net/bug.php?id=25725
56+
$code = @highlight_string($code, true);
57+
// remove main code/span tags
58+
$code = preg_replace('#^<code.*?>\s*<span.*?>(.*)</span>\s*</code>#s', '\\1', $code);
59+
60+
$code = $this->createLines(preg_split('#<br />#', $code), $line, $count);
61+
62+
$code = str_replace('&nbsp;', ' ', $code);
63+
$code = str_replace(array_values($this->styles), array_keys($this->styles), $code);
64+
$code = preg_replace_callback($this->regex, function ($match) {
65+
$keys = array_keys($match);
66+
return sprintf('<span class="%s">%s</span>', $keys[count($match) - 2], $match[0]);
67+
}, $code);
68+
69+
return $code;
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function supports($file)
76+
{
77+
return 'php' === pathinfo($file, PATHINFO_EXTENSION);
78+
}
79+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Debug;
13+
14+
/**
15+
* Simple Twig syntax highlighter
16+
*
17+
* @author Martin Hasoň <martin.hason@gmail.com>
18+
*/
19+
class TwigHighlighter extends Highlighter
20+
{
21+
protected $regexString = '"[^"\\\\]*?(?:\\\\.[^"\\\\]*?)*?"|\'[^\'\\\\]*?(?:\\\\.[^\'\\\\]*?)*?\'';
22+
protected $regexTags;
23+
protected $regex;
24+
25+
public function __construct()
26+
{
27+
$this->regexTags = '{({{-?|{%-?|{#)((?:'.$this->regexString.'|[^"\']*?)+?)(-?}}|-?%}|#})}s';
28+
$this->regexKeywords = 'and|or|with';
29+
$this->regex = '
30+
/(?:
31+
(?P<string>'.$this->regexString.')|
32+
(?P<number>\b\d+(?:\.\d+)?\b)|
33+
(?P<variable>(?<!\.)\b[a-z][a-z0-9_]*(?=\.|\[|\s*=))|
34+
(?P<name>\b[a-z0-9_]+(?=\s*\()|(?<=\||\|\s)[a-z0-9_]+\b)|
35+
(?P<operator>(?:\*\*|\.\.|==|!=|>=|<=|\/\/|\?:|[+\-~\*\/%\.=><\|\(\)\[\]\{\}\?:,]))|
36+
(?P<keyword>\b(?:if|and|or|b-and|b-xor|b-or|in|matches|starts with|ends with|is|not|as|import|with|true|false|null|none)\b)
37+
)/xi'
38+
;
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function highlight($code, $line = -1, $count = -1)
45+
{
46+
$regex = $this->regex;
47+
$code = preg_replace_callback($this->regexTags, function ($matches) use ($regex) {
48+
if ($matches[1] == '{#') {
49+
return '<span class="comment">' . $matches[0] . '</span>';
50+
}
51+
52+
$matches[2] = preg_replace_callback($regex, function ($match) {
53+
$keys = array_keys($match);
54+
55+
return sprintf('<span class="%s">%s</span>', $keys[count($match) - 2], $match[0]);
56+
}, $matches[2]);
57+
58+
if ($matches[1][1] == '%') {
59+
$matches[2] = preg_replace('/^(\s*)([a-z0-9_]+)/i', '\\1<span class="keyword">\\2</span>', $matches[2]);
60+
}
61+
62+
return '<span class="tag">'.$matches[1].'</span>'.$matches[2].'<span class="tag">'.$matches[3].'</span>';
63+
}, htmlspecialchars(str_replace(array("\r\n", "\r"), "\n", $code), ENT_NOQUOTES));
64+
65+
return $this->createLines(explode("\n", $code), $line, $count);
66+
}
67+
68+
/**
69+
* {@inheritdoc}
70+
*/
71+
public function supports($file)
72+
{
73+
return 'twig' === pathinfo($file, PATHINFO_EXTENSION);
74+
}
75+
}

src/Symfony/Bridge/Twig/Extension/CodeExtension.php

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Symfony\Bridge\Twig\Extension;
1313

14+
use Symfony\Bridge\Twig\Debug\Highlighter;
15+
use Symfony\Bridge\Twig\Debug\PHPHighlighter;
16+
1417
if (!defined('ENT_SUBSTITUTE')) {
1518
define('ENT_SUBSTITUTE', 8);
1619
}
@@ -25,19 +28,25 @@ class CodeExtension extends \Twig_Extension
2528
private $fileLinkFormat;
2629
private $rootDir;
2730
private $charset;
31+
private $highlighters = array();
2832

2933
/**
3034
* Constructor.
3135
*
3236
* @param string $fileLinkFormat The format for links to source files
3337
* @param string $rootDir The project root directory
3438
* @param string $charset The charset
39+
* @param array $highlighters The syntax highlighters
3540
*/
36-
public function __construct($fileLinkFormat, $rootDir, $charset)
41+
public function __construct($fileLinkFormat, $rootDir, $charset, $highlighters = array())
3742
{
3843
$this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
3944
$this->rootDir = str_replace('\\', '/', dirname($rootDir)).'/';
4045
$this->charset = $charset;
46+
47+
foreach ($highlighters as $highlighter) {
48+
$this->addHighlighter($highlighter);
49+
}
4150
}
4251

4352
/**
@@ -136,21 +145,22 @@ public function formatArgsAsText($args)
136145
*/
137146
public function fileExcerpt($file, $line)
138147
{
139-
if (is_readable($file)) {
140-
// highlight_file could throw warnings
141-
// see https://bugs.php.net/bug.php?id=25725
142-
$code = @highlight_file($file, true);
143-
// remove main code/span tags
144-
$code = preg_replace('#^<code.*?>\s*<span.*?>(.*)</span>\s*</code>#s', '\\1', $code);
145-
$content = preg_split('#<br />#', $code);
146-
147-
$lines = array();
148-
for ($i = max($line - 3, 1), $max = min($line + 3, count($content)); $i <= $max; $i++) {
149-
$lines[] = '<li'.($i == $line ? ' class="selected"' : '').'><code>'.self::fixCodeMarkup($content[$i - 1]).'</code></li>';
148+
if (!is_readable($file)) {
149+
return;
150+
}
151+
152+
foreach ($this->highlighters as $highlighter) {
153+
if ($highlighter->supports($file)) {
154+
break;
150155
}
156+
unset($highlighter);
157+
}
151158

152-
return '<ol start="'.max($line - 3, 1).'">'.implode("\n", $lines).'</ol>';
159+
if (!isset($highlighter)) {
160+
$highlighter = new PHPHighlighter();
153161
}
162+
163+
return $highlighter->highlight(file_get_contents($file), $line, 3);
154164
}
155165

156166
/**
@@ -210,6 +220,16 @@ public function formatFileFromText($text)
210220
}, $text);
211221
}
212222

223+
/**
224+
* Adds a syntax highlighter
225+
*
226+
* @param Highlighter $highlighter A syntax highlighter
227+
*/
228+
public function addHighlighter(Highlighter $highlighter)
229+
{
230+
$this->highlighters[] = $highlighter;
231+
}
232+
213233
public function getName()
214234
{
215235
return 'code';

0 commit comments

Comments
 (0)