Kodoc.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. <?php
  2. /**
  3. * Documentation generator.
  4. *
  5. * @package Kohana/Userguide
  6. * @category Base
  7. * @author Kohana Team
  8. * @copyright (c) Kohana Team
  9. * @license https://koseven.ga/LICENSE.md
  10. */
  11. class Kohana_Kodoc {
  12. /**
  13. * @var string PCRE fragment for matching 'Class', 'Class::method', 'Class::method()' or 'Class::$property'
  14. */
  15. public static $regex_class_member = '((\w++)(?:::(\$?\w++))?(?:\(\))?)';
  16. /**
  17. * Make a class#member API link using an array of matches from [Kodoc::$regex_class_member]
  18. *
  19. * @param array $matches array( 1 => link text, 2 => class name, [3 => member name] )
  20. * @return string
  21. */
  22. public static function link_class_member($matches)
  23. {
  24. $link = $matches[1];
  25. $class = $matches[2];
  26. $member = NULL;
  27. if (isset($matches[3]))
  28. {
  29. // If the first char is a $ it is a property, e.g. Kohana::$base_url
  30. if ($matches[3][0] === '$')
  31. {
  32. $member = '#property:'.substr($matches[3], 1);
  33. }
  34. elseif (preg_match('/^[A-Z_\x7f-\xff][A-Z0-9_\x7f-\xff]*$/', $matches[3]))
  35. {
  36. $member = '#constant:'.substr($matches[3],0);
  37. }
  38. else
  39. {
  40. $member = '#'.$matches[3];
  41. }
  42. }
  43. return HTML::anchor(Route::get('docs/api')->uri(['class' => $class]).$member, $link, NULL, NULL, TRUE);
  44. }
  45. public static function factory($class)
  46. {
  47. return new Kodoc_Class($class);
  48. }
  49. /**
  50. * Creates an html list of all classes sorted by category (or package if no category)
  51. *
  52. * @return string the html for the menu
  53. */
  54. public static function menu()
  55. {
  56. $classes = Kodoc::classes();
  57. ksort($classes);
  58. $menu = [];
  59. $route = Route::get('docs/api');
  60. foreach ($classes as $class)
  61. {
  62. if (Kodoc::is_transparent($class, $classes))
  63. continue;
  64. $class = Kodoc_Class::factory($class);
  65. // Test if we should show this class
  66. if ( ! Kodoc::show_class($class))
  67. continue;
  68. $link = HTML::anchor($route->uri(['class' => $class->class->name]), $class->class->name);
  69. if (isset($class->tags['package']))
  70. {
  71. foreach ($class->tags['package'] as $package)
  72. {
  73. if (isset($class->tags['category']))
  74. {
  75. foreach ($class->tags['category'] as $category)
  76. {
  77. $menu[$package][$category][] = $link;
  78. }
  79. }
  80. else
  81. {
  82. $menu[$package]['Base'][] = $link;
  83. }
  84. }
  85. }
  86. else
  87. {
  88. $menu['[Unknown]']['Base'][] = $link;
  89. }
  90. }
  91. // Sort the packages
  92. ksort($menu);
  93. return View::factory('userguide/api/menu')
  94. ->bind('menu', $menu);
  95. }
  96. /**
  97. * Returns an array of all the classes available, built by listing all files in the classes folder.
  98. *
  99. * @param array array of files, obtained using Kohana::list_files
  100. * @return array an array of all the class names
  101. */
  102. public static function classes(array $list = NULL)
  103. {
  104. if ($list === NULL)
  105. {
  106. $list = Kohana::list_files('classes');
  107. }
  108. $classes = [];
  109. // This will be used a lot!
  110. $ext_length = strlen(EXT);
  111. foreach ($list as $name => $path)
  112. {
  113. if (is_array($path))
  114. {
  115. $classes += Kodoc::classes($path);
  116. }
  117. elseif (substr($name, -$ext_length) === EXT)
  118. {
  119. // Remove "classes/" and the extension
  120. $class = substr($name, 8, -$ext_length);
  121. // Convert slashes to underscores
  122. $class = str_replace(DIRECTORY_SEPARATOR, '_', $class);
  123. $classes[$class] = $class;
  124. }
  125. }
  126. return $classes;
  127. }
  128. /**
  129. * Get all classes and methods of files in a list.
  130. *
  131. * > I personally don't like this as it was used on the index page. Way too much stuff on one page. It has potential for a package index page though.
  132. * > For example: class_methods( Kohana::list_files('classes/sprig') ) could make a nice index page for the sprig package in the api browser
  133. * > ~bluehawk
  134. *
  135. */
  136. public static function class_methods(array $list = NULL)
  137. {
  138. $list = Kodoc::classes($list);
  139. $classes = [];
  140. foreach ($list as $class)
  141. {
  142. // Skip transparent extension classes
  143. if (Kodoc::is_transparent($class))
  144. continue;
  145. $_class = new ReflectionClass($class);
  146. $methods = [];
  147. foreach ($_class->getMethods() as $_method)
  148. {
  149. $declares = $_method->getDeclaringClass()->name;
  150. // Remove the transparent prefix from declaring classes
  151. if ($child = Kodoc::is_transparent($declares))
  152. {
  153. $declares = $child;
  154. }
  155. if ($declares === $_class->name OR $declares === "Core")
  156. {
  157. $methods[] = $_method->name;
  158. }
  159. }
  160. sort($methods);
  161. $classes[$_class->name] = $methods;
  162. }
  163. return $classes;
  164. }
  165. /**
  166. * Generate HTML for the content of a tag.
  167. *
  168. * @param string $tag Name of the tag without @
  169. * @param string $text Content of the tag
  170. * @return string HTML
  171. */
  172. public static function format_tag($tag, $text)
  173. {
  174. if ($tag === 'license')
  175. {
  176. if (strpos($text, '://') !== FALSE)
  177. return HTML::anchor($text);
  178. }
  179. elseif ($tag === 'link')
  180. {
  181. $split = preg_split('/\s+/', $text, 2);
  182. return HTML::anchor(
  183. $split[0],
  184. isset($split[1]) ? $split[1] : $split[0]
  185. );
  186. }
  187. elseif ($tag === 'copyright')
  188. {
  189. // Convert the copyright symbol
  190. return str_replace('(c)', '&copy;', $text);
  191. }
  192. elseif ($tag === 'throws')
  193. {
  194. $route = Route::get('docs/api');
  195. if (preg_match('/^(\w+)\W(.*)$/D', $text, $matches))
  196. {
  197. return HTML::anchor(
  198. $route->uri(['class' => $matches[1]]),
  199. $matches[1]
  200. ).' '.$matches[2];
  201. }
  202. return HTML::anchor(
  203. $route->uri(['class' => $text]),
  204. $text
  205. );
  206. }
  207. elseif ($tag === 'see' OR $tag === 'uses')
  208. {
  209. if (preg_match('/^'.Kodoc::$regex_class_member.'/', $text, $matches))
  210. return Kodoc::link_class_member($matches);
  211. }
  212. return $text;
  213. }
  214. /**
  215. * Parse a comment to extract the description and the tags
  216. *
  217. * [!!] Converting the output to HTML in this method is deprecated in 3.3
  218. *
  219. * @param string $comment The DocBlock to parse
  220. * @param boolean $html Whether or not to convert the return values
  221. * to HTML (deprecated)
  222. * @return array array(string $description, array $tags)
  223. */
  224. public static function parse($comment, $html = TRUE)
  225. {
  226. // Normalize all new lines to \n
  227. $comment = str_replace(["\r\n", "\n"], "\n", $comment);
  228. // Split into lines while capturing without leading whitespace
  229. preg_match_all('/^\s*\* ?(.*)\n/m', $comment, $lines);
  230. // Tag content
  231. $tags = [];
  232. /**
  233. * Process a tag and add it to $tags
  234. *
  235. * @param string $tag Name of the tag without @
  236. * @param string $text Content of the tag
  237. * @return void
  238. */
  239. $add_tag = function ($tag, $text) use ($html, & $tags)
  240. {
  241. // Don't show @access lines, they are shown elsewhere
  242. if ($tag !== 'access')
  243. {
  244. if ($html)
  245. {
  246. $text = Kodoc::format_tag($tag, $text);
  247. }
  248. // Add the tag
  249. $tags[$tag][] = $text;
  250. }
  251. };
  252. $comment = $tag = NULL;
  253. $end = count($lines[1]) - 1;
  254. foreach ($lines[1] as $i => $line)
  255. {
  256. // Search this line for a tag
  257. if (preg_match('/^@(\S+)\s*(.+)?$/', $line, $matches))
  258. {
  259. if ($tag)
  260. {
  261. // Previous tag is finished
  262. $add_tag($tag, $text);
  263. }
  264. $tag = $matches[1];
  265. $text = isset($matches[2]) ? $matches[2] : '';
  266. if ($i === $end)
  267. {
  268. // No more lines
  269. $add_tag($tag, $text);
  270. }
  271. }
  272. elseif ($tag)
  273. {
  274. // This is the continuation of the previous tag
  275. $text .= "\n".$line;
  276. if ($i === $end)
  277. {
  278. // No more lines
  279. $add_tag($tag, $text);
  280. }
  281. }
  282. else
  283. {
  284. $comment .= "\n".$line;
  285. }
  286. }
  287. $comment = trim($comment, "\n");
  288. if ($comment AND $html)
  289. {
  290. // Parse the comment with Markdown
  291. $comment = Kodoc_Markdown::markdown($comment);
  292. }
  293. return [$comment, $tags];
  294. }
  295. /**
  296. * Get the source of a function
  297. *
  298. * @param string the filename
  299. * @param int start line?
  300. * @param int end line?
  301. */
  302. public static function source($file, $start, $end)
  303. {
  304. if ( ! $file) return FALSE;
  305. $file = file($file, FILE_IGNORE_NEW_LINES);
  306. $file = array_slice($file, $start - 1, $end - $start + 1);
  307. if (preg_match('/^(\s+)/', $file[0], $matches))
  308. {
  309. $padding = strlen($matches[1]);
  310. foreach ($file as & $line)
  311. {
  312. $line = substr($line, $padding);
  313. }
  314. }
  315. return implode("\n", $file);
  316. }
  317. /**
  318. * Test whether a class should be shown, based on the api_packages config option
  319. *
  320. * @param Kodoc_Class the class to test
  321. * @return bool whether this class should be shown
  322. */
  323. public static function show_class(Kodoc_Class $class)
  324. {
  325. $api_packages = Kohana::$config->load('userguide.api_packages');
  326. // If api_packages is true, all packages should be shown
  327. if ($api_packages === TRUE)
  328. return TRUE;
  329. // Get the package tags for this class (as an array)
  330. $packages = Arr::get($class->tags, 'package', ['None']);
  331. $show_this = FALSE;
  332. // Loop through each package tag
  333. foreach ($packages as $package)
  334. {
  335. // If this package is in the allowed packages, set show this to true
  336. if (in_array($package, explode(',', $api_packages)))
  337. $show_this = TRUE;
  338. }
  339. return $show_this;
  340. }
  341. /**
  342. * Checks whether a class is a transparent extension class or not.
  343. *
  344. * This method takes an optional $classes parameter, a list of all defined
  345. * class names. If provided, the method will return false unless the extension
  346. * class exists. If not, the method will only check known transparent class
  347. * prefixes.
  348. *
  349. * Transparent prefixes are defined in the userguide.php config file:
  350. *
  351. * 'transparent_prefixes' => array(
  352. * 'Kohana' => TRUE,
  353. * );
  354. *
  355. * Module developers can therefore add their own transparent extension
  356. * namespaces and exclude them from the userguide.
  357. *
  358. * @param string $class The name of the class to check for transparency
  359. * @param array $classes An optional list of all defined classes
  360. * @return false If this is not a transparent extension class
  361. * @return string The name of the class that extends this (in the case provided)
  362. * @throws InvalidArgumentException If the $classes array is provided and the $class variable is not lowercase
  363. */
  364. public static function is_transparent($class, $classes = NULL)
  365. {
  366. static $transparent_prefixes = NULL;
  367. if ( ! $transparent_prefixes)
  368. {
  369. $transparent_prefixes = Kohana::$config->load('userguide.transparent_prefixes');
  370. }
  371. // Split the class name at the first underscore
  372. $segments = explode('_',$class,2);
  373. if ((count($segments) == 2) AND (isset($transparent_prefixes[$segments[0]])))
  374. {
  375. if ($segments[1] === 'Core')
  376. {
  377. // Cater for Module extends Module_Core naming
  378. $child_class = $segments[0];
  379. }
  380. else
  381. {
  382. // Cater for Foo extends Module_Foo naming
  383. $child_class = $segments[1];
  384. }
  385. // It is only a transparent class if the unprefixed class also exists
  386. if ($classes AND ! isset($classes[$child_class]))
  387. return FALSE;
  388. // Return the name of the child class
  389. return $child_class;
  390. }
  391. else
  392. {
  393. // Not a transparent class
  394. return FALSE;
  395. }
  396. }
  397. } // End Kodoc