third_party/Python-Markdown: Update to 3.4.1

All modifications are from upstream

Change-Id: I463e327b3cf15aeb9cd86da03b3c28c880278e19
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4048800
Auto-Submit: 揚帆起航 <uioptt24@gmail.com>
Commit-Queue: Dirk Pranke <dpranke@google.com>
Reviewed-by: Dirk Pranke <dpranke@google.com>
Cr-Commit-Position: refs/heads/main@{#1076461}
NOKEYCHECK=True
GitOrigin-RevId: 0d51553dcded67dffaa87f79d32d8c3d1790be46
diff --git a/README.chromium b/README.chromium
index 6e531db..f78fac4 100644
--- a/README.chromium
+++ b/README.chromium
@@ -1,6 +1,6 @@
 Name: Python-Markdown
-URL: https://files.pythonhosted.org/packages/49/02/37bd82ae255bb4dfef97a4b32d95906187b7a7a74970761fca1360c4ba22/Markdown-3.3.4.tar.gz
-Version: 3.3.4
+URL: https://files.pythonhosted.org/packages/85/7e/133e943e97a943d2f1d8bae0c5060f8ac50e6691754eb9dbe036b047a9bb/Markdown-3.4.1.tar.gz
+Version: 3.4.1
 Revision: None
 Security Critical: no
 License: BSD
diff --git a/README.md b/README.md
index 7b979b0..c2c573e 100644
--- a/README.md
+++ b/README.md
@@ -60,5 +60,5 @@
 Code of Conduct
 ---------------
 
-Everyone interacting in the Python-Markdown project's codebases, issue trackers,
+Everyone interacting in the Python-Markdown project's code bases, issue trackers,
 and mailing lists is expected to follow the [Code of Conduct].
diff --git a/markdown/__init__.py b/markdown/__init__.py
index e05af10..d88b1e9 100644
--- a/markdown/__init__.py
+++ b/markdown/__init__.py
@@ -19,43 +19,10 @@
 License: BSD (see LICENSE.md for details).
 """
 
-import sys
-
-# TODO: Remove this check at some point in the future.
-# (also remove flake8's 'ignore E402' comments below)
-if sys.version_info[0] < 3:  # pragma: no cover
-    raise ImportError('A recent version of Python 3 is required.')
-
-from .core import Markdown, markdown, markdownFromFile  # noqa: E402
-from .util import PY37                                  # noqa: E402
-from .pep562 import Pep562                              # noqa: E402
-from .__meta__ import __version__, __version_info__     # noqa: E402
-import warnings                                         # noqa: E402
+from .core import Markdown, markdown, markdownFromFile
+from .__meta__ import __version__, __version_info__  # noqa
 
 # For backward compatibility as some extensions expect it...
 from .extensions import Extension  # noqa
 
 __all__ = ['Markdown', 'markdown', 'markdownFromFile']
-
-__deprecated__ = {
-    "version": ("__version__", __version__),
-    "version_info": ("__version_info__", __version_info__)
-}
-
-
-def __getattr__(name):
-    """Get attribute."""
-
-    deprecated = __deprecated__.get(name)
-    if deprecated:
-        warnings.warn(
-            "'{}' is deprecated. Use '{}' instead.".format(name, deprecated[0]),
-            category=DeprecationWarning,
-            stacklevel=(3 if PY37 else 4)
-        )
-        return deprecated[1]
-    raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))
-
-
-if not PY37:
-    Pep562(__name__)
diff --git a/markdown/__main__.py b/markdown/__main__.py
index 7d78b7e..0184008 100644
--- a/markdown/__main__.py
+++ b/markdown/__main__.py
@@ -70,7 +70,7 @@
     parser.add_option("-c", "--extension_configs",
                       dest="configfile", default=None,
                       help="Read extension configurations from CONFIG_FILE. "
-                      "CONFIG_FILE must be of JSON or YAML format. YAML"
+                      "CONFIG_FILE must be of JSON or YAML format. YAML "
                       "format requires that a python YAML library be "
                       "installed. The parsed JSON or YAML must result in a "
                       "python dictionary which would be accepted by the "
diff --git a/markdown/__meta__.py b/markdown/__meta__.py
index 79ae2f8..ccabee5 100644
--- a/markdown/__meta__.py
+++ b/markdown/__meta__.py
@@ -26,7 +26,7 @@
 # (1, 2, 0, 'beta', 2) => "1.2b2"
 # (1, 2, 0, 'rc', 4) => "1.2rc4"
 # (1, 2, 0, 'final', 0) => "1.2"
-__version_info__ = (3, 3, 4, 'final', 0)
+__version_info__ = (3, 4, 1, 'final', 0)
 
 
 def _get_version(version_info):
diff --git a/markdown/blockparser.py b/markdown/blockparser.py
index 39219fd..b0ca4b1 100644
--- a/markdown/blockparser.py
+++ b/markdown/blockparser.py
@@ -69,12 +69,6 @@
         self.state = State()
         self.md = md
 
-    @property
-    @util.deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
     def parseDocument(self, lines):
         """ Parse a markdown document into an ElementTree.
 
diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py
index 8518e50..3d0ff86 100644
--- a/markdown/blockprocessors.py
+++ b/markdown/blockprocessors.py
@@ -286,7 +286,7 @@
         m = self.RE.search(block)
         if m:
             before = block[:m.start()]  # Lines before blockquote
-            # Pass lines before blockquote in recursively for parsing forst.
+            # Pass lines before blockquote in recursively for parsing first.
             self.parser.parseBlocks(parent, [before])
             # Remove ``> `` from beginning of each line.
             block = '\n'.join(
@@ -321,7 +321,7 @@
 
     TAG = 'ol'
     # The integer (python string) with which the lists starts (default=1)
-    # Eg: If list is intialized as)
+    # Eg: If list is initialized as)
     #   3. Item
     # The ol tag will get starts="3" attribute
     STARTSWITH = '1'
@@ -559,7 +559,7 @@
 class ReferenceProcessor(BlockProcessor):
     """ Process link references. """
     RE = re.compile(
-        r'^[ ]{0,3}\[([^\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*\n?[ ]*((["\'])(.*)\4|\((.*)\))?[ ]*$', re.MULTILINE
+        r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', re.MULTILINE
     )
 
     def test(self, parent, block):
diff --git a/markdown/core.py b/markdown/core.py
index 2f7f2d5..f6a171c 100644
--- a/markdown/core.py
+++ b/markdown/core.py
@@ -82,7 +82,7 @@
             # Other elements which Markdown should not be mucking up the contents of.
             'canvas', 'colgroup', 'dd', 'body', 'dt', 'group', 'iframe', 'li', 'legend',
             'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script',
-            'style', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video'
+            'style', 'summary', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video'
         ]
 
         self.registeredExtensions = []
@@ -122,7 +122,7 @@
             if isinstance(ext, str):
                 ext = self.build_extension(ext, configs.get(ext, {}))
             if isinstance(ext, Extension):
-                ext._extendMarkdown(self)
+                ext.extendMarkdown(self)
                 logger.debug(
                     'Successfully loaded extension "%s.%s".'
                     % (ext.__class__.__module__, ext.__class__.__name__)
@@ -150,7 +150,7 @@
         """
         configs = dict(configs)
 
-        entry_points = [ep for ep in util.INSTALLED_EXTENSIONS if ep.name == ext_name]
+        entry_points = [ep for ep in util.get_installed_extensions() if ep.name == ext_name]
         if entry_points:
             ext = entry_points[0].load()
             return ext(**configs)
diff --git a/markdown/extensions/__init__.py b/markdown/extensions/__init__.py
index 4bc8e5f..2d8d72a 100644
--- a/markdown/extensions/__init__.py
+++ b/markdown/extensions/__init__.py
@@ -19,14 +19,13 @@
 License: BSD (see LICENSE.md for details).
 """
 
-import warnings
 from ..util import parseBoolValue
 
 
 class Extension:
     """ Base class for extensions to subclass. """
 
-    # Default config -- to be overriden by a subclass
+    # Default config -- to be overridden by a subclass
     # Must be of the following format:
     #     {
     #       'key': ['value', 'description']
@@ -70,36 +69,16 @@
         for key, value in items:
             self.setConfig(key, value)
 
-    def _extendMarkdown(self, *args):
-        """ Private wrapper around extendMarkdown. """
-        md = args[0]
-        try:
-            self.extendMarkdown(md)
-        except TypeError as e:
-            if "missing 1 required positional argument" in str(e):
-                # Must be a 2.x extension. Pass in a dumby md_globals.
-                self.extendMarkdown(md, {})
-                warnings.warn(
-                    "The 'md_globals' parameter of '{}.{}.extendMarkdown' is "
-                    "deprecated.".format(self.__class__.__module__, self.__class__.__name__),
-                    category=DeprecationWarning,
-                    stacklevel=2
-                )
-            else:
-                raise
-
     def extendMarkdown(self, md):
         """
-        Add the various proccesors and patterns to the Markdown Instance.
+        Add the various processors and patterns to the Markdown Instance.
 
-        This method must be overriden by every extension.
+        This method must be overridden by every extension.
 
         Keyword arguments:
 
         * md: The Markdown instance.
 
-        * md_globals: Global variables in the markdown module namespace.
-
         """
         raise NotImplementedError(
             'Extension "%s.%s" must define an "extendMarkdown"'
diff --git a/markdown/extensions/admonition.py b/markdown/extensions/admonition.py
index 9c66b4f..cb8d901 100644
--- a/markdown/extensions/admonition.py
+++ b/markdown/extensions/admonition.py
@@ -49,9 +49,9 @@
         self.content_indention = 0
 
     def parse_content(self, parent, block):
-        """Get sibling admontion.
+        """Get sibling admonition.
 
-        Retrieve the appropriate siblimg element. This can get trickly when
+        Retrieve the appropriate sibling element. This can get tricky when
         dealing with lists.
 
         """
@@ -72,8 +72,8 @@
         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.
+            # If the last child is a list and the content is sufficiently indented
+            # to be under it, then the content's sibling is in the list.
             last_child = self.lastChild(sibling)
             indent = 0
             while last_child:
@@ -83,12 +83,12 @@
                 ):
 
                     # The expectation is that we'll find an <li> or <dt>.
-                    # We should get it's last child as well.
+                    # We should get its 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
+                    # text's indentation level so it will be evaluated correctly
                     # under the list.
                     block = block[self.tab_length:]
                     indent += self.tab_length
diff --git a/markdown/extensions/codehilite.py b/markdown/extensions/codehilite.py
index 9eed561..a54ba21 100644
--- a/markdown/extensions/codehilite.py
+++ b/markdown/extensions/codehilite.py
@@ -23,6 +23,7 @@
     from pygments import highlight
     from pygments.lexers import get_lexer_by_name, guess_lexer
     from pygments.formatters import get_formatter_by_name
+    from pygments.util import ClassNotFound
     pygments = True
 except ImportError:  # pragma: no cover
     pygments = False
@@ -63,12 +64,14 @@
     * use_pygments: Pass code to pygments for code highlighting. If `False`, the code is
       instead wrapped for highlighting by a JavaScript library. Default: `True`.
 
+    * pygments_formatter: The name of a Pygments formatter or a formatter class used for
+      highlighting the code blocks. Default: `html`.
+
     * linenums: An alias to Pygments `linenos` formatter option. Default: `None`.
 
     * css_class: An alias to Pygments `cssclass` formatter option. Default: 'codehilite'.
 
-    * lang_prefix: Prefix prepended to the language when `use_pygments` is `False`.
-      Default: "language-".
+    * lang_prefix: Prefix prepended to the language. Default: "language-".
 
     Other Options:
     Any other options are accepted and passed on to the lexer and formatter. Therefore,
@@ -80,6 +83,10 @@
     Formatter options: https://pygments.org/docs/formatters/#HtmlFormatter
     Lexer Options: https://pygments.org/docs/lexers/
 
+    Additionally, when Pygments is enabled, the code's language is passed to the
+    formatter as an extra option `lang_str`, whose value being `{lang_prefix}{lang}`.
+    This option has no effect to the Pygments's builtin formatters.
+
     Advanced Usage:
         code = CodeHilite(
             src = some_code,
@@ -99,6 +106,7 @@
         self.guess_lang = options.pop('guess_lang', True)
         self.use_pygments = options.pop('use_pygments', True)
         self.lang_prefix = options.pop('lang_prefix', 'language-')
+        self.pygments_formatter = options.pop('pygments_formatter', 'html')
 
         if 'linenos' not in options:
             options['linenos'] = options.pop('linenums', None)
@@ -112,7 +120,7 @@
 
         self.options = options
 
-    def hilite(self):
+    def hilite(self, shebang=True):
         """
         Pass code to the [Pygments](http://pygments.pocoo.org/) highliter with
         optional line numbers. The output should then be styled with css to
@@ -125,7 +133,7 @@
 
         self.src = self.src.strip('\n')
 
-        if self.lang is None:
+        if self.lang is None and shebang:
             self._parseHeader()
 
         if pygments and self.use_pygments:
@@ -139,7 +147,17 @@
                         lexer = get_lexer_by_name('text', **self.options)
                 except ValueError:  # pragma: no cover
                     lexer = get_lexer_by_name('text', **self.options)
-            formatter = get_formatter_by_name('html', **self.options)
+            if not self.lang:
+                # Use the guessed lexer's language instead
+                self.lang = lexer.aliases[0]
+            lang_str = f'{self.lang_prefix}{self.lang}'
+            if isinstance(self.pygments_formatter, str):
+                try:
+                    formatter = get_formatter_by_name(self.pygments_formatter, **self.options)
+                except ClassNotFound:
+                    formatter = get_formatter_by_name('html', **self.options)
+            else:
+                formatter = self.pygments_formatter(lang_str=lang_str, **self.options)
             return highlight(self.src, lexer, formatter)
         else:
             # just escape and build markup usable by JS highlighting libs
@@ -221,7 +239,7 @@
 
 
 class HiliteTreeprocessor(Treeprocessor):
-    """ Hilight source code in code blocks. """
+    """ Highlight source code in code blocks. """
 
     def code_unescape(self, text):
         """Unescape code."""
@@ -237,11 +255,12 @@
         blocks = root.iter('pre')
         for block in blocks:
             if len(block) == 1 and block[0].tag == 'code':
+                local_config = self.config.copy()
                 code = CodeHilite(
                     self.code_unescape(block[0].text),
                     tab_length=self.md.tab_length,
-                    style=self.config.pop('pygments_style', 'default'),
-                    **self.config
+                    style=local_config.pop('pygments_style', 'default'),
+                    **local_config
                 )
                 placeholder = self.md.htmlStash.store(code.hilite())
                 # Clear codeblock in etree instance
@@ -253,7 +272,7 @@
 
 
 class CodeHiliteExtension(Extension):
-    """ Add source code hilighting to markdown codeblocks. """
+    """ Add source code highlighting to markdown codeblocks. """
 
     def __init__(self, **kwargs):
         # define default configs
@@ -278,7 +297,11 @@
             'lang_prefix': [
                 'language-',
                 'Prefix prepended to the language when use_pygments is false. Default: "language-"'
-            ]
+            ],
+            'pygments_formatter': ['html',
+                                   'Use a specific formatter for Pygments highlighting.'
+                                   'Default: "html"',
+                                   ],
             }
 
         for key, value in kwargs.items():
diff --git a/markdown/extensions/def_list.py b/markdown/extensions/def_list.py
index 0e8e452..17549f0 100644
--- a/markdown/extensions/def_list.py
+++ b/markdown/extensions/def_list.py
@@ -87,7 +87,7 @@
 class DefListIndentProcessor(ListIndentProcessor):
     """ Process indented children of definition list items. """
 
-    # Defintion lists need to be aware of all list types
+    # Definition lists need to be aware of all list types
     ITEM_TYPES = ['dd', 'li']
     LIST_TYPES = ['dl', 'ol', 'ul']
 
diff --git a/markdown/extensions/extra.py b/markdown/extensions/extra.py
index ebd168c..909ba07 100644
--- a/markdown/extensions/extra.py
+++ b/markdown/extensions/extra.py
@@ -16,7 +16,7 @@
 are not part of PHP Markdown Extra, and therefore, not part of
 Python-Markdown Extra. If you really would like Extra to include
 additional extensions, we suggest creating your own clone of Extra
-under a differant name. You could also edit the `extensions` global
+under a different name. You could also edit the `extensions` global
 variable defined below, but be aware that such changes may be lost
 when you upgrade to any future version of Python-Markdown.
 
diff --git a/markdown/extensions/fenced_code.py b/markdown/extensions/fenced_code.py
index 716b467..409166a 100644
--- a/markdown/extensions/fenced_code.py
+++ b/markdown/extensions/fenced_code.py
@@ -22,6 +22,7 @@
 from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines
 from .attr_list import get_attrs, AttrListExtension
 from ..util import parseBoolValue
+from ..serializers import _escape_attrib_html
 import re
 
 
@@ -42,13 +43,13 @@
 class FencedBlockPreprocessor(Preprocessor):
     FENCED_BLOCK_RE = re.compile(
         dedent(r'''
-            (?P<fence>^(?:~{3,}|`{3,}))[ ]*                      # opening fence
-            ((\{(?P<attrs>[^\}\n]*)\})?|                         # (optional {attrs} or
-            (\.?(?P<lang>[\w#.+-]*))?[ ]*                        # optional (.)lang
-            (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?) # optional hl_lines)
-            [ ]*\n                                               # newline (end of opening fence)
-            (?P<code>.*?)(?<=\n)                                 # the code block
-            (?P=fence)[ ]*$                                      # closing fence
+            (?P<fence>^(?:~{3,}|`{3,}))[ ]*                          # opening fence
+            ((\{(?P<attrs>[^\}\n]*)\})|                              # (optional {attrs} or
+            (\.?(?P<lang>[\w#.+-]*)[ ]*)?                            # optional (.)lang
+            (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines)
+            \n                                                       # newline (end of opening fence)
+            (?P<code>.*?)(?<=\n)                                     # the code block
+            (?P=fence)[ ]*$                                          # closing fence
         '''),
         re.MULTILINE | re.DOTALL | re.VERBOSE
     )
@@ -116,34 +117,28 @@
                         **local_config
                     )
 
-                    code = highliter.hilite()
+                    code = highliter.hilite(shebang=False)
                 else:
                     id_attr = lang_attr = class_attr = kv_pairs = ''
                     if lang:
-                        lang_attr = ' class="{}{}"'.format(self.config.get('lang_prefix', 'language-'), lang)
+                        prefix = self.config.get('lang_prefix', 'language-')
+                        lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"'
                     if classes:
-                        class_attr = ' class="{}"'.format(' '.join(classes))
+                        class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"'
                     if id:
-                        id_attr = ' id="{}"'.format(id)
+                        id_attr = f' id="{_escape_attrib_html(id)}"'
                     if self.use_attr_list and config and not config.get('use_pygments', False):
                         # Only assign key/value pairs to code element if attr_list ext is enabled, key/value pairs
                         # were defined on the code block, and the `use_pygments` key was not set to True. The
                         # `use_pygments` key could be either set to False or not defined. It is omitted from output.
-                        kv_pairs = ' ' + ' '.join(
-                            '{k}="{v}"'.format(k=k, v=v) for k, v in config.items() if k != 'use_pygments'
+                        kv_pairs = ''.join(
+                            f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments'
                         )
-                    code = '<pre{id}{cls}><code{lang}{kv}>{code}</code></pre>'.format(
-                        id=id_attr,
-                        cls=class_attr,
-                        lang=lang_attr,
-                        kv=kv_pairs,
-                        code=self._escape(m.group('code'))
-                    )
+                    code = self._escape(m.group('code'))
+                    code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>'
 
                 placeholder = self.md.htmlStash.store(code)
-                text = '{}\n{}\n{}'.format(text[:m.start()],
-                                           placeholder,
-                                           text[m.end():])
+                text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}'
             else:
                 break
         return text.split("\n")
diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py
index f6f4c85..96ed5c2 100644
--- a/markdown/extensions/footnotes.py
+++ b/markdown/extensions/footnotes.py
@@ -47,6 +47,10 @@
                 ["&#8617;",
                  "The text string that links from the footnote "
                  "to the reader's place."],
+            "SUPERSCRIPT_TEXT":
+                ["{}",
+                 "The text string that links from the reader's place "
+                 "to the footnote."],
             "BACKLINK_TITLE":
                 ["Jump back to footnote %d in the text",
                  "The text string used for the title HTML attribute "
@@ -170,6 +174,9 @@
         ol = etree.SubElement(div, "ol")
         surrogate_parent = etree.Element("div")
 
+        # Backward compatibility with old '%d' placeholder
+        backlink_title = self.getConfig("BACKLINK_TITLE").replace("%d", "{}")
+
         for index, id in enumerate(self.footnotes.keys(), start=1):
             li = etree.SubElement(ol, "li")
             li.set("id", self.makeFootnoteId(id))
@@ -185,7 +192,7 @@
             backlink.set("class", "footnote-backref")
             backlink.set(
                 "title",
-                self.getConfig("BACKLINK_TITLE") % (index)
+                backlink_title.format(index)
             )
             backlink.text = FN_BACKLINK_TEXT
 
@@ -228,7 +235,7 @@
                 # Any content before match is continuation of this footnote, which may be lazily indented.
                 before = therest[:m2.start()].rstrip('\n')
                 fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(before)]).lstrip('\n')
