Skip to content

Commit 9550239

Browse files
committed
feat: better parse and link @link tags
1 parent 4da09dc commit 9550239

File tree

3 files changed

+197
-13
lines changed

3 files changed

+197
-13
lines changed

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,11 +1885,6 @@ parameters:
18851885
count: 1
18861886
path: src/Renderer/ThemeSet.php
18871887

1888-
-
1889-
message: "#^Method Doctum\\\\Renderer\\\\TwigExtension\\:\\:getSnippet\\(\\) has no return typehint specified\\.$#"
1890-
count: 1
1891-
path: src/Renderer/TwigExtension.php
1892-
18931888
-
18941889
message: "#^Method Doctum\\\\Renderer\\\\TwigExtension\\:\\:parseDesc\\(\\) has no return typehint specified\\.$#"
18951890
count: 1

src/Renderer/TwigExtension.php

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use Doctum\Reflection\Reflection;
1717
use Doctum\Reflection\ClassReflection;
18+
use Doctum\Reflection\FunctionReflection;
1819
use Doctum\Reflection\MethodReflection;
1920
use Doctum\Reflection\PropertyReflection;
2021
use Doctum\Tree;
@@ -94,15 +95,15 @@ public function pathForNamespace(array $context, string $namespace): string
9495

9596
public function pathForMethod(array $context, MethodReflection $method)
9697
{
97-
/** @var Reflection */
98+
/** @var Reflection $class */
9899
$class = $method->getClass();
99100

100101
return $this->relativeUri($this->currentDepth) . str_replace('\\', '/', $class->getName()) . '.html#method_' . $method->getName();
101102
}
102103

