66
77use Alexkart \CurlBuilder \Command ;
88use cebe \markdown \GithubMarkdown ;
9+ use Closure ;
910use Psr \Http \Message \ServerRequestInterface ;
1011use RuntimeException ;
1112use Throwable ;
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 */
5355final 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}
0 commit comments