Commit 5bbbf10b authored by Dries's avatar Dries

- Patch #130987 by merlinofchaos: added theme registry for easier themability.

parent 21c5b717
......@@ -2,6 +2,7 @@
Drupal 6.0, xxxx-xx-xx (development version)
----------------------
- Added theme registry: modules can directly provide .tpl.php files for their themes without having to create theme_ functions.
- Added versioning support to node terms.
- Made it easier to theme the forum overview page.
- Drupal works with error reporting set to E_ALL.
......
......@@ -929,6 +929,15 @@ function drupal_maintenance_theme() {
drupal_add_css(drupal_get_path('module', 'system') .'/defaults.css', 'module');
drupal_add_css(drupal_get_path('module', 'system') .'/system.css', 'module');
$theme = '';
// Special case registry of theme functions used by the installer
$themes = drupal_common_themes();
foreach ($themes as $hook => $info) {
if (!isset($info['file']) && !isset($info['function'])) {
$themes[$hook]['function'] = 'theme_' . $hook;
}
}
_theme_set_registry($themes);
}
/**
......
......@@ -2292,3 +2292,193 @@ function element_child($key) {
function element_children($element) {
return array_filter(array_keys((array) $element), 'element_child');
}
/**
* Provide theme registration for themes across .inc files.
*/
function drupal_common_themes() {
return array(
// theme.inc
'placeholder' => array(
'arguments' => array('text' => NULL)
),
'page' => array(
'arguments' => array('content' => NULL, 'show_blocks' => TRUE),
),
'maintenance_page' => array(
'arguments' => array('content' => NULL, 'messages' => TRUE),
),
'install_page' => array(
'arguments' => array('content' => NULL),
),
'task_list' => array(
'arguments' => array('items' => NULL, 'active' => NULL),
),
'status_messages' => array(
'arguments' => array('display' => NULL),
),
'links' => array(
'arguments' => array('links' => NULL, 'attributes' => array('class' => 'links')),
),
'image' => array(
'arguments' => array('path' => NULL, 'alt' => '', 'title' => '', 'attributes' => NULL, 'getsize' => TRUE),
),
'breadcrumb' => array(
'arguments' => array('breadcrumb' => NULL),
),
'help' => array(
'arguments' => array(),
),
'node' => array(
'arguments' => array('node' => NULL, 'teaser' => FALSE, 'page' => FALSE),
),
'submenu' => array(
'arguments' => array('links' => NULL),
),
'table' => array(
'arguments' => array('header' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL),
),
'table_select_header_cell' => array(
'arguments' => array(),
),
'tablesort_indicator' => array(
'arguments' => array('style' => NULL),
),
'box' => array(
'arguments' => array('title' => NULL, 'content' => NULL, 'region' => 'main'),
),
'block' => array(
'arguments' => array('block' => NULL),
),
'mark' => array(
'arguments' => array('type' => MARK_NEW),
),
'item_list' => array(
'arguments' => array('items' => array(), 'title' => NULL, 'type' => 'ul', 'attributes' => NULL),
),
'more_help_link' => array(
'arguments' => array('url' => NULL),
),
'xml_icon' => array(
'arguments' => array('url' => NULL),
),
'feed_icon' => array(
'arguments' => array('url' => NULL),
),
'closure' => array(
'arguments' => array('main' => 0),
),
'blocks' => array(
'arguments' => array('region' => NULL),
),
'username' => array(
'arguments' => array('object' => NULL),
),
'progress_bar' => array(
'arguments' => array('percent' => NULL, 'message' => NULL),
),
// from pager.inc
'pager' => array(
'arguments' => array('tags' => array(), 'limit' => 10, 'element' => 0, 'parameters' => array()),
),
'pager_first' => array(
'arguments' => array('text' => NULL, 'limit' => NULL, 'element' => 0, 'parameters' => array()),
),
'pager_previous' => array(
'arguments' => array('text' => NULL, 'limit' => NULL, 'element' => 0, 'interval' => 1, 'parameters' => array()),
),
'pager_next' => array(
'arguments' => array('text' => NULL, 'limit' => NULL, 'element' => 0, 'interval' => 1, 'parameters' => array()),
),
'pager_last' => array(
'arguments' => array('text' => NULL, 'limit' => NULL, 'element' => 0, 'parameters' => array()),
),
'pager_list' => array(
'arguments' => array('limit' => NULL, 'element' => 0, 'quantity' => 5, 'text' => '', 'parameters' => array()),
),
'pager_link' => array(
'arguments' => array('text' => NULL, 'page_new' => NULL, 'element' => NULL, 'parameters' => array(), 'attributes' => array()),
),
// from locale.inc
'locale_admin_manage_screen' => array(
'arguments' => array('form' => NULL),
),
// from menu.inc
'menu_item_link' => array(
'arguments' => array('item' => NULL),
),
'menu_tree' => array(
'arguments' => array('tree' => NULL),
),
'menu_item' => array(
'arguments' => array('link' => NULL, 'has_children' => NULL, 'menu' => ''),
),
'menu_local_task' => array(
'arguments' => array('link' => NULL, 'active' => FALSE),
),
'menu_local_tasks' => array(
'arguments' => array(),
),
// from form.inc
'select' => array(
'arguments' => array('element' => NULL),
),
'fieldset' => array(
'arguments' => array('element' => NULL),
),
'radio' => array(
'arguments' => array('element' => NULL),
),
'radios' => array(
'arguments' => array('element' => NULL),
),
'password_confirm' => array(
'arguments' => array('element' => NULL),
),
'date' => array(
'arguments' => array('element' => NULL),
),
'item' => array(
'arguments' => array('element' => NULL),
),
'checkbox' => array(
'arguments' => array('element' => NULL),
),
'checkboxes' => array(
'arguments' => array('element' => NULL),
),
'submit' => array(
'arguments' => array('element' => NULL),
),
'button' => array(
'arguments' => array('element' => NULL),
),
'hidden' => array(
'arguments' => array('element' => NULL),
),
'token' => array(
'arguments' => array('element' => NULL),
),
'textfield' => array(
'arguments' => array('element' => NULL),
),
'form' => array(
'arguments' => array('element' => NULL),
),
'textarea' => array(
'arguments' => array('element' => NULL),
),
'markup' => array(
'arguments' => array('element' => NULL),
),
'password' => array(
'arguments' => array('element' => NULL),
),
'file' => array(
'arguments' => array('element' => NULL),
),
'form_element' => array(
'arguments' => array('element' => NULL, 'value' => NULL),
),
);
}
......@@ -457,7 +457,9 @@ function drupal_render_form($form_id, &$form) {
// Don't override #theme if someone already set it.
if (!isset($form['#theme'])) {
if (theme_get_function($form_id)) {
init_theme();
$registry = theme_get_registry();
if (isset($registry[$form_id])) {
$form['#theme'] = $form_id;
}
}
......
......@@ -72,6 +72,7 @@ function init_theme() {
if (strpos($themes[$theme]->filename, '.theme')) {
// file is a theme; include it
include_once './' . $themes[$theme]->filename;
_theme_load_registry($theme);
}
elseif (strpos($themes[$theme]->description, '.engine')) {
// file is a template; include its engine
......@@ -80,9 +81,115 @@ function init_theme() {
if (function_exists($theme_engine .'_init')) {
call_user_func($theme_engine .'_init', $themes[$theme]);
}
_theme_load_registry($theme, $theme_engine);
}
}
/**
* Retrieve the stored theme registry. If the theme registry is already
* in memory it will be returned; otherwise it will attempt to load the
* registry from cache. If this fails, it will construct the registry and
* cache it.
*/
function theme_get_registry($registry = NULL) {
static $theme_registry = NULL;
if (isset($registry)) {
$theme_registry = $registry;
}
return $theme_registry;
}
/**
* Store the theme registry in memory.
*/
function _theme_set_registry($registry) {
// Pass through for setting of static variable.
return theme_get_registry($registry);
}
/**
* Get the theme_registry cache from the database; if it doesn't exist, build
* it.
*/
function _theme_load_registry($theme, $theme_engine = NULL) {
$cache = cache_get("theme_registry:$theme", 'cache');
if (isset($cache->data)) {
$registry = unserialize($cache->data);
}
else {
$registry = _theme_build_registry($theme, $theme_engine);
_theme_save_registry($theme, $registry);
}
_theme_set_registry($registry);
}
/**
* Write the theme_registry cache into the database.
*/
function _theme_save_registry($theme, $registry) {
cache_set("theme_registry:$theme", 'cache', serialize($registry));
}
/**
* Force the system to rebuild the theme registry; this should be called
* when modules are added to the system, or when a dynamic system needs
* to add more theme hooks.
*/
function drupal_rebuild_theme_registry() {
cache_clear_all('theme_registry', 'cache', TRUE);
}
/**
* Process a single invocation of the theme hook.
*/
function _theme_process_registry(&$cache, $name, $type) {
$function = $name .'_theme';
if (function_exists($function)) {
$result = $function($cache);
// Automatically find paths
$path = drupal_get_path($type, $name);
foreach ($result as $hook => $info) {
$result[$hook]['type'] = $type;
// if function and file are left out, default to standard naming
// conventions.
if (!isset($info['file']) && !isset($info['function'])) {
$result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name .'_') . $hook;
}
if (isset($info['file']) && !isset($info['path'])) {
$result[$hook]['file'] = $path .'/'. $info['file'];
}
// If 'arguments' have been defined previously, carry them forward.
// This should happen if a theme overrides a Drupal defined theme
// function, for example.
if (!isset($info['arguments']) && isset($cache[$hook])) {
$result[$hook]['arguments'] = $cache[$hook]['arguments'];
}
}
$cache = array_merge($cache, $result);
}
}
/**
* Rebuild the hook theme_registry cache.
*/
function _theme_build_registry($theme, $theme_engine) {
$cache = array();
foreach (module_implements('theme') as $module) {
_theme_process_registry($cache, $module, 'module');
}
if ($theme_engine) {
_theme_process_registry($cache, $theme_engine, 'theme_engine');
}
_theme_process_registry($cache, $theme, 'theme');
return $cache;
}
/**
* Provides a list of currently available themes.
*
......@@ -140,18 +247,48 @@ function list_theme_engines($refresh = FALSE) {
}
/**
* Generate the themed representation of a Drupal object.
* Generate the themed output.
*
* All requests for theme hooks must go through this function. It examines
* the request and routes it to the appropriate theme function. The theme
* registry is checked to determine which implementation to use, which may
* be a function or a template.
*
* If the implementation is a function, it is executed and its return value
* passed along.
*
* All requests for themed functions must go through this function. It examines
* the request and routes it to the appropriate theme function. If the current
* theme does not implement the requested function, then the current theme
* engine is checked. If neither the engine nor theme implement the requested
* function, then the base theme function is called.
* If the implementation is a template, the arguments are converted to a
* $variables array. This array is then modified by the theme engine (if
* applicable) and the theme. The following functions may be used to modify
* the $variables array:
*
* For example, to retrieve the HTML that is output by theme_page($output), a
* module should call theme('page', $output).
* ENGINE_engine_variables(&$variables)
* This function should only be implemented by theme engines and is exists
* so that the theme engine can set necessary variables. It is commonly
* used to set global variables such as $directory and $is_front_page.
* ENGINE_engine_variables_HOOK(&$variables)
* This is the same as the previous function, but is called only per hook.
* ENGINE_variables_HOOK(&$variables)
* ENGINE_variables(&$variables)
* This is meant to be used by themes that utilize a theme engine; as it is
* good practice for these themes to use the theme engine's name for
* their functions so that they may share code. In PHPTemplate, these
* functions will appear in template.php
* THEME_variables_HOOK(&$variables)
* THEME_variables(&$variables)
* These functions are based upon the raw theme; they should primarily be
* used by themes that do not use an engine or by themes that need small
* changes to what has already been established in the theme engine version
* of the function.
*
* @param $function
* There are two special variables that these hooks can set:
* 'template_file' and 'template_files'. These will be merged together
* to form a list of 'suggested' alternate template files to use, in
* reverse order of priority. template_file will always be a higher
* priority than items in template_files. theme() will then look for these
* files, one at a time, and use the first one
* that exists.
* @param $hook
* The name of the theme function to call.
* @param ...
* Additional arguments to pass along to the theme function.
......@@ -159,48 +296,125 @@ function list_theme_engines($refresh = FALSE) {
* An HTML string that generates the themed output.
*/
function theme() {
static $functions;
$args = func_get_args();
$function = array_shift($args);
$hook = array_shift($args);
if (!isset($functions[$function])) {
$functions[$function] = theme_get_function($function);
static $hooks = NULL;
if (!isset($hooks)) {
init_theme();
$hooks = theme_get_registry();
}
if ($functions[$function]) {
return call_user_func_array($functions[$function], $args);
if (!isset($hooks[$hook])) {
return;
}
}
/**
* Determine if a theme function exists, and if so return which one was found.
*
* @param $function
* The name of the theme function to test.
* @return
* The name of the theme function that should be used, or FALSE if no function exists.
*/
function theme_get_function($function) {
global $theme, $theme_engine;
$info = $hooks[$hook];
// Because theme() is called a lot, calling init_theme() only to have it
// smartly return is a noticeable performance hit. Don't do it.
if (!isset($theme)) {
init_theme();
if (isset($info['function'])) {
// The theme call is a function.
// Include a file if this theme function is held elsewhere.
if (!empty($info['file'])) {
include_once($info['file']);
}
return call_user_func_array($info['function'], $args);
}
else {
// The theme call is a 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++;
}
}
if (($theme != '') && function_exists($theme .'_'. $function)) {
// call theme function
return $theme .'_'. $function;
}
elseif (($theme != '') && isset($theme_engine) && function_exists($theme_engine .'_'. $function)) {
// call engine function
return $theme_engine .'_'. $function;
// default render function and extension.
$render_function = 'theme_render_template';
$extension = '.tpl.php';
$variables_list = array();
// Run through the theme engine variables, if necessary
global $theme_engine;
if (isset($theme_engine)) {
// Call each of our variable override functions. We allow
// several to create cleaner code.
$variables_list[] = $theme_engine .'_engine_variables';
$variables_list[] = $theme_engine .'_engine_variables_'. $hook;
$variables_list[] = $theme_engine .'_variables';
$variables_list[] = $theme_engine .'_variables_'. $hook;
// 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();
}
}
}
// Add theme specific variable substitution:
global $theme;
$variables_list[] = $theme .'_variables';
$variables_list[] = $theme .'_variables_'. $hook;
// This construct ensures that we can keep a reference through
// call_user_func_array.
$args = array(&$variables, $hook);
foreach ($variables_list as $variables_function) {
if (function_exists($variables_function)) {
call_user_func_array($variables_function, $args);
}
}
// 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);
}
if (empty($template_file)) {
$template_file = $hooks[$hook]['file'] . $extension;
if (isset($hooks[$hook]['path'])) {
$template_file = $hooks[$hook]['path'] .'/'. $template_file;
}
}
return $render_function($template_file, $variables);
}
elseif (function_exists('theme_'. $function)){
// call Drupal function
return 'theme_'. $function;
}
/**
* Choose which template file to actually render; these are all
* suggested templates from the theme.
*/
function drupal_discover_template($suggestions, $extension = '.tpl.php') {
global $theme_engine;
// Loop through any suggestions in FIFO order.
$suggestions = array_reverse($suggestions);
foreach ($suggestions as $suggestion) {
if (!empty($suggestion) && file_exists($file = path_to_theme() .'/'. $suggestion . $extension)) {
return $file;
}
}
return FALSE;
}
/**
......@@ -348,16 +562,81 @@ function theme_get_setting($setting_name, $refresh = FALSE) {
}
/**
* @defgroup themeable Themeable functions
* Render a system default template, which is essentially a PHP template.
*
* @param $file
* The filename of the template to render.
* @param $variables
* A keyed array of variables that will appear in the output.
*
* @return
* The output generated by the template.
*/
function theme_render_template($file, $variables) {
extract($variables, EXTR_SKIP); // Extract the variables to a local namespace
ob_start(); // Start output buffering
include "./$file"; // Include the file
$contents = ob_get_contents(); // Get the contents of the buffer
ob_end_clean(); // End buffering and discard
return $contents; // Return the contents
}
/**
* @defgroup themeable Default theme implementations
* @{
* Functions that display HTML, and which can be customized by themes.
* Functions and templates that present output to the user, and can be
* implemented by themes.
*
* Drupal's presentation layer is a pluggable system known as the theme
* layer. Each theme can take control over most of Drupal's output, and
* has complete control over the CSS.
*
* All functions that produce HTML for display should be themeable. This means
* that they should be named with the theme_ prefix, and invoked using theme()
* rather than being called directly. This allows themes to override the display
* of any Drupal object.
* Inside Drupal, the theme layer is utilized by the use of the theme()
* function, which is passed the name of a component (the theme hook)
* and several arguments. For example, theme('table', $header, $rows);
*
* As of Drupal 6, every theme hook is required to be registered by the
* module that owns it, so that Drupal can tell what to do with it and
* to make it simple for themes to identify and override the behavior
* for these calls.
*
* The theme hooks are registered via hook_theme(), which returns an
* array of arrays with information about the hook. It describes the
* arguments the function or template will need, and provides
* defaults for the template in case they are not filled in. If the default
* implementation is a function, by convention it is named theme_HOOK().
*
* Each module should provide a default implementation for themes that
* it registers. This implementation may be either a function or a template;
* if it is a function it must be specified via hook_theme(). By convention,
* default implementations of theme hooks are named theme_HOOK. Default
* template implementations are stored in the module directory.
*
* Drupal's default template renderer is a simple PHP parsing engine that
* includes the template and stores the output. Drupal's theme engines
* can provide alternate template engines, such as XTemplate, Smarty and
* PHPTal. The most common template engine is PHPTemplate (included with
* Drupal and implemented in phptemplate.engine, which uses Drupal's default
* template renderer.
*
* In order to create theme-specific implementations of these hooks,