Skip to content

Commit 59c96e6

Browse files
vjiksamdark
andauthored
Add $traceLink parameter to HtmlRenderer (#151)
Co-authored-by: Alexander Makarov <[email protected]>
1 parent 78edcff commit 59c96e6

File tree

5 files changed

+110
-7
lines changed

5 files changed

+110
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 4.1.1 under development
44

55
- Enh #150: Cleanup templates, remove legacy code (@vjik)
6+
- New #151: Add `$traceLink` parameter to `HtmlRenderer` to allow linking to trace files (@vjik)
67

78
## 4.1.0 April 18, 2025
89

src/Renderer/HtmlRenderer.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Alexkart\CurlBuilder\Command;
88
use cebe\markdown\GithubMarkdown;
9+
use Closure;
910
use Psr\Http\Message\ServerRequestInterface;
1011
use RuntimeException;
1112
use Throwable;
@@ -48,6 +49,7 @@
4849
/**
4950
* Formats throwable into HTML string.
5051
*
52+
* @psalm-type TraceLinkClosure = Closure(string $file, int|null $line): (string|null)
5153
* @psalm-import-type DebugBacktraceType from ErrorException
5254
*/
5355
final class HtmlRenderer implements ThrowableRendererInterface
@@ -99,6 +101,11 @@ final class HtmlRenderer implements ThrowableRendererInterface
99101
*/
100102
public readonly ?string $traceHeaderLine;
101103

104+
/**
105+
* @psalm-var TraceLinkClosure
106+
*/
107+
public readonly Closure $traceLinkGenerator;
108+
102109
/**
103110
* @var string[]|null The list of vendor paths is determined automatically.
104111
*
@@ -120,7 +127,20 @@ final class HtmlRenderer implements ThrowableRendererInterface
120127
* information.
121128
* @param int|null $maxSourceLines The maximum number of source code lines to be displayed. Defaults to 19.
122129
* @param int|null $maxTraceLines The maximum number of trace source code lines to be displayed. Defaults to 13.
123-
* @param string|null $traceHeaderLine The trace header line with placeholders to be substituted. Defaults to null.
130+
* @param string|null $traceHeaderLine Deprecated, use {@see traceLink} instead. The trace header line with
131+
* placeholders to be substituted. Defaults to null.
132+
* @param Closure|string|null $traceLink The trace link. It can be a string with placeholders `file` and `line` to
133+
* be substituted or a closure that accepts `file` and `line` parameters and returns a string or null. Examples:
134+
* - string "ide://open?file={file}&line={line}";
135+
* - closure:
136+
* ```php
137+
* static function (string $file, ?int $line): string {
138+
* return strtr(
139+
* 'phpstorm://open?file={file}&line={line}',
140+
* ['{file}' => $file, '{line}' => (string) $line],
141+
* );
142+
* }
143+
* ```
124144
*
125145
* @psalm-param array{
126146
* template?: string,
@@ -129,6 +149,7 @@ final class HtmlRenderer implements ThrowableRendererInterface
129149
* maxTraceLines?: int,
130150
* traceHeaderLine?: string,
131151
* } $settings
152+
* @psalm-param string|TraceLinkClosure|null $traceLink
132153
*/
133154
public function __construct(
134155
array $settings = [],
@@ -137,6 +158,7 @@ public function __construct(
137158
?int $maxSourceLines = null,
138159
?int $maxTraceLines = null,
139160
?string $traceHeaderLine = null,
161+
string|Closure|null $traceLink = null,
140162
) {
141163
$this->markdownParser = new GithubMarkdown();
142164
$this->markdownParser->html5 = true;
@@ -157,6 +179,7 @@ public function __construct(
157179
$this->traceHeaderLine = $traceHeaderLine
158180
?? $settings['traceHeaderLine']
159181
?? null;
182+
$this->traceLinkGenerator = $this->createTraceLinkGenerator($traceLink);
160183
}
161184

162185
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
@@ -703,4 +726,23 @@ public function removeAnonymous(string $value): string
703726

704727
return $anonymousPosition !== false ? substr($value, 0, $anonymousPosition) : $value;
705728
}
729+
730+
/**
731+
* @psalm-param string|TraceLinkClosure|null $traceLink
732+
* @psalm-return TraceLinkClosure
733+
*/
734+
private function createTraceLinkGenerator(string|Closure|null $traceLink): Closure
735+
{
736+
if ($traceLink === null) {
737+
return static fn(): string|null => null;
738+
}
739+
740+
if (is_string($traceLink)) {
741+
return static function (string $file, ?int $line) use ($traceLink): string {
742+
return str_replace(['{file}', '{line}'], [$file, (string) $line], $traceLink);
743+
};
744+
}
745+
746+
return $traceLink;
747+
}
706748
}

templates/_call-stack-item.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,34 @@
3434
<div class="flex-1 mw-100">
3535
<?php if ($file !== null): ?>
3636
<span class="file-name">
37-
<?= "{$index}. in {$this->htmlEncode($file)}" ?>
38-
<?php if ($this->traceHeaderLine !== null): ?>
39-
<?= strtr($this->traceHeaderLine, ['{file}' => $file, '{line}' => (int) $line + 1, '{icon}' => $icon]) ?>
40-
<?php endif ?>
37+
<?php
38+
if ($this->traceHeaderLine === null) {
39+
echo "$index. ";
40+
$traceLink = ($this->traceLinkGenerator)($file, $line);
41+
echo $traceLink === null
42+
? $this->htmlEncode($file)
43+
: sprintf(
44+
'<a href="%s" class="trace-link">%s %s</a>',
45+
$this->htmlEncode($traceLink),
46+
$this->htmlEncode($file),
47+
$icon,
48+
);
49+
} else {
50+
echo "$index. " . $this->htmlEncode($file);
51+
echo strtr($this->traceHeaderLine, [
52+
'{file}' => $file,
53+
'{line}' => (int) $line + 1,
54+
'{icon}' => $icon,
55+
]);
56+
}
57+
?>
4158
</span>
4259
<?php endif ?>
4360

4461
<?php if ($function !== null): ?>
4562
<span class="function-info word-break">
4663
<?php
47-
echo $file === null ? "{$index}." : '&mdash;&nbsp;';
64+
echo $file === null ? "$index. " : '&mdash;&nbsp;';
4865
$function = $class === null ? $function : "{$this->removeAnonymous($class)}::$function";
4966

5067
echo '<span class="function">' . $this->htmlEncode($function) . '</span>';

templates/development.css

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,17 @@ main {
572572
color: var(--element-wrap-text-color);
573573
}
574574

575-
.call-stack ul li .element-wrap .file-name:hover,
575+
.call-stack ul li .element-wrap .file-name .trace-link {
576+
color: var(--element-wrap-text-color);
577+
text-decoration: none;
578+
}
579+
.call-stack ul li .element-wrap .file-name .trace-link:hover {
580+
color: var(--element-wrap-hover-text-color);
581+
}
582+
.call-stack ul li .element-wrap .file-name .trace-link:hover path {
583+
fill: var(--icon-color);
584+
}
585+
576586
.call-stack ul li .element-code-wrap .code-wrap .lines-item:hover {
577587
color: var(--element-wrap-hover-text-color);
578588
}

tests/Renderer/HtmlRendererTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,39 @@ public function testIsVendorFileWithPathsAlreadyAdded(): void
303303
$this->assertFalse($this->invokeMethod($renderer, 'isVendorFile', ['file' => __FILE__]));
304304
}
305305

