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
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ See the [Contributing Guide](contributing.md) for details.
* Stop definition list nesting short of the recursion limit, so deeply nested
definitions are kept as text instead of raising an uncaught
`RecursionError` (#1700).
* Fix quadratic processing time for many consecutive abbreviation definitions
in the `abbr` extension, such as a glossary appended to every page (#1702).

## [3.11.3] - 2026-11-05

Expand Down
37 changes: 23 additions & 14 deletions markdown/extensions/abbr.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,22 +157,31 @@ def run(self, parent: etree.Element, blocks: list[str]) -> bool:

"""
block = blocks.pop(0)
m = self.RE.search(block)
if m:
start = end = -1
for m in self.RE.finditer(block):
# After the first definition, take only the definitions on the lines that
# directly follow it, so the rest of the block is parsed as before.
if end >= 0 and (m.start() != end + 1 or '\n' in m.group()):
break
abbr = m.group('abbr').strip()
title = m.group('title').strip()
if title and abbr:
if title == "''" or title == '""':
self.abbrs.pop(abbr, None)
else:
self.abbrs[abbr] = title
if block[m.end():].strip():
# Add any content after match back to blocks as separate block
blocks.insert(0, block[m.end():].lstrip('\n'))
if block[:m.start()].strip():
# Add any content before match back to blocks as separate block
blocks.insert(0, block[:m.start()].rstrip('\n'))
return True
if not (title and abbr):
break
if title == "''" or title == '""':
self.abbrs.pop(abbr, None)
else:
self.abbrs[abbr] = title
if start < 0:
start = m.start()
end = m.end()
if end >= 0:
if block[end:].strip():
# Add any content after match back to blocks as separate block
blocks.insert(0, block[end:].lstrip('\n'))
if block[:start].strip():
# Add any content before match back to blocks as separate block
blocks.insert(0, block[:start].rstrip('\n'))
return True
# No match. Restore block.
blocks.insert(0, block)
return False
Expand Down
46 changes: 46 additions & 0 deletions tests/test_syntax/extensions/test_abbr.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,52 @@ def test_abbr_clear(self):
)
)

def test_many_abbr_definitions(self):
"""
Consecutive definitions should be consumed together rather than returning
the remainder of the block to be parsed once per definition.
"""
count = 10000
ext = AbbrExtension()
text = ''.join(f'*[ABBR{i}]: Abbreviation {i}\n' for i in range(count))
self.assertEqual(Markdown(extensions=[ext]).convert(text), '')
self.assertEqual(ext.abbrs, {f'ABBR{i}': f'Abbreviation {i}' for i in range(count)})

def test_abbr_definitions_before_setext_underline(self):
self.assertMarkdownRenders(
self.dedent(
"""
*[ABBR]: Abbreviation
*[HTML]: Hyper Text Markup Language
===
"""
),
self.dedent(
"""
<p>===</p>
"""
)
)

def test_three_abbr_definitions_before_setext_underline(self):
self.assertMarkdownRenders(
self.dedent(
"""
*[ABBR]: Abbr
*[HTML]: Mark
*[CSS]: Styles
===
ABBR, HTML and CSS
"""
),
self.dedent(
"""
<p>===
<abbr title="Abbr">ABBR</abbr>, <abbr title="Mark">HTML</abbr> and <abbr title="Styles">CSS</abbr></p>
"""
)
)

def test_abbr_reset(self):
ext = AbbrExtension()
md = Markdown(extensions=[ext])
Expand Down
Loading