Skip to content
devel_themer.module 16.4 KiB
Newer Older
/**
 * Implementation of hook_menu().
 */
function devel_themer_menu() {
  $items = array();
  $items['admin/settings/devel_themer'] = array(
    'title' => 'Devel Themer',
    'description' =>  t('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'),
    'type' => MENU_NORMAL_ITEM
  );
  $items['devel_themer/enable'] = array(
    'title' => 'Devel Themer Enable',
    'page callback' => 'devel_themer_toggle',
    'page arguments' => array(1),
    'access arguments' => array('access devel information'),
    'type' => MENU_CALLBACK,
  );
  $items['devel_themer/disable'] = array(
    'title' => 'Devel Themer Enable',
    'page callback' => 'devel_themer_toggle',
    'page arguments' => array(0),
    'access arguments' => array('access devel information'),
    'type' => MENU_CALLBACK,
  );
function devel_themer_toggle($value) {
  $_SESSION['devel_themer_toggle'] = (int) $value;
function devel_themer_admin_settings() {
  $form['devel_themer_log'] = array('#type' => 'checkbox',
    '#title' => t('Display theme log'),
    '#default_value' => variable_get('devel_themer_log', FALSE),
    '#description' => t('Display the list of theme templates and theme functions which could have been be used for a given page. The one that was actually used is bolded. This is the same data as the represented in the popup, but all calls are listed in chronological order and can alternately be sorted by time.'),
  );
  return system_settings_form($form);
}


function devel_themer_init() {
  if (user_access('access devel information')) {
    $path = drupal_get_path('module', 'devel_themer');
    drupal_add_css($path .'/devel_themer.css');
    drupal_add_js($path .'/devel_themer.js');
    drupal_add_js($path .'/jquery-ui-drag.min.js');
    // these needs to happen after all the other CSS
    drupal_set_html_head('<!--[if IE]>
    <link href="' . $path .'/devel_themer_ie_fix.css" rel="stylesheet" type="text/css" media="screen" />
<![endif]-->');
    if (!devel_silent() && variable_get('devel_themer_log', FALSE)) {
      register_shutdown_function('devel_themer_shutdown');
    }
function devel_themer_shutdown() {
  print devel_themer_log();
/**
 * An implementation of hook_theme_registry_alter()
 * Iterate over theme registry, injecting our catch function into every theme function.
 * The catch function logs theme calls and performs divine nastiness.
 *
 * @return void
 **/
function devel_themer_theme_registry_alter($theme_registry) {
  foreach ($theme_registry as $hook => $data) {
    // if (isset($data['function'])) {
      // Copy over original registry of the hook so it can be caught later.
      $theme_registry[$hook]['devel_themer'] = $theme_registry[$hook];
      // Replace with our catch function.
      $theme_registry[$hook]['function'] = 'devel_themer_catch_function';
      $theme_registry[$hook]['type'] = 'module';
      $theme_registry[$hook]['theme path'] = drupal_get_path('module', 'devel_themer');
  }
}

/**
 * Show all theme templates and functions that could have been used on this page.
 **/
function devel_themer_log() {
  $extension = devel_get_theme_extension();
  if (isset($GLOBALS['devel_theme_calls'])) {
    foreach ($GLOBALS['devel_theme_calls'] as $counter => $call) {
      $id = "devel_theme_log_link_$counter";
      $marker = "<div id=\"$id\" class=\"devel_theme_log_link\"></div>\n";
      if (count($call['candidates']) > 1) {
        $used = $call['used'];
        foreach ($call['candidates'] as $key => $value) {
          if ($call['type'] == 'tpl') {
            $test = $value == basename($used);
          }
          else {
            $test = $value == $used;
          }
          $call['candidates'][$key] = $test ? "<strong>$used</strong>" : $value;
        }
      }
      $name = $call['type'] == 'func' ? $call['name']. '()' : $call['name']. $extension;
      $rows[] = array($call['time'], $marker. $name, implode(', ', $call['candidates']));
    $header = array('Time (ms)', 'Template/Function', "Candidate template files or function names");
    $output = theme('table', $header, $rows);
    return $output;
  }
}

// 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';
    }
  }
  return $extension;
}

/**
 * Log template file suggestions into a $GLOBAL.
*/
// function devel_themer_preprocess($vars, $hook) {
//   $counter = devel_counter();
//   $GLOBALS['devel_theme_calls'][$counter] = array(
//     'name' => $hook,
//     'type' => 'tpl',
//     'candidates' => $vars['template_files'],
//   );
//   // add in a 'template' file if it was declared
//   if (isset($vars['template_file'])) {
//     array_unshift($GLOBALS['devel_theme_calls'][$counter]['candidates'] = $vars['template_file']);
//   }
//   // add the plain template name
//   array_unshift($GLOBALS['devel_theme_calls'][$counter]['candidates'], $hook);
// }
// 
// // A wrapper function so we can know what template actually got called. Log that info in the GLOBAL for later display.
// // I found no other easy way to determine this. Patches welcome.
// function phptemplate_render_template($file, $variables) {
//   $counter = devel_counter(FALSE);
// 
//   $timer_name = "thmr_$counter";
//   timer_start($timer_name);
//   $output = theme_render_template($file, $variables);
//   $time = timer_stop($timer_name);
//   $GLOBALS['devel_theme_calls'][$counter]['time'] = $time['time'];
// 
//   $GLOBALS['devel_theme_calls'][$counter]['used'] = $file;
//   $GLOBALS['devel_theme_calls'][$counter]['args'] = $variables;
// 
//   // awful attempt to get position #2 in the assoc array $variables
//   $i=0;
//   foreach ($variables as $key => $var) {
//     if ($i == 1) {
//       $name = $key;
//     }
//     $i++;
//   }
//   list($prefix, $suffix) = devel_theme_call_marker($name, $counter, 'tpl');
//   drupal_add_js(array("thmr_$counter" => array('arguments' => devel_print_object($variables, '$', FALSE), 'candidates' => array_reverse($GLOBALS['devel_theme_calls'][$counter]['candidates']), 'used' => $GLOBALS['devel_theme_calls'][$counter]['used'])), 'setting', 'header', FALSE, FALSE, FALSE);
//   return $prefix. "\n  ". $output. "\n". $suffix. "\n";
// }
 * Intercepts all theme calls (including templates), adds to template log, and dispatches to original theme function.
 * This function gets injected into theme registry in devel_exit().
 */
function devel_themer_catch_function() {
  $args = func_get_args();

  // Get the function that is normally called.
  $trace = debug_backtrace();
  $hook = $trace[2]['args'][0];
  array_unshift($args, $hook);
  
  $timer_name = "thmr_$counter";
  timer_start($timer_name);
  /**
   * The twin of theme(). All rendering done through here.
   */
  list($return, $meta) = call_user_func_array('devel_themer_theme_twin', $args);
  $time = timer_stop($timer_name);
  if (!empty($return)) {
    list($prefix, $suffix) = devel_theme_call_marker($hook, $counter, 'func');
    $start_return = substr($return, 0, 31);
    $start_prefix = substr($prefix, 0, 31);

    $skip = array('theme_hidden');
    // Pass the call to the original function. Wrap as needed.
    if ($start_return != $start_prefix && !in_array($hook, $skip)) {
      $output = $prefix. "\n  ". $return. $suffix. "\n";
      // TODO: this is fishy
      // drupal_add_js(array("thmr_$counter" => array('args' => devel_print_object($args, NULL, FALSE), 'candidates' => $candidates)), 'setting', 'header', FALSE, FALSE, FALSE);
  
  if ($meta['type'] == 'func') {
    $name = $meta['used'];
    $used = $meta['used'];
    $candidates = array_keys($meta['wildcards']);
    $args = devel_print_object($meta['variables'], NULL, FALSE);
  }
  else {
    $name = $meta['hook'];
    $candidates = isset($meta['template_files']) ? array_keys($meta['template_files']) : array();
    $used = $meta['template_file'];
    devel_print_object($meta['variables'], '$', FALSE);
  $GLOBALS['devel_theme_calls']["thmr_$counter"] = array(
    'type' => $meta['type'],
    'empty' => empty($return),
    'time' => $time['time'],
    'used' => $used,
    'candidates' => $candidates,
    // 'suggestions' => isset($suggestions) ? $suggestions : array(),
  return isset($output) ? $output : '';
}

/**
 * An unfortunate copy/paste of theme(). This one is called by the devel_themer_catch_function()
 * and processes all theme calls but gives us info about the candidates, timings, etc. Without this twin, 
 * it was impossible to capture calls to module owned templates (e.g. user_profile) and awkward to determine
 * which template was finally called and how long it took.
 *
 **/
function devel_themer_theme_twin() {
  $args = func_get_args();
  $hook = array_shift($args);

  static $hooks = NULL;
  if (!isset($hooks)) {
    init_theme();
    $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;
  // extract the original registry info that we set aside in the devel element.
  $info = $hooks[$hook]['devel_themer'];

  global $theme_path;
  $temp = $theme_path;
  $meta['path'] = $theme_path;
  // point path_to_theme() to the currently used theme path:
  // MW: hope this isn't needed
  // $theme_path = $info[$hook]['theme path'];

  // Include a file if the theme function or preprocess function is held elsewhere.
  if (!empty($info['file'])) {
    $include_file = $info['file'];
    if (isset($info['path'])) {
      $include_file = $info['path'] .'/'. $include_file;
    }
    include_once($include_file);
  }
  if (isset($info['function'])) {
    // The theme call is a function.
    $output = call_user_func_array($info['function'], $args);
    $meta['type'] = 'func';
    $meta['used'] = $info['function'];
    $meta['variables'] = $args;
  }
  else {
    // The theme call is a template.
    $meta['type'] = 'tpl';
    $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 ($hooks[$hook]['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($info['theme paths'], $suggestions, $extension);
      // Log all the candidate files and note which was actually used.
      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 = $hooks[$hook]['template'] . $extension;
      $meta['template_files'][$template_file] = TRUE;
      if (isset($hooks[$hook]['path'])) {
        $template_file = $hooks[$hook]['path'] .'/'. $template_file;
      }
    }
    $output = $render_function($template_file, $variables);
    $meta['template_file'] = $template_file;
    $meta['variables'] = array_keys($variables);
  }
  // restore path_to_theme()
  // $theme_path = $temp;
  return array($output, $meta);
/**
 * An implementation of hook_footer(). Emit huge js array for the benefit of the popup.
// function devel_themer_footer() {
//   drupal_add_js(array("devel_themer" => $GLOBALS['devel_theme_calls']), 'setting', 'footer', FALSE, FALSE, FALSE);
// }

// we emit the huge js array here instead of hook_footer so we can catch theme('page')
function devel_themer_exit() {
  if (!empty($GLOBALS['devel_theme_calls'])) {
    print '<script type="text/javascript">jQuery.extend(Drupal.settings, '.  drupal_to_js($GLOBALS['devel_theme_calls']) .");</script>\n";
  }
}

function devel_theme_call_marker($name, $counter, $type) {
  $id = "thmr_". $counter;
  return array("<span id=\"$id\" thmr_key=\"$name\" thmr_type=\"$type\" class=\"thmr_call\">", "</span>\n");
}

// 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(
      'parents' => t('Parents: '),
      'function_called' => t('Function called: '),
      'template_called' => t('Template called: '),
      'candidate_files' => t('Candidate template files: '),
      'candidate_functions' => t('Candidate function names: '),
      'drupal_api_docs' => t('link to Drupal API documentation'),
      'function_arguments' => t('Function Arguments'),
      'template_variables' => t('Template Variables'),
      'file_used' => t('File used: '),
      'api_site' => variable_get('devel_api_site', 'http://api.drupal.org/'),
      'drupal_version' => $majorver,
    ))
    , 'setting');

  $title = t('Drupal Themer Information');
  $intro = t('Click on any element on the page to see the Drupal theme function or template that created it.');

  $popup = <<<EOT
  <div id="themer-fixeder">
  <div id="themer-relativer">
  <div id="themer-popup">
      <div class="topper">
        <span class="close">X</span> $title
      </div>
      <div id="parents" class="row">

      </div>
      <div class="info row">
        <div class="starter">$intro</div>
        <dl>
          <dt class="key-type">

          </dt>
          <dd class="key">

          </dd>
          <dt class="candidates-type">

          </dt>
          <dd class="candidates">

          </dd>
          <div class="used">
          </div>
        </dl>
      </div><!-- /info -->
      <div class="attributes row">

      </div><!-- /attributes -->
    </div><!-- /themer-popup -->
EOT;

  drupal_add_js(array('thmr_popup' => $popup), 'setting');