reverted: --- b/core/lib/Drupal/Core/Theme/ThemeManager.php +++ a/core/lib/Drupal/Core/Theme/ThemeManager.php @@ -140,46 +140,48 @@ } $theme_registry = $this->themeRegistry->getRuntime(); + + // If an array of hook candidates were passed, use the first one that has an + // implementation. + if (is_array($hook)) { + foreach ($hook as $candidate) { + if ($theme_registry->has($candidate)) { + break; + } - $theme_hooks = (array) $hook; - $hook_found = NULL; - // Check for hook implementation. If an array of hook candidates is passed, - // use the first one that has an implementation. - foreach ($theme_hooks as $candidate) { - if ($theme_registry->has($candidate)) { - $hook_found = $candidate; - break; } + $hook = $candidate; } + // Save the original theme hook, so it can be supplied to theme variable + // preprocess callbacks. + $original_hook = $hook; + + // If there's no implementation, check for more generic fallbacks. + // If there's still no implementation, log an error and return an empty + // string. + if (!$theme_registry->has($hook)) { + // Iteratively strip everything after the last '__' delimiter, until an + // implementation is found. + while ($pos = strrpos($hook, '__')) { + $hook = substr($hook, 0, $pos); + if ($theme_registry->has($hook)) { + break; + } - - $derived_hooks = []; - // Remove the last item of the array which is the original hook and use it - // as a pattern. - $hook_pattern = array_pop($theme_hooks); - // Get the generic fallbacks. If a valid hook implementation has not been - // found, check each fallback until one is. - while ($pos = strrpos($hook_pattern, '__')) { - // Save all derived hooks to be used later as a theme suggestion. - $derived_hooks[] = $hook_pattern; - $hook_pattern = substr($hook_pattern, 0, $pos); - if (!$hook_found && $theme_registry->has($hook_pattern)) { - $hook_found = $hook_pattern; } + if (!$theme_registry->has($hook)) { + // Only log a message when not trying theme suggestions ($hook being an + // array). + if (!isset($candidate)) { + \Drupal::logger('theme')->warning('Theme hook %hook not found.', ['%hook' => $hook]); + } + // There is no theme implementation for the hook passed. Return FALSE so + // the function calling + // \Drupal\Core\Theme\ThemeManagerInterface::render() can differentiate + // between a hook that exists and renders an empty string, and a hook + // that is not implemented. + return FALSE; - } - if (!$hook_found) { - // Do not log a warning for arrays of theme hooks with no implemented - // hooks. It's valid to pass an array of theme hooks where none of the - // hooks are implemented. We only log missing theme hook implementations - // for single (string) hooks. - if (is_string($hook)) { - \Drupal::logger('theme')->warning('Theme hook %hook not found.', ['%hook' => $hook]); } - // There is no theme implementation for the hook(s) passed. Return FALSE - // so the calling function can differentiate between a hook that exists - // and renders an empty string, and hooks that are not implemented. - return FALSE; } + - $hook = $hook_found; - $theme_hooks = array_merge($theme_hooks, $derived_hooks); $info = $theme_registry->get($hook); // If a renderable array is passed as $variables, then set $variables to @@ -208,11 +210,9 @@ elseif (!empty($info['render element'])) { $variables += [$info['render element'] => []]; } + // Supply original caller info. - - // Save the original theme hook, so it can be supplied to theme variable - // preprocess callbacks. $variables += [ + 'theme_hook_original' => $original_hook, - 'theme_hook_original' => $candidate, ]; // Set base hook for later use. For example if '#theme' => 'node__article' @@ -228,9 +228,12 @@ // Invoke hook_theme_suggestions_HOOK(). $suggestions = $this->moduleHandler->invokeAll('theme_suggestions_' . $base_theme_hook, [$variables]); + // If the theme implementation 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; + } - // Prioritize suggestions from hook_theme_suggestions_HOOK() implementations - // above other suggestions. - $suggestions = array_merge($suggestions, array_reverse($theme_hooks)); // Invoke hook_theme_suggestions_alter() and // hook_theme_suggestions_HOOK_alter(). reverted: --- b/core/modules/system/src/Tests/Theme/ThemeSuggestionsAlterTest.php +++ a/core/modules/system/src/Tests/Theme/ThemeSuggestionsAlterTest.php @@ -152,31 +152,6 @@ } /** - * Tests hook_theme_suggestions_HOOK() with direct array suggestions. - */ - public function testArraySuggestions() { - $this->drupalGet('theme-test/array-suggestions'); - $this->assertText('Template for testing suggestion hooks with an array of theme suggestions.'); - - $this->drupalGet('/theme-test/array-suggestions-specific'); - $this->assertText('Template for testing suggestion hooks with an array of theme suggestions, overrides a less specific suggestion.'); - - // Enable the theme_suggestions_test module to test modules implementing - // suggestions hooks. - \Drupal::service('module_installer')->install(['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. * * hook_theme_suggestions_alter() should fire before reverted: --- b/core/modules/system/src/Tests/Theme/TwigDebugMarkupTest.php +++ a/core/modules/system/src/Tests/Theme/TwigDebugMarkupTest.php @@ -19,30 +19,22 @@ public static $modules = ['theme_test', 'node']; /** + * Tests debug markup added to Twig template output. - * {@inheritdoc} */ + public function testTwigDebugMarkup() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = $this->container->get('renderer'); + $extension = twig_extension(); - protected function setUp() { - parent::setUp(); - \Drupal::service('theme_handler')->install(['test_theme']); + $this->config('system.theme')->set('default', 'test_theme')->save(); - \Drupal::service('theme_handler')->setDefault('test_theme'); $this->drupalCreateContentType(['type' => 'page']); - // Enable debug, rebuild the service container, and clear all caches. $parameters = $this->container->getParameter('twig.config'); $parameters['debug'] = TRUE; $this->setContainerParameter('twig.config', $parameters); $this->rebuildContainer(); $this->resetAll(); - } - /** - * Tests debug markup added to Twig template output. - */ - public function testTwigDebugMarkup() { - /** @var \Drupal\Core\Render\RendererInterface $renderer */ - $renderer = $this->container->get('renderer'); - $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')); @@ -88,65 +80,4 @@ $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(['theme_suggestions_test']); - $extension = twig_extension(); - $this->drupalGet('theme-test/array-suggestions'); - $output = $this->getRawContent(); - - $expected = "THEME HOOK: 'theme_test_array_suggestions'"; - $this->assertTrue(strpos($output, $expected) !== FALSE, 'Theme call information found.'); - - $expected = ''; - $message = 'Suggested template files found in order and correct suggestion shown as current template.'; - $this->assertTrue(strpos($output, $expected) !== FALSE, $message); - } - - /** - * Tests debug markup for specific suggestions without implementation. - */ - public function testUnimplementedSpecificSuggestionsTwigDebugMarkup() { - $extension = twig_extension(); - $this->drupalGet('theme-test/specific-suggestion'); - $output = $this->getRawContent(); - - $expected = "THEME HOOK: 'theme_test_specific_suggestions__not__found'"; - $this->assertTrue(strpos($output, $expected) !== FALSE, 'Theme call information found.'); - - $message = 'Suggested template files found in order and base template shown as current template.'; - $expected = ''; - $this->assertTrue(strpos($output, $expected) !== FALSE, $message); - } - - /** - * Tests debug markup for specific suggestions. - */ - public function testSpecificSuggestionsTwigDebugMarkup() { - $extension = twig_extension(); - $this->drupalGet('theme-test/specific-suggestion-alter'); - $output = $this->getRawContent(); - - $expected = "THEME HOOK: 'theme_test_specific_suggestions__variant'"; - $this->assertTrue(strpos($output, $expected) !== FALSE, 'Theme call information found.'); - - $expected = ''; - $message = 'Suggested template files found in order and suggested template shown as current.'; - $this->assertTrue(strpos($output, $expected) !== FALSE, $message); - } - } reverted: --- b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module +++ a/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module @@ -55,10 +55,3 @@ 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 ['theme_test_array_suggestions__suggestion_from_hook']; -} reverted: --- b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php +++ a/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php @@ -106,13 +106,6 @@ } /** - * Menu callback for testing direct suggestions without implementation. - */ - public function specificSuggestion() { - return ['#theme' => 'theme_test_specific_suggestions__not__found']; - } - - /** * Menu callback for testing suggestion alter hooks with specific suggestions. */ public function specificSuggestionAlter() { @@ -135,33 +128,6 @@ } /** - * Menu callback for testing suggestion hooks with an array of theme hooks. - */ - public function arraySuggestions() { - return [ - '#theme' => [ - 'theme_test_array_suggestions__not_implemented', - 'theme_test_array_suggestions__not_implemented_2', - 'theme_test_array_suggestions', - ], - ]; - } - - /** - * Menu callback for testing suggestion hooks with an array of theme hooks. - */ - public function arraySuggestionsSpecific() { - return [ - '#theme' => [ - 'theme_test_array_suggestions__implemented__not_implemented', - 'theme_test_array_suggestions__implemented', - 'theme_test_array_suggestions__not_implemented', - 'theme_test_array_suggestions', - ], - ]; - } - - /** * Controller to ensure that no theme is initialized. * * @return \Symfony\Component\HttpFoundation\JsonResponse reverted: --- b/core/modules/system/tests/modules/theme_test/templates/theme-test-array-suggestions--implemented.html.twig +++ /dev/null @@ -1,2 +0,0 @@ -{# Output for Theme API test #} -Template for testing suggestion hooks with an array of theme suggestions, overrides a less specific suggestion. reverted: --- b/core/modules/system/tests/modules/theme_test/templates/theme-test-array-suggestions.html.twig +++ /dev/null @@ -1,2 +0,0 @@ -{# Output for Theme API test #} -Template for testing suggestion hooks with an array of theme suggestions. reverted: --- b/core/modules/system/tests/modules/theme_test/theme_test.module +++ a/core/modules/system/tests/modules/theme_test/theme_test.module @@ -57,12 +57,6 @@ 'variables' => [], 'function' => 'theme_theme_test_function_template_override', ]; - $items['theme_test_array_suggestions'] = [ - 'variables' => [], - ]; - $items['theme_test_array_suggestions__implemented'] = [ - 'variables' => [], - ]; $info['test_theme_not_existing_function'] = [ 'function' => 'test_theme_not_existing_function', ]; reverted: --- b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml +++ a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml @@ -76,13 +76,6 @@ 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: @@ -104,20 +97,6 @@ requirements: _access: 'TRUE' -theme_test.array_suggestions: - path: '/theme-test/array-suggestions' - defaults: - _controller: '\Drupal\theme_test\ThemeTestController::arraySuggestions' - requirements: - _access: 'TRUE' - -theme_test.array_suggestions_specific: - path: '/theme-test/array-suggestions-specific' - defaults: - _controller: '\Drupal\theme_test\ThemeTestController::arraySuggestionsSpecific' - requirements: - _access: 'TRUE' - theme_test.non_html: path: '/theme-test/non-html' defaults: reverted: --- b/core/modules/system/tests/themes/test_theme/templates/theme-test-array-suggestions--suggestion-from-hook.html.twig +++ /dev/null @@ -1,2 +0,0 @@ -{# Output for Theme API test #} -Template overridden based on new theme suggestion provided by a module via hook_theme_suggestions_HOOK(). reverted: --- b/core/modules/views/tests/src/Functional/ViewsThemeIntegrationTest.php +++ a/core/modules/views/tests/src/Functional/ViewsThemeIntegrationTest.php @@ -78,32 +78,4 @@ $this->assertRaw('' . count($this->dataSet()) . ' items found.', 'Views group title added by test_subtheme.test_subtheme_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' => [], - ]; - - /** @var \Drupal\Core\Render\RendererInterface $renderer */ - $renderer = $this->container->get('renderer'); - - $output = $renderer->renderRoot($build); - $expected = ' * views-view--test-page-display--default.html.twig' . PHP_EOL - . ' * views-view--default.html.twig' . PHP_EOL - . ' * views-view--test-page-display.html.twig' . PHP_EOL - . ' x views-view.html.twig' . PHP_EOL; - $this->assertTrue(strpos($output, $expected) !== FALSE); - } - } reverted: --- b/core/themes/engines/twig/twig.engine +++ a/core/themes/engines/twig/twig.engine @@ -79,21 +79,31 @@ // 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) { - // If this template was invoked directly (e.g.: '#theme' => - // 'links__node') include the base theme suggestion ('links' in this - // example). - $suggestions[] = substr($variables['theme_hook_original'], 0, $pos); - } - else { $suggestions[] = $variables['theme_hook_original']; } - foreach ($suggestions as &$suggestion) { $template = strtr($suggestion, '_', '-') . $extension; $prefix = ($template == $current_template) ? 'x' : '*'; only in patch2: unchanged: --- a/web/core/lib/Drupal/Core/Theme/ThemeManager.php +++ b/web/core/lib/Drupal/Core/Theme/ThemeManager.php @@ -141,47 +141,46 @@ public function render($hook, array $variables) { $theme_registry = $this->themeRegistry->getRuntime(); - // If an array of hook candidates were passed, use the first one that has an - // implementation. - if (is_array($hook)) { - foreach ($hook as $candidate) { - if ($theme_registry->has($candidate)) { - break; - } + $theme_hooks = (array) $hook; + $hook_found = NULL; + // Check for hook implementation. If an array of hook candidates is passed, + // use the first one that has an implementation. + foreach ($theme_hooks as $candidate) { + if ($theme_registry->has($candidate)) { + $hook_found = $candidate; + break; } - $hook = $candidate; } - // Save the original theme hook, so it can be supplied to theme variable - // preprocess callbacks. - $original_hook = $hook; - - // If there's no implementation, check for more generic fallbacks. - // If there's still no implementation, log an error and return an empty - // string. - if (!$theme_registry->has($hook)) { - // Iteratively strip everything after the last '__' delimiter, until an - // implementation is found. - while ($pos = strrpos($hook, '__')) { - $hook = substr($hook, 0, $pos); - if ($theme_registry->has($hook)) { - break; - } - } - if (!$theme_registry->has($hook)) { - // Only log a message when not trying theme suggestions ($hook being an - // array). - if (!isset($candidate)) { - \Drupal::logger('theme')->warning('Theme hook %hook not found.', ['%hook' => $hook]); - } - // There is no theme implementation for the hook passed. Return FALSE so - // the function calling - // \Drupal\Core\Theme\ThemeManagerInterface::render() can differentiate - // between a hook that exists and renders an empty string, and a hook - // that is not implemented. - return FALSE; + + $derived_hooks = []; + // Remove the last item of the array which is the original hook and use it + // as a pattern. + $hook_pattern = array_pop($theme_hooks); + // Get the generic fallbacks. If a valid hook implementation has not been + // found, check each fallback until one is. + while ($pos = strrpos($hook_pattern, '__')) { + // Save all derived hooks to be used later as a theme suggestion. + $derived_hooks[] = $hook_pattern; + $hook_pattern = substr($hook_pattern, 0, $pos); + if (!$hook_found && $theme_registry->has($hook_pattern)) { + $hook_found = $hook_pattern; } } - + if (!$hook_found) { + // Do not log a warning for arrays of theme hooks with no implemented + // hooks. It's valid to pass an array of theme hooks where none of the + // hooks are implemented. We only log missing theme hook implementations + // for single (string) hooks. + if (is_string($hook)) { + \Drupal::logger('theme')->warning('Theme hook %hook not found.', ['%hook' => $hook]); + } + // There is no theme implementation for the hook(s) passed. Return FALSE + // so the calling function can differentiate between a hook that exists + // and renders an empty string, and hooks that are not implemented. + return FALSE; + } + $hook = $hook_found; + $theme_hooks = array_merge($theme_hooks, $derived_hooks); $info = $theme_registry->get($hook); // If a renderable array is passed as $variables, then set $variables to @@ -210,9 +209,11 @@ public function render($hook, array $variables) { elseif (!empty($info['render element'])) { $variables += [$info['render element'] => []]; } - // Supply original caller info. + + // Save the original theme hook, so it can be supplied to theme variable + // preprocess callbacks. $variables += [ - 'theme_hook_original' => $original_hook, + 'theme_hook_original' => $candidate, ]; // Set base hook for later use. For example if '#theme' => 'node__article' @@ -228,12 +229,9 @@ public function render($hook, array $variables) { // Invoke hook_theme_suggestions_HOOK(). $suggestions = $this->moduleHandler->invokeAll('theme_suggestions_' . $base_theme_hook, [$variables]); - // If the theme implementation 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; - } + // Prioritize suggestions from hook_theme_suggestions_HOOK() implementations + // above other suggestions. + $suggestions = array_merge($suggestions, array_reverse($theme_hooks)); // Invoke hook_theme_suggestions_alter() and // hook_theme_suggestions_HOOK_alter(). only in patch2: unchanged: --- a/web/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module +++ b/web/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 ['theme_test_array_suggestions__suggestion_from_hook']; +} only in patch2: unchanged: --- a/web/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php +++ b/web/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php @@ -105,6 +105,13 @@ public function generalSuggestionAlter() { return ['#theme' => 'theme_test_general_suggestions']; } + /** + * Menu callback for testing direct suggestions without implementation. + */ + public function specificSuggestion() { + return ['#theme' => 'theme_test_specific_suggestions__not__found']; + } + /** * Menu callback for testing suggestion alter hooks with specific suggestions. */ @@ -126,6 +133,32 @@ public function suggestionAlterInclude() { return ['#theme' => 'theme_test_suggestions_include']; } + /** + * Menu callback for testing suggestion hooks with an array of theme hooks. + */ + public function arraySuggestions() { + return [ + '#theme' => [ + 'theme_test_array_suggestions__not_implemented', + 'theme_test_array_suggestions__not_implemented_2', + 'theme_test_array_suggestions', + ], + ]; + } + /** + * Menu callback for testing suggestion hooks with an array of theme hooks. + */ + public function arraySuggestionsSpecific() { + return [ + '#theme' => [ + 'theme_test_array_suggestions__implemented__not_implemented', + 'theme_test_array_suggestions__implemented', + 'theme_test_array_suggestions__not_implemented', + 'theme_test_array_suggestions', + ], + ]; + } + /** * Controller to ensure that no theme is initialized. * only in patch2: unchanged: --- /dev/null +++ b/web/core/modules/system/tests/modules/theme_test/templates/theme-test-array-suggestions--implemented.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template for testing suggestion hooks with an array of theme suggestions, overrides a less specific suggestion. \ No newline at end of file only in patch2: unchanged: --- /dev/null +++ b/web/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. \ No newline at end of file only in patch2: unchanged: --- a/web/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/web/core/modules/system/tests/modules/theme_test/theme_test.module @@ -57,6 +57,12 @@ function theme_test_theme($existing, $type, $theme, $path) { 'variables' => [], 'function' => 'theme_theme_test_function_template_override', ]; + $items['theme_test_array_suggestions'] = [ + 'variables' => [], + ]; + $items['theme_test_array_suggestions__implemented'] = [ + 'variables' => [], + ]; $info['test_theme_not_existing_function'] = [ 'function' => 'test_theme_not_existing_function', ]; only in patch2: unchanged: --- a/web/core/modules/system/tests/modules/theme_test/theme_test.routing.yml +++ b/web/core/modules/system/tests/modules/theme_test/theme_test.routing.yml @@ -76,6 +76,13 @@ 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 +104,20 @@ 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.array_suggestions_specific: + path: '/theme-test/array-suggestions-specific' + defaults: + _controller: '\Drupal\theme_test\ThemeTestController::arraySuggestionsSpecific' + requirements: + _access: 'TRUE' + theme_test.non_html: path: '/theme-test/non-html' defaults: only in patch2: unchanged: --- a/web/core/modules/system/tests/src/Functional/Theme/ThemeSuggestionsAlterTest.php +++ b/web/core/modules/system/tests/src/Functional/Theme/ThemeSuggestionsAlterTest.php @@ -152,6 +152,31 @@ 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() with direct array suggestions. + */ + public function testArraySuggestions() { + $this->drupalGet('theme-test/array-suggestions'); + $this->assertText('Template for testing suggestion hooks with an array of theme suggestions.'); + + $this->drupalGet('/theme-test/array-suggestions-specific'); + $this->assertText('Template for testing suggestion hooks with an array of theme suggestions, overrides a less specific suggestion.'); + + // Enable the theme_suggestions_test module to test modules implementing + // suggestions hooks. + \Drupal::service('module_installer')->install(['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. * only in patch2: unchanged: --- a/web/core/modules/system/tests/src/Functional/Theme/TwigDebugMarkupTest.php +++ b/web/core/modules/system/tests/src/Functional/Theme/TwigDebugMarkupTest.php @@ -20,22 +20,30 @@ class TwigDebugMarkupTest extends BrowserTestBase { public static $modules = ['theme_test', 'node']; /** - * Tests debug markup added to Twig template output. + * {@inheritdoc} */ - public function testTwigDebugMarkup() { - /** @var \Drupal\Core\Render\RendererInterface $renderer */ - $renderer = $this->container->get('renderer'); - $extension = twig_extension(); + protected function setUp() { + parent::setUp(); + \Drupal::service('theme_handler')->install(['test_theme']); - $this->config('system.theme')->set('default', 'test_theme')->save(); + \Drupal::service('theme_handler')->setDefault('test_theme'); $this->drupalCreateContentType(['type' => 'page']); + // Enable debug, rebuild the service container, and clear all caches. $parameters = $this->container->getParameter('twig.config'); $parameters['debug'] = TRUE; $this->setContainerParameter('twig.config', $parameters); $this->rebuildContainer(); $this->resetAll(); + } + /** + * Tests debug markup added to Twig template output. + */ + public function testTwigDebugMarkup() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = $this->container->get('renderer'); + $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')); @@ -80,4 +88,65 @@ public 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(['theme_suggestions_test']); + $extension = twig_extension(); + $this->drupalGet('theme-test/array-suggestions'); + $output = $this->getRawContent(); + + $expected = "THEME HOOK: 'theme_test_array_suggestions'"; + $this->assertTrue(strpos($output, $expected) !== FALSE, 'Theme call information found.'); + + $expected = ''; + $message = 'Suggested template files found in order and correct suggestion shown as current template.'; + $this->assertTrue(strpos($output, $expected) !== FALSE, $message); + } + + /** + * Tests debug markup for specific suggestions without implementation. + */ + public function testUnimplementedSpecificSuggestionsTwigDebugMarkup() { + $extension = twig_extension(); + $this->drupalGet('theme-test/specific-suggestion'); + $output = $this->getRawContent(); + + $expected = "THEME HOOK: 'theme_test_specific_suggestions__not__found'"; + $this->assertTrue(strpos($output, $expected) !== FALSE, 'Theme call information found.'); + + $message = 'Suggested template files found in order and base template shown as current template.'; + $expected = ''; + $this->assertTrue(strpos($output, $expected) !== FALSE, $message); + } + + /** + * Tests debug markup for specific suggestions. + */ + public function testSpecificSuggestionsTwigDebugMarkup() { + $extension = twig_extension(); + $this->drupalGet('theme-test/specific-suggestion-alter'); + $output = $this->getRawContent(); + + $expected = "THEME HOOK: 'theme_test_specific_suggestions__variant'"; + $this->assertTrue(strpos($output, $expected) !== FALSE, 'Theme call information found.'); + + $expected = ''; + $message = 'Suggested template files found in order and suggested template shown as current.'; + $this->assertTrue(strpos($output, $expected) !== FALSE, $message); + } + } only in patch2: unchanged: --- /dev/null +++ b/web/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(). \ No newline at end of file only in patch2: unchanged: --- a/web/core/modules/views/tests/src/Functional/ViewsThemeIntegrationTest.php +++ b/web/core/modules/views/tests/src/Functional/ViewsThemeIntegrationTest.php @@ -78,4 +78,32 @@ public function testThemedViewPage() { $this->assertRaw('' . count($this->dataSet()) . ' items found.', 'Views group title added by test_subtheme.test_subtheme_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' => [], + ]; + + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = $this->container->get('renderer'); + + $output = $renderer->renderRoot($build); + $expected = ' * views-view--test-page-display--default.html.twig' . PHP_EOL + . ' * views-view--default.html.twig' . PHP_EOL + . ' * views-view--test-page-display.html.twig' . PHP_EOL + . ' x views-view.html.twig' . PHP_EOL; + $this->assertTrue(strpos($output, $expected) !== FALSE); + } + } only in patch2: unchanged: --- a/web/core/themes/engines/twig/twig.engine +++ b/web/core/themes/engines/twig/twig.engine @@ -79,29 +79,17 @@ 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) { + // If this template was invoked directly (e.g.: '#theme' => + // 'links__node') include the base theme suggestion ('links' in this + // example). + $suggestions[] = substr($variables['theme_hook_original'], 0, $pos); + } + else { $suggestions[] = $variables['theme_hook_original']; } foreach ($suggestions as &$suggestion) {