-                # Add back to blocks everything from begining of match forward for next iteration.
+                # Add back to blocks everything from beginning of match forward for next iteration.
                 blocks.insert(0, therest[m2.start():])
             else:
                 # All remaining lines of block are continuation of this footnote, which may be lazily indented.
@@ -264,7 +271,7 @@
                     # Any content before match is continuation of this footnote, which may be lazily indented.
                     before = block[:m.start()].rstrip('\n')
                     fn_blocks.append(self.detab(before))
-                    # Add back to blocks everything from begining of match forward for next iteration.
+                    # Add back to blocks everything from beginning of match forward for next iteration.
                     blocks.insert(0, block[m.start():])
                     # End of this footnote.
                     break
@@ -303,7 +310,9 @@
             sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True))
             a.set('href', '#' + self.footnotes.makeFootnoteId(id))
             a.set('class', 'footnote-ref')
-            a.text = str(list(self.footnotes.footnotes.keys()).index(id) + 1)
+            a.text = self.footnotes.getConfig("SUPERSCRIPT_TEXT").format(
+                list(self.footnotes.footnotes.keys()).index(id) + 1
+            )
             return sup, m.start(0), m.end(0)
         else:
             return None, None, None
@@ -355,7 +364,7 @@
         self.offset = 0
         for div in root.iter('div'):
             if div.attrib.get('class', '') == 'footnote':
