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