diff --git a/CHANGELOG.md b/CHANGELOG.md index 66d8b5b..59cf815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 5.0.0 under development -- Enh #104: Render exception class PHPDoc description with safe markdown links in HTML debug output (@dbuhonov) +- Enh #104, #168: Render exception class PHPDoc description with safe markdown links in HTML debug output (@dbuhonov, @vjik) - Chg #162: Replace deprecated `ThrowableResponseFactory` class usage to new one, and remove it (@vjik) - Enh #163: Explicitly import classes, functions, and constants in "use" section (@mspirkov) - Bug #164: Fix missing items in stack trace HTML output when handling a PHP error (@vjik) diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index 1d2d531..b2e9b09 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -212,18 +212,16 @@ public function render(Throwable $t, ?ServerRequestInterface $request = null): E public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { - $solution = null; - $exceptionDescription = null; $displayThrowable = $t; - if ($t instanceof CompositeException) { $displayThrowable = $t->getFirstException(); } - if ($displayThrowable instanceof FriendlyExceptionInterface) { - $solution = $displayThrowable->getSolution(); - } else { - $exceptionDescription = $this->getThrowableDescription($displayThrowable); + $exceptionDescription = $displayThrowable instanceof FriendlyExceptionInterface + ? $displayThrowable->getSolution() + : $this->getThrowableDescription($displayThrowable); + if ($exceptionDescription !== null) { + $exceptionDescription = $this->parseMarkdown($exceptionDescription); } return new ErrorData( @@ -231,7 +229,6 @@ public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = n 'request' => $request, 'throwable' => $t, 'displayThrowable' => $displayThrowable, - 'solution' => $solution, 'exceptionClass' => $displayThrowable::class, 'exceptionMessage' => $displayThrowable->getMessage(), 'exceptionDescription' => $exceptionDescription, @@ -576,11 +573,8 @@ public function removeAnonymous(string $value): string * suitable for direct inclusion in the error template. * Inline {@see ...}/{@link ...} annotations are rendered as markdown links. * - * The returned value is an HTML snippet (for example, containing

