diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cded392
--- /dev/null
+++ b/README.md
@@ -0,0 +1,294 @@
+## SUMMARY
+
+Twig Tweak module provides a Twig extension with some useful functions and
+filters.
+
+## Usage
+
+### Drupal View
+```twig
+{{ drupal_view('who_s_new', 'block_1') }}
+```
+
+### Drupal View Result
+```twig
+{{ drupal_view('who_s_new', 'block_1') }}
+```
+
+### Drupal Block
+In order to list all registered plugin IDs fetch them with block plugin manager.
+With Drush it can be done like follows:
+```
+drush ev "print_r(array_keys(\Drupal::service('plugin.manager.block')->getDefinitions()));"
+```
+
+```twig
+{# Print block using default configuration. #}
+{{ drupal_block('system_branding_block') }}
+
+{# Print block using custom configuration. #}
+{{ drupal_block('system_branding_block', {label: 'Branding', use_site_name: false})
+
+{# Bypass block.html.twig theming. #}
+{{ drupal_block('system_branding_block', wrapper=false) }}
+```
+
+@see https://www.drupal.org/node/2964457#block-plugin
+
+### Drupal Region
+
+```twig
+{# Print 'Sidebar First' region of the default site theme. #}
+{{ drupal_region('sidebar_first') }}
+
+{# Print 'Sidebar First' region of Bartik theme. #}
+{{ drupal_region('sidebar_first', 'bartik') }}
+```
+
+### Drupal Entity
+```twig
+{# Print a content block which ID is 1. #}
+{{ drupal_entity('block_content', 1) }}
+
+{# Print a node's teaser. #}
+{{ drupal_entity('node', 123, 'teaser') }}
+
+{# Print Branding block which was previously disabled on #}
+{# admin/structure/block page. #}
+{{ drupal_entity('block', 'bartik_branding', check_access=false) }}
+```
+
+### Drupal Entity Form
+```twig
+{# Print edit form for node 1. #}
+{{ drupal_entity_form('node', 1) }}
+
+{# Print add form for Article content type. #}
+{{ drupal_entity_form('node', values={type: 'article'}) }}
+
+{# Print user register form. #}
+{{ drupal_entity_form('user', NULL, 'register', check_access=false) }}
+```
+
+### Drupal Field
+```twig
+{{ drupal_field('field_image', 'node', 1) }}
+{{ drupal_field('field_image', 'node', 1, 'teaser') }}
+{{ drupal_field('field_image', 'node', 1, {type: 'image_url', settings: {image_style: 'large'}}) }}
+```
+
+### Drupal Menu
+```twig
+{{ drupal_menu('main') }}
+```
+
+### Drupal Form
+```twig
+{{ drupal_form('Drupal\\search\\Form\\SearchBlockForm') }}
+```
+
+### Drupal Image
+
+```twig
+{# Render image specified by file ID. #}
+{{ drupal_image(123) }}
+
+{# Render image specified by file UUID. #}
+{{ drupal_image('9bb27144-e6b2-4847-bd24-adcc59613ec0') }}
+
+{# Render image specified by file URI. #}
+{{ drupal_image('public://ocean.jpg') }}
+
+{# 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}) }}
+
+{# Render responsive image. #}
+{{ drupal_image('public://ocean.jpg', 'wide', responsive=true) }}
+```
+
+
+### Drupal Token
+```twig
+{{ drupal_token('site:name') }}
+```
+
+### Drupal Config
+```twig
+{{ drupal_config('system.site', 'name') }}
+```
+
+### Drupal Dump
+```twig
+{# Basic usage. #}
+{{ drupal_dump(var) }}
+
+{# Same as above but shorter. #}
+{{ dd(var) }}
+
+{# Dump all available variables for the current template. #}
+{{ dd() }}
+```
+
+### Drupal Title
+```twig
+{{ drupal_title() }}
+```
+
+### Drupal URL
+```twig
+{# Basic usage. #}
+{{ drupal_url('node/1') }}
+
+{# Complex URL. #}
+{{ drupal_url('node/1', {query: {foo: 'bar'}, fragment: 'example', absolute: true}) }}
+```
+
+### Drupal Link
+
+```twig
+{# It supports the same options as drupal_url(), plus attributes. #}
+{{ drupal_link('View'|t, 'node/1', {attributes: {target: '_blank'}}) }}
+
+{# This link will only be shown for privileged users. #}
+{{ drupal_link('Example'|t, '/admin', check_access=true) }}
+```
+
+### Drupal Messages
+```twig
+{{ drupal_messages() }}
+```
+
+### Drupal Breadcrumb
+```twig
+{{ drupal_breadcrumb() }}
+```
+
+### Drupal Breakpoint
+```twig
+{{ drupal_breakpoint() }}
+```
+
+### Contextual Links
+```twig
+{# Basic usage. #}
+
+ {{ contextual_links('entity.view.edit_form:view=frontpage&display_id=feed_1') }}
+ {{ drupal_view('frontpage') }}
+
+{# Multiple links. #}
+
+ {{ contextual_links('node:node=123|block_content:block_content=123') }}
+ {{ content }}
+
+```
+
+### Token Replace
+```twig
+{# Basic usage. #}
+{{ '[site:name]
[site:slogan]
'|token_replace }}
+
+{# This is more suited to large markup. #}
+{% apply token_replace %}
+ [site:name]
+ [site:slogan]
+{% endapply %}
+```
+
+### Preg Replace
+```twig
+{{ 'Drupal - community plumbing!'|preg_replace('/(Drupal)/', '$1') }}
+```
+For simple string interpolation consider using built-in `replace` or `format`
+Twig filters.
+
+### Image Style
+```twig
+{{ 'public://images/ocean.jpg'|image_style('thumbnail') }}
+```
+
+### Transliterate
+```twig
+{{ 'Привет!'|transliterate }}
+```
+new TwigFilter('transliterate', [self::class, 'transliterateFilter']),
+
+### Check Markup
+```twig
+{{ 'bold strong'|check_markup('restricted_html') }}
+```
+
+### Truncate
+```twig
+{{ 'Some long text'|truncate(10, true) }}
+```
+
+### View
+```twig
+{# Do not put this into node.html.twig template to avoid recursion. #}
+{{ node|view }}
+{{ node|view('teaser') }}
+
+{{ node.field_image|view }}
+{{ node.field_image[0]|view }}
+{{ node.field_image|view('teaser') }}
+{{ node.field_image|view({settings: {image_style: 'thumbnail'}}) }}
+```
+
+### With
+```twig
+{# Set top level value. #}
+{{ content.field_image|with('#title', 'Photo'|t) }}
+
+{# Set nested value. #}
+{{ content|with(['field_image', '#title'], 'Photo'|t) }}
+```
+
+### Children
+```twig
+
+ {% for tag in content.field_tags|children %}
+ - {{ tag }}
+ {% endfor %}
+
+```
+
+### File URL
+For string arguments it works similar to core `file_url()` Twig function.
+```twig
+{{ 'public://sea.jpg'|file_url }}
+```
+
+When field item list passed the URL will be extracted from the first item. In
+order to get URL of specific item specify its delta explicitly using array
+notation.
+```twig
+{{ node.field_image|file_url }}
+{{ node.field_image[0]|file_url }}
+```
+
+Media fields are fully supported including OEmbed resources.
+```twig
+{{ node.field_media|file_url }}
+```
+
+### PHP
+PHP filter is disabled by default. You can enable it in settings.php file as
+follows:
+```twug
+$settings['twig_tweak_enable_php_filter'] = TRUE;
+```
+
+```twig
+{{ 'return date('Y');'|php }}
+```
+
+Using PHP filter is discouraged as it may cause security implications. In fact
+it is very rarely needed. The above code can be replaced with following.
+```twig
+{{ 'now'|date('Y') }}
+```
+
+## LINKS
+Project page: https://www.drupal.org/project/twig_tweak
+Twig home page: http://twig.sensiolabs.org
+Drupal 8 Twig documentation: https://www.drupal.org/docs/8/theming/twig
diff --git a/README.txt b/README.txt
deleted file mode 100644
index 7c57093..0000000
--- a/README.txt
+++ /dev/null
@@ -1,9 +0,0 @@
--- SUMMARY --
-
-Twig Tweak module provides a Twig extension with some useful functions and
-filters. See src/TwigExtension.php for details.
-
--- LINKS --
-Project page: https://www.drupal.org/project/twig_tweak
-Twig home page: http://twig.sensiolabs.org
-Drupal 8 Twig documentation: https://www.drupal.org/docs/8/theming/twig
diff --git a/composer.json b/composer.json
index 63882ab..29a3a6e 100644
--- a/composer.json
+++ b/composer.json
@@ -1,18 +1,19 @@
{
- "name": "drupal/twig_tweak",
- "type": "drupal-module",
- "description": "A Twig extension with some useful functions and filters for Drupal development.",
- "keywords": ["Drupal", "Twig"],
- "license": "GPL-2.0+",
- "homepage": "https://www.drupal.org/project/twig_tweak",
- "support": {
- "issues": "https://www.drupal.org/project/issues/twig_tweak",
- "source": "https://git.drupalcode.org/project/twig_tweak"
- },
- "require": {
- "drupal/core": "^8.7 || ^9.0"
- },
- "suggest": {
- "symfony/var-dumper": "Better dump() function for debugging Twig variables"
- }
+ "name": "drupal/twig_tweak",
+ "type": "drupal-module",
+ "description": "A Twig extension with some useful functions and filters for Drupal development.",
+ "keywords": ["Drupal", "Twig"],
+ "license": "GPL-2.0+",
+ "homepage": "https://www.drupal.org/project/twig_tweak",
+ "support": {
+ "issues": "https://www.drupal.org/project/issues/twig_tweak",
+ "source": "https://git.drupalcode.org/project/twig_tweak"
+ },
+ "require": {
+ "php": ">=7.3",
+ "drupal/core": "^9.0"
+ },
+ "suggest": {
+ "symfony/var-dumper": "Better dump() function for debugging Twig variables"
+ }
}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..e172d4a
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,16 @@
+
+
+ PHP CodeSniffer configuration for Twig Tweak module.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TwigExtension.php b/src/TwigExtension.php
deleted file mode 100644
index 5fedc92..0000000
--- a/src/TwigExtension.php
+++ /dev/null
@@ -1,1201 +0,0 @@
- TRUE];
- $all_options = ['needs_environment' => TRUE, 'needs_context' => TRUE];
- return [
- new \Twig_SimpleFunction('drupal_view', 'views_embed_view'),
- new \Twig_SimpleFunction('drupal_view_result', 'views_get_view_result'),
- new \Twig_SimpleFunction('drupal_block', [$this, 'drupalBlock']),
- new \Twig_SimpleFunction('drupal_region', [$this, 'drupalRegion']),
- new \Twig_SimpleFunction('drupal_entity', [$this, 'drupalEntity']),
- new \Twig_SimpleFunction('drupal_entity_form', [$this, 'drupalEntityForm']),
- new \Twig_SimpleFunction('drupal_field', [$this, 'drupalField']),
- new \Twig_SimpleFunction('drupal_menu', [$this, 'drupalMenu']),
- new \Twig_SimpleFunction('drupal_form', [$this, 'drupalForm']),
- new \Twig_SimpleFunction('drupal_image', [$this, 'drupalImage']),
- new \Twig_SimpleFunction('drupal_token', [$this, 'drupalToken']),
- new \Twig_SimpleFunction('drupal_config', [$this, 'drupalConfig']),
- new \Twig_SimpleFunction('drupal_dump', [$this, 'drupalDump'], $context_options),
- new \Twig_SimpleFunction('dd', [$this, 'drupalDump'], $context_options),
- new \Twig_SimpleFunction('drupal_title', [$this, 'drupalTitle']),
- new \Twig_SimpleFunction('drupal_url', [$this, 'drupalUrl']),
- new \Twig_SimpleFunction('drupal_link', [$this, 'drupalLink']),
- new \Twig_SimpleFunction('drupal_messages', [$this, 'drupalMessages']),
- new \Twig_SimpleFunction('drupal_breadcrumb', [$this, 'drupalBreadcrumb']),
- new \Twig_SimpleFunction('drupal_breakpoint', [$this, 'drupalBreakpoint'], $all_options),
- new \Twig_SimpleFunction('contextual_links', [$this, 'contextualLinks']),
- ];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getFilters() {
- $filters = [
- new \Twig_SimpleFilter('token_replace', [$this, 'tokenReplaceFilter']),
- new \Twig_SimpleFilter('preg_replace', [$this, 'pregReplaceFilter']),
- new \Twig_SimpleFilter('image_style', [$this, 'imageStyle']),
- new \Twig_SimpleFilter('transliterate', [$this, 'transliterate']),
- new \Twig_SimpleFilter('check_markup', [$this, 'checkMarkup']),
- new \Twig_SimpleFilter('truncate', [$this, 'truncate']),
- new \Twig_SimpleFilter('view', [$this, 'view']),
- new \Twig_SimpleFilter('with', [$this, 'with']),
- new \Twig_SimpleFilter('children', [$this, 'children']),
- new \Twig_SimpleFilter('file_url', [$this, 'fileUrl']),
- ];
- // PHP filter should be enabled in settings.php file.
- if (Settings::get('twig_tweak_enable_php_filter')) {
- $filters[] = new \Twig_SimpleFilter('php', [$this, 'phpFilter']);
- }
- return $filters;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getName() {
- return 'twig_tweak';
- }
-
- /**
- * Builds the render array for a block.
- *
- * In order to list all registered plugin IDs fetch them with block plugin
- * manager. With Drush it can be done like follows:
- * @code
- * drush ev "print_r(array_keys(\Drupal::service('plugin.manager.block')->getDefinitions()));"
- * @endcode
- *
- * Examples:
- * @code
- * {# Print block using default configuration. #}
- * {{ drupal_block('system_branding_block') }}
- *
- * {# Print block using custom configuration. #}
- * {{ drupal_block('system_branding_block', {label: 'Branding', use_site_name: false})
- *
- * {# Bypass block.html.twig theming. #}
- * {{ drupal_block('system_branding_block', wrapper=false) }}
- * @endcode
- *
- * @see https://www.drupal.org/node/2964457#block-plugin
- *
- * @param mixed $id
- * The string of block plugin to render.
- * @param array $configuration
- * (optional) Pass on any configuration to the plugin block.
- * @param bool $wrapper
- * (optional) Whether or not use block template for rendering.
- *
- * @return null|array
- * A render array for the block or NULL if the block cannot be rendered.
- */
- public function drupalBlock($id, array $configuration = [], $wrapper = TRUE) {
-
- $configuration += ['label_display' => BlockPluginInterface::BLOCK_LABEL_VISIBLE];
-
- /** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */
- $block_plugin = \Drupal::service('plugin.manager.block')
- ->createInstance($id, $configuration);
-
- // Inject runtime contexts.
- if ($block_plugin instanceof ContextAwarePluginInterface) {
- $contexts = \Drupal::service('context.repository')->getRuntimeContexts($block_plugin->getContextMapping());
- \Drupal::service('context.handler')->applyContextMapping($block_plugin, $contexts);
- }
-
- $build = [];
- $access = $block_plugin->access(\Drupal::currentUser(), TRUE);
- if ($access->isAllowed()) {
- // Title block needs special treatment.
- if ($block_plugin instanceof TitleBlockPluginInterface) {
- $request = \Drupal::request();
- $route_match = \Drupal::routeMatch();
- $title = \Drupal::service('title_resolver')->getTitle($request, $route_match->getRouteObject());
- $block_plugin->setTitle($title);
- }
-
- $build['content'] = $block_plugin->build();
-
- if ($block_plugin instanceof TitleBlockPluginInterface) {
- $build['content']['#cache']['contexts'][] = 'url';
- }
-
- if ($wrapper && !Element::isEmpty($build['content'])) {
- $build += [
- '#theme' => 'block',
- '#attributes' => [],
- '#contextual_links' => [],
- '#configuration' => $block_plugin->getConfiguration(),
- '#plugin_id' => $block_plugin->getPluginId(),
- '#base_plugin_id' => $block_plugin->getBaseId(),
- '#derivative_plugin_id' => $block_plugin->getDerivativeId(),
- ];
- }
- }
-
- CacheableMetadata::createFromRenderArray($build)
- ->merge(CacheableMetadata::createFromObject($access))
- ->applyTo($build);
-
- return $build;
- }
-
- /**
- * Builds the render array of a given region.
- *
- * Examples:
- * @code
- * {# Print 'Sidebar First' region of the default site theme. #}
- * {{ drupal_region('sidebar_first') }}
- *
- * {# Print 'Sidebar First' region of Bartik theme. #}
- * {{ drupal_region('sidebar_first', 'bartik') }}
- * @endcode
- *
- * @param string $region
- * The region to build.
- * @param string $theme
- * (optional) The name of the theme to load the region. If it is not
- * provided then default theme will be used.
- *
- * @return array
- * A render array to display the region content.
- */
- public function drupalRegion($region, $theme = NULL) {
-
- $entity_type_manager = \Drupal::entityTypeManager();
- $blocks = $entity_type_manager->getStorage('block')->loadByProperties([
- 'region' => $region,
- 'theme' => $theme ?: \Drupal::config('system.theme')->get('default'),
- ]);
-
- $view_builder = $entity_type_manager->getViewBuilder('block');
-
- $build = [];
-
- $cache_metadata = new CacheableMetadata();
-
- /* @var $blocks \Drupal\block\BlockInterface[] */
- foreach ($blocks as $id => $block) {
- $access = $block->access('view', NULL, TRUE);
- $cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($access));
- if ($access->isAllowed()) {
- $block_plugin = $block->getPlugin();
- if ($block_plugin instanceof TitleBlockPluginInterface) {
- $request = \Drupal::request();
- if ($route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) {
- $block_plugin->setTitle(\Drupal::service('title_resolver')->getTitle($request, $route));
- }
- }
- $build[$id] = $view_builder->view($block);
- }
- }
-
- if ($build) {
- $build['#region'] = $region;
- $build['#theme_wrappers'] = ['region'];
- $cache_metadata->applyTo($build);
- }
-
- return $build;
- }
-
- /**
- * Returns the render array to represent and entity.
- *
- * Examples:
- * @code
- * {# Print a content block which ID is 1. #}
- * {{ drupal_entity('block_content', 1) }}
- *
- * {# Print a node's teaser. #}
- * {{ drupal_entity('node', 123, 'teaser') }}
- *
- * {# Print Branding block which was previously disabled on #}
- * {# admin/structure/block page. #}
- * {{ drupal_entity('block', 'bartik_branding', check_access=false) }}
- * @endcode
- *
- * @param string $entity_type
- * The entity type.
- * @param mixed $id
- * (optional) The ID of the entity to build.
- * @param string $view_mode
- * (optional) The view mode that should be used to render the entity.
- * @param string $langcode
- * (optional) For which language the entity should be rendered, defaults to
- * the current content language.
- * @param bool $check_access
- * (optional) Indicates that access check is required.
- *
- * @return null|array
- * A render array for the entity or NULL if the entity does not exist.
- */
- public function drupalEntity($entity_type, $id = NULL, $view_mode = NULL, $langcode = NULL, $check_access = TRUE) {
- $entity_type_manager = \Drupal::entityTypeManager();
- if ($id) {
- $entity = $entity_type_manager->getStorage($entity_type)->load($id);
- }
- else {
- @trigger_error('Loading entities from route is deprecated in Twig Tweak 2.4 and will not be supported in Twig Tweak 3.0', E_USER_DEPRECATED);
- $entity = \Drupal::routeMatch()->getParameter($entity_type);
- }
-
- $build = [];
-
- if ($entity) {
- $access = $check_access ? $entity->access('view', NULL, TRUE) : AccessResult::allowed();
- if ($access->isAllowed()) {
- $build = $entity_type_manager
- ->getViewBuilder($entity_type)
- ->view($entity, $view_mode, $langcode);
- }
- CacheableMetadata::createFromRenderArray($build)
- ->merge(CacheableMetadata::createFromObject($entity))
- ->merge(CacheableMetadata::createFromObject($access))
- ->applyTo($build);
- }
-
- return $build;
- }
-
- /**
- * Gets the built and processed entity form for the given entity type.
- *
- * Examples:
- * @code
- * {# Print edit form for node 1. #}
- * {{ drupal_entity_form('node', 1) }}
- *
- * {# Print add form for Article content type. #}
- * {{ drupal_entity_form('node', values={type: 'article'}) }}
- *
- * {# Print user register form. #}
- * {{ drupal_entity_form('user', NULL, 'register', check_access=false) }}
- * @endcode
- *
- * @param string $entity_type
- * The entity type.
- * @param mixed $id
- * (optional) The ID of the entity to build. If empty then new entity will
- * be created.
- * @param string $form_mode
- * (optional) The mode identifying the form variation to be returned.
- * @param array $values
- * (optional) An array of values to set, keyed by property name.
- * @param bool $check_access
- * (optional) Indicates that access check is required.
- *
- * @return array
- * The processed form for the given entity type and form mode.
- */
- public function drupalEntityForm($entity_type, $id = NULL, $form_mode = 'default', array $values = [], $check_access = TRUE) {
- $entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type);
- if ($id) {
- $entity = $entity_storage->load($id);
- $operation = 'update';
- }
- else {
- $entity = $entity_storage->create($values);
- $operation = 'create';
- }
-
- $build = [];
-
- if ($entity) {
- $access = $check_access ? $entity->access($operation, NULL, TRUE) : AccessResult::allowed();
- if ($access->isAllowed()) {
- $build = \Drupal::service('entity.form_builder')->getForm($entity, $form_mode);
- }
- CacheableMetadata::createFromRenderArray($build)
- ->merge(CacheableMetadata::createFromObject($entity))
- ->merge(CacheableMetadata::createFromObject($access))
- ->applyTo($build);
- }
-
- return $build;
- }
-
- /**
- * Returns the render array for a single entity field.
- *
- * Example:
- * @code
- * {{ drupal_field('field_image', 'node', 1) }}
- * {{ drupal_field('field_image', 'node', 1, 'teaser') }}
- * {{ drupal_field('field_image', 'node', 1, {type: 'image_url', settings: {image_style: 'large'}}) }}
- * @endcode
- *
- * @param string $field_name
- * The field name.
- * @param string $entity_type
- * The entity type.
- * @param mixed $id
- * The ID of the entity to render.
- * @param string $view_mode
- * (optional) The view mode that should be used to render the field.
- * @param string $langcode
- * (optional) Language code to load translation.
- * @param bool $check_access
- * (optional) Indicates that access check is required.
- *
- * @return null|array
- * A render array for the field or NULL if the value does not exist.
- */
- public function drupalField($field_name, $entity_type, $id = NULL, $view_mode = 'default', $langcode = NULL, $check_access = TRUE) {
- $entity_type_manager = \Drupal::entityTypeManager();
-
- if ($id) {
- $entity = $entity_type_manager->getStorage($entity_type)->load($id);
- }
- else {
- @trigger_error('Loading entities from route is deprecated in Twig Tweak 2.4 and will not be supported in Twig Tweak 3.0', E_USER_DEPRECATED);
- $entity = \Drupal::routeMatch()->getParameter($entity_type);
- }
-
- $build = [];
-
- if ($entity) {
- $access = $check_access ? $entity->access('view', NULL, TRUE) : AccessResult::allowed();
- if ($access->isAllowed()) {
- $entity = \Drupal::service('entity.repository')
- ->getTranslationFromContext($entity, $langcode);
- if (!isset($entity->{$field_name})) {
- // @todo Trigger error here.
- return;
- }
- $build = $entity->{$field_name}->view($view_mode);
- }
- CacheableMetadata::createFromRenderArray($build)
- ->merge(CacheableMetadata::createFromObject($access))
- ->merge(CacheableMetadata::createFromObject($entity))
- ->applyTo($build);
- }
-
- return $build;
- }
-
- /**
- * Returns the render array for Drupal menu.
- *
- * Example:
- * @code
- * {{ drupal_menu('main') }}
- * @endcode
- *
- * @param string $menu_name
- * The name of the menu.
- * @param int $level
- * (optional) Initial menu level.
- * @param int $depth
- * (optional) Maximum number of menu levels to display.
- * @param bool $expand
- * (optional) Expand all menu links.
- *
- * @return array
- * A render array for the menu.
- */
- public function drupalMenu($menu_name, $level = 1, $depth = 0, $expand = FALSE) {
- /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */
- $menu_tree = \Drupal::service('menu.link_tree');
- $parameters = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name);
-
- // Adjust the menu tree parameters based on the block's configuration.
- $parameters->setMinDepth($level);
- // When the depth is configured to zero, there is no depth limit. When depth
- // is non-zero, it indicates the number of levels that must be displayed.
- // Hence this is a relative depth that we must convert to an actual
- // (absolute) depth, that may never exceed the maximum depth.
- if ($depth > 0) {
- $parameters->setMaxDepth(min($level + $depth - 1, $menu_tree->maxDepth()));
- }
-
- // If expandedParents is empty, the whole menu tree is built.
- if ($expand) {
- $parameters->expandedParents = [];
- }
-
- $tree = $menu_tree->load($menu_name, $parameters);
- $manipulators = [
- ['callable' => 'menu.default_tree_manipulators:checkAccess'],
- ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
- ];
- $tree = $menu_tree->transform($tree, $manipulators);
- return $menu_tree->build($tree);
- }
-
- /**
- * Builds and processes a form for a given form ID.
- *
- * Example:
- * @code
- * {{ drupal_form('Drupal\\search\\Form\\SearchBlockForm') }}
- * @endcode
- *
- * @param string $form_id
- * The form ID.
- * @param ...
- * Additional arguments are passed to form constructor.
- *
- * @return array
- * A render array to represent the form.
- */
- public function drupalForm($form_id) {
- $callback = [\Drupal::formBuilder(), 'getForm'];
- return call_user_func_array($callback, func_get_args());
- }
-
- /**
- * Builds an image.
- *
- * Examples:
- * @code
- * {# Render image specified by file ID. #}
- * {{ drupal_image(123) }}
- *
- * {# Render image specified by file UUID. #}
- * {{ drupal_image('9bb27144-e6b2-4847-bd24-adcc59613ec0') }}
- *
- * {# Render image specified by file URI. #}
- * {{ drupal_image('public://ocean.jpg') }}
- *
- * {# 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}) }}
- *
- * {# Render responsive image. #}
- * {{ drupal_image('public://ocean.jpg', 'wide', responsive=true) }}
- * @endcode
- *
- * @param mixed $property
- * A property to identify the image.
- * @param string $style
- * (optional) Image style.
- * @param array $attributes
- * (optional) Image attributes.
- * @param bool $responsive
- * (optional) Indicates that the provided image style is responsive.
- * @param bool $check_access
- * (optional) Indicates that access check is required.
- *
- * @return array|null
- * A render array to represent the image.
- */
- public function drupalImage($property, $style = NULL, array $attributes = [], $responsive = FALSE, $check_access = TRUE) {
-
- // Determine property type by its value.
- if (preg_match('/^\d+$/', $property)) {
- $property_type = 'fid';
- }
- elseif (Uuid::isValid($property)) {
- $property_type = 'uuid';
- }
- else {
- $property_type = 'uri';
- }
-
- $files = \Drupal::entityTypeManager()
- ->getStorage('file')
- ->loadByProperties([$property_type => $property]);
-
- $build = [];
-
- // To avoid ambiguity render nothing unless exact one image has been found.
- if (count($files) != 1) {
- return $build;
- }
-
- $file = reset($files);
-
- $access = $check_access ? $file->access('view', NULL, TRUE) : AccessResult::allowed();
-
- if ($access->isAllowed()) {
- $build['#uri'] = $file->getFileUri();
- $build['#attributes'] = $attributes;
- if ($style) {
- if ($responsive) {
- $build['#type'] = 'responsive_image';
- $build['#responsive_image_style_id'] = $style;
- }
- else {
- $build['#theme'] = 'image_style';
- $build['#style_name'] = $style;
- }
- }
- else {
- $build['#theme'] = 'image';
- }
- }
-
- CacheableMetadata::createFromRenderArray($build)
- ->merge(CacheableMetadata::createFromObject($access))
- ->applyTo($build);
-
- return $build;
- }
-
- /**
- * Replaces a given tokens with appropriate value.
- *
- * Example:
- * @code
- * {{ drupal_token('site:name') }}
- * @endcode
- *
- * @param string $token
- * A replaceable token.
- * @param array $data
- * (optional) An array of keyed objects. For simple replacement scenarios
- * 'node', 'user', and others are common keys, with an accompanying node or
- * user object being the value. Some token types, like 'site', do not
- * require any explicit information from $data and can be replaced even if
- * it is empty.
- * @param array $options
- * (optional) A keyed array of settings and flags to control the token
- * replacement process.
- *
- * @return string
- * The token value.
- *
- * @see \Drupal\Core\Utility\Token::replace()
- */
- public function drupalToken($token, array $data = [], array $options = []) {
- return \Drupal::token()->replace("[$token]", $data, $options);
- }
-
- /**
- * Retrieves data from a given configuration object.
- *
- * Example:
- * @code
- * {{ drupal_config('system.site', 'name') }}
- * @endcode
- *
- * @param string $name
- * The name of the configuration object to construct.
- * @param string $key
- * A string that maps to a key within the configuration data.
- *
- * @return mixed
- * The data that was requested.
- */
- public function drupalConfig($name, $key) {
- return \Drupal::config($name)->get($key);
- }
-
- /**
- * Dumps information about variables.
- *
- * Examples:
- * @code
- * {# Basic usage. #}
- * {{ drupal_dump(var) }}
- *
- * {# Same as above but shorter. #}
- * {{ dd(var) }}
- *
- * {# Dump all available variables for the current template. #}
- * {{ dd() }}
- * @endcode
- *
- * @param array $context
- * Variables from the Twig template.
- * @param mixed $variable
- * (optional) The variable to dump.
- */
- public function drupalDump(array $context, $variable = NULL) {
- $var_dumper = '\Symfony\Component\VarDumper\VarDumper';
- if (class_exists($var_dumper)) {
- call_user_func($var_dumper . '::dump', func_num_args() == 1 ? $context : $variable);
- }
- else {
- trigger_error('Could not dump the variable because symfony/var-dumper component is not installed.', E_USER_WARNING);
- }
- }
-
- /**
- * Returns a title for the current route.
- *
- * @return array
- * A render array to represent page title.
- */
- public function drupalTitle() {
- $title = \Drupal::service('title_resolver')->getTitle(
- \Drupal::request(),
- \Drupal::routeMatch()->getRouteObject()
- );
- $build['#markup'] = render($title);
- $build['#cache']['contexts'] = ['url'];
- return $build;
- }
-
- /**
- * Generates a URL from an internal path.
- *
- * Examples:
- * @code
- * {# Basic usage. #}
- * {{ drupal_url('node/1) }}
- *
- * {# Complex URL. #}
- * {{ drupal_url('node/1', {query: {foo: 'bar'}, fragment: 'example', absolute: true}) }}
- * @endcode
- *
- * @param string $user_input
- * User input for a link or path.
- * @param array $options
- * (optional) An array of options.
- * @param bool $check_access
- * (optional) Indicates that access check is required.
- *
- * @return \Drupal\Core\Url|null
- * A new Url object or null if the URL is not accessible.
- *
- * @see \Drupal\Core\Url::fromUserInput()
- */
- public function drupalUrl($user_input, array $options = [], $check_access = FALSE) {
- if (isset($options['langcode'])) {
- $language_manager = \Drupal::languageManager();
- if ($language = $language_manager->getLanguage($options['langcode'])) {
- $options['language'] = $language;
- }
- }
- if (!in_array($user_input[0], ['/', '#', '?'])) {
- $user_input = '/' . $user_input;
- }
- $url = Url::fromUserInput($user_input, $options);
- if (!$check_access || $url->access()) {
- return $url;
- }
- }
-
- /**
- * Generates a link from an internal path.
- *
- * Examples:
- * @code
- * {# It supports the same options as drupal_url(), plus attributes. #}
- * {{ drupal_link('View'|t, 'node/1', {attributes: {target: '_blank'}}) }}
- *
- * {# This link will only be shown for privileged users. #}
- * {{ drupal_link('Example'|t, '/admin', check_access=true) }}
- * @endcode
- *
- * @param string $text
- * The text to be used for the link.
- * @param string $user_input
- * User input for a link or path.
- * @param array $options
- * (optional) An array of options.
- * @param bool $check_access
- * (optional) Indicates that access check is required.
- *
- * @return \Drupal\Core\Link|null
- * A new Link object or null of the URL is not accessible.
- *
- * @see \Drupal\Core\Link::fromTextAndUrl()
- */
- public function drupalLink($text, $user_input, array $options = [], $check_access = FALSE) {
- $url = $this->drupalUrl($user_input, $options, $check_access);
- if ($url) {
- // The text has been processed by twig already, convert it to a safe
- // object for the render system.
- // @see \Drupal\Core\Template\TwigExtension::getLink()
- if ($text instanceof \Twig_Markup) {
- $text = Markup::create($text);
- }
- return Link::fromTextAndUrl($text, $url);
- }
- }
-
- /**
- * Displays status messages.
- */
- public function drupalMessages() {
- return ['#type' => 'status_messages'];
- }
-
- /**
- * Builds the breadcrumb.
- */
- public function drupalBreadcrumb() {
- return \Drupal::service('breadcrumb')
- ->build(\Drupal::routeMatch())
- ->toRenderable();
- }
-
- /**
- * Builds contextual links.
- *
- * Examples:
- * @code
- * {# Basic usage. #}
- *
- * {{ contextual_links('entity.view.edit_form:view=frontpage&display_id=feed_1') }}
- * {{ drupal_view('frontpage') }}
- *
- *
- * {# Multiple links. #}
- *
- * {{ contextual_links('node:node=123|block_content:block_content=123') }}
- * {{ content }}
- *
- * @endcode
- *
- * @param string $id
- * A serialized representation of a #contextual_links property value array.
- *
- * @return array
- * A renderable array representing contextual links.
- *
- * @see https://www.drupal.org/node/2133283
- */
- public function contextualLinks($id) {
- $build['#cache']['contexts'] = ['user.permissions'];
- if (\Drupal::currentUser()->hasPermission('access contextual links')) {
- $build['#type'] = 'contextual_links_placeholder';
- $build['#id'] = $id;
- }
- return $build;
- }
-
- /**
- * Emits a breakpoint to the debug client.
- *
- * @param \Twig_Environment $environment
- * The Twig environment instance.
- * @param array $context
- * Variables from the Twig template.
- */
- public function drupalBreakpoint(\Twig_Environment $environment, array $context) {
- if (function_exists('xdebug_break')) {
- xdebug_break();
- }
- else {
- trigger_error('Could not make a break because xdebug is not available.', E_USER_WARNING);
- }
- }
-
- /**
- * Replaces all tokens in a given string with appropriate values.
- *
- * Example:
- * @code
- * {# Basic usage. #}
- * {{ '[site:name]
[site:slogan]
'|token_replace }}
- *
- * {# This is more suited to large markup (requires Twig >= 1.41). #}
- * {% apply token_replace %}
- * [site:name]
- * [site:slogan]
- * {% endapply %}
- * @endcode
- *
- * @param string $text
- * An HTML string containing replaceable tokens.
- *
- * @return string
- * The entered HTML text with tokens replaced.
- */
- public function tokenReplaceFilter($text) {
- return \Drupal::token()->replace($text);
- }
-
- /**
- * Performs a regular expression search and replace.
- *
- * Example:
- * @code
- * {{ 'Drupal - community plumbing!'|preg_replace('/(Drupal)/', '$1') }}
- * @endcode
- *
- * For simple string interpolation consider using built-in 'replace' or
- * 'format' Twig filters.
- *
- * @param string $text
- * The text to search and replace.
- * @param string $pattern
- * The pattern to search for.
- * @param string $replacement
- * The string to replace.
- *
- * @return string
- * The new text if matches are found, otherwise unchanged text.
- */
- public function pregReplaceFilter($text, $pattern, $replacement) {
- return preg_replace($pattern, $replacement, $text);
- }
-
- /**
- * Returns the URL of this image derivative for an original image path or URI.
- *
- * Example:
- * @code
- * {{ 'public://images/ocean.jpg'|image_style('thumbnail') }}
- * @endcode
- *
- * @param string $path
- * The path or URI to the original image.
- * @param string $style
- * The image style.
- *
- * @return string|null
- * The absolute URL where a style image can be downloaded, suitable for use
- * in an tag. Requesting the URL will cause the image to be created.
- */
- public function imageStyle($path, $style) {
-
- if (!$image_style = ImageStyle::load($style)) {
- trigger_error(sprintf('Could not load image style %s.', $style));
- return;
- }
-
- if (!$image_style->supportsUri($path)) {
- trigger_error(sprintf('Could not apply image style %s.', $style));
- return;
- }
-
- return file_url_transform_relative($image_style->buildUrl($path));
- }
-
- /**
- * Transliterates text from Unicode to US-ASCII.
- *
- * Example:
- * @code
- * {{ 'Привет!'|transliterate }}
- * @endcod
- *
- * @param string $string
- * The string to transliterate.
- * @param string $langcode
- * (optional) The language code of the language the string is in. Defaults
- * to 'en' if not provided. Warning: this can be unfiltered user input.
- * @param string $unknown_character
- * (optional) The character to substitute for characters in $string without
- * transliterated equivalents. Defaults to '?'.
- * @param int $max_length
- * (optional) If provided, return at most this many characters, ensuring
- * that the transliteration does not split in the middle of an input
- * character's transliteration.
- *
- * @return string
- * $string with non-US-ASCII characters transliterated to US-ASCII
- * characters, and unknown characters replaced with $unknown_character.
- */
- public function transliterate($string, $langcode = 'en', $unknown_character = '?', $max_length = NULL) {
- return \Drupal::transliteration()->transliterate($string, $langcode, $unknown_character, $max_length);
- }
-
- /**
- * Runs all the enabled filters on a piece of text.
- *
- * Example.
- * @code
- * {{ 'bold strong'|check_markup('restricted_html') }}
- * @endcode
- *
- * @param string $text
- * The text to be filtered.
- * @param string|null $format_id
- * (optional) The machine name of the filter format to be used to filter the
- * text. Defaults to the fallback format. See filter_fallback_format().
- * @param string $langcode
- * (optional) The language code of the text to be filtered.
- * @param array $filter_types_to_skip
- * (optional) An array of filter types to skip, or an empty array (default)
- * to skip no filter types.
- *
- * @return \Drupal\Component\Render\MarkupInterface
- * The filtered text.
- *
- * @see check_markup()
- */
- public function checkMarkup($text, $format_id = NULL, $langcode = '', array $filter_types_to_skip = []) {
- return check_markup($text, $format_id, $langcode, $filter_types_to_skip);
- }
-
- /**
- * Truncates a UTF-8-encoded string safely to a number of characters.
- *
- * Example:
- * @code
- * {{ 'Some long text'|truncate(10, true) }}
- * @endcode
- *
- * @param string $string
- * The string to truncate.
- * @param int $max_length
- * An upper limit on the returned string length, including trailing ellipsis
- * if $add_ellipsis is TRUE.
- * @param bool $wordsafe
- * (optional) If TRUE, attempt to truncate on a word boundary.
- * @param bool $add_ellipsis
- * (optional) If TRUE, add '...' to the end of the truncated string.
- * @param int $min_wordsafe_length
- * (optional) If TRUE, the minimum acceptable length for truncation.
- *
- * @return string
- * The truncated string.
- *
- * @see \Drupal\Component\Utility\Unicode::truncate()
- */
- public function truncate($string, $max_length, $wordsafe = FALSE, $add_ellipsis = FALSE, $min_wordsafe_length = 1) {
- return Unicode::truncate($string, $max_length, $wordsafe, $add_ellipsis, $min_wordsafe_length);
- }
-
- /**
- * Adds new element to the array.
- *
- * Examples:
- * @code
- * {# Set top level value. #}
- * {{ content.field_image|with('#title', 'Photo'|t) }}
- *
- * {# Set nested value. #}
- * {{ content|with(['field_image', '#title'], 'Photo'|t) }}
- * @endcode
- *
- * @param array $build
- * The renderable array to add the child item.
- * @param mixed $key
- * The key of the new element.
- * @param mixed $element
- * The element to add.
- *
- * @return array
- * The modified array.
- */
- public function with(array $build, $key, $element) {
- if (is_array($key)) {
- NestedArray::setValue($build, $key, $element);
- }
- else {
- $build[$key] = $element;
- }
- return $build;
- }
-
- /**
- * Returns a render array for entity, field list or field item.
- *
- * Examples:
- *
- * Do not put this into node.html.twig template to avoid recursion.
- * @code
- * {{ node|view }}
- * {{ node|view('teaser') }}
- * @endcode
- *
- * @code
- * {{ node.field_image|view }}
- * {{ node.field_image[0]|view }}
- * {{ node.field_image|view('teaser') }}
- * {{ node.field_image|view({settings: {image_style: 'thumbnail'}}) }}
- * @endcode
- *
- * @param mixed $object
- * The object to build a render array from.
- * @param string|array $display_options
- * Can be either the name of a view mode, or an array of display settings.
- * @param string $langcode
- * (optional) For which language the entity should be rendered, defaults to
- * the current content language.
- * @param bool $check_access
- * (optional) Indicates that access check for an entity is required.
- *
- * @return array
- * A render array to represent the object.
- */
- public function view($object, $display_options = 'default', $langcode = NULL, $check_access = TRUE) {
- $build = [];
- if ($object instanceof FieldItemListInterface || $object instanceof FieldItemInterface) {
- return $object->view($display_options);
- }
- elseif ($object instanceof EntityInterface) {
- $build = [];
- $access = $check_access ? $object->access('view', NULL, TRUE) : AccessResult::allowed();
- if ($access->isAllowed()) {
- $build = \Drupal::entityTypeManager()
- ->getViewBuilder($object->getEntityTypeId())
- ->view($object, $display_options, $langcode);
- }
- CacheableMetadata::createFromRenderArray($build)
- ->merge(CacheableMetadata::createFromObject($object))
- ->merge(CacheableMetadata::createFromObject($access))
- ->applyTo($build);
- }
- return $build;
- }
-
- /**
- * Filters out the children of a render array, optionally sorted by weight.
- *
- * Example:
- * @code
- *
- * {% for tag in content.field_tags|children %}
- * - {{ tag }}
- * {% endfor %}
- *
- * @endcode
- *
- * @param array $build
- * The render array whose children are to be filtered.
- * @param bool $sort
- * Boolean to indicate whether the children should be sorted by weight.
- *
- * @return array
- * The element's children.
- */
- public function children(array $build, $sort = FALSE) {
- $keys = Element::children($build, $sort);
- return array_intersect_key($build, array_flip($keys));
- }
-
- /**
- * Returns a URL path to the file.
- *
- * Examples:
- *
- * For string arguments it works similar to core file_url() Twig function.
- * @code
- * {{ 'public://sea.jpg'|file_url }}
- * @endcode
- *
- * When field item list passed the URL will be extracted from the first item.
- * In order to get URL of specific item specify its delta explicitly using
- * array notation.
- * @code
- * {{ node.field_image|file_url }}
- * {{ node.field_image[0]|file_url }}
- * @endcode
- *
- * Media fields are fully supported including OEmbed resources.
- * @code
- * {{ node.field_media|file_url }}
- * @endcode
- *
- * @param string|object $input
- * Can be either file URI or an object that contains the URI.
- *
- * @return string|null
- * A URL that may be used to access the file.
- */
- public function fileUrl($input) {
- if (is_string($input)) {
- return file_url_transform_relative(file_create_url($input));
- }
- if ($input instanceof EntityReferenceFieldItemListInterface) {
- $referenced_entities = $input->referencedEntities();
- if (isset($referenced_entities[0])) {
- return self::getUrlFromEntity($referenced_entities[0]);
- }
- }
- elseif ($input instanceof EntityReferenceItem) {
- return self::getUrlFromEntity($input->entity);
- }
- }
-
- /**
- * Extracts file URL form content entity.
- *
- * @param object $entity
- * Entity object that contains information about the file.
- *
- * @return string|null
- * A URL that may be used to access the file.
- */
- private static function getUrlFromEntity($entity) {
- if ($entity instanceof MediaInterface) {
- $source = $entity->getSource();
- $value = $source->getSourceFieldValue($entity);
- if ($source instanceof OEmbedInterface) {
- return $value;
- }
- elseif ($file = File::load($value)) {
- return $file->createFileUrl();
- }
- }
- elseif ($entity instanceof FileInterface) {
- return $entity->createFileUrl();
- }
- }
-
- /**
- * Evaluates a string of PHP code.
- *
- * PHP filter is disabled by default. You can enable it in settings.php file
- * as follows:
- * @code
- * $settings['twig_tweak_enable_php_filter'] = TRUE;
- * @endcode
- *
- * Usage example:
- * @code
- * {{ 'return date('Y');'|php }}
- * @endcode
- *
- * Using PHP filter is discouraged as it may cause security implications. In
- * fact it is very rarely needed.
- *
- * The above code can be replaced with following.
- * @code
- * {{ 'now'|date('Y') }}
- * @endcode
- *
- * @param string $code
- * Valid PHP code to be evaluated.
- *
- * @return mixed
- * The eval() result.
- */
- public function phpFilter($code) {
- ob_start();
- // @codingStandardsIgnoreStart
- print eval($code);
- // @codingStandardsIgnoreEnd
- $output = ob_get_contents();
- ob_end_clean();
- return $output;
- }
-
-}
diff --git a/src/TwigTweakExtension.php b/src/TwigTweakExtension.php
new file mode 100644
index 0000000..1b83e3f
--- /dev/null
+++ b/src/TwigTweakExtension.php
@@ -0,0 +1,591 @@
+ TRUE];
+ $all_options = ['needs_environment' => TRUE, 'needs_context' => TRUE];
+
+ return [
+ new TwigFunction('drupal_view', 'views_embed_view'),
+ new TwigFunction('drupal_view_result', 'views_get_view_result'),
+ new TwigFunction('drupal_block', [self::class, 'drupalBlock']),
+ new TwigFunction('drupal_region', [self::class, 'drupalRegion']),
+ new TwigFunction('drupal_entity', [self::class, 'drupalEntity']),
+ new TwigFunction('drupal_entity_form', [self::class, 'drupalEntityForm']),
+ new TwigFunction('drupal_field', [self::class, 'drupalField']),
+ new TwigFunction('drupal_menu', [self::class, 'drupalMenu']),
+ new TwigFunction('drupal_form', [self::class, 'drupalForm']),
+ new TwigFunction('drupal_image', [self::class, 'drupalImage']),
+ new TwigFunction('drupal_token', [self::class, 'drupalToken']),
+ new TwigFunction('drupal_config', [self::class, 'drupalConfig']),
+ new TwigFunction('drupal_dump', [self::class, 'drupalDump'], $context_options),
+ new TwigFunction('dd', [self::class, 'drupalDump'], $context_options),
+ new TwigFunction('drupal_title', [self::class, 'drupalTitle']),
+ new TwigFunction('drupal_url', [self::class, 'drupalUrl']),
+ new TwigFunction('drupal_link', [self::class, 'drupalLink']),
+ new TwigFunction('drupal_messages', function (): array {
+ return ['#type' => 'status_messages'];
+ }),
+ new TwigFunction('drupal_breadcrumb', [self::class, 'drupalBreadcrumb']),
+ new TwigFunction('drupal_breakpoint', [self::class, 'drupalBreakpoint'], $all_options),
+ new TwigFunction('drupal_contextual_links', [self::class, 'drupalContextualLinks']),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilters(): array {
+
+ $filters = [
+ new TwigFilter('token_replace', [self::class, 'tokenReplaceFilter']),
+ new TwigFilter('preg_replace', [self::class, 'pregReplaceFilter']),
+ new TwigFilter('image_style', [self::class, 'imageStyleFilter']),
+ new TwigFilter('transliterate', [self::class, 'transliterateFilter']),
+ new TwigFilter('check_markup', 'check_markup'),
+ new TwigFilter('truncate', [Unicode::class, 'truncate']),
+ new TwigFilter('view', [self::class, 'viewFilter']),
+ new TwigFilter('with', [self::class, 'withFilter']),
+ new TwigFilter('children', [self::class, 'childrenFilter']),
+ new TwigFilter('file_url', [self::class, 'fileUrlFilter']),
+ ];
+
+ if (Settings::get('twig_tweak_enable_php_filter')) {
+ $filters[] = new TwigFilter('php', [self::class, 'phpFilter']);
+ }
+
+ return $filters;
+ }
+
+ /**
+ * Builds the render array for a block.
+ */
+ public static function drupalBlock(string $id, array $configuration = [], bool $wrapper = TRUE): array {
+ return \Drupal::service('twig_tweak.block_view_builder')->build($id, $configuration, $wrapper);
+ }
+
+ /**
+ * Builds the render array of a given region.
+ */
+ public static function drupalRegion(string $region, string $theme = NULL): array {
+ return \Drupal::service('twig_tweak.region_view_builder')->build($region, $theme);
+ }
+
+ /**
+ * Returns the render array to represent an entity.
+ */
+ public static function drupalEntity(string $entity_type, ?string $id, string $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array {
+ $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load((string) $id);
+ if ($entity) {
+ return \Drupal::service('twig_tweak.entity_view_builder')
+ ->build($entity, $view_mode, $langcode, $check_access);
+ }
+ return [];
+ }
+
+ /**
+ * Gets the built and processed entity form for the given entity type.
+ */
+ 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 = $id ? $entity_storage->load($id) : $entity_storage->create($values);
+ if ($entity) {
+ return \Drupal::service('twig_tweak.entity_form_view_builder')
+ ->build($entity, $form_mode, $check_access);
+ }
+ return [];
+ }
+
+ /**
+ * 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 {
+ $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load((string) $id);
+ if ($entity) {
+ return \Drupal::service('twig_tweak.field_view_builder')
+ ->build($entity, $field_name, $view_mode, $langcode, $check_access);
+ }
+ return [];
+ }
+
+ /**
+ * Returns the render array for Drupal menu.
+ */
+ public static function drupalMenu(string $menu_name, int $level = 1, int $depth = 0, bool $expand = FALSE): array {
+ return \Drupal::service('twig_tweak.menu_view_builder')->build($menu_name, $level, $depth, $expand);
+ }
+
+ /**
+ * Builds and processes a form for a given form ID.
+ *
+ * @param string $form_id
+ * The form ID.
+ * @param mixed $args
+ * Additional arguments are passed to form constructor.
+ *
+ * @return array
+ * A render array to represent the form.
+ */
+ public static function drupalForm(string $form_id, ...$args): array {
+ $callback = [\Drupal::formBuilder(), 'getForm'];
+ return call_user_func_array($callback, func_get_args());
+ }
+
+ /**
+ * Builds an image.
+ */
+ public static function drupalImage(string $property, string $style = NULL, array $attributes = [], bool $responsive = FALSE, bool $check_access = TRUE): array {
+
+ // Determine property type by its value.
+ if (preg_match('/^\d+$/', $property)) {
+ $property_type = 'fid';
+ }
+ elseif (Uuid::isValid($property)) {
+ $property_type = 'uuid';
+ }
+ else {
+ $property_type = 'uri';
+ }
+
+ $files = \Drupal::entityTypeManager()
+ ->getStorage('file')
+ ->loadByProperties([$property_type => $property]);
+
+ // To avoid ambiguity render nothing unless exact one image has been found.
+ if (count($files) != 1) {
+ return [];
+ }
+
+ $file = reset($files);
+ return \Drupal::service('twig_tweak.image_view_builder')->build($file, $style, $attributes, $responsive, $check_access);
+ }
+
+ /**
+ * Replaces a given token with appropriate value.
+ *
+ * @param string $token
+ * A replaceable token.
+ * @param array $data
+ * (optional) An array of keyed objects. For simple replacement scenarios
+ * 'node', 'user', and others are common keys, with an accompanying node or
+ * user object being the value. Some token types, like 'site', do not
+ * require any explicit information from $data and can be replaced even if
+ * it is empty.
+ * @param array $options
+ * (optional) A keyed array of settings and flags to control the token
+ * replacement process.
+ *
+ * @return string
+ * The token value.
+ *
+ * @see \Drupal\Core\Utility\Token::replace()
+ */
+ public static function drupalToken(string $token, array $data = [], array $options = []): string {
+ return \Drupal::token()->replace("[$token]", $data, $options);
+ }
+
+ /**
+ * Retrieves data from a given configuration object.
+ *
+ * @param string $name
+ * The name of the configuration object to construct.
+ * @param string $key
+ * A string that maps to a key within the configuration data.
+ *
+ * @return mixed
+ * The data that was requested.
+ */
+ public static function drupalConfig(string $name, string $key) {
+ return \Drupal::config($name)->get($key);
+ }
+
+ /**
+ * Dumps information about variables.
+ *
+ * @param array $context
+ * Variables from the Twig template.
+ * @param mixed $variable
+ * (optional) The variable to dump.
+ */
+ public static function drupalDump(array $context, $variable = NULL): void {
+ $var_dumper = '\Symfony\Component\VarDumper\VarDumper';
+ if (class_exists($var_dumper)) {
+ call_user_func($var_dumper . '::dump', func_num_args() == 1 ? $context : $variable);
+ }
+ else {
+ trigger_error('Could not dump the variable because symfony/var-dumper component is not installed.', E_USER_WARNING);
+ }
+ }
+
+ /**
+ * Returns a title for the current route.
+ */
+ public static function drupalTitle(): array {
+ $title = \Drupal::service('title_resolver')->getTitle(
+ \Drupal::request(),
+ \Drupal::routeMatch()->getRouteObject()
+ );
+ $build['#markup'] = render($title);
+ $build['#cache']['contexts'] = ['url'];
+ return $build;
+ }
+
+ /**
+ * Generates a URL from an internal path.
+ *
+ * @param string $user_input
+ * User input for a link or path.
+ * @param array $options
+ * (optional) An array of options.
+ * @param bool $check_access
+ * (optional) Indicates that access check is required.
+ *
+ * @return \Drupal\Core\Url|null
+ * A new Url object or null if the URL is not accessible.
+ *
+ * @see \Drupal\Core\Url::fromUserInput()
+ */
+ public static function drupalUrl(string $user_input, array $options = [], bool $check_access = FALSE): ?Url {
+ if (isset($options['langcode'])) {
+ $language_manager = \Drupal::languageManager();
+ if ($language = $language_manager->getLanguage($options['langcode'])) {
+ $options['language'] = $language;
+ }
+ }
+ if (!in_array($user_input[0], ['/', '#', '?'])) {
+ $user_input = '/' . $user_input;
+ }
+ $url = Url::fromUserInput($user_input, $options);
+ return (!$check_access || $url->access()) ? $url : NULL;
+ }
+
+ /**
+ * Generates a link from an internal path.
+ *
+ * @param string|\Twig\Markup $text
+ * The text to be used for the link.
+ * @param string $user_input
+ * User input for a link or path.
+ * @param array $options
+ * (optional) An array of options.
+ * @param bool $check_access
+ * (optional) Indicates that access check is required.
+ *
+ * @return \Drupal\Core\Link|null
+ * A new Link object or null of the URL is not accessible.
+ *
+ * @see \Drupal\Core\Link::fromTextAndUrl()
+ */
+ public static function drupalLink($text, string $user_input, array $options = [], bool $check_access = FALSE): ?Link {
+ $url = self::drupalUrl($user_input, $options, $check_access);
+ if ($url) {
+ // The text has been processed by twig already, convert it to a safe
+ // object for the render system.
+ // @see \Drupal\Core\Template\TwigExtension::getLink()
+ if ($text instanceof TwigMarkup) {
+ $text = Markup::create($text);
+ }
+ return Link::fromTextAndUrl($text, $url);
+ }
+ return NULL;
+ }
+
+ /**
+ * Builds the breadcrumb.
+ */
+ public static function drupalBreadcrumb(): array {
+ return \Drupal::service('breadcrumb')
+ ->build(\Drupal::routeMatch())
+ ->toRenderable();
+ }
+
+ /**
+ * Builds contextual links.
+ *
+ * @param string $id
+ * A serialized representation of a #contextual_links property value array.
+ *
+ * @return array
+ * A renderable array representing contextual links.
+ *
+ * @see https://www.drupal.org/node/2133283
+ */
+ public static function drupalContextualLinks(string $id): array {
+ $build['#cache']['contexts'] = ['user.permissions'];
+ if (\Drupal::currentUser()->hasPermission('access contextual links')) {
+ $build['#type'] = 'contextual_links_placeholder';
+ $build['#id'] = $id;
+ }
+ return $build;
+ }
+
+ /**
+ * Emits a breakpoint to the debug client.
+ *
+ * @param \Twig\Environment $environment
+ * The Twig environment instance.
+ * @param array $context
+ * Variables from the current Twig template.
+ */
+ public static function drupalBreakpoint(Environment $environment, array $context): void {
+ if (function_exists('xdebug_break')) {
+ xdebug_break();
+ }
+ else {
+ trigger_error('Could not make a break because xdebug is not available.', E_USER_WARNING);
+ }
+ }
+
+ /**
+ * Replaces all tokens in a given string with appropriate values.
+ *
+ * @param string $text
+ * An HTML string containing replaceable tokens.
+ *
+ * @return string
+ * The entered HTML text with tokens replaced.
+ */
+ public static function tokenReplaceFilter(string $text): string {
+ return \Drupal::token()->replace($text);
+ }
+
+ /**
+ * Performs a regular expression search and replace.
+ *
+ * @param string $text
+ * The text to search and replace.
+ * @param string $pattern
+ * The pattern to search for.
+ * @param string $replacement
+ * The string to replace.
+ *
+ * @return string
+ * The new text if matches are found, otherwise unchanged text.
+ */
+ public static function pregReplaceFilter(string $text, string $pattern, string $replacement): string {
+ return preg_replace($pattern, $replacement, $text);
+ }
+
+ /**
+ * Returns the URL of this image derivative for an original image path or URI.
+ *
+ * @param string $path
+ * The path or URI to the original image.
+ * @param string $style
+ * The image style.
+ *
+ * @return string|null
+ * The absolute URL where a style image can be downloaded, suitable for use
+ * in an tag. Requesting the URL will cause the image to be created.
+ */
+ public static function imageStyleFilter(string $path, string $style): ?string {
+
+ if (!$image_style = ImageStyle::load($style)) {
+ trigger_error(sprintf('Could not load image style %s.', $style));
+ return NULL;
+ }
+
+ if (!$image_style->supportsUri($path)) {
+ trigger_error(sprintf('Could not apply image style %s.', $style));
+ return NULL;
+ }
+
+ return file_url_transform_relative($image_style->buildUrl($path));
+ }
+
+ /**
+ * Transliterates text from Unicode to US-ASCII.
+ *
+ * @param string $text
+ * The $text to transliterate.
+ * @param string $langcode
+ * (optional) The language code of the language the string is in. Defaults
+ * to 'en' if not provided. Warning: this can be unfiltered user input.
+ * @param string $unknown_character
+ * (optional) The character to substitute for characters in $string without
+ * transliterated equivalents. Defaults to '?'.
+ * @param int $max_length
+ * (optional) If provided, return at most this many characters, ensuring
+ * that the transliteration does not split in the middle of an input
+ * character's transliteration.
+ *
+ * @return string
+ * $string with non-US-ASCII characters transliterated to US-ASCII
+ * characters, and unknown characters replaced with $unknown_character.
+ */
+ public static function transliterateFilter(string $text, string $langcode = 'en', string $unknown_character = '?', int $max_length = NULL) {
+ return \Drupal::transliteration()->transliterate($text, $langcode, $unknown_character, $max_length);
+ }
+
+ /**
+ * Returns a render array for entity, field list or field item.
+ *
+ * @param mixed $object
+ * The object to build a render array from.
+ * @param string|array $view_mode
+ * Can be either the name of a view mode, or an array of display settings.
+ * @param string $langcode
+ * (optional) For which language the entity should be rendered, defaults to
+ * the current content language.
+ * @param bool $check_access
+ * (optional) Indicates that access check for an entity is required.
+ *
+ * @return array
+ * A render array to represent the object.
+ */
+ public static function viewFilter(object $object, ?string $view_mode = 'default', string $langcode = NULL, bool $check_access = TRUE): array {
+ $build = [];
+ if ($object instanceof FieldItemListInterface || $object instanceof FieldItemInterface) {
+ $build = $object->view($view_mode);
+ }
+ elseif ($object instanceof EntityInterface) {
+ $build = \Drupal::service('twig_tweak.entity_view_builder')->build($object, $view_mode, $langcode, $check_access);
+ }
+ return $build;
+ }
+
+ /**
+ * Adds new element to the array.
+ *
+ * @param array $build
+ * The renderable array to add the child item.
+ * @param mixed $key
+ * The key of the new element.
+ * @param mixed $element
+ * The element to add.
+ *
+ * @return array
+ * The modified array.
+ */
+ public static function withFilter(array $build, $key, $element): array {
+ if (is_array($key)) {
+ NestedArray::setValue($build, $key, $element);
+ }
+ else {
+ $build[$key] = $element;
+ }
+ return $build;
+ }
+
+ /**
+ * Filters out the children of a render array, optionally sorted by weight.
+ *
+ * @param array $build
+ * The render array whose children are to be filtered.
+ * @param bool $sort
+ * Boolean to indicate whether the children should be sorted by weight.
+ *
+ * @return array
+ * The element's children.
+ */
+ public static function childrenFilter(array $build, bool $sort = FALSE): array {
+ $keys = Element::children($build, $sort);
+ return array_intersect_key($build, array_flip($keys));
+ }
+
+ /**
+ * Returns a URL path to the file.
+ *
+ * @param string|object $input
+ * Can be either file URI or an object that contains the URI.
+ *
+ * @return string|null
+ * A URL that may be used to access the file.
+ */
+ public static function fileUrlFilter($input): ?string {
+ if (is_string($input)) {
+ return file_url_transform_relative(file_create_url($input));
+ }
+ if ($input instanceof EntityReferenceFieldItemListInterface) {
+ $referenced_entities = $input->referencedEntities();
+ if (isset($referenced_entities[0])) {
+ return self::getUrlFromEntity($referenced_entities[0]);
+ }
+ }
+ elseif ($input instanceof EntityReferenceItem) {
+ return self::getUrlFromEntity($input->entity);
+ }
+ }
+
+ /**
+ * Evaluates a string of PHP code.
+ *
+ * @param string $code
+ * Valid PHP code to be evaluated.
+ *
+ * @return mixed
+ * The eval() result.
+ */
+ public static function phpFilter(string $code) {
+ ob_start();
+ // @codingStandardsIgnoreStart
+ print eval($code);
+ // @codingStandardsIgnoreEnd
+ $output = ob_get_contents();
+ ob_end_clean();
+ return $output;
+ }
+
+ /**
+ * Extracts file URL form content entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * Entity object that contains information about the file.
+ *
+ * @return string|null
+ * A URL that may be used to access the file.
+ */
+ private static function getUrlFromEntity(EntityInterface $entity): ?string {
+ if ($entity instanceof MediaInterface) {
+ $source = $entity->getSource();
+ $value = $source->getSourceFieldValue($entity);
+ if ($source instanceof OEmbedInterface) {
+ return $value;
+ }
+ elseif ($file = File::load($value)) {
+ return $file->createFileUrl();
+ }
+ }
+ elseif ($entity instanceof FileInterface) {
+ return $entity->createFileUrl();
+ }
+ }
+
+}
diff --git a/src/View/BlockViewBuilder.php b/src/View/BlockViewBuilder.php
new file mode 100644
index 0000000..02339f8
--- /dev/null
+++ b/src/View/BlockViewBuilder.php
@@ -0,0 +1,155 @@
+pluginManagerBlock = $plugin_manager_block;
+ $this->contextRepository = $context_repository;
+ $this->contextHandler = $context_handler;
+ $this->account = $account;
+ $this->requestStack = $request_stack;
+ $this->routeMatch = $route_match;
+ $this->titleResolver = $title_resolver;
+ }
+
+ /**
+ * Builds the render array for a block.
+ *
+ * @param string $id
+ * The string of block plugin to render.
+ * @param array $configuration
+ * (optional) Pass on any configuration to the plugin block.
+ * @param bool $wrapper
+ * (optional) Whether or not use block template for rendering.
+ *
+ * @return array
+ * A renderable array representing the content of the block.
+ */
+ public function build(string $id, array $configuration = [], bool $wrapper = TRUE): array {
+
+ $configuration += ['label_display' => BlockPluginInterface::BLOCK_LABEL_VISIBLE];
+
+ /** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */
+ $block_plugin = $this->pluginManagerBlock->createInstance($id, $configuration);
+
+ // Inject runtime contexts.
+ if ($block_plugin instanceof ContextAwarePluginInterface) {
+ $contexts = $this->contextRepository->getRuntimeContexts($block_plugin->getContextMapping());
+ $this->contextHandler->applyContextMapping($block_plugin, $contexts);
+ }
+
+ $build = [];
+ $access = $block_plugin->access($this->account, TRUE);
+ if ($access->isAllowed()) {
+ // Title block needs a special treatment.
+ if ($block_plugin instanceof TitleBlockPluginInterface) {
+ $request = $this->requestStack->getCurrentRequest();
+ $title = $this->titleResolver->getTitle($request, $this->routeMatch->getRouteObject());
+ $block_plugin->setTitle($title);
+ }
+
+ $build['content'] = $block_plugin->build();
+
+ if ($block_plugin instanceof TitleBlockPluginInterface) {
+ $build['content']['#cache']['contexts'][] = 'url';
+ }
+
+ if ($wrapper && !Element::isEmpty($build['content'])) {
+ $build += [
+ '#theme' => 'block',
+ '#attributes' => [],
+ '#contextual_links' => [],
+ '#configuration' => $block_plugin->getConfiguration(),
+ '#plugin_id' => $block_plugin->getPluginId(),
+ '#base_plugin_id' => $block_plugin->getBaseId(),
+ '#derivative_plugin_id' => $block_plugin->getDerivativeId(),
+ ];
+ }
+ }
+
+ CacheableMetadata::createFromRenderArray($build)
+ ->merge(CacheableMetadata::createFromObject($access))
+ ->applyTo($build);
+
+ return $build;
+ }
+
+}
diff --git a/src/View/EntityFormViewBuilder.php b/src/View/EntityFormViewBuilder.php
new file mode 100644
index 0000000..77a0b40
--- /dev/null
+++ b/src/View/EntityFormViewBuilder.php
@@ -0,0 +1,60 @@
+entityFormBuilder = $entity_form_builder;
+ }
+
+ /**
+ * Gets the built and processed entity form for the given entity type.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity.
+ * @param string $form_mode
+ * (optional) The mode identifying the form variation to be returned.
+ * @param bool $check_access
+ * (optional) Indicates that access check is required.
+ *
+ * @return array
+ * 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 {
+
+ $build = [];
+
+ $operation = $entity->isNew() ? 'create' : 'update';
+ $access = $check_access ? $entity->access($operation, NULL, TRUE) : AccessResult::allowed();
+ if ($access->isAllowed()) {
+ $build = $this->entityFormBuilder->getForm($entity, $form_mode);
+ }
+
+ CacheableMetadata::createFromRenderArray($build)
+ ->merge(CacheableMetadata::createFromObject($entity))
+ ->merge(CacheableMetadata::createFromObject($access))
+ ->applyTo($build);
+
+ return $build;
+ }
+
+}
diff --git a/src/View/EntityViewBuilder.php b/src/View/EntityViewBuilder.php
new file mode 100644
index 0000000..9395fac
--- /dev/null
+++ b/src/View/EntityViewBuilder.php
@@ -0,0 +1,47 @@
+entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * Builds a render array for a given entity.
+ */
+ public function build(EntityInterface $entity, string $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array {
+ $build = [];
+ $access = $check_access ? $entity->access('view', NULL, TRUE) : AccessResult::allowed();
+ if ($access->isAllowed()) {
+ $build = $this->entityTypeManager
+ ->getViewBuilder($entity->getEntityTypeId())
+ ->view($entity, $view_mode, $langcode);
+ }
+ CacheableMetadata::createFromRenderArray($build)
+ ->merge(CacheableMetadata::createFromObject($entity))
+ ->merge(CacheableMetadata::createFromObject($access))
+ ->applyTo($build);
+ return $build;
+ }
+
+}
diff --git a/src/View/FieldViewBuilder.php b/src/View/FieldViewBuilder.php
new file mode 100644
index 0000000..e259567
--- /dev/null
+++ b/src/View/FieldViewBuilder.php
@@ -0,0 +1,74 @@
+entityRepository = $entity_repository;
+ }
+
+ /**
+ * Returns the render array for a single entity field.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity.
+ * @param string $field_name
+ * The field name.
+ * @param string|array $view_mode
+ * (optional) The view mode that should be used to render the field.
+ * @param string $langcode
+ * (optional) Language code to load translation.
+ * @param bool $check_access
+ * (optional) Indicates that access check is required.
+ *
+ * @return array
+ * A render array for the field.
+ */
+ public function build(
+ EntityInterface $entity,
+ string $field_name,
+ $view_mode = 'full',
+ string $langcode = NULL,
+ bool $check_access = TRUE
+ ): array {
+
+ $build = [];
+
+ $access = $check_access ? $entity->access('view', NULL, TRUE) : AccessResult::allowed();
+ if ($access->isAllowed()) {
+ $entity = $this->entityRepository->getTranslationFromContext($entity, $langcode);
+ if (!isset($entity->{$field_name})) {
+ // @todo Trigger error here.
+ return [];
+ }
+ $build = $entity->{$field_name}->view($view_mode);
+ }
+
+ CacheableMetadata::createFromRenderArray($build)
+ ->merge(CacheableMetadata::createFromObject($access))
+ ->merge(CacheableMetadata::createFromObject($entity))
+ ->applyTo($build);
+
+ return $build;
+ }
+
+}
diff --git a/src/View/ImageViewBuilder.php b/src/View/ImageViewBuilder.php
new file mode 100644
index 0000000..64f8326
--- /dev/null
+++ b/src/View/ImageViewBuilder.php
@@ -0,0 +1,61 @@
+access('view', NULL, TRUE) : AccessResult::allowed();
+
+ $build = [];
+ if ($access->isAllowed()) {
+ $build['#uri'] = $file->getFileUri();
+ $build['#attributes'] = $attributes;
+ if ($style) {
+ if ($responsive) {
+ $build['#type'] = 'responsive_image';
+ $build['#responsive_image_style_id'] = $style;
+ }
+ else {
+ $build['#theme'] = 'image_style';
+ $build['#style_name'] = $style;
+ }
+ }
+ else {
+ $build['#theme'] = 'image';
+ }
+ }
+
+ CacheableMetadata::createFromRenderArray($build)
+ ->merge(CacheableMetadata::createFromObject($access))
+ ->applyTo($build);
+
+ return $build;
+ }
+
+}
diff --git a/src/View/MenuViewBuilder.php b/src/View/MenuViewBuilder.php
new file mode 100644
index 0000000..23c4b8f
--- /dev/null
+++ b/src/View/MenuViewBuilder.php
@@ -0,0 +1,70 @@
+menuLinkTree = $menu_link_tree;
+ }
+
+ /**
+ * Returns the render array for a menu.
+ *
+ * @param string $menu_name
+ * The name of the menu.
+ * @param int $level
+ * (optional) Initial menu level.
+ * @param int $depth
+ * (optional) Maximum number of menu levels to display.
+ * @param bool $expand
+ * (optional) Expand all menu links.
+ *
+ * @return array
+ * A render array for the menu.
+ *
+ * @see \Drupal\system\Plugin\Block\SystemMenuBlock::build()
+ */
+ public function build(string $menu_name, int $level = 1, int $depth = 0, bool $expand = FALSE): array {
+ $parameters = $this->menuLinkTree->getCurrentRouteMenuTreeParameters($menu_name);
+
+ // Adjust the menu tree parameters based on the block's configuration.
+ $parameters->setMinDepth($level);
+ // When the depth is configured to zero, there is no depth limit. When depth
+ // is non-zero, it indicates the number of levels that must be displayed.
+ // Hence this is a relative depth that we must convert to an actual
+ // (absolute) depth, that may never exceed the maximum depth.
+ if ($depth > 0) {
+ $parameters->setMaxDepth(min($level + $depth - 1, $this->menuLinkTree->maxDepth()));
+ }
+
+ // If expandedParents is empty, the whole menu tree is built.
+ if ($expand) {
+ $parameters->expandedParents = [];
+ }
+
+ $tree = $this->menuLinkTree->load($menu_name, $parameters);
+ $manipulators = [
+ ['callable' => 'menu.default_tree_manipulators:checkAccess'],
+ ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
+ ];
+ $tree = $this->menuLinkTree->transform($tree, $manipulators);
+ return $this->menuLinkTree->build($tree);
+ }
+
+}
diff --git a/src/View/RegionViewBuilder.php b/src/View/RegionViewBuilder.php
new file mode 100644
index 0000000..d11c0a9
--- /dev/null
+++ b/src/View/RegionViewBuilder.php
@@ -0,0 +1,111 @@
+entityTypeManager = $entity_type_manager;
+ $this->configFactory = $config_factory;
+ $this->requestStack = $request_stack;
+ $this->titleResolver = $title_resolver;
+ }
+
+ /**
+ * Builds the render array of a given region.
+ *
+ * @param string $region
+ * The region to build.
+ * @param string $theme
+ * (optional) The name of the theme to load the region. If it is not
+ * provided then default theme will be used.
+ *
+ * @return array
+ * A render array to display the region content.
+ */
+ public function build(string $region, string $theme = NULL): array {
+
+ $blocks = $this->entityTypeManager->getStorage('block')->loadByProperties([
+ 'region' => $region,
+ 'theme' => $theme ?: $this->configFactory->get('system.theme')->get('default'),
+ ]);
+
+ $view_builder = $this->entityTypeManager->getViewBuilder('block');
+
+ $build = [];
+
+ $cache_metadata = new CacheableMetadata();
+
+ /* @var $blocks \Drupal\block\BlockInterface[] */
+ foreach ($blocks as $id => $block) {
+ $access = $block->access('view', NULL, TRUE);
+ $cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($access));
+ if ($access->isAllowed()) {
+ $block_plugin = $block->getPlugin();
+ if ($block_plugin instanceof TitleBlockPluginInterface) {
+ $request = $this->requestStack->getCurrentRequest();
+ if ($route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) {
+ $block_plugin->setTitle($this->titleResolver->getTitle($request, $route));
+ }
+ }
+ $build[$id] = $view_builder->view($block);
+ }
+ }
+
+ if ($build) {
+ $build['#region'] = $region;
+ $build['#theme_wrappers'] = ['region'];
+ $cache_metadata->applyTo($build);
+ }
+
+ return $build;
+ }
+
+}
diff --git a/tests/src/Functional/TwigTweakTest.php b/tests/src/Functional/TwigTweakTest.php
index 4e4f24e..e85ea68 100644
--- a/tests/src/Functional/TwigTweakTest.php
+++ b/tests/src/Functional/TwigTweakTest.php
@@ -3,14 +3,14 @@
namespace Drupal\Tests\twig_tweak\Functional;
use Drupal\Core\Link;
+use Drupal\Core\Render\Markup;
use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\TestFileCreationTrait;
use Drupal\file\Entity\File;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\media\Entity\Media;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
-use Drupal\Core\Render\Markup;
-use Drupal\Tests\BrowserTestBase;
-use Drupal\Tests\TestFileCreationTrait;
use Drupal\user\Entity\Role;
/**
@@ -18,10 +18,15 @@ use Drupal\user\Entity\Role;
*
* @group twig_tweak
*/
-class TwigTweakTest extends BrowserTestBase {
+final class TwigTweakTest extends BrowserTestBase {
use TestFileCreationTrait;
+ /**
+ * {@inheritdoc}
+ */
+ protected $defaultTheme = 'classy';
+
/**
* {@inheritdoc}
*/
@@ -40,7 +45,7 @@ class TwigTweakTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
- public function setUp() {
+ public function setUp(): void {
parent::setUp();
$test_files = $this->getTestFiles('image');
@@ -88,292 +93,274 @@ class TwigTweakTest extends BrowserTestBase {
'breakpoint_group' => 'responsive_image',
])->save();
- // Setup Russian.
+ // Setup Russian language.
ConfigurableLanguage::createFromLangcode('ru')->save();
}
/**
* Tests output produced by the Twig extension.
*/
- public function testOutput() {
- // Title block rendered through drupal_region() is cached by some reason.
- \Drupal::service('cache_tags.invalidator')->invalidateTags(['block_view']);
- $this->drupalGet('');
+ public function testOutput(): void {
+
+ $this->drupalGet('twig-tweak-test');
- // -- Test default views display.
+ // -- View (default display).
$xpath = '//div[@class = "tt-view-default"]';
$xpath .= '//div[contains(@class, "view-twig-tweak-test") and contains(@class, "view-display-id-default")]';
$xpath .= '/div[@class = "view-content"]//ul[count(./li) = 3]/li';
- $this->assertByXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]');
- $this->assertByXpath($xpath . '//a[contains(@href, "/node/2") and text() = "Beta"]');
- $this->assertByXpath($xpath . '//a[contains(@href, "/node/3") and text() = "Gamma"]');
+ $this->assertXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]');
+ $this->assertXpath($xpath . '//a[contains(@href, "/node/2") and text() = "Beta"]');
+ $this->assertXpath($xpath . '//a[contains(@href, "/node/3") and text() = "Gamma"]');
- // -- Test page_1 view display.
+ // -- View (page_1 display).
$xpath = '//div[@class = "tt-view-page_1"]';
$xpath .= '//div[contains(@class, "view-twig-tweak-test") and contains(@class, "view-display-id-page_1")]';
$xpath .= '/div[@class = "view-content"]//ul[count(./li) = 3]/li';
- $this->assertByXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]');
- $this->assertByXpath($xpath . '//a[contains(@href, "/node/2") and text() = "Beta"]');
- $this->assertByXpath($xpath . '//a[contains(@href, "/node/3") and text() = "Gamma"]');
+ $this->assertXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]');
+ $this->assertXpath($xpath . '//a[contains(@href, "/node/2") and text() = "Beta"]');
+ $this->assertXpath($xpath . '//a[contains(@href, "/node/3") and text() = "Gamma"]');
- // -- Test view argument.
+ // -- View with arguments.
$xpath = '//div[@class = "tt-view-page_1-with-argument"]';
$xpath .= '//div[contains(@class, "view-twig-tweak-test")]';
$xpath .= '/div[@class = "view-content"]//ul[count(./li) = 1]/li';
- $this->assertByXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]');
+ $this->assertXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]');
- // -- Test view result.
+ // -- View result.
$xpath = '//div[@class = "tt-view-result" and text() = 3]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test block.
+ // -- Block.
$xpath = '//div[@class = "tt-block"]';
$xpath .= '/img[contains(@src, "/core/themes/classy/logo.svg") and @alt="Home"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test block with wrapper.
+ // -- Block with wrapper.
$xpath = '//div[@class = "tt-block-with-wrapper"]';
$xpath .= '/div[@class = "block block-system block-system-branding-block"]';
$xpath .= '/h2[text() = "Branding"]';
$xpath .= '/following-sibling::a[img[contains(@src, "/core/themes/classy/logo.svg") and @alt="Home"]]';
$xpath .= '/following-sibling::div[@class = "site-name"]/a';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test region.
+ // -- Region.
$xpath = '//div[@class = "tt-region"]/div[@class = "region region-sidebar-first"]';
- $xpath .= '/div[contains(@class, "block-page-title-block") and h1[@class="page-title" and text() = "Log in"]]';
+ $xpath .= '/div[contains(@class, "block-page-title-block") and h1[@class="page-title" and text() = "Twig Tweak Test"]]';
$xpath .= '/following-sibling::div[contains(@class, "block-system-powered-by-block")]/span[. = "Powered by Drupal"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test entity default view mode.
+ // -- Entity (default view mode).
$xpath = '//div[@class = "tt-entity-default"]';
$xpath .= '/article[contains(@class, "node") and not(contains(@class, "node--view-mode-teaser"))]';
$xpath .= '/h2/a/span[text() = "Alpha"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test entity teaser view mode.
+ // -- Entity (teaser view mode).
$xpath = '//div[@class = "tt-entity-teaser"]';
$xpath .= '/article[contains(@class, "node") and contains(@class, "node--view-mode-teaser")]';
$xpath .= '/h2/a/span[text() = "Alpha"]';
- $this->assertByXpath($xpath);
-
- // -- Test loading entity from URL.
- $xpath = '//div[@class = "tt-entity-from-url" and not(text())]';
- $this->assertByXpath($xpath);
- $this->drupalGet('/node/2');
- $xpath = '//div[@class = "tt-entity-from-url"]';
- $xpath .= '/article[contains(@class, "node")]';
- $xpath .= '/h2/a/span[text() = "Beta"]';
- $this->assertByXpath($xpath);
-
- // -- Test access to entity add form.
+ $this->assertXpath($xpath);
+
+ // -- Entity add form (unprivileged user).
$xpath = '//div[@class = "tt-entity-add-form"]/form';
$this->assertSession()->elementNotExists('xpath', $xpath);
- // -- Test access to entity edit form.
+ // -- Entity edit form (unprivileged user).
$xpath = '//div[@class = "tt-entity-edit-form"]/form';
$this->assertSession()->elementNotExists('xpath', $xpath);
// Grant require permissions and test the forms again.
$permissions = ['create page content', 'edit any page content'];
- $this->grantPermissions(Role::load(Role::ANONYMOUS_ID), $permissions);
- $this->drupalGet('/node/2');
+ /** @var \Drupal\user\RoleInterface $role */
+ $role = Role::load(Role::ANONYMOUS_ID);
+ $this->grantPermissions($role, $permissions);
+ $this->drupalGet($this->getUrl());
- // -- Test entity add form.
+ // -- Entity add form.
$xpath = '//div[@class = "tt-entity-add-form"]/form';
$xpath .= '//input[@name = "title[0][value]" and @value = ""]';
$xpath .= '/../../../div/input[@type = "submit" and @value = "Save"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test entity edit form.
+ // -- Entity edit form.
$xpath = '//div[@class = "tt-entity-edit-form"]/form';
$xpath .= '//input[@name = "title[0][value]" and @value = "Alpha"]';
$xpath .= '/../../../div/input[@type = "submit" and @value = "Save"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test field.
+ // -- Field.
$xpath = '//div[@class = "tt-field"]/div[contains(@class, "field--name-body")]/p[text() != ""]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test menu (default).
+ // -- Menu.
$xpath = '//div[@class = "tt-menu-default"]/ul[@class = "menu"]/li/a[text() = "Link 1"]/../ul[@class = "menu"]/li/ul[@class = "menu"]/li/a[text() = "Link 3"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test menu (level).
+ // -- Menu with level option.
$xpath = '//div[@class = "tt-menu-level"]/ul[@class = "menu"]/li/a[text() = "Link 2"]/../ul[@class = "menu"]/li/a[text() = "Link 3"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test menu (depth).
+ // -- Menu with depth option.
$xpath = '//div[@class = "tt-menu-depth"]/ul[@class = "menu"]/li[not(ul)]/a[text() = "Link 1"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test form.
+ // -- Form.
$xpath = '//div[@class = "tt-form"]/form[@class="system-cron-settings"]/input[@type = "submit" and @value = "Run cron"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test image by FID.
+ // -- Image by FID.
$xpath = '//div[@class = "tt-image-by-fid"]/img[contains(@src, "/files/image-test.png")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test image by URI.
+ // -- Image by URI.
$xpath = '//div[@class = "tt-image-by-uri"]/img[contains(@src, "/files/image-test.png")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test image by UUID.
+ // -- Image by UUID.
$xpath = '//div[@class = "tt-image-by-uuid"]/img[contains(@src, "/files/image-test.png")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test image with style.
+ // -- Image with style.
$xpath = '//div[@class = "tt-image-with-style"]/img[contains(@src, "/files/styles/thumbnail/public/image-test.png")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test image with responsive style.
+ // -- Image with responsive style.
$xpath = '//div[@class = "tt-image-with-responsive-style"]/picture/img[contains(@src, "/files/image-test.png")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test token.
+ // -- Token.
$xpath = '//div[@class = "tt-token" and text() = "Drupal"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test token with context.
- $xpath = '//div[@class = "tt-token-data" and text() = "Beta"]';
- $this->assertByXpath($xpath);
+ // -- Token with context.
+ $xpath = '//div[@class = "tt-token-data" and text() = "Alpha"]';
+ $this->assertXpath($xpath);
- // -- Test config.
+ // -- Config.
$xpath = '//div[@class = "tt-config" and text() = "Anonymous"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test page title.
- $xpath = '//div[@class = "tt-title" and text() = "Beta"]';
- $this->assertByXpath($xpath);
+ // -- Page title.
+ $xpath = '//div[@class = "tt-title" and text() = "Twig Tweak Test"]';
+ $this->assertXpath($xpath);
- // -- Test URL.
+ // -- URL.
$url = Url::fromUserInput('/node/1', ['absolute' => TRUE])->toString();
$xpath = sprintf('//div[@class = "tt-url"]/div[@data-case="default" and text() = "%s"]', $url);
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test URL (with langcode).
+ // -- URL with langcode.
$url = str_replace('node/1', 'ru/node/1', $url);
$xpath = sprintf('//div[@class = "tt-url"]/div[@data-case="with-langcode" and text() = "%s"]', $url);
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test link.
+ // -- Link.
$url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]);
$link = Link::fromTextAndUrl('Edit', $url)->toString();
$xpath = '//div[@class = "tt-link"]';
self::assertEquals($link, trim($this->xpath($xpath)[0]->getHtml()));
- // -- Test link with HTML.
+ // -- Link with HTML.
$text = Markup::create('Edit');
$url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]);
$link = Link::fromTextAndUrl($text, $url)->toString();
$xpath = '//div[@class = "tt-link-html"]';
self::assertEquals($link, trim($this->xpath($xpath)[0]->getHtml()));
- // -- Test status messages.
+ // -- Status messages.
$xpath = '//div[@class = "tt-messages"]//div[contains(@class, "messages--status") and contains(., "Hello world!")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test breadcrumb.
+ // -- Breadcrumb.
$xpath = '//div[@class = "tt-breadcrumb"]/nav[@class = "breadcrumb"]/ol/li/a[text() = "Home"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test protected link.
+ // -- Protected link.
$xpath = '//div[@class = "tt-link-access"]';
self::assertEquals('', trim($this->xpath($xpath)[0]->getHtml()));
- // -- Test token replacement.
+ // -- Token replacement.
$xpath = '//div[@class = "tt-token-replace" and text() = "Site name: Drupal"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test contextual links.
+ // -- Contextual links.
$xpath = '//div[@class="tt-contextual-links" and not(div[@data-contextual-id])]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- $this->grantPermissions(Role::load(Role::ANONYMOUS_ID), ['access contextual links']);
+ /** @var \Drupal\user\RoleInterface $role */
+ $role = Role::load(Role::ANONYMOUS_ID);
+ $this->grantPermissions($role, ['access contextual links']);
$this->drupalGet($this->getUrl());
$xpath = '//div[@class="tt-contextual-links" and div[@data-contextual-id]]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test preg replacement.
+ // -- Replace (preg).
$xpath = '//div[@class = "tt-preg-replace" and text() = "FOO-bar"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test image style.
+ // -- Image style.
$xpath = '//div[@class = "tt-image-style" and contains(text(), "styles/thumbnail/public/images/ocean.jpg")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test transliteration.
+ // -- Transliterate.
$xpath = '//div[@class = "tt-transliterate" and contains(text(), "Privet!")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test text format.
+ // -- Text format.
$xpath = '//div[@class = "tt-check-markup"]';
self::assertEquals('bold strong', trim($this->xpath($xpath)[0]->getHtml()));
- // -- Test truncation.
+ // -- Truncate.
$xpath = '//div[@class = "tt-truncate" and text() = "Hello…"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test 'with'.
+ // -- 'with'.
$xpath = '//div[@class = "tt-with"]/b[text() = "Example"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test nested 'with'.
+ // -- Nested 'with'.
$xpath = '//div[@class = "tt-with-nested" and text() = "{alpha:{beta:{gamma:456}}}"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test 'children'.
+ // -- 'children'.
$xpath = '//div[@class = "tt-children" and text() = "doremi"]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test entity view.
- $xpath = '//div[@class = "tt-node-view"]/article[contains(@class, "node--view-mode-default")]/h2[a/span[text() = "Beta"]]';
+ // -- Entity view.
+ $xpath = '//div[@class = "tt-node-view"]/article[contains(@class, "node--view-mode-default")]/h2[a/span[text() = "Alpha"]]';
$xpath .= '/following-sibling::div[@class = "node__content"]/div/p';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test Field list view.
- $xpath = '//div[@class = "tt-field-list-view"]/span[contains(@class, "field--name-title") and text() = "Beta"]';
- $this->assertByXpath($xpath);
+ // -- Field list view.
+ $xpath = '//div[@class = "tt-field-list-view"]/span[contains(@class, "field--name-title") and text() = "Alpha"]';
+ $this->assertXpath($xpath);
- // -- Test field item view.
- $xpath = '//div[@class = "tt-field-item-view" and text() = "Beta"]';
- $this->assertByXpath($xpath);
+ // -- Field item view.
+ $xpath = '//div[@class = "tt-field-item-view" and text() = "Alpha"]';
+ $this->assertXpath($xpath);
- // -- Test file URL from URI.
+ // -- File URL from URI.
$xpath = '//div[@class = "tt-file-url-from-uri" and contains(text(), "/files/image-test.png")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test file URL from image field.
- $this->drupalGet('/node/1');
+ // -- File URL from image field.
$xpath = '//div[@class = "tt-file-url-from-image-field" and contains(text(), "/files/image-test.png")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test file URL from a specific image field item.
+ // -- File URL from a specific image field item.
$xpath = '//div[@class = "tt-file-url-from-image-field-delta" and contains(text(), "/files/image-test.png")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
- // -- Test file URL from media field.
+ // -- File URL from media field.
$xpath = '//div[@class = "tt-file-url-from-media-field" and contains(text(), "/files/image-1.png")]';
- $this->assertByXpath($xpath);
+ $this->assertXpath($xpath);
}
/**
* Checks that an element specified by a the xpath exists on the current page.
*/
- public function assertByXpath($xpath) {
+ private function assertXpath(string $xpath): void {
$this->assertSession()->elementExists('xpath', $xpath);
}
- /**
- * {@inheritdoc}
- */
- protected function initFrontPage() {
- // Intentionally empty. The parent implementation does a request to the
- // front page to init cookie. This causes some troubles in rendering
- // attached Twig template because page content type is not created at that
- // moment. We can skip this step since this test does not rely on any
- // session data.
- }
-
}
diff --git a/tests/src/Kernel/AccessTest.php b/tests/src/Kernel/AccessTest.php
deleted file mode 100644
index ee8d547..0000000
--- a/tests/src/Kernel/AccessTest.php
+++ /dev/null
@@ -1,598 +0,0 @@
-installEntitySchema('node');
- $this->installEntitySchema('user');
- $this->installConfig(['system']);
-
- $node_type = NodeType::create([
- 'type' => 'article',
- 'name' => 'Article',
- ]);
- $node_type->save();
-
- $values = [
- 'type' => 'article',
- 'status' => NodeInterface::PUBLISHED,
- // @see twig_tweak_test_node_access()
- 'title' => 'Entity access test',
- ];
- $this->node = Node::create($values);
- $this->node->save();
-
- $this->twigExtension = $this->container->get('twig_tweak.twig_extension');
- }
-
- /**
- * Test callback.
- */
- public function testDrupalEntity() {
-
- // -- Unprivileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 1']);
-
- $build = $this->twigExtension->drupalEntity('node', $this->node->id());
- self::assertArrayNotHasKey('#node', $build);
- $expected_cache = [
- 'contexts' => ['user.permissions'],
- 'tags' => ['node:1'],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Unprivileged user without access check.
- $build = $this->twigExtension->drupalEntity('node', $this->node->id(), NULL, NULL, FALSE);
- self::assertArrayHasKey('#node', $build);
- $expected_cache = [
- 'tags' => [
- 'node:1',
- 'node_view',
- ],
- 'contexts' => [],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 2'], ['access content']);
-
- $build = $this->twigExtension->drupalEntity('node', $this->node->id());
- self::assertArrayHasKey('#node', $build);
- $expected_cache = [
- 'tags' => [
- 'node:1',
- 'node_view',
- 'tag_from_twig_tweak_test_node_access',
- ],
- 'contexts' => [
- 'user',
- 'user.permissions',
- ],
- 'max-age' => 50,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user without access check.
- $build = $this->twigExtension->drupalEntity('node', $this->node->id(), NULL, NULL, FALSE);
- self::assertArrayHasKey('#node', $build);
- $expected_cache = [
- 'tags' => [
- 'node:1',
- 'node_view',
- ],
- 'contexts' => [],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
- }
-
- /**
- * Test callback.
- */
- public function testDrupalField() {
-
- // -- Unprivileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 1']);
-
- $build = $this->twigExtension->drupalField('title', 'node', $this->node->id());
- self::assertArrayNotHasKey('#items', $build);
- $expected_cache = [
- 'contexts' => ['user.permissions'],
- 'tags' => ['node:1'],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Unprivileged user without access check.
- $build = $this->twigExtension->drupalField('title', 'node', $this->node->id(), 'default', NULL, FALSE);
- self::assertArrayHasKey('#items', $build);
- $expected_cache = [
- 'contexts' => [],
- 'tags' => ['node:1'],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 2'], ['access content']);
-
- $build = $this->twigExtension->drupalField('title', 'node', $this->node->id());
- self::assertArrayHasKey('#items', $build);
- $expected_cache = [
- 'contexts' => [
- 'user',
- 'user.permissions',
- ],
- 'tags' => [
- 'node:1',
- 'tag_from_twig_tweak_test_node_access',
- ],
- 'max-age' => 50,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user without access check.
- $build = $this->twigExtension->drupalField('title', 'node', $this->node->id(), 'default', NULL, FALSE);
- self::assertArrayHasKey('#items', $build);
- $expected_cache = [
- 'contexts' => [],
- 'tags' => ['node:1'],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
- }
-
- /**
- * Test callback.
- */
- public function testDrupalEntityEditForm() {
-
- // -- Unprivileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 1']);
-
- $build = $this->twigExtension->drupalEntityForm('node', $this->node->id());
- self::assertArrayNotHasKey('form_id', $build);
- $expected_cache = [
- 'contexts' => ['user.permissions'],
- 'tags' => ['node:1'],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Unprivileged user without access check.
- $build = $this->twigExtension->drupalEntityForm('node', $this->node->id(), 'default', [], FALSE);
- self::assertArrayHasKey('form_id', $build);
- $expected_cache = [
- 'contexts' => ['user.roles:authenticated'],
- 'tags' => [
- 'config:core.entity_form_display.node.article.default',
- 'node:1',
- ],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 2'], ['access content']);
-
- $build = $this->twigExtension->drupalEntityForm('node', $this->node->id());
- self::assertArrayHasKey('#form_id', $build);
- $expected_cache = [
- 'contexts' => [
- 'user',
- 'user.permissions',
- 'user.roles:authenticated',
- ],
- 'tags' => [
- 'config:core.entity_form_display.node.article.default',
- 'node:1',
- 'tag_from_twig_tweak_test_node_access',
- ],
- 'max-age' => 50,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user without access check.
- $build = $this->twigExtension->drupalEntityForm('node', $this->node->id(), 'default', [], FALSE);
- self::assertArrayHasKey('#form_id', $build);
- $expected_cache = [
- 'contexts' => ['user.roles:authenticated'],
- 'tags' => [
- 'config:core.entity_form_display.node.article.default',
- 'node:1',
- ],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
- }
-
- /**
- * Test callback.
- */
- public function testDrupalEntityAddForm() {
-
- $node_values = ['type' => 'article'];
-
- // -- Unprivileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 1']);
-
- $build = $this->twigExtension->drupalEntityForm('node', NULL, 'default', $node_values);
- self::assertArrayNotHasKey('form_id', $build);
- $expected_cache = [
- 'contexts' => ['user.permissions'],
- 'tags' => [],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Unprivileged user without access check.
- $build = $this->twigExtension->drupalEntityForm('node', NULL, 'default', $node_values, FALSE);
- self::assertArrayHasKey('form_id', $build);
- $expected_cache = [
- 'contexts' => ['user.roles:authenticated'],
- 'tags' => ['config:core.entity_form_display.node.article.default'],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 2'], ['access content', 'create article content']);
-
- $build = $this->twigExtension->drupalEntityForm('node', NULL, 'default', $node_values);
- self::assertArrayHasKey('form_id', $build);
- $expected_cache = [
- 'contexts' => [
- 'user.permissions',
- 'user.roles:authenticated',
- ],
- 'tags' => ['config:core.entity_form_display.node.article.default'],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user without access check.
- $build = $this->twigExtension->drupalEntityForm('node', NULL, 'default', $node_values);
- self::assertArrayHasKey('form_id', $build);
- $expected_cache = [
- 'contexts' => [
- 'user.permissions',
- 'user.roles:authenticated',
- ],
- 'tags' => ['config:core.entity_form_display.node.article.default'],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
- }
-
- /**
- * Test callback.
- *
- * @see \Drupal\twig_tweak_test\Plugin\Block\FooBlock
- */
- public function testDrupalBlock() {
-
- // -- Privileged user.
- $this->setUpCurrentUser(['name' => 'User 1']);
-
- $build = $this->twigExtension->drupalBlock('twig_tweak_test_foo');
- $expected_content = [
- '#markup' => 'Foo',
- '#cache' => [
- 'contexts' => ['url'],
- 'tags' => ['tag_from_build'],
- ],
- ];
- self::assertSame($expected_content, $build['content']);
- $expected_cache = [
- 'contexts' => ['user'],
- 'tags' => ['tag_from_blockAccess'],
- 'max-age' => 35,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Unprivileged user.
- $this->setUpCurrentUser(['name' => 'User 2']);
-
- $build = $this->twigExtension->drupalBlock('twig_tweak_test_foo');
- self::assertArrayNotHasKey('content', $build);
- $expected_cache = [
- 'contexts' => ['user'],
- 'tags' => ['tag_from_blockAccess'],
- 'max-age' => 35,
- ];
- self::assertSame($expected_cache, $build['#cache']);
- }
-
- /**
- * Test callback.
- */
- public function testDrupalRegion() {
-
- // @codingStandardsIgnoreStart
- $create_block = function ($id) {
- return new class(['id' => $id], 'block') extends Block {
- public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
- $result = AccessResult::allowedIf($this->id == 'block_1');
- $result->cachePerUser();
- $result->addCacheTags(['tag_for_' . $this->id]);
- $result->setCacheMaxAge(123);
- return $return_as_object ? $result : $result->isAllowed();
- }
- public function getPlugin() {
- return NULL;
- }
- };
- };
- // @codingStandardsIgnoreEnd
-
- $storage = $this->createMock(EntityStorageInterface::class);
- $blocks = [
- 'block_1' => $create_block('block_1'),
- 'block_2' => $create_block('block_2'),
- ];
- $storage->expects($this->any())
- ->method('loadByProperties')
- ->willReturn($blocks);
-
- $view_builder = $this->createMock(BlockViewBuilder::class);
- $content = [
- '#markup' => 'foo',
- '#cache' => [
- 'tags' => ['tag_from_view'],
- ],
- ];
- $view_builder->expects($this->any())
- ->method('view')
- ->willReturn($content);
-
- $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
- $entity_type_manager->expects($this->any())
- ->method('getStorage')
- ->willReturn($storage);
- $entity_type_manager->expects($this->any())
- ->method('getViewBuilder')
- ->willReturn($view_builder);
-
- $this->container->set('entity_type.manager', $entity_type_manager);
-
- $build = $this->twigExtension->drupalRegion('bar');
- $expected_build = [
- 'block_1' => [
- '#markup' => 'foo',
- '#cache' => [
- 'tags' => ['tag_from_view'],
- ],
- ],
- '#region' => 'bar',
- '#theme_wrappers' => ['region'],
- '#cache' => [
- 'contexts' => ['user'],
- 'tags' => [
- 'tag_for_block_1',
- 'tag_for_block_2',
- ],
- 'max-age' => 123,
- ],
- ];
- self::assertSame($expected_build, $build);
- }
-
- /**
- * Test callback.
- */
- public function testDrupalImage() {
-
- // @codingStandardsIgnoreStart
- $create_image = function ($uri) {
- $file = new class(['uri' => $uri], 'file') extends File {
- public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
- $is_public = parse_url($this->getFileUri(), PHP_URL_SCHEME) == 'public';
- $result = AccessResult::allowedIf($is_public);
- $result->cachePerUser();
- $result->addCacheTags(['tag_for_' . $this->getFileUri()]);
- $result->setCacheMaxAge(123);
- return $return_as_object ? $result : $result->isAllowed();
- }
- public function getPlugin() {
- return NULL;
- }
- };
- $file->setFileUri($uri);
- return $file;
- };
- // @codingStandardsIgnoreEnd
-
- $storage = $this->createMock(EntityStorageInterface::class);
- $map = [
- [
- ['uri' => 'public://ocean.jpg'],
- [$create_image('public://ocean.jpg')],
- ],
- [
- ['uri' => 'private://sea.jpg'],
- [$create_image('private://sea.jpg')],
- ],
- ];
- $storage->method('loadByProperties')
- ->will($this->returnValueMap($map));
-
- $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
- $entity_type_manager->method('getStorage')->willReturn($storage);
-
- $this->container->set('entity_type.manager', $entity_type_manager);
-
- // -- Public image with access check.
- $build = $this->twigExtension->drupalImage('public://ocean.jpg');
- $expected_build = [
- '#uri' => 'public://ocean.jpg',
- '#attributes' => [],
- '#theme' => 'image',
- '#cache' => [
- 'contexts' => ['user'],
- 'tags' => ['tag_for_public://ocean.jpg'],
- 'max-age' => 123,
- ],
- ];
- self::assertSame($expected_build, $build);
-
- // -- Public image without access check.
- $build = $this->twigExtension->drupalImage('public://ocean.jpg', NULL, [], NULL, FALSE);
- $expected_build = [
- '#uri' => 'public://ocean.jpg',
- '#attributes' => [],
- '#theme' => 'image',
- '#cache' => [
- 'contexts' => [],
- 'tags' => [],
- 'max-age' => Cache::PERMANENT,
- ],
- ];
- self::assertSame($expected_build, $build);
-
- // -- Private image with access check.
- $build = $this->twigExtension->drupalImage('private://sea.jpg');
- $expected_build = [
- '#cache' => [
- 'contexts' => ['user'],
- 'tags' => ['tag_for_private://sea.jpg'],
- 'max-age' => 123,
- ],
- ];
- self::assertSame($expected_build, $build);
-
- // -- Private image without access check.
- $build = $this->twigExtension->drupalImage('private://sea.jpg', NULL, [], NULL, FALSE);
- $expected_build = [
- '#uri' => 'private://sea.jpg',
- '#attributes' => [],
- '#theme' => 'image',
- '#cache' => [
- 'contexts' => [],
- 'tags' => [],
- 'max-age' => Cache::PERMANENT,
- ],
- ];
- self::assertSame($expected_build, $build);
- }
-
- /**
- * Test callback.
- */
- public function testView() {
-
- // -- Unprivileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 1']);
-
- $build = $this->twigExtension->view($this->node);
- self::assertArrayNotHasKey('#node', $build);
- $expected_cache = [
- 'contexts' => ['user.permissions'],
- 'tags' => ['node:1'],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Unprivileged user without access check.
- $build = $this->twigExtension->view($this->node, NULL, NULL, FALSE);
- self::assertArrayHasKey('#node', $build);
- $expected_cache = [
- 'tags' => [
- 'node:1',
- 'node_view',
- ],
- 'contexts' => [],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user with access check.
- $this->setUpCurrentUser(['name' => 'User 2'], ['access content']);
-
- $build = $this->twigExtension->view($this->node, NULL);
- self::assertArrayHasKey('#node', $build);
- $expected_cache = [
- 'tags' => [
- 'node:1',
- 'node_view',
- 'tag_from_twig_tweak_test_node_access',
- ],
- 'contexts' => [
- 'user',
- 'user.permissions',
- ],
- 'max-age' => 50,
- ];
- self::assertSame($expected_cache, $build['#cache']);
-
- // -- Privileged user without access check.
- $build = $this->twigExtension->view($this->node, NULL, NULL, FALSE);
- self::assertArrayHasKey('#node', $build);
- $expected_cache = [
- 'tags' => [
- 'node:1',
- 'node_view',
- ],
- 'contexts' => [],
- 'max-age' => Cache::PERMANENT,
- ];
- self::assertSame($expected_cache, $build['#cache']);
- }
-
-}
diff --git a/tests/src/Kernel/BlockViewBuilderTest.php b/tests/src/Kernel/BlockViewBuilderTest.php
new file mode 100644
index 0000000..e075075
--- /dev/null
+++ b/tests/src/Kernel/BlockViewBuilderTest.php
@@ -0,0 +1,119 @@
+container->get('twig_tweak.block_view_builder');
+
+ // -- Default output.
+ $this->setUpCurrentUser(['name' => 'User 1']);
+ $build = $view_builder->build('twig_tweak_test_foo');
+ $expected_build = [
+ 'content' => [
+ '#markup' => 'Foo',
+ '#cache' => [
+ 'contexts' => ['url'],
+ 'tags' => ['tag_from_build'],
+ ],
+ ],
+ '#theme' => 'block',
+ '#attributes' => [],
+ '#contextual_links' => [],
+ '#configuration' => [
+ 'id' => 'twig_tweak_test_foo',
+ 'label' => '',
+ 'provider' => 'twig_tweak_test',
+ 'label_display' => 'visible',
+ 'content' => 'Foo',
+ ],
+ '#plugin_id' => 'twig_tweak_test_foo',
+ '#base_plugin_id' => 'twig_tweak_test_foo',
+ '#derivative_plugin_id' => NULL,
+ '#cache' => [
+ 'contexts' => ['user'],
+ 'tags' => ['tag_from_blockAccess'],
+ 'max-age' => 35,
+ ],
+ ];
+ self::assertSame($expected_build, $build);
+ self::assertSame('Foo
', $this->renderPlain($build));
+
+ // -- Non-default configuration.
+ $build = $view_builder->build('twig_tweak_test_foo', ['content' => 'Bar', 'label' => 'Example']);
+ $expected_build['content']['#markup'] = 'Bar';
+ $expected_build['#configuration']['label'] = 'Example';
+ $expected_build['#configuration']['content'] = 'Bar';
+ self::assertSame($expected_build, $build);
+ self::assertSame('Example
Bar', $this->renderPlain($build));
+
+ // -- Without wrapper.
+ $build = $view_builder->build('twig_tweak_test_foo', [], FALSE);
+ $expected_build = [
+ 'content' => [
+ '#markup' => 'Foo',
+ '#cache' => [
+ 'contexts' => ['url'],
+ 'tags' => ['tag_from_build'],
+ ],
+ ],
+ '#cache' => [
+ 'contexts' => ['user'],
+ 'tags' => ['tag_from_blockAccess'],
+ 'max-age' => 35,
+ ],
+ ];
+ self::assertSame($expected_build, $build);
+ self::assertSame('Foo', $this->renderPlain($build));
+
+ // -- Unprivileged user.
+ $this->setUpCurrentUser(['name' => 'User 2']);
+ $build = $view_builder->build('twig_tweak_test_foo');
+ $expected_build = [
+ '#cache' => [
+ 'contexts' => ['user'],
+ 'tags' => ['tag_from_blockAccess'],
+ 'max-age' => 35,
+ ],
+ ];
+ self::assertSame($expected_build, $build);
+ self::assertSame('', $this->renderPlain($build));
+ }
+
+ /**
+ * Renders a render array.
+ */
+ private function renderPlain(array $build): string {
+ $renderer = $this->container->get('renderer');
+ return rtrim(preg_replace('#\s{2,}#', '', $renderer->renderPlain($build)));
+ }
+
+}
diff --git a/tests/src/Kernel/EntityFormViewBuilderTest.php b/tests/src/Kernel/EntityFormViewBuilderTest.php
new file mode 100644
index 0000000..1df47bb
--- /dev/null
+++ b/tests/src/Kernel/EntityFormViewBuilderTest.php
@@ -0,0 +1,128 @@
+installConfig(['system']);
+ $this->installEntitySchema('node');
+ NodeType::create(['type' => 'article'])->save();
+ $this->setUpCurrentUser(['name' => 'User 1'], ['edit any article content', 'access content']);
+ }
+
+ /**
+ * Test callback.
+ */
+ public function testEntityViewBuilder(): void {
+
+ $view_builder = $this->container->get('twig_tweak.entity_form_view_builder');
+
+ $values = [
+ 'type' => 'article',
+ 'title' => 'Public node',
+ ];
+ $public_node = Node::create($values);
+ $public_node->save();
+
+ $values = [
+ 'type' => 'article',
+ 'title' => 'Private node',
+ ];
+ $private_node = Node::create($values);
+ $private_node->save();
+
+ // -- Default mode.
+ $build = $view_builder->build($public_node);
+
+ self::assertArrayHasKey('#form_id', $build);
+ $expected_cache = [
+ 'contexts' => [
+ 'user',
+ 'user.permissions',
+ 'user.roles:authenticated',
+ ],
+ 'tags' => [
+ 'config:core.entity_form_display.node.article.default',
+ 'node:1',
+ 'tag_from_twig_tweak_test_node_access',
+ ],
+ 'max-age' => 50,
+ ];
+ self::assertSame($expected_cache, $build['#cache']);
+ self::assertContains('