From 9b0c7693cac9cfdbdfad5661be67277f8fbf90ae Mon Sep 17 00:00:00 2001
From: Mark Carver <mark.carver@me.com>
Date: Sat, 26 Oct 2013 15:19:01 -0700
Subject: Issue #2035055 by Mark Carver, c4rl: Introduce
 hook_theme_prepare[_alter]() and deprecate hook_preprocess_HOOK()

---
 core/includes/theme.inc                            | 135 +++++++++++++------
 .../Drupal/system/Tests/Theme/ThemePrepareTest.php |  53 ++++++++
 .../lib/Drupal/theme_test/ThemeTestController.php  |  18 +++
 .../tests/modules/theme_test/theme_test.module     |  79 +++++++++++
 .../modules/theme_test/theme_test.routing.yml      |  22 ++-
 .../tests/themes/test_theme/test_theme.theme       |  57 ++++++++
 core/modules/system/theme.api.php                  | 148 ++++++++++++++++++++-
 7 files changed, 466 insertions(+), 46 deletions(-)
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Theme/ThemePrepareTest.php

diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index b1ca7c0..4f1fb61 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -830,37 +830,20 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  * engine, it overrides the default implementation of the 'page' theme hook by
  * containing a 'page.html.twig' file within its folder structure).
  *
- * @subsection sub_preprocess_templates Preprocessing for Template Files
- * If the implementation is a template file, several functions are called before
- * the template file is invoked to modify the $variables array. These make up
- * the "preprocessing" phase, and are executed (if they exist), in the following
- * order (note that in the following list, HOOK indicates the theme hook name,
- * MODULE indicates a module name, THEME indicates a theme name, and ENGINE
- * indicates a theme engine name):
- * - template_preprocess(&$variables, $hook): Creates a default set of variables
- *   for all theme hooks with template implementations.
- * - template_preprocess_HOOK(&$variables): Should be implemented by the module
- *   that registers the theme hook, to set up default variables.
- * - MODULE_preprocess(&$variables, $hook): hook_preprocess() is invoked on all
- *   implementing modules.
- * - MODULE_preprocess_HOOK(&$variables): hook_preprocess_HOOK() is invoked on
- *   all implementing modules, so that modules that didn't define the theme hook
- *   can alter the variables.
- * - ENGINE_engine_preprocess(&$variables, $hook): Allows the theme engine to
- *   set necessary variables for all theme hooks with template implementations.
- * - ENGINE_engine_preprocess_HOOK(&$variables): Allows the theme engine to set
- *   necessary variables for the particular theme hook.
- * - THEME_preprocess(&$variables, $hook): Allows the theme to set necessary
- *   variables for all theme hooks with template implementations.
- * - THEME_preprocess_HOOK(&$variables): Allows the theme to set necessary
- *   variables specific to the particular theme hook.
- *
- * @subsection sub_preprocess_theme_funcs Preprocessing for Theme Functions
- * If the implementation is a function, only the theme-hook-specific preprocess
- * functions (the ones ending in _HOOK) are called from the list above. This is
- * because theme hooks with function implementations need to be fast, and
- * calling the non-theme-hook-specific preprocess functions for them would incur
- * a noticeable performance penalty.
+ * @subsection sub_prepare_variables Preparing Variables for Theme Hooks
+ * Several functions are invoked before theme functions or template files are
+ * provided their available $variables. These functions make up the "prepare"
+ * phase, and are executed (if they exist), in the following
+ * order (HOOK indicates the profile, module, theme engine or theme name,
+ * THEME_HOOK indicates the name of the specific theme hook called):
+ * - HOOK_theme_prepare(&$variables, $hook): Prepare a set of variables
+ *   for all theme hook implementations.
+ * - HOOK_theme_prepare_THEME_HOOK(&$variables): Prepare a set of
+ *   variables for a specific theme hook implementation.
+ * - HOOK_theme_prepare_alter(&$variables, $hook): Alter a set of prepared
+ *   variables for all theme hook implementations.
+ * - HOOK_theme_prepare_THEME_HOOK_alter(&$variables, $context): Alter a set of
+ *   prepared variables for a specific theme hook implementation.
  *
  * @subsection sub_alternate_suggestions Suggesting Alternate Hooks
  * Alternate hooks can be suggested by implementing the hook-specific