306+
public static function dataTraceLinkGenerator(): iterable
307+
{
308+
yield [null, static fn() => null];
309+
yield [
310+
'phpstorm://open?file=test.php&line=42',
311+
static fn(string $file, int|null $line) => "phpstorm://open?file=$file&line=$line",
312+
];
313+
yield [
314+
'phpstorm://open?file=test.php&line=42',
315+
'phpstorm://open?file={file}&line={line}',
316+
];
317+
yield [
318+
'phpstorm://open?file=test.php&line=',
319+
'phpstorm://open?file={file}&line={line}',
320+
'test.php',
321+
null,
322+
];
323+
}
324+
325+
#[DataProvider('dataTraceLinkGenerator')]
326+
public function testTraceLinkGenerator(
327+
string|null $expected,
328+
mixed $traceLink,
329+
string $file = 'test.php',
330+
int|null $line = 42,
331+
): void {
332+
$renderer = new HtmlRenderer(traceLink: $traceLink);
333+
334+
$link = ($renderer->traceLinkGenerator)($file, $line);
335+
336+
$this->assertSame($expected, $link);
337+
}
338+
306339
private function createServerRequestMock(): ServerRequestInterface
307340
{
308341
$serverRequestMock = $this->createMock(ServerRequestInterface::class);

0 commit comments

Comments
 (0)