From 7e163dbb196c57e1a2bdaba894b8456e78f745b3 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Sun, 29 Sep 2013 08:19:59 +0100
Subject: [PATCH] Issue #1751194 by Cottser, mikl, effulgentsia, benjifisher:
 Introduce hook_theme_suggestions_HOOK() and
 hook_theme_suggestions_HOOK_alter().

---
 core/includes/theme.inc                       | 134 +++++++++---------
 .../Core/Extension/UpdateModuleHandler.php    |   5 +
 core/modules/block/block.module               |  51 ++++---
 .../block/Tests/BlockPreprocessUnitTest.php   |  57 ++++++++
 .../BlockTemplateSuggestionsUnitTest.php      |  13 +-
 core/modules/field/field.module               |  25 ++--
 core/modules/forum/forum.module               |  44 +++---
 core/modules/node/node.module                 |  18 ++-
 core/modules/search/search.pages.inc          |  16 ++-
 .../Tests/Theme/ThemeSuggestionsAlterTest.php | 120 ++++++++++++++++
 core/modules/system/system.module             |  48 +++++++
 .../theme_suggestions_test.info.yml           |   7 +
 .../theme_suggestions_test.module             |  27 ++++
 .../Drupal/theme_test/ThemeTestController.php |  28 ++++
 .../theme-test-specific-suggestions.html.twig |   2 +
 .../theme-test-suggestion-provided.html.twig  |   2 +
 .../theme-test-suggestions.html.twig          |   2 +
 .../modules/theme_test/theme_test.module      |  29 ++++
 .../modules/theme_test/theme_test.routing.yml |  28 ++++
 ...ecific-suggestions--variant--foo.html.twig |   5 +
 ...st-specific-suggestions--variant.html.twig |   5 +
 ...me-test-suggestion-provided--foo.html.twig |   2 +
 ...est-suggestions--module-override.html.twig |   2 +
 ...test-suggestions--theme-override.html.twig |   2 +
 .../tests/themes/test_theme/test_theme.theme  |  40 ++++++
 core/modules/system/theme.api.php             |  61 ++++++++
 core/modules/taxonomy/taxonomy.module         |  17 ++-
 core/modules/views/views.module               |  44 ++++--
 core/modules/views_ui/views_ui.theme.inc      |   8 +-
 29 files changed, 699 insertions(+), 143 deletions(-)
 create mode 100644 core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php
 create mode 100644 core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml
 create mode 100644 core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module
 create mode 100644 core/modules/system/tests/modules/theme_test/templates/theme-test-specific-suggestions.html.twig
 create mode 100644 core/modules/system/tests/modules/theme_test/templates/theme-test-suggestion-provided.html.twig
 create mode 100644 core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig
 create mode 100644 core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig
 create mode 100644 core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig
 create mode 100644 core/modules/system/tests/themes/test_theme/templates/theme-test-suggestion-provided--foo.html.twig
 create mode 100644 core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig
 create mode 100644 core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig

diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 2f4a601021cd..094a339e2bff 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -862,17 +862,16 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  * a noticeable performance penalty.
  *
  * @subsection sub_alternate_suggestions Suggesting Alternate Hooks
- * There are two special variables that these preprocess functions can set:
- * 'theme_hook_suggestion' and 'theme_hook_suggestions'. These will be merged
- * together to form a list of 'suggested' alternate theme hooks to use, in
- * reverse order of priority. theme_hook_suggestion will always be a higher
- * priority than items in theme_hook_suggestions. theme() will use the highest
- * priority implementation that exists. If none exists, theme() will use the
- * implementation for the theme hook it was called with. These suggestions are
- * similar to, and are used for similar reasons as, calling theme() with an
- * array as the $hook parameter (see below). The difference is whether the
- * suggestions are determined by the code that calls theme() or by a preprocess
- * function.
+ * Alternate hooks can be suggested by implementing the hook-specific
+ * hook_theme_suggestions_HOOK_alter() or the generic
+ * hook_theme_suggestions_alter(). These alter hooks are used to manipulate an
+ * array of suggested alternate theme hooks to use, in reverse order of
+ * priority. theme() will use the highest priority implementation that exists.
+ * If none exists, theme() will use the implementation for the theme hook it was
+ * called with. These suggestions are similar to and are used for similar
+ * reasons as calling theme() with an array as the $hook parameter (see below).
+ * The difference is whether the suggestions are determined by the code that
+ * calls theme() or by altering the suggestions via the suggestion alter hooks.
  *
  * @param $hook
  *   The name of the theme hook to call. If the name contains a
@@ -1002,11 +1001,41 @@ function theme($hook, $variables = array()) {
     'theme_hook_original' => $original_hook,
   );
 
