Skip to content

Commit c49ef21

Browse files
committed
Add new compiler pass to move anchors
Anchors should be part of the first compound node after their definition. Anchors at the end of a compound node are positioned wrong by the parser So they are moved to the next compoundnode.
1 parent 0a51254 commit c49ef21

File tree

25 files changed

+322
-39
lines changed

25 files changed

+322
-39
lines changed

packages/guides/src/Compiler/CompilerContext.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,22 @@ public function getDocumentNode(): DocumentNode
4242
return $this->shadowTree->getRoot()->getNode();
4343
}
4444

45-
public function withShadowTree(DocumentNode $documentNode): static
45+
public function withDocumentShadowTree(DocumentNode $documentNode): static
4646
{
4747
$that = clone $this;
4848
$that->shadowTree = TreeNode::createFromDocument($documentNode);
4949

5050
return $that;
5151
}
5252

53+
public function withShadowTree(TreeNode $shadowTree): static
54+
{
55+
$that = clone $this;
56+
$that->shadowTree = $shadowTree;
57+
58+
return $that;
59+
}
60+
5361
/** @return TreeNode<DocumentNode> */
5462
public function getShadowTree(): TreeNode
5563
{

packages/guides/src/Compiler/DocumentNodeTraverser.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ private function traverseForTransformer(
5050
}
5151

5252
foreach ($shadowNode->getChildren() as $shadowChild) {
53-
$this->traverseForTransformer($transformer, $shadowChild, $compilerContext);
53+
$this->traverseForTransformer($transformer, $shadowChild, $compilerContext->withShadowTree($shadowNode));
5454
}
5555

5656
if (!$supports) {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
4+
5+
use LogicException;
6+
use phpDocumentor\Guides\Compiler\CompilerContext;
7+
use phpDocumentor\Guides\Compiler\NodeTransformer;
8+
use phpDocumentor\Guides\Compiler\ShadowTree\TreeNode;
9+
use phpDocumentor\Guides\Nodes\AnchorNode;
10+
use phpDocumentor\Guides\Nodes\Node;
11+
use phpDocumentor\Guides\Nodes\SectionNode;
12+
13+
final class MoveAnchorTransformer implements NodeTransformer
14+
{
15+
private array $seen = [];
16+
17+
public function enterNode(Node $node, CompilerContext $compilerContext): Node
18+
{
19+
return $node;
20+
}
21+
22+
public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null
23+
{
24+
//When exists in seen, it means that the node has already been processed. Ignore it.
25+
if (isset($this->seen[spl_object_hash($node)])) {
26+
return $node;
27+
}
28+
29+
$this->seen[spl_object_hash($node)] = spl_object_hash($node);
30+
$parent = $compilerContext->getShadowTree();
31+
$position = $parent->findPosition($node);
32+
if ($position === null) {
33+
throw new LogicException('Node not found in shadow tree');
34+
}
35+
36+
return $this->attemptMoveToNeighbour($parent, $position, $node);
37+
}
38+
39+
public function supports(Node $node): bool
40+
{
41+
return $node instanceof AnchorNode;
42+
}
43+
44+
public function getPriority(): int
45+
{
46+
return 30000;
47+
}
48+
49+
private function attemptMoveToNeighbour(TreeNode $parent, int $position, Node $node): ?Node
50+
{
51+
$current = $this->findNextSection($parent, $position);
52+
if ($current === null) {
53+
if ($parent->getParent() === null) {
54+
return $node;
55+
}
56+
57+
$position = $parent->getParent()->findPosition($parent->getNode());
58+
return $this->attemptMoveToNeighbour($parent->getParent(), $position, $node);
59+
}
60+
61+
if ($current->getNode() instanceof SectionNode) {
62+
$current->pushChild($node);
63+
return null;
64+
}
65+
66+
return $node;
67+
}
68+
69+
private function findNextSection($parent, $position): TreeNode|null
70+
{
71+
$children = new \ArrayIterator($parent->getChildren());
72+
if ($children->count() <= $position + 1) {
73+
return null;
74+
}
75+
76+
$children->seek($position + 1);
77+
while ($children->valid() && $children->current()->getNode() instanceof AnchorNode) {
78+
$children->next();
79+
}
80+
81+
return $children->current();
82+
}
83+
}

packages/guides/src/Compiler/NodeTransformers/TransformerPass.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function run(array $documents, CompilerContext $compilerContext): array
3434
continue;
3535
}
3636

37-
$compilerContext = $compilerContext->withShadowTree($document);
37+
$compilerContext = $compilerContext->withDocumentShadowTree($document);
3838
$documents[$key] = $this->documentNodeTraverser->traverse($document, $compilerContext);
3939
}
4040

