diff --git a/doc/templates.rst b/doc/templates.rst
index 8f03565a2..1570f9a8b 100644
--- a/doc/templates.rst
+++ b/doc/templates.rst
@@ -162,6 +162,36 @@ Use the ``spaceless`` tag to remove whitespace between HTML tags:
{# output will be
foo
#}
+In addition to the spaceless tag you can also control whitespace on a per tag
+level. By using the whitespace control modifier on your tags you can trim
+leading and or trailing whitespace from any tag type:
+
+.. code-block:: jinja
+
+ {% set value = 'no spaces' %}
+ {#- No leading/trailing whitespace -#}
+ {%- if true -%}
+ {{- value -}}
+ {%- endif -%}
+
+ {# output 'spaces' #}
+
+The above sample shows the default whitespace control modifier, and how you can
+use it to remove whitespace around tags. Trimming space will consume all whitespace
+for that side of the tag. It is possible to use whitespace trimming on one side
+of a tag:
+
+.. code-block:: jinja
+
+ {% set value = 'no spaces' %}
+ {{- value }}
+
+ {# outputs 'value ' #}
+
+..versionadded:: 1.1
+
+ Tag level whitespace control was added in 1.1
+
Escaping
--------
diff --git a/lib/Twig/Lexer.php b/lib/Twig/Lexer.php
index 93647f69c..16dda512c 100644
--- a/lib/Twig/Lexer.php
+++ b/lib/Twig/Lexer.php
@@ -48,6 +48,7 @@ class Twig_Lexer implements Twig_LexerInterface
'tag_comment' => array('{#', '#}'),
'tag_block' => array('{%', '%}'),
'tag_variable' => array('{{', '}}'),
+ 'whitespace_trim' => '-'
), $options);
}
@@ -110,17 +111,21 @@ class Twig_Lexer implements Twig_LexerInterface
protected function lexData()
{
$pos = $this->end;
- if (false !== ($tmpPos = strpos($this->code, $this->options['tag_comment'][0], $this->cursor)) && $tmpPos < $pos) {
- $pos = $tmpPos;
- $token = $this->options['tag_comment'][0];
- }
- if (false !== ($tmpPos = strpos($this->code, $this->options['tag_variable'][0], $this->cursor)) && $tmpPos < $pos) {
- $pos = $tmpPos;
- $token = $this->options['tag_variable'][0];
- }
- if (false !== ($tmpPos = strpos($this->code, $this->options['tag_block'][0], $this->cursor)) && $tmpPos < $pos) {
- $pos = $tmpPos;
- $token = $this->options['tag_block'][0];
+ $append = '';
+
+ // Find the first token after the cursor.
+ foreach (array('tag_comment', 'tag_variable', 'tag_block') as $type) {
+ $tmpPos = strpos($this->code, $this->options[$type][0], $this->cursor);
+ if (false !== $tmpPos && $tmpPos < $pos) {
+ $trimBlock = false;
+ $append = '';
+ $pos = $tmpPos;
+ $token = $this->options[$type][0];
+ if (strpos($this->code, $this->options['whitespace_trim'], $pos) === ($pos + strlen($token))) {
+ $trimBlock = true;
+ $append = $this->options['whitespace_trim'];
+ }
+ }
}
// if no matches are left we return the rest of the template as simple text token
@@ -129,26 +134,26 @@ class Twig_Lexer implements Twig_LexerInterface
$this->cursor = $this->end;
return;
}
-
+
// push the template text first
- $text = substr($this->code, $this->cursor, $pos - $this->cursor);
+ $text = $textContent = substr($this->code, $this->cursor, $pos - $this->cursor);
+ if (true === $trimBlock) {
+ $text = rtrim($text);
+ }
$this->pushToken(Twig_Token::TEXT_TYPE, $text);
- $this->moveCursor($text.$token);
+ $this->moveCursor($textContent . $token . $append);
switch ($token) {
case $this->options['tag_comment'][0]:
- if (false === $pos = strpos($this->code, $this->options['tag_comment'][1], $this->cursor)) {
+ $commentEndRegex = '/.*?(?:' . preg_quote($this->options['whitespace_trim'], '/')
+ . preg_quote($this->options['tag_comment'][1], '/') . '\s*|'
+ . preg_quote($this->options['tag_comment'][1], '/') . ')\n?/As';
+
+ if (!preg_match($commentEndRegex, $this->code, $match, null, $this->cursor)) {
throw new Twig_Error_Syntax('unclosed comment', $this->lineno, $this->filename);
}
- $this->moveCursor(substr($this->code, $this->cursor, $pos - $this->cursor) . $this->options['tag_comment'][1]);
-
- // mimics the behavior of PHP by removing the newline that follows instructions if present
- if ("\n" === substr($this->code, $this->cursor, 1)) {
- ++$this->cursor;
- ++$this->lineno;
- }
-
+ $this->moveCursor($match[0]);
break;
case $this->options['tag_block'][0]:
@@ -172,16 +177,13 @@ class Twig_Lexer implements Twig_LexerInterface
protected function lexBlock()
{
- if (empty($this->brackets) && preg_match('/\s*'.preg_quote($this->options['tag_block'][1], '/').'/A', $this->code, $match, null, $this->cursor)) {
+ $trimTag = preg_quote($this->options['whitespace_trim'] . $this->options['tag_block'][1], '/');
+ $endTag = preg_quote($this->options['tag_block'][1], '/');
+
+ if (empty($this->brackets) && preg_match('/\s*(?:' . $trimTag . '\s*|\s*' . $endTag . ')\n?/A', $this->code, $match, null, $this->cursor)) {
$this->pushToken(Twig_Token::BLOCK_END_TYPE);
$this->moveCursor($match[0]);
$this->state = self::STATE_DATA;
-
- // mimics the behavior of PHP by removing the newline that follows instructions if present
- if ("\n" === substr($this->code, $this->cursor, 1)) {
- ++$this->cursor;
- ++$this->lineno;
- }
}
else {
$this->lexExpression();
@@ -190,7 +192,10 @@ class Twig_Lexer implements Twig_LexerInterface
protected function lexVar()
{
- if (empty($this->brackets) && preg_match('/\s*'.preg_quote($this->options['tag_variable'][1], '/').'/A', $this->code, $match, null, $this->cursor)) {
+ $trimTag = preg_quote($this->options['whitespace_trim'] . $this->options['tag_variable'][1], '/');
+ $endTag = preg_quote($this->options['tag_variable'][1], '/');
+
+ if (empty($this->brackets) && preg_match('/\s*' . $trimTag . '\s*|\s*' . $endTag . '/A', $this->code, $match, null, $this->cursor)) {
$this->pushToken(Twig_Token::VAR_END_TYPE);
$this->moveCursor($match[0]);
$this->state = self::STATE_DATA;
diff --git a/test/Twig/Tests/Fixtures/tags/trim_block.test b/test/Twig/Tests/Fixtures/tags/trim_block.test
new file mode 100644
index 000000000..1d2273f88
--- /dev/null
+++ b/test/Twig/Tests/Fixtures/tags/trim_block.test
@@ -0,0 +1,74 @@
+--TEST--
+Whitespace trimming on tags.
+--TEMPLATE--
+{{ 5 * '{#-'|length }}
+{{ '{{-'|length * 5 + '{%-'|length }}
+
+Trim on control tag:
+{% for i in range(1, 9) -%}
+ {{ i }}
+{%- endfor %}
+
+
+Trim on output tag:
+{% for i in range(1, 9) %}
+ {{- i -}}
+{% endfor %}
+
+
+Trim comments:
+
+{#- Invisible -#}
+
+After the comment.
+
+Trim leading space:
+{% if leading %}
+
+ {{- leading }}
+{% endif %}
+
+{%- if leading %}
+ {{- leading }}
+
+{%- endif %}
+
+
+Trim trailing space:
+{% if trailing -%}
+ {{ trailing -}}
+
+{% endif -%}
+
+Combined:
+
+{%- if both -%}
+
+
+{%- endif -%}
+
+end
+--DATA--
+return array('leading' => 'leading space', 'trailing' => 'trailing space', 'both' => 'both')
+--EXPECT--
+15
+18
+
+Trim on control tag:
+123456789
+
+Trim on output tag:
+123456789
+
+Trim comments:After the comment.
+
+Trim leading space:
+leading space
+leading space
+
+Trim trailing space:
+trailing spaceCombined:end