Commit Graph

402 Commits

Author SHA1 Message Date
Fabien Potencier be220e6fd8 Warn about untrusted input with the default Tempest markdown converter 2026-08-27 13:07:34 +02:00
Fabien Potencier 1de0bfceb4 Honor date formatter prototype calendars 2026-08-27 08:44:26 +02:00
Romain Monteil aa17f59877 Add support for tempest/markdown in markdown-extra 2026-08-12 13:02:21 +02:00
Marvin Feldmann 163f1b6341 [Intl] Add format_list filter using PHP 8.5's IntlListFormatter 2026-07-08 22:22:44 +02:00
Fabien Potencier 36cd35dff4 Document how to customize the markdown_to_html converter 2026-06-05 21:52:27 +02:00
Fabien Potencier 3190b9ae12 Pre-escape HTML input on the spaceless filter 2026-05-20 08:29:48 +02: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
Arnaud Ligny 734720e5e7 fix documentation typos for singular filter arguments 2026-02-25 01:28:45 +01:00
Matthias Pigulla 04aa3df49f Add html_attr_relaxed escaping strategy 2026-02-08 18:59:07 +01:00
Fabien Potencier 77cd2fd9f7 Fix grammar and spelling mistakes in documentation 2026-01-17 22:34:06 +01:00
Always-Victorious 28c2606be7 Update u.rst to clarify truncate method's third argument behavior
Current wording "to preserve whole words" suggests setting the argument to ``true`` preserves words. However, the parameter is ``$cut`` (defaults to ``true`` for cutting at exact length), and ``false`` is required to preserve whole words.
2026-01-15 15:43:42 +01:00
David ALLIX f7daf71acc Add 'invoke' to filters index 2025-11-26 20:33:56 +01:00
Thomas Landauer b9a4e04731 Update inky_to_html.rst: Updating link
Page: https://twig.symfony.com/doc/3.x/filters/inky_to_html.html
2025-10-19 19:03:40 +02:00
Brett df3a281fc1 Update replace.rst
Missing `fruits` variable declaration.
2025-10-07 07:09:13 +08:00
Javier Eguiluz 50327b5019 [Doc] Tweaks in the escaping article 2025-09-25 11:22:21 +02:00
LucileDT d0177b94cd Add note to format_datetime explaining how to install required extensions 2025-08-23 22:02:14 +02:00
bodendorfer-simplethings b40934891b Fix wrong rounding_mode values 2025-08-15 14:30:41 +02:00
AndCycle bb6382c1bb Update reduce.rst
remove note, better argument for example
reduce doesn't use array_reduce anymore
https://github.com/twigphp/Twig/issues/3802
2025-05-12 16:54:49 +08:00
Simon André 84b0499dbd [Docs] Replace = by : in code examples
* function/include
* filters/format_number
2025-03-20 07:23:44 +01:00
Fabien Potencier 79d828b09d Fix version 2025-02-14 08:47:54 +01:00
Fabien Potencier 8ff19090f3 Fix precedence rules 2025-02-14 08:38:53 +01:00
omarius 498fb5da4c Fixing minor typo in Update inline_css.rst 2025-01-22 13:51:54 +01:00
Fabien Potencier 3e93e91f59 Finish the work 2025-01-12 18:48:43 +01:00
Lorenz Schäfer dacbe51b3a Apply suggestions from code review
Co-authored-by: Fabien Potencier <fabien@potencier.org>
2025-01-12 18:46:05 +01:00
Lorenz Schäfer 2b95bd6092 typo
Co-authored-by: Christian Flothmann <christian.flothmann@gmail.com>
2025-01-12 18:46:05 +01:00
Lorenz d58ac9a1a8 docs and changelog 2025-01-12 18:45:53 +01:00
dmjohnsson23 728b361e9d Clarify documentation for escape filter
It was somewhat unclear from the documentation what the intended purpose of the 'js' escape strategy is.
2024-11-11 15:41:24 -07:00
Fabien Potencier 98e7f7141a minor #4432 Fix mistake in docs for keys filter (ruudk)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Fix mistake in docs for `keys` filter