packages/guides/src/Compiler/ShadowTree/TreeNode.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ public function addChild(Node $child): void
123123
$this->node->addChildNode($child);
124124
}
125125

126+
public function pushChild(Node $child): void
127+
{
128+
if ($this->node instanceof CompoundNode === false) {
129+
throw new LogicException('Cannot add a child to a non-compound node');
130+
}
131+
132+
$shadowNode = self::createFromNode($child, $this);
133+
$shadowNode->setRoot($this->root);
134+
array_unshift($this->children, $shadowNode);
135+
$this->node->pushChildNode($child);
136+
}
137+
126138
/** @return self<Node>|self<DocumentNode>|null */
127139
public function getParent(): TreeNode|null
128140
{
@@ -165,4 +177,15 @@ public function replaceChild(Node $oldChildNode, Node $newChildNode): void
165177
}
166178
}
167179
}
180+
181+
public function findPosition(Node $node): ?int
182+
{
183+
foreach ($this->children as $key => $child) {
184+
if ($child->getNode() === $node) {
185+
return $key;
186+
}
187+
}
188+
189+
return null;
190+
}
168191
}

packages/guides/src/Nodes/CompoundNode.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ public function addChildNode(Node $node): void
3939
$this->value[] = $node;
4040
}
4141

42+
43+
public function pushChildNode(Node $node): void
44+
{
45+
array_unshift($this->value, $node);
46+
}
47+
4248
/** @return $this<TValue> */
4349
public function removeNode(int $key): self
4450
{

packages/guides/tests/unit/Compiler/DocumentNodeTraverserTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function getPriority(): int
4747
},
4848
]), 2000);
4949

50-
$actual = $traverser->traverse($document, (new CompilerContext(new ProjectNode()))->withShadowTree($document));
50+
$actual = $traverser->traverse($document, (new CompilerContext(new ProjectNode()))->withDocumentShadowTree($document));
5151

