Skip to content

Commit 611cf6d

Browse files
authored
Fix complex scenarios with lists and admonitions (#1006)
Add better logic to admonitions to account for more complex list cases Fixes #1004
1 parent be7ba7b commit 611cf6d

File tree

3 files changed

+270
-5
lines changed

3 files changed

+270
-5
lines changed

docs/change_log/release-3.3.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ The following bug fixes are included in the 3.3 release:
7575
* Avoid a `RecursionError` from deeply nested blockquotes (#799).
7676
* Fix issues with complex emphasis (#979).
7777
* Fix unescaping of HTML characters `<>` in CodeHilite (#990).
78+
* Fix complex scenarios involving lists and admonitions (#1004)
7879

7980
[spec]: https://www.w3.org/TR/html5/text-level-semantics.html#the-code-element
8081
[fenced_code]: ../extensions/fenced_code_blocks.md

markdown/extensions/admonition.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,82 @@ class AdmonitionProcessor(BlockProcessor):
4040
RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)')
4141
RE_SPACES = re.compile(' +')
4242

43-
def test(self, parent, block):
43+
def __init__(self, parser):
44+
"""Initialization."""
45+
46+
super().__init__(parser)
47+
48+
self.current_sibling = None
49+
self.content_indention = 0
50+
51+
def get_sibling(self, parent, block):
52+
"""Get sibling admontion.
53+
54+
Retrieve the appropriate siblimg element. This can get trickly when
55+
dealing with lists.
56+
57+
"""
58+
59+
# We already acquired the block via test
60+
if self.current_sibling is not None:
61+
sibling = self.current_sibling
62+
block = block[self.content_indent:]
63+
self.current_sibling = None
64+
self.content_indent = 0
65+
return sibling, block
66+
4467
sibling = self.lastChild(parent)
45-
return self.RE.search(block) or \
46-
(block.startswith(' ' * self.tab_length) and sibling is not None and
47-
sibling.get('class', '').find(self.CLASSNAME) != -1)
68+
69+
if sibling is None or sibling.get('class', '').find(self.CLASSNAME) == -1:
70+
sibling = None
71+
else:
72+
# If the last child is a list and the content is idented sufficient
73+
# to be under it, then the content's is sibling is in the list.
74+
last_child = self.lastChild(sibling)
75+
indent = 0
76+
while last_child:
77+
if (
78+
sibling and block.startswith(' ' * self.tab_length * 2) and
79+
last_child and last_child.tag in ('ul', 'ol', 'dl')
80+
):
81+
82+
# The expectation is that we'll find an <li> or <dt>.
83+
# We should get it's last child as well.
84+
sibling = self.lastChild(last_child)
85+
last_child = self.lastChild(sibling) if sibling else None
86+
87+
# Context has been lost at this point, so we must adjust the
88+
# text's identation level so it will be evaluated correctly
89+
# under the list.
90+
block = block[self.tab_length:]
91+
indent += self.tab_length
92+
else:
93+
last_child = None
94+
95+
if not block.startswith(' ' * self.tab_length):
96+
sibling = None
97+
98+
if sibling is not None:
99+
self.current_sibling = sibling
100+
self.content_indent = indent
101+
102+
return sibling, block
103+
104+
def test(self, parent, block):
105+
106+
if self.RE.search(block):
107+
return True
108+
else:
109+
return self.get_sibling(parent, block)[0] is not None
48110

49111
def run(self, parent, blocks):
50-
sibling = self.lastChild(parent)
51112
block = blocks.pop(0)
52113
m = self.RE.search(block)
53114

54115
if m:
55116
block = block[m.end():] # removes the first line
117+
else:
118+
sibling, block = self.get_sibling(parent, block)
56119

57120
block, theRest = self.detab(block)
58121

@@ -65,6 +128,13 @@ def run(self, parent, blocks):
65128
p.text = title
66129
p.set('class', self.CLASSNAME_TITLE)
67130
else:
131+
# Sibling is a list item, but we need to wrap it's content should be wrapped in <p>
132+
if sibling.tag in ('li', 'dd') and sibling.text:
133+
text = sibling.text
134+
sibling.text = ''
135+
p = etree.SubElement(sibling, 'p')
136+
p.text = text
137+
68138
div = sibling
69139

70140
self.parser.parseChunk(div, block)
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""
2+
Python Markdown
3+
4+
A Python implementation of John Gruber's Markdown.
5+
6+
Documentation: https://python-markdown.github.io/
7+
GitHub: https://github.com/Python-Markdown/markdown/
8+
PyPI: https://pypi.org/project/Markdown/
9+
10+
Started by Manfred Stienstra (http://www.dwerg.net/).
11+
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
12+
Currently maintained by Waylan Limberg (https://github.com/waylan),
13+
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
14+
15+
Copyright 2007-2019 The Python Markdown Project (v. 1.7 and later)
16+
Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
17+
Copyright 2004 Manfred Stienstra (the original version)
18+
19+
License: BSD (see LICENSE.md for details).
20+
"""
21+
22+
from markdown.test_tools import TestCase
23+
24+
25+
class TestAdmonition(TestCase):
26+
27+
def test_with_lists(self):
28+
self.assertMarkdownRenders(
29+
self.dedent(
30+
'''
31+
- List
32+
33+
!!! note "Admontion"
34+
35+
- Paragraph
36+
37+
Paragraph
38+
'''
39+
),
40+
self.dedent(
41+
'''
42+
<ul>
43+
<li>
44+
<p>List</p>
45+
<div class="admonition note">
46+
<p class="admonition-title">Admontion</p>
47+
<ul>
48+
<li>
49+
<p>Paragraph</p>
50+
<p>Paragraph</p>
51+
</li>
52+
</ul>
53+
</div>
54+
</li>
55+
</ul>
56+
'''
57+
),
58+
extensions=['admonition']
59+
)
60+
61+
def test_with_big_lists(self):
62+
self.assertMarkdownRenders(
63+
self.dedent(
64+
'''
65+
- List
66+
67+
!!! note "Admontion"
68+
69+
- Paragraph
70+
71+
Paragraph
72+
73+
- Paragraph
74+
75+
paragraph
76+
'''
77+
),
78+
self.dedent(
79+
'''
80+
<ul>
81+
<li>
82+
<p>List</p>
83+
<div class="admonition note">
84+
<p class="admonition-title">Admontion</p>
85+
<ul>
86+
<li>
87+
<p>Paragraph</p>
88+
<p>Paragraph</p>
89+
</li>
90+
<li>
91+
<p>Paragraph</p>
92+
<p>paragraph</p>
93+
</li>
94+
</ul>
95+
</div>
96+
</li>
97+
</ul>
98+
'''
99+
),
100+
extensions=['admonition']
101+
)
102+
103+
def test_with_complex_lists(self):
104+
self.assertMarkdownRenders(
105+
self.dedent(
106+
'''
107+
- List
108+
109+
!!! note "Admontion"
110+
111+
- Paragraph
112+
113+
!!! note "Admontion"
114+
115+
1. Paragraph
116+
117+
Paragraph
118+
'''
119+
),
120+
self.dedent(
121+
'''
122+
<ul>
123+
<li>
124+
<p>List</p>
125+
<div class="admonition note">
126+
<p class="admonition-title">Admontion</p>
127+
<ul>
128+
<li>
129+
<p>Paragraph</p>
130+
<div class="admonition note">
131+
<p class="admonition-title">Admontion</p>
132+
<ol>
133+
<li>
134+
<p>Paragraph</p>
135+
<p>Paragraph</p>
136+
</li>
137+
</ol>
138+
</div>
139+
</li>
140+
</ul>
141+
</div>
142+
</li>
143+
</ul>
144+
'''
145+
),
146+
extensions=['admonition']
147+
)
148+
149+
def test_definition_list(self):
150+
self.assertMarkdownRenders(
151+
self.dedent(
152+
'''
153+
- List
154+
155+
!!! note "Admontion"
156+
157+
Term
158+
159+
: Definition
160+
161+
More text
162+
163+
: Another
164+
definition
165+
166+
Even more text
167+
'''
168+
),
169+
self.dedent(
170+
'''
171+
<ul>
172+
<li>
173+
<p>List</p>
174+
<div class="admonition note">
175+
<p class="admonition-title">Admontion</p>
176+
<dl>
177+
<dt>Term</dt>
178+
<dd>
179+
<p>Definition</p>
180+
<p>More text</p>
181+
</dd>
182+
<dd>
183+
<p>Another
184+
definition</p>
185+
<p>Even more text</p>
186+
</dd>
187+
</dl>
188+
</div>
189+
</li>
190+
</ul>
191+
'''
192+
),
193+
extensions=['admonition', 'def_list']
194+
)

0 commit comments

Comments
 (0)