-                # Footnotes shoul be under the first orderd list under
+                # Footnotes should be under the first ordered list under
                 # the footnote div.  So once we find it, quit.
                 for ol in div.iter('ol'):
                     self.handle_duplicates(ol)
diff --git a/markdown/extensions/legacy_attrs.py b/markdown/extensions/legacy_attrs.py
index b51d778..445aba1 100644
--- a/markdown/extensions/legacy_attrs.py
+++ b/markdown/extensions/legacy_attrs.py
@@ -26,7 +26,7 @@
 Prior to Python-Markdown version 3.0, the Markdown class had an `enable_attributes`
 keyword which was on by default and provided for attributes to be defined for elements
 using the format `{@key=value}`. This extension is provided as a replacement for
-backward compatability. New documents should be authored using attr_lists. However,
+backward compatibility. New documents should be authored using attr_lists. However,
 numerious documents exist which have been using the old attribute format for many
 years. This extension can be used to continue to render those documents correctly.
 """
diff --git a/markdown/extensions/legacy_em.py b/markdown/extensions/legacy_em.py
index 7fddb77..360988b 100644
--- a/markdown/extensions/legacy_em.py
+++ b/markdown/extensions/legacy_em.py
@@ -2,7 +2,7 @@
 Legacy Em Extension for Python-Markdown
 =======================================
 
-This extention provides legacy behavior for _connected_words_.
+This extension provides legacy behavior for _connected_words_.
 
 Copyright 2015-2018 The Python Markdown Project
 
diff --git a/markdown/extensions/md_in_html.py b/markdown/extensions/md_in_html.py
index 86cf00d..ec7dcba 100644
--- a/markdown/extensions/md_in_html.py
+++ b/markdown/extensions/md_in_html.py
@@ -33,13 +33,14 @@
         self.block_level_tags = set(md.block_level_elements.copy())
         # Block-level tags in which the content only gets span level parsing
         self.span_tags = set(
-            ['address', 'dd', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'legend', 'li', 'p', 'td', 'th']
+            ['address', 'dd', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'legend', 'li', 'p', 'summary', 'td', 'th']
         )
         # Block-level tags which never get their content parsed.
         self.raw_tags = set(['canvas', 'math', 'option', 'pre', 'script', 'style', 'textarea'])
-        # Block-level tags in which the content gets parsed as blocks
+
         super().__init__(md, *args, **kwargs)
 
+        # Block-level tags in which the content gets parsed as blocks
         self.block_tags = set(self.block_level_tags) - (self.span_tags | self.raw_tags | self.empty_tags)
         self.span_and_blocks_tags = self.block_tags | self.span_tags
 
@@ -247,11 +248,11 @@
 
     def parse_element_content(self, element):
         """
