diff --git a/mdit_py_plugins/colon_fence.py b/mdit_py_plugins/colon_fence.py index 70c95ed..185548c 100644 --- a/mdit_py_plugins/colon_fence.py +++ b/mdit_py_plugins/colon_fence.py @@ -153,7 +153,7 @@ def _render( return ( "
"
         + content
         + "
\n" diff --git a/mdit_py_plugins/tasklists/__init__.py b/mdit_py_plugins/tasklists/__init__.py index d80f475..089bb5d 100644 --- a/mdit_py_plugins/tasklists/__init__.py +++ b/mdit_py_plugins/tasklists/__init__.py @@ -21,6 +21,7 @@ from uuid import uuid4 from markdown_it import MarkdownIt +from markdown_it.common.utils import escapeHtml from markdown_it.rules_core import StateCore from markdown_it.token import Token @@ -132,7 +133,8 @@ def end_label() -> Token: def after_label(content: str, checkbox_id: str) -> Token: token = Token("html_inline", "", 0) token.content = ( - f'' + '' ) token.attrs = {"for": checkbox_id} return token diff --git a/tests/test_colon_fence.py b/tests/test_colon_fence.py index fee609b..f03d949 100644 --- a/tests/test_colon_fence.py +++ b/tests/test_colon_fence.py @@ -36,3 +36,36 @@ def test_plugin_parse(data_regression): ) ) data_regression.check([t.as_dict() for t in tokens]) + + +@pytest.mark.parametrize( + "source,expected", + [ + pytest.param( + ':::a"onmouseover="alert(1)\nx\n:::\n', + '
x\n'
+            "
\n", + id="raw-quote", + ), + pytest.param( + ":::a"><xss>\nx\n:::\n", + '
x\n
\n', + id="entity-encoded", + ), + pytest.param( + # benign: same attribute value as before, now well-formed + ":::a&b\nx\n:::\n", + '
x\n
\n', + id="ampersand", + ), + ], +) +def test_block_name_is_escaped(source, expected): + """The block name must not break out of the ``class`` attribute (#143). + + With ``html=False`` the output must not carry markup from the source, + but the name used to be interpolated into the attribute unescaped + (and the entity-encoded form was unescaped first, then interpolated). + """ + md = MarkdownIt("commonmark", {"html": False}).use(colon_fence_plugin) + assert md.render(source) == expected diff --git a/tests/test_tasklists.py b/tests/test_tasklists.py index 427c336..7a21218 100644 --- a/tests/test_tasklists.py +++ b/tests/test_tasklists.py @@ -1,4 +1,5 @@ from pathlib import Path +import re from textwrap import dedent from markdown_it import MarkdownIt @@ -32,3 +33,49 @@ def test_all(line, title, input, expected): text = md.render(input) print(text) assert text.rstrip() == expected.rstrip() + + +@pytest.mark.parametrize( + "source,label", + [ + pytest.param( + "- [ ] \n", + " <img src=x onerror=alert(1)>", + id="raw-html", + ), + pytest.param("- [x] a & b\n", " a & b", id="ampersand"), + pytest.param("- [ ] plain task\n", " plain task", id="plain"), + ], +) +def test_label_after_is_escaped(source, label): + """The ``label_after`` label must not carry raw HTML from the source (#143). + + The label is built from the item's source text and emitted as an + ``html_inline`` token, so it used to bypass ``html=False``. + """ + md = MarkdownIt("commonmark", {"html": False}).use( + tasklists_plugin, label=True, label_after=True + ) + text = re.sub(r"task-item-[0-9a-f-]+", "task-item-ID", md.render(source)) + checked = ' checked="checked"' if source.startswith("- [x]") else "" + assert text == ( + '\n" + ) + + +def test_label_after_entity_is_shown_literally(): + """The ``label_after`` label is the item's raw source text, escaped (#143). + + So a source entity is shown as written (``&``), not decoded. + Only the label is asserted: the inline children before the last one + are also rendered outside the label, which is a separate, existing issue. + """ + md = MarkdownIt("commonmark", {"html": False}).use( + tasklists_plugin, label=True, label_after=True + ) + labels = re.findall(r"", md.render("- [ ] a & b\n")) + assert labels == [" a &amp; b"]