diff --git a/docs/reference/restructuredtext/term-index.rst b/docs/reference/restructuredtext/term-index.rst new file mode 100644 index 000000000..b9dcc72ac --- /dev/null +++ b/docs/reference/restructuredtext/term-index.rst @@ -0,0 +1,175 @@ +.. include:: /include.rst.txt + +.. _term-index: + +========== +Term index +========== + +.. contents:: + +.. index:: reST directives; seealso + +The ``index`` directive marks terms for a project-wide index, the way a +printed book's index points readers from a term to every page that discusses +it. Collected entries can then be rendered as a ``genindex`` page, either +project-wide or scoped to part of the project (e.g. one changelog version). + +.. _index-directive: + +The ``index`` directive +======================= + +Collects one or more entries, project-wide, to be aggregated into a +``genindex`` page later. The directive itself is invisible in the rendered +page -- it produces no visible output where it's written. + +Entry types +----------- + +Each line of an ``index`` directive declares one entry. The prefix before the +first colon selects the entry type; without a recognized prefix, the whole +line is treated as a plain ``single`` term: + +``single`` + A top-level term, optionally with a subterm: + + .. code-block:: + + .. index:: single: installation + .. index:: single: installation; troubleshooting + +``pair`` + Shorthand for two reciprocal ``single`` entries, so the term is findable + either way round: + + .. code-block:: + + .. index:: pair: configuration; file + + This is equivalent to writing both + ``single: configuration; file`` and ``single: file; configuration``. + +``triple`` + Shorthand for three reciprocal entries covering every rotation of the + given terms: + + .. code-block:: + + .. index:: triple: access; token; refresh + +``module`` + Nests the given name under a literal top-level "module" term: + + .. code-block:: + + .. index:: module: Acme\Bundle\FooBundle + +``see`` / ``seealso`` + A cross-reference from one term to another, rendered without its own + link -- only as a pointer to the target term: + + .. code-block:: + + .. index:: see: token; access token + .. index:: seealso: OAuth; access token + +Prefixing an entry with ``!`` marks it as the "main" definition of that term, +which themes can render distinctly (e.g. bold) from its other occurrences: + +.. code-block:: + + .. index:: ! access token + +Several entries can also be declared at once, one per line, under a single +directive: + +.. code-block:: + + .. index:: + single: configuration + pair: configuration; file + see: token; access token + +Comma-separated, type-less form +------------------------------- + +A line may also hold several comma-separated terms at once, each becoming +its own ``single`` entry -- the convention used by e.g. TYPO3 Core's +Changelog files: + +.. code-block:: + + .. index:: Backend, PHP-API, ext:core + +.. _genindex-template: + +The ``genindex`` template +========================= + +The full, project-wide index is rendered by giving a document the +``template`` field, set to ``genindex``: + +.. code-block:: + :caption: genindex.rst + + :orphan: + :template: genindex + + Index + ===== + +.. _genindex-directive: + +The ``genindex`` directive +========================== + +The ``.. genindex::`` directive renders the same kind of listing inline, +anywhere a document chooses to place it. Unlike the ``template`` field, +which produces at most one page for the *whole* project, ``genindex`` can +be placed anywhere and used more than once -- e.g. one listing per version +directory in a changelog: + +.. code-block:: + + Index for 12.4 + -------------- + + .. genindex:: + :scope: Changelog/12.4/ + + Full index + ---------- + + .. genindex:: + +``:scope:`` accepts a comma-separated list of path prefixes; entries from +documents whose path doesn't start with one of them are left out of that +particular listing. Omitting ``:scope:`` includes every entry in the +project. + +Both the ``genindex`` template and the ``.. genindex::`` directive group +terms under an A-Z jumpbox with one heading per letter by default. For a +small listing -- e.g. a single changelog version -- that grouping can add +more noise than it saves navigation, so it can be turned off: + +.. code-block:: + + .. genindex:: + :scope: Changelog/12.4/ + :no-letter-index: + +Index terms on sections +======================= + +Independently of any ``genindex`` page, every term from an ``index`` entry +is also recorded on the section it resolves to (the next heading following +the directive, or the document's own top-level section if none follows). +A theme's section template can expose these as a search-key data attribute, +e.g. ``data-guides-index-terms="configuration,file"``. + +This makes the terms available to external tooling that crawls the +rendered HTML rather than the reST source -- for example, a custom search +engine such as TYPO3's Elasticsearch integration can pick up the attribute +and index a section under the same terms an author tagged it with via +``index``, without needing its own copy of the genindex logic. diff --git a/packages/guides-restructured-text/resources/config/guides-restructured-text.php b/packages/guides-restructured-text/resources/config/guides-restructured-text.php index 7491b6144..c9a156e44 100644 --- a/packages/guides-restructured-text/resources/config/guides-restructured-text.php +++ b/packages/guides-restructured-text/resources/config/guides-restructured-text.php @@ -23,6 +23,7 @@ use phpDocumentor\Guides\RestructuredText\Directives\ErrorDirective; use phpDocumentor\Guides\RestructuredText\Directives\FigureDirective; use phpDocumentor\Guides\RestructuredText\Directives\GeneralDirective; +use phpDocumentor\Guides\RestructuredText\Directives\GenIndexDirective; use phpDocumentor\Guides\RestructuredText\Directives\HighlightDirective; use phpDocumentor\Guides\RestructuredText\Directives\HighlightsDirective; use phpDocumentor\Guides\RestructuredText\Directives\HintDirective; @@ -89,6 +90,7 @@ use phpDocumentor\Guides\RestructuredText\Parser\Productions\FieldList\OrphanFieldListItemRule; use phpDocumentor\Guides\RestructuredText\Parser\Productions\FieldList\ProjectFieldListItemRule; use phpDocumentor\Guides\RestructuredText\Parser\Productions\FieldList\RevisionFieldListItemRule; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\FieldList\TemplateFieldListItemRule; use phpDocumentor\Guides\RestructuredText\Parser\Productions\FieldList\TocDepthFieldListItemRule; use phpDocumentor\Guides\RestructuredText\Parser\Productions\FieldList\VersionFieldListItemRule; use phpDocumentor\Guides\RestructuredText\Parser\Productions\FieldListRule; @@ -212,6 +214,7 @@ ->set(IncludeDirective::class) ->arg('$startingRule', service(DocumentRule::class)) ->set(IndexDirective::class) + ->set(GenIndexDirective::class) ->set(LaTeXMain::class) ->set(ListTableDirective::class) ->set(LiteralincludeDirective::class) @@ -354,6 +357,9 @@ ->set(TocDepthFieldListItemRule::class) ->tag('phpdoc.guides.parser.rst.fieldlist') + ->set(TemplateFieldListItemRule::class) + ->tag('phpdoc.guides.parser.rst.fieldlist') + ->set(VersionFieldListItemRule::class) ->args([ '$logger' => service(LoggerInterface::class), diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/GenIndexDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/GenIndexDirective.php new file mode 100644 index 000000000..684323536 --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/GenIndexDirective.php @@ -0,0 +1,69 @@ +getOptionString('scope')); + $prefixes = array_values(array_filter(array_map(trim(...), $prefixes), static fn (string $prefix): bool => $prefix !== '')); + + return new GenIndexNode([], $prefixes, !$directive->hasOption('no-letter-index')); + } +} diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/IndexDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/IndexDirective.php index 023779540..b8cdfba7f 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/IndexDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/IndexDirective.php @@ -13,10 +13,146 @@ namespace phpDocumentor\Guides\RestructuredText\Directives; -final class IndexDirective extends SubDirective +use phpDocumentor\Guides\Nodes\Index\IndexEntryNode; +use phpDocumentor\Guides\Nodes\Index\IndexEntryType; +use phpDocumentor\Guides\Nodes\Index\IndexNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use Psr\Log\LoggerInterface; + +use function array_filter; +use function array_map; +use function array_values; +use function explode; +use function levenshtein; +use function mb_strpos; +use function mb_substr; +use function sprintf; +use function str_starts_with; +use function strtolower; +use function substr; +use function trim; + +/** + * Collects index entries, example: + * + * .. index:: single: installation + * + * .. index:: + * single: configuration + * pair: configuration; file + * see: token; access token + * ! main entry example + * + * A line may also hold several comma-separated, type-less entries at once + * (the convention used by e.g. TYPO3's Core Changelog files), each becoming + * its own "single" entry: + * + * .. index:: Backend, PHP-API, NotScanned, ext:core + * + * The directive itself is invisible in the rendered page; entries are + * collected project-wide to build the `genindex` page. + * + * @link https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-index + */ +final class IndexDirective extends BaseDirective { + /** + * A colon-prefixed segment whose prefix is this close (or closer) to a + * real type name is very likely a typo of it, e.g. "sindle:" -> "single:" + * (distance 1). Anything further away, e.g. "ext:" in "ext:core", is + * left alone as an intentional literal colon -- the comma-separated, + * type-less form (see class docblock) routinely contains those. + */ + private const TYPO_DISTANCE_THRESHOLD = 2; + + public function __construct(private readonly LoggerInterface $logger) + { + } + public function getName(): string { return 'index'; } + + /** {@inheritDoc} */ + public function process( + BlockContext $blockContext, + Directive $directive, + ): Node|null { + $data = trim($directive->getData()); + $lines = $data !== '' ? [$data] : $blockContext->getDocumentIterator()->toArray(); + $lines = array_values(array_filter(array_map(trim(...), $lines), static fn (string $line): bool => $line !== '')); + + $segments = []; + foreach ($lines as $line) { + foreach (explode(',', $line) as $segment) { + $segments[] = $segment; + } + } + + $segments = array_values(array_filter(array_map(trim(...), $segments), static fn (string $segment): bool => $segment !== '')); + + return new IndexNode(array_map( + fn (string $segment): IndexEntryNode => $this->parseLine($segment, $blockContext), + $segments, + )); + } + + private function parseLine(string $line, BlockContext $blockContext): IndexEntryNode + { + $type = IndexEntryType::Single; + + $colonPosition = mb_strpos($line, ':'); + if ($colonPosition !== false) { + $candidate = strtolower(trim(mb_substr($line, 0, $colonPosition))); + $candidateType = IndexEntryType::tryFrom($candidate); + if ($candidateType !== null) { + $type = $candidateType; + $line = trim(mb_substr($line, $colonPosition + 1)); + } else { + $this->warnIfLikelyTypo($candidate, $line, $blockContext); + } + } + + $main = false; + if (str_starts_with($line, '!')) { + $main = true; + $line = trim(substr($line, 1)); + } + + $parts = array_map(trim(...), explode(';', $line)); + + return new IndexEntryNode($type, $parts, $main); + } + + private function warnIfLikelyTypo(string $candidate, string $line, BlockContext $blockContext): void + { + $closestType = null; + $closestDistance = null; + foreach (IndexEntryType::cases() as $case) { + $distance = levenshtein($candidate, $case->value); + if ($closestDistance !== null && $distance >= $closestDistance) { + continue; + } + + $closestType = $case; + $closestDistance = $distance; + } + + if ($closestType === null || $closestDistance > self::TYPO_DISTANCE_THRESHOLD) { + return; + } + + $this->logger->warning( + sprintf( + '.. index:: "%s:" is not a known entry type, did you mean "%s:"? Treating it as a literal term instead: "%s"', + $candidate, + $closestType->value, + $line, + ), + $blockContext->getLoggerInformation(), + ); + } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/FieldList/TemplateFieldListItemRule.php b/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/FieldList/TemplateFieldListItemRule.php new file mode 100644 index 000000000..18da2044a --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/FieldList/TemplateFieldListItemRule.php @@ -0,0 +1,34 @@ +getTerm()) === 'template'; + } + + public function apply(FieldListItemNode $fieldListItemNode, BlockContext $blockContext): MetadataNode + { + return new TemplateNode($fieldListItemNode->getPlaintextContent()); + } +} diff --git a/packages/guides-restructured-text/tests/unit/Directives/IndexDirectiveTest.php b/packages/guides-restructured-text/tests/unit/Directives/IndexDirectiveTest.php new file mode 100644 index 000000000..5e0acc35e --- /dev/null +++ b/packages/guides-restructured-text/tests/unit/Directives/IndexDirectiveTest.php @@ -0,0 +1,85 @@ +logHandler = new TestHandler(); + $logger = new Logger('test'); + $logger->pushHandler($this->logHandler); + $this->directive = new IndexDirective($logger); + } + + #[DataProvider('typoProvider')] + public function testLikelyTypoLogsWarning(string $typo, string $suggestion): void + { + $node = $this->directive->process( + $this->createContext(''), + new Directive('', 'index', $typo . ': foo'), + ); + + self::assertInstanceOf(IndexNode::class, $node); + self::assertCount(1, $node->getEntries()); + self::assertSame(IndexEntryType::Single, $node->getEntries()[0]->getType()); + + self::assertTrue($this->logHandler->hasWarningThatContains( + 'not a known entry type, did you mean "' . $suggestion . ':"?', + )); + } + + /** @return array */ + public static function typoProvider(): array + { + return [ + 'single letter dropped' => ['singl', 'single'], + 'transposed letters' => ['sindle', 'single'], + 'extra letter' => ['pairr', 'pair'], + ]; + } + + public function testUnrelatedLiteralColonDoesNotLogAnything(): void + { + $node = $this->directive->process( + $this->createContext(''), + new Directive('', 'index', 'ext:core'), + ); + + self::assertInstanceOf(IndexNode::class, $node); + self::assertFalse($this->logHandler->hasWarningRecords()); + } + + public function testKnownEntryTypeDoesNotLogAnything(): void + { + $node = $this->directive->process( + $this->createContext(''), + new Directive('', 'index', 'single: foo'), + ); + + self::assertInstanceOf(IndexNode::class, $node); + self::assertFalse($this->logHandler->hasWarningRecords()); + } +} diff --git a/packages/guides/resources/config/guides.php b/packages/guides/resources/config/guides.php index 5196a65b6..949011ca6 100644 --- a/packages/guides/resources/config/guides.php +++ b/packages/guides/resources/config/guides.php @@ -15,9 +15,13 @@ use phpDocumentor\Guides\EventListener\LoadSettingsFromComposer; use phpDocumentor\Guides\NodeRenderers\Html\BreadCrumbNodeRenderer; use phpDocumentor\Guides\NodeRenderers\Html\DocumentNodeRenderer; +use phpDocumentor\Guides\NodeRenderers\Html\GenIndexNodeRenderer; +use phpDocumentor\Guides\NodeRenderers\Html\GenIndexRowNodeRenderer; +use phpDocumentor\Guides\NodeRenderers\Html\GenIndexTermNodeRenderer; use phpDocumentor\Guides\NodeRenderers\Html\MenuEntryRenderer; use phpDocumentor\Guides\NodeRenderers\Html\MenuNodeRenderer; use phpDocumentor\Guides\NodeRenderers\Html\TableNodeRenderer; +use phpDocumentor\Guides\NodeRenderers\Html\TemplateMetadataNodeRenderer; use phpDocumentor\Guides\NodeRenderers\OutputAwareDelegatingNodeRenderer; use phpDocumentor\Guides\Parser; use phpDocumentor\Guides\ReferenceResolvers\AnchorHyperlinkResolver; @@ -198,6 +202,14 @@ ->set(DocumentNodeRenderer::class) ->tag('phpdoc.guides.noderenderer.html') + ->set(GenIndexNodeRenderer::class) + ->tag('phpdoc.guides.noderenderer.html') + ->set(GenIndexTermNodeRenderer::class) + ->tag('phpdoc.guides.noderenderer.html') + ->set(GenIndexRowNodeRenderer::class) + ->tag('phpdoc.guides.noderenderer.html') + ->set(TemplateMetadataNodeRenderer::class) + ->tag('phpdoc.guides.noderenderer.html') ->set(TableNodeRenderer::class) ->tag('phpdoc.guides.noderenderer.html') ->set(MenuNodeRenderer::class) diff --git a/packages/guides/resources/template/html/body/genindex.html.twig b/packages/guides/resources/template/html/body/genindex.html.twig new file mode 100644 index 000000000..0c61952fc --- /dev/null +++ b/packages/guides/resources/template/html/body/genindex.html.twig @@ -0,0 +1,52 @@ +{% if node.showLetterIndex %} +{% set currentLetter = null %} +
+{% for term in node.terms %} +{% set letter = term.term|slice(0, 1)|upper %} +{% if letter != currentLetter %} + {{ letter }} +{% set currentLetter = letter %} +{% endif %} +{% endfor %} +
+ +{% set currentLetter = null %} +{% for term in node.terms %} +{% set letter = term.term|slice(0, 1)|upper %} +{% if letter != currentLetter %} +{% if currentLetter is not null %} + + + + +{% endif %} + +

