'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('#