Repository: twigphp/Twig (php)

Issue:
Allow comments within components / function calls
It is so natural to comment out HTML attributes that it's easy to think that Twig supports this. So easy that even the Symfony UX documentation has an example.

```twig
// ...

<div class="toggle-password-container"> // Add "toggle-password-container" or a class that applies position: relative to this container.
    <label for="password">Password</label>
    <input
        id="password"
        name="password"
        type="password"
        {{ stimulus_controller('symfony/ux-toggle-password/toggle-password', {
                {# visibleLabel: 'Show password', // If you want to modify this label. #}
                {# visibleIcon: 'Some svg icon', // If you want to modify this icon. #}
                {# hiddenLabel: 'Hide password', // If you want to modify this label. #}
                {# hiddenIcon: 'Some svg icon', // If you want to modify this icon. #}
                buttonClasses: ['toggle-password-button'], // Add as many classes as you wish. "toggle-password-button" is needed to activate the default CSS.
        }) }}
    >
</div>

// ...
```

https://symfony.com/bundles/ux-toggle-password/current/index.html#usage-without-symfony-forms

I started to make a PR to fix the docs, but it's not easy to decide where to move it.  It'd be awesome if twig allowed this, as I frequently use it for in-line comments as well as disabling attributes.


Pull request description:
Add support for inline comments
Closes #4001

Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 6522bb12..8fcd9640 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
 # 3.15.0 (2024-XX-XX)
 
+ * Add support for inline comments
  * Add support for accessing class constants with the dot operator
  * Add `Profile::getStartTime()` and `Profile::getEndTime()`
  * Fix "ignore missing" when used on an "embed" tag
diff --git a/doc/templates.rst b/doc/templates.rst
index 1da89bcf..0cd90922 100644
--- a/doc/templates.rst
+++ b/doc/templates.rst
@@ -259,9 +259,9 @@ Go to the :doc:`tags<tags/index>` page to learn more about the built-in tags.
 Comments
 --------
 
