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: * @param PhpDocTextNode[]|string $nodes indexed by []
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: * @param PhpDocTextNode[]|string $nodes indexed by []
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: * @param PhpDocTextNode[] $nodes indexed by []
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: