Skip to content

Commit 28bd83f

Browse files
authored
Merge pull request #1326 from phpDocumentor/feature/improved-interlink
Improve interlink architecture
2 parents a48668a + e8073c4 commit 28bd83f

14 files changed

Lines changed: 562 additions & 31 deletions

File tree

docs/developers/extensions/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ Some ways to extend the guides:
3636
structure
3737
templates
3838
text-roles
39+
interlinks
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
.. include:: /include.rst.txt
2+
3+
.. _custom_interlink_resolvers:
4+
5+
===================
6+
Interlink Resolvers
7+
===================
8+
9+
Interlinks are external references resolved from inventory files.
10+
The format is inspired by Sphinx intersphinx inventories and lets guides resolve
11+
links like ``project-id:target`` against a configured inventory source.
12+
13+
Implement a custom resolver
14+
===========================
15+
16+
Create a class implementing
17+
:php:class:`phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLinkResolver`.
18+
19+
The key method is ``resolveInventoryLink()``. It receives the parsed cross-reference
20+
node and should return a :php:class:`phpDocumentor\Guides\ReferenceResolvers\Interlink\ResolvedInventoryLink`
21+
when the target can be resolved, otherwise ``null``.
22+
23+
Register your resolver in DI
24+
============================
25+
26+
Register your service with the tag ``phpdoc.guides.interlink_resolver``.
27+
The chained resolver collects all services with this tag.
28+
29+
.. code-block:: php
30+
:caption: your-extension/resources/config/your-extension.php
31+
32+
<?php
33+
34+
declare(strict_types=1);
35+
36+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
37+
use YourVendor\YourExtension\Interlink\MyInventoryResolver;
38+
39+
return static function (ContainerConfigurator $container): void {
40+
$container->services()
41+
->defaults()
42+
->autowire()
43+
->autoconfigure()
44+
->set(MyInventoryResolver::class)
45+
->tag('phpdoc.guides.interlink_resolver');
46+
};
47+
48+
Your resolver is then considered together with the built-in default repository.
49+
50+
Disable the default repository
51+
==============================
52+
53+
If your extension should fully control interlink resolution, disable the built-in
54+
``DefaultInventoryRepository`` by setting the parameter
55+
``phpdoc.guides.interlink.default_repository.enabled`` to ``false``.
56+
57+
.. code-block:: php
58+
:caption: your-extension/resources/config/your-extension.php
59+
60+
<?php
61+
62+
declare(strict_types=1);
63+
64+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
65+
66+
return static function (ContainerConfigurator $container): void {
67+
$container->parameters()
68+
->set('phpdoc.guides.interlink.default_repository.enabled', false);
69+
};
70+
71+
When disabled, the default repository reports no available inventories, so only
72+
custom tagged resolvers participate in interlink resolution.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.. include:: /include.rst.txt
2+
3+
=========
4+
Interlinks
5+
=========
6+
7+
Interlinks let you reference documentation in other projects.
8+
The feature is inspired by Sphinx intersphinx links: guides reads inventory data
9+
for configured external projects and resolves references by inventory id.
10+
11+
Configure external inventories
12+
==============================
13+
14+
Define one or more inventories in ``guides.xml``:
15+
16+
.. code-block:: xml
17+
:caption: guides.xml
18+
19+
<?xml version="1.0" encoding="UTF-8" ?>
20+
<guides>
21+
<inventory id="t3coreapi" url="https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/"/>
22+
</guides>
23+
24+
The ``id`` is the interlink domain used in references.
25+
26+
Use interlinks in reStructuredText
27+
==================================
28+
29+
Interlinks are used in reference-oriented text roles by prefixing the target with
30+
``<inventory-id>:``.
31+
32+
Examples with ``:ref:``
33+
-----------------------
34+
35+
.. code-block::
36+
37+
:ref:`t3coreapi:assets`
38+
:ref:`Working with assets <t3coreapi:assets>`
39+
40+
Examples with ``:doc:``
41+
-----------------------
42+
43+
.. code-block::
44+
45+
:doc:`t3coreapi:ApiOverview/Assets/Index`
46+
:doc:`Assets chapter <t3coreapi:ApiOverview/Assets/Index>`
47+
48+
Resolution behavior
49+
===================
50+
51+
- The resolver first selects a repository that reports the requested inventory id.
52+
- It then resolves the target in that inventory.
53+
- If no repository provides the inventory id, a warning is logged and the link is not resolved.
54+
55+
See also
56+
========
57+
58+
- :ref:`Text Roles <basic-text-role>`
59+
- :ref:`custom_interlink_resolvers`
60+

docs/reference/restructuredtext/text-roles.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Text Roles
66

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

9+
For external project references using inventory ids, see :doc:`interlinks`.
10+
911
You can also :ref:`add your own custom text roles <custom_text_roles>`.
1012

1113
Currently the following text roles are implemented:

packages/guides/resources/config/guides.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use phpDocumentor\Guides\ReferenceResolvers\EmailReferenceResolver;
3131
use phpDocumentor\Guides\ReferenceResolvers\ExternalReferenceResolver;
3232
use phpDocumentor\Guides\ReferenceResolvers\ImageReferenceResolverPreRender;
33+
use phpDocumentor\Guides\ReferenceResolvers\Interlink\ChainedInventoryLinkResolver;
3334
use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryLoader;
3435
use phpDocumentor\Guides\ReferenceResolvers\Interlink\DefaultInventoryRepository;
3536
use phpDocumentor\Guides\ReferenceResolvers\Interlink\InventoryLoader;
@@ -74,7 +75,8 @@
7475

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

7981
$container->services()
8082
->defaults()
@@ -129,8 +131,13 @@
129131

130132
->set(DocumentNodeTraverser::class)
131133

132-
->set(InventoryRepository::class, DefaultInventoryRepository::class)
134+
->set(DefaultInventoryRepository::class)
133135
->arg('$inventoryConfigs', param('phpdoc.guides.inventories'))
136+
->arg('$enabled', param('phpdoc.guides.interlink.default_repository.enabled'))
137+
->tag('phpdoc.guides.interlink_resolver')
138+
139+
->set(InventoryRepository::class, ChainedInventoryLinkResolver::class)
140+
->arg('$repositories', tagged_iterator('phpdoc.guides.interlink_resolver'))
134141

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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\Interlink;
15+
16+
use Doctrine\Deprecations\Deprecation;
17+
use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
18+
use phpDocumentor\Guides\ReferenceResolvers\Message;
19+
use phpDocumentor\Guides\ReferenceResolvers\Messages;
20+
use phpDocumentor\Guides\RenderContext;
21+
22+
use function array_key_exists;
23+
use function array_merge;
24+
use function sprintf;
25+
26+
final class ChainedInventoryLinkResolver implements InventoryLinkResolver
27+
{
28+
/** @var array<string, InventoryRepository|null> */
29+
private array $cachedRepositories = [];
30+
31+
/** @param iterable<InventoryRepository> $repositories */
32+
public function __construct(
33+
private readonly iterable $repositories,
34+
) {
35+
}
36+
37+
public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null
38+
{
39+
return $this->resolveInventoryLink($node, $renderContext, $messages)?->getLink();
40+
}
41+
42+
public function hasInventory(string $key): bool
43+
{
44+
return $this->findInventoryRepository($key) !== null;
45+
}
46+
47+
public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
48+
{
49+
Deprecation::trigger(
50+
'phpDocumentor/guides',
51+
'https://github.com/phpDocumentor/guides/issues',
52+
'InventoryRepository::getInventory() is deprecated. Implement '
53+
. 'InventoryLinkResolver::resolveInventoryLink() for one-call interlink resolution.',
54+
);
55+
56+
return $this->findInventoryRepository($node->getInterlinkDomain())?->getInventory($node, $renderContext, $messages);
57+
}
58+
59+
public function resolveInventoryLink(
60+
CrossReferenceNode $node,
61+
RenderContext $renderContext,
62+
Messages $messages,
63+
): ResolvedInventoryLink|null {
64+
$repository = $this->findInventoryRepository($node->getInterlinkDomain());
65+
if ($repository === null) {
66+
$messages->addWarning(
67+
new Message(
68+
sprintf('Inventory with key %s not found. ', $node->getInterlinkDomain()),
69+
array_merge($renderContext->getLoggerInformation(), $node->getDebugInformation()),
70+
),
71+
);
72+
73+
return null;
74+
}
75+
76+
if ($repository instanceof InventoryLinkResolver) {
77+
return $repository->resolveInventoryLink($node, $renderContext, $messages);
78+
}
79+
80+
$inventory = $repository->getInventory($node, $renderContext, $messages);
81+
$link = $repository->getLink($node, $renderContext, $messages);
82+
if ($inventory === null || $link === null) {
83+
return null;
84+
}
85+
86+
return new ResolvedInventoryLink($inventory->getBaseUrl(), $link);
87+
}
88+
89+
private function findInventoryRepository(string $key): InventoryRepository|null
90+
{
91+
if (array_key_exists($key, $this->cachedRepositories)) {
92+
return $this->cachedRepositories[$key];
93+
}
94+
95+
foreach ($this->repositories as $repository) {
96+
if ($repository->hasInventory($key)) {
97+
$this->cachedRepositories[$key] = $repository;
98+
99+
return $repository;
100+
}
101+
}
102+
103+
$this->cachedRepositories[$key] = null;
104+
105+
return null;
106+
}
107+
}

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace phpDocumentor\Guides\ReferenceResolvers\Interlink;
1515

