diff --git a/core/includes/theme.inc b/core/includes/theme.inc index d382b39..2bdbaa3 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 = '__') { @@ -2916,12 +2921,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 @@ -2982,7 +2981,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 5fcf990..7d7ac9c 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 8bcce1f..f1112d6 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -485,6 +485,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. * * Default template: block.html.twig. @@ -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/BlockTemplateSuggestionsUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php similarity index 76% copy from core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php copy to core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php index cf50e78..513fda5 100644 --- a/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php +++ b/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\block\Tests\BlockTemplateSuggestionsUnitTest. + * Contains \Drupal\block\Tests\BlockPreprocessUnitTest. */ namespace Drupal\block\Tests; @@ -12,7 +12,7 @@ /** * Unit tests for template_preprocess_block(). */ -class BlockTemplateSuggestionsUnitTest extends WebTestBase { +class BlockPreprocessUnitTest extends WebTestBase { /** * Modules to enable. @@ -23,16 +23,16 @@ class BlockTemplateSuggestionsUnitTest extends WebTestBase { public static function getInfo() { return array( - 'name' => 'Block template suggestions', + 'name' => 'Block preprocess', 'description' => 'Test the template_preprocess_block() function.', 'group' => 'Block', ); } /** - * Test if template_preprocess_block() handles the suggestions right. + * Tests block classes with template_preprocess_block(). */ - function testBlockThemeHookSuggestions() { + 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. @@ -51,7 +51,6 @@ function testBlockThemeHookSuggestions() { // 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'); } diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php index cf50e78..8289eb4 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 681fc63..4e48cb2 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -786,6 +786,22 @@ function field_page_build(&$page) { } /** + * 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. * * Default template: field.html.twig. @@ -842,15 +858,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 f7bf99a..16aee73 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -581,6 +581,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. * * Default template: forums.html.twig. @@ -627,23 +654,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 b924c12..c06835c 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -640,6 +640,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. * * Default template: node.html.twig. @@ -728,11 +741,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 9d4aebe..5b1e1a0 100644 --- a/core/modules/search/search.pages.inc +++ b/core/modules/search/search.pages.inc @@ -75,6 +75,13 @@ function search_view($plugin_id = NULL, $keys = '') { } /** + * 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. * * Default template: search-results.html.twig. @@ -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 0000000..00cd008 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php @@ -0,0 +1,120 @@ + '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 96d6e70..744a570 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -907,6 +907,54 @@ function system_menu() { } /** + * 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. */ function _system_batch_theme() { 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 0000000..95f429f --- /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 0000000..9ecaeeb --- /dev/null +++ b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module @@ -0,0 +1,27 @@ + '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 0000000..6e112fd --- /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 0000000..c9d96dd --- /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 0000000..dfc848c --- /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 08e9c01..7c205c8 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 b75ee53..b4a7fd6 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 0000000..7e0b485 --- /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. + +

Theme hook suggestions: +{{ theme_hook_suggestions|join("
") }}

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 0000000..655db4e --- /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. + +

Theme hook suggestions: +{{ theme_hook_suggestions|join("
") }}

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 0000000..eec7992 --- /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 0000000..26ce57b --- /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 0000000..dee829f --- /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 62b5abf..9b10b2b 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 ebda671..34758da 100644 --- a/core/modules/system/theme.api.php +++ b/core/modules/system/theme.api.php @@ -158,6 +158,67 @@ function hook_preprocess_HOOK(&$variables) { } /** + * 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. * * @param array $theme_list diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 81537b6..77a9a20 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 } /** + * 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. * * Default template: taxonomy-term.html.twig. @@ -446,9 +460,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 d8e0712..fb987fb 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; } } @@ -280,6 +278,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 8ae5733..38a8fe4 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']); }