Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,287 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\Attribute.
*/
namespace Drupal\Core\Template;
use Drupal\Component\Utility\SafeStringInterface;
/**
* Collects, sanitizes, and renders HTML attributes.
*
* To use, optionally pass in an associative array of defined attributes, or
* add attributes using array syntax. For example:
* @code
* $attributes = new Attribute(array('id' => 'socks'));
* $attributes['class'] = array('black-cat', 'white-cat');
* $attributes['class'][] = 'black-white-cat';
* echo '<cat' . $attributes . '>';
* // Produces <cat id="socks" class="black-cat white-cat black-white-cat">
* @endcode
*
* $attributes always prints out all the attributes. For example:
* @code
* $attributes = new Attribute(array('id' => 'socks'));
* $attributes['class'] = array('black-cat', 'white-cat');
* $attributes['class'][] = 'black-white-cat';
* echo '<cat class="cat ' . $attributes['class'] . '"' . $attributes . '>';
* // Produces <cat class="cat black-cat white-cat black-white-cat" id="socks" class="cat black-cat white-cat black-white-cat">
* @endcode
*
* When printing out individual attributes to customize them within a Twig
* template, use the "without" filter to prevent attributes that have already
* been printed from being printed again. For example:
* @code
* <cat class="{{ attributes.class }} my-custom-class"{{ attributes|without('class') }}>
* {# Produces <cat class="cat black-cat white-cat black-white-cat my-custom-class" id="socks"> #}
* @endcode
*
* The attribute keys and values are automatically sanitized for output with
* htmlspecialchars() and the entire attribute string is marked safe for output.
*/
class Attribute implements \ArrayAccess, \IteratorAggregate, SafeStringInterface {
/**
* Stores the attribute data.
*
* @var array
*/
protected $storage = array();
/**
* Constructs a \Drupal\Core\Template\Attribute object.
*
* @param array $attributes
* An associative array of key-value pairs to be converted to attributes.
*/
public function __construct($attributes = array()) {
foreach ($attributes as $name => $value) {
$this->offsetSet($name, $value);
}
}
/**
* Implements ArrayAccess::offsetGet().
*/
public function offsetGet($name) {
if (isset($this->storage[$name])) {
return $this->storage[$name];
}
}
/**
* Implements ArrayAccess::offsetSet().
*/
public function offsetSet($name, $value) {
$this->storage[$name] = $this->createAttributeValue($name, $value);
}
/**
* Creates the different types of attribute values.
*
* @param string $name
* The attribute name.
* @param mixed $value
* The attribute value.
*
* @return \Drupal\Core\Template\AttributeValueBase
* An AttributeValueBase representation of the attribute's value.
*/
protected function createAttributeValue($name, $value) {
// If the value is already an AttributeValueBase object, return it
// straight away.
if ($value instanceOf AttributeValueBase) {
return $value;
}
// An array value or 'class' attribute name are forced to always be an
// AttributeArray value for consistency.
if (is_array($value) || $name == 'class') {
// Cast the value to an array if the value was passed in as a string.
// @todo Decide to fix all the broken instances of class as a string
// in core or cast them.
$value = new AttributeArray($name, (array) $value);
}
elseif (is_bool($value)) {
$value = new AttributeBoolean($name, $value);
}
elseif (!is_object($value)) {
$value = new AttributeString($name, $value);
}
return $value;
}
/**
* Implements ArrayAccess::offsetUnset().
*/
public function offsetUnset($name) {
unset($this->storage[$name]);
}
/**
* Implements ArrayAccess::offsetExists().
*/
public function offsetExists($name) {
return isset($this->storage[$name]);
}
/**
* Adds classes or merges them on to array of existing CSS classes.
*
* @param string|array ...
* CSS classes to add to the class attribute array.
*
* @return $this
*/
public function addClass() {
$args = func_get_args();
if ($args) {
$classes = array();
foreach ($args as $arg) {
// Merge the values passed in from the classes array.
// The argument is cast to an array to support comma separated single
// values or one or more array arguments.
$classes = array_merge($classes, (array) $arg);
}
// Merge if there are values, just add them otherwise.
if (isset($this->storage['class']) && $this->storage['class'] instanceOf AttributeArray) {
// Merge the values passed in from the class value array.
$classes = array_merge($this->storage['class']->value(), $classes);
$this->storage['class']->exchangeArray($classes);
}
else {
$this->offsetSet('class', $classes);
}
}
return $this;
}
/**
* Sets values for an attribute key.
*
* @param string $attribute
* Name of the attribute.
* @param string|array $value
* Value(s) to set for the given attribute key.
*
* @return $this
*/
public function setAttribute($attribute, $value) {
$this->offsetSet($attribute, $value);
return $this;
}
/**
* Removes an attribute from an Attribute object.
*
* @param string|array ...
* Attributes to remove from the attribute array.
*
* @return $this
*/
public function removeAttribute() {
$args = func_get_args();
foreach ($args as $arg) {
// Support arrays or multiple arguments.
if (is_array($arg)) {
foreach ($arg as $value) {
unset($this->storage[$value]);
}
}
else {
unset($this->storage[$arg]);
}
}
return $this;
}
/**
* Removes argument values from array of existing CSS classes.
*
* @param string|array ...
* CSS classes to remove from the class attribute array.
*
* @return $this
*/
public function removeClass() {
// With no class attribute, there is no need to remove.
if (isset($this->storage['class']) && $this->storage['class'] instanceOf AttributeArray) {
$args = func_get_args();
$classes = array();
foreach ($args as $arg) {
// Merge the values passed in from the classes array.
// The argument is cast to an array to support comma separated single
// values or one or more array arguments.
$classes = array_merge($classes, (array) $arg);
}
// Remove the values passed in from the value array. Use array_values() to
// ensure that the array index remains sequential.
$classes = array_values(array_diff($this->storage['class']->value(), $classes));
$this->storage['class']->exchangeArray($classes);
}
return $this;
}
/**
* Checks if the class array has the given CSS class.
*
* @param string $class
* The CSS class to check for.
*
* @return bool
* Returns TRUE if the class exists, or FALSE otherwise.
*/
public function hasClass($class) {
if (isset($this->storage['class']) && $this->storage['class'] instanceOf AttributeArray) {
return in_array($class, $this->storage['class']->value());
}
else {
return FALSE;
}
}
/**
* Implements the magic __toString() method.
*/
public function __toString() {
$return = '';
/** @var \Drupal\Core\Template\AttributeValueBase $value */
foreach ($this->storage as $name => $value) {
$rendered = $value->render();
if ($rendered) {
$return .= ' ' . $rendered;
}
}
return $return;
}
/**
* Implements the magic __clone() method.
*/
public function __clone() {
foreach ($this->storage as $name => $value) {
$this->storage[$name] = clone $value;
}
}
/**
* Implements IteratorAggregate::getIterator().
*/
public function getIterator() {
return new \ArrayIterator($this->storage);
}
/**
* Returns the whole array.
*/
public function storage() {
return $this->storage;
}
}

