Skip to content

Commit c4d0a14

Browse files
authored
Merge pull request #26 from idvorkin/develop
Add support for headings that include nested divs
2 parents 25d68b4 + 05ea8dc commit c4d0a14

File tree

2 files changed

+94
-20
lines changed

2 files changed

+94
-20
lines changed

markdownify/__init__.py

+48-20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
convert_heading_re = re.compile(r'convert_h(\d+)')
77
line_beginning_re = re.compile(r'^', re.MULTILINE)
88
whitespace_re = re.compile(r'[\r\n\s\t ]+')
9+
html_heading_re = re.compile(r'h[1-6]')
910

1011

1112
# Heading styles
@@ -61,22 +62,28 @@ def __init__(self, **options):
6162

6263
def convert(self, html):
6364
soup = BeautifulSoup(html, 'html.parser')
64-
return self.process_tag(soup, children_only=True)
65+
return self.process_tag(soup, convert_as_inline=False, children_only=True)
6566

66-
def process_tag(self, node, children_only=False):
67+
def process_tag(self, node, convert_as_inline, children_only=False):
6768
text = ''
69+
# markdown headings can't include block elements (elements w/newlines)
70+
isHeading = html_heading_re.match(node.name) is not None
71+
convert_children_as_inline = convert_as_inline
72+
73+
if not children_only and isHeading:
74+
convert_children_as_inline = True
6875

6976
# Convert the children first
7077
for el in node.children:
7178
if isinstance(el, NavigableString):
7279
text += self.process_text(six.text_type(el))
7380
else:
74-
text += self.process_tag(el)
81+
text += self.process_tag(el, convert_children_as_inline)
7582

7683
if not children_only:
7784
convert_fn = getattr(self, 'convert_%s' % node.name, None)
7885
if convert_fn and self.should_convert_tag(node.name):
79-
text = convert_fn(node, text)
86+
text = convert_fn(node, text, convert_as_inline)
8087

8188
return text
8289

@@ -89,8 +96,8 @@ def __getattr__(self, attr):
8996
if m:
9097
n = int(m.group(1))
9198

92-
def convert_tag(el, text):
93-
return self.convert_hn(n, el, text)
99+
def convert_tag(el, text, convert_as_inline):
100+
return self.convert_hn(n, el, text, convert_as_inline)
94101

95102
convert_tag.__name__ = 'convert_h%s' % n
96103
setattr(self, convert_tag.__name__, convert_tag)
@@ -116,10 +123,12 @@ def underline(self, text, pad_char):
116123
text = (text or '').rstrip()
117124
return '%s\n%s\n\n' % (text, pad_char * len(text)) if text else ''
118125

119-
def convert_a(self, el, text):
126+
def convert_a(self, el, text, convert_as_inline):
120127
prefix, suffix, text = chomp(text)
121128
if not text:
122129
return ''
130+
if convert_as_inline:
131+
return text
123132
href = el.get('href')
124133
title = el.get('title')
125134
if self.options['autolinks'] and text == href and not title:
@@ -128,22 +137,32 @@ def convert_a(self, el, text):
128137
title_part = ' "%s"' % title.replace('"', r'\"') if title else ''
129138
return '%s[%s](%s%s)%s' % (prefix, text, href, title_part, suffix) if href else text
130139

131-
def convert_b(self, el, text):
132-
return self.convert_strong(el, text)
140+
def convert_b(self, el, text, convert_as_inline):
141+
return self.convert_strong(el, text, convert_as_inline)
142+
143+
def convert_blockquote(self, el, text, convert_as_inline):
144+
145+
if convert_as_inline:
146+
return text
133147

134-
def convert_blockquote(self, el, text):
135148
return '\n' + line_beginning_re.sub('> ', text) if text else ''
136149

137-
def convert_br(self, el, text):
150+
def convert_br(self, el, text, convert_as_inline):
151+
if convert_as_inline:
152+
return ""
153+
138154
return ' \n'
139155

140-
def convert_em(self, el, text):
156+
def convert_em(self, el, text, convert_as_inline):
141157
prefix, suffix, text = chomp(text)
142158
if not text:
143159
return ''
144160
return '%s*%s*%s' % (prefix, text, suffix)
145161

146-
def convert_hn(self, n, el, text):
162+
def convert_hn(self, n, el, text, convert_as_inline):
163+
if convert_as_inline:
164+
return text
165+
147166
style = self.options['heading_style']
148167
text = text.rstrip()
149168
if style == UNDERLINED and n <= 2:
@@ -154,10 +173,14 @@ def convert_hn(self, n, el, text):
154173
return '%s %s %s\n\n' % (hashes, text, hashes)
155174
return '%s %s\n\n' % (hashes, text)
156175