-        Resursively parse the text content of an etree Element as Markdown.
+        Recursively parse the text content of an etree Element as Markdown.
 
         Any block level elements generated from the Markdown will be inserted as children of the element in place
         of the text content. All `markdown` attributes are removed. For any elements in which Markdown parsing has
-        been dissabled, the text content of it and its chidlren are wrapped in an `AtomicString`.
+        been disabled, the text content of it and its chidlren are wrapped in an `AtomicString`.
         """
 
         md_attr = element.attrib.pop('markdown', 'off')
@@ -267,7 +268,7 @@
             for child in list(element):
                 self.parse_element_content(child)
 
-            # Parse Markdown text in tail of children. Do this seperate to avoid raw HTML parsing.
+            # Parse Markdown text in tail of children. Do this separate to avoid raw HTML parsing.
             # Save the position of each item to be inserted later in reverse.
             tails = []
             for pos, child in enumerate(element):
@@ -328,7 +329,7 @@
                 # Cleanup stash. Replace element with empty string to avoid confusing postprocessor.
                 self.parser.md.htmlStash.rawHtmlBlocks.pop(index)
                 self.parser.md.htmlStash.rawHtmlBlocks.insert(index, '')
-                # Comfirm the match to the blockparser.
+                # Confirm the match to the blockparser.
                 return True
         # No match found.
         return False
diff --git a/markdown/extensions/smarty.py b/markdown/extensions/smarty.py
index 894805f..c4bfd58 100644
--- a/markdown/extensions/smarty.py
+++ b/markdown/extensions/smarty.py
@@ -83,7 +83,7 @@
 from . import Extension
 from ..inlinepatterns import HtmlInlineProcessor, HTML_RE
 from ..treeprocessors import InlineProcessor
-from ..util import Registry, deprecated
+from ..util import Registry
 
 
 # Constants for quote education.
@@ -155,12 +155,6 @@
         self.replace = replace
         self.md = md
 
-    @property
-    @deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
     def handleMatch(self, m, data):
         result = ''
         for part in self.replace:
diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py
index 4b027bb..c8b1024 100644
--- a/markdown/extensions/tables.py
+++ b/markdown/extensions/tables.py
@@ -30,9 +30,11 @@
     RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`+)|(`+)|(\\\|)|(\|))')
     RE_END_BORDER = re.compile(r'(?<!\\)(?:\\\\)*\|$')
 
-    def __init__(self, parser):
+    def __init__(self, parser, config):
         self.border = False
         self.separator = ''
+        self.config = config
+
         super().__init__(parser)
 
     def test(self, parent, block):
@@ -126,7 +128,10 @@
             except IndexError:  # pragma: no cover
                 c.text = ""
             if a:
-                c.set('align', a)
+                if self.config['use_align_attribute']:
+                    c.set('align', a)
+                else:
+                    c.set('style', f'text-align: {a};')
 
     def _split_row(self, row):
         """ split a row of text into list of cells. """
@@ -200,7 +205,7 @@
             if not throw_out:
                 good_pipes.append(pipe)
 
-        # Split row according to table delimeters.
+        # Split row according to table delimiters.
         pos = 0
         for pipe in good_pipes:
             elements.append(row[pos:pipe])
@@ -212,11 +217,19 @@
 class TableExtension(Extension):
     """ Add tables to Markdown. """
 
+    def __init__(self, **kwargs):
+        self.config = {
+            'use_align_attribute': [False, 'True to use align attribute instead of style.'],
+        }
+
+        super().__init__(**kwargs)
+
     def extendMarkdown(self, md):
         """ Add an instance of TableProcessor to BlockParser. """
         if '|' not in md.ESCAPED_CHARS:
             md.ESCAPED_CHARS.append('|')
-        md.parser.blockprocessors.register(TableProcessor(md.parser), 'table', 75)
+        processor = TableProcessor(md.parser, self.getConfigs())
+        md.parser.blockprocessors.register(processor, 'table', 75)
 
 
 def makeExtension(**kwargs):  # pragma: no cover
diff --git a/markdown/extensions/toc.py b/markdown/extensions/toc.py
index d64ec16..1ded18d 100644
--- a/markdown/extensions/toc.py
+++ b/markdown/extensions/toc.py
@@ -16,23 +16,26 @@
 from . import Extension
 from ..treeprocessors import Treeprocessor
 from ..util import code_escape, parseBoolValue, AMP_SUBSTITUTE, HTML_PLACEHOLDER_RE, AtomicString
-from ..postprocessors import UnescapePostprocessor
+from ..treeprocessors import UnescapeTreeprocessor
 import re
 import html
 import unicodedata
 import xml.etree.ElementTree as etree
 
 
-def slugify(value, separator, encoding='ascii'):
+def slugify(value, separator, unicode=False):
     """ Slugify a string, to make it URL friendly. """
-    value = unicodedata.normalize('NFKD', value).encode(encoding, 'ignore')
-    value = re.sub(r'[^\w\s-]', '', value.decode(encoding)).strip().lower()
+    if not unicode:
+        # Replace Extended Latin characters with ASCII, i.e. žlutý → zluty
+        value = unicodedata.normalize('NFKD', value)
+        value = value.encode('ascii', 'ignore').decode('ascii')
+    value = re.sub(r'[^\w\s-]', '', value).strip().lower()
     return re.sub(r'[{}\s]+'.format(separator), separator, value)
 
 
 def slugify_unicode(value, separator):
     """ Slugify a string, to make it URL friendly while preserving Unicode characters. """
-    return slugify(value, separator, 'utf-8')
+    return slugify(value, separator, unicode=True)
 
 
 IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$')
@@ -81,8 +84,8 @@
 
 def unescape(text):
     """ Unescape escaped text. """
-    c = UnescapePostprocessor()
-    return c.run(text)
+    c = UnescapeTreeprocessor()
+    return c.unescape(text)
 
 
 def nest_toc_tokens(toc_list):
@@ -157,6 +160,7 @@
         self.base_level = int(config["baselevel"]) - 1
         self.slugify = config["slugify"]
         self.sep = config["separator"]
+        self.toc_class = config["toc_class"]
         self.use_anchors = parseBoolValue(config["anchorlink"])
         self.anchorlink_class = config["anchorlink_class"]
         self.use_permalinks = parseBoolValue(config["permalink"], False)
@@ -192,7 +196,12 @@
             # To keep the output from screwing up the
             # validation by putting a <div> inside of a <p>
             # we actually replace the <p> in its entirety.
-            if c.text and c.text.strip() == self.marker:
+
+            # The <p> element may contain more than a single text content
+            # (nl2br can introduce a <br>). In this situation, c.text returns
+            # the very first content, ignore children contents or tail content.
+            # len(c) == 0 is here to ensure there is only text in the <p>.
+            if c.text and c.text.strip() == self.marker and len(c) == 0:
                 for i in range(len(p)):
                     if p[i] == c:
                         p[i] = elem
@@ -231,7 +240,7 @@
     def build_toc_div(self, toc_list):
         """ Return a string div given a toc list. """
         div = etree.Element("div")
-        div.attrib["class"] = "toc"
+        div.attrib["class"] = self.toc_class
 
         # Add title to the div
         if self.title:
@@ -280,10 +289,10 @@
                     toc_tokens.append({
                         'level': int(el.tag[-1]),
                         'id': el.attrib["id"],
-                        'name': unescape(stashedHTML2text(
+                        'name': stashedHTML2text(
                             code_escape(el.attrib.get('data-toc-label', text)),
                             self.md, strip_entities=False
-                        ))
+                        )
                     })
 
                 # Remove the data-toc-label attribute as it is no longer needed
@@ -320,6 +329,9 @@
             "title": ["",
                       "Title to insert into TOC <div> - "
                       "Defaults to an empty string"],
+            "toc_class": ['toc',
+                          'CSS class(es) used for the link. '
+                          'Defaults to "toclink"'],
             "anchorlink": [False,
                            "True if header should be a self link - "
                            "Defaults to False"],
@@ -357,7 +369,7 @@
         self.reset()
         tocext = self.TreeProcessorClass(md, self.getConfigs())
         # Headerid ext is set to '>prettify'. With this set to '_end',
-        # it should always come after headerid ext (and honor ids assinged
+        # it should always come after headerid ext (and honor ids assigned
         # by the header id extension) if both are used. Same goes for
         # attr_list extension. This must come last because we don't want
         # to redefine ids after toc is created. But we do want toc prettified.
diff --git a/markdown/htmlparser.py b/markdown/htmlparser.py
index c08856a..3512d1a 100644
--- a/markdown/htmlparser.py
+++ b/markdown/htmlparser.py
@@ -20,7 +20,7 @@
 """
 
 import re
