$file_name, 'branch' => $branch, 'object_type' => 'file', 'file_name' => $file_name, 'title' => strpos($file_name, '/') ? substr($file_name, strrpos($file_name, '/') + 1) : $file_name, 'documentation' => '', 'version' => '', 'modified' => filemtime($file_path), 'source' => str_replace(array("\r\n", "\r"), array("\n", "\n"), file_get_contents($file_path)), 'content' => '', 'class' => '', ); $match = array(); if (preg_match('!\$'.'Id: .*?,v (.*?) (.*?) (.*?) (.*?) Exp \$!', $docblock['source'], $match)) { $docblock['version'] = $match[1] .' (checked in on '. $match[2] .' at '. $match[3] .' by '. $match[4] .')'; } $callback($docblock); } /** * Saves contents of a file as a single piece of text documentation. * * Callback for api_parse_file(). * * @param $docblock * Array from api_parse_file() containing the file contents and information * about the file, branch, etc. */ Function api_parse_text_file($docblock) { $docblock['documentation'] = api_format_documentation($docblock['source']); $docblock['code'] = api_format_php($docblock['source']); api_save_documentation(array($docblock)); } /** * Saves contents of a file as a single piece of HTML documentation. * * Escapes any HTML special characters in the text, so that it can be * displayed to show the HTML tags. * * Callback for api_parse_file(). * * @param $docblock * Array from api_parse_file() containing the file contents and information * about the file, branch, etc. */ function api_parse_html_file($docblock) { $docblock['code'] = '
' . htmlspecialchars($docblock['source']) . ''; $title_match = array(); if (preg_match('!
, , tags. // We don't apply any processing to the contents of these tags to avoid messing // up code. We look for matched pairs and allow basic nesting. For example: // "processedignored ignoredprocessed" $chunks = preg_split('@(?(?:pre|script|style|object)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE); // Note: PHP ensures the array consists of alternating delimiters and literals // and begins and ends with a literal (inserting NULL as required). $ignore = FALSE; $ignoretag = ''; $output = ''; foreach ($chunks as $i => $chunk) { if ($i % 2) { // Opening or closing tag? $open = ($chunk[1] != '/'); list($tag) = preg_split('/[ >]/', substr($chunk, 2 - $open), 2); if (!$ignore) { if ($open) { $ignore = TRUE; $ignoretag = $tag; } } // Only allow a matching tag to close it. elseif (!$open && $ignoretag == $tag) { $ignore = FALSE; $ignoretag = ''; } } elseif (!$ignore) { $chunk = api_format_documentation_lists($chunk); $chunk = preg_replace('|\n*$|', '', $chunk) ."\n\n"; // just to make things a little easier, pad the end $chunk = preg_replace('|
\s*
|', "\n\n", $chunk); $chunk = preg_replace('!(<'. $block .'[^>]*>)!', "\n$1", $chunk); // Space things out a little $chunk = preg_replace('!('. $block .'>)!', "$1\n\n", $chunk); // Space things out a little $chunk = preg_replace("/\n\n+/", "\n\n", $chunk); // take care of duplicates $chunk = preg_replace('/\n?(.+?)(?:\n\s*\n|\z)/s', "$1
\n", $chunk); // make paragraphs, including one at the end $chunk = preg_replace('|\s*
\n|', '', $chunk); // under certain strange conditions it could create a P of entirely whitespace $chunk = preg_replace("|(
]*)>|i', "', $chunk); $chunk = preg_replace('!', '", $chunk); $chunk = str_replace('
\s*(?'. $block .'[^>]*>)!', "$1", $chunk); $chunk = preg_replace('!(?'. $block .'[^>]*>)\s*
!', "$1", $chunk); $chunk = preg_replace('/&([^#])(?![A-Za-z0-9]{1,8};)/', '&$1', $chunk); } $output .= $chunk; } return $output; } /** * Regular expression callback for @code in api_format_documentation(). */ function api_format_embedded_php($matches) { return "\n\n". api_format_php("") ."\n\n"; } /** * Formats documentation lists as HTML lists. * * Parses a block of text for lists that uses hyphens or asterisks as bullets, * and format the lists as proper HTML lists. */ function api_format_documentation_lists($documentation) { $lines = explode("\n", $documentation); $output = ''; $bullet_indents = array(-1); foreach ($lines as $line) { preg_match('!^( *)([*-] )?(.*)$!', $line, $matches); $indent = strlen($matches[1]); $bullet_exists = $matches[2]; if ($indent < $bullet_indents[0]) { // First close off any lists that have completed. while ($indent < $bullet_indents[0]) { array_shift($bullet_indents); $output .= ''; } } if ($indent == $bullet_indents[0]) { if ($bullet_exists) { // A new bullet at the same indent means a new list item. $output .= ''. trim($output) .'
';
}
/**
* Keeps track of references while parsing API files.
*
* Since we may parse a file containing a reference before we have parsed the
* file containing the referenced object, we keep track of the references
* using a scratch table and save the references to the database table after the
* referenced object has been parsed.
*
* @param $branch
* Branch the reference is in.
* @param $to_type
* Type of object being referenced.
* @param $to_name
* Name of object being referenced.
* @param $from_did
* Documentation ID of the object that references this object.
*/
function api_reference($branch, $to_type, $to_name, $from_did) {
static $is_php_function = array();
if ($to_type == 'function' && !isset($is_php_function[$to_name])) {
$is_php_function[$to_name] = (db_result(db_query_range("SELECT 1 FROM {api_documentation} d INNER JOIN {api_branch} b ON b.branch_id = d.branch_id AND b.type = 'php' WHERE d.object_name = '%s'", $to_name, 1, 1)));
}
if ($to_type != 'function' || !$is_php_function[$to_name]) {
db_query("INSERT INTO {api_reference_storage} (object_name, branch_id, object_type, from_did) VALUES ('%s', '%s', '%s', %d)", $to_name, $branch->branch_id, $to_type, $from_did);
}
}
/**
* Registers a shutdown function for cron, making sure to do it just once.
*
* @see api_shutdown()
*/
function api_schedule_shutdown() {
static $scheduled = FALSE;
if (!$scheduled) {
register_shutdown_function('api_shutdown');
$scheduled = TRUE;
}
}
/**
* Cleans up at the end of the cron job.
*
* Updates the collected references, updates the JSON object list, and clears
* the cache.
*/
function api_shutdown() {
// Figure out all the dids of the object/branch/types.
db_query('UPDATE {api_reference_storage} r INNER JOIN {api_documentation} d ON r.object_name = d.object_name AND r.branch_id = d.branch_id AND r.object_type = d.object_type SET r.to_did = d.did');
// Save overrides
$changed_classes = array();
$result = db_query("SELECT ad.did, ad.object_type, ad.member_name, ad.class_did, ad.documentation, af.parameters, af.return_value, ad.see, ad.throws, ad.var FROM {api_documentation} ad LEFT JOIN {api_overrides} ao ON ao.did = ad.did LEFT JOIN {api_function} af ON af.did = ad.did WHERE ad.class_did <> 0 AND ao.did IS NULL");
while ($object = db_fetch_object($result)) {
$changed_classes[$object->class_did] = TRUE;
$override = array(
'did' => $object->did,
'overrides_did' => 0,
'documented_did' => api_has_documentation($object) ? $object->did : 0,
'root_did' => $object->did,
);
$overrides_did = 0;
$parents = array($object->class_did);
while ($parent = array_shift($parents)) {
$result_parents = db_query("SELECT ad.did, ars.to_did, ad.summary, ad.documentation, af.parameters, af.return_value, ad.see, ad.throws, ad.var, ad.class_did FROM {api_reference_storage} ars LEFT JOIN {api_documentation} ad ON ad.class_did = ars.to_did AND ad.object_type = '%s' AND ad.member_name = '%s' LEFT JOIN {api_function} af ON af.did = ad.did WHERE ars.from_did = %d AND ars.object_type IN ('class', 'interface')", $object->object_type, $object->member_name, $parent);
while ($parent_class = db_fetch_object($result_parents)) {
$parents[] = $parent_class->to_did;
if (!empty($parent_class->class_did)) {
$changed_classes[$parent_class->class_did] = TRUE;
}
if (!is_null($parent_class->did)) { // If documented
if ($override['overrides_did'] === 0) {
$override['overrides_did'] = $parent_class->did;
}
if ($override['documented_did'] === 0 && api_has_documentation($parent_class)) {
$override['documented_did'] = $parent_class->did;
// Save the inherited summary
$inherited_summary = array(
'summary' => $parent_class->summary,
'did' => $object->did,
);
drupal_write_record('api_documentation', $inherited_summary, 'did');
}
$override['root_did'] = $parent_class->did;
}
}
}
drupal_write_record('api_overrides', $override);
}
// Save members
while (list($class_did,) = each($changed_classes)) {
// Add child classes to $changed_classes
$result_chidren = db_query("SELECT ars.from_did FROM {api_reference_storage} ars WHERE ars.to_did = %d AND ars.object_type IN ('class', 'interface') AND ars.from_did <> 0", $class_did);
while ($child = db_fetch_object($result_chidren)) {
$changed_classes[$child->from_did] = TRUE;
}
// Walk up the tree to find members
$members = array('');
$parents = array($class_did);
while ($parent = array_shift($parents)) {
$result_parents = db_query("SELECT ars.to_did FROM {api_reference_storage} ars WHERE ars.from_did = %d AND ars.object_type IN ('class', 'interface') AND ars.to_did <> 0", $parent);
while ($object = db_fetch_object($result_parents)) {
$parents[] = $object->to_did;
}
$result_members = db_query("SELECT did, member_name FROM {api_documentation} WHERE class_did = %d AND member_name NOT IN (" . db_placeholders($members, 'text') . ")", array_merge(array($parent), array_keys($members)));
while ($member = db_fetch_object($result_members)) {
$members[$member->member_name] = $member->did;
}
}
// Save them
array_shift($members);
db_query("DELETE FROM {api_members} WHERE class_did = %d", $class_did);
foreach ($members as $did) {
$api_member = array(
'class_did' => $class_did,
'did' => $did,
);
drupal_write_record('api_members', $api_member);
}
}
// Save JSON autocomplete cache
$directory = file_directory_path();
if (is_dir($directory) && is_writable($directory) && (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC)) {
$path = file_create_path('api');
file_check_directory($path, FILE_CREATE_DIRECTORY);
$date = gmdate('U');
foreach (api_get_branch_names() as $branch_name) {
$new_json = api_autocomplete($branch_name, FALSE);
$old_file_path = variable_get('api_autocomplete_path_' . $branch_name, FALSE);
if ($old_file_path !== FALSE) {
if (md5($new_json) === md5(file_get_contents($old_file_path))) {
continue; // No changes, no file write.
}
// Delete in the future, help avoid race conditions.
job_queue_add('file_delete', t('Remove expired API JSON, %path.'), array('%path' => $old_file_path));
}
$file_name = $path . '/api-' . $branch_name . '-' . $date . '.json';
file_save_data($new_json, $file_name, FILE_EXISTS_REPLACE);
variable_set('api_autocomplete_path_' . $branch_name, $file_name);
}
}
cache_clear_all();
}
function api_has_documentation($object) {
foreach (array('documentation', 'parameters', 'return_value', 'see', 'throws', 'var') as $member) {
if (!empty($object->$member)) {
return TRUE;
}
}
return FALSE;
}
/**
* Updates all branches, by calling their update functions.
*
* @see api_update_branch_php()
* @see api_update_branch_files()
*/
function api_update_all_branches() {
foreach (api_get_branches() as $branch) {
$function = 'api_update_branch_' . $branch->type;
$function($branch);
watchdog('api', 'Updated %project branch %branch_name.', array('%branch_name' => $branch->branch_name, '%project' => $branch->project));
}
}
/**
* Updates a PHP branch.
*
* Queries the branch URL to get an updated list of functions, and saves each
* function in the database.
*
* @see api_update_all_branches()
*/
function api_update_branch_php($branch) {
$response = drupal_http_request($branch->summary);
if ($response->code === '200') {
$docblocks = array();
preg_match_all('!^[a-zA-Z0-9_]+ ([a-zA-Z0-9_]+)\(.*\n.*$!m', $response->data, $function_matches, PREG_SET_ORDER);
foreach ($function_matches as $function_match) {
$docblocks[] = array(
'branch' => $branch,
'file_name' => $branch->summary,
'object_type' => 'function',
'object_name' => $function_match[1],
'title' => $function_match[1],
'documentation' => $function_match[0],
'content' => '',
'class' => '',
);
}
api_save_documentation($docblocks);
}
}
/**
* Updates a files branch.
*
* Checks the current directories included in the branch to make an updated
* list of files. Removes documentation from files that no longer exist, adds
* documentation from new files, and updates documentation for any files that
* have changed.
*
* @see api_update_all_branches()
*/
function api_update_branch_files($branch) {
static $parse_functions = array(
'php' => 'api_parse_php_file',
'module' => 'api_parse_php_file',
'inc' => 'api_parse_php_file',
'install' => 'api_parse_php_file',
'engine' => 'api_parse_php_file',
'theme' => 'api_parse_php_file',
'profile' => 'api_parse_php_file',
'txt' => 'api_parse_text_file',
'htm' => 'api_parse_html_file',
'html' => 'api_parse_html_file',
);
// List all documented files for the branch.
$files = array();
$result = db_query("SELECT f.did, f.modified, d.object_name FROM {api_documentation} d INNER JOIN {api_file} f ON d.did = f.did WHERE d.branch_id = %d AND d.object_type = 'file'", $branch->branch_id);
while ($file = db_fetch_object($result)) {
$files[$file->object_name] = $file;
}
foreach (api_scan_directories($branch->directories, $branch->excluded_directories) as $path => $file_name) {
preg_match('!\.([a-z]*)$!', $file_name, $matches);
if (isset($matches[1]) && isset($parse_functions[$matches[1]])) {
if (isset($files[$file_name])) {
$parse = (filemtime($path) > $files[$file_name]->modified);
unset($files[$file_name]); // All remaining files will be removed.
}
else { // New file.
$parse = TRUE;
}
if ($parse) {
job_queue_add('api_parse_file', t('API parse %branch %file'), array($parse_functions[$matches[1]], $path, $branch, '%file' => $file_name, '%branch' => $branch->branch_name), drupal_get_path('module', 'api') .'/parser.inc', TRUE);
}
}
}
// Remove outdated files.
foreach (array_keys($files) as $file_name) {
watchdog('api', 'Removing %file.', array('%file' => $file_name));
$result = db_query("SELECT ad.did FROM {api_documentation} ad WHERE ad.file_name = '%s' AND ad.branch_id = %d", $file_name, $branch->branch_id);
while ($doc = db_fetch_object($result)) {
db_query("DELETE FROM {api_documentation} WHERE did = %d", $doc->did);
db_query("DELETE FROM {api_file} WHERE did = %d", $doc->did);
db_query("DELETE FROM {api_function} WHERE did = %d", $doc->did);
db_query("DELETE FROM {api_reference_storage} WHERE from_did = %d OR to_did = %d", $doc->did, $doc->did);
}
api_schedule_shutdown();
}
}
/**
* Finds all the files in the directories specified for a branch.
*
* @param $directories
* List of directories to scan, as text (separated by newlines).
* @param $excluded_directories
* List of directories to exclude, as text (separated by newlines).
*
* @return
* Associative array of files, where the keys are the full paths to the
* files and the values are the file names.
*/
function api_scan_directories($directories, $excluded_directories) {
$directory_array = explode("\n", $directories);
$excluded_array = explode("\n", $excluded_directories);
if (count($directory_array) > 1) {
$directories_components = array();
foreach ($directory_array as $directory) {
$directory_components = array();
$parts = explode(DIRECTORY_SEPARATOR, $directory);
foreach ($parts as $part) {
if (strlen($part)) {
array_unshift($directory_components, reset($directory_components) . DIRECTORY_SEPARATOR . $part);
}
}
$directories_components[] = $directory_components;
}
$common_ancestor_components = call_user_func_array('array_intersect', $directories_components);
$common_ancestor = reset($common_ancestor_components);
}
else {
$common_ancestor = $directories;
}
$source_files = array();
foreach ($directory_array as $directory) {
$files = file_scan_directory($directory, '.*');
foreach ($files as $path => $file) {
if (strpos($path, '/.') !== FALSE) {
continue;
}
$excluded = FALSE;
// If the file is in an excluded path, ignore it
foreach ($excluded_array as $excluded_path) {
if (!empty($excluded_path) && (strpos($path, $excluded_path) === 0)) {
$excluded = TRUE;
}
}
if (!$excluded) {
$file_name = substr($path, strlen($common_ancestor) + 1);
$source_files[$path] = $file_name;
}
}
}
return $source_files;
}