diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php
index 332a5d6c3725e..2ba78f516933d 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php
+++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php
@@ -119,39 +119,85 @@ public function formatArgsAsText(array $args): string
*/
public function fileExcerpt(string $file, int $line, int $srcContext = 3): ?string
{
- if (is_file($file) && is_readable($file)) {
- // highlight_file could throw warnings
- // see https://bugs.php.net/25725
- $code = @highlight_file($file, true);
- if (\PHP_VERSION_ID >= 80300) {
- // remove main pre/code tags
- $code = preg_replace('#^
\s*(.*)\s*#s', '\\1', $code);
- // split multiline span tags
- $code = preg_replace_callback('#]++)>((?:[^<\\n]*+\\n)++[^<]*+)#', function ($m) {
- return "".str_replace("\n", "\n", $m[2]).'';
- }, $code);
- $content = explode("\n", $code);
- } else {
- // remove main code/span tags
- $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code);
- // split multiline spans
- $code = preg_replace_callback('#]++)>((?:[^<]*+
)++[^<]*+)#', fn ($m) => "".str_replace('
', "
", $m[2]).'', $code);
- $content = explode('
', $code);
- }
+ if (!is_file($file) || !is_readable($file)) {
+ return null;
+ }
+
+ $contents = file_get_contents($file);
+
+ if (!str_contains($contents, ' $srcContext) {
- $srcContext = \count($content);
+ $srcContext = \count($lines);
}
- for ($i = max($line - $srcContext, 1), $max = min($line + $srcContext, \count($content)); $i <= $max; ++$i) {
- $lines[] = ''.self::fixCodeMarkup($content[$i - 1]).'
';
- }
+ return $this->formatFileExcerpt(
+ $this->extractExcerptLines($lines, $line, $srcContext),
+ $line,
+ $srcContext
+ );
+ }
- return ''.implode("\n", $lines).'
';
+ // highlight_string could throw warnings
+ // see https://bugs.php.net/25725
+ $code = @highlight_string($contents, true);
+
+ if (\PHP_VERSION_ID >= 80300) {
+ // remove main pre/code tags
+ $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code);
+ // split multiline span tags
+ $code = preg_replace_callback(
+ '#]++)>((?:[^<\\n]*+\\n)++[^<]*+)#',
+ static fn (array $m): string => "".str_replace("\n", "\n", $m[2]).'',
+ $code
+ );
+ $lines = explode("\n", $code);
+ } else {
+ // remove main code/span tags
+ $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code);
+ // split multiline spans
+ $code = preg_replace_callback(
+ '#]++)>((?:[^<]*+
)++[^<]*+)#',
+ static fn (array $m): string => "".str_replace('
', "
", $m[2]).'',
+ $code
+ );
+ $lines = explode('
', $code);
}
- return null;
+ if (0 > $srcContext) {
+ $srcContext = \count($lines);
+ }
+
+ return $this->formatFileExcerpt(
+ array_map(
+ static fn (string $line): string => self::fixCodeMarkup($line),
+ $this->extractExcerptLines($lines, $line, $srcContext),
+ ),
+ $line,
+ $srcContext
+ );
+ }
+
+ private function extractExcerptLines(array $lines, int $selectedLine, int $srcContext): array
+ {
+ return \array_slice(
+ $lines,
+ max($selectedLine - $srcContext, 0),
+ min($srcContext * 2 + 1, \count($lines) - $selectedLine + $srcContext),
+ true
+ );
+ }
+
+ private function formatFileExcerpt(array $lines, int $selectedLine, int $srcContext): string
+ {
+ $start = max($selectedLine - $srcContext, 1);
+
+ return "".implode("\n", array_map(
+ static fn (string $line, int $num): string => '{$line}
",
+ $lines,
+ array_keys($lines),
+ )).'
';
}
/**
@@ -241,7 +287,7 @@ protected static function fixCodeMarkup(string $line): string
// missing tag at the end of line
$opening = strpos($line, '');
- if (false !== $opening && (false === $closing || $closing > $opening)) {
+ if (false !== $opening && (false === $closing || $closing < $opening)) {
$line .= '';
}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/hello_world.json b/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/hello_world.json
new file mode 100644
index 0000000000000..56cc557387321
--- /dev/null
+++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/hello_world.json
@@ -0,0 +1,4 @@
+[
+ "Hello",
+ "World!"
+]
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/hello_world.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/hello_world.php
new file mode 100644
index 0000000000000..4d7bf8fdf167e
--- /dev/null
+++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/hello_world.php
@@ -0,0 +1,4 @@
+assertEquals($expected, $this->render($template));
}
+ /**
+ * @dataProvider fileExcerptIntegrationProvider
+ */
+ public function testFileExcerptIntegration(string $expected, array $data)
+ {
+ $template = <<<'TWIG'
+{{ file_path|file_excerpt(line, src_context) }}
+TWIG;
+ $html = $this->render($template, $data);
+
+ // highlight_file function output changed sing PHP 8.3
+ // see https://github.com/php/php-src/blob/e2667f17bc24e3cd200bb3eda457f566f1f77f8f/UPGRADING#L239-L242
+ if (\PHP_VERSION_ID < 80300) {
+ $html = str_replace(' ', ' ', $html);
+ }
+
+ $html = html_entity_decode($html);
+
+ $this->assertEquals($expected, $html);
+ }
+
+ public static function fileExcerptIntegrationProvider()
+ {
+ $fixturesPath = realpath(__DIR__.\DIRECTORY_SEPARATOR.'..'.\DIRECTORY_SEPARATOR.'Fixtures');
+
+ yield 'php file' => [
+ 'expected' => <<<'HTML'
+
+
+echo 'Hello';
+echo 'World!';
+
+HTML,
+ 'data' => [
+ 'file_path' => $fixturesPath.\DIRECTORY_SEPARATOR.'hello_world.php',
+ 'line' => 0,
+ 'src_context' => 3,
+ ],
+ ];
+
+ yield 'php file with selected line and no source context' => [
+ 'expected' => <<<'HTML'
+
+
+echo 'Hello';
+echo 'World!';
+
+HTML,
+ 'data' => [
+ 'file_path' => $fixturesPath.\DIRECTORY_SEPARATOR.'hello_world.php',
+ 'line' => 1,
+ 'src_context' => -1,
+ ],
+ ];
+
+ yield 'php file excerpt with selected line and custom source context' => [
+ 'expected' => <<<'HTML'
+echo 'Hello';
+echo 'World!';
+
+HTML,
+ 'data' => [
+ 'file_path' => $fixturesPath.\DIRECTORY_SEPARATOR.'hello_world.php',
+ 'line' => 3,
+ 'src_context' => 1,
+ ],
+ ];
+
+ yield 'php file excerpt with out of bound selected line' => [
+ 'expected' => <<<'HTML'
+
+HTML,
+ 'data' => [
+ 'file_path' => $fixturesPath.\DIRECTORY_SEPARATOR.'hello_world.php',
+ 'line' => 100,
+ 'src_context' => 1,
+ ],
+ ];
+
+ yield 'json file' => [
+ 'expected' => <<<'HTML'
+[
+ "Hello",
+ "World!"
+]
+
+HTML,
+ 'data' => [
+ 'file_path' => $fixturesPath.\DIRECTORY_SEPARATOR.'hello_world.json',
+ 'line' => 0,
+ 'src_context' => 3,
+ ],
+ ];
+ }
+
public function testFormatFileFromTextIntegration()
{
$template = <<<'TWIG'