Commit 76608ff7 authored by alexpott's avatar alexpott

Issue #2289999 by dawehner, Cottser | Fabianx: Add an easy way to create HTML...

Issue #2289999 by dawehner, Cottser | Fabianx: Add an easy way to create HTML on the fly without having to create a theme function / template.
parent 4338c8bb
...@@ -1030,6 +1030,25 @@ function theme_disable($theme_list) { ...@@ -1030,6 +1030,25 @@ function theme_disable($theme_list) {
return \Drupal::service('theme_handler')->disable($theme_list); return \Drupal::service('theme_handler')->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_inline_template(&$element) {
/** @var \Drupal\Core\Template\TwigEnvironment $environment */
$environment = \Drupal::service('twig');
$markup = $environment->renderInline($element['#template'], $element['#context']);
$element['#markup'] = $markup;
return $element;
}
/** /**
* @addtogroup themeable * @addtogroup themeable
* @{ * @{
......
...@@ -30,6 +30,13 @@ class TwigEnvironment extends \Twig_Environment { ...@@ -30,6 +30,13 @@ class TwigEnvironment extends \Twig_Environment {
*/ */
protected $templateClasses; 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 * Constructs a TwigEnvironment object and stores cache and storage
* internally. * internally.
...@@ -38,6 +45,10 @@ public function __construct(\Twig_LoaderInterface $loader = NULL, $options = arr ...@@ -38,6 +45,10 @@ public function __construct(\Twig_LoaderInterface $loader = NULL, $options = arr
// @todo Pass as arguments from the DIC. // @todo Pass as arguments from the DIC.
$this->cache_object = \Drupal::cache(); $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 DRUPAL_ROOT . '/core/themes/engines/twig/twig.engine';
// Set twig path namespace for themes and modules. // Set twig path namespace for themes and modules.
$namespaces = array(); $namespaces = array();
foreach ($module_handler->getModuleList() as $name => $extension) { foreach ($module_handler->getModuleList() as $name => $extension) {
...@@ -55,6 +66,7 @@ public function __construct(\Twig_LoaderInterface $loader = NULL, $options = arr ...@@ -55,6 +66,7 @@ public function __construct(\Twig_LoaderInterface $loader = NULL, $options = arr
} }
$this->templateClasses = array(); $this->templateClasses = array();
$this->stringLoader = new \Twig_Loader_String();
parent::__construct($loader, $options); parent::__construct($loader, $options);
} }
...@@ -62,18 +74,21 @@ public function __construct(\Twig_LoaderInterface $loader = NULL, $options = arr ...@@ -62,18 +74,21 @@ public function __construct(\Twig_LoaderInterface $loader = NULL, $options = arr
/** /**
* Checks if the compiled template needs an update. * Checks if the compiled template needs an update.
*/ */
public function needsUpdate($cache_filename, $name) { protected function isFresh($cache_filename, $name) {
$cid = 'twig:' . $cache_filename; $cid = 'twig:' . $cache_filename;
$obj = $this->cache_object->get($cid); $obj = $this->cache_object->get($cid);
$mtime = isset($obj->data) ? $obj->data : FALSE; $mtime = isset($obj->data) ? $obj->data : FALSE;
return $mtime !== FALSE && !$this->isTemplateFresh($name, $mtime); return $mtime === FALSE || $this->isTemplateFresh($name, $mtime);
} }
/** /**
* Compile the source and write the compiled template to disk. * 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) { public function updateCompiledTemplate($cache_filename, $name, $inline = FALSE) {
$source = $this->loader->getSource($name); $source = $this->getLoader($inline)->getSource($name);
$compiled_source = $this->compileSource($source, $name); $compiled_source = $this->compileSource($source, $name);
$this->storage()->save($cache_filename, $compiled_source); $this->storage()->save($cache_filename, $compiled_source);
// Save the last modification time // Save the last modification time
...@@ -81,6 +96,22 @@ public function updateCompiledTemplate($cache_filename, $name) { ...@@ -81,6 +96,22 @@ public function updateCompiledTemplate($cache_filename, $name) {
$this->cache_object->set($cid, REQUEST_TIME); $this->cache_object->set($cid, REQUEST_TIME);
} }
/**
* 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(). * Implements Twig_Environment::loadTemplate().
* *
...@@ -88,9 +119,24 @@ public function updateCompiledTemplate($cache_filename, $name) { ...@@ -88,9 +119,24 @@ public function updateCompiledTemplate($cache_filename, $name) {
* *
* This is a straight copy from loadTemplate() changed to use * This is a straight copy from loadTemplate() changed to use
* drupal_php_storage(). * 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) { public function loadTemplate($name, $index = NULL, $inline = FALSE) {
$cls = $this->getTemplateClass($name, $index); $cls = $this->getTemplateClass($name, $index, $inline);
if (isset($this->loadedTemplates[$cls])) { if (isset($this->loadedTemplates[$cls])) {
return $this->loadedTemplates[$cls]; return $this->loadedTemplates[$cls];
...@@ -100,19 +146,19 @@ public function loadTemplate($name, $index = NULL) { ...@@ -100,19 +146,19 @@ public function loadTemplate($name, $index = NULL) {
$cache_filename = $this->getCacheFilename($name); $cache_filename = $this->getCacheFilename($name);
if ($cache_filename === FALSE) { if ($cache_filename === FALSE) {
$source = $this->loader->getSource($name); $compiled_source = $this->compileSource($this->getLoader($inline)->getSource($name), $name);
$compiled_source = $this->compileSource($source, $name);
eval('?' . '>' . $compiled_source); eval('?' . '>' . $compiled_source);
} else { }
else {
// If autoreload is on, check that the template has not been // If autoreload is on, check that the template has not been
// modified since the last compilation. // modified since the last compilation.
if ($this->isAutoReload() && $this->needsUpdate($cache_filename, $name)) { if ($this->isAutoReload() && !$this->isFresh($cache_filename, $name)) {
$this->updateCompiledTemplate($cache_filename, $name); $this->updateCompiledTemplate($cache_filename, $name, $inline);
} }
if (!$this->storage()->load($cache_filename)) { if (!$this->storage()->load($cache_filename)) {
$this->updateCompiledTemplate($cache_filename, $name); $this->updateCompiledTemplate($cache_filename, $name, $inline);
$this->storage()->load($cache_filename); $this->storage()->load($cache_filename);
} }
} }
...@@ -138,18 +184,52 @@ protected function storage() { ...@@ -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 // 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 // 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 // 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. // node.html.twig for the output of each node and the same compiled class.
$cache_index = $name . (NULL === $index ? '' : '_' . $index); $cache_index = $name . (NULL === $index ? '' : '_' . $index);
if (!isset($this->templateClasses[$cache_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]; return $this->templateClasses[$cache_index];
} }
/**
* Renders a twig string directly.
*
* Warning: You should use the render element 'inline_template' 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 renderInline($template_string, array $context = array()) {
return $this->loadTemplate($template_string, NULL, TRUE)->render($context);
}
} }
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
namespace Drupal\field_ui; namespace Drupal\field_ui;
use Drupal\Component\Plugin\PluginManagerBase; use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\String; use Drupal\Component\Utility\String;
use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\Display\EntityDisplayInterface; use Drupal\Core\Entity\Display\EntityDisplayInterface;
...@@ -401,14 +400,10 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, Ent ...@@ -401,14 +400,10 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, Ent
$this->alterSettingsSummary($summary, $plugin, $field_definition); $this->alterSettingsSummary($summary, $plugin, $field_definition);
if (!empty($summary)) { if (!empty($summary)) {
$summary_escaped = '';
$separator = '';
foreach ($summary as $summary_item) {
$summary_escaped .= $separator . SafeMarkup::escape($summary_item);
$separator = '<br />';
}
$field_row['settings_summary'] = array( $field_row['settings_summary'] = array(
'#markup' => SafeMarkup::set('<div class="field-plugin-summary">' . $summary_escaped . '</div>'), '#type' => 'inline_template',
'#template' => '<div class="field-plugin-summary">{{ summary|safe_join("<br />") }}</div>',
'#context' => array('summary' => $summary),
'#cell_attributes' => array('class' => array('field-plugin-summary-cell')), '#cell_attributes' => array('class' => array('field-plugin-summary-cell')),
); );
} }
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\Theme\TwigEnvironmentTest.
*/
namespace Drupal\system\Tests\Theme;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the twig environment.
*
* @see \Drupal\Core\Template\TwigEnvironment
* @group Twig
*/
class TwigEnvironmentTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system');
/**
* Tests inline templates.
*/
public function testInlineTemplate() {
/** @var \Drupal\Core\Template\TwigEnvironment $environment */
$environment = \Drupal::service('twig');
$this->assertEqual($environment->renderInline('test-no-context'), 'test-no-context');
$this->assertEqual($environment->renderInline('test-with-context {{ lama }}', array('lama' => 'muuh')), 'test-with-context muuh');
$element = array();
$element['test'] = array(
'#type' => 'inline_template',
'#template' => 'test-with-context {{ lama }}',
'#context' => array('lama' => 'muuh'),
);
$this->assertEqual(drupal_render($element), 'test-with-context muuh');
}
}
...@@ -306,6 +306,11 @@ function system_element_info() { ...@@ -306,6 +306,11 @@ function system_element_info() {
'#theme' => 'page', '#theme' => 'page',
'#title' => '', '#title' => '',
); );
$types['inline_template'] = array(
'#pre_render' => array('drupal_render_inline_template'),
'#template' => '',
'#context' => array(),
);
// By default, we don't want Ajax commands being rendered in the context of an // 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. // 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 // However, modules can set these properties (for example, to provide an HTML
......
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
namespace Drupal\views_ui; namespace Drupal\views_ui;
use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Timer; use Drupal\Component\Utility\Timer;
use Drupal\Component\Utility\Xss; use Drupal\Component\Utility\Xss;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
...@@ -680,8 +679,8 @@ public function renderPreview($display_id, $args = array()) { ...@@ -680,8 +679,8 @@ public function renderPreview($display_id, $args = array()) {
} }
} }
$rows['query'][] = array( $rows['query'][] = array(
SafeMarkup::set('<strong>' . t('Query') . '</strong>'), array('data' => array('#type' => 'inline_template', '#template' => "<strong>{% trans 'Query' %}</strong>")),
SafeMarkup::set('<pre>' . String::checkPlain(strtr($query_string, $quoted)) . '</pre>'), array('data' => array('#type' => 'inline_template', '#template' => '<pre>{{ query }}</pre>', '#context' => array('query' => strtr($query_string, $quoted)))),
); );
if (!empty($this->additionalQueries)) { if (!empty($this->additionalQueries)) {
$queries = '<strong>' . t('These queries were run during view rendering:') . '</strong>'; $queries = '<strong>' . t('These queries were run during view rendering:') . '</strong>';
...@@ -694,14 +693,14 @@ public function renderPreview($display_id, $args = array()) { ...@@ -694,14 +693,14 @@ public function renderPreview($display_id, $args = array()) {
} }
$rows['query'][] = array( $rows['query'][] = array(
SafeMarkup::set('<strong>' . t('Other queries') . '</strong>'), array('data' => array('#type' => 'inline_template', '#template' => "<strong>{% trans 'Other queries' %}</strong>")),
SafeMarkup::set('<pre>' . $queries . '</pre>'), SafeMarkup::set('<pre>' . $queries . '</pre>'),
); );
} }
} }
if ($show_info) { if ($show_info) {
$rows['query'][] = array( $rows['query'][] = array(
SafeMarkup::set('<strong>' . t('Title') . '</strong>'), array('data' => array('#type' => 'inline_template', '#template' => "<strong>{% trans 'Title' %}</strong>")),
Xss::filterAdmin($this->executable->getTitle()), Xss::filterAdmin($this->executable->getTitle()),
); );
if (isset($path)) { if (isset($path)) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment