diff --git a/packages/guides-markdown/src/Markdown/Parsers/InlineParsers/LinkParser.php b/packages/guides-markdown/src/Markdown/Parsers/InlineParsers/LinkParser.php index 46a8132f5..97f1e7d33 100644 --- a/packages/guides-markdown/src/Markdown/Parsers/InlineParsers/LinkParser.php +++ b/packages/guides-markdown/src/Markdown/Parsers/InlineParsers/LinkParser.php @@ -22,7 +22,9 @@ use Psr\Log\LoggerInterface; use function assert; +use function explode; use function filter_var; +use function str_contains; use function str_ends_with; use function substr; @@ -49,11 +51,20 @@ protected function createInlineNode(CommonMarkNode $commonMarkNode, string|null { assert($commonMarkNode instanceof Link); - $url = $commonMarkNode->getUrl(); + $url = $commonMarkNode->getUrl(); + $anchor = ''; + if (str_contains($url, '#')) { + $exploded = explode('#', $url, 2); + $url = $exploded[0]; + $anchor = '#' . $exploded[1]; + } + if (str_ends_with($url, '.md') && filter_var($url, FILTER_VALIDATE_URL) === false) { $url = substr($url, 0, -3); } + $url .= $anchor; + return new HyperLinkNode($content ? [new PlainTextInlineNode($content)] : $children, $url); } diff --git a/packages/guides/src/ReferenceResolvers/PageHyperlinkResolver.php b/packages/guides/src/ReferenceResolvers/PageHyperlinkResolver.php index 2bdc5a4a8..633823fb0 100644 --- a/packages/guides/src/ReferenceResolvers/PageHyperlinkResolver.php +++ b/packages/guides/src/ReferenceResolvers/PageHyperlinkResolver.php @@ -20,6 +20,8 @@ use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface; use function count; +use function explode; +use function str_contains; use function str_ends_with; use function strlen; use function substr; @@ -46,7 +48,15 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess return false; } - $canonicalDocumentName = $this->documentNameResolver->canonicalUrl($renderContext->getDirName(), $node->getTargetReference()); + $targetReference = $node->getTargetReference(); + $anchor = ''; + if (str_contains($targetReference, '#')) { + $exploded = explode('#', $targetReference, 2); + $targetReference = $exploded[0]; + $anchor = '#' . $exploded[1]; + } + + $canonicalDocumentName = $this->documentNameResolver->canonicalUrl($renderContext->getDirName(), $targetReference); if (str_ends_with($canonicalDocumentName, '.' . $renderContext->getOutputFormat())) { $canonicalDocumentName = substr($canonicalDocumentName, 0, 0 - strlen('.' . $renderContext->getOutputFormat())); } @@ -56,7 +66,7 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess return false; } - $node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $document->getFile())); + $node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $document->getFile()) . $anchor); if (count($node->getChildren()) === 0) { $node->addChildNode(new PlainTextInlineNode($document->getTitle()->toString())); } diff --git a/packages/guides/tests/unit/ReferenceResolvers/PageHyperlinkResolverTest.php b/packages/guides/tests/unit/ReferenceResolvers/PageHyperlinkResolverTest.php new file mode 100644 index 000000000..ea2ec3ed0 --- /dev/null +++ b/packages/guides/tests/unit/ReferenceResolvers/PageHyperlinkResolverTest.php @@ -0,0 +1,85 @@ +projectNode = new ProjectNode('some-name'); + $this->projectNode->addDocumentEntry($documentEntry); + $this->renderContext = $this->createMock(RenderContext::class); + $this->renderContext->expects(self::any())->method('getProjectNode')->willReturn($this->projectNode); + $this->renderContext->method('getDirName')->willReturn(''); + $this->renderContext->method('getOutputFormat')->willReturn('html'); + $this->documentNameResolver = self::createMock(DocumentNameResolverInterface::class); + $this->urlGenerator = self::createMock(UrlGeneratorInterface::class); + $this->subject = new PageHyperlinkResolver($this->urlGenerator, $this->documentNameResolver); + } + + #[DataProvider('pathProvider')] + public function testPageHyperlinkResolver(string $expected, string $input, string $path): void + { + $this->documentNameResolver->expects(self::once())->method('canonicalUrl')->with('', $path)->willReturn($path); + $node = new HyperLinkNode([], $input); + $this->urlGenerator->expects(self::once())->method('generateCanonicalOutputUrl')->willReturn($path); + $messages = new Messages(); + self::assertTrue($this->subject->resolve($node, $this->renderContext, $messages)); + self::assertEmpty($messages->getWarnings()); + self::assertEquals($expected, $node->getUrl()); + } + + public function testDocumentNotFound(): void + { + $this->documentNameResolver->expects(self::once())->method('canonicalUrl')->with('', 'nonexistent-page')->willReturn('nonexistent-page'); + $node = new HyperLinkNode([], 'nonexistent-page#anchor'); + $messages = new Messages(); + self::assertFalse($this->subject->resolve($node, $this->renderContext, $messages)); + self::assertEquals('', $node->getUrl()); + } + + /** @return string[][] */ + public static function pathProvider(): array + { + return [ + 'plain' => [ + 'expected' => 'some-document', + 'input' => 'some-document', + 'path' => 'some-document', + ], + 'withAnchor' => [ + 'expected' => 'some-document#anchor', + 'input' => 'some-document#anchor', + 'path' => 'some-document', + ], + ]; + } +} diff --git a/tests/Integration/tests/markdown/link-page-fragment-md/expected/index.html b/tests/Integration/tests/markdown/link-page-fragment-md/expected/index.html new file mode 100644 index 000000000..210a7201f --- /dev/null +++ b/tests/Integration/tests/markdown/link-page-fragment-md/expected/index.html @@ -0,0 +1,12 @@ + + +
+

Page A

+ +

See Section Two on Page B.

+ + +

See Page B without fragment.

+ +
+ diff --git a/tests/Integration/tests/markdown/link-page-fragment-md/input/guides.xml b/tests/Integration/tests/markdown/link-page-fragment-md/input/guides.xml new file mode 100644 index 000000000..cb7023e46 --- /dev/null +++ b/tests/Integration/tests/markdown/link-page-fragment-md/input/guides.xml @@ -0,0 +1,8 @@ + + + + diff --git a/tests/Integration/tests/markdown/link-page-fragment-md/input/index.md b/tests/Integration/tests/markdown/link-page-fragment-md/input/index.md new file mode 100644 index 000000000..dc57e660f --- /dev/null +++ b/tests/Integration/tests/markdown/link-page-fragment-md/input/index.md @@ -0,0 +1,5 @@ +# Page A + +See [Section Two on Page B](page-b.md#section-two). + +See [Page B without fragment](page-b.md). diff --git a/tests/Integration/tests/markdown/link-page-fragment-md/input/page-b.md b/tests/Integration/tests/markdown/link-page-fragment-md/input/page-b.md new file mode 100644 index 000000000..2dd067df4 --- /dev/null +++ b/tests/Integration/tests/markdown/link-page-fragment-md/input/page-b.md @@ -0,0 +1,5 @@ +# Page B + +## Section Two + +Content here.