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) {
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
* @{
......
......@@ -30,6 +30,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 DRUPAL_ROOT . '/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) {
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);
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
......@@ -81,6 +96,22 @@ public function updateCompiledTemplate($cache_filename, $name) {
$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().
*
......@@ -88,9 +119,24 @@ public function updateCompiledTemplate($cache_filename, $name) {
*
* 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 '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 @@
namespace Drupal\field_ui;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\String;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
......@@ -401,14 +400,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 = '<br />';
}
$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')),
);
}
......
<?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() {
'#theme' => 'page',
'#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
// 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
......
......@@ -8,7 +8,6 @@
namespace Drupal\views_ui;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Timer;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Form\FormStateInterface;
......@@ -680,8 +679,8 @@ public function renderPreview($display_id, $args = array()) {
}
}
$rows['query'][] = array(
SafeMarkup::set('<strong>' . t('Query') . '</strong>'),
SafeMarkup::set('<pre>' . String::checkPlain(strtr($query_string, $quoted)) . '</pre>'),
array('data' => array('#type' => 'inline_template', '#template' => "<strong>{% trans 'Query' %}</strong>")),
array('data' => array('#type' => 'inline_template', '#template' => '<pre>{{ query }}</pre>', '#context' => array('query' => strtr($query_string, $quoted)))),
);
if (!empty($this->additionalQueries)) {
$queries = '<strong>' . t('These queries were run during view rendering:') . '</strong>';
......@@ -694,14 +693,14 @@ public function renderPreview($display_id, $args = 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>'),
);
}
}
if ($show_info) {
$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()),
);
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