diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php index 6850fcb..5c79ed3 100644 --- a/core/lib/Drupal/Core/Theme/ThemeManager.php +++ b/core/lib/Drupal/Core/Theme/ThemeManager.php @@ -152,6 +152,17 @@ protected function theme($hook, $variables = array()) { } $theme_registry = $this->themeRegistry->getRuntime(); + $theme_hooks = (array) $hook; + $hook_pattern = array_pop($theme_hooks); + $derived_hooks = array(); + + // Add all variants for directly called suggestions like + // '#theme' => 'comment__node__article'. + while ($pos = strrpos($hook_pattern, '__')) { + $derived_hooks[] = $hook_pattern; + $hook_pattern = substr($hook_pattern, 0, $pos); + } + $theme_hooks = array_merge($theme_hooks, array_reverse($derived_hooks)); // If an array of hook candidates were passed, use the first one that has an // implementation. @@ -242,9 +253,7 @@ protected function theme($hook, $variables = array()) { // 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; - } + $suggestions = array_merge($suggestions, (array) $theme_hooks); // Invoke hook_theme_suggestions_alter() and // hook_theme_suggestions_HOOK_alter(). diff --git a/core/modules/system/src/Tests/Theme/ThemeSuggestionsAlterTest.php b/core/modules/system/src/Tests/Theme/ThemeSuggestionsAlterTest.php index 1db3583..ece21d1 100644 --- a/core/modules/system/src/Tests/Theme/ThemeSuggestionsAlterTest.php +++ b/core/modules/system/src/Tests/Theme/ThemeSuggestionsAlterTest.php @@ -156,6 +156,30 @@ public function testSuggestionsAlterInclude() { $this->assertText('Function suggested via suggestion alter hook found in include file.', 'Include file loaded for second request.'); } + + /** + * Tests hook_theme_suggestions_HOOK(). + */ + public function testArraySuggestions() { + $this->drupalGet('theme-test/array-suggestions'); + $this->assertText('Template for testing suggestion hooks with an array of theme suggestions.'); + + // Enable the theme_suggestions_test module to test modules implementing + // suggestions hooks. + \Drupal::service('module_installer')->install(array('theme_suggestions_test')); + + // Install test_theme to provide a template for the suggestion added in + // theme_suggestions_test module. + $this->config('system.theme') + ->set('default', 'test_theme') + ->save(); + + $this->resetAll(); + $this->drupalGet('theme-test/array-suggestions'); + $this->assertText('Template overridden based on new theme suggestion provided by a module via hook_theme_suggestions_HOOK().'); + } + + /** * Tests execution order of theme suggestion alter hooks. * diff --git a/core/modules/system/src/Tests/Theme/TwigDebugMarkupTest.php b/core/modules/system/src/Tests/Theme/TwigDebugMarkupTest.php index 4b6d528..027ae71 100644 --- a/core/modules/system/src/Tests/Theme/TwigDebugMarkupTest.php +++ b/core/modules/system/src/Tests/Theme/TwigDebugMarkupTest.php @@ -24,10 +24,11 @@ class TwigDebugMarkupTest extends WebTestBase { public static $modules = array('theme_test', 'node'); /** - * Tests debug markup added to Twig template output. + * {@inheritdoc} */ - function testTwigDebugMarkup() { - $extension = twig_extension(); + protected function setUp() { + parent::setUp(); + \Drupal::service('theme_handler')->install(array('test_theme')); $this->config('system.theme')->set('default', 'test_theme')->save(); $this->drupalCreateContentType(array('type' => 'page')); @@ -37,7 +38,13 @@ function testTwigDebugMarkup() { $this->setContainerParameter('twig.config', $parameters); $this->rebuildContainer(); $this->resetAll(); + } + /** + * Tests debug markup added to Twig template output. + */ + function testTwigDebugMarkup() { + $extension = twig_extension(); $cache = $this->container->get('theme.registry')->get(); // Create array of Twig templates. $templates = drupal_find_theme_templates($cache, $extension, drupal_get_path('theme', 'test_theme')); @@ -83,4 +90,37 @@ function testTwigDebugMarkup() { $this->assertFalse(strpos($output, '') !== FALSE, 'Twig debug markup not found in theme output when debug is disabled.'); } + /** + * Tests debug markup for array suggestions and hook_theme_suggestions_HOOK(). + */ + public function testArraySuggestionsTwigDebugMarkup() { + \Drupal::service('module_installer')->install(array('theme_suggestions_test')); + $extension = twig_extension(); + + $this->drupalGet('theme-test/array-suggestions'); + $this->assertTrue(strpos($this->getRawContent(), "THEME HOOK: 'theme_test_array_suggestions'") !== FALSE, 'Theme call information found.'); + $this->assertTrue(strpos($this->getRawContent(), '') !== FALSE, 'Suggested template files found in order and correct suggestion shown as current template.'); + } + + /** + * Tests debug markup for specific suggestions without implementation. + */ + public function testUnimplementedSpecificSuggestionsTwigDebugMarkup() { + $extension = twig_extension(); + $this->drupalGet('theme-test/specific-suggestion'); + $this->assertTrue(strpos($this->getRawContent(), "THEME HOOK: 'theme_test_specific_suggestions__not__found'") !== FALSE, 'Theme call information found.'); + $this->assertTrue(strpos($this->getRawContent(), '') !== FALSE, 'Suggested template files found in order and base template shown as current template.'); + } + + + /** + * Tests debug markup for specific suggestions. + */ + public function testSpecificSuggestionsTwigDebugMarkup() { + $extension = twig_extension(); + $this->drupalGet('theme-test/specific-suggestion-alter'); + $this->assertTrue(strpos($this->getRawContent(), "THEME HOOK: 'theme_test_specific_suggestions__variant'") !== FALSE, 'Theme call information found.'); + $this->assertTrue(strpos($this->getRawContent(), '') !== FALSE, 'Suggested template files found in order and suggested template shown as current.'); + } + } 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 index 974856a..1d8cbf2 100644 --- 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 @@ -55,3 +55,10 @@ function theme_suggestions_test_theme_suggestions_theme_test_specific_suggestion function theme_suggestions_test_theme_suggestions_theme_test_suggestions_include_alter(array &$suggestions, array $variables, $hook) { $suggestions[] = 'theme_suggestions_test_include'; } + +/** + * Implements hook_theme_suggestions_HOOK(). + */ +function theme_suggestions_test_theme_suggestions_theme_test_array_suggestions(array $variables) { + return array('theme_test_array_suggestions__suggestion_from_hook'); +} diff --git a/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php index 335ed83..d74e10c 100644 --- a/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php +++ b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php @@ -110,6 +110,14 @@ function generalSuggestionAlter() { return array('#theme' => 'theme_test_general_suggestions'); } + + /** + * Menu callback for testing direct suggestions without implementation. + */ + public function specificSuggestion() { + return array('#theme' => 'theme_test_specific_suggestions__not__found'); + } + /** * Menu callback for testing suggestion alter hooks with specific suggestions. */ @@ -133,6 +141,14 @@ function suggestionAlterInclude() { } /** + * Menu callback for testing suggestion hooks with an array of theme hooks. + */ + public function arraySuggestions() { + return array('#theme' => array('theme_test_array_suggestions__not_implemented', 'theme_test_array_suggestions')); + } + + + /** * Controller to ensure that no theme is initialized. * * @return \Symfony\Component\HttpFoundation\JsonResponse diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-array-suggestions.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-array-suggestions.html.twig new file mode 100644 index 0000000..bb52340 --- /dev/null +++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-array-suggestions.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template for testing suggestion hooks with an array of theme suggestions. 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 7ba5c2d..b4778d1 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -52,6 +52,9 @@ function theme_test_theme($existing, $type, $theme, $path) { 'variables' => array(), 'function' => 'theme_theme_test_function_template_override', ); + $items['theme_test_array_suggestions'] = array( + 'variables' => array(), + ); $info['test_theme_not_existing_function'] = array( 'function' => 'test_theme_not_existing_function', ); 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 2dbd188..75a94b2 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 @@ -76,6 +76,14 @@ suggestion_provided: requirements: _access: 'TRUE' +theme_test.specific_suggestion: + path: '/theme-test/specific-suggestion' + defaults: + _controller: '\Drupal\theme_test\ThemeTestController::specificSuggestion' + requirements: + _access: 'TRUE' + + specific_suggestion_alter: path: '/theme-test/specific-suggestion-alter' defaults: @@ -97,6 +105,13 @@ suggestion_alter_include: requirements: _access: 'TRUE' +theme_test.array_suggestions: + path: '/theme-test/array-suggestions' + defaults: + _controller: '\Drupal\theme_test\ThemeTestController::arraySuggestions' + requirements: + _access: 'TRUE' + theme_test.non_html: path: '/theme-test/non-html' defaults: diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-array-suggestions--suggestion-from-hook.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-array-suggestions--suggestion-from-hook.html.twig new file mode 100644 index 0000000..2a00d59 --- /dev/null +++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-array-suggestions--suggestion-from-hook.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template overridden based on new theme suggestion provided by a module via hook_theme_suggestions_HOOK(). diff --git a/core/modules/views/src/Tests/ViewsThemeIntegrationTest.php b/core/modules/views/src/Tests/ViewsThemeIntegrationTest.php index 91de60c..6c67ee8 100644 --- a/core/modules/views/src/Tests/ViewsThemeIntegrationTest.php +++ b/core/modules/views/src/Tests/ViewsThemeIntegrationTest.php @@ -81,4 +81,29 @@ public function testThemedViewPage() { $this->assertRaw("test_basetheme_views_post_render", "Views title changed by test_basetheme.test_basetheme_views_post_render"); } + /** + * Tests the views theme suggestions in debug mode. + */ + public function testThemeSuggestionsInDebug() { + $parameters = $this->container->getParameter('twig.config'); + $parameters['debug'] = TRUE; + $this->setContainerParameter('twig.config', $parameters); + $this->rebuildContainer(); + $this->resetAll(); + + $build = [ + '#type' => 'view', + '#name' => 'test_page_display', + '#display_id' => 'default', + '#arguments' => array(), + ]; + + $output = drupal_render($build, TRUE); + + $this->assertTrue(strpos($output, '* views-view--test-page-display--default.html.twig') !== FALSE); + $this->assertTrue(strpos($output, '* views-view--default.html.twig') !== FALSE); + $this->assertTrue(strpos($output, '* views-view--test-page-display.html.twig') !== FALSE); + $this->assertTrue(strpos($output, 'x views-view.html.twig') !== FALSE); + } + } diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index a7a8b27..01fa777 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -69,31 +69,18 @@ function twig_render_template($template_file, array $variables) { // If there are theme suggestions, reverse the array so more specific // suggestions are shown first. if (!empty($variables['theme_hook_suggestions'])) { - $variables['theme_hook_suggestions'] = array_reverse($variables['theme_hook_suggestions']); - } - // Add debug output for directly called suggestions like - // '#theme' => 'comment__node__article'. - if (strpos($variables['theme_hook_original'], '__') !== FALSE) { - $derived_suggestions[] = $hook = $variables['theme_hook_original']; - while ($pos = strrpos($hook, '__')) { - $hook = substr($hook, 0, $pos); - $derived_suggestions[] = $hook; - } - // Get the value of the base hook (last derived suggestion) and append it - // to the end of all theme suggestions. - $base_hook = array_pop($derived_suggestions); - $variables['theme_hook_suggestions'] = array_merge($derived_suggestions, $variables['theme_hook_suggestions']); - $variables['theme_hook_suggestions'][] = $base_hook; - } - if (!empty($variables['theme_hook_suggestions'])) { + $suggestions = array_reverse($variables['theme_hook_suggestions']); $extension = twig_extension(); $current_template = basename($template_file); - $suggestions = $variables['theme_hook_suggestions']; - // Only add the original theme hook if it wasn't a directly called - // suggestion. - if (strpos($variables['theme_hook_original'], '__') === FALSE) { + + $pos = strpos($variables['theme_hook_original'], '__'); + if ( $pos !== FALSE) { + $suggestions[] = substr($variables['theme_hook_original'], 0, $pos); + } else { $suggestions[] = $variables['theme_hook_original']; } + // Only add the original theme hook if it wasn't a directly called + // suggestion. foreach ($suggestions as &$suggestion) { $template = strtr($suggestion, '_', '-') . $extension; $prefix = ($template == $current_template) ? 'x' : '*';