Commits
-------

deb37c30c4 Fix mistake in docs for `keys` filter
2024-11-04 08:50:22 +01:00
Ruud Kamphuis deb37c30c4 Fix mistake in docs for keys filter 2024-11-04 08:50:19 +01:00
Ruud Kamphuis b4ed19b5e1 Add find to Filters docs
https://twig.symfony.com/doc/3.x/filters/index.html
https://twig.symfony.com/doc/3.x/filters/find.html
2024-10-31 15:57:00 +01:00
Fabien Potencier f363ab258b Fix markup 2024-10-04 08:05:19 +02:00
Fabien Potencier 53441af5ff Fix markup 2024-10-03 14:23:13 +02:00
Fabien Potencier c4d4141187 Fix markup 2024-10-03 14:09:21 +02:00
unknown e08ca837df Added informations in format_currency and format_number docs 2024-10-03 14:00:37 +02:00
Fabien Potencier 1d31ac4b77 Tweak previous merge 2024-09-28 09:57:08 +02:00
Fabien Potencier 8efdab4780 minor #3983 Added details in the doc of filters and functions of IntlExtension (FlorianChenot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Added details in the doc of filters and functions of IntlExtension

Commits
-------

8716392603 Added details in the doc of filters and functions of IntlExtension
2024-09-28 09:46:54 +02:00
unknown 8716392603 Added details in the doc of filters and functions of IntlExtension 2024-09-28 09:46:50 +02:00
Thomas Landauer bd33033fd1 Update plural.rst: Fixing filter name
Info is taken from https://github.com/twigphp/Twig/pull/4357#issuecomment-2378435199

Besides, I changed to a more illustrative example word. One of the rare cases where ChatGPT was indeed useful... ;-)
2024-09-27 10:46:21 +02:00
Thomas Landauer cc3a2b7bac Update plural.rst: Fixing(?) broken links
More important question: Is the filter's name `plural` or `pluralize`??
2024-09-27 01:23:50 +02:00
Hugo Alliaume ac88c9ae75 [Doc] Document about html and html_attr strategies
Co-authored-by: Fabien Potencier <fabien@potencier.org>
2024-09-20 20:33:08 +02:00
Fabien Potencier 03f695fe95 minor #4308 [Doc] Provide an alternative for the deprecated spaceless filter (javiereguiluz, fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

[Doc] Provide an alternative for the deprecated spaceless filter

When a feature is deprecated, it's recommended to provide an alternative for it (or mention that there's no alternative).

So, let's mention the whitespace control features in the deprecation message of the `spaceless` filter.

Commits
-------

5a94ddc76e Suggest wording
b5d19ee4be [Doc] Provide an alternative for the deprecated spaceless filter
2024-09-14 10:56:32 +02:00
Fabien Potencier 5a94ddc76e Suggest wording 2024-09-14 10:56:07 +02:00
Fabien Potencier 7961788224 Clarify docs for the u filter 2024-09-12 23:07:08 +02:00
Fabien Potencier 08f28fd06c Remove most usage of foo/bar/baz in the docs 2024-09-12 21:40:08 +02:00
Fabien Potencier b1c35cd1fc Tweak docs 2024-09-12 19:19:56 +02:00
Javier Eguiluz b5d19ee4be [Doc] Provide an alternative for the deprecated spaceless filter 2024-09-12 11:42:41 +02:00
Fabien Potencier 9fd4c487e7 Deprecate the spaceless filter 2024-08-26 16:25:20 +02:00
Fabien Potencier 84116e5ff7 Fix typos in CHANGELOG 2024-08-18 17:52:16 +02:00
Fabien Potencier dbc934ba78 Fix typo 2024-08-17 11:28:14 +02:00