157-
def convert_i(self, el, text):
158-
return self.convert_em(el, text)
176+
def convert_i(self, el, text, convert_as_inline):
177+
return self.convert_em(el, text, convert_as_inline)
178+
179+
def convert_list(self, el, text, convert_as_inline):
180+
181+
# Converting a list to inline is undefined.
182+
# Ignoring convert_to_inline for list.
159183

160-
def convert_list(self, el, text):
161184
nested = False
162185
while el:
163186
if el.name == 'li':
@@ -172,7 +195,7 @@ def convert_list(self, el, text):
172195
convert_ul = convert_list
173196
convert_ol = convert_list
174197

175-
def convert_li(self, el, text):
198+
def convert_li(self, el, text, convert_as_inline):
176199
parent = el.parent
177200
if parent is not None and parent.name == 'ol':
178201
if parent.get("start"):
@@ -190,20 +213,25 @@ def convert_li(self, el, text):
190213
bullet = bullets[depth % len(bullets)]
191214
return '%s %s\n' % (bullet, text or '')
192215

193-
def convert_p(self, el, text):
216+
def convert_p(self, el, text, convert_as_inline):
217+
if convert_as_inline:
218+
return text
194219
return '%s\n\n' % text if text else ''
195220

196-
def convert_strong(self, el, text):
221+
def convert_strong(self, el, text, convert_as_inline):
197222
prefix, suffix, text = chomp(text)
198223
if not text:
199224
return ''
200225
return '%s**%s**%s' % (prefix, text, suffix)
201226

202-
def convert_img(self, el, text):
227+
def convert_img(self, el, text, convert_as_inline):
203228
alt = el.attrs.get('alt', None) or ''
204229
src = el.attrs.get('src', None) or ''
205230
title = el.attrs.get('title', None) or ''
206231
title_part = ' "%s"' % title.replace('"', r'\"') if title else ''
232+
if convert_as_inline:
233+
return alt
234+
207235
return '![%s](%s%s)' % (alt, src, title_part)
208236

209237

tests/test_conversions.py

+46
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,52 @@ def test_hn():
107107
assert md('<h6>Hello</h6>') == '###### Hello\n\n'
108108

109109

110+
def test_hn_nested_tag_heading_style():
111+
assert md('<h1>A <p>P</p> C </h1>', heading_style=ATX_CLOSED) == '# A P C #\n\n'
112+
assert md('<h1>A <p>P</p> C </h1>', heading_style=ATX) == '# A P C\n\n'
113+
114+
115+
def test_hn_nested_simple_tag():
116+
tag_to_markdown = [
117+
("strong", "**strong**"),
118+
("b", "**b**"),
119+
("em", "*em*"),
120+
("i", "*i*"),
121+
("p", "p"),
122+
("a", "a"),
123+
("div", "div"),
124+
("blockquote", "blockquote"),
125+
]
126+
127+
for tag, markdown in tag_to_markdown:
128+
assert md('<h3>A <' + tag + '>' + tag + '</' + tag + '> B</h3>') == '### A ' + markdown + ' B\n\n'
129+
130+
assert md('<h3>A <br>B</h3>', heading_style=ATX) == '### A B\n\n'
131+
132+
# Nested lists not supported
133+
# assert md('<h3>A <ul><li>li1</i><li>l2</li></ul></h3>', heading_style=ATX) == '### A li1 li2 B\n\n'
134+
135+
136+
def test_hn_nested_img():
137+
assert md('<img src="/path/to/img.jpg" alt="Alt text" title="Optional title" />') == '![Alt text](/path/to/img.jpg "Optional title")'
138+
assert md('<img src="/path/to/img.jpg" alt="Alt text" />') == '![Alt text](/path/to/img.jpg)'
139+
image_attributes_to_markdown = [
140+
("", ""),
141+
("alt='Alt Text'", "Alt Text"),
142+
("alt='Alt Text' title='Optional title'", "Alt Text"),
143+
]
144+
for image_attributes, markdown in image_attributes_to_markdown:
145+
assert md('<h3>A <img src="/path/to/img.jpg " ' + image_attributes + '/> B</h3>') == '### A ' + markdown + ' B\n\n'
146+
147+
148+
def test_hr():
149+
assert md('<hr>hr</hr>') == 'hr'
150+
151+
152+
def test_head():
153+
assert md('<head>head</head>') == 'head'
154+
155+
110156
def test_atx_headings():
111157
assert md('<h1>Hello</h1>', heading_style=ATX) == '# Hello\n\n'
112158
assert md('<h2>Hello</h2>', heading_style=ATX) == '## Hello\n\n'

0 commit comments

Comments
 (0)