PageRenderTime 60ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/parser.inc

https://github.com/Br3nda/drupal-module-api
PHP | 884 lines | 662 code | 119 blank | 103 comment | 94 complexity | 87ce6482493fad356ee2e88a48ccd133 MD5 | raw file
  1. <?php
  2. // $Id: parser.inc,v 1.41 2008/12/27 03:09:36 drumm Exp $
  3. /**
  4. * @file
  5. * The PHP documentation parser that generates content for api.module.
  6. */
  7. /**
  8. * Parse out function definitions from the PHP manual.
  9. */
  10. function api_parse_php_manual($location) {
  11. $response = drupal_http_request($location);
  12. if ($response->code == 200) {
  13. $function_matches = array();
  14. preg_match_all('!^[a-zA-Z0-9_]+ ([a-zA-Z0-9_]+)\(.*\n.*$!m', $response->data, $function_matches, PREG_SET_ORDER);
  15. foreach ($function_matches as $function_match) {
  16. $docblock = array(
  17. 'object_name' => $function_match[1],
  18. 'branch_name' => 'php',
  19. 'object_type' => 'function',
  20. 'file_name' => $location,
  21. 'title' => $function_match[1],
  22. 'summary' => api_documentation_summary($function_match[0]),
  23. 'documentation' => $function_match[0],
  24. 'code' => '',
  25. 'signature' => '',
  26. 'start_line' => 0,
  27. 'parameters' => '',
  28. 'return_value' => '',
  29. );
  30. api_save_documentation(array($docblock));
  31. }
  32. }
  33. }
  34. /**
  35. * Read in the file at the given path and parse it as if it consisted entirely of
  36. * documentation.
  37. */
  38. function api_parse_text_file($file_path, $branch_name, $file_name) {
  39. $source = file_get_contents($file_path);
  40. // Set up documentation block for file, in case it is not explicitly defined.
  41. $docblock = array(
  42. 'object_name' => $file_name,
  43. 'branch_name' => $branch_name,
  44. 'object_type' => 'file',
  45. 'file_name' => $file_name,
  46. 'title' => strpos($file_name, '/') ? substr($file_name, strrpos($file_name, '/') + 1) : $file_name,
  47. 'summary' => api_documentation_summary(api_format_documentation($source, $branch_name)),
  48. 'documentation' => api_format_documentation($source, $branch_name),
  49. 'code' => api_format_php($source, $file_path),
  50. 'version' => '',
  51. 'modified' => filemtime($file_path),
  52. );
  53. $version_match = array();
  54. if (preg_match('!\$'.'Id: .*?,v (.*?) (.*?) (.*?) (.*?) Exp \$!', $source, $version_match)) {
  55. $docblock['version'] = $version_match[1] .' (checked in on '. $version_match[2] .' at '. $version_match[3] .' by '. $version_match[4] .')';
  56. }
  57. api_save_documentation(array($docblock), $branch_name, $file_name);
  58. }
  59. /**
  60. * Read in the file at the given path and parse it as an HTML file.
  61. */
  62. function api_parse_html_file($file_path, $branch_name, $file_name) {
  63. $source = file_get_contents($file_path);
  64. // Set up documentation block for file, in case it is not explicitly defined.
  65. $docblock = array(
  66. 'object_name' => $file_name,
  67. 'branch_name' => $branch_name,
  68. 'object_type' => 'file',
  69. 'file_name' => $file_name,
  70. 'title' => strpos($file_name, '/') ? substr($file_name, strrpos($file_name, '/') + 1) : $file_name,
  71. 'summary' => '',
  72. 'documentation' => '',
  73. 'code' => '<pre>'. htmlspecialchars($source) .'</pre>',
  74. 'version' => '',
  75. 'modified' => filemtime($file_path),
  76. );
  77. $title_match = array();
  78. if (preg_match('!<title>(.*)</title>!is', $source, $title_match)) {
  79. $docblock['title'] = $title_match[1];
  80. }
  81. $documentation_match = array();
  82. if (preg_match('!<body>(.*?</h1>)?(.*)</body>!is', $source, $documentation_match)) {
  83. $docblock['documentation'] = $documentation_match[2];
  84. $docblock['summary'] = api_documentation_summary($documentation_match[2]);
  85. }
  86. $version_match = array();
  87. if (preg_match('!\$'.'Id: .*?,v (.*?) (.*?) (.*?) (.*?) Exp \$!', $source, $version_match)) {
  88. $docblock['version'] = $version_match[1] .' (checked in on '. $version_match[2] .' at '. $version_match[3] .' by '. $version_match[4] .')';
  89. }
  90. api_save_documentation(array($docblock), $branch_name, $file_name);
  91. }
  92. /**
  93. * Read in the file at the given path and parse its documentation.
  94. */
  95. function api_parse_php_file($file_path, $branch_name, $file_name) {
  96. $source = file_get_contents($file_path);
  97. // Convert Mac/Win line breaks to Unix format.
  98. $source = str_replace("\r\n", "\n", $source);
  99. $source = str_replace("\r", "\n", $source);
  100. $docblocks = array();
  101. // Set up documentation block for file, in case it is not explicitly defined.
  102. $docblocks[0] = array(
  103. 'object_name' => $file_name,
  104. 'branch_name' => $branch_name,
  105. 'object_type' => 'file',
  106. 'file_name' => $file_name,
  107. 'title' => strpos($file_name, '/') ? substr($file_name, strrpos($file_name, '/') + 1) : $file_name,
  108. 'summary' => '',
  109. 'documentation' => '',
  110. 'code' => api_format_php($source, $file_path),
  111. 'version' => '',
  112. 'modified' => filemtime($file_path),
  113. );
  114. $version_match = array();
  115. if (preg_match('!\$'.'Id: .*?,v (.*?) (.*?) (.*?) (.*?) Exp \$!', $source, $version_match)) {
  116. $docblocks[0]['version'] = $version_match[1] .' (checked in on '. $version_match[2] .' at '. $version_match[3] .' by '. $version_match[4] .')';
  117. }
  118. $nested_groups = array();
  119. $docblock_matches = array();
  120. preg_match_all('!/\*\*(.*?)\*/!s', $source, $docblock_matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
  121. foreach ($docblock_matches as $docblock_match) {
  122. $docblock = array(
  123. 'object_name' => '',
  124. 'branch_name' => $branch_name,
  125. 'object_type' => '',
  126. 'file_name' => $file_name,
  127. 'title' => '',
  128. 'summary' => '',
  129. 'documentation' => '',
  130. 'code' => '',
  131. );
  132. $docblock['content'] = str_replace(array("\n *", "\n "), array("\n", "\n"), $docblock_match[1][0]);
  133. $docblock['start'] = $docblock_match[0][1];
  134. $docblock['length'] = strlen($docblock_match[0][0]);
  135. $code_start = $docblock['start'] + $docblock['length'] + 1;
  136. $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n");
  137. // Determine what kind of documentation block this is.
  138. if (substr($source, $code_start, 8) == 'function') {
  139. $function_matches = array();
  140. $docblock['object_type'] = 'function';
  141. preg_match('!^function (&?([a-zA-Z0-9_]+)\(.*?)\s*\{!', substr($source, $code_start), $function_matches);
  142. $docblock['object_name'] = (isset($function_matches[2]) ? $function_matches[2] : '');
  143. $docblock['title'] = (isset($function_matches[2]) ? $function_matches[2] : '');
  144. $docblock['signature'] = (isset($function_matches[1]) ? $function_matches[1] : '');
  145. // We rely on the Drupal coding convention that functions are closed in column 1.
  146. $code_end = strpos($source, "\n}", $code_start) + 2;
  147. $docblock['code'] = substr($source, $code_start, $code_end - $code_start);
  148. $docblock['code'] = api_format_php("<?php\n". $docblock['code'] ."\n?>");
  149. // Find parameter definitions.
  150. $param_match = array();
  151. $offset = 0;
  152. $docblock['parameters'] = '';
  153. while (preg_match('!@param(.*?)(?=\n@|\n\n|$)!s', substr($docblock['content'], $offset), $param_match, PREG_OFFSET_CAPTURE)) {
  154. $docblock['content'] = str_replace($param_match[0][0], '', $docblock['content']);
  155. $docblock['parameters'] .= "\n\n". $param_match[1][0];
  156. $offset = $param_match[0][1];
  157. }
  158. $docblock['parameters'] = api_format_documentation($docblock['parameters'], $branch_name);
  159. // Find return value definitions.
  160. $return_matches = array();
  161. $docblock['return_value'] = '';
  162. preg_match_all('!@return(.*?)(\n@|\n\n|$)!s', $docblock['content'], $return_matches, PREG_SET_ORDER);
  163. foreach ($return_matches as $return_match) {
  164. $docblock['content'] = str_replace($return_match[0], '', $docblock['content']);
  165. $docblock['return_value'] .= "\n\n". $return_match[1];
  166. }
  167. $docblock['return_value'] = api_format_documentation($docblock['return_value'], $branch_name);
  168. $docblock['function calls'] = api_parse_function_calls($docblock['code']);
  169. // Determine group membership.
  170. $group_matches = array();
  171. preg_match_all('!@(ingroup|addtogroup) ([a-zA-Z0-9_]+)!', $docblock['content'], $group_matches);
  172. //var_dump($group_matches);
  173. $docblock['groups'] = $group_matches[2];
  174. $docblock['content'] = preg_replace('!@ingroup.*?\n!', '', $docblock['content']);
  175. foreach ($nested_groups as $group_id) {
  176. if (!empty($group_id)) {
  177. $docblock['groups'][] = $group_id;
  178. }
  179. }
  180. $methods = api_parse_class_methods($source);
  181. }
  182. elseif (substr($source, $code_start, 5) == 'class') {
  183. $class_matches = array ();
  184. preg_match('!^class ([a-zA-Z0-9_]+) extends [a-zA-Z0-9_]+ {!i', substr($source, $code_start), $class_matches);
  185. if (!count($class_matches)) {
  186. preg_match('!^class ([a-zA-Z0-9_]+) {!i', substr($source, $code_start), $class_matches);
  187. }
  188. $docblock['object_type'] = 'class';
  189. $docblock['object_name'] = $class_matches[1];
  190. $docblock['title'] = $class_matches[1];
  191. $code_end = strpos($source, "\n}", $code_start) + 2;
  192. $docblock['code'] = substr($source, $code_start, $code_end - $code_start);
  193. $docblock['code'] = api_format_php("<?php\n" . $docblock['code'] . "\n\?\>");
  194. $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n");
  195. foreach ($nested_groups as $group_id) {
  196. if (!empty ($group_id)) {
  197. $docblock['groups'][] = $group_id;
  198. }
  199. }
  200. }
  201. else if (substr($source, $code_start, 6) == 'define') {
  202. $constant_matches = array();
  203. $docblock['object_type'] = 'constant';
  204. preg_match('!^define\([\'"]([a-zA-Z0-9_]+)[\'"]!', substr($source, $code_start), $constant_matches);
  205. $docblock['object_name'] = $constant_matches[1];
  206. $docblock['title'] = $constant_matches[1];
  207. $code_end = strpos($source, ';', $code_start) + 1;
  208. $docblock['code'] = substr($source, $code_start, $code_end - $code_start);
  209. $docblock['code'] = api_format_php("<?php\n". $docblock['code'] ."\n?>");
  210. // Determine group membership.
  211. $group_matches = array();
  212. preg_match_all('!@(ingroup|addtogroup) ([a-zA-Z0-9_]+)!', $docblock['content'], $group_matches);
  213. $docblock['groups'] = $group_matches[2];
  214. $docblock['content'] = preg_replace('!@ingroup.*?\n!', '', $docblock['content']);
  215. foreach ($nested_groups as $group_id) {
  216. if (!empty($group_id)) {
  217. $docblock['groups'][] = $group_id;
  218. }
  219. }
  220. }
  221. else if (substr($source, $code_start, 6) == 'global') {
  222. $global_matches = array();
  223. $docblock['object_type'] = 'global';
  224. preg_match('!^global (\$[a-zA-Z0-9_]+)!', substr($source, $code_start), $global_matches);
  225. $docblock['object_name'] = substr($global_matches[1], 1);
  226. $docblock['title'] = $global_matches[1];
  227. $code_end = strpos($source, ';', $code_start) + 1;
  228. $docblock['code'] = substr($source, $code_start, $code_end - $code_start);
  229. $docblock['code'] = api_format_php("<?php\n". $docblock['code'] ."\n?>");
  230. $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n");
  231. // Determine group membership.
  232. $group_matches = array();
  233. preg_match_all('!@(ingroup|addtogroup) ([a-zA-Z0-9_]+)!', $docblock['content'], $group_matches);
  234. $docblock['groups'] = $group_matches[2];
  235. $docblock['content'] = preg_replace('!@ingroup.*?\n!', '', $docblock['content']);
  236. foreach ($nested_groups as $group_id) {
  237. if (!empty($group_id)) {
  238. $docblock['groups'][] = $group_id;
  239. }
  240. }
  241. }
  242. else if (strpos($docblock['content'], '@mainpage') !== FALSE) {
  243. $mainpage_matches = array();
  244. preg_match('!@mainpage (.*?)\n!', $docblock['content'], $mainpage_matches);
  245. $docblock['title'] = $mainpage_matches[1];
  246. $docblock['content'] = preg_replace('!@mainpage.*?\n!', '', $docblock['content']);
  247. $docblock['object_type'] = 'mainpage';
  248. $docblock['object_name'] = $branch_name;
  249. }
  250. else if (strpos($docblock['content'], '@file') !== FALSE) {
  251. $docblocks[0]['content'] = str_replace('@file', '', $docblock['content']);
  252. $docblocks[0]['documentation'] = api_format_documentation($docblocks[0]['content'], $branch_name);
  253. $docblocks[0]['summary'] = api_documentation_summary($docblocks[0]['documentation']);
  254. }
  255. else if (strpos($docblock['content'], '@defgroup') !== FALSE) {
  256. $group_matches = array();
  257. if (preg_match('!@defgroup ([a-zA-Z0-9_.-]+) +(.*?)\n!', $docblock['content'], $group_matches)) {
  258. $docblock['object_name'] = $group_matches[1];
  259. $docblock['title'] = $group_matches[2];
  260. $docblock['content'] = preg_replace('!@defgroup.*?\n!', '', $docblock['content']);
  261. $docblock['object_type'] = 'group';
  262. }
  263. else {
  264. watchdog('api', 'Malformed @defgroup in %file at line %line.', array('%file' => $file_path, '%line' => $docblock['start_line']), WATCHDOG_NOTICE);
  265. }
  266. }
  267. // Handle nested function groups.
  268. if (strpos($docblock['content'], '@{') !== FALSE) {
  269. if ($docblock['object_type'] == 'group') {
  270. array_push($nested_groups, $docblock['object_name']);
  271. }
  272. else {
  273. $group_matches = array();
  274. if (preg_match('!@(ingroup|addtogroup) ([a-zA-Z0-9_]+)!', $docblock['content'], $group_matches)) {
  275. array_push($nested_groups, $group_matches[2]);
  276. }
  277. else {
  278. array_push($nested_groups, '');
  279. }
  280. }
  281. }
  282. if (strpos($docblock['content'], '@}') !== FALSE) {
  283. array_pop($nested_groups);
  284. }
  285. if ($docblock['object_type'] != '') {
  286. $docblock['documentation'] = api_format_documentation($docblock['content'], $branch_name);
  287. $docblock['summary'] = api_documentation_summary($docblock['documentation']);
  288. $docblocks[] = $docblock;
  289. }
  290. }
  291. // Find undocumented functions.
  292. $function_matches = array();
  293. preg_match_all('%(?<!\*/\n)^function (&?([a-zA-Z0-9_]+)\(.*?)\s*\{%sm', $source, $function_matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
  294. foreach ($function_matches as $function_match) {
  295. $docblock = array(
  296. 'object_name' => $function_match[2][0],
  297. 'branch_name' => $branch_name,
  298. 'object_type' => 'function',
  299. 'file_name' => $file_name,
  300. 'title' => $function_match[2][0],
  301. 'summary' => '',
  302. 'documentation' => '',
  303. 'code' => '');
  304. $docblock['signature'] = $function_match[1][0];
  305. $docblock['parameters'] = '';
  306. $docblock['return_value'] = '';
  307. $docblock['groups'] = array();
  308. $code_start = $function_match[0][1];
  309. $code_end = strpos($source, "\n}", $code_start) + 2;
  310. $docblock['code'] = substr($source, $code_start, $code_end - $code_start);
  311. $docblock['code'] = api_format_php("<?php\n". $docblock['code'] ."\n?>");
  312. $docblock['function calls'] = api_parse_function_calls($docblock['code']);
  313. $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n");
  314. $docblocks[] = $docblock;
  315. }
  316. // Find undocumented constants.
  317. $constant_matches = array();
  318. preg_match_all('%(?<!\*/\n)^define\([\'"]([a-zA-Z0-9_]+)[\'"]%sm', $source, $constant_matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
  319. foreach ($constant_matches as $constant_match) {
  320. $docblock = array(
  321. 'object_name' => $constant_match[1][0],
  322. 'branch_name' => $branch_name,
  323. 'object_type' => 'constant',
  324. 'file_name' => $file_name,
  325. 'title' => $constant_match[1][0],
  326. 'summary' => '',
  327. 'documentation' => '',
  328. 'code' => '');
  329. $docblock['groups'] = array();
  330. $code_start = $constant_match[0][1];
  331. $code_end = strpos($source, ';', $code_start) + 1;
  332. $docblock['code'] = substr($source, $code_start, $code_end - $code_start);
  333. $docblock['code'] = api_format_php("<?php\n". $docblock['code'] ."\n?>");
  334. $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n");
  335. $docblocks[] = $docblock;
  336. }
  337. // Find undocumented globals.
  338. $global_matches = array();
  339. preg_match_all('%(?<!\*/\n)^global (\$[a-zA-Z0-9_]+)%sm', $source, $global_matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
  340. foreach ($global_matches as $global_match) {
  341. $docblock = array(
  342. 'object_name' => $global_match[1][0],
  343. 'branch_name' => $branch_name,
  344. 'object_type' => 'global',
  345. 'file_name' => $file_name,
  346. 'title' => $global_match[1][0],
  347. 'summary' => '',
  348. 'documentation' => '',
  349. 'code' => '',
  350. );
  351. $docblock['groups'] = array();
  352. $code_start = $global_match[0][1];
  353. $code_end = strpos($source, ';', $code_start) + 1;
  354. $docblock['code'] = substr($source, $code_start, $code_end - $code_start);
  355. $docblock['code'] = api_format_php("<?php\n". $docblock['code'] ."\n?>");
  356. $docblock['start_line'] = substr_count(substr($source, 0, $code_start), "\n");
  357. $docblocks[] = $docblock;
  358. }
  359. api_save_documentation($docblocks, $branch_name, $file_name);
  360. }
  361. function api_parse_class_methods($source) {
  362. //var_dump($source);
  363. }
  364. /**
  365. * Find functions called in a formatted block of code.
  366. */
  367. function api_parse_function_calls($code) {
  368. $function_call_matches = array();
  369. $function_calls = array();
  370. preg_match_all('!<span class="php-function-or-constant">([a-zA-Z0-9_]+)</span>\(!', $code, $function_call_matches, PREG_SET_ORDER);
  371. array_shift($function_call_matches); // Remove the first match, the function declaration itself.
  372. foreach ($function_call_matches as $function_call_match) {
  373. $function_calls[$function_call_match[1]] = $function_call_match[1];
  374. }
  375. return $function_calls;
  376. }
  377. /**
  378. * Save a documentation block into the database.
  379. *
  380. * @param $docblocks
  381. * An array containing information about the documentation block.
  382. * @param $branch_name
  383. * If set, old documentation will be removed for the branch and file name.
  384. * @param $file_name
  385. * If set, old documentation will be removed for the branch and file name.
  386. * @return
  387. * The documentation ID of the inserted/updated construct.
  388. */
  389. function api_save_documentation($docblocks, $branch_name = NULL, $file_name = NULL) {
  390. if (!is_null($branch_name)) {
  391. $old_dids = array();
  392. $result = db_query("SELECT did FROM {api_documentation} WHERE branch_name = '%s' AND file_name = '%s'", $branch_name, $file_name);
  393. while ($object = db_fetch_object($result)) {
  394. $old_dids[] = $object->did;
  395. }
  396. }
  397. $dids = array();
  398. foreach ($docblocks as $docblock) {
  399. if (!drupal_validate_utf8($docblock['code'])) {
  400. watchdog('error', 'Invalid chars in !filename', array('!filename' => $docblock['file_name']));
  401. return;
  402. }
  403. //ensure our strings are
  404. $did = db_result(db_query("SELECT did FROM {api_documentation} WHERE object_name = '%s' AND branch_name = '%s' AND object_type = '%s'", $docblock['object_name'], $docblock['branch_name'], $docblock['object_type'], $docblock['file_name']));
  405. if ($did > 0) {
  406. db_query("UPDATE {api_documentation} SET title = '%s', file_name = '%s', summary = '%s', documentation = '%s', code = '%s' WHERE did = %d", $docblock['title'], $docblock['file_name'], $docblock['summary'], $docblock['documentation'], $docblock['code'], $did);
  407. }
  408. else {
  409. db_query("INSERT INTO {api_documentation} (object_name, branch_name, object_type, title, file_name, summary, documentation, code) " .
  410. "VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')",
  411. $docblock['object_name'], $docblock['branch_name'], $docblock['object_type'], $docblock['title'], $docblock['file_name'], $docblock['summary'], $docblock['documentation'], $docblock['code']);
  412. $did = db_last_insert_id('api_documentation', 'did');
  413. if (!$did) {
  414. drupal_set_message('Failed to save documentation', $did);
  415. }
  416. }
  417. switch ($docblock['object_type']) {
  418. case 'function':
  419. db_query('DELETE FROM {api_function} WHERE did = %d', $did);
  420. db_query("INSERT INTO {api_function} (did, signature, start_line, parameters, return_value) VALUES (%d, '%s', %d, '%s', '%s')", $did, $docblock['signature'], $docblock['start_line'], $docblock['parameters'], $docblock['return_value']);
  421. if (isset($docblock['function calls'])) {
  422. db_query("DELETE FROM {api_reference_storage} WHERE branch_name = '%s' AND object_type = 'function' AND from_did = %d", $docblock['branch_name'], $did);
  423. foreach ($docblock['function calls'] as $function_name) {
  424. api_reference($docblock['branch_name'], 'function', $function_name, $did);
  425. }
  426. }
  427. break;
  428. case 'file':
  429. db_query('DELETE FROM {api_file} WHERE did = %d', $did);
  430. db_query("INSERT INTO {api_file} (did, modified, version) VALUES (%d, %d, '%s')", $did, $docblock['modified'], $docblock['version']);
  431. break;
  432. }
  433. if (isset($docblock['groups'])) {
  434. db_query("DELETE FROM {api_reference_storage} WHERE branch_name = '%s' AND object_type = 'group' AND from_did = %d", $docblock['branch_name'], $did);
  435. foreach ($docblock['groups'] as $group_name) {
  436. api_reference($docblock['branch_name'], 'group', $group_name, $did);
  437. }
  438. }
  439. $dids[] = $did;
  440. }
  441. if (!is_null($branch_name)) {
  442. $old_dids = array_diff($old_dids, $dids);
  443. if (count($old_dids) > 0) {
  444. $old_dids = implode(',', $old_dids);
  445. db_query('DELETE FROM {api_documentation} WHERE did IN (%s)', $old_dids);
  446. db_query('DELETE FROM {api_file} WHERE did IN (%s)', $old_dids);
  447. db_query('DELETE FROM {api_function} WHERE did IN (%s)', $old_dids);
  448. db_query('DELETE FROM {api_reference_storage} WHERE from_did IN (%s) OR to_did IN (%s)', $old_dids, $old_dids);
  449. }
  450. }
  451. api_schedule_update_references();
  452. api_schedule_cache_clear();
  453. }
  454. /**
  455. * Format a documentation block as HTML.
  456. */
  457. function api_format_documentation($documentation, $branch_name) {
  458. // Don't do processing on empty text (so we don't end up with empty paragraphs).
  459. $documentation = trim($documentation);
  460. if (empty($documentation)) {
  461. return '';
  462. }
  463. $documentation = check_plain($documentation);
  464. // @link full URLs.
  465. $documentation = preg_replace('!@link ((http://|https://|ftp://|mailto:|smb://|afp://|file://|gopher://|news://|ssl://|sslv2://|sslv3://|tls://|tcp://|udp://)([a-zA-Z0-9@:%_+*~#?&=.,/;-]*[a-zA-Z0-9@:%_+*~#&=/;-])) (.*?) @endlink!', '<a href="$1">$4</a>', $documentation);
  466. // Site URLs.
  467. $documentation = preg_replace('!@link /([a-zA-Z0-9_/-]+) (.*?) @endlink!', str_replace('%24', '$', l('$2', '$1')), $documentation);
  468. //$documentation = preg_replace('!@link ([a-zA-Z0-9_]+) (.*?) @endlink!', str_replace('%24', '$', l('$2', 'api/group/$1/'. $branch_name)), $documentation);
  469. // Process the @see tag
  470. $documentation = preg_replace('!\n@see (.*[^.])\.?!', '<h3>See also</h3><p>$1</p>', $documentation);
  471. // Replace left over curly braces
  472. $documentation = preg_replace('!@[{}]!', '', $documentation);
  473. // Process the @code @endcode tags.
  474. $documentation = preg_replace_callback('!@code(.+?)@endcode!s', 'api_format_embedded_php', $documentation);
  475. // Format lists
  476. $documentation = api_format_documentation_lists($documentation);
  477. // Convert newlines into paragraphs.
  478. $documentation = preg_replace('|\n*$|', '', $documentation) ."\n\n"; // just to make things a little easier, pad the end
  479. $documentation = preg_replace('!\n?(.+?)(?:\n\s*\n|\z)!s', "<p>$1</p>\n", $documentation); // make paragraphs, including one at the end
  480. $documentation = preg_replace('!<p>\s*?</p>!', '', $documentation); // under certain strange conditions it could create a P of entirely whitespace
  481. return $documentation;
  482. }
  483. /**
  484. * Regexp replace callback for code tags.
  485. */
  486. function api_format_embedded_php($matches) {
  487. return "\n\n". api_format_php("<?php\n". decode_entities($matches[1]) ."\n?>") ."\n\n";
  488. }
  489. /*
  490. * Parse a block of text for lists that use hyphens or asterisks as bullets, and
  491. * format them as proper HTML lists.
  492. */
  493. function api_format_documentation_lists($documentation) {
  494. $lines = explode("\n", $documentation);
  495. $output = '';
  496. $bullet_indents = array(-1);
  497. foreach ($lines as $line) {
  498. $matches = array();
  499. preg_match('!^( *)([*-] )?(.*)$!', $line, $matches);
  500. $indent = strlen($matches[1]);
  501. $bullet_exists = $matches[2];
  502. if ($indent < $bullet_indents[0]) {
  503. // First close off any lists that have completed.
  504. while ($indent < $bullet_indents[0]) {
  505. array_shift($bullet_indents);
  506. $output .= '</li></ul>';
  507. }
  508. }
  509. if ($indent == $bullet_indents[0]) {
  510. if ($bullet_exists) {
  511. // A new bullet at the same indent means a new list item.
  512. $output .= '</li><li>';
  513. }
  514. else {
  515. // If the indent is the same, but there is no bullet, that also
  516. // signifies the end of the list.
  517. array_shift($bullet_indents);
  518. $output .= '</li></ul>';
  519. }
  520. }
  521. if ($indent > $bullet_indents[0] && $bullet_exists) {
  522. // A new list at a lower level.
  523. array_unshift($bullet_indents, $indent);
  524. $output .= '<ul><li>';
  525. }
  526. $output .= $matches[3] ."\n";
  527. }
  528. // Clean up any unclosed lists.
  529. array_pop($bullet_indents);
  530. foreach ($bullet_indents as $indent) {
  531. $output .= '</li></ul>';
  532. }
  533. return $output;
  534. }
  535. /**
  536. * Retrieve a summary from a documentation block.
  537. */
  538. function api_documentation_summary($documentation) {
  539. $pos = strpos($documentation, "</p>");
  540. if ($pos !== FALSE) {
  541. $documentation = substr($documentation, 0, $pos);
  542. }
  543. $documentation = trim(strip_tags($documentation));
  544. if (strlen($documentation) > 255) {
  545. return substr($documentation, 0, strrpos(substr($documentation, 0, 252), ' ')) .'...';
  546. }
  547. else {
  548. return $documentation;
  549. }
  550. }
  551. /**
  552. * Colorize and format a PHP script.
  553. */
  554. function api_format_php($code, $filename = null) {
  555. $output = '';
  556. if (!defined('T_ML_COMMENT')) {
  557. define('T_ML_COMMENT', T_COMMENT);
  558. }
  559. if (!defined('T_DOC_COMMENT')) {
  560. define('T_DOC_COMMENT', T_COMMENT);
  561. }
  562. if (!drupal_validate_utf8($code)) {
  563. drupal_set_message(t('%filename contains bad unicode', array('%filename' => $filename)));
  564. return ;
  565. }
  566. $tokens = token_get_all( utf8_decode($code));
  567. $in_string = FALSE;
  568. foreach ($tokens as $token) {
  569. if ($in_string) {
  570. if ($token == '"') {
  571. $output .= '"</span>';
  572. $in_string = FALSE;
  573. }
  574. else {
  575. $output .= is_array($token) ? htmlspecialchars($token[1]) : htmlspecialchars($token);
  576. }
  577. continue;
  578. }
  579. else if ($token == '"') {
  580. $output .= '<span class="php-string">"';
  581. $in_string = TRUE;
  582. continue;
  583. }
  584. if (is_array($token)) {
  585. $type = $token[0];
  586. $value = htmlspecialchars($token[1]);
  587. switch ($type) {
  588. case T_OPEN_TAG:
  589. case T_CLOSE_TAG:
  590. $output .= '<span class="php-boundry">'. $value .'</span>';
  591. break;
  592. case T_COMMENT:
  593. case T_ML_COMMENT:
  594. case T_DOC_COMMENT:
  595. $output .= '<span class="php-comment">'. $value .'</span>';
  596. break;
  597. case T_VARIABLE:
  598. $output .= '<span class="php-variable">'. $value .'</span>';
  599. break;
  600. case T_CONSTANT_ENCAPSED_STRING:
  601. case T_INLINE_HTML:
  602. $output .= '<span class="php-string">'. $value .'</span>';
  603. break;
  604. case T_STRING:
  605. $output .= '<span class="php-function-or-constant">'. $value .'</span>';
  606. break;
  607. case T_LNUMBER:
  608. case T_DNUMBER:
  609. $output .= '<span class="php-constant">'. $value .'</span>';
  610. break;
  611. case T_ARRAY_CAST: case T_ARRAY: case T_AS: case T_BOOL_CAST:
  612. case T_BREAK: case T_CASE: case T_CLASS: case T_CONST:
  613. case T_CONTINUE: case T_DECLARE: case T_DEFAULT: case T_DO:
  614. case T_DOUBLE_CAST: case T_ECHO: case T_ELSE: case T_ELSEIF:
  615. case T_EMPTY: case T_ENDDECLARE: case T_ENDFOR: case T_ENDFOREACH:
  616. case T_ENDIF: case T_ENDSWITCH: case T_ENDWHILE: case T_EVAL:
  617. case T_EXIT: case T_EXTENDS: case T_FOR: case T_FOREACH:
  618. case T_FUNCTION: case T_GLOBAL: case T_IF: case T_INCLUDE_ONCE:
  619. case T_INCLUDE: case T_INT_CAST: case T_ISSET: case T_LIST:
  620. case T_NEW: case T_OBJECT_CAST: case T_PRINT:
  621. case T_REQUIRE_ONCE: case T_REQUIRE: case T_RETURN: case T_STATIC:
  622. case T_STRING_CAST: case T_SWITCH: case T_UNSET_CAST: case T_UNSET:
  623. case T_USE: case T_VAR: case T_WHILE:
  624. $output .= '<span class="php-keyword">'. $value .'</span>';
  625. break;
  626. default:
  627. $output .= $value;
  628. }
  629. }
  630. else {
  631. $output .= $token;
  632. }
  633. }
  634. // Manage whitespace:
  635. return '<pre class="php"><code>'. trim($output) .'</code></pre>';
  636. }
  637. /**
  638. * Since we may parse a file containing a reference before we have parsed the
  639. * file containing the referenced object, we keep track of the references
  640. * using a scratch table and save the references to the database table after the
  641. * referenced object has been parsed.
  642. */
  643. function api_reference($branch_name, $to_type, $to_name, $from_did) {
  644. static $is_php_function = array();
  645. if ($to_type == 'function' && !isset($is_php_function[$to_name])) {
  646. $is_php_function[$to_name] = (db_result(db_query("SELECT COUNT(*) FROM {api_documentation} WHERE object_name = '%s' AND branch_name = 'php'", $to_name)) == 1);
  647. }
  648. if ($to_type != 'function' || !$is_php_function[$to_name]) {
  649. db_query("INSERT INTO {api_reference_storage} (object_name, branch_name, object_type, from_did) VALUES ('%s', '%s', '%s', %d)", $to_name, $branch_name, $to_type, $from_did);
  650. }
  651. }
  652. /**
  653. * References do not need to be repeatedly updated, just at the end of a
  654. * request that involved parsing.
  655. */
  656. function api_schedule_update_references() {
  657. static $scheduled = FALSE;
  658. if (!$scheduled) {
  659. register_shutdown_function('api_update_references');
  660. $scheduled = TRUE;
  661. }
  662. }
  663. /**
  664. * Update the references that were collected.
  665. */
  666. function api_update_references() {
  667. // Figure out all the dids of the object/branch/types.
  668. //db_query('UPDATE {api_reference_storage} r, {api_documentation} d SET r.to_did = d.did WHERE r.object_name = d.object_name AND r.branch_name = d.branch_name AND r.object_type = d.object_type');
  669. db_query('UPDATE {api_reference_storage} r SET to_did = d.did' .
  670. ' FROM {api_documentation} d WHERE r.object_name = d.object_name AND r.branch_name = d.branch_name AND r.object_type = d.object_type');
  671. }
  672. function api_schedule_cache_clear() {
  673. register_shutdown_function('cache_clear_all');
  674. }
  675. function api_update_all_branches() {
  676. foreach (api_get_branches() as $branch) {
  677. api_update_branch($branch);
  678. }
  679. }
  680. function api_update_branch($branch) {
  681. static $parse_functions = array(
  682. 'php' => 'api_parse_php_file',
  683. 'module' => 'api_parse_php_file',
  684. 'inc' => 'api_parse_php_file',
  685. 'install' => 'api_parse_php_file',
  686. 'engine' => 'api_parse_php_file',
  687. 'theme' => 'api_parse_php_file',
  688. 'profile' => 'api_parse_php_file',
  689. 'txt' => 'api_parse_text_file',
  690. 'htm' => 'api_parse_html_file',
  691. 'html' => 'api_parse_html_file',
  692. );
  693. // List all documented files for the branch.
  694. $files = array();
  695. $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_name = '%s' AND d.object_type = 'file'", $branch->branch_name);
  696. while ($file = db_fetch_object($result)) {
  697. $files[$file->object_name] = $file;
  698. }
  699. foreach (api_scan_directories($branch->directory) as $path => $file_name) {
  700. preg_match('!\.([a-z]*)$!', $file_name, $matches);
  701. if (isset($matches[1]) && isset($parse_functions[$matches[1]])) {
  702. if (isset($files[$file_name])) {
  703. $parse = (filemtime($path) > $files[$file_name]->modified);
  704. unset($files[$file_name]); // All remaining files will be removed.
  705. }
  706. else { // New file.
  707. $parse = TRUE;
  708. }
  709. if ($parse) {
  710. job_queue_add($parse_functions[$matches[1]], t('API parse %branch %file'), array($path, '%branch' => $branch->branch_name, '%file' => $file_name), drupal_get_path('module', 'api') .'/parser.inc', TRUE);
  711. }
  712. }
  713. }
  714. // Remove outdated files.
  715. foreach (array_keys($files) as $file_name) {
  716. watchdog('api', 'Removing %file.', array('%file' => $file_name));
  717. $result = db_query("SELECT ad.did FROM {api_documentation} ad WHERE ad.file_name = '%s' AND ad.branch_name = '%s'", $file_name, $branch->branch_name);
  718. while ($doc = db_fetch_object($result)) {
  719. db_query("DELETE FROM {api_documentation} WHERE did = %d", $doc->did);
  720. db_query("DELETE FROM {api_file} WHERE did = %d", $doc->did);
  721. db_query("DELETE FROM {api_function} WHERE did = %d", $doc->did);
  722. db_query("DELETE FROM {api_reference_storage} WHERE from_did = %d OR to_did = %d", $doc->did, $doc->did);
  723. }
  724. api_schedule_cache_clear();
  725. }
  726. }
  727. /**
  728. * Find all the files in the directories specified for a branch.
  729. */
  730. function api_scan_directories($directories) {
  731. $directory_array = explode("\n", $directories);
  732. foreach ($directory_array as $key => $directory) {
  733. $directory_array[$key] = trim($directory);
  734. }
  735. if (count($directory_array) > 1) {
  736. $directories_components = array();
  737. foreach ($directory_array as $directory) {
  738. $directory_components = array();
  739. $parts = explode('/', $directory);
  740. foreach ($parts as $part) {
  741. if (strlen($part)) {
  742. array_unshift($directory_components, reset($directory_components) .'/'. $part);
  743. }
  744. }
  745. $directories_components[] = $directory_components;
  746. }
  747. $common_ancestor_components = call_user_func_array('array_intersect', $directories_components);
  748. $common_ancestor = reset($common_ancestor_components);
  749. }
  750. else {
  751. $common_ancestor = $directories;
  752. }
  753. $source_files = array();
  754. foreach ($directory_array as $directory) {
  755. $files = file_scan_directory($directory, '.*');
  756. foreach ($files as $path => $file) {
  757. if (strpos($path, '/.') !== FALSE) {
  758. continue;
  759. }
  760. $file_name = substr($path, strlen($common_ancestor) + 1);
  761. $source_files[$path] = $file_name;
  762. }
  763. }
  764. return $source_files;
  765. }