Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ and this project adheres to the
[Python Version Specification](https://packaging.python.org/en/latest/specifications/version-specifiers/).
See the [Contributing Guide](contributing.md) for details.

## [Unreleased]

### Fixed

* 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.

## [3.11.1] - 2026-10-13

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion markdown/extensions/attr_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def isheader(elem: Element) -> bool:

class AttrListTreeprocessor(Treeprocessor):

BASE_RE = r'\{\:?[ ]*([^\}\n ][^\n]*)[ ]*\}'
BASE_RE = r'\{\:?[ ]*([^\}\n ][^\n]*)\}'
HEADER_RE = re.compile(r'[ ]+{}[ ]*$'.format(BASE_RE))
BLOCK_RE = re.compile(r'\n[ ]*{}[ ]*$'.format(BASE_RE))
INLINE_RE = re.compile(r'^{}'.format(BASE_RE))
Expand Down
12 changes: 12 additions & 0 deletions tests/test_syntax/extensions/test_attr_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ def test_backslash_escape_value(self):
'<h1 id="*Foo*"><code>*Foo*</code></h1>'
)

def test_unclosed_curly_with_long_run_of_spaces(self):
"""
An unclosed attribute list followed by a long run of spaces should be
left as plain text, no matter how many trailing spaces there are.

The content and the trailing spaces both match the same part of the
pattern, so without care the regex backtracks quadratically over the
spaces while failing to find the closing brace.
"""
text = 'foo\n{a' + (' ' * 50000)
self.assertMarkdownRenders(text, f'<p>{text}</p>')

def test_table_td(self):
self.assertMarkdownRenders(
self.dedent(
Expand Down