'Devel Themer', 'description' => 'Display or hide the textual template log', 'page callback' => 'drupal_get_form', 'page arguments' => array('devel_themer_admin_settings'), 'access arguments' => array('administer site configuration'), 'file' => 'devel_themer.admin.inc', 'type' => MENU_NORMAL_ITEM, ); $items['devel_themer/variables/%'] = array( 'title' => 'Theme Development AJAX variables', 'page callback' => 'devel_themer_ajax_variables', 'page arguments' => array(2), 'delivery callback' => 'ajax_deliver', 'access arguments' => array('access devel information'), 'type' => MENU_CALLBACK, ); return $items; } /** * A menu callback used by popup to retrieve variables from cache for a recent page. * * @param $key * The unique key that is sent to the browser to identify the variables * for a theme hook. * @return string * A chunk of HTML with the devel_print_object() rendering of the variables. */ function devel_themer_ajax_variables($key) { $content = devel_themer_load_krumo($key); if (empty($content)) { $content = t('Unable to load variables from temporary storage.'); } $commands[] = ajax_command_replace('div.themer-variables', '
' . $content . '
'); return array('#type' => 'ajax', '#commands' => $commands); } /** * Implements hook_init(). */ function devel_themer_init() { // Make sure the temporary directory exists. $directory = "temporary://devel_themer"; file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); if (user_access('access devel information')) { // Add requisite libraries. drupal_add_library('system', 'jquery.form'); drupal_add_library('system', 'drupal.ajax'); drupal_add_library('system', 'ui.draggable'); drupal_add_library('system', 'ui.resizable'); // Add this module's JS and CSS. $path = drupal_get_path('module', 'devel_themer'); drupal_add_js($path . '/devel_themer.js'); drupal_add_css($path . '/devel_themer.css'); drupal_add_css($path . '/devel_themer_ie_fix.css', array( 'browsers' => array('IE' => 'lt IE 7', '!IE' => FALSE), 'media' => 'screen', 'weight' => 20, 'preprocess' => FALSE, )); // Add krumo JS and CSS. We can't rely on krumo automatically doing this, // because we add our HTML dynamically after initial page load. if (has_krumo()) { $path_to_devel = drupal_get_path('module', 'devel'); // Krumo files don't work correctly when aggregated. drupal_add_js($path_to_devel . '/krumo/krumo.js', array('preprocess' => FALSE)); drupal_add_css($path_to_devel . '/krumo/skins/default/skin.css', array('preprocess' => FALSE)); } devel_themer_popup(); if (!devel_silent() && variable_get('devel_themer_log', FALSE)) { register_shutdown_function('devel_themer_shutdown'); } } } function devel_themer_shutdown() { print devel_themer_log(); } /** * Show all theme templates and functions that could have been used on this page. */ function devel_themer_log() { if (isset($GLOBALS['devel_theme_calls'])) { foreach ($GLOBALS['devel_theme_calls'] as $counter => $call) { // Sometimes $call is a string. Not sure why. if (is_array($call)) { $id = "devel_theme_log_link_$counter"; $marker = "
\n"; $items = array(); $used = $call['used']; if ($call['type'] == 'func') { $name = $call['name'] . '()'; foreach ($call['candidates'] as $item) { if ($item == $used) { $items[] = "$used"; } else { $items[] = $item; } } } else { $name = $call['name']; foreach ($call['candidates'] as $item) { if ($item == basename($used)) { $items[] = "$used"; } else { $items[] = $item; } } } $rows[] = array($call['duration'], $marker . $name, implode(', ', $items)); unset($items); } } $header = array('Duration (ms)', 'Template/Function', "Candidate template files or function names"); $output = theme('table', $header, $rows); return $output; } } /** * Implements hook_theme_registry_alter(). * * Route all theme hooks to devel_themer_catch_function(). */ function devel_themer_theme_registry_alter(&$theme_registry) { foreach ($theme_registry as $hook => $data) { // We wrap all hooks with our custom handler. $theme_registry[$hook] = array( 'function' => 'devel_themer_catch_function', 'theme path' => $data['theme path'], 'variables' => array(), 'original' => $data, ); } } /** * Implements hook_module_implements_alter(). * * Ensure devel_themer_theme_registry_alter() runs as late as possible. */ function devel_themer_module_implements_alter(&$implementations, $hook) { if (in_array($hook, array('page_alter', 'theme_registry_alter'))) { // Unsetting and resetting moves the item to the end of the array. $group = $implementations['devel_themer']; unset($implementations['devel_themer']); $implementations['devel_themer'] = $group; } } /** * Injects markers into the html returned by theme functions/templates. * * Uses simplehtmldom to add a thmr attribute to toplevel html elements. * A toplevel text element will be wrapped in a span. * * @param string $html * @param string $marker */ function devel_themer_inject_markers($html, $marker) { if (!module_exists('simplehtmldom')) { drupal_set_message(t('Simplehtmldom module is missing and required by Theme developer.'), 'error', FALSE); return $html; } $html_dom = new simple_html_dom(); $html_dom->load($html); foreach ($html_dom->root->nodes as $element) { if ($element->nodetype == HDOM_TYPE_TEXT) { if (trim($element->innertext) !== '') { $element->innertext = "{$element->innertext}"; } } elseif ($element->hasAttribute(DEVEL_THEMER_ATTRIBUTE)) { $element->setAttribute(DEVEL_THEMER_ATTRIBUTE, "$marker " . $element->getAttribute(DEVEL_THEMER_ATTRIBUTE)); } else { $element->setAttribute(DEVEL_THEMER_ATTRIBUTE, $marker); } } $html = (string)$html_dom; // Release memory. $html_dom->clear(); unset($html_dom); return $html; } /** * Returns the arguments passed to the original theme function. * * This function uses debug_backtrace to find these arguments and assumes that * theme() is two levels up. This function is called by * devel_themer_catch_function() which is called by theme(). */ function _devel_themer_get_theme_arguments() { $trace = debug_backtrace(FALSE); $hook = $trace[2]['args'][0]; if (sizeof($trace[2]['args']) > 1) { $variables = $trace[2]['args'][1]; if (!is_array($variables)) { watchdog('devel_themer', 'Variables should be passed as an associative array to the theme hook !hook.', array('!hook' => is_array($hook) ? implode(', ', $hook) : $hook), WATCHDOG_ERROR); } } else { $variables = array(); } return array($hook, $variables); } /** * Intercepts all theme calls (including templates), adds to template log, and dispatches to original theme function. */ function devel_themer_catch_function() { list($hook, $variables) = _devel_themer_get_theme_arguments(); $counter = devel_counter(); $key = "thmr_" . $counter; timer_start($key); // The twin of theme(). All rendering done through here. $meta = array( 'name' => $hook, 'process functions' => array(), 'preprocess functions' => array(), 'suggestions' => array(), 'variables' => $variables, ); $return = devel_themer_theme_twin($hook, $variables, $meta); $time = timer_stop($key); if (!empty($return) && !is_array($return) && !is_object($return) && user_access('access devel information')) { // Check for themer attribute in content returned. Apply word boundaries so // that 'thmr_10' doesn't match 'thmr_1'. if (!preg_match("/\\b$key\\b/", $return)) { // Exclude wrapping a SPAN around content returned by theme functions // whose result is not intended for HTML usage. $exclude = array('options_none'); // theme_html_tag() is a low-level theme function intended primarily for // markup added to the document HEAD. $exclude[] = 'html_tag'; // DATE MODULE: Inline labels for date select lists shouldn't be wrapped. if (strpos($meta['hook'], 'date_part_label_') === 0 && $variables['element']['#type'] == 'date_select' && $variables['element']['#date_label_position'] == 'within') { $exclude[] = $hook; } if (!in_array($hook, $exclude)) { $return = devel_themer_inject_markers($return, $key); } } if ($meta['type'] == 'function') { global $theme; // If the function hasn't been overwritten by the current theme, add it // as a suggestion. if ("{$theme}_{$meta['suggested_hook']}()" != $meta['used']) { $meta['suggestions'][] = $meta['suggested_hook']; } foreach ($meta['suggestions'] as $delta => $suggestion) { $meta['suggestions'][$delta] = "{$theme}_{$suggestion}()"; } $meta['search'] = 'theme_' . $meta['suggested_hook']; } else { // If the template hasn't been overwritten by the theme, add it as a // suggestion. if (FALSE === strpos($meta['template_file'], path_to_theme() . '/')) { $meta['suggestions'][] = $meta['suggested_hook']; } foreach ($meta['suggestions'] as $delta => $suggestion) { $meta['suggestions'][$delta] = strtr($suggestion, '_', '-') . $meta['extension']; } $meta['search'] = strtr($meta['suggested_hook'], '_', '-') . $meta['extension']; } $GLOBALS['devel_theme_calls'][$key] = array( 'id' => $key, 'name' => $meta['suggested_hook'], 'used' => ($meta['type'] == 'function') ? $meta['used'] : $meta['template_file'], 'type' => $meta['type'], 'duration' => $time['time'], 'candidates' => $meta['suggestions'], 'preprocessors' => $meta['preprocess functions'], 'processors' => $meta['process functions'], 'search' => $meta['search'], // Variables are stored on the server and sent to browser via Ajax. 'variables' => devel_themer_store_krumo($meta['variables']), ); } return $return; } /** * Returns true if compression of temporary files is enabled. */ function devel_themer_compression_enabled() { return variable_get('devel_themer_compress_temporary_files', TRUE) && function_exists('gzcompress'); } /** * Temporarily store theme hook variables so that they can be request via ajax * when needed. */ function devel_themer_store_krumo($variables) { // Sometimes serialize will fail, so we will use krumo_ob as a backup. // We don't use krumo_ob by default because it's resource heavy. $data = serialize($variables); if (empty($data)) { $data = krumo_ob($variables); $filename = 'k' . sha1($data); } else { $filename = 's' . sha1($data); } if (devel_themer_compression_enabled()) { $filename = 'c' . $filename; $data = gzcompress($data); } // Write the variables information to the a file. It will be retrieved on demand via AJAX. // We used to write this to DB but was getting 'Warning: Got a packet bigger than 'max_allowed_packet' bytes' // Writing to temp dir means we don't worry about folder existence/perms and cleanup is free. file_put_contents("temporary://devel_themer/$filename", $data); return $filename; } /** * Implements hook_cron(). */ function devel_themer_cron() { // We don't use managed temporary files any more because of performance issues // so we need to clean up old files ourselves. foreach (file_scan_directory('temporary://devel_themer/', '/.*/') as $file) { if (filemtime($file->uri) < REQUEST_TIME - DRUPAL_MAXIMUM_TEMP_FILE_AGE) { unlink($file->uri); } } } /** * Load stored variables. */ function devel_themer_load_krumo($key) { $data = file_get_contents("temporary://devel_themer/$key"); if (empty($data)) { return FALSE; } if ($key{0} == 'c') { if (function_exists('gzuncompress')) { $data = gzuncompress($data); $key = substr($key, 1); } else { return FALSE; } } if ($key{0} == 's') { $data = krumo_ob(unserialize($data)); } return $data; } /** * Nearly clones the Drupal API theme() function. * * It should behave exactly as the core theme() function. The only difference * is a third parameter $meta which is used to collect meta data about the * theming process. * * The code differences between theme() and devel_themer_theme_twin() should * be kept to a minimum. We should only add lines to collect meta data and * any occurrence of $hooks[$hook] should be replaced by * $hooks[$hook]['original']. * * @param $meta * An associative array with the following keys: * - 'suggestions': Candidate hooks which have been looked at but don't have * an implementation. * - 'hook': The first found hook with an implementation. * - 'suggested_hook': Processor functions can suggest other theme hooks. * - 'used': The specific theme function/template that is actually used. * - 'preprocess functions': The preprocess functions. * - 'process functions: The process functions. * - 'type': template or function. * - 'extension': Holds the template extension. This is only set if type * is template. * - 'template_file': Holds the full path to the template. This is only set * if type is template. * @see theme(). */ function devel_themer_theme_twin($hook, $variables, &$meta) { // If called before all modules are loaded, we do not necessarily have a full // theme registry to work with, and therefore cannot process the theme // request properly. See also _theme_load_registry(). if (!module_load_all(NULL) && !defined('MAINTENANCE_MODE')) { throw new Exception(t('theme() may not be called until all modules are loaded.')); } $hooks = theme_get_registry(FALSE); // If an array of hook candidates were passed, use the first one that has an // implementation. if (is_array($hook)) { foreach ($hook as $candidate) { if (isset($hooks[$candidate])) { break; } $meta['suggestions'][] = $candidate; } $hook = $candidate; } // If there's no implementation, check for more generic fallbacks. If there's // still no implementation, log an error and return an empty string. if (!isset($hooks[$hook])) { // Iteratively strip everything after the last '__' delimiter, until an // implementation is found. while ($pos = strrpos($hook, '__')) { $hook = substr($hook, 0, $pos); if (isset($hooks[$hook])) { break; } $meta['suggestions'][] = $hook; } if (!isset($hooks[$hook])) { // Only log a message when not trying theme suggestions ($hook being an // array). if (!isset($candidate)) { watchdog('devel_themer', 'Theme hook %hook not found.', array('%hook' => $hook), WATCHDOG_WARNING); } return ''; } } $info = $hooks[$hook]['original']; $meta['suggested_hook'] = $meta['hook'] = $hook; global $theme_path; $temp = $theme_path; // point path_to_theme() to the currently used theme path: $theme_path = $info['theme path']; // Include a file if the theme function or variable processor is held // elsewhere. if (!empty($info['includes'])) { foreach ($info['includes'] as $include_file) { include_once DRUPAL_ROOT . '/' . $include_file; } } // If a renderable array is passed as $variables, then set $variables to // the arguments expected by the theme function. if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) { $element = $variables; $variables = array(); if (isset($info['variables'])) { foreach (array_keys($info['variables']) as $name) { if (isset($element["#$name"])) { $variables[$name] = $element["#$name"]; } } } else { $variables[$info['render element']] = $element; } } // Merge in argument defaults. if (!empty($info['variables'])) { $variables += $info['variables']; } elseif (!empty($info['render element'])) { $variables += array($info['render element'] => array()); } // Invoke the variable processors, if any. The processors may specify // alternate suggestions for which hook's template/function to use. If the // hook is a suggestion of a base hook, invoke the variable processors of // the base hook, but retain the suggestion as a high priority suggestion to // be used unless overridden by a variable processor function. if (isset($info['base hook'])) { $base_hook = $info['base hook']; $base_hook_info = $hooks[$base_hook]['original']; // Include files required by the base hook, since its variable processors // might reside there. if (!empty($base_hook_info['includes'])) { foreach ($base_hook_info['includes'] as $include_file) { include_once DRUPAL_ROOT . '/' . $include_file; } } if (isset($base_hook_info['preprocess functions']) || isset($base_hook_info['process functions'])) { $variables['theme_hook_suggestion'] = $hook; $hook = $base_hook; $info = $base_hook_info; } } if (isset($info['preprocess functions']) || isset($info['process functions'])) { $variables['theme_hook_suggestions'] = array(); foreach (array('preprocess functions', 'process functions') as $phase) { if (!empty($info[$phase])) { foreach ($info[$phase] as $processor_function) { if (function_exists($processor_function)) { $meta[$phase][] = $processor_function; // We don't want a poorly behaved process function changing $hook. $hook_clone = $hook; $processor_function($variables, $hook_clone); } } } } // If the preprocess/process functions specified hook suggestions, and the // suggestion exists in the theme registry, use it instead of the hook that // theme() was called with. This allows the preprocess/process step to // route to a more specific theme hook. For example, a function may call // theme('node', ...), but a preprocess function can add 'node__article' as // a suggestion, enabling a theme to have an alternate template file for // article nodes. Suggestions are checked in the following order: // - The 'theme_hook_suggestion' variable is checked first. It overrides // all others. // - The 'theme_hook_suggestions' variable is checked in FILO order, so the // last suggestion added to the array takes precedence over suggestions // added earlier. $suggestions = array(); if (!empty($variables['theme_hook_suggestions'])) { $suggestions = $variables['theme_hook_suggestions']; } if (!empty($variables['theme_hook_suggestion'])) { $suggestions[] = $variables['theme_hook_suggestion']; } foreach (array_reverse($suggestions) as $suggestion) { if (isset($hooks[$suggestion])) { $info = $hooks[$suggestion]['original']; $meta['suggested_hook'] = $suggestion; break; } $meta['suggestions'][] = $suggestion; } } // Generate the output using either a function or a template. $output = ''; if (isset($info['function'])) { $meta['type'] = 'function'; $meta['used'] = $info['function'] . '()'; if (function_exists($info['function'])) { $output = $info['function']($variables); } } else { $meta['type'] = 'template'; // Default render function and extension. $render_function = 'theme_render_template'; $extension = '.tpl.php'; // The theme engine may use a different extension and a different renderer. global $theme_engine; if (isset($theme_engine)) { 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; // In some cases, a template implementation may not have had // template_preprocess() run (for example, if the default implementation is // a function, but a template overrides that default implementation). In // these cases, a template should still be able to expect to have access to // the variables provided by template_preprocess(), so we add them here if // they don't already exist. We don't want to run template_preprocess() // twice (it would be inefficient and mess up zebra striping), so we use the // 'directory' variable to determine if it has already run, which while not // completely intuitive, is reasonably safe, and allows us to save on the // overhead of adding some new variable to track that. if (!isset($variables['directory'])) { $default_template_variables = array(); template_preprocess($default_template_variables, $hook); $variables += $default_template_variables; } // Render the output using the template file. $template_file = $info['template'] . $extension; $meta['used'] = $template_file; if (isset($info['path'])) { $template_file = $info['path'] . '/' . $template_file; } $meta['template_file'] = $template_file; $output = $render_function($template_file, $variables); } $meta['variables'] = $variables; // restore path_to_theme() $theme_path = $temp; return $output; } /** * Implements hook_page_alter(). */ function devel_themer_page_alter(&$page) { $page['#post_render'][] = 'devel_themer_post_process_page'; } function devel_themer_post_process_page($page, $elements) { if (!empty($GLOBALS['devel_theme_calls']) && $_SERVER['REQUEST_METHOD'] != 'POST') { $GLOBALS['devel_theme_calls']['devel_themer_uri'] = url("devel_themer/variables"); $javascript = '\n"; $page = preg_replace('##', "\n$javascript\n", $page, 1); } return $page; } // just hand out next counter, or return current value function devel_counter($increment = TRUE) { static $counter = 0; if ($increment) { $counter++; } return $counter; } /** * Return the popup template * placed here for easy editing */ function devel_themer_popup() { $majorver = substr(VERSION, 0, strpos(VERSION, '.')); // add translatable strings drupal_add_js(array('thmrStrings' => array( 'themer_info' => t('Themer info'), 'toggle_throbber' => ' ', 'parents' => t('Parents:') . ' ', 'function_called' => t('Function called:') . ' ', 'template_called' => t('Template called:') . ' ', 'candidate_files' => t('Candidate template files:') . ' ', 'preprocessors' => t('Preprocess functions:') . ' ', 'processors' => t('Process functions:') . ' ', 'candidate_functions' => t('Candidate function names:') . ' ', 'drupal_api_docs' => t('link to Drupal API documentation'), 'source_link_title' => t('link to source code'), 'function_arguments' => t('Function Arguments'), 'template_variables' => t('Template Variables'), 'file_used' => t('File used:') . ' ', 'duration' => t('Duration:') . ' ', 'api_site' => variable_get('devel_api_site', 'http://api.drupal.org/'), 'drupal_version' => $majorver, 'source_link' => url('devel/source', array('query' => array('file' => ''))), )) , 'setting'); $title = t('Drupal Themer Information'); $intro = t('Click on any element to see information about the Drupal theme function or template that created it.'); $popup = <<
X $title
$intro
EOT; drupal_add_js(array('thmr_popup' => $popup), 'setting'); }