-  // Invoke the variable preprocessors, if any. The preprocessors 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 preprocessors of
-  // the base hook, but retain the suggestion as a high priority suggestion to
-  // be used unless overridden by a variable preprocessor function.
+  // Set base hook for later use. For example if '#theme' => 'node__article'
+  // is called, we run hook_theme_suggestions_node_alter() rather than
+  // hook_theme_suggestions_node__article_alter(), and also pass in the base
+  // hook as the last parameter to the suggestions alter hooks.
+  if (isset($info['base hook'])) {
+    $base_theme_hook = $info['base hook'];
+  }
+  else {
+    $base_theme_hook = $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
+  // '#theme' => 'node__article', add it to the suggestions array before
+  // invoking suggestion alter hooks.
+  if (isset($info['base hook'])) {
+    $suggestions[] = $hook;
+  }
+  // Allow suggestions to be altered via hook_theme_suggestions_HOOK_alter().
+  Drupal::moduleHandler()->alter('theme_suggestions_' . $base_theme_hook, $suggestions, $variables);
+
+  // Check if each suggestion exists in the theme registry, and if so,
+  // use it instead of the hook that theme() was called with. For example, a
+  // function may call theme('node', ...), but a module can add
+  // 'node__article' as a suggestion via hook_theme_suggestions_HOOK_alter(),
+  // enabling a theme to have an alternate template file for article nodes.
+  foreach (array_reverse($suggestions) as $suggestion) {
+    if (isset($hooks[$suggestion])) {
+      $info = $hooks[$suggestion];
+      break;
+    }
+  }
+
+  // Invoke the variable preprocessors, if any.
   if (isset($info['base hook'])) {
     $base_hook = $info['base hook'];
     $base_hook_info = $hooks[$base_hook];
@@ -1017,44 +1046,20 @@ function theme($hook, $variables = array()) {
         include_once DRUPAL_ROOT . '/' . $include_file;
       }
     }
+    // Replace the preprocess functions with those from the base hook.
     if (isset($base_hook_info['preprocess functions'])) {
-      $variables['theme_hook_suggestion'] = $hook;
-      $hook = $base_hook;
-      $info = $base_hook_info;
+      // Set a variable for the 'theme_hook_suggestion'. This is used to
+      // maintain backwards compatibility with template engines.
+      $theme_hook_suggestion = $hook;
+      $info['preprocess functions'] = $base_hook_info['preprocess functions'];
     }
   }
   if (isset($info['preprocess functions'])) {
-    $variables['theme_hook_suggestions'] = array();
     foreach ($info['preprocess functions'] as $preprocessor_function) {
       if (function_exists($preprocessor_function)) {
         $preprocessor_function($variables, $hook, $info);
       }
     }
-    // If the preprocess 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 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];
-        break;
-      }
-    }
   }
 
   // Generate the output using either a function or a template.
@@ -1117,6 +1122,16 @@ function theme($hook, $variables = array()) {
     if (isset($info['path'])) {
       $template_file = $info['path'] . '/' . $template_file;
     }
+    // Add the theme suggestions to the variables array just before rendering
+    // the template for backwards compatibility with template engines.
+    $variables['theme_hook_suggestions'] = $suggestions;
+    // For backwards compatibility, pass 'theme_hook_suggestion' on to the
+    // template engine. This is only set when calling a direct suggestion like
+    // '#theme' => 'menu_tree__shortcut_default' when the template exists in the
+    // current theme.
+    if (isset($theme_hook_suggestion)) {
+      $variables['theme_hook_suggestion'] = $theme_hook_suggestion;
+    }
     $output = $render_function($template_file, $variables);
   }
 
@@ -2593,11 +2608,6 @@ function template_preprocess_html(&$variables) {
     drupal_add_html_head($element, $name);
   }
 
-  // Populate the page template suggestions.
-  if ($suggestions = theme_get_suggestions(arg(), 'html')) {
-    $variables['theme_hook_suggestions'] = $suggestions;
-  }
-
   drupal_add_library('system', 'html5shiv', TRUE);
 
   // Render page_top and page_bottom into top level variables.
@@ -2706,11 +2716,6 @@ function template_preprocess_page(&$variables) {
     $variables['node'] = $node;
   }
 
