Skip to content

Commit

Permalink
Fix complex scenarios with lists and admonitions (Python-Markdown#1006)
Browse files Browse the repository at this point in the history
Add better logic to admonitions to account for more complex list cases

Fixes Python-Markdown#1004
  • Loading branch information
facelessuser authored Jul 26, 2020
1 parent be7ba7b commit 611cf6d
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/change_log/release-3.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ The following bug fixes are included in the 3.3 release:
* Avoid a `RecursionError` from deeply nested blockquotes (#799).
* Fix issues with complex emphasis (#979).
* Fix unescaping of HTML characters `<>` in CodeHilite (#990).
* Fix complex scenarios involving lists and admonitions (#1004)

[spec]: https://www.w3.org/TR/html5/text-level-semantics.html#the-code-element
[fenced_code]: ../extensions/fenced_code_blocks.md
Expand Down
80 changes: 75 additions & 5 deletions markdown/extensions/admonition.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,82 @@ class AdmonitionProcessor(BlockProcessor):
RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)')
RE_SPACES = re.compile(' +')

def test(self, parent, block):
def __init__(self, parser):
"""Initialization."""

super().__init__(parser)

self.current_sibling = None
self.content_indention = 0

def get_sibling(self, parent, block):
"""Get sibling admontion.
Retrieve the appropriate siblimg element. This can get trickly when
dealing with lists.
"""

# We already acquired the block via test
if self.current_sibling is not None:
sibling = self.current_sibling
block = block[self.content_indent:]
self.current_sibling = None
self.content_indent = 0
return sibling, block

sibling = self.lastChild(parent)
return self.RE.search(block) or \
(block.startswith(' ' * self.tab_length) and sibling is not None and
sibling.get('class', '').find(self.CLASSNAME) != -1)

if sibling is None or sibling.get('class', '').find(self.CLASSNAME) == -1:
sibling = None
else:
# If the last child is a list and the content is idented sufficient
# to be under it, then the content's is sibling is in the list.
last_child = self.lastChild(sibling)
indent = 0
while last_child:
if (
sibling and block.startswith(' ' * self.tab_length * 2) and
last_child and last_child.tag in ('ul', 'ol', 'dl')
):

# The expectation is that we'll find an <li> or <dt>.
# We should get it's last child as well.
sibling = self.lastChild(last_child)
last_child = self.lastChild(sibling) if sibling else None

# Context has been lost at this point, so we must adjust the
# text's identation level so it will be evaluated correctly
# under the list.
block = block[self.tab_length:]
indent += self.tab_length
else:
last_child = None

if not block.startswith(' ' * self.tab_length):
sibling = None

if sibling is not None:
self.current_sibling = sibling
self.content_indent = indent

return sibling, block

def test(self, parent, block):

if self.RE.search(block):
return True
else:
return self.get_sibling(parent, block)[0] is not None

def run(self, parent, blocks):
sibling = self.lastChild(parent)
block = blocks.pop(0)
m = self.RE.search(block)

if m:
block = block[m.end():] # removes the first line
else:
sibling, block = self.get_sibling(parent, block)

block, theRest = self.detab(block)

Expand All @@ -65,6 +128,13 @@ def run(self, parent, blocks):
p.text = title
p.set('class', self.CLASSNAME_TITLE)
else:
# Sibling is a list item, but we need to wrap it's content should be wrapped in <p>
if sibling.tag in ('li', 'dd') and sibling.text:
text = sibling.text
sibling.text = ''
p = etree.SubElement(sibling, 'p')
p.text = text

div = sibling

self.parser.parseChunk(div, block)
Expand Down
194 changes: 194 additions & 0 deletions tests/test_syntax/extensions/test_admonition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"""
Python Markdown
A Python implementation of John Gruber's Markdown.
Documentation: https://python-markdown.github.io/
GitHub: https://github.com/Python-Markdown/markdown/
PyPI: https://pypi.org/project/Markdown/
Started by Manfred Stienstra (http://www.dwerg.net/).
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
Currently maintained by Waylan Limberg (https://github.com/waylan),
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
Copyright 2007-2019 The Python Markdown Project (v. 1.7 and later)
Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
Copyright 2004 Manfred Stienstra (the original version)
License: BSD (see LICENSE.md for details).
"""

from markdown.test_tools import TestCase


class TestAdmonition(TestCase):

def test_with_lists(self):
self.assertMarkdownRenders(
self.dedent(
'''
- List
!!! note "Admontion"
- Paragraph
Paragraph
'''
),
self.dedent(
'''
<ul>
<li>
<p>List</p>
<div class="admonition note">
<p class="admonition-title">Admontion</p>
<ul>
<li>
<p>Paragraph</p>
<p>Paragraph</p>
</li>
</ul>
</div>
</li>
</ul>
'''
),
extensions=['admonition']
)

def test_with_big_lists(self):
self.assertMarkdownRenders(
self.dedent(
'''
- List
!!! note "Admontion"
- Paragraph
Paragraph
- Paragraph
paragraph
'''
),
self.dedent(
'''
<ul>
<li>
<p>List</p>
<div class="admonition note">
<p class="admonition-title">Admontion</p>
<ul>
<li>
<p>Paragraph</p>
<p>Paragraph</p>
</li>
<li>
<p>Paragraph</p>
<p>paragraph</p>
</li>
</ul>
</div>
</li>
</ul>
'''
),
extensions=['admonition']
)

def test_with_complex_lists(self):
self.assertMarkdownRenders(
self.dedent(
'''
- List
!!! note "Admontion"
- Paragraph
!!! note "Admontion"
1. Paragraph
Paragraph
'''
),
self.dedent(
'''
<ul>
<li>
<p>List</p>
<div class="admonition note">
<p class="admonition-title">Admontion</p>
<ul>
<li>
<p>Paragraph</p>
<div class="admonition note">
<p class="admonition-title">Admontion</p>
<ol>
<li>
<p>Paragraph</p>
<p>Paragraph</p>
</li>
</ol>
</div>
</li>
</ul>
</div>
</li>
</ul>
'''
),
extensions=['admonition']
)

def test_definition_list(self):
self.assertMarkdownRenders(
self.dedent(
'''
- List
!!! note "Admontion"
Term
: Definition
More text
: Another
definition
Even more text
'''
),
self.dedent(
'''
<ul>
<li>
<p>List</p>
<div class="admonition note">
<p class="admonition-title">Admontion</p>
<dl>
<dt>Term</dt>
<dd>
<p>Definition</p>
<p>More text</p>
</dd>
<dd>
<p>Another
definition</p>
<p>Even more text</p>
</dd>
</dl>
</div>
</li>
</ul>
'''
),
extensions=['admonition', 'def_list']
)

0 comments on commit 611cf6d

Please sign in to comment.