Skip to content

Commit

Permalink
Merge pull request #747 from phpDocumentor/feature/link-to-page
Browse files Browse the repository at this point in the history
[FEATURE] Support Hyperlinks to local pages in rst and md
  • Loading branch information
jaapio authored Dec 13, 2023
2 parents e130f50 + 69769f7 commit 437edd1
Show file tree
Hide file tree
Showing 22 changed files with 242 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Psr\Log\LoggerInterface;
use RuntimeException;

use function ltrim;
use function md5;
use function sprintf;
use function strtolower;
Expand Down Expand Up @@ -54,7 +55,7 @@ public function parse(ParserContext $parserContext, string $contents): DocumentN

private function parseDocument(NodeWalker $walker, string $hash): DocumentNode
{
$document = new DocumentNode($hash, $this->getParserContext()->getCurrentAbsolutePath());
$document = new DocumentNode($hash, ltrim($this->getParserContext()->getCurrentAbsolutePath(), '/'));
$this->document = $document;

while ($event = $walker->next()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
use Psr\Log\LoggerInterface;

use function assert;
use function is_string;
use function filter_var;
use function str_ends_with;
use function substr;

use const FILTER_VALIDATE_URL;

/** @extends AbstractInlineTextDecoratorParser<HyperLinkNode> */
final class LinkParser extends AbstractInlineTextDecoratorParser
Expand All @@ -32,11 +36,14 @@ protected function getType(): string
protected function createInlineNode(CommonMarkNode $commonMarkNode, string|null $content): InlineNode
{
assert($commonMarkNode instanceof Link);
if (is_string($content)) {
return new HyperLinkNode($content, $commonMarkNode->getUrl());

$content ??= $commonMarkNode->getUrl();
$url = $commonMarkNode->getUrl();
if (str_ends_with($url, '.md') && filter_var($url, FILTER_VALIDATE_URL) === false) {
$url = substr($url, 0, -3);
}

return new HyperLinkNode($commonMarkNode->getUrl(), $commonMarkNode->getUrl());
return new HyperLinkNode($content, $url);
}

protected function supportsCommonMarkNode(CommonMarkNode $commonMarkNode): bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace phpDocumentor\Guides\RestructuredText\Parser\Productions\InlineRules;

use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;

Expand All @@ -25,7 +25,7 @@ public function applies(InlineLexer $lexer): bool
return $lexer->token?->type === InlineLexer::BACKTICK;
}

public function apply(BlockContext $blockContext, InlineLexer $lexer): HyperLinkNode|null
public function apply(BlockContext $blockContext, InlineLexer $lexer): AbstractLinkInlineNode|null
{
$text = '';
$embeddedUrl = null;
Expand Down Expand Up @@ -64,7 +64,7 @@ public function apply(BlockContext $blockContext, InlineLexer $lexer): HyperLink
return null;
}

private function createAnonymousReference(BlockContext $blockContext, string $link, string|null $embeddedUrl): HyperLinkNode
private function createAnonymousReference(BlockContext $blockContext, string $link, string|null $embeddedUrl): AbstractLinkInlineNode
{
$node = $this->createReference($blockContext, $link, $embeddedUrl, false);
$blockContext->getDocumentParserContext()->pushAnonymous($link);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace phpDocumentor\Guides\RestructuredText\Parser\Productions\InlineRules;

use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;

Expand All @@ -26,7 +26,7 @@ public function applies(InlineLexer $lexer): bool
return $lexer->token?->type === InlineLexer::ANONYMOUSE_REFERENCE;
}

public function apply(BlockContext $blockContext, InlineLexer $lexer): HyperLinkNode|null
public function apply(BlockContext $blockContext, InlineLexer $lexer): AbstractLinkInlineNode|null
{
$node = $this->createAnonymousReference(
$blockContext,
Expand All @@ -37,7 +37,7 @@ public function apply(BlockContext $blockContext, InlineLexer $lexer): HyperLink
return $node;
}

private function createAnonymousReference(BlockContext $blockContext, string $link): HyperLinkNode
private function createAnonymousReference(BlockContext $blockContext, string $link): AbstractLinkInlineNode
{
$node = $this->createReference($blockContext, $link, null, false);
$blockContext->getDocumentParserContext()->pushAnonymous($link);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,42 @@

namespace phpDocumentor\Guides\RestructuredText\Parser\Productions\InlineRules;

use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode;
use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;

use function filter_var;
use function preg_replace;
use function str_ends_with;
use function str_replace;
use function substr;
use function trim;

use const FILTER_VALIDATE_URL;

abstract class ReferenceRule extends AbstractInlineRule
{
protected function createReference(BlockContext $blockContext, string $link, string|null $embeddedUrl = null, bool $registerLink = true): HyperLinkNode
protected function createReference(BlockContext $blockContext, string $link, string|null $embeddedUrl = null, bool $registerLink = true): AbstractLinkInlineNode
{
// the link may have a new line in it, so we need to strip it
// before setting the link and adding a token to be replaced
$link = str_replace("\n", ' ', $link);
$link = trim(preg_replace('/\s+/', ' ', $link) ?? '');

$targetLink = $embeddedUrl ?? $link;
if (str_ends_with($targetLink, '.rst') && filter_var($targetLink, FILTER_VALIDATE_URL) === false) {
$targetLink = substr($targetLink, 0, -4);

return new DocReferenceNode($targetLink, $link);
}

if ($registerLink && $embeddedUrl !== null) {
$blockContext->getDocumentParserContext()->setLink($link, $embeddedUrl);
}

return new HyperLinkNode($link, $embeddedUrl ?? $link);
return new HyperLinkNode($link, $targetLink);
}

protected function parseEmbeddedUrl(InlineLexer $lexer): string|null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace phpDocumentor\Guides\RestructuredText\Parser\Productions\InlineRules;

use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;

Expand All @@ -24,7 +24,7 @@ public function applies(InlineLexer $lexer): bool
return $lexer->token?->type === InlineLexer::EMAIL;
}

public function apply(BlockContext $blockContext, InlineLexer $lexer): HyperLinkNode|null
public function apply(BlockContext $blockContext, InlineLexer $lexer): AbstractLinkInlineNode|null
{
$value = $lexer->token?->value ?? '';
$node = $this->createReference($blockContext, $value, $value, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace phpDocumentor\Guides\RestructuredText\Parser\Productions\InlineRules;

use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;

Expand All @@ -24,7 +24,7 @@ public function applies(InlineLexer $lexer): bool
return $lexer->token?->type === InlineLexer::HYPERLINK;
}

public function apply(BlockContext $blockContext, InlineLexer $lexer): HyperLinkNode|null
public function apply(BlockContext $blockContext, InlineLexer $lexer): AbstractLinkInlineNode|null
{
$value = $lexer->token?->value ?? '';
$node = $this->createReference($blockContext, $value, $value, false);
Expand Down
3 changes: 3 additions & 0 deletions packages/guides/resources/config/guides.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use phpDocumentor\Guides\ReferenceResolvers\ExternalReferenceResolver;
use phpDocumentor\Guides\ReferenceResolvers\InterlinkReferenceResolver;
use phpDocumentor\Guides\ReferenceResolvers\InternalReferenceResolver;
use phpDocumentor\Guides\ReferenceResolvers\PageHyperlinkResolver;
use phpDocumentor\Guides\ReferenceResolvers\ReferenceResolver;
use phpDocumentor\Guides\ReferenceResolvers\ReferenceResolverPreRender;
use phpDocumentor\Guides\ReferenceResolvers\SluggerAnchorReducer;
Expand Down Expand Up @@ -124,6 +125,8 @@

->set(AnchorHyperlinkResolver::class)

->set(PageHyperlinkResolver::class)

->set(AnchorReferenceResolver::class)

->set(InternalReferenceResolver::class)
Expand Down
60 changes: 60 additions & 0 deletions packages/guides/src/ReferenceResolvers/PageHyperlinkResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace phpDocumentor\Guides\ReferenceResolvers;

use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
use phpDocumentor\Guides\Nodes\Inline\LinkInlineNode;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface;

use function str_ends_with;
use function strlen;
use function substr;

/**
* Resolves named and anonymous references to source files
*
* `Link Text <../page.rst>`_ or [Link Text](path/to/another/page.md)
*/
class PageHyperlinkResolver implements ReferenceResolver
{
// Named links and anchors take precedence
public final const PRIORITY = -200;

public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly DocumentNameResolverInterface $documentNameResolver,
) {
}

public function resolve(LinkInlineNode $node, RenderContext $renderContext): bool
{
if (!$node instanceof HyperLinkNode) {
return false;
}

$canonicalDocumentName = $this->documentNameResolver->canonicalUrl($renderContext->getDirName(), $node->getTargetReference());
if (str_ends_with($canonicalDocumentName, '.' . $renderContext->getOutputFormat())) {
$canonicalDocumentName = substr($canonicalDocumentName, 0, 0 - strlen('.' . $renderContext->getOutputFormat()));
}

$document = $renderContext->getProjectNode()->findDocumentEntry($canonicalDocumentName);
if ($document === null) {
return false;
}

$node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $document->getFile()));
if ($node->getValue() === '') {
$node->setValue($document->getTitle()->toString());
}

return true;
}

public static function getPriority(): int
{
return self::PRIORITY;
}
}
16 changes: 16 additions & 0 deletions tests/Integration/tests/hyperlink-to-page/expected/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- content start -->
<div class="section" id="some-file">
<h1>Some file</h1>



<ul>
<li><a href="/page1.html">link page 1</a></li>

<li><a href="/subpages/subpage1.html">link subpage 1</a></li>

</ul>

</div>

<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!-- content start -->
<div class="section" id="some-file">
<h1>Some file</h1>

<a id="page1"></a>


<ul>
<li><a href="/page1.html">link page 1</a></li>

<li><a href="/subpages/subpage1.html">link subpage 1</a></li>

<li><a href="/page1.html">link page 1</a></li>

<li><a href="/page1.html">link page 1</a></li>

<li><a href="/subpages/subpage1.html">link subpage 1</a></li>

<li><a href="/page1.html">link page 1</a></li>

<li><a href="/subpages/index.html#page1">link page 1</a></li>

<li><a href="/subpages/subpage1.html">link subpage 1</a></li>

<li><a href="/subpages/index.html#page1">link page 1</a></li>

</ul>

</div>

<!-- content end -->
6 changes: 6 additions & 0 deletions tests/Integration/tests/hyperlink-to-page/input/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Some file
=========

* `link page 1 <page1.rst>`_

* `link subpage 1 <subpages/subpage1.rst>`_
2 changes: 2 additions & 0 deletions tests/Integration/tests/hyperlink-to-page/input/page1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Page 1
======
17 changes: 17 additions & 0 deletions tests/Integration/tests/hyperlink-to-page/input/subpages/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.. _page1:

Some file
=========

* `link page 1 </page1.rst>`_
* `link subpage 1 <subpage1.rst>`_
* `link page 1 <../page1.rst>`_


* `link page 1 </page1.html>`_
* `link subpage 1 <subpage1.html>`_
* `link page 1 <../page1.html>`_

* `link page 1 </page1>`_
* `link subpage 1 <subpage1>`_
* `link page 1 <../page1>`_
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Subpage 1
=========
16 changes: 16 additions & 0 deletions tests/Integration/tests/markdown/link-page-md/expected/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- content start -->
<h1>Markdown with links</h1>

<p>This is a Markdown document with some basic formatting.</p>


<ul>
<li><p><a href="/page1.html">Page 1</a></p></li>

<li><p><a href="/page1.html">Page 1</a></p></li>

<li><p><a href="/subpages/subpage1.html">Subpage 1</a></p></li>

</ul>

<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- content start -->
<h1>Markdown with links</h1>

<p>This is a Markdown document with some basic formatting.</p>


<ul>
<li><p><a href="/subpages/subpage1.html">Subpage 1</a></p></li>

<li><p><a href="/page1.html">Page 1</a></p></li>

<li><p><a href="/page1.html">Page 1</a></p></li>

<li><p><a href="/page1.html">Page 1</a></p></li>

<li><p><a href="/page1.html">Page 1</a></p></li>

</ul>

<!-- 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>
7 changes: 7 additions & 0 deletions tests/Integration/tests/markdown/link-page-md/input/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Markdown with links

This is a Markdown document with some basic formatting.

* [Page 1](page1.md)
* [Page 1](/page1.md)
* [Subpage 1](subpages/subpage1.md)
3 changes: 3 additions & 0 deletions tests/Integration/tests/markdown/link-page-md/input/page1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Page 1

This is a Markdown document with some basic formatting.
Loading

0 comments on commit 437edd1

Please sign in to comment.