-import importlib
+import importlib.util
 import sys
 
 
@@ -113,7 +113,7 @@
                 return m.end()
             else:  # pragma: no cover
                 # Value of self.lineno must exceed total number of lines.
-                # Find index of begining of last line.
+                # Find index of beginning of last line.
                 return self.rawdata.rfind('\n')
         return 0
 
diff --git a/markdown/inlinepatterns.py b/markdown/inlinepatterns.py
index b0621a8..eb313bd 100644
--- a/markdown/inlinepatterns.py
+++ b/markdown/inlinepatterns.py
@@ -160,10 +160,10 @@
 AUTOLINK_RE = r'<((?:[Ff]|[Hh][Tt])[Tt][Pp][Ss]?://[^<>]*)>'
 
 # <me@example.com>
-AUTOMAIL_RE = r'<([^<> !]*@[^@<> ]*)>'
+AUTOMAIL_RE = r'<([^<> !]+@[^@<> ]+)>'
 
 # <...>
-HTML_RE = r'(<([a-zA-Z/][^<>]*|!--(?:(?!<!--|-->).)*--)>)'
+HTML_RE = r'(<(\/?[a-zA-Z][^<>@ ]*( [^<>]*)?|!--(?:(?!<!--|-->).)*--)>)'
 
 # "&#38;" (decimal) or "&#x26;" (hex) or "&amp;" (named)
 ENTITY_RE = r'(&(?:\#[0-9]+|\#x[0-9a-fA-F]+|[a-zA-Z0-9]+);)'
