Verified Commit d985697c authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3301373 by hawkeye.twolf, lauriii, alexpott, mherchel: Create twig |add_suggestion filter

parent f7d8fe6f
Loading
Loading
Loading
Loading
+59 −0
Changes for core/lib/Drupal/Core/Template/TwigExtension.php: 59 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -143,6 +143,8 @@ public function getFilters() {
      // This filter will render a renderable array to use the string results.
      new TwigFilter('render', [$this, 'renderVar']),
      new TwigFilter('format_date', [$this->dateFormatter, 'format']),
      // Add new theme hook suggestions directly from a Twig template.
      new TwigFilter('add_suggestion', [$this, 'suggestThemeHook']),
    ];
  }

@@ -653,4 +655,61 @@ public function withoutFilter($element) {
    return $filtered_element;
  }

  /**
   * Adds a theme suggestion to the element.
   *
   * @param array|null $element
   *   A theme element render array.
   * @param string|\Stringable $suggestion
   *   The theme suggestion part to append to the existing theme hook(s).
   *
   * @return array|null
   *   The element with the full theme suggestion added as the highest priority.
   */
  public function suggestThemeHook(?array $element, string|\Stringable $suggestion): ?array {
    // Make sure we have a valid theme element render array.
    if (empty($element['#theme'])) {
      // Throw assertion for render arrays that contain more than just metadata
      // (e.g., don't assert on empty field content).
      assert(array_diff_key($element ?? [], [
        '#cache' => TRUE,
        '#weight' => TRUE,
        '#attached' => TRUE,
      ]) === [], 'Invalid target for the "|add_suggestion" Twig filter; element does not have a "#theme" key.');
      return $element;
    }

    // Replace dashes with underscores to support suggestions that match the
    // target template name rather than the underlying theme hook.
    $suggestion = str_replace('-', '_', $suggestion);

    // Transform the theme hook to a format that supports multiple suggestions.
    if (!is_iterable($element['#theme'])) {
      $element['#theme'] = [$element['#theme']];
    }

    // Add _new_ suggestions for each existing theme hook. Simply modifying the
    // existing items (appending to each theme hook instead of adding new ones)
    // would cause the original hooks to be unavailable as fallbacks.
    //
    // Start with the lowest priority theme hook.
    foreach (array_reverse($element['#theme']) as $theme_hook) {
      // Add new suggestions to the front (highest priority).
      array_unshift($element['#theme'], $theme_hook . '__' . $suggestion);
    }

    // Reset the "#printed" flag to make sure the content gets rendered with the
    // new suggestion in place.
    unset($element['#printed']);

    // Add a cache key to prevent using render cache from before the suggestion
    // was added. If there are no cache keys already set, don't add one, as that
    // would enable caching on this element where there wasn't any before.
    if (isset($element['#cache']['keys'])) {
      $element['#cache']['keys'][] = $suggestion;
    }

    return $element;
  }

}
+118 −0
Changes for core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php: 118 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -2,6 +2,8 @@

namespace Drupal\Tests\Core\Template;

// cspell:ignore mila

use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\GeneratedLink;
use Drupal\Core\Render\RenderableInterface;
@@ -424,6 +426,122 @@ public function testLinkWithOverriddenAttributes() {
    $this->assertEquals(['foo', 'bar'], $build['#url']->getOption('attributes')['class']);
  }

  /**
   * Tests Twig 'add_suggestion' filter.
   *
   * @covers ::suggestThemeHook
   * @dataProvider providerTestTwigAddSuggestionFilter
   */
  public function testTwigAddSuggestionFilter($original_render_array, $suggestion, $expected_render_array) {
    $processed_render_array = $this->systemUnderTest->suggestThemeHook($original_render_array, $suggestion);
    $this->assertEquals($expected_render_array, $processed_render_array);
  }

  /**
   * A data provider for ::testTwigAddSuggestionFilter().
   *
   * @return \Iterator
   */
  public function providerTestTwigAddSuggestionFilter(): \Iterator {
    yield 'suggestion should be added' => [
      [
        '#theme' => 'kitten',
        '#name' => 'Mila',
      ],
      'cute',
      [
        '#theme' => [
          'kitten__cute',
          'kitten',
        ],
        '#name' => 'Mila',
      ],
    ];

    yield 'suggestion should extend existing suggestions' => [
      [
        '#theme' => 'kitten__stripy',
        '#name' => 'Mila',
      ],
      'cute',
      [
        '#theme' => [
          'kitten__stripy__cute',
          'kitten__stripy',
        ],
        '#name' => 'Mila',
      ],
    ];

    yield 'suggestion should have highest priority' => [
      [
        '#theme' => [
          'kitten__stripy',
          'kitten',
        ],
        '#name' => 'Mila',
      ],
      'cute',
      [
        '#theme' => [
          'kitten__stripy__cute',
          'kitten__cute',
          'kitten__stripy',
          'kitten',
        ],
        '#name' => 'Mila',
      ],
    ];

    yield '#printed should be removed after suggestion was added' => [
      [
        '#theme' => 'kitten',
        '#name' => 'Mila',
        '#printed' => TRUE,
      ],
      'cute',
      [
        '#theme' => [
          'kitten__cute',
          'kitten',
        ],
        '#name' => 'Mila',
      ],
    ];

    yield 'cache key should be added' => [
      [
        '#theme' => 'kitten',
        '#name' => 'Mila',
        '#cache' => [
          'keys' => [
            'kitten',
          ],
        ],
      ],
      'cute',
      [
        '#theme' => [
          'kitten__cute',
          'kitten',
        ],
        '#name' => 'Mila',
        '#cache' => [
          'keys' => [
            'kitten',
            'cute',
          ],
        ],
      ],
    ];

    yield 'null/missing content should be ignored' => [
      NULL,
      'cute',
      NULL,
    ];
  }

}

class TwigExtensionTestString {