Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/developers/extensions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ Some ways to extend the guides:
structure
templates
text-roles
interlinks
72 changes: 72 additions & 0 deletions docs/developers/extensions/interlinks.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.. include:: /include.rst.txt

.. _custom_interlink_resolvers:

===================
Interlink Resolvers
===================

Interlinks are external references resolved from inventory files.
The format is inspired by Sphinx intersphinx inventories and lets guides resolve
links like ``project-id:target`` against a configured inventory source.

Implement a custom resolver
===========================

Create a class implementing
:php:class:`phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver`.

The key method is ``resolveInventoryLink()``. It receives the parsed cross-reference
node and should return a :php:class:`phpDocumentor\Guides\ReferenceResolvers\Interlink\ResolvedInventoryLink`
when the target can be resolved, otherwise ``null``.

Register your resolver in DI
============================

Register your service with the tag ``phpdoc.guides.interlink_resolver``.
The chained resolver collects all services with this tag.

.. code-block:: php
:caption: your-extension/resources/config/your-extension.php

<?php

declare(strict_types=1);

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use YourVendor\YourExtension\Interlink\MyInventoryResolver;

return static function (ContainerConfigurator $container): void {
$container->services()
->defaults()
->autowire()
->autoconfigure()
->set(MyInventoryResolver::class)
->tag('phpdoc.guides.interlink_resolver');
};

Your resolver is then considered together with the built-in default repository.

Disable the default repository
==============================

If your extension should fully control interlink resolution, disable the built-in
``DefaultInventoryRepository`` by setting the parameter
``phpdoc.guides.interlink.default_repository.enabled`` to ``false``.

.. code-block:: php
:caption: your-extension/resources/config/your-extension.php

<?php

declare(strict_types=1);

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $container): void {
$container->parameters()
->set('phpdoc.guides.interlink.default_repository.enabled', false);
};

When disabled, the default repository reports no available inventories, so only
custom tagged resolvers participate in interlink resolution.
60 changes: 60 additions & 0 deletions docs/reference/restructuredtext/interlinks.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
.. include:: /include.rst.txt

=========
Interlinks
=========

Interlinks let you reference documentation in other projects.
The feature is inspired by Sphinx intersphinx links: guides reads inventory data
for configured external projects and resolves references by inventory id.

Configure external inventories
==============================

Define one or more inventories in ``guides.xml``:

.. code-block:: xml
:caption: guides.xml

<?xml version="1.0" encoding="UTF-8" ?>
<guides>
<inventory id="t3coreapi" url="https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/"/>
</guides>

The ``id`` is the interlink domain used in references.

Use interlinks in reStructuredText
==================================

Interlinks are used in reference-oriented text roles by prefixing the target with
``<inventory-id>:``.

Examples with ``:ref:``
-----------------------

.. code-block::

:ref:`t3coreapi:assets`
:ref:`Working with assets <t3coreapi:assets>`

Examples with ``:doc:``
-----------------------

.. code-block::

:doc:`t3coreapi:ApiOverview/Assets/Index`
:doc:`Assets chapter <t3coreapi:ApiOverview/Assets/Index>`

Resolution behavior
===================

- The resolver first selects a repository that reports the requested inventory id.
- It then resolves the target in that inventory.
- If no repository provides the inventory id, a warning is logged and the link is not resolved.

See also
========

- :ref:`Text Roles <basic-text-role>`
- :ref:`custom_interlink_resolvers`

2 changes: 2 additions & 0 deletions docs/reference/restructuredtext/text-roles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Text Roles

Text roles can be used to style content inline. Some text roles have advanced processing such as reference resolving.

For external project references using inventory ids, see :doc:`interlinks`.

You can also :ref:`add your own custom text roles <custom_text_roles>`.

Currently the following text roles are implemented:
Expand Down
11 changes: 9 additions & 2 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\EmailReferenceResolver;
use phpDocumentor\Guides\ReferenceResolvers\ExternalReferenceResolver;
use phpDocumentor\Guides\ReferenceResolvers\ImageReferenceResolverPreRender;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\ChainedInventoryLinkResolver;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryLoader;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryRepository;
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLoader;
Expand Down Expand Up @@ -74,7 +75,8 @@

