Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface;

use function count;
use function str_starts_with;

/**
* Resolves references with an anchor URL.
*
* A link is an anchor if it starts with a hashtag
* Looks up the anchor in the project's internal targets and produces a
* canonical URL. For fragment-only references (starting with #) that don't
* match any known target, falls back to a bare fragment URL.
*/
final class AnchorHyperlinkResolver implements ReferenceResolver
{
Expand All @@ -49,6 +52,12 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess
if ($target === null) {
$target = $renderContext->getProjectNode()->getInternalTarget($reducedAnchor, SectionNode::STD_TITLE);
if ($target === null) {
if (str_starts_with($node->getTargetReference(), '#')) {
$node->setUrl($node->getTargetReference());

return true;
}

return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use function filter_var;
use function parse_url;
use function preg_match;
use function str_starts_with;

use const FILTER_VALIDATE_EMAIL;
use const PHP_URL_SCHEME;
Expand All @@ -39,12 +38,6 @@ final class ExternalReferenceResolver implements ReferenceResolver

public function resolve(LinkInlineNode $node, RenderContext $renderContext, Messages $messages): bool
{
if (str_starts_with($node->getTargetReference(), '#')) {
$node->setUrl($node->getTargetReference());

return true;
}

if (filter_var($node->getTargetReference(), FILTER_VALIDATE_EMAIL)) {
$node->setUrl('mailto:' . $node->getTargetReference());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/

namespace phpDocumentor\Guides\ReferenceResolvers;

use phpDocumentor\Guides\Meta\InternalTarget;
use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use phpDocumentor\Guides\Nodes\ProjectNode;
use phpDocumentor\Guides\Nodes\SectionNode;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\Stub;
use PHPUnit\Framework\TestCase;

final class AnchorHyperlinkResolverTest extends TestCase
{
private AnchorNormalizer&MockObject $anchorReducer;
private RenderContext&MockObject $renderContext;
private Stub&UrlGeneratorInterface $urlGenerator;
private ProjectNode $projectNode;
private AnchorHyperlinkResolver $subject;

protected function setUp(): void
{
$this->anchorReducer = $this->createMock(AnchorNormalizer::class);
$this->renderContext = $this->createMock(RenderContext::class);
$this->urlGenerator = self::createStub(UrlGeneratorInterface::class);
$this->projectNode = new ProjectNode('test');
$this->renderContext->method('getProjectNode')->willReturn($this->projectNode);
$this->subject = new AnchorHyperlinkResolver(
$this->anchorReducer,
$this->urlGenerator,
);
}

public function testFragmentReferenceMatchingSectionReturnsCanonicalUrl(): void
{
$internalTarget = new InternalTarget('index', 'section-one', 'Section One', SectionNode::STD_TITLE);
$this->projectNode->addLinkTarget('section-one', $internalTarget);
$this->anchorReducer->expects(self::once())->method('reduceAnchor')->with('#section-one')->willReturn('section-one');
$this->urlGenerator->method('generateCanonicalOutputUrl')->willReturn('/index.html#section-one');

$node = new HyperLinkNode([new PlainTextInlineNode('Section One')], '#section-one');
$messages = new Messages();
self::assertTrue($this->subject->resolve($node, $this->renderContext, $messages));
self::assertEquals('/index.html#section-one', $node->getUrl());
self::assertEmpty($messages->getWarnings());
}

public function testFragmentReferenceNotMatchingAnySectionFallsBackToBareFragment(): void
{
$this->anchorReducer->expects(self::once())->method('reduceAnchor')->with('#nonexistent')->willReturn('nonexistent');

$node = new HyperLinkNode([new PlainTextInlineNode('Some Link')], '#nonexistent');
$messages = new Messages();
self::assertTrue($this->subject->resolve($node, $this->renderContext, $messages));
self::assertEquals('#nonexistent', $node->getUrl());
self::assertEmpty($messages->getWarnings());
}

public function testBareHashFallsBackToBareFragment(): void
{
$this->anchorReducer->expects(self::once())->method('reduceAnchor')->with('#')->willReturn('');

$node = new HyperLinkNode([new PlainTextInlineNode('Top')], '#');
$messages = new Messages();
self::assertTrue($this->subject->resolve($node, $this->renderContext, $messages));
self::assertEquals('#', $node->getUrl());
self::assertEmpty($messages->getWarnings());
}

public function testNonFragmentReferenceNotMatchingReturnsFalse(): void
{
$this->anchorReducer->expects(self::once())->method('reduceAnchor')->with('some-ref')->willReturn('some-ref');

$node = new HyperLinkNode([new PlainTextInlineNode('Some Link')], 'some-ref');
$messages = new Messages();
self::assertFalse($this->subject->resolve($node, $this->renderContext, $messages));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/

namespace phpDocumentor\Guides\ReferenceResolvers;

use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use phpDocumentor\Guides\RenderContext;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

final class ExternalReferenceResolverTest extends TestCase
{
private RenderContext&MockObject $renderContext;
private ExternalReferenceResolver $subject;

protected function setUp(): void
{
$this->renderContext = $this->createMock(RenderContext::class);
$this->subject = new ExternalReferenceResolver();
}

public function testFragmentOnlyReferenceIsNotResolved(): void
{
$node = new HyperLinkNode([new PlainTextInlineNode('#section-one')], '#section-one');
$messages = new Messages();
self::assertFalse($this->subject->resolve($node, $this->renderContext, $messages));
}

public function testHttpUrlIsResolved(): void
{
$node = new HyperLinkNode([new PlainTextInlineNode('Example')], 'https://example.com');
$messages = new Messages();
self::assertTrue($this->subject->resolve($node, $this->renderContext, $messages));
self::assertEquals('https://example.com', $node->getUrl());
}

public function testEmailIsResolved(): void
{
$node = new HyperLinkNode([new PlainTextInlineNode('Email')], 'user@example.com');
$messages = new Messages();
self::assertTrue($this->subject->resolve($node, $this->renderContext, $messages));
self::assertEquals('mailto:user@example.com', $node->getUrl());
}

public function testUnknownSchemeIsNotResolved(): void
{
$node = new HyperLinkNode([new PlainTextInlineNode('Link')], 'some-page');
$messages = new Messages();
self::assertFalse($this->subject->resolve($node, $this->renderContext, $messages));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!-- content start -->

<div class="section" id="page-with-fragment-links">
<h1>Page with Fragment Links</h1>
<div class="section" id="table-of-contents">
<h2>Table of Contents</h2>


<ul>
<li class="dash"><a href="/index.html#section-one">Section One</a></li>
<li class="dash"><a href="/index.html#section-two">Section Two</a></li>
</ul>

</div>
<div class="section" id="section-one">
<h2>Section One</h2>

<p>Content of section one.</p>

</div>
<div class="section" id="section-two">
<h2>Section Two</h2>

<p>Content of section two.</p>

</div>
</div>
<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<guides xmlns="https://www.phpdoc.org/guides"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.phpdoc.org/guides packages/guides-cli/resources/schema/guides.xsd"
input-format="md"
>
<project title="Project Title" version="6.4"/>
</guides>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Page with Fragment Links

## Table of Contents
- [Section One](#section-one)
- [Section Two](#section-two)

## Section One

Content of section one.

## Section Two

Content of section two.
10 changes: 5 additions & 5 deletions tests/Integration/tests/markdown/readme-md/expected/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ <h2>Table of Contents</h2>


<ul>
<li class="dash"><a href="#installation">Installation</a></li>
<li class="dash"><a href="#usage">Usage</a></li>
<li class="dash"><a href="#safety-guidelines">Safety Guidelines</a></li>
<li class="dash"><a href="#maintenance">Maintenance</a></li>
<li class="dash"><a href="#troubleshooting">Troubleshooting</a></li>
<li class="dash"><a href="/index.html#installation">Installation</a></li>
<li class="dash"><a href="/index.html#usage">Usage</a></li>
<li class="dash"><a href="/index.html#safety-guidelines">Safety Guidelines</a></li>
<li class="dash"><a href="/index.html#maintenance">Maintenance</a></li>
<li class="dash"><a href="/index.html#troubleshooting">Troubleshooting</a></li>
</ul>

<hr />
Expand Down