Skip to content

Commit b701c34

Browse files
authored
Refactor HTML Parser (#803)
The HTML parser has been completely replaced. The new HTML parser is built on Python's html.parser.HTMLParser, which alleviates various bugs and simplifies maintenance of the code. The md_in_html extension has been rebuilt on the new HTML Parser, which drastically simplifies it. Note that raw HTML elements with a markdown attribute defined are now converted to ElementTree Elements and are rendered by the serializer. Various bugs have been fixed. Link reference parsing, abbreviation reference parsing and footnote reference parsing has all been moved from preprocessors to blockprocessors, which allows them to be nested within other block level elements. Specifically, this change was necessary to maintain the current behavior in the rebuilt md_in_html extension. A few random edge-case bugs (see the included tests) were resolved in the process. Closes #595, closes #780, closes #830 and closes #1012.
1 parent 90e750b commit b701c34

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3538
-1229
lines changed

.spell-dict

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Treeprocessor
131131
Treeprocessors
132132
tuple
133133
tuples
134+
unclosed
134135
unescape
135136
unescaping
136137
unittest

docs/change_log/release-3.3.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ The following new features have been included in the 3.3 release:
6666
Any random HTML attribute can be defined and set on the `<code>` tag of fenced code
6767
blocks when the `attr_list` extension is enabled (#816).
6868

69+
* The HTML parser has been completely replaced. The new HTML parser is built on Python's
70+
[html.parser.HTMLParser](https://docs.python.org/3/library/html.parser.html), which
71+
alleviates various bugs and simplify maintenance of the code (#803, #830).
72+
73+
* The [Markdown in HTML](../extensions/md_in_html.md) extension has been rebuilt on the
74+
new HTML Parser, which drastically simplifies it. Note that raw HTML elements with a
75+
`markdown` attribute defined are now converted to ElementTree Elements and are rendered
76+
by the serializer. Various bugs have been fixed (#803, #595, #780, and #1012).
77+
78+
* Link reference parsing, abbreviation reference parsing and footnote reference parsing
79+
has all been moved from `preprocessors` to `blockprocessors`, which allows them to be
80+
nested within other block level elements. Specifically, this change was necessary to
81+
maintain the current behavior in the rebuilt Markdown in HTML extension. A few random
82+
edge-case bugs (see the included tests) were resolved in the process (#803).
83+
6984
## Bug fixes
7085

7186
The following bug fixes are included in the 3.3 release:

docs/extensions/md_in_html.md

Lines changed: 187 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -4,122 +4,234 @@ title: Markdown in HTML Extension
44

55
## Summary
66

7-
An extensions that parses Markdown inside of HTML tags.
7+
An extension that parses Markdown inside of HTML tags.
88

9-
## Usage
9+
## Syntax
1010

11-
From the Python interpreter:
11+
By default, Markdown ignores any content within a raw HTML block-level element. With the `md-in-html` extension
12+
enabled, the content of a raw HTML block-level element can be parsed as Markdown by including a `markdown` attribute
13+
on the opening tag. The `markdown` attribute will be stripped from the output, while all other attributes will be
14+
preserved.
1215

13-
```pycon
14-
>>> import markdown
15-
>>> html = markdown.markdown(text, extensions=['md_in_html'])
16-
```
16+
The `markdown` attribute can be assigned one of three values: [`"1"`](#1), [`"block"`](#block), or [`"span"`](#span).
1717

18-
Unlike the other Extra features, this feature is built into the markdown core and
19-
is turned on when `markdown.extensions.extra` or `markdown.extensions.md_in_html`
20-
is enabled.
18+
!!! note
2119

22-
The content of any raw HTML block element can be Markdown-formatted simply by
23-
adding a `markdown` attribute to the opening tag. The markdown attribute will be
24-
stripped from the output, but all other attributes will be preserved.
20+
The expressions "block-level" and "span-level" as used in this document refer to an element's designation
21+
according to the HTML specification. Whereas the `"span"` and `"block"` values assigned to the `markdown`
22+
attribute refer to the Markdown parser's behavior.
2523

26-
If the markdown value is set to `1` (recommended) or any value other than `span`
27-
or `block`, the default behavior will be executed: `p`,`h[1-6]`,`li`,`dd`,`dt`,
28-
`td`,`th`,`legend`, and `address` elements skip block parsing while others do not.
29-
If the default is overridden by a value of `span`, *block parsing will be skipped*
30-
regardless of tag. If the default is overridden by a value of `block`,
31-
*block parsing will occur* regardless of tag.
24+
### `markdown="1"` { #1 }
3225

33-
#### Simple Example:
26+
When the `markdown` attribute is set to `"1"`, then the parser will use the default behavior for that specific tag.
3427

35-
```md
36-
This is *true* markdown text.
28+
The following tags have the `block` behavior by default: `address`, `article`, `aside`, `blockquote`, `body`,
29+
`colgroup`, `details`, `div`, `dl`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `iframe`, `header`, `hr`,
30+
`main`, `menu`, `nav`, `map`, `noscript`, `object`, `ol`, `section`, `table`, `tbody`, `thead`, `tfoot`, `tr`, and
31+
`ul`.
3732

33+
For example, the following:
34+
35+
```
3836
<div markdown="1">
39-
This is *true* markdown text.
37+
This is a *Markdown* Paragraph.
4038
</div>
4139
```
4240

43-
#### Result:
41+
... is rendered as:
4442

45-
```html
46-
<p>This is <em>true</em> markdown text.</p>
43+
``` html
4744
<div>
48-
<p>This is <em>true</em> markdown text.</p>
45+
<p>This is a <em>Markdown</em> Paragraph.</p>
4946
</div>
5047
```
5148

52-
### Nested Markdown Inside HTML Blocks
49+
The following tags have the `span` behavior by default: `address`, `dd`, `dt`, `h[1-6]`, `legend`, `li`, `p`, `td`,
50+
and `th`.
5351

54-
Nested elements are more sensitive and must be used cautiously. To avoid
55-
unexpected results:
52+
For example, the following:
5653

57-
* Only nest elements within block mode elements.
58-
* Follow the closing tag of inner elements with a blank line.
59-
* Only have one level of nesting.
54+
```
55+
<p markdown="1">
56+
This is not a *Markdown* Paragraph.
57+
</p>
58+
```
6059

61-
#### Complex Example:
60+
... is rendered as:
6261

63-
```md
64-
<div markdown="1" name="Example">
62+
``` html
63+
<p>
64+
This is not a <em>Markdown</em> Paragraph.
65+
</p>
66+
```
6567

66-
The text of the `Example` element.
68+
### `markdown="block"` { #block }
6769

68-
<div markdown="1" name="DefaultBlockMode">
69-
This text gets wrapped in `p` tags.
70-
</div>
70+
When the `markdown` attribute is set to `"block"`, then the parser will force the `block` behavior on the contents of
71+
the element so long as it is one of the `block` or `span` tags.
7172

72-
The tail of the `DefaultBlockMode` subelement.
73+
The content of a `block` element is parsed into block-level content. In other words, the text is rendered as
74+
paragraphs, headers, lists, blockquotes, etc. Any inline syntax within those elements is processed as well.
7375

74-
<p markdown="1" name="DefaultSpanMode">
75-
This text *is not* wrapped in additional `p` tags.
76-
</p>
76+
For example, the following:
7777

78-
The tail of the `DefaultSpanMode` subelement.
78+
```
79+
<section markdown="block">
80+
# A header.
7981
80-
<div markdown="span" name="SpanModeOverride">
81-
This `div` block is not wrapped in paragraph tags.
82-
Note: Subelements are not required to have tail text.
83-
</div>
82+
A *Markdown* paragraph.
8483
85-
<p markdown="block" name="BlockModeOverride">
86-
This `p` block *is* foolishly wrapped in further paragraph tags.
87-
</p>
84+
* A list item.
85+
* A second list item.
8886
89-
The tail of the `BlockModeOverride` subelement.
87+
</section>
88+
```
89+
90+
... is rendered as:
91+
92+
``` html
93+
<section>
94+
<h1>A header.</h1>
95+
<p>A <em>Markdown</em> paragraph.</p>
96+
<ul>
97+
<li>A list item.</li>
98+
<li>A second list item.</li>
99+
</ul>
100+
</section>
101+
```
102+
103+
!!! warning
104+
105+
Forcing elements to be parsed as `block` elements when they are not by default could result in invalid HTML.
106+
For example, one could force a `<p>` element to be nested within another `<p>` element. In most cases it is
107+
recommended to use the default behavior of `markdown="1"`. Explicitly setting `markdown="block"` should be
108+
reserved for advanced users who understand the HTML specification and how browsers parse and render HTML.
109+
110+
### `markdown="span"` { #span }
111+
112+
When the `markdown` attribute is set to `"span"`, then the parser will force the `span` behavior on the contents
113+
of the element so long as it is one of the `block` or `span` tags.
114+
115+
The content of a `span` element is not parsed into block-level content. In other words, the content will not be
116+
rendered as paragraphs, headers, etc. Only inline syntax will be rendered, such as links, strong, emphasis, etc.
117+
118+
For example, the following:
90119

91-
<div name="RawHtml">
92-
Raw HTML blocks may also be nested.
120+
```
121+
<div markdown="span">
122+
# *Not* a header
93123
</div>
124+
```
94125

126+
... is rendered as:
127+
128+
``` html
129+
<div>
130+
# <em>Not</em> a header
95131
</div>
132+
```
133+
134+
### Ignored Elements
135+
136+
The following tags are always ignored, regardless of any `markdown` attribute: `canvas`, `math`, `option`, `pre`,
137+
`script`, `style`, and `textarea`. All other raw HTML tags are treated as span-level tags and are not affected by this
138+
extension.
139+
140+
### Nesting
96141

97-
This text is after the markdown in HTML.
142+
When nesting multiple levels of raw HTML elements, a `markdown` attribute must be defined for each block-level
143+
element. For any block-level element which does not have a `markdown` attribute, everything inside that element is
144+
ignored, including child elements with `markdown` attributes.
145+
146+
For example, the following:
147+
148+
```
149+
<article id="my-article" markdown="1">
150+
# Article Title
151+
152+
A Markdown paragraph.
153+
154+
<section id="section-1" markdown="1">
155+
## Section 1 Title
156+
157+
<p>Custom raw **HTML** which gets ignored.</p>
158+
159+
</section>
160+
161+
<section id="section-2" markdown="1">
162+
## Section 2 Title
163+
164+
<p markdown="1">**Markdown** content.</p>
165+
166+
</section>
167+
168+
</article>
98169
```
99170

100-
#### Complex Result:
171+
... is rendered as:
101172

102173
```html
103-
<div name="Example">
104-
<p>The text of the <code>Example</code> element.</p>
105-
<div name="DefaultBlockMode">
106-
<p>This text gets wrapped in <code>p</code> tags.</p>
174+
<article id="my-article">
175+
<h1>Article Title</h1>
176+
<p>A Markdown paragraph.</p>
177+
<section id="section-1">
178+
<h2>Section 1 Title</h2>
179+
<p>Custom raw **HTML** which gets ignored.</p>
180+
</section>
181+
<section id="section-2">
182+
<h2>Section 2 Title</h2>
183+
<p><strong>Markdown</strong> content.</p>
184+
</section>
185+
</article>
186+
```
187+
188+
When the value of an element's `markdown` attribute is more permissive that its parent, then the parent's stricter
189+
behavior is enforced. For example, a `block` element nested within a `span` element will be parsed using the `span`
190+
behavior. However, if the value of an element's `markdown` attribute is the same as, or more restrictive than, its
191+
parent, the the child element's behavior is observed. For example, a `block` element may contain either `block`
192+
elements or `span` elements as children and each element will be parsed using the specified behavior.
193+
194+
### Tag Normalization
195+
196+
While the default behavior is for Markdown to not alter raw HTML, as this extension is parsing the content of raw HTML elements, it will do some normalization of the tags of block-level elements. For example, the following raw HTML:
197+
198+
```
199+
<div markdown="1">
200+
<p markdown="1">A Markdown paragraph with *no* closing tag.
201+
<p>A raw paragraph with *no* closing tag.
107202
</div>
108-
<p>The tail of the <code>DefaultBlockMode</code> subelement.</p>
109-
<p name="DefaultSpanMode">
110-
This text <em>is not</em> wrapped in additional <code>p</code> tags.</p>
111-
<p>The tail of the <code>DefaultSpanMode</code> subelement.</p>
112-
<div name="SpanModeOverride">
113-
This <code>div</code> block is not wrapped in paragraph tags.
114-
Note: Subelements are not required to have tail text.</div>
115-
<p name="BlockModeOverride">
116-
<p>This <code>p</code> block <em>is</em> foolishly wrapped in further paragraph tags.</p>
203+
```
204+
205+
... is rendered as:
206+
207+
``` html
208+
<div>
209+
<p>A Markdown paragraph with <em>no</em> closing tag.
210+
</p>
211+
<p>A raw paragraph with *no* closing tag.
117212
</p>
118-
<p>The tail of the <code>BlockModeOverride</code> subelement.</p>
119-
<div name="RawHtml">
120-
Raw HTML blocks may also be nested.
121213
</div>
214+
```
122215

123-
</div>
124-
<p>This text is after the markdown in HTML.</p>
216+
Notice that the parser properly recognizes that an unclosed `<p>` tag ends when another `<p>` tag begins or when the
217+
parent element ends. In both cases, a closing `</p>` was added to the end of the element, regardless of whether a
218+
`markdown` attribute was assigned to the element.
219+
220+
To avoid any normalization, an element must not be a descendant of any block-level element which has a `markdown`
221+
attribute defined.
222+
223+
!!! warning
224+
225+
The normalization behavior is only documented here so that document authors are not surprised when their carefully
226+
crafted raw HTML is altered by Markdown. This extension should not be relied on to normalize and generate valid
227+
HTML. For the best results, always include valid raw HTML (with both opening and closing tags) in your Markdown
228+
documents.
229+
230+
## Usage
231+
232+
From the Python interpreter:
233+
234+
``` pycon
235+
>>> import markdown
236+
>>> html = markdown.markdown(text, extensions=['md_in_html'])
125237
```

markdown/blockprocessors.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def build_block_parser(md, **kwargs):
5151
parser.blockprocessors.register(OListProcessor(parser), 'olist', 40)
5252
parser.blockprocessors.register(UListProcessor(parser), 'ulist', 30)
5353
parser.blockprocessors.register(BlockQuoteProcessor(parser), 'quote', 20)
54+
parser.blockprocessors.register(ReferenceProcessor(parser), 'reference', 15)
5455
parser.blockprocessors.register(ParagraphProcessor(parser), 'paragraph', 10)
5556
return parser
5657

@@ -554,6 +555,35 @@ def run(self, parent, blocks):
554555
)
555556

556557

558+
class ReferenceProcessor(BlockProcessor):
559+
""" Process link references. """
560+
RE = re.compile(
561+
r'^[ ]{0,3}\[([^\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*\n?[ ]*((["\'])(.*)\4|\((.*)\))?[ ]*$', re.MULTILINE
562+
)
563+
564+
def test(self, parent, block):
565+
return True
566+
567+
def run(self, parent, blocks):
568+
block = blocks.pop(0)
569+
m = self.RE.search(block)
570+
if m:
571+
id = m.group(1).strip().lower()
572+
link = m.group(2).lstrip('<').rstrip('>')
573+
title = m.group(5) or m.group(6)
574+
self.parser.md.references[id] = (link, title)
575+
if block[m.end():].strip():
576+
# Add any content after match back to blocks as separate block
577+
blocks.insert(0, block[m.end():].lstrip('\n'))
578+
if block[:m.start()].strip():
579+
# Add any content before match back to blocks as separate block
580+
blocks.insert(0, block[:m.start()].rstrip('\n'))
581+
return True
582+
# No match. Restore block.
583+
blocks.insert(0, block)
584+
return False
585+
586+
557587
class ParagraphProcessor(BlockProcessor):
558588
""" Process Paragraph blocks. """
559589

0 commit comments

Comments
 (0)