Index: devel.module =================================================================== RCS file: /cvs/drupal/contributions/modules/devel/devel.module,v retrieving revision 1.187 diff -u -p -r1.187 devel.module --- devel.module 16 Aug 2007 12:57:48 -0000 1.187 +++ devel.module 17 Aug 2007 14:46:38 -0000 @@ -1,5 +1,5 @@ $data) { - // Add in devel_preprocess so it's used as the last variable preprocessor. - // This way, it can gather *all* template suggestions, not just the ones set by modules. - if (!isset($data['function']) && !in_array('devel_preprocess', $data['preprocess functions'])) { - $theme_registry[$hook]['preprocess functions'][] = 'devel_preprocess'; - } - elseif (isset($data['function'])) { - // For intercepting theme functions not connected to template files. - // Copy over original registry of the hook so it can be caught later. - $theme_registry[$hook]['devel'] = $theme_registry[$hook]; - // Replace the defaults to be intercepted. - $theme_registry[$hook]['function'] = 'devel_catch_theme_function'; - $theme_registry[$hook]['type'] = 'module'; - $theme_registry[$hook]['theme path'] = drupal_get_path('module', 'devel'); - } + // Supply alternate hook (theme function) free of previous data. + $theme_registry_alt[$hook]['function'] = 'devel_catch_theme_function'; + $theme_registry_alt[$hook]['type'] = 'module'; + $theme_registry_alt[$hook]['theme path'] = drupal_get_path('module', 'devel'); + // Set default arguments from origial. + $theme_registry_alt[$hook]['arguments'] = $theme_registry[$hook]['arguments']; + // Copy original hook data and set hook for easy access to be used in devel_catch_theme_function(). + $theme_registry_alt[$hook]['devel'] = $theme_registry[$hook]; + $theme_registry_alt[$hook]['devel']['hook'] = $hook; } - _theme_save_registry($GLOBALS['theme_info'], $theme_registry); + // TODO: This still doesn't work. + //_theme_save_registry($GLOBALS['theme_info'], $theme_registry); + cache_set("theme_registry:garland", $theme_registry_alt); // Hard coded just to test. + } /** - * Show all theme templates that could have been used on this page. - * TODO: highlight the one that was actually used + * Process and display all theme data used on a page. **/ function devel_template_log() { if (variable_get('dev_template_log', 0)) { - $extension = devel_get_theme_extension(); - $header = array('Template name', "Template files ($extension)"); if (isset($GLOBALS['devel_theme_functions'])) { - foreach ($GLOBALS['devel_theme_functions'] as $counter => $function) { - // TODO: path_to_theme() is not right in hook_footer() - // TODO: drupal_discover_template() needs leading './' so as to avoid lookup in whole include pat - // $used = drupal_discover_template($function['template_files'], $extension); - // array_push($function['template_files'], $function['function']); - // $key = array_search($used, $function['template_files']); - // $function['template_files'][$key] = theme('placeholder', $function['template_files'][$key]); - $id = "devel_template_log_link_$counter"; - $marker = "
\n"; - $rows[] = array($marker. $function['function'], implode(', ', $function['template_files'])); - // unset($function['template_files']); - } - return theme('table', $header, $rows); - } - } -} + + $version = drupal_major_version_map(VERSION); + $api = variable_get('devel_api_url', 'api.drupal.org'); + $i = 0; + + foreach ($GLOBALS['devel_theme_functions'] as $hook_count => $data) { + $i++; + $hook = $data['hook']; + + $function = l('api', "http://$api/api/$version/function/". $data['function']); + $function .= ' | '. $data['function'] .''; + + foreach ($data['wildcards'] as $wildcard => $state) { + $wildcards[$hook][] = $state ? "$wildcard" : $wildcard; + } + foreach ($data['template_files'] as $template => $state) { + // TODO: $template uses the full path if it was suggested from a module. + // !! path_to_theme() bug.!! + if ($state == NULL) { + $templates[$hook][] = "$template"; + } + else { + $templates[$hook][] = ($state ? "$template" : "$template"); + } + } + // If the hook has a template, then it has these.. + if (isset($templates[$hook])) { + $variables[$hook] = isset($variables[$hook]) ? array_merge($variables[$hook], $data['variables']) : $data['variables']; + foreach ($data['preprocess functions'] as $pre_func) { + $linked_preprocess[] = l('api', "http://$api/api/function/$pre_func/$version") .' | '. $pre_func .''; + } + $preprocess[$hook] = isset($preprocess[$hook]) ? array_merge($preprocess[$hook], $linked_preprocess) : $linked_preprocess; + } -// would be nice if theme() broke this into separate function so we don't copy logic here. this one is better - has cache -function devel_get_theme_extension() { - global $theme_engine; - static $extension = NULL; - - if (!$extension) { - $extension_function = $theme_engine .'_extension'; - if (function_exists($extension_function)) { - $extension = $extension_function(); - } - else { - $extension = '.tpl.php'; + // We need to run these last. Check by comparing counters. + // Multiple iterations of each hook will be boiled down to one set of data. + if (devel_counter($hook, TRUE) == $data['counter']) { + + $start = array_sum($data['timer']['start']); + $end = array_sum($data['timer']['end']); + $timer = $end - $start; + + static $total_timer = array(); + $total_timer[$hook] = $timer; + + $run_count = devel_counter($hook, TRUE); + + $rows[$hook .'_head'][1] = array( + 'data' => $function .' '. format_plural($run_count, '1 run', '!count runs', array('!count' => $run_count )) .' ('. $timer .' sec)', + 'class' => 'devel-function-head', + ); + $rows[$hook .'_head'][2] = array( + 'data' => $data['function_type'], + 'class' => 'devel-function-head', + ); + $rows[$hook .'_head'][3] = array( + 'data' => $data['path'], + 'class' => 'devel-function-head', + ); + + if (isset($wildcards[$hook])) { + $unique_wildcards = array_unique($wildcards[$hook]); + + $rows[$hook .'_wildcards'][] = array( + 'data' => " +
+
wild card functions:
+
". implode(', ', $unique_wildcards) ."
+
+ " + , + 'colspan' => '3', + ); + } + + if (isset($templates[$hook])) { + $unique_templates = array_unique($templates[$hook]); + if (isset($preprocess[$hook])) { + $unique_preprocess = array_unique($preprocess[$hook]); + } + else { + $unique_preprocess = array('No preprocessors invoked.'); + } + $unique_variables = array_unique($variables[$hook]); + $rows[$hook .'_templates'][] = array( + 'data' => " +
+
variable functions:
+
". implode('
', $unique_preprocess) ."
+
+
+
templates:
+
". implode('
', $unique_templates) ."
+
+
+
variables:
+
$". implode(', $', $unique_variables) ."
+
+ ", + 'colspan' => '3', + ); + } + } + } + + $header = array('Theme function', 'type', 'path'); + if (!isset($rows)) { + $rows = array(); + } + + $total_timer = '

Total rendering time for '. $i .' iterations of theme functions: '.array_sum($total_timer) .' seconds.

'; + + return $total_timer . theme('table', $header, $rows, array('class' => 'devel-theme-function-list')); } } - return $extension; } /** - * This function gets injected into the registry in devel_exit(). It logs theme template calls. -*/ -function devel_preprocess($vars, $function) { - $counter = devel_counter(); - $GLOBALS['devel_theme_functions'][$counter] = array( - 'function' => $function, - 'template_files' => $vars['template_files'], - ); -} - -/** - * Intercepts theme *functions*, adds to template log, and dispatches to original theme function. + * Intercepts all theme functions, adds to template log, and dispatches to original theme function. * This function gets injected into theme registry in devel_exit(). */ function devel_catch_theme_function() { - static $i=0; - $args = func_get_args(); // Get the function that is normally called. $trace = debug_backtrace(); - $call_theme_func = $trace[2]['args'][0]; + $hook = $trace[2]['args'][0]; + + // Start timer!!!!! + $start = microtime(); - // Get registry for the original function data. - $theme_registry = theme_get_registry(); - $hook_registry_data = $theme_registry[$call_theme_func]['devel']; + /** + * The twin of theme(). All rendering done through here. + */ + $args = func_get_args(); + $theme_data = devel_evil_twin_theme($hook, $args); - // Include a file if this theme function is held elsewhere. Partial copy of theme(). - if (!empty($hook_registry_data['file'])) { - $function_file = $hook_registry_data['file']; - if (isset($hook_registry_data['path'])) { - $function_file = $hook_registry_data['path'] .'/'. $function_file; - } - include_once($function_file); + // End timer!!!!! + $theme_data['meta']['timer']['end'][] = microtime(); + // Append start timer!!!!! + $theme_data['meta']['timer']['start'][] = $start; + + $counter = devel_counter($theme_data['meta']['hook']); + $theme_data['meta']['counter'] = $counter; + $theme_data['meta']['hook'] = $theme_data['meta']['hook']; + + // Bail if there's not output. Prevents un-needed markup. + if ($theme_data['output']) { + $output = devel_template_marker($counter, $theme_data['meta']) . $theme_data['output']; } - - // log the call - $counter = devel_counter(); - $GLOBALS['devel_theme_functions'][$counter] = array( - 'function' => $hook_registry_data['function'], - 'template_files' => array(), - ); + else { + $theme_data['meta']['no_output'] = TRUE; + $output = ''; + } + + // $theme data['meta'] is keyed with the following: + // wildcards, path, function, function_type, extension, preprocess functions, template_files, variables (keys) plus the above. + // @see devel_template_log() where this global is processed. + $GLOBALS['devel_theme_functions'][$hook .'_'. $counter] = $theme_data['meta']; - return devel_template_marker($counter). call_user_func_array($hook_registry_data['function'], $args); + return $output; } -function devel_template_marker($counter) { - $id = "devel_template_log_call_". $counter; - return "
\n"; +function devel_template_marker($counter, $meta) { + $id = $meta['function'] .'-'. $counter; + $class = 'devel-function-log-call'; + $class .= ' function-'. $meta['function']; + if (isset($meta['template_files'])) { + $class .= ' '. implode(' ', $meta['preprocess functions']); + $class .= ' '. $meta['template_files'][array_search(TRUE, $meta['template_files'])]; + } + return "\n"; +} + +// hand out counter per hook. +function devel_counter($hook, $get = FALSE) { + static $counter = array(); + if (!isset($counter[$hook])) { + $counter[$hook] = 0; + } + if (!$get) { + $counter[$hook]++; + } + return $counter[$hook]; } -// just hand out next counter -function devel_counter() { - static $counter = 0; - $counter++; - return $counter; +/** + * Copied from theme() with slight modifications to gather data about the + * function as it is being run. Is this a little overboard? + */ +function devel_evil_twin_theme($hook, $args) {; + + // Get registry for the original function data. + $hooks = theme_get_registry(); + + // Gather all possible wildcard functions. + $meta['wildcards'] = array(); + if (is_array($hook)) { + foreach ($hook as $candidate) { + $meta['wildcards'][$candidate] = FALSE; + if (isset($hooks[$candidate])) { + $meta['wildcards'][$candidate] = TRUE; + break; + } + } + $hook = $candidate; + } + + $meta['hook'] = $hook; + $info = $hooks[$hook]['devel']; + + // We can add something here. Maybe a notice or rebuild the registry? + if ($info['hook'] != $hook) { + return; + } + + global $theme_path; + // point path_to_theme() to the currently used theme path: + $theme_path = $info['theme path']; + $meta['path'] = $theme_path; + + if (isset($info['function'])) { + // The theme call is a function. + $meta['function'] = $info['function']; + $meta['function_type'] = 'function'; + // Include a file if this theme function is held elsewhere. + if (!empty($info['file'])) { + $function_file = $info['file']; + if (isset($info['path'])) { + $function_file = $info['path'] .'/'. $function_file; + } + include_once($function_file); + } + $output = call_user_func_array($info['function'], $args); + } + else { + // The theme call is a template. + $meta['function'] = 'theme_'. $hook; + $meta['function_type'] = 'template'; + $variables = array( + 'template_files' => array() + ); + if (!empty($info['arguments'])) { + $count = 0; + foreach ($info['arguments'] as $name => $default) { + $variables[$name] = isset($args[$count]) ? $args[$count] : $default; + $count++; + } + } + + // default render function and extension. + $render_function = 'theme_render_template'; + $extension = '.tpl.php'; + + // Run through the theme engine variables, if necessary + global $theme_engine; + if (isset($theme_engine)) { + // If theme or theme engine is implementing this, it may have + // a different extension and a different renderer. + if ($info['type'] != 'module') { + if (function_exists($theme_engine .'_render_template')) { + $render_function = $theme_engine .'_render_template'; + } + $extension_function = $theme_engine .'_extension'; + if (function_exists($extension_function)) { + $extension = $extension_function(); + } + } + } + $meta['extension'] = $extension; + + if (isset($info['preprocess functions']) && is_array($info['preprocess functions'])) { + // This construct ensures that we can keep a reference through + // call_user_func_array. + $args = array(&$variables, $hook); + foreach ($info['preprocess functions'] as $preprocess_function) { + if (function_exists($preprocess_function)) { + call_user_func_array($preprocess_function, $args); + } + } + $meta['preprocess functions'] = $info['preprocess functions']; + } + + // Get suggestions for alternate templates out of the variables + // that were set. This lets us dynamically choose a template + // from a list. The order is FILO, so this array is ordered from + // least appropriate first to most appropriate last. + $suggestions = array(); + + if (isset($variables['template_files'])) { + $suggestions = $variables['template_files']; + } + if (isset($variables['template_file'])) { + $suggestions[] = $variables['template_file']; + } + + if ($suggestions) { + $template_file = drupal_discover_template($suggestions, $extension); + foreach ($suggestions as $candidate) { + $meta['template_files'][$candidate . $extension] = FALSE; + if ($candidate . $extension == $template_file) { + $meta['template_files'][$candidate . $extension] = TRUE; + } + elseif (file_exists(path_to_theme() .'/'. $candidate . $extension)) { + $meta['template_files'][$candidate . $extension] = NULL; + } + } + } + + if (empty($template_file)) { + $template_file = $info['file'] . $extension; + $meta['template_files'][$template_file] = TRUE; + if (isset($info['path'])) { + $template_file = $info['path'] .'/'. $template_file; + } + } + $output = $render_function($template_file, $variables); + + $meta['variables'] = array_keys($variables); + } + + return array('output' => $output, 'meta' => $meta); } // Menu callback. I would love prettier hierarchy browser for this. Index: devel.css =================================================================== RCS file: /cvs/drupal/contributions/modules/devel/devel.css,v retrieving revision 1.3 diff -u -p -r1.3 devel.css --- devel.css 16 Aug 2007 05:49:58 -0000 1.3 +++ devel.css 17 Aug 2007 14:47:07 -0000 @@ -2,6 +2,35 @@ padding-top: 2em; } -.devel_template_log_call, .devel_template_log_link { +.devel-function-log-call { + font-size: .01em; display: none; +} + +.devel-theme-function-list { + font-size: 1em; +} +.devel-theme-function-list dl { + margin-top: 0; +} +.devel-wildcard-active { + text-decoration: underline; +} +.devel-template-does-not-exist { + text-decoration: line-through; + font-style: italic; +} +.devel-templates, .devel-preprocessors { + width: 45%; + float: left; +} +.devel-variables { + float: left; +} +.devel-theme-function-list .function-run { + font-size: .85em; +} +.devel-timer { + font-style: italic; + font-size: .9em; } \ No newline at end of file