Skip to content

Commit 55491a3

Browse files
Merge pull request #174 from phel-lang/update-api-docs
Update api docs
2 parents d27e477 + 3a18ad3 commit 55491a3

File tree

5 files changed

+230
-85
lines changed

5 files changed

+230
-85
lines changed

build/src/ApiGenerator/Application/ApiMarkdownGenerator.php

Lines changed: 186 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
final readonly class ApiMarkdownGenerator
1111
{
1212
public function __construct(
13-
private ApiFacadeInterface $apiFacade
13+
private ApiFacadeInterface $apiFacade,
1414
) {
1515
}
1616

@@ -19,68 +19,203 @@ public function __construct(
1919
*/
2020
public function generate(): array
2121
{
22-
$result = $this->zolaHeaders();
23-
24-
/** @var list<PhelFunction> $phelFns */
22+
$result = $this->buildZolaHeaders();
2523
$phelFns = $this->apiFacade->getPhelFunctions();
24+
$groupedByNamespace = $this->groupFunctionsByNamespace($phelFns);
25+
26+
$namespaces = [];
27+
foreach ($groupedByNamespace as $namespace => $functions) {
28+
$namespaces[] = $this->buildNamespaceSection($namespace, $functions);
29+
}
30+
31+
return array_merge($result, ...$namespaces);
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;
43+
}
44+
return $grouped;
45+
}
46+
47+
/**
48+
* @param list<PhelFunction> $functions
49+
* @return list<string>
50+
*/
51+
private function buildNamespaceSection(string $namespace, array $functions): array
52+
{
53+
$elements = [];
54+
foreach ($functions as $fn) {
55+
$elements[] = $this->buildFunctionSection($fn);
56+
}
57+
58+
return array_merge(['', '---', '', "## `{$namespace}`", ''], ...$elements);
59+
}
60+
61+
/**
62+
* @return list<string>
63+
*/
64+
private function buildFunctionSection(PhelFunction $fn): array
65+
{
66+
$lines = ["### `{$fn->nameWithNamespace()}`"];
67+
68+
if ($deprecation = $this->buildDeprecationNotice($fn)) {
69+
$lines[] = $deprecation;
70+
}
71+
72+
// Handle exceptional documentation blocks
73+
if ($fn->name === 'with-mock-wrapper' || $fn->name === 'with-mocks') {
74+
$input = preg_replace('/```phel/', '```clojure', $fn->doc, 1);
75+
$input = preg_replace('/^[ \t]+/m', '', $input);
76+
$input = preg_replace('/(?<!\n)\n(```phel)/', "\n\n$1", $input);
77+
} else {
78+
$input = $fn->doc;
79+
}
80+
81+
$lines[] = $input;
82+
83+
if ($example = $this->buildExampleSection($fn)) {
84+
$lines = array_merge($lines, $example);
85+
}
86+
87+
if ($seeAlso = $this->buildSeeAlsoSection($fn)) {
88+
$lines = array_merge($lines, $seeAlso);
89+
}
90+
91+
if ($sourceLink = $this->buildSourceLink($fn)) {
92+
$lines[] = $sourceLink;
93+
}
94+
$lines[] = '';
95+
96+
return $lines;
97+
}
98+
99+
private function buildDeprecationNotice(PhelFunction $fn): ?string
100+
{
101+
if (!isset($fn->meta['deprecated'])) {
102+
return null;
103+
}
104+
105+
$message = sprintf(
106+
'<small><span style="color: red; font-weight: bold;">Deprecated</span>: %s',
107+
$fn->meta['deprecated'],
108+
);
109+
110+
if (isset($fn->meta['superseded-by'])) {
111+
$supersededBy = $fn->meta['superseded-by'];
112+
$anchor = $this->sanitizeAnchor($supersededBy);
113+
$message .= sprintf(
114+
' &mdash; Use [`%s`](#%s) instead',
115+
$supersededBy,
116+
$anchor,
117+
);
118+
}
119+
120+
return $message . '</small>';
121+
}
122+
123+
/**
124+
* @return list<string>|null
125+
*/
126+
private function buildExampleSection(PhelFunction $fn): ?array
127+
{
128+
if (!isset($fn->meta['example'])) {
129+
return null;
30130
}
31131

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-
' &mdash; 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-
}
132+
return [
133+
'',
134+
'**Example:**',
135+
'',
136+
'```clojure',
137+
$fn->meta['example'],
138+
'```',
139+
];
140+
}
141+
142+
/**
143+
* @return list<string>|null
144+
*/
145+
private function buildSeeAlsoSection(PhelFunction $fn): ?array
146+
{
147+
if (!isset($fn->meta['see-also'])) {
148+
return null;
65149
}
66150

67-
return $result;
151+
$functionNames = $this->extractFunctionNames($fn->meta['see-also']);
152+
$links = $this->buildFunctionLinks($functionNames);
153+
154+
return [
155+
'',
156+
'**See also:** ' . implode(', ', $links),
157+
];
158+
}
159+
160+
/**
161+
* @return list<string>
162+
*/
163+
private function extractFunctionNames(mixed $seeAlso): array
164+
{
165+
return iterator_to_array($seeAlso);
166+
}
167+
168+
/**
169+
* @param list<string> $functionNames
170+
* @return list<string>
171+
*/
172+
private function buildFunctionLinks(array $functionNames): array
173+
{
174+
return array_map(
175+
fn(string $func) => sprintf('[`%s`](#%s)', $func, $this->sanitizeAnchor($func)),
176+
$functionNames,
177+
);
178+
}
179+
180+
private function buildSourceLink(PhelFunction $fn): ?string
181+
{
182+
if ($fn->githubUrl !== '') {
183+
return sprintf('<small>[[View source](%s)]</small>', $fn->githubUrl);
184+
}
185+
186+
if ($fn->docUrl !== '') {
187+
return sprintf('<small>[[Read more](%s)]</small>', $fn->docUrl);
188+
}
189+
190+
return null;
191+
}
192+
193+
/**
194+
* Sanitize function name to match Zola's anchor generation.
195+
* Removes special characters that Zola doesn't include in anchors.
196+
*
197+
* Examples:
198+
* "empty?" becomes "empty"
199+
* "set!" becomes "set"
200+
* "php-array-to-map" stays "php-array-to-map"
201+
*/
202+
private function sanitizeAnchor(string $funcName): string
203+
{
204+
return preg_replace('/[^a-zA-Z0-9_-]/', '', $funcName);
68205
}
69206

