Compare commits

...

58 Commits
3.1.x ... 3.x

Author SHA1 Message Date
Thomas Frobieter c096aece9f Issue #3407926: Extend documentation for drupal_url() + add example for taxomy_term 9 months ago
Chi 4b72a2c3da #3356042: Clean-up image builder 9 months ago
Julian Pustkuchen 898b4521d1 Issue #3356042: drupal_image() needs width / height attributes for fully working image cache scale (Width and height calculation only) 9 months ago
James Shields b3947e26b7 Upload New logo.png File 9 months ago
Chi ed7ae10bc6 Issue #3373333 by paulsheldrake, Chi: New filter to base64 encode and image 10 months ago
Chi 34ce474c21 Update hook documentation 10 months ago
Chi 9e1594ce42 Change min PHP version in CI config 10 months ago
Chi 81ac9123dd Add CI config 10 months ago
Jeff Mann 5ad73a7494 Issue #2980324 by JeffM2001, Chi: Document how to use block contexts 10 months ago
Chi 740ce17fd2 Issue #3386691 by juagarc4: drupal_field not working with Layout Builder enabled 10 months ago
Kyrylo Loboda 4111163f51 Issue #3405514 by lobodacyril, kevinquillen: drupal_entity works incorrect when the source content is unpublished 10 months ago
Chi ce34d528ae Issue #3223186: Add a test for external URL 10 months ago
prashantdsala 91992ea10d Applied changes from the patch provided in #5 10 months ago
Chi 41144dddb5 Clean-up documentation 10 months ago
Chi 271e723d67 Issue #3402615: Update documentation for drupal_view_result 10 months ago
Julian Pustkuchen 248ea4d7db Issue #3356080: Add lazy loading parameter to drupal_image() or add attribute example in Cheat Sheet 10 months ago
Reinhard Hutter f2dbdeaa24 Update description of with filter 1 year ago
Bill Seremetis 4a4cf380c5 Issue #3384540: Update 2.x -> 3.x documentation 1 year ago
Akshay Singh a165bdd94e Issue #3379607: use statements MUST be sorted alphabetically 1 year ago
Julian Pustkuchen d7c8b28690 Issue #3359592: Better explain $view_mode parameter for drupal_field() 1 year ago
Julian Pustkuchen 2e11f9b7ad Issue #3359146: Wrong preg_replace in ImageViewBuilderTest removes relevant attributes 1 year ago
Julian Pustkuchen adcf39754b Issue #3356079: Is the drupal_image() responsive parameter example correct? 1 year ago
Alison Jo 832a021ccb Issue #3363045: Feature(s) missing documentation 1 year ago
Julian Pustkuchen 0aea215552 Issue #3356155: Update twig/twig 1 year ago
Chi 25155dde83 Fix tests 1 year ago
Chi d77a4f7b8a Clean-up README.md 2 years ago
Samata soni a0c6fdc056 Update README.md 2 years ago
Chi 2c70fa668f Fix tests 2 years ago
Daniel Korte 75c512a6ca Issue #3324280 by Daniel Korte: Use TwigFilter instead of TwigFunction in hook_twig_tweak_filters_alter() 2 years ago
Chi 6073d18a46 Fix documentation of Field View Builder 2 years ago
Chi 8b33f99a8f Document magic properties in extractor services 2 years ago
Chi 8aa63ea1f2 Code clean-up 2 years ago
JeroenT 3e1a888711 Issue #3279991: drupal_entity_form add support for content moderation 2 years ago
Chi b13ba8b707 Fix image view builder test 2 years ago
Barry Fisher b36c208cc2 Issue #3260248 by Barry_Fisher: BlockViewBuilder error prevents cron from running when called from CLI 2 years ago
Ranjith Kumar K U 2de249ba8c Issue #3295743 by ranjith_kumar_k_u, Dharti Patel, dipesh_goswami, akshaydalvi212, Chi: PHPCS Drupal Coding Standard Issues 2 years ago
Chi f2805a1cc1 Fix tests 2 years ago
Anybody 848e2a8a63 Issue #3309673: (How to) overwrite drupal_view() views settings or provide parameters? 2 years ago
Chi b25684f98a Issue #3134193 by Anybody: Update cheat sheet 2 years ago
Chi 0883b1594e Clean-up documentation 2 years ago
Chi f7a9c7d997 Issue #3282742 by kenrbnsn: Support Twig 3 2 years ago
Jasper Lammens 27b50a6cb6 Add missing reference 3 years ago
kimberllyAmaral 3fd384e455 Update links 3 years ago
Chi 530adc97b8 Support Drupal 10 3 years ago
Chi 8dedaf4e61 Code clean-up 3 years ago
hudri 63947e19ad drupal_image: return first image by fid in case of ambiguous selector 3 years ago
gpotter fd7ee18be2 Issue #3222666 by gpotter, theRuslan: drupal_menu() incorrectly caches the active parent menu item in two-leveled menu 3 years ago
Chi 4037c5f7cf Fix broken tests 3 years ago
kristiaanvandeneynde 7a8e01b9b9 Issue #3245953 by kristiaanvandeneynde: TwigExtension::drupalTitle() should check for NullRouteMatch 3 years ago
Chi d4a7cf8af5 Add entity_url and entity_link filters 3 years ago
Chi c853e4027a Revert "Remove usage of deprecated functions" 3 years ago
Chi 0014fee04c Remove usage of deprecated functions 3 years ago
Chi 700d6c4ef9 Fix tests 3 years ago
Chi 3b17ed3500 Code clean-up 3 years ago
Juan Peña 1aad751621 Issue #3219625: Possible Variable Overwrite 3 years ago
sonfd 58ca88b667 Issue #3222014 by sonfd: Allow custom display settings for drupal_field() 3 years ago
Chi 22adcf19b9 Fix spelling 4 years ago
xandeadx c336ca8a19 Issue #3212340 by xandeadx: drupal_block() show fatal error if block plugin return NULL 4 years ago
  1. 9
      .gitlab-ci.yml
  2. 30
      README.md
  3. 11
      composer.json
  4. 73
      docs/cheat-sheet.md
  5. 19
      docs/migration-to-3.x.md
  6. BIN
      logo.png
  7. 77
      src/TwigTweakExtension.php
  8. 1
      src/UriExtractor.php
  9. 15
      src/UrlExtractor.php
  10. 18
      src/View/BlockViewBuilder.php
  11. 7
      src/View/EntityFormViewBuilder.php
  12. 16
      src/View/EntityViewBuilder.php
  13. 8
      src/View/FieldViewBuilder.php
  14. 63
      src/View/ImageViewBuilder.php
  15. 2
      src/View/MenuViewBuilder.php
  16. 4
      src/View/RegionViewBuilder.php
  17. 67
      tests/src/Functional/TwigTweakTest.php
  18. 8
      tests/src/Kernel/AbstractExtractorTestCase.php
  19. 45
      tests/src/Kernel/AbstractTestCase.php
  20. 15
      tests/src/Kernel/BlockViewBuilderTest.php
  21. 8
      tests/src/Kernel/CacheMetadataExtractorTest.php
  22. 13
      tests/src/Kernel/EntityFormViewBuilderTest.php
  23. 17
      tests/src/Kernel/EntityViewBuilderTest.php
  24. 11
      tests/src/Kernel/FieldViewBuilderTest.php
  25. 174
      tests/src/Kernel/ImageViewBuilderTest.php
  26. 23
      tests/src/Kernel/RegionViewBuilderTest.php
  27. 2
      tests/src/Kernel/UrlExtractorTest.php
  28. 10
      tests/twig_tweak_test/config/install/block.block.claro_powered_by_drupal.yml
  29. 19
      tests/twig_tweak_test/config/install/block.block.classy_content.yml
  30. 17
      tests/twig_tweak_test/config/install/block.block.classy_page_title.yml
  31. 19
      tests/twig_tweak_test/config/install/block.block.classy_status_messages.yml
  32. 17
      tests/twig_tweak_test/templates/twig-tweak-test.html.twig
  33. 2
      tests/twig_tweak_test/twig_tweak_test.info.yml
  34. 35
      twig_tweak.api.php
  35. 2
      twig_tweak.info.yml
  36. 5
      twig_tweak.services.yml

9
.gitlab-ci.yml

@ -0,0 +1,9 @@
include:
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
variables:
CORE_PHP_MIN: '8.1'

30
README.md

@ -1,15 +1,23 @@
# Twig Tweak # Twig Tweak
The module provides a Twig extension with some useful functions and filters. The Twig Tweak module provides a Twig extension with some useful functions
and filters that can improve development experience.
Some functions and filters are built in the Twig Tweak module, while others are
simple wrappers to integrate Drupal functions and services into Twig (for
example: `format_size`, `drupal_view_result`).
## Requirements
This module requires no modules outside of Drupal core.
## Installation ## Installation
Install as usual.
```shell - Install as you would normally install a contributed Drupal module. For further
composer require drupal/twig_tweak information, see [Installing Drupal Modules](https://www.drupal.org/docs/extending-drupal/installing-drupal-modules).
drush en twig_tweak
``` ## Links
## LINKS - Project page: https://www.drupal.org/project/twig_tweak
* Project page: https://www.drupal.org/project/twig_tweak - Twig home page: https://twig.symfony.com/
* Twig home page: https://twig.sensiolabs.org - Drupal Twig documentation: https://www.drupal.org/docs/theming-drupal/twig-in-drupal
* Drupal 8 Twig documentation: https://www.drupal.org/docs/8/theming/twig

11
composer.json

@ -12,11 +12,18 @@
"require": { "require": {
"php": ">=7.3", "php": ">=7.3",
"ext-json": "*", "ext-json": "*",
"drupal/core": "^9.0", "drupal/core": "^9.3 || ^10.0",
"twig/twig": "^2.12", "twig/twig": "^2.15.3 || ^3.4.3",
"symfony/polyfill-php80": "^1.17" "symfony/polyfill-php80": "^1.17"
}, },
"suggest": { "suggest": {
"symfony/var-dumper": "Better dump() function for debugging Twig variables" "symfony/var-dumper": "Better dump() function for debugging Twig variables"
},
"extra": {
"drush": {
"services": {
"drush.services.yml": "^9 || ^10 || ^11"
}
}
} }
} }

73
docs/cheat-sheet.md

