link text, 2 => class name, [3 => member name] ) * @return string */ public static function link_class_member($matches) { $link = $matches[1]; $class = $matches[2]; $member = NULL; if (isset($matches[3])) { // If the first char is a $ it is a property, e.g. Kohana::$base_url if ($matches[3][0] === '$') { $member = '#property:'.substr($matches[3], 1); } else { $member = '#'.$matches[3]; } } return HTML::anchor(Route::get('docs/api')->uri(array('class' => $class)).$member, $link, NULL, NULL, TRUE); } public static function factory($class) { return new Kodoc_Class($class); } /** * Creates an html list of all classes sorted by category (or package if no category) * * @return string the html for the menu */ public static function menu() { $classes = Kodoc::classes(); ksort($classes); $menu = array(); $route = Route::get('docs/api'); foreach ($classes as $class) { if (Kodoc::is_transparent($class, $classes)) continue; $class = Kodoc_Class::factory($class); // Test if we should show this class if ( ! Kodoc::show_class($class)) continue; $link = HTML::anchor($route->uri(array('class' => $class->class->name)), $class->class->name); if (isset($class->tags['package'])) { foreach ($class->tags['package'] as $package) { if (isset($class->tags['category'])) { foreach ($class->tags['category'] as $category) { $menu[$package][$category][] = $link; } } else { $menu[$package]['Base'][] = $link; } } } else { $menu['[Unknown]']['Base'][] = $link; } } // Sort the packages ksort($menu); return View::factory('userguide/api/menu') ->bind('menu', $menu); } /** * Returns an array of all the classes available, built by listing all files in the classes folder. * * @param array array of files, obtained using Kohana::list_files * @return array an array of all the class names */ public static function classes(array $list = NULL) { if ($list === NULL) { $list = Kohana::list_files('classes'); } $classes = array(); // This will be used a lot! $ext_length = strlen(EXT); foreach ($list as $name => $path) { if (is_array($path)) { $classes += Kodoc::classes($path); } elseif (substr($name, -$ext_length) === EXT) { // Remove "classes/" and the extension $class = substr($name, 8, -$ext_length); // Convert slashes to underscores $class = str_replace(DIRECTORY_SEPARATOR, '_', $class); $classes[$class] = $class; } } return $classes; } /** * Get all classes and methods of files in a list. * * > 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. * > For example: class_methods( Kohana::list_files('classes/sprig') ) could make a nice index page for the sprig package in the api browser * > ~bluehawk * */ public static function class_methods(array $list = NULL) { $list = Kodoc::classes($list); $classes = array(); foreach ($list as $class) { // Skip transparent extension classes if (Kodoc::is_transparent($class)) continue; $_class = new ReflectionClass($class); $methods = array(); foreach ($_class->getMethods() as $_method) { $declares = $_method->getDeclaringClass()->name; // Remove the transparent prefix from declaring classes if ($child = Kodoc::is_transparent($declares)) { $declares = $child; } if ($declares === $_class->name OR $declares === "Core") { $methods[] = $_method->name; } } sort($methods); $classes[$_class->name] = $methods; } return $classes; } /** * Generate HTML for the content of a tag. * * @param string $tag Name of the tag without @ * @param string $text Content of the tag * @return string HTML */ public static function format_tag($tag, $text) { if ($tag === 'license') { if (strpos($text, '://') !== FALSE) return HTML::anchor($text); } elseif ($tag === 'link') { $split = preg_split('/\s+/', $text, 2); return HTML::anchor( $split[0], isset($split[1]) ? $split[1] : $split[0] ); } elseif ($tag === 'copyright') { // Convert the copyright symbol return str_replace('(c)', '©', $text); } elseif ($tag === 'throws') { $route = Route::get('docs/api'); if (preg_match('/^(\w+)\W(.*)$/D', $text, $matches)) { return HTML::anchor( $route->uri(array('class' => $matches[1])), $matches[1] ).' '.$matches[2]; } return HTML::anchor( $route->uri(array('class' => $text)), $text ); } elseif ($tag === 'see' OR $tag === 'uses') { if (preg_match('/^'.Kodoc::$regex_class_member.'/', $text, $matches)) return Kodoc::link_class_member($matches); } return $text; } /** * Parse a comment to extract the description and the tags * * [!!] Converting the output to HTML in this method is deprecated in 3.3 * * @param string $comment The DocBlock to parse * @param boolean $html Whether or not to convert the return values * to HTML (deprecated) * @return array array(string $description, array $tags) */ public static function parse($comment, $html = TRUE) { // Normalize all new lines to \n $comment = str_replace(array("\r\n", "\n"), "\n", $comment); // Split into lines while capturing without leading whitespace preg_match_all('/^\s*\* ?(.*)\n/m', $comment, $lines); // Tag content $tags = array(); /** * Process a tag and add it to $tags * * @param string $tag Name of the tag without @ * @param string $text Content of the tag * @return void */ $add_tag = function($tag, $text) use ($html, &$tags) { // Don't show @access lines, they are shown elsewhere if ($tag !== 'access') { if ($html) { $text = Kodoc::format_tag($tag, $text); } // Add the tag $tags[$tag][] = $text; } }; $comment = $tag = null; $end = count($lines[1]) - 1; foreach ($lines[1] as $i => $line) { // Search this line for a tag if (preg_match('/^@(\S+)\s*(.+)?$/', $line, $matches)) { if ($tag) { // Previous tag is finished $add_tag($tag, $text); } $tag = $matches[1]; $text = isset($matches[2]) ? $matches[2] : ''; if ($i === $end) { // No more lines $add_tag($tag, $text); } } elseif ($tag) { // This is the continuation of the previous tag $text .= "\n".$line; if ($i === $end) { // No more lines $add_tag($tag, $text); } } else { $comment .= "\n".$line; } } $comment = trim($comment, "\n"); if ($comment AND $html) { // Parse the comment with Markdown $comment = Kodoc_Markdown::markdown($comment); } return array($comment, $tags); } /** * Get the source of a function * * @param string the filename * @param int start line? * @param int end line? */ public static function source($file, $start, $end) { if ( ! $file) return FALSE; $file = file($file, FILE_IGNORE_NEW_LINES); $file = array_slice($file, $start - 1, $end - $start + 1); if (preg_match('/^(\s+)/', $file[0], $matches)) { $padding = strlen($matches[1]); foreach ($file as & $line) { $line = substr($line, $padding); } } return implode("\n", $file); } /** * Test whether a class should be shown, based on the api_packages config option * * @param Kodoc_Class the class to test * @return bool whether this class should be shown */ public static function show_class(Kodoc_Class $class) { $api_packages = Kohana::$config->load('userguide.api_packages'); // If api_packages is true, all packages should be shown if ($api_packages === TRUE) return TRUE; // Get the package tags for this class (as an array) $packages = Arr::get($class->tags, 'package', array('None')); $show_this = FALSE; // Loop through each package tag foreach ($packages as $package) { // If this package is in the allowed packages, set show this to true if (in_array($package, explode(',', $api_packages))) $show_this = TRUE; } return $show_this; } /** * Checks whether a class is a transparent extension class or not. * * This method takes an optional $classes parameter, a list of all defined * class names. If provided, the method will return false unless the extension * class exists. If not, the method will only check known transparent class * prefixes. * * Transparent prefixes are defined in the userguide.php config file: * * 'transparent_prefixes' => array( * 'Kohana' => TRUE, * ); * * Module developers can therefore add their own transparent extension * namespaces and exclude them from the userguide. * * @param string $class The name of the class to check for transparency * @param array $classes An optional list of all defined classes * @return false If this is not a transparent extension class * @return string The name of the class that extends this (in the case provided) * @throws InvalidArgumentException If the $classes array is provided and the $class variable is not lowercase */ public static function is_transparent($class, $classes = NULL) { static $transparent_prefixes = NULL; if ( ! $transparent_prefixes) { $transparent_prefixes = Kohana::$config->load('userguide.transparent_prefixes'); } // Split the class name at the first underscore $segments = explode('_',$class,2); if ((count($segments) == 2) AND (isset($transparent_prefixes[$segments[0]]))) { if ($segments[1] === 'Core') { // Cater for Module extends Module_Core naming $child_class = $segments[0]; } else { // Cater for Foo extends Module_Foo naming $child_class = $segments[1]; } // It is only a transparent class if the unprefixed class also exists if ($classes AND ! isset($classes[$child_class])) return FALSE; // Return the name of the child class return $child_class; } else { // Not a transparent class return FALSE; } } } // End Kodoc