View file

@ -0,0 +1,104 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\AttributeArray.
*/
namespace Drupal\Core\Template;
/**
* A class that defines a type of Attribute that can be added to as an array.
*
* To use with Attribute, the array must be specified.
* Correct:
* @code
* $attributes = new Attribute();
* $attributes['class'] = array();
* $attributes['class'][] = 'cat';
* @endcode
* Incorrect:
* @code
* $attributes = new Attribute();
* $attributes['class'][] = 'cat';
* @endcode
*
* @see \Drupal\Core\Template\Attribute
*/
class AttributeArray extends AttributeValueBase implements \ArrayAccess, \IteratorAggregate {
/**
* Ensures empty array as a result of array_filter will not print '$name=""'.
*
* @see \Drupal\Core\Template\AttributeArray::__toString()
* @see \Drupal\Core\Template\AttributeValueBase::render()
*/
const RENDER_EMPTY_ATTRIBUTE = FALSE;
/**
* Implements ArrayAccess::offsetGet().
*/
public function offsetGet($offset) {
return $this->value[$offset];
}
/**
* Implements ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
if (isset($offset)) {
$this->value[$offset] = $value;
}
else {
$this->value[] = $value;
}
}
/**
* Implements ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
unset($this->value[$offset]);
}
/**
* Implements ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return isset($this->value[$offset]);
}
/**
* Implements the magic __toString() method.
*/
public function __toString() {
// Filter out any empty values before printing.
$this->value = array_unique(array_filter($this->value));
return htmlspecialchars(implode(' ', $this->value), ENT_QUOTES, 'UTF-8');
}
/**
* Implements IteratorAggregate::getIterator().
*/
public function getIterator() {
return new \ArrayIterator($this->value);
}
/**
* Exchange the array for another one.
*
* @see ArrayObject::exchangeArray
*
* @param array $input
* The array input to replace the internal value.
*
* @return array
* The old array value.
*/
public function exchangeArray($input) {
$old = $this->value;
$this->value = $input;
return $old;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\AttributeBoolean.
*/
namespace Drupal\Core\Template;
/**
* A class that defines a type of boolean HTML attribute.
*
* Boolean HTML attributes are not attributes with values of TRUE/FALSE.
* They are attributes that if they exist in the tag, they are TRUE.
* Examples include selected, disabled, checked, readonly.
*
* To set a boolean attribute on the Attribute class, set it to TRUE.
* @code
* $attributes = new Attribute();
* $attributes['disabled'] = TRUE;
* echo '<select' . $attributes . '/>';
* // produces <select disabled>;
* $attributes['disabled'] = FALSE;
* echo '<select' . $attributes . '/>';
* // produces <select>;
* @endcode
*
* @see \Drupal\Core\Template\Attribute
*/
class AttributeBoolean extends AttributeValueBase {
/**
* Overrides AttributeValueBase::render().
*/
public function render() {
return $this->__toString();
}
/**
* Implements the magic __toString() method.
*/
public function __toString() {
return $this->value === FALSE ? '' : htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\AttributeString.
*/
namespace Drupal\Core\Template;
/**
* A class that represents most standard HTML attributes.
*
* To use with the Attribute class, set the key to be the attribute name
* and the value the attribute value.
* @code
* $attributes = new Attribute(array());
* $attributes['id'] = 'socks';
* $attributes['style'] = 'background-color:white';
* echo '<cat ' . $attributes . '>';
* // Produces: <cat id="socks" style="background-color:white">.
* @endcode
*
* @see \Drupal\Core\Template\Attribute
*/
class AttributeString extends AttributeValueBase {
/**
* Implements the magic __toString() method.
*/
public function __toString() {
return htmlspecialchars($this->value, ENT_QUOTES, 'UTF-8');
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\AttributeValueBase.
*/
namespace Drupal\Core\Template;
/**
* Defines the base class for an attribute type.
*
* @see \Drupal\Core\Template\Attribute
*/
abstract class AttributeValueBase {
/**
* Renders '$name=""' if $value is an empty string.
*
* @see \Drupal\Core\Template\AttributeValueBase::render()
*/
const RENDER_EMPTY_ATTRIBUTE = TRUE;
/**
* The value itself.
*
* @var mixed
*/
protected $value;
/**
* The name of the value.
*
* @var mixed
*/
protected $name;
/**
* Constructs a \Drupal\Core\Template\AttributeValueBase object.
*/
public function __construct($name, $value) {
$this->name = $name;
$this->value = $value;
}
/**
* Returns a string representation of the attribute.
*
* While __toString only returns the value in a string form, render()
* contains the name of the attribute as well.
*
* @return string
* The string representation of the attribute.
*/
public function render() {
$value = (string) $this;
if (isset($this->value) && static::RENDER_EMPTY_ATTRIBUTE || !empty($value)) {
return htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8') . '="' . $value . '"';
}
}
/**
* Returns the raw value.
*/
public function value() {
return $this->value;
}
/**
* Implements the magic __toString() method.
*/
abstract function __toString();
}

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\Loader\FilesystemLoader.
*/
namespace Drupal\Core\Template\Loader;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
/**
* Loads templates from the filesystem.
*
* This loader adds module and theme template paths as namespaces to the Twig
* filesystem loader so that templates can be referenced by namespace, like
* @block/block.html.twig or @mytheme/page.html.twig.
*/
class FilesystemLoader extends \Twig_Loader_Filesystem {
/**
* Constructs a new FilesystemLoader object.
*
* @param string|array $paths
* A path or an array of paths to check for templates.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler service.
*/
public function __construct($paths = array(), ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
parent::__construct($paths);
// Add namespaced paths for modules and themes.
$namespaces = array();
foreach ($module_handler->getModuleList() as $name => $extension) {
$namespaces[$name] = $extension->getPath();
}
foreach ($theme_handler->listInfo() as $name => $extension) {
$namespaces[$name] = $extension->getPath();
}
foreach ($namespaces as $name => $path) {
$this->addPath($path . '/templates', $name);
}
}
/**
* Adds a path where templates are stored.
*
* @param string $path
* A path where to look for templates.
* @param string $namespace
* (optional) A path name.
*/
public function addPath($path, $namespace = self::MAIN_NAMESPACE) {
// Invalidate the cache.
$this->cache = array();
$this->paths[$namespace][] = rtrim($path, '/\\');
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\Loader\StringLoader.
*/
namespace Drupal\Core\Template\Loader;
/**
* Loads string templates, also known as inline templates.
*
* This loader is intended to be used in a Twig loader chain and whitelists
* string templates that begin with the following comment:
* @code
* {# inline_template_start #}
* @endcode
*
* This class override ensures that the string loader behaves as expected in
* the loader chain. If Twig's string loader is used as is, any string (even a
* reference to a file-based Twig template) is treated as a valid template and
* is rendered instead of a \Twig_Error_Loader exception being thrown.
*
* @see \Drupal\Core\Template\TwigEnvironment::renderInline()
* @see \Drupal\Core\Render\Element\InlineTemplate
* @see twig_render_template()
*/
class StringLoader extends \Twig_Loader_String {
/**
* {@inheritdoc}
*/
public function exists($name) {
if (strpos($name, '{# inline_template_start #}') === 0) {
return TRUE;
}
else {
return FALSE;
}
}
}

View file

@ -0,0 +1,69 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\Loader\ThemeRegistryLoader.
*/
namespace Drupal\Core\Template\Loader;
use Drupal\Core\Theme\Registry;
/**
* Loads templates based on information from the Drupal theme registry.
*
* Allows for template inheritance based on the currently active template.
*/
class ThemeRegistryLoader extends \Twig_Loader_Filesystem {
/**
* The theme registry used to determine which template to use.
*
* @var \Drupal\Core\Theme\Registry
*/
protected $themeRegistry;
/**
* Constructs a new ThemeRegistryLoader object.
*
* @param \Drupal\Core\Theme\Registry $theme_registry
* The theme registry.
*/
public function __construct(Registry $theme_registry) {
$this->themeRegistry = $theme_registry;
}
/**
* Finds the path to the requested template.
*
* @param string $name
* The name of the template to load.
*
* @return string
* The path to the template.
*
* @throws \Twig_Error_Loader
* Thrown if a template matching $name cannot be found.
*/
protected function findTemplate($name) {
// Allow for loading based on the Drupal theme registry.
$hook = str_replace('.html.twig', '', strtr($name, '-', '_'));
$theme_registry = $this->themeRegistry->getRuntime();
if ($theme_registry->has($hook)) {
$info = $theme_registry->get($hook);
if (isset($info['path'])) {
$path = $info['path'] . '/' . $name;
}
elseif (isset($info['template'])) {
$path = $info['template'] . '.html.twig';
}
if (isset($path) && is_file($path)) {
return $this->cache[$name] = $path;
}
}
throw new \Twig_Error_Loader(sprintf('Unable to find template "%s" in the Drupal theme registry.', $name));
}
}

View file

@ -0,0 +1,207 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\TwigEnvironment.
*/
namespace Drupal\Core\Template;
use Drupal\Core\PhpStorage\PhpStorageFactory;
/**
* A class that defines a Twig environment for Drupal.
*
* Instances of this class are used to store the configuration and extensions,
* and are used to load templates from the file system or other locations.
*
* @see core\vendor\twig\twig\lib\Twig\Environment.php
*/
class TwigEnvironment extends \Twig_Environment {
protected $cache_object = NULL;
protected $storage = NULL;
/**
* Static cache of template classes.
*
* @var array
*/
protected $templateClasses;
/**
* Constructs a TwigEnvironment object and stores cache and storage
* internally.
*
* @param string $root
* The app root.
* @param \Twig_LoaderInterface $loader
* The Twig loader or loader chain.
* @param array $options
* The options for the Twig environment.
*/
public function __construct($root, \Twig_LoaderInterface $loader = NULL, $options = array()) {
// @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 TwigExtension::escapeFilter() are called.
require_once $root . '/core/themes/engines/twig/twig.engine';
$this->templateClasses = array();
$options += array(
// @todo Ensure garbage collection of expired files.
'cache' => TRUE,
'debug' => FALSE,
'auto_reload' => NULL,
);
// Ensure autoescaping is always on.
$options['autoescape'] = TRUE;
$this->loader = $loader;
parent::__construct($this->loader, $options);
}
/**
* Checks if the compiled template needs an update.
*/
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.
*/
public function updateCompiledTemplate($cache_filename, $name) {
$source = $this->loader->getSource($name);
$compiled_source = $this->compileSource($source, $name);
$this->storage()->save($cache_filename, $compiled_source);
// Save the last modification time
$cid = 'twig:' . $cache_filename;
$this->cache_object->set($cid, REQUEST_TIME);
}
/**
* 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.
*
* @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);
if (isset($this->loadedTemplates[$cls])) {
return $this->loadedTemplates[$cls];
}
if (!class_exists($cls, FALSE)) {
$cache_filename = $this->getCacheFilename($name);
if ($cache_filename === FALSE) {
$compiled_source = $this->compileSource($this->loader->getSource($name), $name);
eval('?' . '>' . $compiled_source);
}
else {
// If autoreload is on, check that the template has not been
// modified since the last compilation.
if ($this->isAutoReload() && !$this->isFresh($cache_filename, $name)) {
$this->updateCompiledTemplate($cache_filename, $name);
}
if (!$this->storage()->load($cache_filename)) {
$this->updateCompiledTemplate($cache_filename, $name);
$this->storage()->load($cache_filename);
}
}
}
if (!$this->runtimeInitialized) {
$this->initRuntime();
}
return $this->loadedTemplates[$cls] = new $cls($this);
}
/**
* Gets the PHP code storage object to use for the compiled Twig files.
*
* @return \Drupal\Component\PhpStorage\PhpStorageInterface
*/
protected function storage() {
if (!isset($this->storage)) {
$this->storage = PhpStorageFactory::get('twig');
}
return $this->storage;
}
/**
* 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.
*
* @return string
* The template class name.
*/
public function getTemplateClass($name, $index = NULL) {
// 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] = $this->templateClassPrefix . hash('sha256', $this->loader->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.
*
* @see \Drupal\Core\Template\Loader\StringLoader::exists()
*/
public function renderInline($template_string, array $context = array()) {
// Prefix all inline templates with a special comment.
$template_string = '{# inline_template_start #}' . $template_string;
return $this->loadTemplate($template_string, NULL)->render($context);
}
}

View file

@ -0,0 +1,513 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\TwigExtension.
*
* This provides a Twig extension that registers various Drupal specific
* extensions to Twig.
*
* @see \Drupal\Core\CoreServiceProvider
*/
namespace Drupal\Core\Template;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\SafeStringInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Core\Url;
/**
* A class providing Drupal Twig extensions.
*
* Specifically Twig functions, filter and node visitors.
*
* @see \Drupal\Core\CoreServiceProvider
*/
class TwigExtension extends \Twig_Extension {
/**
* The URL generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* Constructs \Drupal\Core\Template\TwigExtension.
*
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(RendererInterface $renderer) {
$this->renderer = $renderer;
}
/**
* Sets the URL generator.
*
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The URL generator.
*
* @return $this
*
* @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
* Use \Drupal\Core\Template\TwigExtension::setUrlGenerator().
*/
public function setGenerators(UrlGeneratorInterface $url_generator) {
return $this->setUrlGenerator($url_generator);
}
/**
* Sets the URL generator.
*
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The URL generator.
*
* @return $this
*/
public function setUrlGenerator(UrlGeneratorInterface $url_generator) {
$this->urlGenerator = $url_generator;
return $this;
}
/**
* Sets the theme manager.
*
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
*
* @return $this
*/
public function setThemeManager(ThemeManagerInterface $theme_manager) {
$this->themeManager = $theme_manager;
return $this;
}
/**
* {@inheritdoc}
*/
public function getFunctions() {
return [
// This function will receive a renderable array, if an array is detected.
new \Twig_SimpleFunction('render_var', array($this, 'renderVar')),
// The url and path function are defined in close parallel to those found
// in \Symfony\Bridge\Twig\Extension\RoutingExtension
new \Twig_SimpleFunction('url', array($this, 'getUrl'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))),
new \Twig_SimpleFunction('path', array($this, 'getPath'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))),
new \Twig_SimpleFunction('url_from_path', array($this, 'getUrlFromPath'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))),
new \Twig_SimpleFunction('link', array($this, 'getLink')),
new \Twig_SimpleFunction('file_url', 'file_create_url'),
new \Twig_SimpleFunction('attach_library', [$this, 'attachLibrary']),
new \Twig_SimpleFunction('active_theme', [$this, 'getActiveTheme']),
];
}
/**
* {@inheritdoc}
*/
public function getFilters() {
return array(
// Translation filters.
new \Twig_SimpleFilter('t', 't', array('is_safe' => array('html'))),
new \Twig_SimpleFilter('trans', 't', array('is_safe' => array('html'))),
// The "raw" filter is not detectable when parsing "trans" tags. To detect
// which prefix must be used for translation (@, !, %), we must clone the
// "raw" filter and give it identifiable names. These filters should only
// be used in "trans" tags.
// @see TwigNodeTrans::compileString()
new \Twig_SimpleFilter('passthrough', 'twig_raw_filter', array('is_safe' => array('html'))),
new \Twig_SimpleFilter('placeholder', [$this, 'escapePlaceholder'], array('is_safe' => array('html'), 'needs_environment' => TRUE)),
// Replace twig's escape filter with our own.
new \Twig_SimpleFilter('drupal_escape', [$this, 'escapeFilter'], array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
// Implements safe joining.
// @todo Make that the default for |join? Upstream issue:
// https://github.com/fabpot/Twig/issues/1420
new \Twig_SimpleFilter('safe_join', 'twig_drupal_join_filter', array('is_safe' => array('html'))),
// Array filters.
new \Twig_SimpleFilter('without', 'twig_without'),
// CSS class and ID filters.
new \Twig_SimpleFilter('clean_class', '\Drupal\Component\Utility\Html::getClass'),
new \Twig_SimpleFilter('clean_id', '\Drupal\Component\Utility\Html::getId'),
// This filter will render a renderable array to use the string results.
new \Twig_SimpleFilter('render', array($this, 'renderVar')),
);
}
/**
* {@inheritdoc}
*/
public function getNodeVisitors() {
// The node visitor is needed to wrap all variables with
// render_var -> TwigExtension->renderVar() function.
return array(
new TwigNodeVisitor(),
);
}
/**
* {@inheritdoc}
*/
public function getTokenParsers() {
return array(
new TwigTransTokenParser(),
);
}
/**
* {@inheritdoc}
*/
public function getName() {
return 'drupal_core';
}
/**
* Generates a URL path given a route name and parameters.
*
* @param $name
* The name of the route.
* @param array $parameters
* An associative array of route parameters names and values.
* @param array $options
* (optional) An associative array of additional options. The 'absolute'
* option is forced to be FALSE.
* @see \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute().
*
* @return string
* The generated URL path (relative URL) for the given route.
*/
public function getPath($name, $parameters = array(), $options = array()) {
$options['absolute'] = FALSE;
return $this->urlGenerator->generateFromRoute($name, $parameters, $options);
}
/**
* Generates an absolute URL given a route name and parameters.
*
* @param $name
* The name of the route.
* @param array $parameters
* An associative array of route parameter names and values.
* @param array $options
* (optional) An associative array of additional options. The 'absolute'
* option is forced to be TRUE.
*
* @return string
* The generated absolute URL for the given route.
*
* @todo Add an option for scheme-relative URLs.
*/
public function getUrl($name, $parameters = array(), $options = array()) {
// Generate URL.
$options['absolute'] = TRUE;
$generated_url = $this->urlGenerator->generateFromRoute($name, $parameters, $options, TRUE);
// Return as render array, so we can bubble the cacheability metadata.
$build = ['#markup' => $generated_url->getGeneratedUrl()];
$generated_url->applyTo($build);
return $build;
}
/**
* Generates an absolute URL given a path.
*
* @param string $path
* The path.
* @param array $options
* (optional) An associative array of additional options. The 'absolute'
* option is forced to be TRUE.
*
* @return string
* The generated absolute URL for the given path.
*
* @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 8.0.0.
*/
public function getUrlFromPath($path, $options = array()) {
// Generate URL.
$options['absolute'] = TRUE;
$generated_url = $this->urlGenerator->generateFromPath($path, $options, TRUE);
// Return as render array, so we can bubble the cacheability metadata.
$build = ['#markup' => $generated_url->getGeneratedUrl()];
$generated_url->applyTo($build);
return $build;
}
/**
* Gets a rendered link from an url object.
*
* @param string $text
* The link text for the anchor tag as a translated string.
* @param \Drupal\Core\Url|string $url
* The URL object or string used for the link.
* @param array $attributes
* An optional array of link attributes.
*
* @return array
* A render array representing a link to the given URL.
*/
public function getLink($text, $url, array $attributes = []) {
if (!$url instanceof Url) {
$url = Url::fromUri($url);
}
if ($attributes) {
if ($existing_attributes = $url->getOption('attributes')) {
$attributes = array_merge($existing_attributes, $attributes);
}
$url->setOption('attributes', $attributes);
}
$build = [
'#type' => 'link',
'#title' => $text,
'#url' => $url,
];
return $build;
}
/**
* Gets the name of the active theme.
*
* @return string
* The name of the active theme.
*/
public function getActiveTheme() {
return $this->themeManager->getActiveTheme()->getName();
}
/**
* Determines at compile time whether the generated URL will be safe.
*
* Saves the unneeded automatic escaping for performance reasons.
*
* The URL generation process percent encodes non-alphanumeric characters.
* Thus, the only character within an URL that must be escaped in HTML is the
* ampersand ("&") which separates query params. Thus we cannot mark
* the generated URL as always safe, but only when we are sure there won't be
* multiple query params. This is the case when there are none or only one
* constant parameter given. E.g. we know beforehand this will not need to
* be escaped:
* - path('route')
* - path('route', {'param': 'value'})
* But the following may need to be escaped:
* - path('route', var)
* - path('route', {'param': ['val1', 'val2'] }) // a sub-array
* - path('route', {'param1': 'value1', 'param2': 'value2'})
* If param1 and param2 reference placeholders in the route, it would not
* need to be escaped, but we don't know that in advance.
*
* @param \Twig_Node $args_node
* The arguments of the path/url functions.
*
* @return array
* An array with the contexts the URL is safe
*/
public function isUrlGenerationSafe(\Twig_Node $args_node) {
// Support named arguments.
$parameter_node = $args_node->hasNode('parameters') ? $args_node->getNode('parameters') : ($args_node->hasNode(1) ? $args_node->getNode(1) : NULL);
if (!isset($parameter_node) || $parameter_node instanceof \Twig_Node_Expression_Array && count($parameter_node) <= 2 &&
(!$parameter_node->hasNode(1) || $parameter_node->getNode(1) instanceof \Twig_Node_Expression_Constant)) {
return array('html');
}
return array();
}
/**
* Attaches an asset library to the template, and hence to the response.
*
* Allows Twig templates to attach asset libraries using
* @code
* {{ attach_library('extension/library_name') }}
* @endcode
*
* @param string $library
* An asset library.
*/
public function attachLibrary($library) {
// Use Renderer::render() on a temporary render array to get additional
// bubbleable metadata on the render stack.
$template_attached = ['#attached' => ['library' => [$library]]];
$this->renderer->render($template_attached);
}
/**
* Provides a placeholder wrapper around ::escapeFilter.
*
* @param \Twig_Environment $env
* A Twig_Environment instance.
* @param mixed $string
* The value to be escaped.
*
* @return string|null
* The escaped, rendered output, or NULL if there is no valid output.
*/
public function escapePlaceholder($env, $string) {
return '<em class="placeholder">' . $this->escapeFilter($env, $string) . '</em>';
}
/**
* Overrides twig_escape_filter().
*
* Replacement function for Twig's escape filter.
*
* @param \Twig_Environment $env
* A Twig_Environment instance.
* @param mixed $arg
* The value to be escaped.
* @param string $strategy
* The escaping strategy. Defaults to 'html'.
* @param string $charset
* The charset.
* @param bool $autoescape
* Whether the function is called by the auto-escaping feature (TRUE) or by
* the developer (FALSE).
*
* @return string|null
* The escaped, rendered output, or NULL if there is no valid output.
*/
public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $charset = NULL, $autoescape = FALSE) {
// Check for a numeric zero int or float.
if ($arg === 0 || $arg === 0.0) {
return 0;
}
// Return early for NULL and empty arrays.
if ($arg == NULL) {
return NULL;
}
// Keep Twig_Markup objects intact to support autoescaping.
if ($autoescape && ($arg instanceOf \Twig_Markup || $arg instanceOf SafeStringInterface)) {
return $arg;
}
$return = NULL;
if (is_scalar($arg)) {
$return = (string) $arg;
}
elseif (is_object($arg)) {
if (method_exists($arg, '__toString')) {
$return = (string) $arg;
}
// You can't throw exceptions in the magic PHP __toString methods, see
// http://php.net/manual/en/language.oop5.magic.php#object.tostring so
// we also support a toString method.
elseif (method_exists($arg, 'toString')) {
$return = $arg->toString();
}
else {
throw new \Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($arg))));
}
}
// We have a string or an object converted to a string: Autoescape it!
if (isset($return)) {
if ($autoescape && SafeMarkup::isSafe($return, $strategy)) {
return $return;
}
// Drupal only supports the HTML escaping strategy, so provide a
// fallback for other strategies.
if ($strategy == 'html') {
return SafeMarkup::checkPlain($return);
}
return twig_escape_filter($env, $return, $strategy, $charset, $autoescape);
}
// This is a normal render array, which is safe by definition, with
// special simple cases already handled.
// Early return if this element was pre-rendered (no need to re-render).
if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) {
return $arg['#markup'];
}
$arg['#printed'] = FALSE;
return $this->renderer->render($arg);
}
/**
* Wrapper around render() for twig printed output.
*
* If an object is passed that has no __toString method an exception is thrown;
* other objects are casted to string. However in the case that the object is an
* instance of a Twig_Markup object it is returned directly to support auto
* escaping.
*
* If an array is passed it is rendered via render() and scalar values are
* returned directly.
*
* @param mixed $arg
* String, Object or Render Array.
*
* @return mixed
* The rendered output or an Twig_Markup object.
*
* @see render
* @see TwigNodeVisitor
*/
public function renderVar($arg) {
// Check for a numeric zero int or float.
if ($arg === 0 || $arg === 0.0) {
return 0;
}
// Return early for NULL and empty arrays.
if ($arg == NULL) {
return NULL;
}
// Optimize for strings as it is likely they come from the escape filter.
if (is_string($arg)) {
return $arg;
}
if (is_scalar($arg)) {
return $arg;
}
if (is_object($arg)) {
if (method_exists($arg, '__toString')) {
return (string) $arg;
}
// You can't throw exceptions in the magic PHP __toString methods, see
// http://php.net/manual/en/language.oop5.magic.php#object.tostring so
// we also support a toString method.
elseif (method_exists($arg, 'toString')) {
return $arg->toString();
}
else {
throw new \Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($arg))));
}
}
// This is a render array, with special simple cases already handled.
// Early return if this element was pre-rendered (no need to re-render).
if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) {
return $arg['#markup'];
}
$arg['#printed'] = FALSE;
return $this->renderer->render($arg);
}
}

