1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace ApiGen\Renderer\Latte; |
4: | |
5: | use ApiGen\Index\Index; |
6: | use ApiGen\Index\NamespaceIndex; |
7: | use ApiGen\Info\ClassInfo; |
8: | use ApiGen\Info\ClassLikeInfo; |
9: | use ApiGen\Info\ClassLikeReferenceInfo; |
10: | use ApiGen\Info\ConstantReferenceInfo; |
11: | use ApiGen\Info\ElementInfo; |
12: | use ApiGen\Info\EnumInfo; |
13: | use ApiGen\Info\FunctionInfo; |
14: | use ApiGen\Info\FunctionReferenceInfo; |
15: | use ApiGen\Info\InterfaceInfo; |
16: | use ApiGen\Info\MemberReferenceInfo; |
17: | use ApiGen\Info\MethodReferenceInfo; |
18: | use ApiGen\Info\PropertyReferenceInfo; |
19: | use ApiGen\Info\TraitInfo; |
20: | use ApiGen\Renderer\Filter; |
21: | use ApiGen\Renderer\SourceHighlighter; |
22: | use ApiGen\Renderer\UrlGenerator; |
23: | use Latte\Runtime\Html; |
24: | use League\CommonMark\ConverterInterface; |
25: | use Nette\Utils\Strings; |
26: | use Nette\Utils\Validators; |
27: | use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; |
28: | use ReflectionFunction; |
29: | |
30: | use function function_exists; |
31: | use function get_debug_type; |
32: | use function html_entity_decode; |
33: | use function implode; |
34: | use function is_array; |
35: | use function is_string; |
36: | use function sprintf; |
37: | use function str_contains; |
38: | use function strip_tags; |
39: | use function strtr; |
40: | use function substr_count; |
41: | |
42: | use const ENT_HTML5; |
43: | use const ENT_QUOTES; |
44: | |
45: | |
46: | class LatteFunctions |
47: | { |
48: | public function __construct( |
49: | protected Filter $filter, |
50: | protected UrlGenerator $url, |
51: | protected SourceHighlighter $sourceHighlighter, |
52: | protected ConverterInterface $markdown, |
53: | ) { |
54: | } |
55: | |
56: | |
57: | public function isClass(ClassLikeInfo $info): bool |
58: | { |
59: | return $info instanceof ClassInfo; |
60: | } |
61: | |
62: | |
63: | public function isInterface(ClassLikeInfo $info): bool |
64: | { |
65: | return $info instanceof InterfaceInfo; |
66: | } |
67: | |
68: | |
69: | public function isTrait(ClassLikeInfo $info): bool |
70: | { |
71: | return $info instanceof TraitInfo; |
72: | } |
73: | |
74: | |
75: | public function isEnum(ClassLikeInfo $info): bool |
76: | { |
77: | return $info instanceof EnumInfo; |
78: | } |
79: | |
80: | |
81: | public function textWidth(string $text): int |
82: | { |
83: | return Strings::length($text) + 3 * substr_count($text, "\t"); |
84: | } |
85: | |
86: | |
87: | public function htmlWidth(Html $html): int |
88: | { |
89: | $text = html_entity_decode(strip_tags((string) $html), ENT_QUOTES | ENT_HTML5, 'UTF-8'); |
90: | return $this->textWidth($text); |
91: | } |
92: | |
93: | |
94: | public function highlight(string $path): Html |
95: | { |
96: | return new Html($this->sourceHighlighter->highlight($path)); |
97: | } |
98: | |
99: | |
100: | |
101: | |
102: | |
103: | public function shortDescription(Index $index, ?ClassLikeInfo $classLike, array|string $nodes): Html |
104: | { |
105: | $description = is_array($nodes) ? $this->descriptionText($index, $classLike, $nodes) : $nodes; |
106: | $base = Strings::before($description, "\n\n") ?? $description; |
107: | $html = $this->markdown->convert($base)->getContent(); |
108: | return new Html(Strings::before($html, '<p>', 2) ?? $html); |
109: | } |
110: | |
111: | |
112: | |
113: | |
114: | |
115: | public function longDescription(Index $index, ?ClassLikeInfo $classLike, array|string $nodes): Html |
116: | { |
117: | $description = is_array($nodes) ? $this->descriptionText($index, $classLike, $nodes) : $nodes; |
118: | return new Html($this->markdown->convert($description)->getContent()); |
119: | } |
120: | |
121: | |
122: | public function elementName(ElementInfo $info): string |
123: | { |
124: | if ($info instanceof ClassLikeInfo || $info instanceof FunctionInfo) { |
125: | return $info->name->short; |
126: | |
127: | } elseif ($info instanceof NamespaceIndex) { |
128: | return $info->name->full === '' ? 'none' : $info->name->full; |
129: | |
130: | } else { |
131: | throw new \LogicException(sprintf('Unexpected element type %s', get_debug_type($info))); |
132: | } |
133: | } |
134: | |
135: | |
136: | public function elementShortDescription(Index $index, ?ClassLikeInfo $classLike, ElementInfo $info): Html |
137: | { |
138: | if ($info instanceof ClassLikeInfo || $info instanceof FunctionInfo) { |
139: | return $this->shortDescription($index, $classLike, $info->description); |
140: | |
141: | } elseif ($info instanceof NamespaceIndex) { |
142: | return new Html(''); |
143: | |
144: | } else { |
145: | throw new \LogicException(sprintf('Unexpected element type %s', get_debug_type($info))); |
146: | } |
147: | } |
148: | |
149: | |
150: | public function elementPageExists(ElementInfo $info): bool |
151: | { |
152: | if ($info instanceof ClassLikeInfo) { |
153: | return $this->filter->filterClassLikePage($info); |
154: | |
155: | } elseif ($info instanceof NamespaceIndex) { |
156: | return $this->filter->filterNamespacePage($info); |
157: | |
158: | } elseif ($info instanceof FunctionInfo) { |
159: | return $this->filter->filterFunctionPage($info); |
160: | |
161: | } else { |
162: | throw new \LogicException(sprintf('Unexpected element type %s', get_debug_type($info))); |
163: | } |
164: | } |
165: | |
166: | |
167: | public function elementUrl(ElementInfo $info): string |
168: | { |
169: | if ($info instanceof ClassLikeInfo) { |
170: | return $this->url->getClassLikeUrl($info); |
171: | |
172: | } elseif ($info instanceof NamespaceIndex) { |
173: | return $this->url->getNamespaceUrl($info); |
174: | |
175: | } elseif ($info instanceof FunctionInfo) { |
176: | return $this->url->getFunctionUrl($info); |
177: | |
178: | } else { |
179: | throw new \LogicException(sprintf('Unexpected element type %s', get_debug_type($info))); |
180: | } |
181: | } |
182: | |
183: | |
184: | |
185: | |
186: | |
187: | protected function descriptionText(Index $index, ?ClassLikeInfo $scope, array $nodes): string |
188: | { |
189: | $text = []; |
190: | |
191: | foreach ($nodes as $node) { |
192: | $url = null; |
193: | $title = null; |
194: | |
195: | foreach ($node->getAttribute('targets') ?? [] as $target) { |
196: | if ($target instanceof ClassLikeReferenceInfo) { |
197: | $classLike = $target->resolve($index, $scope); |
198: | |
199: | if ($classLike !== null && $this->filter->filterClassLikePage($classLike)) { |
200: | $url = $this->url->getClassLikeUrl($classLike); |
201: | $title = $classLike->name->full; |
202: | break; |
203: | } |
204: | |
205: | } elseif ($target instanceof MemberReferenceInfo) { |
206: | $classLike = $target->classLike->resolve($index, $scope); |
207: | |
208: | if ($classLike === null || !$this->filter->filterClassLikePage($classLike)) { |
209: | continue; |
210: | |
211: | } elseif ($target instanceof ConstantReferenceInfo) { |
212: | $member = $classLike->constants[$target->name] ?? null; |
213: | $memberLabel = $member?->name; |
214: | |
215: | } elseif ($target instanceof PropertyReferenceInfo) { |
216: | $member = $classLike->properties[$target->name] ?? null; |
217: | $memberLabel = $member !== null ? '$' . $member->name : null; |
218: | |
219: | } elseif ($target instanceof MethodReferenceInfo) { |
220: | $member = $classLike->methods[$target->nameLower] ?? null; |
221: | $memberLabel = $member !== null ? $member->name . '()' : null; |
222: | |
223: | } else { |
224: | throw new \LogicException('Unexpected member reference type: ' . get_debug_type($target)); |
225: | } |
226: | |
227: | if ($member !== null && $memberLabel !== null) { |
228: | $url = $this->url->getMemberUrl($classLike, $member); |
229: | $title = $classLike->name->full . '::' . $memberLabel; |
230: | break; |
231: | } |
232: | |
233: | } elseif ($target instanceof FunctionReferenceInfo) { |
234: | $function = $index->function[$target->fullLower] ?? null; |
235: | |
236: | if ($function !== null && $this->filter->filterFunctionPage($function)) { |
237: | $url = $this->url->getFunctionUrl($function); |
238: | $title = $function->name->full . '()'; |
239: | break; |
240: | |
241: | } elseif (function_exists($target->fullLower) && !str_contains($target->fullLower, '\\') && (new ReflectionFunction($target->fullLower))->isInternal()) { |
242: | $url = sprintf('https://www.php.net/manual/en/function.%s', strtr($target->fullLower, '_', '-')); |
243: | $title = $target->full . '()'; |
244: | break; |
245: | } |
246: | |
247: | } elseif (is_string($target) && Validators::isUrl($target)) { |
248: | $url = $target; |
249: | break; |
250: | } |
251: | } |
252: | |
253: | if ($url === null) { |
254: | $text[] = $node->text; |
255: | |
256: | } elseif ($title === null || $title === $node->text) { |
257: | $text[] = sprintf('[%s](%s)', $node->text, $url); |
258: | |
259: | } else { |
260: | $text[] = sprintf('[%s](%s "%s")', $node->text, $url, $title); |
261: | } |
262: | } |
263: | |
264: | return implode(' ', $text); |
265: | } |
266: | } |
267: | |