Skip to content

Commit fc485cf

Browse files
authored
Merge pull request #1014 from phpDocumentor/backport/1.x/pr-1013
[1.x] [BUGFIX] Fix doc references containing anchors
2 parents 91c9076 + bf4df9f commit fc485cf

File tree

8 files changed

+263
-5
lines changed

8 files changed

+263
-5
lines changed

packages/guides/src/ReferenceResolvers/DocReferenceResolver.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
use phpDocumentor\Guides\RenderContext;
1919
use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface;
2020

21+
use function explode;
2122
use function sprintf;
23+
use function str_contains;
2224

2325
final class DocReferenceResolver implements ReferenceResolver
2426
{
@@ -40,7 +42,15 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess
4042
return false;
4143
}
4244

43-
$canonicalDocumentName = $this->documentNameResolver->canonicalUrl($renderContext->getDirName(), $node->getTargetReference());
45+
$targetReference = $node->getTargetReference();
46+
$anchor = '';
47+
if (str_contains($targetReference, '#')) {
48+
$exploded = explode('#', $targetReference, 2);
49+
$targetReference = $exploded[0];
50+
$anchor = '#' . $exploded[1];
51+
}
52+
53+
$canonicalDocumentName = $this->documentNameResolver->canonicalUrl($renderContext->getDirName(), $targetReference);
4454

4555
$document = $renderContext->getProjectNode()->findDocumentEntry($canonicalDocumentName);
4656
if ($document === null) {
@@ -53,7 +63,7 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess
5363
return false;
5464
}
5565

56-
$node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $document->getFile()));
66+
$node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $document->getFile()) . $anchor);
5767
if ($node->getValue() === '') {
5868
$node->setValue($document->getTitle()->toString());
5969
}

packages/guides/src/ReferenceResolvers/Interlink/InventoryGroup.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@
1414
namespace phpDocumentor\Guides\ReferenceResolvers\Interlink;
1515

1616
use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
17+
use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode;
1718
use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer;
1819
use phpDocumentor\Guides\ReferenceResolvers\Message;
1920
use phpDocumentor\Guides\ReferenceResolvers\Messages;
2021
use phpDocumentor\Guides\RenderContext;
2122

2223
use function array_key_exists;
2324
use function array_merge;
25+
use function explode;
2426
use function sprintf;
27+
use function str_contains;
2528

