From a8420bbb2be20a9eb1a5b38b82764799cd50701b Mon Sep 17 00:00:00 2001 From: "lina.wolf" Date: Thu, 6 Jul 2023 18:05:53 +0000 Subject: [PATCH] Make Intersphinx inventories configurable And add functional tests for them Update Index.rst (#134) (#135) Nowadays, RealURL should not be used in 'most' TYPO3 installations ;) Found in the section: Dependency Management Co-authored-by: Peter Oberzier --- docs/configuration.rst | 17 +++++----- packages/guides-cli/src/Command/Run.php | 13 +++++--- packages/guides/resources/config/guides.php | 18 ++++++++++ .../template/html/inline/citation.html.twig | 2 +- .../template/html/inline/footnote.html.twig | 2 +- packages/guides/src/Intersphinx/Inventory.php | 14 ++++++++ .../src/Intersphinx/InventoryLoader.php | 23 ++++++------- .../src/Intersphinx/InventoryRepository.php | 17 ++++++++-- .../guides/src/Intersphinx/JsonLoader.php | 4 +-- packages/guides/src/Meta/ExternalTarget.php | 33 +++++++++++++++++++ packages/guides/src/Meta/InternalTarget.php | 16 ++++++++- packages/guides/src/Meta/Target.php | 12 +++++++ .../IntersphinxReferenceResolver.php | 2 +- .../guides/src/Settings/ProjectSettings.php | 28 ++++++++++++---- .../guides/src/Settings/SettingsManager.php | 2 +- packages/guides/src/Twig/AssetsExtension.php | 11 +++++-- .../unit/Intersphinx/InventoryLoaderTest.php | 29 +++++++--------- .../intersphinx-link/expected/index.html | 16 +++++++++ .../tests/intersphinx-link/input/index.rst | 7 ++++ .../tests/intersphinx-link/input/settings.php | 9 +++++ .../tests/settings/input/settings.php | 8 +++-- 21 files changed, 218 insertions(+), 65 deletions(-) create mode 100644 packages/guides/src/Meta/ExternalTarget.php create mode 100644 packages/guides/src/Meta/Target.php create mode 100644 tests/Integration/tests/intersphinx-link/expected/index.html create mode 100644 tests/Integration/tests/intersphinx-link/input/index.rst create mode 100644 tests/Integration/tests/intersphinx-link/input/settings.php diff --git a/docs/configuration.rst b/docs/configuration.rst index bd9314a7e..0c4b414f2 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -19,16 +19,15 @@ you can do so by creating a ``settings.php`` file in the input directory of the manual you are building (that is the directory you would specify as a first argument to the CLI). -That file needs to return a ``ProjectSettings``, and typically looks as +That file needs to return an `array`, and typically looks as follows: -.. code-block:: php +.. code-block:: php - 'My Project', + 'version' => '3.1.4', + 'inventories' => ['t3coreapi' => 'https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/'], + ]; diff --git a/packages/guides-cli/src/Command/Run.php b/packages/guides-cli/src/Command/Run.php index 41e2d3d5c..f5bb8e207 100644 --- a/packages/guides-cli/src/Command/Run.php +++ b/packages/guides-cli/src/Command/Run.php @@ -16,6 +16,7 @@ use phpDocumentor\Guides\Handlers\CompileDocumentsCommand; use phpDocumentor\Guides\Handlers\ParseDirectoryCommand; use phpDocumentor\Guides\Handlers\RenderCommand; +use phpDocumentor\Guides\Intersphinx\InventoryRepository; use phpDocumentor\Guides\Nodes\ProjectNode; use phpDocumentor\Guides\Settings\ProjectSettings; use phpDocumentor\Guides\Settings\SettingsManager; @@ -33,6 +34,7 @@ use function count; use function getcwd; use function implode; +use function is_array; use function is_countable; use function is_dir; use function is_file; @@ -47,6 +49,7 @@ public function __construct( private readonly Logger $logger, private readonly ThemeManager $themeManager, private readonly SettingsManager $settingsManager, + private readonly InventoryRepository $inventoryRepository, ) { parent::__construct('run'); @@ -117,18 +120,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if (is_file($inputDir . '/settings.php')) { - $settings = require $inputDir . '/settings.php'; - if (!$settings instanceof ProjectSettings) { - throw new RuntimeException('settings.php must return an instance of ' . ProjectSettings::class); + $settingsArray = require $inputDir . '/settings.php'; + if (!is_array($settingsArray)) { + throw new RuntimeException('settings.php must return an array!'); } + $settings = new ProjectSettings($settingsArray); $this->settingsManager->setProjectSettings($settings); $projectNode = new ProjectNode( $settings->getTitle(), $settings->getVersion(), ); + $this->inventoryRepository->initialize($settings->getInventories()); } else { - $this->settingsManager->setProjectSettings(new ProjectSettings()); + $this->settingsManager->setProjectSettings(new ProjectSettings([])); $projectNode = new ProjectNode(); } diff --git a/packages/guides/resources/config/guides.php b/packages/guides/resources/config/guides.php index ec516e2ad..ba58404e0 100644 --- a/packages/guides/resources/config/guides.php +++ b/packages/guides/resources/config/guides.php @@ -9,6 +9,9 @@ use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Compiler\NodeTransformers\CustomNodeTransformerFactory; use phpDocumentor\Guides\Compiler\NodeTransformers\NodeTransformerFactory; +use phpDocumentor\Guides\Intersphinx\InventoryLoader; +use phpDocumentor\Guides\Intersphinx\InventoryRepository; +use phpDocumentor\Guides\Intersphinx\JsonLoader; use phpDocumentor\Guides\NodeRenderers\DefaultNodeRenderer; use phpDocumentor\Guides\NodeRenderers\DelegatingNodeRenderer; use phpDocumentor\Guides\NodeRenderers\Html\DocumentNodeRenderer; @@ -24,6 +27,7 @@ use phpDocumentor\Guides\ReferenceResolvers\DocReferenceResolver; use phpDocumentor\Guides\ReferenceResolvers\ExternalReferenceResolver; use phpDocumentor\Guides\ReferenceResolvers\InternalReferenceResolver; +use phpDocumentor\Guides\ReferenceResolvers\IntersphinxReferenceResolver; use phpDocumentor\Guides\ReferenceResolvers\ReferenceResolver; use phpDocumentor\Guides\ReferenceResolvers\ReferenceResolverPreRender; use phpDocumentor\Guides\ReferenceResolvers\RefReferenceResolver; @@ -42,6 +46,8 @@ use phpDocumentor\Guides\UrlGeneratorInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Contracts\HttpClient\HttpClientInterface; use Twig\Loader\FilesystemLoader; use function Symfony\Component\DependencyInjection\Loader\Configurator\param; @@ -99,6 +105,16 @@ ->set(DocumentNodeTraverser::class) + ->set(InventoryRepository::class) + + ->set(InventoryLoader::class) + + ->set(JsonLoader::class) + + + ->set(HttpClientInterface::class) + ->factory([HttpClient::class, 'create']) + ->set(UrlGenerator::class) ->set(ExternalReferenceResolver::class) @@ -109,6 +125,8 @@ ->set(RefReferenceResolver::class) + ->set(IntersphinxReferenceResolver::class) + ->set(DelegatingReferenceResolver::class) ->arg('$resolvers', tagged_iterator('phpdoc.guides.reference_resolver', defaultPriorityMethod: 'getPriority')) diff --git a/packages/guides/resources/template/html/inline/citation.html.twig b/packages/guides/resources/template/html/inline/citation.html.twig index 88010f8c7..9f18943e4 100644 --- a/packages/guides/resources/template/html/inline/citation.html.twig +++ b/packages/guides/resources/template/html/inline/citation.html.twig @@ -1,5 +1,5 @@ {%- if node.internalTarget -%} - [{{- node.value -}}] + [{{- node.value -}}] {%- else -%} [{{- node.value -}}] {%- endif -%} diff --git a/packages/guides/resources/template/html/inline/footnote.html.twig b/packages/guides/resources/template/html/inline/footnote.html.twig index 88010f8c7..9f18943e4 100644 --- a/packages/guides/resources/template/html/inline/footnote.html.twig +++ b/packages/guides/resources/template/html/inline/footnote.html.twig @@ -1,5 +1,5 @@ {%- if node.internalTarget -%} - [{{- node.value -}}] + [{{- node.value -}}] {%- else -%} [{{- node.value -}}] {%- endif -%} diff --git a/packages/guides/src/Intersphinx/Inventory.php b/packages/guides/src/Intersphinx/Inventory.php index 5b1b527cb..335a53777 100644 --- a/packages/guides/src/Intersphinx/Inventory.php +++ b/packages/guides/src/Intersphinx/Inventory.php @@ -14,6 +14,8 @@ final class Inventory /** @var InventoryGroup[] */ private array $groups = []; + private bool $isLoaded = false; + public function __construct(private readonly string $baseUrl) { } @@ -59,4 +61,16 @@ public function hasInventoryGroup(string $key): bool return array_key_exists($lowerCaseKey, $this->groups); } + + public function isLoaded(): bool + { + return $this->isLoaded; + } + + public function setIsLoaded(bool $isLoaded): Inventory + { + $this->isLoaded = $isLoaded; + + return $this; + } } diff --git a/packages/guides/src/Intersphinx/InventoryLoader.php b/packages/guides/src/Intersphinx/InventoryLoader.php index a24bd1787..a03b5d301 100644 --- a/packages/guides/src/Intersphinx/InventoryLoader.php +++ b/packages/guides/src/Intersphinx/InventoryLoader.php @@ -9,21 +9,14 @@ final class InventoryLoader { public function __construct( - private readonly InventoryRepository $inventoryRepository, private readonly JsonLoader $jsonLoader, private readonly string $pathToJson = 'objects.inv.json', ) { } - public function getInventoryRepository(): InventoryRepository - { - return $this->inventoryRepository; - } - /** @param array $json */ - public function loadInventoryFromJson(string $key, string $baseUrl, array $json): void + public function loadInventoryFromJson(Inventory $inventory, array $json): void { - $newInventory = new Inventory($baseUrl); foreach ($json as $groupKey => $groupArray) { $group = new InventoryGroup(); if (is_array($groupArray)) { @@ -37,16 +30,20 @@ public function loadInventoryFromJson(string $key, string $baseUrl, array $json) } } - $newInventory->addGroup($groupKey, $group); + $inventory->addGroup($groupKey, $group); } - $this->inventoryRepository->addInventory($key, $newInventory); + $inventory->setIsLoaded(true); } - public function loadInventoryFromUrl(string $key, string $url): void + public function loadInventory(Inventory $inventory): void { - $json = $this->jsonLoader->loadJsonFromUrl($url . $this->pathToJson); + if ($inventory->isLoaded()) { + return; + } + + $json = $this->jsonLoader->loadJsonFromUrl($inventory->getBaseUrl() . $this->pathToJson); - $this->loadInventoryFromJson($key, $url, $json); + $this->loadInventoryFromJson($inventory, $json); } } diff --git a/packages/guides/src/Intersphinx/InventoryRepository.php b/packages/guides/src/Intersphinx/InventoryRepository.php index 93de81b62..a906d60cf 100644 --- a/packages/guides/src/Intersphinx/InventoryRepository.php +++ b/packages/guides/src/Intersphinx/InventoryRepository.php @@ -11,9 +11,20 @@ class InventoryRepository { - /** @param array $inventories */ - public function __construct(private array $inventories) + /** @var array */ + private array $inventories = []; + + public function __construct(private readonly InventoryLoader $inventoryLoader) + { + } + + /** @param array $inventoryConfigs */ + public function initialize(array $inventoryConfigs): void { + $this->inventories = []; + foreach ($inventoryConfigs as $key => $url) { + $this->inventories[$key] = new Inventory($url); + } } public function hasInventory(string $key): bool @@ -30,6 +41,8 @@ public function getInventory(string $key): Inventory throw new RuntimeException('Inventory with key ' . $lowerCaseKey . ' not found. ', 1_671_398_986); } + $this->inventoryLoader->loadInventory($this->inventories[$lowerCaseKey]); + return $this->inventories[$lowerCaseKey]; } diff --git a/packages/guides/src/Intersphinx/JsonLoader.php b/packages/guides/src/Intersphinx/JsonLoader.php index d03c89448..e751df42b 100644 --- a/packages/guides/src/Intersphinx/JsonLoader.php +++ b/packages/guides/src/Intersphinx/JsonLoader.php @@ -8,7 +8,6 @@ use RuntimeException; use Symfony\Contracts\HttpClient\HttpClientInterface; -use function implode; use function is_array; use function json_decode; @@ -27,9 +26,8 @@ public function loadJsonFromUrl(string $url): array 'GET', $url, ); - $jsonString = implode("\n", $response->toArray()); - return $this->loadJsonFromString($jsonString, $url); + return $response->toArray(); } /** @return array */ diff --git a/packages/guides/src/Meta/ExternalTarget.php b/packages/guides/src/Meta/ExternalTarget.php new file mode 100644 index 000000000..58ade56d5 --- /dev/null +++ b/packages/guides/src/Meta/ExternalTarget.php @@ -0,0 +1,33 @@ +url; + } + + public function getTitle(): string|null + { + return $this->title; + } +} diff --git a/packages/guides/src/Meta/InternalTarget.php b/packages/guides/src/Meta/InternalTarget.php index 26b88fc5c..8c3481687 100644 --- a/packages/guides/src/Meta/InternalTarget.php +++ b/packages/guides/src/Meta/InternalTarget.php @@ -13,8 +13,10 @@ namespace phpDocumentor\Guides\Meta; -class InternalTarget +class InternalTarget implements Target { + private string $url; + public function __construct( private readonly string $documentPath, protected string $anchorName, @@ -41,4 +43,16 @@ public function getTitle(): string|null { return $this->title; } + + public function getUrl(): string + { + return $this->url; + } + + public function setUrl(string $url): InternalTarget + { + $this->url = $url; + + return $this; + } } diff --git a/packages/guides/src/Meta/Target.php b/packages/guides/src/Meta/Target.php new file mode 100644 index 000000000..426024a27 --- /dev/null +++ b/packages/guides/src/Meta/Target.php @@ -0,0 +1,12 @@ +setValue($link->getTitle()); } - return false; + return true; } public static function getPriority(): int diff --git a/packages/guides/src/Settings/ProjectSettings.php b/packages/guides/src/Settings/ProjectSettings.php index b7cb5e4c3..7b484abcb 100644 --- a/packages/guides/src/Settings/ProjectSettings.php +++ b/packages/guides/src/Settings/ProjectSettings.php @@ -4,21 +4,37 @@ namespace phpDocumentor\Guides\Settings; +use function is_array; +use function is_string; + class ProjectSettings { - public function __construct( - private string|null $title = null, - private string|null $version = null, - ) { + /** @var array */ + private array $inventories = []; + private string $title = ''; + private string $version = ''; + + /** @param array> $settingsArray */ + public function __construct(array $settingsArray) + { + $this->title = isset($settingsArray['title']) && is_string($settingsArray['title']) ? $settingsArray['title'] : ''; + $this->version = isset($settingsArray['version']) && is_string($settingsArray['version']) ? $settingsArray['version'] : ''; + $this->inventories = isset($settingsArray['inventories']) && is_array($settingsArray['inventories']) ? $settingsArray['inventories'] : []; } - public function getTitle(): string|null + public function getTitle(): string { return $this->title; } - public function getVersion(): string|null + public function getVersion(): string { return $this->version; } + + /** @return array */ + public function getInventories(): array + { + return $this->inventories; + } } diff --git a/packages/guides/src/Settings/SettingsManager.php b/packages/guides/src/Settings/SettingsManager.php index 49ab48468..79d6b49d7 100644 --- a/packages/guides/src/Settings/SettingsManager.php +++ b/packages/guides/src/Settings/SettingsManager.php @@ -10,7 +10,7 @@ class SettingsManager public function __construct(ProjectSettings|null $projectSettings = null) { - $this->projectSettings = $projectSettings ?? new ProjectSettings(); + $this->projectSettings = $projectSettings ?? new ProjectSettings([]); } public function getProjectSettings(): ProjectSettings diff --git a/packages/guides/src/Twig/AssetsExtension.php b/packages/guides/src/Twig/AssetsExtension.php index e464247f0..2bb001ee7 100644 --- a/packages/guides/src/Twig/AssetsExtension.php +++ b/packages/guides/src/Twig/AssetsExtension.php @@ -16,6 +16,7 @@ use League\Flysystem\Exception; use LogicException; use phpDocumentor\Guides\Meta\InternalTarget; +use phpDocumentor\Guides\Meta\Target; use phpDocumentor\Guides\NodeRenderers\NodeRenderer; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\RenderContext; @@ -47,7 +48,7 @@ public function getFunctions(): array new TwigFunction('asset', $this->asset(...), ['is_safe' => ['html'], 'needs_context' => true]), new TwigFunction('renderNode', $this->renderNode(...), ['is_safe' => ['html'], 'needs_context' => true]), new TwigFunction('renderLink', $this->renderLink(...), ['is_safe' => ['html'], 'needs_context' => true]), - new TwigFunction('renderInternalTarget', $this->renderInternalTarget(...), ['is_safe' => ['html'], 'needs_context' => true]), + new TwigFunction('renderTarget', $this->renderTarget(...), ['is_safe' => ['html'], 'needs_context' => true]), ]; } @@ -108,9 +109,13 @@ public function renderNode(array $context, Node|array|null $node): string } /** @param array{env: RenderContext} $context */ - public function renderInternalTarget(array $context, InternalTarget $internalTarget): string + public function renderTarget(array $context, Target $target): string { - return $context['env']->relativeDocUrl($internalTarget->getDocumentPath(), $internalTarget->getAnchor()); + if ($target instanceof InternalTarget) { + return $context['env']->relativeDocUrl($target->getDocumentPath(), $target->getAnchor()); + } + + return $target->getUrl(); } /** @param array{env: RenderContext} $context */ diff --git a/packages/guides/tests/unit/Intersphinx/InventoryLoaderTest.php b/packages/guides/tests/unit/Intersphinx/InventoryLoaderTest.php index 34b96813c..f73076057 100644 --- a/packages/guides/tests/unit/Intersphinx/InventoryLoaderTest.php +++ b/packages/guides/tests/unit/Intersphinx/InventoryLoaderTest.php @@ -25,39 +25,34 @@ final class InventoryLoaderTest extends TestCase protected function setUp(): void { $this->jsonLoader = $this->createMock(JsonLoader::class); - $this->inventoryRepository = new InventoryRepository([]); - $this->inventoryLoader = new InventoryLoader($this->inventoryRepository, $this->jsonLoader); + $this->inventoryLoader = new InventoryLoader($this->jsonLoader); + $this->inventoryRepository = new InventoryRepository($this->inventoryLoader); $jsonString = file_get_contents(__DIR__ . '/input/objects.inv.json'); assertIsString($jsonString); $this->json = (array) json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR); - $this->inventoryLoader->loadInventoryFromJson('somekey', 'https://example.com/', $this->json); + $inventory = new Inventory('https://example.com/'); + $this->inventoryLoader->loadInventoryFromJson($inventory, $this->json); + $this->inventoryRepository->addInventory('somekey', $inventory); } public function testInventoryLoaderLoadsInventory(): void { - $inventory = $this->inventoryLoader->getInventoryRepository()->getInventory('somekey'); + $inventory = $this->inventoryRepository->getInventory('somekey'); self::assertGreaterThan(1, count($inventory->getGroups())); } - public function testLoadInventoryFromUrl(): void + public function testInventoryIsLoadedExactlyOnce(): void { - $this->jsonLoader->expects(self::atLeastOnce())->method('loadJsonFromUrl')->willReturn($this->json); - $this->inventoryLoader->loadInventoryFromUrl('somekey', 'https://example.com/'); - $inventory = $this->inventoryLoader->getInventoryRepository()->getInventory('somekey'); + $this->jsonLoader->expects(self::once())->method('loadJsonFromUrl')->willReturn($this->json); + $inventory = new Inventory('https://example.com/'); + $this->inventoryLoader->loadInventory($inventory); + $this->inventoryLoader->loadInventory($inventory); self::assertGreaterThan(1, count($inventory->getGroups())); } public function testInventoryLoaderGetInventoryIsCaseInsensitive(): void { - $inventory = $this->inventoryLoader->getInventoryRepository()->getInventory('SomeKey'); - self::assertGreaterThan(1, count($inventory->getGroups())); - } - - public function testInventoryKeyIsCaseInsensitive(): void - { - $inventoryLoaderWithCamelCaseKey = new InventoryLoader($this->inventoryRepository, $this->jsonLoader); - $inventoryLoaderWithCamelCaseKey->loadInventoryFromJson('CamelCaseKey', 'https://example.com/', $this->json); - $inventory = $inventoryLoaderWithCamelCaseKey->getInventoryRepository()->getInventory('camelcasekey'); + $inventory = $this->inventoryRepository->getInventory('SomeKey'); self::assertGreaterThan(1, count($inventory->getGroups())); } } diff --git a/tests/Integration/tests/intersphinx-link/expected/index.html b/tests/Integration/tests/intersphinx-link/expected/index.html new file mode 100644 index 000000000..4ab24bcf6 --- /dev/null +++ b/tests/Integration/tests/intersphinx-link/expected/index.html @@ -0,0 +1,16 @@ + + + + Document Title - My Project + + + +
+

Document Title

+ +

Lorem Ipsum Dolor.

+

See the TYPO3 documentation

+
+ + + diff --git a/tests/Integration/tests/intersphinx-link/input/index.rst b/tests/Integration/tests/intersphinx-link/input/index.rst new file mode 100644 index 000000000..0cf0bd402 --- /dev/null +++ b/tests/Integration/tests/intersphinx-link/input/index.rst @@ -0,0 +1,7 @@ +============== +Document Title +============== + +Lorem Ipsum Dolor. + +See the :ref:`TYPO3 documentation ` diff --git a/tests/Integration/tests/intersphinx-link/input/settings.php b/tests/Integration/tests/intersphinx-link/input/settings.php new file mode 100644 index 000000000..eb637d64a --- /dev/null +++ b/tests/Integration/tests/intersphinx-link/input/settings.php @@ -0,0 +1,9 @@ + 'My Project', + 'version' => 'main (development)', + 'inventories' => ['t3coreapi' => 'https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/'], +]; diff --git a/tests/Integration/tests/settings/input/settings.php b/tests/Integration/tests/settings/input/settings.php index 396841728..7a5a81086 100644 --- a/tests/Integration/tests/settings/input/settings.php +++ b/tests/Integration/tests/settings/input/settings.php @@ -2,6 +2,8 @@ declare(strict_types=1); -use phpDocumentor\Guides\Settings\ProjectSettings; - -return new ProjectSettings('My Project', '3.1.4'); +return [ + 'title' => 'My Project', + 'version' => '3.1.4', + 'inventories' => ['t3coreapi' => 'https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/'], +];