#!/usr/bin/php This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ define('VERSION', '1.4'); if (!defined('T_COMMENT') || !extension_loaded('tokenizer') || version_compare(phpversion(), '4.3.0', '<')) { error('You need PHP version 4.3.0 or greater with the "tokenizer" extension to run this script, please upgrade'); exit; } if (!isset($argv[0])) { // not being run command line error('This program must be run from the command line using the CLI version of PHP'); exit; } elseif (!isset($argv[1])) { // use default config file if (is_file('default.ini')) { warning('Using default config file "default.ini"'); $argv[1] = 'default.ini'; } else { error('Usage: phpdoc.php [config_file]'); exit; } } /** Path PHPDoctor is running from */ define('PATH', dirname($argv[0])); /** Default template path */ define('DEFAULT_TEMPLATE', 'templates/default'); // read config file if (is_file($argv[1])) { $settings = @parse_ini_file($argv[1]); if (count($settings) == 0) { error('Could not parse configuration file "'.$argv[1].'"'); exit; } } else { error('Could not find configuration file "'.$argv[1].'"'); exit; } // record start time $startTime = getTime(); // set config directives if (isset($settings['files'])) $in_file = $settings['files']; if (isset($settings['file_path'])) { define('FILE_PATH', prefixFilename('', $settings['file_path'])); } else { define('FILE_PATH', prefixFilename('', dirname($argv[1]))); } if (isset($settings['output_dir'])) { define('OUT_DIR', prefixFilename(pathAppendSlash($settings['output_dir']), FILE_PATH)); } else { define('OUT_DIR', prefixFilename('apidocs/', FILE_PATH)); } if (isset($settings['title'])) define('LONG_TITLE', $settings['title']); else define('LONG_TITLE', 'Noname'); if (isset($settings['short_title'])) define('SHORT_TITLE', $settings['short_title']); else define('SHORT_TITLE', LONG_TITLE); if (isset($settings['subdirs'])) define('SUBDIRS', $settings['subdirs']); else define('SUBDIRS', FALSE); if (isset($settings['file_list'])) define('FILE_LIST', $settings['file_list']); if (isset($settings['template'])) define('TEMPLATE', 'templates/'.$settings['template']); else define('TEMPLATE', DEFAULT_TEMPLATE); if (isset($settings['globals'])) define('NO_GLOBAL', !$settings['globals']); else define('NO_GLOBAL', FALSE); if (isset($settings['constants'])) define('NO_CONSTANT', !$settings['constants']); else define('NO_CONSTANT', FALSE); if (isset($settings['tree'])) define('NO_TREE', !$settings['tree']); else define('NO_TREE', FALSE); if (isset($settings['default_package'])) define('DEFAULT_PACKAGE', $settings['default_package']); else define('DEFAULT_PACKAGE', strtolower(SHORT_TITLE)); if (isset($settings['warnings'])) define('DISPLAY_WARNINGS', $settings['warnings']); else define('DISPLAY_WARNINGS', TRUE); if (isset($settings['footer'])) define('FOOTER', $settings['footer']); else define('FOOTER', 'This document was generated by PHPDoctor: The PHP Documentation Creator v'.VERSION.'.'); if (isset($settings['overview_comment'])) define('OVERVIEW_COMMENT', prefixFilename($settings['overview_comment'], FILE_PATH)); else define('OVERVIEW_COMMENT', prefixFilename('overview.html', FILE_PATH)); if (isset($settings['package_comment_dir'])) define('PACKAGE_COMMENT_DIR', prefixFilename(pathAppendSlash($settings['package_comment_dir']), FILE_PATH)); else define('PACKAGE_COMMENT_DIR', FILE_PATH); if (isset($settings['hide_private'])) define('HIDE_PRIVATE', $settings['hide_private']); else define('HIDE_PRIVATE', FALSE); // define tokienizer constants if not defined so we don't throw undefined constant errors later on if (!defined('T_DOC_COMMENT')) define('T_DOC_COMMENT', 0); if (!defined('T_ML_COMMENT')) define('T_ML_COMMENT', 0); if (!defined('T_PRIVATE')) define('T_PRIVATE', 0); if (!defined('T_PROTECTED')) define('T_PROTECTED', 0); if (!defined('T_PUBLIC')) define('T_PUBLIC', 0); if (!defined('T_ABSTRACT')) define('T_ABSTRACT', 0); if (!defined('T_FINAL')) define('T_FINAL', 0); if (!defined('T_INTERFACE')) define('T_INTERFACE', 0); if (!defined('T_IMPLEMENTS')) define('T_IMPLEMENTS', 0); if (!defined('T_CONST')) define('T_CONST', 0); if (defined('FILE_LIST')) { $filename = FILE_LIST; echo 'Reading file list ', $filename, "...\n"; $filename = prefixFilename($filename, FILE_PATH); if (!is_file($filename)) { error('Could not open file '.$filename); exit; } $files = file($filename); $fileList = ''; foreach ($files as $file) { $fileList .= rtrim($file).','; } $fileList = substr($fileList, 0, -1); if (isset($in_file)) { $in_file .= ','.$fileList; } else { $in_file = $fileList; } } if (!isset($in_file) || $in_file == '') { error('No files specified to parse in config file, halting'); exit; } $objects = array(); $packages = array(); $summary = array(); $classTree = array(); $interfaceTree = array(); $pos = strpos($in_file, ','); if ($pos !== FALSE) { $inFiles = explode(',', $in_file); } else { $inFiles = array($in_file); } echo 'Output directory set as ', OUT_DIR, "\n"; if (is_dir(OUT_DIR)) { warning('Output directory already exists, overwriting existing files.'); } echo 'Documentation title set as ', LONG_TITLE, "\n"; echo 'Documentation short title set as ', SHORT_TITLE, "\n"; if (NO_GLOBAL) echo "Globals will not be parsed.\n"; if (NO_CONSTANT) echo "Constants will not be parsed.\n"; readFiles($inFiles, FILE_PATH); replaceInlineLinks($objects); setSeeLinks($objects); //dumpToFile($objects); foreach ($objects as $package_name => $package) { $package_summary = array(); $package_classTree = array(); $package_interfaceTree = array(); //set package dir $objects[$package_name]['dir'] = packageToDir($package_name); $package['package'] = $package_name; echo 'Writing package ', $package_name, "...\n"; if (isset($package['params'])) { echo ' ', count($package['params']), " globals parsed.\n"; } if (isset($package['constants'])) { echo ' ', count($package['constants']), " constants parsed.\n"; } if (isset($package['classes'])) { echo ' ', count($package['classes']), " classes parsed.\n"; } if (isset($package['interfaces'])) { echo ' ', count($package['interfaces']), " interfaces parsed.\n"; } if (isset($package['functions'])) { echo ' ', count($package['functions']), " functions parsed.\n"; } if (isset($package['classes'])) { // we have classes, output them echo " Writing classes...\n"; ksort($package['classes']); foreach ($package['classes'] as $class) { if (isset($class['shortDescription'])) { $description = $class['shortDescription']; } else { $description = 'No description'; } addToSummary($package_summary, 'classes', $class['name'], $description, $package_name); if (isset($class['extends'])) { addToTree($package_classTree, $class['name'], $package_name, $class['extends']); } else { addToTree($package_classTree, $class['name'], $package_name); } echo ' Writing ', $class['name'], "\n"; $classDetails = array('this' => &$class); if (isset($class['extends'])) { getParentsDetails('classes', $class['extends'], $classDetails, $objects); } if (isset($class['implements'])) { foreach ($class['implements'] as $interface) { getParentsDetails('interfaces', $interface, $classDetails, $objects); } } $contents = plugTemplate($classDetails, 'class', $package_name); writeFile($contents, $class['name'].'.html', $package_name); } } if (isset($package['interfaces'])) { // we have interfaces, output them echo " Writing interfaces...\n"; ksort($package['interfaces']); foreach ($package['interfaces'] as $interface) { if (isset($interface['shortDescription'])) { $description = $interface['shortDescription']; } else { $description = 'No description'; } addToSummary($package_summary, 'interfaces', $interface['name'], $description, $package_name); if (isset($interface['extends'])) { addToTree($package_interfaceTree, $interface['name'], $package_name, $interface['extends']); } else { addToTree($package_interfaceTree, $interface['name'], $package_name); } echo ' Writing ', $interface['name'], "\n"; $interfaceDetails = array('this' => &$interface); if (isset($interface['extends'])) { getParentsDetails('interfaces', $interface['extends'], $interfaceDetails, $objects); } if (isset($interface['implements'])) { foreach ($interface['implements'] as $inter) { getParentsDetails('interfaces', $inter, $interfaceDetails, $objects); } } $contents = plugTemplate($interfaceDetails, 'interface', $package_name); writeFile($contents, $interface['name'].'.html', $package_name); } } if (isset($package['functions'])) { // we have functions, output them echo " Writing functions...\n"; ksort($package['functions']); foreach ($package['functions'] as $function) { if (isset($function['shortDescription'])) { $description = $function['shortDescription']; } else { $description = 'No description'; } addToSummary($package_summary, 'functions', $function['name'], $description, $package_name); } $contents = plugTemplate($package['functions'], 'package-functions', $package_name); writeFile($contents, 'package-functions.html', $package_name); } if (isset($package['params'])) { // we have globals, output them echo " Writing globals...\n"; ksort($package['params']); foreach ($package['params'] as $key => $global) { foreach ($global as $key2 => $item) { // since we can have multiple occurances of the same global, we'll pick it up more than once, so we have to consolidate these occurances into a single item if (is_array($item) && isset($item[0])) { $global[$key2] = $item[0]; $package['params'][$key][$key2] = $item[0]; } } if (isset($global['shortDescription'])) { $description = $global['shortDescription']; } else { $description = 'No description'; } addToSummary($package_summary, 'globals', $global['name'], $description, $package_name); } $contents = plugTemplate($package['params'], 'package-globals', $package_name); writeFile($contents, 'package-globals.html', $package_name); } if (isset($package['constants'])) { // we have globals, output them echo " Writing constants...\n"; ksort($package['constants']); foreach ($package['constants'] as $constant) { if (isset($constant['shortDescription'])) { $description = $constant['shortDescription']; } else { $description = 'No description'; } addToSummary($package_summary, 'constants', $constant['name'], $description, $package_name); } $contents = plugTemplate($package['constants'], 'package-constants', $package_name); writeFile($contents, 'package-constants.html', $package_name); } echo " Writing package summary...\n"; echo " DEBUG: package_name: " . PACKAGE_COMMENT_DIR.$package_name.'.html' . "\n"; $comment_file = PACKAGE_COMMENT_DIR.$package_name.'.html'; if (is_file($comment_file)) { echo ' Reading package comment file ', $comment_file, "\n"; $comment = getHTMLContents($comment_file); } else { $comment = FALSE; } $contents = plugTemplate($package, 'package-summary', $package_name, $comment); writeFile($contents, 'package-summary.html', $package_name); echo " Writing package index...\n"; $contents = plugTemplate($package_summary, 'package-frame', $package_name); writeFile($contents, 'package-frame.html', $package_name); if (!NO_TREE) { echo " Writing package class tree...\n"; $tree = array('classes' => &$package_classTree, 'interfaces' => &$package_interfaceTree); $contents = plugTemplate($tree, 'package-tree', $package_name); writeFile($contents, 'package-tree.html', $package_name); } $summary = mergeArrays($summary, $package_summary); $classTree = mergeArrays($classTree, $package_classTree); $interfaceTree = mergeArrays($interfaceTree, $package_interfaceTree); } // write indexes and stuff echo "Writing package list...\n"; $contents = plugTemplate($objects, 'overview-frame', FALSE); writeFile($contents, 'overview-frame.html'); echo "Writing overview summary...\n"; if (is_file(OVERVIEW_COMMENT)) { echo ' Reading overview file ', OVERVIEW_COMMENT, "\n"; $comment = getHTMLContents(OVERVIEW_COMMENT); } else { $comment = FALSE; } $contents = plugTemplate($objects, 'overview-summary', FALSE, $comment); writeFile($contents, 'overview-summary.html'); echo "Writing overview index...\n"; $contents = plugTemplate($summary, 'package-frame', FALSE); writeFile($contents, 'allitems-frame.html'); if (NO_TREE) { echo "Not generating class tree\n"; } else { echo "Writing class tree...\n"; $tree = array('classes' => &$classTree, 'interfaces' => &$interfaceTree); $contents = plugTemplate($tree, 'overview-tree', FALSE); writeFile($contents, 'overview-tree.html'); } echo "Writing frameset...\n"; $contents = plugTemplate($summary, 'index', FALSE); writeFile($contents, 'index.html'); echo "Copying stylesheet...\n"; copy(PATH.'/'.getTemplateFile('stylesheet.css'), OUT_DIR.'stylesheet.css'); echo "Done\n"; echo 'Documentation generated in ', round(getTime() - $startTime, 2), " seconds\n"; /** * Read input files and parse them into tokens. This function is * recursively called by itself if PHPDoctor is searching sub * directories. * * @param array inFiles Array of input files to parse */ function readFiles($inFiles, $dir) { global $objects, $tokens; foreach ($inFiles as $filename) { $filename = prefixFilename($filename, $dir); if (!is_file($filename)) { $globResults = glob(str_replace('\\', '/', $filename)); // switch slashes since old versions of glob need forward slashes if ($globResults) { foreach ($globResults as $filename) { if (!is_dir($filename)) { echo 'Reading file ', $filename, "\n"; $fileString = @file_get_contents($filename); if ($fileString) { $tokens = token_get_all($fileString); $objects = mergeArrays($objects, parseTokens($tokens)); } else { error('Could not read file '.$filename); } } } } else { if (!SUBDIRS) { error('Could not find file '.$filename); } } } else { echo 'Reading file ', $filename, "\n"; $fileString = @file_get_contents($filename); if ($fileString) { $tokens = token_get_all($fileString); $objects = mergeArrays($objects, parseTokens($tokens)); } else { error('Could not read file '.$filename); } } } if (SUBDIRS) { // recurse into subdir $globResults = glob($dir.'*', GLOB_ONLYDIR); // get subdirs if ($globResults) { foreach ($globResults as $filename) { echo 'Reading directory ', $filename, "...\n"; readFiles($inFiles, $filename); } } } } /** * Write doc tags to output. Doc tag results are encased in HTML definition list tags. * * @param array object Object to write tags for */ function writeTags(&$object) { if (isset($object['params'])) { echo '
Parameters:
'; foreach ($object['params'] as $param) { if (isset($param['name'])) { echo '
', $param['name'], ''; if (isset($param['description'])) { echo ' - ', $param['description'], '
'; } } } echo '
'; } if (isset($object['author'])) { echo '
Author:
'; echo join(', ', $object['author']); echo '
'; } if (isset($object['deprecated'])) { echo '
Deprecated:
'; echo join(', ', $object['deprecated']); echo '
'; } if (isset($object['return']['description'])) { echo '
Returns:
', $object['return']['description'], '
'; } if (isset($object['see'])) { echo '
See Also:
'; echo join(', ', $object['see']); echo '
'; } if (isset($object['since'])) { echo '
Since:
'; echo join(', ', $object['since']); echo '
'; } if (isset($object['version'])) { echo '
Version:
'; echo join(', ', $object['version']); echo '
'; } } /** * Get the details of the classes parent from the objects array. This function * recursively calls itself to get parents details. * * @param str type Type of object we are dealing with * @param str parent Name of parent class * @param array classDetails * @param array objects */ function getParentsDetails($type, $parent, &$classDetails, &$objects) { if (!isset($classDetails[$parent])) { foreach ($objects as $package) { // search through packages for parent class if (isset($package[$type][$parent])) { $classDetails[$parent] = $package[$type][$parent]; } } if (!isset($classDetails[$parent])) { // parent has not been parsed, add dummy entry // not implemented, not sure whether we'll really need this yet } if (isset($classDetails[$parent]['extends'])) { getParentsDetails($type, $classDetails[$parent]['extends'], $classDetails, $objects); } if (isset($classDetails[$parent]['implements'])) { foreach ($classDetails[$parent]['implements'] as $interface) { getParentsDetails('interfaces', $interface, $classDetails, $objects); } } } } /** * Plug values into a template file. * * @param array t Template items * @param str template Name of template to use * @return str The template with the values plugged in */ function &plugTemplate(&$t, $template, $package_name = NULL, $comment = FALSE) { if (isset($package_name) && $package_name) { // get package name and calculate directory depth this template will be placed at $package_dir = packageToDir($package_name).'/'; $link_path = '../'; $depth = substr_count($package_dir, '/'); for ($foo = 1; $foo < $depth; $foo++) { $link_path .= '../'; } } elseif (isset($package_name)) { // overview template $package_dir = ''; $link_path = ''; } else { // default package $package_dir = ''; $link_path = ''; $package_name = DEFAULT_PACKAGE; } ob_start(); include(getTemplateFile($template.'.php')); $contents = ob_get_contents(); ob_end_clean(); return $contents; } /** * Get the full path and name of a template file. * * @param str filename Name of template file to get * @return str The full path and name of the template file */ function getTemplateFile($filename) { if (file_exists(TEMPLATE.'/'.$filename)) { // first search in defined templates directory return TEMPLATE.'/'.$filename; } else { // use default template return DEFAULT_TEMPLATE.'/'.$filename; } } /** * Write a warning message to standard output. * * @param str msg Warning message to output */ function warning($msg) { if (!defined('DISPLAY_WARNINGS') || DISPLAY_WARNINGS) fwrite(STDERR, 'WARNING: '.$msg."\n"); } /** * Write an error message to standard output. * * @param str msg Error message to output */ function error($msg) { fwrite(STDERR, 'ERROR: '.$msg."\n"); } /** * Write a file given contents and a filename * * @param str contents Content to write * @param str filename File to write to * @param str package Package to write file into */ function writeFile($contents, $filename, $package = NULL) { if ($package) { $dir = OUT_DIR.packageToDir($package); } else { $dir = OUT_DIR; } if (!is_dir($dir)) { makeDir($dir); } $fp = fopen($dir.'/'.$filename, 'w'); if ($fp) { fwrite($fp, $contents); } } /** Turn a package name into the package directory name */ function packageToDir($package) { $package = str_replace('\\', '/', $package); $package = str_replace('.', '/', $package); return $package; } /** Make a directory and all parents as needed */ function makeDir($dir) { $dirs = explode('/', $dir); $dir_to_make = FALSE; foreach($dirs as $dir) { if ($dir_to_make) { $dir_to_make .= '/'.$dir; } else { $dir_to_make = $dir; } if (!is_dir($dir_to_make)) mkdir($dir_to_make); } } /** * Add a classes details to the tree generator array. * * @param array tree Tree generator array * @param str name Name of class to add * @param str extends Name of parent of class to add */ function addToTree(&$tree, $name, $package, $extends = NULL) { if (is_array($name)) $name = $name[0]; $array['name'] = $name; $array['package'] = $package; $array['extends'] = $extends; $array['dir'] = packageToDir($package); $tree[$name] = $array; } /** * Add an objects details to the summary generator array. * * @param array summary Summary generator array * @param str type Type of object to add * @param str name Name of object to add * @param str description Description of object to add */ function addToSummary(&$summary, $type, $name, $description, $package) { if (is_array($name)) $name = $name[0]; $array['name'] = $name; $array['shortDescription'] = $description; $array['package'] = $package; $array['dir'] = packageToDir($package); $summary[$type][$name] = $array; } /** * Turn a link doc tag into a URI. * * @param str link Link to process * @return str The text with the links processed */ function parseLink($link, $packageName = DEFAULT_PACKAGE) { global $objects; $objectName = FALSE; $itemName = FALSE; $root = substr_count($packageName, '.'); $root = str_repeat('../', $root + 1); $link = preg_replace('/\(.*\)/U', '', $link); // remove parameters $pos = strpos($link, ' '); if ($pos !== FALSE) { $obj = substr($link, 0, $pos); $label = substr($link, $pos); } else { $obj = $link; } $pos = strrpos($obj, '.'); if ($pos !== FALSE) { // get package name $packageName = substr($obj, 0, $pos); $obj = substr($obj, $pos + 1); } $packageDir = packageToDir($packageName); $pos = strpos($obj, '::'); $pos2 = strpos($obj, '#'); if (preg_match('/.*<\/a>/', $link)) { return $link; } elseif ($pos !== FALSE) { $objectName = substr($obj, 0, $pos); $itemName = substr($obj, $pos + 2); } elseif ($pos2 !== FALSE) { $objectName = substr($obj, 0, $pos2); $itemName = substr($obj, $pos2 + 1); } else { $itemName = $obj; } if (!isset($label)) { if ($objectName) { $label = $objectName.'::'.$itemName; } else { $label = $itemName; } } if (isset($objects[$packageName]['classes'][$objectName]['functions'][$itemName]) || isset($objects[$packageName]['interfaces'][$objectName]['functions'][$itemName])) { return ''.$label.''; } elseif (isset($objects[$packageName]['classes'][$itemName]) || isset($objects[$packageName]['interfaces'][$itemName])) { return ''.$label.''; } elseif (isset($objects[$packageName]['functions'][$itemName])) { return ''.$label.''; } elseif (isset($objects[$packageName]['constants'][$itemName])) { return ''.$label.''; } elseif (isset($objects[$packageName]['globals'][$itemName])) { return ''.$label.''; } return '"'.$link.'"'; } /** * Replace inline link doc tags with HTML tags. This function recursively traverses an array looking for description entries to parse for link tags. * * @param array items Object array to traverse for links * @return array The parsed array */ function replaceInlineLinks(&$items, $packageName = DEFAULT_PACKAGE) { foreach ($items as $key => $item) { if (isset($items['package']) && !is_array($items['package'])) { $packageName = $items['package']; } if (is_array($item)) { $items[$key] = replaceInlineLinks($item, $packageName); } elseif ($key == 'shortDescription' || $key == 'description') { $text = $item; $matches = array(); preg_match_all('/{@link .*}/U', $text, $matches, PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER); if (isset($matches[0]) && count($matches[0]) > 0) { $newtext = ''; $lastPos = 0; foreach ($matches[0] as $match) { $link = substr($match[0], 7, strlen($match[0]) - 8); $newtext .= substr($text, $lastPos, $match[1] - $lastPos).''.parseLink($link, $packageName).''; $lastPos = $match[1] + strlen($match[0]); } $text = $newtext.substr($text, $lastPos); } $matches = array(); preg_match_all('/{@linkplain .*}/U', $text, $matches, PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER); if (isset($matches[0]) && count($matches[0]) > 0) { $newtext = ''; $lastPos = 0; foreach ($matches[0] as $match) { $link = substr($match[0], 12, strlen($match[0]) - 13); $newtext .= substr($text, $lastPos, $match[1] - $lastPos).parseLink($link, $packageName); $lastPos = $match[1] + strlen($match[0]); } $text = $newtext.substr($text, $lastPos); } $items[$key] = $text; } } return $items; } /** * Replace see doc tags with HTML tags. This function recursively traverses an array looking for see entries to convert. * * @param array items Object array to traverse for links * @return array The parsed array */ function setSeeLinks(&$items) { foreach ($items as $key => $item) { if ($key === 'see' && is_array($item)) { if (isset($items['package']) && !is_array($items['package'])) { $packageName = $items['package']; } else { $packageName = DEFAULT_PACKAGE; } foreach ($item as $seekey => $see) { $items[$key][$seekey] = parseLink(trim($see), $packageName); } } elseif (is_array($item)) { $items[$key] = setSeeLinks($item); } } return $items; } /** * Parse the token array into objects. * * @param array tokens Token array * @return array Object array */ function parseTokens(&$tokens) { echo "Parsing tokens...\n"; $objects = array(); $docComment = array(); $access = 'public'; $abstract = FALSE; $final = FALSE; $static = FALSE; $var = FALSE; $const = FALSE; $object = &$objects[]; $object['inBody'] = 0; $open_curly_braces = FALSE; $in_parsed_string = FALSE; foreach ($tokens as $key => $token) { if (!$in_parsed_string && is_array($token)) { switch ($token[0]) { case T_COMMENT: // read comment case T_ML_COMMENT: // and multiline comment (deprecated I think) case T_DOC_COMMENT: // and catch PHP5 doc comment token too $result = processDocComment($token[1]); if ($result) $docComment = mergeArrays($docComment, $result); break; case T_CLASS: // read class $subObject = array(); $subObject['..'] = &$object; $subObject['package'] = DEFAULT_PACKAGE; $subObject['name'] = getNext($tokens, $key, T_STRING); $subObject['abstract'] = $abstract; $subObject['final'] = $final; $subObject['docComment'] = $docComment; if (isset($docComment['package'])) $subObject['package'] = $docComment['package']; $subObject['dir'] = packageToDir($subObject['package']); $object = &$object[$subObject['package']]['classes'][$subObject['name']]; if ($object) { $object = array_merge($object, $subObject); } else { $object = $subObject; } $docComment = array(); $abstract = FALSE; $final = FALSE; break; case T_INTERFACE: // read interface $subObject = array(); $subObject['..'] = &$object; $subObject['package'] = DEFAULT_PACKAGE; $subObject['name'] = getNext($tokens, $key, T_STRING); $subObject['abstract'] = $abstract; $subObject['final'] = $final; $subObject['docComment'] = $docComment; if (isset($docComment['package'])) $subObject['package'] = $docComment['package']; $subObject['dir'] = packageToDir($subObject['package']); $object = &$object[$subObject['package']]['interfaces'][$subObject['name']]; if ($object) { $object = array_merge($object, $subObject); } else { $object = $subObject; } $docComment = array(); $abstract = FALSE; $final = FALSE; break; case T_EXTENDS: // get extends clause $object['extends'] = getNext($tokens, $key, T_STRING); break; case T_IMPLEMENTS: // get implements clause while($tokens[++$key] != '{') { if ($tokens[$key][0] == T_STRING) { $object['implements'][] = $tokens[$key][1]; } } break; case T_PRIVATE: $access = 'private'; break; case T_PROTECTED: $access = 'protected'; break; case T_ABSTRACT: $abstract = TRUE; break; case T_FINAL: $final = TRUE; break; case T_STATIC: $static = TRUE; break; case T_VAR: $var = TRUE; break; case T_CONST: $const = TRUE; break; case T_FUNCTION: // read function $subObject = array(); $subObject['..'] = &$object; if (!isset($object['name'])) { // only set package name for top level functions $subObject['package'] = DEFAULT_PACKAGE; } $subObject['name'] = getNext($tokens, $key, T_STRING); $subObject['access'] = $access; if (substr($subObject['name'], 0, 1) == '_' && $subObject['access'] == 'public') $subObject['access'] = 'private'; $subObject['abstract'] = $abstract; $subObject['final'] = $final; $subObject['static'] = $static; $subObject['return']['type'] = 'void'; $subObject['docComment'] = $docComment; if (isset($docComment['package'])) $subObject['package'] = $docComment['package']; if (!HIDE_PRIVATE || ($subObject['access'] != 'private' && (!isset($docComment['access']) || $docComment['access'] != 'private'))) { if (isset($subObject['package'])) { $subObject['dir'] = packageToDir($subObject['package']); $object = &$object[$subObject['package']]['functions'][$subObject['name']]; } else { $object = &$object['functions'][$subObject['name']]; } if ($object) { $object = array_merge($object, $subObject); } else { $object = $subObject; } } $docComment = array(); $access = 'public'; $abstract = FALSE; $final = FALSE; $static = FALSE; break; case T_STRING: if (!NO_CONSTANT && $token[1] == 'define') { // read global constant $subObject = array(); if (!isset($object['classes']) || !isset($object['interfaces'])) { // only set package name for top level functions $subObject['package'] = DEFAULT_PACKAGE; } $subObject['name'] = trim(getNext($tokens, $key, T_CONSTANT_ENCAPSED_STRING), '\'"'); $subObject['type'] = 'const'; addDocComment($subObject, $docComment); if (isset($docComment['package'])) $subObject['package'] = $docComment['package']; if (isset($subObject['package'])) { $subObject['dir'] = packageToDir($subObject['package']); if (isset($object['params'][$subObject['name']])) { $object[$subObject['package']]['constants'][$subObject['name']] = array_merge($object['params'][$subObject['name']], $subObject); } else { $object[$subObject['package']]['constants'][$subObject['name']] = $subObject; } } else { if (isset($object['params'][$subObject['name']])) { $object['constants'][$subObject['name']] = array_merge($object['params'][$subObject['name']], $subObject); } else { $object['constants'][$subObject['name']] = $subObject; } } $docComment = array(); } elseif ($const) { // member constant $subObject = array(); $subObject['final'] = $final; $subObject['static'] = $static; $subObject['type'] = 'const'; addDocComment($subObject, $docComment); do { if ($tokens[$key][0] == T_STRING) { $subObject['access'] = $access; $subObject['name'] = $tokens[$key][1]; $object['vars'][] = $subObject; } $key++; } while($tokens[$key] != ';'); $docComment = array(); $final = FALSE; $static = FALSE; $const = FALSE; } break; case T_VARIABLE: // read variables / parameters if ($var || $access == 'private' || $access == 'protected') { // member var $subObject = array(); $subObject['final'] = $final; $subObject['static'] = $static; $subObject['type'] = 'var'; $subObject['default'] = ''; addDocComment($subObject, $docComment); $default = FALSE; do { $key++; if ($tokens[$key] == '=') { $default = TRUE; } elseif ($tokens[$key] == ',' || $tokens[$key] == ';') { $subObject['access'] = $access; $subObject['name'] = getPrev($tokens, $key, T_VARIABLE); if (substr($subObject['name'], 1, 1) == '_' && $subObject['access'] == 'public') $subObject['access'] = 'private'; $subObject['default'] = trim($subObject['default']); if (!HIDE_PRIVATE || $subObject['access'] != 'private') { $object['vars'][] = $subObject; } $subObject['default'] = ''; $default = FALSE; } elseif($default) { if (is_array($tokens[$key])) { $subObject['default'] .= $tokens[$key][1]; } else { $subObject['default'] .= $tokens[$key]; } } } while($tokens[$key] != ';'); $docComment = array(); $access = 'public'; $final = FALSE; $static = FALSE; $var = FALSE; } elseif ((!isset($object['inBody']) || $object['inBody'] == 0) && (!NO_GLOBAL || isset($object['..']))) { $subObject = array(); if (!isset($object['name'])) { // only set package name for top level functions $subObject['package'] = DEFAULT_PACKAGE; } $subObject['name'] = $tokens[$key][1]; if (isset($tokens[$key - 1][0]) && is_array($tokens[$key - 2]) && $tokens[$key - 2][0] == T_STRING && $tokens[$key - 1][0] == T_WHITESPACE) { $subObject['type'] = $tokens[$key - 2][1]; } else { $subObject['type'] = 'var'; } addDocComment($subObject, $docComment); if ($tokens[$key + 1] == '=' || $tokens[$key + 2] == '=') { $subObject['default'] = ''; $key2 = $key + 1; do { if (is_array($tokens[$key2])) { if ($tokens[$key2][1] != '=') $subObject['default'] .= $tokens[$key2][1]; } else { if ($tokens[$key2] != '=') $subObject['default'] .= $tokens[$key2]; } $key2++; } while(isset($tokens[$key2]) && $tokens[$key2] != ';' && $tokens[$key2] != ',' && $tokens[$key2] != ')'); $subObject['default'] = trim($subObject['default'], ' ()'); } if (isset($docComment['package'])) $subObject['package'] = $docComment['package']; if (isset($subObject['package'])) { $subObject['dir'] = packageToDir($subObject['package']); if (isset($object['params'][substr($tokens[$key][1], 1)])) { $object[$subObject['package']]['params'][substr($tokens[$key][1], 1)] = array_merge($subObject, $object['params'][substr($tokens[$key][1], 1)]); } else { $object[$subObject['package']]['params'][substr($tokens[$key][1], 1)] = $subObject; } } else { if (isset($object['params'][substr($tokens[$key][1], 1)])) { $object['params'][substr($tokens[$key][1], 1)] = array_merge($subObject, $object['params'][substr($tokens[$key][1], 1)]); } else { $object['params'][substr($tokens[$key][1], 1)] = $subObject; } } $docComment = array(); } break; case T_CURLY_OPEN: case T_DOLLAR_OPEN_CURLY_BRACES: // we must catch this so we don't accidently step out of the current block $open_curly_braces = TRUE; break; } } else { switch ($token) { case '{': if (!$in_parsed_string) { if (!isset($object['inBody'])) $object['inBody'] = 0; $object['inBody']++; } break; case '}': if (!$in_parsed_string) { if ($open_curly_braces) { // end of var curly brace syntax $open_curly_braces = FALSE; } else { $object['inBody']--; if ($object['inBody'] == 0 && isset($object['..'])) { addDocComment($object, $object['docComment']); $object = &$object['..']; $docComment = array(); $access = 'public'; $abstract = FALSE; $final = FALSE; $static = FALSE; } } } break; case ';': // case for closing abstract functions if (!$in_parsed_string && !isset($object['inBody'])) { addDocComment($object, $object['docComment']); $object = &$object['..']; $docComment = array(); $access = 'public'; $abstract = FALSE; $final = FALSE; $static = FALSE; } break; case '"': // catch parsed strings so as to ignore tokens within $in_parsed_string = !$in_parsed_string; break; } } } //print_r(get_defined_constants()); //dumpToFile($tokens); //dumpToFile($objects); return cleanObjectArray($objects[0]); } /** * Get next token of a certain type from token array * * @param array tokens Token array to search * @param int key Key to start searching from * @param int whatToGet Type of token to look for * @return str Value of found token */ function getNext(&$tokens, $key, $whatToGet) { $key++; while(!is_array($tokens[$key]) || $tokens[$key][0] != $whatToGet) { $key++; if (!isset($tokens[$key])) return FALSE; } return $tokens[$key][1]; } /** * Get previous token of a certain type from token array * * @param array tokens Token array to search * @param int key Key to start searching from * @param int whatToGet Type of token to look for * @return str Value of found token */ function getPrev(&$tokens, $key, $whatToGet) { $key--; while(!is_array($tokens[$key]) || $tokens[$key][0] != $whatToGet) { $key--; if (!isset($tokens[$key])) return FALSE; } return $tokens[$key][1]; } /** * Clean up object array after generation. This function removes work fields from the array. * * @param array items Object array to tidy * @return Tidied object array */ function cleanObjectArray($items) { if (is_array($items)) { foreach ($items as $key => $item) { if ($key === 'inBody' || $key === '..' || $key === 'docComment') { unset($items[$key]); } else { $items[$key] = cleanObjectArray($item); } } } return $items; } /** * Turn an array into a space seperated string * * @param array array Array to turn into a string * @param int startIndex Ignore items before this index */ function arrayToString(&$array, $startIndex) { $string = ''; foreach ($array as $index => $item) { if ($index >= $startIndex) $string .= $item.' '; } return $string; } /** * Process a doc comment into a doc tag array. * * @param str comment The comment to process * @return array Array of doc comment data */ function processDocComment($comment) { $docComment = array(); $comment = explode("\n", $comment); if (substr(trim($comment[0]), 0, 3) != '/**') return FALSE; $tagName = 'shortDescription'; $paramName = FALSE; $tagIndex = FALSE; foreach ($comment as $line) { $line = trim($line, "\t\r /*"); // strip whitespace and asterix's from beginning $words = array(); foreach (explode(' ', $line) as $word) { // explode line into words and eat whitespace if ($word != '') $words[] = trim($word, "\t "); } if (isset($words[0])) { if (substr($words[0], 0, 1) == '@') { // first word is tag $tagName = $words[0]; // set tag name $paramName = FALSE; $tagIndex = FALSE; } switch ($tagName) { // get tag contents case '@package': if (isset($words[1])) $docComment['package'] = rtrim(arrayToString($words, 1)); break; case '@access': if (isset($words[1])) $docComment['access'] = $words[1]; break; case '@param': if ($paramName && isset($docComment['params'][$paramName]['description'])) { $docComment['params'][$paramName]['description'] .= arrayToString($words, 0); } elseif (isset($words[2])) { if (substr($words[2], 0, 1) == '$') { $paramName = substr($words[2], 1); } elseif (substr($words[2], 0, 2) == '&$') { $paramName = substr($words[2], 2); } else { $paramName = $words[2]; } $docComment['params'][$paramName]['name'] = $paramName; $docComment['params'][$paramName]['type'] = $words[1]; if (isset($words[3])) $docComment['params'][$paramName]['description'] = arrayToString($words, 3); } break; case '@return': if (isset($docComment['return']['description'])) { $docComment['return']['description'] .= arrayToString($words, 0); } elseif (isset($words[1])) { $docComment['return']['type'] = $words[1]; if (isset($words[2])) $docComment['return']['description'] = arrayToString($words, 2); } break; case '@see': if (isset($words[1])) $docComment['see'][] = arrayToString($words, 1); break; case '@var': case '@type': if (isset($docComment['type']) && isset($docComment['description'])) { $docComment['description'] .= arrayToString($words, 0); } elseif (isset($words[1])) { $docComment['type'] = $words[1]; if (isset($words[2])) $docComment['description'] = arrayToString($words, 2); } break; case 'shortDescription': case 'description': if (!isset($docComment[$tagName])) $docComment[$tagName] = ''; foreach ($words as $word) { $docComment[$tagName] .= $word.' '; if ($tagName == 'shortDescription') { if (substr($word, -1, 1) == '.') { $docComment['description'] = ''; $tagName = 'description'; } } } $docComment[$tagName] = $docComment[$tagName]; break; default: $tag = substr($tagName, 1); if ($tagIndex && isset($docComment[$tag][$tagIndex])) { $docComment[$tag][$tagIndex] .= arrayToString($words, 0); } else { $docComment[$tag][] = arrayToString($words, 1); $tagIndex = count($docComment[$tag]); } } } } return $docComment; } /** * Add a doc comment to an object. * * @param array subObject Object to add doc comment data to * @param array docComment Doc comment to add */ function addDocComment(&$subObject, $docComment) { if (isset($docComment['shortDescription'])) { $subObject['shortDescription'] = $docComment['shortDescription']; } if (isset($docComment['description'])) { $subObject['description'] = $docComment['description']; } foreach ($docComment as $tag => $value) { if ($tag == 'params') { foreach($value as $name => $param) { if (!isset($subObject['params'][$name])) { // param doc comment found for non-defined parameter $warningStr = 'Unknown param "'.$name.'" found for method "'; if (isset($subObject['..']['name'])) $warningStr .= $subObject['..']['name'].'::'; $warningStr .= $subObject['name'].'".'; warning($warningStr); } } } if (isset($subObject[$tag]) && is_array($value)) { // merge it $subObject[$tag] = mergeArrays($subObject[$tag], $value); } else { // just add it $subObject[$tag] = $value; } } } /** * Recursively merge two arrays into a single array. Same values are overwritten by the values in array two. * * @param array one Array one * @param array two Array two * * @return array Merged array */ function mergeArrays($one, $two) { foreach ($two as $key => $item) { if (isset($one[$key]) && is_array($one[$key]) && is_array($item)) { $one[$key] = mergeArrays($one[$key], $item); } else { $one[$key] = $item; } } return $one; } /** * Generate the class tree. This function is called recursively for each class with children. * * @param array t Object array * @param str name Name of parent class * @param int depth Current depth of recursion */ function classTree(&$t, $path, $name = NULL, $depth = 2) { if ($name == NULL) { $class = $t['this']; } elseif (isset($t[$name])) { $class = $t[$name]; } if (isset($class['extends'])) { $depth = classTree($t, $path, $class['extends'], $depth); } if ($name == NULL) { echo '', $class['package'], '.', $class['name'], '
'; } else { if (isset($class)) { echo '', $class['package'], '.', $class['name'], '
'; } else { echo $name, '
'; } for ($bar = 0; $bar < $depth; $bar++) echo ' '; echo '|
'; for ($bar = 0; $bar < $depth; $bar++) echo ' '; echo '+--'; } return $depth + 6; } /** * Recursively draw the tree for the given classes * * @param array t Array of classes to draw * @param str parent Name of parent node, used for recursive call * @param str path path to draw tree from, used in recursive call */ function drawTree($t, $includePackage = FALSE, $parent = NULL, $path = '') { $output = FALSE; echo ''; } /** * Get body of a HTML document * * @param filename * @return str */ function getHTMLContents($filename) { if ($contents = file_get_contents($filename)) { if (preg_match('/(.+)<\/body>/s', $contents, $matches)) { return $matches[1]; } } warning('Could not find body in HTML file '.$filename); return ''; } /** * Check if filename is relative, and if so prefix path */ function prefixFilename($filename, $path) { if (substr($filename, 0, 1) != '/' && substr($filename, 1, 2) != ':\\') { // if path is local, add prefix $path = pathAppendSlash($path); $filename = $path.$filename; } return $filename; } /** * Add trailing slash to path if not present */ function pathAppendSlash($path) { if (substr($path, -1, 1) != '/' && substr($path, -1, 1) != '\\') $path .= '/'; return $path; } /** * Get the current time in microseconds * * @return int */ function getTime() { $microtime = explode(' ', microtime()); return $microtime[0] + $microtime[1]; } /** * Dump a variable to a file * * @param mixed var Variable to dump * @param bool append Append to existing file and continue execution */ function dumpToFile(&$var, $append = FALSE) { // dump parsed tokens if ($append) { $fp = fopen('output.txt', 'a'); } else { $fp = fopen('output.txt', 'w'); } if ($fp) { ob_start(); print_r($var); $output = ob_get_contents(); ob_end_clean(); fwrite($fp, $output); fclose($fp); } if (!$append) die; } ?>