diff --git a/packages/guides/src/Compiler/Passes/ImplicitHyperlinkTargetPass.php b/packages/guides/src/Compiler/Passes/ImplicitHyperlinkTargetPass.php index 6c7fcbd5f..fb918de13 100644 --- a/packages/guides/src/Compiler/Passes/ImplicitHyperlinkTargetPass.php +++ b/packages/guides/src/Compiler/Passes/ImplicitHyperlinkTargetPass.php @@ -25,9 +25,7 @@ use function array_merge; use function current; use function in_array; -use function key; use function next; -use function prev; /** * Resolves the hyperlink target for each section in the document. @@ -50,44 +48,42 @@ public function run(array $documents, CompilerContextInterface $compilerContext) $knownReferences = $this->fetchExplicitReferences($document); $nodes = $document->getNodes(); - $node = current($nodes); - do { - if ($node instanceof AnchorNode) { - // override implicit section reference if an anchor precedes the section - $key = key($nodes); - $section = next($nodes); - if (!$section instanceof SectionNode) { - prev($nodes); - continue; - } - - $section->getTitle()->setId($node->getValue()); - if ($key !== null) { - $document = $document->removeNode($key); - } - - continue; - } + $this->deduplicateSectionIds($nodes, $knownReferences); - if (!($node instanceof SectionNode)) { - continue; - } + return $document; + }, $documents); + } + /** + * @param array $nodes + * @param list $knownIds + * + * @return list + */ + private function deduplicateSectionIds(array $nodes, array $knownIds): array + { + $node = current($nodes); + do { + if ($node instanceof SectionNode) { $realId = $sectionId = $node->getTitle()->getId(); // resolve conflicting references by appending an increasing number $i = 1; - while (in_array($realId, $knownReferences, true)) { + while (in_array($realId, $knownIds, true)) { $realId = $sectionId . '-' . ($i++); } $node->getTitle()->setId($realId); - $knownReferences[] = $realId; - //phpcs:ignore SlevomatCodingStandard.ControlStructures.AssignmentInCondition.AssignmentInCondition - } while ($node = next($nodes)); + $knownIds[] = $realId; + } - return $document; - }, $documents); + if ($node instanceof CompoundNode) { + $knownIds = $this->deduplicateSectionIds($node->getChildren(), $knownIds); + } + //phpcs:ignore SlevomatCodingStandard.ControlStructures.AssignmentInCondition.AssignmentInCondition + } while ($node = next($nodes)); + + return $knownIds; } /** @return string[] */ diff --git a/packages/guides/tests/unit/Compiler/Passes/ImplicitHyperlinkTargetPassTest.php b/packages/guides/tests/unit/Compiler/Passes/ImplicitHyperlinkTargetPassTest.php index 383697c09..907093e09 100644 --- a/packages/guides/tests/unit/Compiler/Passes/ImplicitHyperlinkTargetPassTest.php +++ b/packages/guides/tests/unit/Compiler/Passes/ImplicitHyperlinkTargetPassTest.php @@ -21,30 +21,35 @@ use phpDocumentor\Guides\Nodes\SectionNode; use phpDocumentor\Guides\Nodes\TitleNode; use PHPUnit\Framework\TestCase; -use Symfony\Component\String\Slugger\AsciiSlugger; final class ImplicitHyperlinkTargetPassTest extends TestCase { public function testAllImplicitUniqueSections(): void { $document = new DocumentNode('1', 'index'); - $expected = new DocumentNode('1', 'index'); - $slugger = new AsciiSlugger(); - foreach (['Document 1', 'Section A', 'Section B'] as $titles) { - $document->addChildNode( + $documentSection = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Index'), 1, 'index')); + $document->addChildNode($documentSection); + foreach (['Document 1' => 'document-1', 'Section A' => 'section-a', 'Section B' => 'section-b'] as $title => $id) { + $documentSection->addChildNode( new SectionNode( - new TitleNode(InlineCompoundNode::getPlainTextInlineNode($titles), 1, $slugger->slug($titles)->lower()->toString()), + new TitleNode(InlineCompoundNode::getPlainTextInlineNode($title), 1, $id), ), ); - $expected->addChildNode( + } + + $expected = new DocumentNode('1', 'index'); + $expectedSection = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Index'), 1, 'index')); + $expected->addChildNode($expectedSection); + foreach (['Document 1' => 'document-1', 'Section A' => 'section-a', 'Section B' => 'section-b'] as $title => $id) { + $expectedSection->addChildNode( new SectionNode( - new TitleNode(InlineCompoundNode::getPlainTextInlineNode($titles), 1, $slugger->slug($titles)->lower()->toString()), + new TitleNode(InlineCompoundNode::getPlainTextInlineNode($title), 1, $id), ), ); } $pass = new ImplicitHyperlinkTargetPass(); - $resultDocuments = $pass->run([clone $document], new CompilerContext(new ProjectNode())); + $resultDocuments = $pass->run([$document], new CompilerContext(new ProjectNode())); self::assertEquals([$expected], $resultDocuments); } @@ -52,54 +57,39 @@ public function testAllImplicitUniqueSections(): void public function testImplicitWithConflict(): void { $document = new DocumentNode('1', 'index'); - $expected = new DocumentNode('1', 'index'); - $slugger = new AsciiSlugger(); - - foreach (['Document 1', 'Section A', 'Section A'] as $titles) { - $document->addChildNode( + $documentSection = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Index'), 1, 'index')); + $document->addChildNode($documentSection); + foreach (['Document 1' => 'document-1', 'Section A' => 'section-a'] as $title => $id) { + $documentSection->addChildNode( new SectionNode( - new TitleNode(InlineCompoundNode::getPlainTextInlineNode($titles), 1, $slugger->slug($titles)->lower()->toString()), - ), - ); - $expected->addChildNode( - new SectionNode( - new TitleNode(InlineCompoundNode::getPlainTextInlineNode($titles), 1, $slugger->slug($titles)->lower()->toString()), + new TitleNode(InlineCompoundNode::getPlainTextInlineNode($title), 1, $id), ), ); } - $pass = new ImplicitHyperlinkTargetPass(); - $resultDocuments = $pass->run([$document], new CompilerContext(new ProjectNode())); - - $section = $expected->getNodes()[2]; - self::assertInstanceOf(SectionNode::class, $section); - $section->getTitle()->setId('section-a-1'); - - self::assertEquals([$expected], $resultDocuments); - } + $documentSection->addChildNode( + new SectionNode( + new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Section A'), 1, 'section-a'), + ), + ); - public function testExplicit(): void - { - $document = new DocumentNode('1', 'index'); $expected = new DocumentNode('1', 'index'); + $expectedSection = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Index'), 1, 'index')); + $expected->addChildNode($expectedSection); + foreach (['Document 1' => 'document-1', 'Section A' => 'section-a'] as $title => $id) { + $expectedSection->addChildNode( + new SectionNode( + new TitleNode(InlineCompoundNode::getPlainTextInlineNode($title), 1, $id), + ), + ); + } - $document->addChildNode(new SectionNode( - new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Document 1'), 1, 'document-1'), - )); - $expected->addChildNode(new SectionNode( - new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Document 1'), 1, 'document-1'), - )); - - $document->addChildNode(new AnchorNode('custom-anchor')); - $expected->addChildNode(new AnchorNode('removed')); - $expected = $expected->removeNode(1); - - $document->addChildNode( - new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Section A'), 1, 'section-a')), + $expectedSection->addChildNode( + new SectionNode( + // conflict in ID, "-1" is added + new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Section A'), 1, 'section-a-1'), + ), ); - $expectedTitle = new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Section A'), 1, 'section-a'); - $expectedTitle->setId('custom-anchor'); - $expected->addChildNode(new SectionNode($expectedTitle)); $pass = new ImplicitHyperlinkTargetPass(); $resultDocuments = $pass->run([$document], new CompilerContext(new ProjectNode())); @@ -110,25 +100,27 @@ public function testExplicit(): void public function testExplicitHasPriorityOverImplicit(): void { $document = new DocumentNode('1', 'index'); - $expected = new DocumentNode('1', 'index'); - - $document->addChildNode( + $documentSection = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Index'), 1, 'index')); + $document->addChildNode($documentSection); + $documentSection->addChildNode( new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Document 1'), 1, 'document-1')), ); - $expectedTitle = new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Document 1'), 1, 'document-1'); - $expectedTitle->setId('document-1-1'); - $expected->addChildNode(new SectionNode($expectedTitle)); - - $document->addChildNode(new AnchorNode('document-1')); - $expected->addChildNode(new AnchorNode('removed')); - $expected = $expected->removeNode(1); + $documentSection->addChildNode(new AnchorNode('document-1')); + $documentSection->addChildNode( + new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Section A'), 1, 'section-a')), + ); - $document->addChildNode( + $expected = new DocumentNode('1', 'index'); + $expectedSection = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Index'), 1, 'index')); + $expected->addChildNode($expectedSection); + $expectedSection->addChildNode( + // "document-1" is claimed by an explicit reference anchor, implicit reference gets the "-1" suffix + new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Document 1'), 1, 'document-1-1')), + ); + $expectedSection->addChildNode(new AnchorNode('document-1')); + $expectedSection->addChildNode( new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Section A'), 1, 'section-a')), ); - $expectedTitle = new TitleNode(InlineCompoundNode::getPlainTextInlineNode('Section A'), 1, 'section-a'); - $expectedTitle->setId('document-1'); - $expected->addChildNode(new SectionNode($expectedTitle)); $pass = new ImplicitHyperlinkTargetPass(); $resultDocuments = $pass->run([$document], new CompilerContext(new ProjectNode())); diff --git a/tests/Functional/tests/titles-ids/titles-ids.html b/tests/Functional/tests/titles-ids/titles-ids.html new file mode 100644 index 000000000..78d4e5779 --- /dev/null +++ b/tests/Functional/tests/titles-ids/titles-ids.html @@ -0,0 +1,27 @@ +
+

+ Main title +

+
+

+ Subtitle +

+
+
+

+ Other Subtitle +

+
+

+ Subtitle +

+

Duplicate titles should get a unique ID through a -1 suffix.

+
+
+
+

+ Other Subtitle +

+

This also works for titles with markup.

+
+
diff --git a/tests/Functional/tests/titles-ids/titles-ids.rst b/tests/Functional/tests/titles-ids/titles-ids.rst new file mode 100644 index 000000000..33f2c7cac --- /dev/null +++ b/tests/Functional/tests/titles-ids/titles-ids.rst @@ -0,0 +1,18 @@ +Main title +========== + +Subtitle +-------- + +Other Subtitle +-------------- + +Subtitle +~~~~~~~~ + +Duplicate titles should get a unique ID through a ``-1`` suffix. + +Other ``Subtitle`` +------------------ + +This also works for titles with markup. diff --git a/tests/Integration/tests/images/image-target/expected/index.html b/tests/Integration/tests/images/image-target/expected/index.html index 0494bd042..76519b63d 100644 --- a/tests/Integration/tests/images/image-target/expected/index.html +++ b/tests/Integration/tests/images/image-target/expected/index.html @@ -33,7 +33,7 @@

Link image to reference

width="400" alt="Alternative text" /> -