103104
public function pathForProperty(array $context, PropertyReflection $property)
104105
{
105-
/** @var Reflection */
106+
/** @var Reflection $class */
106107
$class = $property->getClass();
107108

108109
return $this->relativeUri($this->currentDepth) . str_replace('\\', '/', $class->getName()) . '.html#property_' . $property->getName();
@@ -152,18 +153,88 @@ public function parseDesc(array $context, ?string $desc, Reflection $classOrFunc
152153

153154
$desc = str_replace(['<code>', '</code>'], ['```', '```'], $desc);
154155

155-
// FIXME: the @see argument is more complex than just a class (Class::Method, local method directly, ...)
156-
$desc = preg_replace_callback(
156+
$desc = (string) preg_replace_callback(
157157
'/@see ([^ ]+)/',
158-
static function ($match) {
159-
return 'see ' . $match[1];
158+
function ($match) use (&$classOrFunctionRefl): string {
159+
return $this->transformContentsIntoLinks($match[1], $classOrFunctionRefl);
160160
},
161161
$desc
162162
);
163163

164+
$desc = (string) preg_replace_callback(
165+
'/\{@link ((?!\})(?<contents>[^ ]+))/',
166+
function (array $match) use (&$classOrFunctionRefl): string {
167+
$data = rtrim($match['contents'], '}');
168+
return $this->transformContentsIntoLinks($data, $classOrFunctionRefl);
169+
},
170+
$desc
171+
);
172+
173+
164174
return $this->markdown->text($desc);
165175
}
166176

177+
public function transformContentsIntoLinks(string $data, Reflection $classOrFunctionRefl): string
178+
{
179+
$isClassReflection = $classOrFunctionRefl instanceof ClassReflection;
180+
$isFunctionReflection = $classOrFunctionRefl instanceof FunctionReflection;
181+
if (! $isClassReflection && ! $isFunctionReflection) {
182+
return $data;
183+
}
184+
185+
/** @var ClassReflection|FunctionReflection $class */
186+
$class = $classOrFunctionRefl;
187+
188+
// Example: Foo::bar_function_on_foo_class
189+
$classMethod = explode('::', trim($data, " \t\n\r"), 2);
190+
191+
// Found "bar_function_on_foo_class", from example: bar_function_on_foo_class
192+
if (count($classMethod) === 1 && $class instanceof ClassReflection) {
193+
// In this case we resolve a link to a method name in the current class
194+
$method = $class->getMethod($classMethod[0]);
195+
if ($method !== false) {
196+
$short = $this->pathForMethod([], $method);
197+
return '[' . $data . '](' . $short . ')';
198+
}
199+
}
200+
201+
/** @var \Doctum\Project|null $project Original one is not realistic */
202+
$project = $class->getProject();
203+
if ($project === null) {
204+
// This should never happen
205+
return $data;
206+
}
207+
208+
$cr = $project->getClass($classMethod[0]);
209+
if ($cr->isPhpClass()) {
210+
$className = $cr->getName();
211+
return '[' . $className . '](https://www.php.net/' . $className . ')';
212+
}
213+
214+
if (! $cr->isProjectClass()) {
215+
return $data;
216+
}
217+
218+
// Found "bar_function_on_foo_class", from example: Foo::bar_function_on_foo_class
219+
if (count($classMethod) === 2) {
220+
// In this case we have a function name to resolve on the previously found class
221+
$method = $cr->getMethod($classMethod[1]);
222+
if ($method !== false) {
223+
$short = $this->pathForMethod([], $method);
224+
return '[' . $data . '](' . $short . ')';
225+
}
226+
}
227+
228+
// Final case, we link the found class
229+
$short = $this->pathForClass([], $cr->getName());
230+
return '[' . $data . '](' . $short . ')';
231+
}
232+
233+
/**
234+
* Seems not to be used
235+
*
236+
* @return string
237+
*/
167238
public function getSnippet(string $string)
168239
{
169240
if (preg_match('/^(.{50,}?)\s.*/m', $string, $matches)) {

tests/Renderer/TwigExtensionTest.php

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace Doctum\Tests\Renderer;
66

7+
use Doctum\Reflection\ClassReflection;
78
use Doctum\Reflection\FunctionReflection;
9+
use Doctum\Reflection\MethodReflection;
10+
use Doctum\Reflection\Reflection;
811
use Doctum\Renderer\TwigExtension;
912
use Doctum\Tests\AbstractTestCase;
1013

@@ -16,7 +19,14 @@ class TwigExtensionTest extends AbstractTestCase
1619
*/
1720
public function dataProviderParseDesc(): array
1821
{
22+
$project = $this->getProject();
23+
$ref1 = new FunctionReflection('my_function', 0);
24+
$ref1->setProject($project);
1925
return [
26+
[
27+
'',
28+
''
29+
],
2030
[
2131
'<p>text</p>',
2232
'<p>text</p>'
@@ -25,6 +35,24 @@ public function dataProviderParseDesc(): array
2535
'<p><p>text</p></p>',
2636
'<p><p>text</p></p>'
2737
],
38+
[
39+
'Hi {@link \PDO}',
40+
'<p>Hi \PDO</p>'
41+
],
42+
[
43+
'Hi {@link \PDO}',
44+
'<p>Hi <a href="https://www.php.net/PDO">PDO</a></p>',
45+
$ref1
46+
],
47+
[
48+
'@see \PDO',
49+
'<p>\PDO</p>'
50+
],
51+
[
52+
'@see \PDO',
53+
'<p><a href="https://www.php.net/PDO">PDO</a></p>',
54+
$ref1
55+
],
2856
[
2957
'# H1' . "\n"
3058
. 'Some text' . "\n"
@@ -161,15 +189,105 @@ public function dataProviderParseDesc(): array
161189
/**
162190
* @dataProvider dataProviderParseDesc
163191
*/
164-
public function testParseDesc(string $intput, string $expectedOutput): void
192+
public function testParseDesc(string $intput, string $expectedOutput, ?Reflection $ref = null): void
165193
{
166194
$extension = new TwigExtension();
167195
$this->assertSame(
168196
$expectedOutput,
169197
$extension->parseDesc(
170198
[],
171199
$intput,
172-
new FunctionReflection('', 0)
200+
$ref === null ? new FunctionReflection('', 0) : $ref
201+
)
202+
);
203+
}
204+
205+
/**
206+
* @return array[]
207+
*/
208+
public function dataProviderTransformContentsIntoLinks(): array
209+
{
210+
$project = $this->getProject();
211+
$ref = new FunctionReflection('', 0);
212+
$ref->setProject($project);
213+
$ref2 = new FunctionReflection('my_function', 0);
214+
$ref2->setProject($project);
215+
$ref3 = new ClassReflection('my_class_name', 0);
216+
$ref3->setProject($project);
217+
$ref4 = new ClassReflection('my_class', 0);
218+
$ref4->addMethod(new MethodReflection('myMethod', 0));
219+
$ref4->setProject($project);
220+
$project->addClass($ref4);
221+
return [
222+
[
223+
'\PDO',
224+
'[PDO](https://www.php.net/PDO)',
225+
$ref
226+
],
227+
[
228+
'PDO',
229+
'[PDO](https://www.php.net/PDO)',
230+
$ref
231+
],
232+
[
233+
'\Foo::methodName',
234+
'\Foo::methodName',
235+
$ref
236+
],
237+
[
238+
'my_function',
239+
'my_function',
240+
$ref2
241+
],
242+
[
243+
'my_class_name',
244+
'my_class_name',
245+
$ref3
246+
],
247+
[
248+
'my_class',
249+
'[my_class](my_class.html)',
250+
$ref3
251+
],
252+
[
253+
'my_class::myMethod',
254+
'[my_class::myMethod](my_class.html#method_myMethod)',
255+
$ref3
256+
],
257+
[
258+
'myMethod',
259+
'myMethod',
260+
$ref3
261+
],
262+
[
263+
'myMethod',
264+
'[myMethod](my_class.html#method_myMethod)',
265+
$ref4
266+
],
267+
[
268+
'my_class::myMethod',
269+
'my_class::myMethod',
270+
new MethodReflection('myMethod', 0)
271+
],
272+
[
273+
'my_class',
274+
'my_class',
275+
new ClassReflection('my_class', 0)
276+
],
277+
];
278+
}
279+
280+
/**
281+
* @dataProvider dataProviderTransformContentsIntoLinks
282+
*/
283+
public function testTransformContentsIntoLinks(string $intput, string $expectedOutput, Reflection $refl): void
284+
{
285+
$extension = new TwigExtension();
286+
$this->assertSame(
287+
$expectedOutput,
288+
$extension->transformContentsIntoLinks(
289+
$intput,
290+
$refl
173291
)
174292
);
175293
}

0 commit comments

Comments
 (0)