16+
use Doctrine\Deprecations\Deprecation;
1617
use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
1718
use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer;
1819
use phpDocumentor\Guides\ReferenceResolvers\Message;
@@ -23,7 +24,7 @@
2324
use function array_merge;
2425
use function sprintf;
2526

26-
final class DefaultInventoryRepository implements InventoryRepository
27+
final class DefaultInventoryRepository implements InventoryLinkResolver
2728
{
2829
/** @var array<string, Inventory> */
2930
private array $inventories = [];
@@ -33,28 +34,58 @@ public function __construct(
3334
private readonly AnchorNormalizer $anchorNormalizer,
3435
private readonly InventoryLoader $inventoryLoader,
3536
array $inventoryConfigs,
37+
private readonly bool $enabled = true,
3638
) {
3739
foreach ($inventoryConfigs as $inventory) {
38-
$this->inventories[$this->anchorNormalizer->reduceAnchor($inventory['id'])] = new Inventory($inventory['url'], $anchorNormalizer);
40+
$this->inventories[$this->anchorNormalizer->reduceAnchor($inventory['id'])]
41+
= new Inventory($inventory['url'], $anchorNormalizer);
3942
}
4043
}
4144

4245
public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null
4346
{
44-
$inventory = $this->getInventory($node, $renderContext, $messages);
45-
$group = $inventory?->getGroup($node, $renderContext, $messages);
46-
47-
return $group?->getLink($node, $renderContext, $messages);
47+
return $this->resolveInventoryLink($node, $renderContext, $messages)?->getLink();
4848
}
4949

5050
public function hasInventory(string $key): bool
5151
{
52+
if (!$this->enabled) {
53+
return false;
54+
}
55+
5256
$reducedKey = $this->anchorNormalizer->reduceAnchor($key);
5357

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

5761
public function getInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
62+
{
63+
Deprecation::trigger(
64+
'phpDocumentor/guides',
65+
'https://github.com/phpDocumentor/guides/issues',
66+
'InventoryRepository::getInventory() is deprecated. Implement '
67+
. 'InventoryLinkResolver::resolveInventoryLink() for one-call interlink resolution.',
68+
);
69+
70+
return $this->findInventory($node, $renderContext, $messages);
71+
}
72+
73+
public function resolveInventoryLink(
74+
CrossReferenceNode $node,
75+
RenderContext $renderContext,
76+
Messages $messages,
77+
): ResolvedInventoryLink|null {
78+
$inventory = $this->findInventory($node, $renderContext, $messages);
79+
$group = $inventory?->getGroup($node, $renderContext, $messages);
80+
$link = $group?->getLink($node, $renderContext, $messages);
81+
if ($inventory === null || $link === null) {
82+
return null;
83+
}
84+
85+
return new ResolvedInventoryLink($inventory->getBaseUrl(), $link);
86+
}
87+
88+
private function findInventory(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): Inventory|null
5889
{
5990
$reducedKey = $this->anchorNormalizer->reduceAnchor($node->getInterlinkDomain());
6091
if (!$this->hasInventory($reducedKey)) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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\Interlink;
15+
16+
use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode;
17+
use phpDocumentor\Guides\ReferenceResolvers\Messages;
18+
use phpDocumentor\Guides\RenderContext;
19+
20+
interface InventoryLinkResolver extends InventoryRepository
21+
{
22+
public function resolveInventoryLink(
23+
CrossReferenceNode $node,
24+
RenderContext $renderContext,
25+
Messages $messages,
26+
): ResolvedInventoryLink|null;
27+
}

0 commit comments

Comments
 (0)