diff --git a/docs/changelog.md b/docs/changelog.md index 6f27060e..5dc7b0f8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -21,6 +21,8 @@ See the [Contributing Guide](contributing.md) for details. * Fix quadratic-time regex backtracking in `attr_list` when a block ends with an unclosed attribute list followed by a long run of spaces, e.g. a line consisting only of `{` and some text with many trailing spaces (#1656). +* Fix quadratic-time regex backtracking in hash headers when a long run of `#` + characters is followed by more text on the same line. * Fix regression in `BacktickInlineProcessor` where text could be lost when opening and closing backticks did not match (#1668). * Escape the language name in the `codehilite` extension's non-Pygments output to diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index cbad917b..728bf8f9 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -458,7 +458,7 @@ class HashHeaderProcessor(BlockProcessor): """ Process Hash Headers. """ # Detect a header at start of any line in block - RE = re.compile(r'(?:^|\n)(?P#{1,6})(?P
(?:\\.|[^\\])*?)#*(?:\n|$)') + RE = re.compile(r'(?:^|\n)(?P#{1,6})(?P
(?:\\.|[^\\#]|#+(?=[^#\n]))*?)#*(?:\n|$)') def test(self, parent: etree.Element, block: str) -> bool: return bool(self.RE.search(block)) diff --git a/tests/test_syntax/blocks/test_headers.py b/tests/test_syntax/blocks/test_headers.py index b1992ad3..60267215 100644 --- a/tests/test_syntax/blocks/test_headers.py +++ b/tests/test_syntax/blocks/test_headers.py @@ -779,3 +779,15 @@ def test_unescaped_hash(self): """ ) ) + + def test_long_run_of_hashes_not_at_end(self): + """ + A long run of hashes followed by more text on the same line is part of + the header text and should not be scanned once per hash when looking + for the closing hashes. + """ + hashes = '#' * 20000 + self.assertMarkdownRenders( + '# ' + hashes + 'x', + '

' + hashes + 'x

' + )