{{ letter }}

+ + + + + +
+
+{% set currentLetter = letter %} +{% endif %} +{{ renderNode(term) }} +{% endfor %} +{% if currentLetter is not null %} +
+
+{% endif %} +{% elseif node.terms|length %} + + + + +
+
+{% for term in node.terms %} +{{ renderNode(term) }} +{% endfor %} +
+
+{% endif %} diff --git a/packages/guides/resources/template/html/body/genindex/row.html.twig b/packages/guides/resources/template/html/body/genindex/row.html.twig new file mode 100644 index 000000000..2e1d2fd8e --- /dev/null +++ b/packages/guides/resources/template/html/body/genindex/row.html.twig @@ -0,0 +1,10 @@ +{% if node.isLink %} +{{ renderNode(node.reference) }} +{% else %} +{{ node.isSeeAlso ? 'see also' : 'see' }} +{% if node.reference %} +{{ renderNode(node.reference) }} +{% else %} +{{ node.seeText }} +{% endif %} +{% endif %} diff --git a/packages/guides/resources/template/html/body/genindex/term.html.twig b/packages/guides/resources/template/html/body/genindex/term.html.twig new file mode 100644 index 000000000..7137ed83f --- /dev/null +++ b/packages/guides/resources/template/html/body/genindex/term.html.twig @@ -0,0 +1,15 @@ +
{{ node.term }}
+{% for row in node.rows %} +
+{{ renderNode(row) }} +
+{% endfor %} +{% if node.subterms|length %} +
+
+{% for subterm in node.subterms %} +{{ renderNode(subterm) }} +{% endfor %} +
+
+{% endif %} diff --git a/packages/guides/resources/template/html/structure/section.html.twig b/packages/guides/resources/template/html/structure/section.html.twig index f2915af78..224b1e264 100644 --- a/packages/guides/resources/template/html/structure/section.html.twig +++ b/packages/guides/resources/template/html/structure/section.html.twig @@ -1,4 +1,4 @@ -
+
{% for childNode in node.children %} {{ renderNode(childNode) }} {% endfor %} diff --git a/packages/guides/src/Compiler/Passes/AutomaticMenuPass.php b/packages/guides/src/Compiler/Passes/AutomaticMenuPass.php index 077578628..40ffcf1a0 100644 --- a/packages/guides/src/Compiler/Passes/AutomaticMenuPass.php +++ b/packages/guides/src/Compiler/Passes/AutomaticMenuPass.php @@ -36,7 +36,10 @@ public function __construct( public function getPriority(): int { - return 20; // must be run very late + // Must run very late, and strictly before GlobalMenuPass (21, not the same + // 20): GlobalMenuPass reads the DocumentEntry parent/child tree this pass + // builds. + return 21; } /** diff --git a/packages/guides/src/Compiler/Passes/IndexCollectorPass.php b/packages/guides/src/Compiler/Passes/IndexCollectorPass.php new file mode 100644 index 000000000..487eb7e79 --- /dev/null +++ b/packages/guides/src/Compiler/Passes/IndexCollectorPass.php @@ -0,0 +1,600 @@ +} + * @phpstan-type GenIndexTermData array{term: string, rows: array, subterms: array} + * @phpstan-type GenIndexTermMap array + */ +final class IndexCollectorPass implements CompilerPass +{ + public function __construct(private readonly LoggerInterface $logger) + { + } + + public function getPriority(): int + { + // Must run after the document tree is otherwise final -- after the + // priority-20 passes + return 10; + } + + /** + * @param DocumentNode[] $documents + * + * @return DocumentNode[] + */ + public function run(array $documents, CompilerContextInterface $compilerContext): array + { + $termMap = []; + foreach ($documents as $document) { + foreach ($this->collectFromDocument($document) as [$entry, $anchor, $title]) { + $this->expandEntry($entry, $anchor, $title, $termMap, $document); + } + } + + if ($termMap === []) { + return $documents; + } + + $this->resolveSeeRows($termMap); + $terms = $this->toTermNodes($termMap); + + foreach ($documents as $document) { + if ($document->getTemplate() === 'genindex') { + $target = $this->findRootSection($document) ?? $document; + $target->addChildNode(new GenIndexNode($terms)); + } + + foreach ($this->findGenIndexNodes($document) as $placeholder) { + $prefixes = $placeholder->getPathPrefixes(); + $scoped = $prefixes === [] ? $terms : $this->toTermNodes($this->filterTermMap($termMap, $prefixes)); + $placeholder->setValue($scoped); + } + } + + return $documents; + } + + /** + * Finds every GenIndexNode already in a document's tree -- the empty + * placeholders GenIndexDirective leaves behind at parse time -- so they + * can be populated now that the project-wide term data actually exists. + * + * @return GenIndexNode[] + */ + private function findGenIndexNodes(Node $node): array + { + if ($node instanceof GenIndexNode) { + return [$node]; + } + + if (!($node instanceof CompoundNode)) { + return []; + } + + $found = []; + foreach ($node->getChildren() as $child) { + foreach ($this->findGenIndexNodes($child) as $item) { + $found[] = $item; + } + } + + return $found; + } + + /** + * A single-pass parser attaches a `.. index::` block to whichever section is + * still open when it's parsed, which is usually the *previous* section, since + * the block conventionally sits right before the heading it documents. So + * anchor resolution can't rely on tree ancestry — it needs the node's true + * document-order position: flatten the whole document first, then for each + * index entry look forward for the next heading. An index block with no + * heading anywhere after it (e.g. the trailing `.. index::` line TYPO3 Core + * Changelog files end with) isn't "attached" to whatever subsection happened + * to be last -- it applies to the page as a whole, so it falls back to the + * document's own uppermost section instead. + * + * @return array + */ + private function collectFromDocument(DocumentNode $document): array + { + $flat = []; + $this->flatten($document, $flat); + + $found = []; + foreach ($flat as $index => [$type, $node]) { + if ($type !== 'index') { + continue; + } + + $section = $this->findNextSection($flat, $index) ?? $this->findRootSection($document); + [$anchor, $title] = $this->resolveAnchor($section, $document); + foreach ($node->getEntries() as $entry) { + $found[] = [$entry, $anchor, $title]; + foreach ($entry->getParts() as $part) { + $section?->addIndexTerm($part); + } + } + } + + return $found; + } + + /** @param array $flat */ + private function flatten(Node $node, array &$flat): void + { + if ($node instanceof IndexNode) { + $flat[] = ['index', $node]; + + return; + } + + if ($node instanceof SectionNode) { + $flat[] = ['section', $node]; + foreach ($node->getChildren() as $child) { + $this->flatten($child, $flat); + } + + return; + } + + if ($node instanceof DocumentNode) { + foreach ($node->getChildren() as $child) { + $this->flatten($child, $flat); + } + + return; + } + + // Anything else (titles, paragraphs, code blocks, directives, ...) is real + // content: it blocks an index entry's lookahead to a later heading. + $flat[] = ['content', $node]; + } + + /** @param array $flat */ + private function findNextSection(array $flat, int $afterIndex): SectionNode|null + { + $count = count($flat); + for ($i = $afterIndex + 1; $i < $count; $i++) { + [$type, $node] = $flat[$i]; + if ($type === 'section' && $node instanceof SectionNode) { + return $node; + } + + if ($type === 'content') { + return null; + } + } + + return null; + } + + /** @return array{0: string|null, 1: string} */ + private function resolveAnchor(SectionNode|null $section, DocumentNode $document): array + { + if ($section !== null) { + return [$section->getId(), $section->getLinkText()]; + } + + $title = $document->getTitle(); + if ($title !== null) { + return [$title->getId(), $title->toString()]; + } + + return [null, $document->getPageTitle() ?? '']; + } + + private function findRootSection(DocumentNode $document): SectionNode|null + { + foreach ($document->getChildren() as $child) { + if ($child instanceof SectionNode) { + return $child; + } + } + + return null; + } + + /** + * Expands one parsed `.. index::` line into one or more term/subterm + * insertions, following Sphinx's `pair`/`triple`/`module` conventions. + * + * @param GenIndexTermMap $termMap + */ + private function expandEntry(IndexEntryNode $entry, string|null $anchor, string $title, array &$termMap, DocumentNode $document): void + { + $parts = $entry->getParts(); + $filePath = $document->getFilePath(); + $row = [ + 'kind' => GenIndexRowKind::Link, + 'main' => $entry->isMain(), + 'anchor' => $anchor, + 'title' => $title, + 'seeText' => null, + 'filePath' => $filePath, + ]; + + switch ($entry->getType()) { + case IndexEntryType::Single: + $this->checkPartCount($entry, 1, 2, $document); + if (count($parts) >= 2) { + $this->addEntry($termMap, $parts[0], $parts[1], $row); + } elseif (count($parts) === 1) { + $this->addEntry($termMap, $parts[0], null, $row); + } + + break; + + case IndexEntryType::Module: + $this->checkPartCount($entry, 1, 1, $document); + if (count($parts) >= 1) { + $this->addEntry($termMap, 'module', $parts[0], $row); + } + + break; + + case IndexEntryType::Pair: + $this->checkPartCount($entry, 2, 2, $document); + if (count($parts) >= 2) { + $this->addEntry($termMap, $parts[0], $parts[1], $row); + $this->addEntry($termMap, $parts[1], $parts[0], $row); + } + + break; + + case IndexEntryType::Triple: + $this->checkPartCount($entry, 3, 3, $document); + if (count($parts) >= 3) { + [$a, $b, $c] = $parts; + $this->addEntry($termMap, $a, $b . ' ' . $c, $row); + $this->addEntry($termMap, $b, $c . ', ' . $a, $row); + $this->addEntry($termMap, $c, $a . ' ' . $b, $row); + } + + break; + + case IndexEntryType::See: + case IndexEntryType::SeeAlso: + $this->checkPartCount($entry, 2, 2, $document); + if (count($parts) >= 2) { + $seeRow = [ + 'kind' => $entry->getType() === IndexEntryType::See ? GenIndexRowKind::See : GenIndexRowKind::SeeAlso, + 'main' => false, + 'anchor' => null, + 'title' => null, + 'seeText' => $parts[1], + 'filePath' => $filePath, + ]; + $this->addEntry($termMap, $parts[0], null, $seeRow); + } + + break; + } + } + + /** + * `.. index::` entry types only ever use a fixed number of parts (e.g. a + * `pair:` entry uses exactly 2). Too many silently drops the extras; + * too few is worse -- the whole entry is silently skipped, since none of + * the `count($parts) >= N` checks above are satisfied at all. Both are + * silent data loss unless we say something. + */ + private function checkPartCount(IndexEntryNode $entry, int $minParts, int $maxParts, DocumentNode $document): void + { + $parts = $entry->getParts(); + $count = count($parts); + + if ($count < $minParts) { + $this->logger->warning( + sprintf( + '.. index:: %s entry needs at least %d part(s), but only got %d; ignoring the whole entry: "%s"', + $entry->getType()->value, + $minParts, + $count, + implode('; ', $parts), + ), + $document->getLoggerInformation(), + ); + + return; + } + + if ($count <= $maxParts) { + return; + } + + $this->logger->warning( + sprintf( + '.. index:: %s entry has %d part(s), but only %d are used for this type; ignoring extra part(s): "%s"', + $entry->getType()->value, + $count, + $maxParts, + implode('; ', array_slice($parts, $maxParts)), + ), + $document->getLoggerInformation(), + ); + } + + /** + * @param GenIndexTermMap $termMap + * @param GenIndexRowData $row + */ + private function addEntry(array &$termMap, string $term, string|null $subterm, array $row): void + { + $key = $this->normalize($term); + if (!isset($termMap[$key])) { + $termMap[$key] = ['term' => $term, 'rows' => [], 'subterms' => []]; + } + + if ($subterm === null) { + $termMap[$key]['rows'][] = $row; + + return; + } + + $subKey = $this->normalize($subterm); + if (!isset($termMap[$key]['subterms'][$subKey])) { + $termMap[$key]['subterms'][$subKey] = ['term' => $subterm, 'rows' => []]; + } + + $termMap[$key]['subterms'][$subKey]['rows'][] = $row; + } + + /** @param GenIndexTermMap $termMap */ + private function resolveSeeRows(array &$termMap): void + { + foreach ($termMap as $key => $term) { + $termMap[$key]['rows'] = $this->resolveSeeRowTargets($term['rows'], $termMap); + foreach ($term['subterms'] as $subKey => $subterm) { + $termMap[$key]['subterms'][$subKey]['rows'] = $this->resolveSeeRowTargets($subterm['rows'], $termMap); + } + } + } + + /** + * @param array $rows + * @param GenIndexTermMap $termMap + * + * @return array + */ + private function resolveSeeRowTargets(array $rows, array $termMap): array + { + foreach ($rows as $index => $row) { + if ($row['kind'] === GenIndexRowKind::Link) { + continue; + } + + $targetKey = $this->normalize($row['seeText'] ?? ''); + $targetLinkRow = $this->findFirstLinkRow($termMap[$targetKey] ?? null); + if ($targetLinkRow === null) { + continue; + } + + $rows[$index]['anchor'] = $targetLinkRow['anchor']; + } + + return $rows; + } + + /** + * @param GenIndexTermData|null $term + * + * @return GenIndexRowData|null + */ + private function findFirstLinkRow(array|null $term): array|null + { + if ($term === null) { + return null; + } + + $row = $this->findFirstLinkRowInRows($term['rows']); + if ($row !== null) { + return $row; + } + + foreach ($term['subterms'] as $subterm) { + $row = $this->findFirstLinkRowInRows($subterm['rows']); + if ($row !== null) { + return $row; + } + } + + return null; + } + + /** + * @param array $rows + * + * @return GenIndexRowData|null + */ + private function findFirstLinkRowInRows(array $rows): array|null + { + foreach ($rows as $row) { + if ($row['kind'] === GenIndexRowKind::Link) { + return $row; + } + } + + return null; + } + + /** + * Restricts a (already see-resolved) term map to rows originating from a + * document under one of $pathPrefixes, dropping any term or subterm left + * with no rows and no surviving subterms of its own. `see`/`seealso` rows + * are filtered by where the entry itself was written, not by what it + * resolves to -- a target found outside the visible scope still links + * correctly, it just isn't itself listed as a separate row here. + * + * @param GenIndexTermMap $termMap + * @param string[] $pathPrefixes + * + * @return GenIndexTermMap + */ + private function filterTermMap(array $termMap, array $pathPrefixes): array + { + $filtered = []; + foreach ($termMap as $key => $term) { + $rows = $this->filterRowsByPath($term['rows'], $pathPrefixes); + + $subterms = []; + foreach ($term['subterms'] as $subKey => $subterm) { + $subRows = $this->filterRowsByPath($subterm['rows'], $pathPrefixes); + if ($subRows === []) { + continue; + } + + $subterms[$subKey] = ['term' => $subterm['term'], 'rows' => $subRows]; + } + + if ($rows === [] && $subterms === []) { + continue; + } + + $filtered[$key] = ['term' => $term['term'], 'rows' => $rows, 'subterms' => $subterms]; + } + + return $filtered; + } + + /** + * @param array $rows + * @param string[] $pathPrefixes + * + * @return array + */ + private function filterRowsByPath(array $rows, array $pathPrefixes): array + { + return array_values(array_filter($rows, static function (array $row) use ($pathPrefixes): bool { + foreach ($pathPrefixes as $prefix) { + if (str_starts_with($row['filePath'], $prefix)) { + return true; + } + } + + return false; + })); + } + + /** + * @param GenIndexTermMap $termMap + * + * @return GenIndexTerm[] + */ + private function toTermNodes(array $termMap): array + { + $terms = []; + foreach ($termMap as $data) { + $terms[] = $this->toTermNode($data); + } + + usort($terms, static fn (GenIndexTerm $a, GenIndexTerm $b): int => strcasecmp($a->getTerm(), $b->getTerm())); + + return $terms; + } + + /** @param GenIndexTermData $data */ + private function toTermNode(array $data): GenIndexTerm + { + $subterms = []; + foreach ($data['subterms'] as $subtermData) { + $subterms[] = $this->toTermNode(['term' => $subtermData['term'], 'rows' => $subtermData['rows'], 'subterms' => []]); + } + + usort($subterms, static fn (GenIndexTerm $a, GenIndexTerm $b): int => strcasecmp($a->getTerm(), $b->getTerm())); + + return new GenIndexTerm($data['term'], $this->toRowNodes($data['rows']), $subterms); + } + + /** + * @param array $rows + * + * @return GenIndexRow[] + */ + private function toRowNodes(array $rows): array + { + usort( + $rows, + static fn (array $a, array $b): int => ($a['kind'] === GenIndexRowKind::Link ? 1 : 0) <=> ($b['kind'] === GenIndexRowKind::Link ? 1 : 0), + ); + + $nodes = []; + foreach ($rows as $row) { + $reference = null; + if ($row['anchor'] !== null) { + $text = $row['kind'] === GenIndexRowKind::Link ? $row['title'] : $row['seeText']; + $reference = new ReferenceNode($row['anchor'], [new PlainTextInlineNode($text ?? '')]); + if ($row['main']) { + $reference->setClasses(['main-entry']); + } + } + + $nodes[] = new GenIndexRow($row['kind'], $row['main'], $reference, $row['seeText']); + } + + return $nodes; + } + + private function normalize(string $term): string + { + return mb_strtolower(trim($term)); + } +} diff --git a/packages/guides/src/NodeRenderers/Html/GenIndexNodeRenderer.php b/packages/guides/src/NodeRenderers/Html/GenIndexNodeRenderer.php new file mode 100644 index 000000000..ad865f7b8 --- /dev/null +++ b/packages/guides/src/NodeRenderers/Html/GenIndexNodeRenderer.php @@ -0,0 +1,44 @@ + */ +final class GenIndexNodeRenderer implements NodeRenderer +{ + public function __construct(private readonly TemplateRenderer $renderer) + { + } + + public function render(Node $node, RenderContext $renderContext): string + { + return $this->renderer->renderTemplate( + $renderContext, + 'body/genindex.html.twig', + ['node' => $node], + ); + } + + public function supports(string $nodeFqcn): bool + { + return $nodeFqcn === GenIndexNode::class || is_a($nodeFqcn, GenIndexNode::class, true); + } +} diff --git a/packages/guides/src/NodeRenderers/Html/GenIndexRowNodeRenderer.php b/packages/guides/src/NodeRenderers/Html/GenIndexRowNodeRenderer.php new file mode 100644 index 000000000..4e27950f0 --- /dev/null +++ b/packages/guides/src/NodeRenderers/Html/GenIndexRowNodeRenderer.php @@ -0,0 +1,44 @@ + */ +final class GenIndexRowNodeRenderer implements NodeRenderer +{ + public function __construct(private readonly TemplateRenderer $renderer) + { + } + + public function render(Node $node, RenderContext $renderContext): string + { + return $this->renderer->renderTemplate( + $renderContext, + 'body/genindex/row.html.twig', + ['node' => $node], + ); + } + + public function supports(string $nodeFqcn): bool + { + return $nodeFqcn === GenIndexRow::class || is_a($nodeFqcn, GenIndexRow::class, true); + } +} diff --git a/packages/guides/src/NodeRenderers/Html/GenIndexTermNodeRenderer.php b/packages/guides/src/NodeRenderers/Html/GenIndexTermNodeRenderer.php new file mode 100644 index 000000000..8c4b4bd68 --- /dev/null +++ b/packages/guides/src/NodeRenderers/Html/GenIndexTermNodeRenderer.php @@ -0,0 +1,44 @@ + */ +final class GenIndexTermNodeRenderer implements NodeRenderer +{ + public function __construct(private readonly TemplateRenderer $renderer) + { + } + + public function render(Node $node, RenderContext $renderContext): string + { + return $this->renderer->renderTemplate( + $renderContext, + 'body/genindex/term.html.twig', + ['node' => $node], + ); + } + + public function supports(string $nodeFqcn): bool + { + return $nodeFqcn === GenIndexTerm::class || is_a($nodeFqcn, GenIndexTerm::class, true); + } +} diff --git a/packages/guides/src/NodeRenderers/Html/TemplateMetadataNodeRenderer.php b/packages/guides/src/NodeRenderers/Html/TemplateMetadataNodeRenderer.php new file mode 100644 index 000000000..52c83e26d --- /dev/null +++ b/packages/guides/src/NodeRenderers/Html/TemplateMetadataNodeRenderer.php @@ -0,0 +1,40 @@ + + */ +final class TemplateMetadataNodeRenderer implements NodeRenderer +{ + public function render(Node $node, RenderContext $renderContext): string + { + return ''; + } + + public function supports(string $nodeFqcn): bool + { + return $nodeFqcn === TemplateNode::class || is_a($nodeFqcn, TemplateNode::class, true); + } +} diff --git a/packages/guides/src/Nodes/DocumentNode.php b/packages/guides/src/Nodes/DocumentNode.php index fad13d235..2552820b1 100644 --- a/packages/guides/src/Nodes/DocumentNode.php +++ b/packages/guides/src/Nodes/DocumentNode.php @@ -20,6 +20,7 @@ use phpDocumentor\Guides\Nodes\Menu\TocNode; use phpDocumentor\Guides\Nodes\Metadata\MetadataNode; use phpDocumentor\Guides\Nodes\Metadata\NavigationTitleNode; +use phpDocumentor\Guides\Nodes\Metadata\TemplateNode; use function array_filter; use function max; @@ -75,6 +76,7 @@ final class DocumentNode extends CompoundNode private SectionEntryNode|null $rootSectionEntry = null; private bool $isRoot = false; private bool $orphan = false; + private string|null $template = null; public function __construct( private readonly string $hash, @@ -144,6 +146,10 @@ public function addHeaderNode(MetadataNode $node): void $this->navigationTitle = $node->getValue(); } + if ($node instanceof TemplateNode) { + $this->template = $node->getValue(); + } + $this->headerNodes[] = $node; } @@ -329,4 +335,9 @@ public function setOrphan(bool $orphan): DocumentNode return $this; } + + public function getTemplate(): string|null + { + return $this->template; + } } diff --git a/packages/guides/src/Nodes/Index/GenIndexNode.php b/packages/guides/src/Nodes/Index/GenIndexNode.php new file mode 100644 index 000000000..6581ac976 --- /dev/null +++ b/packages/guides/src/Nodes/Index/GenIndexNode.php @@ -0,0 +1,65 @@ + + */ +final class GenIndexNode extends CompoundNode +{ + /** + * @param GenIndexTerm[] $terms + * @param string[] $pathPrefixes empty means unscoped (whole project) + */ + public function __construct( + array $terms, + private readonly array $pathPrefixes = [], + private readonly bool $showLetterIndex = true, + ) { + parent::__construct($terms); + } + + /** @return GenIndexTerm[] */ + public function getTerms(): array + { + return $this->value; + } + + /** @return string[] */ + public function getPathPrefixes(): array + { + return $this->pathPrefixes; + } + + /** + * Whether to group terms under an A-Z jumpbox + per-letter headings, or + * just list them flat. The letter grouping is of little use for a small + * list, e.g. a single changelog version's worth of terms. + */ + public function showLetterIndex(): bool + { + return $this->showLetterIndex; + } +} diff --git a/packages/guides/src/Nodes/Index/GenIndexRow.php b/packages/guides/src/Nodes/Index/GenIndexRow.php new file mode 100644 index 000000000..09318b5f3 --- /dev/null +++ b/packages/guides/src/Nodes/Index/GenIndexRow.php @@ -0,0 +1,71 @@ + + */ +final class GenIndexRow extends AbstractNode +{ + public function __construct( + private readonly GenIndexRowKind $kind, + private readonly bool $main = false, + private readonly ReferenceNode|null $reference = null, + private readonly string|null $seeText = null, + ) { + $this->value = $reference; + } + + public function getKind(): GenIndexRowKind + { + return $this->kind; + } + + public function isLink(): bool + { + return $this->kind === GenIndexRowKind::Link; + } + + public function isSee(): bool + { + return $this->kind === GenIndexRowKind::See; + } + + public function isSeeAlso(): bool + { + return $this->kind === GenIndexRowKind::SeeAlso; + } + + public function isMain(): bool + { + return $this->main; + } + + public function getReference(): ReferenceNode|null + { + return $this->reference; + } + + /** The literal term text a "see"/"seealso" row points at. */ + public function getSeeText(): string|null + { + return $this->seeText; + } +} diff --git a/packages/guides/src/Nodes/Index/GenIndexRowKind.php b/packages/guides/src/Nodes/Index/GenIndexRowKind.php new file mode 100644 index 000000000..3c3b70df1 --- /dev/null +++ b/packages/guides/src/Nodes/Index/GenIndexRowKind.php @@ -0,0 +1,24 @@ + + */ +final class GenIndexTerm extends AbstractNode +{ + /** + * @param GenIndexRow[] $rows flat rows: "see"/"seealso" first, then plain links + * @param GenIndexTerm[] $subterms one level of nested sub-entries + */ + public function __construct( + private readonly string $term, + private readonly array $rows, + private readonly array $subterms, + ) { + $this->value = $term; + } + + public function getTerm(): string + { + return $this->term; + } + + /** @return GenIndexRow[] */ + public function getRows(): array + { + return $this->rows; + } + + /** @return GenIndexTerm[] */ + public function getSubterms(): array + { + return $this->subterms; + } + + public function hasSubterms(): bool + { + return $this->subterms !== []; + } +} diff --git a/packages/guides/src/Nodes/Index/IndexEntryNode.php b/packages/guides/src/Nodes/Index/IndexEntryNode.php new file mode 100644 index 000000000..bff605cb3 --- /dev/null +++ b/packages/guides/src/Nodes/Index/IndexEntryNode.php @@ -0,0 +1,49 @@ + + */ +final class IndexEntryNode extends AbstractNode +{ + /** @param string[] $parts */ + public function __construct( + private readonly IndexEntryType $type, + array $parts, + private readonly bool $main = false, + ) { + $this->value = $parts; + } + + public function getType(): IndexEntryType + { + return $this->type; + } + + /** @return string[] */ + public function getParts(): array + { + return $this->value; + } + + public function isMain(): bool + { + return $this->main; + } +} diff --git a/packages/guides/src/Nodes/Index/IndexEntryType.php b/packages/guides/src/Nodes/Index/IndexEntryType.php new file mode 100644 index 000000000..9e559ed8a --- /dev/null +++ b/packages/guides/src/Nodes/Index/IndexEntryType.php @@ -0,0 +1,27 @@ +> + */ +final class IndexNode extends AbstractNode +{ + /** @param IndexEntryNode[] $entries */ + public function __construct(private readonly array $entries) + { + $this->value = []; + } + + /** @return IndexEntryNode[] */ + public function getEntries(): array + { + return $this->entries; + } +} diff --git a/packages/guides/src/Nodes/Metadata/TemplateNode.php b/packages/guides/src/Nodes/Metadata/TemplateNode.php new file mode 100644 index 000000000..5ddee976f --- /dev/null +++ b/packages/guides/src/Nodes/Metadata/TemplateNode.php @@ -0,0 +1,25 @@ + */ final class SectionNode extends CompoundNode implements LinkTargetNode @@ -21,11 +22,34 @@ final class SectionNode extends CompoundNode implements LinkTargetNode public const STD_LABEL = 'std:label'; public const STD_TITLE = 'std:title'; + /** @var string[] */ + private array $indexTerms = []; + public function __construct(private readonly TitleNode $title) { parent::__construct([$title]); } + /** + * Records that a `.. index::` entry resolved to this section, e.g. so a + * theme's section template can render it as a search-key data attribute. + * Idempotent -- adding the same term twice has no extra effect. + */ + public function addIndexTerm(string $term): void + { + if (in_array($term, $this->indexTerms, true)) { + return; + } + + $this->indexTerms[] = $term; + } + + /** @return string[] */ + public function getIndexTerms(): array + { + return $this->indexTerms; + } + public function getTitle(): TitleNode { return $this->title; diff --git a/packages/guides/tests/unit/Compiler/Passes/IndexCollectorPassTest.php b/packages/guides/tests/unit/Compiler/Passes/IndexCollectorPassTest.php new file mode 100644 index 000000000..801f8f12c --- /dev/null +++ b/packages/guides/tests/unit/Compiler/Passes/IndexCollectorPassTest.php @@ -0,0 +1,111 @@ +genIndexDocument([ + new IndexEntryNode(IndexEntryType::Single, ['valid']), + new IndexEntryNode(IndexEntryType::Pair, ['onlyonepart']), + ]); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::once()) + ->method('warning') + ->with( + '.. index:: pair entry needs at least 2 part(s), but only got 1; ignoring the whole entry: "onlyonepart"', + self::anything(), + ); + + $pass = new IndexCollectorPass($logger); + [$result] = $pass->run([$document], new CompilerContext(new ProjectNode())); + + $terms = $this->getGenIndexTerms($result); + self::assertCount(1, $terms); + self::assertSame('valid', $terms[0]->getTerm()); + } + + public function testTooManyPartsLogsWarningAndIgnoresExtraParts(): void + { + $document = $this->genIndexDocument([ + new IndexEntryNode(IndexEntryType::Pair, ['a', 'b', 'c']), + ]); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::once()) + ->method('warning') + ->with( + '.. index:: pair entry has 3 part(s), but only 2 are used for this type; ignoring extra part(s): "c"', + self::anything(), + ); + + $pass = new IndexCollectorPass($logger); + [$result] = $pass->run([$document], new CompilerContext(new ProjectNode())); + + $terms = $this->getGenIndexTerms($result); + self::assertEqualsCanonicalizing( + ['a', 'b'], + array_map(static fn (GenIndexTerm $term): string => $term->getTerm(), $terms), + ); + } + + public function testWellFormedEntryDoesNotLogAnything(): void + { + $document = $this->genIndexDocument([ + new IndexEntryNode(IndexEntryType::Pair, ['a', 'b']), + ]); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::never())->method('warning'); + + $pass = new IndexCollectorPass($logger); + $pass->run([$document], new CompilerContext(new ProjectNode())); + } + + /** @param IndexEntryNode[] $entries */ + private function genIndexDocument(array $entries): DocumentNode + { + $document = new DocumentNode('1', 'index'); + $document->addHeaderNode(new TemplateNode('genindex')); + $document->addChildNode(new IndexNode($entries)); + + return $document; + } + + /** @return GenIndexTerm[] */ + private function getGenIndexTerms(DocumentNode $document): array + { + foreach ($document->getNodes(GenIndexNode::class) as $node) { + return $node->getTerms(); + } + + self::fail('Expected a GenIndexNode to be present.'); + } +} diff --git a/tests/Integration/tests/directives/directive-genindex-scoped/expected/Changelog/12.3/Feature-100-OldThing.html b/tests/Integration/tests/directives/directive-genindex-scoped/expected/Changelog/12.3/Feature-100-OldThing.html new file mode 100644 index 000000000..3b2ab2158 --- /dev/null +++ b/tests/Integration/tests/directives/directive-genindex-scoped/expected/Changelog/12.3/Feature-100-OldThing.html @@ -0,0 +1,9 @@ + +
+

Feature: #100 - Old Thing

+
+

Description

+

Something was added in 12.3.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-genindex-scoped/expected/Changelog/12.4/Breaking-300-RemovedThing.html b/tests/Integration/tests/directives/directive-genindex-scoped/expected/Changelog/12.4/Breaking-300-RemovedThing.html new file mode 100644 index 000000000..354cb02b6 --- /dev/null +++ b/tests/Integration/tests/directives/directive-genindex-scoped/expected/Changelog/12.4/Breaking-300-RemovedThing.html @@ -0,0 +1,9 @@ + +
+

Breaking: #300 - Removed Thing

+
+

Description

+

Something was removed in 12.4.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-genindex-scoped/expected/Changelog/12.4/Feature-200-NewThing.html b/tests/Integration/tests/directives/directive-genindex-scoped/expected/Changelog/12.4/Feature-200-NewThing.html new file mode 100644 index 000000000..0ee7573fd --- /dev/null +++ b/tests/Integration/tests/directives/directive-genindex-scoped/expected/Changelog/12.4/Feature-200-NewThing.html @@ -0,0 +1,9 @@ + +
+

Feature: #200 - New Thing

+
+

Description

+

Something was added in 12.4.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-genindex-scoped/expected/index.html b/tests/Integration/tests/directives/directive-genindex-scoped/expected/index.html new file mode 100644 index 000000000..c92556d19 --- /dev/null +++ b/tests/Integration/tests/directives/directive-genindex-scoped/expected/index.html @@ -0,0 +1,220 @@ + +
+

Changelog

+

Simulates a TYPO3-style Changelog with per-version directories, demonstrating +the scoped .. genindex:: directive: two scoped listings (one per version) +and one unscoped listing, all on the same page.

+ +
+

Index For 12.4

+ +
+ B + E + J + P +
+ +

B

+ + + + + +
+
+
Backend
+
+ Feature: #200 - New Thing +
+
+
+ +

E

+ + + + + +
+
+
ext:core
+
+ Breaking: #300 - Removed Thing +
+
+
+ +

J

+ + + + + +
+
+
JavaScript
+
+ Feature: #200 - New Thing +
+
+
+ +

P

+ + + + + +
+
+
PHP-API
+
+ Breaking: #300 - Removed Thing +
+
+
+
+
+

Index For 12.3

+ +
+ B + P +
+ +

B

+ + + + + +
+
+
Backend
+
+ Feature: #100 - Old Thing +
+
+
+ +

P

+ + + + + +
+
+
PHP-API
+
+ Feature: #100 - Old Thing +
+
+
+
+
+

Full Index

+ +
+ B + E + J + P +
+ +

B

+ + + + + +
+
+
Backend
+
+ Feature: #100 - Old Thing +
+
+ Feature: #200 - New Thing +
+
+
+ +

E

+ + + + + +
+
+
ext:core
+
+ Breaking: #300 - Removed Thing +
+
+
+ +

J

+ + + + + +
+
+
JavaScript
+
+ Feature: #200 - New Thing +
+
+
+ +

P

+ + + + + +
+
+
PHP-API
+
+ Feature: #100 - Old Thing +
+
+ Breaking: #300 - Removed Thing +
+
+
+
+
+ diff --git a/tests/Integration/tests/directives/directive-genindex-scoped/input/Changelog/12.3/Feature-100-OldThing.rst b/tests/Integration/tests/directives/directive-genindex-scoped/input/Changelog/12.3/Feature-100-OldThing.rst new file mode 100644 index 000000000..75c2beac1 --- /dev/null +++ b/tests/Integration/tests/directives/directive-genindex-scoped/input/Changelog/12.3/Feature-100-OldThing.rst @@ -0,0 +1,10 @@ +=========================== +Feature: #100 - Old Thing +=========================== + +Description +=========== + +Something was added in 12.3. + +.. index:: Backend, PHP-API diff --git a/tests/Integration/tests/directives/directive-genindex-scoped/input/Changelog/12.4/Breaking-300-RemovedThing.rst b/tests/Integration/tests/directives/directive-genindex-scoped/input/Changelog/12.4/Breaking-300-RemovedThing.rst new file mode 100644 index 000000000..4e88df8fa --- /dev/null +++ b/tests/Integration/tests/directives/directive-genindex-scoped/input/Changelog/12.4/Breaking-300-RemovedThing.rst @@ -0,0 +1,10 @@ +=============================== +Breaking: #300 - Removed Thing +=============================== + +Description +=========== + +Something was removed in 12.4. + +.. index:: PHP-API, ext:core diff --git a/tests/Integration/tests/directives/directive-genindex-scoped/input/Changelog/12.4/Feature-200-NewThing.rst b/tests/Integration/tests/directives/directive-genindex-scoped/input/Changelog/12.4/Feature-200-NewThing.rst new file mode 100644 index 000000000..55d6b4f5d --- /dev/null +++ b/tests/Integration/tests/directives/directive-genindex-scoped/input/Changelog/12.4/Feature-200-NewThing.rst @@ -0,0 +1,10 @@ +=========================== +Feature: #200 - New Thing +=========================== + +Description +=========== + +Something was added in 12.4. + +.. index:: Backend, JavaScript diff --git a/tests/Integration/tests/directives/directive-genindex-scoped/input/index.rst b/tests/Integration/tests/directives/directive-genindex-scoped/input/index.rst new file mode 100644 index 000000000..9d42e9880 --- /dev/null +++ b/tests/Integration/tests/directives/directive-genindex-scoped/input/index.rst @@ -0,0 +1,29 @@ +Changelog +========= + +Simulates a TYPO3-style Changelog with per-version directories, demonstrating +the scoped ``.. genindex::`` directive: two scoped listings (one per version) +and one unscoped listing, all on the same page. + +.. toctree:: + + Changelog/12.3/Feature-100-OldThing + Changelog/12.4/Feature-200-NewThing + Changelog/12.4/Breaking-300-RemovedThing + +Index For 12.4 +-------------- + +.. genindex:: + :scope: Changelog/12.4/ + +Index For 12.3 +-------------- + +.. genindex:: + :scope: Changelog/12.3/ + +Full Index +---------- + +.. genindex:: diff --git a/tests/Integration/tests/directives/directive-index-changelog/expected/42/Deprecation-11111-OldWidgetConstant.html b/tests/Integration/tests/directives/directive-index-changelog/expected/42/Deprecation-11111-OldWidgetConstant.html new file mode 100644 index 000000000..e1fb649ee --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/expected/42/Deprecation-11111-OldWidgetConstant.html @@ -0,0 +1,13 @@ + +
+

Deprecation: #11111 - WIDGET_LEGACY_MODE constant

+
+

What Was Deprecated

+

The class constant WIDGET_LEGACY_MODE has been marked as deprecated.

+
+
+

Migration Path

+

Using the constant will trigger a deprecation warning.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-changelog/expected/42/index.html b/tests/Integration/tests/directives/directive-index-changelog/expected/42/index.html new file mode 100644 index 000000000..8be2ec7f8 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/expected/42/index.html @@ -0,0 +1,63 @@ + +
+

Version 42

+ +
+

Index For This Version

+ +
+ B + P +
+ +

B

+ + + + + +
+
+
Backend
+
+ Deprecation: #11111 - WIDGET_LEGACY_MODE constant +
+
+
+ +

P

+ + + + + +
+
+
PartiallyScanned
+
+ Deprecation: #11111 - WIDGET_LEGACY_MODE constant +
+ +
PHP-API
+
+ Deprecation: #11111 - WIDGET_LEGACY_MODE constant +
+
+
+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-changelog/expected/43/Breaking-67890-RemovedLegacyWidgetLoader.html b/tests/Integration/tests/directives/directive-index-changelog/expected/43/Breaking-67890-RemovedLegacyWidgetLoader.html new file mode 100644 index 000000000..1f8e62e1a --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/expected/43/Breaking-67890-RemovedLegacyWidgetLoader.html @@ -0,0 +1,19 @@ + +
+

Breaking: #67890 - Removed WidgetLoader

+
+

What Was Removed

+

The legacy WidgetLoader class has been removed in favor of the new +Widget API.

+
+
+

Consequences

+

Any code still calling WidgetLoader::load() will fail with a fatal +error.

+
+
+

Affected Installations

+

Installations with extensions that use the old hook-based widget mechanism.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-changelog/expected/43/Feature-12345-NewWidgetApi.html b/tests/Integration/tests/directives/directive-index-changelog/expected/43/Feature-12345-NewWidgetApi.html new file mode 100644 index 000000000..d5452be28 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/expected/43/Feature-12345-NewWidgetApi.html @@ -0,0 +1,14 @@ + +
+

Feature: #12345 - Widget API

+
+

What Changed

+

A new PHP API for registering custom widgets was introduced.

+
+
+

Impact On Extension Authors

+

Extension authors can now register widgets without relying on the legacy +hook-based mechanism.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-changelog/expected/43/index.html b/tests/Integration/tests/directives/directive-index-changelog/expected/43/index.html new file mode 100644 index 000000000..165c0a255 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/expected/43/index.html @@ -0,0 +1,150 @@ + +
+

Version 43

+ +
+

Index For This Version

+ + + + + +
+
+
Backend
+
+ Breaking: #67890 - Removed WidgetLoader +
+
+ Feature: #12345 - Widget API +
+ +
ext:core
+
+ Breaking: #67890 - Removed WidgetLoader +
+
+ Feature: #12345 - Widget API +
+ +
NotScanned
+
+ Breaking: #67890 - Removed WidgetLoader +
+ +
PHP-API
+
+ Feature: #12345 - Widget API +
+
+
+
+
+

Full Alphabetical Index For This Version

+

Two .. genindex:: directives on one page, both scoped to 43/: the +compact listing above shares the exact same underlying term data with the +grouped listing below, just rendered with different options.

+ +
+ B + E + N + P +
+ +

B

+ + + + + +
+
+
Backend
+
+ Breaking: #67890 - Removed WidgetLoader +
+
+ Feature: #12345 - Widget API +
+
+
+ +

E

+ + + + + +
+
+
ext:core
+
+ Breaking: #67890 - Removed WidgetLoader +
+
+ Feature: #12345 - Widget API +
+
+
+ +

N

+ + + + + +
+
+
NotScanned
+
+ Breaking: #67890 - Removed WidgetLoader +
+
+
+ +

P

+ + + + + +
+
+
PHP-API
+
+ Feature: #12345 - Widget API +
+
+
+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-changelog/expected/genindex.html b/tests/Integration/tests/directives/directive-index-changelog/expected/genindex.html new file mode 100644 index 000000000..3b9cd6c9d --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/expected/genindex.html @@ -0,0 +1,89 @@ + +
+

Index

+ +
+ B + E + N + P +
+ +

B

+ + + + + +
+
+
Backend
+
+ Deprecation: #11111 - WIDGET_LEGACY_MODE constant +
+
+ Breaking: #67890 - Removed WidgetLoader +
+
+ Feature: #12345 - Widget API +
+
+
+ +

E

+ + + + + +
+
+
ext:core
+
+ Breaking: #67890 - Removed WidgetLoader +
+
+ Feature: #12345 - Widget API +
+
+
+ +

N

+ + + + + +
+
+
NotScanned
+
+ Breaking: #67890 - Removed WidgetLoader +
+
+
+ +

P

+ + + + + +
+
+
PartiallyScanned
+
+ Deprecation: #11111 - WIDGET_LEGACY_MODE constant +
+ +
PHP-API
+
+ Deprecation: #11111 - WIDGET_LEGACY_MODE constant +
+
+ Feature: #12345 - Widget API +
+
+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-changelog/expected/index.html b/tests/Integration/tests/directives/directive-index-changelog/expected/index.html new file mode 100644 index 000000000..569c96bec --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/expected/index.html @@ -0,0 +1,74 @@ + +
+

Changelog

+

Simulates the TYPO3 Core Changelog convention: one directory per version, +each containing one page per change. Every change page ends with a single, +type-less, comma-separated .. index:: line right after the last section, +with no heading following it. Each version directory's own overview page +carries a .. genindex:: scoped to that directory, alongside the +project-wide :template: genindex page for everything combined.

+ +
+ diff --git a/tests/Integration/tests/directives/directive-index-changelog/input/42/Deprecation-11111-OldWidgetConstant.rst b/tests/Integration/tests/directives/directive-index-changelog/input/42/Deprecation-11111-OldWidgetConstant.rst new file mode 100644 index 000000000..48c0a8b4b --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/input/42/Deprecation-11111-OldWidgetConstant.rst @@ -0,0 +1,15 @@ +===================================================== +Deprecation: #11111 - WIDGET_LEGACY_MODE constant +===================================================== + +What Was Deprecated +==================== + +The class constant ``WIDGET_LEGACY_MODE`` has been marked as deprecated. + +Migration Path +=============== + +Using the constant will trigger a deprecation warning. + +.. index:: Backend, PHP-API, PartiallyScanned diff --git a/tests/Integration/tests/directives/directive-index-changelog/input/42/index.rst b/tests/Integration/tests/directives/directive-index-changelog/input/42/index.rst new file mode 100644 index 000000000..9827db58f --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/input/42/index.rst @@ -0,0 +1,12 @@ +Version 42 +=========== + +.. toctree:: + + Deprecation-11111-OldWidgetConstant + +Index For This Version +----------------------- + +.. genindex:: + :scope: 42/ diff --git a/tests/Integration/tests/directives/directive-index-changelog/input/43/Breaking-67890-RemovedLegacyWidgetLoader.rst b/tests/Integration/tests/directives/directive-index-changelog/input/43/Breaking-67890-RemovedLegacyWidgetLoader.rst new file mode 100644 index 000000000..83bd63c83 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/input/43/Breaking-67890-RemovedLegacyWidgetLoader.rst @@ -0,0 +1,22 @@ +========================================= +Breaking: #67890 - Removed WidgetLoader +========================================= + +What Was Removed +================= + +The legacy ``WidgetLoader`` class has been removed in favor of the new +Widget API. + +Consequences +============ + +Any code still calling ``WidgetLoader::load()`` will fail with a fatal +error. + +Affected Installations +======================= + +Installations with extensions that use the old hook-based widget mechanism. + +.. index:: Backend, NotScanned, ext:core diff --git a/tests/Integration/tests/directives/directive-index-changelog/input/43/Feature-12345-NewWidgetApi.rst b/tests/Integration/tests/directives/directive-index-changelog/input/43/Feature-12345-NewWidgetApi.rst new file mode 100644 index 000000000..ff0dca123 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/input/43/Feature-12345-NewWidgetApi.rst @@ -0,0 +1,16 @@ +============================= +Feature: #12345 - Widget API +============================= + +What Changed +============ + +A new PHP API for registering custom widgets was introduced. + +Impact On Extension Authors +============================ + +Extension authors can now register widgets without relying on the legacy +hook-based mechanism. + +.. index:: Backend, PHP-API, ext:core diff --git a/tests/Integration/tests/directives/directive-index-changelog/input/43/index.rst b/tests/Integration/tests/directives/directive-index-changelog/input/43/index.rst new file mode 100644 index 000000000..a75f1d6ba --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/input/43/index.rst @@ -0,0 +1,24 @@ +Version 43 +=========== + +.. toctree:: + + Feature-12345-NewWidgetApi + Breaking-67890-RemovedLegacyWidgetLoader + +Index For This Version +----------------------- + +.. genindex:: + :scope: 43/ + :no-letter-index: + +Full Alphabetical Index For This Version +------------------------------------------ + +Two `.. genindex::` directives on one page, both scoped to ``43/``: the +compact listing above shares the exact same underlying term data with the +grouped listing below, just rendered with different options. + +.. genindex:: + :scope: 43/ diff --git a/tests/Integration/tests/directives/directive-index-changelog/input/genindex.rst b/tests/Integration/tests/directives/directive-index-changelog/input/genindex.rst new file mode 100644 index 000000000..f86377348 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/input/genindex.rst @@ -0,0 +1,5 @@ +:orphan: +:template: genindex + +Index +===== diff --git a/tests/Integration/tests/directives/directive-index-changelog/input/index.rst b/tests/Integration/tests/directives/directive-index-changelog/input/index.rst new file mode 100644 index 000000000..bc7f01862 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-changelog/input/index.rst @@ -0,0 +1,14 @@ +Changelog +========= + +Simulates the TYPO3 Core Changelog convention: one directory per version, +each containing one page per change. Every change page ends with a single, +type-less, comma-separated ``.. index::`` line right after the last section, +with no heading following it. Each version directory's own overview page +carries a ``.. genindex::`` scoped to that directory, alongside the +project-wide ``:template: genindex`` page for everything combined. + +.. toctree:: + + 43/index + 42/index diff --git a/tests/Integration/tests/directives/directive-index-features/expected/genindex.html b/tests/Integration/tests/directives/directive-index-features/expected/genindex.html new file mode 100644 index 000000000..cb7e9cc38 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/expected/genindex.html @@ -0,0 +1,237 @@ + +
+

Index

+ +
+ A + B + C + D + E + F + G + H + I + M + W +
+ +

A

+ + + + + +
+
+
alpha
+
+ Single Entry +
+
+
+ +

B

+ + + + + +
+
+
beta
+
+
+
subbeta
+
+ Single Entry With Subterm +
+
+
+ +
bravo
+
+
+
charlie
+
+ Pair Entry +
+
+
+
+
+ +

C

+ + + + + +
+
+
charlie
+
+
+
bravo
+
+ Pair Entry +
+
+
+
+
+ +

D

+ + + + + +
+
+
delta
+
+
+
echo foxtrot
+
+ Triple Entry +
+
+
+
+
+ +

E

+ + + + + +
+
+
echo
+
+
+
foxtrot, delta
+
+ Triple Entry +
+
+
+
+
+ +

F

+ + + + + +
+
+
foxtrot
+
+
+
delta echo
+
+ Triple Entry +
+
+
+
+
+ +

G

+ + + + + +
+
+
golf
+
+ See Target +
+
+
+ +

H

+ + + + + +
+
+
hotel
+
+ see + golf +
+
+
+ +

I

+ + + + + +
+
+
india
+
+ see also + juliet +
+
+
+ +

M

+ + + + + +
+
+
main entry demo
+
+ Main Entry +
+ +
module
+
+
+
mymodule
+
+ Module Entry +
+
+
+
+
+ +

W

+ + + + + +
+
+
widget
+
+ Widget Reference (Page One) +
+
+ Widget Reference (Page Three) +
+
+ Widget Reference (Page Two) +
+
+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-features/expected/index.html b/tests/Integration/tests/directives/directive-index-features/expected/index.html new file mode 100644 index 000000000..711d28f53 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/expected/index.html @@ -0,0 +1,66 @@ + +
+

Index Feature Showcase

+

This project exercises every .. index:: entry type across multiple pages, +demonstrating that genindex aggregates entries project-wide rather than +per-page.

+ +
+

Main Entry

+

A ! entry line marks the entry as the "main" definition, rendered with +the main-entry class. Defined on the root page to show main-entry +detection isn't tied to any particular page.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-features/expected/page-one.html b/tests/Integration/tests/directives/directive-index-features/expected/page-one.html new file mode 100644 index 000000000..66717e4f4 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/expected/page-one.html @@ -0,0 +1,25 @@ + +
+

Page One

+
+

Single Entry

+

A plain single: term entry: one top-level term with a direct link.

+
+
+

Single Entry With Subterm

+

A single: term; subterm entry: a two-level entry (term > subterm), with +no reciprocal entry generated (unlike pair).

+
+
+

See Target

+

Defines "golf" as its own term, so a see entry on another page (see +page-three) can resolve and link across pages back to this one.

+
+
+

Widget Reference (Page One)

+

One of several occurrences of the same term ("widget") across different +pages, so its genindex entry accumulates multiple result links under one +<dt>.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-features/expected/page-three.html b/tests/Integration/tests/directives/directive-index-features/expected/page-three.html new file mode 100644 index 000000000..b53679d89 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/expected/page-three.html @@ -0,0 +1,25 @@ + +
+

Page Three

+
+

Module Entry

+

A module: name entry: always nests under the literal top-level term +"module".

+
+
+

See Entry (Cross-Page)

+

A see: entry; target entry whose target ("golf") is defined on a +different page (page-one): the "see" row must resolve and link across pages.

+
+
+

See Also Entry (Unresolved Target)

+

A seealso: entry; target entry whose target ("juliet") is not defined +anywhere in the project: it renders as plain, unlinked text.

+
+
+

Widget Reference (Page Three)

+

A third occurrence of "widget", giving its genindex entry three result +links in total, one per originating page.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-features/expected/page-two.html b/tests/Integration/tests/directives/directive-index-features/expected/page-two.html new file mode 100644 index 000000000..add1ae4ca --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/expected/page-two.html @@ -0,0 +1,21 @@ + +
+

Page Two

+
+

Pair Entry

+

A pair: a; b entry: generates both "bravo > charlie" and its reciprocal +"charlie > bravo".

+
+
+

Triple Entry

+

A triple: a; b; c entry: generates three rotations, "delta > echo +foxtrot", "echo > foxtrot, delta" (note the comma), and "foxtrot > delta +echo".

+
+
+

Widget Reference (Page Two)

+

Another occurrence of "widget", adding a second result link to its genindex +entry.

+
+
+ diff --git a/tests/Integration/tests/directives/directive-index-features/input/genindex.rst b/tests/Integration/tests/directives/directive-index-features/input/genindex.rst new file mode 100644 index 000000000..f86377348 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/input/genindex.rst @@ -0,0 +1,5 @@ +:orphan: +:template: genindex + +Index +===== diff --git a/tests/Integration/tests/directives/directive-index-features/input/index.rst b/tests/Integration/tests/directives/directive-index-features/input/index.rst new file mode 100644 index 000000000..6f2178ea1 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/input/index.rst @@ -0,0 +1,21 @@ +Index Feature Showcase +======================= + +This project exercises every `.. index::` entry type across multiple pages, +demonstrating that genindex aggregates entries project-wide rather than +per-page. + +.. toctree:: + + page-one + page-two + page-three + +.. index:: ! main entry demo + +Main Entry +---------- + +A ``! entry`` line marks the entry as the "main" definition, rendered with +the ``main-entry`` class. Defined on the root page to show main-entry +detection isn't tied to any particular page. diff --git a/tests/Integration/tests/directives/directive-index-features/input/page-one.rst b/tests/Integration/tests/directives/directive-index-features/input/page-one.rst new file mode 100644 index 000000000..7b6354762 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/input/page-one.rst @@ -0,0 +1,34 @@ +Page One +======== + +.. index:: single: alpha + +Single Entry +------------ + +A plain ``single: term`` entry: one top-level term with a direct link. + +.. index:: single: beta; subbeta + +Single Entry With Subterm +-------------------------- + +A ``single: term; subterm`` entry: a two-level entry (term > subterm), with +no reciprocal entry generated (unlike ``pair``). + +.. index:: single: golf + +See Target +---------- + +Defines "golf" as its own term, so a ``see`` entry on another page (see +page-three) can resolve and link across pages back to this one. + +.. index:: single: widget + +Widget Reference (Page One) +---------------------------- + +One of several occurrences of the same term ("widget") across different +pages, so its genindex entry accumulates multiple result links under one +``
``. diff --git a/tests/Integration/tests/directives/directive-index-features/input/page-three.rst b/tests/Integration/tests/directives/directive-index-features/input/page-three.rst new file mode 100644 index 000000000..e851ead2a --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/input/page-three.rst @@ -0,0 +1,34 @@ +Page Three +========== + +.. index:: module: mymodule + +Module Entry +------------ + +A ``module: name`` entry: always nests under the literal top-level term +"module". + +.. index:: see: hotel; golf + +See Entry (Cross-Page) +------------------------ + +A ``see: entry; target`` entry whose target ("golf") is defined on a +different page (page-one): the "see" row must resolve and link across pages. + +.. index:: seealso: india; juliet + +See Also Entry (Unresolved Target) +------------------------------------ + +A ``seealso: entry; target`` entry whose target ("juliet") is not defined +anywhere in the project: it renders as plain, unlinked text. + +.. index:: single: widget + +Widget Reference (Page Three) +------------------------------- + +A third occurrence of "widget", giving its genindex entry three result +links in total, one per originating page. diff --git a/tests/Integration/tests/directives/directive-index-features/input/page-two.rst b/tests/Integration/tests/directives/directive-index-features/input/page-two.rst new file mode 100644 index 000000000..00ff9531a --- /dev/null +++ b/tests/Integration/tests/directives/directive-index-features/input/page-two.rst @@ -0,0 +1,27 @@ +Page Two +======== + +.. index:: pair: bravo; charlie + +Pair Entry +---------- + +A ``pair: a; b`` entry: generates both "bravo > charlie" and its reciprocal +"charlie > bravo". + +.. index:: triple: delta; echo; foxtrot + +Triple Entry +------------ + +A ``triple: a; b; c`` entry: generates three rotations, "delta > echo +foxtrot", "echo > foxtrot, delta" (note the comma), and "foxtrot > delta +echo". + +.. index:: single: widget + +Widget Reference (Page Two) +---------------------------- + +Another occurrence of "widget", adding a second result link to its genindex +entry. diff --git a/tests/Integration/tests/directives/directive-index/expected/genindex.html b/tests/Integration/tests/directives/directive-index/expected/genindex.html new file mode 100644 index 000000000..39968b91b --- /dev/null +++ b/tests/Integration/tests/directives/directive-index/expected/genindex.html @@ -0,0 +1,367 @@ + +
+

Index

+ +
+ A + C + D + E + F + I + L + M + O + P + T +
+ +

A

+ + + + + +
+
+
access token
+
+ Authentication +
+ +
API
+
+
+
authentication
+
+ Authentication +
+
+
+ +
authentication
+
+
+
API
+
+ Authentication +
+ +
token
+
+ Authentication +
+
+
+
+
+ +

C

+ + + + + +
+
+
cache
+
+
+
invalidation
+
+ Performance +
+
+
+ +
CLI
+
+
+
command deploy
+
+ Deploying from the CLI +
+
+
+ +
command
+
+
+
deploy, CLI
+
+ Deploying from the CLI +
+ +
deployment
+
+ Deploying from the CLI +
+
+
+ +
configuration
+
+ Configuration +
+
+
+
environment variables
+
+ Configuration +
+ +
file
+
+ Configuration +
+
+
+
+
+ +

D

+ + + + + +
+
+
deploy
+
+
+
CLI command
+
+ Deploying from the CLI +
+
+
+ +
deploy command
+
+ Deploying from the CLI +
+ +
deployment
+
+
+
command
+
+ Deploying from the CLI +
+
+
+ +
diagnostics
+
+
+
logging
+
+ Troubleshooting +
+
+
+
+
+ +

E

+ + + + + +
+
+
environment variables
+
+
+
configuration
+
+ Configuration +
+
+
+ +
error handling
+
+
+
logging
+
+ Troubleshooting +
+
+
+
+
+ +

F

+ + + + + +
+
+
file
+
+
+
configuration
+
+ Configuration +
+
+
+
+
+ +

I

+ + + + + +
+
+
installation
+
+ Installation +
+ +
invalidation
+
+
+
cache
+
+ Performance +
+
+
+
+
+ +

L

+ + + + + +
+
+
logging
+
+
+
diagnostics
+
+ Troubleshooting +
+ +
error handling
+
+ Troubleshooting +
+
+
+
+
+ +

M

+ + + + + +
+
+
main entry example
+
+ Main Index Entry +
+ +
module
+
+
+
example_package
+
+ Python Module +
+
+
+
+
+ +

O

+ + + + + +
+
+
optimization
+
+ see also + caching +
+
+
+ +

P

+ + + + + +
+
+
performance
+
+
+
optimization
+
+ Performance +
+
+
+ +
Python module
+
+
+
example_package
+
+ Python Module +
+
+
+
+
+ +

T

+ + + + + +
+
+
token
+
+ see + access token +
+
+
+
authentication
+
+ Authentication +
+
+
+ +
troubleshooting
+
+ Troubleshooting +
+
+
+
+ diff --git a/tests/Integration/tests/directives/directive-index/expected/index.html b/tests/Integration/tests/directives/directive-index/expected/index.html new file mode 100644 index 000000000..e5c30253a --- /dev/null +++ b/tests/Integration/tests/directives/directive-index/expected/index.html @@ -0,0 +1,67 @@ + +
+

Example Documentation Page

+

This page demonstrates several Sphinx/reStructuredText index entries.

+ +
+

Installation

+

This section has a simple index entry for installation.

+

To install the package, run:

+
pip install example-package
+
+
+

Configuration

+

This section creates multiple index entries. In the generated index, readers +could find this section under "configuration", "configuration; file", and +"configuration; environment variables".

+

Configuration can be loaded from a file or from environment variables.

+
+
+

Authentication

+

The API uses access tokens for authentication.

+

An access token identifies the client application and authorizes API requests.

+
+
+

Deploying from the CLI

+

Use the deploy command to publish the application:

+
example-cli deploy --environment production
+
+
+

Performance

+

Caching can improve performance, but cache invalidation must be handled +carefully.

+
+
+

Main Index Entry

+

The exclamation mark marks this as a main index entry in Sphinx.

+

This is useful when several pages mention a concept, but one page is the +primary explanation of that concept.

+
+
+

Python Module

+

This section indexes a Python module.

+

The package exposes the example_package module.

+
+
+

Troubleshooting

+

When debugging problems, enable diagnostic logging and inspect error messages.

+

Useful troubleshooting steps include:

+
    +
  • checking configuration values
  • +
  • verifying authentication tokens
  • +
  • reviewing log output
  • +
+
+
+

Glossary

+
+
access token
+
A credential used to authenticate API requests.
+
configuration file
+
A file containing settings used by the application.
+
cache invalidation
+
The process of removing or refreshing stale cached data.
+
+
+
+ diff --git a/tests/Integration/tests/directives/directive-index/input/genindex.rst b/tests/Integration/tests/directives/directive-index/input/genindex.rst new file mode 100644 index 000000000..f86377348 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index/input/genindex.rst @@ -0,0 +1,5 @@ +:orphan: +:template: genindex + +Index +===== diff --git a/tests/Integration/tests/directives/directive-index/input/index.rst b/tests/Integration/tests/directives/directive-index/input/index.rst new file mode 100644 index 000000000..f0a95ab91 --- /dev/null +++ b/tests/Integration/tests/directives/directive-index/input/index.rst @@ -0,0 +1,128 @@ +Example Documentation Page +========================== + +This page demonstrates several Sphinx/reStructuredText index entries. + +.. index:: single: installation + +Installation +------------ + +This section has a simple index entry for ``installation``. + +To install the package, run: + +.. code-block:: console + + pip install example-package + + +.. index:: + single: configuration + pair: configuration; file + pair: configuration; environment variables + +Configuration +------------- + +This section creates multiple index entries. In the generated index, readers +could find this section under "configuration", "configuration; file", and +"configuration; environment variables". + +Configuration can be loaded from a file or from environment variables. + + +.. index:: + pair: API; authentication + pair: authentication; token + single: access token + see: token; access token + +Authentication +-------------- + +The API uses access tokens for authentication. + +An access token identifies the client application and authorizes API requests. + + +.. index:: + triple: CLI; command; deploy + pair: deployment; command + single: deploy command + +Deploying from the CLI +---------------------- + +Use the ``deploy`` command to publish the application: + +.. code-block:: console + + example-cli deploy --environment production + + +.. index:: + single: performance; optimization + pair: cache; invalidation + seealso: optimization; caching + +Performance +----------- + +Caching can improve performance, but cache invalidation must be handled +carefully. + + +.. index:: ! main entry example + +Main Index Entry +---------------- + +The exclamation mark marks this as a main index entry in Sphinx. + +This is useful when several pages mention a concept, but one page is the +primary explanation of that concept. + + +.. index:: + module: example_package + single: Python module; example_package + +Python Module +------------- + +This section indexes a Python module. + +The package exposes the ``example_package`` module. + + +.. index:: + single: troubleshooting + pair: error handling; logging + pair: logging; diagnostics + +Troubleshooting +--------------- + +When debugging problems, enable diagnostic logging and inspect error messages. + +Useful troubleshooting steps include: + +* checking configuration values +* verifying authentication tokens +* reviewing log output + + +Glossary +-------- + +.. glossary:: + + access token + A credential used to authenticate API requests. + + configuration file + A file containing settings used by the application. + + cache invalidation + The process of removing or refreshing stale cached data. \ No newline at end of file