Commit 1a7cd1b5 authored by alexpott's avatar alexpott

Issue #2073811 by dawehner, pwolanin, joelpittet, Cottser, Mark Carver,...

Issue #2073811 by dawehner, pwolanin, joelpittet, Cottser, Mark Carver, steveoliver: Add a url generator twig extension.
parent 39fc34a9
......@@ -4,7 +4,7 @@
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "e1a7eeed7d09f5c76f642f07b7a0a579",
"hash": "2b2309c748ec92230880579aaedfa1e6",
"packages": [
{
"name": "doctrine/annotations",
......@@ -2027,17 +2027,17 @@
},
{
"name": "symfony/serializer",
"version": "v2.4.1",
"version": "v2.5.2",
"target-dir": "Symfony/Component/Serializer",
"source": {
"type": "git",
"url": "https://github.com/symfony/Serializer.git",
"reference": "60c54346958604379392672a3a998650a169a7f4"
"reference": "33185b290310ab1fd8283fb8ed2a434cdb88b9b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/Serializer/zipball/60c54346958604379392672a3a998650a169a7f4",
"reference": "60c54346958604379392672a3a998650a169a7f4",
"url": "https://api.github.com/repos/symfony/Serializer/zipball/33185b290310ab1fd8283fb8ed2a434cdb88b9b9",
"reference": "33185b290310ab1fd8283fb8ed2a434cdb88b9b9",
"shasum": ""
},
"require": {
......@@ -2046,7 +2046,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
"dev-master": "2.5-dev"
}
},
"autoload": {
......@@ -2061,7 +2061,9 @@
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Symfony Community",
......@@ -2070,7 +2072,7 @@
],
"description": "Symfony Serializer Component",
"homepage": "http://symfony.com",
"time": "2014-01-01 08:14:50"
"time": "2014-07-09 09:05:48"
},
{
"name": "symfony/translation",
......@@ -2423,12 +2425,8 @@
"time": "2013-06-12 19:46:58"
}
],
"packages-dev": [
],
"aliases": [
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"symfony/yaml": 20,
......@@ -2438,10 +2436,9 @@
"symfony-cmf/routing": 15,
"phpunit/phpunit-mock-objects": 20
},
"prefer-stable": false,
"platform": {
"php": ">=5.4.2"
},
"platform-dev": [
]
"platform-dev": []
}
......@@ -1696,6 +1696,25 @@ function template_preprocess_table(&$variables) {
}
}
/**
* Prepares variables for tablesort indicator templates.
*
* Default template: tablesort-indicator.html.twig.
*
* @param array $variables
* An associative array containing:
* - style: Set to either 'asc' or 'desc'. This determines which icon to show.
*/
function template_preprocess_tablesort_indicator(&$variables) {
// Provide the image attributes for an ascending or descending image.
if ($variables['style'] == 'asc') {
$variables['arrow_asc'] = file_create_url('core/misc/arrow-asc.png');
}
else {
$variables['arrow_desc'] = file_create_url('core/misc/arrow-desc.png');
}
}
/**
* Prepares variables for item list templates.
*
......
......@@ -84,6 +84,9 @@ public static function registerTwig(ContainerBuilder $container) {
->addArgument(DRUPAL_ROOT);
$container->setAlias('twig.loader', 'twig.loader.filesystem');
$twig_extension = new Definition('Drupal\Core\Template\TwigExtension');
$twig_extension->addMethodCall('setGenerators', array(new Reference('url_generator')));
$container->register('twig', 'Drupal\Core\Template\TwigEnvironment')
->addArgument(new Reference('twig.loader'))
->addArgument(array(
......@@ -99,7 +102,7 @@ public static function registerTwig(ContainerBuilder $container) {
))
->addArgument(new Reference('module_handler'))
->addArgument(new Reference('theme_handler'))
->addMethodCall('addExtension', array(new Definition('Drupal\Core\Template\TwigExtension')))
->addMethodCall('addExtension', array($twig_extension))
// @todo Figure out what to do about debugging functions.
// @see http://drupal.org/node/1804998
->addMethodCall('addExtension', array(new Definition('Twig_Extension_Debug')))
......
......@@ -12,6 +12,8 @@
namespace Drupal\Core\Template;
use Drupal\Core\Routing\UrlGeneratorInterface;
/**
* A class providing Drupal Twig extensions.
*
......@@ -21,14 +23,36 @@
*/
class TwigExtension extends \Twig_Extension {
/**
* The URL generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* Constructs \Drupal\Core\Template\TwigExtension.
*
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The URL generator.
*/
public function setGenerators(UrlGeneratorInterface $url_generator) {
$this->urlGenerator = $url_generator;
return $this;
}
/**
* {@inheritdoc}
*/
public function getFunctions() {
return array(
new \Twig_SimpleFunction('url', 'url'),
// This function will receive a renderable array, if an array is detected.
new \Twig_SimpleFunction('render_var', 'twig_render_var'),
// 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'))),
);
}
......@@ -88,4 +112,101 @@ 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()) {
$options['absolute'] = TRUE;
return $this->urlGenerator->generateFromRoute($name, $parameters, $options);
}
/**
* 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.
*/
public function getUrlFromPath($path, $options = array()) {
$options['absolute'] = TRUE;
return $this->urlGenerator->generateFromPath($path, $options);
}
/**
* 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();
}
}
......@@ -24,7 +24,7 @@
</dl>
{% else %}
<p>
{% set create_content = url('admin/structure/types/add') %}
{% set create_content = path('node.type_add') %}
{% trans %}
You have not created any content types yet. Go to the <a href="{{ create_content }}">content type creation page</a> to add a new content type.
{% endtrans %}
......
......@@ -41,4 +41,26 @@ function testTwigVariableDataTypes() {
}
}
/**
* Tests the url and url_generate Twig functions.
*/
public function testTwigUrlGenerator() {
$this->drupalGet('twig-theme-test/url-generator');
// Find the absolute URL of the current site.
$url_generator = $this->container->get('url_generator');
$expected = array(
'path (as route) not absolute: ' . $url_generator->generateFromRoute('user.register'),
'url (as route) absolute: ' . $url_generator->generateFromRoute('user.register', array(), array('absolute' => TRUE)),
'path (as route) not absolute with fragment: ' . $url_generator->generateFromRoute('user.register', array(), array('fragment' => 'bottom')),
'url (as route) absolute despite option: ' . $url_generator->generateFromRoute('user.register', array(), array('absolute' => TRUE)),
'url (as route) absolute with fragment: ' . $url_generator->generateFromRoute('user.register', array(), array('absolute' => TRUE, 'fragment' => 'bottom')),
);
// Make sure we got something.
$content = $this->drupalGetContent();
$this->assertFalse(empty($content), 'Page content is not empty');
foreach ($expected as $string) {
$this->assertRaw('<div>' . $string . '</div>');
}
}
}
......@@ -6,11 +6,13 @@
* Available variables:
* - style: Either 'asc' or 'desc', indicating the sorting direction.
*
* @see template_preprocess_tablesort_indicator()
*
* @ingroup themeable
*/
#}
{% if style == 'asc' -%}
<img src="{{ url('core/misc/arrow-asc.png') }}" width="13" height="13" alt="{{ 'sort ascending'|t }}" title="{{ 'sort ascending'|t }}" />
<img src="{{ arrow_asc }}" width="13" height="13" alt="{{ 'sort ascending'|t }}" title="{{ 'sort ascending'|t }}" />
{% else -%}
<img src="{{ url('core/misc/arrow-desc.png') }}" width="13" height="13" alt="{{ 'sort descending'|t }}" title="{{ 'sort descending'|t }}" />
<img src="{{ arrow_desc }}" width="13" height="13" alt="{{ 'sort descending'|t }}" title="{{ 'sort descending'|t }}" />
{% endif %}
......@@ -28,4 +28,13 @@ public function transBlockRender() {
);
}
/**
* Renders for testing url_generator functions in a Twig template.
*/
public function urlGeneratorRender() {
return array(
'#theme' => 'twig_theme_test_url_generator',
);
}
}
{# Test the url and path twig functions #}
<div>path (as route) not absolute: {{ path('user.register') }}</div>
<div>url (as route) absolute: {{ url('user.register') }}</div>
<div>path (as route) not absolute with fragment: {{ path('user.register', {}, {'fragment': 'bottom' }) }}</div>
<div>url (as route) absolute despite option: {{ url('user.register', {}, {'absolute': false }) }}</div>
<div>url (as route) absolute with fragment: {{ url('user.register', {}, {'fragment': 'bottom' }) }}</div>
......@@ -23,6 +23,10 @@ function twig_theme_test_theme($existing, $type, $theme, $path) {
'variables' => array('script' => ''),
'template' => 'twig-raw-test',
);
$items['twig_theme_test_url_generator'] = array(
'variables' => array(),
'template' => 'twig_theme_test.url_generator',
);
return $items;
}
......
......@@ -11,3 +11,10 @@ twig_theme_test.trans:
_content: '\Drupal\twig_theme_test\TwigThemeTestController::transBlockRender'
requirements:
_access: 'TRUE'
twig_theme_test_url_generator:
path: '/twig-theme-test/url-generator'
defaults:
_content: '\Drupal\twig_theme_test\TwigThemeTestController::urlGeneratorRender'
requirements:
_access: 'TRUE'
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Template\TwigExtensionTest.
*/
namespace Drupal\Tests\Core\Template;
use Drupal\Core\Template\TwigExtension;
use Drupal\Tests\UnitTestCase;
/**
* Tests the twig extension.
*
* @group Template
*
* @coversDefaultClass \Drupal\Core\Template\TwigExtension
*/
class TwigExtensionTest extends UnitTestCase {
/**
* Tests the escaping
* @dataProvider providerTestEscaping
*
* @covers
*/
public function testEscaping($template, $expected) {
$twig = new \Twig_Environment(NULL, array(
'debug' => TRUE,
'cache' => FALSE,
'autoescape' => TRUE,
'optimizations' => 0
));
$twig->addExtension((new TwigExtension())->setGenerators($this->getMock('Drupal\Core\Routing\UrlGeneratorInterface')));
$nodes = $twig->parse($twig->tokenize($template));
$this->assertSame($expected, $nodes->getNode('body')
->getNode(0)
->getNode('expr') instanceof \Twig_Node_Expression_Filter);
}
/**
* Provides tests data for testEscaping
*
* @return array
* An array of test data each containing of a twig template string and
* a boolean expecting whether the path will be safe.
*/
public function providerTestEscaping() {
return array(
array('{{ path("foo") }}', FALSE),
array('{{ path("foo", {}) }}', FALSE),
array('{{ path("foo", { foo: "foo" }) }}', FALSE),
array('{{ path("foo", foo) }}', TRUE),
array('{{ path("foo", { foo: foo }) }}', TRUE),
array('{{ path("foo", { foo: ["foo", "bar"] }) }}', TRUE),
array('{{ path("foo", { foo: "foo", bar: "bar" }) }}', TRUE),
array('{{ path(name = "foo", parameters = {}) }}', FALSE),
array('{{ path(name = "foo", parameters = { foo: "foo" }) }}', FALSE),
array('{{ path(name = "foo", parameters = foo) }}', TRUE),
array(
'{{ path(name = "foo", parameters = { foo: ["foo", "bar"] }) }}',
TRUE
),
array('{{ path(name = "foo", parameters = { foo: foo }) }}', TRUE),
array(
'{{ path(name = "foo", parameters = { foo: "foo", bar: "bar" }) }}',
TRUE
),
);
}
}
......@@ -120,9 +120,6 @@ function seven_preprocess_node_add_list(&$variables) {
$variables['types'][$type->type]['url'] = \Drupal::url('node.add', array('node_type' => $type->type));
}
}
else {
$variables['add_content_type_url'] = \Drupal::url('node.type_add');
}
}
/**
......
......@@ -8,7 +8,6 @@
* - url: Path to the add content of this type page.
* - label: The title of this type of content.
* - description: Description of this type of content.
* - add_content_type_url: Path to the add content type page.
*
* @see template_preprocess_node_add_list()
* @see seven_preprocess_node_add_list()
......@@ -22,8 +21,9 @@
</ul>
{% else %}
<p>
{% set create_content = path('node.type_add') %}
{% trans %}
You have not created any content types yet. Go to the <a href="{{ add_content_type_url }}">content type creation page</a> to add a new content type.
You have not created any content types yet. Go to the <a href="{{ create_content }}">content type creation page</a> to add a new content type.
{% endtrans %}
</p>
{% endif %}
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