fix(markdown): render ~~strikethrough~~ in task and note descriptions

CommonMark and GFM both define ~~text~~ as strikethrough, and Toast UI
emits it when the user toggles strikethrough in the WYSIWYG editor.
Python markdown's 'extra' extension does not implement strikethrough,
so the wrapping tildes leak through to the rendered HTML and the user
sees ~~text~~ instead of struck-through text on /tasks/<id>, notes,
client view, etc.

Add a regex pass to _normalize_toastui_markdown that converts
~~text~~ to <del>text</del> before markdown parsing. The bleach
sanitizer already permits <del> via the existing allowed_tags list,
so the rendered HTML survives the sanitization pass intact.

The regex is non-greedy and stays on a single line so multiple
strikethroughs on the same line each get their own pair.
This commit is contained in:
MacJediWizard
2026-05-07 11:39:33 -04:00
parent 7394af7940
commit 6dd74a861a
+8 -1
View File
@@ -25,7 +25,7 @@ def _normalize_toastui_markdown(text):
"""Clean up artefacts of Toast UI Editor's WYSIWYG-to-markdown conversion.
Toast UI follows CommonMark and over-escapes punctuation when round-
tripping rich text through its editor. Two patterns matter for display:
tripping rich text through its editor. Three patterns matter for display:
1. Line-leading ``\\- `` (also ``\\* ``, ``\\+ ``) prevents Python markdown
from parsing the line as a list item. Toast UI emits this when the
@@ -34,6 +34,10 @@ def _normalize_toastui_markdown(text):
(commas, colons, semicolons, etc.) renders as a literal ``\\,`` in
the HTML output. Stripping the backslash is safe because none of
those chars carry markdown meaning anywhere in a paragraph.
3. ``~~text~~`` strikethrough is part of CommonMark/GFM but the Python
``markdown`` library's ``extra`` extension does not implement it,
so it leaks through as literal ``~~...~~`` text. Convert to inline
``<del>`` HTML, which markdown passes through and bleach allows.
"""
import re
@@ -43,6 +47,9 @@ def _normalize_toastui_markdown(text):
text = re.sub(r"^(\s*)\\([\-+*])(\s)", r"\1\2\3", text, flags=re.MULTILINE)
# Drop backslashes before punctuation Python markdown does not handle.
text = re.sub(r"\\([^" + _PYMD_ESCAPABLE + r"])", r"\1", text)
# Convert ~~strikethrough~~ to <del>...</del>. Non-greedy and same-line
# only so multiple strikethroughs on one line each get their own pair.
text = re.sub(r"~~(.+?)~~", r"<del>\1</del>", text)
return text