From 0d1821b0d3e2a46634c654d8224adaff5debd59a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 16 Sep 2024 12:03:13 +0200 Subject: [PATCH 1/8] fix typo --- doc/deprecated.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a648b054a..7222b4f66 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -252,7 +252,7 @@ Testing Utilities instead, which will be abstract in 4.0. * The data providers ``getTests()`` and ``getLegacyTests()`` on - ``Twig\Test\IntegrationTestCase`` are considered final als of Twig 3.13. + ``Twig\Test\IntegrationTestCase`` are considered final as of Twig 3.13. Environment ----------- From c5486b07b930bca151c2e983b724abcc58438769 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 16 Sep 2024 12:21:35 +0200 Subject: [PATCH 2/8] fix intl-extra tests --- extra/intl-extra/Tests/Fixtures/currency_names.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/intl-extra/Tests/Fixtures/currency_names.test b/extra/intl-extra/Tests/Fixtures/currency_names.test index bc2c54d02..7834d721a 100644 --- a/extra/intl-extra/Tests/Fixtures/currency_names.test +++ b/extra/intl-extra/Tests/Fixtures/currency_names.test @@ -10,7 +10,7 @@ return []; --EXPECT-- 0 -292 -292 +293 +293 US Dollar dollar des États-Unis From 52ded207d33d5bc721d19d5f1db8e8c39a8e1b76 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 14 Sep 2024 10:30:39 +0200 Subject: [PATCH 3/8] Allow Twig callable arguments to use camel or snake names --- CHANGELOG | 2 + doc/deprecated.rst | 3 + src/Util/CallableArgumentsExtractor.php | 39 +++++++---- tests/Util/CallableArgumentsExtractorTest.php | 66 +++++++++++++++++-- 4 files changed, 94 insertions(+), 16 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ad3841a5d..ea986469c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.15.0 (2024-XX-XX) + * Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature + They were automatically converted to snake-cased before * Deprecate the `attribute` function; use the `.` notation and wrap the name with parenthesis instead * Add support for argument unpackaging * Add JSON support for the file extension escaping strategy diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 7222b4f66..3a80f6322 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -286,3 +286,6 @@ Functions/Filters/Tests $twig->addFunction(new TwigFunction('upper', 'upper', [ 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12'), ])); + +* For variadic arguments, use snake-case for the argument name to ease the + transition to 4.0. diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index fedee6215..d8625169d 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -40,22 +40,25 @@ final class CallableArgumentsExtractor public function extractArguments(Node $arguments): array { $extractedArguments = []; + $extractedArgumentNameMap = []; $named = false; foreach ($arguments as $name => $node) { if (!\is_int($name)) { $named = true; - $name = $this->normalizeName($name); } elseif ($named) { throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } - $extractedArguments[$name] = $node; + $extractedArguments[$normalizedName = $this->normalizeName($name)] = $node; + $extractedArgumentNameMap[$normalizedName] = $name; } if (!$named && !$this->twigCallable->isVariadic()) { $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + $argName = $this->toSnakeCase($this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName()); + + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $argName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } return $extractedArguments; @@ -76,7 +79,7 @@ final class CallableArgumentsExtractor $optionalArguments = []; $pos = 0; foreach ($callableParameters as $callableParameter) { - $callableParameterName = $this->normalizeName($callableParameter->name); + $callableParameterName = $callableParameter->name; if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) { if ('start' === $callableParameterName) { $callableParameterName = 'low'; @@ -86,8 +89,9 @@ final class CallableArgumentsExtractor } $callableParameterNames[] = $callableParameterName; + $normalizedCallableParameterName = $this->normalizeName($callableParameterName); - if (\array_key_exists($callableParameterName, $extractedArguments)) { + if (\array_key_exists($normalizedCallableParameterName, $extractedArguments)) { if (\array_key_exists($pos, $extractedArguments)) { throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -95,13 +99,13 @@ final class CallableArgumentsExtractor if (\count($missingArguments)) { throw new SyntaxError(\sprintf( 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', - $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $callableParameterNames), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->node->getTemplateLine(), $this->node->getSourceContext()); } $arguments = array_merge($arguments, $optionalArguments); - $arguments[] = $extractedArguments[$callableParameterName]; - unset($extractedArguments[$callableParameterName]); + $arguments[] = $extractedArguments[$normalizedCallableParameterName]; + unset($extractedArguments[$normalizedCallableParameterName]); $optionalArguments = []; } elseif (\array_key_exists($pos, $extractedArguments)) { $arguments = array_merge($arguments, $optionalArguments); @@ -118,7 +122,7 @@ final class CallableArgumentsExtractor $missingArguments[] = $callableParameterName; } else { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->toSnakeCase($callableParameterName), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } } @@ -128,7 +132,13 @@ final class CallableArgumentsExtractor if (\is_int($key)) { $arbitraryArguments->addElement($value); } else { - $arbitraryArguments->addElement($value, new ConstantExpression($key, $this->node->getTemplateLine())); + $originalKey = $extractedArgumentNameMap[$key]; + if ($originalKey !== $this->toSnakeCase($originalKey)) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Using "snake_case" for variadic arguments is required for a smooth upgrade with Twig 4.0; rename "%s" to "%s" in "%s" at line %d.', $originalKey, $this->toSnakeCase($originalKey), $this->node->getSourceContext()->getName(), $this->node->getTemplateLine())); + } + $arbitraryArguments->addElement($value, new ConstantExpression($this->toSnakeCase($originalKey), $this->node->getTemplateLine())); + // I Twig 4.0, don't convert the key: + // $arbitraryArguments->addElement($value, new ConstantExpression($originalKey, $this->node->getTemplateLine())); } unset($extractedArguments[$key]); } @@ -151,7 +161,7 @@ final class CallableArgumentsExtractor throw new SyntaxError( \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', - \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $callableParameterNames) + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)) ), $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() @@ -163,7 +173,12 @@ final class CallableArgumentsExtractor private function normalizeName(string $name): string { - return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); + return strtolower(str_replace('_', '', $name)); + } + + private function toSnakeCase(string $name): string + { + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z0-9])([A-Z])/'], '\1_\2', $name)); } private function getCallableParameters(): array diff --git a/tests/Util/CallableArgumentsExtractorTest.php b/tests/Util/CallableArgumentsExtractorTest.php index cfea0f8a9..182283183 100644 --- a/tests/Util/CallableArgumentsExtractorTest.php +++ b/tests/Util/CallableArgumentsExtractorTest.php @@ -12,15 +12,20 @@ namespace Twig\Tests\Util; */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Error\SyntaxError; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; +use Twig\Node\Expression\VariadicExpression; use Twig\Node\Node; +use Twig\Source; use Twig\TwigFunction; use Twig\Util\CallableArgumentsExtractor; class CallableArgumentsExtractorTest extends TestCase { + use ExpectDeprecationTrait; + public function testGetArguments() { $this->assertEquals(['U', null], $this->getArguments('date', 'date', ['format' => 'U', 'timestamp' => null])); @@ -29,7 +34,7 @@ class CallableArgumentsExtractorTest extends TestCase public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments() { $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date".'); + $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date" in "test.twig" at line 2.'); $this->getArguments('date', 'date', ['timestamp' => 123456, 'Y-m-d']); } @@ -37,7 +42,7 @@ class CallableArgumentsExtractorTest extends TestCase public function testGetArgumentsWhenArgumentIsDefinedTwice() { $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Argument "format" is defined twice for function "date".'); + $this->expectExceptionMessage('Argument "format" is defined twice for function "date" in "test.twig" at line 2.'); $this->getArguments('date', 'date', ['Y-m-d', 'format' => 'U']); } @@ -80,6 +85,54 @@ class CallableArgumentsExtractorTest extends TestCase $this->assertEquals(['arg1'], $this->getArguments('custom_static_function', __CLASS__.'::customStaticFunction', ['arg1' => 'arg1'])); } + /** + * @dataProvider getGetArgumentsConversionData + */ + public function testGetArgumentsConversion($arg1, $arg2) + { + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg1) => '';"), [$arg1 => null])); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg2) => '';"), [$arg2 => null])); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg1) => '';"), [$arg2 => null])); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg2) => '';"), [$arg1 => null])); + } + + public static function getGetArgumentsConversionData() + { + yield ['some_name', 'some_name']; + yield ['someName', 'some_name']; + yield ['no_svg', 'noSVG']; + yield ['error_404', 'error404']; + yield ['errCode_404', 'err_code_404']; + yield ['errCode404', 'err_code_404']; + yield ['aBc', 'a_b_c']; + yield ['aBC', 'a_b_c']; + } + + /** + * @group legacy + */ + public function testGetArgumentsConversionForVariadics() + { + $this->expectDeprecation('Since twig/twig 3.15: Using "snake_case" for variadic arguments is required for a smooth upgrade with Twig 4.0; rename "someNumberVariadic" to "some_number_variadic" in "test.twig" at line 2.'); + + $this->assertEquals([ + new ConstantExpression('a', 0), + new ConstantExpression(12, 0), + new VariadicExpression([ + new ConstantExpression('some_text_variadic', 2), new ConstantExpression('a', 0), + new ConstantExpression('some_number_variadic', 2), new ConstantExpression(12, 0), + ], 2), + ], $this->getArguments('custom', eval("return fn (string \$someText, int \$some_number, ...\$args) => '';"), ['some_text' => 'a', 'someNumber' => 12, 'some_text_variadic' => 'a', 'someNumberVariadic' => 12], true)); + } + + public function testGetArgumentsError() + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Value for argument "some_name" is required for function "custom_static_function" in "test.twig" at line 2.'); + + $this->getArguments('custom_static_function', [$this, 'customFunctionSnakeCamel'], ['someCity' => 'Paris']); + } + public function testResolveArgumentsWithMissingParameterForArbitraryArguments() { $this->expectException(SyntaxError::class); @@ -119,6 +172,10 @@ class CallableArgumentsExtractorTest extends TestCase { } + public function customFunctionSnakeCamel($someName, $some_city) + { + } + public function customFunctionWithArbitraryArguments() { } @@ -126,14 +183,15 @@ class CallableArgumentsExtractorTest extends TestCase private function getArguments(string $name, $callable, array $args, bool $isVariadic = false): array { $function = new TwigFunction($name, $callable, ['is_variadic' => $isVariadic]); - $node = new ExpressionCall($function, new Node([]), 0); + $node = new ExpressionCall($function, new Node([]), 2); + $node->setSourceContext(new Source('', 'test.twig')); foreach ($args as $name => $arg) { $args[$name] = new ConstantExpression($arg, 0); } $arguments = (new CallableArgumentsExtractor($node, $function))->extractArguments(new Node($args)); foreach ($arguments as $name => $argument) { - $arguments[$name] = $argument->getAttribute('value'); + $arguments[$name] = $isVariadic ? $argument : $argument->getAttribute('value'); } return $arguments; From 37fa03681eb05a807b2859895c574a6e22743dba Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 19 Sep 2024 11:48:28 +0200 Subject: [PATCH 4/8] fix the handling of predefined arguments in CallExpression --- src/Node/Expression/CallExpression.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 920f65caf..5154d8bf9 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -334,7 +334,7 @@ abstract class CallExpression extends AbstractExpression [ 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], - ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()), + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), 'function' => (new TwigFunction( $this->hasAttribute('name') ? $this->getAttribute('name') : $current->getName(), $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), @@ -344,7 +344,7 @@ abstract class CallExpression extends AbstractExpression 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], - ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()), + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), 'filter' => (new TwigFilter( $this->getAttribute('name'), $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), @@ -354,7 +354,7 @@ abstract class CallExpression extends AbstractExpression 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], - ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()), + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), }); return $this->getAttribute('twig_callable'); From ebbbaf7069b402b4d1b9baeb569cd9ac66be911b Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 19 Sep 2024 13:43:42 +0200 Subject: [PATCH 5/8] Use a more precise return type in AbstractTwigCallable --- src/AbstractTwigCallable.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index 2e9b34d18..d85f0f861 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -104,6 +104,9 @@ abstract class AbstractTwigCallable implements TwigCallableInterface return $this->options['needs_context']; } + /** + * @return static + */ public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self { $new = clone $this; From 07b626a96fed66fe03a67b9b0699d0f8418643eb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 19 Sep 2024 23:13:12 +0200 Subject: [PATCH 6/8] Fix deprecation messages --- src/ExpressionParser.php | 4 ++-- src/Node/Expression/CallExpression.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 07786eeac..d09cfbb48 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -319,7 +319,7 @@ class ExpressionParser } /** - * @deprecated since 3.11, use parseSequenceExpression() instead + * @deprecated since Twig 3.11, use parseSequenceExpression() instead */ public function parseArrayExpression() { @@ -360,7 +360,7 @@ class ExpressionParser } /** - * @deprecated since 3.11, use parseMappingExpression() instead + * @deprecated since Twig 3.11, use parseMappingExpression() instead */ public function parseHashExpression() { diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 5154d8bf9..8e999c7eb 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -128,7 +128,7 @@ abstract class CallExpression extends AbstractExpression } /** - * @deprecated since 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead + * @deprecated since Twig 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead */ protected function getArguments($callable, $arguments) { @@ -258,7 +258,7 @@ abstract class CallExpression extends AbstractExpression } /** - * @deprecated since 3.12 + * @deprecated since Twig 3.12 */ protected function normalizeName(string $name): string { From 3be417d2ebafadd6c74e7347e0747e094835386b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 20 Sep 2024 11:43:16 +0200 Subject: [PATCH 7/8] Improve date methods phpdoc --- src/Extension/CoreExtension.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index e0e6ed5d3..59937aec5 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -443,9 +443,9 @@ final class CoreExtension extends AbstractExtension * * {{ post.published_at|date("m/d/Y") }} * - * @param \DateTimeInterface|\DateInterval|string $date A date - * @param string|null $format The target format, null to use the default - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * @param \DateTimeInterface|\DateInterval|string|int|null $date A date, a timestamp or null to use the current time + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged */ public function formatDate($date, $format = null, $timezone = null): string { @@ -466,8 +466,8 @@ final class CoreExtension extends AbstractExtension * * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} * - * @param \DateTimeInterface|string $date A date - * @param string $modifier A modifier string + * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time + * @param string $modifier A modifier string * * @return \DateTime|\DateTimeImmutable * @@ -506,8 +506,8 @@ final class CoreExtension extends AbstractExtension * {# do something #} * {% endif %} * - * @param \DateTimeInterface|string|null $date A date or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged * * @return \DateTime|\DateTimeImmutable */ From ac88c9ae757d3eebee6ef5721bed591d131a551e Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Thu, 19 Sep 2024 11:57:45 +0200 Subject: [PATCH 8/8] [Doc] Document about `html` and `html_attr` strategies Co-authored-by: Fabien Potencier --- doc/filters/escape.rst | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/doc/filters/escape.rst b/doc/filters/escape.rst index 574724e35..cf1ad61ef 100644 --- a/doc/filters/escape.rst +++ b/doc/filters/escape.rst @@ -39,7 +39,8 @@ And here is how to escape variables included in JavaScript code: The ``escape`` filter supports the following escaping strategies for HTML documents: -* ``html``: escapes a string for the **HTML body** context. +* ``html``: escapes a string for the **HTML body** context, + or for HTML attributes values **inside quotes**. * ``js``: escapes a string for the **JavaScript** context. @@ -50,7 +51,8 @@ documents: * ``url``: escapes a string for the **URI or parameter** contexts. This should not be used to escape an entire URI; only a subcomponent being inserted. -* ``html_attr``: escapes a string for the **HTML attribute** context. +* ``html_attr``: escapes a string for the **HTML attribute** context, + **without quotes** around HTML attribute values. Note that doing contextual escaping in HTML documents is hard and choosing the right escaping strategy depends on a lot of factors. Please, read related @@ -90,6 +92,27 @@ to learn more about this topic. {{ var|escape(strategy)|raw }} {# won't be double-escaped #} {% endautoescape %} +.. tip:: + + The ``html_attr`` escaping strategy can be useful when you need to + escape a **dynamic HTML attribute name**: + + .. code-block:: html+twig + +

+ + It can also be used for escaping a **dynamic HTML attribute value** + if it is not quoted, but this is **less performant**. + Instead, it is recommended to quote the HTML attribute value and use + the ``html`` escaping strategy: + + .. code-block:: html+twig + +

+ + {# is equivalent to, but is less performant #} +

+ Custom Escapers ---------------