Skip to content

Commit 161b8fd

Browse files
committed
Fixed escaping of alt text in ContentFormat.img()
1 parent 05ae5f2 commit 161b8fd

File tree

2 files changed

+61
-3
lines changed

2 files changed

+61
-3
lines changed

blog/models.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from urllib.parse import urlparse
23

34
from django.conf import settings
@@ -6,6 +7,7 @@
67
from django.test import RequestFactory
78
from django.utils import timezone
89
from django.utils.cache import _generate_cache_header_key
10+
from django.utils.html import format_html
911
from django.utils.translation import gettext_lazy as _
1012
from django_hosts.resolvers import reverse
1113
from docutils.core import publish_parts
@@ -21,12 +23,27 @@
2123
}
2224
BLOG_DOCUTILS_SETTINGS.update(getattr(settings, "BLOG_DOCUTILS_SETTINGS", {}))
2325

26+
# List copied from:
27+
# https://github.com/Python-Markdown/markdown/blob/3.8/markdown/core.py#L112
28+
_MD_ESCAPE_CHARS = "\\`*_{}[]>()#+-.!"
29+
_MD_ESCAPE_REGEX = re.compile(f"[{re.escape(_MD_ESCAPE_CHARS)}]")
30+
2431

2532
def _md_slugify(value, separator):
2633
# matches the `id_prefix` setting of BLOG_DOCUTILS_SETTINGS
2734
return "s" + separator + _md_title_slugify(value, separator)
2835

2936

37+
def _md_escape(s):
38+
# Add a backslash \ before any reserved characters
39+
return _MD_ESCAPE_REGEX.sub(r"\\\g<0>", s)
40+
41+
42+
def _rst_escape(s):
43+
# New lines mess up rst, it's easier to replace them with spaces.
44+
return s.replace("\n", " ")
45+
46+
3047
class EntryQuerySet(models.QuerySet):
3148
def published(self):
3249
return self.active().filter(pub_date__lte=timezone.now())
@@ -70,9 +87,9 @@ def img(self, url, alt_text):
7087
"""
7188
CF = type(self)
7289
return {
73-
CF.REST: f".. image:: {url}\n :alt: {alt_text}",
74-
CF.HTML: f'<img src="{url}" alt="{alt_text}">',
75-
CF.MARKDOWN: f"![{alt_text}]({url})",
90+
CF.REST: f".. image:: {url}\n :alt: {_rst_escape(alt_text)}",
91+
CF.HTML: format_html('<img src="{}" alt="{}">', url, alt_text),
92+
CF.MARKDOWN: f"![{_md_escape(alt_text)}]({url})",
7693
}[self]
7794

7895

blog/tests.py

+41
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,44 @@ def test_contentformat_image_tags(self):
274274
cf.img(url="/test/image.png", alt_text="TEST"),
275275
expected,
276276
)
277+
278+
def test_alt_text_html_escape(self):
279+
testdata = [
280+
(ContentFormat.HTML, 'te"st', '<img src="." alt="te&quot;st">'),
281+
(ContentFormat.HTML, "te<st>", '<img src="." alt="te&lt;st&gt;">'),
282+
(ContentFormat.MARKDOWN, 'te"st', '<img src="." alt="te&quot;st">'),
283+
(ContentFormat.MARKDOWN, "te[st]", '<img src="." alt="te[st]">'),
284+
(ContentFormat.MARKDOWN, "te{st}", '<img src="." alt="te{st}">'),
285+
(ContentFormat.MARKDOWN, "te<st>", '<img src="." alt="te&lt;st&gt;">'),
286+
(ContentFormat.MARKDOWN, "test*", '<img src="." alt="test*">'),
287+
(ContentFormat.MARKDOWN, "test_", '<img src="." alt="test_">'),
288+
(ContentFormat.MARKDOWN, "test`", '<img src="." alt="test`">'),
289+
(ContentFormat.MARKDOWN, "test+", '<img src="." alt="test+">'),
290+
(ContentFormat.MARKDOWN, "test-", '<img src="." alt="test-">'),
291+
(ContentFormat.MARKDOWN, "test.", '<img src="." alt="test.">'),
292+
(ContentFormat.MARKDOWN, "test!", '<img src="." alt="test!">'),
293+
(ContentFormat.MARKDOWN, "te\nst", '<img src="." alt="te\nst">'),
294+
(ContentFormat.REST, 'te"st', '<img src="." alt="te&quot;st">'),
295+
(ContentFormat.REST, "te[st]", '<img src="." alt="te[st]">'),
296+
(ContentFormat.REST, "te{st}", '<img src="." alt="te{st}">'),
297+
(ContentFormat.REST, "te<st>", '<img src="." alt="te&lt;st&gt;">'),
298+
(ContentFormat.REST, "te:st", '<img src="." alt="te:st">'),
299+
(ContentFormat.REST, "test*", '<img src="." alt="test*">'),
300+
(ContentFormat.REST, "test_", '<img src="." alt="test_">'),
301+
(ContentFormat.REST, "test`", '<img src="." alt="test`">'),
302+
(ContentFormat.REST, "test+", '<img src="." alt="test+">'),
303+
(ContentFormat.REST, "test-", '<img src="." alt="test-">'),
304+
(ContentFormat.REST, "test.", '<img src="." alt="test.">'),
305+
(ContentFormat.REST, "test!", '<img src="." alt="test!">'),
306+
(ContentFormat.REST, "te\nst", '<img src="." alt="te st">'),
307+
]
308+
for cf, alt_text, expected in testdata:
309+
# RST doesn't like an empty src, so we use . instead
310+
img_tag = cf.img(url=".", alt_text=alt_text)
311+
if cf is ContentFormat.MARKDOWN:
312+
expected = f"<p>{expected}</p>"
313+
with self.subTest(cf=cf, alt_text=alt_text):
314+
self.assertHTMLEqual(
315+
ContentFormat.to_html(cf, img_tag),
316+
expected,
317+
)

0 commit comments

Comments
 (0)