return static function (ContainerConfigurator $container): void {
$container->parameters()
->set('phpdoc.guides.base_template_paths', [__DIR__ . '/../../../guides/resources/template/html']);
->set('phpdoc.guides.base_template_paths', [__DIR__ . '/../../../guides/resources/template/html'])
->set('phpdoc.guides.interlink.default_repository.enabled', true);

$container->services()
->defaults()
Expand Down Expand Up @@ -129,8 +131,13 @@

->set(DocumentNodeTraverser::class)

->set(InventoryRepository::class, DefaultInventoryRepository::class)
->set(DefaultInventoryRepository::class)
->arg('$inventoryConfigs', param('phpdoc.guides.inventories'))
->arg('$enabled', param('phpdoc.guides.interlink.default_repository.enabled'))
->tag('phpdoc.guides.interlink_resolver')

->set(InventoryRepository::class, ChainedInventoryLinkResolver::class)
->arg('$repositories', tagged_iterator('phpdoc.guides.interlink_resolver'))

->set(InventoryLoader::class, DefaultInventoryLoader::class)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?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\Interlink;

use Doctrine\Deprecations\Deprecation;
use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
use phpDocumentor\Guides\ReferenceResolvers\Message;
use phpDocumentor\Guides\ReferenceResolvers\Messages;
use phpDocumentor\Guides\RenderContext;

use function array_key_exists;
use function array_merge;
use function sprintf;

final class ChainedInventoryLinkResolver implements InventoryLinkResolver
{
/** @var array<string, InventoryRepository|null> */
private array $cachedRepositories = [];

/** @param iterable<InventoryRepository> $repositories */
public function __construct(
private readonly iterable $repositories,
) {
}

public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null
{
return $this->resolveInventoryLink($node, $renderContext, $messages)?->getLink();
}

public function hasInventory(string $key): bool
{
return $this->findInventoryRepository($key) !== null;
}

public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
{
Deprecation::trigger(
'phpDocumentor/guides',
'https://github.com/phpDocumentor/guides/issues',
'InventoryRepository::getInventory() is deprecated. Implement '
. 'InventoryLinkResolver::resolveInventoryLink() for one-call interlink resolution.',
);

return $this->findInventoryRepository($node->getInterlinkDomain())?->getInventory($node, $renderContext, $messages);
}

public function resolveInventoryLink(
CrossReferenceNode $node,
RenderContext $renderContext,
Messages $messages,
): ResolvedInventoryLink|null {
$repository = $this->findInventoryRepository($node->getInterlinkDomain());
if ($repository === null) {
$messages->addWarning(
new Message(
sprintf('Inventory with key %s not found. ', $node->getInterlinkDomain()),
array_merge($renderContext->getLoggerInformation(), $node->getDebugInformation()),
),
);

return null;
}

if ($repository instanceof InventoryLinkResolver) {
return $repository->resolveInventoryLink($node, $renderContext, $messages);
}

$inventory = $repository->getInventory($node, $renderContext, $messages);
$link = $repository->getLink($node, $renderContext, $messages);
if ($inventory === null || $link === null) {
return null;
}

return new ResolvedInventoryLink($inventory->getBaseUrl(), $link);
}

private function findInventoryRepository(string $key): InventoryRepository|null
{
if (array_key_exists($key, $this->cachedRepositories)) {
return $this->cachedRepositories[$key];
}

foreach ($this->repositories as $repository) {
if ($repository->hasInventory($key)) {
$this->cachedRepositories[$key] = $repository;

return $repository;
}
}

$this->cachedRepositories[$key] = null;

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace phpDocumentor\Guides\ReferenceResolvers\Interlink;

use Doctrine\Deprecations\Deprecation;
use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer;
use phpDocumentor\Guides\ReferenceResolvers\Message;
Expand All @@ -23,7 +24,7 @@
use function array_merge;
use function sprintf;

final class DefaultInventoryRepository implements InventoryRepository
final class DefaultInventoryRepository implements InventoryLinkResolver
{
/** @var array<string, Inventory> */
private array $inventories = [];
Expand All @@ -33,28 +34,58 @@ public function __construct(
private readonly AnchorNormalizer $anchorNormalizer,
private readonly InventoryLoader $inventoryLoader,
array $inventoryConfigs,
private readonly bool $enabled = true,
) {
foreach ($inventoryConfigs as $inventory) {
$this->inventories[$this->anchorNormalizer->reduceAnchor($inventory['id'])] = new Inventory($inventory['url'], $anchorNormalizer);
$this->inventories[$this->anchorNormalizer->reduceAnchor($inventory['id'])]
= new Inventory($inventory['url'], $anchorNormalizer);
}
}

public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null
{
$inventory = $this->getInventory($node, $renderContext, $messages);
$group = $inventory?->getGroup($node, $renderContext, $messages);

return $group?->getLink($node, $renderContext, $messages);
return $this->resolveInventoryLink($node, $renderContext, $messages)?->getLink();
}

public function hasInventory(string $key): bool
{
if (!$this->enabled) {
return false;
}

$reducedKey = $this->anchorNormalizer->reduceAnchor($key);

return array_key_exists($reducedKey, $this->inventories);
}

public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
{
Deprecation::trigger(
'phpDocumentor/guides',
'https://github.com/phpDocumentor/guides/issues',
'InventoryRepository::getInventory() is deprecated. Implement '
. 'InventoryLinkResolver::resolveInventoryLink() for one-call interlink resolution.',
);

return $this->findInventory($node, $renderContext, $messages);
}

public function resolveInventoryLink(
CrossReferenceNode $node,
RenderContext $renderContext,
Messages $messages,
): ResolvedInventoryLink|null {
$inventory = $this->findInventory($node, $renderContext, $messages);
$group = $inventory?->getGroup($node, $renderContext, $messages);
$link = $group?->getLink($node, $renderContext, $messages);
if ($inventory === null || $link === null) {
return null;
}

return new ResolvedInventoryLink($inventory->getBaseUrl(), $link);
}

private function findInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
{
$reducedKey = $this->anchorNormalizer->reduceAnchor($node->getInterlinkDomain());
if (!$this->hasInventory($reducedKey)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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\Interlink;

use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
use phpDocumentor\Guides\ReferenceResolvers\Messages;
use phpDocumentor\Guides\RenderContext;

interface InventoryLinkResolver extends InventoryRepository
{
public function resolveInventoryLink(
CrossReferenceNode $node,
RenderContext $renderContext,
Messages $messages,
): ResolvedInventoryLink|null;
}
Loading
Loading