-To comment-out part of a line in a template, use the comment syntax ``{# ...
-#}``. This is useful for debugging or to add information for other template
-designers or yourself:
+To comment-out part of a template, use the comment syntax ``{# ... #}``. This
+is useful for debugging or to add information for other template designers or
+yourself:
 
 .. code-block:: twig
 
@@ -271,6 +271,44 @@ designers or yourself:
         {% endfor %}
     #}
 
+.. versionadded:: 3.15
+
+    Inline comments were added in Twig 3.15.
+
+If you want to add comments inside a block, variable, or comment, use an inline
+comment. They start with ``#`` and continue to the end of the line:
+
+.. code-block:: twig
+
+    {{
+        # this is an inline comment
+        "Hello World"|upper
+        # this is an inline comment
+    }}
+
+    {{
+        {
+            # this is an inline comment
+            fruit: 'apple', # this is an inline comment
+            color: 'red', # this is an inline comment
+        }|join(', ')
+    }}
+
+Inline comments can also be on the same line as the expression:
+
+.. code-block:: twig
+
+    {{
+        "Hello World"|upper # this is an inline comment
+    }}
+
+As inline comments continue until the end of the current line, the following
+code does not work as ``}}``would be part of the comment:
+
+.. code-block:: twig
+
+    {{ "Hello World"|upper # this is an inline comment }}
+
 Including other Templates
 -------------------------
 
diff --git a/src/Lexer.php b/src/Lexer.php
index 982c87e3..17547912 100644
--- a/src/Lexer.php
+++ b/src/Lexer.php
@@ -48,6 +48,7 @@ class Lexer
     public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
     public const REGEX_DQ_STRING_DELIM = '/"/A';
     public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
+    public const REGEX_INLINE_COMMENT = '/#[^\n]*/A';
     public const PUNCTUATION = '()[]{}?:.,|';
 
     private const SPECIAL_CHARS = [
@@ -384,6 +385,10 @@ class Lexer
             $this->pushState(self::STATE_STRING);
             $this->moveCursor($match[0]);
         }
+        // inline comment
+        elseif (preg_match(self::REGEX_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
+            $this->moveCursor($match[0]);
+        }
         // unlexable
         else {
             throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);


Test patch:
diff --git a/tests/LexerTest.php b/tests/LexerTest.php
index 04e9930b..53a6cb15 100644
--- a/tests/LexerTest.php
+++ b/tests/LexerTest.php
@@ -534,4 +534,102 @@ bar
         yield ['日本では、春になると桜の花が咲きます。多くの人々は、公園や川の近くに集まり、お花見を楽しみます。桜の花びらが風に舞い、まるで雪のように見える瞬間は、とても美しいです。'];
         yield ['في العالم العربي، يُعتبر الخط العربي أحد أجمل أشكال الفن. يُستخدم الخط في تزيين المساجد والكتب والمخطوطات القديمة. يتميز الخط العربي بجماله وتناسقه، ويُعتبر رمزًا للثقافة الإسلامية.'];
     }
+
+    public function testInlineCommentWithHashInString()
+    {
+        $lexer = new Lexer(new Environment(new ArrayLoader()));
+        $stream = $lexer->tokenize(new Source('{{ "me # this is NOT an inline comment" }}', 'index'));
+        $stream->expect(Token::VAR_START_TYPE);
+        $stream->expect(Token::STRING_TYPE, 'me # this is NOT an inline comment');
+        $stream->expect(Token::VAR_END_TYPE);
+        $this->assertTrue($stream->isEOF());
+    }
+
+    /**
+     * @dataProvider getTemplateForInlineCommentsForVariable
+     */
+    public function testInlineCommentForVariable(string $template)
+    {
+        $lexer = new Lexer(new Environment(new ArrayLoader()));
+        $stream = $lexer->tokenize(new Source($template, 'index'));
+        $stream->expect(Token::VAR_START_TYPE);
+        $stream->expect(Token::STRING_TYPE, 'me');
+        $stream->expect(Token::VAR_END_TYPE);
+        $this->assertTrue($stream->isEOF());
+    }
+
+    public static function getTemplateForInlineCommentsForVariable()
+    {
+        yield ['{{
+            "me"
+            # this is an inline comment
+        }}'];
+        yield ['{{
+            # this is an inline comment
+            "me"
+        }}'];
+        yield ['{{
+            "me" # this is an inline comment
+        }}'];
+        yield ['{{
+            # this is an inline comment
+            "me" # this is an inline comment
+            # this is an inline comment
+        }}'];
+    }
+
+    /**
+     * @dataProvider getTemplateForInlineCommentsForBlock
+     */
+    public function testInlineCommentForBlock(string $template)
+    {
+        $lexer = new Lexer(new Environment(new ArrayLoader()));
+        $stream = $lexer->tokenize(new Source($template, 'index'));
+        $stream->expect(Token::BLOCK_START_TYPE);
+        $stream->expect(Token::NAME_TYPE, 'if');
+        $stream->expect(Token::NAME_TYPE, 'true');
+        $stream->expect(Token::BLOCK_END_TYPE);
+        $stream->expect(Token::TEXT_TYPE, 'me');
+        $stream->expect(Token::BLOCK_START_TYPE);
+        $stream->expect(Token::NAME_TYPE, 'endif');
+        $stream->expect(Token::BLOCK_END_TYPE);
+        $this->assertTrue($stream->isEOF());
+    }
+
+    public static function getTemplateForInlineCommentsForBlock()
+    {
+        yield ['{%
+            if true
+            # this is an inline comment
+        %}me{% endif %}'];
+        yield ['{%
+            # this is an inline comment
+            if true
+        %}me{% endif %}'];
+        yield ['{%
+            if true # this is an inline comment
+        %}me{% endif %}'];
+        yield ['{%
+            # this is an inline comment
+            if true # this is an inline comment
+            # this is an inline comment
+        %}me{% endif %}'];
+    }
+
+    /**
+     * @dataProvider getTemplateForInlineCommentsForComment
+     */
+    public function testInlineCommentForComment(string $template)
+    {
+        $lexer = new Lexer(new Environment(new ArrayLoader()));
+        $stream = $lexer->tokenize(new Source($template, 'index'));
+        $this->assertTrue($stream->isEOF());
+    }
+
+    public static function getTemplateForInlineCommentsForComment()
+    {
+        yield ['{#
+            Some regular comment # this is an inline comment
+        #}'];
+    }
 }