View file

@ -0,0 +1,190 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\TwigNodeTrans.
*
* This Twig extension was originally based on Twig i18n extension. It has been
* severely modified to work properly with the complexities of the Drupal
* translation system.
*
* @see http://twig.sensiolabs.org/doc/extensions/i18n.html
* @see https://github.com/fabpot/Twig-extensions
*/
namespace Drupal\Core\Template;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Site\Settings;
/**
* A class that defines the Twig 'trans' tag for Drupal.
*/
class TwigNodeTrans extends \Twig_Node {
/**
* {@inheritdoc}
*/
public function __construct(\Twig_NodeInterface $body, \Twig_NodeInterface $plural = NULL, \Twig_Node_Expression $count = NULL, \Twig_Node_Expression $options = NULL, $lineno, $tag = NULL) {
parent::__construct(array(
'count' => $count,
'body' => $body,
'plural' => $plural,
'options' => $options,
), array(), $lineno, $tag);
}
/**
* {@inheritdoc}
*/
public function compile(\Twig_Compiler $compiler) {
$compiler->addDebugInfo($this);
$options = $this->getNode('options');
list($singular, $tokens) = $this->compileString($this->getNode('body'));
$plural = NULL;
if (NULL !== $this->getNode('plural')) {
list($plural, $pluralTokens) = $this->compileString($this->getNode('plural'));
$tokens = array_merge($tokens, $pluralTokens);
}
// Start writing with the function to be called.
$compiler->write('echo ' . (empty($plural) ? 't' : '\Drupal::translation()->formatPlural') . '(');
// Move the count to the beginning of the parameters list.
if (!empty($plural)) {
$compiler->raw('abs(')->subcompile($this->getNode('count'))->raw('), ');
}
// Write the singular text parameter.
$compiler->subcompile($singular);
// Write the plural text parameter, if necessary.
if (!empty($plural)) {
$compiler->raw(', ')->subcompile($plural);
}
// Write any tokens found as an associative array parameter, otherwise just
// leave as an empty array.
$compiler->raw(', array(');
foreach ($tokens as $token) {
$compiler->string($token->getAttribute('placeholder'))->raw(' => ')->subcompile($token)->raw(', ');
}
$compiler->raw(')');
// Write any options passed.
if (!empty($options)) {
$compiler->raw(', ')->subcompile($options);
}
// Write function closure.
$compiler->raw(')');
// @todo Add debug output, see https://www.drupal.org/node/2512672
// End writing.
$compiler->raw(";\n");
}
/**
* Extracts the text and tokens for the "trans" tag.
*
* @param \Twig_NodeInterface $body
* The node to compile.
*
* @return array
* Returns an array containing the two following parameters:
* - string $text
* The extracted text.
* - array $tokens
* The extracted tokens as new \Twig_Node_Expression_Name instances.
*/
protected function compileString(\Twig_NodeInterface $body) {
if ($body instanceof \Twig_Node_Expression_Name || $body instanceof \Twig_Node_Expression_Constant || $body instanceof \Twig_Node_Expression_TempName) {
return array($body, array());
}
$tokens = array();
if (count($body)) {
$text = '';
foreach ($body as $node) {
if (get_class($node) === 'Twig_Node' && $node->getNode(0) instanceof \Twig_Node_SetTemp) {
$node = $node->getNode(1);
}
if ($node instanceof \Twig_Node_Print) {
$n = $node->getNode('expr');
while ($n instanceof \Twig_Node_Expression_Filter) {
$n = $n->getNode('node');
}
$args = $n;
// Support TwigExtension->renderVar() function in chain.
if ($args instanceof \Twig_Node_Expression_Function) {
$args = $n->getNode('arguments')->getNode(0);
}
// Detect if a token implements one of the filters reserved for
// modifying the prefix of a token. The default prefix used for
// translations is "@". This escapes the printed token and makes them
// safe for templates.
// @see TwigExtension::getFilters()
$argPrefix = '@';
while ($args instanceof \Twig_Node_Expression_Filter) {
switch ($args->getNode('filter')->getAttribute('value')) {
case 'passthrough':
$argPrefix = '!';
break;
case 'placeholder':
$argPrefix = '%';
break;
}
$args = $args->getNode('node');
}
if ($args instanceof \Twig_Node_Expression_GetAttr) {
$argName = array();
// Reuse the incoming expression.
$expr = $args;
// Assemble a valid argument name by walking through the expression.
$argName[] = $args->getNode('attribute')->getAttribute('value');
while ($args->hasNode('node')) {
$args = $args->getNode('node');
if ($args instanceof \Twig_Node_Expression_Name) {
$argName[] = $args->getAttribute('name');
}
else {
$argName[] = $args->getNode('attribute')->getAttribute('value');
}
}
$argName = array_reverse($argName);
$argName = implode('.', $argName);
}
else {
$argName = $n->getAttribute('name');
if (!is_null($args)) {
$argName = $args->getAttribute('name');
}
$expr = new \Twig_Node_Expression_Name($argName, $n->getLine());
}
$placeholder = sprintf('%s%s', $argPrefix, $argName);
$text .= $placeholder;
$expr->setAttribute('placeholder', $placeholder);
$tokens[] = $expr;
}
else {
$text .= $node->getAttribute('data');
}
}
}
else {
$text = $body->getAttribute('data');
}
return array(new \Twig_Node(array(new \Twig_Node_Expression_Constant(trim($text), $body->getLine()))), $tokens);
}
}