70207
/**
71208
* @return list<string>
72209
*/
73-
private function zolaHeaders(): array
210+
private function buildZolaHeaders(): array
74211
{
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[] = '';
83-
84-
return $result;
212+
return [
213+
'+++',
214+
'title = "API"',
215+
'weight = 110',
216+
'template = "page-api.html"',
217+
'aliases = [ "/api" ]',
218+
'+++',
219+
];
85220
}
86221
}

build/tests/php/ApiGenerator/Domain/ApiMarkdownGeneratorTest.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public function test_generate_page_without_phel_functions(): void
2424
'template = "page-api.html"',
2525
'aliases = [ "/api" ]',
2626
'+++',
27-
'',
2827
];
2928

3029
self::assertEquals($expected, $generator->generate());
@@ -53,12 +52,13 @@ public function test_generate_page_with_one_phel_function(): void
5352
'aliases = [ "/api" ]',
5453
'+++',
5554
'',
56-
'',
5755
'---',
5856
'',
5957
'## `ns-1`',
58+
'',
6059
'### `ns-1/function-1`',
6160
'The doc from function 1',
61+
'',
6262
];
6363

6464
self::assertEquals($expected, $generator->generate());
@@ -91,14 +91,16 @@ public function test_generate_page_with_multiple_phel_functions_in_same_group():
9191
'aliases = [ "/api" ]',
9292
'+++',
9393
'',
94-
'',
9594
'---',
9695
'',
9796
'## `core`',
97+
'',
9898
'### `function-1`',
9999
'The doc from function 1',
100+
'',
100101
'### `function-2`',
101102
'The doc from function 2',
103+
'',
102104
];
103105

104106
self::assertEquals($expected, $generator->generate());
@@ -131,18 +133,21 @@ public function test_generate_page_with_multiple_phel_functions_in_different_gro
131133
'aliases = [ "/api" ]',
132134
'+++',
133135
'',
134-
'',
135136
'---',
136137
'',
137138
'## `ns-1`',
139+
'',
138140
'### `ns-1/function-1`',
139141
'The doc from function 1',
140142
'',
143+
'',
141144
'---',
142145
'',
143146
'## `ns-2`',
147+
'',
144148
'### `ns-2/function-2`',
145149
'The doc from function 2',
150+
'',
146151
];
147152

148153
self::assertEquals($expected, $generator->generate());

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"require": {
1212
"php": ">=8.3",
1313
"ext-json": "*",
14-
"phel-lang/phel-lang": "^0.26",
14+
"phel-lang/phel-lang": "dev-main",
1515
"gacela-project/gacela": "^1.12"
1616
},
1717
"require-dev": {

0 commit comments

Comments
 (0)