? test_theme_log.patch 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 20 Aug 2007 06:31:19 -0000 @@ -2,6 +2,52 @@ padding-top: 2em; } -.devel_template_log_call, .devel_template_log_link { - display: none; +/* This should never be shown. Used as markers in the DOM */ +.devel-dom-marker { + display: none !important; +} + +.devel-log-js { +} +.devel-theme-function-list { + font-size: 1em; +} +.devel-theme-function-list dl { + margin-top: 0; +} +.wildcards-inactive, .templates-inactive, .devel-function-no-output { + font-style: italic; +} +.templates-does-not-exist, .wildcards-does-not-exist { + text-decoration: line-through; + font-style: italic; +} +.templates-does-not-exist:hover, .wildcards-does-not-exist:hover { + text-decoration: none; +} +.devel-templates, .devel-preprocessors { + width: 45%; + float: left; +} +.devel-variables { + float: left; +} +.devel-function-run { + font-size: .85em; +} +.devel-timer { + font-style: italic; + font-size: .9em; +} + +#template-log-link { + position: fixed; + top: 0; + right: 0; + padding: 0 .7em; + text-align: right; + background: #fff; + border: 1px; + opacity: .8; + z-index: 9999; } \ No newline at end of file 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 20 Aug 2007 06:31:20 -0000 @@ -1,5 +1,5 @@ '. devel_timer() .' '. $query_summary. ''; + $output .= '
'. devel_timer() .' '. $query_summary. '
'; } // Query log on. @@ -862,130 +883,314 @@ function devel_reinstall_submit($form, & } /** - * Iterate over theme registry, injecting our preprocessor or catch function into every entry. - * - * @return void + * This manually rebuilds the theme registry. Each hook changes its pointer to + * 'devel_catch_theme_function()'. Then an duplicate of the original hook is + * created prepended with 'devel__'. This allows all theme functions to be + * intercepted by allowing the second invocation of theme() from the catch + * function using the secondary, e.g. 'devel__HOOK' data. **/ function devel_theme_registry_inject() { + global $theme_info; + $theme_registry = theme_get_registry(); foreach ($theme_registry as $hook => $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'; + // The default hooks are caught with devel_catch_theme_function(). + // Supply new pointers to devel. + $alt_registry[$hook]['function'] = 'devel_catch_theme_function'; + $alt_registry[$hook]['type'] = 'module'; + $alt_registry[$hook]['theme path'] = drupal_get_path('module', 'devel'); + // Set default arguments from origial. + // This ensures alignment since the same data is passed through theme() twice. + $alt_registry[$hook]['arguments'] = $theme_registry[$hook]['arguments']; + // 'devel__HOOK' is needed so theme() can be called from 'devel_catch_theme_function()' + // The original hook data is passed to this alternate. + $alt_registry['devel__'. $hook] = $theme_registry[$hook]; + // For templated functions only. + if (!isset($theme_registry[$hook]['function'])) { + // Make a copy of the registry as the default last argument for all templated functions for easy access. + $alt_registry['devel__'. $hook]['arguments']['hook_registry'] = $theme_registry[$hook]; + // 'devel_preprocesss' is needed to clean the 'devel__' prefix from the hook. + // This is done to be safe since the normal preprocess functions would see the prefix in the hook parameter. + $alt_registry['devel__'. $hook]['preprocess functions'] = array('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'); + else { + // Not needed for normal theme functions. + unset($alt_registry['devel__'. $hook]['preprocess functions']); } } - _theme_save_registry($GLOBALS['theme_info'], $theme_registry); + + _theme_save_registry($theme_info, $alt_registry); } /** - * 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']); + $version = drupal_major_version_map(VERSION); + $api = variable_get('devel_api_url', 'api.drupal.org'); + + $group_keys = array('wildcards', 'preprocessors', 'templates', 'variables'); + $group_prefix = array('', '', '', '$'); + $group_suffix = array('()', '()', '', ''); + $total_run_count = 0; + $minus_output = 0; + foreach (devel_collect_theme_data() as $hook => $hook_data) { + $output_count = 0; + foreach ($hook_data['dynamic'] as $run_count => $dynamic_data) { + if (!empty($dynamic_data['output'])) { + $output_count++; + } + foreach ($group_keys as $pos => $group) { + if (isset($dynamic_data[$group])) { + foreach ($dynamic_data[$group] as $state => $group_array) { + $prefix = ''. $group_prefix[$pos]; + $suffix = $group_suffix[$pos] .''; + foreach ($group_array as $group_item) { + if (in_array($group, array('wildcards', 'preprocessors', 'templates')) && $state == 'active') { + $prefix .= ''; + $suffix .= ''; + } + $collect[$hook][$group][] = $prefix . $group_item . $suffix; + } + } + } + } } - return theme('table', $header, $rows); - } - } -} -// 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(); + $function = ""; + $function .= isset($hook_data['function']) ? $hook_data['function'] .'()' : "template function: $hook"; + $function .= ""; + + $lookup = l('api', "http://$api/api/$version/function/". $hook_data['function']); + + $lookup = l('apis', "http://$api/apis/$version/". 'preprocess_'. $hook); + + $run_text = format_plural($hook_data['run count'], '1 run', '!count runs', array('!count' => $hook_data['run count'])); + $output_text = format_plural($output_count, 'output 1 time', 'output !count times', array('!count' => $output_count)); + + $rows[$hook .'_head'][1] = array( + 'data' => $function .' '. $run_text .', '. $output_text .'.', + 'class' => 'devel-function-head', + ); + $rows[$hook .'_head'][2] = array( + 'data' => $lookup, + 'class' => 'devel-function-head', + ); + $rows[$hook .'_head'][3] = array( + 'data' => isset($hook_data['function']) ? 'function' : 'template', + 'class' => 'devel-function-head', + ); + $rows[$hook .'_head'][4] = array( + 'data' => $hook_data['theme path'], + 'class' => 'devel-function-head', + ); + + if (!empty($collect[$hook])) { + $break = array('wildcards' => ', ', 'preprocessors' => '
', 'templates' => '
', 'variables' => ', '); + $row_output = ''; + foreach ($collect[$hook] as $group => $group_data) { + $row_output .= " +
+
". $group .":
+
". implode($break[$group], array_unique($group_data)) ."
+
"; + $rows[$hook .'_data'][1] = array( + 'data' => "$row_output", + 'colspan' => '4', + ); + } + } + $total_run_count = $total_run_count + $hook_data['run count']; + $minus_output = $minus_output + ($hook_data['run count'] - $output_count); } - else { - $extension = '.tpl.php'; + + $header = array('Theme function', 'lookup', 'type', 'path'); + if (!isset($rows)) { + $rows = array(); } + $time_info = '

Theme function iterations: '. $total_run_count .' ('. $minus_output .' runs resulted in no output.)

'; + $link_div = ''; + + return $time_info . theme('table', $header, $rows, array('id' => 'devel-theme-logs', 'class' => 'devel-theme-function-list')) . $link_div; } - 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. - * This function gets injected into theme registry in devel_exit(). + * Used to intercept all theme functions, collect theming data and dispatches + * to the original theme function. + * + * This function gets injected into theme registry in devel_exit(). + * + * @see devel_theme_registry_inject() */ function devel_catch_theme_function() { - static $i=0; $args = func_get_args(); + // Get registry. + $hooks = theme_get_registry(); // Get the function that is normally called. $trace = debug_backtrace(); - $call_theme_func = $trace[2]['args'][0]; - - // Get registry for the original function data. - $theme_registry = theme_get_registry(); - $hook_registry_data = $theme_registry[$call_theme_func]['devel']; + $hook = $trace[2]['args'][0]; - // 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; + // Check for wildcard functions, collect data and provide 'devel__HOOK' for each wildcard. + // This is needed due to the swap in the registry. + if (is_array($hook)) { + $flag_first = FALSE; + foreach ($hook as $candidate) { + $devel_hook[] = 'devel__'. $candidate; + if (isset($hooks['devel__'. $candidate]) && !$flag_first) { + $wildcards['active'][] = $candidate; + $flag_first = TRUE; + } + else { + $state = isset($hooks['devel__'. $candidate]) ? 'inactive' : 'does not exist'; + $wildcards[$state][] = $candidate; + } } - include_once($function_file); + $hook = $candidate; + } + else { + $devel_hook = 'devel__'. $hook; } + // $hook is definitely a string at this point.. + + // Increment counter. + devel_counter($hook); - // log the call - $counter = devel_counter(); - $GLOBALS['devel_theme_functions'][$counter] = array( - 'function' => $hook_registry_data['function'], - 'template_files' => array(), - ); + /** + * This runs theme() once again with new hook pointing to original data. + * The original 'preprocess functions' is the default last parameter for templated functions. + * It will be available within $variables inside devel_preprocess(). + */ + $args = array_merge(array($devel_hook), $args); + $output = call_user_func_array('theme', $args); + + $add_data['registry'] = $hooks['devel__'. $hook]; + if (isset($wildcards)) { + $add_data['wildcards'] = $wildcards; + } + if (!empty($output)) { + $add_data['output'] = TRUE; + } + // Collect for the template log. + $return_data = devel_collect_theme_data($hook, $add_data, TRUE); + $return_data['hook'] = $hook; + + // Bail if there's no output. Prevents un-needed markup. + if ($output) { + $output = ($hook != 'page' ? devel_template_marker($return_data, $output) : $output); + } - 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"; +/** + * Used to handle preprocess function. Done here to filter hook names and to + * get easy access to all variables. + */ +function devel_preprocess(&$variables, $hook) { + // Filter out 'devel_' prefix. + $hook = str_replace('devel__', '', $hook); + $registry = $variables['hook_registry']; + unset($variables['hook_registry']); + /** + * This is a partially copied from theme(). Process all variable functions here. + */ + if (is_array($registry['preprocess functions'])) { + $args = array(&$variables, $hook); + foreach ($registry['preprocess functions'] as $preprocess_function) { + if (function_exists($preprocess_function)) { + call_user_func_array($preprocess_function, $args); + } + } + } + /** + * Process templates and template suggestions. + */ + $templates = $variables['template_files']; + $ext = devel_get_theme_extension(); + // Checks for the existance of each suggestion. FIFO, reverse order. + $flag_first = FALSE; + foreach (array_reverse($templates) as $template) { + $check_template = path_to_theme() .'/'. $template .$ext; + if ($exists = file_exists($check_template) && !$flag_first) { + $state = 'active'; + $flag_first = TRUE; + } + elseif ($exists) { + $state = 'inactive'; + } + else { + $state = 'does not exist'; + } + $data['templates'][$state][] = $check_template; + } + // Get default template not part of suggestions. + $default_template = isset($registry['path']) ? $registry['path'] .'/' : ''; + $default_template .= $registry['file'] . $ext; + // Get state of the default template. + $state = $flag_first ? 'inactive' : 'active'; + $data['templates'][$state][] = $default_template .' -base'; + // Add in variables. + $data['variables']['active'] = array_keys($variables); + // Collect for the template log. + devel_collect_theme_data($hook, $data); } -// just hand out next counter -function devel_counter() { - static $counter = 0; - $counter++; - return $counter; +/** + * Collects data used for a loaded page. Parsed through devel_template_log(). + */ +function devel_collect_theme_data($hook = NULL, $data = NULL) { + static $theme_data = array(); + if (isset($hook)) { + // Get current hook count. + $count = devel_counter($hook, TRUE); + $theme_data[$hook]['run count'] = $count; + // Filter out items that doesn't need repeating. + if (isset($data['registry']['arguments']['hook_registry']['preprocess functions'])) { + $theme_data[$hook]['dynamic'][1]['preprocessors']['active'] = $data['registry']['arguments']['hook_registry']['preprocess functions']; + unset($data['registry']['arguments']); + unset($data['registry']['preprocess functions']); + } + if (isset($data['registry'])) { + $theme_data[$hook] = array_merge($theme_data[$hook], $data['registry']); + unset($data['registry']); + } + if (!empty($data)) { + if (!isset($theme_data[$hook]['dynamic'][$count])) { + $theme_data[$hook]['dynamic'][$count] = array(); + } + $theme_data[$hook]['dynamic'][$count] = array_merge($theme_data[$hook]['dynamic'][$count], $data); + } + } + return isset($hook) && !empty($theme_data[$hook]) ? $theme_data[$hook] : $theme_data; +} + +/** + * Sprinkles non-harmful markers within the page DOM. + * + * It looks like two markers have to be set to let jQuery select within the + * range of the markers. + */ +function devel_template_marker($data, $output) { + $id = 'theme-hook-'. form_clean_id($data['hook']) .'-'. devel_counter($data['hook'], TRUE); + $class = 'devel-dom-marker'; + $start_class = $class .' theme-hook-'. form_clean_id($data['hook']); + $end_class = $class .' end-theme-hook-'. form_clean_id($data['hook']); + return "$output"; +} + +// 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]; } // Menu callback. I would love prettier hierarchy browser for this. Index: jquery.devel.js =================================================================== RCS file: jquery.devel.js diff -N jquery.devel.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ jquery.devel.js 20 Aug 2007 06:31:20 -0000 @@ -0,0 +1,21 @@ + +Drupal.behaviors.browseTemplate = function (context) { + // Move to top of DOM then hide logs. + $('body').prepend($('#devel-theme-logs').addClass('devel-log-js').hide()); + + $('#template-log-link').click(function() { + $('#devel-theme-logs').slideDown('medium').click(function() { + $(this).hide(); + }); + }); + + $('#devel-theme-logs a.hook-pointer').click(function() { + var pointer = $(this).attr('id'); + $('.' + pointer).each(function() { + // Outlines do not affect the DOM.. + $('~ :not(.devel-dom-marker):eq(0)', this).css({border: '1px solid red'}); + }); + $(this.parentNode.parentNode.parentNode.parentNode).slideUp('fast'); + return false; + }); +}