-  // Populate the page template suggestions.
-  if ($suggestions = theme_get_suggestions(arg(), 'page')) {
-    $variables['theme_hook_suggestions'] = $suggestions;
-  }
-
   // Prepare render array for messages. drupal_get_messages() is called later,
   // when this variable is rendered in a theme function or template file.
   $variables['messages'] = array(
@@ -2731,9 +2736,10 @@ function template_preprocess_page(&$variables) {
 /**
  * Generate an array of suggestions from path arguments.
  *
- * This is typically called for adding to the 'theme_hook_suggestions' or
- * 'attributes' class key variables from within preprocess functions, when
- * wanting to base the additional suggestions on the path of the current page.
+ * This is typically called for adding to the suggestions in
+ * hook_theme_suggestions_HOOK_alter() or adding to 'attributes' class key
+ * variables from within preprocess functions, when wanting to base the
+ * additional suggestions or classes on the path of the current page.
  *
  * @param $args
  *   An array of path arguments, such as from function arg().
@@ -2747,9 +2753,8 @@ function template_preprocess_page(&$variables) {
  *
  * @return
  *   An array of suggestions, suitable for adding to
- *   $variables['theme_hook_suggestions'] within a preprocess function or to
- *   $variables['attributes']['class'] if the suggestions represent extra CSS
- *   classes.
+ *   hook_theme_suggestions_HOOK_alter() or to $variables['attributes']['class']
+ *   if the suggestions represent extra CSS classes.
  */
 function theme_get_suggestions($args, $base, $delimiter = '__') {
 
@@ -2923,12 +2928,6 @@ function template_preprocess_maintenance_page(&$variables) {
     $variables['attributes']['class'][] = 'sidebar-' . $variables['layout'];
   }
 
-  // Dead databases will show error messages so supplying this template will
-  // allow themers to override the page and the content completely.
-  if (isset($variables['db_is_active']) && !$variables['db_is_active']) {
-    $variables['theme_hook_suggestion'] = 'maintenance_page__offline';
-  }
-
   $variables['head'] = drupal_get_html_head();
 
   // While this code is used in the installer, the language module may not be
@@ -2989,7 +2988,6 @@ function template_preprocess_region(&$variables) {
 
   $variables['attributes']['class'][] = 'region';
   $variables['attributes']['class'][] = drupal_html_class('region-' . $variables['region']);
-  $variables['theme_hook_suggestions'][] = 'region__' . $variables['region'];
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php b/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php
index 5fcf9901c7b6..7d7ac9c93f3e 100644
--- a/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php
@@ -24,6 +24,11 @@ public function getImplementations($hook) {
     if (substr($hook, -6) === '_alter') {
       return array();
     }
+    // theme() is called during updates and fires hooks, so whitelist the
+    // system module.
+    if (substr($hook, 0, 6) == 'theme_') {
+      return array('system');
+    }
     switch ($hook) {
       // hook_requirements is necessary for updates to work.
       case 'requirements':
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index 8bcce1f7c1a1..f1112d60fb90 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -484,6 +484,39 @@ function block_rebuild() {
   }
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function block_theme_suggestions_block(array $variables) {
+  $suggestions = array();
+
+  $suggestions[] = 'block__' . $variables['elements']['#configuration']['module'];
+  // Hyphens (-) and underscores (_) play a special role in theme suggestions.
+  // Theme suggestions should only contain underscores, because within
+  // drupal_find_theme_templates(), underscores are converted to hyphens to
+  // match template file names, and then converted back to underscores to match
+  // pre-processing and other function names. So if your theme suggestion
+  // contains a hyphen, it will end up as an underscore after this conversion,
+  // and your function names won't be recognized. So, we need to convert
+  // hyphens to underscores in block deltas for the theme suggestions.
+
+  // We can safely explode on : because we know the Block plugin type manager
+  // enforces that delimiter for all derivatives.
+  $parts = explode(':', $variables['elements']['#plugin_id']);
+  $suggestion = 'block';
+  while ($part = array_shift($parts)) {
+    $suggestions[] = $suggestion .= '__' . strtr($part, '-', '_');
+  }
+
+  if ($id = $variables['elements']['#block']->id()) {
+    $config_id = explode('.', $id);
+    $machine_name = array_pop($config_id);
+    $suggestions[] = 'block__' . $machine_name;
+  }
+
+  return $suggestions;
+}
+
 /**
  * Prepares variables for block templates.
  *
@@ -527,29 +560,11 @@ function template_preprocess_block(&$variables) {
   // Add default class for block content.
   $variables['content_attributes']['class'][] = 'content';
 
-  $variables['theme_hook_suggestions'][] = 'block__' . $variables['configuration']['module'];
-  // Hyphens (-) and underscores (_) play a special role in theme suggestions.
-  // Theme suggestions should only contain underscores, because within
-  // drupal_find_theme_templates(), underscores are converted to hyphens to
-  // match template file names, and then converted back to underscores to match
-  // pre-processing and other function names. So if your theme suggestion
-  // contains a hyphen, it will end up as an underscore after this conversion,
-  // and your function names won't be recognized. So, we need to convert
-  // hyphens to underscores in block deltas for the theme suggestions.
-
-  // We can safely explode on : because we know the Block plugin type manager
-  // enforces that delimiter for all derivatives.
-  $parts = explode(':', $variables['plugin_id']);
-  $suggestion = 'block';
-  while ($part = array_shift($parts)) {
-    $variables['theme_hook_suggestions'][] = $suggestion .= '__' . strtr($part, '-', '_');
-  }
   // Create a valid HTML ID and make sure it is unique.
   if ($id = $variables['elements']['#block']->id()) {
     $config_id = explode('.', $id);
     $machine_name = array_pop($config_id);
     $variables['attributes']['id'] = drupal_html_id('block-' . $machine_name);
-    $variables['theme_hook_suggestions'][] = 'block__' . $machine_name;
   }
 }
 
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php
new file mode 100644
index 000000000000..513fda54ee14
--- /dev/null
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block\Tests\BlockPreprocessUnitTest.
+ */
+
+namespace Drupal\block\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Unit tests for template_preprocess_block().
+ */
+class BlockPreprocessUnitTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('block');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Block preprocess',
+      'description' => 'Test the template_preprocess_block() function.',
+      'group' => 'Block',
+    );
+  }
+
+  /**
+   * Tests block classes with template_preprocess_block().
+   */
+  function testBlockClasses() {
+    // Define a block with a derivative to be preprocessed, which includes both
+    // an underscore (not transformed) and a hyphen (transformed to underscore),
+    // and generates possibilities for each level of derivative.
+    // @todo Clarify this comment.
+    $block = entity_create('block', array(
+      'plugin' => 'system_menu_block:admin',
+      'region' => 'footer',
+      'id' => \Drupal::config('system.theme')->get('default') . '.machinename',
+    ));
+
+    $variables = array();
+    $variables['elements']['#block'] = $block;
+    $variables['elements']['#configuration'] = $block->getPlugin()->getConfiguration();
+    $variables['elements']['#plugin_id'] = $block->get('plugin');
+    $variables['elements']['content'] = array();
+    // Test adding a class to the block content.
+    $variables['content_attributes']['class'][] = 'test-class';
+    template_preprocess_block($variables);
+    $this->assertEqual($variables['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes');
+  }
+
+}
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php
index cf50e7880406..8289eb4ed1dd 100644
--- a/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php
@@ -10,7 +10,7 @@
 use Drupal\simpletest\WebTestBase;
 
 /**
- * Unit tests for template_preprocess_block().
+ * Unit tests for block_theme_suggestions_block().
  */
 class BlockTemplateSuggestionsUnitTest extends WebTestBase {
 
@@ -24,13 +24,13 @@ class BlockTemplateSuggestionsUnitTest extends WebTestBase {
   public static function getInfo() {
     return array(
       'name' => 'Block template suggestions',
-      'description' => 'Test the template_preprocess_block() function.',
+      'description' => 'Test the block_theme_suggestions_block() function.',
       'group' => 'Block',
     );
   }
 
   /**
-   * Test if template_preprocess_block() handles the suggestions right.
+   * Tests template suggestions from block_theme_suggestions_block().
    */
   function testBlockThemeHookSuggestions() {
     // Define a block with a derivative to be preprocessed, which includes both
@@ -48,11 +48,8 @@ function testBlockThemeHookSuggestions() {
     $variables['elements']['#configuration'] = $block->getPlugin()->getConfiguration();
     $variables['elements']['#plugin_id'] = $block->get('plugin');
     $variables['elements']['content'] = array();
-    // Test adding a class to the block content.
-    $variables['content_attributes']['class'][] = 'test-class';
-    template_preprocess_block($variables);
-    $this->assertEqual($variables['theme_hook_suggestions'], array('block__system', 'block__system_menu_block', 'block__system_menu_block__admin', 'block__machinename'));
-    $this->assertEqual($variables['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes');
+    $suggestions = block_theme_suggestions_block($variables);
+    $this->assertEqual($suggestions, array('block__system', 'block__system_menu_block', 'block__system_menu_block__admin', 'block__machinename'));
   }
 
 }
diff --git a/core/modules/field/field.module b/core/modules/field/field.module
index 01c9181edaa0..f4a6299dcd1f 100644
--- a/core/modules/field/field.module
+++ b/core/modules/field/field.module
@@ -659,6 +659,22 @@ function field_page_build(&$page) {
   $page['#attached']['css'][$path . '/css/field.module.css'] = array('every_page' => TRUE);
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function field_theme_suggestions_field(array $variables) {
+  $suggestions = array();
+  $element = $variables['element'];
+
+  $suggestions[] = 'field__' . $element['#field_type'];
+  $suggestions[] = 'field__' . $element['#field_name'];
+  $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#bundle'];
+  $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'];
+  $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'];
+
+  return $suggestions;
+}
+
 /**
  * Prepares variables for field templates.
  *
@@ -716,15 +732,6 @@ function template_preprocess_field(&$variables, $hook) {
     $variables['attributes']['class'][] = 'clearfix';
   }
 
-  // Add specific suggestions that can override the default implementation.
-  $variables['theme_hook_suggestions'] = array(
-    'field__' . $element['#field_type'],
-    'field__' . $element['#field_name'],
-    'field__' . $element['#entity_type'] . '__' . $element['#bundle'],
-    'field__' . $element['#entity_type'] . '__' . $element['#field_name'],
-    'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'],
-  );
-
   static $default_attributes;
   if (!isset($default_attributes)) {
     $default_attributes = new Attribute;
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index 77ef530003fe..090d5934bead 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -588,6 +588,33 @@ function forum_preprocess_block(&$variables) {
   }
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function forum_theme_suggestions_forums(array $variables) {
+  $suggestions = array();
+  $tid = $variables['term']->id();
+
+  // Provide separate template suggestions based on what's being output. Topic
+  // ID is also accounted for. Check both variables to be safe then the inverse.
+  // Forums with topic IDs take precedence.
+  if ($variables['forums'] && !$variables['topics']) {
+    $suggestions[] = 'forums__containers';
+    $suggestions[] = 'forums__' . $tid;
+    $suggestions[] = 'forums__containers__' . $tid;
+  }
+  elseif (!$variables['forums'] && $variables['topics']) {
+    $suggestions[] = 'forums__topics';
+    $suggestions[] = 'forums__' . $tid;
+    $suggestions[] = 'forums__topics__' . $tid;
+  }
+  else {
+    $suggestions[] = 'forums__' . $tid;
+  }
+
+  return $suggestions;
+}
+
 /**
  * Prepares variables for forums templates.
  *
@@ -635,23 +662,6 @@ function template_preprocess_forums(&$variables) {
     else {
       $variables['topics'] = array();
     }
-
-    // Provide separate template suggestions based on what's being output. Topic id is also accounted for.
-    // Check both variables to be safe then the inverse. Forums with topic ID's take precedence.
-    if ($variables['forums'] && !$variables['topics']) {
-      $variables['theme_hook_suggestions'][] = 'forums__containers';
-      $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
-      $variables['theme_hook_suggestions'][] = 'forums__containers__' . $variables['tid'];
-    }
-    elseif (!$variables['forums'] && $variables['topics']) {
-      $variables['theme_hook_suggestions'][] = 'forums__topics';
-      $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
-      $variables['theme_hook_suggestions'][] = 'forums__topics__' . $variables['tid'];
-    }
-    else {
-      $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
-    }
-
   }
   else {
     $variables['forums'] = array();
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 376efa447986..24f46ecfd5af 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -641,6 +641,19 @@ function node_preprocess_block(&$variables) {
   }
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function node_theme_suggestions_node(array $variables) {
+  $suggestions = array();
+  $node = $variables['elements']['#node'];
+
+  $suggestions[] = 'node__' . $node->bundle();
+  $suggestions[] = 'node__' . $node->id();
+
+  return $suggestions;
+}
+
 /**
  * Prepares variables for node templates.
  *
@@ -730,11 +743,6 @@ function template_preprocess_node(&$variables) {
   if (isset($variables['preview'])) {
     $variables['attributes']['class'][] = 'preview';
   }
-
-  // Clean up name so there are no underscores.
-  $variables['theme_hook_suggestions'][] = 'node__' . $node->bundle();
-  $variables['theme_hook_suggestions'][] = 'node__' . $node->id();
-
   $variables['content_attributes']['class'][] = 'content';
 }
 
diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc
index 9d4aebee702d..5b1e1a06153a 100644
--- a/core/modules/search/search.pages.inc
+++ b/core/modules/search/search.pages.inc
@@ -74,6 +74,13 @@ function search_view($plugin_id = NULL, $keys = '') {
   return $build;
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function search_theme_suggestions_search_results(array $variables) {
+  return array('search_results__' . $variables['plugin_id']);
+}
+
 /**
  * Prepares variables for search results templates.
  *
@@ -100,7 +107,13 @@ function template_preprocess_search_results(&$variables) {
   // @todo Revisit where this help text is added, see also
   //   http://drupal.org/node/1918856.
   $variables['help'] = search_help('search#noresults', drupal_help_arg());
-  $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['plugin_id'];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function search_theme_suggestions_search_result(array $variables) {
+  return array('search_result__' . $variables['plugin_id']);
 }
 
 /**
@@ -148,7 +161,6 @@ function template_preprocess_search_result(&$variables) {
   // Provide separated and grouped meta information..
   $variables['info_split'] = $info;
   $variables['info'] = implode(' - ', $info);
-  $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['plugin_id'];
 }
 
 /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php
new file mode 100644
index 000000000000..00cd0081aeac
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Theme\ThemeSuggestionsAlterTest.
+ */
+
+namespace Drupal\system\Tests\Theme;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests theme suggestion alter hooks.
+ */
+class ThemeSuggestionsAlterTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('theme_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Theme suggestions alter',
+      'description' => 'Test theme suggestion alter hooks.',
+      'group' => 'Theme',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    theme_enable(array('test_theme'));
+  }
+
+  /**
+   * Tests that hooks to provide theme suggestions work.
+   */
+  function testTemplateSuggestions() {
+    $this->drupalGet('theme-test/suggestion-provided');
+    $this->assertText('Template for testing suggestions provided by the module declaring the theme hook.');
+
+    // Enable test_theme, it contains a template suggested by theme_test.module
+    // in theme_test_theme_suggestions_theme_test_suggestion_provided().
+    config('system.theme')
+      ->set('default', 'test_theme')
+      ->save();
+
+    $this->drupalGet('theme-test/suggestion-provided');
+    $this->assertText('Template overridden based on suggestion provided by the module declaring the theme hook.');
+  }
+
+  /**
+   * Tests that theme suggestion alter hooks work for templates.
+   */
+  function testTemplateSuggestionsAlter() {
+    $this->drupalGet('theme-test/suggestion-alter');
+    $this->assertText('Original template.');
+
+    // Enable test_theme and test that themes can alter template suggestions.
+    config('system.theme')
+      ->set('default', 'test_theme')
+      ->save();
+    $this->drupalGet('theme-test/suggestion-alter');
+    $this->assertText('Template overridden based on new theme suggestion provided by the test_theme theme.');
+
+    // Enable the theme_suggestions_test module to test modules implementing
+    // suggestions alter hooks.
+    \Drupal::moduleHandler()->install(array('theme_suggestions_test'));
+    $this->drupalGet('theme-test/suggestion-alter');
+    $this->assertText('Template overridden based on new theme suggestion provided by a module.');
+  }
+
+  /**
+   * Tests that theme suggestion alter hooks work for specific theme calls.
+   */
+  function testSpecificSuggestionsAlter() {
+    // Test that the default template is rendered.
+    $this->drupalGet('theme-test/specific-suggestion-alter');
+    $this->assertText('Template for testing specific theme calls.');
+
+    config('system.theme')
+      ->set('default', 'test_theme')
+      ->save();
+
+    // Test a specific theme call similar to '#theme' => 'node__article'.
+    $this->drupalGet('theme-test/specific-suggestion-alter');
+    $this->assertText('Template matching the specific theme call.');
+    $this->assertText('theme_test_specific_suggestions__variant', 'Specific theme call is added to the suggestions array.');
+
+    // Ensure that the base hook is used to determine the suggestion alter hook.
+    \Drupal::moduleHandler()->install(array('theme_suggestions_test'));
+    $this->drupalGet('theme-test/specific-suggestion-alter');
+    $this->assertText('Template overridden based on suggestion alter hook determined by the base hook.');
+    $this->assertTrue(strpos($this->drupalGetContent(), 'theme_test_specific_suggestions__variant') < strpos($this->drupalGetContent(), 'theme_test_specific_suggestions__variant__foo'), 'Specific theme call is added to the suggestions array before the suggestions alter hook.');
+  }
+
+  /**
+   * Tests that theme suggestion alter hooks work for theme functions.
+   */
+  function testThemeFunctionSuggestionsAlter() {
+    $this->drupalGet('theme-test/function-suggestion-alter');
+    $this->assertText('Original theme function.');
+
+    // Enable test_theme and test that themes can alter theme suggestions.
+    config('system.theme')
+      ->set('default', 'test_theme')
+      ->save();
+    $this->drupalGet('theme-test/function-suggestion-alter');
+    $this->assertText('Theme function overridden based on new theme suggestion provided by the test_theme theme.');
+
+    // Enable the theme_suggestions_test module to test modules implementing
+    // suggestions alter hooks.
+    \Drupal::moduleHandler()->install(array('theme_suggestions_test'));
+    $this->drupalGet('theme-test/function-suggestion-alter');
+    $this->assertText('Theme function overridden based on new theme suggestion provided by a module.');
+  }
+
+}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 820d57d1955a..8437e4c47c3f 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -909,6 +909,54 @@ function system_menu() {
   return $items;
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function system_theme_suggestions_html(array $variables) {
+  return theme_get_suggestions(arg(), 'html');
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function system_theme_suggestions_page(array $variables) {
+  return theme_get_suggestions(arg(), 'page');
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function system_theme_suggestions_maintenance_page(array $variables) {
+  $suggestions = array();
+
+  // Dead databases will show error messages so supplying this template will
+  // allow themers to override the page and the content completely.
+  $offline = defined('MAINTENANCE_MODE');
+  try {
+    drupal_is_front_page();
+  }
+  catch (Exception $e) {
+    // The database is not yet available.
+    $offline = TRUE;
+  }
+  if ($offline) {
+    $suggestions[] = 'maintenance_page__offline';
+  }
+
+  return $suggestions;
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function system_theme_suggestions_region(array $variables) {
+  $suggestions = array();
+  if (!empty($variables['elements']['#region'])) {
+    $suggestions[] = 'region__' . $variables['elements']['#region'];
+  }
+  return $suggestions;
+}
+
 /**
  * Theme callback for the default batch page.
  */
diff --git a/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml
new file mode 100644
index 000000000000..95f429f2894a
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml
@@ -0,0 +1,7 @@
+name: 'Theme suggestions test'
+type: module
+description: 'Support module for testing theme suggestions.'
+package: Testing
+version: VERSION
+core: 8.x
+hidden: true
diff --git a/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module
new file mode 100644
index 000000000000..9ecaeebd63bc
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Support module for testing theme suggestions.
+ */
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function theme_suggestions_test_theme_suggestions_theme_test_suggestions_alter(array &$suggestions, array $variables) {
+  $suggestions[] = 'theme_test_suggestions__' . 'module_override';
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function theme_suggestions_test_theme_suggestions_theme_test_function_suggestions_alter(array &$suggestions, array $variables) {
+  $suggestions[] = 'theme_test_function_suggestions__' . 'module_override';
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function theme_suggestions_test_theme_suggestions_theme_test_specific_suggestions_alter(array &$suggestions, array $variables) {
+  $suggestions[] = 'theme_test_specific_suggestions__' . 'variant__foo';
+}
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 31bbd93613d0..8da6c62206c0 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
@@ -92,4 +92,32 @@ public function testRequestListener() {
     return $GLOBALS['theme_test_output'];
   }
 
+  /**
+   * Menu callback for testing suggestion alter hooks with template files.
+   */
+  function suggestionProvided() {
+    return array('#theme' => 'theme_test_suggestion_provided');
+  }
+
+  /**
+   * Menu callback for testing suggestion alter hooks with template files.
+   */
+  function suggestionAlter() {
+    return array('#theme' => 'theme_test_suggestions');
+  }
+
+  /**
+   * Menu callback for testing suggestion alter hooks with specific suggestions.
+   */
+  function specificSuggestionAlter() {
+    return array('#theme' => 'theme_test_specific_suggestions__variant');
+  }
+
+  /**
+   * Menu callback for testing suggestion alter hooks with theme functions.
+   */
+  function functionSuggestionAlter() {
+    return array('#theme' => 'theme_test_function_suggestions');
+  }
+
 }
diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-specific-suggestions.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-specific-suggestions.html.twig
new file mode 100644
index 000000000000..6e112fd723af
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-specific-suggestions.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template for testing specific theme calls.
diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestion-provided.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestion-provided.html.twig
new file mode 100644
index 000000000000..c9d96dd0587d
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestion-provided.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template for testing suggestions provided by the module declaring the theme hook.
diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig
new file mode 100644
index 000000000000..dfc848c94d9c
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Original template.
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 08e9c0128695..7c205c88ff85 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.module
+++ b/core/modules/system/tests/modules/theme_test/theme_test.module
@@ -14,6 +14,21 @@ function theme_test_theme($existing, $type, $theme, $path) {
   $items['theme_test_template_test_2'] = array(
     'template' => 'theme_test.template_test',
   );
+  $items['theme_test_suggestion_provided'] = array(
+    'template' => 'theme-test-suggestion-provided',
+    'variables' => array(),
+  );
+  $items['theme_test_specific_suggestions'] = array(
+    'template' => 'theme-test-specific-suggestions',
+    'variables' => array(),
+  );
+  $items['theme_test_suggestions'] = array(
+    'template' => 'theme-test-suggestions',
+    'variables' => array(),
+  );
+  $items['theme_test_function_suggestions'] = array(
+    'variables' => array(),
+  );
   $items['theme_test_foo'] = array(
     'variables' => array('foo' => NULL),
   );
@@ -131,3 +146,17 @@ function template_preprocess_theme_test_render_element(&$variables) {
 function theme_theme_test_render_element_children($variables) {
   return drupal_render($variables['element']);
 }
+
+/**
+ * Returns HTML for a theme function suggestion test.
+ */
+function theme_theme_test_function_suggestions($variables) {
+  return 'Original theme function.';
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function theme_test_theme_suggestions_theme_test_suggestion_provided(array $variables) {
+  return array('theme_test_suggestion_provided__' . 'foo');
+}
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 b75ee53f1cf5..b4a7fd64fe98 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
@@ -41,3 +41,31 @@ theme_test.request_listener:
     _content: '\Drupal\theme_test\ThemeTestController::testRequestListener'
   requirements:
     _access: 'TRUE'
+
+suggestion_alter:
+  path: '/theme-test/suggestion-alter'
+  defaults:
+    _content: '\Drupal\theme_test\ThemeTestController::suggestionAlter'
+  requirements:
+    _permission: 'access content'
+
+suggestion_provided:
+  path: '/theme-test/suggestion-provided'
+  defaults:
+    _content: '\Drupal\theme_test\ThemeTestController::suggestionProvided'
+  requirements:
+    _permission: 'access content'
+
+specific_suggestion_alter:
+  path: '/theme-test/specific-suggestion-alter'
+  defaults:
+    _content: '\Drupal\theme_test\ThemeTestController::specificSuggestionAlter'
+  requirements:
+    _permission: 'access content'
+
+function_suggestion_alter:
+  path: '/theme-test/function-suggestion-alter'
+  defaults:
+    _content: '\Drupal\theme_test\ThemeTestController::functionSuggestionAlter'
+  requirements:
+    _permission: 'access content'
diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig
new file mode 100644
index 000000000000..7e0b485ce227
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig
@@ -0,0 +1,5 @@
+{# Output for Theme API test #}
+Template overridden based on suggestion alter hook determined by the base hook.
+
+<p>Theme hook suggestions:
+{{ theme_hook_suggestions|join("<br />") }}</p>
diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig
new file mode 100644
index 000000000000..655db4e059d5
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig
@@ -0,0 +1,5 @@
+{# Output for Theme API test #}
+Template matching the specific theme call.
+
+<p>Theme hook suggestions:
+{{ theme_hook_suggestions|join("<br />") }}</p>
diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestion-provided--foo.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestion-provided--foo.html.twig
new file mode 100644
index 000000000000..eec7992998b0
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestion-provided--foo.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template overridden based on suggestion provided by the module declaring the theme hook.
diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig
new file mode 100644
index 000000000000..26ce57bdae28
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template overridden based on new theme suggestion provided by a module.
diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig
new file mode 100644
index 000000000000..dee829f9750e
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template overridden based on new theme suggestion provided by the test_theme theme.
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 62b5abf15b41..9b10b2b03d23 100644
--- a/core/modules/system/tests/themes/test_theme/test_theme.theme
+++ b/core/modules/system/tests/themes/test_theme/test_theme.theme
@@ -29,3 +29,43 @@ function test_theme_theme_test__suggestion($variables) {
 function test_theme_theme_test_alter_alter(&$data) {
   $data = 'test_theme_theme_test_alter_alter was invoked';
 }
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function test_theme_theme_suggestions_theme_test_suggestions_alter(array &$suggestions, array $variables) {
+  // Theme alter hooks run after module alter hooks, so add this theme
+  // suggestion to the beginning of the array so that the suggestion added by
+  // the theme_suggestions_test module can be picked up when that module is
+  // enabled.
+  array_unshift($suggestions, 'theme_test_suggestions__' . 'theme_override');
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function test_theme_theme_suggestions_theme_test_function_suggestions_alter(array &$suggestions, array $variables) {
+  // Theme alter hooks run after module alter hooks, so add this theme
+  // suggestion to the beginning of the array so that the suggestion added by
+  // the theme_suggestions_test module can be picked up when that module is
+  // enabled.
+  array_unshift($suggestions, 'theme_test_function_suggestions__' . 'theme_override');
+}
+
+/**
+ * Returns HTML for a theme function suggestion test.
+ *
+ * Implements the theme_test_function_suggestions__theme_override suggestion.
+ */
+function test_theme_theme_test_function_suggestions__theme_override($variables) {
+  return 'Theme function overridden based on new theme suggestion provided by the test_theme theme.';
+}
+
+/**
+ * Returns HTML for a theme function suggestion test.
+ *
+ * Implements the theme_test_function_suggestions__module_override suggestion.
+ */
+function test_theme_theme_test_function_suggestions__module_override($variables) {
+  return 'Theme function overridden based on new theme suggestion provided by a module.';
+}
diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php
index ebda67117d44..34758dafda12 100644
--- a/core/modules/system/theme.api.php
+++ b/core/modules/system/theme.api.php
@@ -157,6 +157,67 @@ function hook_preprocess_HOOK(&$variables) {
   $variables['attributes']['typeof'] = array('foaf:Image');
 }
 
+/**
+ * Provides alternate named suggestions for a specific theme hook.
+ *
+ * This hook allows the module implementing hook_theme() for a theme hook to
+ * provide alternative theme function or template name suggestions. This hook is
+ * only invoked for the first module implementing hook_theme() for a theme hook.
+ *
+ * HOOK is the least-specific version of the hook being called. For example, if
+ * '#theme' => 'node__article' is called, then node_theme_suggestions_node()
+ * will be invoked, not node_theme_suggestions_node__article(). The specific
+ * hook called (in this case 'node__article') is available in
+ * $variables['theme_hook_original'].
+ *
+ * @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.
+ *
+ * @return array
+ *   An array of theme suggestions.
+ *
+ * @see hook_theme_suggestions_HOOK_alter()
+ */
+function hook_theme_suggestions_HOOK(array $variables) {
+  $suggestions = array();
+
+  $suggestions[] = 'node__' . $variables['elements']['#langcode'];
+
+  return $suggestions;
+}
+
+/**
+ * Alters named suggestions for a specific theme hook.
+ *
+ * This hook allows any module or theme to provide altenative theme function or
+ * template name suggestions and reorder or remove suggestions provided by
+ * hook_theme_suggestions_HOOK() or by earlier invocations of this hook.
+ *
+ * HOOK is the least-specific version of the hook being called. For example, if
+ * '#theme' => 'node__article' is called, then node_theme_suggestions_node()
+ * will be invoked, not node_theme_suggestions_node__article(). The specific
+ * hook called (in this case 'node__article') is available in
+ * $variables['theme_hook_original'].
+ *
+ * @todo Add @code sample.
+ *
+ * @param array $suggestions
+ *   An array of theme suggestions.
+ * @param array $variables
+ *   An array of variables passed to the theme hook. Note that this hook is
+ *   invoked before any preprocessing.
+ *
+ * @see hook_theme_suggestions_HOOK()
+ */
+function hook_theme_suggestions_HOOK_alter(array &$suggestions, array $variables) {
+  if (empty($variables['header'])) {
+    $suggestions[] = 'hookname__' . 'no_header';
+  }
+}
+
 /**
  * Respond to themes being enabled.
  *
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 4f9cec1be3a5..62eb1281002e 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -406,6 +406,20 @@ function taxonomy_term_view_multiple(array $terms, $view_mode = 'full', $langcod
   return entity_view_multiple($terms, $view_mode, $langcode);
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function taxonomy_theme_suggestions_taxonomy_term(array $variables) {
+  $suggestions = array();
+
+  $term = $variables['elements']['#term'];
+
+  $suggestions[] = 'taxonomy_term__' . $term->bundle();
+  $suggestions[] = 'taxonomy_term__' . $term->id();
+
+  return $suggestions;
+}
+
 /**
  * Prepares variables for taxonomy term templates.
  *
@@ -447,9 +461,6 @@ function template_preprocess_taxonomy_term(&$variables) {
   $variables['attributes']['class'][] = 'taxonomy-term';
   $vocabulary_name_css = str_replace('_', '-', $term->bundle());
   $variables['attributes']['class'][] = 'vocabulary-' . $vocabulary_name_css;
-
-  $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->bundle();
-  $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->id();
 }
 
 /**
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index c238f4f77c56..7a6b346cd3f3 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -261,15 +261,13 @@ function views_preprocess_node(&$variables) {
   // \Drupal\views\Plugin\views\row\EntityRow::preRender().
   if (!empty($variables['node']->view) && $variables['node']->view->storage->id()) {
     $variables['view'] = $variables['node']->view;
-    $variables['theme_hook_suggestions'][] = 'node__view__' . $variables['node']->view->storage->id();
-    if (!empty($variables['node']->view->current_display)) {
-      $variables['theme_hook_suggestions'][] = 'node__view__' . $variables['node']->view->storage->id() . '__' . $variables['node']->view->current_display;
-
-      // If a node is being rendered in a view, and the view does not have a path,
-      // prevent drupal from accidentally setting the $page variable:
-      if ($variables['page'] && $variables['view_mode'] == 'full' && !$variables['view']->display_handler->hasPath()) {
-        $variables['page'] = FALSE;
-      }
+    // If a node is being rendered in a view, and the view does not have a path,
+    // prevent drupal from accidentally setting the $page variable:
+    if (!empty($variables['view']->current_display)
+        && $variables['page']
+        && $variables['view_mode'] == 'full'
+        && !$variables['view']->display_handler->hasPath()) {
+      $variables['page'] = FALSE;
     }
   }
 
@@ -279,6 +277,19 @@ function views_preprocess_node(&$variables) {
   }
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function views_theme_suggestions_node_alter(array &$suggestions, array $variables) {
+  $node = $variables['elements']['#node'];
+  if (!empty($node->view) && $node->view->storage->id()) {
+    $suggestions[] = 'node__view__' . $node->view->storage->id();
+    if (!empty($node->view->current_display)) {
+      $suggestions[] = 'node__view__' . $node->view->storage->id() . '__' . $node->view->current_display;
+    }
+  }
+}
+
 /**
  * A theme preprocess function to automatically allow view-based node
  * templates if called from a view.
@@ -288,9 +299,18 @@ function views_preprocess_comment(&$variables) {
   // \Drupal\views\Plugin\views\row\EntityRow::preRender().
   if (!empty($variables['comment']->view) && $variables['comment']->view->storage->id()) {
     $variables['view'] = &$variables['comment']->view;
-    $variables['theme_hook_suggestions'][] = 'comment__view__' . $variables['comment']->view->storage->id();
-    if (!empty($variables['node']->view->current_display)) {
-      $variables['theme_hook_suggestions'][] = 'comment__view__' . $variables['comment']->view->storage->id() . '__' . $variables['comment']->view->current_display;
+  }
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function views_theme_suggestions_comment_alter(array &$suggestions, array $variables) {
+  $comment = $variables['elements']['#comment'];
+  if (!empty($comment->view) && $comment->view->storage->id()) {
+    $suggestions[] = 'comment__view__' . $comment->view->storage->id();
+    if (!empty($comment->view->current_display)) {
+      $suggestions[] = 'comment__view__' . $comment->view->storage->id() . '__' . $comment->view->current_display;
     }
   }
 }
diff --git a/core/modules/views_ui/views_ui.theme.inc b/core/modules/views_ui/views_ui.theme.inc
index 8ae5733c4868..38a8fe4f52c4 100644
--- a/core/modules/views_ui/views_ui.theme.inc
+++ b/core/modules/views_ui/views_ui.theme.inc
@@ -483,5 +483,11 @@ function template_preprocess_views_ui_view_preview_section(&$variables) {
     );
     $variables['links'] = $build;
   }
-  $variables['theme_hook_suggestions'][] = 'views_ui_view_preview_section__' . $variables['section'];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function views_ui_theme_suggestions_views_ui_view_preview_section(array $variables) {
+  return array('views_ui_view_preview_section__' . $variables['section']);
 }
-- 
GitLab