1010use phpDocumentor \Guides \Nodes \DocumentTree \SectionEntryNode ;
1111use phpDocumentor \Guides \Nodes \Menu \MenuEntryNode ;
1212use phpDocumentor \Guides \Nodes \Menu \MenuNode ;
13+ use phpDocumentor \Guides \Nodes \Menu \NavMenuNode ;
1314use phpDocumentor \Guides \Nodes \Menu \TocNode ;
1415use phpDocumentor \Guides \Nodes \Node ;
1516use Psr \Log \LoggerInterface ;
1819use function assert ;
1920use function explode ;
2021use function implode ;
22+ use function in_array ;
2123use function preg_match ;
2224use function sprintf ;
2325use function str_replace ;
2426use function str_starts_with ;
2527
2628/** @implements NodeTransformer<MenuNode> */
27- class TocNodeWithDocumentEntryTransformer implements NodeTransformer
29+ class MenuNodeAddEntryTransformer implements NodeTransformer
2830{
2931 public function __construct (
3032 private readonly LoggerInterface $ logger ,
@@ -38,12 +40,13 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node
3840
3941 public function leaveNode (Node $ node , CompilerContext $ compilerContext ): Node |null
4042 {
41- if (!$ node instanceof TocNode) {
43+ if (!$ node instanceof TocNode && ! $ node instanceof NavMenuNode ) {
4244 return $ node ;
4345 }
4446
4547 $ files = $ node ->getFiles ();
4648 $ glob = $ node ->hasOption ('glob ' );
49+ $ globExclude = explode (', ' , $ node ->getOption ('globExclude ' ) . '' );
4750
4851 $ documentEntries = $ compilerContext ->getProjectNode ()->getAllDocumentEntries ();
4952 $ currentPath = $ compilerContext ->getDocumentNode ()->getFilePath ();
@@ -53,48 +56,25 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu
5356 foreach ($ files as $ file ) {
5457 foreach ($ documentEntries as $ documentEntry ) {
5558 if (
56- !self ::isEqualAbsolutePath ($ documentEntry ->getFile (), $ file , $ currentPath , $ glob )
57- && !self ::isEqualRelativePath ($ documentEntry ->getFile (), $ file , $ currentPath , $ glob )
59+ !self ::isEqualAbsolutePath ($ documentEntry ->getFile (), $ file , $ currentPath , $ glob, $ globExclude )
60+ && !self ::isEqualRelativePath ($ documentEntry ->getFile (), $ file , $ currentPath , $ glob, $ globExclude )
5861 ) {
5962 continue ;
6063 }
6164
65+ if ($ node instanceof TocNode && $ glob && self ::isCurrent ($ documentEntry , $ currentPath )) {
66+ // TocNodes do not select the current page in glob mode. In a menu we might want to display it
67+ continue ;
68+ }
69+
6270 $ documentEntriesInTree [] = $ documentEntry ;
6371 $ menuEntry = new MenuEntryNode ($ documentEntry ->getFile (), $ documentEntry ->getTitle (), [], false , 1 );
6472 if (!$ node ->hasOption ('titlesonly ' )) {
65- foreach ($ documentEntry ->getSections () as $ section ) {
66- // We do not add the main section as it repeats the document title
67- foreach ($ section ->getChildren () as $ subSectionEntryNode ) {
68- assert ($ subSectionEntryNode instanceof SectionEntryNode);
69- $ currentLevel = $ menuEntry ->getLevel () + 1 ;
70- $ sectionMenuEntry = new MenuEntryNode ($ documentEntry ->getFile (), $ subSectionEntryNode ->getTitle (), [], false , $ currentLevel , $ subSectionEntryNode ->getId ());
71- $ menuEntry ->addSection ($ sectionMenuEntry );
72- $ this ->addSubSections ($ sectionMenuEntry , $ subSectionEntryNode , $ documentEntry , $ currentLevel );
73- }
74- }
73+ $ this ->addSubSectionsToMenuEntries ($ documentEntry , $ menuEntry );
7574 }
7675
77- foreach ($ documentEntriesInTree as $ documentEntryInToc ) {
78- if ($ documentEntryInToc ->isRoot ()) {
79- // The root page may not be attached to any other
80- continue ;
81- }
82-
83- if ($ documentEntryInToc ->getParent () !== null && $ documentEntryInToc ->getParent () !== $ compilerContext ->getDocumentNode ()->getDocumentEntry ()) {
84- $ this ->logger ->warning (sprintf (
85- 'Document %s has been added to parents %s and %s ' ,
86- $ documentEntryInToc ->getFile (),
87- $ documentEntryInToc ->getParent ()->getFile (),
88- $ compilerContext ->getDocumentNode ()->getDocumentEntry ()->getFile (),
89- ));
90- }
91-
92- if ($ documentEntryInToc ->getParent () !== null ) {
93- continue ;
94- }
95-
96- $ documentEntryInToc ->setParent ($ compilerContext ->getDocumentNode ()->getDocumentEntry ());
97- $ compilerContext ->getDocumentNode ()->getDocumentEntry ()->addChild ($ documentEntryInToc );
76+ if ($ node instanceof TocNode) {
77+ $ this ->attachDocumentEntriesToParents ($ documentEntriesInTree , $ compilerContext , $ currentPath );
9878 }
9979
10080 $ menuEntries [] = $ menuEntry ;
@@ -110,6 +90,11 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu
11090 return $ node ;
11191 }
11292
93+ private function isCurrent (DocumentEntryNode $ documentEntry , string $ currentPath ): bool
94+ {
95+ return $ documentEntry ->getFile () === $ currentPath ;
96+ }
97+
11398 private function addSubSections (MenuEntryNode $ sectionMenuEntry , SectionEntryNode $ sectionEntryNode , DocumentEntryNode $ documentEntry , int $ currentLevel ): void
11499 {
115100 foreach ($ sectionEntryNode ->getChildren () as $ subSectionEntryNode ) {
@@ -120,7 +105,7 @@ private function addSubSections(MenuEntryNode $sectionMenuEntry, SectionEntryNod
120105
121106 public function supports (Node $ node ): bool
122107 {
123- return $ node instanceof TocNode;
108+ return $ node instanceof TocNode || $ node instanceof NavMenuNode ;
124109 }
125110
126111 public function getPriority (): int
@@ -129,7 +114,8 @@ public function getPriority(): int
129114 return 4500 ;
130115 }
131116
132- private static function isEqualAbsolutePath (string $ actualFile , string $ expectedFile , string $ currentFile , bool $ glob ): bool
117+ /** @param String[] $globExclude */
118+ private static function isEqualAbsolutePath (string $ actualFile , string $ expectedFile , string $ currentFile , bool $ glob , array $ globExclude ): bool
133119 {
134120 if (!self ::isAbsoluteFile ($ expectedFile )) {
135121 return false ;
@@ -139,10 +125,11 @@ private static function isEqualAbsolutePath(string $actualFile, string $expected
139125 return true ;
140126 }
141127
142- return self ::isGlob ($ glob , $ actualFile , $ currentFile , $ expectedFile , '/ ' );
128+ return self ::isGlob ($ glob , $ actualFile , $ currentFile , $ expectedFile , '/ ' , $ globExclude );
143129 }
144130
145- private static function isEqualRelativePath (string $ actualFile , string $ expectedFile , string $ currentFile , bool $ glob ): bool
131+ /** @param String[] $globExclude */
132+ private static function isEqualRelativePath (string $ actualFile , string $ expectedFile , string $ currentFile , bool $ glob , array $ globExclude ): bool
146133 {
147134 if (self ::isAbsoluteFile ($ expectedFile )) {
148135 return false ;
@@ -157,12 +144,13 @@ private static function isEqualRelativePath(string $actualFile, string $expected
157144 return true ;
158145 }
159146
160- return self ::isGlob ($ glob , $ actualFile , $ currentFile , $ absoluteExpectedFile , '' );
147+ return self ::isGlob ($ glob , $ actualFile , $ currentFile , $ absoluteExpectedFile , '' , $ globExclude );
161148 }
162149
163- private static function isGlob (bool $ glob , string $ documentEntryFile , string $ currentPath , string $ file , string $ prefix ): bool
150+ /** @param String[] $globExclude */
151+ private static function isGlob (bool $ glob , string $ documentEntryFile , string $ currentPath , string $ file , string $ prefix , array $ globExclude ): bool
164152 {
165- if ($ glob && $ documentEntryFile !== $ currentPath ) {
153+ if ($ glob && ! in_array ( $ documentEntryFile, $ globExclude ) ) {
166154 $ file = str_replace ('* ' , '[^\/]* ' , $ file );
167155 $ pattern = '`^ ' . $ file . '$` ' ;
168156
@@ -184,4 +172,56 @@ public static function isAbsoluteFile(string $expectedFile): bool
184172 {
185173 return str_starts_with ($ expectedFile , '/ ' );
186174 }
175+
176+ /** @param DocumentEntryNode[] $documentEntriesInTree */
177+ private function attachDocumentEntriesToParents (
178+ array $ documentEntriesInTree ,
179+ CompilerContext $ compilerContext ,
180+ string $ currentPath ,
181+ ): void {
182+ foreach ($ documentEntriesInTree as $ documentEntryInToc ) {
183+ if ($ documentEntryInToc ->isRoot () || $ currentPath === $ documentEntryInToc ->getFile ()) {
184+ // The root page may not be attached to any other
185+ continue ;
186+ }
187+
188+ if ($ documentEntryInToc ->getParent () !== null && $ documentEntryInToc ->getParent () !== $ compilerContext ->getDocumentNode ()->getDocumentEntry ()) {
189+ $ this ->logger ->warning (sprintf (
190+ 'Document %s has been added to parents %s and %s. The `toctree` directive changes the '
191+ . 'position of documents in the document tree. Use the `menu` directive to only display a menu without changing the document tree. ' ,
192+ $ documentEntryInToc ->getFile (),
193+ $ documentEntryInToc ->getParent ()->getFile (),
194+ $ compilerContext ->getDocumentNode ()->getDocumentEntry ()->getFile (),
195+ ));
196+ }
197+
198+ if ($ documentEntryInToc ->getParent () !== null ) {
199+ continue ;
200+ }
201+
202+ $ documentEntryInToc ->setParent ($ compilerContext ->getDocumentNode ()->getDocumentEntry ());
203+ $ compilerContext ->getDocumentNode ()->getDocumentEntry ()->addChild ($ documentEntryInToc );
204+ }
205+ }
206+
207+ private function addSubSectionsToMenuEntries (DocumentEntryNode $ documentEntry , MenuEntryNode $ menuEntry ): void
208+ {
209+ foreach ($ documentEntry ->getSections () as $ section ) {
210+ // We do not add the main section as it repeats the document title
211+ foreach ($ section ->getChildren () as $ subSectionEntryNode ) {
212+ assert ($ subSectionEntryNode instanceof SectionEntryNode);
213+ $ currentLevel = $ menuEntry ->getLevel () + 1 ;
214+ $ sectionMenuEntry = new MenuEntryNode (
215+ $ documentEntry ->getFile (),
216+ $ subSectionEntryNode ->getTitle (),
217+ [],
218+ false ,
219+ $ currentLevel ,
220+ $ subSectionEntryNode ->getId (),
221+ );
222+ $ menuEntry ->addSection ($ sectionMenuEntry );
223+ $ this ->addSubSections ($ sectionMenuEntry , $ subSectionEntryNode , $ documentEntry , $ currentLevel );
224+ }
225+ }
226+ }
187227}
0 commit comments