@ -4,10 +4,17 @@
```twig ```twig
{{ drupal_view('who_s_new', 'block_1') }} {{ drupal_view('who_s_new', 'block_1') }}
``` ```
```twig
{# Specify additional parameters which map to contextual filters you have configured in your view. #}
{{ drupal_view('who_s_new', 'block_1', arg_1, arg_2, arg_3) }}
```
## Drupal View Result ## Drupal View Result
Checks results for a given view. Note that the results themselves are not printable.
```twig ```twig
{{ drupal_view_result('who_s_new', 'block_1') }} {% if drupal_view_result('cart')|length == 0 %}
{{ 'Your cart is empty.'|t }}
{% endif %}
``` ```
## Drupal Block ## Drupal Block
@ -25,6 +32,9 @@ drush ev "print_r(array_keys(\Drupal::service('plugin.manager.block')->getDefini
{# Bypass block.html.twig theming. #} {# Bypass block.html.twig theming. #}
{{ drupal_block('system_branding_block', wrapper=false) }} {{ drupal_block('system_branding_block', wrapper=false) }}
{# For block plugin that has a required context supply a context mapping to tell the block instance where to get that context from. #}
{{ drupal_block('plugin_id', {context_mapping: {node: '@node.node_route_context:node'}}) }}
``` ```
See [rendering blocks with Twig Tweak](blocks.md#block-plugin) for details. See [rendering blocks with Twig Tweak](blocks.md#block-plugin) for details.
@ -64,10 +74,23 @@ See [rendering blocks with Twig Tweak](blocks.md#block-plugin) for details.
``` ```
## Drupal Field ## Drupal Field
Note that drupal_field() does not work for view modes powered by Layout Builder.
```twig ```twig
{# Render field_image from node 1 in view_mode "full" (default). #}
{{ drupal_field('field_image', 'node', 1) }} {{ drupal_field('field_image', 'node', 1) }}
{# Render field_image from node 1 in view_mode "teaser". #}
{{ drupal_field('field_image', 'node', 1, 'teaser') }} {{ drupal_field('field_image', 'node', 1, 'teaser') }}
{# Render field_image from node 1 and instead of a view mode, provide an array of display options. #}
{# @see https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Entity!EntityViewBuilderInterface.php/function/EntityViewBuilderInterface%3A%3AviewField #}
{{ drupal_field('field_image', 'node', 1, {type: 'image_url', settings: {image_style: 'large'}}) }} {{ drupal_field('field_image', 'node', 1, {type: 'image_url', settings: {image_style: 'large'}}) }}
{# Render field_image from node 1 in view_mode "teaser" in English with access check disabled. #}
{{ drupal_field('field_image', 'node', 1, 'teaser', 'en', FALSE) }}
{# Render field_image from node 1 in view_mode "full" (default) with access check disabled (named argument). #}
{{ drupal_field('field_image', 'node', 1, check_access=false) }}
``` ```
## Drupal Menu ## Drupal Menu
@ -98,7 +121,11 @@ See [rendering blocks with Twig Tweak](blocks.md#block-plugin) for details.
{# Render image using 'thumbnail' image style and custom attributes. #} {# Render image using 'thumbnail' image style and custom attributes. #}
{{ drupal_image('public://ocean.jpg', 'thumbnail', {alt: 'The alternative text'|t, title: 'The title text'|t}) }} {{ drupal_image('public://ocean.jpg', 'thumbnail', {alt: 'The alternative text'|t, title: 'The title text'|t}) }}
{# Render responsive image. #} {# Render image using 'thumbnail' image style with lazy/eager loading (by attribute). #}
{{ drupal_image('public://ocean.jpg', 'thumbnail', {loading: 'lazy'}) }}
{{ drupal_image('public://ocean.jpg', 'thumbnail', {loading: 'eager'}) }}
{# Render responsive image (using a named argument). #}
{{ drupal_image('public://ocean.jpg', 'wide', responsive=true) }} {{ drupal_image('public://ocean.jpg', 'wide', responsive=true) }}
``` ```
@ -132,6 +159,8 @@ See [rendering blocks with Twig Tweak](blocks.md#block-plugin) for details.
## Drupal URL ## Drupal URL
```twig ```twig
{# The function accepts a valid internal path, such as "/node/1", "/taxonomy/term/1", a query string like "?query," or a fragment like "#anchor". #}
{# Basic usage. #} {# Basic usage. #}
{{ drupal_url('node/1') }} {{ drupal_url('node/1') }}
@ -168,12 +197,12 @@ See [rendering blocks with Twig Tweak](blocks.md#block-plugin) for details.
```twig ```twig
{# Basic usage. #} {# Basic usage. #}
<div class="contextual-region"> <div class="contextual-region">
{{ drupal_contextual_links('entity.view.edit_form:view=frontpage&display_id=feed_1:') }} {{ drupal_contextual_links('entity.view.edit_form:view=frontpage:display_id=feed_1') }}
{{ drupal_view('frontpage') }} {{ drupal_view('frontpage') }}
</div> </div>
{# Multiple links. #} {# Multiple links. #}
<div class="contextual-region"> <div class="contextual-region">
{{ drupal_contextual_links('node:node=123|block_content:block_content=123:') }} {{ drupal_contextual_links('node:node=123:|block_content:block_content=123:') }}
{{ content }} {{ content }}
</div> </div>
``` ```
@ -222,8 +251,9 @@ images when used in an `<img/>` tag.
``` ```
## Format size ## Format size
Generates a string representation for the given byte count.
```twig ```twig
{{ 12345|format_size() }} {{ 12345|format_size }}
``` ```
## Truncate ## Truncate
@ -248,7 +278,7 @@ images when used in an `<img/>` tag.
``` ```
## With ## With
This is an opposite of core `without` filter. This is an opposite of core `without` filter and adds properties instead of removing it.
```twig ```twig
{# Set top-level value. #} {# Set top-level value. #}
{{ content.field_image|with('#title', 'Photo'|t) }} {{ content.field_image|with('#title', 'Photo'|t) }}
@ -257,6 +287,15 @@ This is an opposite of core `without` filter.
{{ content|with(['field_image', '#title'], 'Photo'|t) }} {{ content|with(['field_image', '#title'], 'Photo'|t) }}
``` ```
## Data URI
The filter generates a URL using the data scheme as defined in [RFC 2397](https://datatracker.ietf.org/doc/html/rfc2397)
```twig
{# Inline image. #}
<img src="{{ '<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="50" fill="lime"/></svg>'|data_uri('image/svg+xml') }}" alt="{{ 'Rectangle'|t }}"/>
{# Image from file system. #}
<img src="{{ source(directory ~ '/images/logo.svg')|data_uri('image/svg+xml') }}" alt="{{ 'Logo'|t }}"/>
```
## Children ## Children
```twig ```twig
<ul> <ul>
@ -311,6 +350,28 @@ It is also possible to extract file URL directly from an entity.
{{ media|file_url }} {{ media|file_url }}
``` ```
## Entity URL
Gets the URL object for the entity.
See \Drupal\Core\Entity\EntityInterface::toUrl()
```twig
{# Creates canonical URL for the node. #}
{{ node|entity_url }}
{# Creates URL for the node edit form. #}
{{ node|entity_url('edit-form') }}
```
## Entity Link
Generates the HTML for a link to this entity.
See \Drupal\Core\Entity\EntityInterface::toLink()
```twig
{# Creates a link to the node using the node's label. #}
{{ node|entity_link }}
{# Creates link to node comment form. #}
{{ node|entity_link('Add new comment'|t, 'canonical', {fragment: 'comment-form'}) }}
```
## Entity translation ## Entity translation
That is typically needed when printing data from referenced entities. That is typically needed when printing data from referenced entities.
```twig ```twig

19
docs/migration-to-3.x.md

@ -7,6 +7,8 @@ Below are known BC breaks that may require updating your Twig templates.
Twig Tweak 3.x requires Drupal 9, Twig 2 and PHP 7.3. Twig Tweak 3.x requires Drupal 9, Twig 2 and PHP 7.3.
## Rendering entities ## Rendering entities
### Entity ID is now required
Entity ID parameter in `drupal_entity()` and `drupal_field()` functions is now Entity ID parameter in `drupal_entity()` and `drupal_field()` functions is now
mandatory. Previously it was possible to load entities from current route by mandatory. Previously it was possible to load entities from current route by
omitting entity ID parameter. However, that was making Twig templates coupled omitting entity ID parameter. However, that was making Twig templates coupled
@ -33,19 +35,32 @@ function preprocess_page(array &$variables): void {
} }
``` ```
### Default view mode has changed
The view mode parameter in `drupal_field()` has changed from `default` to `full`. If you are using `drupal_field()` without specifying a view mode, you should update your templates to specify the `default` one.
Before:
```
{{ drupal_field('field_name', 'node', node.id) }}
```
After:
```
{{ drupal_field('field_name', 'node', node.id, 'default') }}
```
## Rendering blocks ## Rendering blocks
`drupal_block()` now moves attributes provided by block plugin to the outer `drupal_block()` now moves attributes provided by block plugin to the outer
element. This may break some CSS rules. element. This may break some CSS rules.
Before: Before:
```HTML ```html
<div> <div>
<div class="from-block-plugin">Block content</div> <div class="from-block-plugin">Block content</div>
</div> </div>
``` ```
After: After:
```HTML ```html
<div class="from-block-plugin"> <div class="from-block-plugin">
<div>Block content</div> <div>Block content</div>
</div> </div>

BIN
logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

77
src/TwigTweakExtension.php

@ -4,6 +4,7 @@ namespace Drupal\twig_tweak;
use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Uuid\Uuid; use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityInterface;
@ -109,9 +110,12 @@ class TwigTweakExtension extends AbstractExtension {
new TwigFilter('truncate', [Unicode::class, 'truncate']), new TwigFilter('truncate', [Unicode::class, 'truncate']),
new TwigFilter('view', [self::class, 'viewFilter']), new TwigFilter('view', [self::class, 'viewFilter']),
new TwigFilter('with', [self::class, 'withFilter']), new TwigFilter('with', [self::class, 'withFilter']),
new TwigFilter('data_uri', [self::class, 'dataUriFilter']),
new TwigFilter('children', [self::class, 'childrenFilter']), new TwigFilter('children', [self::class, 'childrenFilter']),
new TwigFilter('file_uri', [self::class, 'fileUriFilter']), new TwigFilter('file_uri', [self::class, 'fileUriFilter']),
new TwigFilter('file_url', [self::class, 'fileUrlFilter']), new TwigFilter('file_url', [self::class, 'fileUrlFilter']),
new TwigFilter('entity_url', [self::class, 'entityUrl']),
new TwigFilter('entity_link', [self::class, 'entityLink']),
new TwigFilter('translation', [self::class, 'entityTranslation']), new TwigFilter('translation', [self::class, 'entityTranslation']),
new TwigFilter('cache_metadata', [self::class, 'CacheMetadata']), new TwigFilter('cache_metadata', [self::class, 'CacheMetadata']),
]; ];
@ -181,7 +185,8 @@ class TwigTweakExtension extends AbstractExtension {
*/ */
public static function drupalEntityForm(string $entity_type, ?string $id = NULL, string $form_mode = 'default', array $values = [], bool $check_access = TRUE): array { public static function drupalEntityForm(string $entity_type, ?string $id = NULL, string $form_mode = 'default', array $values = [], bool $check_access = TRUE): array {
$entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type); $entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type);
$entity = $id ? $entity_storage->load($id) : $entity_storage->create($values); $entity = $id ?
\Drupal::service('entity.repository')->getActive($entity_type, $id) : $entity_storage->create($values);
if ($entity) { if ($entity) {
return \Drupal::service('twig_tweak.entity_form_view_builder') return \Drupal::service('twig_tweak.entity_form_view_builder')
->build($entity, $form_mode, $check_access); ->build($entity, $form_mode, $check_access);
@ -192,7 +197,7 @@ class TwigTweakExtension extends AbstractExtension {
/** /**
* Returns the render array for a single entity field. * Returns the render array for a single entity field.
*/ */
public static function drupalField(string $field_name, string $entity_type, string $id, string $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array { public static function drupalField(string $field_name, string $entity_type, string $id, $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array {
$entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($id); $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($id);
if ($entity) { if ($entity) {
return \Drupal::service('twig_tweak.field_view_builder') return \Drupal::service('twig_tweak.field_view_builder')
@ -244,11 +249,13 @@ class TwigTweakExtension extends AbstractExtension {
->getStorage('file') ->getStorage('file')
->loadByProperties([$selector_type => $selector]); ->loadByProperties([$selector_type => $selector]);
// To avoid ambiguity render nothing unless exact one image has been found. if (count($files) == 0) {
if (count($files) != 1) {
return []; return [];
} }
// To avoid ambiguity order by fid.
ksort($files);
$file = reset($files); $file = reset($files);
return \Drupal::service('twig_tweak.image_view_builder')->build($file, $style, $attributes, $responsive, $check_access); return \Drupal::service('twig_tweak.image_view_builder')->build($file, $style, $attributes, $responsive, $check_access);
} }
@ -312,19 +319,22 @@ class TwigTweakExtension extends AbstractExtension {
/** /**
* Returns a title for the current route. * Returns a title for the current route.
*
* @todo Test it with NullRouteMatch
*/ */
public static function drupalTitle(): array { public static function drupalTitle(): array {
$title = \Drupal::service('title_resolver')->getTitle( $title = NULL;
\Drupal::request(), if ($route = \Drupal::routeMatch()->getRouteObject()) {
\Drupal::routeMatch()->getRouteObject() $title = \Drupal::service('title_resolver')->getTitle(\Drupal::request(), $route);
); }
$build['#markup'] = render($title); $build['#markup'] = is_array($title) ?
\Drupal::service('renderer')->render($title) : $title;
$build['#cache']['contexts'] = ['url']; $build['#cache']['contexts'] = ['url'];
return $build; return $build;
} }
/** /**
* Generates a URL from an internal path. * Generates a URL from an internal or external path.
* *
* @param string $user_input * @param string $user_input
* User input for a link or path. * User input for a link or path.
@ -345,6 +355,9 @@ class TwigTweakExtension extends AbstractExtension {
$options['language'] = $language; $options['language'] = $language;
} }
} }
if (UrlHelper::isExternal($user_input)) {
return Url::fromUri($user_input, $options);
}
if (!in_array($user_input[0], ['/', '#', '?'])) { if (!in_array($user_input[0], ['/', '#', '?'])) {
$user_input = '/' . $user_input; $user_input = '/' . $user_input;
} }
@ -497,7 +510,8 @@ class TwigTweakExtension extends AbstractExtension {
return NULL; return NULL;
} }
return file_url_transform_relative($image_style->buildUrl($path)); return \Drupal::service('file_url_generator')
->transformRelative($image_style->buildUrl($path));
} }
/** /**
@ -547,7 +561,7 @@ class TwigTweakExtension extends AbstractExtension {
/** @var \Drupal\Core\Entity\Plugin\DataType\EntityAdapter $parent */ /** @var \Drupal\Core\Entity\Plugin\DataType\EntityAdapter $parent */
if ($parent = $object->getParent()) { if ($parent = $object->getParent()) {
CacheableMetadata::createFromRenderArray($build) CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($parent->getEntity())) ->addCacheableDependency($parent->getEntity())
->applyTo($build); ->applyTo($build);
} }
} }
@ -557,6 +571,19 @@ class TwigTweakExtension extends AbstractExtension {
return $build; return $build;
} }
/**
* Creates a data URI (RFC 2397).
*/
public static function dataUriFilter(string $data, string $mime, array $parameters = []): string {
$uri = 'data:' . $mime;
foreach ($parameters as $key => $value) {
$uri .= ';' . $key . '=' . rawurlencode($value);
}
$uri .= \str_starts_with($data, 'text/') ?
',' . rawurlencode($data) : ';base64,' . base64_encode($data);
return $uri;
}
/** /**
* Adds new element to the array. * Adds new element to the array.
* *
@ -624,6 +651,30 @@ class TwigTweakExtension extends AbstractExtension {
return \Drupal::service('twig_tweak.url_extractor')->extractUrl($input, $relative); return \Drupal::service('twig_tweak.url_extractor')->extractUrl($input, $relative);
} }
/**
* Gets the URL object for the entity.
*
* @todo Remove this once Drupal allows `toUrl` method in the sandbox policy.
*
* @see https://www.drupal.org/node/2907810
* @see \Drupal\Core\Entity\EntityInterface::toUrl()
*/
public static function entityUrl(EntityInterface $entity, string $rel = 'canonical', array $options = []): Url {
return $entity->toUrl($rel, $options);
}
/**
* Gets the URL object for the entity.
*
* @todo Remove this once Drupal allows `toLink` method in the sandbox policy.
*
* @see https://www.drupal.org/node/2907810
* @see \Drupal\Core\Entity\EntityInterface::toLink()
*/
public static function entityLink(EntityInterface $entity, ?string $text = NULL, string $rel = 'canonical', array $options = []): Link {
return $entity->toLink($text, $rel, $options);
}
/** /**
* Returns the translation for the given entity. * Returns the translation for the given entity.
* *
@ -666,7 +717,7 @@ class TwigTweakExtension extends AbstractExtension {
*/ */
public static function phpFilter(array $context, string $code) { public static function phpFilter(array $context, string $code) {
// Make Twig variables available in PHP code. // Make Twig variables available in PHP code.
extract($context); extract($context, EXTR_SKIP);
ob_start(); ob_start();
// phpcs:ignore Drupal.Functions.DiscouragedFunctions.Discouraged // phpcs:ignore Drupal.Functions.DiscouragedFunctions.Discouraged
print eval($code); print eval($code);

1
src/UriExtractor.php

@ -41,6 +41,7 @@ class UriExtractor {
public function extractUri(?object $input): ?string { public function extractUri(?object $input): ?string {
$entity = $input; $entity = $input;
if ($input instanceof EntityReferenceFieldItemListInterface) { if ($input instanceof EntityReferenceFieldItemListInterface) {
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item */
if ($item = $input->first()) { if ($item = $input->first()) {
$entity = $item->entity; $entity = $item->entity;
} }

15
src/UrlExtractor.php

@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface; use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldItemList; use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\file\FileInterface; use Drupal\file\FileInterface;
use Drupal\link\LinkItemInterface; use Drupal\link\LinkItemInterface;
use Drupal\media\MediaInterface; use Drupal\media\MediaInterface;
@ -24,11 +25,19 @@ class UrlExtractor {
*/ */
protected $entityTypeManager; protected $entityTypeManager;
/**
* The file URL generator.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected $fileUrlGenerator;
/** /**
* Constructs a UrlExtractor object. * Constructs a UrlExtractor object.
*/ */
public function __construct(EntityTypeManagerInterface $entity_type_manager) { public function __construct(EntityTypeManagerInterface $entity_type_manager, FileUrlGeneratorInterface $file_url_generator) {
$this->entityTypeManager = $entity_type_manager; $this->entityTypeManager = $entity_type_manager;
$this->fileUrlGenerator = $file_url_generator;
} }
/** /**
@ -44,8 +53,7 @@ class UrlExtractor {
*/ */
public function extractUrl($input, bool $relative = TRUE): ?string { public function extractUrl($input, bool $relative = TRUE): ?string {
if (is_string($input)) { if (is_string($input)) {
$url = file_create_url($input); return $this->fileUrlGenerator->{$relative ? 'generateString' : 'generateAbsoluteString'}($input);
return $relative ? file_url_transform_relative($url) : $url;
} }
elseif ($input instanceof LinkItemInterface) { elseif ($input instanceof LinkItemInterface) {
return $input->getUrl()->toString(); return $input->getUrl()->toString();
@ -56,6 +64,7 @@ class UrlExtractor {
$entity = $input; $entity = $input;
if ($input instanceof EntityReferenceFieldItemListInterface) { if ($input instanceof EntityReferenceFieldItemListInterface) {
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item */
if ($item = $input->first()) { if ($item = $input->first()) {
$entity = $item->entity; $entity = $item->entity;
} }

18
src/View/BlockViewBuilder.php

@ -7,9 +7,9 @@ use Drupal\Core\Block\TitleBlockPluginInterface;
use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface; use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface; use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Render\Element; use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
@ -121,10 +121,15 @@ class BlockViewBuilder {
if ($access->isAllowed()) { if ($access->isAllowed()) {
// Title block needs a special treatment. // Title block needs a special treatment.
if ($block_plugin instanceof TitleBlockPluginInterface) { if ($block_plugin instanceof TitleBlockPluginInterface) {
// Account for the scenario that a NullRouteMatch is returned. This, for
// example, is the case when Search API is indexing the site during
// Drush cron.
if ($route = $this->routeMatch->getRouteObject()) {
$request = $this->requestStack->getCurrentRequest(); $request = $this->requestStack->getCurrentRequest();
$title = $this->titleResolver->getTitle($request, $this->routeMatch->getRouteObject()); $title = $this->titleResolver->getTitle($request, $route);
$block_plugin->setTitle($title); $block_plugin->setTitle($title);
} }
}
// Place the content returned by the block plugin into a 'content' child // Place the content returned by the block plugin into a 'content' child
// element, as a way to allow the plugin to have complete control of its // element, as a way to allow the plugin to have complete control of its
@ -135,8 +140,9 @@ class BlockViewBuilder {
if ($block_plugin instanceof TitleBlockPluginInterface) { if ($block_plugin instanceof TitleBlockPluginInterface) {
$build['content']['#cache']['contexts'][] = 'url'; $build['content']['#cache']['contexts'][] = 'url';
} }
// Some blocks return null instead of array when empty.
if ($wrapper && !Element::isEmpty($build['content'])) { // @see https://www.drupal.org/project/drupal/issues/3212354
if ($wrapper && is_array($build['content']) && !Element::isEmpty($build['content'])) {
$build += [ $build += [
'#theme' => 'block', '#theme' => 'block',
'#id' => $configuration['id'] ?? NULL, '#id' => $configuration['id'] ?? NULL,
@ -161,8 +167,8 @@ class BlockViewBuilder {
} }
CacheableMetadata::createFromRenderArray($build) CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($access)) ->addCacheableDependency($access)
->merge(CacheableMetadata::createFromObject($block_plugin)) ->addCacheableDependency($block_plugin)
->applyTo($build); ->applyTo($build);
if (!isset($build['#cache']['keys'])) { if (!isset($build['#cache']['keys'])) {

7
src/View/EntityFormViewBuilder.php

@ -29,6 +29,8 @@ class EntityFormViewBuilder {
/** /**
* Gets the built and processed entity form for the given entity type. * Gets the built and processed entity form for the given entity type.
* *
* @todo Add langcode parameter.
*
* @param \Drupal\Core\Entity\EntityInterface $entity * @param \Drupal\Core\Entity\EntityInterface $entity
* The entity. * The entity.
* @param string $form_mode * @param string $form_mode
@ -40,7 +42,6 @@ class EntityFormViewBuilder {
* The processed form for the given entity type and form mode. * The processed form for the given entity type and form mode.
*/ */
public function build(EntityInterface $entity, string $form_mode = 'default', bool $check_access = TRUE): array { public function build(EntityInterface $entity, string $form_mode = 'default', bool $check_access = TRUE): array {
$build = []; $build = [];
$operation = $entity->isNew() ? 'create' : 'update'; $operation = $entity->isNew() ? 'create' : 'update';
@ -50,8 +51,8 @@ class EntityFormViewBuilder {
} }
CacheableMetadata::createFromRenderArray($build) CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($entity)) ->addCacheableDependency($access)
->merge(CacheableMetadata::createFromObject($access)) ->addCacheableDependency($entity)
->applyTo($build); ->applyTo($build);
return $build; return $build;

16
src/View/EntityViewBuilder.php

@ -5,6 +5,7 @@ namespace Drupal\twig_tweak\View;
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface;
/** /**
@ -19,11 +20,19 @@ class EntityViewBuilder {
*/ */
protected $entityTypeManager; protected $entityTypeManager;
/**
* The entity repository service.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/** /**
* Constructs an EntityViewBuilder object. * Constructs an EntityViewBuilder object.
*/ */
public function __construct(EntityTypeManagerInterface $entity_type_manager) { public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository) {
$this->entityTypeManager = $entity_type_manager; $this->entityTypeManager = $entity_type_manager;
$this->entityRepository = $entity_repository;
} }
/** /**
@ -31,6 +40,7 @@ class EntityViewBuilder {
*/ */
public function build(EntityInterface $entity, string $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array { public function build(EntityInterface $entity, string $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array {
$build = []; $build = [];
$entity = $this->entityRepository->getTranslationFromContext($entity, $langcode);
$access = $check_access ? $entity->access('view', NULL, TRUE) : AccessResult::allowed(); $access = $check_access ? $entity->access('view', NULL, TRUE) : AccessResult::allowed();
if ($access->isAllowed()) { if ($access->isAllowed()) {
$build = $this->entityTypeManager $build = $this->entityTypeManager
@ -38,8 +48,8 @@ class EntityViewBuilder {
->view($entity, $view_mode, $langcode); ->view($entity, $view_mode, $langcode);
} }
CacheableMetadata::createFromRenderArray($build) CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($entity)) ->addCacheableDependency($access)
->merge(CacheableMetadata::createFromObject($access)) ->addCacheableDependency($entity)
->applyTo($build); ->applyTo($build);
return $build; return $build;
} }

8
src/View/FieldViewBuilder.php

@ -34,7 +34,7 @@ class FieldViewBuilder {
* @param string $field_name * @param string $field_name
* The field name. * The field name.
* @param string|array $view_mode * @param string|array $view_mode
* (optional) The view mode that should be used to render the field. * (optional) The view mode or display options.
* @param string $langcode * @param string $langcode
* (optional) Language code to load translation. * (optional) Language code to load translation.
* @param bool $check_access * @param bool $check_access
@ -42,6 +42,8 @@ class FieldViewBuilder {
* *
* @return array * @return array
* A render array for the field. * A render array for the field.
*
* @see \Drupal\Core\Entity\EntityViewBuilderInterface::viewField()
*/ */
public function build( public function build(
EntityInterface $entity, EntityInterface $entity,
@ -64,8 +66,8 @@ class FieldViewBuilder {
} }
CacheableMetadata::createFromRenderArray($build) CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($access)) ->addCacheableDependency($access)
->merge(CacheableMetadata::createFromObject($entity)) ->addCacheableDependency($entity)
->applyTo($build); ->applyTo($build);
return $build; return $build;

63
src/View/ImageViewBuilder.php

@ -4,6 +4,7 @@ namespace Drupal\twig_tweak\View;
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Image\ImageFactory;
use Drupal\file\FileInterface; use Drupal\file\FileInterface;
/** /**
@ -11,6 +12,20 @@ use Drupal\file\FileInterface;
*/ */
class ImageViewBuilder { class ImageViewBuilder {
/**
* The provider image factory.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;
/**
* Constructs an ImageViewBuilder object.
*/
public function __construct(ImageFactory $imageFactory) {
$this->imageFactory = $imageFactory;
}
/** /**
* Builds an image. * Builds an image.
* *
@ -29,14 +44,45 @@ class ImageViewBuilder {
* A renderable array to represent the image. * A renderable array to represent the image.
*/ */
public function build(FileInterface $file, string $style = NULL, array $attributes = [], bool $responsive = FALSE, bool $check_access = TRUE): array { public function build(FileInterface $file, string $style = NULL, array $attributes = [], bool $responsive = FALSE, bool $check_access = TRUE): array {
$access = $check_access ? $file->access('view', NULL, TRUE) : AccessResult::allowed(); $access = $check_access ? $file->access('view', NULL, TRUE) : AccessResult::allowed();
$build = []; $build = $access->isAllowed() ? $this->doBuild($file, $style, $attributes, $responsive) : [];
if ($access->isAllowed()) {
CacheableMetadata::createFromRenderArray($build)
->addCacheableDependency($access)
->addCacheableDependency($file)
->applyTo($build);
return $build;
}
/**
* Actually builds the image.
*/
private function doBuild(FileInterface $file, string $style = NULL, array $attributes = [], bool $responsive = FALSE): array {
$build['#uri'] = $file->getFileUri(); $build['#uri'] = $file->getFileUri();
$build['#attributes'] = $attributes; $build['#attributes'] = $attributes;
if ($style) {
if (!$style) {
$build['#theme'] = 'image';
return $build;
}
$build['#width'] = $attributes['width'] ?? NULL;
$build['#height'] = $attributes['height'] ?? NULL;
if (!$build['#width'] && !$build['#height']) {
// If an image style is given, image module needs the original image
// dimensions to calculate image style's width and height and set the
// attributes.
// @see https://www.drupal.org/project/twig_tweak/issues/3356042
$image = $this->imageFactory->get($file->getFileUri());
if ($image->isValid()) {
$build['#width'] = $image->getWidth();
$build['#height'] = $image->getHeight();
}
}
if ($responsive) { if ($responsive) {
$build['#type'] = 'responsive_image'; $build['#type'] = 'responsive_image';
$build['#responsive_image_style_id'] = $style; $build['#responsive_image_style_id'] = $style;
@ -45,15 +91,6 @@ class ImageViewBuilder {
$build['#theme'] = 'image_style'; $build['#theme'] = 'image_style';
$build['#style_name'] = $style; $build['#style_name'] = $style;
} }
}
else {
$build['#theme'] = 'image';
}
}
CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($access))
->applyTo($build);
return $build; return $build;
} }

2
src/View/MenuViewBuilder.php

@ -76,6 +76,8 @@ class MenuViewBuilder {
]; ];
} }
$build['#cache']['contexts'][] = 'route.menu_active_trails:' . $menu_name;
return $build; return $build;
} }

4
src/View/RegionViewBuilder.php

@ -7,7 +7,7 @@ use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Drupal\Core\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
/** /**
@ -89,7 +89,7 @@ class RegionViewBuilder {
/** @var \Drupal\block\BlockInterface[] $blocks */ /** @var \Drupal\block\BlockInterface[] $blocks */
foreach ($blocks as $id => $block) { foreach ($blocks as $id => $block) {
$access = $block->access('view', NULL, TRUE); $access = $block->access('view', NULL, TRUE);
$cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($access)); $cache_metadata = $cache_metadata->addCacheableDependency($access);
if ($access->isAllowed()) { if ($access->isAllowed()) {
$block_plugin = $block->getPlugin(); $block_plugin = $block->getPlugin();
if ($block_plugin instanceof TitleBlockPluginInterface) { if ($block_plugin instanceof TitleBlockPluginInterface) {

67
tests/src/Functional/TwigTweakTest.php

@ -5,12 +5,13 @@ namespace Drupal\Tests\twig_tweak\Functional;
use Drupal\Core\Link; use Drupal\Core\Link;
use Drupal\Core\Render\Markup; use Drupal\Core\Render\Markup;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\file\Entity\File; use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\language\Entity\ConfigurableLanguage; use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\media\Entity\Media; use Drupal\media\Entity\Media;
use Drupal\responsive_image\Entity\ResponsiveImageStyle; use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\user\Entity\Role; use Drupal\user\Entity\Role;
/** /**
@ -25,7 +26,7 @@ final class TwigTweakTest extends BrowserTestBase {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected $defaultTheme = 'classy'; protected $defaultTheme = 'claro';
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -53,14 +54,14 @@ final class TwigTweakTest extends BrowserTestBase {
$image_file = File::create([ $image_file = File::create([
'uri' => $test_files[0]->uri, 'uri' => $test_files[0]->uri,
'uuid' => 'b2c22b6f-7bf8-4da4-9de5-316e93487518', 'uuid' => 'b2c22b6f-7bf8-4da4-9de5-316e93487518',
'status' => FILE_STATUS_PERMANENT, 'status' => FileInterface::STATUS_PERMANENT,
]); ]);
$image_file->save(); $image_file->save();
$media_file = File::create([ $media_file = File::create([
'uri' => $test_files[8]->uri, 'uri' => $test_files[8]->uri,
'uuid' => '5dd794d0-cb75-4130-9296-838aebc1fe74', 'uuid' => '5dd794d0-cb75-4130-9296-838aebc1fe74',
'status' => FILE_STATUS_PERMANENT, 'status' => FileInterface::STATUS_PERMANENT,
]); ]);
$media_file->save(); $media_file->save();
@ -133,21 +134,20 @@ final class TwigTweakTest extends BrowserTestBase {
// -- Block. // -- Block.
$xpath = '//div[@class = "tt-block"]'; $xpath = '//div[@class = "tt-block"]';
$xpath .= '/img[contains(@src, "/core/themes/classy/logo.svg") and @alt="Home"]'; $xpath .= '/img[contains(@src, "/core/themes/claro/logo.svg") and @alt="Home"]';
$this->assertXpath($xpath); $this->assertXpath($xpath);
// -- Block with wrapper. // -- Block with wrapper.
$xpath = '//div[@class = "tt-block-with-wrapper"]'; $xpath = '//div[@class = "tt-block-with-wrapper"]';
$xpath .= '/div[@class = "block block-system block-system-branding-block"]'; $xpath .= '/div[@class = "block block-system block-system-branding-block"]';
$xpath .= '/h2[text() = "Branding"]'; $xpath .= '/h2[text() = "Branding"]';
$xpath .= '/following-sibling::a[img[contains(@src, "/core/themes/classy/logo.svg") and @alt="Home"]]'; $xpath .= '/following-sibling::a[img[contains(@src, "/core/themes/claro/logo.svg") and @alt="Home"]]';
$xpath .= '/following-sibling::div[@class = "site-name"]/a'; $xpath .= '/following-sibling::div[@class = "site-name"]/a';
$this->assertXpath($xpath); $this->assertXpath($xpath);
// -- Region. // -- Region.
$xpath = '//div[@class = "tt-region"]/div[@class = "region region-sidebar-first"]'; $xpath = '//div[@class = "tt-region"]/div[@class = "region region-highlighted"]';
$xpath .= '/div[contains(@class, "block-page-title-block") and h1[@class="page-title" and text() = "Twig Tweak Test"]]'; $xpath .= '/div[contains(@class, "block-system-powered-by-block")]/span[. = "Powered by Drupal"]';
$xpath .= '/following-sibling::div[contains(@class, "block-system-powered-by-block")]/span[. = "Powered by Drupal"]';
$this->assertXpath($xpath); $this->assertXpath($xpath);
// -- Entity (default view mode). // -- Entity (default view mode).
@ -190,13 +190,13 @@ final class TwigTweakTest extends BrowserTestBase {
// -- Entity add form. // -- Entity add form.
$xpath = '//div[@class = "tt-entity-add-form"]/form'; $xpath = '//div[@class = "tt-entity-add-form"]/form';
$xpath .= '//input[@name = "title[0][value]" and @value = ""]'; $xpath .= '//input[@name = "title[0][value]" and @value = ""]';
$xpath .= '/../../../div/input[@type = "submit" and @value = "Save"]'; $xpath .= '/../../../../..//div/input[@type = "submit" and @value = "Save"]';
$this->assertXpath($xpath); $this->assertXpath($xpath);
// -- Entity edit form. // -- Entity edit form.
$xpath = '//div[@class = "tt-entity-edit-form"]/form'; $xpath = '//div[@class = "tt-entity-edit-form"]/form';
$xpath .= '//input[@name = "title[0][value]" and @value = "Alpha"]'; $xpath .= '//input[@name = "title[0][value]" and @value = "Alpha"]';
$xpath .= '/../../../div/input[@type = "submit" and @value = "Save"]'; $xpath .= '/../../../../..//div/input[@type = "submit" and @value = "Save"]';
$this->assertXpath($xpath); $this->assertXpath($xpath);
// -- Field. // -- Field.
@ -265,18 +265,23 @@ final class TwigTweakTest extends BrowserTestBase {
$xpath = sprintf('//div[@class = "tt-url"]/div[@data-case="with-langcode" and text() = "%s"]', $url); $xpath = sprintf('//div[@class = "tt-url"]/div[@data-case="with-langcode" and text() = "%s"]', $url);
$this->assertXpath($xpath); $this->assertXpath($xpath);
// -- External URL.
$url = 'https://example.com/node?foo=bar&page=1#here';
$xpath = sprintf('//div[@class = "tt-url"]/div[@data-case="external" and text() = "%s"]', $url);
$this->assertXpath($xpath);
// -- Link. // -- Link.
$url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]); $url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]);
$link = Link::fromTextAndUrl('Edit', $url)->toString(); $link = Link::fromTextAndUrl('Edit', $url)->toString();
$xpath = '//div[@class = "tt-link"]'; $xpath = '//div[@class = "tt-link"]';
self::assertEquals($link, $this->xpath($xpath)[0]->getHtml()); self::assertSame((string) $link, $this->xpath($xpath)[0]->getHtml());
// -- Link with HTML. // -- Link with HTML.
$text = Markup::create('<b>Edit</b>'); $text = Markup::create('<b>Edit</b>');
$url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]); $url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]);
$link = Link::fromTextAndUrl($text, $url)->toString(); $link = Link::fromTextAndUrl($text, $url)->toString();
$xpath = '//div[@class = "tt-link-html"]'; $xpath = '//div[@class = "tt-link-html"]';
self::assertEquals($link, $this->xpath($xpath)[0]->getHtml()); self::assertSame((string) $link, $this->xpath($xpath)[0]->getHtml());
// -- Status messages. // -- Status messages.
$xpath = '//div[@class = "tt-messages"]//div[contains(@class, "messages--status") and contains(., "Hello world!")]'; $xpath = '//div[@class = "tt-messages"]//div[contains(@class, "messages--status") and contains(., "Hello world!")]';
@ -337,6 +342,14 @@ final class TwigTweakTest extends BrowserTestBase {
$xpath = '//div[@class = "tt-with-nested" and text() = "{alpha:{beta:{gamma:456}}}"]'; $xpath = '//div[@class = "tt-with-nested" and text() = "{alpha:{beta:{gamma:456}}}"]';
$this->assertXpath($xpath); $this->assertXpath($xpath);
// -- Data URI (SVG).
$xpath = '//div[@class = "tt-data-uri-svg"]/img[@src = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iNTAiIGZpbGw9ImxpbWUiLz48L3N2Zz4="]';
$this->assertXpath($xpath);
// -- Data URI (Iframe).
$xpath = '//div[@class = "tt-data-uri-iframe"]/iframe[@src = "data:text/html;charset=UTF-8;base64,PGgxPkhlbGxvIHdvcmxkITwvaDE+"]';
$this->assertXpath($xpath);
// -- 'children'. // -- 'children'.
$xpath = '//div[@class = "tt-children" and text() = "doremi"]'; $xpath = '//div[@class = "tt-children" and text() = "doremi"]';
$this->assertXpath($xpath); $this->assertXpath($xpath);
@ -390,6 +403,30 @@ final class TwigTweakTest extends BrowserTestBase {
$xpath = '//div[@class = "tt-file-url-from-media-field" and contains(text(), "/files/image-1.png")]'; $xpath = '//div[@class = "tt-file-url-from-media-field" and contains(text(), "/files/image-1.png")]';
$this->assertXpath($xpath); $this->assertXpath($xpath);
// -- Entity URL (canonical).
$xpath = '//div[@class = "tt-entity-url" and contains(text(), "/node/1#test") and not(contains(text(), "http"))]';
$this->assertXpath($xpath);
// -- Entity URL (absolute).
$xpath = '//div[@class = "tt-entity-url-absolute" and contains(text(), "/node/1") and contains(text(), "http")]';
$this->assertXpath($xpath);
// -- Entity URL (edit form).
$xpath = '//div[@class = "tt-entity-url-edit-form" and contains(text(), "/node/1/edit")]';
$this->assertXpath($xpath);
// -- Entity Link (canonical).
$xpath = '//div[@class = "tt-entity-link"]/a[text() = "Alpha" and contains(@href, "/node/1") and not(contains(@href, "http"))]';
$this->assertXpath($xpath);
// -- Entity Link (absolute).
$xpath = '//div[@class = "tt-entity-link-absolute"]/a[text() = "Example" and contains(@href, "/node/1") and contains(@href, "http")]';
$this->assertXpath($xpath);
// -- Entity Link (edit form).
$xpath = '//div[@class = "tt-entity-link-edit-form"]/a[text() = "Edit" and contains(@href, "/node/1/edit")]';
$this->assertXpath($xpath);
// -- Entity translation. // -- Entity translation.
// This is just a smoke test because the node is not translatable. // This is just a smoke test because the node is not translatable.
$xpath = '//div[@class = "tt-translation" and contains(text(), "Alpha")]'; $xpath = '//div[@class = "tt-translation" and contains(text(), "Alpha")]';
@ -409,7 +446,7 @@ final class TwigTweakTest extends BrowserTestBase {
} }
/** /**
* Checks that an element specified by a the xpath exists on the current page. * Checks that an element specified by the xpath exists on the current page.
*/ */
private function assertXpath(string $xpath): void { private function assertXpath(string $xpath): void {
$this->assertSession()->elementExists('xpath', $xpath); $this->assertSession()->elementExists('xpath', $xpath);

8
tests/src/Kernel/AbstractExtractorTestCase.php

@ -3,6 +3,7 @@
namespace Drupal\Tests\twig_tweak\Kernel; namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\file\Entity\File; use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\KernelTests\KernelTestBase; use Drupal\KernelTests\KernelTestBase;
use Drupal\media\Entity\Media; use Drupal\media\Entity\Media;
use Drupal\node\Entity\Node; use Drupal\node\Entity\Node;
@ -49,22 +50,23 @@ abstract class AbstractExtractorTestCase extends KernelTestBase {
$this->installConfig(['node', 'twig_tweak_test']); $this->installConfig(['node', 'twig_tweak_test']);
$this->installSchema('file', 'file_usage'); $this->installSchema('file', 'file_usage');
$this->installEntitySchema('user');
$this->installEntitySchema('file'); $this->installEntitySchema('file');
$this->installEntitySchema('media'); $this->installEntitySchema('media');
$test_files = $this->getTestFiles('image'); $test_files = $this->getTestFiles('image');
//
$image_file = File::create([ $image_file = File::create([
'uri' => $test_files[0]->uri, 'uri' => $test_files[0]->uri,
'uuid' => 'a2cb2b6f-7bf8-4da4-9de5-316e93487518', 'uuid' => 'a2cb2b6f-7bf8-4da4-9de5-316e93487518',
'status' => FILE_STATUS_PERMANENT, 'status' => FileInterface::STATUS_PERMANENT,
]); ]);
$image_file->save(); $image_file->save();
$media_file = File::create([ $media_file = File::create([
'uri' => $test_files[2]->uri, 'uri' => $test_files[2]->uri,
'uuid' => '5dd794d0-cb75-4130-9296-838aebc1fe74', 'uuid' => '5dd794d0-cb75-4130-9296-838aebc1fe74',
'status' => FILE_STATUS_PERMANENT, 'status' => FileInterface::STATUS_PERMANENT,
]); ]);
$media_file->save(); $media_file->save();

45
tests/src/Kernel/AbstractTestCase.php

@ -0,0 +1,45 @@
<?php
namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\KernelTests\KernelTestBase;
use PHPUnit\Framework\Assert as PHPUnitAssert;
/**
* A base class for Twig Tweak kernel tests.
*/
abstract class AbstractTestCase extends KernelTestBase {
/**
* Asserts cache metadata.
*/
protected static function assertCache(array $expected_cache, array $actual_cache): void {
self::sortCache($expected_cache);
self::sortCache($actual_cache);
PHPUnitAssert::assertSame($expected_cache, $actual_cache);
}
/**
* Asserts render array.
*/
protected static function assertRenderArray(array $expected_build, array $actual_build): void {
self::sortCache($expected_build['#cache']);
self::sortCache($actual_build['#cache']);
PHPUnitAssert::assertSame($expected_build, $actual_build);
}
/**
* Sort cache metadata.
*
* @see https://www.drupal.org/node/3230171
*/
private static function sortCache(array &$cache): void {
if (\array_key_exists('tags', $cache)) {
sort($cache['tags']);
}
if (\array_key_exists('contexts', $cache)) {
sort($cache['contexts']);
}
}
}

15
tests/src/Kernel/BlockViewBuilderTest.php

@ -54,8 +54,8 @@ final class BlockViewBuilderTest extends KernelTestBase {
'#configuration' => [ '#configuration' => [
'id' => 'twig_tweak_test_foo', 'id' => 'twig_tweak_test_foo',
'label' => '', 'label' => '',
'provider' => 'twig_tweak_test',
'label_display' => 'visible', 'label_display' => 'visible',
'provider' => 'twig_tweak_test',
'content' => 'Foo', 'content' => 'Foo',
], ],
'#plugin_id' => 'twig_tweak_test_foo', '#plugin_id' => 'twig_tweak_test_foo',
@ -73,6 +73,19 @@ final class BlockViewBuilderTest extends KernelTestBase {
], ],
], ],
]; ];
// @todo Remove this once we drop support for Drupal 9.2.
// @see https://www.drupal.org/node/3230199
if (\version_compare(\Drupal::VERSION, '9.3.0-dev', '<')) {
$expected_build['#configuration'] = [
'id' => 'twig_tweak_test_foo',
'label' => '',
'provider' => 'twig_tweak_test',
'label_display' => 'visible',
'content' => 'Foo',
];
}
self::assertSame($expected_build, $build); self::assertSame($expected_build, $build);
self::assertSame('<div id="foo">Foo</div>', $this->renderPlain($build)); self::assertSame('<div id="foo">Foo</div>', $this->renderPlain($build));

8
tests/src/Kernel/CacheMetadataExtractorTest.php

@ -3,14 +3,13 @@
namespace Drupal\Tests\twig_tweak\Kernel; namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableMetadata;
use Drupal\KernelTests\KernelTestBase;
/** /**
* A test for Cache Metadata Extractor service. * A test for Cache Metadata Extractor service.
* *
* @group twig_tweak * @group twig_tweak
*/ */
final class CacheMetadataExtractorTest extends KernelTestBase { final class CacheMetadataExtractorTest extends AbstractTestCase {
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -88,10 +87,11 @@ final class CacheMetadataExtractorTest extends KernelTestBase {
'max-age' => 10, 'max-age' => 10,
], ],
]; ];
self::assertSame($expected_build, $build); self::assertRenderArray($expected_build, $build);
// -- Wrong type. // -- Wrong type.
self::expectErrorMessage('The input should be either instance of Drupal\Core\Cache\CacheableDependencyInterface or array. stdClass was given.'); $exception = new \InvalidArgumentException('The input should be either instance of Drupal\Core\Cache\CacheableDependencyInterface or array. stdClass was given.');
self::expectExceptionObject($exception);
/* @noinspection PhpParamsInspection */ /* @noinspection PhpParamsInspection */
$extractor->extractCacheMetadata(new \stdClass()); $extractor->extractCacheMetadata(new \stdClass());
} }

13
tests/src/Kernel/EntityFormViewBuilderTest.php

@ -3,17 +3,16 @@
namespace Drupal\Tests\twig_tweak\Kernel; namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\node\Entity\Node; use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType; use Drupal\node\Entity\NodeType;
use Drupal\Tests\user\Traits\UserCreationTrait;
/** /**
* A test for EntityFormViewBuilder. * A test for EntityFormViewBuilder.
* *
* @group twig_tweak * @group twig_tweak
*/ */
final class EntityFormViewBuilderTest extends KernelTestBase { final class EntityFormViewBuilderTest extends AbstractTestCase {
use UserCreationTrait; use UserCreationTrait;
@ -47,7 +46,7 @@ final class EntityFormViewBuilderTest extends KernelTestBase {
/** /**
* Test callback. * Test callback.
*/ */
public function testEntityViewBuilder(): void { public function testEntityFormViewBuilder(): void {
$view_builder = $this->container->get('twig_tweak.entity_form_view_builder'); $view_builder = $this->container->get('twig_tweak.entity_form_view_builder');
@ -82,7 +81,7 @@ final class EntityFormViewBuilderTest extends KernelTestBase {
], ],
'max-age' => 50, 'max-age' => 50,
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
self::assertStringContainsString('<form class="node-article-form node-form" ', $this->renderPlain($build)); self::assertStringContainsString('<form class="node-article-form node-form" ', $this->renderPlain($build));
// -- Private node with access check. // -- Private node with access check.
@ -100,7 +99,7 @@ final class EntityFormViewBuilderTest extends KernelTestBase {
], ],
'max-age' => 50, 'max-age' => 50,
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
self::assertSame('', $this->renderPlain($build)); self::assertSame('', $this->renderPlain($build));
// -- Private node without access check. // -- Private node without access check.
@ -117,7 +116,7 @@ final class EntityFormViewBuilderTest extends KernelTestBase {
], ],
'max-age' => Cache::PERMANENT, 'max-age' => Cache::PERMANENT,
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
self::assertStringContainsString('<form class="node-article-form node-form" ', $this->renderPlain($build)); self::assertStringContainsString('<form class="node-article-form node-form" ', $this->renderPlain($build));
} }

17
tests/src/Kernel/EntityViewBuilderTest.php

@ -3,7 +3,6 @@
namespace Drupal\Tests\twig_tweak\Kernel; namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node; use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType; use Drupal\node\Entity\NodeType;
use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\Tests\user\Traits\UserCreationTrait;
@ -13,7 +12,7 @@ use Drupal\Tests\user\Traits\UserCreationTrait;
* *
* @group twig_tweak * @group twig_tweak
*/ */
final class EntityViewBuilderTest extends KernelTestBase { final class EntityViewBuilderTest extends AbstractTestCase {
use UserCreationTrait; use UserCreationTrait;
@ -84,10 +83,10 @@ final class EntityViewBuilderTest extends KernelTestBase {
], ],
'bin' => 'render', 'bin' => 'render',
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
$expected_html = <<< 'HTML' $expected_html = <<< 'HTML'
<article role="article"> <article>
<h2><a href="/node/1" rel="bookmark"><span>Public node</span></a></h2> <h2><a href="/node/1" rel="bookmark"><span>Public node</span></a></h2>
<div></div> <div></div>
</article> </article>
@ -117,10 +116,10 @@ final class EntityViewBuilderTest extends KernelTestBase {
], ],
'bin' => 'render', 'bin' => 'render',
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
$expected_html = <<< 'HTML' $expected_html = <<< 'HTML'
<article role="article"> <article>
<h2><a href="/node/1" rel="bookmark"><span>Public node</span></a></h2> <h2><a href="/node/1" rel="bookmark"><span>Public node</span></a></h2>
<div> <div>
<ul class="links inline"> <ul class="links inline">
@ -150,7 +149,7 @@ final class EntityViewBuilderTest extends KernelTestBase {
], ],
'max-age' => 50, 'max-age' => 50,
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
self::assertSame('', $this->renderPlain($build)); self::assertSame('', $this->renderPlain($build));
@ -172,10 +171,10 @@ final class EntityViewBuilderTest extends KernelTestBase {
], ],
'bin' => 'render', 'bin' => 'render',
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
$expected_html = <<< 'HTML' $expected_html = <<< 'HTML'
<article role="article"> <article>
<h2><a href="/node/2" rel="bookmark"><span>Private node</span></a></h2> <h2><a href="/node/2" rel="bookmark"><span>Private node</span></a></h2>
<div></div> <div></div>
</article> </article>

11
tests/src/Kernel/FieldViewBuilderTest.php

@ -3,17 +3,16 @@
namespace Drupal\Tests\twig_tweak\Kernel; namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\node\Entity\Node; use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType; use Drupal\node\Entity\NodeType;
use Drupal\Tests\user\Traits\UserCreationTrait;
/** /**
* A test for FieldViewBuilder. * A test for FieldViewBuilder.
* *
* @group twig_tweak * @group twig_tweak
*/ */
final class FieldViewBuilderTest extends KernelTestBase { final class FieldViewBuilderTest extends AbstractTestCase {
use UserCreationTrait; use UserCreationTrait;
@ -74,7 +73,7 @@ final class FieldViewBuilderTest extends KernelTestBase {
], ],
'max-age' => 50, 'max-age' => 50,
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
self::assertSame('<span>Public node</span>', $this->renderPlain($build)); self::assertSame('<span>Public node</span>', $this->renderPlain($build));
@ -93,7 +92,7 @@ final class FieldViewBuilderTest extends KernelTestBase {
], ],
'max-age' => 50, 'max-age' => 50,
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
$expected_html = '<span><a href="/node/1" hreflang="en">Public node</a></span>'; $expected_html = '<span><a href="/node/1" hreflang="en">Public node</a></span>';
self::assertSame($expected_html, $this->renderPlain($build)); self::assertSame($expected_html, $this->renderPlain($build));
@ -112,7 +111,7 @@ final class FieldViewBuilderTest extends KernelTestBase {
], ],
'max-age' => 50, 'max-age' => 50,
]; ];
self::assertSame($expected_cache, $build['#cache']); self::assertCache($expected_cache, $build['#cache']);
self::assertSame('', $this->renderPlain($build)); self::assertSame('', $this->renderPlain($build));
// -- Private node without access check. // -- Private node without access check.

174
tests/src/Kernel/ImageViewBuilderTest.php

@ -4,17 +4,18 @@ namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\Entity\File; use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\image\Entity\ImageStyle; use Drupal\image\Entity\ImageStyle;
use Drupal\KernelTests\KernelTestBase;
use Drupal\responsive_image\Entity\ResponsiveImageStyle; use Drupal\responsive_image\Entity\ResponsiveImageStyle;
/** /**
* A test for ImageViewBuilderTest. * A test class for testing the image view builder.
* *
* @group twig_tweak * @group twig_tweak
*/ */
final class ImageViewBuilderTest extends KernelTestBase { final class ImageViewBuilderTest extends AbstractTestCase {
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -30,15 +31,93 @@ final class ImageViewBuilderTest extends KernelTestBase {
'breakpoint', 'breakpoint',
]; ];
/**
* The public image URI.
*
* @var string
*/
protected string $publicImageUri;
/**
* The private image URI.
*
* @var string
*/
protected string $privateImageUri;
/**
* The public image file.
*
* @var \Drupal\file\FileInterface
*/
protected $publicImage;
/**
* The private image file.
*
* @var \Drupal\file\FileInterface
*/
protected $privateImage;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function setUp(): void { public function setUp(): void {
parent::setUp(); parent::setUp();
$this->installEntitySchema('file'); $this->installEntitySchema('file');
$this->installSchema('file', 'file_usage'); $this->installSchema('file', 'file_usage');
ImageStyle::create(['name' => 'large'])->save();
ResponsiveImageStyle::create(['id' => 'wide'])->save(); $file_system = $this->container->get('file_system');
$file_system->prepareDirectory($this->siteDirectory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
$private_directory = $this->siteDirectory . '/private';
$file_system->prepareDirectory($private_directory, FileSystemInterface::CREATE_DIRECTORY);
$this->setSetting('file_private_path', $private_directory);
$image_style = ImageStyle::create([
'name' => 'small',
'label' => 'Small',
]);
// Add a crop effect:
$image_style->addImageEffect([
'id' => 'image_resize',
'data' => [
'width' => 10,
'height' => 10,
],
'weight' => 0,
]);
$image_style->save();
$responsive_image_style = ResponsiveImageStyle::create([
'id' => 'wide',
'label' => 'Wide',
'breakpoint_group' => 'twig_tweak_image_view_builder',
'fallback_image_style' => 'small',
]);
$responsive_image_style->save();
// Create a copy of a test image file in root. Original sizes: 40x20px.
$this->publicImageUri = 'public://image-test-do.jpg';
$file_system->copy('core/tests/fixtures/files/image-test.jpg', $this->publicImageUri, FileSystemInterface::EXISTS_REPLACE);
$this->assertFileExists($this->publicImageUri);
$this->publicImage = File::create([
'uri' => $this->publicImageUri,
'status' => FileInterface::STATUS_PERMANENT,
]);
$this->publicImage->save();
// Create a copy of a test image file in root. Original sizes: 40x20px.
$this->privateImageUri = 'private://image-test-do.png';
$file_system->copy('core/tests/fixtures/files/image-test.png', $this->privateImageUri, FileSystemInterface::EXISTS_REPLACE);
$this->assertFileExists($this->privateImageUri);
$this->privateImage = File::create([
'uri' => $this->privateImageUri,
'status' => FileInterface::STATUS_PERMANENT,
]);
$this->privateImage->save();
} }
/** /**
@ -54,21 +133,20 @@ final class ImageViewBuilderTest extends KernelTestBase {
* Test callback. * Test callback.
*/ */
public function testImageViewBuilder(): void { public function testImageViewBuilder(): void {
$view_builder = $this->container->get('twig_tweak.image_view_builder'); $view_builder = $this->container->get('twig_tweak.image_view_builder');
/** @var \Drupal\file\FileInterface $public_image */ $uri = $this->publicImage->getFileUri();
$public_image = File::create(['uri' => 'public://ocean.jpg']); $image = \Drupal::service('image.factory')->get($uri);
$public_image->save(); $imageOriginalWidth = $image->getWidth();
$imageOriginalHeight = $image->getHeight();
/** @var \Drupal\file\FileInterface $private_image */ self::assertTrue($image->isValid());
$private_image = File::create(['uri' => 'private://sea.jpg']); self::assertEquals(40, $imageOriginalWidth);
$private_image->save(); self::assertEquals(20, $imageOriginalHeight);
// -- Without style. // -- Without style.
$build = $view_builder->build($public_image); $build = $view_builder->build($this->publicImage);
$expected_build = [ $expected_build = [
'#uri' => 'public://ocean.jpg', '#uri' => $this->publicImageUri,
'#attributes' => [], '#attributes' => [],
'#theme' => 'image', '#theme' => 'image',
'#cache' => [ '#cache' => [
@ -76,37 +154,47 @@ final class ImageViewBuilderTest extends KernelTestBase {
'user', 'user',
'user.permissions', 'user.permissions',
], ],
'tags' => ['tag_for_public://ocean.jpg'], 'tags' => [
'file:1',
'tag_for_' . $this->publicImageUri,
],
'max-age' => 70, 'max-age' => 70,
], ],
]; ];
self::assertSame($expected_build, $build); self::assertRenderArray($expected_build, $build);
self::assertSame('<img src="/files/ocean.jpg" alt="" />', $this->renderPlain($build)); self::assertSame('<img src="/files/image-test-do.jpg" alt="" />', $this->renderPlain($build));
// -- With style. // -- With style.
$build = $view_builder->build($public_image, 'large', ['alt' => 'Ocean']); $build = $view_builder->build($this->publicImage, 'small', ['alt' => 'Image Test Do']);
$expected_build = [ $expected_build = [
'#uri' => 'public://ocean.jpg', '#uri' => $this->publicImageUri,
'#attributes' => ['alt' => 'Ocean'], '#attributes' => ['alt' => 'Image Test Do'],
'#width' => $imageOriginalWidth,
'#height' => $imageOriginalHeight,
'#theme' => 'image_style', '#theme' => 'image_style',
'#style_name' => 'large', '#style_name' => 'small',
'#cache' => [ '#cache' => [
'contexts' => [ 'contexts' => [
'user', 'user',
'user.permissions', 'user.permissions',
], ],
'tags' => ['tag_for_public://ocean.jpg'], 'tags' => [
'file:1',
'tag_for_' . $this->publicImageUri,
],
'max-age' => 70, 'max-age' => 70,
], ],
]; ];
self::assertSame($expected_build, $build); self::assertRenderArray($expected_build, $build);
self::assertSame('<img alt="Ocean" src="/files/styles/large/public/ocean.jpg?itok=abc" />', $this->renderPlain($build)); self::assertSame('<img alt="Image Test Do" src="/files/styles/small/public/image-test-do.jpg?itok=abc" width="10" height="10" loading="lazy" />', $this->renderPlain($build));
// -- With responsive style. // -- With responsive style.
$build = $view_builder->build($public_image, 'wide', ['alt' => 'Ocean'], TRUE); $build = $view_builder->build($this->publicImage, 'wide', ['alt' => 'Image Test Do'], TRUE);
$expected_build = [ $expected_build = [
'#uri' => 'public://ocean.jpg', '#uri' => $this->publicImageUri,
'#attributes' => ['alt' => 'Ocean'], '#attributes' => ['alt' => 'Image Test Do'],
'#width' => $imageOriginalWidth,
'#height' => $imageOriginalHeight,
'#type' => 'responsive_image', '#type' => 'responsive_image',
'#responsive_image_style_id' => 'wide', '#responsive_image_style_id' => 'wide',
'#cache' => [ '#cache' => [
@ -114,39 +202,45 @@ final class ImageViewBuilderTest extends KernelTestBase {
'user', 'user',
'user.permissions', 'user.permissions',
], ],
'tags' => ['tag_for_public://ocean.jpg'], 'tags' => [
'file:1',
'tag_for_' . $this->publicImageUri,
],
'max-age' => 70, 'max-age' => 70,
], ],
]; ];
self::assertSame($expected_build, $build); self::assertRenderArray($expected_build, $build);
self::assertSame('<picture><img src="/files/ocean.jpg" alt="Ocean" /></picture>', $this->renderPlain($build)); self::assertSame('<picture><img src="/files/styles/small/public/image-test-do.jpg?itok=abc" width="10" height="10" alt="Image Test Do" loading="lazy" /></picture>', $this->renderPlain($build));
// -- Private image with access check. // -- Private image with access check.
$build = $view_builder->build($private_image); $build = $view_builder->build($this->privateImage);
$expected_build = [ $expected_build = [
'#cache' => [ '#cache' => [
'contexts' => ['user'], 'contexts' => ['user'],
'tags' => ['tag_for_private://sea.jpg'], 'tags' => [
'file:2',
'tag_for_' . $this->privateImageUri,
],
'max-age' => 70, 'max-age' => 70,
], ],
]; ];
self::assertSame($expected_build, $build); self::assertRenderArray($expected_build, $build);
self::assertSame('', $this->renderPlain($build)); self::assertSame('', $this->renderPlain($build));
// -- Private image without access check. // -- Private image without access check.
$build = $view_builder->build($private_image, NULL, [], FALSE, FALSE); $build = $view_builder->build($this->privateImage, NULL, [], FALSE, FALSE);
$expected_build = [ $expected_build = [
'#uri' => 'private://sea.jpg', '#uri' => $this->privateImageUri,
'#attributes' => [], '#attributes' => [],
'#theme' => 'image', '#theme' => 'image',
'#cache' => [ '#cache' => [
'contexts' => [], 'contexts' => [],
'tags' => [], 'tags' => ['file:2'],
'max-age' => Cache::PERMANENT, 'max-age' => Cache::PERMANENT,
], ],
]; ];
self::assertSame($expected_build, $build); self::assertRenderArray($expected_build, $build);
self::assertSame('<img src="/files/sea.jpg" alt="" />', $this->renderPlain($build)); self::assertSame('<img src="/files/image-test-do.png" alt="" />', $this->renderPlain($build));
} }
/** /**
@ -155,7 +249,7 @@ final class ImageViewBuilderTest extends KernelTestBase {
private function renderPlain(array $build): string { private function renderPlain(array $build): string {
$html = $this->container->get('renderer')->renderPlain($build); $html = $this->container->get('renderer')->renderPlain($build);
$html = preg_replace('#src=".+/files/#s', 'src="/files/', $html); $html = preg_replace('#src=".+/files/#s', 'src="/files/', $html);
$html = preg_replace('#\?itok=.+"#', '?itok=abc"', $html); $html = preg_replace('#\?itok=.+?"#', '?itok=abc"', $html);
$html = preg_replace(['#\s{2,}#', '#\n#'], '', $html); $html = preg_replace(['#\s{2,}#', '#\n#'], '', $html);
return rtrim($html); return rtrim($html);
} }

23
tests/src/Kernel/RegionViewBuilderTest.php

@ -5,7 +5,6 @@ namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\block\Entity\Block; use Drupal\block\Entity\Block;
use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\Tests\user\Traits\UserCreationTrait;
/** /**
@ -13,7 +12,7 @@ use Drupal\Tests\user\Traits\UserCreationTrait;
* *
* @group twig_tweak * @group twig_tweak
*/ */
final class RegionViewBuilderTest extends KernelTestBase { final class RegionViewBuilderTest extends AbstractTestCase {
use UserCreationTrait; use UserCreationTrait;
@ -34,12 +33,12 @@ final class RegionViewBuilderTest extends KernelTestBase {
public function setUp(): void { public function setUp(): void {
parent::setUp(); parent::setUp();
$this->installEntitySchema('block'); $this->installEntitySchema('block');
$this->container->get('theme_installer')->install(['stable']); $this->container->get('theme_installer')->install(['stark']);
$values = [ $values = [
'id' => 'public_block', 'id' => 'public_block',
'plugin' => 'system_powered_by_block', 'plugin' => 'system_powered_by_block',
'theme' => 'stable', 'theme' => 'stark',
'region' => 'sidebar_first', 'region' => 'sidebar_first',
]; ];
Block::create($values)->save(); Block::create($values)->save();
@ -47,7 +46,7 @@ final class RegionViewBuilderTest extends KernelTestBase {
$values = [ $values = [
'id' => 'private_block', 'id' => 'private_block',
'plugin' => 'system_powered_by_block', 'plugin' => 'system_powered_by_block',
'theme' => 'stable', 'theme' => 'stark',
'region' => 'sidebar_first', 'region' => 'sidebar_first',
]; ];
Block::create($values)->save(); Block::create($values)->save();
@ -62,7 +61,7 @@ final class RegionViewBuilderTest extends KernelTestBase {
$renderer = $this->container->get('renderer'); $renderer = $this->container->get('renderer');
$build = $view_builder->build('sidebar_first'); $build = $view_builder->build('sidebar_first');
// The build should be empty because 'stable' is not a default theme. // The build should be empty because 'stark' is not a default theme.
$expected_build = [ $expected_build = [
'#cache' => [ '#cache' => [
'contexts' => [], 'contexts' => [],
@ -73,7 +72,7 @@ final class RegionViewBuilderTest extends KernelTestBase {
self::assertSame($expected_build, $build); self::assertSame($expected_build, $build);
// Specify the theme name explicitly. // Specify the theme name explicitly.
$build = $view_builder->build('sidebar_first', 'stable'); $build = $view_builder->build('sidebar_first', 'stark');
$expected_build = [ $expected_build = [
// Only public_block should be rendered. // Only public_block should be rendered.
// @see twig_tweak_test_block_access() // @see twig_tweak_test_block_access()
@ -117,7 +116,8 @@ final class RegionViewBuilderTest extends KernelTestBase {
'max-age' => 123, 'max-age' => 123,
], ],
]; ];
self::assertSame($expected_build, $build);
self::assertRenderArray($expected_build, $build);
$expected_html = <<< 'HTML' $expected_html = <<< 'HTML'
<div> <div>
@ -129,15 +129,16 @@ final class RegionViewBuilderTest extends KernelTestBase {
$actual_html = $renderer->renderPlain($build); $actual_html = $renderer->renderPlain($build);
self::assertSame(self::normalizeHtml($expected_html), self::normalizeHtml($actual_html)); self::assertSame(self::normalizeHtml($expected_html), self::normalizeHtml($actual_html));
// Set 'stable' as default site theme and check if the view builder without // Set 'stark' as default site theme and check if the view builder without
// 'theme' argument returns the same result. // 'theme' argument returns the same result.
$this->container->get('config.factory') $this->container->get('config.factory')
->getEditable('system.theme') ->getEditable('system.theme')
->set('default', 'stable') ->set('default', 'stark')
->save(); ->save();
$build = $view_builder->build('sidebar_first'); $build = $view_builder->build('sidebar_first');
self::assertSame($expected_build, $build); self::assertRenderArray($expected_build, $build);
Html::resetSeenIds(); Html::resetSeenIds();
$actual_html = $renderer->renderPlain($expected_build); $actual_html = $renderer->renderPlain($expected_build);
self::assertSame(self::normalizeHtml($expected_html), self::normalizeHtml($actual_html)); self::assertSame(self::normalizeHtml($expected_html), self::normalizeHtml($actual_html));

2
tests/src/Kernel/UrlExtractorTest.php

@ -15,7 +15,7 @@ final class UrlExtractorTest extends AbstractExtractorTestCase {
public function testUrlExtractor(): void { public function testUrlExtractor(): void {
$extractor = $this->container->get('twig_tweak.url_extractor'); $extractor = $this->container->get('twig_tweak.url_extractor');
$base_url = file_create_url(''); $base_url = \Drupal::service('file_url_generator')->generateAbsoluteString('');
$request = \Drupal::request(); $request = \Drupal::request();
$absolute_url = "{$request->getScheme()}://{$request->getHost()}/foo/bar.txt"; $absolute_url = "{$request->getScheme()}://{$request->getHost()}/foo/bar.txt";

10
tests/twig_tweak_test/config/install/block.block.classy_powered_by_drupal.yml → tests/twig_tweak_test/config/install/block.block.claro_powered_by_drupal.yml

@ -4,11 +4,11 @@ dependencies:
module: module:
- system - system
theme: theme:
- classy - claro
id: classy_powered_by_drupal id: claro_powered_by_drupal
theme: classy theme: claro
region: sidebar_first region: highlighted
weight: 20 weight: 0
provider: null provider: null
plugin: system_powered_by_block plugin: system_powered_by_block
settings: settings:

19
tests/twig_tweak_test/config/install/block.block.classy_content.yml

@ -1,19 +0,0 @@
langcode: en
status: true
dependencies:
module:
- system
theme:
- classy
id: classy_content
theme: classy
region: content
weight: 0
provider: null
plugin: system_main_block
settings:
id: system_main_block
label: 'Main page content'
provider: system
label_display: '0'
visibility: { }

17
tests/twig_tweak_test/config/install/block.block.classy_page_title.yml

@ -1,17 +0,0 @@
langcode: en
status: true
dependencies:
theme:
- classy
id: classy_page_title
theme: classy
region: sidebar_first
weight: 0
provider: null
plugin: page_title_block
settings:
id: page_title_block
label: 'Page title'
provider: core
label_display: '0'
visibility: { }

19
tests/twig_tweak_test/config/install/block.block.classy_status_messages.yml

@ -1,19 +0,0 @@
langcode: en
status: true
dependencies:
module:
- system
theme:
- classy
id: classy_status_messages
theme: classy
region: sidebar_first
weight: 10
provider: null
plugin: system_messages_block
settings:
id: system_messages_block
label: 'Status messages'
provider: system
label_display: '0'
visibility: { }

17
tests/twig_tweak_test/templates/twig-tweak-test.html.twig

@ -30,7 +30,7 @@
<div class="tt-view-result">{{ drupal_view_result('twig_tweak_test', 'page_1')|length }}</div> <div class="tt-view-result">{{ drupal_view_result('twig_tweak_test', 'page_1')|length }}</div>
<div class="tt-block">{{ drupal_block('system_branding_block', {use_site_name: false}, false) }}</div> <div class="tt-block">{{ drupal_block('system_branding_block', {use_site_name: false}, false) }}</div>
<div class="tt-block-with-wrapper">{{ drupal_block('system_branding_block', {label: 'Branding'}) }}</div> <div class="tt-block-with-wrapper">{{ drupal_block('system_branding_block', {label: 'Branding'}) }}</div>
<div class="tt-region">{{ drupal_region('sidebar_first') }}</div> <div class="tt-region">{{ drupal_region('highlighted') }}</div>
<div class="tt-entity-default">{{ drupal_entity('node', 1) }}</div> <div class="tt-entity-default">{{ drupal_entity('node', 1) }}</div>
<div class="tt-entity-teaser">{{ drupal_entity('node', 1, 'teaser') }}</div> <div class="tt-entity-teaser">{{ drupal_entity('node', 1, 'teaser') }}</div>
<div class="tt-entity-uuid">{{ drupal_entity('node', 'ad1b902a-344f-41d1-8c61-a69f0366dbfa') }}</div> <div class="tt-entity-uuid">{{ drupal_entity('node', 'ad1b902a-344f-41d1-8c61-a69f0366dbfa') }}</div>
@ -54,6 +54,7 @@
<div class="tt-url"> <div class="tt-url">
<div data-case="default">{{ drupal_url('node/1', {absolute: true}) }}</div> <div data-case="default">{{ drupal_url('node/1', {absolute: true}) }}</div>
<div data-case="with-langcode">{{ drupal_url('node/1', {absolute: true, langcode: 'ru'}) }}</div> <div data-case="with-langcode">{{ drupal_url('node/1', {absolute: true, langcode: 'ru'}) }}</div>
<div data-case="external">{{ drupal_url('https://example.com/node?foo=bar', {query: {page: 1}, fragment: 'here'}) }}</div>
</div> </div>
<div class="tt-link">{{ drupal_link('Edit', 'node/1/edit', {absolute: true}) }}</div> <div class="tt-link">{{ drupal_link('Edit', 'node/1/edit', {absolute: true}) }}</div>
<div class="tt-link-html">{% set link_text %}<b>Edit</b>{% endset %}{{ drupal_link(link_text, 'node/1/edit', {absolute: true}) }}</div> <div class="tt-link-html">{% set link_text %}<b>Edit</b>{% endset %}{{ drupal_link(link_text, 'node/1/edit', {absolute: true}) }}</div>
@ -66,10 +67,16 @@
<div class="tt-image-style">{{ 'public://images/ocean.jpg'|image_style('thumbnail') }}</div> <div class="tt-image-style">{{ 'public://images/ocean.jpg'|image_style('thumbnail') }}</div>
<div class="tt-transliterate">{{ 'Привет!'|transliterate('ru') }}</div> <div class="tt-transliterate">{{ 'Привет!'|transliterate('ru') }}</div>
<div class="tt-check-markup">{{ '<b>bold</b> <strong>strong</strong>'|check_markup('twig_tweak_test') }}</div> <div class="tt-check-markup">{{ '<b>bold</b> <strong>strong</strong>'|check_markup('twig_tweak_test') }}</div>
<div class="tt-format-size">{{ 12345|format_size() }}</div> <div class="tt-format-size">{{ 12345|format_size }}</div>
<div class="tt-truncate">{{ 'Hello world!'|truncate(10, true, true) }}</div> <div class="tt-truncate">{{ 'Hello world!'|truncate(10, true, true) }}</div>
<div class="tt-with">{{ {'#markup':'Example'}|with('#prefix', '<b>')|with('#suffix', '</b>') }}</div> <div class="tt-with">{{ {'#markup':'Example'}|with('#prefix', '<b>')|with('#suffix', '</b>') }}</div>
<div class="tt-with-nested">{{ {alpha: {beta: {gamma: 123}}}|with(['alpha', 'beta', 'gamma'], 456)|json_encode|replace({'"':''}) }}</div> <div class="tt-with-nested">{{ {alpha: {beta: {gamma: 123}}}|with(['alpha', 'beta', 'gamma'], 456)|json_encode|replace({'"':''}) }}</div>
<div class="tt-data-uri-svg">
<img src="{{ '<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="50" fill="lime"/></svg>'|data_uri('image/svg+xml') }}" alt="{{ 'Rectangle'|t }}" style="height: 50px;"/>
</div>
<div class="tt-data-uri-iframe">
<iframe src="{{ '<h1>Hello world!</h1>'|data_uri('text/html', {charset: 'UTF-8'}) }}"></iframe>
</div>
<div class="tt-children"> <div class="tt-children">
{%- {%-
set build = { set build = {
@ -99,6 +106,12 @@
<div class="tt-file-url-from-image-field">{{ node.field_image|file_url }}</div> <div class="tt-file-url-from-image-field">{{ node.field_image|file_url }}</div>
<div class="tt-file-url-from-image-field-delta">{{ node.field_image[0]|file_url }}</div> <div class="tt-file-url-from-image-field-delta">{{ node.field_image[0]|file_url }}</div>
<div class="tt-file-url-from-media-field">{{ node.field_media|file_url }}</div> <div class="tt-file-url-from-media-field">{{ node.field_media|file_url }}</div>
<div class="tt-entity-url">{{ node|entity_url(options={fragment: 'test'}) }}</div>
<div class="tt-entity-url-absolute">{{ node|entity_url(options={absolute: true}) }}</div>
<div class="tt-entity-url-edit-form">{{ node|entity_url('edit-form') }}</div>
<div class="tt-entity-link">{{ node|entity_link }}</div>
<div class="tt-entity-link-absolute">{{ node|entity_link('Example', options={absolute: true}) }}</div>
<div class="tt-entity-link-edit-form">{{ node|entity_link('Edit', 'edit-form') }}</div>
<div class="tt-translation">{{ (node|translation).title.value }}</div> <div class="tt-translation">{{ (node|translation).title.value }}</div>
<div class="tt-functions_alter">{{ foo('bar') }}</div> <div class="tt-functions_alter">{{ foo('bar') }}</div>
<div class="tt-filters_alter">{{ 'foo'|bar }}</div> <div class="tt-filters_alter">{{ 'foo'|bar }}</div>

2
tests/twig_tweak_test/twig_tweak_test.info.yml

@ -2,7 +2,7 @@ name: Twig tweak test
type: module type: module
description: Support module for Twig tweak testing. description: Support module for Twig tweak testing.
package: Testing package: Testing
core_version_requirement: ^9 core_version_requirement: ^9 || ^10
dependencies: dependencies:
- drupal:system (>= 9.0) - drupal:system (>= 9.0)
- drupal:block - drupal:block

35
twig_tweak.api.php

@ -7,6 +7,7 @@
use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Unicode;
use Drupal\node\NodeInterface; use Drupal\node\NodeInterface;
use Twig\TwigFilter;
use Twig\TwigFunction; use Twig\TwigFunction;
use Twig\TwigTest; use Twig\TwigTest;
@ -22,20 +23,16 @@ use Twig\TwigTest;
* Twig functions to alter. * Twig functions to alter.
*/ */
function hook_twig_tweak_functions_alter(array &$functions): void { function hook_twig_tweak_functions_alter(array &$functions): void {
// @phpcs:disable
// A simple way to implement lazy loaded global variables. // A simple way to implement lazy loaded global variables.
$functions[] = new TwigFunction('var', function (string $name) { $callback = static fn (string $name): ?string =>
$value = NULL; match ($name) {
switch ($name) { 'foo' => 'Foo',
case 'foo': 'bar' => 'Bar',
$value = 'Foo'; default => NULL,
break; };
$functions[] = new TwigFunction('var', $callback);
case 'bar': // @phpcs:enable
$value = 'Bar';
break;
}
return $value;
});
} }
/** /**
@ -45,9 +42,9 @@ function hook_twig_tweak_functions_alter(array &$functions): void {
* Twig filters to alter. * Twig filters to alter.
*/ */
function hook_twig_tweak_filters_alter(array &$filters): void { function hook_twig_tweak_filters_alter(array &$filters): void {
$filters[] = new TwigFunction('str_pad', 'str_pad'); $filters[] = new TwigFilter('str_pad', 'str_pad');
$filters[] = new TwigFunction('ucfirst', [Unicode::class, 'ucfirst']); $filters[] = new TwigFilter('ucfirst', [Unicode::class, 'ucfirst']);
$filters[] = new TwigFunction('lcfirst', [Unicode::class, 'lcfirst']); $filters[] = new TwigFilter('lcfirst', [Unicode::class, 'lcfirst']);
} }
/** /**
@ -57,9 +54,9 @@ function hook_twig_tweak_filters_alter(array &$filters): void {
* Twig tests to alter. * Twig tests to alter.
*/ */
function hook_twig_tweak_tests_alter(array &$tests): void { function hook_twig_tweak_tests_alter(array &$tests): void {
$tests[] = new TwigTest('outdated', function (NodeInterface $node): bool { $callback = static fn (NodeInterface $node): bool =>
return \Drupal::time()->getRequestTime() - $node->getCreatedTime() > 3600 * 24 * 365; \Drupal::time()->getRequestTime() - $node->getCreatedTime() > 3600 * 24 * 365;
}); $tests[] = new TwigTest('outdated', $callback);
} }
/** /**

2
twig_tweak.info.yml

@ -1,6 +1,6 @@
name: Twig Tweak name: Twig Tweak
type: module type: module
description: Provides some extra Twig functions and filters. description: Provides some extra Twig functions and filters.
core_version_requirement: ^9 core_version_requirement: ^9 || ^10
dependencies: dependencies:
- drupal:system (>=9.0) - drupal:system (>=9.0)

5
twig_tweak.services.yml

@ -15,7 +15,7 @@ services:
twig_tweak.entity_view_builder: twig_tweak.entity_view_builder:
class: Drupal\twig_tweak\View\EntityViewBuilder class: Drupal\twig_tweak\View\EntityViewBuilder
arguments: ['@entity_type.manager'] arguments: ['@entity_type.manager', '@entity.repository']
twig_tweak.entity_form_view_builder: twig_tweak.entity_form_view_builder:
class: Drupal\twig_tweak\View\EntityFormViewBuilder class: Drupal\twig_tweak\View\EntityFormViewBuilder
@ -31,10 +31,11 @@ services:
twig_tweak.image_view_builder: twig_tweak.image_view_builder:
class: Drupal\twig_tweak\View\ImageViewBuilder class: Drupal\twig_tweak\View\ImageViewBuilder
arguments: ['@image.factory']
twig_tweak.url_extractor: twig_tweak.url_extractor:
class: Drupal\twig_tweak\UrlExtractor class: Drupal\twig_tweak\UrlExtractor
arguments: ['@entity_type.manager'] arguments: ['@entity_type.manager', '@file_url_generator']
twig_tweak.uri_extractor: twig_tweak.uri_extractor:
class: Drupal\twig_tweak\UriExtractor class: Drupal\twig_tweak\UriExtractor

Loading…
Cancel
Save