@@ -211,12 +211,6 @@
 
         self.md = md
 
-    @property
-    @util.deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
     def getCompiledRegExp(self):
         """ Return a compiled regular expression. """
         return self.compiled_re
@@ -673,7 +667,7 @@
                         bracket_count -= 1
                     elif backtrack_count > 0:
                         backtrack_count -= 1
-                        # We've found our backup end location if the title doesn't reslove.
+                        # We've found our backup end location if the title doesn't resolve.
                         if backtrack_count == 0:
                             last_bracket = index + 1
 
diff --git a/markdown/pep562.py b/markdown/pep562.py
deleted file mode 100644
index b130d3b..0000000
--- a/markdown/pep562.py
+++ /dev/null
@@ -1,245 +0,0 @@
-"""
-Backport of PEP 562.
-
-https://pypi.org/search/?q=pep562
-
-Licensed under MIT
-Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-documentation files (the "Software"), to deal in the Software without restriction, including without limitation
-the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
-and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions
-of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
-TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
-THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
-CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-IN THE SOFTWARE.
-"""
-import sys
-from collections import namedtuple
-import re
-
-__all__ = ('Pep562',)
-
-RE_VER = re.compile(
-    r'''(?x)
-    (?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<micro>\d+))?
-    (?:(?P<type>a|b|rc)(?P<pre>\d+))?
-    (?:\.post(?P<post>\d+))?
-    (?:\.dev(?P<dev>\d+))?
-    '''
-)
-
-REL_MAP = {
-    ".dev": "",
-    ".dev-alpha": "a",
-    ".dev-beta": "b",
-    ".dev-candidate": "rc",
-    "alpha": "a",
-    "beta": "b",
-    "candidate": "rc",
-    "final": ""
-}
-
-DEV_STATUS = {
-    ".dev": "2 - Pre-Alpha",
-    ".dev-alpha": "2 - Pre-Alpha",
-    ".dev-beta": "2 - Pre-Alpha",
-    ".dev-candidate": "2 - Pre-Alpha",
-    "alpha": "3 - Alpha",
-    "beta": "4 - Beta",
-    "candidate": "4 - Beta",
-    "final": "5 - Production/Stable"
-}
-
-PRE_REL_MAP = {"a": 'alpha', "b": 'beta', "rc": 'candidate'}
-
-
-class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre", "post", "dev"])):
-    """
-    Get the version (PEP 440).
-
-    A biased approach to the PEP 440 semantic version.
-
-    Provides a tuple structure which is sorted for comparisons `v1 > v2` etc.
-      (major, minor, micro, release type, pre-release build, post-release build, development release build)
-    Release types are named in is such a way they are comparable with ease.
-    Accessors to check if a development, pre-release, or post-release build. Also provides accessor to get
-    development status for setup files.
-
-    How it works (currently):
-
-    - You must specify a release type as either `final`, `alpha`, `beta`, or `candidate`.
-    - To define a development release, you can use either `.dev`, `.dev-alpha`, `.dev-beta`, or `.dev-candidate`.
-      The dot is used to ensure all development specifiers are sorted before `alpha`.
-      You can specify a `dev` number for development builds, but do not have to as implicit development releases
-      are allowed.
-    - You must specify a `pre` value greater than zero if using a prerelease as this project (not PEP 440) does not
-      allow implicit prereleases.
-    - You can optionally set `post` to a value greater than zero to make the build a post release. While post releases
-      are technically allowed in prereleases, it is strongly discouraged, so we are rejecting them. It should be
-      noted that we do not allow `post0` even though PEP 440 does not restrict this. This project specifically
-      does not allow implicit post releases.
-    - It should be noted that we do not support epochs `1!` or local versions `+some-custom.version-1`.
-
-    Acceptable version releases:
-
-    ```
-    Version(1, 0, 0, "final")                    1.0
-    Version(1, 2, 0, "final")                    1.2
-    Version(1, 2, 3, "final")                    1.2.3
-    Version(1, 2, 0, ".dev-alpha", pre=4)        1.2a4
-    Version(1, 2, 0, ".dev-beta", pre=4)         1.2b4
-    Version(1, 2, 0, ".dev-candidate", pre=4)    1.2rc4
-    Version(1, 2, 0, "final", post=1)            1.2.post1
-    Version(1, 2, 3, ".dev")                     1.2.3.dev0
-    Version(1, 2, 3, ".dev", dev=1)              1.2.3.dev1
-    ```
-
-    """
-
-    def __new__(cls, major, minor, micro, release="final", pre=0, post=0, dev=0):
-        """Validate version info."""
-
-        # Ensure all parts are positive integers.
-        for value in (major, minor, micro, pre, post):
-            if not (isinstance(value, int) and value >= 0):
-                raise ValueError("All version parts except 'release' should be integers.")
-
-        if release not in REL_MAP:
-            raise ValueError("'{}' is not a valid release type.".format(release))
-
-        # Ensure valid pre-release (we do not allow implicit pre-releases).
-        if ".dev-candidate" < release < "final":
-            if pre == 0:
-                raise ValueError("Implicit pre-releases not allowed.")
-            elif dev:
-                raise ValueError("Version is not a development release.")
-            elif post:
-                raise ValueError("Post-releases are not allowed with pre-releases.")
-
-        # Ensure valid development or development/pre release
-        elif release < "alpha":
-            if release > ".dev" and pre == 0:
-                raise ValueError("Implicit pre-release not allowed.")
-            elif post:
-                raise ValueError("Post-releases are not allowed with pre-releases.")
-
-        # Ensure a valid normal release
-        else:
-            if pre:
-                raise ValueError("Version is not a pre-release.")
-            elif dev:
-                raise ValueError("Version is not a development release.")
-
-        return super().__new__(cls, major, minor, micro, release, pre, post, dev)
-
-    def _is_pre(self):
-        """Is prerelease."""
-
-        return self.pre > 0
-
-    def _is_dev(self):
-        """Is development."""
-
-        return bool(self.release < "alpha")
-
-    def _is_post(self):
-        """Is post."""
-
-        return self.post > 0
-
-    def _get_dev_status(self):  # pragma: no cover
-        """Get development status string."""
-
-        return DEV_STATUS[self.release]
-
-    def _get_canonical(self):
-        """Get the canonical output string."""
-
-        # Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed..
-        if self.micro == 0:
-            ver = "{}.{}".format(self.major, self.minor)
-        else:
-            ver = "{}.{}.{}".format(self.major, self.minor, self.micro)
-        if self._is_pre():
-            ver += '{}{}'.format(REL_MAP[self.release], self.pre)
-        if self._is_post():
-            ver += ".post{}".format(self.post)
-        if self._is_dev():
-            ver += ".dev{}".format(self.dev)
-
-        return ver
-
-
-def parse_version(ver, pre=False):
-    """Parse version into a comparable Version tuple."""
-
-    m = RE_VER.match(ver)
-
-    # Handle major, minor, micro
-    major = int(m.group('major'))
-    minor = int(m.group('minor')) if m.group('minor') else 0
-    micro = int(m.group('micro')) if m.group('micro') else 0
-
-    # Handle pre releases
-    if m.group('type'):
-        release = PRE_REL_MAP[m.group('type')]
-        pre = int(m.group('pre'))
-    else:
-        release = "final"
-        pre = 0
-
-    # Handle development releases
-    dev = m.group('dev') if m.group('dev') else 0
-    if m.group('dev'):
-        dev = int(m.group('dev'))
-        release = '.dev-' + release if pre else '.dev'
-    else:
-        dev = 0
-
-    # Handle post
-    post = int(m.group('post')) if m.group('post') else 0
-
-    return Version(major, minor, micro, release, pre, post, dev)
-
-
-class Pep562:
-    """
-    Backport of PEP 562 <https://pypi.org/search/?q=pep562>.
-
-    Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`.
-    The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed.
-    """
-
-    def __init__(self, name):
-        """Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7."""
-
-        self._module = sys.modules[name]
-        self._get_attr = getattr(self._module, '__getattr__', None)
-        self._get_dir = getattr(self._module, '__dir__', None)
-        sys.modules[name] = self
-
-    def __dir__(self):
-        """Return the overridden `dir` if one was provided, else apply `dir` to the module."""
-
-        return self._get_dir() if self._get_dir else dir(self._module)
-
-    def __getattr__(self, name):
-        """Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present."""
-
-        try:
-            return getattr(self._module, name)
-        except AttributeError:
-            if self._get_attr:
-                return self._get_attr(name)
-            raise
-
-
-__version_info__ = Version(1, 0, 0, "final")
-__version__ = __version_info__._get_canonical()
diff --git a/markdown/postprocessors.py b/markdown/postprocessors.py
index 2e572f6..498f7e8 100644
--- a/markdown/postprocessors.py
+++ b/markdown/postprocessors.py
@@ -37,7 +37,6 @@
     postprocessors = util.Registry()
     postprocessors.register(RawHtmlPostprocessor(md), 'raw_html', 30)
     postprocessors.register(AndSubstitutePostprocessor(), 'amp_substitute', 20)