, , - * elements) and is intended to be inserted into the template as-is, - * without additional HTML-escaping. - * - * @return string|null HTML fragment describing the throwable, or null if no description is available. + * @return string|null Markdown string with inline HTML (`` elements) describing the throwable, or `null` if + * no description is available. */ private function getThrowableDescription(Throwable $throwable): ?string { @@ -668,9 +662,7 @@ static function (array $matches): string { $normalized[] = $imageMarker . $label . ' (' . $this->htmlEncode($target) . ')'; } - $normalized = trim(implode('', $normalized)); - - return $this->parseMarkdown($normalized); + return trim(implode('', $normalized)); } /** diff --git a/templates/development.css b/templates/development.css index 04bc91a..957e852 100644 --- a/templates/development.css +++ b/templates/development.css @@ -144,7 +144,7 @@ header { --previous-arrow-color: #e51717; } -header .solution { +header .exception-description { --text-color: var(--page-text-color); --link-color: #00617b; --link-hover-color: #1191b3; @@ -257,115 +257,115 @@ header .exception-message { color: var(--exception-message-text-color); } -header .solution { +header .exception-description { margin-top: 24px; font-size: 16px; line-height: 22px; color: var(--text-color); } -header .solution h1 { +header .exception-description h1 { margin-top: 24px; font-size: 26px; line-height: 32px; font-weight: bold; } -header .solution h2 { +header .exception-description h2 { margin-top: 24px; font-size: 22px; line-height: 28px; font-weight: bold; } -header .solution h3 { +header .exception-description h3 { margin-top: 24px; font-size: 20px; line-height: 26px; font-weight: bold; } -header .solution h4 { +header .exception-description h4 { margin-top: 24px; font-size: 18px; line-height: 24px; font-weight: bold; } -header .solution h5 { +header .exception-description h5 { margin-top: 24px; font-size: 16px; line-height: 22px; font-weight: bold; } -header .solution h6 { +header .exception-description h6 { margin-top: 24px; font-size: 14px; line-height: 20px; font-weight: bold; } -header .solution p { +header .exception-description p { margin-top: 16px; } -header .solution a { +header .exception-description a { color: var(--link-color); text-decoration: underline; } -header .solution a:hover { +header .exception-description a:hover { color: var(--link-hover-color); } -header .solution h1:first-child, -header .solution h2:first-child, -header .solution h3:first-child, -header .solution h4:first-child, -header .solution h5:first-child, -header .solution h6:first-child, -header .solution p:first-child { +header .exception-description h1:first-child, +header .exception-description h2:first-child, +header .exception-description h3:first-child, +header .exception-description h4:first-child, +header .exception-description h5:first-child, +header .exception-description h6:first-child, +header .exception-description p:first-child { margin-top: 0; } -header .solution blockquote { +header .exception-description blockquote { margin: 18px 0 18px 4px; padding: 3px 0 2px 16px; border-left: 4px solid var(--blockquote-border-color); color: var(--blockquote-text-color); } -header .solution ul, -header .solution ol { +header .exception-description ul, +header .exception-description ol { padding: 0; margin: 16px 0 0 32px; } -header .solution li ul, -header .solution li ol { +header .exception-description li ul, +header .exception-description li ol { margin: 0 0 0 24px; } -header .solution li { +header .exception-description li { margin: 8px 0 0 0; } -header .solution ul { +header .exception-description ul { list-style: outside; } -header .solution pre, -header .solution code { +header .exception-description pre, +header .exception-description code { font-family: monospace; } -header .solution code { +header .exception-description code { padding: 2px 6px; font-size: 90%; background-color: var(--code-bg-color); border-radius: 6px; } -header .solution pre { +header .exception-description pre { margin: 24px 0; width: 100%; box-sizing: border-box; @@ -374,23 +374,23 @@ header .solution pre { border-radius: 8px; background: var(--pre-bg-color); } -header .solution pre code { +header .exception-description pre code { font-size: 100%; padding: 0; width: max-content; } -header .solution table { +header .exception-description table { margin: 16px 0 0 0; border-collapse: collapse; } -header .solution td, -header .solution th { +header .exception-description td, +header .exception-description th { padding: 6px 12px; border: 1px solid var(--table-border-color); } -header .solution HR { +header .exception-description HR { margin: 24px 0; border: 1px solid var(--separator-color); border-width: 1px 0 0 0; @@ -836,7 +836,7 @@ main { --exception-message-text-color: rgba(255, 255, 255, 0.8); } -.dark-theme header .solution { +.dark-theme header .exception-description { --text-color: rgba(255, 255, 255, 0.8); --link-color: #03a9f4; --link-hover-color: #39b9f3; @@ -918,7 +918,7 @@ main { --exception-message-text-color: rgba(255, 255, 255, 0.8); } - body:not(.light-theme) header .solution { + body:not(.light-theme) header .exception-description { --text-color: rgba(255, 255, 255, 0.8); --link-color: #03a9f4; --link-hover-color: #39b9f3; diff --git a/templates/development.php b/templates/development.php index 73b316f..e446977 100644 --- a/templates/development.php +++ b/templates/development.php @@ -9,7 +9,6 @@ * @var ServerRequestInterface|null $request * @var Throwable $throwable * @var Throwable $displayThrowable - * @var string|null $solution * @var string $exceptionClass * @var string $exceptionMessage * @var string|null $exceptionDescription @@ -90,11 +89,7 @@ -

- - - -
parseMarkdown($solution) ?>
+
renderPreviousExceptions($throwable) ?> @@ -206,7 +201,7 @@ class="copy-clipboard" const DARK_THEME = 'dark-theme'; window.onload = function() { - const codeBlocks = document.querySelectorAll('.solution pre code,.codeBlock'); + const codeBlocks = document.querySelectorAll('.exception-description pre code,.codeBlock'); const callStackItems = document.getElementsByClassName('call-stack-item'); // If there are grouped vendor package files diff --git a/tests/Renderer/HtmlRendererTest.php b/tests/Renderer/HtmlRendererTest.php index 7d0b563..430f3b6 100644 --- a/tests/Renderer/HtmlRendererTest.php +++ b/tests/Renderer/HtmlRendererTest.php @@ -19,7 +19,6 @@ use Yiisoft\ErrorHandler\Tests\Support\TestDocBlockException; use Yiisoft\ErrorHandler\Tests\Support\TestEmptyDescriptionDocBlockException; use Yiisoft\ErrorHandler\Tests\Support\TestExceptionWithoutDocBlock; -use Yiisoft\ErrorHandler\Tests\Support\TestFriendlyException; use Yiisoft\ErrorHandler\Tests\Support\TestHelper; use Yiisoft\ErrorHandler\Tests\Support\TestInlineCodeDocBlockException; use Yiisoft\ErrorHandler\Tests\Support\TestLeadingMarkdownLinkDocBlockException; @@ -86,7 +85,7 @@ public function testVerboseOutputRendersThrowableDescriptionFromDocComment(): vo $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); $result = (string) $errorData; - $this->assertStringContainsString('
', $result); + $this->assertStringContainsString('
', $result); $this->assertStringContainsString('Test summary with RuntimeException.', $result); $this->assertStringContainsString( 'Yii Framework', @@ -101,7 +100,7 @@ public function testVerboseOutputDoesNotRenderThrowableDescriptionWhenNoDocComme $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); - $this->assertStringNotContainsString('
', (string) $errorData); + $this->assertStringNotContainsString('
', (string) $errorData); } public function testVerboseOutputDoesNotRenderThrowableDescriptionWhenDocCommentHasNoDescription(): void @@ -111,19 +110,7 @@ public function testVerboseOutputDoesNotRenderThrowableDescriptionWhenDocComment $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); - $this->assertStringNotContainsString('
', (string) $errorData); - } - - public function testVerboseOutputKeepsFriendlyExceptionBehaviorWithoutDescriptionDuplication(): void - { - $renderer = new HtmlRenderer(); - $exception = new TestFriendlyException('exception-test-message'); - - $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); - $result = (string) $errorData; - - $this->assertStringContainsString('
', $result); - $this->assertStringNotContainsString('
', $result); + $this->assertStringNotContainsString('
', (string) $errorData); } public function testVerboseOutputUsesFirstExceptionFromCompositeException(): void @@ -148,7 +135,7 @@ public function testVerboseOutputEscapesUnsafeThrowableDescriptionLinks(): void $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); $result = (string) $errorData; - preg_match('/
(.*?)<\/div>/s', $result, $matches); + preg_match('/
(.*?)<\/div>/s', $result, $matches); $description = $matches[1] ?? ''; $this->assertStringNotContainsString('href="javascript:alert(1)"', $result); @@ -172,7 +159,7 @@ public function testVerboseOutputEscapesUnsafeThrowableDescriptionMarkdownPayloa $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); $result = (string) $errorData; - preg_match('/
(.*?)<\/div>/s', $result, $matches); + preg_match('/
(.*?)<\/div>/s', $result, $matches); $description = $matches[1] ?? ''; $this->assertNotSame('', $description); @@ -199,7 +186,7 @@ public function testVerboseOutputEscapesNonHttpSchemesInThrowableDescriptionMark $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); $result = (string) $errorData; - preg_match('/
(.*?)<\/div>/s', $result, $matches); + preg_match('/
(.*?)<\/div>/s', $result, $matches); $description = $matches[1] ?? ''; $this->assertNotSame('', $description); @@ -221,7 +208,7 @@ public function testVerboseOutputEscapesOwaspFilterEvasionThrowableDescriptionPa $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); $result = (string) $errorData; - preg_match('/
(.*?)<\/div>/s', $result, $matches); + preg_match('/
(.*?)<\/div>/s', $result, $matches); $description = $matches[1] ?? ''; $this->assertNotSame('', $description); @@ -272,7 +259,7 @@ public function testVerboseOutputDoesNotDoubleEncodeSafeThrowableDescriptionLink $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); $result = (string) $errorData; - preg_match('/
(.*?)<\/div>/s', $result, $matches); + preg_match('/
(.*?)<\/div>/s', $result, $matches); $description = $matches[1] ?? ''; $this->assertStringContainsString( @@ -292,7 +279,7 @@ public function testVerboseOutputRendersThrowableDescriptionStartingWithMarkdown $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); $result = (string) $errorData; - preg_match('/
(.*?)<\/div>/s', $result, $matches); + preg_match('/
(.*?)<\/div>/s', $result, $matches); $description = $matches[1] ?? ''; $this->assertNotSame('', $description); @@ -309,7 +296,7 @@ public function testVerboseOutputRendersThrowableDescriptionLinksWithParentheses $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); $result = (string) $errorData; - preg_match('/
(.*?)<\/div>/s', $result, $matches); + preg_match('/
(.*?)<\/div>/s', $result, $matches); $description = $matches[1] ?? ''; $this->assertNotSame('', $description);