Commit Graph

7449 Commits

Author SHA1 Message Date
Fabien Potencier 447d0b2331 Fix sandbox __toString bypasses 2026-05-20 00:18:59 +02:00
Fabien Potencier 7f3056a28e security #cve-2026-46640 Fix sandbox bypass: PHP code injection via _self / import macro reference (alexandre-daubois, fabpot)
This PR was merged into the twig-3.x branch.
2026-05-19 23:43:36 +02:00
Fabien Potencier ea3f7a2844 Validate macro name in MacroReferenceExpression constructor
The name passed to MacroReferenceExpression is emitted as raw PHP in
compile() via "->{$name}(...)". Callers were expected to validate
the name, but a missing check led to CVE-2026-XXXXX (PHP code injection
via _self / import macro reference): defense-in-depth, validate the
name in the constructor so the class is safe by construction.
2026-05-19 23:42:31 +02:00
Alexandre Daubois 324fa60545 Fix sandbox bypass: PHP code injection via _self / import macro reference 2026-05-19 23:41:59 +02:00
Fabien Potencier aeb37f4801 Fix deprecations in tests 2026-05-19 23:05:30 +02:00
Fabien Potencier 1a023fef43 security #cve-2026-46638 Fix sandbox bypass in the {% sandbox %} tag when including a preloaded template (alexandre-daubois)
This PR was merged into the twig-3.x branch.
2026-05-19 23:02:22 +02:00
Alexandre Daubois 819c6a89fe Fix sandbox bypass in the {% sandbox %} tag when including a preloaded template 2026-05-19 23:00:58 +02:00
Fabien Potencier ea7879a67f security #cve-2026-46633 Fix sandbox bypass: PHP code injection via {% use %} template name (alexandre-daubois, fabpot)
This PR was merged into the twig-3.x branch.
2026-05-19 23:00:20 +02:00
Fabien Potencier 679447fa29 Encode single quotes as \x27 in Compiler::string()
This is a defense-in-depth measure: callers must always concatenate the
result into a double-quoted PHP context, but if one ever (mistakenly)
embeds it inside a single-quoted PHP literal, an attacker-controlled
single quote in the source value could break out of that context. The
previous commit fixed exactly such a bug in ModuleNode for the {% use %}
template name.

Encoding ' as the hex escape \x27 guarantees that the emitted PHP source
never contains a literal single quote derived from user input, while the
decoded runtime value is unchanged. \' is not used because it is not a
recognized escape sequence in PHP double-quoted strings (the backslash
would be kept literally).
2026-05-19 22:58:27 +02:00
Alexandre Daubois e9ff55f691 Fix sandbox bypass: PHP code injection via {% use %} template name 2026-05-19 22:50:45 +02:00
Fabien Potencier afe54db2e5 security #cve-2026-46629 Fix unbounded memoisation of IntlDateFormatter / NumberFormatter (alexandre-daubois)
This PR was merged into the twig-3.x branch.
2026-05-19 22:47:15 +02:00
Alexandre Daubois 6add9066fc Fix unbounded memoisation of IntlDateFormatter / NumberFormatter 2026-05-19 22:44:48 +02:00
Fabien Potencier b675555ea2 security #cve-2026-46637 Fix XSS and pre-escape input on HTML-emitting filters in the extras (nicolas-grekas)
This PR was squashed before being merged into the twig-3.x branch.
2026-05-19 22:37:15 +02:00
Fabien Potencier e494400ce4 Fix deprecation 2026-05-19 22:05:26 +02:00
Fabien Potencier a8eb5a8a74 security #cve-2026-46639 Fix sandbox bypass in object destructuring assignment (alexandre-daubois)
This PR was merged into the twig-3.x branch.
2026-05-19 21:37:04 +02:00
Fabien Potencier c8dfd62064 security #cve-2026-24425 Fix sandbox bypass: propagate Source to checkArrow for source-policy sandboxing (fabpot)
This PR was merged into the twig-3.x branch.
2026-05-19 21:30:51 +02:00
Fabien Potencier 50535718b8 Bump version 2026-05-19 17:02:22 +02:00
Fabien Potencier 058a1789a9 Bump version 2026-05-17 08:41:52 +01:00
Fabien Potencier 0dade995be Prepare the 3.25.0 release v3.25.0 2026-05-17 08:41:26 +01:00
Fabien Potencier 9519a22752 Update CHANGELOG 2026-05-17 08:41:04 +01:00
Fabien Potencier fd0760d972 feature #4795 Lazy load EscaperRuntime in EscaperExtension (GromNaN)
This PR was merged into the 3.x branch.