-    postprocessors.register(UnescapePostprocessor(), 'unescape', 10)
     return postprocessors
 
 
@@ -65,6 +64,8 @@
 class RawHtmlPostprocessor(Postprocessor):
     """ Restore raw html to the document. """
 
+    BLOCK_LEVEL_REGEX = re.compile(r'^\<\/?([^ >]+)')
+
     def run(self, text):
         """ Iterate over html stash and restore html. """
         replacements = OrderedDict()
@@ -99,7 +100,7 @@
             return self.run(processed_text)
 
     def isblocklevel(self, html):
-        m = re.match(r'^\<\/?([^ >]+)', html)
+        m = self.BLOCK_LEVEL_REGEX.match(html)
         if m:
             if m.group(1)[0] in ('!', '?', '@', '%'):
                 # Comment, php etc...
@@ -120,6 +121,10 @@
         return text
 
 
+@util.deprecated(
+    "This class will be removed in the future; "
+    "use 'treeprocessors.UnescapeTreeprocessor' instead."
+)
 class UnescapePostprocessor(Postprocessor):
     """ Restore escaped chars """
 
diff --git a/markdown/test_tools.py b/markdown/test_tools.py
index 21ae1a7..2ce0e74 100644
--- a/markdown/test_tools.py
+++ b/markdown/test_tools.py
@@ -42,7 +42,7 @@
 
     The `assertMarkdownRenders` method accepts the source text, the expected
     output, and any keywords to pass to Markdown. The `default_kwargs` are used
-    except where overridden by `kwargs`. The ouput and expected ouput are passed
+    except where overridden by `kwargs`. The output and expected output are passed
     to `TestCase.assertMultiLineEqual`. An AssertionError is raised with a diff
     if the actual output does not equal the expected output.
 
@@ -195,7 +195,7 @@
     text-based test files and define various behaviors/defaults for those tests.
     The following properties are supported:
 
-    location: A path to the directory fo test files. An absolute path is preferred.
+    location: A path to the directory of test files. An absolute path is preferred.
     exclude: A list of tests to exclude. Each test name should comprise the filename
              without an extension.
     normalize: A boolean value indicating if the HTML should be normalized.
diff --git a/markdown/treeprocessors.py b/markdown/treeprocessors.py
index eb6bf41..e9f48ca 100644
--- a/markdown/treeprocessors.py
+++ b/markdown/treeprocessors.py
@@ -19,6 +19,7 @@
 License: BSD (see LICENSE.md for details).
 """
 
+import re
 import xml.etree.ElementTree as etree
 from . import util
 from . import inlinepatterns
@@ -29,6 +30,7 @@
     treeprocessors = util.Registry()
     treeprocessors.register(InlineProcessor(md), 'inline', 20)
     treeprocessors.register(PrettifyTreeprocessor(md), 'prettify', 10)
+    treeprocessors.register(UnescapeTreeprocessor(md), 'unescape', 0)
     return treeprocessors
 
 
@@ -75,12 +77,6 @@
         self.inlinePatterns = md.inlinePatterns
         self.ancestors = []
 
-    @property
-    @util.deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
     def __makePlaceholder(self, type):
         """ Generate a placeholder """
         id = "%04d" % len(self.stashed_nodes)
@@ -331,7 +327,7 @@
 
         Iterate over ElementTree, find elements with inline tag, apply inline
         patterns and append newly created Elements to tree.  If you don't
-        want to process your data with inline paterns, instead of normal
+        want to process your data with inline patterns, instead of normal
         string, use subclass AtomicString:
 
             node.text = markdown.AtomicString("This will not be processed.")
@@ -412,8 +408,6 @@
             for e in elem:
                 if self.md.is_block_level(e.tag):
                     self._prettifyETree(e)
-            if not elem.tail or not elem.tail.strip():
-                elem.tail = i
         if not elem.tail or not elem.tail.strip():
             elem.tail = i
 
@@ -433,4 +427,32 @@
         pres = root.iter('pre')
         for pre in pres:
             if len(pre) and pre[0].tag == 'code':
-                pre[0].text = util.AtomicString(pre[0].text.rstrip() + '\n')
+                code = pre[0]
+                # Only prettify code containing text only
+                if not len(code) and code.text is not None:
+                    code.text = util.AtomicString(code.text.rstrip() + '\n')
+
+
+class UnescapeTreeprocessor(Treeprocessor):
+    """ Restore escaped chars """
+
+    RE = re.compile(r'{}(\d+){}'.format(util.STX, util.ETX))
+
+    def _unescape(self, m):
+        return chr(int(m.group(1)))
+
+    def unescape(self, text):
+        return self.RE.sub(self._unescape, text)
+
+    def run(self, root):
+        """ Loop over all elements and unescape all text. """
+        for elem in root.iter():
+            # Unescape text content
+            if elem.text and not elem.tag == 'code':
+                elem.text = self.unescape(elem.text)
+            # Unescape tail content
+            if elem.tail:
+                elem.tail = self.unescape(elem.tail)
+            # Unescape attribute values
+            for key, value in elem.items():
+                elem.set(key, self.unescape(value))
diff --git a/markdown/util.py b/markdown/util.py
index 2cb2317..e6b08e5 100644
--- a/markdown/util.py
+++ b/markdown/util.py
@@ -21,31 +21,11 @@
 
 import re
 import sys
-from collections import namedtuple
-from functools import wraps
 import warnings
-import xml.etree.ElementTree
-from .pep562 import Pep562
+from collections import namedtuple
+from functools import wraps, lru_cache
 from itertools import count
 
-try:
-    from importlib import metadata
-except ImportError:
-    # <PY38 use backport
-    import importlib_metadata as metadata
-
-PY37 = (3, 7) <= sys.version_info
-
-
-# TODO: Remove deprecated variables in a future release.
-__deprecated__ = {
-    'etree': ('xml.etree.ElementTree', xml.etree.ElementTree),
-    'string_type': ('str', str),
-    'text_type': ('str', str),
-    'int2str': ('chr', chr),
-    'iterrange': ('range', range)
-}
-
 
 """
 Constants you might want to modify
