diff --git a/core/includes/theme.inc b/core/includes/theme.inc index e7ec8df..3ac0278 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -2308,6 +2308,7 @@ function drupal_common_theme() { ), 'indentation' => array( 'variables' => array('size' => 1), + 'function' => 'theme_indentation', ), // From theme.maintenance.inc. 'maintenance_page' => array( @@ -2324,9 +2325,15 @@ function drupal_common_theme() { ), 'authorize_message' => array( 'variables' => array('message' => NULL, 'success' => TRUE), + 'function' => 'theme_authorize_message', + 'path' => 'core/includes', + 'file' => 'theme.maintenance.inc', ), 'authorize_report' => array( 'variables' => array('messages' => array()), + 'function' => 'theme_authorize_report', + 'path' => 'core/includes', + 'file' => 'theme.maintenance.inc', ), // From pager.inc. 'pager' => array( diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index da3122c..aa07fa9 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -401,6 +401,9 @@ protected function build() { * @see _theme() * @see hook_theme() * @see list_themes() + * @see twig_render_template() + * + * @throws \BadFunctionCallException */ protected function processExtension(&$cache, $name, $type, $theme, $path) { $result = array(); @@ -430,12 +433,6 @@ protected function processExtension(&$cache, $name, $type, $theme, $path) { $result[$hook]['type'] = $type; $result[$hook]['theme path'] = $path; - // If function and file are omitted, default to standard naming - // conventions. - if (!isset($info['template']) && !isset($info['function'])) { - $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name . '_') . $hook; - } - if (isset($cache[$hook]['includes'])) { $result[$hook]['includes'] = $cache[$hook]['includes']; } @@ -451,20 +448,37 @@ protected function processExtension(&$cache, $name, $type, $theme, $path) { $result[$hook]['includes'][] = $include_file; } + // Templates are the default implementation for a theme hook. If a + // theme hook provides a theme function callback, check to ensure that + // it actually exists. + if (isset($info['function']) && !function_exists($info['function'])) { + throw new \BadFunctionCallException(sprintf( + 'Theme hook "%s" refers to a theme function callback that does not exist: "%s"', + $hook, + $info['function'] + )); + } + // Provide a default naming convention for 'template' based on the + // hook used. If the template does not exist, the theme engine used + // should throw an exception at runtime when attempting to include + // the template file. + elseif (!isset($info['template'])) { + $info['template'] = strtr($hook, '_', '-'); + $result[$hook]['template'] = $info['template']; + } + + // Prepend the current theming path when none is set. This is required + // for the default theme engine to know where the template lives. + if (isset($result[$hook]['template']) && !isset($info['path'])) { + $result[$hook]['path'] = $path . '/templates'; + } + // If the default keys are not set, use the default values registered // by the module. if (isset($cache[$hook])) { $result[$hook] += array_intersect_key($cache[$hook], $hook_defaults); } - // The following apply only to theming hooks implemented as templates. - if (isset($info['template'])) { - // Prepend the current theming path when none is set. - if (!isset($info['path'])) { - $result[$hook]['template'] = $path . '/templates/' . $info['template']; - } - } - // Preprocess variables for all theming hooks, whether the hook is // implemented as a template or as a function. Ensure they are arrays. if (!isset($info['preprocess functions']) || !is_array($info['preprocess functions'])) { diff --git a/core/modules/book/book.module b/core/modules/book/book.module index 861ed73..6b1a940 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -62,6 +62,7 @@ function book_theme() { ), 'book_link' => array( 'render element' => 'element', + 'function' => 'theme_book_link', ), 'book_export_html' => array( 'variables' => array('title' => NULL, 'contents' => NULL, 'depth' => NULL), @@ -70,6 +71,7 @@ function book_theme() { 'book_admin_table' => array( 'render element' => 'form', 'file' => 'book.admin.inc', + 'function' => 'theme_book_admin_table', ), 'book_all_books_block' => array( 'render element' => 'book_menus', diff --git a/core/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module index 5bce9dd..5721172 100644 --- a/core/modules/field_ui/field_ui.module +++ b/core/modules/field_ui/field_ui.module @@ -66,6 +66,7 @@ function field_ui_theme() { return array( 'field_ui_table' => array( 'render element' => 'elements', + 'function' => 'theme_field_ui_table', ), ); } diff --git a/core/modules/language/language.module b/core/modules/language/language.module index 381fbd4..0f489f6 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -100,10 +100,12 @@ function language_theme() { 'language_negotiation_configure_browser_form_table' => array( 'render element' => 'form', 'file' => 'language.admin.inc', + 'function' => 'theme_language_negotiation_configure_browser_form_table', ), 'language_content_settings_table' => array( 'render element' => 'element', 'file' => 'language.admin.inc', + 'function' => 'theme_language_content_settings_table', ), ); } diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 01a0b8e..912fd72 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -181,6 +181,7 @@ function locale_theme() { 'locale_translate_edit_form_strings' => array( 'render element' => 'form', 'file' => 'locale.pages.inc', + 'function' => 'theme_locale_translate_edit_form_strings', ), 'locale_translation_last_check' => array( 'variables' => array('last' => NULL), diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module index 148203a..cc9eb79 100644 --- a/core/modules/menu_ui/menu_ui.module +++ b/core/modules/menu_ui/menu_ui.module @@ -78,6 +78,7 @@ function menu_ui_theme() { 'menu_overview_form' => array( 'file' => 'menu_ui.admin.inc', 'render element' => 'form', + 'function' => 'theme_menu_overview_form', ), ); } diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 091d601..4db9d2c 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -140,6 +140,7 @@ function node_theme() { ), 'node_search_admin' => array( 'render element' => 'form', + 'function' => 'theme_node_search_admin', ), 'node_add_list' => array( 'variables' => array('content' => NULL), diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index 63c732a..13f3b45 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -80,6 +80,7 @@ function responsive_image_theme() { 'attributes' => array(), 'mapping_id' => array(), ), + 'function' => 'theme_responsive_image', ), 'responsive_image_formatter' => array( 'variables' => array( @@ -88,6 +89,7 @@ function responsive_image_theme() { 'image_style' => NULL, 'mapping_id' => array(), ), + 'function' => 'theme_responsive_image_formatter', ), 'responsive_image_source' => array( 'variables' => array( @@ -96,6 +98,7 @@ function responsive_image_theme() { 'dimensions' => NULL, 'media' => NULL, ), + 'function' => 'theme_responsive_image_source', ), ); } diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index f1c9b23..818e63d 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -981,16 +981,20 @@ function hook_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_ * path, include it here. This path should be relative to the Drupal root * directory. * - template: If specified, this theme implementation is a template, and - * this is the template file without an extension. Do not put .html.twig on - * this file; that extension will be added automatically by the default + * this is the template file without an extension. If neither 'template' + * nor 'function' is specified, a default template name will be assumed. + * For example, if a module registers the 'search_result' theme hook, + * 'search-result' will be assigned to its template. Do not put .html.twig + * on this file, that extension will be added automatically by the default * rendering engine (which is Twig). If 'path' above is specified, the * template should also be in this path. - * - function: If specified, this will be the function name to invoke for - * this implementation. If neither 'template' nor 'function' is specified, - * a default function name will be assumed. For example, if a module - * registers the 'node' theme hook, 'theme_node' will be assigned to its - * function. If the chameleon theme registers the node hook, it will be - * assigned 'chameleon_node' as its function. + * - function: If the theme implementation is not a template but a theme + * function, you must specify the function name. For example, a theme hook + * of 'search_result' would, by convention, use a theme function of + * 'theme_search_result'. So, if you are building a theme called + * 'chameleon', that registers a new 'search_result' hook and theme + * function, then you would usually use 'chameleon_search_result' as the + * theme function name. * - base hook: Used for _theme() suggestions only: the base theme hook name. * Instead of this suggestion's implementation being used directly, the base * hook will be invoked with this implementation as its first suggestion. diff --git a/core/modules/system/system.module b/core/modules/system/system.module index c863fdd..3c3534a 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -182,10 +182,12 @@ function system_theme() { 'system_modules_details' => array( 'render element' => 'form', 'file' => 'system.admin.inc', + 'function' => 'theme_system_modules_details', ), 'system_modules_uninstall' => array( 'render element' => 'form', 'file' => 'system.admin.inc', + 'function' => 'theme_system_modules_uninstall', ), 'status_report' => array( 'variables' => array('requirements' => NULL), @@ -214,6 +216,7 @@ function system_theme() { ), 'system_compact_link' => array( 'variables' => array(), + 'function' => 'theme_system_compact_link', ), )); } 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 bd17563..e23a842 100644 --- a/core/modules/system/tests/modules/common_test/common_test.module +++ b/core/modules/system/tests/modules/common_test/common_test.module @@ -123,6 +123,7 @@ function common_test_theme() { ), 'common_test_empty' => array( 'variables' => array('foo' => 'foo'), + 'function' => 'theme_common_test_empty', ), ); } 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 e963c2f..974856a 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 @@ -12,6 +12,7 @@ function theme_suggestions_test_theme() { $items['theme_suggestions_test_include'] = array( 'file' => 'theme_suggestions_test.inc', 'variables' => array(), + 'function' => 'theme_theme_suggestions_test_include', ); return $items; } 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 dd71fb7..8616f02 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -9,6 +9,7 @@ function theme_test_theme($existing, $type, $theme, $path) { $items['theme_test'] = array( 'file' => 'theme_test.inc', 'variables' => array('foo' => ''), + 'function' => 'theme_theme_test', ); $items['theme_test_template_test'] = array( 'template' => 'theme_test.template_test', @@ -34,12 +35,15 @@ function theme_test_theme($existing, $type, $theme, $path) { ); $items['theme_test_function_suggestions'] = array( 'variables' => array(), + 'function' => 'theme_theme_test_function_suggestions', ); $items['theme_test_suggestions_include'] = array( 'variables' => array(), + 'function' => 'theme_theme_test_suggestions_include', ); $items['theme_test_foo'] = array( 'variables' => array('foo' => NULL), + 'function' => 'theme_theme_test_foo', ); $items['theme_test_render_element'] = array( 'render element' => 'elements', @@ -47,9 +51,11 @@ function theme_test_theme($existing, $type, $theme, $path) { ); $items['theme_test_render_element_children'] = array( 'render element' => 'element', + 'function' => 'theme_theme_test_render_element_children', ); $items['theme_test_function_template_override'] = array( 'variables' => array(), + 'function' => 'theme_theme_test_function_template_override', ); $info['test_theme_not_existing_function'] = array( 'function' => 'test_theme_not_existing_function', diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index b9174bc..ff81d67 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -41,9 +41,6 @@ function toolbar_theme($existing, $type, $theme, $path) { 'render element' => 'element', 'template' => 'toolbar', ); - $items['toolbar_item'] = array( - 'render element' => 'element', - ); return $items; } diff --git a/core/modules/update/update.module b/core/modules/update/update.module index 65e1364..ffe320b 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -204,11 +204,11 @@ function update_theme() { 'file' => 'update.report.inc', 'template' => 'update-project-status', ), + // We are using template instead of '#type' => 'table' here to keep markup + // out of preprocess and allow for easier changes to markup. 'update_version' => array( 'variables' => array('version' => NULL, 'title' => NULL, 'attributes' => array()), 'file' => 'update.report.inc', - // We are using template instead of '#type' => 'table' here to keep markup - // out of preprocess and allow for easier changes to markup. 'template' => 'update-version', ), ); diff --git a/core/modules/views/views.module b/core/modules/views/views.module index 84b324f..12aaa14 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -145,6 +145,7 @@ function views_theme($existing, $type, $theme, $path) { // Default view themes $hooks['views_view_field'] = $base + array( 'variables' => array('view' => NULL, 'field' => NULL, 'row' => NULL), + 'function' => 'theme_views_view_field', ); $hooks['views_view_grouping'] = $base + array( 'variables' => array('view' => NULL, 'grouping' => NULL, 'grouping_level' => NULL, 'rows' => NULL, 'title' => NULL), @@ -177,35 +178,44 @@ function views_theme($existing, $type, $theme, $path) { 'variables' => $variables[$type], ); - // For the views module we ensure views.theme.inc is included. - if ($def['provider'] == 'views') { - $def['theme_file'] = 'views.theme.inc'; - } // We always use the module directory as base dir. $module_dir = drupal_get_path('module', $def['provider']); + $hooks[$def['theme']]['path'] = $module_dir; + // For the views module we ensure views.theme.inc is included. + if ($def['provider'] == 'views') { + if (!isset($hooks[$def['theme']]['includes'])) { + $hooks[$def['theme']]['includes'] = array(); + } + if (!in_array('views.theme.inc', $hooks[$def['theme']]['includes'])) { + $hooks[$def['theme']]['includes'][] = $module_dir . '/views.theme.inc'; + } + } // The theme_file definition is always relative to the modules directory. - if (isset($def['theme_file'])) { - $hooks[$def['theme']]['path'] = $module_dir; + elseif (!empty($def['theme_file'])) { $hooks[$def['theme']]['file'] = $def['theme_file']; } + // Whenever we got a theme file, we include it directly so we can // auto-detect the theme function. if (isset($def['theme_file'])) { - $include = DRUPAL_ROOT . '/' . $module_dir. '/' . $def['theme_file']; + $include = DRUPAL_ROOT . '/' . $module_dir . '/' . $def['theme_file']; if (is_file($include)) { require_once $include; } } - // If there is no theme function for the given theme definition, we assume - // a template file shall be used. By default this file is located in the - // /templates directory of the module's folder. - // If a module wants to define its own location it has to set - // register_theme of the plugin to FALSE and implement hook_theme() by - // itself. + + // If there is no theme function for the given theme definition, it must + // be a template file. By default this file is located in the /templates + // directory of the module's folder. If a module wants to define its own + // location it has to set register_theme of the plugin to FALSE and + // implement hook_theme() by itself. if (!function_exists('theme_' . $def['theme'])) { - $hooks[$def['theme']]['path'] = $module_dir; - $hooks[$def['theme']]['template'] = 'templates/' . drupal_clean_css_identifier($def['theme']); + $hooks[$def['theme']]['path'] .= '/templates'; + $hooks[$def['theme']]['template'] = drupal_clean_css_identifier($def['theme']); + } + else { + $hooks[$def['theme']]['function'] = 'theme_' . $def['theme']; } } } diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module index 3960294..46adb0f 100644 --- a/core/modules/views_ui/views_ui.module +++ b/core/modules/views_ui/views_ui.module @@ -100,6 +100,7 @@ function views_ui_theme() { 'views_ui_build_group_filter_form' => array( 'render element' => 'form', 'file' => 'views_ui.theme.inc', + 'function' => 'theme_views_ui_build_group_filter_form', ), // On behalf of a plugin diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index c859b65..6311ada 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -38,23 +38,28 @@ function twig_init(Extension $theme) { * If the Twig debug setting is enabled, HTML comments including _theme() call * and template file name suggestions will surround the template markup. * - * @param $template_file + * @param string $template_file * The file name of the template to render. - * @param $variables + * @param array $variables * A keyed array of variables that will appear in the output. * - * @return + * @return string * The output generated by the template, plus any debug information. */ function twig_render_template($template_file, $variables) { /** @var \Twig_Environment $twig_service */ $twig_service = \Drupal::service('twig'); - $output = array( - 'debug_prefix' => '', - 'debug_info' => '', - 'rendered_markup' => $twig_service->loadTemplate($template_file)->render($variables), - 'debug_suffix' => '', - ); + try { + $output = array( + 'debug_prefix' => '', + 'debug_info' => '', + 'rendered_markup' => $twig_service->loadTemplate($template_file)->render($variables), + 'debug_suffix' => '', + ); + } + catch (\Twig_Error_Loader $e) { + drupal_set_message($e->getMessage(), 'error'); + } if ($twig_service->isDebug()) { $output['debug_prefix'] .= "\n\n"; $output['debug_prefix'] .= "\n";