diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 248c795..cb62241 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1031,6 +1031,25 @@ function theme_disable($theme_list) { } /** + * Renders a twig string directly. + * + * @param string $template_string + * The template string to render with placeholders. + * @param array $context + * An array of parameters to pass to the template. + * + * @return string + * The rendered inline template. + */ +function drupal_render_twig_inline(&$element) { + /** @var \Drupal\Core\Template\TwigEnvironment $environment */ + $environment = \Drupal::service('twig'); + $markup = $environment->renderInlineTemplate($element['#template'], $element['#context']); + $element['#markup'] = $markup; + return $element; +} + +/** * @addtogroup themeable * @{ */ diff --git a/core/lib/Drupal/Core/Template/TwigEnvironment.php b/core/lib/Drupal/Core/Template/TwigEnvironment.php index c671899..f0a8050 100644 --- a/core/lib/Drupal/Core/Template/TwigEnvironment.php +++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php @@ -31,6 +31,13 @@ class TwigEnvironment extends \Twig_Environment { protected $templateClasses; /** + * The string loader implementation used for inline template rendering. + * + * @var \Twig_Loader_String + */ + protected $stringLoader; + + /** * Constructs a TwigEnvironment object and stores cache and storage * internally. */ @@ -38,6 +45,10 @@ public function __construct(\Twig_LoaderInterface $loader = NULL, $options = arr // @todo Pass as arguments from the DIC. $this->cache_object = \Drupal::cache(); + // Ensure that twig.engine is loaded, given that it is needed to render a + // template because functions like twig_drupal_escape_filter are called. + require_once 'core/themes/engines/twig/twig.engine'; + // Set twig path namespace for themes and modules. $namespaces = array(); foreach ($module_handler->getModuleList() as $name => $extension) { @@ -55,6 +66,7 @@ public function __construct(\Twig_LoaderInterface $loader = NULL, $options = arr } $this->templateClasses = array(); + $this->stringLoader = new \Twig_Loader_String(); parent::__construct($loader, $options); } @@ -62,18 +74,21 @@ public function __construct(\Twig_LoaderInterface $loader = NULL, $options = arr /** * Checks if the compiled template needs an update. */ - public function needsUpdate($cache_filename, $name) { - $cid = 'twig:' . $cache_filename; - $obj = $this->cache_object->get($cid); - $mtime = isset($obj->data) ? $obj->data : FALSE; - return $mtime !== FALSE && !$this->isTemplateFresh($name, $mtime); + protected function isFresh($cache_filename, $name) { + $cid = 'twig:' . $cache_filename; + $obj = $this->cache_object->get($cid); + $mtime = isset($obj->data) ? $obj->data : FALSE; + return $mtime === FALSE || $this->isTemplateFresh($name, $mtime); } /** * Compile the source and write the compiled template to disk. + * + * @param bool $inline + * TRUE, if the $cache_filename is a rendered template. */ - public function updateCompiledTemplate($cache_filename, $name) { - $source = $this->loader->getSource($name); + public function updateCompiledTemplate($cache_filename, $name, $inline = FALSE) { + $source = $this->getLoader($inline)->getSource($name); $compiled_source = $this->compileSource($source, $name); $this->storage()->save($cache_filename, $compiled_source); // Save the last modification time @@ -82,15 +97,46 @@ public function updateCompiledTemplate($cache_filename, $name) { } /** + * Gets the Loader instance. + * + * @param bool $inline + * TRUE, if the string loader is requested. + * + * @return \Twig_LoaderInterface + * A Twig_LoaderInterface instance + */ + public function getLoader($inline = FALSE) { + if (!isset($this->loader)) { + throw new \LogicException('You must set a loader first.'); + } + return $inline ? $this->stringLoader : $this->loader; + } + + /** * Implements Twig_Environment::loadTemplate(). * * We need to overwrite this function to integrate with drupal_php_storage(). * * This is a straight copy from loadTemplate() changed to use * drupal_php_storage(). + * + * @param string $name + * The template name or the string which should be rendered as template. + * @param int $index + * The index if it is an embedded template. + * @param bool $inline + * TRUE, if the $name is a rendered template. + * + * @return \Twig_TemplateInterface + * A template instance representing the given template name. + * + * @throws \Twig_Error_Loader + * When the template cannot be found. + * @throws \Twig_Error_Syntax + * When an error occurred during compilation. */ - public function loadTemplate($name, $index = NULL) { - $cls = $this->getTemplateClass($name, $index); + public function loadTemplate($name, $index = NULL, $inline = FALSE) { + $cls = $this->getTemplateClass($name, $index, $inline); if (isset($this->loadedTemplates[$cls])) { return $this->loadedTemplates[$cls]; @@ -100,19 +146,19 @@ public function loadTemplate($name, $index = NULL) { $cache_filename = $this->getCacheFilename($name); if ($cache_filename === FALSE) { - $source = $this->loader->getSource($name); - $compiled_source = $this->compileSource($source, $name); + $compiled_source = $this->compileSource($this->getLoader($inline)->getSource($name), $name); eval('?' . '>' . $compiled_source); - } else { + } + else { // If autoreload is on, check that the template has not been // modified since the last compilation. - if ($this->isAutoReload() && $this->needsUpdate($cache_filename, $name)) { - $this->updateCompiledTemplate($cache_filename, $name); + if ($this->isAutoReload() && !$this->isFresh($cache_filename, $name)) { + $this->updateCompiledTemplate($cache_filename, $name, $inline); } if (!$this->storage()->load($cache_filename)) { - $this->updateCompiledTemplate($cache_filename, $name); + $this->updateCompiledTemplate($cache_filename, $name, $inline); $this->storage()->load($cache_filename); } } @@ -138,18 +184,52 @@ protected function storage() { } /** - * {@inheritdoc} + * Gets the template class associated with the given string. + * + * @param string $name + * The name for which to calculate the template class name. + * @param int $index + * The index if it is an embedded template. + * @param bool $inline + * TRUE, if the $name is a rendered template. + * + * @return string + * The template class name. */ - public function getTemplateClass($name, $index = null) { + public function getTemplateClass($name, $index = NULL, $inline = FALSE) { // We override this method to add caching because it gets called multiple // times when the same template is used more than once. For example, a page // rendering 50 nodes without any node template overrides will use the same // node.html.twig for the output of each node and the same compiled class. $cache_index = $name . (NULL === $index ? '' : '_' . $index); if (!isset($this->templateClasses[$cache_index])) { - $this->templateClasses[$cache_index] = parent::getTemplateClass($name, $index); + $this->templateClasses[$cache_index] = $this->templateClassPrefix . hash('sha256', $this->getLoader($inline)->getCacheKey($name)) . (NULL === $index ? '' : '_' . $index); } return $this->templateClasses[$cache_index]; } + /** + * Renders a twig string directly. + * + * Warning: You should use the render element 'twig_inline' together with + * the #template attribute instead of this method directly. + * On top of that you have to ensure that the template string is not dynamic + * but just an ordinary static php string, because there may be installations + * using read-only PHPStorage that want to generate all possible twig + * templates as part of a build step. So it is important that an automated + * script can find the templates and extract them. This is only possible if + * the template is a regular string. + * + * @param string $template_string + * The template string to render with placeholders. + * @param array $context + * An array of parameters to pass to the template. + * + * @return string + * The rendered inline template. + */ + public function renderInlineTemplate($template_string, array $context = array()) { + return $this->loadTemplate($template_string, NULL, TRUE)->render($context); + } + } diff --git a/core/modules/field_ui/src/DisplayOverviewBase.php b/core/modules/field_ui/src/DisplayOverviewBase.php index f450313..0936bfa 100644 --- a/core/modules/field_ui/src/DisplayOverviewBase.php +++ b/core/modules/field_ui/src/DisplayOverviewBase.php @@ -404,14 +404,10 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, Ent $this->alterSettingsSummary($summary, $plugin, $field_definition); if (!empty($summary)) { - $summary_escaped = ''; - $separator = ''; - foreach ($summary as $summary_item) { - $summary_escaped .= $separator . SafeMarkup::escape($summary_item); - $separator = '
'; - } $field_row['settings_summary'] = array( - '#markup' => SafeMarkup::set('
' . $summary_escaped . '
'), + '#type' => 'twig_inline', + '#template' => '
{{ summary|join("
") }}
', + '#context' => array('summary' => $summary), '#cell_attributes' => array('class' => array('field-plugin-summary-cell')), ); } diff --git a/core/modules/system/src/Tests/Theme/TwigEnvironmentTest.php b/core/modules/system/src/Tests/Theme/TwigEnvironmentTest.php new file mode 100644 index 0000000..c4f5c2c --- /dev/null +++ b/core/modules/system/src/Tests/Theme/TwigEnvironmentTest.php @@ -0,0 +1,46 @@ +assertEqual($environment->renderInlineTemplate('test-no-context'), 'test-no-context'); + $this->assertEqual($environment->renderInlineTemplate('test-with-context {{ lama }}', array('lama' => 'muuh')), 'test-with-context muuh'); + + $element = array(); + $element['test'] = array( + '#type' => 'twig_inline', + '#template' => 'test-with-context {{ lama }}', + '#context' => array('lama' => 'muuh'), + ); + $this->assertEqual(drupal_render($element), 'test-with-context muuh'); + } + +} + diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 117dc49..6483578 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -304,6 +304,11 @@ function system_element_info() { '#theme' => 'page', '#title' => '', ); + $types['twig_inline'] = array( + '#pre_render' => array('drupal_render_twig_inline'), + '#template' => '', + '#context' => array(), + ); // By default, we don't want Ajax commands being rendered in the context of an // HTML page, so we don't provide defaults for #theme or #theme_wrappers. // However, modules can set these properties (for example, to provide an HTML diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index 81f85fe..71f133e 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -679,8 +679,8 @@ public function renderPreview($display_id, $args = array()) { } } $rows['query'][] = array( - SafeMarkup::set('' . t('Query') . ''), - SafeMarkup::set('
' . String::checkPlain(strtr($query_string, $quoted)) . '
'), + array('data' => array('#type' => 'twig_inline', '#template' => ' {% trans "Query" %} ')), + array('data' => array('#type' => 'twig_inline', '#template' => '
 {{ query }} 
', '#context' => array('query' => strtr($query_string, $quoted)))), ); if (!empty($this->additionalQueries)) { $queries = '' . t('These queries were run during view rendering:') . ''; @@ -693,14 +693,14 @@ public function renderPreview($display_id, $args = array()) { } $rows['query'][] = array( - SafeMarkup::set('' . t('Other queries') . ''), + array('data' => array('#type' => 'twig_inline', '#template' => ' {% trans "Other queries" %} ')), SafeMarkup::set('
' . $queries . '
'), ); } } if ($show_info) { $rows['query'][] = array( - SafeMarkup::set('' . t('Title') . ''), + array('data' => array('#type' => 'twig_inline', '#template' => ' {% trans "Title" %} ')), Xss::filterAdmin($this->executable->getTitle()), ); if (isset($path)) {