@@ -83,8 +63,6 @@
 -----------------------------------------------------------------------------
 """
 
-# Only load extension entry_points once.
-INSTALLED_EXTENSIONS = metadata.entry_points().get('markdown.extensions', ())
 RTL_BIDI_RANGES = (
     ('\u0590', '\u07FF'),
     # Hebrew (0590-05FF), Arabic (0600-06FF),
@@ -100,32 +78,36 @@
 """
 
 
+@lru_cache(maxsize=None)
+def get_installed_extensions():
+    if sys.version_info >= (3, 10):
+        from importlib import metadata
+    else:  # <PY310 use backport
+        import importlib_metadata as metadata
+    # Only load extension entry_points once.
+    return metadata.entry_points(group='markdown.extensions')
+
+
 def deprecated(message, stacklevel=2):
     """
     Raise a DeprecationWarning when wrapped function/method is called.
 
-    Borrowed from https://stackoverflow.com/a/48632082/866026
+    Usage:
+        @deprecated("This method will be removed in version X; use Y instead.")
+        def some_method()"
+            pass
     """
-    def deprecated_decorator(func):
+    def wrapper(func):
         @wraps(func)
         def deprecated_func(*args, **kwargs):
             warnings.warn(
-                "'{}' is deprecated. {}".format(func.__name__, message),
+                f"'{func.__name__}' is deprecated. {message}",
                 category=DeprecationWarning,
                 stacklevel=stacklevel
             )
             return func(*args, **kwargs)
         return deprecated_func
-    return deprecated_decorator
-
-
-@deprecated("Use 'Markdown.is_block_level' instead.")
-def isBlockLevel(tag):
-    """Check if the tag is a block level HTML tag."""
-    if isinstance(tag, str):
-        return tag.lower().rstrip('/') in BLOCK_LEVEL_ELEMENTS
-    # Some ElementTree tags are not strings, so return False.
-    return False
+    return wrapper
 
 
 def parseBoolValue(value, fail_on_errors=True, preserve_none=False):
@@ -159,8 +141,7 @@
 
 
 def _get_stack_depth(size=2):
-    """Get stack size for caller's frame.
-    See https://stackoverflow.com/a/47956089/866026
+    """Get current stack depth, performantly.
     """
     frame = sys._getframe(size)
 
@@ -171,7 +152,7 @@
 
 
 def nearing_recursion_limit():
-    """Return true if current stack depth is withing 100 of maximum limit."""
+    """Return true if current stack depth is within 100 of maximum limit."""
     return sys.getrecursionlimit() - _get_stack_depth() < 100
 
 
@@ -190,12 +171,6 @@
     def __init__(self, md=None):
         self.md = md
 
-    @property
-    @deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
 
 class HtmlStash:
     """
@@ -346,7 +321,7 @@
         * `priority`: An integer or float used to sort against all items.
 
         If an item is registered with a "name" which already exists, the
-        existing item is replaced with the new item. Tread carefully as the
+        existing item is replaced with the new item. Treat carefully as the
         old item is lost with no way to recover it. The new item will be
         sorted according to its priority and will **not** retain the position
         of the old item.
@@ -381,102 +356,3 @@
         if not self._is_sorted:
             self._priority.sort(key=lambda item: item.priority, reverse=True)
             self._is_sorted = True
-
-    # Deprecated Methods which provide a smooth transition from OrderedDict
-
-    def __setitem__(self, key, value):
-        """ Register item with priorty 5 less than lowest existing priority. """
-        if isinstance(key, str):
-            warnings.warn(
-                'Using setitem to register a processor or pattern is deprecated. '
-                'Use the `register` method instead.',
-                DeprecationWarning,
-                stacklevel=2,
-            )
-            if key in self:
-                # Key already exists, replace without altering priority
-                self._data[key] = value
-                return
-            if len(self) == 0:
-                # This is the first item. Set priority to 50.
-                priority = 50
-            else:
-                self._sort()
-                priority = self._priority[-1].priority - 5
-            self.register(value, key, priority)
-        else:
-            raise TypeError
-
-    def __delitem__(self, key):
-        """ Deregister an item by name. """
-        if key in self:
-            self.deregister(key)
-            warnings.warn(
-                'Using del to remove a processor or pattern is deprecated. '
-                'Use the `deregister` method instead.',
-                DeprecationWarning,
-                stacklevel=2,
-            )
-        else:
-            raise KeyError('Cannot delete key {}, not registered.'.format(key))
-
-    def add(self, key, value, location):
-        """ Register a key by location. """
-        if len(self) == 0:
-            # This is the first item. Set priority to 50.
-            priority = 50
-        elif location == '_begin':
-            self._sort()
-            # Set priority 5 greater than highest existing priority
-            priority = self._priority[0].priority + 5
-        elif location == '_end':
-            self._sort()
-            # Set priority 5 less than lowest existing priority
-            priority = self._priority[-1].priority - 5
-        elif location.startswith('<') or location.startswith('>'):
-            # Set priority halfway between existing priorities.
-            i = self.get_index_for_name(location[1:])
-            if location.startswith('<'):
-                after = self._priority[i].priority
-                if i > 0:
-                    before = self._priority[i-1].priority
-                else:
-                    # Location is first item`
-                    before = after + 10
-            else:
-                # location.startswith('>')
-                before = self._priority[i].priority
-                if i < len(self) - 1:
-                    after = self._priority[i+1].priority
-                else:
-                    # location is last item
-                    after = before - 10
-            priority = before - ((before - after) / 2)
-        else:
-            raise ValueError('Not a valid location: "%s". Location key '
-                             'must start with a ">" or "<".' % location)
-        self.register(value, key, priority)
-        warnings.warn(
-            'Using the add method to register a processor or pattern is deprecated. '
-            'Use the `register` method instead.',
-            DeprecationWarning,
-            stacklevel=2,
-        )
-
-
-def __getattr__(name):
-    """Get attribute."""
-
-    deprecated = __deprecated__.get(name)
-    if deprecated:
-        warnings.warn(
-            "'{}' is deprecated. Use '{}' instead.".format(name, deprecated[0]),
-            category=DeprecationWarning,
-            stacklevel=(3 if PY37 else 4)
-        )
-        return deprecated[1]
-    raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))
-
-
-if not PY37:
-    Pep562(__name__)