Implement a token()-based parser. From: <> --- api.admin.inc | 8 + parser.inc | 321 ++++++------------------------------------- parser/element.argument.inc | 43 ++++++ parser/element.class.inc | 65 +++++++++ parser/element.file.inc | 8 + parser/element.function.inc | 51 +++++++ parser/element.global.inc | 39 +++++ parser/element.inc | 39 +++++ parser/file.inc | 94 +++++++++++++ parser/token.inc | 201 +++++++++++++++++++++++++++ 10 files changed, 589 insertions(+), 280 deletions(-) diff --git api.admin.inc api.admin.inc index ce007a9..25c369c 100644 --- api.admin.inc +++ api.admin.inc @@ -217,6 +217,12 @@ function api_reindex_form() { } function api_reindex_form_submit($form, &$form_state) { - db_query("UPDATE {api_file} SET modified = 52"); + // XXX: Debug code. + db_query("DELETE FROM {api_documentation}"); + db_query("DELETE FROM {api_file}"); + db_query("DELETE FROM {api_function"); + db_query("DELETE FROM {api_reference_storage"); + variable_del("cron_semaphore"); + drupal_set_message(t('All files have been tagged for reindexing. The index will be rebuilt during the next few runs of !cron.', array('!cron' => l('cron.php', 'admin/reports/status/run-cron')))); } diff --git parser.inc parser.inc index cd1c34f..7bd8f4a 100644 --- parser.inc +++ parser.inc @@ -102,303 +102,66 @@ function api_parse_html_file($file_path, $branch_name, $file_name) { * Read in the file at the given path and parse its documentation. */ function api_parse_php_file($file_path, $branch_name, $file_name) { - $source = file_get_contents($file_path); - - // Convert Mac/Win line breaks to Unix format. - $source = str_replace("\r\n", "\n", $source); - $source = str_replace("\r", "\n", $source); + // XXX: Debug code. + echo $file_name . " "; flush(); + + $root_dir = drupal_get_path('module', 'api'); + require_once $root_dir . '/parser/token.inc'; + require_once $root_dir . '/parser/file.inc'; + require_once $root_dir . '/parser/element.inc'; + require_once $root_dir . '/parser/element.file.inc'; + require_once $root_dir . '/parser/element.argument.inc'; + require_once $root_dir . '/parser/element.function.inc'; + require_once $root_dir . '/parser/element.class.inc'; + require_once $root_dir . '/parser/element.global.inc'; + + $source_file = new PHPSourceFile($file_path, $file_name); + $parsed_file = new PHPFile(NULL, $source_file); $docblocks = array(); + _api_documentation_recurse($docblocks, $parsed_file, $branch_name, $file_name); - // Set up documentation block for file, in case it is not explicitly defined. - $docblocks[0] = array( - 'object_name' => $file_name, - 'branch_name' => $branch_name, - 'object_type' => 'file', - 'file_name' => $file_name, - 'title' => strpos($file_name, '/') ? substr($file_name, strrpos($file_name, '/') + 1) : $file_name, - 'summary' => '', - 'documentation' => '', - 'code' => api_format_php($source), - 'version' => '', - 'modified' => filemtime($file_path), - ); - $version_match = array(); - if (preg_match('!\$'.'Id: .*?,v (.*?) (.*?) (.*?) (.*?) Exp \$!', $source, $version_match)) { - $docblocks[0]['version'] = $version_match[1] .' (checked in on '. $version_match[2] .' at '. $version_match[3] .' by '. $version_match[4] .')'; - } - - $nested_groups = array(); + api_save_documentation($docblocks, $branch_name, $file_name); +} - $docblock_matches = array(); - preg_match_all('!/\*\*(.*?)\*/!s', $source, $docblock_matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); +function _api_documentation_recurse(array &$docblocks, AbstractPHPCodeElement $block, $branch_name, $file_name) { + $map = array( + 'PHPFunction' => 'function', + 'PHPClass' => 'class', + 'PHPFile' => 'file', + ); - foreach ($docblock_matches as $docblock_match) { + if (isset($map[get_class($block)])) { $docblock = array( - 'object_name' => '', + 'object_type' => $map[get_class($block)], + 'object_name' => $block->name, 'branch_name' => $branch_name, - 'object_type' => '', + 'class' => $block->class, 'file_name' => $file_name, - 'title' => '', - 'summary' => '', - 'documentation' => '', - 'code' => '', + 'code' => (string) $block, + 'start_line' => $block->line, ); - $docblock['content'] = str_replace(array("\n *", "\n "), array("\n", "\n"), $docblock_match[1][0]); - $docblock['start'] = $docblock_match[0][1]; - $docblock['length'] = strlen($docblock_match[0][0]); - $code_start = $docblock['start'] + $docblock['length'] + 1; - $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n"); - - // Determine what kind of documentation block this is. - if (substr($source, $code_start, 8) == 'function') { - $function_matches = array(); - - $docblock['object_type'] = 'function'; - preg_match('!^function (&?([a-zA-Z0-9_]+)\(.*?)\s*\{!', substr($source, $code_start), $function_matches); - $docblock['object_name'] = $function_matches[2]; - $docblock['title'] = $function_matches[2]; - $docblock['signature'] = $function_matches[1]; - - // We rely on the Drupal coding convention that functions are closed in column 1. - $code_end = strpos($source, "\n}", $code_start) + 2; - $docblock['code'] = substr($source, $code_start, $code_end - $code_start); - $docblock['code'] = api_format_php(""); - - // Find parameter definitions. - $param_match = array(); - $offset = 0; - $docblock['parameters'] = ''; - while (preg_match('!@param(.*?)(?=\n@|\n\n|$)!s', substr($docblock['content'], $offset), $param_match, PREG_OFFSET_CAPTURE)) { - $docblock['content'] = str_replace($param_match[0][0], '', $docblock['content']); - $docblock['parameters'] .= "\n\n". $param_match[1][0]; - $offset = $param_match[0][1]; - } - $docblock['parameters'] = api_format_documentation($docblock['parameters'], $branch_name); - - // Find return value definitions. - $return_matches = array(); - $docblock['return_value'] = ''; - preg_match_all('!@return(.*?)(\n@|\n\n|$)!s', $docblock['content'], $return_matches, PREG_SET_ORDER); - foreach ($return_matches as $return_match) { - $docblock['content'] = str_replace($return_match[0], '', $docblock['content']); - $docblock['return_value'] .= "\n\n". $return_match[1]; - } - $docblock['return_value'] = api_format_documentation($docblock['return_value'], $branch_name); - - $docblock['function calls'] = api_parse_function_calls($docblock['code']); - - // Determine group membership. - $group_matches = array(); - preg_match_all('!@(ingroup|addtogroup) ([a-zA-Z0-9_]+)!', $docblock['content'], $group_matches); - $docblock['groups'] = $group_matches[2]; - $docblock['content'] = preg_replace('!@ingroup.*?\n!', '', $docblock['content']); - - foreach ($nested_groups as $group_id) { - if (!empty($group_id)) { - $docblock['groups'][] = $group_id; - } - } - } - else if (substr($source, $code_start, 6) == 'define') { - $constant_matches = array(); - - $docblock['object_type'] = 'constant'; - preg_match('!^define\([\'"]([a-zA-Z0-9_]+)[\'"]!', substr($source, $code_start), $constant_matches); - $docblock['object_name'] = $constant_matches[1]; - $docblock['title'] = $constant_matches[1]; - - $code_end = strpos($source, ';', $code_start) + 1; - $docblock['code'] = substr($source, $code_start, $code_end - $code_start); - $docblock['code'] = api_format_php(""); - - // Determine group membership. - $group_matches = array(); - preg_match_all('!@(ingroup|addtogroup) ([a-zA-Z0-9_]+)!', $docblock['content'], $group_matches); - $docblock['groups'] = $group_matches[2]; - $docblock['content'] = preg_replace('!@ingroup.*?\n!', '', $docblock['content']); - - foreach ($nested_groups as $group_id) { - if (!empty($group_id)) { - $docblock['groups'][] = $group_id; - } - } - } - else if (substr($source, $code_start, 6) == 'global') { - $global_matches = array(); - $docblock['object_type'] = 'global'; - preg_match('!^global (\$[a-zA-Z0-9_]+)!', substr($source, $code_start), $global_matches); - - $docblock['object_name'] = substr($global_matches[1], 1); - $docblock['title'] = $global_matches[1]; - - $code_end = strpos($source, ';', $code_start) + 1; - $docblock['code'] = substr($source, $code_start, $code_end - $code_start); - $docblock['code'] = api_format_php(""); - - $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n"); - - // Determine group membership. - $group_matches = array(); - preg_match_all('!@(ingroup|addtogroup) ([a-zA-Z0-9_]+)!', $docblock['content'], $group_matches); - $docblock['groups'] = $group_matches[2]; - $docblock['content'] = preg_replace('!@ingroup.*?\n!', '', $docblock['content']); - - foreach ($nested_groups as $group_id) { - if (!empty($group_id)) { - $docblock['groups'][] = $group_id; - } - } - } - else if (strpos($docblock['content'], '@mainpage') !== FALSE) { - $mainpage_matches = array(); - preg_match('!@mainpage (.*?)\n!', $docblock['content'], $mainpage_matches); - $docblock['title'] = $mainpage_matches[1]; - $docblock['content'] = preg_replace('!@mainpage.*?\n!', '', $docblock['content']); - $docblock['object_type'] = 'mainpage'; - $docblock['object_name'] = $branch_name; - } - else if (strpos($docblock['content'], '@file') !== FALSE) { - $docblocks[0]['content'] = str_replace('@file', '', $docblock['content']); - $docblocks[0]['documentation'] = api_format_documentation($docblocks[0]['content'], $branch_name); - $docblocks[0]['summary'] = api_documentation_summary($docblocks[0]['documentation']); - } - else if (strpos($docblock['content'], '@defgroup') !== FALSE) { - $group_matches = array(); - if (preg_match('!@defgroup ([a-zA-Z0-9_.-]+) +(.*?)\n!', $docblock['content'], $group_matches)) { - $docblock['object_name'] = $group_matches[1]; - $docblock['title'] = $group_matches[2]; - $docblock['content'] = preg_replace('!@defgroup.*?\n!', '', $docblock['content']); - $docblock['object_type'] = 'group'; - } - else { - watchdog('api', 'Malformed @defgroup in %file at line %line.', array('%file' => $file_path, '%line' => $docblock['start_line']), WATCHDOG_NOTICE); - } + if (isset($block->documentation)) { + $docblock['summary'] = $block->documentation->summary; + $docblock['documentation'] = $block->documentation->body; } - // Handle nested function groups. - if (strpos($docblock['content'], '@{') !== FALSE) { - if ($docblock['object_type'] == 'group') { - array_push($nested_groups, $docblock['object_name']); - } - else { - $group_matches = array(); - if (preg_match('!@(ingroup|addtogroup) ([a-zA-Z0-9_]+)!', $docblock['content'], $group_matches)) { - array_push($nested_groups, $group_matches[2]); - } - else { - array_push($nested_groups, ''); - } - } - } - if (strpos($docblock['content'], '@}') !== FALSE) { - array_pop($nested_groups); - } - - if ($docblock['object_type'] != '') { - $docblock['documentation'] = api_format_documentation($docblock['content'], $branch_name); - $docblock['summary'] = api_documentation_summary($docblock['documentation']); - $docblocks[] = $docblock; - } - } + $docblock['title'] = isset($block->title) ? $block->title : $block->name; - // Find undocumented functions. - $function_matches = array(); - preg_match_all('%(? $function_match[2][0], - 'branch_name' => $branch_name, - 'object_type' => 'function', - 'file_name' => $file_name, - 'title' => $function_match[2][0], - 'summary' => '', - 'documentation' => '', - 'code' => ''); - $docblock['signature'] = $function_match[1][0]; + // XXX: E_ALL temporary hack. + $docblock['signature'] = ''; $docblock['parameters'] = ''; $docblock['return_value'] = ''; - $docblock['groups'] = array(); - - $code_start = $function_match[0][1]; - $code_end = strpos($source, "\n}", $code_start) + 2; - $docblock['code'] = substr($source, $code_start, $code_end - $code_start); - $docblock['code'] = api_format_php(""); - - $docblock['function calls'] = api_parse_function_calls($docblock['code']); - - $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n"); - - $docblocks[] = $docblock; - } - - // Find undocumented constants. - $constant_matches = array(); - preg_match_all('%(? $constant_match[1][0], - 'branch_name' => $branch_name, - 'object_type' => 'constant', - 'file_name' => $file_name, - 'title' => $constant_match[1][0], - 'summary' => '', - 'documentation' => '', - 'code' => ''); - $docblock['groups'] = array(); - - $code_start = $constant_match[0][1]; - $code_end = strpos($source, ';', $code_start) + 1; - $docblock['code'] = substr($source, $code_start, $code_end - $code_start); - $docblock['code'] = api_format_php(""); - - $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n"); - - $docblocks[] = $docblock; - } - - // Find undocumented globals. - $global_matches = array(); - preg_match_all('%(? $global_match[1][0], - 'branch_name' => $branch_name, - 'object_type' => 'global', - 'file_name' => $file_name, - 'title' => $global_match[1][0], - 'summary' => '', - 'documentation' => '', - 'code' => '', - ); - $docblock['groups'] = array(); - - $code_start = $global_match[0][1]; - $code_end = strpos($source, ';', $code_start) + 1; - $docblock['code'] = substr($source, $code_start, $code_end - $code_start); - $docblock['code'] = api_format_php(""); - - $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n"); - + $docblocks[] = $docblock; } - api_save_documentation($docblocks, $branch_name, $file_name); -} - -/** - * Find functions called in a formatted block of code. - */ -function api_parse_function_calls($code) { - $function_call_matches = array(); - $function_calls = array(); - preg_match_all('!([a-zA-Z0-9_]+)\(!', $code, $function_call_matches, PREG_SET_ORDER); - array_shift($function_call_matches); // Remove the first match, the function declaration itself. - foreach ($function_call_matches as $function_call_match) { - $function_calls[$function_call_match[1]] = $function_call_match[1]; + foreach ($block->children as $child_block) { + if (!isset($child_block)) { + // ??? + } + _api_documentation_recurse($docblocks, $child_block, $branch_name, $file_name); } - - return $function_calls; } /** diff --git parser/element.argument.inc parser/element.argument.inc new file mode 100644 index 0000000..a85bd5a --- /dev/null +++ parser/element.argument.inc @@ -0,0 +1,43 @@ +getStartLine($parent, $file); + + $level = 0; + $argument = array(); + while ($file->hasTokens()) { + $token = $file->claimNext($this); + if ($level == 0 && $token->source == ',') { + $this->arguments[] = $argument; + $argument = array(); + continue; + } + if ($token->source == '(') { + $level++; + } + if ($token->source == ')') { + if ($level > 0) { + $level--; + } + else { + // Give the token back to the parent. + $file->giveUp($token); + array_pop($this->children); + if (!empty($argument)) { + $this->arguments[] = $argument; + } + break; + } + } + $argument[] = $token; + } + } +} diff --git parser/element.class.inc parser/element.class.inc new file mode 100644 index 0000000..c379630 --- /dev/null +++ parser/element.class.inc @@ -0,0 +1,65 @@ +claimNextExpecting($this, T_CLASS); + + // Backtrace parent tokens to find modifiers. + while ($token = $parent->pop()) { + if (!in_array($token->type, array(T_DOC_COMMENT, T_COMMENT, T_PUBLIC, T_PRIVATE, T_PROTECTED, T_STATIC, T_ABSTRACT, T_WHITESPACE))) { + // Not one of the token we want, let the parent have it. + $parent->push($token); + break; + } + if ($token->type == T_DOC_COMMENT && empty($this->documentation)) { + // Only the first T_DOC_COMMENT (the closest one to the function) will be taken into account. + $this->documentation = $token; + continue; + } + // Add that token at the beginning of this element. + $this->unshift($token); + } + + // Determine the real start line of this element. + $this->getStartLine($parent, $file); + + // Class name. + $name = $file->claimNextExpecting($this, T_STRING); + $this->name = $name->source; + $this->class = $this->name; + + // Extends and Implements. + while ($file->hasTokens()) { + if ($file->claimNextIf($this, T_EXTENDS)) { + while ($identifier = $file->claimNextIf($this, T_STRING)) { + $this->extends[] = $identifier->source; + if (!$file->claimNextIf($this, 0, ",")) { + break; + } + } + } + else if ($file->claimNextIf($this, T_IMPLEMENTS)) { + while ($identifier = $file->claimNextIf($this, T_STRING)) { + $this->implements[] = $identifier->source; + if (!$file->claimNextIf($this, 0, ",")) { + break; + } + } + } + else { + break; + } + } + + // The content of the class. + $file->claimNextExpecting($this, 0, "{"); + $this->push(new PHPCodeBlock($this, $file)); + } +} diff --git parser/element.file.inc parser/element.file.inc new file mode 100644 index 0000000..dd0b220 --- /dev/null +++ parser/element.file.inc @@ -0,0 +1,8 @@ +name = $file->name; + parent::__construct($parent, $file); + } +} diff --git parser/element.function.inc parser/element.function.inc new file mode 100644 index 0000000..0e75c9b --- /dev/null +++ parser/element.function.inc @@ -0,0 +1,51 @@ +claimNextExpecting($this, T_FUNCTION); + + // Backtrace parent tokens to find modifiers. + while ($token = array_pop($parent->children)) { + if (!in_array($token->type, array(T_DOC_COMMENT, T_COMMENT, T_PUBLIC, T_PRIVATE, T_PROTECTED, T_STATIC, T_ABSTRACT, T_WHITESPACE))) { + // Not one of the token we want, let the parent have it. + $parent->push($token); + break; + } + if ($token->type == T_DOC_COMMENT && empty($this->documentation)) { + // Only the first T_DOC_COMMENT (the closest one to the function) will be taken into account. + $this->documentation = $token; + continue; + } + // Add the token at the begining of the array. + array_unshift($this->children, $token); + } + + // Now we can determine the real start line of this element. + $this->getStartLine($parent, $file); + + // Function name. + $prefix = $file->claimNextIf($this, 0, "&") ? '&' : ''; + $name = $file->claimNextExpecting($this, T_STRING); + $this->name = $prefix . (isset($this->class) ? $this->class . '::' : '') . $name->source; + + // Arguments. + $file->claimNextExpecting($this, 0, "("); + $this->children[] = $this->arguments = new PHPArguments($this, $file); + $file->claimNextExpecting($this, 0, ")"); + + if ($file->claimNextIf($this, 0, "{")) { + $this->push(new PHPCodeBlock($this, $file)); + $file->claimNextExpecting($this, 0, "}"); + } + else { + $file->claimNextExpecting($this, 0, ";"); + } + } + +} diff --git parser/element.global.inc parser/element.global.inc new file mode 100644 index 0000000..66c864a --- /dev/null +++ parser/element.global.inc @@ -0,0 +1,39 @@ +claimNextExpecting($this, T_VARIABLE); + $this->name = $variable->source; + + // Backtrace parent tokens to find documentation. + while ($token = $parent->pop()) { + if (!in_array($token->type, array(T_DOC_COMMENT, T_WHITESPACE))) { + // Not one of the token we want, let the parent have it. + $parent->push($token); + break; + } + if ($token->type == T_DOC_COMMENT && empty($this->documentation)) { + // Only the first T_DOC_COMMENT (the closest one to the function) will be taken into account. + $this->documentation = $token; + } + $this->unshift($token); + } + + // Now we can determine the real start line of this element. + $this->getStartLine($parent, $file); + + while ($file->claimNext($this)->source !== ';') { + // Get all other tokens until the next statement. + } + } +} diff --git parser/element.inc parser/element.inc new file mode 100644 index 0000000..1e9ae16 --- /dev/null +++ parser/element.inc @@ -0,0 +1,39 @@ +getStartLine($parent, $file); + if (!$this->line) { + print_r($this); + die(); + } + + while ($file->hasTokens()) { + if ($file->claimNextIf($this, 0, '{')) { + $this->push(new PHPCodeBlock($this, $file)); + $file->claimNextExpecting($this, 0, '}'); + } + else if ($file->nextTokenMatch($this, 0, '}')) { + break; + } + else if ($file->nextTokenMatch($this, T_CLASS)) { + $this->push(new PHPClass($this, $file)); + } + else if ($file->nextTokenMatch($this, T_FUNCTION)) { + $this->push(new PHPFunction($this, $file)); + } + /* + else if ($file->nextTokenMatch($this, T_VARIABLE)) { + $this->push(new PHPGlobal($this, $file)); + } + */ + else { + $file->claimNext($this); + } + } + } +} diff --git parser/file.inc parser/file.inc new file mode 100644 index 0000000..59f7581 --- /dev/null +++ parser/file.inc @@ -0,0 +1,94 @@ +file_name = $file_name; + $this->name = basename($file_name); + + $code = file_get_contents($file_path); + $tokens = token_get_all($code); + + $line = 0; + foreach($tokens as $k => $token) { + if (is_string($token)) { + $token = new PHPToken(0, $token, $line); + } + else { + list($type, $source, $line) = $token; + if ($type == T_DOC_COMMENT) { + $token = new PHPDocumentationBlock($type, $source, $line); + } + else { + $token = new PHPToken($type, $source, $line); + } + } + $tokens[$k] = $token; + } + $this->tokens = $tokens; + } + + /** + * Skip any whitespace tokens. + */ + public function skipWhiteSpace(AbstractPHPCodeElement $to) { + while (isset($this->tokens[0]) && $this->tokens[0]->type == T_WHITESPACE) { + $to->push(array_shift($this->tokens)); + } + } + + /** + * Check if the next token match the indicated parameters. + */ + public function nextTokenMatch(AbstractPHPCodeElement $to, $type, $source = NULL) { + $this->skipWhiteSpace($to); + return isset($this->tokens[0]) && $this->tokens[0]->type == $type && (!$source || $this->tokens[0]->source == $source); + } + + /** + * Claim the next token. + */ + public function claimNext(AbstractPHPCodeElement $to) { + return $to->push(array_shift($this->tokens)); + } + + /** + * Claim the next token if it matches the indicated parameters. + */ + public function claimNextIf(AbstractPHPCodeElement $to, $type, $source = NULL) { + $this->skipWhiteSpace($to); + if ($this->nextTokenMatch($to, $type, $source)) { + return $this->claimNext($to); + } + } + + /** + * Claim the next token, and fails if it does not matches the indicated parameters. + */ + public function claimNextExpecting(AbstractPHPCodeElement $to, $type, $source = NULL) { + if (!($return = $this->claimNextIf($to, $type, $source))) { + if (isset($this->tokens[0])) { + throw new Exception("Parse error, unexpected token " . $this->tokens[0]->token_name . "(" . $this->tokens[0]->source . "), on line " . $this->tokens[0]->line . "."); + } + else { + throw new Exception("Parse error, unexpected end of file."); + } + } + return $return; + } + + /** + * Give up that token. + */ + public function giveUp(AbstractPHPCodeElement $token) { + array_unshift($this->tokens, $token); + } + + /** + */ + public function hasTokens() { + return !empty($this->tokens); + } +} diff --git parser/token.inc parser/token.inc new file mode 100644 index 0000000..4bf481e --- /dev/null +++ parser/token.inc @@ -0,0 +1,201 @@ +class)) { + $this->class = $parent->class; + } + } + + public function push($child) { + $this->children[] = $child; + return $child; + } + + public function pop() { + return array_pop($this->children); + } + + public function shift() { + return array_shift($this->children); + } + + public function unshift($child) { + array_unshift($this->children, $child); + } + + /** + * Split the whitespace between that element and its parent. + */ + public function splitWhitespace(AbstractPHPCodeElement $parent = NULL, PHPSourceFile $file) { + if (empty($this->children)) { + // If the element has no children yet, try to get whitespace from the token stream. + $file->claimNextIf($this, T_WHITESPACE); + } + + // Treat the whitespace if there is one. + if ($token = $this->shift()) { + if ($token->type == T_WHITESPACE) { + $last_line_break = strrpos($token->source, "\n"); + if ($last_line_break !== FALSE) { + // We let the parent have the first part of the whitespace, including the line break. + $parent_whitespace = substr($token->source, 0, $last_line_break + 1); + $our_whitespace = substr($token->source, $last_line_break + 1); + + // Push a part of the whitespace at the end of the parent. + if (strlen($parent_whitespace) > 0) { + $parent->push(new PHPToken(T_WHITESPACE, $parent_whitespace, $token->line)); + } + // And the other at the beginning of that element. + if (strlen($our_whitespace) > 0) { + $this->unshift(new PHPToken(T_WHITESPACE, $our_whitespace, $token->line + substr_count($parent_whitespace, "\n"))); + } + } + } + else { + $this->unshift($token); + } + } + } + + /** + * Determine the start line of this element. + */ + public function getStartLine(AbstractPHPCodeElement $parent = NULL, PHPSourceFile $file) { + // First, split the whitespace between this element and its parent. + if ($parent) { + $this->splitWhitespace($parent, $file); + } + + if (!empty($this->children)) { + // If the element has children, the start line is the first non whitespace + // and non comment children. + foreach ($this->children as $element) { + if (!isset($element->type) || ($element->type !== T_WHITESPACE && $element->type !== T_COMMENT && $element->type !== T_DOC_COMMENT)) { + $this->line = $element->line; + break; + } + } + } + + if (!$this->line) { + // Else, the start line is the first non-whitespace and non-comment token + // from the token stream. + foreach ($file->tokens as $token) { + if (!isset($token->type) || ($token->type !== T_WHITESPACE && $token->type !== T_COMMENT && $token->type !== T_DOC_COMMENT)) { + $this->line = $token->line; + break; + } + } + } + } + + public function __toString() { + $output = isset($this->source) ? $this->source : ''; + foreach ($this->children as $child) { + $output .= (string) $child; + } + return $output; + } +} + +class PHPToken extends AbstractPHPCodeElement { + public $type = 0; + + public function __construct($type, $source, $line) { + $this->type = $type; + $this->token_name = token_name($type); + $this->source = $source; + $this->line = $line; + } +} + +class PHPDocumentationBlock extends PHPToken { + public $object_type; + public $raw_source; + public $elements; + + public $group_definition = FALSE; + + function __construct($type, $source, $line) { + parent::__construct($type, $source, $line); + + if (strpos($source, '@defgroup') !== FALSE) { + if (preg_match('!@defgroup ([a-zA-Z0-9_.-]+) +(.*?)\n!', $source, $group_matches)) { + list (, $this->name, $this->title) = $group_matches; + $this->body = preg_replace('!@defgroup.*?\n!', '', $source); + $this->group_definition = TRUE; + return; + } + } + + // Save the raw source. + $this->raw_source = $this->source; + + // Parse lines and strip stars. + $this->lines = array_map(array($this, 'trimStars'), explode("\n", $this->source)); + + // Remove first and last lines if empty. + if (empty($this->lines[0])) { + unset($this->lines[0]); + } + $last_line = array_pop($this->lines); + if (!empty($last_line)) { + $this->lines[] = $last_line; + } + + // Title is all the text up to the first new line. + $title = array(); + while ($current_line = array_shift($this->lines)) { + $title[] = $current_line; + } + $this->title = implode(' ', $title); + + // Documentation itself. + $this->body = implode("\n", $this->lines); + } + + protected function trimStars($string) { + return preg_replace('@^\s*(/\*\*|\*/?)\s*@', '', $string); + } +} + +