Skip to content

Commit a3c8def

Browse files
committed
Fixed escaping of alt text in ContentFormat.img()
1 parent 01fa337 commit a3c8def

File tree

2 files changed

+61
-3
lines changed

2 files changed

+61
-3
lines changed

blog/models.py

Lines changed: 20 additions & 3 deletions
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
@@ -8,6 +9,7 @@
89
from django.utils import timezone
910
from django.utils.cache import _generate_cache_header_key
1011
from django.utils.formats import date_format
12+
from django.utils.html import format_html
1113
from django.utils.translation import gettext_lazy as _
1214
from django_hosts.resolvers import get_host, reverse, reverse_host
1315
from docutils.core import publish_parts
@@ -23,12 +25,27 @@
2325
}
2426
BLOG_DOCUTILS_SETTINGS.update(getattr(settings, "BLOG_DOCUTILS_SETTINGS", {}))
2527

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

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

3138

39+
def _md_escape(s):
40+
# Add a backslash \ before any reserved characters
41+
return _MD_ESCAPE_REGEX.sub(r"\\\g<0>", s)
42+
43+
44+
def _rst_escape(s):
45+
# New lines mess up rst, it's easier to replace them with spaces.
46+
return s.replace("\n", " ")
47+
48+
3249
class EntryQuerySet(models.QuerySet):
3350
def published(self):
3451
return self.active().filter(pub_date__lte=timezone.now())
@@ -72,9 +89,9 @@ def img(self, url, alt_text):
7289
"""
7390
CF = type(self)
7491
return {
75-
CF.REST: f".. image:: {url}\n :alt: {alt_text}",
76-
CF.HTML: f'<img src="{url}" alt="{alt_text}">',
77-
CF.MARKDOWN: f"![{alt_text}]({url})",
92+
CF.REST: f".. image:: {url}\n :alt: {_rst_escape(alt_text)}",
93+
CF.HTML: format_html('<img src="{}" alt="{}">', url, alt_text),
94+
CF.MARKDOWN: f"![{_md_escape(alt_text)}]({url})",
7895
}[self]
7996

8097

blog/tests.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,44 @@ def test_full_url(self):
297297
r"http://www\.djangoproject\.localhost:8000"
298298
r"/m/blog/images/2005/07/test(_\w+)?\.png",
299299
)
300+
301+
def test_alt_text_html_escape(self):
302+
testdata = [
303+
(ContentFormat.HTML, 'te"st', '<img src="." alt="te&quot;st">'),
304+
(ContentFormat.HTML, "te<st>", '<img src="." alt="te&lt;st&gt;">'),
305+
(ContentFormat.MARKDOWN, 'te"st', '<img src="." alt="te&quot;st">'),
306+
(ContentFormat.MARKDOWN, "te[st]", '<img src="." alt="te[st]">'),
307+
(ContentFormat.MARKDOWN, "te{st}", '<img src="." alt="te{st}">'),
308+
(ContentFormat.MARKDOWN, "te<st>", '<img src="." alt="te&lt;st&gt;">'),
309+
(ContentFormat.MARKDOWN, "test*", '<img src="." alt="test*">'),
310+
(ContentFormat.MARKDOWN, "test_", '<img src="." alt="test_">'),
311+
(ContentFormat.MARKDOWN, "test`", '<img src="." alt="test`">'),
312+
(ContentFormat.MARKDOWN, "test+", '<img src="." alt="test+">'),
313+
(ContentFormat.MARKDOWN, "test-", '<img src="." alt="test-">'),
314+
(ContentFormat.MARKDOWN, "test.", '<img src="." alt="test.">'),
315+
(ContentFormat.MARKDOWN, "test!", '<img src="." alt="test!">'),
316+
(ContentFormat.MARKDOWN, "te\nst", '<img src="." alt="te\nst">'),
317+
(ContentFormat.REST, 'te"st', '<img src="." alt="te&quot;st">'),
318+
(ContentFormat.REST, "te[st]", '<img src="." alt="te[st]">'),
319+
(ContentFormat.REST, "te{st}", '<img src="." alt="te{st}">'),
320+
(ContentFormat.REST, "te<st>", '<img src="." alt="te&lt;st&gt;">'),
321+
(ContentFormat.REST, "te:st", '<img src="." alt="te:st">'),
322+
(ContentFormat.REST, "test*", '<img src="." alt="test*">'),
323+
(ContentFormat.REST, "test_", '<img src="." alt="test_">'),
324+
(ContentFormat.REST, "test`", '<img src="." alt="test`">'),
325+
(ContentFormat.REST, "test+", '<img src="." alt="test+">'),
326+
(ContentFormat.REST, "test-", '<img src="." alt="test-">'),
327+
(ContentFormat.REST, "test.", '<img src="." alt="test.">'),
328+
(ContentFormat.REST, "test!", '<img src="." alt="test!">'),
329+
(ContentFormat.REST, "te\nst", '<img src="." alt="te st">'),
330+
]
331+
for cf, alt_text, expected in testdata:
332+
# RST doesn't like an empty src, so we use . instead
333+
img_tag = cf.img(url=".", alt_text=alt_text)
334+
if cf is ContentFormat.MARKDOWN:
335+
expected = f"<p>{expected}</p>"
336+
with self.subTest(cf=cf, alt_text=alt_text):
337+
self.assertHTMLEqual(
338+
ContentFormat.to_html(cf, img_tag),
339+
expected,
340+
)

0 commit comments

Comments
 (0)