5252
self::assertInstanceOf(DocumentNode::class, $actual);
5353
self::assertEquals(
@@ -97,7 +97,7 @@ public function getPriority(): int
9797

9898
$traverser = new DocumentNodeTraverser(new CustomNodeTransformerFactory($transformers), 2000);
9999

100-
$actual = $traverser->traverse($document, (new CompilerContext(new ProjectNode()))->withShadowTree($document));
100+
$actual = $traverser->traverse($document, (new CompilerContext(new ProjectNode()))->withDocumentShadowTree($document));
101101

102102
self::assertInstanceOf(DocumentNode::class, $actual);
103103
self::assertEquals(
@@ -150,7 +150,7 @@ public function getPriority(): int
150150

151151
$traverser = new DocumentNodeTraverser(new CustomNodeTransformerFactory($transformers), 2000);
152152

153-
$actual = $traverser->traverse($document, (new CompilerContext(new ProjectNode()))->withShadowTree($document));
153+
$actual = $traverser->traverse($document, (new CompilerContext(new ProjectNode()))->withDocumentShadowTree($document));
154154

155155
self::assertInstanceOf(DocumentNode::class, $actual);
156156
self::assertEquals(

packages/guides/tests/unit/Compiler/NodeTransformers/ClassNodeTransformerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ private static function getCompilerContext(string $path): CompilerContext
2020
{
2121
$context = new CompilerContext(new ProjectNode());
2222

23-
return $context->withShadowTree(new DocumentNode('123', $path));
23+
return $context->withDocumentShadowTree(new DocumentNode('123', $path));
2424
}
2525

2626
public function testLeaveNodeWillReturnNullWhenNodeIsClass(): void

packages/guides/tests/unit/Compiler/NodeTransformers/DocumentEntryRegistrationTransformerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ private static function getCompilerContext(string $path): CompilerContext
2626
{
2727
$context = new CompilerContext(new ProjectNode());
2828

29-
return $context->withShadowTree(new DocumentNode('123', $path));
29+
return $context->withDocumentShadowTree(new DocumentNode('123', $path));
3030
}
3131

3232
public function testLeaveNodeWillReturnDocumentNodeWithEntry(): void
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
4+
5+
use phpDocumentor\Guides\Compiler\CompilerContext;
6+
use phpDocumentor\Guides\Compiler\DocumentNodeTraverser;
7+
use phpDocumentor\Guides\Nodes\AnchorNode;
8+
use phpDocumentor\Guides\Nodes\DocumentNode;
9+
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
10+
use phpDocumentor\Guides\Nodes\ProjectNode;
11+
use phpDocumentor\Guides\Nodes\SectionNode;
12+
use phpDocumentor\Guides\Nodes\TitleNode;
13+
use PHPUnit\Framework\TestCase;
14+
15+
final class MoveAnchorTransformerTest extends TestCase
16+
{
17+
private DocumentNodeTraverser $documentNodeTraverser;
18+
19+
protected function setUp(): void
20+
{
21+
$this->documentNodeTraverser = new DocumentNodeTraverser(new class implements NodeTransformerFactory {
22+
public function getTransformers(): iterable
23+
{
24+
return [
25+
new MoveAnchorTransformer()
26+
];
27+
}
28+
29+
public function getPriorities(): array
30+
{
31+
return [];
32+
}
33+
}, 30000);
34+
}
35+
36+
public function testAnchorNodeShouldBeMovedToNextSectionNodeWhenPositionedAboveSection(): void
37+
{
38+
$node = new AnchorNode('foo');
39+
$section = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('foo'), 1, 'id'));
40+
41+
$document = new DocumentNode('123', 'some/path');
42+
$document->addChildNode($node);
43+
$document->addChildNode($section);
44+
45+
$context = (new CompilerContext(new ProjectNode('test', 'test')))->withDocumentShadowTree($document);
46+
47+
$this->documentNodeTraverser->traverse($document, $context);
48+
49+
self::assertCount(1, $context->getDocumentNode()->getChildren());
50+
self::assertCount(1, $section->getChildren());
51+
self::assertSame($node, $section->getChildren()[0]);
52+
}
53+
54+
public function testMultipleAnchorsShouldBeMovedToNextSectionNodeWhenPositionedAboveSection(): void
55+
{
56+
$node1 = new AnchorNode('foo');
57+
$node2 = new AnchorNode('bar');
58+
$node3 = new AnchorNode('bar2');
59+
$node4 = new AnchorNode('bar3');
60+
$section = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('foo'), 1, 'id'));
61+
62+
$document = new DocumentNode('123', 'some/path');
63+
$document->addChildNode($node1);
64+
$document->addChildNode($node2);
65+
$document->addChildNode($node3);
66+
$document->addChildNode($node4);
67+
$document->addChildNode($section);
68+
69+
$context = (new CompilerContext(new ProjectNode('test', 'test')))->withDocumentShadowTree($document);
70+
71+
$this->documentNodeTraverser->traverse($document, $context);
72+
73+
self::assertCount(1, $context->getDocumentNode()->getChildren());
74+
self::assertCount(4, $section->getChildren());
75+
self::assertEquals($node4, $section->getChildren()[0]);
76+
self::assertEquals($node3, $section->getChildren()[1]);
77+
self::assertEquals($node2, $section->getChildren()[2]);
78+
self::assertEquals($node1, $section->getChildren()[3]);
79+
}
80+
81+
public function testAnchorShouldNotBeMovedTwice(): void
82+
{
83+
$node1 = new AnchorNode('foo');
84+
$section = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('foo'), 1, 'id'));
85+
$subSection = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('foo'), 1, 'id'));
86+
$section->addChildNode(new AnchorNode('bar'));
87+
$section->addChildNode($subSection);
88+
89+
$document = new DocumentNode('123', 'some/path');
90+
$document->addChildNode($node1);
91+
$document->addChildNode($section);
92+
93+
$context = (new CompilerContext(new ProjectNode('test', 'test')))->withDocumentShadowTree($document);
94+
95+
$this->documentNodeTraverser->traverse($document, $context);
96+
97+
self::assertCount(1, $context->getDocumentNode()->getChildren());
98+
$updatedSection = $context->getDocumentNode()->getChildren()[0];
99+
100+
self::assertCount(2, $updatedSection->getChildren());
101+
self::assertEquals([$node1, $subSection], $context->getDocumentNode()->getChildren()[0]->getChildren());
102+
103+
$updatedSubSection = $updatedSection->getChildren()[1];
104+
self::assertCount(1, $updatedSubSection->getChildren());
105+
}
106+
107+
public function testNoMoveWhenAnchorIsOnlyChild(): void
108+
{
109+
$node = new AnchorNode('foo');
110+
111+
$document = new DocumentNode('123', 'some/path');
112+
$document->addChildNode($node);
113+
114+
$context = (new CompilerContext(new ProjectNode('test', 'test')))->withDocumentShadowTree($document);
115+
116+
$this->documentNodeTraverser->traverse($document, $context);
117+
118+
self::assertCount(1, $context->getDocumentNode()->getChildren());
119+
self::assertSame($node, $context->getDocumentNode()->getChildren()[0]);
120+
}
121+
122+
public function testMoveAnchorsAtTheEndOfSectionToNextSection(): void
123+
{
124+
$node1 = new AnchorNode('foo');
125+
$node2 = new AnchorNode('bar');
126+
$section1 = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('foo'), 1, 'id'));
127+
$section1->addChildNode($node1);
128+
$section1->addChildNode($node2);
129+
130+
$section2 = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('foo'), 1, 'id'));
131+
132+
$document = new DocumentNode('123', 'some/path');
133+
$document->addChildNode($section1);
134+
$document->addChildNode($section2);
135+
136+
$context = (new CompilerContext(new ProjectNode('test', 'test')))->withDocumentShadowTree($document);
137+
138+
$this->documentNodeTraverser->traverse($document, $context);
139+
140+
self::assertCount(2, $context->getDocumentNode()->getChildren());
141+
self::assertCount(0, $context->getDocumentNode()->getChildren()[0]->getChildren());
142+
self::assertCount(2, $context->getDocumentNode()->getChildren()[1]->getChildren());
143+
}
144+
145+
146+
public function testMoveAnchorsAtTheEndOfSectionToNextParentNeighbourSection(): void
147+
{
148+
$node1 = new AnchorNode('foo');
149+
$node2 = new AnchorNode('bar');
150+
$section1 = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('foo'), 1, 'id'));
151+
$subSection = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('foo'), 2, 'id'));
152+
$subSection->addChildNode($node1);
153+
$subSection->addChildNode($node2);
154+
$section1->addChildNode($subSection);
155+
156+
$section2 = new SectionNode(new TitleNode(InlineCompoundNode::getPlainTextInlineNode('foo'), 1, 'id'));
157+
158+
$document = new DocumentNode('123', 'some/path');
159+
$document->addChildNode($section1);
160+
$document->addChildNode($section2);
161+
162+
$context = (new CompilerContext(new ProjectNode('test', 'test')))->withDocumentShadowTree($document);
163+
164+
$this->documentNodeTraverser->traverse($document, $context);
165+
166+
self::assertCount(2, $context->getDocumentNode()->getChildren());
167+
self::assertCount(1, $context->getDocumentNode()->getChildren()[0]->getChildren());
168+
self::assertCount(2, $context->getDocumentNode()->getChildren()[1]->getChildren());
169+
}
170+
}

0 commit comments

Comments
 (0)