Discussion
----------

Lazy load `EscaperRuntime` in `EscaperExtension`

This allows overriding `EscaperRuntime` via a custom runtime loader.

Previously, `EscaperExtension::setEnvironment()` was calling `$environment->getRuntime(EscaperRuntime::class)` eagerly. Since this method is called from `Environment::__construct()`, the runtime was resolved before any custom runtime loader could be injected, making it impossible to override `EscaperRuntime`.

- Required by https://github.com/symfony/symfony/pull/63929

Commits
-------

b73ab8cfd9 Lazy load EscaperRuntime in EscaperExtension
2026-05-17 07:52:38 +01:00
Fabien Potencier d0579ededc Fix sandbox bypass: propagate sandbox state to checkArrow for source-policy sandboxing 2026-05-16 18:56:08 +01:00
Fabien Potencier cec6bfb32b feature #4800 Add a needs_is_sandboxed option for filters, functions, and tests (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Add a `needs_is_sandboxed` option for filters, functions, and tests

This PR introduces a new `needs_is_sandboxed` option for Twig callables.

When set to `true`, Twig passes the current sandbox state (enabled/disabled) as a boolean to the callable. This lets filters, functions, and tests adapt their behavior depending on whether the sandbox is enabled or not.

Commits
-------

5462817da0 Add a `needs_is_sandboxed` option for filters, functions, and tests
2026-05-16 18:53:36 +01:00
Fabien Potencier 5462817da0 Add a needs_is_sandboxed option for filters, functions, and tests 2026-05-16 18:52:17 +01:00
Fabien Potencier c3f37a5195 Bump version 2026-05-16 09:51:43 +01:00
Fabien Potencier 57c2c0f0ee bug #4797 Make embeds deterministic (itsalmostchristmas)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Make embeds deterministic

Using `{% embed 'foo.twig' %}...{% endembed %}` makes compiled templates non-deterministic. This poses a problem for developers who wish to provide reproducible, pre-compiled builds of Twig templates.

The cause is `mt_rand()` used to generate template indices. The change I propose uses an incrementing counter instead. To keep tests passing, the number must be non-zero and unique per PHP process (reusing indices across `Twig\Parser` instances causes fatal errors).