View file

@ -0,0 +1,70 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\TwigNodeVisitor.
*/
namespace Drupal\Core\Template;
/**
* Provides a Twig_NodeVisitor to change the generated parse-tree.
*
* This is used to ensure that everything printed is wrapped via the
* TwigExtension->renderVar() function in order to just write {{ content }}
* in templates instead of having to write {{ render_var(content) }}.
*
* @see twig_render
*/
class TwigNodeVisitor implements \Twig_NodeVisitorInterface {
/**
* {@inheritdoc}
*/
function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
return $node;
}
/**
* {@inheritdoc}
*/
function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
// We use this to inject a call to render_var -> TwigExtension->renderVar()
// before anything is printed.
if ($node instanceof \Twig_Node_Print) {
if (!empty($this->skipRenderVarFunction)) {
// No need to add the callback, we have escape active already.
unset($this->skipRenderVarFunction);
return $node;
}
$class = get_class($node);
$line = $node->getLine();
return new $class(
new \Twig_Node_Expression_Function('render_var', new \Twig_Node(array($node->getNode('expr'))), $line),
$line
);
}
// Change the 'escape' filter to our own 'drupal_escape' filter.
else if ($node instanceof \Twig_Node_Expression_Filter) {
$name = $node->getNode('filter')->getAttribute('value');
if ('escape' == $name || 'e' == $name) {
// Use our own escape filter that is SafeMarkup aware.
$node->getNode('filter')->setAttribute('value', 'drupal_escape');
// Store that we have a filter active already that knows how to deal with render arrays.
$this->skipRenderVarFunction = TRUE;
}
}
return $node;
}
/**
* {@inheritdoc}
*/
function getPriority() {
// Just above the Optimizer, which is the normal last one.
return 256;
}
}