@@ -876,11 +859,11 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  *
  * @param $hook
  *   The name of the theme hook to call. If the name contains a
- *   double-underscore ('__') and there isn't an implementation for the full
- *   name, the part before the '__' is checked. This allows a fallback to a
+ *   double-underscore ('__') and there is not an implementation for the full
+ *   name, the name before the '__' is checked. This allows a fallback to a
  *   more generic implementation. For example, if theme('links__node', ...) is
  *   called, but there is no implementation of that theme hook, then the
- *   'links' implementation is used. This process is iterative, so if
+ *   'links' implementation is used instead. This process is iterative, so if
  *   theme('links__contextual__node', ...) is called, theme() checks for the
  *   following implementations, and uses the first one that exists:
  *   - links__contextual__node
@@ -894,7 +877,7 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  *   convention is not desired or is insufficient.
  * @param $variables
  *   An associative array of variables to merge with defaults from the theme
- *   registry, pass to preprocess functions for modification, and finally, pass
+ *   registry, pass to prepare functions for modification, and finally, pass
  *   to the function or template implementing the theme hook. Alternatively,
  *   this can be a renderable array, in which case, its properties are mapped to
  *   variables expected by the theme hook implementations.
@@ -907,6 +890,8 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  * @see themeable
  * @see hook_theme()
  * @see template_preprocess()