A demonstration of the problem is available on the [Twig Playground](https://twig.symfony.com/play?data=eyJ0ZW1wbGF0ZXMiOltbImluZGV4LnR3aWciLCJ7JSBlbWJlZCAnZGlhbG9nLnR3aWcnICV9XG4gICAgeyUgYmxvY2sgbWVzc2FnZSAlfVxuICAgICAgICBUYXNrIGZhaWxlZCBzdWNjZXNzZnVsbHlcbiAgICB7JSBlbmRibG9jayAlfVxueyUgZW5kZW1iZWQgJX0iXSxbImRpYWxvZy50d2lnIiwiPGRpYWxvZz57JSBibG9jayBtZXNzYWdlICV9eyUgZW5kYmxvY2sgJX08L2RpYWxvZz4iXV0sImNvbnRleHQiOnt9LCJ2ZXJzaW9uIjoiMy4yMS4xIiwib3B0aW9ucyI6eyJzdHJpY3RfdmFyaWFibGVzIjp0cnVlLCJjaGFyc2V0IjoiVVRGLTgiLCJhdXRvZXNjYXBlIjoiIn19), where the compiled output of `index.twig` differs on every recompilation.

Commits
-------

2b4aa4458a Make embeds deterministic
2026-05-16 09:48:31 +01:00
itsalmostchristmas 2b4aa4458a Make embeds deterministic 2026-05-16 09:48:23 +01:00
Nicolas Grekas e36489d352 Pre-escape HTML input on inline_css and inky_to_html filters
Both filters consume HTML on the input side. Adding `pre_escape => 'html'`
makes the autoescaper escape attacker-controlled inputs before the filters
process them, so they are no longer reachable via `{{ user_input|inline_css }}`
or `{{ user_input|inky_to_html }}` without an explicit `|raw`.
2026-05-15 15:14:14 +02:00
Nicolas Grekas 84982072c7 Fix XSS by adjusting is_safe annotation on HTML-emitting filters
The `html_to_markdown` filter emits plain Markdown text, so the
`is_safe` annotation is dropped entirely and autoescape now handles
its output according to the surrounding context.

The `markdown_to_html` and `inline_css` filters emit HTML, not text
safe in every escaping context, so `is_safe => ['all']` produced
unescaped HTML when their output was interpolated into a JS, CSS or
URL context. The annotation is now `is_safe => ['html']`.
2026-05-15 15:14:02 +02:00
Fabien Potencier ab8dd4aadd minor #4799 [Doc] Document loose comparison in the in operator (Amoifr)
This PR was merged into the 3.x branch.

Discussion
----------

[Doc] Document loose comparison in the `in` operator

Clarifies the comparison semantics of the `in` operator in the *Containment Operators* section of `doc/templates.rst`:

- `in` uses a loose comparison (similar to `==`) for sequences, mappings, and `Traversable` objects, like PHP's `in_array()`.
- Pointer to `same as` for strict comparisons.
- Warning about boolean left operands (e.g. `true in ['foo', 'bar']` returns `true`), which surprises users.
- Note that string containment only accepts `string`, `int`, `float` on the left.

Closes #4650 (or at least the documentation half of it — the `Investigate before 4.x` label suggests the maintainers may want to revisit the actual semantics in 4.x; this PR addresses `@gorenstein`'s explicit request to "at least improve the documentation").

Commits
-------

8af8e93970 [Doc] Document loose comparison in the `in` operator
2026-05-14 18:42:46 +01:00
Fabien Potencier f765a753fb minor #4798 [Doc] Reword whitespace control note about first-newline removal (Amoifr)
This PR was merged into the 3.x branch.

Discussion
----------

[Doc] Reword whitespace control note about first-newline removal

Fixes #4720.

The wording on line 1224 of `doc/templates.rst` ("removal of the first newline **inherited from PHP**") was misleading — Twig does not inherit anything from PHP, it deliberately implements the same behavior (just like Jinja does, which uses *"like in PHP"* in its own documentation).

The note already exists a few lines above (line 1210: *"The first newline after a template tag is removed automatically (like in PHP)."*), so the second mention can just point back to it instead of duplicating the (incorrect) attribution.

Commits
-------

6904b23fb6 [Doc] Reword whitespace control note about first-newline removal
2026-05-14 18:41:00 +01:00
Pascal CESCON - Amoifr 8af8e93970 [Doc] Document loose comparison in the in operator
Clarifies that `in` performs a loose comparison on sequences, mappings,
and `Traversable` objects (similar to `==`), points to `same as` for
strict comparisons, and warns about boolean left operands.

Refs #4650
2026-05-08 18:35:48 +02:00
Pascal CESCON - Amoifr 6904b23fb6 [Doc] Reword whitespace control note about first-newline removal
The previous wording ("inherited from PHP") was misleading: Twig does
not inherit anything from PHP, it deliberately implements the same
behavior, like Jinja does. The behavior was already explained earlier
in the same section, so just point back to it instead.
2026-05-07 15:06:12 +02:00
Fabien Potencier 99d95dd6b8 minor #4796 Stop publishing extra package minor versions with no changes (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Stop publishing extra package minor versions with no changes

Commits
-------

48733e0f09 Stop publishing extra package minor versions with no changes
2026-05-01 09:42:07 +02:00
Fabien Potencier 48733e0f09 Stop publishing extra package minor versions with no changes 2026-05-01 09:41:08 +02:00
Jérôme Tamarelle b73ab8cfd9 Lazy load EscaperRuntime in EscaperExtension
Previously, setEnvironment() called getRuntime(EscaperRuntime::class) eagerly,
which prevented overriding EscaperRuntime via a custom runtime loader since
Environment::__construct() calls setEnvironment() before any loader can be injected.
2026-04-30 13:43:11 +02:00
Alexandre Daubois 3fe13f98f8 Fix sandbox bypass in object destructuring assignment 2026-04-28 12:59:06 +02:00
Fabien Potencier ecb310e129 minor #4787 Reword parent-child analogy in doc/tags/extends.rst (llupa)
This PR was merged into the 3.x branch.

Discussion
----------

Reword parent-child analogy in `doc/tags/extends.rst`

Commits
-------

64a70e96a6 Replace parent-child analogy in `doc/tags/extends.rst`
2026-04-11 11:27:40 +02:00
Fabien Potencier f3d5a9f071 Fix typo 2026-03-22 10:08:53 +01:00
llupa 64a70e96a6 Replace parent-child analogy in doc/tags/extends.rst 2026-03-20 09:50:10 +01:00
Fabien Potencier 76688da07c minor #4786 doc: Add missing toctree entries and fix ordering (llupa)
This PR was merged into the 3.x branch.

Discussion
----------

doc: Add missing toctree entries and fix ordering

- Add missing `sequence` and `mapping` to tests/index.rst toctree
- Fix alphabetical ordering of `guard`/`flush` in tags/index.rst
- Add orphaned `operators_precedence` to main index.rst toctree

Commits
-------

84a433ed28 doc: Add missing toctree entries and fix ordering
2026-03-19 15:32:58 +01:00
llupa 84a433ed28 doc: Add missing toctree entries and fix ordering
- Add missing `sequence` and `mapping` to tests/index.rst toctree
- Fix alphabetical ordering of `guard`/`flush` in tags/index.rst
- Add orphaned `operators_precedence` to main index.rst toctree
2026-03-19 15:18:09 +01:00
Fabien Potencier 4d44d3cb92 Fix CHANGELOG 2026-03-17 22:37:10 +01:00
Fabien Potencier 4bfdb9ed6c Bump version 2026-03-17 22:31:57 +01:00
Fabien Potencier a6769aefb3 Prepare the 3.24.0 release v3.24.0 2026-03-17 22:31:11 +01:00
Fabien Potencier 8abec84013 minor #4784 Add two tests for error conditions in #3930 (mpdude)
This PR was merged into the 3.x branch.

Discussion
----------

Add two tests for error conditions in #3930

Here are two test cases for potential errors in `html_attr_merge`, asked for by `@fabpot` in https://github.com/twigphp/Twig/pull/3930#pullrequestreview-3956645279.

Commits
-------

25bfb5957c Add two tests for error conditions in #3930
2026-03-17 11:29:37 +01:00
Fabien Potencier 2fcc93954d Fix CS 2026-03-17 08:26:25 +01:00
Matthias Pigulla 25bfb5957c Add two tests for error conditions in #3930 2026-03-17 08:24:08 +01:00
Fabien Potencier 8b93364bf6 feature #3930 Add an html_attr function to make outputting HTML attributes easier (mpdude, polarbirke)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Add an `html_attr` function to make outputting HTML attributes easier

**Updated:** This description has been updated to reflect changes from the discussion up to https://github.com/twigphp/Twig/pull/3930#issuecomment-3870445987.

This PR suggests adding an `html_attr` function and two filters `html_attr_merge` and `html_attr_type`. Together, they are intended to make it easier to collect HTML attributes in arrays, in order to pass them in Twig to included templates or macros, and to ultimately print such attribute sets as.

`html_attr_merge` can be used to either merge such arrays over default values, or to override (say, inside a macro) particular values in a given attribute array. As described in #3907, it favors overwriting simple (scalar) attribute values and appending to multi-valued attributes over all the other operations one could conceive (like, for example, replacing a list of two CSS `class` names with two other ones). This is a design decision to keep the API simple and optimized for the primary use case that I see. But, since we're mostly dealing with arrays after all, users are free to do in parallel any other kind of array wrangling they see fit.

So, this PR is _not_ trying to design a full-fledged, object-oriented API with all the necessary methods to add, replace, remove attributes; to add, change or toggle elements in "list" style attributes like `class`; to provide extension points for custom (arbitrary) attributes or to provide a fluent API to do all that from within PHP code. See https://github.com/symfony/ux/issues/3269 for a Symfony UX component RFC that does that.

A little bit of special case handling is present for `aria-*`, `data-*` and inline CSS `style` attributes. But apart from that, there is no special knowledge about the attributes defined in HTML, ARIA or other standards, nor about the structure and sementic of attribute values. The approach works in a generic way, so it should be possible to use it for many custom attributes as well.

In order to support "list" style attributes like `class`, `aria-labelledby` or `srcset` that may come in different flavors, users have to be disciplined and consistently use iterables (arrays) to represent such attribute values, possibly assisted by the `html_attr_type` filter (see below).

When printing attributes, names and values are escaped. For names, the `html_attr_relaxed` strategy (#4743) is used.

#### Motivation and practical examples

I have seen repeating patterns when dealing with HTML attributes in Twig templates and macros. Typical examples can be found in Symfony's form theme, where an `attr` variable is present in various blocks.

https://github.com/symfony/symfony/blob/4a5d8cf03e1e31d1a7591921c6fa1fe7ec1c2015/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig#L453-L458

```twig
{%- block widget_attributes -%}
    id="{{ id }}" name="{{ full_name }}"
    {%- if disabled %} disabled="disabled"{% endif -%}
    {%- if required %} required="required"{% endif -%}
    {{ block('attributes') }}
{%- endblock widget_attributes -%}
```

Could be along the lines of:

`{{ html_attr(attr, { id, name: full_name, disabled: disabled ? true : false, required : required ? true : false }) }}`.

If `disabled` and `required` were guaranteed to be booleans (I haven't checked), even better:

`{{ html_attr(attr, { id, name: full_name, disabled, required }) }}`

https://github.com/symfony/symfony/blob/4a5d8cf03e1e31d1a7591921c6fa1fe7ec1c2015/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig#L347-L360

```twig
    {%- set attr = {} -%}
    {%- set aria_describedby = [] -%}
    {%- if help -%}
        {%- set aria_describedby = aria_describedby|merge([id ~ '_help']) -%}
    {%- endif -%}
    {%- if errors|length > 0 -%}
        {%- set aria_describedby = aria_describedby|merge(errors|map((_, index) => id ~ '_error' ~ (index + 1))) -%}
    {%- endif -%}
    {%- if aria_describedby|length > 0 -%}
        {%- set attr = attr|merge({'aria-describedby': aria_describedby|join(' ')}) -%}
    {%- endif -%}
    {%- if errors|length > 0 -%}
        {%- set attr = attr|merge({'aria-invalid': 'true'}) -%}
    {%- endif -%}
```

Could be:

```twig
  {%- set attr = {}|html_attr_merge(
    help ? { 'aria-describedby': [id ~ '_help'] },
    errors|length ? { 'aria-invalid': true, 'aria-describedby': errors|map((_, index) => id ~ '_error' ~ (index + 1)) }
  ) -%}
```

https://github.com/symfony/symfony/blob/4a5d8cf03e1e31d1a7591921c6fa1fe7ec1c2015/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig#L470-L481

```twig
{% block attributes -%}
    {%- for attrname, attrvalue in attr -%}
        {{- ' ' -}}
        {%- if attrname in ['placeholder', 'title'] -%}
            {{- attrname }}="{{ translation_domain is same as(false) or attrvalue is null ? attrvalue : attrvalue|trans(attr_translation_parameters, translation_domain) }}"
        {%- elseif attrvalue is same as(true) -%}
            {{- attrname }}="{{ attrname }}"
        {%- elseif attrvalue is not same as(false) -%}
            {{- attrname }}="{{ attrvalue }}"
        {%- endif -%}
    {%- endfor -%}
{%- endblock attributes -%}
```

This should basically be the same as `{{ html_attr(attr) }}`, ignoring edge cases for `null` values. Handling of the `translation_domain` might require a preceding `html_attr_merge` step to replace values with translations.

Finally,

```twig
    {% set id = 'id value' %}
    {% set href = 'href value' %}
    {% set disabled = true %}

    <div {{ html_attr(
        { id, href },
        disabled ? { 'aria-disabled': 'true' },
        not disabled ? { 'aria-enabled' : true },
        { class: ['zero', 'first'] },
        { class: ['second'] },
        true ? { class: 'third' },
        { style: { color: 'red' } },
        { style: { 'background-color': 'green' } },
        { style: { color: 'blue' } },
        { 'data-test': 'some value' },
        { 'data-test': 'other value', 'data-bar': 'baz' }},
        { 'dangerous=yes foo' : 'xss' },
        { style: ['text-decoration: underline'] },
    ) }}></div>
```

will generate HTML markup:

```
<div id="id value" href="href value" aria-disabled="true" class="zero first second third" style="color: red; background-color: green; color: blue; text-decoration: underline;" data-test="other value" data-bar="baz" dangerous&#x3D;yes&#x20;foo="xss"></div>
```

#### Details on `html_attr_merge`

This filter merges an `attr` style array with one or several other arrays given as arguments. All of those arrays should reasonably be mappings, i. e. use keys that denote attribute names, and not sequences or lists with numeric keys.

Empty arrays, empty strings or false values in the argument list will be ignored, which can be used to conditionally include values in the merge list like so:

```twig
{% set attr = attr|html_attr_merge(
  condition ? { attrname: "attrvalue", other: "value" }
) %}
```

The merging of attribute values is similar to PHP's `array_merge` function. Latter (right) values generally override former (left) values, as follows:

When two values are to be merged and both are either scalars or objects, the latter (right) value overrides the previous (left) one.

When both values are iterables, `array_merge`/spread operator behavior is used: Numeric indices will be appended, whereas non-numeric ones will be replaced. This can be used to override designated elements in sets like CSS classes:

```twig
{% set attr = { class: ['foo', 'bar'] }|html_attr_merge(
  { class: { importance: 'normal' } },
  critical ? { class: { importance: 'high' } }
) %}
```

To provide more flexibility with regards to different merging strategies, the `MergeInterface` is provided as a flex point for advanced use cases of power users.

* When an attribute value that represents a "right hand side" value has to be merged and implements `MergeInterface`, its `mergeInto()` method will be passed the previous (left hand side) value. That method will return the merge result.

* Otherwise, when the "left hand side" value implements `MergeInterface`, `appendFrom()` will be passed the new (right hand side) value. Again, that method returns the merge result.

Other combinations of values are rejected, an exception is thrown. This is a design decision to clearly and early notify users of combinations that might have unclear or unpredictable results, like merging a string like `'foo'` with an array like `['bar', 'baz']` for a `class` attribute – should this override, since one value is a scalar, or append, since the other one is an array?

#### Details on `html_attr`

The `html_attr()` function prints attribute-arrays as HTML. It will perform appropriate escaping of attribute names and values.

One or several attribute arrays can be passed to `html_attr`, and `html_attr_merge` will be used first to merge those.

In general, scalar attribute values (including the empty string `''`) will be printed as-is. For booleans and `null` values, the following extra rules apply:

* For `aria-*`, boolean `true` and `false` will be coalesced to `"true"` and `"false"`, respectively.
* For `data-*`, boolean `true` will be coalesced to `"true"`, and non-scalar values will be JSON-encoded
* Otherwise, `false` and `null` attribute values will always omit printing of the attribute.
* `true` values will print the attribute as `attributeName=""`. This is equivalent to printing `<... attributeName>`, but is X(HT)ML compliant. The user-agent will fill in the attribute's _empty default value_.

These rules derive from the comparison provided at https://github.com/symfony/ux/issues/3269#issuecomment-3708588342 that shows how React and Vue as front-end frameworks behave in the same situation.

For values that implement `AttributeValueInterface`, its `getValue(): ?string` method will be called first. The attribute will be omitted for a `null` return value, otherwise printed with the returned string.

This interface could be used to provide (outside the scope of this PR or even outside Twig itself) classes that could e. g. help building more complex attribute values, like for image `srcset`. But the primary reason for adding it was to be able to deal with attributes values that are lists or hashes and need to be printed in different ways:

* attributes like `class` or `aria-labelledby` use [space-separated tokens](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#space-separated-tokens) as their value
* `srcset` or `sizes` for `<img>` use [comma-separated tokens](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#comma-separated-tokens)
* other standards/extensions may have even other concepts

`html_attr` will generally print array values as a space-separated list of values.

A special case is the attribute name `style` if its value is an array. It will be converted to inline CSS. Values with numeric keys will be printed followed by a `;`. Non-numeric keys will print a pattern of `key: value;`.

#### Details on `html_attr_type`

The `html_attr_type` filter can be used to convert an array passed into it into implementations of the mentioned interfaces in a few predefined ways. It takes a single argument indicating the type to use – similar to the `escape` filter in Twig that knows about `html`, `js`, `css` and a few more.

* `sst` means `space separated tokens`
* `cst` means `comma separated tokens`
* `style` means "inline CSS", for completeness

So, the following will construct an `attr` array for an `img` tag, where `sizes` needs to be printed separated by commas.

```twig
{% set attr = {
  srcset: ['small.jpg 480w']|html_attr_type('cst'),
  alt: 'A cute kitten'
} %}
{# amend the srcset #}
{% set attr = attr|html_attr_merge({ srcset: ['medium.jpg 800w', 'large.jpg 1200w'] }) %}
<img {{ html_attr(attr) />
```

This works because the `SeparatedTokenList` that is used for `sst` and `cst` implements merge behavior where a given value can be extended by merging arrays.

#### Design considerations

Exposing behavior for different attribute types through these interfaces may not be the 100% perfect, nice, automagic solution. _But_ it has the big advantage that we are not committing ourselves to a particular list of attributes for which standards-specific knowledge would have to be put in the code. I would consider that a maintenance nightmare, since it would require us to decide which attribute/special case to support and which not. Every single change in that list would be BC-breaking.

The two interfaces put control over that in the hands of power users or extension authors. Arbitrary ways could be conceived to create instances of these interfaces.

The `html_attr_type` filter provides built-in access to the two types I see relevant in the HTML 5 standard, the space and comma separated token lists.

The built-in default conversion of arrays to space-separated token lists should further reduce visibility of that problem for average template authors, who can hopefully ignore the problem most of the time.

Closes #3907, which outlined the initial idea.

#### TODO:

- [x] Get initial feedback
- [x] Add tests
- [x] Concept for attributes that employ [comma-separated tokens](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#comma-separated-tokens)
- [x] Add documentation
- [x] Add docblocks and type hints
- [x] Make a decision: [Escape attribute names or not?](https://github.com/twigphp/Twig/pull/3930#issuecomment-3743429100)

Co-authored-by: `@polarbirke`

Commits
-------

42c12fa720 Add an `html_attr` function to make outputting HTML attributes easier
2026-03-17 07:39:37 +01:00
Matthias Pigulla 42c12fa720 Add an html_attr function to make outputting HTML attributes easier 2026-03-17 07:39:33 +01:00