|
| 1 | +"""Tests for the `html_inline` rule.""" |
| 2 | + |
| 3 | +import importlib |
| 4 | + |
| 5 | +import pytest |
| 6 | + |
| 7 | +from markdown_it import MarkdownIt |
| 8 | + |
| 9 | + |
| 10 | +@pytest.mark.parametrize( |
| 11 | + "src", |
| 12 | + [ |
| 13 | + "<a>", # open tag |
| 14 | + "<a/>", # self-closing open tag |
| 15 | + "</a>", # close tag |
| 16 | + "<!-->", # short comment |
| 17 | + "<!--->", # short comment |
| 18 | + "<!---->", # shortest full comment |
| 19 | + "<??>", # empty processing instruction |
| 20 | + "<!a>", # declaration |
| 21 | + "<![CDATA[]]>", # empty CDATA section |
| 22 | + ], |
| 23 | +) |
| 24 | +def test_shortest_constructs(src): |
| 25 | + """The shortest legal form of each construct is passed through as raw html.""" |
| 26 | + assert MarkdownIt("commonmark").renderInline(src) == src |
| 27 | + |
| 28 | + |
| 29 | +@pytest.mark.parametrize("opener", ["<![CDATA[", "<!--", "<?", "<!a", "<a", "</a"]) |
| 30 | +def test_unterminated_openers_skip_the_regex(opener, monkeypatch): |
| 31 | + """A run of openers that are never terminated must not run the tag regex. |
| 32 | +
|
| 33 | + Every alternative of ``HTML_TAG_RE`` ends in a specific terminator, and its |
| 34 | + lazy sub-patterns rescan to the end of the input on each failed attempt, |
| 35 | + which made such runs quadratic. ``html_inline`` now rejects an opener in |
| 36 | + constant time when the terminator it needs cannot occur, so the regex is |
| 37 | + never invoked here; before the fix it ran once per opener. |
| 38 | + """ |
| 39 | + module = importlib.import_module("markdown_it.rules_inline.html_inline") |
| 40 | + |
| 41 | + real = module.HTML_TAG_RE |
| 42 | + calls = [] |
| 43 | + |
| 44 | + class Counting: |
| 45 | + def match(self, *args, **kwargs): |
| 46 | + calls.append(args[1] if len(args) > 1 else None) |
| 47 | + return real.match(*args, **kwargs) |
| 48 | + |
| 49 | + monkeypatch.setattr(module, "HTML_TAG_RE", Counting()) |
| 50 | + src = "x" + opener * 1_000 # leading "x" keeps it out of `html_block` |
| 51 | + html = MarkdownIt("commonmark").render(src) |
| 52 | + assert calls == [] |
| 53 | + # the openers all render as escaped text |
| 54 | + assert html == "<p>x" + (opener.replace("<", "<")) * 1_000 + "</p>\n" |
| 55 | + |
| 56 | + |
| 57 | +def test_terminated_constructs_still_reach_the_regex(monkeypatch): |
| 58 | + """The quick-reject must be exactly that: a terminated construct is still |
| 59 | + handed to the regex and parsed as before.""" |
| 60 | + module = importlib.import_module("markdown_it.rules_inline.html_inline") |
| 61 | + |
| 62 | + real = module.HTML_TAG_RE |
| 63 | + calls = [] |
| 64 | + |
| 65 | + class Counting: |
| 66 | + def match(self, *args, **kwargs): |
| 67 | + calls.append(1) |
| 68 | + return real.match(*args, **kwargs) |
| 69 | + |
| 70 | + monkeypatch.setattr(module, "HTML_TAG_RE", Counting()) |
| 71 | + src = "x <!-- c --> <?pi?> <![CDATA[d]]> <!D> <a>b</a>" |
| 72 | + html = MarkdownIt("commonmark").render(src) |
| 73 | + assert calls, "expected the regex to run for terminated constructs" |
| 74 | + assert html == "<p>" + src + "</p>\n" |
0 commit comments