+ *
+ * @throws Exception
  */
 function theme($hook, $variables = array()) {
   static $default_attributes;
@@ -997,10 +982,6 @@ function theme($hook, $variables = array()) {
   elseif (!empty($info['render element'])) {
     $variables += array($info['render element'] => array());
   }
-  // Supply original caller info.
-  $variables += array(
-    'theme_hook_original' => $original_hook,
-  );

   // Set base hook for later use. For example if '#theme' => 'node__article'
   // is called, we run hook_theme_suggestions_node_alter() rather than
@@ -1013,6 +994,12 @@ function theme($hook, $variables = array()) {
     $base_theme_hook = $hook;
   }

+  // Supply original caller info.
+  $variables += array(
+    'theme_hook' => $base_theme_hook,
+    'theme_hook_original' => $original_hook,
+  );
+
   // Invoke hook_theme_suggestions_HOOK().
   $suggestions = Drupal::moduleHandler()->invokeAll('theme_suggestions_' . $base_theme_hook, array($variables));
   // If theme() was invoked with a direct theme suggestion like
@@ -1055,6 +1042,35 @@ function theme($hook, $variables = array()) {
       $info['preprocess functions'] = $base_hook_info['preprocess functions'];
     }
   }
+
+  // A list of hooks that will be invoked for the given theme hook.
+  $prepare_hooks = array(
+    'theme_prepare',
+    'theme_prepare_' . $base_theme_hook,
+  );
+  // If the original theme hook is not the same as the base theme hook, then
+  // invoke the original theme hook suggestion after the base theme hook.
+  if ($hook !== $original_hook) {
+    $prepare_hooks[] = 'theme_prepare_' . $original_hook;
+  }
+
+  // Invokes hook_theme_prepare(), hook_theme_prepare_THEME_HOOK() for enabled
+  // modules.
+  foreach ($prepare_hooks as $prepare_hook) {
+    $variables = NestedArray::mergeDeep($variables, (array) Drupal::moduleHandler()->invokeAll($prepare_hook, array($variables, $base_theme_hook)));
+  }
+
+  // Invokes hook_theme_prepare(), hook_theme_prepare_THEME_HOOK() for the active
+  // theme, which takes precedence over modules.
+  // @todo Replace with Drupal::themeHandler()->invoke() once implemented.
+  // @see https://drupal.org/node/2029819
+  foreach ($prepare_hooks as $prepare_hook) {
+    $variables = NestedArray::mergeDeep($variables, (array) theme_invoke($prepare_hook, array($variables, $base_theme_hook)));
+  }
+
+  // Invokes hook_preprocess() and hook_preprocess_HOOK() implementations. This
+  // phase is deprecated and is run in between hook_theme_prepare[_THEME_HOOK]()
+  // and hook_theme_prepare[_THEME_HOOK]_alter().
   if (isset($info['preprocess functions'])) {
     foreach ($info['preprocess functions'] as $preprocessor_function) {
       if (function_exists($preprocessor_function)) {
@@ -1063,6 +1079,14 @@ function theme($hook, $variables = array()) {
     }
   }

+  // Invokes hook_theme_prepare_alter(), hook_theme_prepare_THEME_ID_alter()
+  // for enabled modules.
+  // @todo Add Drupal::themeHandler()->alter() invocations once implemented.
+  // @see https://drupal.org/node/2029819
+  foreach ($prepare_hooks as $prepare_hook) {
+    Drupal::moduleHandler()->alter($prepare_hook, $variables, $base_theme_hook);
+  }
+
   // Generate the output using either a function or a template.
   $output = '';
   if (isset($info['function'])) {
@@ -1548,6 +1572,39 @@ function theme_disable($theme_list) {
 }

 /**
+ * Rudimentary and temporary method for invoking theme hooks.
+ *
+ * @param string $hook
+ *   The hook name to invoke.
+ * @param array $args
+ *   The array of arguments to pass to hook invocation.
+ *
+ * @return mixed
+ *   The value returned varies on which hook is invoked.
+ *
+ * @todo Remove once a proper Drupal::themeHandler() is created.
+ * @see https://drupal.org/node/2029819
+ */
+function theme_invoke($hook, $args = array()) {
+  // Allow the active theme to invoke hooks after the theme system has been
+  // initialized.
+  global $theme, $base_theme_info;
+  if (isset($theme)) {
+    $theme_keys = array();
+    foreach ($base_theme_info as $base) {
+      $theme_keys[] = $base->name;
+    }
+    $theme_keys[] = $theme;
+    foreach ($theme_keys as $theme_key) {
+      $function = $theme_key . '_' . $hook;
+      if (function_exists($function)) {
+        return call_user_func_array($function, $args);
+      }
+    }
+  }
+}
+
+/**
  * @addtogroup themeable
  * @{
  */
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemePrepareTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemePrepareTest.php
new file mode 100644
index 0000000..8b1ed5d
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemePrepareTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Theme\ThemePrepareTest.
+ */
+
+namespace Drupal\system\Tests\Theme;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests hook_theme_prepare() and hook_theme_prepare_alter() implementations.
+ */
+class ThemePrepareTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('theme_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Theme Prepare API',
+      'description' => 'Test theme prepare and alter functions.',
+      'group' => 'Theme',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    theme_enable(array('test_theme'));
+  }
+
+  function testThemePrepare() {
+    $this->drupalGet('theme-test/prepare');
+    $this->assertText('Inserted variable1 via hook_theme_prepare().');
+    $this->assertText('Inserted variable2 via hook_theme_prepare_BASE_THEME_ID().');
+    $this->assertText('Inserted themeVariable1 via theme hook_theme_prepare().');
+    $this->assertText('Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID().');
+  }
+
+  function testThemePrepareAlter() {
+    $this->drupalGet('theme-test/prepare-alter');
+    $this->assertText('Inserted variable1 via hook_theme_prepare() was altered by hook_theme_prepare_alter().');
+    $this->assertText('Inserted variable2 via hook_theme_prepare_BASE_THEME_ID() was altered by hook_theme_prepare_BASE_THEME_ID_alter().');
+    $this->assertText('Inserted themeVariable1 via theme hook_theme_prepare() was altered by theme hook_theme_prepare_alter().');
+    $this->assertText('Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID() was altered by theme hook_theme_prepare_BASE_THEME_ID_alter().');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php
index 8da6c62..1389cea 100644
--- a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php
+++ b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php
@@ -120,4 +120,22 @@ function functionSuggestionAlter() {
     return array('#theme' => 'theme_test_function_suggestions');
   }

+  /**
+   * Menu callback for testing hook_theme_prepare().
+   */
+  function prepare() {
+    return array(
+      '#theme' => 'theme_test_prepare_variable',
+    );
+  }
+
+  /**
+   * Menu callback for testing hook_theme_prepare_alter().
+   */
+  function prepareAlter() {
+    return array(
+      '#theme' => 'theme_test_prepare_alter_variable',
+    );
+  }
+
 }
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module
index badea81..bab08b3 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.module
+++ b/core/modules/system/tests/modules/theme_test/theme_test.module
@@ -42,6 +42,12 @@ function theme_test_theme($existing, $type, $theme, $path) {
   $items['theme_test_function_template_override'] = array(
     'variables' => array(),
   );
+  $items['theme_test_prepare_variable'] = array(
+    'variables' => array('element' => array()),
+  );
+  $items['theme_test_prepare_alter_variable'] = array(
+    'variables' => array('element' => array()),
+  );
   return $items;
 }

@@ -74,6 +80,14 @@ function theme_test_menu() {
     'theme callback' => '_theme_custom_theme',
     'route_name' => 'theme_test.function_template_override',
   );
+  $items['theme-test/prepare'] = array(
+    'theme callback' => '_theme_custom_theme',
+    'route_name' => 'theme_test.prepare',
+  );
+  $items['theme-test/prepare-alter'] = array(
+    'theme callback' => '_theme_custom_theme',
+    'route_name' => 'theme_test.prepare_alter',
+  );
   return $items;
 }
 /**
@@ -166,3 +180,68 @@ function theme_theme_test_function_suggestions($variables) {
 function theme_test_theme_suggestions_theme_test_suggestion_provided(array $variables) {
   return array('theme_test_suggestion_provided__' . 'foo');
 }
+
+function theme_theme_test_prepare_variable($variables) {
+  return drupal_render($variables['element']);
+}
+
+function theme_theme_test_prepare_alter_variable($variables) {
+  return drupal_render($variables['element']);
+}
+
+/**
+ * Implements hook_theme_prepare().
+ */
+function theme_test_theme_prepare($context) {
+  $variables = array();
+  if ($context['theme_hook'] == 'theme_test_prepare_variable' || $context['theme_hook'] == 'theme_test_prepare_alter_variable') {
+    $variables['element'] = array(
+      'variable1' => array(
+        '#markup' => '<p>Inserted variable1 via hook_theme_prepare().</p>',
+      ),
+    );
+  }
+  return $variables;
+}
+
+/**
+ * Implements hook_theme_prepare_BASE_THEME_ID().
+ */
+function theme_test_theme_prepare_theme_test_prepare_variable() {
+  $variables = array();
+  $variables['element'] = array(
+    'variable2' => array(
+      '#markup' => '<p>Inserted variable2 via hook_theme_prepare_BASE_THEME_ID().</p>',
+    ),
+  );
+  return $variables;
+}
+
+/**
+ * Implements hook_theme_prepare_BASE_THEME_ID().
+ */
+function theme_test_theme_prepare_theme_test_prepare_alter_variable() {
+  $variables = array();
+  $variables['element'] = array(
+    'variable2' => array(
+      '#markup' => '<p>Inserted variable2 via hook_theme_prepare_BASE_THEME_ID().</p>',
+    ),
+  );
+  return $variables;
+}
+
+/**
+ * Implements hook_theme_prepare_alter().
+ */
+function theme_test_theme_prepare_alter(&$variables, $context) {
+  if ($context['theme_hook'] == 'theme_test_prepare_alter_variable') {
+    $variables['element']['variable1']['#markup'] = '<p>Inserted variable1 via hook_theme_prepare() was altered by hook_theme_prepare_alter().</p>';
+  }
+}
+
+/**
+ * Implements hook_theme_prepare_BASE_THEME_ID_alter().
+ */
+function theme_test_theme_prepare_theme_test_prepare_alter_variable_alter(&$variables) {
+  $variables['element']['variable2']['#markup'] = '<p>Inserted variable2 via hook_theme_prepare_BASE_THEME_ID() was altered by hook_theme_prepare_BASE_THEME_ID_alter().</p>';
+}
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
index b4a7fd6..53842d4 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
+++ b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
@@ -42,30 +42,44 @@ theme_test.request_listener:
   requirements:
     _access: 'TRUE'

-suggestion_alter:
+theme_test.suggestion_alter:
   path: '/theme-test/suggestion-alter'
   defaults:
     _content: '\Drupal\theme_test\ThemeTestController::suggestionAlter'
   requirements:
     _permission: 'access content'

-suggestion_provided:
+theme_test.suggestion_provided:
   path: '/theme-test/suggestion-provided'
   defaults:
     _content: '\Drupal\theme_test\ThemeTestController::suggestionProvided'
   requirements:
     _permission: 'access content'

-specific_suggestion_alter:
+theme_test.specific_suggestion_alter:
   path: '/theme-test/specific-suggestion-alter'
   defaults:
     _content: '\Drupal\theme_test\ThemeTestController::specificSuggestionAlter'
   requirements:
     _permission: 'access content'

-function_suggestion_alter:
+theme_test.function_suggestion_alter:
   path: '/theme-test/function-suggestion-alter'
   defaults:
     _content: '\Drupal\theme_test\ThemeTestController::functionSuggestionAlter'
   requirements:
     _permission: 'access content'
+
+theme_test.prepare:
+  path: '/theme-test/prepare'
+  defaults:
+    _content: '\Drupal\theme_test\ThemeTestController::prepare'
+  requirements:
+    _permission: 'access content'
+
+theme_test.prepare_alter:
+  path: '/theme-test/prepare-alter'
+  defaults:
+    _content: '\Drupal\theme_test\ThemeTestController::prepareAlter'
+  requirements:
+    _permission: 'access content'
diff --git a/core/modules/system/tests/themes/test_theme/test_theme.theme b/core/modules/system/tests/themes/test_theme/test_theme.theme
index 9b10b2b..2fec7d3 100644
--- a/core/modules/system/tests/themes/test_theme/test_theme.theme
+++ b/core/modules/system/tests/themes/test_theme/test_theme.theme
@@ -69,3 +69,60 @@ function test_theme_theme_test_function_suggestions__theme_override($variables)
 function test_theme_theme_test_function_suggestions__module_override($variables) {
   return 'Theme function overridden based on new theme suggestion provided by a module.';
 }
+
+/**
+ * Implements hook_theme_prepare().
+ */
+function test_theme_theme_prepare($context) {
+  $variables = array();
+  if ($context['theme_hook'] == 'theme_test_prepare_variable' || $context['theme_hook'] == 'theme_test_prepare_alter_variable') {
+    $variables['element'] = array(
+      'themeVariable1' => array(
+        '#markup' => '<p>Inserted themeVariable1 via theme hook_theme_prepare().</p>',
+      ),
+    );
+  }
+  return $variables;
+}
+
+/**
+ * Implements hook_theme_prepare_THEME_HOOK().
+ */
+function test_theme_theme_prepare_theme_test_prepare_variable() {
+  $variables = array();
+  $variables['element'] = array(
+    'themeVariable2' => array(
+      '#markup' => '<p>Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID().</p>',
+    ),
+  );
+  return $variables;
+}
+
+/**
+ * Implements hook_theme_prepare_THEME_HOOK().
+ */
+function test_theme_theme_prepare_theme_test_prepare_alter_variable() {
+  $variables = array();
+  $variables['element'] = array(
+    'themeVariable2' => array(
+      '#markup' => '<p>Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID().</p>',
+    ),
+  );
+  return $variables;
+}
+
+/**
+ * Implements hook_theme_prepare_alter().
+ */
+function test_theme_theme_prepare_alter(&$variables, $context) {
+  if ($context['theme_hook'] == 'theme_test_prepare_alter_variable') {
+    $variables['element']['themeVariable1']['#markup'] = '<p>Inserted themeVariable1 via theme hook_theme_prepare() was altered by theme hook_theme_prepare_alter().</p>';
+  }
+}
+
+/**
+ * Implements hook_theme_prepare_BASE_THEME_ID_alter().
+ */
+function test_theme_theme_prepare_theme_test_prepare_alter_variable_alter(&$variables) {
+  $variables['element']['themeVariable2']['#markup'] = '<p>Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID() was altered by theme hook_theme_prepare_BASE_THEME_ID_alter().</p>';
+}
diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php
index 34758da..b8e24fe 100644
--- a/core/modules/system/theme.api.php
+++ b/core/modules/system/theme.api.php
@@ -104,9 +104,15 @@ function hook_form_system_theme_settings_alter(&$form, &$form_state) {
  *   The variables array (modify in place).
  * @param $hook
  *   The name of the theme hook.
+ *
+ * @deprecated
+ * @see hook_theme_prepare()
  */
 function hook_preprocess(&$variables, $hook) {
- static $hooks;
+  // This hook has been deprecated, do not use it. Use the following instead:
+  // hook_theme_prepare().
+
+  static $hooks;

   // Add contextual links to the variables, if the user has permission.

@@ -150,14 +156,150 @@ function hook_preprocess(&$variables, $hook) {
  *
  * @param $variables
  *   The variables array (modify in place).
+ *
+ * @deprecated
+ * @see hook_theme_prepare_THEME_HOOK()
  */
 function hook_preprocess_HOOK(&$variables) {
+  // This hook has been deprecated, do not use it. Use the following instead:
+  // hook_theme_prepare_THEME_HOOK().
+
   // This example is from rdf_preprocess_image(). It adds an RDF attribute
   // to the image hook's variables.
   $variables['attributes']['typeof'] = array('foaf:Image');
 }

 /**
+ * Prepares variables for theme functions and templates.
+ *
+ * This hook allows any module or theme to prepare variables for any theme hook.
+ * hook_theme_prepare_THEME_HOOK() can be used to prepare variables for a
+ * specific theme hook.
+ *
+ * For more detailed information, see theme().
+ *
+ * @param array $variables
+ *   The associative array of variables, passed by reference (modify in place).
+ * @param string $hook
+ *   The name of the theme hook called.
+ *
+ * @see hook_theme_prepare_alter()
+ * @see hook_theme_prepare_THEME_HOOK()
+ * @see hook_theme_prepare_THEME_HOOK_alter()
+ */
+function hook_theme_prepare(&$variables, $hook) {
+  static $hooks;
+
+  // Add contextual links to the variables, if the user has permission.
+  if (!user_access('access contextual links')) {
+    return;
+  }
+
+  if (!isset($hooks)) {
+    $hooks = theme_get_registry();
+  }
+
+  // Determine the primary theme function argument.
+  if (isset($hooks[$hook]['variables'])) {
+    $keys = array_keys($hooks[$hook]['variables']);
+    $key = $keys[0];
+  }
+  else {
+    $key = $hooks[$hook]['render element'];
+  }
+
+  if (isset($key) && !empty($variables[$key])) {
+    $element = $variables[$key];
+  }
+
+  if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) {
+    $variables['title_suffix']['contextual_links'] = contextual_links_view($element);
+    if (!empty($variables['title_suffix']['contextual_links'])) {
+      $variables['attributes']['class'][] = 'contextual-links-region';
+    }
+  }
+}
+
+/**
+ * Alters the prepared variables used in theme functions and templates.
+ *
+ * This hook allows any module or theme to alter existing variables provided by
+ * earlier invocations of hook_theme_prepare() or
+ * hook_theme_prepare_THEME_HOOK(). This hook should only be used when it is
+ * needed to override or add to the $variables array for a theme hook it did not
+ * define.
+ *
+ * For more detailed information, see theme().
+ *
+ * @param array $variables
+ *   The associative array of variables, passed by reference (modify in place).
+ * @param string $hook
+ *   The name of the theme hook called.
+ *
+ * @see hook_theme_prepare()
+ * @see hook_theme_prepare_THEME_HOOK()
+ * @see hook_theme_prepare_THEME_HOOK_alter()
+ */
+function hook_theme_prepare_alter(&$variables, $hook) {
+  // Replace the name of the user whom authored article nodes.
+  if ($hook === 'node__article') {
+    $variables['name'] = t('Anonymous Unicorn');
+  }
+}
+
+/**
+ * Prepares variables for a specific theme hook or theme hook suggestion.
+ *
+ * This hook allow any module or theme to prepare theme variables for a specific
+ * theme hook or theme hook suggestion.
+ *
+ * THEME_HOOK can be either the base theme hook or a more specific suggestion
+ * using the double-underscore ('__') notation.
+ *
+ * For more detailed information, see theme().
+ *
+ * @param array $variables
+ *   The associative array of variables, passed by reference (modify in place).
+ *
+ * @see rdf_preprocess_image()
+ * @see hook_theme_prepare_THEME_HOOK_alter()
+ * @see hook_theme_prepare_alter()
+ * @see hook_theme_prepare()
+ */
+function hook_theme_prepare_THEME_HOOK(&$variables) {
+  // This example is from rdf_preprocess_image(). It adds an RDF attribute
+  // to the image hook's variables.
+  $variables['attributes']['typeof'] = array('foaf:Image');
+}
+
+/**
+ * Alters prepared variables for a specific theme hook or theme hook suggestion.
+ *
+ * This hook allows any module or theme to alter existing variables provided by
+ * earlier invocations of hook_theme_prepare() or
+ * hook_theme_prepare_THEME_HOOK(). It should only be used when it is needed
+ * to override or add to the theme variable for a theme hook it did not define.
+ *
+ * THEME_HOOK can be either the base theme hook or a more specific suggestion
+ * using the double-underscore ('__') notation.
+ *
+ * For more detailed information, see theme().
+ *
+ * @param array $variables
+ *   The associative array of variables, passed by reference (modify in place).
+ *
+ * @see hook_theme_prepare_THEME_HOOK()
+ * @see hook_theme_prepare_alter()
+ * @see hook_theme_prepare()
+ */
+function hook_theme_prepare_THEME_HOOK_alter(&$variables) {
+  // Replace the name of the user whom authored article nodes; assuming
+  // THEME_HOOK was "node__article" and placed in node.module, the function name
+  // would read: node_theme_prepare_node__article_alter.
+  $variables['name'] = t('Anonymous Unicorn');
+}
+
+/**
  * Provides alternate named suggestions for a specific theme hook.
  *
  * This hook allows the module implementing hook_theme() for a theme hook to
@@ -173,8 +315,8 @@ function hook_preprocess_HOOK(&$variables) {
  * @todo Add @code sample.
  *
  * @param array $variables
- *   An array of variables passed to the theme hook. Note that this hook is
- *   invoked before any preprocessing.
+ *   An array of variables passed to the theme hook. NOTE: this hook is invoked
+ *   prior to hook_theme_prepare(). Some variables may not always be available.
  *
  * @return array
  *   An array of theme suggestions.
--
1.8.3.4