2629
final class InventoryGroup
2730
{
@@ -47,7 +50,15 @@ public function hasLink(string $key): bool
4750

4851
public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null
4952
{
50-
$reducedKey = $this->anchorNormalizer->reduceAnchor($node->getTargetReference());
53+
$targetReference = $node->getTargetReference();
54+
$anchor = '';
55+
if ($node instanceof DocReferenceNode && str_contains($targetReference, '#')) {
56+
$exploded = explode('#', $targetReference, 2);
57+
$targetReference = $exploded[0];
58+
$anchor = '#' . $exploded[1];
59+
}
60+
61+
$reducedKey = $this->anchorNormalizer->reduceAnchor($targetReference);
5162
if (!array_key_exists($reducedKey, $this->links)) {
5263
$messages->addWarning(
5364
new Message(
@@ -64,6 +75,11 @@ public function getLink(CrossReferenceNode $node, RenderContext $renderContext,
6475
return null;
6576
}
6677

67-
return $this->links[$reducedKey];
78+
$link = $this->links[$reducedKey];
79+
if ($anchor !== '') {
80+
$link = $link->withPath($link->getPath() . $anchor);
81+
}
82+
83+
return $link;
6884
}
6985
}

packages/guides/src/ReferenceResolvers/Interlink/InventoryLink.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ final class InventoryLink
2222
public function __construct(
2323
private readonly string $project,
2424
private readonly string $version,
25-
private readonly string $path,
25+
private string $path,
2626
private readonly string $title,
2727
) {
2828
if (preg_match('/^([a-zA-Z0-9-_.]+\/)*([a-zA-Z0-9-_.])+\.html(#[^#]*)?$/', $path) < 1) {
@@ -49,4 +49,12 @@ public function getTitle(): string
4949
{
5050
return $this->title;
5151
}
52+
53+
public function withPath(string $path): InventoryLink
54+
{
55+
$that = clone$this;
56+
$that->path = $path;
57+
58+
return $that;
59+
}
5260
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link https://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Guides\Interlink;
15+
16+
use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode;
17+
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryGroup;
18+
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLink;
19+
use phpDocumentor\Guides\ReferenceResolvers\Messages;
20+
use phpDocumentor\Guides\ReferenceResolvers\NullAnchorNormalizer;
21+
use phpDocumentor\Guides\RenderContext;
22+
use PHPUnit\Framework\Attributes\DataProvider;
23+
use PHPUnit\Framework\MockObject\MockObject;
24+
use PHPUnit\Framework\TestCase;
25+
26+
final class InventoryGroupTest extends TestCase
27+
{
28+
private InventoryGroup $inventoryGroup;
29+
30+
private RenderContext&MockObject $renderContext;
31+
32+
protected function setUp(): void
33+
{
34+
$this->inventoryGroup = new InventoryGroup(new NullAnchorNormalizer());
35+
$this->renderContext = $this->createMock(RenderContext::class);
36+
}
37+
38+
#[DataProvider('linkProvider')]
39+
public function testGetLinkFromInterlinkGroup(string $expected, string $input, string $path): void
40+
{
41+
$this->inventoryGroup->addLink($path, new InventoryLink('', '', $path . '.html', ''));
42+
$messages = new Messages();
43+
$link = $this->inventoryGroup->getLink(
44+
new DocReferenceNode($input, '', 'interlink'),
45+
$this->renderContext,
46+
$messages,
47+
);
48+
self::assertEmpty($messages->getWarnings());
49+
self::assertEquals($expected, $link?->getPath());
50+
}
51+
52+
/** @return string[][] */
53+
public static function linkProvider(): array
54+
{
55+
return [
56+
'plain' => [
57+
'expected' => 'some-document.html',
58+
'input' => 'some-document',
59+
'path' => 'some-document',
60+
],
61+
'withAnchor' => [
62+
'expected' => 'some-document.html#anchor',
63+
'input' => 'some-document#anchor',
64+
'path' => 'some-document',
65+
],
66+
];
67+
}
68+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link https://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Guides\ReferenceResolvers;
15+
16+
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
17+
use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode;
18+
use phpDocumentor\Guides\Nodes\ProjectNode;
19+
use phpDocumentor\Guides\Nodes\TitleNode;
20+
use phpDocumentor\Guides\RenderContext;
21+
use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface;
22+
use PHPUnit\Framework\Attributes\DataProvider;
23+
use PHPUnit\Framework\MockObject\MockObject;
24+
use PHPUnit\Framework\TestCase;
25+
26+
final class DocReferenceResolverTest extends TestCase
27+
{
28+
private RenderContext&MockObject $renderContext;
29+
private ProjectNode $projectNode;
30+
private MockObject&UrlGeneratorInterface $urlGenerator;
31+
private MockObject&DocumentNameResolverInterface $documentNameResolver;
32+
private DocReferenceResolver $subject;
33+
34+
protected function setUp(): void
35+
{
36+
$documentEntry = new DocumentEntryNode('some-document', TitleNode::emptyNode());
37+
$this->projectNode = new ProjectNode('some-name');
38+
$this->projectNode->addDocumentEntry($documentEntry);
39+
$this->renderContext = $this->createMock(RenderContext::class);
40+
$this->renderContext->expects(self::once())->method('getProjectNode')->willReturn($this->projectNode);
41+
$this->documentNameResolver = self::createMock(DocumentNameResolverInterface::class);
42+
$this->urlGenerator = self::createMock(UrlGeneratorInterface::class);
43+
$this->subject = new DocReferenceResolver($this->urlGenerator, $this->documentNameResolver);
44+
}
45+
46+
#[DataProvider('pathProvider')]
47+
public function testDocumentReducer(string $expected, string $input, string $path): void
48+
{
49+
$this->documentNameResolver->expects(self::once())->method('canonicalUrl')->with('', $path)->willReturn($path);
50+
$input = new DocReferenceNode($input);
51+
$this->urlGenerator->expects(self::once())->method('generateCanonicalOutputUrl')->willReturn($path);
52+
$messages = new Messages();
53+
self::assertTrue($this->subject->resolve($input, $this->renderContext, $messages));
54+
self::assertEmpty($messages->getWarnings());
55+
self::assertEquals($expected, $input->getUrl());
56+
}
57+
58+
/** @return string[][] */
59+
public static function pathProvider(): array
60+
{
61+
return [
62+
'plain' => [
63+
'expected' => 'some-document',
64+
'input' => 'some-document',
65+
'path' => 'some-document',
66+
],
67+
'withAnchor' => [
68+
'expected' => 'some-document#anchor',
69+
'input' => 'some-document#anchor',
70+
'path' => 'some-document',
71+
],
72+
];
73+
}
74+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link https://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Guides\ReferenceResolvers;
15+
16+
use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode;
17+
use phpDocumentor\Guides\ReferenceResolvers\Interlink\Inventory;
18+
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLink;
19+
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryRepository;
20+
use phpDocumentor\Guides\RenderContext;
21+
use PHPUnit\Framework\Attributes\DataProvider;
22+
use PHPUnit\Framework\MockObject\MockObject;
23+
use PHPUnit\Framework\TestCase;
24+
25+
final class InterlinkReferenceResolverTest extends TestCase
26+
{
27+
private RenderContext&MockObject $renderContext;
28+
private MockObject&InventoryRepository $inventoryRepository;
29+
private InterlinkReferenceResolver $subject;
30+
private AnchorNormalizer $anchorNormalizer;
31+
32+
protected function setUp(): void
33+
{
34+
$this->renderContext = $this->createMock(RenderContext::class);
35+
$this->inventoryRepository = $this->createMock(InventoryRepository::class);
36+
$this->anchorNormalizer = new NullAnchorNormalizer();
37+
$this->subject = new InterlinkReferenceResolver($this->inventoryRepository);
38+
}
39+
40+
#[DataProvider('pathProvider')]
41+
public function testDocumentReducer(string $expected, string $input, string $path): void
42+
{
43+
$input = new DocReferenceNode($input, '', 'interlink-target');
44+
$inventoryLink = new InventoryLink('project', '1.0', $path, '');
45+
$inventory = new Inventory('base-url/', $this->anchorNormalizer);
46+
$this->inventoryRepository->expects(self::once())->method('getInventory')->willReturn($inventory);
47+
$this->inventoryRepository->expects(self::once())->method('getLink')->willReturn($inventoryLink);
48+
$messages = new Messages();
49+
self::assertTrue($this->subject->resolve($input, $this->renderContext, $messages));
50+
self::assertEmpty($messages->getWarnings());
51+
self::assertEquals($expected, $input->getUrl());
52+
}
53+
54+
/** @return string[][] */
55+
public static function pathProvider(): array
56+
{
57+
return [
58+
'plain' => [
59+
'expected' => 'base-url/some-document.html',
60+
'input' => 'some-document',
61+
'path' => 'some-document.html',
62+
],
63+
'withAnchor' => [
64+
'expected' => 'base-url/some-document.html#anchor',
65+
'input' => 'some-document#anchor',
66+
'path' => 'some-document.html#anchor',
67+
],
68+
];
69+
}
70+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<!-- content start -->
22
<div class="section" id="root">
33
<h1>Root</h1>
4+
5+
6+
<ul>
7+
<li><a href="/subfolder/index.html">Subfolder</a></li>
8+
<li><a href="/subfolder/index.html#subfolder-index">Subfolder</a></li>
9+
<li><a href="/subfolder/index.html#something">Subfolder</a></li>
10+
</ul>
11+
412
</div>
513
<!-- content end -->
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
====
22
Root
33
====
4+
5+
* :doc:`Subfolder <subfolder/index>`
6+
* :doc:`Subfolder <subfolder/index#subfolder-index>`
7+
* :doc:`Subfolder <subfolder/index#something>`

0 commit comments

Comments
 (0)