View file

@ -0,0 +1,109 @@
<?php
/**
* @file
* Contains \Drupal\Core\Template\TwigTransTokenParser.
*
* @see http://twig.sensiolabs.org/doc/extensions/i18n.html
* @see https://github.com/fabpot/Twig-extensions
*/
namespace Drupal\Core\Template;
/**
* A class that defines the Twig 'trans' token parser for Drupal.
*
* The token parser converts a token stream created from template source
* code into an Abstract Syntax Tree (AST). The AST will later be compiled
* into PHP code usable for runtime execution of the template.
*
* @see \Twig_TokenParser
*/
class TwigTransTokenParser extends \Twig_TokenParser {
/**
* {@inheritdoc}
*/
public function parse(\Twig_Token $token) {
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$body = NULL;
$options = NULL;
$count = NULL;
$plural = NULL;
if (!$stream->test(\Twig_Token::BLOCK_END_TYPE) && $stream->test(\Twig_Token::STRING_TYPE)) {
$body = $this->parser->getExpressionParser()->parseExpression();
}
if (!$stream->test(\Twig_Token::BLOCK_END_TYPE) && $stream->test(\Twig_Token::NAME_TYPE, 'with')) {
$stream->next();
$options = $this->parser->getExpressionParser()->parseExpression();
}
if (!$body) {
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideForFork'));
if ('plural' === $stream->next()->getValue()) {
$count = $this->parser->getExpressionParser()->parseExpression();
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
$plural = $this->parser->subparse(array($this, 'decideForEnd'), TRUE);
}
}
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
$this->checkTransString($body, $lineno);
$node = new TwigNodeTrans($body, $plural, $count, $options, $lineno, $this->getTag());
return $node;
}
/**
* Detect a 'plural' switch or the end of a 'trans' tag.
*/
public function decideForFork($token) {
return $token->test(array('plural', 'endtrans'));
}
/**
* Detect the end of a 'trans' tag.
*/
public function decideForEnd($token) {
return $token->test('endtrans');
}
/**
* {@inheritdoc}
*/
public function getTag() {
return 'trans';
}
/**
* Ensure that any nodes that are parsed are only of allowed types.
*
* @param \Twig_NodeInterface $body
* The expression to check.
* @param integer $lineno
* The source line.
*
* @throws \Twig_Error_Syntax
*/
protected function checkTransString(\Twig_NodeInterface $body, $lineno) {
foreach ($body as $node) {
if (
$node instanceof \Twig_Node_Text
||
($node instanceof \Twig_Node_Print && $node->getNode('expr') instanceof \Twig_Node_Expression_Name)
||
($node instanceof \Twig_Node_Print && $node->getNode('expr') instanceof \Twig_Node_Expression_GetAttr)
||
($node instanceof \Twig_Node_Print && $node->getNode('expr') instanceof \Twig_Node_Expression_Filter)
) {
continue;
}
throw new \Twig_Error_Syntax(sprintf('The text to be translated with "trans" can only contain references to simple variables'), $lineno);
}
}
}