55namespace PhelWeb \ApiGenerator \Application ;
66
77use Phel \Api \Transfer \PhelFunction ;
8+ use Phel \Lang \Symbol ;
89use Phel \Shared \Facade \ApiFacadeInterface ;
910
1011final readonly class ApiMarkdownGenerator
@@ -19,68 +20,206 @@ public function __construct(
1920 */
2021 public function generate (): array
2122 {
22- $ result = $ this ->zolaHeaders ();
23-
24- /** @var list<PhelFunction> $phelFns */
23+ $ result = $ this ->buildZolaHeaders ();
2524 $ phelFns = $ this ->apiFacade ->getPhelFunctions ();
25+ $ groupedByNamespace = $ this ->groupFunctionsByNamespace ($ phelFns );
26+
27+ foreach ($ groupedByNamespace as $ namespace => $ functions ) {
28+ $ result = array_merge ($ result , $ this ->buildNamespaceSection ($ namespace , $ functions ));
29+ }
30+
31+ return $ result ;
32+ }
2633
27- $ groupedByNamespace = [];
34+ /**
35+ * @param list<PhelFunction> $phelFns
36+ * @return array<string, list<PhelFunction>>
37+ */
38+ private function groupFunctionsByNamespace (array $ phelFns ): array
39+ {
40+ $ grouped = [];
2841 foreach ($ phelFns as $ fn ) {
29- $ groupedByNamespace [$ fn ->namespace ][] = $ fn ;
42+ $ grouped [$ fn ->namespace ][] = $ fn ;
3043 }
44+ return $ grouped ;
45+ }
3146
32- foreach ($ groupedByNamespace as $ namespace => $ fns ) {
33-
34- $ result [] = "" ;
35- $ result [] = "--- " ;
36- $ result [] = "" ;
37- $ result [] = "## ` {$ namespace }` " ;
38-
39- /** @var PhelFunction $fn */
40- foreach ($ fns as $ fn ) {
41- $ result [] = "### ` {$ fn ->nameWithNamespace ()}` " ;
42- if (isset ($ fn ->meta ['deprecated ' ])) {
43- $ deprecatedMessage = sprintf (
44- '<small><span style="color: red; font-weight: bold;">Deprecated</span>: %s ' ,
45- $ fn ->meta ['deprecated ' ]
46- );
47- if (isset ($ fn ->meta ['superseded-by ' ])) {
48- $ supersededBy = $ fn ->meta ['superseded-by ' ];
49- $ deprecatedMessage .= sprintf (
50- ' — Use [`%s`](#%s) instead ' ,
51- $ supersededBy ,
52- $ supersededBy
53- );
54- }
55- $ deprecatedMessage .= '</small> ' ;
56- $ result [] = $ deprecatedMessage ;
57- }
58- $ result [] = $ fn ->doc ;
59- if ($ fn ->githubUrl !== '' ) {
60- $ result [] = sprintf ('<small>[[View source](%s)]</small> ' , $ fn ->githubUrl );
61- } elseif ($ fn ->docUrl !== '' ) {
62- $ result [] = sprintf ('<small>[[Read more](%s)]</small> ' , $ fn ->docUrl );
63- }
64- }
47+ /**
48+ * @param list<PhelFunction> $functions
49+ * @return list<string>
50+ */
51+ private function buildNamespaceSection (string $ namespace , array $ functions ): array
52+ {
53+ $ lines = [
54+ '' ,
55+ '--- ' ,
56+ '' ,
57+ "## ` {$ namespace }` " ,
58+ ];
59+
60+ foreach ($ functions as $ fn ) {
61+ $ lines = array_merge ($ lines , $ this ->buildFunctionSection ($ fn ));
6562 }
6663
67- return $ result ;
64+ return $ lines ;
6865 }
6966
7067 /**
7168 * @return list<string>
7269 */
73- private function zolaHeaders ( ): array
70+ private function buildFunctionSection ( PhelFunction $ fn ): array
7471 {
75- $ result = [];
76- $ result [] = '+++ ' ;
77- $ result [] = 'title = "API" ' ;
78- $ result [] = 'weight = 110 ' ;
79- $ result [] = 'template = "page-api.html" ' ;
80- $ result [] = 'aliases = [ "/api" ] ' ;
81- $ result [] = '+++ ' ;
82- $ result [] = '' ;
72+ $ lines = ["### ` {$ fn ->nameWithNamespace ()}` " ];
8373
84- return $ result ;
74+ if ($ deprecation = $ this ->buildDeprecationNotice ($ fn )) {
75+ $ lines [] = $ deprecation ;
76+ }
77+
78+ $ lines [] = $ fn ->doc ;
79+
80+ if ($ example = $ this ->buildExampleSection ($ fn )) {
81+ $ lines = array_merge ($ lines , $ example );
82+ }
83+
84+ if ($ seeAlso = $ this ->buildSeeAlsoSection ($ fn )) {
85+ $ lines = array_merge ($ lines , $ seeAlso );
86+ }
87+
88+ if ($ sourceLink = $ this ->buildSourceLink ($ fn )) {
89+ $ lines [] = $ sourceLink ;
90+ }
91+
92+ return $ lines ;
93+ }
94+
95+ private function buildDeprecationNotice (PhelFunction $ fn ): ?string
96+ {
97+ if (!isset ($ fn ->meta ['deprecated ' ])) {
98+ return null ;
99+ }
100+
101+ $ message = sprintf (
102+ '<small><span style="color: red; font-weight: bold;">Deprecated</span>: %s ' ,
103+ $ fn ->meta ['deprecated ' ]
104+ );
105+
106+ if (isset ($ fn ->meta ['superseded-by ' ])) {
107+ $ supersededBy = $ fn ->meta ['superseded-by ' ];
108+ $ anchor = $ this ->sanitizeAnchor ($ supersededBy );
109+ $ message .= sprintf (
110+ ' — Use [`%s`](#%s) instead ' ,
111+ $ supersededBy ,
112+ $ anchor
113+ );
114+ }
115+
116+ return $ message . '</small> ' ;
117+ }
118+
119+ /**
120+ * @return list<string>|null
121+ */
122+ private function buildExampleSection (PhelFunction $ fn ): ?array
123+ {
124+ if (!isset ($ fn ->meta ['example ' ])) {
125+ return null ;
126+ }
127+
128+ return [
129+ '' ,
130+ '**Example:** ' ,
131+ '' ,
132+ '```phel ' ,
133+ $ fn ->meta ['example ' ],
134+ '``` ' ,
135+ ];
136+ }
137+
138+ /**
139+ * @return list<string>|null
140+ */
141+ private function buildSeeAlsoSection (PhelFunction $ fn ): ?array
142+ {
143+ if (!isset ($ fn ->meta ['see-also ' ])) {
144+ return null ;
145+ }
146+
147+ $ functionNames = $ this ->extractFunctionNames ($ fn ->meta ['see-also ' ]);
148+ $ links = $ this ->buildFunctionLinks ($ functionNames );
149+
150+ return [
151+ '' ,
152+ '**See also:** ' . implode (', ' , $ links ),
153+ ];
154+ }
155+
156+ /**
157+ * @return list<string>
158+ */
159+ private function extractFunctionNames (mixed $ seeAlso ): array
160+ {
161+ return array_map (
162+ fn (Symbol $ symbol ) => $ symbol ->getName (),
163+ iterator_to_array ($ seeAlso )
164+ );
165+ }
166+
167+ /**
168+ * @param list<string> $functionNames
169+ * @return list<string>
170+ */
171+ private function buildFunctionLinks (array $ functionNames ): array
172+ {
173+ return array_map (
174+ fn (string $ func ) => sprintf (
175+ '[`%s`](#%s) ' ,
176+ $ func ,
177+ $ this ->sanitizeAnchor ($ func )
178+ ),
179+ $ functionNames
180+ );
181+ }
182+
183+ private function buildSourceLink (PhelFunction $ fn ): ?string
184+ {
185+ if ($ fn ->githubUrl !== '' ) {
186+ return sprintf ('<small>[[View source](%s)]</small> ' , $ fn ->githubUrl );
187+ }
188+
189+ if ($ fn ->docUrl !== '' ) {
190+ return sprintf ('<small>[[Read more](%s)]</small> ' , $ fn ->docUrl );
191+ }
192+
193+ return null ;
194+ }
195+
196+ /**
197+ * Sanitize function name to match Zola's anchor generation.
198+ * Removes special characters that Zola doesn't include in anchors.
199+ *
200+ * Examples:
201+ * "empty?" becomes "empty"
202+ * "set!" becomes "set"
203+ * "php-array-to-map" stays "php-array-to-map"
204+ */
205+ private function sanitizeAnchor (string $ funcName ): string
206+ {
207+ return preg_replace ('/[^a-zA-Z0-9_-]/ ' , '' , $ funcName );
208+ }
209+
210+ /**
211+ * @return list<string>
212+ */
213+ private function buildZolaHeaders (): array
214+ {
215+ return [
216+ '+++ ' ,
217+ 'title = "API" ' ,
218+ 'weight = 110 ' ,
219+ 'template = "page-api.html" ' ,
220+ 'aliases = [ "/api" ] ' ,
221+ '+++ ' ,
222+ '' ,
223+ ];
85224 }
86225}
0 commit comments