diff --git a/core/includes/common.inc b/core/includes/common.inc index 3ff8096..887e615 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -4684,26 +4684,35 @@ function drupal_render(&$elements) { // Call the element's #theme function if it is set. Then any children of the // element have to be rendered there. If the internal #render_children // property is set, do not call the #theme function to prevent infinite - // recursion. - if (isset($elements['#theme']) && !isset($elements['#render_children'])) { + // recursion. Assume that if #theme is set it represents an implemented hook. + $theme_is_implemented = isset($elements['#theme']); + if ($theme_is_implemented && !isset($elements['#render_children'])) { $elements['#children'] = theme($elements['#theme'], $elements); + + // If theme() returns FALSE this means that the hook in #theme was not found + // in the registry and so we need to update our flag accordingly. This is + // common for theme suggestions. + $theme_is_implemented = ($elements['#children'] !== FALSE); } - // If #theme was not set and the element has children, render them now. - // This is the same process as drupal_render_children() but is inlined - // for speed. - if ($elements['#children'] === '') { + // #theme is either not set or does not exist in the registry. + if (!$theme_is_implemented) { + // If #theme is not implemented and the element has children, render them + // now. This is the same process as drupal_render_children() but is inlined + // for speed. foreach ($children as $key) { $elements['#children'] .= drupal_render($elements[$key]); } - } - // If #theme was not set, but the element has raw #markup, prepend the content - // in #markup to #children. #children may contain the rendered content - // supplied by #theme, or the rendered child elements, as processed above. If - // both #theme and #markup are set, then #theme is responsible for rendering - // the element. Eventually assigned #theme_wrappers will expect both the - // element's #markup and the rendered content of child elements in #children. - if (!isset($elements['#theme']) && isset($elements['#markup'])) { - $elements['#children'] = $elements['#markup'] . $elements['#children']; + + // If #theme is not implemented and the element has raw #markup as a + // fallback, prepend the content in #markup to #children. In this case + // #children will contain whatever is provided by #pre_render prepended to + // what is rendered recursively above. If #theme is implemented then it is + // the responsibility of that theme implementation to render #markup if + // required. Eventually #theme_wrappers will expect both #markup and + // #children to be a single string as #children. + if (isset($elements['#markup'])) { + $elements['#children'] = $elements['#markup'] . $elements['#children']; + } } // Add any JavaScript state information associated with the element. diff --git a/core/includes/theme.inc b/core/includes/theme.inc index a7da8d0..3b60eff 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -932,8 +932,9 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) { * properties are mapped to variables expected by the theme hook * implementations. * - * @return - * An HTML string representing the themed output. + * @return string|false + * An HTML string representing the themed output or FALSE if the passed $hook + * is not implemented. * * @see themeable * @see hook_theme() @@ -982,7 +983,10 @@ function theme($hook, $variables = array()) { if (!isset($candidate)) { watchdog('theme', 'Theme hook %hook not found.', array('%hook' => $hook), WATCHDOG_WARNING); } - return ''; + // There is no theme implementation for the hook passed. Return FALSE so + // the function calling theme() can differentiate between a hook that + // exists and renders an empty string and a hook that is not implemented. + return FALSE; } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php index 31e4f34..e72df47 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php @@ -77,6 +77,38 @@ function testDrupalRenderBasics() { 'value' => array('#markup' => 'foo'), 'expected' => 'foo', ), + // Test handling of #children and child renderable elements. The golden + // rule here is that if #theme is implemented it is 100% responsible for + // rendering children. + array( + 'name' => '#theme is not set, #children is set and array has children', + 'value' => array( + '#children' => 'foo', + 'child' => array('#markup' => 'bar'), + ), + 'expected' => 'foobar', + ), + array( + 'name' => '#theme is implemented, #children is set and array has children', + 'value' => array( + '#theme' => 'common_test_foo', + '#children' => 'baz', + 'child' => array('#markup' => 'boo'), + ), + 'expected' => 'foobar', + ), + array( + 'name' => '#theme is implemented, #render_children is set, #children is set and array has children', + 'value' => array( + '#theme' => 'common_test_foo', + '#children' => 'baz', + '#render_children' => TRUE, + 'child' => array( + '#markup' => 'boo', + ), + ), + 'expected' => 'baz', + ), ); foreach($types as $type) { $this->assertIdentical(drupal_render($type['value']), $type['expected'], '"' . $type['name'] . '" input rendered correctly by drupal_render().'); @@ -84,6 +116,59 @@ function testDrupalRenderBasics() { } /** + * Tests fallback rendering behaviour when #theme is not implemented. + * + * If #theme is set and is an implemented theme hook then theme() is 100% + * responsible for rendering the array including children and #markup. + * + * If #theme is not set or is not found in the registry then drupal_render() + * should recursively render child attributes of the array and #markup. + * + * This dual rendering behaviour is only relevant to the internal processing + * of drupal_render() before #theme_wrappers are called, so not #prefix and + * #suffix for example. + */ + function testDrupalRenderFallbackRender() { + // Theme suggestion is not implemented, #markup should be rendered. + $theme_suggestion_not_implemented_has_markup = array( + '#theme' => array('suggestionnotimplemented'), + '#markup' => 'foo', + ); + $rendered = drupal_render($theme_suggestion_not_implemented_has_markup); + $this->assertIdentical($rendered, 'foo'); + + // Theme suggestion is not implemented, children should be rendered. + $theme_suggestion_not_implemented_has_children = array( + '#theme' => array('suggestionnotimplemented'), + 'child' => array( + '#markup' => 'foo', + ), + ); + $rendered = drupal_render($theme_suggestion_not_implemented_has_children); + $this->assertIdentical($rendered, 'foo'); + + // Theme suggestion is implemented but returns empty string, #markup should + // not be rendered. + $theme_implemented_is_empty_has_markup = array( + '#theme' => array('common_test_empty'), + '#markup' => 'foo', + ); + $rendered = drupal_render($theme_implemented_is_empty_has_markup); + $this->assertIdentical($rendered, ''); + + // Theme suggestion is implemented but returns empty string, children should + // not be rendered. + $theme_implemented_is_empty_has_children = array( + '#theme' => array('common_test_empty'), + 'child' => array( + '#markup' => 'foo', + ), + ); + $rendered = drupal_render($theme_implemented_is_empty_has_children); + $this->assertIdentical($rendered, ''); + } + + /** * Tests sorting by weight. */ function testDrupalRenderSorting() { diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php index 32f9321..072bfb0 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php @@ -204,7 +204,7 @@ function testRegistryRebuild() { // throws an exception. $this->rebuildContainer(); $this->container->get('module_handler')->loadAll(); - $this->assertIdentical(theme('theme_test_foo', array('foo' => 'b')), '', 'The theme registry does not contain theme_test_foo, because the module is disabled.'); + $this->assertIdentical(theme('theme_test_foo', array('foo' => 'b')), FALSE, 'The theme registry does not contain theme_test_foo, because the module is disabled.'); module_enable(array('theme_test'), FALSE); // After enabling/disabling a module during a test, we need to rebuild the diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module index 2c11a8b..bd44401 100644 --- a/core/modules/system/tests/modules/common_test/common_test.module +++ b/core/modules/system/tests/modules/common_test/common_test.module @@ -164,10 +164,27 @@ function common_test_theme() { 'variables' => array('foo' => 'foo', 'bar' => 'bar'), 'template' => 'common-test-foo', ), + 'common_test_empty' => array( + 'variables' => array('foo' => 'foo'), + ), ); } /** + * Provides a theme function for drupal_render(). + */ +function theme_common_test_foo($variables) { + return $variables['foo'] . $variables['bar']; +} + +/** + * Always returns an empty string. + */ +function theme_common_test_empty($variables) { + return ''; +} + +/** * Implements hook_library_info_alter(). */ function common_test_library_info_alter(&$libraries, $module) {