diff --git a/django/utils/html.py b/django/utils/html.py
index 154c820d343..3ad920aca02 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -4,6 +4,7 @@ import html
import json
import re
import warnings
+from collections.abc import Mapping
from html.parser import HTMLParser
from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit
@@ -155,7 +156,12 @@ def format_html_join(sep, format_string, args_generator):
"""
return mark_safe(
conditional_escape(sep).join(
- format_html(format_string, *args) for args in args_generator
+ (
+ format_html(format_string, **args)
+ if isinstance(args, Mapping)
+ else format_html(format_string, *args)
+ )
+ for args in args_generator
)
)
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index d6c70a9bb01..33e0fceadf5 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -699,10 +699,29 @@ escaping HTML.
joined using ``sep``. ``sep`` is also passed through
:func:`conditional_escape`.
- ``args_generator`` should be an iterator that returns the sequence of
- ``args`` that will be passed to :func:`format_html`. For example::
+ ``args_generator`` should be an iterator that yields arguments to pass to
+ :func:`format_html`, either sequences of positional arguments or mappings of
+ keyword arguments.
- format_html_join("\n", "
{} {}", ((u.first_name, u.last_name) for u in users))
+ For example, tuples can be used for positional arguments::
+
+ format_html_join(
+ "\n",
+ "{} {}",
+ ((u.first_name, u.last_name) for u in users),
+ )
+
+ Or dictionaries can be used for keyword arguments::
+
+ format_html_join(
+ "\n",
+ '{id} {title}',
+ ({"id": b.id, "title": b.title} for b in books),
+ )
+
+ .. versionchanged:: 5.2
+
+ Support for mappings in ``args_generator`` was added.
.. function:: json_script(value, element_id=None, encoder=None)
diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt
index fa25737432e..0caeef01cfe 100644
--- a/docs/releases/5.2.txt
+++ b/docs/releases/5.2.txt
@@ -267,6 +267,10 @@ Utilities
values. This aligns with the :py:class:`str` addition behavior and allows
``__radd__`` to be used if available.
+* :func:`~django.utils.html.format_html_join` now supports taking an iterable
+ of mappings, passing their contents as keyword arguments to
+ :func:`~django.utils.html.format_html`.
+
Validators
~~~~~~~~~~
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index 82dbd58f127..f6373e30489 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -10,6 +10,7 @@ from django.utils.html import (
escape,
escapejs,
format_html,
+ format_html_join,
html_safe,
json_script,
linebreaks,
@@ -75,6 +76,26 @@ class TestUtilsHtml(SimpleTestCase):
name = "Adam"
self.assertEqual(format_html(f"{name}"), "Adam")
+ def test_format_html_join_with_positional_arguments(self):
+ self.assertEqual(
+ format_html_join(
+ "\n",
+ "{}) {}",
+ [(1, "Emma"), (2, "Matilda")],
+ ),
+ "1) Emma\n2) Matilda",
+ )
+
+ def test_format_html_join_with_keyword_arguments(self):
+ self.assertEqual(
+ format_html_join(
+ "\n",
+ "{id}) {text}",
+ [{"id": 1, "text": "Emma"}, {"id": 2, "text": "Matilda"}],
+ ),
+ "1) Emma\n2) Matilda",
+ )
+
def test_linebreaks(self):
items = (
("para1\n\npara2\r\rpara3", "para1
\n\npara2
\n\npara3
"),