core/core.services.yml | 5 ++ .../Core/Template/Loader/ThemeRegistryLoader.php | 68 ++++++++++++++++++++ .../Core/Template/ThemeRegistryNodeVisitor.php | 72 +++++++++++++--------- core/modules/image/image.field.inc | 31 +--------- .../image/templates/image-formatter.html.twig | 18 ++++-- core/modules/system/components/a/a.html.twig | 1 + core/modules/system/components/a/a.yml | 30 +++++++++ core/modules/system/templates/html.html.twig | 8 ++- .../templates/field/image-formatter.html.twig | 19 ------ core/themes/classy/templates/layout/html.html.twig | 25 +------- .../templates/field/image-formatter.html.twig | 18 ------ core/themes/stable/templates/layout/html.html.twig | 47 -------------- .../navigation/menu-local-tasks.html.twig | 21 ------- 13 files changed, 169 insertions(+), 194 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index 5965675..6f7399f 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1547,6 +1547,11 @@ services: arguments: ['@app.root', '@module_handler', '@theme_handler'] tags: - { name: twig.loader, priority: 100 } + twig.loader.theme_registry: + class: Drupal\Core\Template\Loader\ThemeRegistryLoader + arguments: ['@theme.registry'] + tags: + - { name: twig.loader, priority: 0 } twig.loader.string: class: Drupal\Core\Template\Loader\StringLoader tags: diff --git a/core/lib/Drupal/Core/Template/Loader/ThemeRegistryLoader.php b/core/lib/Drupal/Core/Template/Loader/ThemeRegistryLoader.php new file mode 100644 index 0000000..67a82dd --- /dev/null +++ b/core/lib/Drupal/Core/Template/Loader/ThemeRegistryLoader.php @@ -0,0 +1,68 @@ +themeRegistry = $theme_registry; + } + + /** + * Finds the path to the requested template. + * + * @param string $name + * The name of the template to load. + * @param bool $throw + * Whether to throw an exception when an error occurs. + * + * @return string + * The path to the template. + * + * @throws \Twig_Error_Loader + * Thrown if a template matching $name cannot be found. + */ + protected function findTemplate($name, $throw = TRUE) { + // Allow for loading based on the Drupal theme registry. + $hook = str_replace('.html.twig', '', strtr($name, '-', '_')); + $theme_registry = $this->themeRegistry->getRuntime(); + + if ($theme_registry->has($hook)) { + $info = $theme_registry->get($hook); + if (isset($info['path'])) { + $path = $info['path'] . '/' . $name; + } + elseif (isset($info['template'])) { + $path = $info['template'] . '.html.twig'; + } + if (isset($path) && is_file($path)) { + return $this->cache[$name] = $path; + } + } + + if ($throw) { + throw new \Twig_Error_Loader(sprintf('Unable to find template "%s" in the Drupal theme registry.', $name)); + } + } + +} diff --git a/core/lib/Drupal/Core/Template/ThemeRegistryNodeVisitor.php b/core/lib/Drupal/Core/Template/ThemeRegistryNodeVisitor.php index 17c0aa9..8f65170 100644 --- a/core/lib/Drupal/Core/Template/ThemeRegistryNodeVisitor.php +++ b/core/lib/Drupal/Core/Template/ThemeRegistryNodeVisitor.php @@ -12,17 +12,22 @@ * * Only updates expressions like: * - {% extends "block.html.twig" %} - * - {% include "block.html.twig" %} * - {% extends "block--search-form-block.html.twig" %} - * - {% include "block--search-form-block.html.twig" %} * * Does not update expressions like: + * - {% extends "@stable/block.twig" %} * - {% extends "@stable/block--search-form-block.html.twig" %} - * - {% include "@stable/block--search-form-block.html.twig" %} + * - {% extends "path/to/block.html.twig" %} * - {% extends "path/to/block--search-form-block.html.twig" %} - * - {% include "path/to/block--search-form-block.html.twig" %} + * + * Ensures that a variant of a theme hook that extends the non-variant does not + * skip the current theme's template: if a theme has + * block--search-form-block.html.twig that's extending block.html.twig, then it + * will use the current theme's block.html.twig if it has it, and not the parent + * theme's. This matches the behavior outside of Twig. * * @see https://www.drupal.org/node/2387069 + * @see http://twig.sensiolabs.org/doc/tags/extends.html#dynamic-inheritance * * @see twig_render */ @@ -52,27 +57,20 @@ protected function doEnterNode(\Twig_Node $node, \Twig_Environment $env) { if ($node instanceof \Twig_Node_Module && $node->hasNode('parent')) { $parent = $node->getNode('parent'); if ($parent && $this->expressionQualifies($parent)) { - $current_file = basename($node->getAttribute('filename')); - $extended_file = $parent->getAttribute('value'); - $candidates = ($current_file === $extended_file) - ? $this->getCandidateParentTemplates($current_file) - : $this->getCandidateTemplates($extended_file); + $current_filename = basename($node->getAttribute('filename')); + $extended_filename = $parent->getAttribute('value'); + assert('strpos($current_filename, "/") === FALSE'); + assert('strpos($extended_filename, "/") === FALSE'); + $path = dirname($node->getAttribute('filename')); + $candidates = ($current_filename === $extended_filename) + ? $this->getCandidateParentTemplates($current_filename, $path) + : $this->getCandidateTemplates($extended_filename); if ($candidates === FALSE) { throw new \Twig_Error(sprintf('Template "%s" extends "%s", but no such parent templates exist.', $node->getAttribute('filename'), $parent->getAttribute('value'))); } $node->setNode('parent', $this->getReplacementNode($parent, $candidates)); } } - elseif ($node instanceof \Twig_Node_Include) { - $include = $node->getNode('expr'); - if ($include && $this->expressionQualifies($include)) { - $candidates = $this->getCandidateTemplates($include->getAttribute('value')); - if (empty($candidates)) { - throw new \Twig_Error(sprintf('Template "%s" includes "%s", but no such templates exist.', $node->getAttribute('filename'), $include->getAttribute('value'))); - } - $node->setNode('expr', $this->getReplacementNode($include, $candidates)); - } - } return $node; } @@ -90,6 +88,10 @@ protected function expressionQualifies(\Twig_Node $node) { return // Only override when extending a single template. $node instanceof \Twig_Node_Expression_Constant + // Only override when the Module node is not a dynamically created one (as + // they are created for Embed nodes). + // @see \Twig_TokenParser_Embed + && $extended_file !== '__parent__' // Only override when the extended template is pointing to a file, i.e // when it doesn't include either a path or a namespace. && $extended_file === basename($extended_file); @@ -120,14 +122,14 @@ protected function getReplacementNode(\Twig_Node_Expression_Constant $node, arra /** * Returns candidate templates based on the theme registry. * - * @param string $template_filepath - * The full path relative to the Drupal root for this template. + * @param string $template_filename + * The template file name. * * @return string[] * The candidate templates, including the current template. */ - protected function getCandidateTemplates($template_filepath) { - $template_filename = basename($template_filepath); + protected function getCandidateTemplates($template_filename) { + assert('strpos($template_filename, "/") === FALSE'); $theme_hook = str_replace('.html.twig', '', strtr($template_filename, '-', '_')); $info = $this->themeRegistry->getRuntime()->get($theme_hook); @@ -140,22 +142,34 @@ protected function getCandidateTemplates($template_filepath) { /** * Returns candidate parent templates based on the theme registry. * - * @param string $template_filepath - * The full path relative to the Drupal root for this template. + * @param string $template_filename + * The template file name. + * @param string $template_path + * The path relative to the Drupal root for the directory that contains this + * template. * * @return string[]|false * The candidate parent templates, or FALSE when none exist. */ - protected function getCandidateParentTemplates($template_filepath) { - $candidates = $this->getCandidateTemplates($template_filepath); + protected function getCandidateParentTemplates($template_filename, $template_path) { + assert('strpos($template_filename, "/") === FALSE'); + $candidates = $this->getCandidateTemplates($template_filename); // The candidates include the current template too, so there must be >=2. if (count($candidates) < 2) { return FALSE; } - // Remove the current template. - array_shift($candidates); + // Remove the current template (and its descendants) from the candidates: + // only return ancestors. + $reversed_candidates = array_reverse($candidates); + for ($i = 0; $i < count($reversed_candidates); $i++) { + if ($reversed_candidates[$i]['path'] === $template_path) { + array_splice($reversed_candidates, $i); + break; + } + } + $candidates = array_reverse($reversed_candidates); return $candidates; } diff --git a/core/modules/image/image.field.inc b/core/modules/image/image.field.inc index 1bf4369..03165e2 100644 --- a/core/modules/image/image.field.inc +++ b/core/modules/image/image.field.inc @@ -48,34 +48,5 @@ function template_preprocess_image_widget(&$variables) { * - url: An optional \Drupal\Core\Url object. */ function template_preprocess_image_formatter(&$variables) { - if ($variables['image_style']) { - $variables['image'] = array( - '#theme' => 'image_style', - '#style_name' => $variables['image_style'], - ); - } - else { - $variables['image'] = array( - '#theme' => 'image', - ); - } - $variables['image']['#attributes'] = $variables['item_attributes']; - - $item = $variables['item']; - - // Do not output an empty 'title' attribute. - if (Unicode::strlen($item->title) != 0) { - $variables['image']['#title'] = $item->title; - } - - if (($entity = $item->entity) && empty($item->uri)) { - $variables['image']['#uri'] = $entity->getFileUri(); - } - else { - $variables['image']['#uri'] = $item->uri; - } - - foreach (array('width', 'height', 'alt') as $key) { - $variables['image']["#$key"] = $item->$key; - } + $variables['item_attributes'] = new \Drupal\Core\Template\Attribute($variables['item_attributes']); } diff --git a/core/modules/image/templates/image-formatter.html.twig b/core/modules/image/templates/image-formatter.html.twig index 63ca3c6..8cbcb25 100644 --- a/core/modules/image/templates/image-formatter.html.twig +++ b/core/modules/image/templates/image-formatter.html.twig @@ -13,8 +13,16 @@ * @ingroup themeable */ #} -{% if url %} - {{ image }} -{% else %} - {{ image }} -{% endif %} +{% embed "a.html.twig" with { url: url, image: item, attributes: item_attributes, style_name: image_style } only %} + {% block content %} + {% include "image.html.twig" with { + uri: image.entity.uri.value, + width: image.width, + height: image.height, + alt: image.alt, + title: image.title, + attributes: attributes, + style_name: style_name + } only %} + {% endblock %} +{% endembed %} diff --git a/core/modules/system/components/a/a.html.twig b/core/modules/system/components/a/a.html.twig new file mode 100644 index 0000000..a51949d --- /dev/null +++ b/core/modules/system/components/a/a.html.twig @@ -0,0 +1 @@ +{% block content %}{{ text }}{% endblock %} diff --git a/core/modules/system/components/a/a.yml b/core/modules/system/components/a/a.yml new file mode 100644 index 0000000..59b6d9b --- /dev/null +++ b/core/modules/system/components/a/a.yml @@ -0,0 +1,30 @@ +label: Anchor +is_container: true +variables: + url: + type: string + desc: 'The URL to link to.' + example: 'https://www.drupal.org' + default: '' +# This fails because everything expects to have an Attribute object to work with +# yet Twig templates cannot generate Attribute objects. +# +# attributes: +# type: string[] +# desc: 'HTML attributes for the anchor.' +# example: { href: 'https://www.drupal.org', hreflang: 'en' } +# default: [] + text: + type: string + example: 'Text' + desc: 'The link text.' + class: + type: string + example: 'focusable' + desc: 'The class attribute.' +documentation: + purpose: … + when: … + how: … + accessibility: … + links: [] diff --git a/core/modules/system/templates/html.html.twig b/core/modules/system/templates/html.html.twig index 195d82e..e0b73fe 100644 --- a/core/modules/system/templates/html.html.twig +++ b/core/modules/system/templates/html.html.twig @@ -38,9 +38,11 @@ Keyboard navigation/accessibility link to main content section in page.html.twig. #} - - {{ 'Skip to main content'|t }} - + {% include "a.html.twig" with { + url: '#main-content', + class: 'visually-hidden focusable', + text: 'Skip to main content'|t + } only %} {{ page_top }} {{ page }} {{ page_bottom }} diff --git a/core/themes/classy/templates/field/image-formatter.html.twig b/core/themes/classy/templates/field/image-formatter.html.twig deleted file mode 100644 index 04a98a3..0000000 --- a/core/themes/classy/templates/field/image-formatter.html.twig +++ /dev/null @@ -1,19 +0,0 @@ -{# -/** - * @file - * Theme override to display a formatted image field. - * - * Available variables: - * - image: A collection of image data. - * - image_style: An optional image style. - * - path: An optional array containing the link 'path' and link 'options'. - * - url: An optional URL the image can be linked to. - * - * @see template_preprocess_image_formatter() - */ -#} -{% if url %} - {{ image }} -{% else %} - {{ image }} -{% endif %} diff --git a/core/themes/classy/templates/layout/html.html.twig b/core/themes/classy/templates/layout/html.html.twig index 8330ccc..ed2cc6e 100644 --- a/core/themes/classy/templates/layout/html.html.twig +++ b/core/themes/classy/templates/layout/html.html.twig @@ -1,3 +1,5 @@ +{% extends "html.html.twig" %} + {# /** * @file @@ -31,25 +33,4 @@ db_offline ? 'db-offline', ] %} - - - - - {{ head_title|safe_join(' | ') }} - - - - - {# - Keyboard navigation/accessibility link to main content section in - page.html.twig. - #} - - {{ page_top }} - {{ page }} - {{ page_bottom }} - - - +{% set attributes = attributes.addClass(body_classes) %} diff --git a/core/themes/stable/templates/field/image-formatter.html.twig b/core/themes/stable/templates/field/image-formatter.html.twig deleted file mode 100644 index d0390c0..0000000 --- a/core/themes/stable/templates/field/image-formatter.html.twig +++ /dev/null @@ -1,18 +0,0 @@ -{# -/** - * @file - * Theme override to display a formatted image field. - * - * Available variables: - * - image: A collection of image data. - * - image_style: An optional image style. - * - url: An optional URL the image can be linked to. - * - * @see template_preprocess_image_formatter() - */ -#} -{% if url %} - {{ image }} -{% else %} - {{ image }} -{% endif %} diff --git a/core/themes/stable/templates/layout/html.html.twig b/core/themes/stable/templates/layout/html.html.twig deleted file mode 100644 index 5e4f25e..0000000 --- a/core/themes/stable/templates/layout/html.html.twig +++ /dev/null @@ -1,47 +0,0 @@ -{# -/** - * @file - * Theme override for the basic structure of a single Drupal page. - * - * Variables: - * - logged_in: A flag indicating if user is logged in. - * - root_path: The root path of the current page (e.g., node, admin, user). - * - node_type: The content type for the current node, if the page is a node. - * - head_title: List of text elements that make up the head_title variable. - * May contain or more of the following: - * - title: The title of the page. - * - name: The name of the site. - * - slogan: The slogan of the site. - * - page_top: Initial rendered markup. This should be printed before 'page'. - * - page: The rendered page markup. - * - page_bottom: Closing rendered markup. This variable should be printed after - * 'page'. - * - db_offline: A flag indicating if the database is offline. - * - placeholder_token: The token for generating head, css, js and js-bottom - * placeholders. - * - * @see template_preprocess_html() - */ -#} - - - - - {{ head_title|safe_join(' | ') }} - - - - - {# - Keyboard navigation/accessibility link to main content section in - page.html.twig. - #} - - {{ 'Skip to main content'|t }} - - {{ page_top }} - {{ page }} - {{ page_bottom }} - - - diff --git a/core/themes/stable/templates/navigation/menu-local-tasks.html.twig b/core/themes/stable/templates/navigation/menu-local-tasks.html.twig deleted file mode 100644 index 3874add..0000000 --- a/core/themes/stable/templates/navigation/menu-local-tasks.html.twig +++ /dev/null @@ -1,21 +0,0 @@ -{# -/** - * @file - * Theme override to display primary and secondary local tasks. - * - * Available variables: - * - primary: HTML list items representing primary tasks. - * - secondary: HTML list items representing primary tasks. - * - * Each item in these variables (primary and secondary) can be individually - * themed in menu-local-task.html.twig. - */ -#} -{% if primary %} -

{{ 'Primary tabs'|t }}

-
    {{ primary }}
-{% endif %} -{% if secondary %} -

{{ 'Secondary tabs'|t }}

-
    {{ secondary }}
-{% endif %}